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
7 changes: 6 additions & 1 deletion src/app/api/providers/[id]/models/discovery/codex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -512,10 +512,15 @@ export function enrichCodexModelsFromGithubCatalog(
githubCatalogModels: CodexDiscoveryModel[]
): CodexDiscoveryModel[] {
const byId = new Map(githubCatalogModels.map((model) => [model.id, model]));
return models.map((model) => {
const enriched = models.map((model) => {
const githubModel = byId.get(model.id);
return githubModel ? { ...githubModel, ...model } : model;
});
// A non-empty live entitlement list is authoritative for membership and
// order. GitHub rows may only fill metadata on those ids. Catalog models
// the account did not return are used only when there is no live list.
if (models.length > 0) return enriched;
return [...githubCatalogModels];
}

export async function fetchCodexDiscoveryModels({
Expand Down
61 changes: 61 additions & 0 deletions tests/unit/codex-discovery-github-catalog-append.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
enrichCodexModelsFromGithubCatalog,
type CodexDiscoveryModel,
} from "@/app/api/providers/[id]/models/discovery/codex";

test("enrichCodexModelsFromGithubCatalog enriches live models with github catalog metadata", () => {
const liveModels: CodexDiscoveryModel[] = [
{
id: "gpt-6-sol",
name: "gpt-6-sol",
display_name: "GPT-6 Sol",
context_length: 128000,
},
];

const githubCatalog: CodexDiscoveryModel[] = [
{
id: "gpt-6-sol",
name: "gpt-6-sol",
display_name: "GPT-6 Sol (Official)",
context_length: 872000,
capabilities: { vision: true, reasoning: true },
},
];

const result = enrichCodexModelsFromGithubCatalog(liveModels, githubCatalog);
assert.equal(result.length, 1);
assert.equal(result[0].id, "gpt-6-sol");
assert.equal(result[0].display_name, "GPT-6 Sol"); // live model overrides display_name if present
assert.equal(result[0].context_length, 128000);
assert.equal(result[0].capabilities?.vision, true);
});

test("enrichCodexModelsFromGithubCatalog returns the GitHub catalog when there is no live list (#15525)", () => {
const liveModels: CodexDiscoveryModel[] = [];

const githubCatalog: CodexDiscoveryModel[] = [
{
id: "gpt-6-sol",
name: "gpt-6-sol",
display_name: "GPT-6 Sol (Official)",
},
{
id: "gpt-6.1-sol",
name: "gpt-6.1-sol",
display_name: "GPT-6.1 Sol",
context_length: 872000,
capabilities: { vision: true, reasoning: true },
},
];

const result = enrichCodexModelsFromGithubCatalog(liveModels, githubCatalog);
assert.equal(result.length, 2);
assert.equal(result[0].id, "gpt-6-sol");
assert.equal(result[1].id, "gpt-6.1-sol");
assert.equal(result[1].display_name, "GPT-6.1 Sol");
assert.equal(result[1].context_length, 872000);
assert.equal(result[1].capabilities?.vision, true);
});
30 changes: 30 additions & 0 deletions tests/unit/provider-models-discovery-split.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,36 @@ test("codex.enrichCodexModelsFromGithubCatalog keeps live entitlement list autho
assert.equal(enriched[0]?.supportsVision, true);
});

test("codex.enrichCodexModelsFromGithubCatalog uses the GitHub catalog when there is no live list", () => {
const catalog = enrichCodexModelsFromGithubCatalog(
[],
[
{
id: "gpt-5.6-luna",
name: "GitHub Luna",
owned_by: "codex",
apiFormat: "responses",
supportedEndpoints: ["responses"],
},
{
id: "gpt-5.6-terra",
name: "GitHub Terra",
owned_by: "codex",
apiFormat: "responses",
supportedEndpoints: ["responses"],
inputTokenLimit: 272000,
},
]
);

assert.deepEqual(
catalog.map((model) => model.id),
["gpt-5.6-luna", "gpt-5.6-terra"]
);
assert.equal(catalog[0]?.name, "GitHub Luna");
assert.equal(catalog[1]?.inputTokenLimit, 272000);
});

test("codex.mergeCodexLiveModelsWithLocalCatalog merges capacity limits conservatively (smaller wins)", () => {
const merged = mergeCodexLiveModelsWithLocalCatalog(
[
Expand Down
Loading