From e5c06dd64a167b5e6135ea7356427332ac17e596 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 8 Mar 2026 16:23:41 +0000 Subject: [PATCH] refactor: use model compat for anthropic tool payload normalization --- src/agents/models-config.providers.ts | 3 + .../pi-embedded-runner-extraparams.test.ts | 51 ++++++++++++++++ src/agents/pi-embedded-runner/extra-params.ts | 58 ++++++++++++++----- src/config/types.models.ts | 1 + 4 files changed, 100 insertions(+), 13 deletions(-) diff --git a/src/agents/models-config.providers.ts b/src/agents/models-config.providers.ts index 8ae03757bea..56dd6b5d948 100644 --- a/src/agents/models-config.providers.ts +++ b/src/agents/models-config.providers.ts @@ -837,6 +837,9 @@ export function buildKimiCodingProvider(): ProviderConfig { cost: KIMI_CODING_DEFAULT_COST, contextWindow: KIMI_CODING_DEFAULT_CONTEXT_WINDOW, maxTokens: KIMI_CODING_DEFAULT_MAX_TOKENS, + compat: { + requiresOpenAiAnthropicToolPayload: true, + }, }, ], }; diff --git a/src/agents/pi-embedded-runner-extraparams.test.ts b/src/agents/pi-embedded-runner-extraparams.test.ts index 97553675759..fb3569369ca 100644 --- a/src/agents/pi-embedded-runner-extraparams.test.ts +++ b/src/agents/pi-embedded-runner-extraparams.test.ts @@ -880,6 +880,57 @@ describe("applyExtraParamsToAgent", () => { ]); }); + it("uses explicit compat metadata for anthropic tool payload normalization", () => { + const payloads: Record[] = []; + const baseStreamFn: StreamFn = (_model, _context, options) => { + const payload: Record = { + tools: [ + { + name: "read", + description: "Read file", + input_schema: { type: "object", properties: {} }, + }, + ], + }; + options?.onPayload?.(payload); + payloads.push(payload); + return {} as ReturnType; + }; + const agent = { streamFn: baseStreamFn }; + + applyExtraParamsToAgent( + agent, + undefined, + "custom-anthropic-proxy", + "proxy-model", + undefined, + "low", + ); + + const model = { + api: "anthropic-messages", + provider: "custom-anthropic-proxy", + id: "proxy-model", + compat: { + requiresOpenAiAnthropicToolPayload: true, + }, + } as Model<"anthropic-messages">; + const context: Context = { messages: [] }; + void agent.streamFn?.(model, context, {}); + + expect(payloads).toHaveLength(1); + expect(payloads[0]?.tools).toEqual([ + { + type: "function", + function: { + name: "read", + description: "Read file", + parameters: { type: "object", properties: {} }, + }, + }, + ]); + }); + it("removes invalid negative Google thinkingBudget and maps Gemini 3.1 to thinkingLevel", () => { const payloads: Record[] = []; const baseStreamFn: StreamFn = (_model, _context, options) => { diff --git a/src/agents/pi-embedded-runner/extra-params.ts b/src/agents/pi-embedded-runner/extra-params.ts index 7ac14f9ee98..23178e1d1fa 100644 --- a/src/agents/pi-embedded-runner/extra-params.ts +++ b/src/agents/pi-embedded-runner/extra-params.ts @@ -794,7 +794,7 @@ function createMoonshotThinkingWrapper( function requiresAnthropicToolPayloadCompatibilityForModel(model: { api?: unknown; provider?: unknown; - baseUrl?: unknown; + compat?: unknown; }): boolean { if (model.api !== "anthropic-messages") { return false; @@ -807,19 +807,49 @@ function requiresAnthropicToolPayloadCompatibilityForModel(model: { return true; } - if (typeof model.baseUrl !== "string" || !model.baseUrl.trim()) { + if (!model.compat || typeof model.compat !== "object" || Array.isArray(model.compat)) { return false; } - try { - const parsed = new URL(model.baseUrl); - const host = parsed.hostname.toLowerCase(); - const pathname = parsed.pathname.toLowerCase(); - return host.endsWith("kimi.com") && pathname.startsWith("/coding"); - } catch { - const normalized = model.baseUrl.toLowerCase(); - return normalized.includes("kimi.com/coding"); + return ( + (model.compat as { requiresOpenAiAnthropicToolPayload?: unknown }) + .requiresOpenAiAnthropicToolPayload === true + ); +} + +function usesOpenAiFunctionAnthropicToolSchemaForModel(model: { + provider?: unknown; + compat?: unknown; +}): boolean { + if (typeof model.provider === "string" && usesOpenAiFunctionAnthropicToolSchema(model.provider)) { + return true; } + if (!model.compat || typeof model.compat !== "object" || Array.isArray(model.compat)) { + return false; + } + return ( + (model.compat as { requiresOpenAiAnthropicToolPayload?: unknown }) + .requiresOpenAiAnthropicToolPayload === true + ); +} + +function usesOpenAiStringModeAnthropicToolChoiceForModel(model: { + provider?: unknown; + compat?: unknown; +}): boolean { + if ( + typeof model.provider === "string" && + usesOpenAiStringModeAnthropicToolChoice(model.provider) + ) { + return true; + } + if (!model.compat || typeof model.compat !== "object" || Array.isArray(model.compat)) { + return false; + } + return ( + (model.compat as { requiresOpenAiAnthropicToolPayload?: unknown }) + .requiresOpenAiAnthropicToolPayload === true + ); } function normalizeOpenAiFunctionAnthropicToolDefinition( @@ -903,19 +933,21 @@ function createAnthropicToolPayloadCompatibilityWrapper( return underlying(model, context, { ...options, onPayload: (payload) => { - const provider = typeof model.provider === "string" ? model.provider : undefined; if ( payload && typeof payload === "object" && requiresAnthropicToolPayloadCompatibilityForModel(model) ) { const payloadObj = payload as Record; - if (Array.isArray(payloadObj.tools) && usesOpenAiFunctionAnthropicToolSchema(provider)) { + if ( + Array.isArray(payloadObj.tools) && + usesOpenAiFunctionAnthropicToolSchemaForModel(model) + ) { payloadObj.tools = payloadObj.tools .map((tool) => normalizeOpenAiFunctionAnthropicToolDefinition(tool)) .filter((tool): tool is Record => !!tool); } - if (usesOpenAiStringModeAnthropicToolChoice(provider)) { + if (usesOpenAiStringModeAnthropicToolChoiceForModel(model)) { payloadObj.tool_choice = normalizeOpenAiStringModeAnthropicToolChoice( payloadObj.tool_choice, ); diff --git a/src/config/types.models.ts b/src/config/types.models.ts index b881269d961..f244c9d0658 100644 --- a/src/config/types.models.ts +++ b/src/config/types.models.ts @@ -26,6 +26,7 @@ export type ModelCompatConfig = { requiresAssistantAfterToolResult?: boolean; requiresThinkingAsText?: boolean; requiresMistralToolIds?: boolean; + requiresOpenAiAnthropicToolPayload?: boolean; }; export type ModelProviderAuthMode = "api-key" | "aws-sdk" | "oauth" | "token";