Skip to content

feat(ai-gateway): custom auto routers on the gateway path - #7809

Merged
iscekic merged 64 commits into
mainfrom
feat/auto-custom-routers-gateway
Oct 19, 2026
Merged

iscekic merged 64 commits into
mainfrom
feat/auto-custom-routers-gateway

Conversation

@iscekic

@iscekic iscekic commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Gateway slice of ADR 0005. Stacked on #7791. The branch also merges #7790, because it reuses that PR's custom router reads.

Gateway path for custom routers

  • kilo-auto/custom/<slug> and kilo-auto/custom/org/<slug> are now auto models in the LLM proxy. The proxy resolves them like this:

    • A personal router belongs to the authenticated user. An organization router needs the active organization context.
    • A missing router gets router_not_found (404), using the contract error codes and statuses. The gateway does not check the kilo-auto-custom-routers flag: only flagged owners can create routers, so a router's existence is the gate on both the gateway and CLI paths (ADR 0005).
  • The proxy calls the internal /decide with:

    • the customRouter reference,
    • the router's base router,
    • the custom model ID as requestedModel,
    • the organization's denied gateway entries.

    It sends no clientEntries.

  • The worker handles null decisions (the default member, or custom_router_unavailable). The gateway serves exactly the gateway entry that the worker returns, with its exact catalog variant. It never falls back outside the pool:

    • A worker that is unreachable, times out, returns a 5xx, or returns an invalid body gets a retryable auto_routing_unavailable (503 with Retry-After).
    • So does a null decision, and so does a pick that is outside the pool, denied by the policy, or a client entry.
    • Worker 4xx rejections pass through with their contract status.
  • Usage rows record the custom router model ID as auto_model. Classifier billing stays on the worker-owned path: the decide request names the custom model ID, and the legacy positive-cost fallback is unchanged.

Organization Auto targets

  • A kilo-auto/org route or fallback target can name kilo-auto/custom/org/<slug>, but only for a router owned by the same organization. The target is resolved through the custom router path above. Targets that name a personal router are rejected, both by validation and by the stored-settings schema.
  • The Organization Auto model pickers list the organization's custom routers.

Decision header

  • A request that carries x-kilo-auto-decision is checked with verifyAutoDecision and AUTO_ROUTING_DECISION_SECRET. That secret is a new gateway env var, documented in ENVIRONMENT.md; it must match the worker's secret.
  • The token's user, organization, and model must match the request. The gateway then serves the signed variant and sets auto_model to the token's router model ID.
  • An invalid token gets a 4xx invalid_auto_decision error with a reason:
    • 400 for a malformed token,
    • 403 for a bad signature, an expired token, or a mismatched user, organization, model, or variant.
  • If the header is present and the gateway has no secret, the request gets a 503. The header is never silently ignored.

Organization policy for auto candidates

  • Auto Frontier evaluates organization policy against the Frontier routing table. Before this change, the gateway built deniedModelIds from the Efficient table or the custom pool, so Frontier could pick a blocked model that the gateway then rejected.
  • The routing table cache keeps separate Efficient and Frontier entries. Only Efficient reads the saved custom pool.
  • Settings-based deny lists expand to the exact custom router candidate IDs, so a suffixed variant of a denied model is denied too.
  • The final gateway authorization check is unchanged.
  • The organization models endpoint lists custom routers owned by the requested organization only, so both Organization Auto pickers show them.

Deploy notes

  • Set AUTO_ROUTING_DECISION_SECRET on the kilocode-ai-gateway project with the same value as the worker's AUTO_ROUTING_DECISION_SECRET_PROD, before CLIs send the header.

Verification

On a work VM:

  • pnpm typecheck passes in packages/web-shared, packages/db, apps/ai-gateway, and apps/web.
  • These Jest suites pass:
    • web-shared auto-model/**, including the new custom-router and auto-decision tests,
    • auto-routing-decision, auto-routing-denied-models, handlers/llm-proxy, llm-proxy-helpers, and handlers/models,
    • apps/web api/auto-routing/custom-routers, organization-modes-router, organization-settings-router, and openrouter-models.
  • oxfmt and oxlint pass on the changed files.
  • After the policy changes: auto-routing-denied-models, handlers/llm-proxy, auto-routing-models, handlers/models, handlers/organization-models, and organization-settings-router pass; packages/web-shared typecheck and oxlint pass.

Adds ADR 0005 shared contracts: custom router model IDs, pool entry
schemas, the public decide API, client eval protocol schemas, web REST
schemas, and the signed auto-decision token. AutoRoutingModeSchema moves
next to AutoRouterSchema so the new module does not import the package
entry.
Inside the accuracy band a candidate whose estimated request cost is
exactly 0 outranks every paid candidate and drops the cost term
(ADR 0005), ordered by effectiveLatency^1.5 / wilson^15. Paid ordering
is unchanged. Bumps RANKING_POLICY_VERSION so tables republish.
Stores ADR 0005 custom router definitions with an XOR owner check,
one unique slug per owner, and entries typed by the contracts pool
entry union. Soft delete removes a user's own routers.
Consumers that typecheck packages/db source would otherwise pull in the
contracts source, which needs a newer TypeScript lib than some of them
set (services/ai-attribution failed on toSorted). The jsonb entry union
mirrors the contracts shape, so call sites that pass contracts values
still fail to compile if the shape drifts.
…ical

An entries update must now send defaultEntryKey so the stored default
cannot fall outside the new pool, and gateway model IDs must not start
with the reserved client: prefix so a candidate model string maps to
exactly one entry.
ADR 0005. The auto-routing worker reads custom router definitions from
PostgreSQL through Hyperdrive with an isolate cache and decides within the
router's pool:

- POST /v1/decide authenticates the caller's Kilo token, resolves the router
  by scope or organization context, checks membership and the paying owner's
  balance, decides, and returns a signed decision ID.
- POST /v1/eval/{plan,case,result} check router editor rights and the owner's
  balance, then forward the router context and grader billing to the
  benchmark worker.
- The internal /decide accepts an optional customRouter reference and
  returns entry, stale, and fallback; platform requests are unchanged.
- Null decisions use the default entry, then the first servable entry, and
  fail with custom_router_unavailable otherwise.

Token pepper, organization context, membership, member balance, the paid
usage balance gate, and the enterprise deny list move to
@kilocode/worker-utils/account-access; the gateway and web callers use it.
…domains on the CLI path

The public decide route now asks the AI gateway which of a router's gateway
entries the organization model policy denies the member. A new internal
gateway route runs the gateway's own evaluation (group policy grants,
provider limits, and the data-collection rule) for named candidates, so the
CLI path and the gateway share one implementation. The worker caches the
answer for a minute and denies every gateway entry when the gateway cannot
answer.

Blocked email domain parsing and matching move to
@kilocode/worker-utils/account-access. Every public worker route rejects a
user in a blocked domain, read from the same Upstash key the web app uses.
…isions

Serve kilo-auto/custom/<slug> and kilo-auto/custom/org/<slug> through the
internal /decide with the router reference and gateway entries only. A worker
outage, a null decision, or a pick outside the pool fails the request instead
of falling back to another model. Organization Auto routes may target an
organization custom router. Requests carrying x-kilo-auto-decision are
verified with AUTO_ROUTING_DECISION_SECRET and attributed to the router.
Only flagged owners can create custom routers, so the gateway resolves a
router without a PostHog check, matching the auto-routing worker (ADR 0005).
…ve-ready

# Conflicts:
#	docs/adr/0005-custom-auto-routers.md
#	packages/auto-routing-contracts/src/custom-router.ts
Base automatically changed from feat/auto-custom-routers-serve to main October 19, 2026 02:02
…to fix/custom-router-gateway-ready

# Conflicts:
#	apps/web/src/app/api/auto-routing/custom-routers/route.test.ts
#	apps/web/src/lib/auto-routing-custom-routers.ts
#	packages/web-shared/src/lib/ai-gateway/auto-model/index.ts
#	packages/web-shared/src/lib/ai-gateway/auto-routing-benchmark-admin-client.test.ts
#	packages/web-shared/src/lib/ai-gateway/auto-routing-benchmark-admin-client.ts
#	packages/web-shared/src/lib/ai-gateway/custom-routers.ts
…eway-ready

# Conflicts:
#	apps/web/src/lib/auto-routing-custom-routers.ts
#	packages/web-shared/src/lib/ai-gateway/auto-model/index.ts
#	packages/web-shared/src/lib/ai-gateway/custom-routers.ts
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.

2 participants