refactor: use model compat for anthropic tool payload normalization

This commit is contained in:
Peter Steinberger
2026-03-08 16:23:41 +00:00
parent efcca3d2ea
commit e5c06dd64a
4 changed files with 100 additions and 13 deletions

View File

@@ -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,
},
},
],
};

View File

@@ -880,6 +880,57 @@ describe("applyExtraParamsToAgent", () => {
]);
});
it("uses explicit compat metadata for anthropic tool payload normalization", () => {
const payloads: Record<string, unknown>[] = [];
const baseStreamFn: StreamFn = (_model, _context, options) => {
const payload: Record<string, unknown> = {
tools: [
{
name: "read",
description: "Read file",
input_schema: { type: "object", properties: {} },
},
],
};
options?.onPayload?.(payload);
payloads.push(payload);
return {} as ReturnType<StreamFn>;
};
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<string, unknown>[] = [];
const baseStreamFn: StreamFn = (_model, _context, options) => {

View File

@@ -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<string, unknown>;
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<string, unknown> => !!tool);
}
if (usesOpenAiStringModeAnthropicToolChoice(provider)) {
if (usesOpenAiStringModeAnthropicToolChoiceForModel(model)) {
payloadObj.tool_choice = normalizeOpenAiStringModeAnthropicToolChoice(
payloadObj.tool_choice,
);

View File

@@ -26,6 +26,7 @@ export type ModelCompatConfig = {
requiresAssistantAfterToolResult?: boolean;
requiresThinkingAsText?: boolean;
requiresMistralToolIds?: boolean;
requiresOpenAiAnthropicToolPayload?: boolean;
};
export type ModelProviderAuthMode = "api-key" | "aws-sdk" | "oauth" | "token";