mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
feat: pass tools through CC compatible bridge
This commit is contained in:
@@ -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[]
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user