Skip to content

feat(gateway): replace Kilo catalog from Gateway and enforce team scope - #15065

Merged
marius-kilocode merged 14 commits into
gateway-login-v2from
gateway-catalog
Oct 15, 2026
Merged

marius-kilocode merged 14 commits into
gateway-login-v2from
gateway-catalog

Conversation

@marius-kilocode

Copy link
Copy Markdown
Collaborator

Stacked on #15064.
Part of #14991 (and #13750), PR 2 of the Gateway series. Plan unit U-E1-04 (catalog authority).

What Problem This Solves

In PR 1 (#15064), the kilo provider still inherited its model list from the bundled models.dev seed:

  • Models added, renamed, or removed on the Kilo Gateway did not appear or disappear.
  • Team allow-lists (/api/organizations/{id}/models) and team default models (/api/organizations/{id}/defaults) were not fetched or enforced.
  • Signed-out sessions showed the full static models.dev list instead of the Gateway's isFree === true catalog.

Why This Change Was Made

Following the architecture in #14991 and #15060:

  1. Gateway catalog and defaults in packages/kilo-gateway-core (src/models.ts, src/defaults.ts):
    • Fetches personal (GET /api/openrouter/models, with 401 fallback to the anonymous free catalog when !organizationID) and team (GET /api/organizations/{id}/models) catalogs.
    • Validates and normalizes each model: positive integer context_length, output limit (top_provider.max_completion_tokens ?? max_completion_tokens ?? Math.ceil(context_length * 0.2)), tool support (supportsTools, treating missing or empty supported_parameters as tool-capable per v1 c4506f7ef87), finite non-negative per-million pricing (cost), input/output modalities, opencode.variants, opencode.ai_sdk_provider (anthropic, openai, openai-compatible, openrouter), opencode.prompt, and metadata (isFree, recommendedIndex, hasUserByokAvailable, mayTrainOnYourPrompts, autoRouting, terminalBench, description, reasoning, family).
    • Maps kilo-auto/small to the top-priority small-model family so Model.small("kilo") selects kilo-auto/small for title generation and utility tasks.
    • Fetches /api/defaults (personal/anonymous) and /api/organizations/{id}/defaults (team) with fallback to kilo-auto/frontier (signed in) or kilo-auto/free (signed out).
    • Implements catalog recovery retry predicate and exponential backoff (30 * 2^min(attempt, 4) up to 300s, honoring Retry-After).
  2. Full catalog authority in packages/core/src/kilocode/gateway.ts (editor.add / models.set):
    • Replaces the kilo model list completely from the Gateway response on startup, login, logout, credential.switched, organization.set, and a 5-minute refresh timer; models.dev models never leak through.
    • Personal and anonymous scopes: persists the last good Gateway catalog per scope (catalog:personal:<hash> / catalog:anonymous:<server>) in plugin storage (without storing tokens) and serves it on transient fetch failure; never falls back to models.dev.
    • Team scope: fails closed on error (models: [], activation: "disabled", sentinel sourceConnection, and model.transform removal so ConfigProviderPlugin in post cannot re-enable static models under a failed team scope).
    • Signed-out scope: filters the anonymous Gateway catalog to isFree === true only and suppresses uncredentialed OpenCode Zen (apiKey === "public") so Model.default() selects Kilo's free default model (kilo/kilo-auto/free).
    • Serializes catalog loads with a semaphore and epoch guard so an in-flight load from an older account or team cannot overwrite a newer selection.
  3. Model metadata RPC (packages/core/src/kilocode/gateway-rpc.ts):
    • Adds models method to kilocode.gateway returning KiloModels.Entry[] metadata (isFree, recommendedIndex, hasUserByokAvailable, mayTrainOnYourPrompts, autoRouting, terminalBench, description, reasoning, family, prompt, aiSDKProvider).

Shared upstream files changed

None in this PR (all changes are in Kilo-owned paths packages/kilo-gateway-core/** and packages/core/**/kilocode/**).

User Impact

  • Signed out, the model picker and models CLI command show only the Kilo Gateway's free models (isFree === true, 14 models on production Gateway) and default to kilo/kilo-auto/free.
  • Signed in on a personal account, the model picker shows the full Gateway personal catalog and defaults to /api/defaults (kilo/kilo-auto/frontier).
  • Signed in on a team, the model picker shows only /api/organizations/{id}/models and fails closed if the team catalog cannot be fetched.

Evidence

Automated checks

  • bun script/check-opencode-annotations.ts: ok (39 shared files against 4d9678a79d6)
  • bun script/check-shared-patches.ts: ok (0 new shared upstream files touched)
  • bun run --cwd packages/kilo-gateway-core typecheck && bun run --cwd packages/kilo-gateway-core build: pass
  • bun run --cwd packages/core typecheck: pass
  • bun run --cwd packages/tui typecheck: pass
  • bun test --cwd packages/kilo-gateway-core: 25 pass, 0 fail (83 assertions)
  • bun test --cwd packages/core test/kilocode/gateway.test.ts test/plugin/provider-kilo.test.ts: 12 pass, 0 fail (53 assertions)

CLI & TUI Self-Test

  1. Live Gateway CLI catalog (bun dev models --standalone):
    • Signed out: returns 14 free models (kilo/kilo-auto/free, kilo/openrouter/free, etc.) and 0 paid models (kilo/anthropic/claude-sonnet-4.6 absent).
    • Signed in (KILO_API_KEY): returns 328 Gateway models including kilo/anthropic/claude-sonnet-4.6 and kilo/kilo-auto/frontier.
  2. TUI model picker across Signed-Out, Personal Account, and Team scopes:

Signed-out model picker showing only Kilo Auto Free and Kilo Auto Small

Personal Account model picker showing Kilo Auto Free, Kilo Auto Frontier, and Kilo Auto Small

Team-scoped model picker showing Claude Sonnet 4.6 (Team)

Comment thread packages/kilo-gateway-core/src/models.ts
Comment thread packages/core/src/kilocode/gateway.ts Outdated
Comment thread packages/core/src/kilocode/gateway.ts Outdated
Comment thread packages/core/src/kilocode/gateway.ts Outdated
Comment thread packages/core/src/kilocode/gateway.ts
Comment thread packages/kilo-gateway-core/src/testing.ts Outdated
@kilo-code-bot

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

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Incremental re-review of 9de13c1285 (8 files, +120/−32). This commit syncs Gateway URL routing with main: kilo-gateway-core/src/server.ts adds aiGatewayFromEnv/apiUrl/isConfiguredBase/modelsUrl/gateway, and the models catalog, defaults, and profile callers now build URLs through them. For the configured base the catalog/inference traffic goes to the AI gateway (https://ai-gateway.kilo.ai/api/v1, or ${KILO_API_URL}/api/gateway/v1), while custom/token-embedded servers keep the legacy /api/... app routes (/api/openrouter/models, /api/organizations/{id}/models, /api/gateway). gateway.ts also keys catalog snapshots on the resolved gateway route and extends the IS_TEST_RUNNER offline guard to KILO_AI_GATEWAY_URL/aiGatewayFromEnv. The fromToken regex + URL.canParse guard, the fixture route matchers, and the updated baseURL expectations are mutually consistent. No new issues found.

The four counter-AI findings left in the PR discussion were checked against current HEAD: the retry self-interrupt and the stray retry-after fixture key are already fixed, and the persisted-snapshot cast and empty-catalog concerns sit on unchanged lines outside this increment.

Files Reviewed (8 files)
  • packages/core/src/kilocode/gateway.ts
  • packages/core/test/kilocode/gateway.test.ts
  • packages/kilo-gateway-core/src/defaults.ts
  • packages/kilo-gateway-core/src/models.ts
  • packages/kilo-gateway-core/src/profile.ts
  • packages/kilo-gateway-core/src/server.ts
  • packages/kilo-gateway-core/src/testing.ts
  • packages/kilo-gateway-core/test/account.test.ts
Previous Review Summaries (7 snapshots, latest commit 0408727)

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

Previous review (commit 0408727)

Status: No Issues Found | Recommendation: Merge

Incremental re-review of 0408727be76 (2 files). The IS_TEST_RUNNER change replaces the Bun.main read with process.argv[1] ?? "", matching the repo's existing entrypoint convention (packages/cli/src/util/process.ts:3) and avoiding the Bun global in the workerd bundle. The defaults.ts change removes the unused scope.fallback field and its dead branch, leaving behavior identical for the sole caller (gateway.ts:186), which resolves the previously reported suggestion.

All previously reported findings are now resolved in current code: the catalog decode validates entries individually (catalogModels), stale personal snapshots are swept on save, the retry no longer self-interrupts, the OpenCode Zen suppression code is gone, the success path passes sourceConnection, and the fixture retry-after header is corrected.

Files Reviewed (2 files)
  • packages/core/src/kilocode/gateway.ts
  • packages/kilo-gateway-core/src/defaults.ts

Previous review (commit ca6ff16)

Status: 1 Suggestion Found | Recommendation: Merge

Re-reviewed the incremental commits ending at ca6ff16e4a (5 files). The change removes the opencode.prompt metadata from kilo-gateway-core and the kilocode.gateway.models RPC entry, and adds a team-default fallback: when the team default model is absent from the fetched catalog, loadCatalog now selects the first catalog model (resolvedDefault), and KiloDefaults.get gained an optional scope.fallback. The prompt removal is complete (no remaining Entry.prompt consumers; RawModel.opencode.prompt is safely ignored by the struct decoder), and the team-default resolution is correct while leaving personal/anonymous behavior unchanged. One new suggestion is below; the prior findings are resolved or outdated in remote state.

Overview

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

SUGGESTION

File Line Issue
packages/kilo-gateway-core/src/defaults.ts 29 New scope.fallback branch is unreachable; the sole caller (gateway.ts:186) never passes it and the team fallback is resolved inline
Files Reviewed (5 files)
  • packages/core/src/kilocode/gateway-rpc.ts - 0 issues
  • packages/core/src/kilocode/gateway.ts - 0 issues
  • packages/kilo-gateway-core/src/defaults.ts - 1 suggestion
  • packages/kilo-gateway-core/src/models.ts - 0 issues
  • packages/kilo-gateway-core/test/models.test.ts - 0 issues

Fix these issues in Kilo Cloud

Previous review (commit f570126)

Status: No Issues Found | Recommendation: Merge

Re-reviewed the incremental commits c8e694a477 and f57012666e (1 file, packages/core/src/kilocode/gateway.ts). The change removes the hardcoded BUILT_IN_FREE_MODELS floor, initializes the catalog empty, adds a test-only skippedOfflineTest state that keeps the provider enabled without fetching over the network, and aligns catalog recovery retries with v1: an empty successful catalog now schedules a retry and the attempt >= 5 cap is dropped so backoff (capped at 300s) continues until recovery or a non-retryable failure. No new issues: retries are still cancelled on reload via cancelRetry, the semaphore permit is released before the forked retry sleeps, and lastGood/snapshot eviction stay bounded. The two prior findings still anchored to current diff lines are re-verified fixed on HEAD: per-entry catalog validation (flatMap(decodeRawModel), packages/kilo-gateway-core/src/models.ts:208-214) and the success path publishing sourceConnection (packages/core/src/kilocode/gateway.ts:360-361,374). The remaining four prior comments are outdated (line: null) in remote state.

Files Reviewed (1 file)
  • packages/core/src/kilocode/gateway.ts - incremental changes, 0 issues

Previous review (commit f4a3360)

Status: No Issues Found | Recommendation: Merge

Re-reviewed the incremental commit f4a3360191 (1 hunk, packages/core/src/kilocode/gateway.ts). The change guards saveSnapshot's persisted catalog:personal: sweep behind a writeKey.startsWith("catalog:personal:") check, so saving an anonymous snapshot (catalog:anonymous:<server>) no longer deletes the user's persisted personal catalog. Correct and well-scoped; no new issues. The two prior findings still anchored to current diff lines are re-verified as fixed on HEAD: per-entry catalog validation (CatalogEnvelope + flatMap(decodeRawModel), packages/kilo-gateway-core/src/models.ts:83-89,210) and the success path now publishing sourceConnection (packages/core/src/kilocode/gateway.ts:367-368,381). The remaining four prior comments are outdated in remote state.

Files Reviewed (1 file)
  • packages/core/src/kilocode/gateway.ts - incremental changes, 0 issues

Previous review (commit fe81421)

Status: No Issues Found | Recommendation: Merge

Re-reviewed the incremental commit fe81421817 (7 hunks in packages/core/src/kilocode/gateway.ts). No new issues. All six prior findings are resolved on current HEAD: per-entry catalog validation (CatalogEnvelope + flatMap(decodeRawModel)), saveSnapshot prefix sweep + MAX_SNAPSHOTS cap, retry chain no longer self-interrupts, success path now publishes sourceConnection, the stray " retry-after" key is gone, and the non-functional OpenCode Zen suppression block is removed. The changes reviewed here — IS_TEST_RUNNER detection via Bun.main (guards offline fetches and the 5-minute refresh), the editor.get(id) provider guard (the kilo provider is still seeded by ModelsDevPlugin before provider transforms replay, and its integration name still comes from the models.dev seed), the models.list(id) default-model check, and dropping credential.updated from the reload filter (login/logout/switch still emit credential.switched) — are behavior-preserving for production and correctly scoped.

Files Reviewed (3 files)
  • packages/core/src/kilocode/gateway.ts - incremental changes, 0 issues
  • packages/kilo-gateway-core/src/models.ts - prior finding re-verified as fixed
  • packages/kilo-gateway-core/src/testing.ts - prior finding re-verified as fixed

Previous review (commit 4afef24)

Status: No Issues Found | Recommendation: Merge

The follow-up commit resolved all prior findings: per-entry catalog validation (CatalogEnvelope + flatMap(decodeRawModel)) prevents one bad model from dropping the catalog, saveSnapshot now sweeps the catalog:personal: prefix and caps lastGood at MAX_SNAPSHOTS, the retry chain no longer self-interrupts, the success path publishes sourceConnection, and the stray " retry-after" header key is removed. The removed OpenCode Zen suppression block was verified non-functional (provider transforms replay in registration order from fresh state, so apiKey is never "public" when Kilo's transform runs), so its removal is not a behavioral regression.

Files Reviewed (4 files)
  • packages/core/src/kilocode/gateway.ts
  • packages/kilo-gateway-core/src/models.ts
  • packages/kilo-gateway-core/src/testing.ts
  • packages/kilo-gateway-core/test/models.test.ts

Previous review (commit 4ee771b)

Status: 6 Issues Found | Recommendation: Address before merge

Overview

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

WARNING

File Line Issue
packages/kilo-gateway-core/src/models.ts 44 One malformed entry drops the entire catalog (all-or-nothing array decode)
packages/core/src/kilocode/gateway.ts 156 Per-token catalog snapshots accumulate in persistent storage with no eviction
packages/core/src/kilocode/gateway.ts 209 Retry self-interrupt aborts the retry chain after one attempt
packages/core/src/kilocode/gateway.ts 274 OpenCode Zen suppression never takes effect (transform ordering)
packages/core/src/kilocode/gateway.ts 310 Success path omits sourceConnection; stale models after credential switch

SUGGESTION

File Line Issue
packages/kilo-gateway-core/src/testing.ts 140 Stray " retry-after" header key with a leading space
Files Reviewed (8 files)
  • packages/core/src/kilocode/gateway-rpc.ts - 0 issues
  • packages/core/src/kilocode/gateway.ts - 4 issues
  • packages/core/test/kilocode/gateway.test.ts - 0 issues
  • packages/kilo-gateway-core/src/defaults.ts - 0 issues
  • packages/kilo-gateway-core/src/index.ts - 0 issues
  • packages/kilo-gateway-core/src/models.ts - 1 issue
  • packages/kilo-gateway-core/src/testing.ts - 1 issue
  • packages/kilo-gateway-core/test/models.test.ts - 0 issues

Fix these issues in Kilo Cloud


Reviewed by deepseek-v4.1-flash · Input: 61.7K · Output: 23.7K · Cached: 637.6K

Comment thread packages/kilo-gateway-core/src/defaults.ts Outdated
@WebReflection

Copy link
Copy Markdown
Contributor

counter-AI findings:

1. Retry chain self-cancels on the second attempt — backoff ladder is effectively broken

packages/core/src/kilocode/gateway.ts:70-75 (cancelRetry) and :206-232 (scheduleRetry, called from inside loadCatalog at :172/:203).

scheduleRetry starts with cancelRetry, which interrupts retryFiber. But when the retry fiber's own loadCatalog fails again, retryFiber still points at the fiber that is currently running, so the chain self-interrupts. With this repo's effect 4.0.0-beta.83, Fiber.interrupt(self) → FiberImpl.interruptUnsafe re-entrantly sets the fiber's _exit (when interruptible, it runs evaluate(failCause) inside the live coroutine), after which resumptions of the abandoned coroutine are dropped (FiberImpl.evaluate early-returns once _exit is set, and callbackOptions resumes via fiber.evaluate).

I reproduced this with a probe replicating the exact scheduling structure (same cancelRetry/sleep/forkIn(scope)/tap shape, compressed delays) against the repo's installed effect build: with all fetches failing, attempts execute load0 → load1 and then stop — the "retry fork for attempt 2" tap never runs, and the post-retry ctx.provider.reload() (:217) never executes. Clearing retryFiber at the top of the retry body makes the full ladder run. Consequences:

  • The designed 5-attempt exponential backoff (retryDelayMs/attempt >= 5) never gets past attempt 1; recovery then depends solely on the 5-minute refresh (:391) or new user events.
  • On the dropped reload, the fail-closed/last-good catalog is updated only in memory; the provider/model services keep the stale snapshot — for a team scope this means the allow-list fail-closed enforcement can silently lag up to a refresh interval.

Fix: clear retryFiber (or exclude the self fiber) at the start of the retry chain scope.

2. Testing fixture constructs an invalid header name (throws in Bun) — latent

packages/kilo-gateway-core/src/testing.ts:140: { " retry-after": ..., "retry-after": ... }. Bun throws TypeError: Invalid header name: ' retry-after' when the Response is constructed (verified with bun 1.4.2). No current test combination hits it (the Retry-After test only sets a non-200 team endpoint, whose branch at :145 uses the clean header), but setting modelsRetryAfter with a non-200 personal catalog (modelsStatus) — a very natural next test — crashes the fixture. The malformed entry is also dead weight; keep only "retry-after".

3. Stored last-good catalog is cast without revalidation (robustness)

packages/core/src/kilocode/gateway.ts (decodeStoredModels → decoded as readonly KiloModels.Model[], consumed by toModelInfo). The persisted fallback is only JSON-parsed and array-checked, never shape-validated. If the KiloModels.Model shape changes across app versions and the Gateway is unreachable (exactly when this path is used), toModelInfo throws inside the provider transform; per packages/core/src/state.ts:176-183 a groupless transform failure rethrows, breaking the state rebuild until restart. A tolerant per-model validation (drop invalid entries) would contain this.

4. Successful-but-empty catalog indistinguishable from failure (minor)

packages/core/src/kilocode/gateway.ts provider transform: when the fetch succeeds with models.length === 0, catalog.failed is false but the provider is still rendered with the __kilo_catalog_unavailable__ sentinel (RPC models meanwhile returns [] successfully). A legitimately empty team allow-list presents as "catalog unavailable" in the UI while the RPC reports success — mixed semantics worth either treating as failure uniformly or rendering an empty available list.

Everything else checks out against the repo at the PR head: KiloHeaders.HEADER_ORGANIZATION, Semaphore.makeUnsafe, Effect.catch, Schema.fromJsonString, Model.small family list ("gpt-luna" is indeed the top small family in packages/core/src/model.ts:275), Provider.Editor.add/sourceConnection sentinel exclusion semantics, ConfigProviderPlugin running after provider plugins in post (so user-configured default_model still wins over the gateway default), credential event type names, and the 401→anonymous fallback keyed correctly per scope (token hash, no token in storage keys).

Base automatically changed from gateway-login-v2 to gateway-plan October 15, 2026 11:17
@marius-kilocode
marius-kilocode changed the base branch from gateway-plan to gateway-login-v2 October 15, 2026 11:27
@marius-kilocode

Copy link
Copy Markdown
Collaborator Author

@WebReflection 1 and 2 are fixed. 3 and 4 I decided not to over-engineer now. Thanks.

@marius-kilocode
marius-kilocode merged commit 5777419 into gateway-login-v2 Oct 15, 2026
11 checks passed
@marius-kilocode
marius-kilocode deleted the gateway-catalog branch October 15, 2026 12:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:cli CLI, TUI and core engine area:gateway Kilo Gateway and auth

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants