fix(codex): expose native model ids in catalog (#2012)

Integrated into release/v3.8.0
This commit is contained in:
Sergey Morozov
2026-05-07 14:49:08 +03:00
committed by GitHub
parent a13d2f9aff
commit d96f0c2fda
7 changed files with 151 additions and 5 deletions

View File

@@ -74,6 +74,7 @@ for (const [aliasOrId, models] of Object.entries(PROVIDER_MODELS)) {
}
const KNOWN_MODEL_IDS = new Set(MODEL_TO_PROVIDERS.keys());
const CODEX_PREFERRED_UNPREFIXED_MODELS = new Set(["gpt-5.5"]);
export const CODEX_NATIVE_UNPREFIXED_MODELS = new Set(["codex-auto-review"]);
/**
* Resolve provider alias to provider ID
@@ -279,6 +280,14 @@ function resolveModelByProviderInference(modelId, extendedContext) {
const nonOpenAIProviders = providers.filter((p) => p !== "openai");
if (CODEX_NATIVE_UNPREFIXED_MODELS.has(modelId)) {
return {
provider: "codex",
model: modelId,
extendedContext,
};
}
if (providers.includes("codex") && CODEX_PREFERRED_UNPREFIXED_MODELS.has(modelId)) {
return {
provider: "codex",

View File

@@ -16,6 +16,7 @@ import { getAllModerationModels } from "@omniroute/open-sse/config/moderationReg
import { getAllVideoModels } from "@omniroute/open-sse/config/videoRegistry.ts";
import { getAllMusicModels } from "@omniroute/open-sse/config/musicRegistry.ts";
import { REGISTRY } from "@omniroute/open-sse/config/providerRegistry.ts";
import { CODEX_NATIVE_UNPREFIXED_MODELS } from "@omniroute/open-sse/services/model.ts";
import { getAllSyncedAvailableModels } from "@/lib/db/models";
import { getCompatibleFallbackModels } from "@/lib/providers/managedAvailableModels";
import { hasEligibleConnectionForModel } from "@/domain/connectionModelRules";
@@ -403,6 +404,33 @@ export async function getUnifiedModelsResponse(
}
}
for (const modelId of CODEX_NATIVE_UNPREFIXED_MODELS) {
if (!providerSupportsModel("codex", modelId)) continue;
if (getModelIsHidden("codex", modelId)) continue;
const alias = providerIdToAlias.codex || "cx";
const aliasId = `${alias}/${modelId}`;
const providerIdModel = `codex/${modelId}`;
const entries = [
{ id: aliasId, parent: null },
{ id: providerIdModel, parent: aliasId },
{ id: modelId, parent: providerIdModel },
];
for (const entry of entries) {
if (models.some((existingModel) => existingModel.id === entry.id)) continue;
models.push({
id: entry.id,
object: "model",
created: timestamp,
owned_by: "codex",
permission: [],
root: modelId,
parent: entry.parent,
});
}
}
try {
const syncedModelsByProvider = await getAllSyncedAvailableModels();
for (const [providerId, syncedModels] of Object.entries(syncedModelsByProvider)) {

View File

@@ -485,7 +485,12 @@ async function handleSingleModelChat(
isCombo: boolean = false
) {
// 1. Resolve model → provider/model
const resolved = await resolveModelOrError(modelStr, body, clientRawRequest?.endpoint);
const resolved = await resolveModelOrError(
modelStr,
body,
clientRawRequest?.endpoint,
clientRawRequest?.headers
);
if (resolved.error) return resolved.error;
const { provider, model, sourceFormat, targetFormat, extendedContext } = resolved;

View File

@@ -43,8 +43,59 @@ const PREFERRED_BY_FAMILY: Record<string, string> = {
mimo: "moonshot",
};
export async function resolveModelOrError(modelStr: string, body: any, endpointPath: string = "") {
const CODEX_NATIVE_RESPONSES_MODELS = new Set(["gpt-5.5"]);
function getHeaderValue(headers: Record<string, unknown> | null | undefined, name: string) {
if (!headers || typeof headers !== "object") return "";
const lowerName = name.toLowerCase();
for (const [key, value] of Object.entries(headers)) {
if (key.toLowerCase() !== lowerName) continue;
return Array.isArray(value) ? value.join(",") : String(value ?? "");
}
return "";
}
function isCodexNativeResponsesRequest(
body: any,
endpointPath: string,
headers: Record<string, unknown> | null | undefined
) {
const normalizedEndpoint = String(endpointPath || "").replace(/\/+$/, "");
if (!/(^|\/)responses(?=\/|$)/i.test(normalizedEndpoint)) return false;
if (/\/responses\/compact$/i.test(normalizedEndpoint)) return true;
const userAgent = getHeaderValue(headers, "user-agent").toLowerCase();
if (userAgent.includes("codex")) return true;
if (getHeaderValue(headers, "x-codex-session-id")) return true;
if (getHeaderValue(headers, "x-codex-window-id")) return true;
if (getHeaderValue(headers, "x-codex-turn-metadata")) return true;
const metadataSource =
body && typeof body === "object" && body.metadata && typeof body.metadata === "object"
? String(body.metadata.source || "")
: "";
return metadataSource.toLowerCase().includes("codex");
}
export async function resolveModelOrError(
modelStr: string,
body: any,
endpointPath: string = "",
requestHeaders: Record<string, unknown> | null | undefined = null
) {
const modelInfo = await getModelInfo(modelStr);
const sourceFormat = detectFormatFromEndpoint(body, endpointPath);
if (
modelInfo.provider === "openai" &&
typeof modelInfo.model === "string" &&
CODEX_NATIVE_RESPONSES_MODELS.has(modelInfo.model) &&
sourceFormat === "openai-responses" &&
isCodexNativeResponsesRequest(body, endpointPath, requestHeaders)
) {
log.info("ROUTING", `${modelStr} → codex/${modelInfo.model} (Codex native responses)`);
modelInfo.provider = "codex";
}
// Forced-rewrite: codex provider doesn't serve DeepSeek/Qwen/Kimi/etc. Reroute
// these to their canonical native provider so the request lands on the right
@@ -113,7 +164,6 @@ export async function resolveModelOrError(modelStr: string, body: any, endpointP
}
const { provider, model, extendedContext } = modelInfo;
const sourceFormat = detectFormatFromEndpoint(body, endpointPath);
const providerAlias = PROVIDER_ID_TO_ALIAS[provider] || provider;
let targetFormat = getModelTargetFormat(providerAlias, model) || getTargetFormat(provider);
if ((modelInfo as any).apiFormat === "responses") {

View File

@@ -89,6 +89,30 @@ test("resolveModelOrError rejects malformed model strings", async () => {
assert.match(json.error.message, /Invalid model format/i);
});
test("resolveModelOrError routes Codex native compact gpt-5.5 requests to Codex", async () => {
const result = await resolveModelOrError(
"gpt-5.5",
{ model: "gpt-5.5", input: "compact this session", reasoning: { effort: "xhigh" } },
"/v1/responses/compact",
{ "user-agent": "codex-cli/0.128.0" }
);
assert.equal(result.provider, "codex");
assert.equal(result.model, "gpt-5.5");
});
test("resolveModelOrError keeps non-Codex gpt-5.5 Responses requests on OpenAI", async () => {
const result = await resolveModelOrError(
"gpt-5.5",
{ model: "gpt-5.5", input: "hello" },
"/v1/responses",
{ "user-agent": "OpenAI/Node" }
);
assert.equal(result.provider, "openai");
assert.equal(result.model, "gpt-5.5");
});
test("checkPipelineGates blocks providers with an open circuit breaker", async () => {
const breaker = getCircuitBreaker("openai");
breaker.state = STATE.OPEN;

View File

@@ -288,6 +288,36 @@ test("v1 models catalog exposes refreshed GitHub Copilot aliases and drops retir
);
});
test("v1 models catalog exposes bare Codex-preferred IDs for native Codex clients", async () => {
await seedConnection("codex", {
authType: "oauth",
name: "codex-native",
apiKey: null,
accessToken: "codex-access",
});
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
const getModel = (id: string) => body.data.find((item) => item.id === id);
assert.equal(response.status, 200);
const modelId = "codex-auto-review";
const bareModel = getModel(modelId);
const providerModel = getModel(`codex/${modelId}`);
const aliasModel = getModel(`cx/${modelId}`);
const openAiModel = getModel(`openai/${modelId}`);
assert.ok(bareModel, `expected bare ${modelId} model`);
assert.ok(providerModel, `expected codex/${modelId} model`);
assert.ok(aliasModel, `expected cx/${modelId} model`);
assert.equal(openAiModel, undefined);
assert.equal(bareModel.owned_by, "codex");
assert.equal(bareModel.parent, providerModel.id);
assert.equal(providerModel.parent, aliasModel.id);
});
test("v1 models catalog exposes Antigravity client-visible preview aliases instead of upstream internal IDs", async () => {
await seedConnection("antigravity", {
authType: "oauth",

View File

@@ -28,9 +28,9 @@ test("getModelInfoCore keeps openai fallback for gpt-4o", async () => {
assert.equal(info.model, "gpt-4o");
});
test("getModelInfoCore routes removed codex-auto-review through the default fallback", async () => {
test("getModelInfoCore routes native codex-auto-review to codex", async () => {
const info = await getModelInfoCore("codex-auto-review", {});
assert.equal(info.provider, "openai");
assert.equal(info.provider, "codex");
assert.equal(info.model, "codex-auto-review");
});