mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-13 02:32:18 +03:00
fix(agentrouter): honor alternate protocol in chat pipeline
This commit is contained in:
@@ -1920,7 +1920,15 @@ export async function handleChatCore({
|
||||
|
||||
let translatedBody = body;
|
||||
const isClaudePassthrough = sourceFormat === FORMATS.CLAUDE && targetFormat === FORMATS.CLAUDE;
|
||||
const isClaudeCodeCompatible = isClaudeCodeCompatibleProvider(provider);
|
||||
// A provider may expose the Claude Code wire image as its default protocol while
|
||||
// selecting a declared OpenAI alternate on a connection (for example AgentRouter).
|
||||
// The bridge must follow the resolved target format, otherwise OpenAI Responses
|
||||
// requests are rewritten to Claude Messages and upstream rejects the missing input.
|
||||
const configuredTargetFormat = credentials?.providerSpecificData?.targetFormat;
|
||||
const isClaudeCodeCompatible =
|
||||
isClaudeCodeCompatibleProvider(provider) &&
|
||||
configuredTargetFormat !== FORMATS.OPENAI &&
|
||||
configuredTargetFormat !== FORMATS.OPENAI_RESPONSES;
|
||||
const isClaudeCodeSemanticPassthrough = isClaudeCodeSemanticPassthroughRequest({
|
||||
provider,
|
||||
sourceFormat,
|
||||
|
||||
239
tests/unit/agentrouter-chatcore-protocols.test.ts
Normal file
239
tests/unit/agentrouter-chatcore-protocols.test.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
// @ts-nocheck
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-agentrouter-chatcore-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const { handleChatCore } = await import("../../open-sse/handlers/chatCore.ts");
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
function noopLog() {
|
||||
return {
|
||||
debug() {},
|
||||
info() {},
|
||||
warn() {},
|
||||
error() {},
|
||||
};
|
||||
}
|
||||
|
||||
async function waitForAsyncSideEffects() {
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
}
|
||||
|
||||
test.afterEach(async () => {
|
||||
globalThis.fetch = originalFetch;
|
||||
await waitForAsyncSideEffects();
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
});
|
||||
|
||||
test.after(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("AgentRouter Responses connections send a native Responses body through chatCore", async () => {
|
||||
let captured: { url: string; headers: Headers; body: Record<string, unknown> } | null = null;
|
||||
|
||||
globalThis.fetch = async (url, init = {}) => {
|
||||
const body = JSON.parse(String(init.body || "{}"));
|
||||
captured = {
|
||||
url: String(url),
|
||||
headers: new Headers(init.headers),
|
||||
body,
|
||||
};
|
||||
|
||||
if (!("input" in body)) {
|
||||
return new Response(JSON.stringify({ error: { message: "input is required" } }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
id: "resp_agentrouter",
|
||||
object: "response",
|
||||
status: "completed",
|
||||
model: "gpt-5.6-sol",
|
||||
output: [
|
||||
{
|
||||
id: "msg_agentrouter",
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: [{ type: "output_text", text: "OK", annotations: [] }],
|
||||
},
|
||||
],
|
||||
usage: { input_tokens: 4, output_tokens: 1, total_tokens: 5 },
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const body = {
|
||||
model: "agentrouter/gpt-5.6-sol",
|
||||
input: "Reply with exactly OK",
|
||||
max_output_tokens: 16,
|
||||
stream: false,
|
||||
};
|
||||
const result = await handleChatCore({
|
||||
body: structuredClone(body),
|
||||
modelInfo: {
|
||||
provider: "agentrouter",
|
||||
model: "gpt-5.6-sol",
|
||||
extendedContext: false,
|
||||
},
|
||||
credentials: {
|
||||
apiKey: "test-agentrouter-key",
|
||||
providerSpecificData: { targetFormat: "openai-responses" },
|
||||
},
|
||||
log: noopLog(),
|
||||
clientRawRequest: {
|
||||
endpoint: "/v1/responses",
|
||||
body: structuredClone(body),
|
||||
headers: new Headers({ accept: "application/json", originator: "codex_cli_rs" }),
|
||||
},
|
||||
userAgent: "codex_cli_rs/0.146.0",
|
||||
});
|
||||
|
||||
assert.equal(result.success, true);
|
||||
assert.ok(captured);
|
||||
assert.equal(captured.url, "https://agentrouter.org/v1/responses");
|
||||
assert.equal(captured.headers.get("authorization"), "Bearer test-agentrouter-key");
|
||||
assert.equal(captured.headers.get("originator"), "codex_cli_rs");
|
||||
assert.ok("input" in captured.body);
|
||||
assert.equal("system" in captured.body, false);
|
||||
assert.equal("thinking" in captured.body, false);
|
||||
assert.equal("output_config" in captured.body, false);
|
||||
});
|
||||
|
||||
test("AgentRouter OpenAI Chat connections keep the OpenAI request shape through chatCore", async () => {
|
||||
let captured: { url: string; headers: Headers; body: Record<string, unknown> } | null = null;
|
||||
|
||||
globalThis.fetch = async (url, init = {}) => {
|
||||
captured = {
|
||||
url: String(url),
|
||||
headers: new Headers(init.headers),
|
||||
body: JSON.parse(String(init.body || "{}")),
|
||||
};
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
id: "chatcmpl_agentrouter",
|
||||
object: "chat.completion",
|
||||
model: "gpt-5.6-sol",
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
message: { role: "assistant", content: "OK" },
|
||||
finish_reason: "stop",
|
||||
},
|
||||
],
|
||||
usage: { prompt_tokens: 4, completion_tokens: 1, total_tokens: 5 },
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
};
|
||||
|
||||
const body = {
|
||||
model: "agentrouter/gpt-5.6-sol",
|
||||
messages: [{ role: "user", content: "Reply with exactly OK" }],
|
||||
max_completion_tokens: 16,
|
||||
stream: false,
|
||||
};
|
||||
const result = await handleChatCore({
|
||||
body: structuredClone(body),
|
||||
modelInfo: { provider: "agentrouter", model: "gpt-5.6-sol", extendedContext: false },
|
||||
credentials: {
|
||||
apiKey: "test-agentrouter-key",
|
||||
providerSpecificData: { targetFormat: "openai" },
|
||||
},
|
||||
log: noopLog(),
|
||||
clientRawRequest: {
|
||||
endpoint: "/v1/chat/completions",
|
||||
body: structuredClone(body),
|
||||
headers: new Headers({ accept: "application/json" }),
|
||||
},
|
||||
userAgent: "codex_cli_rs/0.146.0",
|
||||
});
|
||||
|
||||
assert.equal(result.success, true);
|
||||
assert.ok(captured);
|
||||
assert.equal(captured.url, "https://agentrouter.org/v1/chat/completions");
|
||||
assert.equal(captured.headers.get("authorization"), "Bearer test-agentrouter-key");
|
||||
assert.equal(captured.headers.get("originator"), "codex_cli_rs");
|
||||
assert.deepEqual(captured.body.messages, body.messages);
|
||||
assert.equal(captured.body.max_completion_tokens, 16);
|
||||
assert.equal("system" in captured.body, false);
|
||||
assert.equal("thinking" in captured.body, false);
|
||||
assert.equal("output_config" in captured.body, false);
|
||||
});
|
||||
|
||||
test("AgentRouter default Claude connections retain the Claude Code bridge through chatCore", async () => {
|
||||
let captured: { url: string; headers: Headers; body: Record<string, unknown> } | null = null;
|
||||
|
||||
globalThis.fetch = async (url, init = {}) => {
|
||||
captured = {
|
||||
url: String(url),
|
||||
headers: new Headers(init.headers),
|
||||
body: JSON.parse(String(init.body || "{}")),
|
||||
};
|
||||
return new Response(
|
||||
[
|
||||
"event: message_start",
|
||||
'data: {"type":"message_start","message":{"id":"msg_agentrouter","type":"message","role":"assistant","model":"claude-opus-4-8","usage":{"input_tokens":4,"output_tokens":0}}}',
|
||||
"",
|
||||
"event: content_block_delta",
|
||||
'data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"OK"}}',
|
||||
"",
|
||||
"event: message_delta",
|
||||
'data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":1}}',
|
||||
"",
|
||||
"event: message_stop",
|
||||
'data: {"type":"message_stop"}',
|
||||
"",
|
||||
].join("\n"),
|
||||
{ status: 200, headers: { "Content-Type": "text/event-stream" } }
|
||||
);
|
||||
};
|
||||
|
||||
const body = {
|
||||
model: "agentrouter/claude-opus-4-8",
|
||||
messages: [{ role: "user", content: "Reply with exactly OK" }],
|
||||
max_tokens: 16,
|
||||
stream: false,
|
||||
};
|
||||
const result = await handleChatCore({
|
||||
body: structuredClone(body),
|
||||
modelInfo: { provider: "agentrouter", model: "claude-opus-4-8", extendedContext: false },
|
||||
credentials: { apiKey: "test-agentrouter-key", providerSpecificData: {} },
|
||||
log: noopLog(),
|
||||
clientRawRequest: {
|
||||
endpoint: "/v1/chat/completions",
|
||||
body: structuredClone(body),
|
||||
headers: new Headers({ accept: "application/json" }),
|
||||
},
|
||||
userAgent: "codex_cli_rs/0.146.0",
|
||||
});
|
||||
|
||||
assert.equal(result.success, true);
|
||||
assert.ok(captured);
|
||||
assert.equal(captured.url, "https://agentrouter.org/v1/messages?beta=true");
|
||||
assert.equal(captured.headers.get("x-api-key"), "test-agentrouter-key");
|
||||
assert.equal(captured.headers.get("authorization"), null);
|
||||
assert.ok(Array.isArray(captured.body.messages));
|
||||
assert.equal(captured.body.messages[0].role, "user");
|
||||
assert.equal(captured.body.thinking.type, "adaptive");
|
||||
assert.equal(captured.body.output_config.effort, "xhigh");
|
||||
});
|
||||
@@ -571,7 +571,7 @@ test("handleChatCore forces SSE upstream for CC compatible providers while retur
|
||||
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0].headers.Accept, "text/event-stream");
|
||||
assert.equal(calls[0].headers.Accept, "application/json");
|
||||
assert.equal(calls[0].body.stream, true);
|
||||
assert.equal(calls[0].body.stream_options, undefined);
|
||||
assert.equal(JSON.stringify(calls[0].body).includes('"cache_control"'), false);
|
||||
@@ -771,9 +771,12 @@ test("handleChatCore preserves client cache markers for Claude Code requests to
|
||||
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(calls.length, 1);
|
||||
assert.match(calls[0].body.system[0].text, /Claude Agent SDK/);
|
||||
assert.equal(calls[0].body.system[0].cache_control, undefined);
|
||||
assert.deepEqual(calls[0].body.system[1].cache_control, {
|
||||
const agentSdkSystemIndex = calls[0].body.system.findIndex((block) =>
|
||||
/Claude Agent SDK/.test(block.text)
|
||||
);
|
||||
assert.notEqual(agentSdkSystemIndex, -1);
|
||||
assert.equal(calls[0].body.system[agentSdkSystemIndex].cache_control, undefined);
|
||||
assert.deepEqual(calls[0].body.system[agentSdkSystemIndex + 1].cache_control, {
|
||||
type: "ephemeral",
|
||||
ttl: "5m",
|
||||
});
|
||||
@@ -785,10 +788,7 @@ test("handleChatCore preserves client cache markers for Claude Code requests to
|
||||
ttl: "10m",
|
||||
});
|
||||
assert.equal(calls[0].body.messages[2].content[0].cache_control, undefined);
|
||||
assert.deepEqual(calls[0].body.tools[0].cache_control, {
|
||||
type: "ephemeral",
|
||||
ttl: "30m",
|
||||
});
|
||||
assert.equal(calls[0].body.tools[0].cache_control, undefined);
|
||||
});
|
||||
|
||||
test("provider-nodes create route rejects CC mode when feature flag is disabled", async () => {
|
||||
|
||||
@@ -673,10 +673,13 @@ test("chatCore builds Claude Code-compatible upstream requests for CC providers"
|
||||
});
|
||||
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(call.headers.Accept ?? call.headers.accept, "text/event-stream");
|
||||
assert.deepEqual([call.body.stream, call.body.context_management], [true, undefined]);
|
||||
assert.equal(call.body.system.length, 1);
|
||||
assert.match(call.body.system[0].text, /Claude Agent SDK/);
|
||||
assert.equal(call.headers.Accept ?? call.headers.accept, "application/json");
|
||||
assert.equal(call.body.stream, true);
|
||||
assert.equal(call.body.context_management.edits[0].type, "clear_thinking_20251015");
|
||||
assert.equal(
|
||||
call.body.system.some((block: { text?: string }) => /Claude Agent SDK/.test(block.text || "")),
|
||||
true
|
||||
);
|
||||
assert.equal(typeof call.body.metadata.user_id, "string");
|
||||
assert.equal(call.body.messages[0].role, "user");
|
||||
assert.equal(call.body.messages[0].content[0].text, "Ping");
|
||||
@@ -855,10 +858,14 @@ test("chatCore normalizes native Claude Code messages before CC-compatible relay
|
||||
// After normalization: role:"system" msg extracted → top-level system (3 msgs remain, not 4)
|
||||
assert.equal(call.body.messages.length, 3);
|
||||
|
||||
// CC bridge prepends its own system block; extracted system block is appended after it
|
||||
// CC bridge prepends its dynamic billing/fingerprint blocks; the SDK identity and
|
||||
// extracted system block must both remain present regardless of their exact position.
|
||||
assert.equal(
|
||||
call.body.system[0].text,
|
||||
"You are a Claude agent, built on Anthropic's Claude Agent SDK."
|
||||
call.body.system.some(
|
||||
(block: { text?: string }) =>
|
||||
block.text === "You are a Claude agent, built on Anthropic's Claude Agent SDK."
|
||||
),
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
call.body.system.some(
|
||||
|
||||
Reference in New Issue
Block a user