diff --git a/packages/ai/src/schema/options.ts b/packages/ai/src/schema/options.ts index 1b5c41a4d82..e6923097999 100644 --- a/packages/ai/src/schema/options.ts +++ b/packages/ai/src/schema/options.ts @@ -178,6 +178,7 @@ export class LanguageModelCompatibility extends Schema.Class { const updated = LanguageModel.update(base, { route: responsesRoute, defaults: { generation: { maxTokens: 20 } }, - compatibility: { toolSchema: "gemini" }, + compatibility: { toolSchema: "gemini", requireFinishReason: false }, }) const updatedInput = LanguageModel.input(updated) @@ -110,7 +110,7 @@ describe("llm constructors", () => { expect(String(updated.id)).toBe("fake-model") expect(updated.route).toBe(responsesRoute) expect(updated.defaults?.generation).toEqual({ maxTokens: 20 }) - expect(updated.compatibility).toEqual({ toolSchema: "gemini" }) + expect(updated.compatibility).toEqual({ toolSchema: "gemini", requireFinishReason: false }) expect(updatedInput.defaults).toBe(updated.defaults) expect(updatedInput.compatibility).toBe(updated.compatibility) expect(String(updatedInput.provider)).toBe("fake") diff --git a/packages/client/src/promise/generated/types.ts b/packages/client/src/promise/generated/types.ts index fe951c75286..6263d57a98f 100644 --- a/packages/client/src/promise/generated/types.ts +++ b/packages/client/src/promise/generated/types.ts @@ -169,6 +169,8 @@ export type EventLogSynced = { type: "log.synced"; aggregateID: string; seq?: nu export type ModelReasoningField = "reasoning" | "reasoning_content" | "reasoning_text" | (string & {}) +export type ModelMaxTokensField = "max_completion_tokens" | "max_tokens" + export type ModelCapabilities = { tools: boolean; input: Array; output: Array } export type ModelVariant = { @@ -1230,7 +1232,11 @@ export type SessionToolCalled = { export type ToolContent1 = ToolTextContent | ToolFileContent1 -export type ModelCompatibility = { reasoningField?: ModelReasoningField } +export type ModelCompatibility = { + reasoningField?: ModelReasoningField + maxTokensField?: ModelMaxTokensField + requireFinishReason?: boolean +} export type ModelCost = { tier?: { type: "context"; size: number } diff --git a/packages/core/test/config/provider.test.ts b/packages/core/test/config/provider.test.ts index 21680c856df..2526843c27e 100644 --- a/packages/core/test/config/provider.test.ts +++ b/packages/core/test/config/provider.test.ts @@ -237,7 +237,11 @@ describe("ConfigProviderPlugin.Plugin", () => { models: { chat: { name: "First", - compatibility: { reasoningField: "vendor_reasoning" }, + compatibility: { + reasoningField: "vendor_reasoning", + maxTokensField: "max_completion_tokens", + requireFinishReason: false, + }, capabilities: { tools: true, input: ["text"], output: ["text"] }, disabled: true, limit: { context: 100, output: 50 }, @@ -318,7 +322,11 @@ describe("ConfigProviderPlugin.Plugin", () => { expect(model.id).toBe(modelID) expect(model.modelID).toBe(Model.ID.make("api-chat")) expect(model.name).toBe("Last") - expect(model.compatibility).toEqual({ reasoningField: "vendor_reasoning" }) + expect(model.compatibility).toEqual({ + reasoningField: "vendor_reasoning", + maxTokensField: "max_completion_tokens", + requireFinishReason: false, + }) expect(model.capabilities).toEqual({ tools: true, input: ["text"], output: ["text"] }) expect(model.enabled).toBe(false) expect(model.limit).toEqual({ context: 100, output: 75 }) diff --git a/packages/core/test/model-resolver.test.ts b/packages/core/test/model-resolver.test.ts index 0e9f30e4fcf..66ad9818152 100644 --- a/packages/core/test/model-resolver.test.ts +++ b/packages/core/test/model-resolver.test.ts @@ -194,7 +194,11 @@ describe("ModelResolver", () => { Effect.gen(function* () { const resolved = yield* ModelResolver.fromCatalogModel( model(Provider.aisdk("@ai-sdk/openai-compatible"), { - compatibility: { reasoningField: "vendor_reasoning" }, + compatibility: { + reasoningField: "vendor_reasoning", + maxTokensField: "max_completion_tokens", + requireFinishReason: false, + }, settings: { apiKey: "settings-secret", baseURL: "https://compatible.example/v1", @@ -204,7 +208,8 @@ describe("ModelResolver", () => { body: {}, }), ) - const request = LLM.request({ model: resolved, prompt: "Hello" }) + const request = LLM.request({ model: resolved, prompt: "Hello", generation: { maxTokens: 10 } }) + const prepared = yield* compileRequest(request) const headers = yield* resolved.route.auth.apply({ request, method: "POST", @@ -216,6 +221,10 @@ describe("ModelResolver", () => { expect(headers.authorization).toBe("Bearer settings-secret") expect(resolved.route.id).toBe("openai-compatible-chat") expect(resolved.compatibility?.reasoningField).toBe("vendor_reasoning") + expect(resolved.compatibility?.maxTokensField).toBe("max_completion_tokens") + expect(resolved.compatibility?.requireFinishReason).toBe(false) + expect(prepared.body).toMatchObject({ max_completion_tokens: 10 }) + expect(prepared.body).not.toHaveProperty("max_tokens") expect(resolved.route.endpoint.baseURL).toBe("https://compatible.example/v1") expect(resolved.route.defaults.http?.body).toEqual({}) }), diff --git a/packages/schema/src/model.ts b/packages/schema/src/model.ts index 2f9d1dd7cc9..a1e396f19e8 100644 --- a/packages/schema/src/model.ts +++ b/packages/schema/src/model.ts @@ -47,9 +47,16 @@ export const ReasoningField: Schema.Codec = Schema.Union([ Schema.String, ]).annotate({ identifier: "Model.ReasoningField" }) +export const MaxTokensField = Schema.Literals(["max_completion_tokens", "max_tokens"]).annotate({ + identifier: "Model.MaxTokensField", +}) +export type MaxTokensField = typeof MaxTokensField.Type + export interface Compatibility extends Schema.Schema.Type {} export const Compatibility = Schema.Struct({ reasoningField: ReasoningField.pipe(optional), + maxTokensField: MaxTokensField.pipe(optional), + requireFinishReason: Schema.Boolean.pipe(optional), }).annotate({ identifier: "Model.Compatibility" }) export interface Capabilities extends Schema.Schema.Type {} diff --git a/packages/schema/test/model.test.ts b/packages/schema/test/model.test.ts index 7a11eb84241..889f2e63896 100644 --- a/packages/schema/test/model.test.ts +++ b/packages/schema/test/model.test.ts @@ -30,3 +30,22 @@ describe("Model.ReasoningField", () => { expect(decode(field)).toBe(field) }) }) + +describe("Model.Compatibility", () => { + test("decodes model compatibility overrides", () => { + const decode = Schema.decodeUnknownSync(Model.Compatibility) + + expect(decode({})).toEqual({}) + expect( + decode({ + reasoningField: "vendor_reasoning", + maxTokensField: "max_completion_tokens", + requireFinishReason: false, + }), + ).toEqual({ + reasoningField: "vendor_reasoning", + maxTokensField: "max_completion_tokens", + requireFinishReason: false, + }) + }) +})