feat(llm): add model defaults and compatibility data (#34436)
This commit is contained in:
parent
6d9539f469
commit
1fd8bf526d
3 changed files with 89 additions and 5 deletions
|
|
@ -1,7 +1,6 @@
|
||||||
import type { RouteDefaultsInput } from "./route/client"
|
|
||||||
import type { Model, ModelID, ProviderID } from "./schema"
|
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
|
* Advanced structural provider definition helper. Built-in providers should
|
||||||
|
|
|
||||||
|
|
@ -134,15 +134,60 @@ export namespace ModelLimits {
|
||||||
input instanceof ModelLimits ? input : new ModelLimits(input ?? {})
|
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 {
|
export class Model {
|
||||||
readonly id: ModelID
|
readonly id: ModelID
|
||||||
readonly provider: ProviderID
|
readonly provider: ProviderID
|
||||||
readonly route: AnyRoute
|
readonly route: AnyRoute
|
||||||
|
readonly defaults?: ModelDefaults
|
||||||
|
readonly compatibility?: ModelCompatibility
|
||||||
|
|
||||||
constructor(input: Model.ConstructorInput) {
|
constructor(input: Model.ConstructorInput) {
|
||||||
this.id = input.id
|
this.id = input.id
|
||||||
this.provider = input.provider
|
this.provider = input.provider
|
||||||
this.route = input.route
|
this.route = input.route
|
||||||
|
this.defaults = input.defaults
|
||||||
|
this.compatibility = input.compatibility
|
||||||
}
|
}
|
||||||
|
|
||||||
static make(input: Model.Input) {
|
static make(input: Model.Input) {
|
||||||
|
|
@ -150,6 +195,8 @@ export class Model {
|
||||||
id: ModelID.make(input.id),
|
id: ModelID.make(input.id),
|
||||||
provider: ProviderID.make(input.provider),
|
provider: ProviderID.make(input.provider),
|
||||||
route: input.route,
|
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,
|
id: model.id,
|
||||||
provider: model.provider,
|
provider: model.provider,
|
||||||
route: model.route,
|
route: model.route,
|
||||||
|
defaults: model.defaults,
|
||||||
|
compatibility: model.compatibility,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -175,11 +224,15 @@ export namespace Model {
|
||||||
readonly id: ModelID
|
readonly id: ModelID
|
||||||
readonly provider: ProviderID
|
readonly provider: ProviderID
|
||||||
readonly route: AnyRoute
|
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 id: string | ModelID
|
||||||
readonly provider: string | ProviderID
|
readonly provider: string | ProviderID
|
||||||
|
readonly defaults?: ModelDefaults.Input
|
||||||
|
readonly compatibility?: ModelCompatibility.Input
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -90,15 +90,47 @@ describe("llm constructors", () => {
|
||||||
provider: "fake",
|
provider: "fake",
|
||||||
route: chatRoute,
|
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(updated).toBeInstanceOf(Model)
|
||||||
expect(String(updated.id)).toBe("fake-model")
|
expect(String(updated.id)).toBe("fake-model")
|
||||||
expect(updated.route).toBe(responsesRoute)
|
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)
|
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", () => {
|
test("builds tool choices from names and tools", () => {
|
||||||
const tool = ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })
|
const tool = ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue