Skip to content

Commit e5c06dd

Browse files
committed
refactor: use model compat for anthropic tool payload normalization
1 parent efcca3d commit e5c06dd

File tree

4 files changed

+100
-13
lines changed

4 files changed

+100
-13
lines changed

src/agents/models-config.providers.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -837,6 +837,9 @@ export function buildKimiCodingProvider(): ProviderConfig {
837837
cost: KIMI_CODING_DEFAULT_COST,
838838
contextWindow: KIMI_CODING_DEFAULT_CONTEXT_WINDOW,
839839
maxTokens: KIMI_CODING_DEFAULT_MAX_TOKENS,
840+
compat: {
841+
requiresOpenAiAnthropicToolPayload: true,
842+
},
840843
},
841844
],
842845
};

src/agents/pi-embedded-runner-extraparams.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -880,6 +880,57 @@ describe("applyExtraParamsToAgent", () => {
880880
]);
881881
});
882882

883+
it("uses explicit compat metadata for anthropic tool payload normalization", () => {
884+
const payloads: Record<string, unknown>[] = [];
885+
const baseStreamFn: StreamFn = (_model, _context, options) => {
886+
const payload: Record<string, unknown> = {
887+
tools: [
888+
{
889+
name: "read",
890+
description: "Read file",
891+
input_schema: { type: "object", properties: {} },
892+
},
893+
],
894+
};
895+
options?.onPayload?.(payload);
896+
payloads.push(payload);
897+
return {} as ReturnType<StreamFn>;
898+
};
899+
const agent = { streamFn: baseStreamFn };
900+
901+
applyExtraParamsToAgent(
902+
agent,
903+
undefined,
904+
"custom-anthropic-proxy",
905+
"proxy-model",
906+
undefined,
907+
"low",
908+
);
909+
910+
const model = {
911+
api: "anthropic-messages",
912+
provider: "custom-anthropic-proxy",
913+
id: "proxy-model",
914+
compat: {
915+
requiresOpenAiAnthropicToolPayload: true,
916+
},
917+
} as Model<"anthropic-messages">;
918+
const context: Context = { messages: [] };
919+
void agent.streamFn?.(model, context, {});
920+
921+
expect(payloads).toHaveLength(1);
922+
expect(payloads[0]?.tools).toEqual([
923+
{
924+
type: "function",
925+
function: {
926+
name: "read",
927+
description: "Read file",
928+
parameters: { type: "object", properties: {} },
929+
},
930+
},
931+
]);
932+
});
933+
883934
it("removes invalid negative Google thinkingBudget and maps Gemini 3.1 to thinkingLevel", () => {
884935
const payloads: Record<string, unknown>[] = [];
885936
const baseStreamFn: StreamFn = (_model, _context, options) => {

src/agents/pi-embedded-runner/extra-params.ts

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -794,7 +794,7 @@ function createMoonshotThinkingWrapper(
794794
function requiresAnthropicToolPayloadCompatibilityForModel(model: {
795795
api?: unknown;
796796
provider?: unknown;
797-
baseUrl?: unknown;
797+
compat?: unknown;
798798
}): boolean {
799799
if (model.api !== "anthropic-messages") {
800800
return false;
@@ -807,19 +807,49 @@ function requiresAnthropicToolPayloadCompatibilityForModel(model: {
807807
return true;
808808
}
809809

810-
if (typeof model.baseUrl !== "string" || !model.baseUrl.trim()) {
810+
if (!model.compat || typeof model.compat !== "object" || Array.isArray(model.compat)) {
811811
return false;
812812
}
813813

814-
try {
815-
const parsed = new URL(model.baseUrl);
816-
const host = parsed.hostname.toLowerCase();
817-
const pathname = parsed.pathname.toLowerCase();
818-
return host.endsWith("kimi.com") && pathname.startsWith("/coding");
819-
} catch {
820-
const normalized = model.baseUrl.toLowerCase();
821-
return normalized.includes("kimi.com/coding");
814+
return (
815+
(model.compat as { requiresOpenAiAnthropicToolPayload?: unknown })
816+
.requiresOpenAiAnthropicToolPayload === true
817+
);
818+
}
819+
820+
function usesOpenAiFunctionAnthropicToolSchemaForModel(model: {
821+
provider?: unknown;
822+
compat?: unknown;
823+
}): boolean {
824+
if (typeof model.provider === "string" && usesOpenAiFunctionAnthropicToolSchema(model.provider)) {
825+
return true;
826+
}
827+
if (!model.compat || typeof model.compat !== "object" || Array.isArray(model.compat)) {
828+
return false;
822829
}
830+
return (
831+
(model.compat as { requiresOpenAiAnthropicToolPayload?: unknown })
832+
.requiresOpenAiAnthropicToolPayload === true
833+
);
834+
}
835+
836+
function usesOpenAiStringModeAnthropicToolChoiceForModel(model: {
837+
provider?: unknown;
838+
compat?: unknown;
839+
}): boolean {
840+
if (
841+
typeof model.provider === "string" &&
842+
usesOpenAiStringModeAnthropicToolChoice(model.provider)
843+
) {
844+
return true;
845+
}
846+
if (!model.compat || typeof model.compat !== "object" || Array.isArray(model.compat)) {
847+
return false;
848+
}
849+
return (
850+
(model.compat as { requiresOpenAiAnthropicToolPayload?: unknown })
851+
.requiresOpenAiAnthropicToolPayload === true
852+
);
823853
}
824854

825855
function normalizeOpenAiFunctionAnthropicToolDefinition(
@@ -903,19 +933,21 @@ function createAnthropicToolPayloadCompatibilityWrapper(
903933
return underlying(model, context, {
904934
...options,
905935
onPayload: (payload) => {
906-
const provider = typeof model.provider === "string" ? model.provider : undefined;
907936
if (
908937
payload &&
909938
typeof payload === "object" &&
910939
requiresAnthropicToolPayloadCompatibilityForModel(model)
911940
) {
912941
const payloadObj = payload as Record<string, unknown>;
913-
if (Array.isArray(payloadObj.tools) && usesOpenAiFunctionAnthropicToolSchema(provider)) {
942+
if (
943+
Array.isArray(payloadObj.tools) &&
944+
usesOpenAiFunctionAnthropicToolSchemaForModel(model)
945+
) {
914946
payloadObj.tools = payloadObj.tools
915947
.map((tool) => normalizeOpenAiFunctionAnthropicToolDefinition(tool))
916948
.filter((tool): tool is Record<string, unknown> => !!tool);
917949
}
918-
if (usesOpenAiStringModeAnthropicToolChoice(provider)) {
950+
if (usesOpenAiStringModeAnthropicToolChoiceForModel(model)) {
919951
payloadObj.tool_choice = normalizeOpenAiStringModeAnthropicToolChoice(
920952
payloadObj.tool_choice,
921953
);

src/config/types.models.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export type ModelCompatConfig = {
2626
requiresAssistantAfterToolResult?: boolean;
2727
requiresThinkingAsText?: boolean;
2828
requiresMistralToolIds?: boolean;
29+
requiresOpenAiAnthropicToolPayload?: boolean;
2930
};
3031

3132
export type ModelProviderAuthMode = "api-key" | "aws-sdk" | "oauth" | "token";

0 commit comments

Comments
 (0)