Skip to content

feat(custom-models): per-key enterprise model catalog - #2698

Merged
steebchen merged 11 commits into
mainfrom
curitiba-v7
Jun 18, 2026
Merged

steebchen merged 11 commits into
mainfrom
curitiba-v7

Conversation

@steebchen

@steebchen steebchen commented Jun 16, 2026 •

Copy link
Copy Markdown
Member

Context

Today, requests routed through a custom provider key (mycustom/<model>) are never billed and have no limits enforced: the gateway builds a mock model with inputPrice/outputPrice: "0" and calculateCosts keys pricing on the static catalog by providerId, which never matches "custom". So even a known id like gpt-5.5 through a custom provider yields null cost on the activity log, and context/output limits are unenforced.

This adds an enterprise per-provider-key custom model catalog. Each entry optionally defines context size, max output, all token prices, and capabilities. The gateway uses those values for cost attribution and limit enforcement. A per-key switch optionally restricts a provider to only catalog-defined models so cost/limits are always known and shown.

What's included

DB (packages/db)

  • New custom_model table (per provider_key, full optional field set; prices stored as text to preserve 3.0e-6 format)
  • provider_key.custom_models_only boolean toggle
  • Relations + custom_model audit actions/resource type; migration generated via the migrations skill

API (apps/api)

  • New enterprise-gated custom-models CRUD routes
  • Extended provider-key PATCH to accept customModelsOnly (enterprise-gated)

Gateway (apps/gateway)

  • findCustomModel cached query
  • Resolve the catalog entry at request time; reject undefined models when the key is restricted; enforce context/maxOutput
  • Thread a customPricing override into calculateCosts (synthetic providerId: "custom" mapping) so requests bill at catalog rates. Models without an entry stay null/unbilled, exactly as before.

UI (apps/ui)

  • New Custom Models org tab: provider-key selector, per-key "Only allow catalog models" toggle, and full-field create/edit/delete dialogs (enterprise-badge gated)
  • Cross-linked both ways with Provider Keys
  • Activity log needs no change — cost columns render real values once populated

Behavior notes

  • A known model id through a custom provider without a catalog entry still gets zero/null cost (no fallback to public pricing) — by design.
  • Management is enterprise-gated, but the gateway honors stored catalog rows/toggle regardless of current plan, so a downgrade never silently un-bills custom traffic or breaks an active restriction.

Verification

  • pnpm build (full) ✅
  • pnpm format ✅
  • pnpm test:unit ✅ — 2004 passed (incl. 2 new calculateCosts custom-pricing tests)

Note: requires applying the new migration to each database (pnpm migrations is already committed; test/dev DBs synced locally).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added custom models management for defining and pricing enterprise-specific models with configurable capabilities, limits, and pricing.
    • Custom model pricing is now applied in billing calculations.
    • Enterprise organizations can restrict provider keys to use only catalog models.
  • UI Enhancements

    • Added Custom Models page to organization dashboard for viewing, creating, editing, and deleting custom models.
    • Added Custom Models navigation link in organization sidebar.
    • Added "Manage models" option in provider key actions for custom providers.

Add an enterprise per-provider-key custom model catalog so requests
through custom providers are billed and limited from defined values.

- DB: custom_model table + provider_key.custom_models_only toggle
- API: enterprise-gated custom-models CRUD + provider key toggle
- Gateway: thread catalog pricing into calculateCosts; enforce
  context/maxOutput; reject undefined models when restricted
- UI: Custom Models org tab with provider-key selector, toggle and
  CRUD dialogs; cross-linked with Provider Keys

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 16, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a custom model catalog feature end-to-end: a new custom_model DB table and customModelsOnly flag on provider_key, API CRUD routes with enterprise/custom-provider gating, gateway enforcement of catalog pricing/capabilities/limits with customPricing threading through all calculateCosts call sites, and a UI management page with create/edit dialog and provider key integration.

Changes

Custom Model Catalog Feature

Layer / File(s) Summary
DB schema, migration, and relations
packages/db/src/schema.ts, packages/db/src/relations.ts, packages/db/migrations/1781714158_youthful_big_bertha.sql, packages/db/migrations/meta/_journal.json
Adds the custom_model table (pricing, capability flags, status enum, partial unique index), customModelsOnly boolean column on provider_key, custom_model.* audit log actions/resource types, Drizzle relations, and the SQL migration with journal entry.
Custom models API router
apps/api/src/routes/custom-models.ts, apps/api/src/routes/index.ts
Implements the customModels Hono/OpenAPI router with Zod schemas, getManageableProviderKey enterprise gating helper, and full CRUD endpoints (list, create, update, soft-delete) with audit logging and SWR cache invalidation, registered at /custom-models.
Provider key customModelsOnly update
apps/api/src/routes/keys-provider.ts
Extends provider key response schema with customModelsOnly: boolean, updates the PATCH endpoint to accept and enforce the field with enterprise/custom-provider gating, dynamic updates object, selective audit changes logging, and SWR cache invalidation on create/update/delete.
Gateway cached query: findCustomModel
apps/gateway/src/lib/cached-queries.ts
Adds CustomModel type alias, customModelTableName, and the SWR-cached findCustomModel(providerKeyId, modelName) lookup returning the active catalog entry or undefined.
calculateCosts customPricing override
apps/gateway/src/lib/costs.ts, apps/gateway/src/lib/costs.spec.ts
Adds optional customPricing?: ProviderModelMapping to calculateCosts, constructing a synthetic ModelDefinition to bypass the static catalog when the override is present; adds Vitest coverage for null and priced cases.
Gateway request handler: catalog enforcement and billing
apps/gateway/src/chat/chat.ts
Adds customModelToProviderMapping(), resolves catalog entry per key/model, enforces customModelsOnly rejection, overrides modelInfo.providers, enforces disabled capabilities and context/output limits, and threads customPricing: customPricingMapping through all calculateCosts call sites.
UI: Custom Models page, dialog, and client
apps/ui/src/app/dashboard/[orgId]/org/custom-models/page.tsx, apps/ui/src/components/custom-models/custom-models-client.tsx, apps/ui/src/components/custom-models/custom-model-dialog.tsx
Adds CustomModelsPage, CustomModelsClient (provider selection, customModelsOnly toggle, model table with edit/delete), and CustomModelDialog (tri-state capabilities, pricing inputs, create/update mutations with cache invalidation).
UI: Provider keys integration and sidebar navigation
apps/ui/src/app/dashboard/[orgId]/org/provider-keys/page.tsx, apps/ui/src/components/provider-keys/provider-keys-client.tsx, apps/ui/src/components/provider-keys/provider-keys-list.tsx, apps/ui/src/components/dashboard/dashboard-sidebar.tsx
Propagates customModelsOnly: boolean through provider key page/client/list types, adds a "Manage models" dropdown link for custom provider keys routing to the custom models page, and adds the Custom Models sidebar nav entry.

Sequence Diagram(s)

sequenceDiagram
  participant UI as CustomModelsClient
  participant API as /api/custom-models
  participant Gateway as Gateway chat.ts
  participant DB as custom_model table
  participant SWR as SWR Cache

  rect rgba(100, 149, 237, 0.5)
    note over UI,SWR: Model Management (UI → API)
    UI->>API: POST /custom-models {providerKeyId, modelName, pricing...}
    API->>DB: check enterprise plan + provider === "custom"
    API->>DB: insert custom_model row
    API->>SWR: invalidate custom_model cache
    API-->>UI: { customModel }
  end

  rect rgba(144, 238, 144, 0.5)
    note over Gateway,SWR: Request Routing (Gateway)
    Gateway->>SWR: findCustomModel(providerKeyId, modelName)
    SWR-->>Gateway: CustomModel or undefined
    Gateway->>Gateway: reject if customModelsOnly and no entry
    Gateway->>Gateway: enforce capability/context/output limits
    Gateway->>Gateway: calculateCosts(customPricing: catalogMapping)
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • theopenco/llmgateway#2416: Both PRs modify apps/gateway/src/chat/chat.ts around the requestedProvider === "custom" handling flow, with overlapping changes to custom-provider routing and validation logic.
  • theopenco/llmgateway#2667: Both PRs modify the gateway's requestedProvider === "custom" path in chat.ts, changing how synthesized custom-provider model limits and capabilities are handled.

Suggested reviewers

  • smakosh
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly summarizes the main change: adding an enterprise per-provider-key custom model catalog feature. It is concise and specific enough to convey the primary addition without excessive detail.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch curitiba-v7

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2dc6b32e44

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +237 to +238
const [customModel] = await db
.insert(tables.customModel)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Invalidate custom model cache on writes

When a gateway has already looked up a catalog entry (or a miss), this insert and the update/delete paths in this route use the plain db client, while findCustomModel reads through the cached cdb select path keyed on custom_model. Because these writes do not trigger RedisCache.onMutate, creating, editing, or deleting a custom model can be ignored by the gateway until the cached select expires, leaving stale pricing/limits or stale rejections in production. Use the cached client or explicitly invalidate the custom_model table after these mutations.

Useful? React with 👍 / 👎.

Comment thread packages/db/src/schema.ts
Comment on lines +1142 to +1144
(table) => [
unique().on(table.providerKeyId, table.modelName),
index("custom_model_provider_key_id_idx").on(table.providerKeyId),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow reusing names after soft delete

This unconditional unique constraint still includes rows whose status was set to deleted, but the API duplicate checks intentionally ignore deleted rows and the delete endpoint only soft-deletes. After a user deletes foo, creating foo again (or renaming another row to foo) will pass the route-level conflict check and then fail on this database constraint instead of allowing the normal recreate flow; make the uniqueness match active/non-deleted rows or rename/hard-delete tombstones.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (1)
apps/ui/src/app/dashboard/[orgId]/org/provider-keys/page.tsx (1)

16-16: ⚡ Quick win

Centralize the provider-key list item type to avoid contract drift.
The same providerKeys item shape is duplicated across these files, and this PR had to update all of them for customModelsOnly. Extract a shared type (or derive once from OpenAPI paths) and reuse it.

  • apps/ui/src/app/dashboard/[orgId]/org/provider-keys/page.tsx#L16-L16: replace inline provider-key item typing with the shared alias.
  • apps/ui/src/components/provider-keys/provider-keys-client.tsx#L25-L25: consume the same shared alias in ProviderKeysClientProps.
  • apps/ui/src/components/provider-keys/provider-keys-list.tsx#L53-L53: consume the same shared alias in ProviderKeysListProps.

As per coding guidelines, **/*.{ts,tsx,js,jsx} should apply DRY principles for code reuse.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ui/src/app/dashboard/`[orgId]/org/provider-keys/page.tsx at line 16,
Extract the duplicated provider-key item type definition (containing properties
like customModelsOnly) into a single shared type alias that can be reused across
multiple files. Create this shared type in a centralized types file or derive it
from OpenAPI paths, then replace the inline type definitions at all three
locations with references to this shared alias: in
apps/ui/src/app/dashboard/[orgId]/org/provider-keys/page.tsx at line 16
(currently defining customModelsOnly inline), in
apps/ui/src/components/provider-keys/provider-keys-client.tsx at line 25 (in
ProviderKeysClientProps), and in
apps/ui/src/components/provider-keys/provider-keys-list.tsx at line 53 (in
ProviderKeysListProps). This will ensure the type contract is centralized and
any future updates only need to be made in one place.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/api/src/routes/custom-models.ts`:
- Around line 91-94: The updateCustomModelSchema allows all fields to be
optional, which permits empty objects like {} to pass validation and trigger
no-op updates with unnecessary audit events. Add a refine() guard to the
updateCustomModelSchema object (similar to the pattern used in the provider-key
update route) that validates at least one field from the customModelFields is
provided, rejecting empty PATCH bodies.
- Around line 105-109: The db.query.providerKey.findFirst query in the provider
key lookup does not filter out soft-deleted records, which allows catalog
mutations to proceed with deleted provider keys. Add a condition to the where
clause that explicitly excludes soft-deleted provider keys by verifying the
deletion timestamp field (e.g., deletedAt) is null or not set, ensuring only
active provider keys can be used for catalog operations.

In `@apps/gateway/src/chat/chat.ts`:
- Around line 2493-2501: The catalog resolution for custom models happens after
validateModelCapabilities(...) has already run, which means catalog fields like
tools, vision, jsonOutput, audio, streaming, and supportedParameters do not gate
the request as intended. Additionally, the finalModelInfo branch rebuilds a
zero-price custom mapping instead of reusing the customPricingMapping that was
created. Move the customModelToProviderMapping call and the modelInfo update
(where customPricingMapping is applied to modelInfo.providers) to occur before
any capability or IAM validation checks, and then ensure that when
finalModelInfo is constructed, it preserves and reuses the customPricingMapping
instead of creating a new zero-price custom mapping.
- Around line 2523-2524: The calculation of requiredContextSize at line 2523
uses max_tokens ?? 0, which defaults to zero output tokens when max_tokens is
omitted, allowing the entire contextSize to be consumed by input. Replace this
default value of 0 with a proper fallback output token budget - either by using
the auto-routing fallback budget or by referencing the catalog maxOutput value -
to ensure output space is reserved for the model's completion generation even
when max_tokens is not explicitly provided.

In `@apps/ui/src/components/custom-models/custom-model-dialog.tsx`:
- Around line 153-160: The num function at line 153 does not validate that
Number(v) produces a valid finite positive integer, allowing NaN or Infinity to
serialize as null in the request body. Before constructing the body object, add
validation logic that checks both contextSize and maxOutput (after applying the
num transformation) to ensure they are finite positive integers. If either value
is invalid, display a toast error message with a descriptive error and prevent
the form submission by returning early. This ensures invalid numeric input is
rejected rather than persisted as null values in the database.

In `@packages/db/src/schema.ts`:
- Around line 1138-1145: The current schema enforces a permanent unique
constraint on the combination of providerKeyId and modelName, but this conflicts
with the soft-delete behavior where deleted rows should be ignored during
duplicate checks. In packages/db/src/schema.ts at lines 1138-1145, replace the
table-level unique() constraint with a partial unique index that only applies to
rows where status is not 'deleted', and ensure the status field is marked as
non-null with a default value of 'active'. In
packages/db/migrations/1781607059_real_morg.sql at lines 29-30, remove the
table-level unique constraint and create the corresponding partial unique index
in the SQL migration to match the schema change.

---

Nitpick comments:
In `@apps/ui/src/app/dashboard/`[orgId]/org/provider-keys/page.tsx:
- Line 16: Extract the duplicated provider-key item type definition (containing
properties like customModelsOnly) into a single shared type alias that can be
reused across multiple files. Create this shared type in a centralized types
file or derive it from OpenAPI paths, then replace the inline type definitions
at all three locations with references to this shared alias: in
apps/ui/src/app/dashboard/[orgId]/org/provider-keys/page.tsx at line 16
(currently defining customModelsOnly inline), in
apps/ui/src/components/provider-keys/provider-keys-client.tsx at line 25 (in
ProviderKeysClientProps), and in
apps/ui/src/components/provider-keys/provider-keys-list.tsx at line 53 (in
ProviderKeysListProps). This will ensure the type contract is centralized and
any future updates only need to be made in one place.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 5adf1a84-9861-40ff-b317-bc86e19e15be

📥 Commits

Reviewing files that changed from the base of the PR and between b9e8c97 and 2dc6b32.

📒 Files selected for processing (19)
  • apps/api/src/routes/custom-models.ts
  • apps/api/src/routes/index.ts
  • apps/api/src/routes/keys-provider.ts
  • apps/gateway/src/chat/chat.ts
  • apps/gateway/src/lib/cached-queries.ts
  • apps/gateway/src/lib/costs.spec.ts
  • apps/gateway/src/lib/costs.ts
  • apps/ui/src/app/dashboard/[orgId]/org/custom-models/page.tsx
  • apps/ui/src/app/dashboard/[orgId]/org/provider-keys/page.tsx
  • apps/ui/src/components/custom-models/custom-model-dialog.tsx
  • apps/ui/src/components/custom-models/custom-models-client.tsx
  • apps/ui/src/components/dashboard/dashboard-sidebar.tsx
  • apps/ui/src/components/provider-keys/provider-keys-client.tsx
  • apps/ui/src/components/provider-keys/provider-keys-list.tsx
  • packages/db/migrations/1781607059_real_morg.sql
  • packages/db/migrations/meta/1781607059_snapshot.json
  • packages/db/migrations/meta/_journal.json
  • packages/db/src/relations.ts
  • packages/db/src/schema.ts

Comment thread apps/api/src/routes/custom-models.ts Outdated
Comment thread apps/api/src/routes/custom-models.ts
Comment thread apps/gateway/src/chat/chat.ts
Comment thread apps/gateway/src/chat/chat.ts Outdated
Comment thread apps/ui/src/components/custom-models/custom-model-dialog.tsx Outdated
Comment thread packages/db/src/schema.ts
Comment on lines +1138 to +1145
status: text({
enum: ["active", "inactive", "deleted"],
}).default("active"),
},
(table) => [
unique().on(table.providerKeyId, table.modelName),
index("custom_model_provider_key_id_idx").on(table.providerKeyId),
index("custom_model_organization_id_idx").on(table.organizationId),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make custom-model uniqueness match the soft-delete contract. The schema and migration both enforce permanent uniqueness for (provider_key_id, model_name), while the API soft-deletes rows and intentionally ignores deleted rows during duplicate checks.

  • packages/db/src/schema.ts#L1138-L1145: replace the table-level unique() with a partial unique index over non-deleted rows, and make status non-null/default active.
  • packages/db/migrations/1781607059_real_morg.sql#L29-L30: remove the table-level unique constraint and create the matching partial unique index in the migration SQL.
📍 Affects 2 files
  • packages/db/src/schema.ts#L1138-L1145 (this comment)
  • packages/db/migrations/1781607059_real_morg.sql#L29-L30
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/db/src/schema.ts` around lines 1138 - 1145, The current schema
enforces a permanent unique constraint on the combination of providerKeyId and
modelName, but this conflicts with the soft-delete behavior where deleted rows
should be ignored during duplicate checks. In packages/db/src/schema.ts at lines
1138-1145, replace the table-level unique() constraint with a partial unique
index that only applies to rows where status is not 'deleted', and ensure the
status field is marked as non-null with a default value of 'active'. In
packages/db/migrations/1781607059_real_morg.sql at lines 29-30, remove the
table-level unique constraint and create the corresponding partial unique index
in the SQL migration to match the schema change.

- Invalidate SWR cache on custom-model and provider-key-toggle writes
  so the gateway picks up catalog/restriction changes promptly
- Partial unique index on (provider_key_id, model_name) for non-deleted
  rows so soft-deleted names can be recreated; status now NOT NULL
- Reject empty custom-model PATCH bodies; exclude soft-deleted provider
  keys from catalog management
- Reuse the catalog mapping in finalModelInfo instead of a zero-price mock
- Reserve a completion budget in the context-size check when max_tokens
  is omitted
- Validate context size / max output as positive integers in the UI

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2730af18bd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/gateway/src/chat/chat.ts
jsonOutput: cm.jsonOutput ?? undefined,
audio: cm.audio ?? undefined,
supportedParameters: cm.supportedParameters ?? undefined,
streaming,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor custom streaming settings

When a catalog entry sets streaming to false or only, this value has no effect: later streaming validation calls getModelStreamingSupport(usedInternalModel, usedProvider, usedRegion), which reads only the static models registry and returns null for custom model ids. As a result streaming requests to a catalog model marked non-streaming are allowed, and models marked streaming-only are not forced to use SSE.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/api/src/routes/keys-provider.ts`:
- Around line 703-705: The cache invalidation call using
invalidateSwrByTables([providerKeyTableName]) is only present in the
PATCH/update endpoint but is missing from the POST /provider and DELETE
/provider/{id} endpoints. Since these endpoints also mutate the provider_key
table, they must also invalidate the SWR cache to prevent stale gateway
behavior. Add the same await invalidateSwrByTables([providerKeyTableName]); call
to both the POST endpoint (after key creation) and the DELETE endpoint (after
key deletion) to ensure cache consistency across all write operations on
provider keys.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 90a0cb54-f344-4f49-a4a0-221eaf6f80b4

📥 Commits

Reviewing files that changed from the base of the PR and between 2dc6b32 and 2730af1.

📒 Files selected for processing (8)
  • apps/api/src/routes/custom-models.ts
  • apps/api/src/routes/keys-provider.ts
  • apps/gateway/src/chat/chat.ts
  • apps/ui/src/components/custom-models/custom-model-dialog.tsx
  • packages/db/migrations/1781624949_big_vindicator.sql
  • packages/db/migrations/meta/1781624949_snapshot.json
  • packages/db/migrations/meta/_journal.json
  • packages/db/src/schema.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/api/src/routes/custom-models.ts
  • apps/ui/src/components/custom-models/custom-model-dialog.tsx
  • apps/gateway/src/chat/chat.ts

Comment thread apps/api/src/routes/keys-provider.ts
- Enforce catalog capability flags (vision, audio, tools, jsonOutput,
  reasoning, streaming) for custom models when explicitly disabled,
  since validateModelCapabilities skips custom providers
- Invalidate provider-key SWR cache on create and delete too, not just
  the update path

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/gateway/src/chat/chat.ts`:
- Around line 2535-2538: The condition in the customModelEntry.reasoning ===
false block at line 2535-2538 incorrectly rejects requests where
reasoning_effort is set to "none". Since "none" is a valid value indicating no
reasoning is requested, update the condition to allow reasoning_effort: "none"
for non-reasoning models. Modify the check to only reject when reasoning_effort
is defined and is NOT equal to "none", or when reasoning_max_tokens is defined.
This way, explicit "none" values will pass through as valid no-reasoning
requests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 4b7d621f-3fee-4521-b0ed-df074b6c6854

📥 Commits

Reviewing files that changed from the base of the PR and between 2730af1 and b965848.

📒 Files selected for processing (2)
  • apps/api/src/routes/keys-provider.ts
  • apps/gateway/src/chat/chat.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/api/src/routes/keys-provider.ts

Comment thread apps/gateway/src/chat/chat.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b96584852f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

modelName: z
.string()
.min(1, "Model name is required")
.regex(/^[\w.:/-]+$/, "Model name contains invalid characters"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject or preserve colon-bearing custom model IDs

Allowing : in modelName creates catalog entries that the gateway cannot match: parseModelInput strips everything after the last colon as a region suffix before findCustomModel looks up the catalog entry. For a custom provider model such as anthropic.claude-v1:0, the API accepts and stores that name, but requests to provider/anthropic.claude-v1:0 look up anthropic.claude-v1; with customModelsOnly enabled the model is rejected even though it is defined.

Useful? React with 👍 / 👎.

cacheWriteInputPrice1h: cm.cacheWriteInputPrice1h ?? undefined,
requestPrice: cm.requestPrice ?? undefined,
webSearchPrice: cm.webSearchPrice ?? undefined,
imageInputPrice: cm.imageInputPrice ?? undefined,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Count images before billing custom image input prices

Threading imageInputPrice into the custom mapping never bills image inputs for arbitrary custom models, because inputImageCount is only set for the hard-coded Gemini image model IDs before the custom catalog lookup. A catalog model with vision plus an image input price will still call calculateCosts with inputImageCount as 0, so image requests record no imageInputCost despite the configured price.

Useful? React with 👍 / 👎.

Custom models produce text output only; multi-modal input (images,
audio) is still supported and priced via input fields. Drop the
image-output (generation) price field, which is too provider-specific
to bill generically.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d1985bd602

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

});
}
// Restricting to a custom catalog is an enterprise feature.
if (providerKey.organization?.plan !== "enterprise") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow disabling catalog restriction after downgrade

When an organization downgrades while a custom provider already has customModelsOnly enabled, this check also rejects { customModelsOnly: false }, so the customer cannot turn off the restriction even though the gateway continues honoring the stored flag and will reject any model missing from the old catalog. Only require the enterprise plan when enabling the restriction, or otherwise allow disabling it for non-enterprise orgs.

Useful? React with 👍 / 👎.

Comment on lines +2518 to +2519
customModelEntry.tools === false &&
(tool_choice !== undefined || (tools && tools.length > 0))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject web_search when tools are disabled

For a request that uses the native web_search tool, the handler removes that entry from tools before reaching this new check, so a catalog entry with tools: false still forwards web search requests instead of enforcing the explicit capability flag. Fresh evidence is that this condition now checks only the post-splice tools array; include webSearchTool (or a separate catalog capability) in the rejection path.

Useful? React with 👍 / 👎.

# Conflicts:
#	apps/gateway/src/chat/chat.ts
#	packages/db/migrations/meta/_journal.json

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 664d00170e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2543 to +2546
const customModelEntry = await findCustomModel(
customProviderKey.id,
requestedModel,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Re-run IAM price checks after loading custom pricing

For custom-provider catalog models, the API key IAM validation has already run above using the synthetic custom model mapping with zero prices, and the real catalog prices are only loaded here. In an org that uses IAM allow_pricing max input/output price limits, a catalog model priced above those limits will still be allowed and billed, because the price guard never sees customPricingMapping; revalidate price-based IAM rules after resolving the custom model entry or move this lookup before IAM validation.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fd18709ad0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

});
// The gateway caches provider keys (incl. customModelsOnly) via SWR;
// invalidate so status/restriction changes take effect promptly.
await invalidateSwrByTables([providerKeyTableName]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Invalidate the Drizzle provider-key cache

When a gateway has already cached this custom provider key, PATCHing customModelsOnly writes through the uncached db client and this only clears the SWR fallback mirror; it does not clear the Drizzle Redis cache used by findCustomProviderKey in apps/gateway/src/lib/cached-queries.ts. Until that cache entry expires, turning the catalog restriction on can keep allowing uncataloged models, and turning it off can keep rejecting them, so use the cached client/onMutate path or explicitly invalidate the Drizzle table cache as well.

Useful? React with 👍 / 👎.

const priceField = z
.string()
.refine(
(v) => v.trim() !== "" && Number.isFinite(Number(v)) && Number(v) >= 0,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Store only Decimal-safe price strings

For direct API callers, values like " 0.000003 " pass this validation because Number(v) is finite, but the untrimmed string is stored and later passed to new Decimal(...) in calculateCosts, which rejects whitespace-padded values and breaks requests for that custom model. Trim the value before storing it or validate with the same Decimal-compatible format the gateway will parse.

Useful? React with 👍 / 👎.

@steebchen
steebchen enabled auto-merge June 18, 2026 15:08
@steebchen
steebchen added this pull request to the merge queue Jun 18, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e9721a6b92

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

reasoning: cm.reasoning ?? undefined,
jsonOutput: cm.jsonOutput ?? undefined,
audio: cm.audio ?? undefined,
supportedParameters: cm.supportedParameters ?? undefined,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor custom supportedParameters for request shaping

When a catalog entry omits parameters such as tool_choice or reasoning_effort from supportedParameters, those fields can still be forwarded to the custom provider. The synthetic mapping stores the allowlist here, but the request-body preparation path re-resolves support from the static model registry, which has no custom provider mapping, so it treats the allowlist as absent for those parameters. A custom OpenAI-compatible backend that rejects tool_choice/reasoning_effort will still receive them despite the catalog configuration; strip these fields using the resolved custom mapping before calling prepareRequestBody or pass the mapping through.

Useful? React with 👍 / 👎.

Merged via the queue into main with commit 582f14c Jun 18, 2026
16 checks passed
@steebchen
steebchen deleted the curitiba-v7 branch June 18, 2026 15:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant