Skip to content

feat(ai-gateway): support public custom LLMs - #7291

Closed
chrarnoldus wants to merge 12 commits into
mainfrom
kilo/long-rotor-qeg
Closed

chrarnoldus wants to merge 12 commits into
mainfrom
kilo/long-rotor-qeg

Conversation

@chrarnoldus

@chrarnoldus chrarnoldus commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Kilo admins can now publish a custom LLM to every user, signed in or not.

Depends on #7780 and #7778. This branch merges #7778, which is stacked on #7780. (#7772 was closed; main already has the same change from #8091.) Once they land on main, the diff shrinks to the feature itself.

Definition schema

  • public: new field, public: { inference_providers: string[] }.
    • It cannot be combined with organization_ids or group_ids.
    • Each provider must be a lowercase slug, and at least one is required.
  • description: required when public is set, optional otherwise.
  • organization_ids: optional, but still required unless public is set.
  • Pricing: custom LLMs are always free ([AISAGE-707] feat(ai-gateway): make custom LLMs always free #7778), so there is no pricing field.

Public IDs

  • The kilo-internal/ prefix requirement is dropped, along with CUSTOM_LLM_PREFIX. [AISAGE-707] refactor(ai-gateway): resolve custom LLMs by catalog lookup and fail closed #7780 already stopped relying on the prefix.
  • A new ID must match the auto free model ID format (GatewayModelIdSchema).
  • It must not contain : or ~, or start with kilo/, kilo-auto/ or kilocode/.
  • It must not be in the OpenRouter model list or start with a direct BYOK provider prefix.
  • It must not differ only in case from an existing custom LLM, because catalog lookups ignore case.
  • Reusing a Kilo-exclusive ID is allowed; the custom LLM then takes precedence.

Routing

  • getProvider checks the custom LLM catalog first, through lookupCustomLlm.
  • If a custom LLM exists for an ID but the caller is not eligible, or its stored credentials are missing or invalid, the request returns the same 404 as an unknown model ID. It never falls back to OpenRouter or to the Kilo-exclusive model with the same ID.
  • Signed-out callers asking for a private custom LLM get the usual paid-model 401, so its existence is not revealed.
  • If the catalog cannot be read and nothing is cached yet, requests for Kilo-exclusive IDs get a 503, because a custom LLM may shadow them.
  • Kilo-exclusive rules do not apply to a custom LLM that reuses an exclusive ID: rate limits, unavailable status, provider pinning, pricing and request rewriting.

Public custom LLMs

  • Access: available to everyone, including anonymous users. canUseCustomLlm treats public custom LLMs as usable by everyone, so Cloud Agent balance eligibility also treats them as free.
  • Allow lists: organization and group provider allow lists are enforced against public.inference_providers.
  • Request-level check: a request whose provider.only / provider.ignore excludes every declared provider gets a 403.
  • Data collection: required, as for other free models.
  • Snapshot: the provider sync lists each public custom LLM under every declared provider. A provider that OpenRouter does not know is created in the snapshot.
  • Catalog: public custom LLMs appear in the gateway catalog with isFree, isCustomLlm and inference_provider_ids. OpenRouter and Kilo-exclusive entries with the same ID are hidden.
  • Excluded like private ones: isCustomLlm keeps public custom LLMs out of the same places as private ones: auto-routing pools, router and default model pickers, and Organization Auto targets.

Admin UI

  • A confirmation dialog appears when a save makes a definition public, recommending local testing first.
  • The list shows whether each custom LLM is public or private.
  • Public IDs are validated before saving.

Verification

  • I did not test the admin UI or the gateway by hand.
  • Typecheck passes for @kilocode/db and @kilocode/web-shared. apps/web typecheck runs out of memory in the sandbox, so CI is the first full check.
  • oxlint and oxfmt pass on changed files.
  • The affected Jest suites (about 4,250 tests) pass against a local Postgres.

Visual Changes

The admin Custom LLMs page has a new Access column, updated help text, and a confirmation dialog when a definition becomes public. I took no screenshots.

Reviewer Notes

  • Snapshot timing: a public custom LLM only appears in the models-by-provider snapshot after the next provider sync. Allow-list enforcement reads inference_providers from the definition, so it does not wait for the sync.
  • provider field: when organization policy narrows providers, provider.only is forwarded to the custom upstream in the request body, as organization privacy settings already are. Use remove_from_body if an upstream rejects the field.
  • Upstream errors: custom LLM upstream errors are passed through unchanged.
  • Credential failures: these behave as before: a decrypt or token-exchange failure fails the request.

@chrarnoldus chrarnoldus self-assigned this Oct 8, 2026
@kilo-code-bot

kilo-code-bot Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: 2 Issues Found | Recommendation: Address before merge

Executive Summary

The highest-risk issue is that custom LLM credential decryption and Google token exchange still throw out of getProvider, so a single broken definition returns a 500 to every caller of a newly public model instead of the intended 404/503.

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
packages/web-shared/src/lib/ai-gateway/providers/get-provider.ts 114 decryptApiKey and the service-account token exchange throw out of getProvider; a broken credential 500s every request instead of returning custom-llm-unavailable/custom-llm-temporarily-unavailable. The earlier thread on this file said d84c835 fixed this, but the guard is absent in the rebuilt branch.

SUGGESTION

File Line Issue
packages/web-shared/src/lib/ai-gateway/custom-llm/custom-llm-catalog.ts 69 Lowercased catalog keys can collide with a legacy case-variant custom_llm2.public_id row; admin validation compares ids exactly, so the id can silently resolve to the other row's definition, credentials and eligibility.
Files Reviewed (16 files)
  • packages/web-shared/src/lib/ai-gateway/providers/get-provider.ts - 1 issue
  • packages/web-shared/src/lib/ai-gateway/custom-llm/custom-llm-catalog.ts - 1 issue
  • packages/web-shared/src/lib/ai-gateway/handlers/llm-proxy.ts
  • packages/web-shared/src/lib/ai-gateway/llm-proxy-helpers.ts
  • packages/web-shared/src/lib/ai-gateway/custom-llm/access.ts
  • packages/web-shared/src/lib/ai-gateway/custom-llm/listAvailableCustomLlms.ts
  • packages/web-shared/src/lib/ai-gateway/is-free-model.ts
  • packages/web-shared/src/lib/ai-gateway/processUsage.ts
  • packages/web-shared/src/lib/ai-gateway/rewriteModelResponse.ts
  • packages/web-shared/src/lib/ai-gateway/providers/apply-provider-specific-logic.ts
  • packages/web-shared/src/lib/ai-gateway/providers/openrouter/index.ts
  • packages/web-shared/src/lib/model-allow.server.ts
  • packages/web-shared/src/lib/organizations/group-policies/model-access/model-access.server.ts
  • packages/db/src/schema-types.ts
  • apps/web/src/routers/admin/custom-llm-router.ts
  • apps/web/src/lib/providers/openrouter/sync-providers.ts

Notes: the previous review SHA was not present in this checkout, so I fell back to a full review of 79cb0d36; change sets from the earlier review threads were re-checked for the current code. Verification was read-only: no typecheck or tests were run.

Fix these issues in Kilo Cloud

Previous Review Summaries (6 snapshots, latest commit 78a9294)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 78a9294)

Status: No Issues Found | Recommendation: Merge

Executive Summary

The only change since the previous review (8fa9c356) is a formatting-only collapse of the @kilocode/db/schema-types import in get-provider.ts; both imported symbols exist, so I found no new correctness, security, or reliability issues.

Files Reviewed (1 file)
  • packages/web-shared/src/lib/ai-gateway/providers/get-provider.ts

Previous review (commit 8fa9c35)

Status: No Issues Found | Recommendation: Merge

Executive Summary

I re-verified every previously reported finding against current HEAD 8fa9c356: the two compile-breaking renames are complete (isFreeModelExcludingCustomLlms is now exported from is-free-model.ts; all getInferenceProviders call sites are updated), the shouldRedactErrorResponse('custom', …) test contradiction is resolved, and the new stream-redaction and custom-LLM cost-stripping tests are backed by implementations. Across the full diff (59 files) I found no new high-confidence correctness, security, or reliability issues.

Files Reviewed (59 files)
  • apps/web/src/app/admin/custom-llms/CustomLlmsContent.tsx
  • apps/web/src/components/auto-routing/AutoRoutingModeCard.test.ts
  • apps/web/src/components/auto-routing/AutoRoutingModeCard.tsx
  • apps/web/src/components/organizations/custom-modes/ModeForm.tsx
  • apps/web/src/components/organizations/providers-and-models/DefaultModelDialog.tsx
  • apps/web/src/lib/auto-routing-pool-validation.test.ts
  • apps/web/src/lib/auto-routing-pool-validation.ts
  • apps/web/src/lib/bot/agent-runner.ts
  • apps/web/src/lib/cloud-agent-next/balance-check-eligibility.test.ts
  • apps/web/src/lib/cloud-agent-next/balance-check-eligibility.ts
  • apps/web/src/lib/providers/openrouter/public-custom-llm-snapshot.test.ts
  • apps/web/src/lib/providers/openrouter/public-custom-llm-snapshot.ts
  • apps/web/src/lib/providers/openrouter/sync-providers.ts
  • apps/web/src/routers/admin/custom-llm-router.test.ts
  • apps/web/src/routers/admin/custom-llm-router.ts
  • apps/web/src/routers/organizations/organization-settings-router.test.ts
  • apps/web/src/tests/openrouter-models.test.ts
  • packages/db/src/schema-types.ts
  • packages/web-shared/src/lib/ai-gateway/auto-model/auto-free-config.ts
  • packages/web-shared/src/lib/ai-gateway/custom-llm/access.ts
  • packages/web-shared/src/lib/ai-gateway/custom-llm/custom-llm-catalog.test.ts
  • packages/web-shared/src/lib/ai-gateway/custom-llm/custom-llm-catalog.ts
  • packages/web-shared/src/lib/ai-gateway/custom-llm/listAvailableCustomLlms.ts
  • packages/web-shared/src/lib/ai-gateway/handlers/audio-transcriptions.test.ts
  • packages/web-shared/src/lib/ai-gateway/handlers/audio-transcriptions.ts
  • packages/web-shared/src/lib/ai-gateway/handlers/edit-completions.ts
  • packages/web-shared/src/lib/ai-gateway/handlers/embeddings.test.ts
  • packages/web-shared/src/lib/ai-gateway/handlers/embeddings.ts
  • packages/web-shared/src/lib/ai-gateway/handlers/fim-completions.ts
  • packages/web-shared/src/lib/ai-gateway/handlers/llm-proxy.test.ts
  • packages/web-shared/src/lib/ai-gateway/handlers/llm-proxy.ts
  • packages/web-shared/src/lib/ai-gateway/is-free-model.ts
  • packages/web-shared/src/lib/ai-gateway/kilo-exclusive-models.ts
  • packages/web-shared/src/lib/ai-gateway/llm-proxy-helpers.test.ts
  • packages/web-shared/src/lib/ai-gateway/llm-proxy-helpers.ts
  • packages/web-shared/src/lib/ai-gateway/local-fake-llm.test.ts
  • packages/web-shared/src/lib/ai-gateway/model-utils.ts
  • packages/web-shared/src/lib/ai-gateway/models.test.ts
  • packages/web-shared/src/lib/ai-gateway/processUsage.test.ts
  • packages/web-shared/src/lib/ai-gateway/processUsage.ts
  • packages/web-shared/src/lib/ai-gateway/providers/apply-provider-specific-logic.ts
  • packages/web-shared/src/lib/ai-gateway/providers/get-provider.custom-llm.test.ts
  • packages/web-shared/src/lib/ai-gateway/providers/get-provider.ts
  • packages/web-shared/src/lib/ai-gateway/providers/kilo-exclusive-model.test.ts
  • packages/web-shared/src/lib/ai-gateway/providers/kilo-exclusive-model.ts
  • packages/web-shared/src/lib/ai-gateway/providers/openrouter/index.test.ts
  • packages/web-shared/src/lib/ai-gateway/providers/openrouter/index.ts
  • packages/web-shared/src/lib/ai-gateway/providers/vercel/index.test.ts
  • packages/web-shared/src/lib/ai-gateway/providers/vercel/index.ts
  • packages/web-shared/src/lib/ai-gateway/rewriteModelResponse.test.ts
  • packages/web-shared/src/lib/ai-gateway/rewriteModelResponse.ts
  • packages/web-shared/src/lib/ai-gateway/usage-shadow-integration.test.ts
  • packages/web-shared/src/lib/model-allow.server.test.ts
  • packages/web-shared/src/lib/model-allow.server.ts
  • packages/web-shared/src/lib/organizations/effective-model-access.server.test.ts
  • packages/web-shared/src/lib/organizations/group-policies/model-access/model-access.server.ts
  • packages/web-shared/src/lib/organizations/organization-auto-model.ts
  • packages/web-shared/src/lib/organizations/organization-types.ts
  • packages/web-shared/src/tests/helpers/custom-llm.helper.ts

Verification Notes

  • Re-read HEAD (8fa9c356201eb3dbf6f01100161e39c8c59f86ed) immediately before publishing; unchanged.
  • Could not produce the incremental diff: the previous review SHA 8c5f0f5 is not present locally (history rewritten), so I reviewed the full PR diff.
  • Reviewer Notes in the PR state that admin edits propagate within the 60 s in-process cache TTL and that there is no cross-instance invalidation; the admin router intentionally no longer calls an invalidation hook (it is now resetCustomLlmCacheForTest). Treating that documented tradeoff as accepted, not a defect.

Previous review (commit 8c5f0f5)

Status: 5 Issues Found | Recommendation: Address before merge

Executive Summary

The latest commit renames symbols without completing the refactor: isFreeModelExcludingCustomLlms is imported from a module that never gained the export, and getInferenceProvider is renamed while three call sites keep the old name, so the branch does not compile.

Overview

Severity Count
CRITICAL 2
WARNING 3
SUGGESTION 0
Issue Details (click to expand)

CRITICAL

File Line Issue
packages/web-shared/src/lib/ai-gateway/handlers/embeddings.ts 13 isFreeModelExcludingCustomLlms is not exported by is-free-model.ts; ~8 changed files fail to compile (TS2305)
packages/web-shared/src/lib/ai-gateway/providers/kilo-exclusive-model.ts 203 getInferenceProvider renamed to getInferenceProviders but sync-providers.ts, llm-proxy-helpers.ts and models.test.ts still import the old name (TS2305)

WARNING

File Line Issue
packages/web-shared/src/lib/ai-gateway/kilo-exclusive-models.ts 272 shouldRedactErrorResponse('custom', ...) now returns true, breaking the existing models.test.ts:326
packages/web-shared/src/lib/ai-gateway/rewriteModelResponse.test.ts 1386 New cost-stripping test has no matching implementation (custom is not in the removeCost gate)
packages/web-shared/src/lib/ai-gateway/rewriteModelResponse.test.ts 1446 New stream-redaction tests have no implementation; custom upstream error bodies still pass through
Files Reviewed (17 files)
  • apps/web/src/lib/bot/agent-runner.ts - 1 issue
  • apps/web/src/lib/cloud-agent-next/balance-check-eligibility.test.ts
  • packages/web-shared/src/lib/ai-gateway/auto-model/auto-free-config.ts - 1 issue
  • packages/web-shared/src/lib/ai-gateway/handlers/audio-transcriptions.test.ts
  • packages/web-shared/src/lib/ai-gateway/handlers/audio-transcriptions.ts - 1 issue
  • packages/web-shared/src/lib/ai-gateway/handlers/edit-completions.ts - 1 issue
  • packages/web-shared/src/lib/ai-gateway/handlers/embeddings.test.ts
  • packages/web-shared/src/lib/ai-gateway/handlers/embeddings.ts - 1 issue
  • packages/web-shared/src/lib/ai-gateway/handlers/fim-completions.ts - 1 issue
  • packages/web-shared/src/lib/ai-gateway/kilo-exclusive-models.ts - 1 issue
  • packages/web-shared/src/lib/ai-gateway/llm-proxy-helpers.test.ts
  • packages/web-shared/src/lib/ai-gateway/local-fake-llm.test.ts
  • packages/web-shared/src/lib/ai-gateway/providers/kilo-exclusive-model.test.ts
  • packages/web-shared/src/lib/ai-gateway/providers/kilo-exclusive-model.ts - 1 issue
  • packages/web-shared/src/lib/ai-gateway/providers/vercel/index.test.ts
  • packages/web-shared/src/lib/ai-gateway/providers/vercel/index.ts - 1 issue
  • packages/web-shared/src/lib/ai-gateway/rewriteModelResponse.test.ts - 2 issues

