diff --git a/package.json b/package.json index 3ee72c1..82d3303 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ocx-cursor", - "version": "1.0.3", + "version": "1.0.4", "description": "Use OpenCodex models in Cursor through an authenticated HTTPS gateway", "keywords": [ "opencodex", diff --git a/src/catalog.mjs b/src/catalog.mjs index 5d3c0af..32c2e78 100644 --- a/src/catalog.mjs +++ b/src/catalog.mjs @@ -73,6 +73,30 @@ export function priorityModelIds(catalogFile = configuredCodexCatalogFile()) { } } +export function codexNativeModels(catalogFile = configuredCodexCatalogFile()) { + try { + const payload = JSON.parse(readFileSync(catalogFile, "utf8")); + return payload.models + .filter((model) => typeof model?.slug === "string" + && !model.slug.includes("/") + && model.visibility === "list" + && model.supported_in_api === true + && model.opencodex_capability_provenance === undefined) + .map((model) => ({ + id: model.slug, + owned_by: "openai", + capabilities: { + input_modalities: model.input_modalities, + context_length: model.context_window, + max_output_tokens: model.max_output_tokens, + reasoning_effort: model.supported_reasoning_levels?.map(({ effort }) => effort), + }, + })); + } catch { + return []; + } +} + export function normalizeActiveCatalog(configured, active, fastModelIds = new Set()) { const configuredById = new Map(configured .filter((model) => typeof model.provider === "string" && typeof model.model === "string") @@ -141,9 +165,10 @@ export async function activeModels(fetchImpl = fetch) { } export async function buildActiveCatalog(options = {}) { + const codexCatalogFile = options.codexCatalogFile || configuredCodexCatalogFile(); return normalizeActiveCatalog( configuredModels(options.ocxBin), - await activeModels(options.fetchImpl), - options.fastModelIds || priorityModelIds(options.codexCatalogFile), + [...await activeModels(options.fetchImpl), ...codexNativeModels(codexCatalogFile)], + options.fastModelIds || priorityModelIds(codexCatalogFile), ); } diff --git a/test/catalog.test.mjs b/test/catalog.test.mjs index 8f108ac..de3aeb9 100644 --- a/test/catalog.test.mjs +++ b/test/catalog.test.mjs @@ -3,7 +3,7 @@ import test from "node:test"; import { mkdtemp, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { aliasFor, normalizeActiveCatalog, priorityModelIds } from "../src/catalog.mjs"; +import { aliasFor, codexNativeModels, normalizeActiveCatalog, priorityModelIds } from "../src/catalog.mjs"; test("aliases Anthropic models without exposing the provider segment", () => { assert.equal(aliasFor("anthropic/claude-sonnet-5"), "opencodex/claude-sonnet-5"); @@ -62,3 +62,35 @@ test("reads Fast support from the OpenCodex priority service tier", async () => ] })); assert.deepEqual([...priorityModelIds(file)], ["gpt-fast"]); }); + +test("reads visible native models from the Codex catalog", async () => { + const directory = await mkdtemp(join(tmpdir(), "ocx-cursor-native-catalog-")); + const file = join(directory, "models.json"); + await writeFile(file, JSON.stringify({ models: [ + { + slug: "gpt-daybreak-blue-latest", + visibility: "list", + supported_in_api: true, + context_window: 272_000, + input_modalities: ["text", "image"], + supported_reasoning_levels: [{ effort: "low" }, { effort: "high" }, { effort: "ultra" }], + }, + { slug: "gpt-hidden", visibility: "hide", supported_in_api: true }, + { + slug: "anthropic/claude-sonnet-5", + visibility: "list", + supported_in_api: true, + }, + ] })); + + assert.deepEqual(codexNativeModels(file), [{ + id: "gpt-daybreak-blue-latest", + owned_by: "openai", + capabilities: { + input_modalities: ["text", "image"], + context_length: 272_000, + max_output_tokens: undefined, + reasoning_effort: ["low", "high", "ultra"], + }, + }]); +});