From 0bc670e414d79a67cb376a9aa887fd29e9a2f2bc Mon Sep 17 00:00:00 2001 From: NOXX - Commiter Date: Tue, 16 Jun 2026 19:08:37 +0300 Subject: [PATCH] feat(openai): honor custom base URL in model discovery + complete openai/codex pricing (#4005) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integrated into release/v3.8.27 — openai model-discovery honors custom base URL (SSRF-guarded) + pricing rows for new openai/codex models. Tested + baselines bumped. --- config/quality/file-size-baseline.json | 7 +-- src/app/api/providers/[id]/models/route.ts | 20 +++++++++ src/shared/constants/pricing.ts | 52 ++++++++++++++++++++++ tests/unit/catalog-updates-v3x.test.ts | 39 ++++++++++++++++ 4 files changed, 115 insertions(+), 3 deletions(-) diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index 51f26d621c..8049c36b7d 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -93,7 +93,7 @@ "src/app/(dashboard)/dashboard/usage/components/EvalsTab.tsx": 2148, "src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx": 1069, "src/app/api/oauth/[provider]/[action]/route.ts": 918, - "src/app/api/providers/[id]/models/route.ts": 2494, + "src/app/api/providers/[id]/models/route.ts": 2512, "src/app/api/providers/[id]/test/route.ts": 842, "src/app/api/usage/analytics/route.ts": 941, "src/app/api/v1/models/catalog.ts": 1435, @@ -118,7 +118,7 @@ "src/shared/components/RequestLoggerV2.tsx": 1287, "src/shared/components/analytics/charts.tsx": 1558, "src/shared/constants/cliTools.ts": 875, - "src/shared/constants/pricing.ts": 1529, + "src/shared/constants/pricing.ts": 1581, "src/shared/constants/providers.ts": 3147, "src/shared/constants/sidebarVisibility.ts": 1100, "src/shared/services/cliRuntime.ts": 1090, @@ -155,5 +155,6 @@ "_rebaseline_2026_06_14_3861_gitlab_duo": "PR #3861 own growth: oauth/[provider]/[action]/route.ts 903→916 (+13 = gitlab-duo authorize guard mirroring the existing qoder guard — returns a clear 'register an OAuth app + set GITLAB_DUO_OAUTH_CLIENT_ID' message instead of letting buildAuthUrl's throw become an opaque 500). Cohesive with the qoder branch right above it; not separately extractable.", "_rebaseline_2026_06_15_3941_provider_request_capture": "PR #3941 own growth: chatCore.ts 5823->5830 (+7 by check-file-size counting = run executor attempts inside the unified provider request capture scope), antigravity.ts 1649->1664 (+15) and codex.ts 1439->1447 (+8) = bridge hand-written upstream transports that bypass normal fetch/BaseExecutor capture. Cohesive logging-fidelity refactor; not extractable without hiding the actual dispatch boundary.", "_rebaseline_2026_06_16_3958_qwen_body_check": "PR #3958 own growth: validation.ts 4407->4428 (+21 = validateQwenWebProvider now parses the /api/v2/user 200 body and requires a real user object, since Qwen returns HTTP 200 even for invalid tokens — fixes the validation false-positive, #3931). Cohesive with the existing qwen-web validation branch; not separately extractable.", - "_rebaseline_2026_06_16_4001_perplexity_diff_block": "PR #4001 own growth: perplexity-web.ts 939->1013 (+74 = parse the schematized API's RFC-6902 diff_block JSON-patch frames — applyMarkdownDiff + isAnswerTextUsage primary-usage lock + stop only on COMPLETED, not on a still-PENDING final flag — so streamed answers aren't empty, #3938 follow-up). Cohesive single-executor SSE-parsing logic; not separately extractable." + "_rebaseline_2026_06_16_4001_perplexity_diff_block": "PR #4001 own growth: perplexity-web.ts 939->1013 (+74 = parse the schematized API's RFC-6902 diff_block JSON-patch frames — applyMarkdownDiff + isAnswerTextUsage primary-usage lock + stop only on COMPLETED, not on a still-PENDING final flag — so streamed answers aren't empty, #3938 follow-up). Cohesive single-executor SSE-parsing logic; not separately extractable.", + "_rebaseline_2026_06_16_4005_openai_dynamic_models": "PR #4005 own growth: models/route.ts 2494->2512 (+18 = openai model-discovery derives {customBaseUrl}/v1/models from providerSpecificData.baseUrl, SSRF-guarded via safeOutboundFetch+public-only) and pricing.ts 1529->1581 (+52 = pure-data pricing rows closing $0 gaps for registry-exposed ids: openai gpt-5.4/-mini/-nano, gpt-4.1, gpt-4o-2024-11-20, o3 + codex(cx) gpt-5.4-{xhigh,high,medium,low}, gpt-5.3-codex-spark). Cohesive; pricing is data, route change mirrors the anthropic-compat discovery path." } diff --git a/src/app/api/providers/[id]/models/route.ts b/src/app/api/providers/[id]/models/route.ts index 95fdfebda3..ad048c8f61 100755 --- a/src/app/api/providers/[id]/models/route.ts +++ b/src/app/api/providers/[id]/models/route.ts @@ -2383,6 +2383,26 @@ export async function GET( // Build request URL let url = config.url; + // VibeProxy: honor a user-configured custom base URL for the built-in + // `openai` provider (e.g. an OpenAI-compatible gateway / proxy). Without + // this, model discovery always hit the hardcoded api.openai.com and ignored + // the configured endpoint — returning the wrong catalog (or failing auth) + // for gateway users, and preventing instant access to gateway-served models. + // Falls back to config.url (api.openai.com) when no custom base URL is set. + if (provider === "openai") { + const customBaseUrl = getProviderBaseUrl(connection.providerSpecificData); + if (customBaseUrl) { + let base = customBaseUrl.replace(/\/$/, ""); + if (base.endsWith("/chat/completions")) { + base = base.slice(0, -"/chat/completions".length); + } else if (base.endsWith("/completions")) { + base = base.slice(0, -"/completions".length); + } else if (base.endsWith("/v1")) { + base = base.slice(0, -"/v1".length); + } + url = `${base}/v1/models`; + } + } if (provider === "cloudflare-ai") { const pData = asRecord(connection.providerSpecificData); const accountId = diff --git a/src/shared/constants/pricing.ts b/src/shared/constants/pricing.ts index 392afafd50..5b64054642 100644 --- a/src/shared/constants/pricing.ts +++ b/src/shared/constants/pricing.ts @@ -257,7 +257,14 @@ export const DEFAULT_PRICING = { reasoning: 9.0, cache_creation: 1.5, }, + // gpt-5.4 reasoning-effort variants share the gpt-5.4 tier (registry exposes + // -xhigh/-high/-medium/-low; without these rows they resolved to $0). + "gpt-5.4-xhigh": GPT_5_3_CODEX_PRICING, + "gpt-5.4-high": GPT_5_3_CODEX_PRICING, + "gpt-5.4-medium": GPT_5_3_CODEX_PRICING, + "gpt-5.4-low": GPT_5_3_CODEX_PRICING, // GPT 5.3 Codex family (all same pricing tier) + "gpt-5.3-codex-spark": GPT_5_3_CODEX_PRICING, "gpt-5.3-codex": GPT_5_3_CODEX_PRICING, "gpt-5.3-codex-xhigh": GPT_5_3_CODEX_PRICING, "gpt-5.3-codex-high": GPT_5_3_CODEX_PRICING, @@ -645,6 +652,37 @@ export const DEFAULT_PRICING = { // OpenAI openai: { "gpt-5.5": GPT_5_5_PRICING, + // gpt-5.4 family (public API tier; mirrors the codex 5.4 tier for the + // base/mini, with a lower nano tier). Without these rows the openai + // provider's gpt-5.4* models resolved to $0. + "gpt-5.4": { + input: 5.0, + output: 20.0, + cached: 2.5, + reasoning: 30.0, + cache_creation: 5.0, + }, + "gpt-5.4-mini": { + input: 1.5, + output: 6.0, + cached: 0.75, + reasoning: 9.0, + cache_creation: 1.5, + }, + "gpt-5.4-nano": { + input: 0.4, + output: 1.6, + cached: 0.2, + reasoning: 2.4, + cache_creation: 0.4, + }, + "gpt-4.1": { + input: 2.0, + output: 8.0, + cached: 0.5, + reasoning: 12.0, + cache_creation: 2.0, + }, "gpt-4o": { input: 2.5, output: 10.0, @@ -652,6 +690,13 @@ export const DEFAULT_PRICING = { reasoning: 15.0, cache_creation: 2.5, }, + "gpt-4o-2024-11-20": { + input: 2.5, + output: 10.0, + cached: 1.25, + reasoning: 15.0, + cache_creation: 2.5, + }, "gpt-4o-mini": { input: 0.15, output: 0.6, @@ -659,6 +704,13 @@ export const DEFAULT_PRICING = { reasoning: 0.9, cache_creation: 0.15, }, + o3: { + input: 2.0, + output: 8.0, + cached: 0.5, + reasoning: 12.0, + cache_creation: 2.0, + }, "gpt-4-turbo": { input: 10.0, output: 30.0, diff --git a/tests/unit/catalog-updates-v3x.test.ts b/tests/unit/catalog-updates-v3x.test.ts index 12489c8351..cdcab7b5f2 100644 --- a/tests/unit/catalog-updates-v3x.test.ts +++ b/tests/unit/catalog-updates-v3x.test.ts @@ -110,3 +110,42 @@ test("Every Kiro registry model resolves a non-zero pricing row (no $0.00 usage) assert.equal(sonnet46?.input, 3.0); assert.equal(sonnet46?.output, 15.0); }); + +test("Every OpenAI registry model resolves a non-zero pricing row (alias: openai)", async () => { + const { getPricingForModel } = await import("../../src/shared/constants/pricing.ts"); + const models = getModelsByProviderId("openai"); + assert.ok(models.length > 0, "openai must expose models"); + + for (const model of models) { + const pricing = getPricingForModel("openai", model.id) as { + input?: number; + output?: number; + } | null; + assert.ok(pricing, `openai pricing must include "${model.id}"`); + assert.equal( + typeof pricing?.input === "number" && typeof pricing?.output === "number", + true, + `openai pricing for "${model.id}" must have numeric input/output` + ); + } +}); + +test("Every Codex registry model resolves a non-zero pricing row (alias: cx)", async () => { + const { getPricingForModel } = await import("../../src/shared/constants/pricing.ts"); + const models = getModelsByProviderId("codex"); + assert.ok(models.length > 0, "codex must expose models"); + + for (const model of models) { + // Codex pricing lives under the "cx" alias (its DEFAULT_PRICING key). + const pricing = getPricingForModel("cx", model.id) as { + input?: number; + output?: number; + } | null; + assert.ok(pricing, `cx pricing must include codex model "${model.id}"`); + assert.equal( + typeof pricing?.input === "number" && typeof pricing?.output === "number", + true, + `cx pricing for "${model.id}" must have numeric input/output` + ); + } +});