mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
parent
a24fb9e96a
commit
c1019d9202
34 changed files with 480 additions and 21 deletions
|
|
@ -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)).
|
||||
|
|
|
|||
|
|
@ -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<string, string | number | boolean | null | { '$var': 'thinking.enabled' | 'thinking.effort'; omitWhenOff?: boolean }>; // chat_template_kwargs values; use $var for pi-controlled thinking values
|
||||
chatTemplateArgs?: Record<string, string | number | boolean | null | { '$var': 'thinking.enabled' | 'thinking.effort'; omitWhenOff?: boolean }>; // 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: {})
|
||||
|
|
|
|||
|
|
@ -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<Model<any>[]> {
|
|||
}
|
||||
}
|
||||
|
||||
function processBasetenModels(provider: ModelsDevProvider | undefined): Model<Api>[] {
|
||||
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<Api>[] = [];
|
||||
|
||||
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<Model<any>[]> {
|
||||
try {
|
||||
console.log("Fetching models from models.dev API...");
|
||||
|
|
@ -1617,7 +1714,7 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
|
|||
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<Model<any>[]> {
|
|||
}
|
||||
}
|
||||
|
||||
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<Model<any>[]> {
|
|||
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<Model<any>[]> {
|
|||
// 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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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<typeof params, "reasoning_effort"> & {
|
||||
chat_template_args?: Record<string, ResolvedChatTemplateKwargValue>;
|
||||
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<string, ChatTemplateKwargValue>,
|
||||
): Record<string, ResolvedChatTemplateKwargValue> | undefined {
|
||||
const kwargs: Record<string, ResolvedChatTemplateKwargValue> = {};
|
||||
const resolvedValues: Record<string, ResolvedChatTemplateKwargValue> = {};
|
||||
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
8
packages/ai/src/providers/baseten.models.ts
Normal file
8
packages/ai/src/providers/baseten.models.ts
Normal file
|
|
@ -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<typeof values, "baseten"> =
|
||||
flattenModelCatalog("baseten", values);
|
||||
15
packages/ai/src/providers/baseten.ts
Normal file
15
packages/ai/src/providers/baseten.ts
Normal file
|
|
@ -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(),
|
||||
});
|
||||
}
|
||||
|
|
@ -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<string, ChatTemplateKwargValue>;
|
||||
/** 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<string, ChatTemplateKwargValue>;
|
||||
/** 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. */
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
141
packages/ai/test/baseten-models.test.ts
Normal file
141
packages/ai/test/baseten-models.test.ts
Normal file
|
|
@ -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<string, unknown> | 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<string, unknown>;
|
||||
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<string, unknown> | undefined;
|
||||
|
||||
await streamSimple(
|
||||
model,
|
||||
{ messages: [{ role: "user", content: "test", timestamp: 0 }] },
|
||||
{
|
||||
apiKey: "test-baseten-key",
|
||||
reasoning: "high",
|
||||
onPayload: (value) => {
|
||||
payload = value as Record<string, unknown>;
|
||||
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<string, unknown> | undefined;
|
||||
|
||||
await streamSimple(
|
||||
model,
|
||||
{ messages: [{ role: "user", content: "test", timestamp: 0 }] },
|
||||
{
|
||||
apiKey: "test-baseten-key",
|
||||
onPayload: (value) => {
|
||||
payload = value as Record<string, unknown>;
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -368,6 +368,7 @@ describe("deferred tools", () => {
|
|||
openRouterRouting: {},
|
||||
vercelGatewayRouting: {},
|
||||
chatTemplateKwargs: {},
|
||||
chatTemplateArgs: {},
|
||||
zaiToolStream: false,
|
||||
supportsStrictMode: false,
|
||||
supportsOpenAIGrammarTools: false,
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ const compat = {
|
|||
openRouterRouting: {},
|
||||
vercelGatewayRouting: {},
|
||||
chatTemplateKwargs: {},
|
||||
chatTemplateArgs: {},
|
||||
zaiToolStream: false,
|
||||
supportsStrictMode: true,
|
||||
supportsOpenAIGrammarTools: false,
|
||||
|
|
|
|||
|
|
@ -1295,6 +1295,7 @@ describe("openai-completions tool_choice", () => {
|
|||
openRouterRouting: {},
|
||||
vercelGatewayRouting: {},
|
||||
chatTemplateKwargs: {},
|
||||
chatTemplateArgs: {},
|
||||
zaiToolStream: false,
|
||||
supportsStrictMode: true,
|
||||
supportsOpenAIGrammarTools: false,
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ const compat: Omit<Required<OpenAICompletionsCompat>, "deferredToolsMode"> & {
|
|||
openRouterRouting: {},
|
||||
vercelGatewayRouting: {},
|
||||
chatTemplateKwargs: {},
|
||||
chatTemplateArgs: {},
|
||||
zaiToolStream: false,
|
||||
supportsStrictMode: true,
|
||||
supportsOpenAIGrammarTools: false,
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// =========================================================================
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<string, string | number | boolean | null | { "$var": "thinking.enabled" | "thinking.effort"; omitWhenOff?: boolean }>;
|
||||
chatTemplateArgs?: Record<string, string | number | boolean | null | { "$var": "thinking.enabled" | "thinking.effort"; omitWhenOff?: boolean }>;
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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` |
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ export const defaultModelPerProvider: Record<KnownProvider, string> = {
|
|||
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",
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ function mergeCompat(
|
|||
const baseNested = base as Record<string, unknown> | undefined;
|
||||
const overrideNested = override as Record<string, unknown>;
|
||||
const mergedNested = merged as Record<string, unknown>;
|
||||
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 (
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue