feat: pass tools through CC compatible bridge

This commit is contained in:
R.D.
2026-04-01 04:55:25 -04:00
parent 32dc3b36ab
commit a381e9aa3b
2 changed files with 145 additions and 2 deletions

View File

@@ -130,12 +130,19 @@ export function buildClaudeCodeCompatibleRequest({
const resolvedSessionId = sessionId || randomUUID();
const effort = resolveClaudeCodeCompatibleEffort(sourceBody, normalizedBody, model);
const maxTokens = resolveClaudeCodeCompatibleMaxTokens(sourceBody, normalizedBody);
const tools = buildClaudeCodeCompatibleTools(normalizedBody, sourceBody);
const toolChoice =
tools.length > 0
? buildClaudeCodeCompatibleToolChoice(
normalizedBody?.["tool_choice"] ?? sourceBody?.["tool_choice"]
)
: undefined;
return {
model,
messages,
system,
tools: [],
tools,
metadata: {
user_id: JSON.stringify({
device_id: createHash("sha256")
@@ -161,6 +168,7 @@ export function buildClaudeCodeCompatibleRequest({
output_config: {
effort,
},
...(toolChoice ? { tool_choice: toolChoice } : {}),
...(stream ? { stream: true } : {}),
};
}
@@ -313,6 +321,81 @@ function convertClaudeCodeCompatibleMessage(message: MessageLike | null | undefi
};
}
function buildClaudeCodeCompatibleTools(
normalizedBody?: Record<string, unknown> | null,
sourceBody?: Record<string, unknown> | null
) {
const rawTools = Array.isArray(normalizedBody?.["tools"])
? normalizedBody?.["tools"]
: Array.isArray(sourceBody?.["tools"])
? sourceBody?.["tools"]
: [];
return rawTools
.map((tool) => convertClaudeCodeCompatibleTool(tool))
.filter((tool): tool is Record<string, unknown> => !!tool);
}
function convertClaudeCodeCompatibleTool(tool: unknown) {
const rawTool = readRecord(tool);
if (!rawTool) return null;
const toolData =
rawTool.type === "function" ? readRecord(rawTool.function) || rawTool : rawTool;
const name = toNonEmptyString(toolData.name);
if (!name) return null;
const rawSchema =
readRecord(toolData.parameters) ||
readRecord(toolData.input_schema) || { type: "object", properties: {}, required: [] };
const inputSchema =
rawSchema.type === "object" && !readRecord(rawSchema.properties)
? { ...rawSchema, properties: {} }
: rawSchema;
const converted: Record<string, unknown> = {
name,
description: toNonEmptyString(toolData.description) || "",
input_schema: inputSchema,
};
if (typeof toolData.defer_loading === "boolean") {
converted.defer_loading = toolData.defer_loading;
}
return converted;
}
function buildClaudeCodeCompatibleToolChoice(choice: unknown) {
if (!choice) return null;
if (typeof choice === "string") {
if (choice === "required") return { type: "any" };
return null;
}
const rawChoice = readRecord(choice);
if (!rawChoice) return null;
if (rawChoice.type === "tool") {
const name = toNonEmptyString(rawChoice.name);
return name ? { type: "tool", name } : null;
}
if (rawChoice.type === "function") {
const functionName =
toNonEmptyString(readRecord(rawChoice.function)?.name) || toNonEmptyString(rawChoice.name);
return functionName ? { type: "tool", name: functionName } : null;
}
if (rawChoice.type === "required" || rawChoice.type === "any") {
return { type: "any" };
}
return null;
}
function contentToText(content: unknown): string {
if (typeof content === "string") {
return content.trim();
@@ -381,6 +464,12 @@ function toNonEmptyString(value: unknown): string | null {
return trimmed || null;
}
function readRecord(value: unknown): Record<string, unknown> | null {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: null;
}
function readNestedString(
source: Record<string, unknown> | null | undefined,
path: string[]

View File

@@ -54,8 +54,25 @@ test("buildClaudeCodeCompatibleRequest keeps order/text while mapping unsupporte
const payload = buildClaudeCodeCompatibleRequest({
sourceBody: {
reasoning_effort: "xhigh",
tool_choice: "required",
},
normalizedBody: {
tools: [
{
type: "function",
function: {
name: "lookup_weather",
description: "Fetch weather",
parameters: {
type: "object",
properties: {
city: { type: "string" },
},
required: ["city"],
},
},
},
],
messages: [
{ role: "system", content: "sys" },
{ role: "user", content: [{ type: "text", text: "u1" }, { type: "image_url" }] },
@@ -85,7 +102,19 @@ test("buildClaudeCodeCompatibleRequest keeps order/text while mapping unsupporte
assert.deepEqual(payload.messages.at(-1).content.at(-1).cache_control, { type: "ephemeral" });
assert.equal(payload.system.length, 4);
assert.equal(payload.system.at(-1).text, "sys");
assert.equal(payload.tools.length, 0);
assert.equal(payload.tools.length, 1);
assert.deepEqual(payload.tools[0], {
name: "lookup_weather",
description: "Fetch weather",
input_schema: {
type: "object",
properties: {
city: { type: "string" },
},
required: ["city"],
},
});
assert.deepEqual(payload.tool_choice, { type: "any" });
assert.equal(payload.context_management.edits[0].type, "clear_thinking_20251015");
assert.equal(JSON.parse(payload.metadata.user_id).session_id, "session-1");
});
@@ -103,6 +132,31 @@ test("buildClaudeCodeCompatibleRequest honors token priority fields", () => {
});
assert.equal(payload.max_tokens, 321);
assert.deepEqual(payload.tools, []);
assert.equal(payload.tool_choice, undefined);
});
test("buildClaudeCodeCompatibleRequest omits auto tool_choice while preserving tools", () => {
const payload = buildClaudeCodeCompatibleRequest({
sourceBody: { tool_choice: "auto" },
normalizedBody: {
messages: [{ role: "user", content: "hi" }],
tools: [
{
type: "function",
function: {
name: "ping",
parameters: { type: "object" },
},
},
],
},
model: "claude-sonnet-4-6",
sessionId: "session-4",
});
assert.equal(payload.tools.length, 1);
assert.equal(payload.tool_choice, undefined);
});
test("DefaultExecutor uses CC-compatible path and headers", () => {