Skip to content

feat(ai-gateway): restrict custom LLMs to allowed country codes#4288

Open
chrarnoldus wants to merge 1 commit into
mainfrom
session/agent_ff3eefa2-9dfd-462f-84e7-8f199d91dd5b
Open

feat(ai-gateway): restrict custom LLMs to allowed country codes#4288
chrarnoldus wants to merge 1 commit into
mainfrom
session/agent_ff3eefa2-9dfd-462f-84e7-8f199d91dd5b

Conversation

@chrarnoldus

Copy link
Copy Markdown
Contributor

Summary

Adds an optional country_codes allow-list to the custom LLM definition (CustomLlmDefinitionSchema), letting admins limit a custom LLM to requests originating from specific Vercel country codes.

  • country_codes is an optional array of ISO 3166-1 alpha-2 codes (e.g. ["US", "GB"]), validated as 2-character strings. It lives in the existing JSONB definition column of custom_llm2, so no DB migration is required; the admin JSON editor validates it via deepStrict(CustomLlmDefinitionSchema).
  • Enforcement happens at the LLM request level in checkCustomLlm (get-provider.ts), using the request's x-vercel-ip-country header (new clientCountry input threaded through both getProvider call sites in the route handler). Matching is case-insensitive and trims whitespace.
  • An absent/empty list leaves the model unrestricted. A non-empty list fails closed: requests whose country is not in the list — or whose country cannot be determined — are rejected with a new 403 country_not_allowed response (new GetProviderResult variant { kind: 'forbidden' } and customLlmCountryNotAllowedResponse helper + country_not_allowed proxy error type).
  • The country check runs after the existing organization_ids check, so it only applies to orgs already permitted to use the model. Model-list output (listAvailableCustomLlms) is intentionally unchanged.

Verification

  • Not manually tested end-to-end in this environment (no local Postgres/Vercel runtime available in the sandbox). The web Jest suite could not be executed here because its global setup provisions a Postgres test database and Docker is unavailable; please run pnpm test in an environment with the test DB.

Visual Changes

N/A — admin custom-LLM editor already uses a raw JSON editor, so the new optional field is editable without UI changes.

Reviewer Notes

  • Fail-closed behavior (deny when x-vercel-ip-country is absent but a list is configured) is intentional to prevent header absence from bypassing the restriction. If an allow-list-all-when-unknown preference is desired instead, flag it for review.
  • country_codes is access control metadata configured by admins, not user PII, and is not persisted as a new column/table, so the GDPR soft-delete flow is unaffected.
  • The new country_not_allowed error type is additive to the zod enum; older clients that don't recognize it will fall back to a generic error message.

Add an optional `country_codes` allow-list (ISO 3166-1 alpha-2) to the
custom LLM definition so admins can limit a custom LLM to requests from
specific Vercel country codes. When the list is non-empty, requests whose
resolved `x-vercel-ip-country` is not in the list are rejected at the LLM
request level with a 403 `country_not_allowed`; an absent/empty list
leaves the model unrestricted, and a configured list fails closed when
the request country is unknown.

No DB migration: the field lives in the existing JSONB `definition`
column and is validated by `CustomLlmDefinitionSchema`.

Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
@chrarnoldus chrarnoldus self-assigned this Jun 27, 2026
if (!normalizedRequestCountry) {
return false;
}
const normalizedAllowed = new Set(allowedCountries.map(code => code.toUpperCase()));

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.

WARNING: Allow-list entries are not trimmed before comparison

requestCountry is normalized with trim().toUpperCase(), but each country_codes entry is only uppercased here. A configured value like " US " or the new whitespace test added in this PR will still miss and incorrectly return forbidden, so the implementation doesn't match the documented behavior.

Suggested change
const normalizedAllowed = new Set(allowedCountries.map(code => code.toUpperCase()));
const normalizedAllowed = new Set(allowedCountries.map(code => code.trim().toUpperCase()));

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

// `x-vercel-ip-country` request header. When non-empty, requests whose
// resolved country is not in the list are rejected at the LLM request level.
// Case-insensitive at enforcement; entries are validated as 2 characters.
export const CustomLlmCountryCodesSchema = z.array(z.string().length(2));

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.

WARNING: country_codes validation accepts non-country values

The admin JSON editor validates through this schema, so entries like " U", "1*", or other non-letter pairs currently pass even though they are not valid Vercel country codes. Those values will later fail closed in isCountryAllowed and can block every request for a model by mistake. Restricting this to two letters keeps the case-insensitive behavior without accepting malformed codes.

Suggested change
export const CustomLlmCountryCodesSchema = z.array(z.string().length(2));
export const CustomLlmCountryCodesSchema = z.array(z.string().regex(/^[A-Za-z]{2}$/));

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: 2 Issues Found | Recommendation: Address before merge

Executive Summary

The new country-based custom LLM gate has two contract mismatches in validation/normalization that can either reject valid-looking allow-lists or let malformed country codes block traffic unexpectedly.

Overview

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

WARNING

File Line Issue
apps/web/src/lib/ai-gateway/custom-llm/country-access.ts 28 Allow-list entries are uppercased but not trimmed, so the implementation disagrees with the new documented/tested whitespace-tolerant behavior.
packages/db/src/schema-types.ts 1523 country_codes accepts any two characters, which allows malformed non-country values to pass admin validation and later fail closed at request time.

Fix these issues in Kilo Cloud

Files Reviewed (8 files)
  • apps/web/src/app/api/openrouter/[...path]/route.test.ts - 0 issues
  • apps/web/src/app/api/openrouter/[...path]/route.ts - 0 issues
  • apps/web/src/lib/ai-gateway/custom-llm/country-access.test.ts - 0 issues
  • apps/web/src/lib/ai-gateway/custom-llm/country-access.ts - 1 issue
  • apps/web/src/lib/ai-gateway/llm-proxy-helpers.ts - 0 issues
  • apps/web/src/lib/ai-gateway/providers/get-provider.ts - 0 issues
  • apps/web/src/lib/proxy-error-types.ts - 0 issues
  • packages/db/src/schema-types.ts - 1 issue

Reviewed by gpt-5.4-20260305 · Input: 119.8K · Output: 13.4K · Cached: 941.8K

Review guidance: REVIEW.md from base branch main

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