mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-12 02:02:13 +03:00
fix(command-code): include tool call arguments
This commit is contained in:
committed by
diegosouzapw
parent
918647af69
commit
9f8755a05b
@@ -48,6 +48,21 @@ function recordOrEmpty(value: unknown): JsonRecord {
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the `arguments` field for an assistant tool-call part that Command
|
||||
* Code's /alpha/generate schema REQUIRES (rejects a missing field with
|
||||
* `missing required field 'arguments'`). Valid source values round-trip:
|
||||
* - object arguments -> JSON string of the object
|
||||
* - string arguments -> the string as-is (already valid JSON)
|
||||
* - missing / empty / invalid JSON -> "{}" (a valid empty-object string)
|
||||
*/
|
||||
function toolCallArgumentsString(value: unknown): string {
|
||||
const parsed = recordOrEmpty(value);
|
||||
if (isRecord(value)) return JSON.stringify(parsed);
|
||||
if (typeof value === "string" && value.trim()) return value;
|
||||
return JSON.stringify(parsed);
|
||||
}
|
||||
|
||||
function normalizeContentText(content: unknown): string {
|
||||
if (typeof content === "string") return content;
|
||||
return asRecordArray(content)
|
||||
@@ -244,11 +259,15 @@ function convertMessages(
|
||||
const id = stringValue(call.id) || "";
|
||||
if (!id || !pairedToolCallIds.has(id)) continue;
|
||||
const fn = isRecord(call.function) ? call.function : {};
|
||||
const parsedInput = recordOrEmpty(fn.arguments);
|
||||
parts.push({
|
||||
type: "tool-call",
|
||||
toolCallId: id,
|
||||
toolName: stringValue(fn.name) || "",
|
||||
input: recordOrEmpty(fn.arguments),
|
||||
input: parsedInput,
|
||||
// /alpha/generate requires this field on assistant tool-call parts;
|
||||
// a missing one is rejected with `missing required field 'arguments'`.
|
||||
arguments: toolCallArgumentsString(fn.arguments),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -56,4 +56,105 @@ describe("CommandCodeExecutor", () => {
|
||||
// Network error is expected in test environment
|
||||
}
|
||||
});
|
||||
|
||||
it("assistant tool-call conversion always emits a valid required arguments field (#regression input[N] missing required field arguments)", async () => {
|
||||
const calls: Array<{ url: string; init: RequestInit; body: unknown }> = [];
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = (async (url: string | URL | Request, init?: RequestInit) => {
|
||||
calls.push({
|
||||
url: String(url),
|
||||
init: init || {},
|
||||
body: JSON.parse(String((init as RequestInit | undefined)?.body)),
|
||||
});
|
||||
return new Response("", { status: 200 });
|
||||
}) as typeof fetch;
|
||||
|
||||
const executor = new mod.CommandCodeExecutor();
|
||||
const pairedId = "call_paired";
|
||||
const body = {
|
||||
messages: [
|
||||
{ role: "user", content: "hi" },
|
||||
{
|
||||
role: "assistant",
|
||||
content: "",
|
||||
tool_calls: [
|
||||
// Missing arguments entirely -> must still get a valid arguments field
|
||||
{ id: "call_missing", type: "function", function: { name: "lookup" } },
|
||||
// Empty string arguments -> "{}"
|
||||
{
|
||||
id: "call_empty",
|
||||
type: "function",
|
||||
function: { name: "lookup", arguments: "" },
|
||||
},
|
||||
// Valid object arguments -> round-trips as JSON string
|
||||
{
|
||||
id: pairedId,
|
||||
type: "function",
|
||||
function: { name: "lookup", arguments: { q: "docs" } },
|
||||
},
|
||||
// Valid string arguments -> preserved as-is
|
||||
{
|
||||
id: "call_string",
|
||||
type: "function",
|
||||
function: { name: "lookup", arguments: '{"q":"string"}' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{ role: "tool", tool_call_id: "call_missing", content: "r1" },
|
||||
{ role: "tool", tool_call_id: "call_empty", content: "r2" },
|
||||
{ role: "tool", tool_call_id: pairedId, content: "r3" },
|
||||
{ role: "tool", tool_call_id: "call_string", content: "r4" },
|
||||
],
|
||||
};
|
||||
|
||||
try {
|
||||
await executor.execute({
|
||||
model: "test",
|
||||
body,
|
||||
stream: false,
|
||||
credentials: { apiKey: "fake-key" },
|
||||
signal: null,
|
||||
});
|
||||
assert.fail("Expected fetch to reject (no real network)");
|
||||
} catch {
|
||||
// Fetch rejection is expected; inspect the captured body
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
|
||||
assert.equal(calls.length, 1, "exactly one upstream call");
|
||||
const sentBody = calls[0].body as {
|
||||
params: { messages: Array<{ role: string; content: unknown }> };
|
||||
};
|
||||
const assistant = sentBody.params.messages.find((m) => m.role === "assistant");
|
||||
assert.ok(assistant, "assistant turn present");
|
||||
const parts = assistant.content as Array<Record<string, unknown>>;
|
||||
const toolCalls = parts.filter((p) => p.type === "tool-call");
|
||||
assert.equal(toolCalls.length, 4, "all four paired tool calls converted");
|
||||
|
||||
for (const call of toolCalls) {
|
||||
assert.equal(
|
||||
typeof call.arguments,
|
||||
"string",
|
||||
`tool-call ${String(call.toolCallId)} must carry a string arguments field`
|
||||
);
|
||||
const parsed = JSON.parse(call.arguments as string);
|
||||
assert.equal(typeof parsed, "object");
|
||||
assert.ok(!Array.isArray(parsed), "arguments must parse to a JSON object");
|
||||
}
|
||||
|
||||
const byId = new Map(toolCalls.map((c) => [String(c.toolCallId), c]));
|
||||
assert.equal(byId.get("call_missing").arguments, "{}", "missing arguments -> empty object");
|
||||
assert.equal(byId.get("call_empty").arguments, "{}", "empty string arguments -> empty object");
|
||||
assert.equal(
|
||||
byId.get(pairedId).arguments,
|
||||
'{"q":"docs"}',
|
||||
"object arguments round-trip as JSON string"
|
||||
);
|
||||
assert.equal(
|
||||
byId.get("call_string").arguments,
|
||||
'{"q":"string"}',
|
||||
"valid string arguments preserved as-is"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user