diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 871a84abf..153431255 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Added Baseten as a built-in OpenAI-compatible provider with models.dev catalog generation and native `chat_template_args` reasoning controls. + ### Fixed - Fixed tool argument validation to preserve values that already match an `anyOf`/`oneOf` union arm before attempting coercion, avoiding nullable unions converting `null` to another primitive value ([#7328](https://github.com/earendil-works/pi/issues/7328)). diff --git a/packages/ai/README.md b/packages/ai/README.md index ed4e4a545..da7f20124 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -76,6 +76,7 @@ Unified LLM API with provider collections, automatic auth resolution, token and - **ZAI Coding Plan (Global)** (with separate China provider) - **MiniMax** (with separate China provider) - **Together AI** +- **Baseten** - **Hugging Face** - **Moonshot AI** (with separate China provider) - **GitHub Copilot** (requires OAuth, see below) @@ -424,6 +425,7 @@ Built-in providers resolve these env vars (Node.js; in browsers pass `apiKey` ex | xAI | `XAI_API_KEY` | | Fireworks | `FIREWORKS_API_KEY` | | Together AI | `TOGETHER_API_KEY` | +| Baseten | `BASETEN_API_KEY` | | OpenRouter | `OPENROUTER_API_KEY` | | Vercel AI Gateway | `AI_GATEWAY_API_KEY` | | ZAI Coding Plan (Global) | `ZAI_API_KEY` | @@ -1167,8 +1169,9 @@ interface OpenAICompletionsCompat { requiresAssistantAfterToolResult?: boolean; // Whether tool results must be followed by an assistant message (default: false) requiresThinkingAsText?: boolean; // Whether thinking blocks must be converted to text (default: false) requiresReasoningContentOnAssistantMessages?: boolean; // Whether all replayed assistant messages must include empty reasoning_content when reasoning is enabled (default: auto-detected for DeepSeek) - thinkingFormat?: 'openai' | 'openrouter' | 'deepseek' | 'together' | 'zai' | 'qwen' | 'chat-template' | 'qwen-chat-template' | 'string-thinking' | 'ant-ling'; // Format for reasoning param: 'openai' uses reasoning_effort, 'openrouter' uses reasoning: { effort }, 'deepseek' uses thinking: { type } plus reasoning_effort when supported, 'together' uses reasoning: { enabled } plus reasoning_effort when supported, 'zai' uses thinking: { type }, 'qwen' uses enable_thinking, 'chat-template' uses configurable chat_template_kwargs, 'qwen-chat-template' uses chat_template_kwargs.enable_thinking and preserve_thinking, 'string-thinking' uses top-level thinking, 'ant-ling' uses reasoning: { effort } only for mapped efforts (default: openai) + thinkingFormat?: 'openai' | 'openrouter' | 'deepseek' | 'together' | 'baseten' | 'zai' | 'qwen' | 'chat-template' | 'qwen-chat-template' | 'string-thinking' | 'ant-ling'; // Format for reasoning param: 'openai' uses reasoning_effort, 'openrouter' uses reasoning: { effort }, 'deepseek' uses thinking: { type } plus reasoning_effort when supported, 'together' uses reasoning: { enabled } plus reasoning_effort when supported, 'baseten' uses configurable chat_template_args plus reasoning_effort when supported, 'zai' uses thinking: { type }, 'qwen' uses enable_thinking, 'chat-template' uses configurable chat_template_kwargs, 'qwen-chat-template' uses chat_template_kwargs.enable_thinking and preserve_thinking, 'string-thinking' uses top-level thinking, 'ant-ling' uses reasoning: { effort } only for mapped efforts (default: openai) chatTemplateKwargs?: Record; // chat_template_kwargs values; use $var for pi-controlled thinking values + chatTemplateArgs?: Record; // chat_template_args values for thinkingFormat: 'baseten'; use $var for pi-controlled thinking values cacheControlFormat?: 'anthropic'; // Anthropic-style cache_control on system prompt, last tool, and last user/assistant text content openRouterRouting?: OpenRouterRouting; // OpenRouter routing preferences (default: {}) vercelGatewayRouting?: VercelGatewayRouting; // Vercel AI Gateway routing preferences (default: {}) diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index 2e0194027..5f2ea81f7 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -86,6 +86,7 @@ interface ModelsDevModel { structured_output?: boolean; reasoning?: boolean; reasoning_options?: ModelsDevReasoningOption[]; + status?: string; limit?: { context?: number; output?: number; @@ -561,6 +562,7 @@ const OPENAI_COMPLETIONS_DEFAULT_COMPAT = { openRouterRouting: {}, vercelGatewayRouting: {}, chatTemplateKwargs: {}, + chatTemplateArgs: {}, zaiToolStream: false, supportsStrictMode: true, supportsOpenAIGrammarTools: false, @@ -651,6 +653,7 @@ function detectOpenAICompletionsCompat(model: Model<"openai-completions">): Open openRouterRouting: {}, vercelGatewayRouting: {}, chatTemplateKwargs: {}, + chatTemplateArgs: {}, zaiToolStream: false, supportsStrictMode: !isMoonshot && !isTogether && !isCloudflareAiGateway && !isNvidia, supportsOpenAIGrammarTools: false, @@ -1088,6 +1091,100 @@ async function fetchAiGatewayModels(): Promise[]> { } } +function processBasetenModels(provider: ModelsDevProvider | undefined): Model[] { + if (!provider?.models) return []; + + const baseUrl = "https://inference.baseten.co/v1"; + const baseCompat: OpenAICompletionsCompat = { + supportsStore: false, + supportsDeveloperRole: false, + supportsReasoningEffort: false, + supportsUsageInStreaming: true, + maxTokensField: "max_tokens", + supportsStrictMode: true, + supportsLongCacheRetention: false, + }; + const reasoningEffortCompat: OpenAICompletionsCompat = { + ...baseCompat, + supportsReasoningEffort: true, + thinkingFormat: "openai", + }; + const toggleReasoningCompat: OpenAICompletionsCompat = { + ...baseCompat, + thinkingFormat: "baseten", + chatTemplateArgs: { enable_thinking: { $var: "thinking.enabled" } }, + }; + const toggleReasoningEffortCompat: OpenAICompletionsCompat = { + ...reasoningEffortCompat, + thinkingFormat: "baseten", + chatTemplateArgs: { enable_thinking: { $var: "thinking.enabled" } }, + }; + const toggleThinkingLevelMap = { + off: "off", + minimal: null, + low: null, + medium: null, + high: "high", + xhigh: null, + max: null, + } as const; + const glm52ThinkingLevelMap = { + off: "none", + minimal: null, + low: null, + medium: null, + high: "high", + xhigh: null, + max: "max", + } as const; + const models: Model[] = []; + + for (const [modelId, model] of Object.entries(provider.models)) { + if (model.status === "deprecated") continue; + + const reasoning = model.reasoning === true; + const reasoningOptions = model.reasoning_options ?? []; + const isGlm52 = modelId === "zai-org/GLM-5.2" || modelId === "zai-org/GLM-5.2-Fast"; + const supportsToggle = reasoningOptions.some((option) => option.type === "toggle") || isGlm52; + const supportsEffort = reasoningOptions.some((option) => option.type === "effort") || isGlm52; + const compat = + supportsToggle && supportsEffort + ? toggleReasoningEffortCompat + : supportsToggle + ? toggleReasoningCompat + : supportsEffort + ? reasoningEffortCompat + : baseCompat; + const thinkingLevelMap = isGlm52 + ? glm52ThinkingLevelMap + : supportsToggle + ? toggleThinkingLevelMap + : getEffortThinkingLevelMap(reasoningOptions); + + models.push({ + id: modelId, + name: model.name || modelId, + api: "openai-completions", + provider: "baseten", + baseUrl, + reasoning, + ...(thinkingLevelMap ? { thinkingLevelMap } : {}), + input: model.modalities?.input?.includes("image") ? ["text", "image"] : ["text"], + cost: { + input: model.cost?.input || 0, + output: model.cost?.output || 0, + cacheRead: model.cost?.cache_read || 0, + cacheWrite: model.cost?.cache_write || 0, + }, + compat, + contextWindow: model.limit?.context || 4096, + maxTokens: model.limit?.output || 4096, + }); + } + + return models; +} + async function loadModelsDevData(): Promise[]> { try { console.log("Fetching models from models.dev API..."); @@ -1617,7 +1714,7 @@ async function loadModelsDevData(): Promise[]> { const togetherProvider = data.together ?? data.togetherai ?? data["together-ai"]; if (togetherProvider?.models) { for (const [modelId, model] of Object.entries(togetherProvider.models)) { - const m = model as ModelsDevModel & { status?: string }; + const m = model as ModelsDevModel; if (m.tool_call !== true) continue; if (m.status === "deprecated") continue; @@ -1646,6 +1743,8 @@ async function loadModelsDevData(): Promise[]> { } } + models.push(...processBasetenModels(data.baseten)); + // Process OpenCode models (Zen and Go) // API mapping based on provider.npm field: // - @ai-sdk/openai → openai-responses @@ -1661,7 +1760,7 @@ async function loadModelsDevData(): Promise[]> { if (!data[variant.key]?.models) continue; for (const [modelId, model] of Object.entries(data[variant.key].models)) { - const m = model as ModelsDevModel & { status?: string }; + const m = model as ModelsDevModel; if (m.tool_call !== true) continue; if (m.status === "deprecated") continue; @@ -1757,7 +1856,7 @@ async function loadModelsDevData(): Promise[]> { // Process GitHub Copilot models if (data["github-copilot"]?.models) { for (const [modelId, model] of Object.entries(data["github-copilot"].models)) { - const m = model as ModelsDevModel & { status?: string }; + const m = model as ModelsDevModel; if (m.tool_call !== true) continue; if (m.status === "deprecated") continue; diff --git a/packages/ai/src/api/openai-completions.ts b/packages/ai/src/api/openai-completions.ts index 569457d31..d15ba410c 100644 --- a/packages/ai/src/api/openai-completions.ts +++ b/packages/ai/src/api/openai-completions.ts @@ -767,10 +767,27 @@ function buildParams( preserve_thinking: true, }; } else if (compat.thinkingFormat === "chat-template" && model.reasoning) { - const chatTemplateKwargs = buildChatTemplateKwargs(model, options, compat); + const chatTemplateKwargs = buildChatTemplateValues(model, options, compat.chatTemplateKwargs); if (chatTemplateKwargs) { (params as any).chat_template_kwargs = chatTemplateKwargs; } + } else if (compat.thinkingFormat === "baseten" && model.reasoning) { + const basetenParams = params as Omit & { + chat_template_args?: Record; + reasoning_effort?: string; + }; + const chatTemplateArgs = buildChatTemplateValues(model, options, compat.chatTemplateArgs); + if (chatTemplateArgs) { + basetenParams.chat_template_args = chatTemplateArgs; + } + if (compat.supportsReasoningEffort) { + const requestedEffort = options?.reasoningEffort; + const mappedEffort = requestedEffort ? model.thinkingLevelMap?.[requestedEffort] : model.thinkingLevelMap?.off; + const effort = mappedEffort === undefined ? requestedEffort : mappedEffort; + if (typeof effort === "string") { + basetenParams.reasoning_effort = effort; + } + } } else if (compat.thinkingFormat === "deepseek" && model.reasoning) { if (options?.reasoningEffort) { (params as any).thinking = { type: "enabled" }; @@ -841,21 +858,21 @@ function buildParams( return params; } -function buildChatTemplateKwargs( +function buildChatTemplateValues( model: Model<"openai-completions">, options: OpenAICompletionsOptions | undefined, - compat: ResolvedOpenAICompletionsCompat, + values: Record, ): Record | undefined { - const kwargs: Record = {}; + const resolvedValues: Record = {}; - for (const [key, value] of Object.entries(compat.chatTemplateKwargs)) { + for (const [key, value] of Object.entries(values)) { const resolved = resolveChatTemplateKwargValue(model, options, value); if (resolved !== undefined) { - kwargs[key] = resolved; + resolvedValues[key] = resolved; } } - return Object.keys(kwargs).length > 0 ? kwargs : undefined; + return Object.keys(resolvedValues).length > 0 ? resolvedValues : undefined; } function resolveChatTemplateKwargValue( @@ -1468,6 +1485,7 @@ function detectCompat(model: Model<"openai-completions">): ResolvedOpenAIComplet openRouterRouting: {}, vercelGatewayRouting: {}, chatTemplateKwargs: {}, + chatTemplateArgs: {}, zaiToolStream: false, supportsStrictMode: !isMoonshot && !isTogether && !isCloudflareAiGateway && !isNvidia, supportsOpenAIGrammarTools: false, @@ -1511,6 +1529,7 @@ function getCompat(model: Model<"openai-completions">): ResolvedOpenAICompletion openRouterRouting: model.compat.openRouterRouting ?? {}, vercelGatewayRouting: model.compat.vercelGatewayRouting ?? detected.vercelGatewayRouting, chatTemplateKwargs: model.compat.chatTemplateKwargs ?? detected.chatTemplateKwargs, + chatTemplateArgs: model.compat.chatTemplateArgs ?? detected.chatTemplateArgs, zaiToolStream: model.compat.zaiToolStream ?? detected.zaiToolStream, supportsStrictMode: model.compat.supportsStrictMode ?? detected.supportsStrictMode, supportsOpenAIGrammarTools: model.compat.supportsOpenAIGrammarTools ?? detected.supportsOpenAIGrammarTools, diff --git a/packages/ai/src/env-api-keys.ts b/packages/ai/src/env-api-keys.ts index 342b0c94c..f535f53ab 100644 --- a/packages/ai/src/env-api-keys.ts +++ b/packages/ai/src/env-api-keys.ts @@ -102,6 +102,7 @@ function getApiKeyEnvVars(provider: string): readonly string[] | undefined { huggingface: "HF_TOKEN", fireworks: "FIREWORKS_API_KEY", together: "TOGETHER_API_KEY", + baseten: "BASETEN_API_KEY", opencode: "OPENCODE_API_KEY", "opencode-go": "OPENCODE_API_KEY", "kimi-coding": "KIMI_API_KEY", diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 5282bed43..8832e4c83 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -5,6 +5,7 @@ import { AMAZON_BEDROCK_MODELS } from "./providers/amazon-bedrock.models.ts"; import { ANT_LING_MODELS } from "./providers/ant-ling.models.ts"; import { ANTHROPIC_MODELS } from "./providers/anthropic.models.ts"; import { AZURE_OPENAI_RESPONSES_MODELS } from "./providers/azure-openai-responses.models.ts"; +import { BASETEN_MODELS } from "./providers/baseten.models.ts"; import { CEREBRAS_MODELS } from "./providers/cerebras.models.ts"; import { CLOUDFLARE_AI_GATEWAY_MODELS } from "./providers/cloudflare-ai-gateway.models.ts"; import { CLOUDFLARE_WORKERS_AI_MODELS } from "./providers/cloudflare-workers-ai.models.ts"; @@ -44,6 +45,7 @@ export const MODELS: { readonly "ant-ling": typeof ANT_LING_MODELS; readonly "anthropic": typeof ANTHROPIC_MODELS; readonly "azure-openai-responses": typeof AZURE_OPENAI_RESPONSES_MODELS; + readonly "baseten": typeof BASETEN_MODELS; readonly "cerebras": typeof CEREBRAS_MODELS; readonly "cloudflare-ai-gateway": typeof CLOUDFLARE_AI_GATEWAY_MODELS; readonly "cloudflare-workers-ai": typeof CLOUDFLARE_WORKERS_AI_MODELS; @@ -82,6 +84,7 @@ export const MODELS: { "ant-ling": ANT_LING_MODELS, "anthropic": ANTHROPIC_MODELS, "azure-openai-responses": AZURE_OPENAI_RESPONSES_MODELS, + "baseten": BASETEN_MODELS, "cerebras": CEREBRAS_MODELS, "cloudflare-ai-gateway": CLOUDFLARE_AI_GATEWAY_MODELS, "cloudflare-workers-ai": CLOUDFLARE_WORKERS_AI_MODELS, diff --git a/packages/ai/src/providers/all.ts b/packages/ai/src/providers/all.ts index 4161a8c65..5e62f8d0a 100644 --- a/packages/ai/src/providers/all.ts +++ b/packages/ai/src/providers/all.ts @@ -6,6 +6,7 @@ import { amazonBedrockProvider } from "./amazon-bedrock.ts"; import { antLingProvider } from "./ant-ling.ts"; import { anthropicProvider } from "./anthropic.ts"; import { azureOpenAIResponsesProvider } from "./azure-openai-responses.ts"; +import { basetenProvider } from "./baseten.ts"; import { cerebrasProvider } from "./cerebras.ts"; import { cloudflareAIGatewayProvider } from "./cloudflare-ai-gateway.ts"; import { cloudflareWorkersAIProvider } from "./cloudflare-workers-ai.ts"; @@ -90,6 +91,7 @@ export function builtinProviders(): Provider[] { antLingProvider(), anthropicProvider(), azureOpenAIResponsesProvider(), + basetenProvider(), cerebrasProvider(), cloudflareAIGatewayProvider(), cloudflareWorkersAIProvider(), diff --git a/packages/ai/src/providers/baseten.models.ts b/packages/ai/src/providers/baseten.models.ts new file mode 100644 index 000000000..22a55ac42 --- /dev/null +++ b/packages/ai/src/providers/baseten.models.ts @@ -0,0 +1,8 @@ +// This file is auto-generated by scripts/generate-models.ts +// Do not edit manually - run 'npm run generate-models' to update + +import values from "./data/baseten.json" with { type: "json" }; +import { flattenModelCatalog, type ModelCatalog } from "../model-catalog.ts"; + +export const BASETEN_MODELS: ModelCatalog = + flattenModelCatalog("baseten", values); diff --git a/packages/ai/src/providers/baseten.ts b/packages/ai/src/providers/baseten.ts new file mode 100644 index 000000000..79371dac0 --- /dev/null +++ b/packages/ai/src/providers/baseten.ts @@ -0,0 +1,15 @@ +import { openAICompletionsApi } from "../api/openai-completions.lazy.ts"; +import { envApiKeyAuth } from "../auth/helpers.ts"; +import { createProvider, type Provider } from "../models.ts"; +import { BASETEN_MODELS } from "./baseten.models.ts"; + +export function basetenProvider(): Provider<"openai-completions"> { + return createProvider({ + id: "baseten", + name: "Baseten", + baseUrl: "https://inference.baseten.co/v1", + auth: { apiKey: envApiKeyAuth("Baseten API key", ["BASETEN_API_KEY"]) }, + models: Object.values(BASETEN_MODELS), + api: openAICompletionsApi(), + }); +} diff --git a/packages/ai/src/types.ts b/packages/ai/src/types.ts index b9ba470a8..1909a04f7 100644 --- a/packages/ai/src/types.ts +++ b/packages/ai/src/types.ts @@ -59,6 +59,7 @@ export type KnownProvider = | "huggingface" | "fireworks" | "together" + | "baseten" | "opencode" | "opencode-go" | "kimi-coding" @@ -537,12 +538,13 @@ export interface OpenAICompletionsCompat { requiresThinkingAsText?: boolean; /** Whether all replayed assistant messages must include an empty reasoning_content field when reasoning is enabled. Default: auto-detected from URL. */ requiresReasoningContentOnAssistantMessages?: boolean; - /** Format for reasoning/thinking parameter. "openai" uses reasoning_effort, "openrouter" uses reasoning: { effort }, "deepseek" uses thinking: { type } plus reasoning_effort when supported, "together" uses reasoning: { enabled } plus reasoning_effort when supported, "zai" uses thinking: { type }, "qwen" uses top-level enable_thinking: boolean, "qwen-chat-template" uses chat_template_kwargs.enable_thinking and preserve_thinking, "chat-template" uses configurable chat_template_kwargs, "string-thinking" uses top-level thinking: string, and "ant-ling" uses reasoning: { effort } only when the mapped effort is non-null. Default: "openai". */ + /** Format for reasoning/thinking parameter. "openai" uses reasoning_effort, "openrouter" uses reasoning: { effort }, "deepseek" uses thinking: { type } plus reasoning_effort when supported, "together" uses reasoning: { enabled } plus reasoning_effort when supported, "baseten" uses configurable chat_template_args plus reasoning_effort when supported, "zai" uses thinking: { type }, "qwen" uses top-level enable_thinking: boolean, "qwen-chat-template" uses chat_template_kwargs.enable_thinking and preserve_thinking, "chat-template" uses configurable chat_template_kwargs, "string-thinking" uses top-level thinking: string, and "ant-ling" uses reasoning: { effort } only when the mapped effort is non-null. Default: "openai". */ thinkingFormat?: | "openai" | "openrouter" | "deepseek" | "together" + | "baseten" | "zai" | "qwen" | "chat-template" @@ -551,6 +553,8 @@ export interface OpenAICompletionsCompat { | "ant-ling"; /** Kwargs to send as `chat_template_kwargs` when `thinkingFormat` is `chat-template`. Use `{ "$var": "thinking.enabled" }` or `{ "$var": "thinking.effort" }` for pi-controlled thinking values. */ chatTemplateKwargs?: Record; + /** Arguments to send as `chat_template_args` when `thinkingFormat` is `baseten`. Use `{ "$var": "thinking.enabled" }` or `{ "$var": "thinking.effort" }` for pi-controlled thinking values. */ + chatTemplateArgs?: Record; /** OpenRouter-compatible routing preferences sent as the `provider` request field. */ openRouterRouting?: OpenRouterRouting; /** Vercel AI Gateway routing preferences. Only used when baseUrl points to Vercel AI Gateway. */ diff --git a/packages/ai/test/abort.test.ts b/packages/ai/test/abort.test.ts index df1904db9..f2c50c869 100644 --- a/packages/ai/test/abort.test.ts +++ b/packages/ai/test/abort.test.ts @@ -189,6 +189,18 @@ describe("AI Providers Abort Tests", () => { }); }); + describe.skipIf(!process.env.BASETEN_API_KEY)("Baseten Provider Abort", () => { + const llm = getModel("baseten", "zai-org/GLM-5.2"); + + it("should abort mid-stream", { retry: 3 }, async () => { + await testAbortSignal(llm, { reasoningEffort: "high" }); + }); + + it("should handle immediate abort", { retry: 3 }, async () => { + await testImmediateAbort(llm, { reasoningEffort: "high" }); + }); + }); + describe.skipIf(!process.env.MINIMAX_API_KEY)("MiniMax Provider Abort", () => { const llm = getModel("minimax", "MiniMax-M2.7"); diff --git a/packages/ai/test/baseten-models.test.ts b/packages/ai/test/baseten-models.test.ts new file mode 100644 index 000000000..60e8cf745 --- /dev/null +++ b/packages/ai/test/baseten-models.test.ts @@ -0,0 +1,141 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { getModel, streamSimple } from "../src/compat.ts"; +import { findEnvKeys, getEnvApiKey } from "../src/env-api-keys.ts"; +import { getSupportedThinkingLevels } from "../src/models.ts"; + +const originalBasetenApiKey = process.env.BASETEN_API_KEY; + +afterEach(() => { + if (originalBasetenApiKey === undefined) { + delete process.env.BASETEN_API_KEY; + } else { + process.env.BASETEN_API_KEY = originalBasetenApiKey; + } +}); + +describe("Baseten models", () => { + it("registers GLM 5.2 as the default OpenAI-compatible reasoning model", () => { + const model = getModel("baseten", "zai-org/GLM-5.2"); + + expect(model).toMatchObject({ + api: "openai-completions", + provider: "baseten", + baseUrl: "https://inference.baseten.co/v1", + reasoning: true, + thinkingLevelMap: { + off: "none", + minimal: null, + low: null, + medium: null, + high: "high", + xhigh: null, + max: "max", + }, + input: ["text"], + contextWindow: 1048576, + maxTokens: 262144, + cost: { + input: 1.4, + output: 4.4, + cacheRead: 0.3, + cacheWrite: 0, + }, + compat: { + supportsStore: false, + supportsDeveloperRole: false, + supportsReasoningEffort: true, + supportsUsageInStreaming: true, + maxTokensField: "max_tokens", + supportsStrictMode: true, + supportsLongCacheRetention: false, + thinkingFormat: "baseten", + chatTemplateArgs: { enable_thinking: { $var: "thinking.enabled" } }, + }, + }); + }); + + it("models Kimi K2.6 reasoning as an explicit off/on toggle", async () => { + const model = getModel("baseten", "moonshotai/Kimi-K2.6"); + let payload: Record | undefined; + + expect(model.thinkingLevelMap).toEqual({ + off: "off", + minimal: null, + low: null, + medium: null, + high: "high", + xhigh: null, + max: null, + }); + expect(model.compat).toMatchObject({ + supportsReasoningEffort: false, + thinkingFormat: "baseten", + chatTemplateArgs: { enable_thinking: { $var: "thinking.enabled" } }, + }); + expect(getSupportedThinkingLevels(model)).toEqual(["off", "high"]); + + await streamSimple( + model, + { messages: [{ role: "user", content: "test", timestamp: 0 }] }, + { + apiKey: "test-baseten-key", + reasoning: "high", + onPayload: (value) => { + payload = value as Record; + throw new Error("payload captured"); + }, + }, + ).result(); + + expect(payload?.chat_template_args).toEqual({ enable_thinking: true }); + expect(payload?.reasoning_effort).toBeUndefined(); + }); + + it("sends Baseten chat_template_args with reasoning effort", async () => { + const model = getModel("baseten", "zai-org/GLM-5.2"); + let payload: Record | undefined; + + await streamSimple( + model, + { messages: [{ role: "user", content: "test", timestamp: 0 }] }, + { + apiKey: "test-baseten-key", + reasoning: "high", + onPayload: (value) => { + payload = value as Record; + throw new Error("payload captured"); + }, + }, + ).result(); + + expect(payload?.chat_template_args).toEqual({ enable_thinking: true }); + expect(payload?.reasoning_effort).toBe("high"); + }); + + it("disables Baseten opt-in reasoning when thinking is off", async () => { + const model = getModel("baseten", "zai-org/GLM-5.2"); + let payload: Record | undefined; + + await streamSimple( + model, + { messages: [{ role: "user", content: "test", timestamp: 0 }] }, + { + apiKey: "test-baseten-key", + onPayload: (value) => { + payload = value as Record; + throw new Error("payload captured"); + }, + }, + ).result(); + + expect(payload?.chat_template_args).toEqual({ enable_thinking: false }); + expect(payload?.reasoning_effort).toBe("none"); + }); + + it("resolves BASETEN_API_KEY from the environment", () => { + process.env.BASETEN_API_KEY = "test-baseten-key"; + + expect(findEnvKeys("baseten")).toEqual(["BASETEN_API_KEY"]); + expect(getEnvApiKey("baseten")).toBe("test-baseten-key"); + }); +}); diff --git a/packages/ai/test/cross-provider-handoff.test.ts b/packages/ai/test/cross-provider-handoff.test.ts index f81513860..1746411c7 100644 --- a/packages/ai/test/cross-provider-handoff.test.ts +++ b/packages/ai/test/cross-provider-handoff.test.ts @@ -108,6 +108,8 @@ const PROVIDER_MODEL_PAIRS: ProviderModelPair[] = [ { provider: "huggingface", model: "moonshotai/Kimi-K2.5", label: "huggingface-kimi-k2.5" }, // Together AI { provider: "together", model: "moonshotai/Kimi-K2.6", label: "together-kimi-k2.6" }, + // Baseten + { provider: "baseten", model: "zai-org/GLM-5.2", label: "baseten-glm-5.2" }, // Kimi For Coding { provider: "kimi-coding", model: "kimi-for-coding", label: "kimi-for-coding" }, // Mistral diff --git a/packages/ai/test/deferred-tools.test.ts b/packages/ai/test/deferred-tools.test.ts index 614203a5b..87e8e2093 100644 --- a/packages/ai/test/deferred-tools.test.ts +++ b/packages/ai/test/deferred-tools.test.ts @@ -368,6 +368,7 @@ describe("deferred tools", () => { openRouterRouting: {}, vercelGatewayRouting: {}, chatTemplateKwargs: {}, + chatTemplateArgs: {}, zaiToolStream: false, supportsStrictMode: false, supportsOpenAIGrammarTools: false, diff --git a/packages/ai/test/empty.test.ts b/packages/ai/test/empty.test.ts index 3f996b37d..310da0508 100644 --- a/packages/ai/test/empty.test.ts +++ b/packages/ai/test/empty.test.ts @@ -386,6 +386,27 @@ describe("AI Providers Empty Message Tests", () => { }); }); + describe.skipIf(!process.env.BASETEN_API_KEY)("Baseten Provider Empty Messages", () => { + const llm = getModel("baseten", "zai-org/GLM-5.2"); + const options = { reasoningEffort: "high" } satisfies StreamOptionsWithExtras; + + it("should handle empty content array", { retry: 3, timeout: 30000 }, async () => { + await testEmptyMessage(llm, options); + }); + + it("should handle empty string content", { retry: 3, timeout: 30000 }, async () => { + await testEmptyStringMessage(llm, options); + }); + + it("should handle whitespace-only content", { retry: 3, timeout: 30000 }, async () => { + await testWhitespaceOnlyMessage(llm, options); + }); + + it("should handle empty assistant message in conversation", { retry: 3, timeout: 30000 }, async () => { + await testEmptyAssistantMessage(llm, options); + }); + }); + describe.skipIf(!process.env.ZAI_API_KEY)("zAI Provider Empty Messages", () => { const llm = getModel("zai", "glm-5.2"); diff --git a/packages/ai/test/image-tool-result.test.ts b/packages/ai/test/image-tool-result.test.ts index 150f3d8b7..8b77e793f 100644 --- a/packages/ai/test/image-tool-result.test.ts +++ b/packages/ai/test/image-tool-result.test.ts @@ -311,6 +311,19 @@ describe("Tool Results with Images", () => { }); }); + describe.skipIf(!process.env.BASETEN_API_KEY)("Baseten Provider (Kimi-K2.6)", () => { + const llm = getModel("baseten", "moonshotai/Kimi-K2.6"); + const options = { reasoningEffort: "high" } satisfies StreamOptionsWithExtras; + + it("should handle tool result with only image", { retry: 3, timeout: 30000 }, async () => { + await handleToolWithImageResult(llm, options); + }); + + it("should handle tool result with text and image", { retry: 3, timeout: 30000 }, async () => { + await handleToolWithTextAndImageResult(llm, options); + }); + }); + describe.skipIf(!process.env.XIAOMI_API_KEY)("Xiaomi MiMo (API billing) Provider (mimo-v2.5-pro)", () => { const llm = getModel("xiaomi", "mimo-v2.5-pro"); diff --git a/packages/ai/test/openai-completions-thinking-as-text.test.ts b/packages/ai/test/openai-completions-thinking-as-text.test.ts index 1f3539fc1..1078e3f5c 100644 --- a/packages/ai/test/openai-completions-thinking-as-text.test.ts +++ b/packages/ai/test/openai-completions-thinking-as-text.test.ts @@ -36,6 +36,7 @@ const compat = { openRouterRouting: {}, vercelGatewayRouting: {}, chatTemplateKwargs: {}, + chatTemplateArgs: {}, zaiToolStream: false, supportsStrictMode: true, supportsOpenAIGrammarTools: false, diff --git a/packages/ai/test/openai-completions-tool-choice.test.ts b/packages/ai/test/openai-completions-tool-choice.test.ts index 0e1ce0e36..5f8125807 100644 --- a/packages/ai/test/openai-completions-tool-choice.test.ts +++ b/packages/ai/test/openai-completions-tool-choice.test.ts @@ -1295,6 +1295,7 @@ describe("openai-completions tool_choice", () => { openRouterRouting: {}, vercelGatewayRouting: {}, chatTemplateKwargs: {}, + chatTemplateArgs: {}, zaiToolStream: false, supportsStrictMode: true, supportsOpenAIGrammarTools: false, diff --git a/packages/ai/test/openai-completions-tool-result-images.test.ts b/packages/ai/test/openai-completions-tool-result-images.test.ts index f9e5cda73..6b977714d 100644 --- a/packages/ai/test/openai-completions-tool-result-images.test.ts +++ b/packages/ai/test/openai-completions-tool-result-images.test.ts @@ -36,6 +36,7 @@ const compat: Omit, "deferredToolsMode"> & { openRouterRouting: {}, vercelGatewayRouting: {}, chatTemplateKwargs: {}, + chatTemplateArgs: {}, zaiToolStream: false, supportsStrictMode: true, supportsOpenAIGrammarTools: false, diff --git a/packages/ai/test/stream.test.ts b/packages/ai/test/stream.test.ts index 18a1db8a4..f4e5b4890 100644 --- a/packages/ai/test/stream.test.ts +++ b/packages/ai/test/stream.test.ts @@ -786,6 +786,31 @@ describe("Generate E2E Tests", () => { }); }); + describe.skipIf(!process.env.BASETEN_API_KEY)("Baseten Provider (GLM 5.2 via OpenAI Completions)", () => { + const llm = getModel("baseten", "zai-org/GLM-5.2"); + const options = { reasoningEffort: "high" } satisfies StreamOptionsWithExtras; + + it("should complete basic text generation", { retry: 3 }, async () => { + await basicTextGeneration(llm, options); + }); + + it("should handle tool calling", { retry: 3 }, async () => { + await handleToolCall(llm, options); + }); + + it("should handle streaming", { retry: 3 }, async () => { + await handleStreaming(llm, options); + }); + + it("should handle thinking mode", { retry: 3 }, async () => { + await handleThinking(llm, options); + }); + + it("should handle multi-turn with thinking and tools", { retry: 3 }, async () => { + await multiTurn(llm, options); + }); + }); + describe.skipIf(!process.env.NVIDIA_API_KEY)("NVIDIA NIM Provider (Nemotron 3 Super via OpenAI Completions)", () => { const llm = getModel("nvidia", "nvidia/nemotron-3-super-120b-a12b"); diff --git a/packages/ai/test/tokens.test.ts b/packages/ai/test/tokens.test.ts index bea1ff1e3..57930f012 100644 --- a/packages/ai/test/tokens.test.ts +++ b/packages/ai/test/tokens.test.ts @@ -193,6 +193,14 @@ describe("Token Statistics on Abort", () => { }); }); + describe.skipIf(!process.env.BASETEN_API_KEY)("Baseten Provider", () => { + const llm = getModel("baseten", "zai-org/GLM-5.2"); + + it("should include token stats when aborted mid-stream", { retry: 3, timeout: 30000 }, async () => { + await testTokensOnAbort(llm, { reasoningEffort: "high" }); + }); + }); + describe.skipIf(!process.env.ZAI_API_KEY)("zAI Provider", () => { const llm = getModel("zai", "glm-5.2"); diff --git a/packages/ai/test/tool-call-without-result.test.ts b/packages/ai/test/tool-call-without-result.test.ts index 625d48f00..ec59c13cd 100644 --- a/packages/ai/test/tool-call-without-result.test.ts +++ b/packages/ai/test/tool-call-without-result.test.ts @@ -198,6 +198,14 @@ describe("Tool Call Without Result Tests", () => { }); }); + describe.skipIf(!process.env.BASETEN_API_KEY)("Baseten Provider", () => { + const model = getModel("baseten", "zai-org/GLM-5.2"); + + it("should filter out tool calls without corresponding tool results", { retry: 3, timeout: 30000 }, async () => { + await testToolCallWithoutResult(model, { reasoningEffort: "high" }); + }); + }); + describe.skipIf(!process.env.ZAI_API_KEY)("zAI Provider", () => { const model = getModel("zai", "glm-5.2"); diff --git a/packages/ai/test/total-tokens.test.ts b/packages/ai/test/total-tokens.test.ts index 535c7e299..f6785f223 100644 --- a/packages/ai/test/total-tokens.test.ts +++ b/packages/ai/test/total-tokens.test.ts @@ -395,6 +395,28 @@ describe("totalTokens field", () => { }); }); + // ========================================================================= + // Baseten + // ========================================================================= + + describe.skipIf(!process.env.BASETEN_API_KEY)("Baseten", () => { + it("GLM 5.2 - should return totalTokens equal to sum of components", { retry: 3, timeout: 60000 }, async () => { + const llm = getModel("baseten", "zai-org/GLM-5.2"); + + console.log(`\nBaseten / ${llm.id}:`); + const { first, second } = await testTotalTokensWithCache(llm, { + apiKey: process.env.BASETEN_API_KEY, + reasoningEffort: "high", + }); + + logUsage("First request", first); + logUsage("Second request", second); + + assertTotalTokensEqualsComponents(first); + assertTotalTokensEqualsComponents(second); + }); + }); + // ========================================================================= // z.ai // ========================================================================= diff --git a/packages/ai/test/unicode-surrogate.test.ts b/packages/ai/test/unicode-surrogate.test.ts index eb1f595f6..a2b47ba2c 100644 --- a/packages/ai/test/unicode-surrogate.test.ts +++ b/packages/ai/test/unicode-surrogate.test.ts @@ -562,6 +562,23 @@ describe("AI Providers Unicode Surrogate Pair Tests", () => { }); }); + describe.skipIf(!process.env.BASETEN_API_KEY)("Baseten Provider Unicode Handling", () => { + const llm = getModel("baseten", "zai-org/GLM-5.2"); + const options = { reasoningEffort: "high" } satisfies StreamOptionsWithExtras; + + it("should handle emoji in tool results", { retry: 3, timeout: 30000 }, async () => { + await testEmojiInToolResults(llm, options); + }); + + it("should handle real-world LinkedIn comment data with emoji", { retry: 3, timeout: 30000 }, async () => { + await testRealWorldLinkedInData(llm, options); + }); + + it("should handle unpaired high surrogate (0xD83D) in tool results", { retry: 3, timeout: 30000 }, async () => { + await testUnpairedHighSurrogate(llm, options); + }); + }); + describe.skipIf(!process.env.ZAI_API_KEY)("zAI Provider Unicode Handling", () => { const llm = getModel("zai", "glm-5.2"); diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 28c96cccf..55b036a14 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -8,6 +8,7 @@ ### Added +- Added built-in Baseten provider support with `BASETEN_API_KEY` authentication and `zai-org/GLM-5.2` as the default model. - Added chainable `pi.registerMarkdownTransformer()` hooks for display-only transformation of user and assistant Markdown. - Added an experimental fullscreen UI mode, selectable through `--ui-mode fullscreen` or `/settings` ([#7304](https://github.com/earendil-works/pi/issues/7304)). - Added a sticky editor, status, widget, and footer dock to fullscreen mode while keeping the transcript independently scrollable. diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index 6b56a6aab..5a6a56073 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -128,6 +128,7 @@ For each built-in provider, pi maintains a list of tool-capable models. Configur - Hugging Face - Fireworks - Together AI +- Baseten - Kimi For Coding - MiniMax - Xiaomi MiMo diff --git a/packages/coding-agent/docs/custom-provider.md b/packages/coding-agent/docs/custom-provider.md index 87fc02f23..1b9b00504 100644 --- a/packages/coding-agent/docs/custom-provider.md +++ b/packages/coding-agent/docs/custom-provider.md @@ -751,8 +751,9 @@ interface ProviderModelConfig { requiresAssistantAfterToolResult?: boolean; requiresThinkingAsText?: boolean; requiresReasoningContentOnAssistantMessages?: boolean; - thinkingFormat?: "openai" | "openrouter" | "deepseek" | "together" | "zai" | "qwen" | "chat-template" | "qwen-chat-template" | "string-thinking" | "ant-ling"; + thinkingFormat?: "openai" | "openrouter" | "deepseek" | "together" | "baseten" | "zai" | "qwen" | "chat-template" | "qwen-chat-template" | "string-thinking" | "ant-ling"; chatTemplateKwargs?: Record; + chatTemplateArgs?: Record; cacheControlFormat?: "anthropic"; sessionAffinityFormat?: "openai" | "openai-nosession" | "openrouter"; sendSessionAffinityHeaders?: boolean; @@ -769,5 +770,5 @@ interface ProviderModelConfig { } ``` -`openrouter` sends `reasoning: { effort }`. `deepseek` sends `thinking: { type: "enabled" | "disabled" }` and `reasoning_effort` when enabled. `together` sends `reasoning: { enabled }` and also `reasoning_effort` when `supportsReasoningEffort` is enabled. `qwen` is for DashScope-style top-level `enable_thinking`. Use `qwen-chat-template` for local Qwen-compatible servers that read `chat_template_kwargs.enable_thinking` and need `preserve_thinking`. Use `chat-template` for configurable `chat_template_kwargs`, for example DeepSeek V3.x behind vLLM with `chatTemplateKwargs: { "thinking": { "$var": "thinking.enabled" } }`. +`openrouter` sends `reasoning: { effort }`. `deepseek` sends `thinking: { type: "enabled" | "disabled" }` and `reasoning_effort` when enabled. `together` sends `reasoning: { enabled }` and also `reasoning_effort` when `supportsReasoningEffort` is enabled. `qwen` is for DashScope-style top-level `enable_thinking`. Use `qwen-chat-template` for local Qwen-compatible servers that read `chat_template_kwargs.enable_thinking` and need `preserve_thinking`. Use `chat-template` for configurable `chat_template_kwargs`, for example DeepSeek V3.x behind vLLM with `chatTemplateKwargs: { "thinking": { "$var": "thinking.enabled" } }`. Use `thinkingFormat: "baseten"` with `chatTemplateArgs` when the provider expects toggle values under `chat_template_args` and optionally supports top-level `reasoning_effort`. `cacheControlFormat: "anthropic"` applies Anthropic-style `cache_control` markers to the system prompt, last tool definition, and last user, assistant, or tool-result text content. diff --git a/packages/coding-agent/docs/models.md b/packages/coding-agent/docs/models.md index fc81cc8a8..a6932def5 100644 --- a/packages/coding-agent/docs/models.md +++ b/packages/coding-agent/docs/models.md @@ -447,8 +447,9 @@ For providers with partial OpenAI compatibility, use the `compat` field. | `requiresAssistantAfterToolResult` | Insert an assistant message before a user message after tool results | | `requiresThinkingAsText` | Convert thinking blocks to plain text | | `requiresReasoningContentOnAssistantMessages` | Include empty `reasoning_content` on all replayed assistant messages when reasoning is enabled | -| `thinkingFormat` | Use `reasoning_effort`, `openrouter`, `deepseek`, `together`, `zai`, `qwen`, `chat-template`, or `qwen-chat-template` thinking parameters | +| `thinkingFormat` | Use `reasoning_effort`, `openrouter`, `deepseek`, `together`, `baseten`, `zai`, `qwen`, `chat-template`, or `qwen-chat-template` thinking parameters | | `chatTemplateKwargs` | `chat_template_kwargs` values for `thinkingFormat: "chat-template"`; use `{ "$var": "thinking.enabled" }` or `{ "$var": "thinking.effort" }` for pi-controlled thinking values | +| `chatTemplateArgs` | `chat_template_args` values for `thinkingFormat: "baseten"`; use `{ "$var": "thinking.enabled" }` or `{ "$var": "thinking.effort" }` for pi-controlled thinking values | | `cacheControlFormat` | Use Anthropic-style `cache_control` markers on the system prompt, last tool definition, and last user, assistant, or tool-result text content. Currently only `anthropic` is supported. | | `sendSessionAffinityHeaders` | For `openai-completions`, send session-affinity headers from the session id when caching is enabled. Default: `false`. | | `sessionAffinityFormat` | For `openai-completions` and `openai-responses`, the session-affinity header format: `openai` sends `session_id`/`x-client-request-id` (completions also `x-session-affinity`), `openai-nosession` omits the underscore-containing `session_id` header, `openrouter` sends `x-session-id`. Does not affect the `prompt_cache_key` body param. Default: auto-detected. | @@ -459,7 +460,7 @@ For providers with partial OpenAI compatibility, use the `compat` field. | `openRouterRouting` | OpenRouter provider routing preferences. This object is sent as-is in the `provider` field of the [OpenRouter API request](https://openrouter.ai/docs/guides/routing/provider-selection). | | `vercelGatewayRouting` | Vercel AI Gateway routing config for provider selection (`only`, `order`) | -`openrouter` uses `reasoning: { effort }`. `together` uses `reasoning: { enabled }` and also `reasoning_effort` when `supportsReasoningEffort` is enabled. `qwen` uses top-level `enable_thinking`. Use `qwen-chat-template` for local Qwen-compatible servers that require `chat_template_kwargs.enable_thinking` and `preserve_thinking`. Use `chat-template` for vLLM/Hugging Face chat templates that need configurable `chat_template_kwargs`, such as `chatTemplateKwargs: { "thinking": { "$var": "thinking.enabled" } }` for DeepSeek V3.x templates. +`openrouter` uses `reasoning: { effort }`. `together` uses `reasoning: { enabled }` and also `reasoning_effort` when `supportsReasoningEffort` is enabled. `qwen` uses top-level `enable_thinking`. Use `qwen-chat-template` for local Qwen-compatible servers that require `chat_template_kwargs.enable_thinking` and `preserve_thinking`. Use `chat-template` for vLLM/Hugging Face chat templates that need configurable `chat_template_kwargs`, such as `chatTemplateKwargs: { "thinking": { "$var": "thinking.enabled" } }` for DeepSeek V3.x templates. Use `thinkingFormat: "baseten"` with `chatTemplateArgs` for providers that expose toggle controls through `chat_template_args` and optionally support top-level `reasoning_effort`. `cacheControlFormat: "anthropic"` is for OpenAI-compatible providers that expose Anthropic-style prompt caching through `cache_control` markers on text content and tool definitions. diff --git a/packages/coding-agent/docs/providers.md b/packages/coding-agent/docs/providers.md index e70dc4dc1..cac9dc202 100644 --- a/packages/coding-agent/docs/providers.md +++ b/packages/coding-agent/docs/providers.md @@ -92,6 +92,7 @@ pi | Hugging Face | `HF_TOKEN` | `huggingface` | | Fireworks | `FIREWORKS_API_KEY` | `fireworks` | | Together AI | `TOGETHER_API_KEY` | `together` | +| Baseten | `BASETEN_API_KEY` | `baseten` | | Kimi For Coding | `KIMI_API_KEY` | `kimi-coding` | | MiniMax | `MINIMAX_API_KEY` | `minimax` | | MiniMax (China) | `MINIMAX_CN_API_KEY` | `minimax-cn` | diff --git a/packages/coding-agent/src/cli/args.ts b/packages/coding-agent/src/cli/args.ts index 3a1954213..6475ea07d 100644 --- a/packages/coding-agent/src/cli/args.ts +++ b/packages/coding-agent/src/cli/args.ts @@ -377,6 +377,7 @@ ${chalk.bold("Environment Variables:")} XAI_API_KEY - xAI Grok API key FIREWORKS_API_KEY - Fireworks API key TOGETHER_API_KEY - Together AI API key + BASETEN_API_KEY - Baseten API key OPENROUTER_API_KEY - OpenRouter API key AI_GATEWAY_API_KEY - Vercel AI Gateway API key ZAI_API_KEY - ZAI Coding Plan API key (Global) diff --git a/packages/coding-agent/src/core/model-config.ts b/packages/coding-agent/src/core/model-config.ts index 9ee6f4368..768ee7929 100644 --- a/packages/coding-agent/src/core/model-config.ts +++ b/packages/coding-agent/src/core/model-config.ts @@ -84,6 +84,7 @@ const OpenAICompletionsCompatSchema = Type.Object({ Type.Literal("openai"), Type.Literal("openrouter"), Type.Literal("together"), + Type.Literal("baseten"), Type.Literal("deepseek"), Type.Literal("zai"), Type.Literal("qwen"), @@ -94,6 +95,7 @@ const OpenAICompletionsCompatSchema = Type.Object({ ]), ), chatTemplateKwargs: Type.Optional(Type.Record(Type.String(), ChatTemplateKwargSchema)), + chatTemplateArgs: Type.Optional(Type.Record(Type.String(), ChatTemplateKwargSchema)), cacheControlFormat: Type.Optional(Type.Literal("anthropic")), openRouterRouting: Type.Optional(OpenRouterRoutingSchema), vercelGatewayRouting: Type.Optional(VercelGatewayRoutingSchema), diff --git a/packages/coding-agent/src/core/model-resolver.ts b/packages/coding-agent/src/core/model-resolver.ts index 0ed15000b..f27a89af3 100644 --- a/packages/coding-agent/src/core/model-resolver.ts +++ b/packages/coding-agent/src/core/model-resolver.ts @@ -39,6 +39,7 @@ export const defaultModelPerProvider: Record = { huggingface: "moonshotai/Kimi-K2.6", fireworks: "accounts/fireworks/models/kimi-k2p6", together: "moonshotai/Kimi-K2.6", + baseten: "zai-org/GLM-5.2", opencode: "kimi-k2.6", "opencode-go": "kimi-k2.6", "kimi-coding": "kimi-for-coding", diff --git a/packages/coding-agent/src/core/provider-composer.ts b/packages/coding-agent/src/core/provider-composer.ts index a29c37620..5cbdea5fb 100644 --- a/packages/coding-agent/src/core/provider-composer.ts +++ b/packages/coding-agent/src/core/provider-composer.ts @@ -84,7 +84,7 @@ function mergeCompat( const baseNested = base as Record | undefined; const overrideNested = override as Record; const mergedNested = merged as Record; - for (const key of ["openRouterRouting", "vercelGatewayRouting", "chatTemplateKwargs"] as const) { + for (const key of ["openRouterRouting", "vercelGatewayRouting", "chatTemplateKwargs", "chatTemplateArgs"] as const) { const baseValue = baseNested?.[key]; const overrideValue = overrideNested[key]; if ( diff --git a/packages/coding-agent/test/model-registry.test.ts b/packages/coding-agent/test/model-registry.test.ts index 2f7faef3b..bd040101f 100644 --- a/packages/coding-agent/test/model-registry.test.ts +++ b/packages/coding-agent/test/model-registry.test.ts @@ -477,7 +477,7 @@ describe("ModelRegistry", () => { api: "openai-completions", models: [ { - id: "demo-model", + id: "kwargs-model", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, @@ -491,19 +491,38 @@ describe("ModelRegistry", () => { }, }, }, + { + id: "args-model", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 1000, + maxTokens: 100, + compat: { + thinkingFormat: "baseten", + chatTemplateArgs: { + enable_thinking: { $var: "thinking.enabled" }, + }, + }, + }, ], }, }); const registry = await createModelRegistry(authStorage, modelsJsonPath); - const compat = registry.find("demo", "demo-model")?.compat as OpenAICompletionsCompat | undefined; + const kwargsCompat = registry.find("demo", "kwargs-model")?.compat as OpenAICompletionsCompat | undefined; + const argsCompat = registry.find("demo", "args-model")?.compat as OpenAICompletionsCompat | undefined; expect(registry.getError()).toBeUndefined(); - expect(compat?.thinkingFormat).toBe("chat-template"); - expect(compat?.chatTemplateKwargs).toEqual({ + expect(kwargsCompat?.thinkingFormat).toBe("chat-template"); + expect(kwargsCompat?.chatTemplateKwargs).toEqual({ preserve_thinking: true, thinking: { $var: "thinking.enabled" }, }); + expect(argsCompat?.thinkingFormat).toBe("baseten"); + expect(argsCompat?.chatTemplateArgs).toEqual({ + enable_thinking: { $var: "thinking.enabled" }, + }); }); test("compat schema accepts Anthropic eager tool input streaming flag", async () => {