Previous review (commit 96981e6)

Status: No Issues Found | Recommendation: Merge

Executive Summary

Incremental commit 96981e6ae resolves both prior findings and introduces no new issues in the changed lines.

Files Reviewed (5 files)
  • packages/web-shared/src/lib/ai-gateway/custom-llm/custom-llm-catalog.ts
  • packages/web-shared/src/lib/ai-gateway/custom-llm/custom-llm-catalog.test.ts
  • packages/web-shared/src/lib/ai-gateway/handlers/llm-proxy.ts
  • packages/web-shared/src/lib/ai-gateway/handlers/llm-proxy.test.ts
  • packages/web-shared/src/lib/ai-gateway/providers/get-provider.ts

Previous review (commit d84c835)

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
packages/web-shared/src/lib/ai-gateway/custom-llm/custom-llm-catalog.ts 88 Cold-cache read failure lets a custom LLM id fall back to the shadowed Kilo-exclusive model, breaking the custom-LLM-owns-its-id invariant under a DB outage.

SUGGESTION

File Line Issue
packages/web-shared/src/lib/ai-gateway/custom-llm/custom-llm-catalog.ts 86 No negative cache: an outage replays a fresh custom_llm2 query and a Sentry event per lookup, several times per request.
Files Reviewed (6 files)
  • packages/web-shared/src/lib/ai-gateway/custom-llm/custom-llm-catalog.ts - 2 issues
  • packages/web-shared/src/lib/ai-gateway/custom-llm/custom-llm-catalog.test.ts
  • packages/web-shared/src/lib/ai-gateway/handlers/llm-proxy.ts
  • packages/web-shared/src/lib/ai-gateway/handlers/llm-proxy.test.ts
  • packages/web-shared/src/lib/ai-gateway/providers/get-provider.ts
  • packages/web-shared/src/lib/ai-gateway/providers/get-provider.custom-llm.test.ts

Fix these issues in Kilo Cloud

Previous review (commit d2b1f5a)

Status: 3 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
packages/web-shared/src/lib/ai-gateway/providers/get-provider.ts 128 Unguarded credential resolution (decryptApiKey / Google token) throws a 500 instead of returning custom-llm-unavailable (404), exposing the new anonymous public path to unhandled errors.
packages/web-shared/src/lib/ai-gateway/custom-llm/custom-llm-catalog.ts 76 Cold-cache custom_llm2 read failure rethrows on the now-unconditional hot path (llm-proxy.ts:488, get-provider.ts:218), turning unrelated model requests into 500s.

SUGGESTION

File Line Issue
packages/web-shared/src/lib/ai-gateway/custom-llm/custom-llm-catalog.ts 82 invalidateCustomLlmCache leaves inFlight intact, so a pre-edit load can re-cache a stale snapshot with a fresh 60 s TTL.
Files Reviewed (36 files)
  • apps/web/src/app/admin/custom-llms/CustomLlmsContent.tsx
  • apps/web/src/components/auto-routing/AutoRoutingModeCard.test.ts
  • apps/web/src/components/auto-routing/AutoRoutingModeCard.tsx
  • apps/web/src/components/organizations/custom-modes/ModeForm.tsx
  • apps/web/src/components/organizations/providers-and-models/DefaultModelDialog.tsx
  • apps/web/src/lib/auto-routing-pool-validation.ts
  • apps/web/src/lib/cloud-agent-next/balance-check-eligibility.ts
  • apps/web/src/lib/code-reviews/prompts/generate-prompt.ts
  • apps/web/src/lib/providers/openrouter/public-custom-llm-snapshot.test.ts
  • apps/web/src/lib/providers/openrouter/public-custom-llm-snapshot.ts
  • apps/web/src/lib/providers/openrouter/sync-providers.ts
  • apps/web/src/routers/admin/custom-llm-router.test.ts
  • apps/web/src/routers/admin/custom-llm-router.ts
  • apps/web/src/routers/organizations/organization-settings-router.test.ts
  • apps/web/src/tests/openrouter-models.test.ts
  • packages/db/src/schema-types.ts
  • packages/web-shared/src/lib/ai-gateway/custom-llm/access.ts
  • packages/web-shared/src/lib/ai-gateway/custom-llm/custom-llm-catalog.test.ts
  • packages/web-shared/src/lib/ai-gateway/custom-llm/custom-llm-catalog.ts
  • packages/web-shared/src/lib/ai-gateway/custom-llm/listAvailableCustomLlms.ts
  • packages/web-shared/src/lib/ai-gateway/handlers/llm-proxy.test.ts
  • packages/web-shared/src/lib/ai-gateway/handlers/llm-proxy.ts
  • packages/web-shared/src/lib/ai-gateway/llm-proxy-helpers.ts
  • packages/web-shared/src/lib/ai-gateway/model-utils.ts
  • packages/web-shared/src/lib/ai-gateway/processUsage.ts
  • packages/web-shared/src/lib/ai-gateway/providers/apply-provider-specific-logic.ts
  • packages/web-shared/src/lib/ai-gateway/providers/get-provider.custom-llm.test.ts
  • packages/web-shared/src/lib/ai-gateway/providers/get-provider.ts
  • packages/web-shared/src/lib/ai-gateway/providers/openrouter/index.ts
  • packages/web-shared/src/lib/model-allow.server.test.ts
  • packages/web-shared/src/lib/model-allow.server.ts
  • packages/web-shared/src/lib/organizations/effective-model-access.server.test.ts
  • packages/web-shared/src/lib/organizations/group-policies/model-access/model-access.server.ts
  • packages/web-shared/src/lib/organizations/organization-auto-model.ts
  • packages/web-shared/src/lib/organizations/organization-types.ts
  • packages/web-shared/src/tests/helpers/custom-llm.helper.ts

Fix these issues in Kilo Cloud


Reviewed by deepseek-v4.1-flash-1017 · Input: 217.6K · Output: 45.8K · Cached: 6.8M

Review guidance: REVIEW.md from base branch main

…/long-rotor-qeg

# Conflicts:
#	packages/web-shared/src/lib/ai-gateway/handlers/llm-proxy.ts

Copy link
Copy Markdown
Contributor Author

Superseded by #8294, which rebases the public custom LLM work onto main now that #7780 and #7778 have landed.

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