mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
When the Codex CLI falls back from WebSocket to HTTP (after 1008 Policy Violation or reconnect exhaustion), it POSTs to /v1/responses with the bare model id it was configured with (e.g. "gpt-5.5") — never the provider-prefixed form. OmniRoute's normal routing resolved that bare id to openrouter, not codex, producing: "No credentials for provider: openrouter" Fix: add resolveResponsesApiModel() (extending modelResolution.ts) and call it in /v1/responses before delegating to handleChat. The function applies the same codex-preference logic as resolveCodexWsModelInfo: bare "gpt-5.5" → codex/gpt-5.5 has a codex provider → rewrite ✅ bare "gpt-4o" → codex/gpt-4o not in registry → pass through ✅ "anthropic/x" → has "/" → skip resolution → pass through ✅ Errors are caught; original request is returned on any failure. 8 unit tests added (TDD — watched each fail before implementing). Refs: openai/codex#15492, openai/codex#13041, openai/codex#13039
92 lines
3.8 KiB
TypeScript
92 lines
3.8 KiB
TypeScript
/**
|
|
* Tests for resolveResponsesApiModel — ensures bare ChatGPT model IDs are
|
|
* codex-preferred when the Codex CLI falls back from WebSocket to HTTP and
|
|
* hits /v1/responses with a bare model id (e.g. "gpt-5.5").
|
|
*
|
|
* Root cause: WS transport requires bare ids (codex/ prefix rejected client-side),
|
|
* but HTTP routing resolves bare "gpt-5.5" → openrouter, not codex.
|
|
* Fix: /v1/responses pre-resolves bare ids with codex preference.
|
|
*/
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { resolveResponsesApiModel } from "../../src/app/api/internal/codex-responses-ws/modelResolution.ts";
|
|
import type { ModelResolver } from "../../src/app/api/internal/codex-responses-ws/modelResolution.ts";
|
|
|
|
/** Resolver that maps model id to a { provider, model } pair. */
|
|
function makeResolver(map: Record<string, { provider: string; model: string }>): ModelResolver {
|
|
return async (id: string) => map[id] ?? {};
|
|
}
|
|
|
|
test("bare gpt-5.5 that resolves to codex is rewritten to codex/gpt-5.5", async () => {
|
|
const resolve = makeResolver({
|
|
"gpt-5.5": { provider: "openrouter", model: "gpt-5.5" },
|
|
"codex/gpt-5.5": { provider: "codex", model: "gpt-5.5" },
|
|
});
|
|
const result = await resolveResponsesApiModel("gpt-5.5", resolve);
|
|
assert.equal(result.model, "codex/gpt-5.5");
|
|
assert.equal(result.changed, true);
|
|
});
|
|
|
|
test("bare gpt-4o that has no codex mapping is passed through unchanged", async () => {
|
|
const resolve = makeResolver({
|
|
"gpt-4o": { provider: "openai", model: "gpt-4o" },
|
|
// codex/gpt-4o is NOT in the registry
|
|
});
|
|
const result = await resolveResponsesApiModel("gpt-4o", resolve);
|
|
assert.equal(result.model, "gpt-4o");
|
|
assert.equal(result.changed, false);
|
|
});
|
|
|
|
test("already-prefixed codex/gpt-5.5 is passed through unchanged", async () => {
|
|
const resolve = makeResolver({
|
|
"codex/gpt-5.5": { provider: "codex", model: "gpt-5.5" },
|
|
});
|
|
const result = await resolveResponsesApiModel("codex/gpt-5.5", resolve);
|
|
assert.equal(result.model, "codex/gpt-5.5");
|
|
assert.equal(result.changed, false);
|
|
});
|
|
|
|
test("bare model that resolves to openrouter AND has no codex equivalent is passed through", async () => {
|
|
const resolve = makeResolver({
|
|
"llama-3.1": { provider: "openrouter", model: "llama-3.1" },
|
|
"codex/llama-3.1": { provider: "openrouter", model: "llama-3.1" }, // no codex match
|
|
});
|
|
const result = await resolveResponsesApiModel("llama-3.1", resolve);
|
|
assert.equal(result.model, "llama-3.1");
|
|
assert.equal(result.changed, false);
|
|
});
|
|
|
|
test("empty model string is passed through unchanged", async () => {
|
|
const resolve = makeResolver({});
|
|
const result = await resolveResponsesApiModel("", resolve);
|
|
assert.equal(result.model, "");
|
|
assert.equal(result.changed, false);
|
|
});
|
|
|
|
test("bare gpt-5.5 that directly resolves to codex (without prefix retry) is rewritten", async () => {
|
|
const resolve = makeResolver({
|
|
"gpt-5.5": { provider: "codex", model: "gpt-5.5" },
|
|
});
|
|
const result = await resolveResponsesApiModel("gpt-5.5", resolve);
|
|
assert.equal(result.model, "codex/gpt-5.5");
|
|
assert.equal(result.changed, true);
|
|
});
|
|
|
|
test("other-provider/ prefix passes through unchanged (not a bare model)", async () => {
|
|
const resolve = makeResolver({
|
|
"anthropic/claude-opus-4": { provider: "anthropic", model: "claude-opus-4" },
|
|
});
|
|
const result = await resolveResponsesApiModel("anthropic/claude-opus-4", resolve);
|
|
assert.equal(result.model, "anthropic/claude-opus-4");
|
|
assert.equal(result.changed, false);
|
|
});
|
|
|
|
test("resolver throwing is handled gracefully — model passes through unchanged", async () => {
|
|
const resolve: ModelResolver = async () => {
|
|
throw new Error("resolver unavailable");
|
|
};
|
|
const result = await resolveResponsesApiModel("gpt-5.5", resolve);
|
|
assert.equal(result.model, "gpt-5.5");
|
|
assert.equal(result.changed, false);
|
|
});
|