Skip to content
Merged
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
1 change: 1 addition & 0 deletions changelog.d/fixes/18560-agentrouter-responses.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **fix(providers):** Route `gpt-6-astra`, `gpt-6.1-sol`, and `gpt-5.6-sol` through AgentRouter's Responses endpoint by default while preserving explicit connection protocol settings ([#18560](https://github.com/cheaper-inference/OmniRoute/pull/18560)) — thanks @trespassmk
18 changes: 12 additions & 6 deletions open-sse/handlers/chatCore/targetFormat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
* Model/custom overrides win first. A declared connection-level alternate protocol wins next. A
* Responses-shaped inbound request otherwise keeps the Responses wire format, except for custom
* OpenAI-compatible connections explicitly configured for Chat.
* AgentRouter may inherit the inbound protocol when no explicit connection override exists.
* AgentRouter defaults Responses-only models to Responses, otherwise inheriting the inbound
* protocol when no explicit connection override exists.
* Returns both `alias` (reused by the handler when stripping the `alias/` prefix off the upstream
* model id) and `targetFormat`.
*/
Expand All @@ -17,6 +18,8 @@ import { resolveAlternateFormat } from "../../config/providers/alternateFormats.
import { getTargetFormat } from "../../services/provider.ts";
import { FORMATS } from "../../translator/formats.ts";

const AGENTROUTER_RESPONSES_MODELS = new Set(["gpt-6-astra", "gpt-6.1-sol", "gpt-5.6-sol"]);

export function resolveChatCoreTargetFormat(opts: {
provider: string;
resolvedModel: string;
Expand Down Expand Up @@ -44,11 +47,14 @@ export function resolveChatCoreTargetFormat(opts: {
)?.targetFormat;
const inferredAgentRouterTargetFormat =
provider === "agentrouter" &&
!(typeof explicitConnectionTargetFormat === "string" && explicitConnectionTargetFormat) &&
(sourceFormat === FORMATS.OPENAI_RESPONSES ||
sourceFormat === FORMATS.OPENAI ||
sourceFormat === FORMATS.CLAUDE)
? sourceFormat
!(typeof explicitConnectionTargetFormat === "string" && explicitConnectionTargetFormat)
? AGENTROUTER_RESPONSES_MODELS.has(resolvedModel)
? FORMATS.OPENAI_RESPONSES
: sourceFormat === FORMATS.OPENAI_RESPONSES ||
sourceFormat === FORMATS.OPENAI ||
sourceFormat === FORMATS.CLAUDE
? sourceFormat
: undefined
: undefined;
const providerTargetFormat = getTargetFormat(provider, providerSpecificData);
const declaredConnectionAlternate = resolveAlternateFormat(
Expand Down
162 changes: 158 additions & 4 deletions tests/unit/agentrouter-chatcore-protocols.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { z } from "zod";
import { getCodexCliRsHeaders } from "../../open-sse/config/codexClient.ts";

const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-agentrouter-chatcore-"));
process.env.DATA_DIR = TEST_DATA_DIR;
Expand Down Expand Up @@ -130,7 +132,7 @@ test("AgentRouter Responses requests automatically use the native Responses prot
assert.equal("output_config" in captured.body, false);
});

test("AgentRouter OpenAI Chat requests automatically use the native Chat protocol", async () => {
test("AgentRouter explicit OpenAI Chat selection uses the native Chat protocol", async () => {
let captured: { url: string; headers: Headers; body: Record<string, unknown> } | null = null;

globalThis.fetch = async (url, init = {}) => {
Expand Down Expand Up @@ -168,7 +170,7 @@ test("AgentRouter OpenAI Chat requests automatically use the native Chat protoco
modelInfo: { provider: "agentrouter", model: "gpt-5.6-sol", extendedContext: false },
credentials: {
apiKey: "test-agentrouter-key",
providerSpecificData: {},
providerSpecificData: { targetFormat: "openai" },
},
log: noopLog(),
clientRawRequest: {
Expand Down Expand Up @@ -328,7 +330,7 @@ test("AgentRouter Responses streaming stays native without a connection protocol
assert.match(text, /"total_tokens":5/);
});

test("AgentRouter OpenAI Chat streaming stays native without a connection protocol override", async () => {
test("AgentRouter explicit OpenAI Chat streaming stays native", async () => {
let captured: { url: string; headers: Headers; body: Record<string, unknown> } | null = null;

globalThis.fetch = async (url, init = {}) => {
Expand Down Expand Up @@ -376,7 +378,10 @@ test("AgentRouter OpenAI Chat streaming stays native without a connection protoc
const result = await handleChatCore({
body: structuredClone(body),
modelInfo: { provider: "agentrouter", model: "gpt-5.6-sol", extendedContext: false },
credentials: { apiKey: "test-agentrouter-key", providerSpecificData: {} },
credentials: {
apiKey: "test-agentrouter-key",
providerSpecificData: { targetFormat: "openai" },
},
log: noopLog(),
clientRawRequest: {
endpoint: "/v1/chat/completions",
Expand Down Expand Up @@ -462,3 +467,152 @@ test("AgentRouter Anthropic streaming stays native without a connection protocol
assert.match(text, /content_block_delta/);
assert.match(text, /message_stop/);
});

const responsesRequestSchema = z.looseObject({ model: z.string(), input: z.unknown().optional() });
const chatReplySchema = z.object({
object: z.literal("chat.completion"),
choices: z.array(
z.object({ message: z.object({ content: z.string() }), finish_reason: z.string() })
),
});
const chatChunkSchema = z.object({
object: z.literal("chat.completion.chunk"),
choices: z.array(
z.object({
delta: z.object({ content: z.string().optional() }),
finish_reason: z.string().nullable(),
})
),
});

for (const stream of [false, true]) {
test(
stream
? "#17997 AgentRouter Chat stream translates Responses events"
: "#17997 AgentRouter Chat input uses Responses and returns Chat JSON",
async () => {
const requests: {
url: string;
headers: Headers;
body: z.infer<typeof responsesRequestSchema>;
}[] = [];
const completedResponse = {
id: "resp_agentrouter_17997",
object: "response",
status: "completed",
model: "gpt-6-astra",
output: [
{
id: "msg_agentrouter_17997",
type: "message",
role: "assistant",
status: "completed",
content: [{ type: "output_text", text: "OK", annotations: [] }],
},
],
usage: { input_tokens: 4, output_tokens: 1, total_tokens: 5 },
};
globalThis.fetch = async (url, init = {}) => {
const rawBody: unknown = JSON.parse(String(init.body || "{}"));
requests.push({
url: String(url),
headers: new Headers(init.headers),
body: responsesRequestSchema.parse(rawBody),
});
if (String(url) === "https://agentrouter.org/v1/chat/completions") {
return new Response(
JSON.stringify({
error: {
message:
"Model gpt-6-astra does not support /v1/chat/completions, please use /v1/responses instead.",
},
}),
{
status: 400,
headers: { "Content-Type": "application/json" },
}
);
}
assert.equal(String(url), "https://agentrouter.org/v1/responses");
if (!stream) {
return new Response(JSON.stringify(completedResponse), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
return createSseResponse([
'event: response.created\ndata: {"type":"response.created","response":{"id":"resp_agentrouter_17997","object":"response","status":"in_progress"}}',
'event: response.output_text.delta\ndata: {"type":"response.output_text.delta","item_id":"msg_agentrouter_17997","output_index":0,"content_index":0,"delta":"OK"}',
"event: response.completed\ndata: " +
JSON.stringify({ type: "response.completed", response: completedResponse }),
]);
};
const body = {
model: "agentrouter/gpt-6-astra",
messages: [{ role: "user", content: "Reply with exactly OK." }],
stream,
};
const credentials = { apiKey: "test-agentrouter-key", providerSpecificData: {} };
const originalCredentials = structuredClone(credentials);
const result = await handleChatCore({
body: structuredClone(body),
modelInfo: { provider: "agentrouter", model: "gpt-6-astra", extendedContext: false },
credentials,
log: noopLog(),
clientRawRequest: {
endpoint: "/v1/chat/completions",
body: structuredClone(body),
headers: new Headers({ accept: stream ? "text/event-stream" : "application/json" }),
},
});
const text = await result.response.text();
assert.equal(result.success, true);
assert.equal(result.response.ok, true);
assert.equal(requests.length, 1);
const [captured] = requests;
assert.ok(captured);
assert.equal(captured.url, "https://agentrouter.org/v1/responses");
assert.equal(captured.body.model, "gpt-6-astra");
assert.ok("input" in captured.body);
for (const key of ["messages", "system", "thinking", "output_config"]) {
assert.equal(key in captured.body, false, `${key} must not leak to Responses`);
}
assert.equal(captured.headers.get("authorization"), "Bearer test-agentrouter-key");
assert.equal(captured.headers.get("originator"), "codex_cli_rs");
assert.equal(captured.headers.get("user-agent"), getCodexCliRsHeaders()["User-Agent"]);
for (const header of ["x-api-key", "anthropic-version", "anthropic-beta", "x-app"]) {
assert.equal(captured.headers.get(header), null);
}
assert.deepEqual(credentials, originalCredentials);
if (!stream) {
const rawReply: unknown = JSON.parse(text);
const reply = chatReplySchema.parse(rawReply);
assert.equal(reply.choices[0].message.content, "OK");
assert.equal(reply.choices[0].finish_reason, "stop");
return;
}
assert.doesNotMatch(text, /event: response\.|"type":"response\./);
const data = text
.split("\n")
.filter((line) => line.startsWith("data: "))
.map((line) => line.slice(6));
assert.equal(data.at(-1), "[DONE]");
const chunks = data
.filter((line) => line !== "[DONE]")
.map((line) => {
const rawChunk: unknown = JSON.parse(line);
return chatChunkSchema.parse(rawChunk);
});
assert.equal(
chunks
.flatMap((chunk) => chunk.choices)
.map((choice) => choice.delta.content || "")
.join(""),
"OK"
);
assert.ok(
chunks.some((chunk) => chunk.choices.some((choice) => choice.finish_reason === "stop"))
);
}
);
}
Loading
Loading