mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
fix(codex): expose native model ids in catalog (#2012)
Integrated into release/v3.8.0
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user