Pārlūkot izejas kodu

feat(llm): add model defaults and compatibility data (#34436)

Shoubhit Dash 1 mēnesi atpakaļ
vecāks
revīzija
1fd8bf526d

+ 1 - 2
packages/llm/src/provider.ts

@@ -1,7 +1,6 @@
-import type { RouteDefaultsInput } from "./route/client"
 import type { Model, ModelID, ProviderID } from "./schema"
 
-export type ModelOptions = RouteDefaultsInput
+export type ModelOptions = Pick<Model.Input, "defaults" | "compatibility">
 
 /**
  * Advanced structural provider definition helper. Built-in providers should

+ 54 - 1
packages/llm/src/schema/options.ts

@@ -134,15 +134,60 @@ export namespace ModelLimits {
     input instanceof ModelLimits ? input : new ModelLimits(input ?? {})
 }
 
+export class ModelDefaults extends Schema.Class<ModelDefaults>("LLM.ModelDefaults")({
+  limits: Schema.optional(ModelLimits),
+  generation: Schema.optional(GenerationOptions),
+  providerOptions: Schema.optional(ProviderOptions),
+  http: Schema.optional(HttpOptions),
+}) {}
+
+export namespace ModelDefaults {
+  export type Input = ModelDefaults | {
+    readonly limits?: ModelLimits.Input
+    readonly generation?: GenerationOptions.Input
+    readonly providerOptions?: ProviderOptions
+    readonly http?: HttpOptions.Input
+  }
+
+  /** Normalize selected-model request defaults without applying precedence. */
+  export const make = (input: Input) => {
+    if (input instanceof ModelDefaults) return input
+    return new ModelDefaults({
+      limits: input.limits === undefined ? undefined : ModelLimits.make(input.limits),
+      generation: input.generation === undefined ? undefined : GenerationOptions.make(input.generation),
+      providerOptions: input.providerOptions,
+      http: input.http === undefined ? undefined : HttpOptions.make(input.http),
+    })
+  }
+}
+
+export const ModelToolSchemaCompatibility = Schema.Literals(["gemini", "moonshot"])
+export type ModelToolSchemaCompatibility = Schema.Schema.Type<typeof ModelToolSchemaCompatibility>
+
+export class ModelCompatibility extends Schema.Class<ModelCompatibility>("LLM.ModelCompatibility")({
+  toolSchema: Schema.optional(ModelToolSchemaCompatibility),
+}) {}
+
+export namespace ModelCompatibility {
+  export type Input = ModelCompatibility | ConstructorParameters<typeof ModelCompatibility>[0]
+
+  /** Normalize model/upstream compatibility metadata without projecting requests. */
+  export const make = (input: Input) => (input instanceof ModelCompatibility ? input : new ModelCompatibility(input))
+}
+
 export class Model {
   readonly id: ModelID
   readonly provider: ProviderID
   readonly route: AnyRoute
+  readonly defaults?: ModelDefaults
+  readonly compatibility?: ModelCompatibility
 
   constructor(input: Model.ConstructorInput) {
     this.id = input.id
     this.provider = input.provider
     this.route = input.route
+    this.defaults = input.defaults
+    this.compatibility = input.compatibility
   }
 
   static make(input: Model.Input) {
@@ -150,6 +195,8 @@ export class Model {
       id: ModelID.make(input.id),
       provider: ProviderID.make(input.provider),
       route: input.route,
+      defaults: input.defaults === undefined ? undefined : ModelDefaults.make(input.defaults),
+      compatibility: input.compatibility === undefined ? undefined : ModelCompatibility.make(input.compatibility),
     })
   }
 
@@ -158,6 +205,8 @@ export class Model {
       id: model.id,
       provider: model.provider,
       route: model.route,
+      defaults: model.defaults,
+      compatibility: model.compatibility,
     }
   }
 
@@ -175,11 +224,15 @@ export namespace Model {
     readonly id: ModelID
     readonly provider: ProviderID
     readonly route: AnyRoute
+    readonly defaults?: ModelDefaults
+    readonly compatibility?: ModelCompatibility
   }
 
-  export type Input = Omit<ConstructorInput, "id" | "provider"> & {
+  export type Input = Omit<ConstructorInput, "id" | "provider" | "defaults" | "compatibility"> & {
     readonly id: string | ModelID
     readonly provider: string | ProviderID
+    readonly defaults?: ModelDefaults.Input
+    readonly compatibility?: ModelCompatibility.Input
   }
 }
 

+ 34 - 2
packages/llm/test/llm.test.ts

@@ -90,15 +90,47 @@ describe("llm constructors", () => {
       provider: "fake",
       route: chatRoute,
     })
-    const updated = Model.update(base, { route: responsesRoute })
+    const updated = Model.update(base, {
+      route: responsesRoute,
+      defaults: { generation: { maxTokens: 20 } },
+      compatibility: { toolSchema: "gemini" },
+    })
+    const updatedInput = Model.input(updated)
 
     expect(updated).toBeInstanceOf(Model)
     expect(String(updated.id)).toBe("fake-model")
     expect(updated.route).toBe(responsesRoute)
-    expect(String(Model.input(updated).provider)).toBe("fake")
+    expect(updated.defaults?.generation).toEqual({ maxTokens: 20 })
+    expect(updated.compatibility).toEqual({ toolSchema: "gemini" })
+    expect(updatedInput.defaults).toBe(updated.defaults)
+    expect(updatedInput.compatibility).toBe(updated.compatibility)
+    expect(String(updatedInput.provider)).toBe("fake")
     expect(Model.update(updated, {})).toBe(updated)
   })
 
+  test("carries model defaults and compatibility through route model selection", () => {
+    const model = chatRoute.model({
+      id: "kimi-k2",
+      defaults: {
+        limits: { context: 128_000, output: 8_192 },
+        generation: { maxTokens: 1_024, stop: ["END"] },
+        providerOptions: { openai: { parallelToolCalls: false } },
+        http: { body: { extra_body: true } },
+      },
+      compatibility: { toolSchema: "moonshot" },
+    })
+    const request = LLM.request({ model, prompt: "Say hello." })
+
+    expect(request.model.defaults?.limits).toEqual({ context: 128_000, output: 8_192 })
+    expect(request.model.defaults?.generation).toEqual({ maxTokens: 1_024, stop: ["END"] })
+    expect(request.model.defaults?.providerOptions).toEqual({ openai: { parallelToolCalls: false } })
+    expect(request.model.defaults?.http).toEqual({ body: { extra_body: true } })
+    expect(request.model.compatibility).toEqual({ toolSchema: "moonshot" })
+    expect(request.generation).toBeUndefined()
+    expect(request.providerOptions).toBeUndefined()
+    expect(request.http).toBeUndefined()
+  })
+
   test("builds tool choices from names and tools", () => {
     const tool = ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })