Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 28 additions & 6 deletions src/adapters/anthropic/passthrough.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,14 +150,36 @@ export function anthropicOAuthWireBody(body: Rec): { body: Rec; toolNames: Map<s
if (isRec(body.tool_choice) && body.tool_choice.type === "tool" && renames(body.tool_choice.name)) {
out.tool_choice = { ...body.tool_choice, name: wireName(body.tool_choice.name) };
}
const isRenamedUse = (block: unknown): block is Rec & { name: string } => isRec(block) && block.type === "tool_use" && renames(block.name);
const mapContentBlocks = (blocks: unknown[]): { blocks: unknown[]; changed: boolean } => {
let changed = false;
const mapped = blocks.map(block => {
if (!isRec(block)) return block;
if (block.type === "tool_use" && renames(block.name)) {
changed = true;
return { ...block, name: wireName(block.name) };
}
if (block.type === "tool_reference" && renames(block.tool_name)) {
changed = true;
return { ...block, tool_name: wireName(block.tool_name) };
}
// tool_reference blocks are nested inside tool_result content. Follow only that
// typed content container; tool inputs and schemas are opaque caller data.
if (block.type === "tool_result" && Array.isArray(block.content)) {
const nested = mapContentBlocks(block.content);
if (nested.changed) {
changed = true;
return { ...block, content: nested.blocks };
}
}
return block;
});
return { blocks: mapped, changed };
};
if (Array.isArray(body.messages)) {
out.messages = body.messages.map(message => {
if (!isRec(message) || !Array.isArray(message.content) || !message.content.some(isRenamedUse)) return message;
return {
...message,
content: message.content.map(block => isRenamedUse(block) ? { ...block, name: wireName(block.name) } : block),
};
if (!isRec(message) || !Array.isArray(message.content)) return message;
const content = mapContentBlocks(message.content);
return content.changed ? { ...message, content: content.blocks } : message;
});
}
return { body: out, toolNames };
Expand Down
3 changes: 3 additions & 0 deletions structure/data-planes/protocol-paths.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,9 @@ builds without a provider UUID.
Key-auth and caller-forward requests retain their metadata. The answer's
`tool_use` names are mapped back for exactly those names. A 401 or 429 is answered as the bridge
answers an unpooled account: no refresh replay, no same-token replay, no rotation.
Native OAuth Messages rename typed `tool_reference.tool_name` blocks consistently with declared
client tools, including nested tool-result content. Cache markers and lifetimes are retained,
and arbitrary tool arguments and input schemas are not traversed.

`handleNativeMessages` mirrors native Chat on the shared pieces: `beginInferenceAttempt`,
`createFinalRequestLog`, the request spend tracker charged per physical send, proactive key
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,14 @@ const SOURCE = {
messages: [
{ role: "user", content: "fixture question" },
{ role: "assistant", content: [
{ type: "tool_use", id: "toolu_1", name: "lookup", input: {} },
{ type: "tool_use", id: "toolu_1", name: "lookup", input: { opaque: { type: "tool_reference", tool_name: "lookup" } } },
{ type: "tool_use", id: "toolu_2", name: "bash", input: { command: "true" } },
] },
{ role: "user", content: [
{ type: "tool_result", tool_use_id: "toolu_1", content: "fixture result" },
{ type: "tool_result", tool_use_id: "toolu_1", content: [
{ type: "text", text: "fixture result", cache_control: { type: "ephemeral", ttl: "1h", scope: "turn" } },
{ type: "tool_reference", tool_name: "lookup" },
] },
{ type: "tool_result", tool_use_id: "toolu_2", content: "" },
] },
],
Expand Down Expand Up @@ -100,10 +103,15 @@ describe("buildAnthropicMessagesPassthroughRequest with OAuth", () => {
expect(wire.tool_choice).toEqual({ type: "tool", name: "custom_lookup" });
const history = wire.messages[1]!.content as { name: string }[];
expect(history.map(block => block.name)).toEqual(["custom_lookup", "bash"]);
expect((history[0] as { input: unknown }).input).toEqual({ opaque: { type: "tool_reference", tool_name: "lookup" } });
const resultContent = (wire.messages[2]!.content as { content: { type: string; tool_name?: string; cache_control?: unknown }[] }[])[0]!.content;
expect(resultContent[1]).toEqual({ type: "tool_reference", tool_name: "custom_lookup" });
expect(resultContent[0]!.cache_control).toEqual({ type: "ephemeral", ttl: "1h", scope: "turn" });
expect([...built.oauthToolNames!]).toEqual([["custom_lookup", "lookup"]]);
// The source is untouched.
expect(SOURCE.tools[0]!.name).toBe("lookup");
expect(SOURCE.system).toBe("fixture system");
expect((SOURCE.messages[2]!.content[0] as { content: { tool_name?: string }[] }).content[1]!.tool_name).toBe("lookup");
});

test("an identity block already present is not repeated", () => {
Expand Down
Loading