fix umans escaped tool roundtrip

This commit is contained in:
oldschoola
2026-06-16 01:32:50 -07:00
parent 251d7dbb86
commit 37ceece944
4 changed files with 76 additions and 23 deletions
+1 -1
View File
@@ -8,7 +8,7 @@
### Fixed
- Fixed Anthropic-compatible Umans requests escaping client `web_search` tools so Kimi answers normally instead of returning raw gateway search results.
- Fixed Anthropic-compatible Umans requests escaping client tool names so Kimi answers normally instead of returning raw gateway search results.
## [16.0.1] - 2026-06-15
+3 -19
View File
@@ -758,18 +758,6 @@ function isUmansGatewayWebSearchEnabled(
return isUmansAnthropicModel(model) && getUmansWebSearchProvider(headers) !== undefined;
}
function shouldEscapeAnthropicBuiltinToolName(
name: string,
escapeBuiltinToolNames: boolean,
useUmansGatewayWebSearch: boolean,
): boolean {
return (
escapeBuiltinToolNames &&
ANTHROPIC_BUILTIN_TOOL_NAMES.has(name.toLowerCase()) &&
!shouldUseUmansGatewayWebSearch(name, useUmansGatewayWebSearch)
);
}
function encodeAnthropicToolName(
name: string,
isOAuthToken: boolean,
@@ -777,17 +765,13 @@ function encodeAnthropicToolName(
useUmansGatewayWebSearch = false,
): string {
if (shouldUseUmansGatewayWebSearch(name, useUmansGatewayWebSearch)) return name;
if (shouldEscapeAnthropicBuiltinToolName(name, escapeBuiltinToolNames, useUmansGatewayWebSearch)) {
return `${claudeToolPrefix}${name}`;
}
if (escapeBuiltinToolNames) return `${claudeToolPrefix}${name}`;
return isOAuthToken ? applyClaudeToolPrefix(name) : name;
}
function decodeAnthropicToolName(name: string, isOAuthToken: boolean, escapeBuiltinToolNames: boolean): string {
if (isOAuthToken) return stripClaudeToolPrefix(name);
if (!escapeBuiltinToolNames) return name;
const stripped = stripClaudeToolPrefix(name);
return ANTHROPIC_BUILTIN_TOOL_NAMES.has(stripped.toLowerCase()) ? stripped : name;
if (isOAuthToken || escapeBuiltinToolNames) return stripClaudeToolPrefix(name);
return name;
}
const ANTHROPIC_MANY_IMAGE_THRESHOLD = 20;
+12 -3
View File
@@ -823,7 +823,7 @@ describe("Anthropic request fingerprint alignment", () => {
expect(payload.metadata?.user_id).not.toBe("invalid-user-id");
expectClaudeMetadataUserId(payload.metadata?.user_id);
});
it("escapes Anthropic built-in tool names for compatible gateways", async () => {
it("escapes Umans client tool names for unambiguous round-trips", async () => {
const tools: Tool[] = [
{
name: "web_search",
@@ -843,6 +843,15 @@ describe("Anthropic request fingerprint alignment", () => {
required: ["path"],
},
},
{
name: "_web_search",
description: "literal prefixed search",
parameters: {
type: "object",
properties: { query: { type: "string" } },
required: ["query"],
},
},
];
const payload = (await captureAnthropicPayload(
@@ -858,7 +867,7 @@ describe("Anthropic request fingerprint alignment", () => {
tool_choice?: { type?: string; name?: string };
};
expect(payload.tools?.map(tool => tool.name)).toEqual(["_web_search", "read"]);
expect(payload.tools?.map(tool => tool.name)).toEqual(["_web_search", "_read", "__web_search"]);
expect(payload.tool_choice).toEqual({ type: "tool", name: "_web_search" });
});
@@ -901,7 +910,7 @@ describe("Anthropic request fingerprint alignment", () => {
tool_choice?: { type?: string; name?: string };
};
expect(payload.tools?.map(tool => tool.name)).toEqual(["web_search", "read"]);
expect(payload.tools?.map(tool => tool.name)).toEqual(["web_search", "_read"]);
expect(payload.tool_choice).toEqual({ type: "tool", name: "web_search" });
});
@@ -383,6 +383,66 @@ describe("anthropic stream envelope handling", () => {
]);
});
it("decodes escaped literal-prefixed Umans tool names", async () => {
vi.spyOn(AnthropicMessages.prototype, "create").mockImplementation(
() =>
createMockRequest([
{
type: "message_start",
message: {
id: "msg_literal_tool",
usage: {
input_tokens: 12,
output_tokens: 0,
cache_read_input_tokens: 0,
cache_creation_input_tokens: 0,
},
},
},
{
type: "content_block_start",
index: 0,
content_block: { type: "tool_use", id: "tool_1", name: "__web_search", input: {} },
},
{
type: "content_block_delta",
index: 0,
delta: { type: "input_json_delta", partial_json: '{"query":"literal"}' },
},
{ type: "content_block_stop", index: 0 },
{
type: "message_delta",
delta: { stop_reason: "tool_use" },
usage: {
input_tokens: 12,
output_tokens: 4,
cache_read_input_tokens: 0,
cache_creation_input_tokens: 0,
},
},
{ type: "message_stop" },
]) as never,
);
const stream = streamAnthropic(umansModel, context, { apiKey: "sk-ant-test" });
const events: AssistantMessageEvent[] = [];
for await (const event of stream) {
events.push(event);
}
const result = await stream.result();
expect(countEvents(events, "toolcall_start")).toBe(1);
expect(result.stopReason).toBe("toolUse");
expect(result.content).toEqual([
{
type: "toolCall",
id: "tool_1",
name: "_web_search",
arguments: { query: "literal" },
},
]);
});
it("ignores Umans gateway web search server blocks and keeps final text", async () => {
vi.spyOn(AnthropicMessages.prototype, "create").mockImplementation(
() =>