From c950d80eeef59824e370acf2f677ae62a9a3cff1 Mon Sep 17 00:00:00 2001 From: romel lauron Date: Fri, 6 Mar 2026 23:11:58 +0800 Subject: [PATCH 01/55] feat: add provider region support for Alibaba Co-Authored-By: Claude Sonnet 4.6 --- apps/gateway/src/chat/chat.ts | 9 + .../chat/tools/resolve-provider-context.ts | 35 + apps/gateway/src/lib/costs.ts | 17 +- .../create-provider-key-dialog.tsx | 56 +- ...26-03-06-provider-region-support-design.md | 128 ++++ .../2026-03-06-provider-region-support.md | 460 +++++++++++++ packages/actions/src/get-provider-endpoint.ts | 21 +- packages/db/src/schema.ts | 1 + packages/models/src/models.ts | 37 ++ packages/models/src/models/alibaba.ts | 629 ++++++++++++++++++ packages/models/src/providers.ts | 34 + 11 files changed, 1419 insertions(+), 8 deletions(-) create mode 100644 docs/plans/2026-03-06-provider-region-support-design.md create mode 100644 docs/plans/2026-03-06-provider-region-support.md diff --git a/apps/gateway/src/chat/chat.ts b/apps/gateway/src/chat/chat.ts index 7a3a16d69a..143afb4725 100644 --- a/apps/gateway/src/chat/chat.ts +++ b/apps/gateway/src/chat/chat.ts @@ -1384,6 +1384,7 @@ chat.openapi(completions, async (c) => { let usedToken: string | undefined; let configIndex = 0; // Index for round-robin environment variables let envVarName: string | undefined; // Environment variable name for health tracking + let usedRegion: string | undefined; if ( project.mode === "credits" && @@ -1742,6 +1743,7 @@ chat.openapi(completions, async (c) => { inputImageCount, null, // webSearchCount project.organizationId, + usedRegion ?? null, ); await insertLog({ @@ -1892,6 +1894,7 @@ chat.openapi(completions, async (c) => { inputImageCount, null, // webSearchCount project.organizationId, + usedRegion ?? null, ); // Estimate cached response size based on content to avoid expensive stringify @@ -2585,6 +2588,7 @@ chat.openapi(completions, async (c) => { inputImageCount, webSearchTool ? 1 : null, // Bill for web search if it was enabled project.organizationId, + usedRegion ?? null, ); } @@ -3591,6 +3595,7 @@ chat.openapi(completions, async (c) => { inputImageCount, webSearchCount, project.organizationId, + usedRegion ?? null, ); // Include costs in response for all users @@ -4641,6 +4646,7 @@ chat.openapi(completions, async (c) => { inputImageCount, webSearchCount, project.organizationId, + usedRegion ?? null, ); // Use costs.promptTokens as canonical value (includes image input @@ -4962,6 +4968,7 @@ chat.openapi(completions, async (c) => { top_p = ctx.top_p; frequency_penalty = ctx.frequency_penalty; presence_penalty = ctx.presence_penalty; + usedRegion = ctx.usedRegion; } catch { failedProviderIds.add(nextProvider.providerId); // Don't consume a retry slot for context-resolution failures @@ -5221,6 +5228,7 @@ chat.openapi(completions, async (c) => { inputImageCount, webSearchTool ? 1 : null, // Bill for web search if it was enabled project.organizationId, + usedRegion ?? null, ); } @@ -5951,6 +5959,7 @@ chat.openapi(completions, async (c) => { inputImageCount, webSearchCount, project.organizationId, + usedRegion ?? null, ); // Use costs.promptTokens as canonical value (includes image input diff --git a/apps/gateway/src/chat/tools/resolve-provider-context.ts b/apps/gateway/src/chat/tools/resolve-provider-context.ts index ea122bb082..2f99f03fbe 100644 --- a/apps/gateway/src/chat/tools/resolve-provider-context.ts +++ b/apps/gateway/src/chat/tools/resolve-provider-context.ts @@ -12,11 +12,13 @@ import { } from "@llmgateway/actions"; import { type BaseMessage, + getProviderEnvValue, hasMaxTokens, type ModelDefinition, type OpenAIRequestBody, type OpenAIToolInput, type Provider, + type ProviderDefinition, type ProviderModelMapping, type ProviderRequestBody, providers, @@ -50,6 +52,7 @@ export interface ProviderContext { frequency_penalty: number | undefined; presence_penalty: number | undefined; headers: Record; + usedRegion: string | undefined; } export interface OriginalRequestParams { @@ -179,6 +182,37 @@ export async function resolveProviderContext( (p) => p.providerId === usedProvider && p.modelName === usedModel, ); + // --- Region resolution --- + const providerDef = providers.find((p) => p.id === usedProvider) as + | ProviderDefinition + | undefined; + const regionConfig = providerDef?.regionConfig; + let usedRegion: string | undefined; + if (regionConfig) { + const optionsKey = regionConfig.optionsKey; + const keyOptions = providerKey?.options as + | Record + | null + | undefined; + usedRegion = + keyOptions?.[optionsKey] ?? + getProviderEnvValue(usedProvider as Provider, "region", configIndex) ?? + regionConfig.defaultRegion; + + // Validate that the selected region is supported by this model mapping + const modelRegions = (providerMappingForSelected as ProviderModelMapping) + ?.regions; + if (modelRegions && modelRegions.length > 0) { + const isValidRegion = modelRegions.some((r) => r.id === usedRegion); + if (!isValidRegion) { + const validIds = modelRegions.map((r) => r.id).join(", "); + throw new HTTPException(400, { + message: `Model "${baseModelName}" is not available in region "${usedRegion}". Available regions: ${validIds}`, + }); + } + } + } + // --- Check if model supports reasoning (from selected provider, not any) --- const supportsReasoning = (providerMappingForSelected as ProviderModelMapping)?.reasoning === true; @@ -361,5 +395,6 @@ export async function resolveProviderContext( frequency_penalty, presence_penalty, headers, + usedRegion, }; } diff --git a/apps/gateway/src/lib/costs.ts b/apps/gateway/src/lib/costs.ts index c87a87d757..3ef948145d 100644 --- a/apps/gateway/src/lib/costs.ts +++ b/apps/gateway/src/lib/costs.ts @@ -8,6 +8,7 @@ import { models, type PricingTier, type ToolCall, + type ProviderRegion, } from "@llmgateway/models"; // Define ChatMessage type to match what gpt-tokenizer expects @@ -100,6 +101,7 @@ export async function calculateCosts( inputImageCount = 0, webSearchCount: number | null = null, organizationId: string | null = null, + region: string | null = null, ) { // Find the model info - try both base model name and provider model name let modelInfo = models.find((m) => m.id === model) as ModelDefinition; @@ -249,12 +251,19 @@ export async function calculateCosts( }; } + // Resolve region-specific pricing when available + let regionPricing: ProviderRegion | undefined; + if (region && providerInfo.regions && providerInfo.regions.length > 0) { + regionPricing = providerInfo.regions.find((r) => r.id === region); + } + // Get pricing based on token count (supports tiered pricing) + // Region-specific pricing takes precedence over mapping-level pricing const pricing = getPricingForTokenCount( - providerInfo.pricingTiers, - providerInfo.inputPrice ?? 0, - providerInfo.outputPrice ?? 0, - providerInfo.cachedInputPrice, + regionPricing?.pricingTiers ?? providerInfo.pricingTiers, + regionPricing?.inputPrice ?? providerInfo.inputPrice ?? 0, + regionPricing?.outputPrice ?? providerInfo.outputPrice ?? 0, + regionPricing?.cachedInputPrice ?? providerInfo.cachedInputPrice, calculatedPromptTokens, ); diff --git a/apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx b/apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx index 663549515b..329d9d8a1a 100644 --- a/apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx +++ b/apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx @@ -25,7 +25,11 @@ import { import { toast } from "@/lib/components/use-toast"; import { useApi } from "@/lib/fetch-client"; -import { providers, type ProviderDefinition } from "@llmgateway/models"; +import { + providers, + type ProviderDefinition, + type ProviderRegionConfig, +} from "@llmgateway/models"; import { ProviderSelect } from "./provider-select"; @@ -72,6 +76,7 @@ export function CreateProviderKeyDialog({ >("ai-foundry"); const [azureValidationModel, setAzureValidationModel] = useState("gpt-4o-mini"); + const [selectedRegion, setSelectedRegion] = useState(""); const [isValidating, setIsValidating] = useState(false); const api = useApi(); @@ -161,6 +166,7 @@ export function CreateProviderKeyDialog({ azure_api_version?: string; azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; + [key: string]: string | undefined; }; organizationId: string; } = { @@ -179,6 +185,17 @@ export function CreateProviderKeyDialog({ aws_bedrock_region_prefix: awsBedrockRegionPrefix, }; } + // Include region in options for providers that support it + const providerDef = providers.find((p) => p.id === selectedProvider) as + | ProviderDefinition + | undefined; + if (providerDef?.regionConfig && selectedRegion) { + payload.options = { + ...payload.options, + [providerDef.regionConfig.optionsKey]: selectedRegion, + }; + } + if (selectedProvider === "azure") { if (!azureResource) { toast({ @@ -416,6 +433,43 @@ export function CreateProviderKeyDialog({ )} + {(() => { + const providerWithRegion = providers.find( + (p) => p.id === selectedProvider, + ) as ProviderDefinition | undefined; + const rc = providerWithRegion?.regionConfig as + | ProviderRegionConfig + | undefined; + if (!rc) { + return null; + } + const effectiveRegion = selectedRegion || rc.defaultRegion; + return ( +
+ + +

+ API keys are region-specific. Make sure your key matches the + selected region. +

+
+ ); + })()} + {selectedProvider === "custom" && ( <>
diff --git a/docs/plans/2026-03-06-provider-region-support-design.md b/docs/plans/2026-03-06-provider-region-support-design.md new file mode 100644 index 0000000000..5d3ca1b571 --- /dev/null +++ b/docs/plans/2026-03-06-provider-region-support-design.md @@ -0,0 +1,128 @@ +# Provider Region Support Design + +## Problem + +Some cloud providers host AI models in specific geographic regions with different API endpoints. Alibaba Cloud's DashScope has 3 regional endpoints (Singapore, US Virginia, China Beijing), each requiring region-specific API keys. Currently, the gateway hardcodes `dashscope-intl.aliyuncs.com` (Singapore). To support models that require different regions, we need a general region abstraction. + +This also applies to AWS Bedrock (which has a hacky region prefix system today) and could extend to other providers in the future. + +## Approach + +Add a `regions` array to `ProviderModelMapping` that declares which regions each model supports. The actual region selection happens at runtime via environment variables (credits mode) or provider key options (api-keys mode). + +## Data Model Changes + +### `ProviderModelMapping` (packages/models/src/models.ts) + +New optional field: + +```typescript +/** Available regions for this model/provider combination. + * When set, the gateway uses the configured region (env var or provider key) + * and validates it against this list. + * When unset, the provider uses its default endpoint. */ +regions?: string[]; +``` + +### `ProviderDefinition` (packages/models/src/providers.ts) + +Add optional region env var to Alibaba's config: + +```typescript +{ + id: "alibaba", + env: { + required: { apiKey: "LLM_ALIBABA_API_KEY" }, + optional: { region: "LLM_ALIBABA_REGION" }, + }, +} +``` + +### `ProviderKeyOptions` (packages/db) + +Add Alibaba region for user-managed provider keys: + +```typescript +export interface ProviderKeyOptions { + // ...existing fields + alibaba_region?: string; +} +``` + +### Alibaba Model Definitions (packages/models/src/models/alibaba.ts) + +Add `regions` to each `providerId: "alibaba"` mapping: + +```typescript +{ + providerId: "alibaba", + modelName: "qwen-plus", + regions: ["singapore", "us-virginia", "cn-beijing"], + // ...rest unchanged +} +``` + +## Endpoint URL Resolution + +In `packages/actions/src/get-provider-endpoint.ts`, the Alibaba case changes from a hardcoded URL to region-aware resolution: + +```typescript +case "alibaba": { + const region = + providerKeyOptions?.alibaba_region ?? + getProviderEnvValue("alibaba", "region", configIndex, "singapore") ?? + "singapore"; + + const baseUrls: Record = { + "singapore": "https://dashscope-intl.aliyuncs.com", + "us-virginia": "https://dashscope-us.aliyuncs.com", + "cn-beijing": "https://dashscope.aliyuncs.com", + }; + + if (imageGenerations) { + url = baseUrls[region] ?? baseUrls["singapore"]; + } else { + url = `${baseUrls[region] ?? baseUrls["singapore"]}/compatible-mode`; + } + break; +} +``` + +## Request-Time Validation + +In `apps/gateway/src/chat/tools/resolve-provider-context.ts`, after the provider mapping is found, validate the configured region: + +```typescript +if (providerMapping.regions && providerMapping.regions.length > 0) { + const configuredRegion = /* resolve from providerKeyOptions or env */; + if (configuredRegion && !providerMapping.regions.includes(configuredRegion)) { + throw new HTTPException(400, { + message: `Model ${model} is not available in region ${configuredRegion}. Available regions: ${providerMapping.regions.join(", ")}`, + }); + } +} +``` + +## UI Changes + +In `apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx`, add a region dropdown for Alibaba provider keys (same pattern as AWS Bedrock region prefix and Azure resource): + +- State: `const [alibabaRegion, setAlibabaRegion] = useState("singapore")` +- Dropdown options: Singapore (default), US (Virginia), China (Beijing) +- Saved to `payload.options.alibaba_region` + +## Files to Modify + +1. `packages/models/src/models.ts` - Add `regions?` to `ProviderModelMapping` +2. `packages/models/src/providers.ts` - Add optional region env var to Alibaba +3. `packages/models/src/models/alibaba.ts` - Add `regions` to all Alibaba provider mappings +4. `packages/db` - Add `alibaba_region` to `ProviderKeyOptions` +5. `packages/actions/src/get-provider-endpoint.ts` - Region-aware URL resolution for Alibaba +6. `apps/gateway/src/chat/tools/resolve-provider-context.ts` - Region validation at request time +7. `apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx` - Alibaba region dropdown +8. `apps/ui/src/lib/api/v1.d.ts` - Update API types if needed + +## Future Work + +- Migrate AWS Bedrock from the hacky `aws_bedrock_region_prefix` to use the same `regions` pattern +- Add region support to other providers as needed (e.g., Google Vertex could use this instead of its custom env var) diff --git a/docs/plans/2026-03-06-provider-region-support.md b/docs/plans/2026-03-06-provider-region-support.md new file mode 100644 index 0000000000..8ab1166fdb --- /dev/null +++ b/docs/plans/2026-03-06-provider-region-support.md @@ -0,0 +1,460 @@ +# Provider Region Support Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add per-model region support so the gateway can route Alibaba Cloud requests to the correct regional endpoint (Singapore, US Virginia, or China Beijing) based on env vars or provider key options. + +**Architecture:** Add `regions?: string[]` to `ProviderModelMapping` to declare region availability per model. Region selection happens at runtime via `LLM_ALIBABA_REGION` env var (credits mode) or `alibaba_region` in `ProviderKeyOptions` (api-keys mode). The gateway validates the configured region against the model's `regions` array and constructs the correct DashScope regional URL. + +**Tech Stack:** TypeScript, Drizzle ORM, Hono, Next.js (React), Radix UI + +--- + +### Task 1: Add `regions` field to `ProviderModelMapping` + +**Files:** +- Modify: `packages/models/src/models.ts:176-182` + +**Step 1: Add the field** + +Add before the closing `}` of `ProviderModelMapping` (after `imageGenerations`): + +```typescript + /** + * Available regions for this model/provider combination. + * When set, the gateway validates the configured region against this list. + * When unset, the provider uses its default endpoint. + */ + regions?: string[]; +``` + +**Step 2: Verify build** + +Run: `pnpm build --filter @llmgateway/models` +Expected: SUCCESS (new optional field, no breaking changes) + +**Step 3: Commit** + +```bash +git add packages/models/src/models.ts +git commit -m "feat: add regions field to ProviderModelMapping" +``` + +--- + +### Task 2: Add optional region env var to Alibaba provider definition + +**Files:** +- Modify: `packages/models/src/providers.ts:198-213` + +**Step 1: Add optional region to Alibaba env config** + +Change the Alibaba provider definition from: + +```typescript + env: { + required: { + apiKey: "LLM_ALIBABA_API_KEY", + }, + }, +``` + +To: + +```typescript + env: { + required: { + apiKey: "LLM_ALIBABA_API_KEY", + }, + optional: { + region: "LLM_ALIBABA_REGION", + }, + }, +``` + +**Step 2: Verify build** + +Run: `pnpm build --filter @llmgateway/models` +Expected: SUCCESS + +**Step 3: Commit** + +```bash +git add packages/models/src/providers.ts +git commit -m "feat: add LLM_ALIBABA_REGION env var" +``` + +--- + +### Task 3: Add `alibaba_region` to `ProviderKeyOptions` + +**Files:** +- Modify: `packages/db/src/schema.ts:388-394` + +**Step 1: Add the field** + +Change `ProviderKeyOptions` from: + +```typescript +export interface ProviderKeyOptions { + aws_bedrock_region_prefix?: "us." | "global." | "eu."; + azure_resource?: string; + azure_api_version?: string; + azure_deployment_type?: "openai" | "ai-foundry"; + azure_validation_model?: string; +} +``` + +To: + +```typescript +export interface ProviderKeyOptions { + aws_bedrock_region_prefix?: "us." | "global." | "eu."; + azure_resource?: string; + azure_api_version?: string; + azure_deployment_type?: "openai" | "ai-foundry"; + azure_validation_model?: string; + alibaba_region?: string; +} +``` + +**Step 2: Verify build** + +Run: `pnpm build --filter @llmgateway/db` +Expected: SUCCESS (JSONB field, no migration needed) + +**Step 3: Commit** + +```bash +git add packages/db/src/schema.ts +git commit -m "feat: add alibaba_region to ProviderKeyOptions" +``` + +--- + +### Task 4: Add `regions` to Alibaba model definitions + +**Files:** +- Modify: `packages/models/src/models/alibaba.ts` + +**Step 1: Add `regions` to every `providerId: "alibaba"` mapping** + +For every provider mapping in `alibaba.ts` that has `providerId: "alibaba"`, add: + +```typescript +regions: ["singapore", "us-virginia", "cn-beijing"], +``` + +This applies to the following models (all with `providerId: "alibaba"`): +- qwen-max +- qwen-max-latest +- qwen-plus +- qwen-plus-latest +- qwen-flash +- qwen-omni-turbo +- qwen-turbo +- qwen3-coder-plus +- qwen-vl-max +- qwen-vl-plus +- qwen3-next-80b-a3b-thinking +- qwen3-next-80b-a3b-instruct +- qwen3-max (uses modelName "qwen3-max-preview") +- qwen3-coder-flash +- qwen3-vl-plus +- qwen3-vl-flash +- qwen3-vl-235b-a22b-instruct +- qwen3-vl-235b-a22b-thinking +- qwen2-5-vl-32b-instruct +- qwen3-max-2026-01-23 +- qwq-plus +- qwen-coder-plus +- qwen35-397b-a17b +- qwen-image-plus +- qwen-image-max +- qwen-image +- qwen-image-max-2025-12-30 +- qwen-image-edit-plus +- qwen-image-edit-max + +Do NOT add `regions` to mappings with other `providerId` values (nebius, novita, cerebras, canopywave). + +Place the `regions` field right after `providerId` and `modelName` for readability. Example: + +```typescript +{ + providerId: "alibaba", + modelName: "qwen-plus", + regions: ["singapore", "us-virginia", "cn-beijing"], + discount: 0.2, + // ...rest +} +``` + +**Step 2: Verify build** + +Run: `pnpm build --filter @llmgateway/models` +Expected: SUCCESS + +**Step 3: Commit** + +```bash +git add packages/models/src/models/alibaba.ts +git commit -m "feat: add regions to Alibaba model definitions" +``` + +--- + +### Task 5: Region-aware endpoint URL resolution for Alibaba + +**Files:** +- Modify: `packages/actions/src/get-provider-endpoint.ts:102-109` + +**Step 1: Replace the hardcoded Alibaba case** + +Change from: + +```typescript + case "alibaba": + // Use different base URL for image generation vs chat completions + if (imageGenerations) { + url = "https://dashscope-intl.aliyuncs.com"; + } else { + url = "https://dashscope-intl.aliyuncs.com/compatible-mode"; + } + break; +``` + +To: + +```typescript + case "alibaba": { + const alibabaRegion = + providerKeyOptions?.alibaba_region ?? + getProviderEnvValue("alibaba", "region", configIndex, "singapore") ?? + "singapore"; + + const alibabaBaseUrls: Record = { + singapore: "https://dashscope-intl.aliyuncs.com", + "us-virginia": "https://dashscope-us.aliyuncs.com", + "cn-beijing": "https://dashscope.aliyuncs.com", + }; + + const alibabaBase = + alibabaBaseUrls[alibabaRegion] ?? alibabaBaseUrls.singapore; + + if (imageGenerations) { + url = alibabaBase; + } else { + url = `${alibabaBase}/compatible-mode`; + } + break; + } +``` + +**Step 2: Verify build** + +Run: `pnpm build --filter @llmgateway/actions` +Expected: SUCCESS + +**Step 3: Commit** + +```bash +git add packages/actions/src/get-provider-endpoint.ts +git commit -m "feat: region-aware Alibaba endpoint resolution" +``` + +--- + +### Task 6: Region validation in resolve-provider-context + +**Files:** +- Modify: `apps/gateway/src/chat/tools/resolve-provider-context.ts:177-189` + +**Step 1: Add region validation after provider mapping lookup** + +After the existing block at line ~180 (`const providerMappingForSelected = ...`) and before the reasoning check, add: + +```typescript + // --- Region validation --- + const providerMappingRegions = (providerMappingForSelected as ProviderModelMapping)?.regions; + if (providerMappingRegions && providerMappingRegions.length > 0) { + const configuredRegion = + providerKey?.options?.alibaba_region ?? + (usedProvider === "alibaba" + ? getProviderEnvValue("alibaba", "region", configIndex, "singapore") ?? "singapore" + : undefined); + + if (configuredRegion && !providerMappingRegions.includes(configuredRegion)) { + throw new HTTPException(400, { + message: `Model ${usedModel} is not available in region "${configuredRegion}". Available regions: ${providerMappingRegions.join(", ")}`, + }); + } + } +``` + +You'll need to add `getProviderEnvValue` to the imports from `@llmgateway/models` if not already imported. + +**Step 2: Check imports** + +Verify `getProviderEnvValue` is imported. Check the existing imports at the top of the file. If missing, add it to the import from `@llmgateway/models`. + +**Step 3: Verify build** + +Run: `pnpm build --filter gateway` +Expected: SUCCESS + +**Step 4: Commit** + +```bash +git add apps/gateway/src/chat/tools/resolve-provider-context.ts +git commit -m "feat: validate region against model regions" +``` + +--- + +### Task 7: UI - Alibaba region dropdown in provider key dialog + +**Files:** +- Modify: `apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx` + +**Step 1: Add state variable** + +After the existing `azureValidationModel` state (line ~74), add: + +```typescript + const [alibabaRegion, setAlibabaRegion] = useState("singapore"); +``` + +**Step 2: Add `alibaba_region` to payload type** + +In the payload type definition (line ~158), add `alibaba_region` to the `options` type: + +```typescript + options?: { + aws_bedrock_region_prefix?: "us." | "global." | "eu."; + azure_resource?: string; + azure_api_version?: string; + azure_deployment_type?: "openai" | "ai-foundry"; + azure_validation_model?: string; + alibaba_region?: string; + }; +``` + +**Step 3: Add payload construction** + +After the Azure payload block (after line ~197), add: + +```typescript + if (selectedProvider === "alibaba") { + payload.options = { + alibaba_region: alibabaRegion, + }; + } +``` + +**Step 4: Add the dropdown JSX** + +After the AWS Bedrock section (after the closing `)}` at line ~347) and before the Azure section, add: + +```tsx + {selectedProvider === "alibaba" && ( +
+ + +

+ Region for Alibaba Cloud DashScope API. API keys are + region-specific. +

+
+ )} +``` + +**Step 5: Add reset in handleClose** + +In the `handleClose` function (line ~224), add after the `setAzureValidationModel` reset: + +```typescript + setAlibabaRegion("singapore"); +``` + +**Step 6: Verify build** + +Run: `pnpm build --filter ui` +Expected: SUCCESS + +**Step 7: Commit** + +```bash +git add apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx +git commit -m "feat(ui): add Alibaba region dropdown to provider key dialog" +``` + +--- + +### Task 8: Final build and format + +**Step 1: Format all code** + +Run: `pnpm format` + +**Step 2: Full build** + +Run: `pnpm build` +Expected: SUCCESS across all packages + +**Step 3: Run unit tests** + +Run: `pnpm test:unit` +Expected: All tests pass + +**Step 4: Commit any formatting changes** + +```bash +git add -A +git commit -m "chore: format" +``` + +--- + +### Task 9: Update API types (if needed) + +**Files:** +- Check: `apps/ui/src/lib/api/v1.d.ts` + +**Step 1: Check if API types are auto-generated** + +The `v1.d.ts` file may be auto-generated from the API schema. Check if there's a generation command. + +Run: `grep -r "openapi\|generate.*types\|v1.d.ts" package.json packages/*/package.json apps/*/package.json | head -20` + +If auto-generated, run the generation command. If manually maintained, add `alibaba_region?: string` to the relevant options type. + +**Step 2: Verify build** + +Run: `pnpm build` +Expected: SUCCESS + +**Step 3: Commit if changes were needed** + +```bash +git add apps/ui/src/lib/api/v1.d.ts +git commit -m "chore: update API types for alibaba_region" +``` diff --git a/packages/actions/src/get-provider-endpoint.ts b/packages/actions/src/get-provider-endpoint.ts index b70067b540..b512dbbcf0 100644 --- a/packages/actions/src/get-provider-endpoint.ts +++ b/packages/actions/src/get-provider-endpoint.ts @@ -1,5 +1,6 @@ import { models, + providers, type ProviderModelMapping, type ProviderId, getProviderEnvValue, @@ -99,14 +100,28 @@ export function getProviderEndpoint( case "moonshot": url = "https://api.moonshot.ai"; break; - case "alibaba": + case "alibaba": { + const alibabaProviderDef = providers.find((p) => p.id === "alibaba"); + const alibabaRegionConfig = alibabaProviderDef?.regionConfig; + const alibabaRegion = + providerKeyOptions?.alibaba_region ?? + getProviderEnvValue("alibaba", "region", configIndex) ?? + alibabaRegionConfig?.defaultRegion ?? + "singapore"; + const alibabaBaseUrl = + ( + alibabaRegionConfig?.endpointMap as + | Record + | undefined + )?.[alibabaRegion] ?? "https://dashscope-intl.aliyuncs.com"; // Use different base URL for image generation vs chat completions if (imageGenerations) { - url = "https://dashscope-intl.aliyuncs.com"; + url = alibabaBaseUrl; } else { - url = "https://dashscope-intl.aliyuncs.com/compatible-mode"; + url = `${alibabaBaseUrl}/compatible-mode`; } break; + } case "nebius": url = "https://api.studio.nebius.com"; break; diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index 1ff0cf6a4b..a3b4cba5c4 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -391,6 +391,7 @@ export interface ProviderKeyOptions { azure_api_version?: string; azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; + alibaba_region?: string; } export const providerKey = pgTable( diff --git a/packages/models/src/models.ts b/packages/models/src/models.ts index 78e7695029..19b1893941 100644 --- a/packages/models/src/models.ts +++ b/packages/models/src/models.ts @@ -47,6 +47,36 @@ export interface PricingTier { cachedInputPrice?: number; } +/** + * Pricing and availability for a specific geographic region. + * When defined on a ProviderModelMapping, the first entry is the default region. + * Top-level inputPrice/outputPrice always reflect the default (first) region + * for backwards compatibility. + */ +export interface ProviderRegion { + /** + * Region identifier (e.g. "singapore", "us-virginia", "cn-beijing") + */ + id: string; + /** + * Price per input token in USD for this region + */ + inputPrice: number; + /** + * Price per output token in USD for this region + */ + outputPrice: number; + /** + * Price per cached input token in USD for this region + */ + cachedInputPrice?: number; + /** + * Context-length based pricing tiers for this region. + * When absent, falls back to the mapping-level pricingTiers. + */ + pricingTiers?: PricingTier[]; +} + export interface ProviderModelMapping { providerId: (typeof providers)[number]["id"]; modelName: string; @@ -179,6 +209,13 @@ export interface ProviderModelMapping { * When true, requests are routed to a provider-specific image generation endpoint. */ imageGenerations?: boolean; + /** + * Available regions and their per-region pricing for this provider mapping. + * The first entry is the default region. Top-level inputPrice/outputPrice + * must match the first region's prices for backwards compatibility. + * When absent, the provider uses a single global endpoint with no region routing. + */ + regions?: ProviderRegion[]; } export type StabilityLevel = "stable" | "beta" | "unstable" | "experimental"; diff --git a/packages/models/src/models/alibaba.ts b/packages/models/src/models/alibaba.ts index 414dac6e33..6b42e54176 100644 --- a/packages/models/src/models/alibaba.ts +++ b/packages/models/src/models/alibaba.ts @@ -15,6 +15,14 @@ export const alibabaModels = [ discount: 0.2, inputPrice: 1.6 / 1e6, outputPrice: 6.4 / 1e6, + regions: [ + { id: "singapore", inputPrice: 1.6 / 1e6, outputPrice: 6.4 / 1e6 }, + { + id: "cn-beijing", + inputPrice: 0.345 / 1e6, + outputPrice: 1.377 / 1e6, + }, + ], requestPrice: 0, contextSize: 131072, maxOutput: 32000, @@ -39,6 +47,14 @@ export const alibabaModels = [ discount: 0.2, inputPrice: 1.6 / 1e6, outputPrice: 6.4 / 1e6, + regions: [ + { id: "singapore", inputPrice: 1.6 / 1e6, outputPrice: 6.4 / 1e6 }, + { + id: "cn-beijing", + inputPrice: 0.345 / 1e6, + outputPrice: 1.377 / 1e6, + }, + ], requestPrice: 0, contextSize: 131072, maxOutput: 32000, @@ -63,6 +79,74 @@ export const alibabaModels = [ inputPrice: 0.4 / 1e6, outputPrice: 1.2 / 1e6, cachedInputPrice: 0.08 / 1e6, + regions: [ + { + id: "singapore", + inputPrice: 0.4 / 1e6, + outputPrice: 1.2 / 1e6, + cachedInputPrice: 0.08 / 1e6, + pricingTiers: [ + { + name: "Up to 256K", + upToTokens: 256000, + inputPrice: 0.4 / 1e6, + outputPrice: 1.2 / 1e6, + cachedInputPrice: 0.08 / 1e6, + }, + { + name: "Over 256K", + upToTokens: Infinity, + inputPrice: 1.2 / 1e6, + outputPrice: 3.6 / 1e6, + cachedInputPrice: 0.24 / 1e6, + }, + ], + }, + { + id: "us-virginia", + inputPrice: 0.115 / 1e6, + outputPrice: 0.287 / 1e6, + cachedInputPrice: 0.023 / 1e6, + pricingTiers: [ + { + name: "Up to 256K", + upToTokens: 256000, + inputPrice: 0.115 / 1e6, + outputPrice: 0.287 / 1e6, + cachedInputPrice: 0.023 / 1e6, + }, + { + name: "Over 256K", + upToTokens: Infinity, + inputPrice: 0.345 / 1e6, + outputPrice: 0.861 / 1e6, + cachedInputPrice: 0.069 / 1e6, + }, + ], + }, + { + id: "cn-beijing", + inputPrice: 0.115 / 1e6, + outputPrice: 0.287 / 1e6, + cachedInputPrice: 0.023 / 1e6, + pricingTiers: [ + { + name: "Up to 256K", + upToTokens: 256000, + inputPrice: 0.115 / 1e6, + outputPrice: 0.287 / 1e6, + cachedInputPrice: 0.023 / 1e6, + }, + { + name: "Over 256K", + upToTokens: Infinity, + inputPrice: 0.345 / 1e6, + outputPrice: 0.861 / 1e6, + cachedInputPrice: 0.069 / 1e6, + }, + ], + }, + ], pricingTiers: [ { name: "Up to 256K", @@ -103,6 +187,74 @@ export const alibabaModels = [ inputPrice: 0.4 / 1e6, outputPrice: 1.2 / 1e6, cachedInputPrice: 0.08 / 1e6, + regions: [ + { + id: "singapore", + inputPrice: 0.4 / 1e6, + outputPrice: 1.2 / 1e6, + cachedInputPrice: 0.08 / 1e6, + pricingTiers: [ + { + name: "Up to 256K", + upToTokens: 256000, + inputPrice: 0.4 / 1e6, + outputPrice: 1.2 / 1e6, + cachedInputPrice: 0.08 / 1e6, + }, + { + name: "Over 256K", + upToTokens: Infinity, + inputPrice: 1.2 / 1e6, + outputPrice: 3.6 / 1e6, + cachedInputPrice: 0.24 / 1e6, + }, + ], + }, + { + id: "us-virginia", + inputPrice: 0.115 / 1e6, + outputPrice: 0.287 / 1e6, + cachedInputPrice: 0.023 / 1e6, + pricingTiers: [ + { + name: "Up to 256K", + upToTokens: 256000, + inputPrice: 0.115 / 1e6, + outputPrice: 0.287 / 1e6, + cachedInputPrice: 0.023 / 1e6, + }, + { + name: "Over 256K", + upToTokens: Infinity, + inputPrice: 0.345 / 1e6, + outputPrice: 0.861 / 1e6, + cachedInputPrice: 0.069 / 1e6, + }, + ], + }, + { + id: "cn-beijing", + inputPrice: 0.115 / 1e6, + outputPrice: 0.287 / 1e6, + cachedInputPrice: 0.023 / 1e6, + pricingTiers: [ + { + name: "Up to 256K", + upToTokens: 256000, + inputPrice: 0.115 / 1e6, + outputPrice: 0.287 / 1e6, + cachedInputPrice: 0.023 / 1e6, + }, + { + name: "Over 256K", + upToTokens: Infinity, + inputPrice: 0.345 / 1e6, + outputPrice: 0.861 / 1e6, + cachedInputPrice: 0.069 / 1e6, + }, + ], + }, + ], pricingTiers: [ { name: "Up to 256K", @@ -143,6 +295,74 @@ export const alibabaModels = [ inputPrice: 0.05 / 1e6, outputPrice: 0.4 / 1e6, cachedInputPrice: 0.01 / 1e6, + regions: [ + { + id: "singapore", + inputPrice: 0.05 / 1e6, + outputPrice: 0.4 / 1e6, + cachedInputPrice: 0.01 / 1e6, + pricingTiers: [ + { + name: "Up to 256K", + upToTokens: 256000, + inputPrice: 0.05 / 1e6, + outputPrice: 0.4 / 1e6, + cachedInputPrice: 0.01 / 1e6, + }, + { + name: "Over 256K", + upToTokens: Infinity, + inputPrice: 0.25 / 1e6, + outputPrice: 2.0 / 1e6, + cachedInputPrice: 0.05 / 1e6, + }, + ], + }, + { + id: "us-virginia", + inputPrice: 0.022 / 1e6, + outputPrice: 0.216 / 1e6, + cachedInputPrice: 0.0044 / 1e6, + pricingTiers: [ + { + name: "Up to 256K", + upToTokens: 256000, + inputPrice: 0.022 / 1e6, + outputPrice: 0.216 / 1e6, + cachedInputPrice: 0.0044 / 1e6, + }, + { + name: "Over 256K", + upToTokens: Infinity, + inputPrice: 0.11 / 1e6, + outputPrice: 1.08 / 1e6, + cachedInputPrice: 0.022 / 1e6, + }, + ], + }, + { + id: "cn-beijing", + inputPrice: 0.022 / 1e6, + outputPrice: 0.216 / 1e6, + cachedInputPrice: 0.0044 / 1e6, + pricingTiers: [ + { + name: "Up to 256K", + upToTokens: 256000, + inputPrice: 0.022 / 1e6, + outputPrice: 0.216 / 1e6, + cachedInputPrice: 0.0044 / 1e6, + }, + { + name: "Over 256K", + upToTokens: Infinity, + inputPrice: 0.11 / 1e6, + outputPrice: 1.08 / 1e6, + cachedInputPrice: 0.022 / 1e6, + }, + ], + }, + ], pricingTiers: [ { name: "Up to 256K", @@ -205,6 +425,14 @@ export const alibabaModels = [ discount: 0.2, inputPrice: 0.05 / 1e6, outputPrice: 0.2 / 1e6, + regions: [ + { id: "singapore", inputPrice: 0.05 / 1e6, outputPrice: 0.2 / 1e6 }, + { + id: "cn-beijing", + inputPrice: 0.044 / 1e6, + outputPrice: 0.087 / 1e6, + }, + ], requestPrice: 0, contextSize: 1000000, maxOutput: 8192, @@ -1064,6 +1292,19 @@ export const alibabaModels = [ discount: 0.2, inputPrice: 0.6 / 1e6, outputPrice: 3.6 / 1e6, + regions: [ + { id: "singapore", inputPrice: 0.6 / 1e6, outputPrice: 3.6 / 1e6 }, + { + id: "us-virginia", + inputPrice: 0.143 / 1e6, + outputPrice: 0.573 / 1e6, + }, + { + id: "cn-beijing", + inputPrice: 0.143 / 1e6, + outputPrice: 0.573 / 1e6, + }, + ], requestPrice: 0, contextSize: 262144, maxOutput: 65536, @@ -1211,6 +1452,14 @@ export const alibabaModels = [ discount: 0.2, inputPrice: 0.8 / 1e6, outputPrice: 2.4 / 1e6, + regions: [ + { id: "singapore", inputPrice: 0.8 / 1e6, outputPrice: 2.4 / 1e6 }, + { + id: "cn-beijing", + inputPrice: 0.23 / 1e6, + outputPrice: 0.574 / 1e6, + }, + ], requestPrice: 0, contextSize: 131072, maxOutput: 8192, @@ -1237,6 +1486,9 @@ export const alibabaModels = [ discount: 0.2, inputPrice: 1.0 / 1e6, outputPrice: 5.0 / 1e6, + regions: [ + { id: "cn-beijing", inputPrice: 1.0 / 1e6, outputPrice: 5.0 / 1e6 }, + ], requestPrice: 0, contextSize: 131072, maxOutput: 8192, @@ -1261,6 +1513,116 @@ export const alibabaModels = [ inputPrice: 0.3 / 1e6, outputPrice: 1.5 / 1e6, cachedInputPrice: 0.06 / 1e6, + regions: [ + { + id: "singapore", + inputPrice: 0.3 / 1e6, + outputPrice: 1.5 / 1e6, + cachedInputPrice: 0.06 / 1e6, + pricingTiers: [ + { + name: "Up to 32K", + upToTokens: 32000, + inputPrice: 0.3 / 1e6, + outputPrice: 1.5 / 1e6, + cachedInputPrice: 0.06 / 1e6, + }, + { + name: "32K-128K", + upToTokens: 128000, + inputPrice: 0.5 / 1e6, + outputPrice: 2.5 / 1e6, + cachedInputPrice: 0.1 / 1e6, + }, + { + name: "128K-256K", + upToTokens: 256000, + inputPrice: 0.8 / 1e6, + outputPrice: 4.0 / 1e6, + cachedInputPrice: 0.16 / 1e6, + }, + { + name: "Over 256K", + upToTokens: Infinity, + inputPrice: 1.6 / 1e6, + outputPrice: 9.6 / 1e6, + cachedInputPrice: 0.32 / 1e6, + }, + ], + }, + { + id: "us-virginia", + inputPrice: 0.144 / 1e6, + outputPrice: 0.574 / 1e6, + cachedInputPrice: 0.029 / 1e6, + pricingTiers: [ + { + name: "Up to 32K", + upToTokens: 32000, + inputPrice: 0.144 / 1e6, + outputPrice: 0.574 / 1e6, + cachedInputPrice: 0.029 / 1e6, + }, + { + name: "32K-128K", + upToTokens: 128000, + inputPrice: 0.24 / 1e6, + outputPrice: 0.957 / 1e6, + cachedInputPrice: 0.048 / 1e6, + }, + { + name: "128K-256K", + upToTokens: 256000, + inputPrice: 0.384 / 1e6, + outputPrice: 1.53 / 1e6, + cachedInputPrice: 0.077 / 1e6, + }, + { + name: "Over 256K", + upToTokens: Infinity, + inputPrice: 0.768 / 1e6, + outputPrice: 3.67 / 1e6, + cachedInputPrice: 0.154 / 1e6, + }, + ], + }, + { + id: "cn-beijing", + inputPrice: 0.144 / 1e6, + outputPrice: 0.574 / 1e6, + cachedInputPrice: 0.029 / 1e6, + pricingTiers: [ + { + name: "Up to 32K", + upToTokens: 32000, + inputPrice: 0.144 / 1e6, + outputPrice: 0.574 / 1e6, + cachedInputPrice: 0.029 / 1e6, + }, + { + name: "32K-128K", + upToTokens: 128000, + inputPrice: 0.24 / 1e6, + outputPrice: 0.957 / 1e6, + cachedInputPrice: 0.048 / 1e6, + }, + { + name: "128K-256K", + upToTokens: 256000, + inputPrice: 0.384 / 1e6, + outputPrice: 1.53 / 1e6, + cachedInputPrice: 0.077 / 1e6, + }, + { + name: "Over 256K", + upToTokens: Infinity, + inputPrice: 0.768 / 1e6, + outputPrice: 3.67 / 1e6, + cachedInputPrice: 0.154 / 1e6, + }, + ], + }, + ], pricingTiers: [ { name: "Up to 32K", @@ -1315,6 +1677,95 @@ export const alibabaModels = [ inputPrice: 0.2 / 1e6, outputPrice: 1.6 / 1e6, cachedInputPrice: 0.04 / 1e6, + regions: [ + { + id: "singapore", + inputPrice: 0.2 / 1e6, + outputPrice: 1.6 / 1e6, + cachedInputPrice: 0.04 / 1e6, + pricingTiers: [ + { + name: "Up to 32K", + upToTokens: 32000, + inputPrice: 0.2 / 1e6, + outputPrice: 1.6 / 1e6, + cachedInputPrice: 0.04 / 1e6, + }, + { + name: "32K-128K", + upToTokens: 128000, + inputPrice: 0.3 / 1e6, + outputPrice: 2.4 / 1e6, + cachedInputPrice: 0.06 / 1e6, + }, + { + name: "Over 128K", + upToTokens: Infinity, + inputPrice: 0.6 / 1e6, + outputPrice: 4.8 / 1e6, + cachedInputPrice: 0.12 / 1e6, + }, + ], + }, + { + id: "us-virginia", + inputPrice: 0.143 / 1e6, + outputPrice: 1.434 / 1e6, + cachedInputPrice: 0.029 / 1e6, + pricingTiers: [ + { + name: "Up to 32K", + upToTokens: 32000, + inputPrice: 0.143 / 1e6, + outputPrice: 1.434 / 1e6, + cachedInputPrice: 0.029 / 1e6, + }, + { + name: "32K-128K", + upToTokens: 128000, + inputPrice: 0.215 / 1e6, + outputPrice: 2.151 / 1e6, + cachedInputPrice: 0.043 / 1e6, + }, + { + name: "Over 128K", + upToTokens: Infinity, + inputPrice: 0.429 / 1e6, + outputPrice: 4.302 / 1e6, + cachedInputPrice: 0.086 / 1e6, + }, + ], + }, + { + id: "cn-beijing", + inputPrice: 0.143 / 1e6, + outputPrice: 1.434 / 1e6, + cachedInputPrice: 0.029 / 1e6, + pricingTiers: [ + { + name: "Up to 32K", + upToTokens: 32000, + inputPrice: 0.143 / 1e6, + outputPrice: 1.434 / 1e6, + cachedInputPrice: 0.029 / 1e6, + }, + { + name: "32K-128K", + upToTokens: 128000, + inputPrice: 0.215 / 1e6, + outputPrice: 2.151 / 1e6, + cachedInputPrice: 0.043 / 1e6, + }, + { + name: "Over 128K", + upToTokens: Infinity, + inputPrice: 0.429 / 1e6, + outputPrice: 4.302 / 1e6, + cachedInputPrice: 0.086 / 1e6, + }, + ], + }, + ], pricingTiers: [ { name: "Up to 32K", @@ -1362,6 +1813,95 @@ export const alibabaModels = [ inputPrice: 0.05 / 1e6, outputPrice: 0.4 / 1e6, cachedInputPrice: 0.01 / 1e6, + regions: [ + { + id: "singapore", + inputPrice: 0.05 / 1e6, + outputPrice: 0.4 / 1e6, + cachedInputPrice: 0.01 / 1e6, + pricingTiers: [ + { + name: "Up to 32K", + upToTokens: 32000, + inputPrice: 0.05 / 1e6, + outputPrice: 0.4 / 1e6, + cachedInputPrice: 0.01 / 1e6, + }, + { + name: "32K-128K", + upToTokens: 128000, + inputPrice: 0.075 / 1e6, + outputPrice: 0.6 / 1e6, + cachedInputPrice: 0.015 / 1e6, + }, + { + name: "Over 128K", + upToTokens: Infinity, + inputPrice: 0.12 / 1e6, + outputPrice: 0.96 / 1e6, + cachedInputPrice: 0.024 / 1e6, + }, + ], + }, + { + id: "us-virginia", + inputPrice: 0.022 / 1e6, + outputPrice: 0.215 / 1e6, + cachedInputPrice: 0.0044 / 1e6, + pricingTiers: [ + { + name: "Up to 32K", + upToTokens: 32000, + inputPrice: 0.022 / 1e6, + outputPrice: 0.215 / 1e6, + cachedInputPrice: 0.0044 / 1e6, + }, + { + name: "32K-128K", + upToTokens: 128000, + inputPrice: 0.033 / 1e6, + outputPrice: 0.323 / 1e6, + cachedInputPrice: 0.0066 / 1e6, + }, + { + name: "Over 128K", + upToTokens: Infinity, + inputPrice: 0.053 / 1e6, + outputPrice: 0.516 / 1e6, + cachedInputPrice: 0.0106 / 1e6, + }, + ], + }, + { + id: "cn-beijing", + inputPrice: 0.022 / 1e6, + outputPrice: 0.215 / 1e6, + cachedInputPrice: 0.0044 / 1e6, + pricingTiers: [ + { + name: "Up to 32K", + upToTokens: 32000, + inputPrice: 0.022 / 1e6, + outputPrice: 0.215 / 1e6, + cachedInputPrice: 0.0044 / 1e6, + }, + { + name: "32K-128K", + upToTokens: 128000, + inputPrice: 0.033 / 1e6, + outputPrice: 0.323 / 1e6, + cachedInputPrice: 0.0066 / 1e6, + }, + { + name: "Over 128K", + upToTokens: Infinity, + inputPrice: 0.053 / 1e6, + outputPrice: 0.516 / 1e6, + cachedInputPrice: 0.0106 / 1e6, + }, + ], + }, + ], pricingTiers: [ { name: "Up to 32K", @@ -1511,6 +2051,95 @@ export const alibabaModels = [ inputPrice: 1.2 / 1e6, outputPrice: 6.0 / 1e6, cachedInputPrice: 0.24 / 1e6, + regions: [ + { + id: "singapore", + inputPrice: 1.2 / 1e6, + outputPrice: 6.0 / 1e6, + cachedInputPrice: 0.24 / 1e6, + pricingTiers: [ + { + name: "Up to 32K", + upToTokens: 32000, + inputPrice: 1.2 / 1e6, + outputPrice: 6.0 / 1e6, + cachedInputPrice: 0.24 / 1e6, + }, + { + name: "32K-128K", + upToTokens: 128000, + inputPrice: 2.4 / 1e6, + outputPrice: 12.0 / 1e6, + cachedInputPrice: 0.48 / 1e6, + }, + { + name: "Over 128K", + upToTokens: Infinity, + inputPrice: 3.0 / 1e6, + outputPrice: 15.0 / 1e6, + cachedInputPrice: 0.6 / 1e6, + }, + ], + }, + { + id: "us-virginia", + inputPrice: 0.359 / 1e6, + outputPrice: 1.434 / 1e6, + cachedInputPrice: 0.072 / 1e6, + pricingTiers: [ + { + name: "Up to 32K", + upToTokens: 32000, + inputPrice: 0.359 / 1e6, + outputPrice: 1.434 / 1e6, + cachedInputPrice: 0.072 / 1e6, + }, + { + name: "32K-128K", + upToTokens: 128000, + inputPrice: 0.718 / 1e6, + outputPrice: 2.868 / 1e6, + cachedInputPrice: 0.144 / 1e6, + }, + { + name: "Over 128K", + upToTokens: Infinity, + inputPrice: 0.898 / 1e6, + outputPrice: 3.585 / 1e6, + cachedInputPrice: 0.18 / 1e6, + }, + ], + }, + { + id: "cn-beijing", + inputPrice: 0.359 / 1e6, + outputPrice: 1.434 / 1e6, + cachedInputPrice: 0.072 / 1e6, + pricingTiers: [ + { + name: "Up to 32K", + upToTokens: 32000, + inputPrice: 0.359 / 1e6, + outputPrice: 1.434 / 1e6, + cachedInputPrice: 0.072 / 1e6, + }, + { + name: "32K-128K", + upToTokens: 128000, + inputPrice: 0.718 / 1e6, + outputPrice: 2.868 / 1e6, + cachedInputPrice: 0.144 / 1e6, + }, + { + name: "Over 128K", + upToTokens: Infinity, + inputPrice: 0.898 / 1e6, + outputPrice: 3.585 / 1e6, + cachedInputPrice: 0.18 / 1e6, + }, + ], + }, + ], pricingTiers: [ { name: "Up to 32K", diff --git a/packages/models/src/providers.ts b/packages/models/src/providers.ts index 910a86b665..eae96d7b51 100644 --- a/packages/models/src/providers.ts +++ b/packages/models/src/providers.ts @@ -6,6 +6,21 @@ export interface ProviderEnvConfig { optional?: Record; } +/** + * Region routing configuration for providers that support multiple geographic endpoints. + * Used by the gateway for endpoint URL resolution and by the UI for the region selector. + */ +export interface ProviderRegionConfig { + /** Key in ProviderKeyOptions where the selected region is stored (e.g. "alibaba_region") */ + optionsKey: string; + /** Region used when none is explicitly configured */ + defaultRegion: string; + /** Ordered list of available regions for this provider, used to populate the UI dropdown */ + regions: { id: string; label: string }[]; + /** Maps region id to its base URL */ + endpointMap: Record; +} + export interface ProviderDefinition { id: string; name: string; @@ -29,6 +44,8 @@ export interface ProviderDefinition { // Priority weight for routing (default: 1). Lower values deprioritize the provider. // e.g., 0.8 means 20% lower priority (score multiplied by 1/0.8 = 1.25) priority?: number; + /** Region routing config - when set, provider supports multiple geographic endpoints */ + regionConfig?: ProviderRegionConfig; } export const providers = [ @@ -204,12 +221,29 @@ export const providers = [ required: { apiKey: "LLM_ALIBABA_API_KEY", }, + optional: { + region: "LLM_ALIBABA_REGION", + }, }, streaming: true, cancellation: true, color: "#FF6A00", website: "https://www.alibabacloud.com", announcement: null, + regionConfig: { + optionsKey: "alibaba_region", + defaultRegion: "singapore", + regions: [ + { id: "singapore", label: "Singapore (default)" }, + { id: "us-virginia", label: "US (Virginia)" }, + { id: "cn-beijing", label: "China (Beijing)" }, + ], + endpointMap: { + singapore: "https://dashscope-intl.aliyuncs.com", + "us-virginia": "https://dashscope-us.aliyuncs.com", + "cn-beijing": "https://dashscope.aliyuncs.com", + }, + }, }, { id: "novita", From fb1eb46433b5ecbdcacdb438db8d554591a332a8 Mon Sep 17 00:00:00 2001 From: romel lauron Date: Fri, 20 Mar 2026 00:42:51 +0800 Subject: [PATCH 02/55] fix: single region resolution + generic support - Resolve region once in resolve-provider-context and pass it to getProviderEndpoint (no dual resolution) - Models without regions skip region logic entirely and use the default endpoint (current behavior) - Generic regionBaseUrl lookup so any provider with regionConfig can use region-based routing Co-Authored-By: Claude Opus 4.6 (1M context) --- .../chat/tools/resolve-provider-context.ts | 39 ++++++++++--------- packages/actions/src/get-provider-endpoint.ts | 28 +++++++------ 2 files changed, 37 insertions(+), 30 deletions(-) diff --git a/apps/gateway/src/chat/tools/resolve-provider-context.ts b/apps/gateway/src/chat/tools/resolve-provider-context.ts index 2f99f03fbe..6928b1fdb4 100644 --- a/apps/gateway/src/chat/tools/resolve-provider-context.ts +++ b/apps/gateway/src/chat/tools/resolve-provider-context.ts @@ -183,26 +183,28 @@ export async function resolveProviderContext( ); // --- Region resolution --- - const providerDef = providers.find((p) => p.id === usedProvider) as - | ProviderDefinition - | undefined; - const regionConfig = providerDef?.regionConfig; + // Only resolve and use a region when the model explicitly declares supported regions. + // Models without regions use the provider's default endpoint (current behavior). + const modelRegions = (providerMappingForSelected as ProviderModelMapping) + ?.regions; let usedRegion: string | undefined; - if (regionConfig) { - const optionsKey = regionConfig.optionsKey; - const keyOptions = providerKey?.options as - | Record - | null + if (modelRegions && modelRegions.length > 0) { + const providerDef = providers.find((p) => p.id === usedProvider) as + | ProviderDefinition | undefined; - usedRegion = - keyOptions?.[optionsKey] ?? - getProviderEnvValue(usedProvider as Provider, "region", configIndex) ?? - regionConfig.defaultRegion; - - // Validate that the selected region is supported by this model mapping - const modelRegions = (providerMappingForSelected as ProviderModelMapping) - ?.regions; - if (modelRegions && modelRegions.length > 0) { + const regionConfig = providerDef?.regionConfig; + if (regionConfig) { + const optionsKey = regionConfig.optionsKey; + const keyOptions = providerKey?.options as + | Record + | null + | undefined; + usedRegion = + keyOptions?.[optionsKey] ?? + getProviderEnvValue(usedProvider as Provider, "region", configIndex) ?? + regionConfig.defaultRegion; + + // Validate that the selected region is supported by this model const isValidRegion = modelRegions.some((r) => r.id === usedRegion); if (!isValidRegion) { const validIds = modelRegions.map((r) => r.id).join(", "); @@ -236,6 +238,7 @@ export async function resolveProviderContext( providerKey?.options ?? undefined, configIndex, isImageGeneration, + usedRegion, ); if (!url) { diff --git a/packages/actions/src/get-provider-endpoint.ts b/packages/actions/src/get-provider-endpoint.ts index b512dbbcf0..7318a00342 100644 --- a/packages/actions/src/get-provider-endpoint.ts +++ b/packages/actions/src/get-provider-endpoint.ts @@ -1,6 +1,7 @@ import { models, providers, + type ProviderDefinition, type ProviderModelMapping, type ProviderId, getProviderEnvValue, @@ -23,6 +24,7 @@ export function getProviderEndpoint( providerKeyOptions?: ProviderKeyOptions, configIndex?: number, imageGenerations?: boolean, + region?: string, ): string { let modelName = model; if (model && model !== "custom") { @@ -38,6 +40,19 @@ export function getProviderEndpoint( } let url: string | undefined; + // Generic region-based base URL resolution. + // Any provider with a regionConfig + endpointMap can use this. + let regionBaseUrl: string | undefined; + if (region) { + const providerDef = providers.find((p) => p.id === provider) as + | ProviderDefinition + | undefined; + const endpointMap = providerDef?.regionConfig?.endpointMap as + | Record + | undefined; + regionBaseUrl = endpointMap?.[region]; + } + if (baseUrl) { url = baseUrl; } else { @@ -101,19 +116,8 @@ export function getProviderEndpoint( url = "https://api.moonshot.ai"; break; case "alibaba": { - const alibabaProviderDef = providers.find((p) => p.id === "alibaba"); - const alibabaRegionConfig = alibabaProviderDef?.regionConfig; - const alibabaRegion = - providerKeyOptions?.alibaba_region ?? - getProviderEnvValue("alibaba", "region", configIndex) ?? - alibabaRegionConfig?.defaultRegion ?? - "singapore"; const alibabaBaseUrl = - ( - alibabaRegionConfig?.endpointMap as - | Record - | undefined - )?.[alibabaRegion] ?? "https://dashscope-intl.aliyuncs.com"; + regionBaseUrl ?? "https://dashscope-intl.aliyuncs.com"; // Use different base URL for image generation vs chat completions if (imageGenerations) { url = alibabaBaseUrl; From 38b74911705a448ae725bf5aa6793a1c7084f10b Mon Sep 17 00:00:00 2001 From: romel lauron Date: Fri, 20 Mar 2026 22:28:41 +0800 Subject: [PATCH 03/55] fix: region review fixes - Reset selectedRegion state on dialog close to prevent stale values - Log error when region not found in cost pricing (upstream bug indicator) - Narrow alibaba_region type to valid union instead of string Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/gateway/src/lib/costs.ts | 10 +++++++++- .../provider-keys/create-provider-key-dialog.tsx | 1 + packages/db/src/schema.ts | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/apps/gateway/src/lib/costs.ts b/apps/gateway/src/lib/costs.ts index 71891e2bda..b8e3ba6a1f 100644 --- a/apps/gateway/src/lib/costs.ts +++ b/apps/gateway/src/lib/costs.ts @@ -251,10 +251,18 @@ export async function calculateCosts( }; } - // Resolve region-specific pricing when available + // Resolve region-specific pricing when available. + // If the region doesn't match, regionPricing stays undefined and top-level + // pricing is used. This shouldn't happen — resolve-provider-context validates + // the region before the request is processed — so log an error to surface it. let regionPricing: ProviderRegion | undefined; if (region && providerInfo.regions && providerInfo.regions.length > 0) { regionPricing = providerInfo.regions.find((r) => r.id === region); + if (!regionPricing) { + logger.error( + `Region "${region}" not found in pricing for model "${model}" provider "${provider}". This indicates a bug — region should have been validated upstream.`, + ); + } } // Get pricing based on token count (supports tiered pricing) diff --git a/apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx b/apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx index 329d9d8a1a..38ff0b3a50 100644 --- a/apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx +++ b/apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx @@ -248,6 +248,7 @@ export function CreateProviderKeyDialog({ setAzureApiVersion("2024-10-21"); setAzureDeploymentType("ai-foundry"); setAzureValidationModel("gpt-4o-mini"); + setSelectedRegion(""); }, 300); }; diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index 89854ca198..c40c060580 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -425,7 +425,7 @@ export interface ProviderKeyOptions { azure_api_version?: string; azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; - alibaba_region?: string; + alibaba_region?: "singapore" | "us-virginia" | "cn-beijing"; } export const providerKey = pgTable( From 6ceec1c5521a038e4c88a04dba5a0682b9605b36 Mon Sep 17 00:00:00 2001 From: romel lauron Date: Fri, 20 Mar 2026 23:13:46 +0800 Subject: [PATCH 04/55] fix: deduplicate pricing tiers and clean up UI - Remove duplicate top-level pricingTiers when regions define them; costs.ts now falls back to regions[0] - Replace IIFE patterns with cleaner conditionals in provider key dialog - Support region-aware tiered pricing in model cards - Relax alibaba_region type to string in schema Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/gateway/src/lib/costs.ts | 23 +-- .../components/models/model-comparison.tsx | 19 ++- .../components/models/model-provider-card.tsx | 118 +++++++------- .../create-provider-key-dialog.tsx | 85 +++++----- apps/ui/src/components/shared/model-card.tsx | 3 +- packages/db/src/schema.ts | 2 +- packages/models/src/models/alibaba.ts | 147 ------------------ 7 files changed, 133 insertions(+), 264 deletions(-) diff --git a/apps/gateway/src/lib/costs.ts b/apps/gateway/src/lib/costs.ts index b8e3ba6a1f..8f8ea79fde 100644 --- a/apps/gateway/src/lib/costs.ts +++ b/apps/gateway/src/lib/costs.ts @@ -252,16 +252,21 @@ export async function calculateCosts( } // Resolve region-specific pricing when available. - // If the region doesn't match, regionPricing stays undefined and top-level - // pricing is used. This shouldn't happen — resolve-provider-context validates - // the region before the request is processed — so log an error to surface it. + // When a region is specified, look it up directly. When no region is specified + // but the mapping defines regions, fall back to regions[0] (the default region). + // This lets model definitions omit top-level inputPrice/outputPrice/pricingTiers + // when regions are defined — the first region is the canonical default. let regionPricing: ProviderRegion | undefined; - if (region && providerInfo.regions && providerInfo.regions.length > 0) { - regionPricing = providerInfo.regions.find((r) => r.id === region); - if (!regionPricing) { - logger.error( - `Region "${region}" not found in pricing for model "${model}" provider "${provider}". This indicates a bug — region should have been validated upstream.`, - ); + if (providerInfo.regions && providerInfo.regions.length > 0) { + if (region) { + regionPricing = providerInfo.regions.find((r) => r.id === region); + if (!regionPricing) { + logger.error( + `Region "${region}" not found in pricing for model "${model}" provider "${provider}". This indicates a bug — region should have been validated upstream.`, + ); + } + } else { + regionPricing = providerInfo.regions[0]; } } diff --git a/apps/ui/src/components/models/model-comparison.tsx b/apps/ui/src/components/models/model-comparison.tsx index 995fa55001..5f384d8051 100644 --- a/apps/ui/src/components/models/model-comparison.tsx +++ b/apps/ui/src/components/models/model-comparison.tsx @@ -343,7 +343,10 @@ function collectModelDetail(modelId?: ModelId): ModelDetail | undefined { requestPrice: getPricingSummary(providersWithInfo, "requestPrice"), imageInputPrice: getPricingSummary(providersWithInfo, "imageInputPrice"), hasTieredPricing: providersWithInfo.some( - (p) => p.pricingTiers && p.pricingTiers.length > 1, + (p) => + (p.pricingTiers ?? p.regions?.[0]?.pricingTiers)?.length !== + undefined && + (p.pricingTiers ?? p.regions?.[0]?.pricingTiers)!.length > 1, ), }; @@ -550,9 +553,12 @@ function renderRowValue( const summary = getProviderPricingSummary(selectedProvider, "inputPrice") ?? detail.aggregated.inputPrice; + const tiers = selectedProvider + ? (selectedProvider.pricingTiers ?? + selectedProvider.regions?.[0]?.pricingTiers) + : undefined; const hasTiered = selectedProvider - ? selectedProvider.pricingTiers && - selectedProvider.pricingTiers.length > 1 + ? tiers && tiers.length > 1 : detail.aggregated.hasTieredPricing; return ; } @@ -560,9 +566,12 @@ function renderRowValue( const summary = getProviderPricingSummary(selectedProvider, "outputPrice") ?? detail.aggregated.outputPrice; + const tiers = selectedProvider + ? (selectedProvider.pricingTiers ?? + selectedProvider.regions?.[0]?.pricingTiers) + : undefined; const hasTiered = selectedProvider - ? selectedProvider.pricingTiers && - selectedProvider.pricingTiers.length > 1 + ? tiers && tiers.length > 1 : detail.aggregated.hasTieredPricing; return ; } diff --git a/apps/ui/src/components/models/model-provider-card.tsx b/apps/ui/src/components/models/model-provider-card.tsx index 63062c164f..d45a3996ac 100644 --- a/apps/ui/src/components/models/model-provider-card.tsx +++ b/apps/ui/src/components/models/model-provider-card.tsx @@ -496,25 +496,64 @@ export function ModelProviderCard({
)} - {provider.pricingTiers && provider.pricingTiers.length > 1 && ( -
-
- Tiered Pricing -
-
- {provider.pricingTiers.map((tier, index) => ( -
- - {tier.upToTokens === Infinity - ? `>${(provider.pricingTiers![index - 1]?.upToTokens || 0) / 1000}K tokens` - : `≤${tier.upToTokens / 1000}K tokens`} - - {provider.discount ? ( - - + {(() => { + const tiers = + provider.pricingTiers ?? provider.regions?.[0]?.pricingTiers; + if (!tiers || tiers.length <= 1) { + return null; + } + return ( +
+
+ Tiered Pricing +
+
+ {tiers.map((tier, index) => ( +
+ + {tier.upToTokens === Infinity + ? `>${(tiers[index - 1]?.upToTokens || 0) / 1000}K tokens` + : `≤${tier.upToTokens / 1000}K tokens`} + + {provider.discount ? ( + + + {formatPrice(tier.inputPrice)} in /{" "} + {tier.cachedInputPrice !== null && + tier.cachedInputPrice !== undefined && ( + <> + {formatPrice(tier.cachedInputPrice)} cached + /{" "} + + )} + {formatPrice(tier.outputPrice)} out + + + {formatPrice( + tier.inputPrice * (1 - provider.discount), + )}{" "} + in /{" "} + {tier.cachedInputPrice !== null && + tier.cachedInputPrice !== undefined && ( + <> + {formatPrice( + tier.cachedInputPrice * + (1 - provider.discount), + )}{" "} + cached /{" "} + + )} + {formatPrice( + tier.outputPrice * (1 - provider.discount), + )}{" "} + out + + + ) : ( + {formatPrice(tier.inputPrice)} in /{" "} {tier.cachedInputPrice !== null && tier.cachedInputPrice !== undefined && ( @@ -525,42 +564,13 @@ export function ModelProviderCard({ )} {formatPrice(tier.outputPrice)} out - - {formatPrice( - tier.inputPrice * (1 - provider.discount), - )}{" "} - in /{" "} - {tier.cachedInputPrice !== null && - tier.cachedInputPrice !== undefined && ( - <> - {formatPrice( - tier.cachedInputPrice * - (1 - provider.discount), - )}{" "} - cached /{" "} - - )} - {formatPrice( - tier.outputPrice * (1 - provider.discount), - )}{" "} - out - - - ) : ( - - {formatPrice(tier.inputPrice)} in /{" "} - {tier.cachedInputPrice !== null && - tier.cachedInputPrice !== undefined && ( - <>{formatPrice(tier.cachedInputPrice)} cached / - )} - {formatPrice(tier.outputPrice)} out - - )} -
- ))} + )} +
+ ))} +
-
- )} + ); + })()}
diff --git a/apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx b/apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx index 38ff0b3a50..f1c65433f0 100644 --- a/apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx +++ b/apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx @@ -25,11 +25,7 @@ import { import { toast } from "@/lib/components/use-toast"; import { useApi } from "@/lib/fetch-client"; -import { - providers, - type ProviderDefinition, - type ProviderRegionConfig, -} from "@llmgateway/models"; +import { providers, type ProviderDefinition } from "@llmgateway/models"; import { ProviderSelect } from "./provider-select"; @@ -85,6 +81,10 @@ export function CreateProviderKeyDialog({ const createMutation = api.useMutation("post", "/keys/provider"); + const selectedProviderDef = providers.find( + (p) => p.id === selectedProvider, + ) as ProviderDefinition | undefined; + // Filter provider keys by selected organization const organizationProviderKeys = existingProviderKeys.filter( (key) => key.organizationId === selectedOrganization.id, @@ -186,13 +186,10 @@ export function CreateProviderKeyDialog({ }; } // Include region in options for providers that support it - const providerDef = providers.find((p) => p.id === selectedProvider) as - | ProviderDefinition - | undefined; - if (providerDef?.regionConfig && selectedRegion) { + if (selectedProviderDef?.regionConfig && selectedRegion) { payload.options = { ...payload.options, - [providerDef.regionConfig.optionsKey]: selectedRegion, + [selectedProviderDef.regionConfig.optionsKey]: selectedRegion, }; } @@ -275,7 +272,10 @@ export function CreateProviderKeyDialog({
{ + setSelectedProvider(value); + setSelectedRegion(""); + }} value={selectedProvider} providers={availableProviders} loading={false} @@ -434,42 +434,33 @@ export function CreateProviderKeyDialog({ )} - {(() => { - const providerWithRegion = providers.find( - (p) => p.id === selectedProvider, - ) as ProviderDefinition | undefined; - const rc = providerWithRegion?.regionConfig as - | ProviderRegionConfig - | undefined; - if (!rc) { - return null; - } - const effectiveRegion = selectedRegion || rc.defaultRegion; - return ( -
- - -

- API keys are region-specific. Make sure your key matches the - selected region. -

-
- ); - })()} + {selectedProviderDef?.regionConfig && ( +
+ + +

+ API keys are region-specific. Make sure your key matches the + selected region. +

+
+ )} {selectedProvider === "custom" && ( <> diff --git a/apps/ui/src/components/shared/model-card.tsx b/apps/ui/src/components/shared/model-card.tsx index 144ee5d912..396d18d5e8 100644 --- a/apps/ui/src/components/shared/model-card.tsx +++ b/apps/ui/src/components/shared/model-card.tsx @@ -134,7 +134,8 @@ export function ModelCard({ modelName, providers }: ModelCardProps) { provider.requestPrice !== 0 && ` / $${(provider.requestPrice * 1000).toFixed(2)} per 1K req`}

- {provider.pricingTiers && provider.pricingTiers.length > 1 && ( + {((provider.pricingTiers ?? provider.regions?.[0]?.pricingTiers) + ?.length ?? 0) > 1 && (

Tiered pricing available

diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index c40c060580..89854ca198 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -425,7 +425,7 @@ export interface ProviderKeyOptions { azure_api_version?: string; azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; - alibaba_region?: "singapore" | "us-virginia" | "cn-beijing"; + alibaba_region?: string; } export const providerKey = pgTable( diff --git a/packages/models/src/models/alibaba.ts b/packages/models/src/models/alibaba.ts index 6b42e54176..7e0d797e88 100644 --- a/packages/models/src/models/alibaba.ts +++ b/packages/models/src/models/alibaba.ts @@ -147,22 +147,6 @@ export const alibabaModels = [ ], }, ], - pricingTiers: [ - { - name: "Up to 256K", - upToTokens: 256000, - inputPrice: 0.4 / 1e6, - outputPrice: 1.2 / 1e6, - cachedInputPrice: 0.08 / 1e6, - }, - { - name: "Over 256K", - upToTokens: Infinity, - inputPrice: 1.2 / 1e6, - outputPrice: 3.6 / 1e6, - cachedInputPrice: 0.24 / 1e6, - }, - ], requestPrice: 0, contextSize: 131072, maxOutput: 32000, @@ -255,22 +239,6 @@ export const alibabaModels = [ ], }, ], - pricingTiers: [ - { - name: "Up to 256K", - upToTokens: 256000, - inputPrice: 0.4 / 1e6, - outputPrice: 1.2 / 1e6, - cachedInputPrice: 0.08 / 1e6, - }, - { - name: "Over 256K", - upToTokens: Infinity, - inputPrice: 1.2 / 1e6, - outputPrice: 3.6 / 1e6, - cachedInputPrice: 0.24 / 1e6, - }, - ], requestPrice: 0, contextSize: 1000000, maxOutput: 32000, @@ -363,22 +331,6 @@ export const alibabaModels = [ ], }, ], - pricingTiers: [ - { - name: "Up to 256K", - upToTokens: 256000, - inputPrice: 0.05 / 1e6, - outputPrice: 0.4 / 1e6, - cachedInputPrice: 0.01 / 1e6, - }, - { - name: "Over 256K", - upToTokens: Infinity, - inputPrice: 0.25 / 1e6, - outputPrice: 2.0 / 1e6, - cachedInputPrice: 0.05 / 1e6, - }, - ], requestPrice: 0, contextSize: 1000000, maxOutput: 32000, @@ -1623,36 +1575,6 @@ export const alibabaModels = [ ], }, ], - pricingTiers: [ - { - name: "Up to 32K", - upToTokens: 32000, - inputPrice: 0.3 / 1e6, - outputPrice: 1.5 / 1e6, - cachedInputPrice: 0.06 / 1e6, - }, - { - name: "32K-128K", - upToTokens: 128000, - inputPrice: 0.5 / 1e6, - outputPrice: 2.5 / 1e6, - cachedInputPrice: 0.1 / 1e6, - }, - { - name: "128K-256K", - upToTokens: 256000, - inputPrice: 0.8 / 1e6, - outputPrice: 4.0 / 1e6, - cachedInputPrice: 0.16 / 1e6, - }, - { - name: "Over 256K", - upToTokens: Infinity, - inputPrice: 1.6 / 1e6, - outputPrice: 9.6 / 1e6, - cachedInputPrice: 0.32 / 1e6, - }, - ], requestPrice: 0, contextSize: 1000000, maxOutput: 65536, @@ -1766,29 +1688,6 @@ export const alibabaModels = [ ], }, ], - pricingTiers: [ - { - name: "Up to 32K", - upToTokens: 32000, - inputPrice: 0.2 / 1e6, - outputPrice: 1.6 / 1e6, - cachedInputPrice: 0.04 / 1e6, - }, - { - name: "32K-128K", - upToTokens: 128000, - inputPrice: 0.3 / 1e6, - outputPrice: 2.4 / 1e6, - cachedInputPrice: 0.06 / 1e6, - }, - { - name: "Over 128K", - upToTokens: Infinity, - inputPrice: 0.6 / 1e6, - outputPrice: 4.8 / 1e6, - cachedInputPrice: 0.12 / 1e6, - }, - ], requestPrice: 0, contextSize: 262144, maxOutput: 32768, @@ -1902,29 +1801,6 @@ export const alibabaModels = [ ], }, ], - pricingTiers: [ - { - name: "Up to 32K", - upToTokens: 32000, - inputPrice: 0.05 / 1e6, - outputPrice: 0.4 / 1e6, - cachedInputPrice: 0.01 / 1e6, - }, - { - name: "32K-128K", - upToTokens: 128000, - inputPrice: 0.075 / 1e6, - outputPrice: 0.6 / 1e6, - cachedInputPrice: 0.015 / 1e6, - }, - { - name: "Over 128K", - upToTokens: Infinity, - inputPrice: 0.12 / 1e6, - outputPrice: 0.96 / 1e6, - cachedInputPrice: 0.024 / 1e6, - }, - ], requestPrice: 0, contextSize: 262144, maxOutput: 32768, @@ -2140,29 +2016,6 @@ export const alibabaModels = [ ], }, ], - pricingTiers: [ - { - name: "Up to 32K", - upToTokens: 32000, - inputPrice: 1.2 / 1e6, - outputPrice: 6.0 / 1e6, - cachedInputPrice: 0.24 / 1e6, - }, - { - name: "32K-128K", - upToTokens: 128000, - inputPrice: 2.4 / 1e6, - outputPrice: 12.0 / 1e6, - cachedInputPrice: 0.48 / 1e6, - }, - { - name: "Over 128K", - upToTokens: Infinity, - inputPrice: 3.0 / 1e6, - outputPrice: 15.0 / 1e6, - cachedInputPrice: 0.6 / 1e6, - }, - ], requestPrice: 0, contextSize: 262144, maxOutput: 65536, From 9adc60ac812af9cc8497650b6b323f65f6cb9f83 Mon Sep 17 00:00:00 2001 From: romel lauron Date: Sun, 22 Mar 2026 22:03:00 +0800 Subject: [PATCH 05/55] feat: extend ProviderRegion with optional overrides Make inputPrice/outputPrice optional on ProviderRegion with fallback to parent mapping. Add discount, requestPrice, webSearchPrice, and maxOutput as optional region overrides. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../chat/tools/resolve-provider-context.ts | 17 +++++++---- apps/gateway/src/lib/costs.ts | 11 +++++-- packages/models/src/models.ts | 30 ++++++++++++++++--- 3 files changed, 45 insertions(+), 13 deletions(-) diff --git a/apps/gateway/src/chat/tools/resolve-provider-context.ts b/apps/gateway/src/chat/tools/resolve-provider-context.ts index 6928b1fdb4..552048544d 100644 --- a/apps/gateway/src/chat/tools/resolve-provider-context.ts +++ b/apps/gateway/src/chat/tools/resolve-provider-context.ts @@ -215,6 +215,11 @@ export async function resolveProviderContext( } } + // --- Resolve region data for post-selection overrides (e.g. maxOutput) --- + const regionData = usedRegion + ? modelRegions?.find((r) => r.id === usedRegion) + : undefined; + // --- Check if model supports reasoning (from selected provider, not any) --- const supportsReasoning = (providerMappingForSelected as ProviderModelMapping)?.reasoning === true; @@ -294,13 +299,13 @@ export async function resolveProviderContext( // --- max_tokens validation --- if (max_tokens !== undefined && providerMappingForSelected) { - if ( - "maxOutput" in providerMappingForSelected && - providerMappingForSelected.maxOutput !== undefined - ) { - if (max_tokens > providerMappingForSelected.maxOutput) { + const effectiveMaxOutput = + regionData?.maxOutput ?? + (providerMappingForSelected as ProviderModelMapping).maxOutput; + if (effectiveMaxOutput !== undefined) { + if (max_tokens > effectiveMaxOutput) { // Silently cap to max output instead of throwing on retry - max_tokens = providerMappingForSelected.maxOutput; + max_tokens = effectiveMaxOutput; } } } diff --git a/apps/gateway/src/lib/costs.ts b/apps/gateway/src/lib/costs.ts index 8f8ea79fde..5443e4e6f7 100644 --- a/apps/gateway/src/lib/costs.ts +++ b/apps/gateway/src/lib/costs.ts @@ -285,11 +285,14 @@ export async function calculateCosts( const cachedInputPrice = new Decimal( pricing.cachedInputPrice ?? pricing.inputPrice, ); - const requestPrice = new Decimal(providerInfo.requestPrice ?? 0); + const requestPrice = new Decimal( + regionPricing?.requestPrice ?? providerInfo.requestPrice ?? 0, + ); // Get effective discount (checks org-specific, global, then hardcoded) // Pass both the root model ID and the provider-specific model name for matching - const hardcodedDiscount = providerInfo.discount ?? 0; + const hardcodedDiscount = + regionPricing?.discount ?? providerInfo.discount ?? 0; const effectiveDiscountResult = await getEffectiveDiscount( organizationId, provider, @@ -393,7 +396,9 @@ export async function calculateCosts( const requestCost = requestPrice.times(discountMultiplier); // Calculate web search cost - const webSearchPrice = new Decimal((providerInfo as any).webSearchPrice ?? 0); + const webSearchPrice = new Decimal( + regionPricing?.webSearchPrice ?? (providerInfo as any).webSearchPrice ?? 0, + ); const webSearchCost = webSearchCount && webSearchCount > 0 ? webSearchPrice.times(webSearchCount).times(discountMultiplier) diff --git a/packages/models/src/models.ts b/packages/models/src/models.ts index f8cb717e0e..8ea558434c 100644 --- a/packages/models/src/models.ts +++ b/packages/models/src/models.ts @@ -59,13 +59,15 @@ export interface ProviderRegion { */ id: string; /** - * Price per input token in USD for this region + * Price per input token in USD for this region. + * When absent, falls back to the mapping-level inputPrice. */ - inputPrice: number; + inputPrice?: number; /** - * Price per output token in USD for this region + * Price per output token in USD for this region. + * When absent, falls back to the mapping-level outputPrice. */ - outputPrice: number; + outputPrice?: number; /** * Price per cached input token in USD for this region */ @@ -75,6 +77,26 @@ export interface ProviderRegion { * When absent, falls back to the mapping-level pricingTiers. */ pricingTiers?: PricingTier[]; + /** + * Discount multiplier (0-1) for this region. + * When absent, falls back to the mapping-level discount. + */ + discount?: number; + /** + * Price per request in USD for this region. + * When absent, falls back to the mapping-level requestPrice. + */ + requestPrice?: number; + /** + * Price per web search query in USD for this region. + * When absent, falls back to the mapping-level webSearchPrice. + */ + webSearchPrice?: number; + /** + * Maximum output size in tokens for this region. + * When absent, falls back to the mapping-level maxOutput. + */ + maxOutput?: number; } export interface ProviderModelMapping { From 015f558a2a88a44993cc9405b699b640a6e92501 Mon Sep 17 00:00:00 2001 From: romel lauron Date: Mon, 23 Mar 2026 01:17:21 +0800 Subject: [PATCH 06/55] feat: regions as separate provider mappings Implement Luca's approach where each region becomes a separate row in model_provider_mapping, scored and routed independently by the existing scoring/failover pipeline. - Add region column to model_provider_mapping with updated unique constraint (modelId, providerId, region) - Add expandProviderRegions helper for runtime expansion of nested region definitions into flat provider entries - Update worker sync to create per-region DB rows - Update metrics key format to include region - Update scoring to match providers by region - Add region-specific env var support (LLM_X_API_KEY__REGION) - Region-aware retry: only skip the failed region, not all regions of the same provider - Simplify costs.ts by removing regionPricing fallback logic - Expose region in API response and UI interface Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/api/src/routes/internal-models.ts | 1 + apps/code/src/lib/api/v1.d.ts | 1 + apps/gateway/src/chat/chat.ts | 67 ++++++++++++++----- .../chat/tools/resolve-provider-context.ts | 58 +++++----------- .../src/chat/tools/retry-with-fallback.ts | 28 ++++++-- apps/gateway/src/lib/costs.ts | 42 +++--------- apps/playground/src/lib/api/v1.d.ts | 1 + .../components/models/model-comparison.tsx | 11 +-- .../components/models/model-provider-card.tsx | 3 +- apps/ui/src/components/shared/model-card.tsx | 3 +- apps/ui/src/lib/api/v1.d.ts | 1 + apps/ui/src/lib/fetch-models.ts | 1 + apps/worker/src/services/sync-models.ts | 19 +++++- .../get-cheapest-from-available-providers.ts | 28 ++++++-- packages/db/src/provider-metrics.ts | 37 ++++++++-- packages/db/src/schema.ts | 3 +- packages/models/src/index.ts | 1 + packages/models/src/models.ts | 14 ++-- packages/models/src/models/alibaba.ts | 35 ++-------- packages/models/src/provider.ts | 22 ++++++ packages/models/src/region-helpers.ts | 37 ++++++++++ packages/models/src/types.ts | 1 + 22 files changed, 257 insertions(+), 157 deletions(-) create mode 100644 packages/models/src/region-helpers.ts diff --git a/apps/api/src/routes/internal-models.ts b/apps/api/src/routes/internal-models.ts index 0d965bbb3d..811170bbe1 100644 --- a/apps/api/src/routes/internal-models.ts +++ b/apps/api/src/routes/internal-models.ts @@ -28,6 +28,7 @@ const modelProviderMappingSchema = z.object({ modelId: z.string(), providerId: z.string(), modelName: z.string(), + region: z.string().nullable(), inputPrice: z.string().nullable(), outputPrice: z.string().nullable(), cachedInputPrice: z.string().nullable(), diff --git a/apps/code/src/lib/api/v1.d.ts b/apps/code/src/lib/api/v1.d.ts index 22986d515b..dd0adf1a82 100644 --- a/apps/code/src/lib/api/v1.d.ts +++ b/apps/code/src/lib/api/v1.d.ts @@ -6923,6 +6923,7 @@ export interface operations { modelId: string; providerId: string; modelName: string; + region: string | null; inputPrice: string | null; outputPrice: string | null; cachedInputPrice: string | null; diff --git a/apps/gateway/src/chat/chat.ts b/apps/gateway/src/chat/chat.ts index ed1c01241b..9207035330 100644 --- a/apps/gateway/src/chat/chat.ts +++ b/apps/gateway/src/chat/chat.ts @@ -43,6 +43,7 @@ import { getProviderMetricsForCombinations, type InferSelectModel, isCachingEnabled, + metricsKey, shortid, type tables, } from "@llmgateway/db"; @@ -64,6 +65,7 @@ import { type ProviderRequestBody, providers, type WebSearchTool, + expandAllProviderRegions, } from "@llmgateway/models"; import { completionsRequestSchema } from "./schemas/completions.js"; @@ -97,6 +99,7 @@ import { type RoutingAttempt, getErrorType, MAX_RETRIES, + providerRetryKey, selectNextProvider, shouldRetryRequest, } from "./tools/retry-with-fallback.js"; @@ -448,8 +451,13 @@ chat.openapi(completions, async (c) => { requestedModel, parseResult.requestedProvider, ); - let modelInfo = modelInfoResult.modelInfo; - const allModelProviders = modelInfoResult.allModelProviders; + let modelInfo = { + ...modelInfoResult.modelInfo, + providers: expandAllProviderRegions(modelInfoResult.modelInfo.providers), + }; + const allModelProviders = expandAllProviderRegions( + modelInfoResult.allModelProviders, + ); let requestedProvider = modelInfoResult.requestedProvider; // Validate that models requiring image input have at least one image in the request @@ -772,8 +780,12 @@ chat.openapi(completions, async (c) => { } const candidateAllowedProviders = candidateIam.allowedProviders; + // Expand region entries into separate candidates for scoring + const expandedCandidateProviders = expandAllProviderRegions( + modelDef.providers, + ); // Check if any of the model's providers are available - const availableModelProviders = modelDef.providers.filter( + const availableModelProviders = expandedCandidateProviders.filter( (provider) => availableProviders.includes(provider.providerId) && (!candidateAllowedProviders || @@ -863,7 +875,7 @@ chat.openapi(completions, async (c) => { // Find the cheapest among the suitable providers for this model for (const provider of suitableProviders) { const totalPrice = - ((provider.inputPrice || 0) + (provider.outputPrice || 0)) / 2; + ((provider.inputPrice ?? 0) + (provider.outputPrice ?? 0)) / 2; if (totalPrice < lowestPrice) { lowestPrice = totalPrice; @@ -880,6 +892,7 @@ chat.openapi(completions, async (c) => { const metricsCombinations = selectedProviders.map((p) => ({ modelId: selectedModel.id, providerId: p.providerId, + region: (p as ProviderModelMapping).region, })); const metricsMap = await getProviderMetricsForCombinations(metricsCombinations); @@ -932,7 +945,10 @@ chat.openapi(completions, async (c) => { // Fallback case: look up the default model definition const fallbackModelDef = models.find((m) => m.id === "gpt-5-nano"); if (fallbackModelDef) { - modelInfo = fallbackModelDef; + modelInfo = { + ...fallbackModelDef, + providers: expandAllProviderRegions(fallbackModelDef.providers), + }; } } // Clear requestedProvider so retry/fallback logic knows this was auto-routed @@ -984,7 +1000,7 @@ chat.openapi(completions, async (c) => { { modelId: baseModelId, providerId: usedProvider }, ]); - const metrics = metricsMap.get(`${baseModelId}:${usedProvider}`); + const metrics = metricsMap.get(metricsKey(baseModelId, usedProvider)); // If we have metrics and uptime is below 90%, route to an alternative if (metrics && metrics.uptime !== undefined && metrics.uptime < 90) { @@ -1061,6 +1077,7 @@ chat.openapi(completions, async (c) => { const metricsCombinations = availableModelProviders.map((p) => ({ modelId: modelWithPricing.id, providerId: p.providerId, + region: (p as ProviderModelMapping).region, })); const allMetricsMap = await getProviderMetricsForCombinations(metricsCombinations); @@ -1070,7 +1087,11 @@ chat.openapi(completions, async (c) => { const betterUptimeProviders = availableModelProviders.filter( (p) => { const providerMetrics = allMetricsMap.get( - `${modelWithPricing.id}:${p.providerId}`, + metricsKey( + modelWithPricing.id, + p.providerId, + (p as ProviderModelMapping).region, + ), ); // If no metrics, assume the provider is healthy (100% uptime) // If has metrics, only include if uptime is better than original @@ -1263,6 +1284,7 @@ chat.openapi(completions, async (c) => { const metricsCombinations = availableModelProviders.map((p) => ({ modelId: modelWithPricing.id, providerId: p.providerId, + region: (p as ProviderModelMapping).region, })); const metricsMap = await getProviderMetricsForCombinations(metricsCombinations); @@ -1321,13 +1343,20 @@ chat.openapi(completions, async (c) => { const metricsCombinations = allModelProviders.map((p) => ({ modelId: baseModelId, providerId: p.providerId, + region: (p as ProviderModelMapping).region, })); metricsMap = await getProviderMetricsForCombinations(metricsCombinations); } // Build provider scores for all providers (including deactivated) with default values for missing metrics const allProviderScores = allModelProviders.map((p) => { - const metrics = metricsMap.get(`${baseModelId}:${p.providerId}`); + const metrics = metricsMap.get( + metricsKey( + baseModelId, + p.providerId, + (p as ProviderModelMapping).region, + ), + ); const price = (p.inputPrice ?? 0) + (p.outputPrice ?? 0); const isSelected = p.providerId === usedProvider; return { @@ -2434,7 +2463,9 @@ chat.openapi(completions, async (c) => { frequency_penalty = ctx.frequency_penalty; presence_penalty = ctx.presence_penalty; } catch { - failedProviderIds.add(nextProvider.providerId); + failedProviderIds.add( + providerRetryKey(nextProvider.providerId, nextProvider.region), + ); // Don't consume a retry slot for context-resolution failures retryAttempt--; continue; @@ -2594,7 +2625,9 @@ chat.openapi(completions, async (c) => { error_type: getErrorType(0), succeeded: false, }); - failedProviderIds.add(usedProvider); + failedProviderIds.add( + providerRetryKey(usedProvider, usedRegion), + ); continue; } @@ -2878,7 +2911,9 @@ chat.openapi(completions, async (c) => { error_type: getErrorType(0), succeeded: false, }); - failedProviderIds.add(usedProvider); + failedProviderIds.add( + providerRetryKey(usedProvider, usedRegion), + ); continue; } @@ -3048,7 +3083,7 @@ chat.openapi(completions, async (c) => { error_type: getErrorType(res.status), succeeded: false, }); - failedProviderIds.add(usedProvider); + failedProviderIds.add(providerRetryKey(usedProvider, usedRegion)); continue; } @@ -5134,7 +5169,9 @@ chat.openapi(completions, async (c) => { presence_penalty = ctx.presence_penalty; usedRegion = ctx.usedRegion; } catch { - failedProviderIds.add(nextProvider.providerId); + failedProviderIds.add( + providerRetryKey(nextProvider.providerId, nextProvider.region), + ); // Don't consume a retry slot for context-resolution failures retryAttempt--; continue; @@ -5325,7 +5362,7 @@ chat.openapi(completions, async (c) => { error_type: getErrorType(0), succeeded: false, }); - failedProviderIds.add(usedProvider); + failedProviderIds.add(providerRetryKey(usedProvider, usedRegion)); continue; } @@ -5754,7 +5791,7 @@ chat.openapi(completions, async (c) => { error_type: getErrorType(res.status), succeeded: false, }); - failedProviderIds.add(usedProvider); + failedProviderIds.add(providerRetryKey(usedProvider, usedRegion)); continue; } diff --git a/apps/gateway/src/chat/tools/resolve-provider-context.ts b/apps/gateway/src/chat/tools/resolve-provider-context.ts index 552048544d..9ace4602d1 100644 --- a/apps/gateway/src/chat/tools/resolve-provider-context.ts +++ b/apps/gateway/src/chat/tools/resolve-provider-context.ts @@ -12,13 +12,12 @@ import { } from "@llmgateway/actions"; import { type BaseMessage, - getProviderEnvValue, + getRegionSpecificEnvValue, hasMaxTokens, type ModelDefinition, type OpenAIRequestBody, type OpenAIToolInput, type Provider, - type ProviderDefinition, type ProviderModelMapping, type ProviderRequestBody, providers, @@ -112,7 +111,7 @@ interface OrgInfo { * Used by the retry loop to quickly set up a new provider context on fallback. */ export async function resolveProviderContext( - providerMapping: { providerId: string; modelName: string }, + providerMapping: { providerId: string; modelName: string; region?: string }, project: ProjectInfo, organization: OrgInfo, modelInfo: ModelDefinition, @@ -178,48 +177,23 @@ export async function resolveProviderContext( } // --- Look up the specific provider mapping for the selected provider --- + // modelInfo.providers is already expanded (regions flattened into separate entries) + const usedRegion = providerMapping.region; const providerMappingForSelected = modelInfo.providers.find( - (p) => p.providerId === usedProvider && p.modelName === usedModel, + (p) => + p.providerId === usedProvider && + p.modelName === usedModel && + (p as ProviderModelMapping).region === usedRegion, ); - // --- Region resolution --- - // Only resolve and use a region when the model explicitly declares supported regions. - // Models without regions use the provider's default endpoint (current behavior). - const modelRegions = (providerMappingForSelected as ProviderModelMapping) - ?.regions; - let usedRegion: string | undefined; - if (modelRegions && modelRegions.length > 0) { - const providerDef = providers.find((p) => p.id === usedProvider) as - | ProviderDefinition - | undefined; - const regionConfig = providerDef?.regionConfig; - if (regionConfig) { - const optionsKey = regionConfig.optionsKey; - const keyOptions = providerKey?.options as - | Record - | null - | undefined; - usedRegion = - keyOptions?.[optionsKey] ?? - getProviderEnvValue(usedProvider as Provider, "region", configIndex) ?? - regionConfig.defaultRegion; - - // Validate that the selected region is supported by this model - const isValidRegion = modelRegions.some((r) => r.id === usedRegion); - if (!isValidRegion) { - const validIds = modelRegions.map((r) => r.id).join(", "); - throw new HTTPException(400, { - message: `Model "${baseModelName}" is not available in region "${usedRegion}". Available regions: ${validIds}`, - }); - } + // Override token with region-specific env var if available (credits/hybrid mode) + if (usedRegion && !providerKey) { + const regionToken = getRegionSpecificEnvValue(usedProvider, usedRegion); + if (regionToken) { + usedToken = regionToken; } } - // --- Resolve region data for post-selection overrides (e.g. maxOutput) --- - const regionData = usedRegion - ? modelRegions?.find((r) => r.id === usedRegion) - : undefined; - // --- Check if model supports reasoning (from selected provider, not any) --- const supportsReasoning = (providerMappingForSelected as ProviderModelMapping)?.reasoning === true; @@ -299,9 +273,9 @@ export async function resolveProviderContext( // --- max_tokens validation --- if (max_tokens !== undefined && providerMappingForSelected) { - const effectiveMaxOutput = - regionData?.maxOutput ?? - (providerMappingForSelected as ProviderModelMapping).maxOutput; + const effectiveMaxOutput = ( + providerMappingForSelected as ProviderModelMapping + ).maxOutput; if (effectiveMaxOutput !== undefined) { if (max_tokens > effectiveMaxOutput) { // Silently cap to max output instead of throwing on retry diff --git a/apps/gateway/src/chat/tools/retry-with-fallback.ts b/apps/gateway/src/chat/tools/retry-with-fallback.ts index 4d16108e7b..87e85a094b 100644 --- a/apps/gateway/src/chat/tools/retry-with-fallback.ts +++ b/apps/gateway/src/chat/tools/retry-with-fallback.ts @@ -56,23 +56,41 @@ export function shouldRetryRequest(opts: { return true; } +/** + * Build a composite key for identifying a provider+region combination. + * Used by the retry system to track which provider-region pairs have been tried. + */ +export function providerRetryKey(providerId: string, region?: string): string { + return region ? `${providerId}:${region}` : providerId; +} + /** * Selects the next-best provider from the scored provider list, * excluding any providers that have already been tried and failed. * Returns the provider mapping with providerId and modelName, or null if none available. + * When region is present on scores, uses composite providerId:region keys for deduplication. */ export function selectNextProvider( - providerScores: Array<{ providerId: string; score: number }>, + providerScores: Array<{ + providerId: string; + score: number; + region?: string; + }>, failedProviders: Set, - modelProviders: Array<{ providerId: string; modelName: string }>, -): { providerId: string; modelName: string } | null { + modelProviders: Array<{ + providerId: string; + modelName: string; + region?: string; + }>, +): { providerId: string; modelName: string; region?: string } | null { const sorted = [...providerScores].sort((a, b) => a.score - b.score); for (const score of sorted) { - if (failedProviders.has(score.providerId)) { + const key = providerRetryKey(score.providerId, score.region); + if (failedProviders.has(key)) { continue; } const mapping = modelProviders.find( - (p) => p.providerId === score.providerId, + (p) => p.providerId === score.providerId && p.region === score.region, ); if (mapping) { return mapping; diff --git a/apps/gateway/src/lib/costs.ts b/apps/gateway/src/lib/costs.ts index 5443e4e6f7..4f9a82ac45 100644 --- a/apps/gateway/src/lib/costs.ts +++ b/apps/gateway/src/lib/costs.ts @@ -8,7 +8,6 @@ import { models, type PricingTier, type ToolCall, - type ProviderRegion, } from "@llmgateway/models"; // Define ChatMessage type to match what gpt-tokenizer expects @@ -251,32 +250,14 @@ export async function calculateCosts( }; } - // Resolve region-specific pricing when available. - // When a region is specified, look it up directly. When no region is specified - // but the mapping defines regions, fall back to regions[0] (the default region). - // This lets model definitions omit top-level inputPrice/outputPrice/pricingTiers - // when regions are defined — the first region is the canonical default. - let regionPricing: ProviderRegion | undefined; - if (providerInfo.regions && providerInfo.regions.length > 0) { - if (region) { - regionPricing = providerInfo.regions.find((r) => r.id === region); - if (!regionPricing) { - logger.error( - `Region "${region}" not found in pricing for model "${model}" provider "${provider}". This indicates a bug — region should have been validated upstream.`, - ); - } - } else { - regionPricing = providerInfo.regions[0]; - } - } - // Get pricing based on token count (supports tiered pricing) - // Region-specific pricing takes precedence over mapping-level pricing + // Each region is now a separate provider mapping with its own pricing, + // so no region-specific lookup is needed here. const pricing = getPricingForTokenCount( - regionPricing?.pricingTiers ?? providerInfo.pricingTiers, - regionPricing?.inputPrice ?? providerInfo.inputPrice ?? 0, - regionPricing?.outputPrice ?? providerInfo.outputPrice ?? 0, - regionPricing?.cachedInputPrice ?? providerInfo.cachedInputPrice, + providerInfo.pricingTiers, + providerInfo.inputPrice ?? 0, + providerInfo.outputPrice ?? 0, + providerInfo.cachedInputPrice, calculatedPromptTokens, ); @@ -285,14 +266,11 @@ export async function calculateCosts( const cachedInputPrice = new Decimal( pricing.cachedInputPrice ?? pricing.inputPrice, ); - const requestPrice = new Decimal( - regionPricing?.requestPrice ?? providerInfo.requestPrice ?? 0, - ); + const requestPrice = new Decimal(providerInfo.requestPrice ?? 0); // Get effective discount (checks org-specific, global, then hardcoded) // Pass both the root model ID and the provider-specific model name for matching - const hardcodedDiscount = - regionPricing?.discount ?? providerInfo.discount ?? 0; + const hardcodedDiscount = providerInfo.discount ?? 0; const effectiveDiscountResult = await getEffectiveDiscount( organizationId, provider, @@ -396,9 +374,7 @@ export async function calculateCosts( const requestCost = requestPrice.times(discountMultiplier); // Calculate web search cost - const webSearchPrice = new Decimal( - regionPricing?.webSearchPrice ?? (providerInfo as any).webSearchPrice ?? 0, - ); + const webSearchPrice = new Decimal((providerInfo as any).webSearchPrice ?? 0); const webSearchCost = webSearchCount && webSearchCount > 0 ? webSearchPrice.times(webSearchCount).times(discountMultiplier) diff --git a/apps/playground/src/lib/api/v1.d.ts b/apps/playground/src/lib/api/v1.d.ts index 22986d515b..dd0adf1a82 100644 --- a/apps/playground/src/lib/api/v1.d.ts +++ b/apps/playground/src/lib/api/v1.d.ts @@ -6923,6 +6923,7 @@ export interface operations { modelId: string; providerId: string; modelName: string; + region: string | null; inputPrice: string | null; outputPrice: string | null; cachedInputPrice: string | null; diff --git a/apps/ui/src/components/models/model-comparison.tsx b/apps/ui/src/components/models/model-comparison.tsx index 5f384d8051..5583b53558 100644 --- a/apps/ui/src/components/models/model-comparison.tsx +++ b/apps/ui/src/components/models/model-comparison.tsx @@ -343,10 +343,7 @@ function collectModelDetail(modelId?: ModelId): ModelDetail | undefined { requestPrice: getPricingSummary(providersWithInfo, "requestPrice"), imageInputPrice: getPricingSummary(providersWithInfo, "imageInputPrice"), hasTieredPricing: providersWithInfo.some( - (p) => - (p.pricingTiers ?? p.regions?.[0]?.pricingTiers)?.length !== - undefined && - (p.pricingTiers ?? p.regions?.[0]?.pricingTiers)!.length > 1, + (p) => p.pricingTiers && p.pricingTiers.length > 1, ), }; @@ -554,8 +551,7 @@ function renderRowValue( getProviderPricingSummary(selectedProvider, "inputPrice") ?? detail.aggregated.inputPrice; const tiers = selectedProvider - ? (selectedProvider.pricingTiers ?? - selectedProvider.regions?.[0]?.pricingTiers) + ? selectedProvider.pricingTiers : undefined; const hasTiered = selectedProvider ? tiers && tiers.length > 1 @@ -567,8 +563,7 @@ function renderRowValue( getProviderPricingSummary(selectedProvider, "outputPrice") ?? detail.aggregated.outputPrice; const tiers = selectedProvider - ? (selectedProvider.pricingTiers ?? - selectedProvider.regions?.[0]?.pricingTiers) + ? selectedProvider.pricingTiers : undefined; const hasTiered = selectedProvider ? tiers && tiers.length > 1 diff --git a/apps/ui/src/components/models/model-provider-card.tsx b/apps/ui/src/components/models/model-provider-card.tsx index d45a3996ac..decc0ff2d9 100644 --- a/apps/ui/src/components/models/model-provider-card.tsx +++ b/apps/ui/src/components/models/model-provider-card.tsx @@ -497,8 +497,7 @@ export function ModelProviderCard({
)} {(() => { - const tiers = - provider.pricingTiers ?? provider.regions?.[0]?.pricingTiers; + const tiers = provider.pricingTiers; if (!tiers || tiers.length <= 1) { return null; } diff --git a/apps/ui/src/components/shared/model-card.tsx b/apps/ui/src/components/shared/model-card.tsx index 396d18d5e8..dc2864f92f 100644 --- a/apps/ui/src/components/shared/model-card.tsx +++ b/apps/ui/src/components/shared/model-card.tsx @@ -134,8 +134,7 @@ export function ModelCard({ modelName, providers }: ModelCardProps) { provider.requestPrice !== 0 && ` / $${(provider.requestPrice * 1000).toFixed(2)} per 1K req`}

- {((provider.pricingTiers ?? provider.regions?.[0]?.pricingTiers) - ?.length ?? 0) > 1 && ( + {(provider.pricingTiers?.length ?? 0) > 1 && (

Tiered pricing available

diff --git a/apps/ui/src/lib/api/v1.d.ts b/apps/ui/src/lib/api/v1.d.ts index 22986d515b..dd0adf1a82 100644 --- a/apps/ui/src/lib/api/v1.d.ts +++ b/apps/ui/src/lib/api/v1.d.ts @@ -6923,6 +6923,7 @@ export interface operations { modelId: string; providerId: string; modelName: string; + region: string | null; inputPrice: string | null; outputPrice: string | null; cachedInputPrice: string | null; diff --git a/apps/ui/src/lib/fetch-models.ts b/apps/ui/src/lib/fetch-models.ts index c555459baf..1b6ed5ed71 100644 --- a/apps/ui/src/lib/fetch-models.ts +++ b/apps/ui/src/lib/fetch-models.ts @@ -21,6 +21,7 @@ export interface ApiModelProviderMapping { modelId: string; providerId: string; modelName: string; + region?: string | null; inputPrice: string | null; outputPrice: string | null; cachedInputPrice: string | null; diff --git a/apps/worker/src/services/sync-models.ts b/apps/worker/src/services/sync-models.ts index c9d6e58ac6..c17223197f 100644 --- a/apps/worker/src/services/sync-models.ts +++ b/apps/worker/src/services/sync-models.ts @@ -8,9 +8,14 @@ import { and, sql, isNotNull, + isNull, } from "@llmgateway/db"; import { logger } from "@llmgateway/logger"; -import { providers, models } from "@llmgateway/models"; +import { + providers, + models, + expandAllProviderRegions, +} from "@llmgateway/models"; export async function syncProvidersAndModels() { logger.info("Starting providers and models sync..."); @@ -93,7 +98,12 @@ export async function syncProvidersAndModels() { }); if (modelDef.providers && modelDef.providers.length > 0) { - for (const mapping of modelDef.providers) { + const expandedProviders = expandAllProviderRegions(modelDef.providers); + for (const mapping of expandedProviders) { + const mappingRegion = + "region" in mapping + ? (mapping.region as string | undefined) + : undefined; const mappings = await database .select() .from(modelProviderMapping) @@ -101,6 +111,9 @@ export async function syncProvidersAndModels() { and( eq(modelProviderMapping.modelId, modelDef.id), eq(modelProviderMapping.providerId, mapping.providerId), + mappingRegion + ? eq(modelProviderMapping.region, mappingRegion) + : isNull(modelProviderMapping.region), ), ) .limit(1); @@ -113,6 +126,7 @@ export async function syncProvidersAndModels() { .update(modelProviderMapping) .set({ modelName: mapping.modelName, + region: mappingRegion ?? null, inputPrice: "inputPrice" in mapping && mapping.inputPrice !== undefined ? mapping.inputPrice.toString() @@ -197,6 +211,7 @@ export async function syncProvidersAndModels() { modelId: modelDef.id, providerId: mapping.providerId, modelName: mapping.modelName, + region: mappingRegion ?? undefined, inputPrice: "inputPrice" in mapping && mapping.inputPrice !== undefined ? mapping.inputPrice.toString() diff --git a/packages/actions/src/get-cheapest-from-available-providers.ts b/packages/actions/src/get-cheapest-from-available-providers.ts index de37c4af8e..e6134e6040 100644 --- a/packages/actions/src/get-cheapest-from-available-providers.ts +++ b/packages/actions/src/get-cheapest-from-available-providers.ts @@ -1,3 +1,4 @@ +import { type ProviderMetrics, metricsKey } from "@llmgateway/db"; import { getProviderDefinition, type ProviderModelMapping, @@ -5,8 +6,6 @@ import { type ModelWithPricing, } from "@llmgateway/models"; -import type { ProviderMetrics } from "@llmgateway/db"; - interface ProviderScore { provider: T; score: number; @@ -64,6 +63,7 @@ export interface RoutingMetadata { selectionReason: string; providerScores: Array<{ providerId: string; + region?: string; score: number; uptime?: number; latency?: number; @@ -128,7 +128,9 @@ export function getCheapestFromAvailableProviders< // Filter out unstable and experimental providers const stableProviders = availableModelProviders.filter((provider) => { const providerInfo = modelWithPricing.providers.find( - (p) => p.providerId === provider.providerId, + (p) => + p.providerId === provider.providerId && + (p as ProviderModelMapping).region === provider.region, ); const providerStability = providerInfo && "stability" in providerInfo @@ -176,7 +178,9 @@ export function getCheapestFromAvailableProviders< for (const provider of stableProviders) { const providerInfo = modelWithPricing.providers.find( - (p) => p.providerId === provider.providerId, + (p) => + p.providerId === provider.providerId && + (p as ProviderModelMapping).region === provider.region, ); const discount = (providerInfo as ProviderModelMapping)?.discount ?? 0; const discountMultiplier = 1 - discount; @@ -185,8 +189,12 @@ export function getCheapestFromAvailableProviders< 2) * discountMultiplier; - const metricsKey = `${modelWithPricing.id}:${provider.providerId}`; - const metrics = metricsMap.get(metricsKey); + const mKey = metricsKey( + modelWithPricing.id, + provider.providerId, + provider.region, + ); + const metrics = metricsMap.get(mKey); providerScores.push({ provider, @@ -299,6 +307,7 @@ export function getCheapestFromAvailableProviders< const priority = providerDef?.priority ?? 1; return { providerId: p.provider.providerId, + region: p.provider.region, score: Number(p.score.toFixed(3)), uptime: p.uptime, latency: p.latency, @@ -327,6 +336,7 @@ function selectByPriceOnly( const providerPrices: Array<{ providerId: string; + region?: string; price: number; effectivePrice: number; priority: number; @@ -334,7 +344,9 @@ function selectByPriceOnly( for (const provider of stableProviders) { const providerInfo = modelWithPricing.providers.find( - (p) => p.providerId === provider.providerId, + (p) => + p.providerId === provider.providerId && + (p as ProviderModelMapping).region === provider.region, ); const discount = (providerInfo as ProviderModelMapping)?.discount ?? 0; const discountMultiplier = 1 - discount; @@ -350,6 +362,7 @@ function selectByPriceOnly( providerPrices.push({ providerId: provider.providerId, + region: provider.region, price: totalPrice, effectivePrice, priority, @@ -367,6 +380,7 @@ function selectByPriceOnly( selectionReason: "price-only-no-metrics", providerScores: providerPrices.map((p) => ({ providerId: p.providerId, + region: p.region, score: 0, price: p.price, priority: p.priority, diff --git a/packages/db/src/provider-metrics.ts b/packages/db/src/provider-metrics.ts index 5e786b57a7..c9315b4e22 100644 --- a/packages/db/src/provider-metrics.ts +++ b/packages/db/src/provider-metrics.ts @@ -1,4 +1,4 @@ -import { and, eq, sql } from "drizzle-orm"; +import { and, eq, isNull, sql } from "drizzle-orm"; import { db } from "./db.js"; import { modelProviderMapping } from "./schema.js"; @@ -6,18 +6,30 @@ import { modelProviderMapping } from "./schema.js"; export interface ProviderMetrics { providerId: string; modelId: string; + region?: string; uptime?: number; // Percentage (0-100, undefined = no data) averageLatency?: number; // Milliseconds (undefined = no data) throughput?: number; // Tokens per second (undefined = no data) totalRequests: number; } +/** + * Build a metrics map key from modelId, providerId, and optional region. + */ +export function metricsKey( + modelId: string, + providerId: string, + region?: string | null, +): string { + return `${modelId}:${providerId}:${region ?? ""}`; +} + /** * Fetches pre-computed routing metrics for all model-provider mappings. * Metrics are computed by the worker with time-tier weighting * (last 1 min = 10x, last 5 min = 3x, last hour = 1x). * - * @returns Map of "modelId:providerId" to metrics + * @returns Map of "modelId:providerId:region" to metrics */ export async function getProviderMetrics(): Promise< Map @@ -26,6 +38,7 @@ export async function getProviderMetrics(): Promise< .select({ modelId: modelProviderMapping.modelId, providerId: modelProviderMapping.providerId, + region: modelProviderMapping.region, routingUptime: modelProviderMapping.routingUptime, routingLatency: modelProviderMapping.routingLatency, routingThroughput: modelProviderMapping.routingThroughput, @@ -45,10 +58,11 @@ export async function getProviderMetrics(): Promise< continue; } - const key = `${row.modelId}:${row.providerId}`; + const key = metricsKey(row.modelId, row.providerId, row.region); metricsMap.set(key, { providerId: row.providerId, modelId: row.modelId, + region: row.region ?? undefined, uptime: row.routingUptime ?? undefined, averageLatency: row.routingLatency ?? undefined, throughput: row.routingThroughput ?? undefined, @@ -66,11 +80,15 @@ export async function getProviderMetrics(): Promise< * Metrics are computed by the worker with time-tier weighting * (last 1 min = 10x, last 5 min = 3x, last hour = 1x). * - * @param combinations - Array of {modelId, providerId} pairs to fetch metrics for - * @returns Map of "modelId:providerId" to metrics + * @param combinations - Array of {modelId, providerId, region?} to fetch metrics for + * @returns Map of "modelId:providerId:region" to metrics */ export async function getProviderMetricsForCombinations( - combinations: Array<{ modelId: string; providerId: string }>, + combinations: Array<{ + modelId: string; + providerId: string; + region?: string; + }>, ): Promise> { if (combinations.length === 0) { return new Map(); @@ -81,6 +99,9 @@ export async function getProviderMetricsForCombinations( and( sql`${modelProviderMapping.modelId} = ${combo.modelId}`, sql`${modelProviderMapping.providerId} = ${combo.providerId}`, + combo.region + ? sql`${modelProviderMapping.region} = ${combo.region}` + : isNull(modelProviderMapping.region), ), ); @@ -88,6 +109,7 @@ export async function getProviderMetricsForCombinations( .select({ modelId: modelProviderMapping.modelId, providerId: modelProviderMapping.providerId, + region: modelProviderMapping.region, routingUptime: modelProviderMapping.routingUptime, routingLatency: modelProviderMapping.routingLatency, routingThroughput: modelProviderMapping.routingThroughput, @@ -112,10 +134,11 @@ export async function getProviderMetricsForCombinations( continue; } - const key = `${row.modelId}:${row.providerId}`; + const key = metricsKey(row.modelId, row.providerId, row.region); metricsMap.set(key, { providerId: row.providerId, modelId: row.modelId, + region: row.region ?? undefined, uptime: row.routingUptime ?? undefined, averageLatency: row.routingLatency ?? undefined, throughput: row.routingThroughput ?? undefined, diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index 89854ca198..18c23095d0 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -821,6 +821,7 @@ export const modelProviderMapping = pgTable( .notNull() .references(() => provider.id, { onDelete: "cascade" }), modelName: text().notNull(), + region: text(), inputPrice: decimal(), outputPrice: decimal(), cachedInputPrice: decimal(), @@ -870,7 +871,7 @@ export const modelProviderMapping = pgTable( statsUpdatedAt: timestamp(), }, (table) => [ - unique().on(table.modelId, table.providerId), + unique().on(table.modelId, table.providerId, table.region), index("model_provider_mapping_status_idx").on(table.status), ], ); diff --git a/packages/models/src/index.ts b/packages/models/src/index.ts index 8681fb7296..d5712756b0 100644 --- a/packages/models/src/index.ts +++ b/packages/models/src/index.ts @@ -5,3 +5,4 @@ export * from "./providers.js"; export * from "./types.js"; export * from "./type-guards.js"; export * from "./helpers.js"; +export * from "./region-helpers.js"; diff --git a/packages/models/src/models.ts b/packages/models/src/models.ts index 8ea558434c..e3621b053e 100644 --- a/packages/models/src/models.ts +++ b/packages/models/src/models.ts @@ -246,10 +246,16 @@ export interface ProviderModelMapping { */ imageGenerations?: boolean; /** - * Available regions and their per-region pricing for this provider mapping. - * The first entry is the default region. Top-level inputPrice/outputPrice - * must match the first region's prices for backwards compatibility. - * When absent, the provider uses a single global endpoint with no region routing. + * Geographic region for this provider mapping. + * Set automatically when a mapping with `regions` is expanded into flat entries. + * When absent (undefined), the provider uses a single global endpoint. + */ + region?: string; + /** + * Available regions for this provider mapping. + * Each region can optionally override pricing and other properties. + * Properties not specified in a region entry are inherited from the parent mapping. + * At sync/routing time, each region is expanded into a separate DB row / candidate. */ regions?: ProviderRegion[]; } diff --git a/packages/models/src/models/alibaba.ts b/packages/models/src/models/alibaba.ts index 7e0d797e88..65c5470722 100644 --- a/packages/models/src/models/alibaba.ts +++ b/packages/models/src/models/alibaba.ts @@ -16,7 +16,7 @@ export const alibabaModels = [ inputPrice: 1.6 / 1e6, outputPrice: 6.4 / 1e6, regions: [ - { id: "singapore", inputPrice: 1.6 / 1e6, outputPrice: 6.4 / 1e6 }, + { id: "singapore" }, { id: "cn-beijing", inputPrice: 0.345 / 1e6, @@ -48,7 +48,7 @@ export const alibabaModels = [ inputPrice: 1.6 / 1e6, outputPrice: 6.4 / 1e6, regions: [ - { id: "singapore", inputPrice: 1.6 / 1e6, outputPrice: 6.4 / 1e6 }, + { id: "singapore" }, { id: "cn-beijing", inputPrice: 0.345 / 1e6, @@ -82,9 +82,6 @@ export const alibabaModels = [ regions: [ { id: "singapore", - inputPrice: 0.4 / 1e6, - outputPrice: 1.2 / 1e6, - cachedInputPrice: 0.08 / 1e6, pricingTiers: [ { name: "Up to 256K", @@ -174,9 +171,6 @@ export const alibabaModels = [ regions: [ { id: "singapore", - inputPrice: 0.4 / 1e6, - outputPrice: 1.2 / 1e6, - cachedInputPrice: 0.08 / 1e6, pricingTiers: [ { name: "Up to 256K", @@ -266,9 +260,6 @@ export const alibabaModels = [ regions: [ { id: "singapore", - inputPrice: 0.05 / 1e6, - outputPrice: 0.4 / 1e6, - cachedInputPrice: 0.01 / 1e6, pricingTiers: [ { name: "Up to 256K", @@ -378,7 +369,7 @@ export const alibabaModels = [ inputPrice: 0.05 / 1e6, outputPrice: 0.2 / 1e6, regions: [ - { id: "singapore", inputPrice: 0.05 / 1e6, outputPrice: 0.2 / 1e6 }, + { id: "singapore" }, { id: "cn-beijing", inputPrice: 0.044 / 1e6, @@ -1245,7 +1236,7 @@ export const alibabaModels = [ inputPrice: 0.6 / 1e6, outputPrice: 3.6 / 1e6, regions: [ - { id: "singapore", inputPrice: 0.6 / 1e6, outputPrice: 3.6 / 1e6 }, + { id: "singapore" }, { id: "us-virginia", inputPrice: 0.143 / 1e6, @@ -1405,7 +1396,7 @@ export const alibabaModels = [ inputPrice: 0.8 / 1e6, outputPrice: 2.4 / 1e6, regions: [ - { id: "singapore", inputPrice: 0.8 / 1e6, outputPrice: 2.4 / 1e6 }, + { id: "singapore" }, { id: "cn-beijing", inputPrice: 0.23 / 1e6, @@ -1438,9 +1429,7 @@ export const alibabaModels = [ discount: 0.2, inputPrice: 1.0 / 1e6, outputPrice: 5.0 / 1e6, - regions: [ - { id: "cn-beijing", inputPrice: 1.0 / 1e6, outputPrice: 5.0 / 1e6 }, - ], + regions: [{ id: "cn-beijing" }], requestPrice: 0, contextSize: 131072, maxOutput: 8192, @@ -1468,9 +1457,6 @@ export const alibabaModels = [ regions: [ { id: "singapore", - inputPrice: 0.3 / 1e6, - outputPrice: 1.5 / 1e6, - cachedInputPrice: 0.06 / 1e6, pricingTiers: [ { name: "Up to 32K", @@ -1602,9 +1588,6 @@ export const alibabaModels = [ regions: [ { id: "singapore", - inputPrice: 0.2 / 1e6, - outputPrice: 1.6 / 1e6, - cachedInputPrice: 0.04 / 1e6, pricingTiers: [ { name: "Up to 32K", @@ -1715,9 +1698,6 @@ export const alibabaModels = [ regions: [ { id: "singapore", - inputPrice: 0.05 / 1e6, - outputPrice: 0.4 / 1e6, - cachedInputPrice: 0.01 / 1e6, pricingTiers: [ { name: "Up to 32K", @@ -1930,9 +1910,6 @@ export const alibabaModels = [ regions: [ { id: "singapore", - inputPrice: 1.2 / 1e6, - outputPrice: 6.0 / 1e6, - cachedInputPrice: 0.24 / 1e6, pricingTiers: [ { name: "Up to 32K", diff --git a/packages/models/src/provider.ts b/packages/models/src/provider.ts index e32b90c87b..28661651a3 100644 --- a/packages/models/src/provider.ts +++ b/packages/models/src/provider.ts @@ -103,3 +103,25 @@ export function validateProviderEnv(provider: Provider): string[] { return errors; } + +/** + * Get a region-specific environment variable value. + * Checks for `{BASE_ENV_VAR}__{REGION}` first, then falls back to the base env var. + * Region is normalized to uppercase with hyphens replaced by underscores. + * + * Example: getRegionSpecificEnvValue("alibaba", "us-virginia") + * → checks LLM_ALIBABA_API_KEY__US_VIRGINIA, then LLM_ALIBABA_API_KEY + */ +export function getRegionSpecificEnvValue( + provider: Provider, + region: string, +): string | undefined { + const baseEnvVar = getProviderEnvVar(provider); + if (!baseEnvVar) { + return undefined; + } + const regionSuffix = region.toUpperCase().replace(/-/g, "_"); + return ( + process.env[`${baseEnvVar}__${regionSuffix}`] ?? process.env[baseEnvVar] + ); +} diff --git a/packages/models/src/region-helpers.ts b/packages/models/src/region-helpers.ts new file mode 100644 index 0000000000..ca275bea4d --- /dev/null +++ b/packages/models/src/region-helpers.ts @@ -0,0 +1,37 @@ +import type { ProviderModelMapping } from "./models.js"; + +/** + * Expands a single ProviderModelMapping with `regions` into multiple flat entries, + * one per region. Each region inherits all properties from the parent mapping + * and can override pricing and other region-specific properties. + * + * Mappings without `regions` are returned as-is in a single-element array. + */ +export function expandProviderRegions( + mapping: ProviderModelMapping, +): ProviderModelMapping[] { + if (!mapping.regions || mapping.regions.length === 0) { + return [mapping]; + } + + return mapping.regions.map(({ id, ...overrides }) => { + // Remove the `regions` array from the expanded entry to avoid confusion + const { regions: _, ...base } = mapping; + return { + ...base, + ...overrides, + region: id, + }; + }); +} + +/** + * Expands all provider mappings in a model's `providers` array. + * Mappings with `regions` are expanded into separate entries per region. + * Mappings without `regions` pass through unchanged. + */ +export function expandAllProviderRegions( + providers: ProviderModelMapping[], +): ProviderModelMapping[] { + return providers.flatMap(expandProviderRegions); +} diff --git a/packages/models/src/types.ts b/packages/models/src/types.ts index 4551dbe611..b7f59b3127 100644 --- a/packages/models/src/types.ts +++ b/packages/models/src/types.ts @@ -329,6 +329,7 @@ export interface ModelWithPricing { export interface AvailableModelProvider { providerId: string; modelName: string; + region?: string; } // Function type definitions From 34827ae65bf29d573c51f82946237bf66f230de6 Mon Sep 17 00:00:00 2001 From: romel lauron Date: Mon, 23 Mar 2026 01:34:16 +0800 Subject: [PATCH 07/55] fix: region-aware provider lookups in chat.ts Move usedRegion declaration earlier and set it at every scoring result point. Expand finalModelInfo.providers for region support. Add region matching to all .providers.find() calls to ensure the correct region entry is matched for capabilities and pricing. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/gateway/src/chat/chat.ts | 49 ++++++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/apps/gateway/src/chat/chat.ts b/apps/gateway/src/chat/chat.ts index 9207035330..8df95e737c 100644 --- a/apps/gateway/src/chat/chat.ts +++ b/apps/gateway/src/chat/chat.ts @@ -619,6 +619,7 @@ chat.openapi(completions, async (c) => { let usedProvider = requestedProvider; let usedModel: string = requestedModel; + let usedRegion: string | undefined; let routingMetadata: RoutingMetadata | undefined; // Extract retention level for data storage cost calculation @@ -906,6 +907,7 @@ chat.openapi(completions, async (c) => { if (cheapestResult) { usedProvider = cheapestResult.provider.providerId; usedModel = cheapestResult.provider.modelName; + usedRegion = (cheapestResult.provider as ProviderModelMapping).region; routingMetadata = { ...cheapestResult.metadata, ...(noFallback ? { noFallback: true } : {}), @@ -1191,6 +1193,8 @@ chat.openapi(completions, async (c) => { if (iamFilteredModelProviders.length === 1) { usedProvider = iamFilteredModelProviders[0].providerId; usedModel = iamFilteredModelProviders[0].modelName; + usedRegion = (iamFilteredModelProviders[0] as ProviderModelMapping) + .region; } else { const providerIds = iamFilteredModelProviders.map((p) => p.providerId); const providerKeys = await findProviderKeysByProviders( @@ -1298,6 +1302,7 @@ chat.openapi(completions, async (c) => { if (cheapestResult) { usedProvider = cheapestResult.provider.providerId; usedModel = cheapestResult.provider.modelName; + usedRegion = (cheapestResult.provider as ProviderModelMapping).region; routingMetadata = { ...cheapestResult.metadata, ...(noFallback ? { noFallback: true } : {}), @@ -1305,10 +1310,14 @@ chat.openapi(completions, async (c) => { } else { usedProvider = availableModelProviders[0].providerId; usedModel = availableModelProviders[0].modelName; + usedRegion = (availableModelProviders[0] as ProviderModelMapping) + .region; } } else { usedProvider = availableModelProviders[0].providerId; usedModel = availableModelProviders[0].modelName; + usedRegion = (availableModelProviders[0] as ProviderModelMapping) + .region; } } } @@ -1398,13 +1407,19 @@ chat.openapi(completions, async (c) => { ], }; } else { - finalModelInfo = models.find( + const rawFinalModelInfo = models.find( (m) => m.id === usedModel || m.providers.some( (p) => p.modelName === usedModel && p.providerId === usedProvider, ), ); + if (rawFinalModelInfo) { + finalModelInfo = { + ...rawFinalModelInfo, + providers: expandAllProviderRegions(rawFinalModelInfo.providers), + }; + } } // Use the canonical model ID from finalModelInfo (looked up after routing) @@ -1413,7 +1428,10 @@ chat.openapi(completions, async (c) => { // Check if this is an image generation model const imageGenProviderMapping = finalModelInfo?.providers.find( - (p) => p.providerId === usedProvider && p.modelName === usedModel, + (p) => + p.providerId === usedProvider && + p.modelName === usedModel && + (p as ProviderModelMapping).region === usedRegion, ); let isImageGeneration = (imageGenProviderMapping as ProviderModelMapping)?.imageGenerations === @@ -1454,8 +1472,6 @@ chat.openapi(completions, async (c) => { let usedToken: string | undefined; let configIndex = 0; // Index for round-robin environment variables let envVarName: string | undefined; // Environment variable name for health tracking - let usedRegion: string | undefined; - if ( project.mode === "credits" && (usedProvider === "custom" || usedProvider === "llmgateway") @@ -1629,7 +1645,10 @@ chat.openapi(completions, async (c) => { // Check if the selected provider supports reasoning (from specific mapping, not any) const selectedProviderMapping = modelInfo.providers.find( - (p) => p.providerId === usedProvider && p.modelName === usedModel, + (p) => + p.providerId === usedProvider && + p.modelName === usedModel && + (p as ProviderModelMapping).region === usedRegion, ); let supportsReasoning = (selectedProviderMapping as ProviderModelMapping)?.reasoning === true; @@ -2053,7 +2072,10 @@ chat.openapi(completions, async (c) => { if (max_tokens !== undefined && finalModelInfo) { // Find the provider mapping for the used provider const providerMapping = finalModelInfo.providers.find( - (p) => p.providerId === usedProvider && p.modelName === usedModel, + (p) => + p.providerId === usedProvider && + p.modelName === usedModel && + (p as ProviderModelMapping).region === usedRegion, ); if ( @@ -2088,7 +2110,10 @@ chat.openapi(completions, async (c) => { // Check if effort parameter is supported by the specific provider being used if (effort !== undefined && finalModelInfo) { const providerMapping = finalModelInfo.providers.find( - (p) => p.providerId === usedProvider && p.modelName === usedModel, + (p) => + p.providerId === usedProvider && + p.modelName === usedModel && + (p as ProviderModelMapping).region === usedRegion, ); if (providerMapping) { @@ -2114,7 +2139,10 @@ chat.openapi(completions, async (c) => { // Strip unsupported parameters based on model's supportedParameters if (finalModelInfo) { const providerMapping = finalModelInfo.providers.find( - (p) => p.providerId === usedProvider && p.modelName === usedModel, + (p) => + p.providerId === usedProvider && + p.modelName === usedModel && + (p as ProviderModelMapping).region === usedRegion, ); const supported = (providerMapping as ProviderModelMapping | undefined) ?.supportedParameters; @@ -2262,7 +2290,10 @@ chat.openapi(completions, async (c) => { ) { // Find the provider mapping for the used provider const providerMapping = finalModelInfo.providers.find( - (p) => p.providerId === usedProvider && p.modelName === usedModel, + (p) => + p.providerId === usedProvider && + p.modelName === usedModel && + (p as ProviderModelMapping).region === usedRegion, ); if ( providerMapping && From d7b121846826af95ddaa279bfb33b38339a1b871 Mon Sep 17 00:00:00 2001 From: romel lauron Date: Tue, 24 Mar 2026 23:18:12 +0800 Subject: [PATCH 08/55] fix: region filtering and type cleanup - Remove unnecessary `as ProviderModelMapping` casts across chat.ts, resolve-provider-context.ts, and get-cheapest-from-available-providers.ts - Remove unused `region` param from calculateCosts - Add region validation in resolveProviderContext - Filter region candidates by available env keys in credits/hybrid mode to avoid wasting retries - Add hasRegionSpecificEnvKey helper - Fix provider-metrics tests for new key format - Simplify sync-models region detection - Add region/stability to ModelWithPricing type Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/gateway/src/chat/chat.ts | 190 +++++++++--------- .../chat/tools/resolve-provider-context.ts | 30 ++- apps/gateway/src/lib/costs.ts | 3 - .../create-provider-key-dialog.tsx | 9 +- apps/worker/src/services/sync-models.ts | 5 +- ee/admin/src/lib/api/v1.d.ts | 1 + .../get-cheapest-from-available-providers.ts | 19 +- packages/db/src/provider-metrics.spec.ts | 46 +++-- packages/models/src/provider.ts | 16 ++ packages/models/src/types.ts | 2 + 10 files changed, 171 insertions(+), 150 deletions(-) diff --git a/apps/gateway/src/chat/chat.ts b/apps/gateway/src/chat/chat.ts index 8df95e737c..40effdb653 100644 --- a/apps/gateway/src/chat/chat.ts +++ b/apps/gateway/src/chat/chat.ts @@ -58,9 +58,11 @@ import { getModelStreamingSupport, hasMaxTokens, hasProviderEnvironmentToken, + hasRegionSpecificEnvKey, type ModelDefinition, models, type Provider, + type ProviderDefinition, type ProviderModelMapping, type ProviderRequestBody, providers, @@ -115,6 +117,36 @@ import { validateModelCapabilities } from "./tools/validate-model-capabilities.j import type { OriginalRequestParams } from "./tools/resolve-provider-context.js"; import type { ServerTypes } from "@/vars.js"; +/** + * Filter expanded region entries to only those with available API keys. + * - Non-regional mappings (no region) pass through unchanged. + * - The default region for a provider always passes (uses the base env key). + * - Non-default regions only pass if a region-specific env key exists + * (e.g. LLM_ALIBABA_API_KEY__US_VIRGINIA). + */ +function filterRegionsByAvailableKeys( + expandedProviders: ProviderModelMapping[], +): ProviderModelMapping[] { + return expandedProviders.filter((mapping) => { + if (!mapping.region) { + return true; + } + const providerDef = providers.find((p) => p.id === mapping.providerId) as + | ProviderDefinition + | undefined; + if (!providerDef?.regionConfig) { + return true; + } + if (mapping.region === providerDef.regionConfig.defaultRegion) { + return true; + } + return hasRegionSpecificEnvKey( + mapping.providerId as Provider, + mapping.region, + ); + }); +} + // Pre-compiled regex pattern to avoid recompilation per request const SSE_FIELD_PATTERN = /^[a-zA-Z_-]+:\s*/; @@ -455,7 +487,7 @@ chat.openapi(completions, async (c) => { ...modelInfoResult.modelInfo, providers: expandAllProviderRegions(modelInfoResult.modelInfo.providers), }; - const allModelProviders = expandAllProviderRegions( + let allModelProviders = expandAllProviderRegions( modelInfoResult.allModelProviders, ); let requestedProvider = modelInfoResult.requestedProvider; @@ -527,6 +559,16 @@ chat.openapi(completions, async (c) => { }); } + // In credits/hybrid mode, only keep regions that have env keys configured. + // This avoids wasting retries on regions where the base key won't work. + if (project.mode !== "api-keys") { + modelInfo = { + ...modelInfo, + providers: filterRegionsByAvailableKeys(modelInfo.providers), + }; + allModelProviders = filterRegionsByAvailableKeys(allModelProviders); + } + // Fetch organization for coding model restriction check and credit validation const organization = await findOrganizationById(project.organizationId); @@ -782,9 +824,12 @@ chat.openapi(completions, async (c) => { const candidateAllowedProviders = candidateIam.allowedProviders; // Expand region entries into separate candidates for scoring - const expandedCandidateProviders = expandAllProviderRegions( - modelDef.providers, - ); + const expandedCandidateProviders = + project.mode === "api-keys" + ? expandAllProviderRegions(modelDef.providers) + : filterRegionsByAvailableKeys( + expandAllProviderRegions(modelDef.providers), + ); // Check if any of the model's providers are available const availableModelProviders = expandedCandidateProviders.filter( (provider) => @@ -796,10 +841,7 @@ chat.openapi(completions, async (c) => { // Filter by context size requirement, reasoning capability, and deprecation status const suitableProviders = availableModelProviders.filter((provider) => { // Skip deprecated provider mappings - if ( - (provider as ProviderModelMapping).deprecatedAt && - now > (provider as ProviderModelMapping).deprecatedAt! - ) { + if (provider.deprecatedAt && now > provider.deprecatedAt!) { return false; } @@ -808,25 +850,19 @@ chat.openapi(completions, async (c) => { const contextSizeMet = modelContextSize >= requiredContextSize; // If no_reasoning is true, exclude reasoning models - if ( - no_reasoning && - (provider as ProviderModelMapping).reasoning === true - ) { + if (no_reasoning && provider.reasoning === true) { return false; } // Check reasoning capability if reasoning_effort is specified - if ( - reasoning_effort !== undefined && - (provider as ProviderModelMapping).reasoning !== true - ) { + if (reasoning_effort !== undefined && provider.reasoning !== true) { return false; } // Check reasoning.max_tokens support if specified if ( reasoning_max_tokens !== undefined && - (provider as ProviderModelMapping).reasoningMaxTokens !== true + provider.reasoningMaxTokens !== true ) { return false; } @@ -834,16 +870,13 @@ chat.openapi(completions, async (c) => { // Check tool capability if tools or tool_choice is specified if ( (tools !== undefined || tool_choice !== undefined) && - (provider as ProviderModelMapping).tools !== true + provider.tools !== true ) { return false; } // Check web search capability if web search tool is requested - if ( - webSearchTool && - (provider as ProviderModelMapping).webSearch !== true - ) { + if (webSearchTool && provider.webSearch !== true) { return false; } @@ -852,20 +885,20 @@ chat.openapi(completions, async (c) => { response_format?.type === "json_object" || response_format?.type === "json_schema" ) { - if ((provider as ProviderModelMapping).jsonOutput !== true) { + if (provider.jsonOutput !== true) { return false; } } // Check JSON schema output capability if json_schema response format is requested if (response_format?.type === "json_schema") { - if ((provider as ProviderModelMapping).jsonOutputSchema !== true) { + if (provider.jsonOutputSchema !== true) { return false; } } // Check vision capability if images are present in messages - if (hasImages && (provider as ProviderModelMapping).vision !== true) { + if (hasImages && provider.vision !== true) { return false; } @@ -893,7 +926,7 @@ chat.openapi(completions, async (c) => { const metricsCombinations = selectedProviders.map((p) => ({ modelId: selectedModel.id, providerId: p.providerId, - region: (p as ProviderModelMapping).region, + region: p.region, })); const metricsMap = await getProviderMetricsForCombinations(metricsCombinations); @@ -907,7 +940,7 @@ chat.openapi(completions, async (c) => { if (cheapestResult) { usedProvider = cheapestResult.provider.providerId; usedModel = cheapestResult.provider.modelName; - usedRegion = (cheapestResult.provider as ProviderModelMapping).region; + usedRegion = cheapestResult.provider.region; routingMetadata = { ...cheapestResult.metadata, ...(noFallback ? { noFallback: true } : {}), @@ -1039,7 +1072,7 @@ chat.openapi(completions, async (c) => { } // If web search tool is requested, only include providers that support it if (webSearchTool) { - if ((provider as ProviderModelMapping).webSearch !== true) { + if (provider.webSearch !== true) { return false; } } @@ -1048,23 +1081,18 @@ chat.openapi(completions, async (c) => { response_format?.type === "json_object" || response_format?.type === "json_schema" ) { - if ((provider as ProviderModelMapping).jsonOutput !== true) { + if (provider.jsonOutput !== true) { return false; } } // If JSON schema output is requested, only include providers that support it if (response_format?.type === "json_schema") { - if ( - (provider as ProviderModelMapping).jsonOutputSchema !== true - ) { + if (provider.jsonOutputSchema !== true) { return false; } } // If images are present in messages, only include providers that support vision - if ( - hasImages && - (provider as ProviderModelMapping).vision !== true - ) { + if (hasImages && provider.vision !== true) { return false; } return true; @@ -1079,7 +1107,7 @@ chat.openapi(completions, async (c) => { const metricsCombinations = availableModelProviders.map((p) => ({ modelId: modelWithPricing.id, providerId: p.providerId, - region: (p as ProviderModelMapping).region, + region: p.region, })); const allMetricsMap = await getProviderMetricsForCombinations(metricsCombinations); @@ -1089,11 +1117,7 @@ chat.openapi(completions, async (c) => { const betterUptimeProviders = availableModelProviders.filter( (p) => { const providerMetrics = allMetricsMap.get( - metricsKey( - modelWithPricing.id, - p.providerId, - (p as ProviderModelMapping).region, - ), + metricsKey(modelWithPricing.id, p.providerId, p.region), ); // If no metrics, assume the provider is healthy (100% uptime) // If has metrics, only include if uptime is better than original @@ -1170,11 +1194,11 @@ chat.openapi(completions, async (c) => { let selectedMapping; if (reasoning_effort !== undefined) { selectedMapping = sameProviderMappings.find( - (p) => (p as ProviderModelMapping).reasoning === true, + (p) => p.reasoning === true, ); } else { selectedMapping = sameProviderMappings.find( - (p) => (p as ProviderModelMapping).reasoning !== true, + (p) => p.reasoning !== true, ); } if (selectedMapping) { @@ -1193,8 +1217,7 @@ chat.openapi(completions, async (c) => { if (iamFilteredModelProviders.length === 1) { usedProvider = iamFilteredModelProviders[0].providerId; usedModel = iamFilteredModelProviders[0].modelName; - usedRegion = (iamFilteredModelProviders[0] as ProviderModelMapping) - .region; + usedRegion = iamFilteredModelProviders[0].region; } else { const providerIds = iamFilteredModelProviders.map((p) => p.providerId); const providerKeys = await findProviderKeysByProviders( @@ -1220,7 +1243,7 @@ chat.openapi(completions, async (c) => { } // If web search tool is requested, only include providers that support it if (webSearchTool) { - if ((provider as ProviderModelMapping).webSearch !== true) { + if (provider.webSearch !== true) { return false; } } @@ -1229,23 +1252,23 @@ chat.openapi(completions, async (c) => { response_format?.type === "json_object" || response_format?.type === "json_schema" ) { - if ((provider as ProviderModelMapping).jsonOutput !== true) { + if (provider.jsonOutput !== true) { return false; } } // If JSON schema output is requested, also include providers that support it if (response_format?.type === "json_schema") { - if ((provider as ProviderModelMapping).jsonOutputSchema !== true) { + if (provider.jsonOutputSchema !== true) { return false; } } // If images are present in messages, only include providers that support vision - if (hasImages && (provider as ProviderModelMapping).vision !== true) { + if (hasImages && provider.vision !== true) { return false; } // If reasoning_effort is specified, only include providers with reasoning support if (reasoning_effort !== undefined) { - if ((provider as ProviderModelMapping).reasoning !== true) { + if (provider.reasoning !== true) { return false; } } @@ -1254,13 +1277,9 @@ chat.openapi(completions, async (c) => { if (reasoning_effort === undefined) { const hasNonReasoningAlternative = modelInfo.providers.some( (p) => - p.providerId === provider.providerId && - (p as ProviderModelMapping).reasoning !== true, + p.providerId === provider.providerId && p.reasoning !== true, ); - if ( - hasNonReasoningAlternative && - (provider as ProviderModelMapping).reasoning === true - ) { + if (hasNonReasoningAlternative && provider.reasoning === true) { return false; } } @@ -1288,7 +1307,7 @@ chat.openapi(completions, async (c) => { const metricsCombinations = availableModelProviders.map((p) => ({ modelId: modelWithPricing.id, providerId: p.providerId, - region: (p as ProviderModelMapping).region, + region: p.region, })); const metricsMap = await getProviderMetricsForCombinations(metricsCombinations); @@ -1302,7 +1321,7 @@ chat.openapi(completions, async (c) => { if (cheapestResult) { usedProvider = cheapestResult.provider.providerId; usedModel = cheapestResult.provider.modelName; - usedRegion = (cheapestResult.provider as ProviderModelMapping).region; + usedRegion = cheapestResult.provider.region; routingMetadata = { ...cheapestResult.metadata, ...(noFallback ? { noFallback: true } : {}), @@ -1310,14 +1329,12 @@ chat.openapi(completions, async (c) => { } else { usedProvider = availableModelProviders[0].providerId; usedModel = availableModelProviders[0].modelName; - usedRegion = (availableModelProviders[0] as ProviderModelMapping) - .region; + usedRegion = availableModelProviders[0].region; } } else { usedProvider = availableModelProviders[0].providerId; usedModel = availableModelProviders[0].modelName; - usedRegion = (availableModelProviders[0] as ProviderModelMapping) - .region; + usedRegion = availableModelProviders[0].region; } } } @@ -1352,7 +1369,7 @@ chat.openapi(completions, async (c) => { const metricsCombinations = allModelProviders.map((p) => ({ modelId: baseModelId, providerId: p.providerId, - region: (p as ProviderModelMapping).region, + region: p.region, })); metricsMap = await getProviderMetricsForCombinations(metricsCombinations); } @@ -1360,11 +1377,7 @@ chat.openapi(completions, async (c) => { // Build provider scores for all providers (including deactivated) with default values for missing metrics const allProviderScores = allModelProviders.map((p) => { const metrics = metricsMap.get( - metricsKey( - baseModelId, - p.providerId, - (p as ProviderModelMapping).region, - ), + metricsKey(baseModelId, p.providerId, p.region), ); const price = (p.inputPrice ?? 0) + (p.outputPrice ?? 0); const isSelected = p.providerId === usedProvider; @@ -1389,10 +1402,11 @@ chat.openapi(completions, async (c) => { // Update baseModelName to match the final usedModel after routing // Find the model definition that corresponds to the final usedModel - let finalModelInfo; + let finalModelInfo: ModelDefinition | undefined; if (usedProvider === "custom") { finalModelInfo = { - model: usedModel, + id: usedModel, + family: "custom", providers: [ { providerId: "custom" as const, @@ -1431,11 +1445,9 @@ chat.openapi(completions, async (c) => { (p) => p.providerId === usedProvider && p.modelName === usedModel && - (p as ProviderModelMapping).region === usedRegion, + p.region === usedRegion, ); - let isImageGeneration = - (imageGenProviderMapping as ProviderModelMapping)?.imageGenerations === - true; + let isImageGeneration = imageGenProviderMapping?.imageGenerations === true; // Create the model mapping values according to new schema let usedModelMapping = usedModel; // Store the original provider model name @@ -1451,7 +1463,7 @@ chat.openapi(completions, async (c) => { ) { // Check if the selected model supports reasoning const selectedModelSupportsReasoning = finalModelInfo.providers.some( - (provider) => (provider as ProviderModelMapping).reasoning === true, + (provider) => provider.reasoning === true, ); if (selectedModelSupportsReasoning) { @@ -1648,10 +1660,9 @@ chat.openapi(completions, async (c) => { (p) => p.providerId === usedProvider && p.modelName === usedModel && - (p as ProviderModelMapping).region === usedRegion, + p.region === usedRegion, ); - let supportsReasoning = - (selectedProviderMapping as ProviderModelMapping)?.reasoning === true; + let supportsReasoning = selectedProviderMapping?.reasoning === true; // Check if messages contain existing tool calls or tool results // If so, use Chat Completions API instead of Responses API @@ -1846,7 +1857,6 @@ chat.openapi(completions, async (c) => { inputImageCount, null, // webSearchCount project.organizationId, - usedRegion ?? null, ); await insertLog({ @@ -1997,7 +2007,6 @@ chat.openapi(completions, async (c) => { inputImageCount, null, // webSearchCount project.organizationId, - usedRegion ?? null, ); // Estimate cached response size based on content to avoid expensive stringify @@ -2075,7 +2084,7 @@ chat.openapi(completions, async (c) => { (p) => p.providerId === usedProvider && p.modelName === usedModel && - (p as ProviderModelMapping).region === usedRegion, + p.region === usedRegion, ); if ( @@ -2113,12 +2122,11 @@ chat.openapi(completions, async (c) => { (p) => p.providerId === usedProvider && p.modelName === usedModel && - (p as ProviderModelMapping).region === usedRegion, + p.region === usedRegion, ); if (providerMapping) { - const params = (providerMapping as ProviderModelMapping) - .supportedParameters; + const params = providerMapping.supportedParameters; if (!params?.includes("effort")) { throw new HTTPException(400, { message: `Model ${usedModel} with provider ${usedProvider} does not support the effort parameter. Try using provider 'anthropic' instead.`, @@ -2142,10 +2150,9 @@ chat.openapi(completions, async (c) => { (p) => p.providerId === usedProvider && p.modelName === usedModel && - (p as ProviderModelMapping).region === usedRegion, + p.region === usedRegion, ); - const supported = (providerMapping as ProviderModelMapping | undefined) - ?.supportedParameters; + const supported = providerMapping?.supportedParameters; if (supported && supported.length > 0) { if (temperature !== undefined && !supported.includes("temperature")) { temperature = undefined; @@ -2293,7 +2300,7 @@ chat.openapi(completions, async (c) => { (p) => p.providerId === usedProvider && p.modelName === usedModel && - (p as ProviderModelMapping).region === usedRegion, + p.region === usedRegion, ); if ( providerMapping && @@ -2717,7 +2724,6 @@ chat.openapi(completions, async (c) => { inputImageCount, webSearchTool ? 1 : null, // Bill for web search if it was enabled project.organizationId, - usedRegion ?? null, ); } @@ -3734,7 +3740,6 @@ chat.openapi(completions, async (c) => { inputImageCount, webSearchCount, project.organizationId, - usedRegion ?? null, ); // Include costs in response for all users @@ -4848,7 +4853,6 @@ chat.openapi(completions, async (c) => { inputImageCount, webSearchCount, project.organizationId, - usedRegion ?? null, ); // Use costs.promptTokens as canonical value (includes image input @@ -5460,7 +5464,6 @@ chat.openapi(completions, async (c) => { inputImageCount, webSearchTool ? 1 : null, // Bill for web search if it was enabled project.organizationId, - usedRegion ?? null, ); } @@ -6200,7 +6203,6 @@ chat.openapi(completions, async (c) => { inputImageCount, webSearchCount, project.organizationId, - usedRegion ?? null, ); // Use costs.promptTokens as canonical value (includes image input diff --git a/apps/gateway/src/chat/tools/resolve-provider-context.ts b/apps/gateway/src/chat/tools/resolve-provider-context.ts index 9ace4602d1..f72ed77f7c 100644 --- a/apps/gateway/src/chat/tools/resolve-provider-context.ts +++ b/apps/gateway/src/chat/tools/resolve-provider-context.ts @@ -18,7 +18,7 @@ import { type OpenAIRequestBody, type OpenAIToolInput, type Provider, - type ProviderModelMapping, + type ProviderDefinition, type ProviderRequestBody, providers, type ToolChoiceType, @@ -183,9 +183,22 @@ export async function resolveProviderContext( (p) => p.providerId === usedProvider && p.modelName === usedModel && - (p as ProviderModelMapping).region === usedRegion, + p.region === usedRegion, ); + // --- Region validation --- + if (usedRegion) { + const providerDef = providers.find((p) => p.id === usedProvider) as + | ProviderDefinition + | undefined; + const validRegions = providerDef?.regionConfig?.regions; + if (validRegions && !validRegions.some((r) => r.id === usedRegion)) { + throw new HTTPException(400, { + message: `Model ${usedModel} is not available in region "${usedRegion}". Available regions: ${validRegions.map((r) => r.id).join(", ")}`, + }); + } + } + // Override token with region-specific env var if available (credits/hybrid mode) if (usedRegion && !providerKey) { const regionToken = getRegionSpecificEnvValue(usedProvider, usedRegion); @@ -195,13 +208,11 @@ export async function resolveProviderContext( } // --- Check if model supports reasoning (from selected provider, not any) --- - const supportsReasoning = - (providerMappingForSelected as ProviderModelMapping)?.reasoning === true; + const supportsReasoning = providerMappingForSelected?.reasoning === true; // --- Image generation check --- const isImageGeneration = - (providerMappingForSelected as ProviderModelMapping)?.imageGenerations === - true; + providerMappingForSelected?.imageGenerations === true; // --- URL resolution --- const url = getProviderEndpoint( @@ -237,8 +248,7 @@ export async function resolveProviderContext( let presence_penalty = originalParams.presence_penalty; if (providerMappingForSelected) { - const supported = (providerMappingForSelected as ProviderModelMapping) - .supportedParameters; + const supported = providerMappingForSelected.supportedParameters; if (supported && supported.length > 0) { if (temperature !== undefined && !supported.includes("temperature")) { temperature = undefined; @@ -273,9 +283,7 @@ export async function resolveProviderContext( // --- max_tokens validation --- if (max_tokens !== undefined && providerMappingForSelected) { - const effectiveMaxOutput = ( - providerMappingForSelected as ProviderModelMapping - ).maxOutput; + const effectiveMaxOutput = providerMappingForSelected.maxOutput; if (effectiveMaxOutput !== undefined) { if (max_tokens > effectiveMaxOutput) { // Silently cap to max output instead of throwing on retry diff --git a/apps/gateway/src/lib/costs.ts b/apps/gateway/src/lib/costs.ts index 4f9a82ac45..b3c323f6c2 100644 --- a/apps/gateway/src/lib/costs.ts +++ b/apps/gateway/src/lib/costs.ts @@ -100,7 +100,6 @@ export async function calculateCosts( inputImageCount = 0, webSearchCount: number | null = null, organizationId: string | null = null, - region: string | null = null, ) { // Find the model info - try both base model name and provider model name let modelInfo = models.find((m) => m.id === model) as ModelDefinition; @@ -251,8 +250,6 @@ export async function calculateCosts( } // Get pricing based on token count (supports tiered pricing) - // Each region is now a separate provider mapping with its own pricing, - // so no region-specific lookup is needed here. const pricing = getPricingForTokenCount( providerInfo.pricingTiers, providerInfo.inputPrice ?? 0, diff --git a/apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx b/apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx index f1c65433f0..91d8d6ed86 100644 --- a/apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx +++ b/apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx @@ -160,14 +160,7 @@ export function CreateProviderKeyDialog({ token: string; name?: string; baseUrl?: string; - options?: { - aws_bedrock_region_prefix?: "us." | "global." | "eu."; - azure_resource?: string; - azure_api_version?: string; - azure_deployment_type?: "openai" | "ai-foundry"; - azure_validation_model?: string; - [key: string]: string | undefined; - }; + options?: Record; organizationId: string; } = { provider: selectedProvider, diff --git a/apps/worker/src/services/sync-models.ts b/apps/worker/src/services/sync-models.ts index c17223197f..6d16e4682f 100644 --- a/apps/worker/src/services/sync-models.ts +++ b/apps/worker/src/services/sync-models.ts @@ -100,10 +100,7 @@ export async function syncProvidersAndModels() { if (modelDef.providers && modelDef.providers.length > 0) { const expandedProviders = expandAllProviderRegions(modelDef.providers); for (const mapping of expandedProviders) { - const mappingRegion = - "region" in mapping - ? (mapping.region as string | undefined) - : undefined; + const mappingRegion = mapping.region; const mappings = await database .select() .from(modelProviderMapping) diff --git a/ee/admin/src/lib/api/v1.d.ts b/ee/admin/src/lib/api/v1.d.ts index 22986d515b..dd0adf1a82 100644 --- a/ee/admin/src/lib/api/v1.d.ts +++ b/ee/admin/src/lib/api/v1.d.ts @@ -6923,6 +6923,7 @@ export interface operations { modelId: string; providerId: string; modelName: string; + region: string | null; inputPrice: string | null; outputPrice: string | null; cachedInputPrice: string | null; diff --git a/packages/actions/src/get-cheapest-from-available-providers.ts b/packages/actions/src/get-cheapest-from-available-providers.ts index e6134e6040..1a54610376 100644 --- a/packages/actions/src/get-cheapest-from-available-providers.ts +++ b/packages/actions/src/get-cheapest-from-available-providers.ts @@ -1,7 +1,6 @@ import { type ProviderMetrics, metricsKey } from "@llmgateway/db"; import { getProviderDefinition, - type ProviderModelMapping, type AvailableModelProvider, type ModelWithPricing, } from "@llmgateway/models"; @@ -129,13 +128,9 @@ export function getCheapestFromAvailableProviders< const stableProviders = availableModelProviders.filter((provider) => { const providerInfo = modelWithPricing.providers.find( (p) => - p.providerId === provider.providerId && - (p as ProviderModelMapping).region === provider.region, + p.providerId === provider.providerId && p.region === provider.region, ); - const providerStability = - providerInfo && "stability" in providerInfo - ? (providerInfo as ProviderModelMapping).stability - : undefined; + const providerStability = providerInfo?.stability; const modelStability = "stability" in modelWithPricing ? (modelWithPricing as { stability?: string }).stability @@ -179,10 +174,9 @@ export function getCheapestFromAvailableProviders< for (const provider of stableProviders) { const providerInfo = modelWithPricing.providers.find( (p) => - p.providerId === provider.providerId && - (p as ProviderModelMapping).region === provider.region, + p.providerId === provider.providerId && p.region === provider.region, ); - const discount = (providerInfo as ProviderModelMapping)?.discount ?? 0; + const discount = providerInfo?.discount ?? 0; const discountMultiplier = 1 - discount; const price = (((providerInfo?.inputPrice ?? 0) + (providerInfo?.outputPrice ?? 0)) / @@ -345,10 +339,9 @@ function selectByPriceOnly( for (const provider of stableProviders) { const providerInfo = modelWithPricing.providers.find( (p) => - p.providerId === provider.providerId && - (p as ProviderModelMapping).region === provider.region, + p.providerId === provider.providerId && p.region === provider.region, ); - const discount = (providerInfo as ProviderModelMapping)?.discount ?? 0; + const discount = providerInfo?.discount ?? 0; const discountMultiplier = 1 - discount; const totalPrice = (((providerInfo?.inputPrice ?? 0) + (providerInfo?.outputPrice ?? 0)) / diff --git a/packages/db/src/provider-metrics.spec.ts b/packages/db/src/provider-metrics.spec.ts index ce587fb93b..958758dd95 100644 --- a/packages/db/src/provider-metrics.spec.ts +++ b/packages/db/src/provider-metrics.spec.ts @@ -5,6 +5,7 @@ import { db } from "./db.js"; import { getProviderMetrics, getProviderMetricsForCombinations, + metricsKey, } from "./provider-metrics.js"; import { provider, model, modelProviderMapping } from "./schema.js"; @@ -90,7 +91,7 @@ describe("provider-metrics", () => { const metrics = await getProviderMetrics(); expect(metrics.size).toBe(1); - const metric = metrics.get("gpt-4:openai"); + const metric = metrics.get(metricsKey("gpt-4", "openai")); expect(metric).toBeDefined(); expect(metric?.modelId).toBe("gpt-4"); expect(metric?.providerId).toBe("openai"); @@ -125,12 +126,14 @@ describe("provider-metrics", () => { expect(metrics.size).toBe(2); - const gptMetric = metrics.get("gpt-4:openai"); + const gptMetric = metrics.get(metricsKey("gpt-4", "openai")); expect(gptMetric?.uptime).toBe(80); expect(gptMetric?.averageLatency).toBe(1000); expect(gptMetric?.totalRequests).toBe(100); - const claudeMetric = metrics.get("claude-3-5-sonnet:anthropic"); + const claudeMetric = metrics.get( + metricsKey("claude-3-5-sonnet", "anthropic"), + ); expect(claudeMetric?.uptime).toBe(95); expect(claudeMetric?.averageLatency).toBe(2000); expect(claudeMetric?.totalRequests).toBe(200); @@ -151,8 +154,10 @@ describe("provider-metrics", () => { const metrics = await getProviderMetrics(); expect(metrics.size).toBe(1); - expect(metrics.has("claude-3-5-sonnet:anthropic")).toBe(true); - expect(metrics.has("gpt-4:openai")).toBe(false); + expect(metrics.has(metricsKey("claude-3-5-sonnet", "anthropic"))).toBe( + true, + ); + expect(metrics.has(metricsKey("gpt-4", "openai"))).toBe(false); }); it("should skip mappings with zero total requests", async () => { @@ -182,7 +187,7 @@ describe("provider-metrics", () => { .where(eq(modelProviderMapping.id, "mapping-1")); const metrics = await getProviderMetrics(); - const metric = metrics.get("gpt-4:openai"); + const metric = metrics.get(metricsKey("gpt-4", "openai")); expect(metric?.uptime).toBe(100); }); @@ -198,7 +203,7 @@ describe("provider-metrics", () => { .where(eq(modelProviderMapping.id, "mapping-1")); const metrics = await getProviderMetrics(); - const metric = metrics.get("gpt-4:openai"); + const metric = metrics.get(metricsKey("gpt-4", "openai")); expect(metric?.uptime).toBe(0); }); @@ -214,7 +219,7 @@ describe("provider-metrics", () => { .where(eq(modelProviderMapping.id, "mapping-1")); const metrics = await getProviderMetrics(); - const metric = metrics.get("gpt-4:openai"); + const metric = metrics.get(metricsKey("gpt-4", "openai")); expect(metric).toBeDefined(); expect(metric?.uptime).toBe(95); expect(metric?.averageLatency).toBeUndefined(); @@ -234,7 +239,7 @@ describe("provider-metrics", () => { .where(eq(modelProviderMapping.id, "mapping-1")); const metrics = await getProviderMetrics(); - const metric = metrics.get("gpt-4:openai"); + const metric = metrics.get(metricsKey("gpt-4", "openai")); expect(metric).toBeDefined(); expect(metric?.uptime).toBeUndefined(); expect(metric?.averageLatency).toBe(500); @@ -275,8 +280,10 @@ describe("provider-metrics", () => { ]); expect(metrics.size).toBe(1); - expect(metrics.has("gpt-4:openai")).toBe(true); - expect(metrics.has("claude-3-5-sonnet:anthropic")).toBe(false); + expect(metrics.has(metricsKey("gpt-4", "openai"))).toBe(true); + expect(metrics.has(metricsKey("claude-3-5-sonnet", "anthropic"))).toBe( + false, + ); }); it("should return metrics for multiple combinations", async () => { @@ -306,10 +313,13 @@ describe("provider-metrics", () => { ]); expect(metrics.size).toBe(2); - expect(metrics.get("gpt-4:openai")?.totalRequests).toBe(100); - expect(metrics.get("claude-3-5-sonnet:anthropic")?.totalRequests).toBe( - 200, + expect(metrics.get(metricsKey("gpt-4", "openai"))?.totalRequests).toBe( + 100, ); + expect( + metrics.get(metricsKey("claude-3-5-sonnet", "anthropic")) + ?.totalRequests, + ).toBe(200); }); it("should only return combinations that have routing data", async () => { @@ -330,8 +340,10 @@ describe("provider-metrics", () => { ]); expect(metrics.size).toBe(1); - expect(metrics.has("gpt-4:openai")).toBe(true); - expect(metrics.has("claude-3-5-sonnet:anthropic")).toBe(false); + expect(metrics.has(metricsKey("gpt-4", "openai"))).toBe(true); + expect(metrics.has(metricsKey("claude-3-5-sonnet", "anthropic"))).toBe( + false, + ); }); it("should skip combinations with zero total requests", async () => { @@ -368,7 +380,7 @@ describe("provider-metrics", () => { ]); expect(metrics.size).toBe(1); - const metric = metrics.get("gpt-4:openai"); + const metric = metrics.get(metricsKey("gpt-4", "openai")); expect(metric?.modelId).toBe("gpt-4"); expect(metric?.providerId).toBe("openai"); expect(metric?.uptime).toBe(75); diff --git a/packages/models/src/provider.ts b/packages/models/src/provider.ts index 28661651a3..4dd2778ec9 100644 --- a/packages/models/src/provider.ts +++ b/packages/models/src/provider.ts @@ -125,3 +125,19 @@ export function getRegionSpecificEnvValue( process.env[`${baseEnvVar}__${regionSuffix}`] ?? process.env[baseEnvVar] ); } + +/** + * Check whether a dedicated env var exists for a specific region + * (i.e. `{BASE_ENV_VAR}__{REGION}`), WITHOUT falling back to the base key. + */ +export function hasRegionSpecificEnvKey( + provider: Provider, + region: string, +): boolean { + const baseEnvVar = getProviderEnvVar(provider); + if (!baseEnvVar) { + return false; + } + const regionSuffix = region.toUpperCase().replace(/-/g, "_"); + return Boolean(process.env[`${baseEnvVar}__${regionSuffix}`]); +} diff --git a/packages/models/src/types.ts b/packages/models/src/types.ts index b7f59b3127..c35b14ad80 100644 --- a/packages/models/src/types.ts +++ b/packages/models/src/types.ts @@ -322,6 +322,8 @@ export interface ModelWithPricing { supportedParameters?: string[]; modelName: string; discount?: number; + region?: string; + stability?: string; }>; } From b0134758423f21e57d18d38e60f72c67b6e74603 Mon Sep 17 00:00:00 2001 From: romel lauron Date: Wed, 25 Mar 2026 00:48:09 +0800 Subject: [PATCH 09/55] fix: region-aware validation and routing - Pass region to getProviderEndpoint in chat.ts - Resolve usedRegion from DB provider key in api-keys and hybrid modes - Add alibaba_region to Zod schemas for key creation - Filter validation models by selected region - Skip image/video generation models in validation - Fix validation error handling in provider key dialog - Add region-aware hybrid mode filtering - Add region debug logging Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/api/src/routes/keys-provider.ts | 2 + apps/code/src/lib/api/v1.d.ts | 4 + apps/gateway/src/chat/chat.ts | 122 +++++++++++++++++- .../chat/tools/resolve-provider-context.ts | 11 ++ apps/playground/src/lib/api/v1.d.ts | 4 + .../create-provider-key-dialog.tsx | 9 ++ apps/ui/src/lib/api/v1.d.ts | 4 + ee/admin/src/lib/api/v1.d.ts | 4 + packages/actions/src/validate-provider-key.ts | 50 ++++++- 9 files changed, 201 insertions(+), 9 deletions(-) diff --git a/apps/api/src/routes/keys-provider.ts b/apps/api/src/routes/keys-provider.ts index b6505aed0e..8f5968c6a6 100644 --- a/apps/api/src/routes/keys-provider.ts +++ b/apps/api/src/routes/keys-provider.ts @@ -33,6 +33,7 @@ const providerKeySchema = z.object({ azure_api_version: z.string().optional(), azure_deployment_type: z.enum(["openai", "ai-foundry"]).optional(), azure_validation_model: z.string().optional(), + alibaba_region: z.string().optional(), }) .nullable(), status: z.enum(["active", "inactive", "deleted"]).nullable(), @@ -60,6 +61,7 @@ const createProviderKeySchema = z.object({ azure_api_version: z.string().optional(), azure_deployment_type: z.enum(["openai", "ai-foundry"]).optional(), azure_validation_model: z.string().optional(), + alibaba_region: z.string().optional(), }) .optional(), organizationId: z.string().min(1, "Organization ID is required"), diff --git a/apps/code/src/lib/api/v1.d.ts b/apps/code/src/lib/api/v1.d.ts index 2161821a5b..58b8dac560 100644 --- a/apps/code/src/lib/api/v1.d.ts +++ b/apps/code/src/lib/api/v1.d.ts @@ -3672,6 +3672,7 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; + alibaba_region?: string; } | null; /** @enum {string|null} */ status: "active" | "inactive" | "deleted" | null; @@ -3707,6 +3708,7 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; + alibaba_region?: string; }; organizationId: string; }; @@ -3735,6 +3737,7 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; + alibaba_region?: string; } | null; /** @enum {string|null} */ status: "active" | "inactive" | "deleted" | null; @@ -3851,6 +3854,7 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; + alibaba_region?: string; } | null; /** @enum {string|null} */ status: "active" | "inactive" | "deleted" | null; diff --git a/apps/gateway/src/chat/chat.ts b/apps/gateway/src/chat/chat.ts index fd686802f7..e753bed6dc 100644 --- a/apps/gateway/src/chat/chat.ts +++ b/apps/gateway/src/chat/chat.ts @@ -568,14 +568,61 @@ chat.openapi(completions, async (c) => { }); } - // In credits/hybrid mode, only keep regions that have env keys configured. - // This avoids wasting retries on regions where the base key won't work. - if (project.mode !== "api-keys") { + // Filter region candidates based on available keys. + // - credits mode: only keep regions with env keys (base key → default region only) + // - hybrid mode: providers with a DB key keep all regions (user chose their region); + // providers without a DB key are filtered like credits mode + // - api-keys mode: no filtering (all regions available, user picks via DB key) + if (project.mode === "credits") { modelInfo = { ...modelInfo, providers: filterRegionsByAvailableKeys(modelInfo.providers), }; allModelProviders = filterRegionsByAvailableKeys(allModelProviders); + } else if (project.mode === "hybrid") { + const dbProviderKeys = await findActiveProviderKeys(project.organizationId); + const providersWithDbKeys = new Set(dbProviderKeys.map((k) => k.provider)); + const filterHybridRegions = ( + expanded: ProviderModelMapping[], + ): ProviderModelMapping[] => + expanded.filter((mapping) => { + // Providers with a DB key: keep all regions + if (providersWithDbKeys.has(mapping.providerId)) { + return true; + } + // Providers without a DB key: filter like credits mode + if (!mapping.region) { + return true; + } + const providerDef = providers.find( + (p) => p.id === mapping.providerId, + ) as ProviderDefinition | undefined; + if (!providerDef?.regionConfig) { + return true; + } + if (mapping.region === providerDef.regionConfig.defaultRegion) { + return true; + } + return hasRegionSpecificEnvKey( + mapping.providerId as Provider, + mapping.region, + ); + }); + modelInfo = { + ...modelInfo, + providers: filterHybridRegions(modelInfo.providers), + }; + allModelProviders = filterHybridRegions(allModelProviders); + } + + const regionCandidates = modelInfo.providers + .filter((p) => p.region) + .map((p) => `${p.providerId}:${p.region}`); + if (regionCandidates.length > 0) { + logger.info("[region-debug] Region candidates after filtering", { + projectMode: project.mode, + regionCandidates, + }); } // Fetch organization for coding model restriction check and credit validation @@ -964,11 +1011,11 @@ chat.openapi(completions, async (c) => { // Expand region entries into separate candidates for scoring const expandedCandidateProviders = - project.mode === "api-keys" - ? expandAllProviderRegions(modelDef.providers) - : filterRegionsByAvailableKeys( + project.mode === "credits" + ? filterRegionsByAvailableKeys( expandAllProviderRegions(modelDef.providers), - ); + ) + : expandAllProviderRegions(modelDef.providers); // Check if any of the model's providers are available const availableModelProviders = expandedCandidateProviders.filter( (provider) => @@ -1373,6 +1420,25 @@ chat.openapi(completions, async (c) => { .filter((p) => hasProviderEnvironmentToken(p.id as Provider)) .map((p) => p.id); + // Build a map of provider → locked region from DB provider keys. + // When a user sets a region in their provider key (e.g. alibaba_region: "cn-beijing"), + // only that region should be a candidate — not all expanded regions. + const providerLockedRegions = new Map(); + for (const key of providerKeys) { + const providerDef = providers.find((p) => p.id === key.provider) as + | ProviderDefinition + | undefined; + const regionKey = providerDef?.regionConfig?.optionsKey; + if (regionKey && key.options) { + const lockedRegion = ( + key.options as Record + )[regionKey]; + if (lockedRegion) { + providerLockedRegions.set(key.provider, lockedRegion); + } + } + } + // Filter model providers to only those available // If web search is requested, also filter to providers that support it // If JSON output is requested, also filter to providers that support it @@ -1381,6 +1447,15 @@ chat.openapi(completions, async (c) => { if (!availableProviders.includes(provider.providerId)) { return false; } + // If the user has a DB key with a locked region, only allow that region + const lockedRegion = providerLockedRegions.get(provider.providerId); + if ( + lockedRegion && + provider.region && + provider.region !== lockedRegion + ) { + return false; + } // If web search tool is requested, only include providers that support it if (webSearchTool) { if (provider.webSearch !== true) { @@ -1641,6 +1716,23 @@ chat.openapi(completions, async (c) => { }); } + // Helper: resolve the locked region from a provider key's options + function resolveRegionFromProviderKey( + key: InferSelectModel, + ): string | undefined { + const providerDef = providers.find((p) => p.id === key.provider) as + | ProviderDefinition + | undefined; + if (!providerDef?.regionConfig) { + return undefined; + } + const regionKey = providerDef.regionConfig.optionsKey; + const explicitRegion = key.options + ? (key.options as Record)[regionKey] + : undefined; + return explicitRegion ?? providerDef.regionConfig.defaultRegion; + } + if (project.mode === "api-keys") { // Get the provider key from the database using cached helper function if (usedProvider === "custom" && customProviderName) { @@ -1663,6 +1755,7 @@ chat.openapi(completions, async (c) => { } usedToken = providerKey.token; + usedRegion ??= resolveRegionFromProviderKey(providerKey); } else if (project.mode === "credits") { // Check both regular credits AND dev plan credits const regularCredits = parseFloat(organization.credits ?? "0"); @@ -1715,6 +1808,12 @@ chat.openapi(completions, async (c) => { if (providerKey) { usedToken = providerKey.token; + usedRegion ??= resolveRegionFromProviderKey(providerKey); + logger.info("[region-debug] Hybrid mode: DB key found", { + provider: usedProvider, + resolvedRegion: usedRegion ?? "none", + keyOptions: providerKey.options, + }); } else { // No API key available, fall back to credits // Check both regular credits AND dev plan credits @@ -1837,7 +1936,16 @@ chat.openapi(completions, async (c) => { providerKey?.options ?? undefined, configIndex, isImageGeneration, + usedRegion, ); + + logger.info("[region-debug] Request resolved", { + provider: usedProvider, + model: usedModel, + region: usedRegion ?? "default", + endpoint: url, + tokenSource: providerKey ? "db-provider-key" : "env-var", + }); } catch (error) { if (usedProvider === "llmgateway" && usedModel !== "custom") { throw new HTTPException(400, { diff --git a/apps/gateway/src/chat/tools/resolve-provider-context.ts b/apps/gateway/src/chat/tools/resolve-provider-context.ts index f72ed77f7c..4f83bd26ce 100644 --- a/apps/gateway/src/chat/tools/resolve-provider-context.ts +++ b/apps/gateway/src/chat/tools/resolve-provider-context.ts @@ -10,6 +10,7 @@ import { getProviderHeaders, prepareRequestBody, } from "@llmgateway/actions"; +import { logger } from "@llmgateway/logger"; import { type BaseMessage, getRegionSpecificEnvValue, @@ -231,6 +232,16 @@ export async function resolveProviderContext( usedRegion, ); + logger.info("[region-debug] Provider context resolved", { + provider: usedProvider, + model: usedModel, + region: usedRegion ?? "none", + endpoint: url ?? "unresolved", + tokenSource: providerKey ? "db-provider-key" : "env-var", + tokenEnvVar: envVarName, + projectMode: project.mode, + }); + if (!url) { throw new HTTPException(400, { message: `No base URL set for provider: ${usedProvider}`, diff --git a/apps/playground/src/lib/api/v1.d.ts b/apps/playground/src/lib/api/v1.d.ts index 2161821a5b..58b8dac560 100644 --- a/apps/playground/src/lib/api/v1.d.ts +++ b/apps/playground/src/lib/api/v1.d.ts @@ -3672,6 +3672,7 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; + alibaba_region?: string; } | null; /** @enum {string|null} */ status: "active" | "inactive" | "deleted" | null; @@ -3707,6 +3708,7 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; + alibaba_region?: string; }; organizationId: string; }; @@ -3735,6 +3737,7 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; + alibaba_region?: string; } | null; /** @enum {string|null} */ status: "active" | "inactive" | "deleted" | null; @@ -3851,6 +3854,7 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; + alibaba_region?: string; } | null; /** @enum {string|null} */ status: "active" | "inactive" | "deleted" | null; diff --git a/apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx b/apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx index 91d8d6ed86..dd67a72f7d 100644 --- a/apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx +++ b/apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx @@ -222,6 +222,15 @@ export function CreateProviderKeyDialog({ void queryClient.invalidateQueries({ queryKey }); setOpen(false); }, + onError: () => { + setIsValidating(false); + toast({ + title: "Validation Failed", + description: + "Failed to validate the API key. Please check your key and region.", + variant: "destructive", + }); + }, }, ); }; diff --git a/apps/ui/src/lib/api/v1.d.ts b/apps/ui/src/lib/api/v1.d.ts index 2161821a5b..58b8dac560 100644 --- a/apps/ui/src/lib/api/v1.d.ts +++ b/apps/ui/src/lib/api/v1.d.ts @@ -3672,6 +3672,7 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; + alibaba_region?: string; } | null; /** @enum {string|null} */ status: "active" | "inactive" | "deleted" | null; @@ -3707,6 +3708,7 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; + alibaba_region?: string; }; organizationId: string; }; @@ -3735,6 +3737,7 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; + alibaba_region?: string; } | null; /** @enum {string|null} */ status: "active" | "inactive" | "deleted" | null; @@ -3851,6 +3854,7 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; + alibaba_region?: string; } | null; /** @enum {string|null} */ status: "active" | "inactive" | "deleted" | null; diff --git a/ee/admin/src/lib/api/v1.d.ts b/ee/admin/src/lib/api/v1.d.ts index 2161821a5b..58b8dac560 100644 --- a/ee/admin/src/lib/api/v1.d.ts +++ b/ee/admin/src/lib/api/v1.d.ts @@ -3672,6 +3672,7 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; + alibaba_region?: string; } | null; /** @enum {string|null} */ status: "active" | "inactive" | "deleted" | null; @@ -3707,6 +3708,7 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; + alibaba_region?: string; }; organizationId: string; }; @@ -3735,6 +3737,7 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; + alibaba_region?: string; } | null; /** @enum {string|null} */ status: "active" | "inactive" | "deleted" | null; @@ -3851,6 +3854,7 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; + alibaba_region?: string; } | null; /** @enum {string|null} */ status: "active" | "inactive" | "deleted" | null; diff --git a/packages/actions/src/validate-provider-key.ts b/packages/actions/src/validate-provider-key.ts index b93e611f53..6fbf1035c7 100644 --- a/packages/actions/src/validate-provider-key.ts +++ b/packages/actions/src/validate-provider-key.ts @@ -3,8 +3,10 @@ import { models, type ProviderModelMapping, type ProviderId, + type ProviderDefinition, type BaseMessage, type ProviderValidationResult, + providers, } from "@llmgateway/models"; import { getProviderEndpoint } from "./get-provider-endpoint.js"; @@ -21,6 +23,17 @@ function getValidationModel( return providerKeyOptions.azure_validation_model; } + // Resolve the selected region from provider key options + const providerDef = providers.find((p) => p.id === provider) as + | ProviderDefinition + | undefined; + const regionKey = providerDef?.regionConfig?.optionsKey; + const selectedRegion = regionKey + ? (providerKeyOptions as Record | undefined)?.[ + regionKey + ] + : undefined; + const currentDate = new Date(); const providerModels = models .flatMap((model) => { @@ -31,6 +44,13 @@ function getValidationModel( return []; } + // If a region is selected, only consider models available in that region + if (selectedRegion && providerMapping.regions) { + if (!providerMapping.regions.some((r) => r.id === selectedRegion)) { + return []; + } + } + const providerStability = "stability" in providerMapping ? (providerMapping.stability as string | undefined) @@ -51,7 +71,13 @@ function getValidationModel( providerMapping.deactivatedAt && currentDate >= providerMapping.deactivatedAt; - if (!isStable || isDeprecated || isDeactivated) { + if ( + !isStable || + isDeprecated || + isDeactivated || + providerMapping.imageGenerations || + providerMapping.videoGenerations + ) { return []; } @@ -142,7 +168,7 @@ export async function validateProviderKey( ? providerKeyOptions.azure_validation_model : modelId; - logger.debug("Validation endpoint configuration", { + logger.info("[region-debug] Validation endpoint configuration", { provider, model: validationModel, modelId, @@ -150,6 +176,17 @@ export async function validateProviderKey( providerKeyOptions, }); + // Resolve region from provider key options for region-aware providers + const providerDef = providers.find((p) => p.id === provider) as + | ProviderDefinition + | undefined; + const regionOptionsKey = providerDef?.regionConfig?.optionsKey; + const validationRegion = regionOptionsKey + ? (( + providerKeyOptions as Record | undefined + )?.[regionOptionsKey] ?? providerDef?.regionConfig?.defaultRegion) + : undefined; + const endpoint = getProviderEndpoint( provider, baseUrl, @@ -161,8 +198,17 @@ export async function validateProviderKey( false, // supportsReasoning - disable for validation false, // hasExistingToolCalls - disable for validation providerKeyOptions, + undefined, // configIndex + undefined, // imageGenerations + validationRegion, ); + logger.info("[region-debug] Validation request", { + endpoint, + validationRegion, + validationModel, + }); + // Check if max_tokens is supported const providerMapping = modelDef?.providers.find( (p) => p.providerId === provider && p.modelName === validationModel, From f99dfa935a22652439615a7875c984e2cbfa4b1a Mon Sep 17 00:00:00 2001 From: romel lauron Date: Wed, 25 Mar 2026 22:27:56 +0800 Subject: [PATCH 10/55] fix: update Alibaba model pricing and regions - Add deepseek-v3.2 for Singapore and cn-beijing - Fix qwen-max/qwen-max-latest context sizes - Update cn-beijing tiered pricing for qwen-plus, qwen-flash, qwen3-coder-flash, qwen3-vl-plus, qwen3-vl-flash, qwen3-max-2026-01-23 - Fix qwen-coder-plus cn-beijing pricing - Remove unsupported us-virginia from qwen-plus-latest, qwen-flash, qwen3.5-397b-a17b - Add contextSize to ProviderRegion type - Add 20% discount for deepseek-v3.2 Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/models/src/models.ts | 5 + packages/models/src/models/alibaba.ts | 302 ++++++++++++++------------ 2 files changed, 163 insertions(+), 144 deletions(-) diff --git a/packages/models/src/models.ts b/packages/models/src/models.ts index 48ca4f3488..e9cbd1497b 100644 --- a/packages/models/src/models.ts +++ b/packages/models/src/models.ts @@ -92,6 +92,11 @@ export interface ProviderRegion { * When absent, falls back to the mapping-level webSearchPrice. */ webSearchPrice?: number; + /** + * Context window size in tokens for this region. + * When absent, falls back to the mapping-level contextSize. + */ + contextSize?: number; /** * Maximum output size in tokens for this region. * When absent, falls back to the mapping-level maxOutput. diff --git a/packages/models/src/models/alibaba.ts b/packages/models/src/models/alibaba.ts index f4ee9078e0..efe2786330 100644 --- a/packages/models/src/models/alibaba.ts +++ b/packages/models/src/models/alibaba.ts @@ -24,8 +24,8 @@ export const alibabaModels = [ }, ], requestPrice: 0, - contextSize: 131072, - maxOutput: 32000, + contextSize: 32768, + maxOutput: 8192, streaming: true, vision: true, tools: true, @@ -53,11 +53,12 @@ export const alibabaModels = [ id: "cn-beijing", inputPrice: 0.345 / 1e6, outputPrice: 1.377 / 1e6, + contextSize: 131072, }, ], requestPrice: 0, - contextSize: 131072, - maxOutput: 32000, + contextSize: 32768, + maxOutput: 8192, streaming: true, vision: true, tools: true, @@ -79,26 +80,24 @@ export const alibabaModels = [ inputPrice: 0.4 / 1e6, outputPrice: 1.2 / 1e6, cachedInputPrice: 0.08 / 1e6, - regions: [ + pricingTiers: [ { - id: "singapore", - pricingTiers: [ - { - name: "Up to 256K", - upToTokens: 256000, - inputPrice: 0.4 / 1e6, - outputPrice: 1.2 / 1e6, - cachedInputPrice: 0.08 / 1e6, - }, - { - name: "Over 256K", - upToTokens: Infinity, - inputPrice: 1.2 / 1e6, - outputPrice: 3.6 / 1e6, - cachedInputPrice: 0.24 / 1e6, - }, - ], + name: "Up to 256K", + upToTokens: 256000, + inputPrice: 0.4 / 1e6, + outputPrice: 1.2 / 1e6, + cachedInputPrice: 0.08 / 1e6, }, + { + name: "Over 256K", + upToTokens: Infinity, + inputPrice: 1.2 / 1e6, + outputPrice: 3.6 / 1e6, + cachedInputPrice: 0.24 / 1e6, + }, + ], + regions: [ + { id: "singapore" }, { id: "us-virginia", inputPrice: 0.115 / 1e6, @@ -128,19 +127,26 @@ export const alibabaModels = [ cachedInputPrice: 0.023 / 1e6, pricingTiers: [ { - name: "Up to 256K", - upToTokens: 256000, + name: "Up to 128K", + upToTokens: 128000, inputPrice: 0.115 / 1e6, outputPrice: 0.287 / 1e6, cachedInputPrice: 0.023 / 1e6, }, { - name: "Over 256K", - upToTokens: Infinity, + name: "128K-256K", + upToTokens: 256000, inputPrice: 0.345 / 1e6, - outputPrice: 0.861 / 1e6, + outputPrice: 2.868 / 1e6, cachedInputPrice: 0.069 / 1e6, }, + { + name: "256K-1M", + upToTokens: 1000000, + inputPrice: 0.689 / 1e6, + outputPrice: 6.881 / 1e6, + cachedInputPrice: 0.138 / 1e6, + }, ], }, ], @@ -168,48 +174,24 @@ export const alibabaModels = [ inputPrice: 0.4 / 1e6, outputPrice: 1.2 / 1e6, cachedInputPrice: 0.08 / 1e6, - regions: [ + pricingTiers: [ { - id: "singapore", - pricingTiers: [ - { - name: "Up to 256K", - upToTokens: 256000, - inputPrice: 0.4 / 1e6, - outputPrice: 1.2 / 1e6, - cachedInputPrice: 0.08 / 1e6, - }, - { - name: "Over 256K", - upToTokens: Infinity, - inputPrice: 1.2 / 1e6, - outputPrice: 3.6 / 1e6, - cachedInputPrice: 0.24 / 1e6, - }, - ], + name: "Up to 256K", + upToTokens: 256000, + inputPrice: 0.4 / 1e6, + outputPrice: 1.2 / 1e6, + cachedInputPrice: 0.08 / 1e6, }, { - id: "us-virginia", - inputPrice: 0.115 / 1e6, - outputPrice: 0.287 / 1e6, - cachedInputPrice: 0.023 / 1e6, - pricingTiers: [ - { - name: "Up to 256K", - upToTokens: 256000, - inputPrice: 0.115 / 1e6, - outputPrice: 0.287 / 1e6, - cachedInputPrice: 0.023 / 1e6, - }, - { - name: "Over 256K", - upToTokens: Infinity, - inputPrice: 0.345 / 1e6, - outputPrice: 0.861 / 1e6, - cachedInputPrice: 0.069 / 1e6, - }, - ], + name: "Over 256K", + upToTokens: Infinity, + inputPrice: 1.2 / 1e6, + outputPrice: 3.6 / 1e6, + cachedInputPrice: 0.24 / 1e6, }, + ], + regions: [ + { id: "singapore" }, { id: "cn-beijing", inputPrice: 0.115 / 1e6, @@ -217,19 +199,26 @@ export const alibabaModels = [ cachedInputPrice: 0.023 / 1e6, pricingTiers: [ { - name: "Up to 256K", - upToTokens: 256000, + name: "Up to 128K", + upToTokens: 128000, inputPrice: 0.115 / 1e6, outputPrice: 0.287 / 1e6, cachedInputPrice: 0.023 / 1e6, }, { - name: "Over 256K", - upToTokens: Infinity, + name: "128K-256K", + upToTokens: 256000, inputPrice: 0.345 / 1e6, - outputPrice: 0.861 / 1e6, + outputPrice: 2.868 / 1e6, cachedInputPrice: 0.069 / 1e6, }, + { + name: "256K-1M", + upToTokens: 1000000, + inputPrice: 0.689 / 1e6, + outputPrice: 6.881 / 1e6, + cachedInputPrice: 0.138 / 1e6, + }, ], }, ], @@ -278,46 +267,31 @@ export const alibabaModels = [ ], }, { - id: "us-virginia", + id: "cn-beijing", inputPrice: 0.022 / 1e6, outputPrice: 0.216 / 1e6, cachedInputPrice: 0.0044 / 1e6, pricingTiers: [ { - name: "Up to 256K", - upToTokens: 256000, + name: "Up to 128K", + upToTokens: 128000, inputPrice: 0.022 / 1e6, outputPrice: 0.216 / 1e6, cachedInputPrice: 0.0044 / 1e6, }, { - name: "Over 256K", - upToTokens: Infinity, - inputPrice: 0.11 / 1e6, - outputPrice: 1.08 / 1e6, - cachedInputPrice: 0.022 / 1e6, - }, - ], - }, - { - id: "cn-beijing", - inputPrice: 0.022 / 1e6, - outputPrice: 0.216 / 1e6, - cachedInputPrice: 0.0044 / 1e6, - pricingTiers: [ - { - name: "Up to 256K", + name: "128K-256K", upToTokens: 256000, - inputPrice: 0.022 / 1e6, - outputPrice: 0.216 / 1e6, - cachedInputPrice: 0.0044 / 1e6, + inputPrice: 0.087 / 1e6, + outputPrice: 0.861 / 1e6, + cachedInputPrice: 0.017 / 1e6, }, { - name: "Over 256K", - upToTokens: Infinity, - inputPrice: 0.11 / 1e6, - outputPrice: 1.08 / 1e6, - cachedInputPrice: 0.022 / 1e6, + name: "256K-1M", + upToTokens: 1000000, + inputPrice: 0.173 / 1e6, + outputPrice: 1.721 / 1e6, + cachedInputPrice: 0.035 / 1e6, }, ], }, @@ -1276,15 +1250,24 @@ export const alibabaModels = [ outputPrice: 3.6 / 1e6, regions: [ { id: "singapore" }, - { - id: "us-virginia", - inputPrice: 0.143 / 1e6, - outputPrice: 0.573 / 1e6, - }, { id: "cn-beijing", - inputPrice: 0.143 / 1e6, - outputPrice: 0.573 / 1e6, + inputPrice: 0.172 / 1e6, + outputPrice: 1.032 / 1e6, + pricingTiers: [ + { + name: "Up to 128K", + upToTokens: 128000, + inputPrice: 0.172 / 1e6, + outputPrice: 1.032 / 1e6, + }, + { + name: "128K-256K", + upToTokens: 256000, + inputPrice: 0.43 / 1e6, + outputPrice: 2.58 / 1e6, + }, + ], }, ], requestPrice: 0, @@ -1466,8 +1449,8 @@ export const alibabaModels = [ providerId: "alibaba", modelName: "qwen-coder-plus", discount: 0.2, - inputPrice: 1.0 / 1e6, - outputPrice: 5.0 / 1e6, + inputPrice: 0.502 / 1e6, + outputPrice: 1.004 / 1e6, regions: [{ id: "cn-beijing" }], requestPrice: 0, contextSize: 131072, @@ -1579,23 +1562,23 @@ export const alibabaModels = [ { name: "32K-128K", upToTokens: 128000, - inputPrice: 0.24 / 1e6, - outputPrice: 0.957 / 1e6, - cachedInputPrice: 0.048 / 1e6, + inputPrice: 0.216 / 1e6, + outputPrice: 0.861 / 1e6, + cachedInputPrice: 0.043 / 1e6, }, { name: "128K-256K", upToTokens: 256000, - inputPrice: 0.384 / 1e6, - outputPrice: 1.53 / 1e6, - cachedInputPrice: 0.077 / 1e6, + inputPrice: 0.359 / 1e6, + outputPrice: 1.434 / 1e6, + cachedInputPrice: 0.072 / 1e6, }, { - name: "Over 256K", - upToTokens: Infinity, - inputPrice: 0.768 / 1e6, - outputPrice: 3.67 / 1e6, - cachedInputPrice: 0.154 / 1e6, + name: "256K-1M", + upToTokens: 1000000, + inputPrice: 0.717 / 1e6, + outputPrice: 3.584 / 1e6, + cachedInputPrice: 0.143 / 1e6, }, ], }, @@ -1643,8 +1626,8 @@ export const alibabaModels = [ cachedInputPrice: 0.06 / 1e6, }, { - name: "Over 128K", - upToTokens: Infinity, + name: "128K-256K", + upToTokens: 256000, inputPrice: 0.6 / 1e6, outputPrice: 4.8 / 1e6, cachedInputPrice: 0.12 / 1e6, @@ -1697,14 +1680,14 @@ export const alibabaModels = [ name: "32K-128K", upToTokens: 128000, inputPrice: 0.215 / 1e6, - outputPrice: 2.151 / 1e6, + outputPrice: 2.15 / 1e6, cachedInputPrice: 0.043 / 1e6, }, { - name: "Over 128K", - upToTokens: Infinity, - inputPrice: 0.429 / 1e6, - outputPrice: 4.302 / 1e6, + name: "128K-256K", + upToTokens: 256000, + inputPrice: 0.43 / 1e6, + outputPrice: 4.301 / 1e6, cachedInputPrice: 0.086 / 1e6, }, ], @@ -1753,8 +1736,8 @@ export const alibabaModels = [ cachedInputPrice: 0.015 / 1e6, }, { - name: "Over 128K", - upToTokens: Infinity, + name: "128K-256K", + upToTokens: 256000, inputPrice: 0.12 / 1e6, outputPrice: 0.96 / 1e6, cachedInputPrice: 0.024 / 1e6, @@ -1806,16 +1789,16 @@ export const alibabaModels = [ { name: "32K-128K", upToTokens: 128000, - inputPrice: 0.033 / 1e6, - outputPrice: 0.323 / 1e6, - cachedInputPrice: 0.0066 / 1e6, + inputPrice: 0.043 / 1e6, + outputPrice: 0.43 / 1e6, + cachedInputPrice: 0.0086 / 1e6, }, { - name: "Over 128K", - upToTokens: Infinity, - inputPrice: 0.053 / 1e6, - outputPrice: 0.516 / 1e6, - cachedInputPrice: 0.0106 / 1e6, + name: "128K-256K", + upToTokens: 256000, + inputPrice: 0.086 / 1e6, + outputPrice: 0.859 / 1e6, + cachedInputPrice: 0.017 / 1e6, }, ], }, @@ -1965,8 +1948,8 @@ export const alibabaModels = [ cachedInputPrice: 0.48 / 1e6, }, { - name: "Over 128K", - upToTokens: Infinity, + name: "128K-252K", + upToTokens: 252000, inputPrice: 3.0 / 1e6, outputPrice: 15.0 / 1e6, cachedInputPrice: 0.6 / 1e6, @@ -2018,16 +2001,16 @@ export const alibabaModels = [ { name: "32K-128K", upToTokens: 128000, - inputPrice: 0.718 / 1e6, - outputPrice: 2.868 / 1e6, - cachedInputPrice: 0.144 / 1e6, + inputPrice: 0.574 / 1e6, + outputPrice: 2.294 / 1e6, + cachedInputPrice: 0.115 / 1e6, }, { - name: "Over 128K", - upToTokens: Infinity, - inputPrice: 0.898 / 1e6, - outputPrice: 3.585 / 1e6, - cachedInputPrice: 0.18 / 1e6, + name: "128K-252K", + upToTokens: 252000, + inputPrice: 1.004 / 1e6, + outputPrice: 4.014 / 1e6, + cachedInputPrice: 0.201 / 1e6, }, ], }, @@ -2099,4 +2082,35 @@ export const alibabaModels = [ }, ], }, + { + id: "deepseek-v3.2", + name: "DeepSeek V3.2", + description: "DeepSeek V3.2 685B parameter model hosted on Alibaba Cloud.", + family: "deepseek", + releasedAt: new Date("2025-03-01"), + providers: [ + { + providerId: "alibaba", + modelName: "deepseek-v3.2", + discount: 0.2, + inputPrice: 0.57 / 1e6, + outputPrice: 1.71 / 1e6, + regions: [ + { id: "singapore" }, + { + id: "cn-beijing", + inputPrice: 0.287 / 1e6, + outputPrice: 0.431 / 1e6, + }, + ], + requestPrice: 0, + contextSize: 131072, + maxOutput: 65536, + streaming: true, + vision: false, + tools: true, + jsonOutput: true, + }, + ], + }, ] as const satisfies ModelDefinition[]; From 55558137076ee17e62e99bd34ce59da21954b0d0 Mon Sep 17 00:00:00 2001 From: romel lauron Date: Wed, 25 Mar 2026 22:41:28 +0800 Subject: [PATCH 11/55] feat: add region column to admin model mappings Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/api/src/routes/admin.ts | 3 +++ ee/admin/src/app/model-provider-mappings/page.tsx | 12 +++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/apps/api/src/routes/admin.ts b/apps/api/src/routes/admin.ts index 5f75c80e32..00efa22978 100644 --- a/apps/api/src/routes/admin.ts +++ b/apps/api/src/routes/admin.ts @@ -4366,6 +4366,7 @@ const modelProviderMappingEntrySchema = z.object({ id: z.string(), modelId: z.string(), modelName: z.string(), + region: z.string().nullable(), providerId: z.string(), providerName: z.string(), status: z.string(), @@ -4594,6 +4595,7 @@ admin.openapi(getModelProviderMappings, async (c) => { id: tables.modelProviderMapping.id, modelId: tables.modelProviderMapping.modelId, modelName: tables.modelProviderMapping.modelName, + region: tables.modelProviderMapping.region, providerId: tables.modelProviderMapping.providerId, providerName: tables.provider.name, status: tables.modelProviderMapping.status, @@ -4644,6 +4646,7 @@ admin.openapi(getModelProviderMappings, async (c) => { id: r.id, modelId: r.modelId, modelName: r.modelName, + region: r.region, providerId: r.providerId, providerName: r.providerName, status: r.status, diff --git a/ee/admin/src/app/model-provider-mappings/page.tsx b/ee/admin/src/app/model-provider-mappings/page.tsx index 8f51a69994..e38c841259 100644 --- a/ee/admin/src/app/model-provider-mappings/page.tsx +++ b/ee/admin/src/app/model-provider-mappings/page.tsx @@ -128,6 +128,15 @@ function MappingRow({ mapping }: { mapping: ModelProviderMappingEntry }) { )}
+ + {mapping.region ? ( + + {mapping.region} + + ) : ( + — + )} + {mapping.status} @@ -333,6 +342,7 @@ export default async function ModelProviderMappingsPage({ {sh("Provider", "providerId")} {sh("Model", "modelId")} + Region Status {sh("Requests", "logsCount")} {sh("Errors", "errorsCount")} @@ -350,7 +360,7 @@ export default async function ModelProviderMappingsPage({ {data.mappings.length === 0 ? ( No mappings found From 5cb200c1111ebc48bdd595b81bb214da3f83bee8 Mon Sep 17 00:00:00 2001 From: romel lauron Date: Wed, 25 Mar 2026 23:08:45 +0800 Subject: [PATCH 12/55] feat: region routing syntax and e2e expansion - Support provider/model:region syntax to pin a specific region (e.g. alibaba/qwen-plus:cn-beijing) - Expand regions in e2e test helpers so each region becomes a separate test case - Fix duplicate deepseek-v3.2 model ID by moving Alibaba provider mapping to deepseek.ts Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/code/src/lib/api/v1.d.ts | 1 + apps/gateway/src/chat-helpers.e2e.ts | 19 +++++++++--- apps/gateway/src/chat/chat.ts | 20 ++++++++++++ .../src/chat/tools/parse-model-input.ts | 13 +++++++- apps/playground/src/lib/api/v1.d.ts | 1 + apps/ui/src/lib/api/v1.d.ts | 1 + ee/admin/src/lib/api/v1.d.ts | 1 + packages/models/src/models/alibaba.ts | 31 ------------------- packages/models/src/models/deepseek.ts | 22 +++++++++++++ 9 files changed, 72 insertions(+), 37 deletions(-) diff --git a/apps/code/src/lib/api/v1.d.ts b/apps/code/src/lib/api/v1.d.ts index 58b8dac560..1c2b9e55b2 100644 --- a/apps/code/src/lib/api/v1.d.ts +++ b/apps/code/src/lib/api/v1.d.ts @@ -2931,6 +2931,7 @@ export interface paths { id: string; modelId: string; modelName: string; + region: string | null; providerId: string; providerName: string; status: string; diff --git a/apps/gateway/src/chat-helpers.e2e.ts b/apps/gateway/src/chat-helpers.e2e.ts index 760f4575ac..22339530cc 100644 --- a/apps/gateway/src/chat-helpers.e2e.ts +++ b/apps/gateway/src/chat-helpers.e2e.ts @@ -10,6 +10,7 @@ import { providers, getConcurrentTestOptions, getTestOptions, + expandAllProviderRegions, } from "@llmgateway/models"; import { @@ -192,14 +193,18 @@ export const testModels = filteredModels // test root model without a specific provider testCases.push({ model: model.id, - providers: model.providers.filter( - (provider: ProviderModelMapping) => provider.test !== "skip", - ), + providers: expandAllProviderRegions( + model.providers as ProviderModelMapping[], + ).filter((provider: ProviderModelMapping) => provider.test !== "skip"), }); } // Create entries for provider-specific requests using provider/model format - for (const provider of model.providers as ProviderModelMapping[]) { + // Expand regions so each provider:region combo becomes a separate test case + const expandedProviders = expandAllProviderRegions( + model.providers as ProviderModelMapping[], + ); + for (const provider of expandedProviders) { // Skip deactivated provider mappings if (provider.deactivatedAt && new Date() > provider.deactivatedAt) { continue; @@ -281,7 +286,11 @@ export const providerModels = filteredModels .flatMap((model) => { const testCases = []; - for (const provider of model.providers as ProviderModelMapping[]) { + // Expand regions so each provider:region combo becomes a separate test case + const expandedProviders = expandAllProviderRegions( + model.providers as ProviderModelMapping[], + ); + for (const provider of expandedProviders) { // Skip deactivated provider mappings if (provider.deactivatedAt && new Date() > provider.deactivatedAt) { continue; diff --git a/apps/gateway/src/chat/chat.ts b/apps/gateway/src/chat/chat.ts index c7e0978480..ecbd2cabc2 100644 --- a/apps/gateway/src/chat/chat.ts +++ b/apps/gateway/src/chat/chat.ts @@ -482,6 +482,7 @@ chat.openapi(completions, async (c) => { const parseResult = parseModelInput(modelInput); const requestedModel = parseResult.requestedModel; const customProviderName = parseResult.customProviderName; + const requestedRegion = parseResult.requestedRegion; // Count input images from messages for cost calculation const inputImageCount = @@ -504,6 +505,25 @@ chat.openapi(completions, async (c) => { ); let requestedProvider = modelInfoResult.requestedProvider; + // If a specific region was requested (e.g. "alibaba/qwen-plus:cn-beijing"), + // filter providers to only those matching the requested region + if (requestedRegion) { + modelInfo = { + ...modelInfo, + providers: modelInfo.providers.filter( + (p) => p.region === requestedRegion, + ), + }; + allModelProviders = allModelProviders.filter( + (p) => p.region === requestedRegion, + ); + if (modelInfo.providers.length === 0) { + throw new HTTPException(400, { + message: `Region '${requestedRegion}' is not available for model ${requestedModel}`, + }); + } + } + // Validate that models requiring image input have at least one image in the request if ( modelInfo.imageInputRequired && diff --git a/apps/gateway/src/chat/tools/parse-model-input.ts b/apps/gateway/src/chat/tools/parse-model-input.ts index bb0d8e9a54..3acfec34e7 100644 --- a/apps/gateway/src/chat/tools/parse-model-input.ts +++ b/apps/gateway/src/chat/tools/parse-model-input.ts @@ -11,6 +11,7 @@ export interface ParseModelInputResult { requestedModel: Model; requestedProvider: Provider | undefined; customProviderName: string | undefined; + requestedRegion: string | undefined; } /** @@ -19,6 +20,7 @@ export interface ParseModelInputResult { * Handles various input formats: * - "auto" or "custom" -> llmgateway provider * - "provider/model" -> specific provider and model + * - "provider/model:region" -> specific provider, model, and region * - "customProvider/model" -> custom provider with any model name * - "model-id" -> model ID lookup * @@ -28,6 +30,7 @@ export function parseModelInput(modelInput: string): ParseModelInputResult { let requestedModel: Model = modelInput as Model; let requestedProvider: Provider | undefined; let customProviderName: string | undefined; + let requestedRegion: string | undefined; // check if there is an exact model match if (modelInput === "auto" || modelInput === "custom") { @@ -48,7 +51,14 @@ export function parseModelInput(modelInput: string): ParseModelInputResult { requestedProvider = providerCandidate as Provider; } // Handle model names with multiple slashes (e.g. together.ai/meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo) - const modelName = split.slice(1).join("/"); + let modelName = split.slice(1).join("/"); + + // Parse optional region suffix (e.g. "qwen-plus:cn-beijing") + if (modelName.includes(":")) { + const colonIdx = modelName.lastIndexOf(":"); + requestedRegion = modelName.slice(colonIdx + 1); + modelName = modelName.slice(0, colonIdx); + } // For custom providers, we don't need to validate the model name // since they can use any OpenAI-compatible model name @@ -131,5 +141,6 @@ export function parseModelInput(modelInput: string): ParseModelInputResult { requestedModel, requestedProvider, customProviderName, + requestedRegion, }; } diff --git a/apps/playground/src/lib/api/v1.d.ts b/apps/playground/src/lib/api/v1.d.ts index 58b8dac560..1c2b9e55b2 100644 --- a/apps/playground/src/lib/api/v1.d.ts +++ b/apps/playground/src/lib/api/v1.d.ts @@ -2931,6 +2931,7 @@ export interface paths { id: string; modelId: string; modelName: string; + region: string | null; providerId: string; providerName: string; status: string; diff --git a/apps/ui/src/lib/api/v1.d.ts b/apps/ui/src/lib/api/v1.d.ts index 58b8dac560..1c2b9e55b2 100644 --- a/apps/ui/src/lib/api/v1.d.ts +++ b/apps/ui/src/lib/api/v1.d.ts @@ -2931,6 +2931,7 @@ export interface paths { id: string; modelId: string; modelName: string; + region: string | null; providerId: string; providerName: string; status: string; diff --git a/ee/admin/src/lib/api/v1.d.ts b/ee/admin/src/lib/api/v1.d.ts index 58b8dac560..1c2b9e55b2 100644 --- a/ee/admin/src/lib/api/v1.d.ts +++ b/ee/admin/src/lib/api/v1.d.ts @@ -2931,6 +2931,7 @@ export interface paths { id: string; modelId: string; modelName: string; + region: string | null; providerId: string; providerName: string; status: string; diff --git a/packages/models/src/models/alibaba.ts b/packages/models/src/models/alibaba.ts index efe2786330..fa4f54f4a1 100644 --- a/packages/models/src/models/alibaba.ts +++ b/packages/models/src/models/alibaba.ts @@ -2082,35 +2082,4 @@ export const alibabaModels = [ }, ], }, - { - id: "deepseek-v3.2", - name: "DeepSeek V3.2", - description: "DeepSeek V3.2 685B parameter model hosted on Alibaba Cloud.", - family: "deepseek", - releasedAt: new Date("2025-03-01"), - providers: [ - { - providerId: "alibaba", - modelName: "deepseek-v3.2", - discount: 0.2, - inputPrice: 0.57 / 1e6, - outputPrice: 1.71 / 1e6, - regions: [ - { id: "singapore" }, - { - id: "cn-beijing", - inputPrice: 0.287 / 1e6, - outputPrice: 0.431 / 1e6, - }, - ], - requestPrice: 0, - contextSize: 131072, - maxOutput: 65536, - streaming: true, - vision: false, - tools: true, - jsonOutput: true, - }, - ], - }, ] as const satisfies ModelDefinition[]; diff --git a/packages/models/src/models/deepseek.ts b/packages/models/src/models/deepseek.ts index d3a3be5b08..30d2b0284e 100644 --- a/packages/models/src/models/deepseek.ts +++ b/packages/models/src/models/deepseek.ts @@ -154,6 +154,28 @@ export const deepseekModels = [ jsonOutputSchema: false, jsonOutput: true, }, + { + providerId: "alibaba", + modelName: "deepseek-v3.2", + discount: 0.2, + inputPrice: 0.57 / 1e6, + outputPrice: 1.71 / 1e6, + regions: [ + { id: "singapore" }, + { + id: "cn-beijing", + inputPrice: 0.287 / 1e6, + outputPrice: 0.431 / 1e6, + }, + ], + requestPrice: 0, + contextSize: 131072, + maxOutput: 65536, + streaming: true, + vision: false, + tools: true, + jsonOutput: true, + }, { providerId: "bytedance", modelName: "deepseek-v3-2-251201", From 91e80dd627b875832787de9347ad18ae7abd7c91 Mon Sep 17 00:00:00 2001 From: romel lauron Date: Wed, 25 Mar 2026 23:25:17 +0800 Subject: [PATCH 13/55] fix: address CodeRabbit review feedback - Propagate usedRegion in streaming retry loop - Use region-specific env vars for initial token resolution in credits/hybrid mode - Carry region in synthesized provider scores and fix isSelected to compare provider+region - Fix low-uptime fallback to exclude only the exact provider+region pair, not all regions - Update hasRegionSpecificEnvKey to accept base key for default region - Fall back to defaultRegion in validate-provider-key - Fix effectiveRegion in create-provider-key dialog - Update envVarName when regional token is resolved - Validate region against model mappings, not provider catalog - Validate alibaba_region against known enum values - Remove unused plan docs Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/api/src/routes/keys-provider.ts | 8 +- apps/code/src/lib/api/v1.d.ts | 12 +- apps/gateway/src/chat/chat.ts | 37 +- .../chat/tools/resolve-provider-context.ts | 23 +- apps/playground/src/lib/api/v1.d.ts | 12 +- .../create-provider-key-dialog.tsx | 15 +- apps/ui/src/lib/api/v1.d.ts | 12 +- apps/worker/src/services/sync-models.ts | 29 +- ...26-03-06-provider-region-support-design.md | 128 ----- .../2026-03-06-provider-region-support.md | 460 ------------------ ee/admin/src/lib/api/v1.d.ts | 12 +- packages/actions/src/validate-provider-key.ts | 4 +- packages/db/src/schema.ts | 2 +- packages/models/src/provider.ts | 15 +- 14 files changed, 122 insertions(+), 647 deletions(-) delete mode 100644 docs/plans/2026-03-06-provider-region-support-design.md delete mode 100644 docs/plans/2026-03-06-provider-region-support.md diff --git a/apps/api/src/routes/keys-provider.ts b/apps/api/src/routes/keys-provider.ts index cbf6c7fcf4..e8bea31a4c 100644 --- a/apps/api/src/routes/keys-provider.ts +++ b/apps/api/src/routes/keys-provider.ts @@ -33,7 +33,9 @@ const providerKeySchema = z.object({ azure_api_version: z.string().optional(), azure_deployment_type: z.enum(["openai", "ai-foundry"]).optional(), azure_validation_model: z.string().optional(), - alibaba_region: z.string().optional(), + alibaba_region: z + .enum(["singapore", "us-virginia", "cn-beijing"]) + .optional(), }) .nullable(), status: z.enum(["active", "inactive", "deleted"]).nullable(), @@ -61,7 +63,9 @@ const createProviderKeySchema = z.object({ azure_api_version: z.string().optional(), azure_deployment_type: z.enum(["openai", "ai-foundry"]).optional(), azure_validation_model: z.string().optional(), - alibaba_region: z.string().optional(), + alibaba_region: z + .enum(["singapore", "us-virginia", "cn-beijing"]) + .optional(), }) .optional(), organizationId: z.string().min(1, "Organization ID is required"), diff --git a/apps/code/src/lib/api/v1.d.ts b/apps/code/src/lib/api/v1.d.ts index 1c2b9e55b2..42e3095265 100644 --- a/apps/code/src/lib/api/v1.d.ts +++ b/apps/code/src/lib/api/v1.d.ts @@ -3673,7 +3673,8 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; - alibaba_region?: string; + /** @enum {string} */ + alibaba_region?: "singapore" | "us-virginia" | "cn-beijing"; } | null; /** @enum {string|null} */ status: "active" | "inactive" | "deleted" | null; @@ -3709,7 +3710,8 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; - alibaba_region?: string; + /** @enum {string} */ + alibaba_region?: "singapore" | "us-virginia" | "cn-beijing"; }; organizationId: string; }; @@ -3738,7 +3740,8 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; - alibaba_region?: string; + /** @enum {string} */ + alibaba_region?: "singapore" | "us-virginia" | "cn-beijing"; } | null; /** @enum {string|null} */ status: "active" | "inactive" | "deleted" | null; @@ -3855,7 +3858,8 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; - alibaba_region?: string; + /** @enum {string} */ + alibaba_region?: "singapore" | "us-virginia" | "cn-beijing"; } | null; /** @enum {string|null} */ status: "active" | "inactive" | "deleted" | null; diff --git a/apps/gateway/src/chat/chat.ts b/apps/gateway/src/chat/chat.ts index ecbd2cabc2..df0dc39812 100644 --- a/apps/gateway/src/chat/chat.ts +++ b/apps/gateway/src/chat/chat.ts @@ -72,6 +72,7 @@ import { providers, type WebSearchTool, expandAllProviderRegions, + getRegionSpecificEnvValue, } from "@llmgateway/models"; import { completionsRequestSchema } from "./schemas/completions.js"; @@ -1243,17 +1244,21 @@ chat.openapi(completions, async (c) => { // Fetch uptime metrics for the requested provider const metricsMap = await getProviderMetricsForCombinations([ - { modelId: baseModelId, providerId: usedProvider }, + { modelId: baseModelId, providerId: usedProvider, region: usedRegion }, ]); - const metrics = metricsMap.get(metricsKey(baseModelId, usedProvider)); + const metrics = metricsMap.get( + metricsKey(baseModelId, usedProvider, usedRegion), + ); // If we have metrics and uptime is below 90%, route to an alternative if (metrics && metrics.uptime !== undefined && metrics.uptime < 90) { const currentUptime = metrics.uptime; // Get available providers for routing const providerIds = modelInfo.providers - .filter((p) => p.providerId !== usedProvider) // Exclude the low-uptime provider + .filter( + (p) => !(p.providerId === usedProvider && p.region === usedRegion), + ) // Exclude the exact low-uptime provider+region pair .map((p) => p.providerId); if (providerIds.length > 0) { @@ -1278,7 +1283,10 @@ chat.openapi(completions, async (c) => { if (!availableProviders.includes(provider.providerId)) { return false; } - if (provider.providerId === usedProvider) { + if ( + provider.providerId === usedProvider && + provider.region === usedRegion + ) { return false; } // If web search tool is requested, only include providers that support it @@ -1619,9 +1627,11 @@ chat.openapi(completions, async (c) => { metricsKey(baseModelId, p.providerId, p.region), ); const price = (p.inputPrice ?? 0) + (p.outputPrice ?? 0); - const isSelected = p.providerId === usedProvider; + const isSelected = + p.providerId === usedProvider && p.region === usedRegion; return { providerId: p.providerId, + region: p.region, score: isSelected ? 1 : 0, price, uptime: metrics?.uptime ?? 0, @@ -1828,6 +1838,14 @@ chat.openapi(completions, async (c) => { usedToken = envResult.token; configIndex = envResult.configIndex; envVarName = envResult.envVarName; + + // Override with region-specific env var if a non-default region is selected + if (usedRegion) { + const regionToken = getRegionSpecificEnvValue(usedProvider, usedRegion); + if (regionToken) { + usedToken = regionToken; + } + } } else if (project.mode === "hybrid") { // First try to get the provider key from the database if (usedProvider === "custom" && customProviderName) { @@ -1893,6 +1911,14 @@ chat.openapi(completions, async (c) => { usedToken = envResult.token; configIndex = envResult.configIndex; envVarName = envResult.envVarName; + + // Override with region-specific env var if a non-default region is selected + if (usedRegion) { + const regionToken = getRegionSpecificEnvValue(usedProvider, usedRegion); + if (regionToken) { + usedToken = regionToken; + } + } } } else { throw new HTTPException(400, { @@ -2794,6 +2820,7 @@ chat.openapi(completions, async (c) => { top_p = ctx.top_p; frequency_penalty = ctx.frequency_penalty; presence_penalty = ctx.presence_penalty; + usedRegion = ctx.usedRegion; } catch { failedProviderIds.add( providerRetryKey(nextProvider.providerId, nextProvider.region), diff --git a/apps/gateway/src/chat/tools/resolve-provider-context.ts b/apps/gateway/src/chat/tools/resolve-provider-context.ts index 78f3dbf170..817fc48fba 100644 --- a/apps/gateway/src/chat/tools/resolve-provider-context.ts +++ b/apps/gateway/src/chat/tools/resolve-provider-context.ts @@ -14,12 +14,12 @@ import { logger } from "@llmgateway/logger"; import { type BaseMessage, getRegionSpecificEnvValue, + getProviderEnvVar, hasMaxTokens, type ModelDefinition, type OpenAIRequestBody, type OpenAIToolInput, type Provider, - type ProviderDefinition, type ProviderRequestBody, providers, type ToolChoiceType, @@ -216,14 +216,16 @@ export async function resolveProviderContext( ); // --- Region validation --- + // Validate against the expanded model-provider mapping (which contains per-model region info) + // rather than the provider-level catalog (which lists all regions the provider supports). if (usedRegion) { - const providerDef = providers.find((p) => p.id === usedProvider) as - | ProviderDefinition - | undefined; - const validRegions = providerDef?.regionConfig?.regions; - if (validRegions && !validRegions.some((r) => r.id === usedRegion)) { + const modelRegions = modelInfo.providers + .filter((p) => p.providerId === usedProvider) + .map((p) => p.region) + .filter(Boolean) as string[]; + if (modelRegions.length > 0 && !modelRegions.includes(usedRegion)) { throw new HTTPException(400, { - message: `Model ${usedModel} is not available in region "${usedRegion}". Available regions: ${validRegions.map((r) => r.id).join(", ")}`, + message: `Model ${usedModel} is not available in region "${usedRegion}". Available regions: ${modelRegions.join(", ")}`, }); } } @@ -233,6 +235,13 @@ export async function resolveProviderContext( const regionToken = getRegionSpecificEnvValue(usedProvider, usedRegion); if (regionToken) { usedToken = regionToken; + // Update envVarName to reflect the regional env var + const baseEnvVar = getProviderEnvVar(usedProvider); + if (baseEnvVar) { + const regionSuffix = usedRegion.toUpperCase().replace(/-/g, "_"); + const regionalEnvVar = `${baseEnvVar}__${regionSuffix}`; + envVarName = process.env[regionalEnvVar] ? regionalEnvVar : baseEnvVar; + } } } diff --git a/apps/playground/src/lib/api/v1.d.ts b/apps/playground/src/lib/api/v1.d.ts index 1c2b9e55b2..42e3095265 100644 --- a/apps/playground/src/lib/api/v1.d.ts +++ b/apps/playground/src/lib/api/v1.d.ts @@ -3673,7 +3673,8 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; - alibaba_region?: string; + /** @enum {string} */ + alibaba_region?: "singapore" | "us-virginia" | "cn-beijing"; } | null; /** @enum {string|null} */ status: "active" | "inactive" | "deleted" | null; @@ -3709,7 +3710,8 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; - alibaba_region?: string; + /** @enum {string} */ + alibaba_region?: "singapore" | "us-virginia" | "cn-beijing"; }; organizationId: string; }; @@ -3738,7 +3740,8 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; - alibaba_region?: string; + /** @enum {string} */ + alibaba_region?: "singapore" | "us-virginia" | "cn-beijing"; } | null; /** @enum {string|null} */ status: "active" | "inactive" | "deleted" | null; @@ -3855,7 +3858,8 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; - alibaba_region?: string; + /** @enum {string} */ + alibaba_region?: "singapore" | "us-virginia" | "cn-beijing"; } | null; /** @enum {string|null} */ status: "active" | "inactive" | "deleted" | null; diff --git a/apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx b/apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx index d8a5fb424b..e16093d920 100644 --- a/apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx +++ b/apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx @@ -73,6 +73,9 @@ export function CreateProviderKeyDialog({ (p) => p.id === selectedProvider, ) as ProviderDefinition | undefined; + const effectiveRegion = + (selectedRegion || selectedProviderDef?.regionConfig?.defaultRegion) ?? ""; + const availableProviders = providers.filter( (provider) => provider.id !== "llmgateway", ); @@ -150,10 +153,10 @@ export function CreateProviderKeyDialog({ }; } // Include region in options for providers that support it - if (selectedProviderDef?.regionConfig && selectedRegion) { + if (selectedProviderDef?.regionConfig && effectiveRegion) { payload.options = { ...payload.options, - [selectedProviderDef.regionConfig.optionsKey]: selectedRegion, + [selectedProviderDef.regionConfig.optionsKey]: effectiveRegion, }; } @@ -410,13 +413,7 @@ export function CreateProviderKeyDialog({ {selectedProviderDef?.regionConfig && (
- diff --git a/apps/ui/src/lib/api/v1.d.ts b/apps/ui/src/lib/api/v1.d.ts index 1c2b9e55b2..42e3095265 100644 --- a/apps/ui/src/lib/api/v1.d.ts +++ b/apps/ui/src/lib/api/v1.d.ts @@ -3673,7 +3673,8 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; - alibaba_region?: string; + /** @enum {string} */ + alibaba_region?: "singapore" | "us-virginia" | "cn-beijing"; } | null; /** @enum {string|null} */ status: "active" | "inactive" | "deleted" | null; @@ -3709,7 +3710,8 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; - alibaba_region?: string; + /** @enum {string} */ + alibaba_region?: "singapore" | "us-virginia" | "cn-beijing"; }; organizationId: string; }; @@ -3738,7 +3740,8 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; - alibaba_region?: string; + /** @enum {string} */ + alibaba_region?: "singapore" | "us-virginia" | "cn-beijing"; } | null; /** @enum {string|null} */ status: "active" | "inactive" | "deleted" | null; @@ -3855,7 +3858,8 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; - alibaba_region?: string; + /** @enum {string} */ + alibaba_region?: "singapore" | "us-virginia" | "cn-beijing"; } | null; /** @enum {string|null} */ status: "active" | "inactive" | "deleted" | null; diff --git a/apps/worker/src/services/sync-models.ts b/apps/worker/src/services/sync-models.ts index 6d16e4682f..e90c9f4068 100644 --- a/apps/worker/src/services/sync-models.ts +++ b/apps/worker/src/services/sync-models.ts @@ -101,20 +101,21 @@ export async function syncProvidersAndModels() { const expandedProviders = expandAllProviderRegions(modelDef.providers); for (const mapping of expandedProviders) { const mappingRegion = mapping.region; - const mappings = await database - .select() - .from(modelProviderMapping) - .where( - and( - eq(modelProviderMapping.modelId, modelDef.id), - eq(modelProviderMapping.providerId, mapping.providerId), - mappingRegion - ? eq(modelProviderMapping.region, mappingRegion) - : isNull(modelProviderMapping.region), - ), - ) - .limit(1); - const existingMapping = mappings[0]; + const existingMapping = ( + await database + .select() + .from(modelProviderMapping) + .where( + and( + eq(modelProviderMapping.modelId, modelDef.id), + eq(modelProviderMapping.providerId, mapping.providerId), + mappingRegion + ? eq(modelProviderMapping.region, mappingRegion) + : isNull(modelProviderMapping.region), + ), + ) + .limit(1) + )[0]; if (existingMapping) { // Use null (not undefined) for missing fields to ensure DB is updated diff --git a/docs/plans/2026-03-06-provider-region-support-design.md b/docs/plans/2026-03-06-provider-region-support-design.md deleted file mode 100644 index 5d3ca1b571..0000000000 --- a/docs/plans/2026-03-06-provider-region-support-design.md +++ /dev/null @@ -1,128 +0,0 @@ -# Provider Region Support Design - -## Problem - -Some cloud providers host AI models in specific geographic regions with different API endpoints. Alibaba Cloud's DashScope has 3 regional endpoints (Singapore, US Virginia, China Beijing), each requiring region-specific API keys. Currently, the gateway hardcodes `dashscope-intl.aliyuncs.com` (Singapore). To support models that require different regions, we need a general region abstraction. - -This also applies to AWS Bedrock (which has a hacky region prefix system today) and could extend to other providers in the future. - -## Approach - -Add a `regions` array to `ProviderModelMapping` that declares which regions each model supports. The actual region selection happens at runtime via environment variables (credits mode) or provider key options (api-keys mode). - -## Data Model Changes - -### `ProviderModelMapping` (packages/models/src/models.ts) - -New optional field: - -```typescript -/** Available regions for this model/provider combination. - * When set, the gateway uses the configured region (env var or provider key) - * and validates it against this list. - * When unset, the provider uses its default endpoint. */ -regions?: string[]; -``` - -### `ProviderDefinition` (packages/models/src/providers.ts) - -Add optional region env var to Alibaba's config: - -```typescript -{ - id: "alibaba", - env: { - required: { apiKey: "LLM_ALIBABA_API_KEY" }, - optional: { region: "LLM_ALIBABA_REGION" }, - }, -} -``` - -### `ProviderKeyOptions` (packages/db) - -Add Alibaba region for user-managed provider keys: - -```typescript -export interface ProviderKeyOptions { - // ...existing fields - alibaba_region?: string; -} -``` - -### Alibaba Model Definitions (packages/models/src/models/alibaba.ts) - -Add `regions` to each `providerId: "alibaba"` mapping: - -```typescript -{ - providerId: "alibaba", - modelName: "qwen-plus", - regions: ["singapore", "us-virginia", "cn-beijing"], - // ...rest unchanged -} -``` - -## Endpoint URL Resolution - -In `packages/actions/src/get-provider-endpoint.ts`, the Alibaba case changes from a hardcoded URL to region-aware resolution: - -```typescript -case "alibaba": { - const region = - providerKeyOptions?.alibaba_region ?? - getProviderEnvValue("alibaba", "region", configIndex, "singapore") ?? - "singapore"; - - const baseUrls: Record = { - "singapore": "https://dashscope-intl.aliyuncs.com", - "us-virginia": "https://dashscope-us.aliyuncs.com", - "cn-beijing": "https://dashscope.aliyuncs.com", - }; - - if (imageGenerations) { - url = baseUrls[region] ?? baseUrls["singapore"]; - } else { - url = `${baseUrls[region] ?? baseUrls["singapore"]}/compatible-mode`; - } - break; -} -``` - -## Request-Time Validation - -In `apps/gateway/src/chat/tools/resolve-provider-context.ts`, after the provider mapping is found, validate the configured region: - -```typescript -if (providerMapping.regions && providerMapping.regions.length > 0) { - const configuredRegion = /* resolve from providerKeyOptions or env */; - if (configuredRegion && !providerMapping.regions.includes(configuredRegion)) { - throw new HTTPException(400, { - message: `Model ${model} is not available in region ${configuredRegion}. Available regions: ${providerMapping.regions.join(", ")}`, - }); - } -} -``` - -## UI Changes - -In `apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx`, add a region dropdown for Alibaba provider keys (same pattern as AWS Bedrock region prefix and Azure resource): - -- State: `const [alibabaRegion, setAlibabaRegion] = useState("singapore")` -- Dropdown options: Singapore (default), US (Virginia), China (Beijing) -- Saved to `payload.options.alibaba_region` - -## Files to Modify - -1. `packages/models/src/models.ts` - Add `regions?` to `ProviderModelMapping` -2. `packages/models/src/providers.ts` - Add optional region env var to Alibaba -3. `packages/models/src/models/alibaba.ts` - Add `regions` to all Alibaba provider mappings -4. `packages/db` - Add `alibaba_region` to `ProviderKeyOptions` -5. `packages/actions/src/get-provider-endpoint.ts` - Region-aware URL resolution for Alibaba -6. `apps/gateway/src/chat/tools/resolve-provider-context.ts` - Region validation at request time -7. `apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx` - Alibaba region dropdown -8. `apps/ui/src/lib/api/v1.d.ts` - Update API types if needed - -## Future Work - -- Migrate AWS Bedrock from the hacky `aws_bedrock_region_prefix` to use the same `regions` pattern -- Add region support to other providers as needed (e.g., Google Vertex could use this instead of its custom env var) diff --git a/docs/plans/2026-03-06-provider-region-support.md b/docs/plans/2026-03-06-provider-region-support.md deleted file mode 100644 index 8ab1166fdb..0000000000 --- a/docs/plans/2026-03-06-provider-region-support.md +++ /dev/null @@ -1,460 +0,0 @@ -# Provider Region Support Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Add per-model region support so the gateway can route Alibaba Cloud requests to the correct regional endpoint (Singapore, US Virginia, or China Beijing) based on env vars or provider key options. - -**Architecture:** Add `regions?: string[]` to `ProviderModelMapping` to declare region availability per model. Region selection happens at runtime via `LLM_ALIBABA_REGION` env var (credits mode) or `alibaba_region` in `ProviderKeyOptions` (api-keys mode). The gateway validates the configured region against the model's `regions` array and constructs the correct DashScope regional URL. - -**Tech Stack:** TypeScript, Drizzle ORM, Hono, Next.js (React), Radix UI - ---- - -### Task 1: Add `regions` field to `ProviderModelMapping` - -**Files:** -- Modify: `packages/models/src/models.ts:176-182` - -**Step 1: Add the field** - -Add before the closing `}` of `ProviderModelMapping` (after `imageGenerations`): - -```typescript - /** - * Available regions for this model/provider combination. - * When set, the gateway validates the configured region against this list. - * When unset, the provider uses its default endpoint. - */ - regions?: string[]; -``` - -**Step 2: Verify build** - -Run: `pnpm build --filter @llmgateway/models` -Expected: SUCCESS (new optional field, no breaking changes) - -**Step 3: Commit** - -```bash -git add packages/models/src/models.ts -git commit -m "feat: add regions field to ProviderModelMapping" -``` - ---- - -### Task 2: Add optional region env var to Alibaba provider definition - -**Files:** -- Modify: `packages/models/src/providers.ts:198-213` - -**Step 1: Add optional region to Alibaba env config** - -Change the Alibaba provider definition from: - -```typescript - env: { - required: { - apiKey: "LLM_ALIBABA_API_KEY", - }, - }, -``` - -To: - -```typescript - env: { - required: { - apiKey: "LLM_ALIBABA_API_KEY", - }, - optional: { - region: "LLM_ALIBABA_REGION", - }, - }, -``` - -**Step 2: Verify build** - -Run: `pnpm build --filter @llmgateway/models` -Expected: SUCCESS - -**Step 3: Commit** - -```bash -git add packages/models/src/providers.ts -git commit -m "feat: add LLM_ALIBABA_REGION env var" -``` - ---- - -### Task 3: Add `alibaba_region` to `ProviderKeyOptions` - -**Files:** -- Modify: `packages/db/src/schema.ts:388-394` - -**Step 1: Add the field** - -Change `ProviderKeyOptions` from: - -```typescript -export interface ProviderKeyOptions { - aws_bedrock_region_prefix?: "us." | "global." | "eu."; - azure_resource?: string; - azure_api_version?: string; - azure_deployment_type?: "openai" | "ai-foundry"; - azure_validation_model?: string; -} -``` - -To: - -```typescript -export interface ProviderKeyOptions { - aws_bedrock_region_prefix?: "us." | "global." | "eu."; - azure_resource?: string; - azure_api_version?: string; - azure_deployment_type?: "openai" | "ai-foundry"; - azure_validation_model?: string; - alibaba_region?: string; -} -``` - -**Step 2: Verify build** - -Run: `pnpm build --filter @llmgateway/db` -Expected: SUCCESS (JSONB field, no migration needed) - -**Step 3: Commit** - -```bash -git add packages/db/src/schema.ts -git commit -m "feat: add alibaba_region to ProviderKeyOptions" -``` - ---- - -### Task 4: Add `regions` to Alibaba model definitions - -**Files:** -- Modify: `packages/models/src/models/alibaba.ts` - -**Step 1: Add `regions` to every `providerId: "alibaba"` mapping** - -For every provider mapping in `alibaba.ts` that has `providerId: "alibaba"`, add: - -```typescript -regions: ["singapore", "us-virginia", "cn-beijing"], -``` - -This applies to the following models (all with `providerId: "alibaba"`): -- qwen-max -- qwen-max-latest -- qwen-plus -- qwen-plus-latest -- qwen-flash -- qwen-omni-turbo -- qwen-turbo -- qwen3-coder-plus -- qwen-vl-max -- qwen-vl-plus -- qwen3-next-80b-a3b-thinking -- qwen3-next-80b-a3b-instruct -- qwen3-max (uses modelName "qwen3-max-preview") -- qwen3-coder-flash -- qwen3-vl-plus -- qwen3-vl-flash -- qwen3-vl-235b-a22b-instruct -- qwen3-vl-235b-a22b-thinking -- qwen2-5-vl-32b-instruct -- qwen3-max-2026-01-23 -- qwq-plus -- qwen-coder-plus -- qwen35-397b-a17b -- qwen-image-plus -- qwen-image-max -- qwen-image -- qwen-image-max-2025-12-30 -- qwen-image-edit-plus -- qwen-image-edit-max - -Do NOT add `regions` to mappings with other `providerId` values (nebius, novita, cerebras, canopywave). - -Place the `regions` field right after `providerId` and `modelName` for readability. Example: - -```typescript -{ - providerId: "alibaba", - modelName: "qwen-plus", - regions: ["singapore", "us-virginia", "cn-beijing"], - discount: 0.2, - // ...rest -} -``` - -**Step 2: Verify build** - -Run: `pnpm build --filter @llmgateway/models` -Expected: SUCCESS - -**Step 3: Commit** - -```bash -git add packages/models/src/models/alibaba.ts -git commit -m "feat: add regions to Alibaba model definitions" -``` - ---- - -### Task 5: Region-aware endpoint URL resolution for Alibaba - -**Files:** -- Modify: `packages/actions/src/get-provider-endpoint.ts:102-109` - -**Step 1: Replace the hardcoded Alibaba case** - -Change from: - -```typescript - case "alibaba": - // Use different base URL for image generation vs chat completions - if (imageGenerations) { - url = "https://dashscope-intl.aliyuncs.com"; - } else { - url = "https://dashscope-intl.aliyuncs.com/compatible-mode"; - } - break; -``` - -To: - -```typescript - case "alibaba": { - const alibabaRegion = - providerKeyOptions?.alibaba_region ?? - getProviderEnvValue("alibaba", "region", configIndex, "singapore") ?? - "singapore"; - - const alibabaBaseUrls: Record = { - singapore: "https://dashscope-intl.aliyuncs.com", - "us-virginia": "https://dashscope-us.aliyuncs.com", - "cn-beijing": "https://dashscope.aliyuncs.com", - }; - - const alibabaBase = - alibabaBaseUrls[alibabaRegion] ?? alibabaBaseUrls.singapore; - - if (imageGenerations) { - url = alibabaBase; - } else { - url = `${alibabaBase}/compatible-mode`; - } - break; - } -``` - -**Step 2: Verify build** - -Run: `pnpm build --filter @llmgateway/actions` -Expected: SUCCESS - -**Step 3: Commit** - -```bash -git add packages/actions/src/get-provider-endpoint.ts -git commit -m "feat: region-aware Alibaba endpoint resolution" -``` - ---- - -### Task 6: Region validation in resolve-provider-context - -**Files:** -- Modify: `apps/gateway/src/chat/tools/resolve-provider-context.ts:177-189` - -**Step 1: Add region validation after provider mapping lookup** - -After the existing block at line ~180 (`const providerMappingForSelected = ...`) and before the reasoning check, add: - -```typescript - // --- Region validation --- - const providerMappingRegions = (providerMappingForSelected as ProviderModelMapping)?.regions; - if (providerMappingRegions && providerMappingRegions.length > 0) { - const configuredRegion = - providerKey?.options?.alibaba_region ?? - (usedProvider === "alibaba" - ? getProviderEnvValue("alibaba", "region", configIndex, "singapore") ?? "singapore" - : undefined); - - if (configuredRegion && !providerMappingRegions.includes(configuredRegion)) { - throw new HTTPException(400, { - message: `Model ${usedModel} is not available in region "${configuredRegion}". Available regions: ${providerMappingRegions.join(", ")}`, - }); - } - } -``` - -You'll need to add `getProviderEnvValue` to the imports from `@llmgateway/models` if not already imported. - -**Step 2: Check imports** - -Verify `getProviderEnvValue` is imported. Check the existing imports at the top of the file. If missing, add it to the import from `@llmgateway/models`. - -**Step 3: Verify build** - -Run: `pnpm build --filter gateway` -Expected: SUCCESS - -**Step 4: Commit** - -```bash -git add apps/gateway/src/chat/tools/resolve-provider-context.ts -git commit -m "feat: validate region against model regions" -``` - ---- - -### Task 7: UI - Alibaba region dropdown in provider key dialog - -**Files:** -- Modify: `apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx` - -**Step 1: Add state variable** - -After the existing `azureValidationModel` state (line ~74), add: - -```typescript - const [alibabaRegion, setAlibabaRegion] = useState("singapore"); -``` - -**Step 2: Add `alibaba_region` to payload type** - -In the payload type definition (line ~158), add `alibaba_region` to the `options` type: - -```typescript - options?: { - aws_bedrock_region_prefix?: "us." | "global." | "eu."; - azure_resource?: string; - azure_api_version?: string; - azure_deployment_type?: "openai" | "ai-foundry"; - azure_validation_model?: string; - alibaba_region?: string; - }; -``` - -**Step 3: Add payload construction** - -After the Azure payload block (after line ~197), add: - -```typescript - if (selectedProvider === "alibaba") { - payload.options = { - alibaba_region: alibabaRegion, - }; - } -``` - -**Step 4: Add the dropdown JSX** - -After the AWS Bedrock section (after the closing `)}` at line ~347) and before the Azure section, add: - -```tsx - {selectedProvider === "alibaba" && ( -
- - -

- Region for Alibaba Cloud DashScope API. API keys are - region-specific. -

-
- )} -``` - -**Step 5: Add reset in handleClose** - -In the `handleClose` function (line ~224), add after the `setAzureValidationModel` reset: - -```typescript - setAlibabaRegion("singapore"); -``` - -**Step 6: Verify build** - -Run: `pnpm build --filter ui` -Expected: SUCCESS - -**Step 7: Commit** - -```bash -git add apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx -git commit -m "feat(ui): add Alibaba region dropdown to provider key dialog" -``` - ---- - -### Task 8: Final build and format - -**Step 1: Format all code** - -Run: `pnpm format` - -**Step 2: Full build** - -Run: `pnpm build` -Expected: SUCCESS across all packages - -**Step 3: Run unit tests** - -Run: `pnpm test:unit` -Expected: All tests pass - -**Step 4: Commit any formatting changes** - -```bash -git add -A -git commit -m "chore: format" -``` - ---- - -### Task 9: Update API types (if needed) - -**Files:** -- Check: `apps/ui/src/lib/api/v1.d.ts` - -**Step 1: Check if API types are auto-generated** - -The `v1.d.ts` file may be auto-generated from the API schema. Check if there's a generation command. - -Run: `grep -r "openapi\|generate.*types\|v1.d.ts" package.json packages/*/package.json apps/*/package.json | head -20` - -If auto-generated, run the generation command. If manually maintained, add `alibaba_region?: string` to the relevant options type. - -**Step 2: Verify build** - -Run: `pnpm build` -Expected: SUCCESS - -**Step 3: Commit if changes were needed** - -```bash -git add apps/ui/src/lib/api/v1.d.ts -git commit -m "chore: update API types for alibaba_region" -``` diff --git a/ee/admin/src/lib/api/v1.d.ts b/ee/admin/src/lib/api/v1.d.ts index 1c2b9e55b2..42e3095265 100644 --- a/ee/admin/src/lib/api/v1.d.ts +++ b/ee/admin/src/lib/api/v1.d.ts @@ -3673,7 +3673,8 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; - alibaba_region?: string; + /** @enum {string} */ + alibaba_region?: "singapore" | "us-virginia" | "cn-beijing"; } | null; /** @enum {string|null} */ status: "active" | "inactive" | "deleted" | null; @@ -3709,7 +3710,8 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; - alibaba_region?: string; + /** @enum {string} */ + alibaba_region?: "singapore" | "us-virginia" | "cn-beijing"; }; organizationId: string; }; @@ -3738,7 +3740,8 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; - alibaba_region?: string; + /** @enum {string} */ + alibaba_region?: "singapore" | "us-virginia" | "cn-beijing"; } | null; /** @enum {string|null} */ status: "active" | "inactive" | "deleted" | null; @@ -3855,7 +3858,8 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; - alibaba_region?: string; + /** @enum {string} */ + alibaba_region?: "singapore" | "us-virginia" | "cn-beijing"; } | null; /** @enum {string|null} */ status: "active" | "inactive" | "deleted" | null; diff --git a/packages/actions/src/validate-provider-key.ts b/packages/actions/src/validate-provider-key.ts index 6fbf1035c7..d083cf4939 100644 --- a/packages/actions/src/validate-provider-key.ts +++ b/packages/actions/src/validate-provider-key.ts @@ -29,9 +29,9 @@ function getValidationModel( | undefined; const regionKey = providerDef?.regionConfig?.optionsKey; const selectedRegion = regionKey - ? (providerKeyOptions as Record | undefined)?.[ + ? ((providerKeyOptions as Record | undefined)?.[ regionKey - ] + ] ?? providerDef?.regionConfig?.defaultRegion) : undefined; const currentDate = new Date(); diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index 4c8059657a..f31e03f4d7 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -426,7 +426,7 @@ export interface ProviderKeyOptions { azure_api_version?: string; azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; - alibaba_region?: string; + alibaba_region?: "singapore" | "us-virginia" | "cn-beijing"; } export const providerKey = pgTable( diff --git a/packages/models/src/provider.ts b/packages/models/src/provider.ts index 4dd2778ec9..648f06f03f 100644 --- a/packages/models/src/provider.ts +++ b/packages/models/src/provider.ts @@ -127,8 +127,9 @@ export function getRegionSpecificEnvValue( } /** - * Check whether a dedicated env var exists for a specific region - * (i.e. `{BASE_ENV_VAR}__{REGION}`), WITHOUT falling back to the base key. + * Check whether an env var exists for a specific region. + * Returns true if a region-specific env var (`{BASE_ENV_VAR}__{REGION}`) exists, + * OR if the base env var exists and the queried region is the provider's default region. */ export function hasRegionSpecificEnvKey( provider: Provider, @@ -139,5 +140,13 @@ export function hasRegionSpecificEnvKey( return false; } const regionSuffix = region.toUpperCase().replace(/-/g, "_"); - return Boolean(process.env[`${baseEnvVar}__${regionSuffix}`]); + if (process.env[`${baseEnvVar}__${regionSuffix}`]) { + return true; + } + // The base key covers the provider's default region + const def = getProviderDefinition(provider); + if (def?.regionConfig?.defaultRegion === region && process.env[baseEnvVar]) { + return true; + } + return false; } From a8237c9599a3dd84166bd14ae53a403fbeffb5c3 Mon Sep 17 00:00:00 2001 From: romel lauron Date: Thu, 26 Mar 2026 16:10:06 +0800 Subject: [PATCH 14/55] feat: support region filter in TEST_MODELS Add optional :region suffix to TEST_MODELS entries for targeting specific provider regions in e2e tests. Example: TEST_MODELS=alibaba/deepseek-v3.2:cn-beijing Without region suffix, all regions of that provider/model match. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/gateway/src/chat-helpers.e2e.ts | 88 ++++++++++++++++----- apps/gateway/src/chat-prompt-caching.e2e.ts | 6 +- 2 files changed, 73 insertions(+), 21 deletions(-) diff --git a/apps/gateway/src/chat-helpers.e2e.ts b/apps/gateway/src/chat-helpers.e2e.ts index 22339530cc..595e4044f7 100644 --- a/apps/gateway/src/chat-helpers.e2e.ts +++ b/apps/gateway/src/chat-helpers.e2e.ts @@ -29,11 +29,65 @@ export const fullMode = process.env.FULL_MODE; export const logMode = process.env.LOG_MODE; // Parse TEST_MODELS environment variable +// Supports optional region filter: "alibaba/deepseek-v3.2:cn-beijing" export const testModelsEnv = process.env.TEST_MODELS; export const specifiedModels = testModelsEnv ? testModelsEnv.split(",").map((m) => m.trim()) : null; +interface ParsedTestModel { + providerId: string; + modelId: string; + region?: string; +} + +function parseTestModel(spec: string): ParsedTestModel { + const [providerModel, region] = spec.split(":"); + const [providerId, ...modelParts] = providerModel.split("/"); + return { + providerId, + modelId: modelParts.join("/"), + region, + }; +} + +const parsedTestModels = specifiedModels?.map(parseTestModel) ?? null; + +/** + * Check if a provider/model/region matches any TEST_MODELS entry. + * "alibaba/model" matches all regions. "alibaba/model:cn-beijing" matches only that region. + */ +export function matchesTestModel( + providerId: string, + modelId: string, + region?: string, +): boolean { + if (!parsedTestModels) { + return false; + } + return parsedTestModels.some( + (t) => + t.providerId === providerId && + t.modelId === modelId && + (t.region === undefined || t.region === region), + ); +} + +/** + * Check if a model (any provider) matches any TEST_MODELS entry. + */ +function modelMatchesAnyTestModel( + modelId: string, + providers: ProviderModelMapping[], +): boolean { + if (!parsedTestModels) { + return false; + } + return providers.some((p) => + matchesTestModel(p.providerId, modelId, p.region), + ); +} + // Parse TEST_PROVIDERS environment variable (filter by provider name) export const testProvidersEnv = process.env.TEST_PROVIDERS; export const specifiedProviders = testProvidersEnv @@ -105,11 +159,9 @@ export const filteredModels = models } } if (specifiedModels) { - const modelInTestModels = model.providers.some( - (provider: ProviderModelMapping) => { - const providerModelId = `${provider.providerId}/${model.id}`; - return specifiedModels.includes(providerModelId); - }, + const modelInTestModels = modelMatchesAnyTestModel( + model.id, + model.providers as ProviderModelMapping[], ); if (modelInTestModels) { return true; @@ -149,11 +201,9 @@ export const filteredModels = models } } if (specifiedModels) { - const modelInTestModels = model.providers.some( - (provider: ProviderModelMapping) => { - const providerModelId = `${provider.providerId}/${model.id}`; - return specifiedModels.includes(providerModelId); - }, + const modelInTestModels = modelMatchesAnyTestModel( + model.id, + model.providers as ProviderModelMapping[], ); if (modelInTestModels) { return true; @@ -171,8 +221,7 @@ export const filteredModels = models if (specifiedProviders) { return specifiedProviders.includes(provider.providerId); } - const providerModelId = `${provider.providerId}/${model.id}`; - return specifiedModels!.includes(providerModelId); + return matchesTestModel(provider.providerId, model.id, provider.region); }); }); @@ -222,8 +271,9 @@ export const testModels = filteredModels continue; } } else { - const providerModelId = `${provider.providerId}/${model.id}`; - if (!specifiedModels!.includes(providerModelId)) { + if ( + !matchesTestModel(provider.providerId, model.id, provider.region) + ) { continue; } } @@ -248,8 +298,9 @@ export const testModels = filteredModels continue; } } else if (specifiedModels) { - const providerModelId = `${provider.providerId}/${model.id}`; - if (!specifiedModels.includes(providerModelId)) { + if ( + !matchesTestModel(provider.providerId, model.id, provider.region) + ) { continue; } } else { @@ -308,8 +359,9 @@ export const providerModels = filteredModels continue; } } else { - const providerModelId = `${provider.providerId}/${model.id}`; - if (!specifiedModels!.includes(providerModelId)) { + if ( + !matchesTestModel(provider.providerId, model.id, provider.region) + ) { continue; } } diff --git a/apps/gateway/src/chat-prompt-caching.e2e.ts b/apps/gateway/src/chat-prompt-caching.e2e.ts index 35f21e4e6d..9fa2bfcef4 100644 --- a/apps/gateway/src/chat-prompt-caching.e2e.ts +++ b/apps/gateway/src/chat-prompt-caching.e2e.ts @@ -10,6 +10,7 @@ import { getTestOptions, hasOnlyModels, logMode, + matchesTestModel, specifiedModels, validateLogByRequestId, validateResponse, @@ -51,10 +52,9 @@ const promptCachingModels = filteredModels continue; } - // Filter by TEST_MODELS if specified + // Filter by TEST_MODELS if specified (supports region: "alibaba/model:cn-beijing") if (specifiedModels) { - const providerModelId = `${provider.providerId}/${model.id}`; - if (!specifiedModels.includes(providerModelId)) { + if (!matchesTestModel(provider.providerId, model.id, provider.region)) { continue; } // TEST_MODELS takes precedence over test: "skip", so don't skip if model is in TEST_MODELS From ff3acb3b9fa15fde5008226ad56cfbeb7d34e96f Mon Sep 17 00:00:00 2001 From: romel lauron Date: Thu, 26 Mar 2026 16:13:40 +0800 Subject: [PATCH 15/55] fix: expand regions in model-level TEST_MODELS filter The model-level filters used raw (unexpanded) providers, so region-specific TEST_MODELS entries like alibaba/model:cn-beijing were not matching nested region arrays. Expand providers before matching in both modelMatchesAnyTestModel and the final filteredModels filter. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/gateway/src/chat-helpers.e2e.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/gateway/src/chat-helpers.e2e.ts b/apps/gateway/src/chat-helpers.e2e.ts index 595e4044f7..e031cd6835 100644 --- a/apps/gateway/src/chat-helpers.e2e.ts +++ b/apps/gateway/src/chat-helpers.e2e.ts @@ -83,7 +83,9 @@ function modelMatchesAnyTestModel( if (!parsedTestModels) { return false; } - return providers.some((p) => + // Expand regions so "alibaba/model:cn-beijing" matches a nested region entry + const expanded = expandAllProviderRegions(providers); + return expanded.some((p) => matchesTestModel(p.providerId, modelId, p.region), ); } @@ -217,7 +219,10 @@ export const filteredModels = models if (!specifiedModels && !specifiedProviders) { return true; } - return model.providers.some((provider: ProviderModelMapping) => { + const expanded = expandAllProviderRegions( + model.providers as ProviderModelMapping[], + ); + return expanded.some((provider: ProviderModelMapping) => { if (specifiedProviders) { return specifiedProviders.includes(provider.providerId); } From c213f0e90b740b816cfdcf87c2c016d6ac1501ee Mon Sep 17 00:00:00 2001 From: romel lauron Date: Thu, 26 Mar 2026 16:21:44 +0800 Subject: [PATCH 16/55] feat: include region in response metadata Add used_region to response metadata and region to routing attempts array so clients can see which region was used. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/gateway/src/chat/chat.ts | 13 +++++++++++++ apps/gateway/src/chat/tools/retry-with-fallback.ts | 1 + .../src/chat/tools/transform-response-to-openai.ts | 6 ++++++ 3 files changed, 20 insertions(+) diff --git a/apps/gateway/src/chat/chat.ts b/apps/gateway/src/chat/chat.ts index df0dc39812..969fecc573 100644 --- a/apps/gateway/src/chat/chat.ts +++ b/apps/gateway/src/chat/chat.ts @@ -252,12 +252,14 @@ const completions = createRoute({ requested_provider: z.string().nullable(), used_model: z.string(), used_provider: z.string(), + used_region: z.string().nullable().optional(), underlying_used_model: z.string(), routing: z .array( z.object({ provider: z.string(), model: z.string(), + region: z.string().optional(), status_code: z.number(), error_type: z.string(), }), @@ -2984,6 +2986,7 @@ chat.openapi(completions, async (c) => { routingAttempts.push({ provider: usedProvider, model: usedModel, + ...(usedRegion && { region: usedRegion }), status_code: 0, error_type: getErrorType(0), succeeded: false, @@ -3273,6 +3276,7 @@ chat.openapi(completions, async (c) => { routingAttempts.push({ provider: usedProvider, model: usedModel, + ...(usedRegion && { region: usedRegion }), status_code: 0, error_type: getErrorType(0), succeeded: false, @@ -3449,6 +3453,7 @@ chat.openapi(completions, async (c) => { routingAttempts.push({ provider: usedProvider, model: usedModel, + ...(usedRegion && { region: usedRegion }), status_code: res.status, error_type: getErrorType(res.status), succeeded: false, @@ -3477,6 +3482,7 @@ chat.openapi(completions, async (c) => { requested_provider: requestedProvider, used_model: baseModelName, used_provider: usedProvider, + ...(usedRegion && { used_region: usedRegion }), underlying_used_model: usedModel, }, }; @@ -3570,6 +3576,7 @@ chat.openapi(completions, async (c) => { routingAttempts.push({ provider: usedProvider, model: usedModel, + ...(usedRegion && { region: usedRegion }), status_code: res.status, error_type: "none", succeeded: true, @@ -5173,6 +5180,7 @@ chat.openapi(completions, async (c) => { requested_provider: requestedProvider ?? null, used_model: baseModelName, used_provider: usedProvider, + ...(usedRegion && { used_region: usedRegion }), underlying_used_model: usedModel, routing: routingAttempts, }, @@ -5798,6 +5806,7 @@ chat.openapi(completions, async (c) => { routingAttempts.push({ provider: usedProvider, model: usedModel, + ...(usedRegion && { region: usedRegion }), status_code: 0, error_type: getErrorType(0), succeeded: false, @@ -6234,6 +6243,7 @@ chat.openapi(completions, async (c) => { routingAttempts.push({ provider: usedProvider, model: usedModel, + ...(usedRegion && { region: usedRegion }), status_code: res.status, error_type: getErrorType(res.status), succeeded: false, @@ -6270,6 +6280,7 @@ chat.openapi(completions, async (c) => { requested_provider: requestedProvider, used_model: baseModelName, used_provider: usedProvider, + ...(usedRegion && { used_region: usedRegion }), underlying_used_model: usedModel, }, }); @@ -6312,6 +6323,7 @@ chat.openapi(completions, async (c) => { routingAttempts.push({ provider: usedProvider, model: usedModel, + ...(usedRegion && { region: usedRegion }), status_code: res.status, error_type: "none", succeeded: true, @@ -6674,6 +6686,7 @@ chat.openapi(completions, async (c) => { false, // showUpgradeMessage - never show since Pro plan is removed annotations, routingAttempts.length > 0 ? routingAttempts : null, + usedRegion, ); // Extract plugin IDs for logging diff --git a/apps/gateway/src/chat/tools/retry-with-fallback.ts b/apps/gateway/src/chat/tools/retry-with-fallback.ts index 87e85a094b..4ccb446c82 100644 --- a/apps/gateway/src/chat/tools/retry-with-fallback.ts +++ b/apps/gateway/src/chat/tools/retry-with-fallback.ts @@ -3,6 +3,7 @@ export const MAX_RETRIES = 2; export interface RoutingAttempt { provider: string; model: string; + region?: string; status_code: number; error_type: string; succeeded: boolean; diff --git a/apps/gateway/src/chat/tools/transform-response-to-openai.ts b/apps/gateway/src/chat/tools/transform-response-to-openai.ts index 3dd4e565b5..583594d686 100644 --- a/apps/gateway/src/chat/tools/transform-response-to-openai.ts +++ b/apps/gateway/src/chat/tools/transform-response-to-openai.ts @@ -81,6 +81,7 @@ export function transformResponseToOpenai( showUpgradeMessage = false, annotations: Annotation[] | null = null, routing: RoutingAttempt[] | null = null, + usedRegion?: string | undefined, ) { let transformedResponse = json; @@ -144,6 +145,7 @@ export function transformResponseToOpenai( requested_provider: requestedProvider, used_model: baseModelName, used_provider: usedProvider, + ...(usedRegion && { used_region: usedRegion }), underlying_used_model: usedModel, ...(routing && { routing }), }, @@ -192,6 +194,7 @@ export function transformResponseToOpenai( requested_provider: requestedProvider, used_model: baseModelName, used_provider: usedProvider, + ...(usedRegion && { used_region: usedRegion }), underlying_used_model: usedModel, ...(routing && { routing }), }, @@ -264,6 +267,7 @@ export function transformResponseToOpenai( requested_provider: requestedProvider, used_model: baseModelName, used_provider: usedProvider, + ...(usedRegion && { used_region: usedRegion }), underlying_used_model: usedModel, ...(routing && { routing }), }; @@ -325,6 +329,7 @@ export function transformResponseToOpenai( requested_provider: requestedProvider, used_model: baseModelName, used_provider: usedProvider, + ...(usedRegion && { used_region: usedRegion }), underlying_used_model: usedModel, ...(routing && { routing }), }, @@ -809,6 +814,7 @@ export function transformResponseToOpenai( requested_provider: requestedProvider, used_model: baseModelName, used_provider: usedProvider, + ...(usedRegion && { used_region: usedRegion }), underlying_used_model: usedModel, ...(routing && { routing }), }; From cf6a5522310c0a3175ac9fc2f4a4ef0dfe7cccb2 Mon Sep 17 00:00:00 2001 From: romel lauron Date: Thu, 26 Mar 2026 16:30:53 +0800 Subject: [PATCH 17/55] fix: region metadata and scoring for direct provider Fix region not appearing in response metadata by: - Resolving usedRegion in the direct-provider path (when requestedProvider is set, not just auto-routing) - Expanding modelWithPricing providers so scoring can match region entries for price lookup - Adding used_region to all Alibaba and other provider-specific metadata blocks in transform-response-to-openai.ts Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/gateway/src/chat/chat.ts | 43 +++++++++++++++---- .../tools/transform-response-to-openai.ts | 6 +++ 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/apps/gateway/src/chat/chat.ts b/apps/gateway/src/chat/chat.ts index 969fecc573..0413b65ea2 100644 --- a/apps/gateway/src/chat/chat.ts +++ b/apps/gateway/src/chat/chat.ts @@ -1321,7 +1321,15 @@ chat.openapi(completions, async (c) => { ); if (availableModelProviders.length > 0) { - const modelWithPricing = models.find((m) => m.id === baseModelId); + const rawModelForFallback = models.find((m) => m.id === baseModelId); + const modelWithPricing = rawModelForFallback + ? { + ...rawModelForFallback, + providers: expandAllProviderRegions( + rawModelForFallback.providers as ProviderModelMapping[], + ), + } + : undefined; if (modelWithPricing) { // Fetch metrics for all available providers @@ -1401,8 +1409,8 @@ chat.openapi(completions, async (c) => { } // For models with multiple provider mappings for the same provider (e.g., routing models - // like grok-4-fast with reasoning and non-reasoning variants), select the correct variant - // based on request capabilities when a specific provider is already set + // like grok-4-fast with reasoning and non-reasoning variants, or regional variants), + // select the correct variant based on request capabilities when a specific provider is already set if ( usedProvider && usedProvider !== "llmgateway" && @@ -1412,20 +1420,29 @@ chat.openapi(completions, async (c) => { (p) => p.providerId === usedProvider, ); if (sameProviderMappings.length > 1) { - let selectedMapping; + let selectedMapping: ProviderModelMapping | undefined; if (reasoning_effort !== undefined) { selectedMapping = sameProviderMappings.find( - (p) => p.reasoning === true, - ); + (p) => (p as ProviderModelMapping).reasoning === true, + ) as ProviderModelMapping | undefined; } else { selectedMapping = sameProviderMappings.find( - (p) => p.reasoning !== true, - ); + (p) => (p as ProviderModelMapping).reasoning !== true, + ) as ProviderModelMapping | undefined; } if (selectedMapping) { usedModel = selectedMapping.modelName; + usedRegion ??= selectedMapping.region; } } + // If region still unset (single mapping or no reasoning variant selection), + // derive it from the first matching entry + if (!usedRegion) { + const firstMatch = sameProviderMappings[0] as + | ProviderModelMapping + | undefined; + usedRegion = firstMatch?.region; + } } if (!usedProvider) { @@ -1549,7 +1566,15 @@ chat.openapi(completions, async (c) => { }); } - const modelWithPricing = models.find((m) => m.id === usedModel); + const rawModelWithPricing = models.find((m) => m.id === usedModel); + const modelWithPricing = rawModelWithPricing + ? { + ...rawModelWithPricing, + providers: expandAllProviderRegions( + rawModelWithPricing.providers as ProviderModelMapping[], + ), + } + : undefined; if (modelWithPricing) { // Fetch uptime/latency metrics from last 5 minutes for provider selection diff --git a/apps/gateway/src/chat/tools/transform-response-to-openai.ts b/apps/gateway/src/chat/tools/transform-response-to-openai.ts index 583594d686..81b3584df5 100644 --- a/apps/gateway/src/chat/tools/transform-response-to-openai.ts +++ b/apps/gateway/src/chat/tools/transform-response-to-openai.ts @@ -370,6 +370,7 @@ export function transformResponseToOpenai( requested_provider: requestedProvider, used_model: baseModelName, used_provider: usedProvider, + ...(usedRegion && { used_region: usedRegion }), underlying_used_model: usedModel, ...(routing && { routing }), }, @@ -396,6 +397,7 @@ export function transformResponseToOpenai( requested_provider: requestedProvider, used_model: baseModelName, used_provider: usedProvider, + ...(usedRegion && { used_region: usedRegion }), underlying_used_model: usedModel, ...(routing && { routing }), }; @@ -499,6 +501,7 @@ export function transformResponseToOpenai( requested_provider: requestedProvider, used_model: baseModelName, used_provider: usedProvider, + ...(usedRegion && { used_region: usedRegion }), underlying_used_model: usedModel, ...(routing && { routing }), }; @@ -586,6 +589,7 @@ export function transformResponseToOpenai( requested_provider: requestedProvider, used_model: baseModelName, used_provider: usedProvider, + ...(usedRegion && { used_region: usedRegion }), underlying_used_model: usedModel, ...(routing && { routing }), }; @@ -673,6 +677,7 @@ export function transformResponseToOpenai( requested_provider: requestedProvider, used_model: baseModelName, used_provider: usedProvider, + ...(usedRegion && { used_region: usedRegion }), underlying_used_model: usedModel, ...(routing && { routing }), }; @@ -761,6 +766,7 @@ export function transformResponseToOpenai( requested_provider: requestedProvider, used_model: baseModelName, used_provider: usedProvider, + ...(usedRegion && { used_region: usedRegion }), underlying_used_model: usedModel, ...(routing && { routing }), }; From 2b08a6cccf18c2d7b7d1ad5455f78ba348128219 Mon Sep 17 00:00:00 2001 From: romel lauron Date: Thu, 26 Mar 2026 16:37:52 +0800 Subject: [PATCH 18/55] feat: show region label in model UI Display region name next to provider name in the model list sidebar and provider detail card when a region is set. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/ui/src/components/models/all-models.tsx | 5 +++++ apps/ui/src/components/models/model-provider-card.tsx | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/apps/ui/src/components/models/all-models.tsx b/apps/ui/src/components/models/all-models.tsx index db155617ea..8f0c397421 100644 --- a/apps/ui/src/components/models/all-models.tsx +++ b/apps/ui/src/components/models/all-models.tsx @@ -271,6 +271,11 @@ const ModelTableRow = React.memo( )} {row.providerInfo?.name ?? row.provider.providerId} + {row.provider.region && ( + + ({row.provider.region}) + + )} {row.provider.deactivatedAt && ( diff --git a/apps/ui/src/components/models/model-provider-card.tsx b/apps/ui/src/components/models/model-provider-card.tsx index decc0ff2d9..d0d9f0133c 100644 --- a/apps/ui/src/components/models/model-provider-card.tsx +++ b/apps/ui/src/components/models/model-provider-card.tsx @@ -135,6 +135,11 @@ export function ModelProviderCard({

{provider.providerInfo?.name ?? provider.providerId} + {provider.region && ( + + ({provider.region}) + + )}

{shouldShowStabilityWarning(providerStability) && ( From 3f44da579f02d0a2fb830ff9e036e102d1bc9c7c Mon Sep 17 00:00:00 2001 From: romel lauron Date: Thu, 26 Mar 2026 16:48:32 +0800 Subject: [PATCH 19/55] fix: clean up stale null-region mapping rows When a model transitions from no-region to region-based, old null-region rows remain in the DB. Delete them during worker sync when region-specific rows exist for the same provider+model. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/worker/src/services/sync-models.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/apps/worker/src/services/sync-models.ts b/apps/worker/src/services/sync-models.ts index e90c9f4068..edf55a11dc 100644 --- a/apps/worker/src/services/sync-models.ts +++ b/apps/worker/src/services/sync-models.ts @@ -280,6 +280,24 @@ export async function syncProvidersAndModels() { }); } } + + // Clean up stale null-region rows for providers that now have region-specific entries. + // When a model transitions from no-region to region-based, old null-region rows + // remain in the DB and confuse the UI. Delete them. + const providersWithRegions = new Set( + expandedProviders.filter((m) => m.region).map((m) => m.providerId), + ); + for (const providerId of providersWithRegions) { + await database + .delete(modelProviderMapping) + .where( + and( + eq(modelProviderMapping.modelId, modelDef.id), + eq(modelProviderMapping.providerId, providerId), + isNull(modelProviderMapping.region), + ), + ); + } } } From 0f3fe4602bbacca34e195f8cd23a0d9414360570 Mon Sep 17 00:00:00 2001 From: romel lauron Date: Thu, 26 Mar 2026 16:57:02 +0800 Subject: [PATCH 20/55] feat: show region in model selectors Add region label to UI and Playground model selector dropdowns next to provider name. Shows "(singapore)" etc. for regional provider mappings. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/playground/src/components/model-selector.tsx | 15 +++++++++++++++ apps/playground/src/lib/fetch-models.ts | 1 + .../models/playground-model-selector.tsx | 11 +++++++++++ 3 files changed, 27 insertions(+) diff --git a/apps/playground/src/components/model-selector.tsx b/apps/playground/src/components/model-selector.tsx index f222dbfbb1..a08107e320 100644 --- a/apps/playground/src/components/model-selector.tsx +++ b/apps/playground/src/components/model-selector.tsx @@ -1212,6 +1212,11 @@ export function ModelSelector({
{disabledReason ?? provider?.name} + {!disabledReason && mapping?.region && ( + + ({mapping.region}) + + )}
@@ -1275,6 +1280,11 @@ export function ModelSelector({
{previewEntry.provider?.name ?? "Auto-select provider"} + {previewEntry.mapping?.region && ( + + ({previewEntry.mapping.region}) + + )}
{previewEntry.model.family} family @@ -1798,6 +1808,11 @@ export function ModelSelector({
{selectedDetails.provider?.name ?? "Auto-select provider"} + {selectedDetails.mapping?.region && ( + + ({selectedDetails.mapping.region}) + + )}
{selectedDetails.model.family} family diff --git a/apps/playground/src/lib/fetch-models.ts b/apps/playground/src/lib/fetch-models.ts index 78fa336c4e..09931b3f07 100644 --- a/apps/playground/src/lib/fetch-models.ts +++ b/apps/playground/src/lib/fetch-models.ts @@ -19,6 +19,7 @@ export interface ApiModelProviderMapping { modelId: string; providerId: string; modelName: string; + region?: string | null; inputPrice: string | null; outputPrice: string | null; cachedInputPrice: string | null; diff --git a/apps/ui/src/components/models/playground-model-selector.tsx b/apps/ui/src/components/models/playground-model-selector.tsx index a5ca1d1e09..8e5f9a9824 100644 --- a/apps/ui/src/components/models/playground-model-selector.tsx +++ b/apps/ui/src/components/models/playground-model-selector.tsx @@ -307,6 +307,12 @@ export function ModelSelector({ getProviderForModel(selectedModel, providers) )?.name } + {(() => { + const mapping = selectedModel.providers.find( + (p) => p.providerId === selectedProviderId, + ); + return mapping?.region ? ` (${mapping.region})` : null; + })()} )}
@@ -544,6 +550,11 @@ export function ModelSelector({ {!rootOnly && ( {provider?.name} + {mapping.region && ( + + ({mapping.region}) + + )} )} From d550ad5a215f3072662bb1c65bfd93f904a6e175 Mon Sep 17 00:00:00 2001 From: romel lauron Date: Thu, 26 Mar 2026 17:02:41 +0800 Subject: [PATCH 21/55] fix: include region in model selector entry keys Duplicate highlights occurred because entry keys didn't include region, causing same-provider/model entries for different regions to share the same key. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/playground/src/components/model-selector.tsx | 2 +- apps/ui/src/components/models/all-models.tsx | 2 +- apps/ui/src/components/models/playground-model-selector.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/playground/src/components/model-selector.tsx b/apps/playground/src/components/model-selector.tsx index a08107e320..c098793c52 100644 --- a/apps/playground/src/components/model-selector.tsx +++ b/apps/playground/src/components/model-selector.tsx @@ -1137,7 +1137,7 @@ export function ModelSelector({ const ProviderIcon = provider ? getProviderIcon(provider.id) : null; - const entryKey = `${mapping!.providerId}-${model.id}-${mapping!.modelName}`; + const entryKey = `${mapping!.providerId}-${model.id}-${mapping!.modelName}${mapping!.region ? `-${mapping!.region}` : ""}`; const providerModelValue = `${mapping!.providerId}/${model.id}`; const disabled = isOptionDisabled?.(providerModelValue) ?? false; diff --git a/apps/ui/src/components/models/all-models.tsx b/apps/ui/src/components/models/all-models.tsx index 8f0c397421..b6eab84d39 100644 --- a/apps/ui/src/components/models/all-models.tsx +++ b/apps/ui/src/components/models/all-models.tsx @@ -1719,7 +1719,7 @@ export function AllModels({
{modelsWithProviders.map((model) => ( Date: Thu, 26 Mar 2026 18:18:41 +0800 Subject: [PATCH 22/55] feat: region suffix in model names and UI updates - Append :region to modelName in expanded provider mappings so each region has a unique identifier for routing and display - Strip :region before sending to upstream provider API - Show region in activity logs, routing attempts, provider scores - Fix model selector to parse :region from URL and match correctly - Fix entry key uniqueness and checkmark matching for regions - Show region label in selected model header - Clean up stale null-region DB rows during worker sync - Add region to DB schema routing metadata types Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/api/src/routes/logs.ts | 7 +++++ apps/gateway/src/chat/chat.ts | 2 +- .../chat/tools/resolve-provider-context.ts | 12 ++++++-- .../src/components/model-selector.tsx | 29 ++++++++++++++----- apps/ui/src/components/dashboard/log-card.tsx | 10 +++++++ .../models/playground-model-selector.tsx | 22 ++++++++++---- apps/ui/src/lib/api/v1.d.ts | 14 +++++++++ packages/db/src/schema.ts | 4 +++ packages/models/src/region-helpers.ts | 19 ++++++++++++ 9 files changed, 102 insertions(+), 17 deletions(-) diff --git a/apps/api/src/routes/logs.ts b/apps/api/src/routes/logs.ts index 52e9bd4bfb..ff08b5b79b 100644 --- a/apps/api/src/routes/logs.ts +++ b/apps/api/src/routes/logs.ts @@ -116,10 +116,16 @@ const logSchema = z.object({ .array( z.object({ providerId: z.string(), + region: z.string().optional(), score: z.number(), uptime: z.number().optional(), latency: z.number().optional(), + throughput: z.number().optional(), price: z.number().optional(), + priority: z.number().optional(), + failed: z.boolean().optional(), + status_code: z.number().optional(), + error_type: z.string().optional(), }), ) .optional(), @@ -128,6 +134,7 @@ const logSchema = z.object({ z.object({ provider: z.string(), model: z.string(), + region: z.string().optional(), status_code: z.number(), error_type: z.string(), succeeded: z.boolean(), diff --git a/apps/gateway/src/chat/chat.ts b/apps/gateway/src/chat/chat.ts index 0413b65ea2..bcac94d4b0 100644 --- a/apps/gateway/src/chat/chat.ts +++ b/apps/gateway/src/chat/chat.ts @@ -1736,7 +1736,7 @@ chat.openapi(completions, async (c) => { let usedModelMapping = usedModel; // Store the original provider model name let usedModelFormatted = formatUsedModelForDisplay( usedProvider, - baseModelName, + usedRegion ? `${baseModelName}:${usedRegion}` : baseModelName, customProviderName, ); // Store in LLMGateway format diff --git a/apps/gateway/src/chat/tools/resolve-provider-context.ts b/apps/gateway/src/chat/tools/resolve-provider-context.ts index 817fc48fba..8f9996d710 100644 --- a/apps/gateway/src/chat/tools/resolve-provider-context.ts +++ b/apps/gateway/src/chat/tools/resolve-provider-context.ts @@ -24,6 +24,7 @@ import { providers, type ToolChoiceType, type WebSearchTool, + stripRegionFromModelName, } from "@llmgateway/models"; import { getProviderEnv } from "./get-provider-env.js"; @@ -135,7 +136,12 @@ export async function resolveProviderContext( ): Promise { const usedProvider = providerMapping.providerId as Provider; const usedModel = providerMapping.modelName; - const baseModelName = modelInfo.id || usedModel; + // Strip :region suffix for the actual upstream API call + const upstreamModelName = stripRegionFromModelName( + usedModel, + providerMapping.region, + ); + const baseModelName = modelInfo.id || upstreamModelName; const usedModelMapping = usedModel; const usedModelFormatted = formatUsedModelForDisplay( usedProvider, @@ -256,7 +262,7 @@ export async function resolveProviderContext( const url = getProviderEndpoint( usedProvider as Provider, providerKey?.baseUrl ?? undefined, - usedModel, + upstreamModelName, usedProvider === "google-ai-studio" || usedProvider === "google-vertex" ? usedToken : undefined, @@ -347,7 +353,7 @@ export async function resolveProviderContext( // --- Request body preparation --- const requestBody: ProviderRequestBody = await prepareRequestBody( usedProvider as Provider, - usedModel, + upstreamModelName, options.messages as BaseMessage[], options.effectiveStream, temperature, diff --git a/apps/playground/src/components/model-selector.tsx b/apps/playground/src/components/model-selector.tsx index c098793c52..2c03b65963 100644 --- a/apps/playground/src/components/model-selector.tsx +++ b/apps/playground/src/components/model-selector.tsx @@ -460,17 +460,26 @@ export function ModelSelector({ }); // Parse value as provider/model-id (preferred). Fallback to model id only. + // Supports region suffix: "alibaba/deepseek-v3.2:cn-beijing" const raw = value ?? ""; - const [selectedProviderId, selectedModelId] = raw.includes("/") + const [selectedProviderId, selectedModelIdRaw] = raw.includes("/") ? (raw.split("/") as [string, string]) : ["", raw]; + // Strip :region suffix for root model lookup, keep raw for mapping match + const selectedModelId = selectedModelIdRaw.includes(":") + ? selectedModelIdRaw.split(":")[0] + : selectedModelIdRaw; const selectedModel = models.find((m) => m.id === selectedModelId); const selectedProviderDef = providers.find( (p) => p.id === selectedProviderId, ); - const selectedMapping = selectedModel?.mappings.find( - (p) => p.providerId === selectedProviderId, - ); + const selectedMapping = + selectedModel?.mappings.find( + (p) => + p.providerId === selectedProviderId && + p.modelName === selectedModelIdRaw, + ) ?? + selectedModel?.mappings.find((p) => p.providerId === selectedProviderId); const selectedEntryKey = selectedModel && selectedProviderId && selectedMapping ? `${selectedProviderId}-${selectedModel.id}-${selectedMapping.modelName}` @@ -718,13 +727,16 @@ export function ModelSelector({ } // Prefer provider-specific entry when a provider is selected + // Match on modelName to distinguish regional variants let entry = selectedProviderId && allEntries.find( (e) => !e.isRoot && e.model.id === selectedModel.id && - e.mapping?.providerId === selectedProviderId, + e.mapping?.providerId === selectedProviderId && + (!selectedMapping || + e.mapping?.modelName === selectedMapping.modelName), ); // Fallback to root entry for the selected model @@ -803,6 +815,9 @@ export function ModelSelector({ selectedProviderDef ?? getProviderForModel(selectedModel, providers) )?.name} + {selectedMapping?.region && ( + ({selectedMapping.region}) + )}
@@ -1137,8 +1152,8 @@ export function ModelSelector({ const ProviderIcon = provider ? getProviderIcon(provider.id) : null; - const entryKey = `${mapping!.providerId}-${model.id}-${mapping!.modelName}${mapping!.region ? `-${mapping!.region}` : ""}`; - const providerModelValue = `${mapping!.providerId}/${model.id}`; + const entryKey = `${mapping!.providerId}-${model.id}-${mapping!.modelName}`; + const providerModelValue = `${mapping!.providerId}/${mapping!.region ? mapping!.modelName : model.id}`; const disabled = isOptionDisabled?.(providerModelValue) ?? false; const disabledReason = diff --git a/apps/ui/src/components/dashboard/log-card.tsx b/apps/ui/src/components/dashboard/log-card.tsx index 73f886ab7c..f5b9f76d19 100644 --- a/apps/ui/src/components/dashboard/log-card.tsx +++ b/apps/ui/src/components/dashboard/log-card.tsx @@ -358,6 +358,11 @@ export function LogCard({ > {score.providerId} + {score.region && ( + + ({score.region}) + + )} {score.failed && ( @@ -425,6 +430,11 @@ export function LogCard({ )} {attempt.provider}/{attempt.model} + {attempt.region && ( + + ({attempt.region}) + + )} {attempt.status_code}{" "} diff --git a/apps/ui/src/components/models/playground-model-selector.tsx b/apps/ui/src/components/models/playground-model-selector.tsx index 24d3c196b1..f6d17370ad 100644 --- a/apps/ui/src/components/models/playground-model-selector.tsx +++ b/apps/ui/src/components/models/playground-model-selector.tsx @@ -87,17 +87,25 @@ export function ModelSelector({ }); // Parse value as provider/model-id (preferred). Fallback to model id only. + // Supports region suffix: "alibaba/deepseek-v3.2:cn-beijing" const raw = value ?? ""; - const [selectedProviderId, selectedModelId] = raw.includes("/") + const [selectedProviderId, selectedModelIdRaw] = raw.includes("/") ? (raw.split("/") as [string, string]) : ["", raw]; + const selectedModelId = selectedModelIdRaw.includes(":") + ? selectedModelIdRaw.split(":")[0] + : selectedModelIdRaw; const selectedModel = models.find((m) => m.id === selectedModelId); const selectedProviderDef = providers.find( (p) => p.id === selectedProviderId, ); - const selectedMapping = selectedModel?.providers.find( - (p) => p.providerId === selectedProviderId, - ); + const selectedMapping = + selectedModel?.providers.find( + (p) => + p.providerId === selectedProviderId && + p.modelName === selectedModelIdRaw, + ) ?? + selectedModel?.providers.find((p) => p.providerId === selectedProviderId); const selectedEntryKey = selectedModel && selectedProviderId && selectedMapping ? `${selectedProviderId}-${selectedModel.id}-${selectedMapping.modelName}` @@ -511,7 +519,7 @@ export function ModelSelector({ : provider ? getProviderIcon(provider.id) : null; - const entryKey = `${mapping.providerId}-${model.id}-${mapping.modelName}${mapping.region ? `-${mapping.region}` : ""}`; + const entryKey = `${mapping.providerId}-${model.id}-${mapping.modelName}`; const isDeprecated = mapping.deprecatedAt && new Date(mapping.deprecatedAt) <= new Date(); @@ -520,7 +528,9 @@ export function ModelSelector({ key={entryKey} value={entryKey} onSelect={() => { - onValueChange?.(`${mapping.providerId}/${model.id}`); + onValueChange?.( + `${mapping.providerId}/${mapping.region ? mapping.modelName : model.id}`, + ); setOpen(false); }} className="p-3 cursor-pointer" diff --git a/apps/ui/src/lib/api/v1.d.ts b/apps/ui/src/lib/api/v1.d.ts index 42e3095265..7b88f72266 100644 --- a/apps/ui/src/lib/api/v1.d.ts +++ b/apps/ui/src/lib/api/v1.d.ts @@ -964,14 +964,21 @@ export interface paths { selectionReason?: string; providerScores?: { providerId: string; + region?: string; score: number; uptime?: number; latency?: number; + throughput?: number; price?: number; + priority?: number; + failed?: boolean; + status_code?: number; + error_type?: string; }[]; routing?: { provider: string; model: string; + region?: string; status_code: number; error_type: string; succeeded: boolean; @@ -1176,14 +1183,21 @@ export interface paths { selectionReason?: string; providerScores?: { providerId: string; + region?: string; score: number; uptime?: number; latency?: number; + throughput?: number; price?: number; + priority?: number; + failed?: boolean; + status_code?: number; + error_type?: string; }[]; routing?: { provider: string; model: string; + region?: string; status_code: number; error_type: string; succeeded: boolean; diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index f31e03f4d7..4dca00fe1b 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -535,6 +535,7 @@ export const log = pgTable( selectionReason?: string; providerScores?: Array<{ providerId: string; + region?: string; score: number; uptime?: number; latency?: number; @@ -551,6 +552,7 @@ export const log = pgTable( routing?: Array<{ provider: string; model: string; + region?: string; status_code: number; error_type: string; succeeded: boolean; @@ -683,6 +685,7 @@ export const videoJob = pgTable( selectionReason?: string; providerScores?: Array<{ providerId: string; + region?: string; score: number; uptime?: number; latency?: number; @@ -699,6 +702,7 @@ export const videoJob = pgTable( routing?: Array<{ provider: string; model: string; + region?: string; status_code: number; error_type: string; succeeded: boolean; diff --git a/packages/models/src/region-helpers.ts b/packages/models/src/region-helpers.ts index ca275bea4d..d18cc4d0a2 100644 --- a/packages/models/src/region-helpers.ts +++ b/packages/models/src/region-helpers.ts @@ -1,5 +1,20 @@ import type { ProviderModelMapping } from "./models.js"; +/** + * Strips the `:region` suffix from a model name that was expanded by expandProviderRegions. + * e.g., "deepseek-v3.2:singapore" → "deepseek-v3.2" + * If the model name has no region suffix, returns it unchanged. + */ +export function stripRegionFromModelName( + modelName: string, + region?: string, +): string { + if (region && modelName.endsWith(`:${region}`)) { + return modelName.slice(0, -(region.length + 1)); + } + return modelName; +} + /** * Expands a single ProviderModelMapping with `regions` into multiple flat entries, * one per region. Each region inherits all properties from the parent mapping @@ -21,6 +36,10 @@ export function expandProviderRegions( ...base, ...overrides, region: id, + // Append :region to modelName so each region has a unique model identifier + // for routing and display. The gateway strips this suffix before sending + // the request to the upstream provider API. + modelName: `${base.modelName}:${id}`, }; }); } From c5a0dbfd874d7f0f17bc02fad75a7c93c85800c5 Mon Sep 17 00:00:00 2001 From: romel lauron Date: Thu, 26 Mar 2026 21:08:12 +0800 Subject: [PATCH 23/55] feat: region logging, auto-select, and pricing fixes - Initialize usedRegion from requestedRegion so explicitly selected regions flow through to logs and metadata - Resolve default region from regionConfig when user selects provider without specifying region (auto-select) - Re-compute usedModelFormatted after region resolution - Keep base (no-region) entry in expandProviderRegions for provider-level auto-select in UI selectors - Remove stale null-region row cleanup (base entry is needed) - Strip :region suffix defensively in resolve-model-info - Show full price precision in playground (no rounding) - Add reasoning: true to Alibaba deepseek-v3.2 mapping Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/code/src/lib/api/v1.d.ts | 14 ++++++++++ apps/gateway/src/chat/chat.ts | 24 ++++++++++++++-- .../src/chat/tools/resolve-model-info.ts | 18 +++++++++--- apps/playground/src/lib/api/v1.d.ts | 14 ++++++++++ apps/playground/src/lib/model-utils.ts | 4 ++- apps/worker/src/services/sync-models.ts | 18 ------------ ee/admin/src/lib/api/v1.d.ts | 14 ++++++++++ packages/models/src/models/deepseek.ts | 1 + packages/models/src/region-helpers.ts | 28 ++++++++++--------- 9 files changed, 97 insertions(+), 38 deletions(-) diff --git a/apps/code/src/lib/api/v1.d.ts b/apps/code/src/lib/api/v1.d.ts index 42e3095265..7b88f72266 100644 --- a/apps/code/src/lib/api/v1.d.ts +++ b/apps/code/src/lib/api/v1.d.ts @@ -964,14 +964,21 @@ export interface paths { selectionReason?: string; providerScores?: { providerId: string; + region?: string; score: number; uptime?: number; latency?: number; + throughput?: number; price?: number; + priority?: number; + failed?: boolean; + status_code?: number; + error_type?: string; }[]; routing?: { provider: string; model: string; + region?: string; status_code: number; error_type: string; succeeded: boolean; @@ -1176,14 +1183,21 @@ export interface paths { selectionReason?: string; providerScores?: { providerId: string; + region?: string; score: number; uptime?: number; latency?: number; + throughput?: number; price?: number; + priority?: number; + failed?: boolean; + status_code?: number; + error_type?: string; }[]; routing?: { provider: string; model: string; + region?: string; status_code: number; error_type: string; succeeded: boolean; diff --git a/apps/gateway/src/chat/chat.ts b/apps/gateway/src/chat/chat.ts index bcac94d4b0..f554be31a4 100644 --- a/apps/gateway/src/chat/chat.ts +++ b/apps/gateway/src/chat/chat.ts @@ -873,7 +873,7 @@ chat.openapi(completions, async (c) => { let usedProvider = requestedProvider; let usedModel: string = requestedModel; - let usedRegion: string | undefined; + let usedRegion: string | undefined = requestedRegion; let routingMetadata: RoutingMetadata | undefined; // Extract retention level for data storage cost calculation @@ -2030,10 +2030,30 @@ chat.openapi(completions, async (c) => { usedRegion, ); + // If region is still unset but the provider supports regions, resolve the + // default region so it appears in logs and metadata. + if (!usedRegion) { + const providerDef = providers.find((p) => p.id === usedProvider) as + | { regionConfig?: { defaultRegion: string } } + | undefined; + if (providerDef?.regionConfig) { + usedRegion = providerDef.regionConfig.defaultRegion; + } + } + + // Re-compute usedModelFormatted now that region may have been resolved + if (usedRegion) { + usedModelFormatted = formatUsedModelForDisplay( + usedProvider, + `${baseModelName}:${usedRegion}`, + customProviderName, + ); + } + logger.info("[region-debug] Request resolved", { provider: usedProvider, model: usedModel, - region: usedRegion ?? "default", + region: usedRegion ?? "none", endpoint: url, tokenSource: providerKey ? "db-provider-key" : "env-var", }); diff --git a/apps/gateway/src/chat/tools/resolve-model-info.ts b/apps/gateway/src/chat/tools/resolve-model-info.ts index 6d17138db1..0f8ba9aa68 100644 --- a/apps/gateway/src/chat/tools/resolve-model-info.ts +++ b/apps/gateway/src/chat/tools/resolve-model-info.ts @@ -50,16 +50,21 @@ export function resolveModelInfo( ], }; } else { + // Strip :region suffix for model lookup (e.g., "deepseek-v3.2:cn-beijing" → "deepseek-v3.2") + const baseRequestedModel = requestedModel.includes(":") + ? requestedModel.split(":")[0] + : requestedModel; + // First try to find by model ID // When a specific provider is requested, prefer the definition that includes that provider let foundModel = requestedProvider ? models.find( (m) => - m.id === requestedModel && + m.id === baseRequestedModel && m.providers.some((p) => p.providerId === requestedProvider), ) : undefined; - foundModel ??= models.find((m) => m.id === requestedModel); + foundModel ??= models.find((m) => m.id === baseRequestedModel); // If not found, search by provider model name // If a specific provider is requested, match both modelName and providerId @@ -68,13 +73,18 @@ export function resolveModelInfo( foundModel = models.find((m) => m.providers.find( (p) => - p.modelName === requestedModel && + (p.modelName === requestedModel || + p.modelName === baseRequestedModel) && p.providerId === requestedProvider, ), ); } else { foundModel = models.find((m) => - m.providers.find((p) => p.modelName === requestedModel), + m.providers.find( + (p) => + p.modelName === requestedModel || + p.modelName === baseRequestedModel, + ), ); } } diff --git a/apps/playground/src/lib/api/v1.d.ts b/apps/playground/src/lib/api/v1.d.ts index 42e3095265..7b88f72266 100644 --- a/apps/playground/src/lib/api/v1.d.ts +++ b/apps/playground/src/lib/api/v1.d.ts @@ -964,14 +964,21 @@ export interface paths { selectionReason?: string; providerScores?: { providerId: string; + region?: string; score: number; uptime?: number; latency?: number; + throughput?: number; price?: number; + priority?: number; + failed?: boolean; + status_code?: number; + error_type?: string; }[]; routing?: { provider: string; model: string; + region?: string; status_code: number; error_type: string; succeeded: boolean; @@ -1176,14 +1183,21 @@ export interface paths { selectionReason?: string; providerScores?: { providerId: string; + region?: string; score: number; uptime?: number; latency?: number; + throughput?: number; price?: number; + priority?: number; + failed?: boolean; + status_code?: number; + error_type?: string; }[]; routing?: { provider: string; model: string; + region?: string; status_code: number; error_type: string; succeeded: boolean; diff --git a/apps/playground/src/lib/model-utils.ts b/apps/playground/src/lib/model-utils.ts index 436de385da..92d7446dd6 100644 --- a/apps/playground/src/lib/model-utils.ts +++ b/apps/playground/src/lib/model-utils.ts @@ -15,7 +15,9 @@ export function formatPrice(price: number | undefined): string { // For the playground we always want to show an explicit per‑million price (to match the /models UI), // otherwise small numbers like 2e‑6 end up rounded to $0.00/1K. const perMillion = price * 1_000_000; - return `$${perMillion.toFixed(2)}/1M tokens`; + // Show full precision (up to 4 decimals) without trailing zeros + const formatted = parseFloat(perMillion.toFixed(4)).toString(); + return `$${formatted}/1M tokens`; } export function formatContextSize(size: number | null | undefined): string { diff --git a/apps/worker/src/services/sync-models.ts b/apps/worker/src/services/sync-models.ts index edf55a11dc..e90c9f4068 100644 --- a/apps/worker/src/services/sync-models.ts +++ b/apps/worker/src/services/sync-models.ts @@ -280,24 +280,6 @@ export async function syncProvidersAndModels() { }); } } - - // Clean up stale null-region rows for providers that now have region-specific entries. - // When a model transitions from no-region to region-based, old null-region rows - // remain in the DB and confuse the UI. Delete them. - const providersWithRegions = new Set( - expandedProviders.filter((m) => m.region).map((m) => m.providerId), - ); - for (const providerId of providersWithRegions) { - await database - .delete(modelProviderMapping) - .where( - and( - eq(modelProviderMapping.modelId, modelDef.id), - eq(modelProviderMapping.providerId, providerId), - isNull(modelProviderMapping.region), - ), - ); - } } } diff --git a/ee/admin/src/lib/api/v1.d.ts b/ee/admin/src/lib/api/v1.d.ts index 42e3095265..7b88f72266 100644 --- a/ee/admin/src/lib/api/v1.d.ts +++ b/ee/admin/src/lib/api/v1.d.ts @@ -964,14 +964,21 @@ export interface paths { selectionReason?: string; providerScores?: { providerId: string; + region?: string; score: number; uptime?: number; latency?: number; + throughput?: number; price?: number; + priority?: number; + failed?: boolean; + status_code?: number; + error_type?: string; }[]; routing?: { provider: string; model: string; + region?: string; status_code: number; error_type: string; succeeded: boolean; @@ -1176,14 +1183,21 @@ export interface paths { selectionReason?: string; providerScores?: { providerId: string; + region?: string; score: number; uptime?: number; latency?: number; + throughput?: number; price?: number; + priority?: number; + failed?: boolean; + status_code?: number; + error_type?: string; }[]; routing?: { provider: string; model: string; + region?: string; status_code: number; error_type: string; succeeded: boolean; diff --git a/packages/models/src/models/deepseek.ts b/packages/models/src/models/deepseek.ts index 30d2b0284e..7edcf6c3e7 100644 --- a/packages/models/src/models/deepseek.ts +++ b/packages/models/src/models/deepseek.ts @@ -171,6 +171,7 @@ export const deepseekModels = [ requestPrice: 0, contextSize: 131072, maxOutput: 65536, + reasoning: true, streaming: true, vision: false, tools: true, diff --git a/packages/models/src/region-helpers.ts b/packages/models/src/region-helpers.ts index d18cc4d0a2..93fac49721 100644 --- a/packages/models/src/region-helpers.ts +++ b/packages/models/src/region-helpers.ts @@ -29,19 +29,21 @@ export function expandProviderRegions( return [mapping]; } - return mapping.regions.map(({ id, ...overrides }) => { - // Remove the `regions` array from the expanded entry to avoid confusion - const { regions: _, ...base } = mapping; - return { - ...base, - ...overrides, - region: id, - // Append :region to modelName so each region has a unique model identifier - // for routing and display. The gateway strips this suffix before sending - // the request to the upstream provider API. - modelName: `${base.modelName}:${id}`, - }; - }); + const { regions: _, ...base } = mapping; + + // Include the base entry (no region) as the provider-level "auto-select region" + // option, plus one entry per region with :region suffix in modelName. + const regionEntries = mapping.regions.map(({ id, ...overrides }) => ({ + ...base, + ...overrides, + region: id, + // Append :region to modelName so each region has a unique model identifier + // for routing and display. The gateway strips this suffix before sending + // the request to the upstream provider API. + modelName: `${base.modelName}:${id}`, + })); + + return [base, ...regionEntries]; } /** From 0789d51eb30e8ce765a7ee56463005c49f695a80 Mon Sep 17 00:00:00 2001 From: romel lauron Date: Thu, 26 Mar 2026 23:31:50 +0800 Subject: [PATCH 24/55] feat: add Alibaba cn-beijing models and fix region key resolution - Add Kimi K2, K2 Thinking, K2.5 for Alibaba cn-beijing - Add GLM-5, GLM-4.7, GLM-4.6 for Alibaba cn-beijing with tiered pricing - Add reasoning: true to Alibaba deepseek-v3.2 - Fix region-specific API key override in api-keys and hybrid modes - Strip :region suffix from model name before sending to upstream API - Fix e2e test model value to include :region for region entries - Fix single-provider mapping to set usedModel from expanded entry Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/gateway/src/chat-helpers.e2e.ts | 4 +- apps/gateway/src/chat-prompt-caching.e2e.ts | 2 +- apps/gateway/src/chat/chat.ts | 29 ++++++- packages/models/src/models/moonshot.ts | 48 +++++++++++ packages/models/src/models/zai.ts | 90 +++++++++++++++++++++ 5 files changed, 166 insertions(+), 7 deletions(-) diff --git a/apps/gateway/src/chat-helpers.e2e.ts b/apps/gateway/src/chat-helpers.e2e.ts index e031cd6835..66c9c7b885 100644 --- a/apps/gateway/src/chat-helpers.e2e.ts +++ b/apps/gateway/src/chat-helpers.e2e.ts @@ -320,7 +320,7 @@ export const testModels = filteredModels } testCases.push({ - model: `${provider.providerId}/${model.id}`, + model: `${provider.providerId}/${provider.region ? provider.modelName : model.id}`, providers: [provider], originalModel: model.id, // Keep track of the original model for reference }); @@ -396,7 +396,7 @@ export const providerModels = filteredModels } testCases.push({ - model: `${provider.providerId}/${model.id}`, + model: `${provider.providerId}/${provider.region ? provider.modelName : model.id}`, provider, originalModel: model.id, // Keep track of the original model for reference }); diff --git a/apps/gateway/src/chat-prompt-caching.e2e.ts b/apps/gateway/src/chat-prompt-caching.e2e.ts index 9fa2bfcef4..bdf01d7302 100644 --- a/apps/gateway/src/chat-prompt-caching.e2e.ts +++ b/apps/gateway/src/chat-prompt-caching.e2e.ts @@ -71,7 +71,7 @@ const promptCachingModels = filteredModels } testCases.push({ - model: `${provider.providerId}/${model.id}`, + model: `${provider.providerId}/${provider.region ? provider.modelName : model.id}`, provider, originalModel: model.id, minCacheableTokens: provider.minCacheableTokens ?? 1024, diff --git a/apps/gateway/src/chat/chat.ts b/apps/gateway/src/chat/chat.ts index f554be31a4..e946791e56 100644 --- a/apps/gateway/src/chat/chat.ts +++ b/apps/gateway/src/chat/chat.ts @@ -73,6 +73,7 @@ import { type WebSearchTool, expandAllProviderRegions, getRegionSpecificEnvValue, + stripRegionFromModelName, } from "@llmgateway/models"; import { completionsRequestSchema } from "./schemas/completions.js"; @@ -1434,9 +1435,12 @@ chat.openapi(completions, async (c) => { usedModel = selectedMapping.modelName; usedRegion ??= selectedMapping.region; } + } else if (sameProviderMappings.length === 1) { + // Single matching entry — use its modelName (may include :region suffix) + usedModel = sameProviderMappings[0].modelName; + usedRegion ??= (sameProviderMappings[0] as ProviderModelMapping).region; } - // If region still unset (single mapping or no reasoning variant selection), - // derive it from the first matching entry + // If region still unset, derive it from the first matching entry if (!usedRegion) { const firstMatch = sameProviderMappings[0] as | ProviderModelMapping @@ -1826,6 +1830,13 @@ chat.openapi(completions, async (c) => { usedToken = providerKey.token; usedRegion ??= resolveRegionFromProviderKey(providerKey); + // Override with region-specific env var if the DB key doesn't match the requested region + if (usedRegion) { + const regionToken = getRegionSpecificEnvValue(usedProvider, usedRegion); + if (regionToken && regionToken !== usedToken) { + usedToken = regionToken; + } + } } else if (project.mode === "credits") { // Check both regular credits AND dev plan credits const regularCredits = parseFloat(organization.credits ?? "0"); @@ -1892,6 +1903,13 @@ chat.openapi(completions, async (c) => { if (providerKey) { usedToken = providerKey.token; usedRegion ??= resolveRegionFromProviderKey(providerKey); + // Override with region-specific env var if the DB key doesn't match the requested region + if (usedRegion) { + const regionToken = getRegionSpecificEnvValue(usedProvider, usedRegion); + if (regionToken && regionToken !== usedToken) { + usedToken = regionToken; + } + } logger.info("[region-debug] Hybrid mode: DB key found", { provider: usedProvider, resolvedRegion: usedRegion ?? "none", @@ -2007,6 +2025,9 @@ chat.openapi(completions, async (c) => { (msg: any) => msg.tool_calls ?? msg.role === "tool", ); + // Strip :region suffix before sending to upstream provider API + const upstreamModelName = stripRegionFromModelName(usedModel, usedRegion); + try { if (!usedProvider) { throw new HTTPException(400, { @@ -2017,7 +2038,7 @@ chat.openapi(completions, async (c) => { url = getProviderEndpoint( usedProvider, providerKey?.baseUrl ?? undefined, - usedModel, + upstreamModelName, usedProvider === "google-ai-studio" || usedProvider === "google-vertex" ? usedToken : undefined, @@ -2630,7 +2651,7 @@ chat.openapi(completions, async (c) => { let requestBody: ProviderRequestBody = await prepareRequestBody( usedProvider, - usedModel, + upstreamModelName, messages as BaseMessage[], effectiveStream, temperature, diff --git a/packages/models/src/models/moonshot.ts b/packages/models/src/models/moonshot.ts index 3dc78616aa..8be53244d9 100644 --- a/packages/models/src/models/moonshot.ts +++ b/packages/models/src/models/moonshot.ts @@ -90,6 +90,22 @@ export const moonshotModels = [ tools: true, jsonOutput: false, }, + { + test: "skip", + providerId: "alibaba", + modelName: "Moonshot-Kimi-K2-Instruct", + inputPrice: 0.574 / 1e6, + outputPrice: 2.294 / 1e6, + regions: [{ id: "cn-beijing" }], + requestPrice: 0, + contextSize: 131072, + maxOutput: 8192, + streaming: true, + vision: false, + tools: true, + webSearch: true, + jsonOutput: false, + }, ], }, { @@ -145,6 +161,22 @@ export const moonshotModels = [ tools: true, jsonOutput: false, }, + { + test: "skip", + providerId: "alibaba", + modelName: "kimi-k2-thinking", + inputPrice: 0.574 / 1e6, + outputPrice: 2.294 / 1e6, + regions: [{ id: "cn-beijing" }], + requestPrice: 0, + contextSize: 262144, + maxOutput: 16384, + reasoning: true, + streaming: true, + vision: false, + tools: true, + jsonOutput: true, + }, ], }, { @@ -231,6 +263,22 @@ export const moonshotModels = [ tools: false, jsonOutput: false, }, + { + test: "skip", + providerId: "alibaba", + modelName: "kimi-k2.5", + inputPrice: 0.574 / 1e6, + outputPrice: 3.011 / 1e6, + regions: [{ id: "cn-beijing" }], + requestPrice: 0, + contextSize: 262144, + maxOutput: 98304, + reasoning: true, + streaming: true, + vision: true, + tools: true, + jsonOutput: false, + }, { providerId: "embercloud", modelName: "kimi-k2.5", diff --git a/packages/models/src/models/zai.ts b/packages/models/src/models/zai.ts index f323d460df..1d72257bd8 100644 --- a/packages/models/src/models/zai.ts +++ b/packages/models/src/models/zai.ts @@ -73,6 +73,36 @@ export const zaiModels = [ tools: true, jsonOutput: true, }, + { + test: "skip", + providerId: "alibaba", + modelName: "glm-5", + inputPrice: 0.573 / 1e6, + outputPrice: 2.58 / 1e6, + pricingTiers: [ + { + name: "Up to 32K", + upToTokens: 32000, + inputPrice: 0.573 / 1e6, + outputPrice: 2.58 / 1e6, + }, + { + name: "Over 32K", + upToTokens: Infinity, + inputPrice: 0.86 / 1e6, + outputPrice: 3.154 / 1e6, + }, + ], + regions: [{ id: "cn-beijing" }], + requestPrice: 0, + contextSize: 202752, + maxOutput: 16384, + streaming: true, + reasoning: true, + vision: false, + tools: true, + jsonOutput: true, + }, { providerId: "embercloud", modelName: "glm-5", @@ -453,6 +483,36 @@ export const zaiModels = [ tools: false, jsonOutput: false, }, + { + test: "skip", + providerId: "alibaba", + modelName: "glm-4.7", + inputPrice: 0.431 / 1e6, + outputPrice: 2.007 / 1e6, + pricingTiers: [ + { + name: "Up to 32K", + upToTokens: 32000, + inputPrice: 0.431 / 1e6, + outputPrice: 2.007 / 1e6, + }, + { + name: "Over 32K", + upToTokens: Infinity, + inputPrice: 0.574 / 1e6, + outputPrice: 2.294 / 1e6, + }, + ], + regions: [{ id: "cn-beijing" }], + requestPrice: 0, + contextSize: 202752, + maxOutput: 16384, + streaming: true, + reasoning: true, + vision: false, + tools: true, + jsonOutput: true, + }, { providerId: "embercloud", modelName: "glm-4.7", @@ -629,6 +689,36 @@ export const zaiModels = [ tools: true, jsonOutput: true, }, + { + test: "skip", + providerId: "alibaba", + modelName: "glm-4.6", + inputPrice: 0.431 / 1e6, + outputPrice: 2.007 / 1e6, + pricingTiers: [ + { + name: "Up to 32K", + upToTokens: 32000, + inputPrice: 0.431 / 1e6, + outputPrice: 2.007 / 1e6, + }, + { + name: "Over 32K", + upToTokens: Infinity, + inputPrice: 0.574 / 1e6, + outputPrice: 2.294 / 1e6, + }, + ], + regions: [{ id: "cn-beijing" }], + requestPrice: 0, + contextSize: 202752, + maxOutput: 16384, + streaming: true, + reasoning: true, + vision: false, + tools: true, + jsonOutput: true, + }, { providerId: "novita", test: "skip", From 0fd709859168c8981a2db03d7ef0799d25baccc4 Mon Sep 17 00:00:00 2001 From: romel lauron Date: Thu, 26 Mar 2026 23:39:41 +0800 Subject: [PATCH 25/55] fix: double region suffix and zero costs for region models - Strip :region from usedModel when looking up finalModelInfo to prevent baseModelName from containing the region suffix, which caused usedModelFormatted to append it twice - Expand providers in calculateCosts so region-specific pricing entries are matched, fixing zero cost calculations - Fix baseModelName fallback to strip region suffix Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/gateway/src/chat/chat.ts | 13 ++++++++----- apps/gateway/src/lib/costs.ts | 25 +++++++++++++++++++++---- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/apps/gateway/src/chat/chat.ts b/apps/gateway/src/chat/chat.ts index e946791e56..8e6e4d137f 100644 --- a/apps/gateway/src/chat/chat.ts +++ b/apps/gateway/src/chat/chat.ts @@ -1701,18 +1701,20 @@ chat.openapi(completions, async (c) => { ], }; } else { + const baseUsedModel = stripRegionFromModelName(usedModel, usedRegion); const rawFinalModelInfo = models.find( (m) => - (m.id === usedModel || - m.providers.some((p) => p.modelName === usedModel)) && + (m.id === baseUsedModel || + m.providers.some((p) => p.modelName === baseUsedModel)) && m.providers.some((p) => p.providerId === usedProvider), ) ?? models.find( (m) => - m.id === usedModel || + m.id === baseUsedModel || m.providers.some( - (p) => p.modelName === usedModel && p.providerId === usedProvider, + (p) => + p.modelName === baseUsedModel && p.providerId === usedProvider, ), ); if (rawFinalModelInfo) { @@ -1725,7 +1727,8 @@ chat.openapi(completions, async (c) => { // Use the canonical model ID from finalModelInfo (looked up after routing) // Fall back to usedModel (raw provider model name) for custom providers - let baseModelName = finalModelInfo?.id ?? usedModel; + let baseModelName = + finalModelInfo?.id ?? stripRegionFromModelName(usedModel, usedRegion); // Check if this is an image generation model const imageGenProviderMapping = finalModelInfo?.providers.find( diff --git a/apps/gateway/src/lib/costs.ts b/apps/gateway/src/lib/costs.ts index b3c323f6c2..037439cceb 100644 --- a/apps/gateway/src/lib/costs.ts +++ b/apps/gateway/src/lib/costs.ts @@ -5,9 +5,11 @@ import { getEffectiveDiscount } from "@llmgateway/db"; import { logger } from "@llmgateway/logger"; import { type ModelDefinition, + type ProviderModelMapping, models, type PricingTier, type ToolCall, + expandAllProviderRegions, } from "@llmgateway/models"; // Define ChatMessage type to match what gpt-tokenizer expects @@ -102,11 +104,17 @@ export async function calculateCosts( organizationId: string | null = null, ) { // Find the model info - try both base model name and provider model name - let modelInfo = models.find((m) => m.id === model) as ModelDefinition; + // Strip :region suffix if present (e.g., "deepseek-v3.2:cn-beijing" → "deepseek-v3.2") + const baseModel = model.includes(":") ? model.split(":")[0] : model; + let modelInfo = models.find( + (m) => m.id === model || m.id === baseModel, + ) as ModelDefinition; if (!modelInfo) { modelInfo = models.find((m) => - m.providers.some((p) => p.modelName === model), + m.providers.some( + (p) => p.modelName === model || p.modelName === baseModel, + ), ) as ModelDefinition; } @@ -224,9 +232,18 @@ export async function calculateCosts( calculatedCompletionTokens ??= 0; // Find the provider-specific pricing - const providerInfo = modelInfo.providers.find( - (p) => p.providerId === provider, + // Expand region entries so we can match the specific region's pricing + const expandedProviders = expandAllProviderRegions( + modelInfo.providers as ProviderModelMapping[], ); + const providerInfo = + expandedProviders.find( + (p) => p.providerId === provider && p.modelName === model, + ) ?? + expandedProviders.find( + (p) => p.providerId === provider && p.modelName === baseModel, + ) ?? + expandedProviders.find((p) => p.providerId === provider); if (!providerInfo) { return { From 514ed9893373f6fb239c5b077761b4ae025b7732 Mon Sep 17 00:00:00 2001 From: romel lauron Date: Thu, 26 Mar 2026 23:55:08 +0800 Subject: [PATCH 26/55] fix: auto-resolve region for single-region models When user selects provider without region (e.g., "Alibaba Cloud") for a model that only exists in specific regions, auto-resolve to the first available regional entry instead of defaulting to the provider's default region which may not support the model. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/gateway/src/chat/chat.ts | 15 ++++++++++----- packages/models/src/region-helpers.ts | 2 -- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/apps/gateway/src/chat/chat.ts b/apps/gateway/src/chat/chat.ts index 8e6e4d137f..1c1407ec08 100644 --- a/apps/gateway/src/chat/chat.ts +++ b/apps/gateway/src/chat/chat.ts @@ -1440,12 +1440,17 @@ chat.openapi(completions, async (c) => { usedModel = sameProviderMappings[0].modelName; usedRegion ??= (sameProviderMappings[0] as ProviderModelMapping).region; } - // If region still unset, derive it from the first matching entry + // If region still unset, derive it from the first regional entry. + // This handles the case where the user selects "Alibaba Cloud" (no region) + // for a model that only exists in specific regions (e.g., cn-beijing only). if (!usedRegion) { - const firstMatch = sameProviderMappings[0] as - | ProviderModelMapping - | undefined; - usedRegion = firstMatch?.region; + const firstRegionalMatch = sameProviderMappings.find( + (p) => (p as ProviderModelMapping).region, + ) as ProviderModelMapping | undefined; + if (firstRegionalMatch) { + usedRegion = firstRegionalMatch.region; + usedModel = firstRegionalMatch.modelName; + } } } diff --git a/packages/models/src/region-helpers.ts b/packages/models/src/region-helpers.ts index 93fac49721..9d99be7e48 100644 --- a/packages/models/src/region-helpers.ts +++ b/packages/models/src/region-helpers.ts @@ -31,8 +31,6 @@ export function expandProviderRegions( const { regions: _, ...base } = mapping; - // Include the base entry (no region) as the provider-level "auto-select region" - // option, plus one entry per region with :region suffix in modelName. const regionEntries = mapping.regions.map(({ id, ...overrides }) => ({ ...base, ...overrides, From dcdd4bf07fede8718d665c1deb34e2b9a35016c6 Mon Sep 17 00:00:00 2001 From: Luca Steeb Date: Wed, 25 Mar 2026 23:29:25 +0700 Subject: [PATCH 27/55] feat: add flags to load script --- scripts/load.sh | 243 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100755 scripts/load.sh diff --git a/scripts/load.sh b/scripts/load.sh new file mode 100755 index 0000000000..f31dfa0f83 --- /dev/null +++ b/scripts/load.sh @@ -0,0 +1,243 @@ +#!/bin/bash + +set -euo pipefail + +DEFAULT_API_URL="https://api.llmgateway.io/v1/chat/completions" +DEFAULT_PAYLOAD_FILE="/Users/steebchen/Downloads/messages.json" +DEFAULT_OUTPUT_DIR="scripts/request-logs" +DEFAULT_MAX_REQUESTS="0" +DEFAULT_REQUESTS_PER_MINUTE="30" + +API_URL="$DEFAULT_API_URL" +PAYLOAD_FILE="$DEFAULT_PAYLOAD_FILE" +OUTPUT_DIR="$DEFAULT_OUTPUT_DIR" +MAX_REQUESTS="$DEFAULT_MAX_REQUESTS" +REQUESTS_PER_MINUTE="$DEFAULT_REQUESTS_PER_MINUTE" +INJECT_PAYLOAD=false + +usage() { + echo "Usage: $0 [options]" >&2 + echo >&2 + echo "Options:" >&2 + echo " --api-url URL Default: $DEFAULT_API_URL" >&2 + echo " --payload-file PATH Default: $DEFAULT_PAYLOAD_FILE" >&2 + echo " --output-dir PATH Default: $DEFAULT_OUTPUT_DIR" >&2 + echo " --max-requests COUNT Default: $DEFAULT_MAX_REQUESTS" >&2 + echo " --requests-per-minute NUMBER Default: $DEFAULT_REQUESTS_PER_MINUTE" >&2 + echo " --inject Inject a cache-busting prefix into the first system message" >&2 + echo " -h, --help Show this help" >&2 +} + +require_value() { + local option="$1" + local value="${2:-}" + + if [[ -z "$value" ]]; then + echo "Missing value for ${option}." >&2 + usage + exit 1 + fi +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --api-url) + require_value "$1" "${2:-}" + API_URL="$2" + shift 2 + continue + ;; + --api-url=*) + require_value "${1%%=*}" "${1#*=}" + API_URL="${1#*=}" + ;; + --payload-file) + require_value "$1" "${2:-}" + PAYLOAD_FILE="$2" + shift 2 + continue + ;; + --payload-file=*) + require_value "${1%%=*}" "${1#*=}" + PAYLOAD_FILE="${1#*=}" + ;; + --output-dir) + require_value "$1" "${2:-}" + OUTPUT_DIR="$2" + shift 2 + continue + ;; + --output-dir=*) + require_value "${1%%=*}" "${1#*=}" + OUTPUT_DIR="${1#*=}" + ;; + --max-requests) + require_value "$1" "${2:-}" + MAX_REQUESTS="$2" + shift 2 + continue + ;; + --max-requests=*) + require_value "${1%%=*}" "${1#*=}" + MAX_REQUESTS="${1#*=}" + ;; + --requests-per-minute) + require_value "$1" "${2:-}" + REQUESTS_PER_MINUTE="$2" + shift 2 + continue + ;; + --requests-per-minute=*) + require_value "${1%%=*}" "${1#*=}" + REQUESTS_PER_MINUTE="${1#*=}" + ;; + --inject) + INJECT_PAYLOAD=true + ;; + -h | --help) + usage + exit 0 + ;; + -*) + echo "Unknown option: $1" >&2 + usage + exit 1 + ;; + *) + echo "Positional arguments are not supported: $1" >&2 + usage + exit 1 + ;; + esac + + shift +done + +if ! [[ "$REQUESTS_PER_MINUTE" =~ ^[0-9]+([.][0-9]+)?$ ]]; then + echo "Requests per minute must be a positive number." >&2 + usage + exit 1 +fi + +if ! [[ "$MAX_REQUESTS" =~ ^[0-9]+$ ]]; then + echo "Max requests must be a non-negative integer." >&2 + usage + exit 1 +fi + +if awk "BEGIN { exit !($REQUESTS_PER_MINUTE > 0) }"; then + INTERVAL_SECONDS="$(awk "BEGIN { printf \"%.6f\", 60 / $REQUESTS_PER_MINUTE }")" +else + echo "Requests per minute must be greater than 0." >&2 + exit 1 +fi + +if [[ -z "${LLM_GATEWAY_API_KEY:-}" ]]; then + echo "LLM_GATEWAY_API_KEY is required." >&2 + exit 1 +fi + +if [[ ! -f "$PAYLOAD_FILE" ]]; then + echo "Payload file not found: $PAYLOAD_FILE" >&2 + exit 1 +fi + +if [[ "$INJECT_PAYLOAD" == "true" ]] && ! command -v jq >/dev/null 2>&1; then + echo "jq is required." >&2 + exit 1 +fi + +REQUEST_URL="$API_URL" +if [[ "$REQUEST_URL" != */v1/chat/completions ]]; then + REQUEST_URL="${REQUEST_URL%/}/v1/chat/completions" +fi + +mkdir -p "$OUTPUT_DIR" + +echo "Sending ${REQUESTS_PER_MINUTE} request(s) per minute (${INTERVAL_SECONDS}s between launches)." +echo "Payload injection: ${INJECT_PAYLOAD}" +echo "Request URL: ${REQUEST_URL}" + +stop_requests() { + echo + echo "Stopping launcher and terminating in-flight requests..." >&2 + + while read -r pid; do + kill "$pid" 2>/dev/null || true + done < <(jobs -pr) + + wait || true + exit 0 +} + +trap stop_requests INT TERM + +request_count=0 + +while true; do + request_count=$((request_count + 1)) + timestamp="$(date -u +"%Y%m%dT%H%M%SZ")" + output_file="${OUTPUT_DIR}/chat-completion-${request_count}-${timestamp}.json" + + echo "[$(date +"%Y-%m-%d %H:%M:%S")] starting request #${request_count} -> ${output_file}" + + ( + temp_payload="" + request_payload="$PAYLOAD_FILE" + trap '[[ -n "$temp_payload" ]] && rm -f "$temp_payload"' EXIT + + if [[ "$INJECT_PAYLOAD" == "true" ]]; then + cache_buster="$(date -u +"%Y-%m-%dT%H:%M:%S").$(python3 -c 'import time; print(f"{time.time_ns() % 1_000_000_000:09d}")' 2>/dev/null || date -u +"000000000")Z" + temp_payload="$(mktemp)" + + jq --arg prefix "[ts:${cache_buster}] " ' + def inject_prefix: + reduce range(0; length) as $i ( + {updated: false, items: .}; + if .updated then + . + elif .items[$i].role == "system" then + .updated = true | + .items[$i].content = ( + if (.items[$i].content | type) == "string" then + $prefix + .items[$i].content + elif (.items[$i].content | type) == "array" then + [{type: "text", text: $prefix}] + .items[$i].content + else + .items[$i].content + end + ) + else + . + end + ) | .items; + + if (.messages | type) != "array" then + error("payload must contain a messages array") + elif ([.messages[] | select(.role == "system")] | length) == 0 then + error("payload must contain at least one system message") + else + .messages |= inject_prefix + end + ' "$PAYLOAD_FILE" > "$temp_payload" + + request_payload="$temp_payload" + fi + + curl -N "$REQUEST_URL" -s \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${LLM_GATEWAY_API_KEY}" \ + -H "X-No-Fallback: true" \ + -d "@${request_payload}" \ + >"$output_file" + ) & + + if [[ "$MAX_REQUESTS" -gt 0 && "$request_count" -ge "$MAX_REQUESTS" ]]; then + break + fi + + sleep "$INTERVAL_SECONDS" +done + +echo "Launched ${request_count} request(s). Waiting for in-flight requests to finish..." +wait || true From 00f686fa62ff91d31220b492c6e90975b9f492e6 Mon Sep 17 00:00:00 2001 From: Luca Steeb Date: Wed, 25 Mar 2026 23:38:16 +0700 Subject: [PATCH 28/55] chore: remove cross-env-shell --- package.json | 5 ++--- pnpm-lock.yaml | 9 --------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 3219dc5434..ef401bc1bc 100644 --- a/package.json +++ b/package.json @@ -21,11 +21,11 @@ "prepare-codex": "mv ~/.nvm ~/.nvm-bak && export PATH=\"${ASDF_DATA_DIR:-$HOME/.asdf}/shims:$PATH\" && . ~/.bashrc && sudo service postgresql start && sudo service redis-server start && sleep 20 && pnpm sync", "push": "pnpm push-dev && pnpm push-test", "push-dev": "pnpm --filter db push", - "push-test": "cross-env-shell \"DATABASE_URL=${DATABASE_URL:-postgres://postgres:pw@localhost:5432/test}\" pnpm --filter db push", + "push-test": "DATABASE_URL=${DATABASE_URL:-postgres://postgres:pw@localhost:5432/test} pnpm --filter db push", "seed": "pnpm --filter db seed", "setup": "pnpm build:core && docker compose down -v && docker compose up -d && sleep 5 && pnpm push-test && pnpm push-dev && pnpm seed", "sync": "pnpm push-dev; pnpm push-test", - "test:e2e": "pnpm build:core && cross-env-shell \"E2E_TEST=true vitest run -c vitest/vitest.e2e.config.mts --no-file-parallelism\"", + "test:e2e": "pnpm build:core && E2E_TEST=true vitest run -c vitest/vitest.e2e.config.mts --no-file-parallelism", "test:unit": "pnpm build:core && vitest run --no-file-parallelism" }, "commitlint": { @@ -43,7 +43,6 @@ "@types/node": "25.3.0", "commitlint": "20.4.3", "concurrently": "^9.2.1", - "cross-env-shell": "7.0.3", "dotenv": "16.5.0", "esbuild-fix-imports-plugin": "1.0.23", "eslint": "^9.34.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 51fdd9aeee..a9e28f466a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,9 +45,6 @@ importers: concurrently: specifier: ^9.2.1 version: 9.2.1 - cross-env-shell: - specifier: 7.0.3 - version: 7.0.3 dotenv: specifier: 16.5.0 version: 16.5.0 @@ -5574,10 +5571,6 @@ packages: crisp-sdk-web@1.0.26: resolution: {integrity: sha512-MBK/+tCNJmt/l9ZYHb0cUUaaF02rCKeqLw1cruY/HU48tPOOnhUM0JdvuPaeq1d5VB2DCILY4rGYE4NeJbXvzg==} - cross-env-shell@7.0.3: - resolution: {integrity: sha512-ZEtnHInQRRzuAbmMpUqu/mk1jaZun14WdcmnpzZZ6Bpi1BLTPkbKioQ6+wkFssf+m/DpdttDpsCX6g8zU9W40A==} - hasBin: true - cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -15563,8 +15556,6 @@ snapshots: crisp-sdk-web@1.0.26: {} - cross-env-shell@7.0.3: {} - cross-spawn@7.0.6: dependencies: path-key: 3.1.1 From ac2ae5ce48102116251214739bc1d523a8b476db Mon Sep 17 00:00:00 2001 From: Luca Steeb Date: Thu, 26 Mar 2026 17:28:37 +0700 Subject: [PATCH 29/55] feat(gateway): add quartz provider --- apps/gateway/src/chat/chat.ts | 73 ++++------ .../gateway/src/chat/tools/extract-content.ts | 1 + apps/gateway/src/chat/tools/extract-images.ts | 1 + .../src/chat/tools/extract-reasoning.ts | 1 + .../src/chat/tools/extract-token-usage.ts | 1 + .../src/chat/tools/extract-tool-calls.ts | 1 + .../src/chat/tools/parse-provider-response.ts | 1 + .../chat/tools/resolve-provider-context.ts | 4 +- .../tools/transform-response-to-openai.ts | 1 + .../tools/transform-streaming-to-openai.ts | 1 + apps/gateway/src/lib/costs.ts | 2 + apps/gateway/src/lib/logs.ts | 2 + apps/gateway/src/models/models.spec.ts | 14 ++ apps/gateway/src/videos/videos.ts | 136 +++++++++--------- .../playground/video-page-client.tsx | 4 +- apps/playground/src/lib/video-gen.ts | 9 +- .../components/provider-keys/provider-logo.ts | 1 + apps/worker/src/services/video-jobs.ts | 12 +- packages/actions/src/get-provider-endpoint.ts | 114 +++++++++------ packages/actions/src/get-provider-headers.ts | 1 + packages/actions/src/models.spec.ts | 4 +- packages/actions/src/prepare-request-body.ts | 1 + packages/actions/src/validate-provider-key.ts | 4 +- packages/models/src/models.ts | 27 +++- packages/models/src/providers.ts | 21 +++ .../shared/src/components/provider-icons.tsx | 2 + 26 files changed, 261 insertions(+), 178 deletions(-) diff --git a/apps/gateway/src/chat/chat.ts b/apps/gateway/src/chat/chat.ts index 1c1407ec08..747ffb946e 100644 --- a/apps/gateway/src/chat/chat.ts +++ b/apps/gateway/src/chat/chat.ts @@ -161,6 +161,18 @@ function filterRegionsByAvailableKeys( }); } +function usesGoogleQueryToken(provider: string): boolean { + return ( + provider === "google-ai-studio" || + provider === "google-vertex" || + provider === "quartz" + ); +} + +function isGoogleCompatibleProvider(provider: string): boolean { + return usesGoogleQueryToken(provider) || provider === "obsidian"; +} + // Pre-compiled regex pattern to avoid recompilation per request const SSE_FIELD_PATTERN = /^[a-zA-Z_-]+:\s*/; @@ -2047,9 +2059,7 @@ chat.openapi(completions, async (c) => { usedProvider, providerKey?.baseUrl ?? undefined, upstreamModelName, - usedProvider === "google-ai-studio" || usedProvider === "google-vertex" - ? usedToken - : undefined, + usesGoogleQueryToken(usedProvider) ? usedToken : undefined, stream, supportsReasoning, hasExistingToolCalls, @@ -2586,11 +2596,7 @@ chat.openapi(completions, async (c) => { // For Google providers, enrich messages with cached thought_signatures // This is needed for multi-turn tool call conversations with Gemini 3+ - if ( - usedProvider === "google-ai-studio" || - usedProvider === "google-vertex" || - usedProvider === "obsidian" - ) { + if (isGoogleCompatibleProvider(usedProvider)) { const { redisClient } = await import("@llmgateway/cache"); for (const message of messages) { if ( @@ -4347,11 +4353,7 @@ chat.openapi(completions, async (c) => { } // For Google providers, add usage information when available - if ( - usedProvider === "google-ai-studio" || - usedProvider === "google-vertex" || - usedProvider === "obsidian" - ) { + if (isGoogleCompatibleProvider(usedProvider)) { const usage = extractTokenUsage( data, usedProvider, @@ -4520,9 +4522,7 @@ chat.openapi(completions, async (c) => { // For providers with custom extraction logic (google-ai-studio, anthropic), // use raw data. For others (like aws-bedrock), use transformed OpenAI format. const contentChunk = extractContent( - usedProvider === "google-ai-studio" || - usedProvider === "google-vertex" || - usedProvider === "obsidian" || + isGoogleCompatibleProvider(usedProvider) || usedProvider === "anthropic" ? data : transformedData, @@ -4539,11 +4539,7 @@ chat.openapi(completions, async (c) => { } // Track image data size for Google providers (for token estimation) - if ( - usedProvider === "google-ai-studio" || - usedProvider === "google-vertex" || - usedProvider === "obsidian" - ) { + if (isGoogleCompatibleProvider(usedProvider)) { const parts = data.candidates?.[0]?.content?.parts ?? []; for (const part of parts) { if (part.inlineData?.data) { @@ -4566,11 +4562,7 @@ chat.openapi(completions, async (c) => { ) { webSearchCount++; } - } else if ( - usedProvider === "google-ai-studio" || - usedProvider === "google-vertex" || - usedProvider === "obsidian" - ) { + } else if (isGoogleCompatibleProvider(usedProvider)) { // For Google, count when grounding metadata is present if (data.candidates?.[0]?.groundingMetadata) { const groundingMetadata = @@ -4602,9 +4594,7 @@ chat.openapi(completions, async (c) => { // For providers with custom extraction logic (google-ai-studio, anthropic), // use raw data. For others, use transformed OpenAI format. const reasoningContentChunk = extractReasoning( - usedProvider === "google-ai-studio" || - usedProvider === "google-vertex" || - usedProvider === "obsidian" || + isGoogleCompatibleProvider(usedProvider) || usedProvider === "anthropic" ? data : transformedData, @@ -4666,6 +4656,7 @@ chat.openapi(completions, async (c) => { switch (usedProvider) { case "google-ai-studio": case "google-vertex": + case "quartz": case "obsidian": // Preserve original Google finish reason for logging if (data.promptFeedback?.blockReason) { @@ -4968,9 +4959,7 @@ chat.openapi(completions, async (c) => { // For Google, check for original finish reasons that indicate content filtering // These include both finishReason values and promptFeedback.blockReason values const isGoogleContentFilterStreaming = - (usedProvider === "google-ai-studio" || - usedProvider === "google-vertex" || - usedProvider === "obsidian") && + isGoogleCompatibleProvider(usedProvider) && (finishReason === "SAFETY" || finishReason === "PROHIBITED_CONTENT" || finishReason === "RECITATION" || @@ -5107,9 +5096,7 @@ chat.openapi(completions, async (c) => { // Only add image input tokens for providers that // exclude them from upstream usage (Google) const providerExcludesImageInput = - usedProvider === "google-ai-studio" || - usedProvider === "google-vertex" || - usedProvider === "obsidian"; + isGoogleCompatibleProvider(usedProvider); const imageInputAdj = providerExcludesImageInput ? inputImageCount * 560 : 0; @@ -5395,11 +5382,7 @@ chat.openapi(completions, async (c) => { ); // Enhanced logging for Google models streaming to debug missing responses - if ( - usedProvider === "google-ai-studio" || - usedProvider === "google-vertex" || - usedProvider === "obsidian" - ) { + if (isGoogleCompatibleProvider(usedProvider)) { logger.debug("Google model streaming response completed", { usedProvider, usedModel, @@ -6625,11 +6608,7 @@ chat.openapi(completions, async (c) => { } // Enhanced logging for Google models to debug missing responses - if ( - usedProvider === "google-ai-studio" || - usedProvider === "google-vertex" || - usedProvider === "obsidian" - ) { + if (isGoogleCompatibleProvider(usedProvider)) { logger.debug("Google model response parsed", { usedProvider, usedModel, @@ -6807,9 +6786,7 @@ chat.openapi(completions, async (c) => { // For Google, check for original finish reasons that indicate content filtering // These include both finishReason values and promptFeedback.blockReason values const isGoogleContentFilter = - (usedProvider === "google-ai-studio" || - usedProvider === "google-vertex" || - usedProvider === "obsidian") && + isGoogleCompatibleProvider(usedProvider) && (finishReason === "SAFETY" || finishReason === "PROHIBITED_CONTENT" || finishReason === "RECITATION" || diff --git a/apps/gateway/src/chat/tools/extract-content.ts b/apps/gateway/src/chat/tools/extract-content.ts index 8fc74b9203..0f0d51c486 100644 --- a/apps/gateway/src/chat/tools/extract-content.ts +++ b/apps/gateway/src/chat/tools/extract-content.ts @@ -7,6 +7,7 @@ export function extractContent(data: any, provider: Provider): string { switch (provider) { case "google-ai-studio": case "google-vertex": + case "quartz": case "obsidian": { const parts = data.candidates?.[0]?.content?.parts ?? []; const contentParts = parts.filter((part: any) => !part.thought); diff --git a/apps/gateway/src/chat/tools/extract-images.ts b/apps/gateway/src/chat/tools/extract-images.ts index e4d42d6988..1d9255af51 100644 --- a/apps/gateway/src/chat/tools/extract-images.ts +++ b/apps/gateway/src/chat/tools/extract-images.ts @@ -12,6 +12,7 @@ export function extractImages(data: any, provider: Provider): ImageObject[] { switch (provider) { case "google-ai-studio": case "google-vertex": + case "quartz": case "obsidian": { const parts = data.candidates?.[0]?.content?.parts ?? []; const imageParts = parts.filter((part: any) => part.inlineData); diff --git a/apps/gateway/src/chat/tools/extract-reasoning.ts b/apps/gateway/src/chat/tools/extract-reasoning.ts index 2723505180..c0da508577 100644 --- a/apps/gateway/src/chat/tools/extract-reasoning.ts +++ b/apps/gateway/src/chat/tools/extract-reasoning.ts @@ -19,6 +19,7 @@ export function extractReasoning(data: any, provider: Provider): string { } case "google-ai-studio": case "google-vertex": + case "quartz": case "obsidian": { const parts = data.candidates?.[0]?.content?.parts ?? []; const reasoningParts = parts.filter((part: any) => part.thought); diff --git a/apps/gateway/src/chat/tools/extract-token-usage.ts b/apps/gateway/src/chat/tools/extract-token-usage.ts index 9fcdf4ecf0..892f49f2f0 100644 --- a/apps/gateway/src/chat/tools/extract-token-usage.ts +++ b/apps/gateway/src/chat/tools/extract-token-usage.ts @@ -49,6 +49,7 @@ export function extractTokenUsage( switch (provider) { case "google-ai-studio": case "google-vertex": + case "quartz": case "obsidian": if (data.usageMetadata) { promptTokens = data.usageMetadata.promptTokenCount ?? null; diff --git a/apps/gateway/src/chat/tools/extract-tool-calls.ts b/apps/gateway/src/chat/tools/extract-tool-calls.ts index bf88b7ac82..ff41cb8200 100644 --- a/apps/gateway/src/chat/tools/extract-tool-calls.ts +++ b/apps/gateway/src/chat/tools/extract-tool-calls.ts @@ -45,6 +45,7 @@ export function extractToolCalls( return null; case "google-ai-studio": case "google-vertex": + case "quartz": case "obsidian": { // Google AI Studio tool calls in streaming // Include thoughtSignature if present (required for Gemini 3 multi-turn conversations) diff --git a/apps/gateway/src/chat/tools/parse-provider-response.ts b/apps/gateway/src/chat/tools/parse-provider-response.ts index 56ba769a16..31c8919510 100644 --- a/apps/gateway/src/chat/tools/parse-provider-response.ts +++ b/apps/gateway/src/chat/tools/parse-provider-response.ts @@ -189,6 +189,7 @@ export function parseProviderResponse( } case "google-ai-studio": case "google-vertex": + case "quartz": case "obsidian": { // Check if response is missing candidates - treat as content filter if (!json.candidates || json.candidates.length === 0) { diff --git a/apps/gateway/src/chat/tools/resolve-provider-context.ts b/apps/gateway/src/chat/tools/resolve-provider-context.ts index 8f9996d710..e9d29f47fc 100644 --- a/apps/gateway/src/chat/tools/resolve-provider-context.ts +++ b/apps/gateway/src/chat/tools/resolve-provider-context.ts @@ -263,7 +263,9 @@ export async function resolveProviderContext( usedProvider as Provider, providerKey?.baseUrl ?? undefined, upstreamModelName, - usedProvider === "google-ai-studio" || usedProvider === "google-vertex" + usedProvider === "google-ai-studio" || + usedProvider === "google-vertex" || + usedProvider === "quartz" ? usedToken : undefined, options.stream, diff --git a/apps/gateway/src/chat/tools/transform-response-to-openai.ts b/apps/gateway/src/chat/tools/transform-response-to-openai.ts index 81b3584df5..d4bea3c950 100644 --- a/apps/gateway/src/chat/tools/transform-response-to-openai.ts +++ b/apps/gateway/src/chat/tools/transform-response-to-openai.ts @@ -88,6 +88,7 @@ export function transformResponseToOpenai( switch (usedProvider) { case "google-ai-studio": case "google-vertex": + case "quartz": case "obsidian": { transformedResponse = { id: `chatcmpl-${Date.now()}`, diff --git a/apps/gateway/src/chat/tools/transform-streaming-to-openai.ts b/apps/gateway/src/chat/tools/transform-streaming-to-openai.ts index e22df74990..3be1cc1bbd 100644 --- a/apps/gateway/src/chat/tools/transform-streaming-to-openai.ts +++ b/apps/gateway/src/chat/tools/transform-streaming-to-openai.ts @@ -302,6 +302,7 @@ export function transformStreamingToOpenai( case "google-ai-studio": case "google-vertex": + case "quartz": case "obsidian": { const mapFinishReason = ( finishReason?: string, diff --git a/apps/gateway/src/lib/costs.ts b/apps/gateway/src/lib/costs.ts index 037439cceb..1ffe39d201 100644 --- a/apps/gateway/src/lib/costs.ts +++ b/apps/gateway/src/lib/costs.ts @@ -345,6 +345,7 @@ export async function calculateCosts( const isGoogleProvider = provider === "google-ai-studio" || provider === "google-vertex" || + provider === "quartz" || provider === "obsidian"; const totalOutputTokens = isGoogleProvider ? calculatedCompletionTokens @@ -420,6 +421,7 @@ export async function calculateCosts( imageInputTokens && (provider === "google-ai-studio" || provider === "google-vertex" || + provider === "quartz" || provider === "obsidian") ? (calculatedPromptTokens || 0) + imageInputTokens : calculatedPromptTokens, diff --git a/apps/gateway/src/lib/logs.ts b/apps/gateway/src/lib/logs.ts index e80a651724..b8f2b4a8e1 100644 --- a/apps/gateway/src/lib/logs.ts +++ b/apps/gateway/src/lib/logs.ts @@ -21,6 +21,7 @@ export function isExpectedUnknownFinishReason( if ( (provider === "google-ai-studio" || provider === "google-vertex" || + provider === "quartz" || provider === "obsidian") && finishReason === "OTHER" ) { @@ -76,6 +77,7 @@ export function getUnifiedFinishReason( break; case "google-ai-studio": case "google-vertex": + case "quartz": case "obsidian": // Google finish reasons (original format, not mapped to OpenAI) if (finishReason === "STOP") { diff --git a/apps/gateway/src/models/models.spec.ts b/apps/gateway/src/models/models.spec.ts index 58f27191b1..5cb212f4c0 100644 --- a/apps/gateway/src/models/models.spec.ts +++ b/apps/gateway/src/models/models.spec.ts @@ -220,6 +220,9 @@ describe("Models API", () => { const googleVertexProvider = videoModel.providers.find( (provider: any) => provider.providerId === "google-vertex", ); + const quartzProvider = videoModel.providers.find( + (provider: any) => provider.providerId === "quartz", + ); expect(obsidianProvider?.pricing.per_second).toBeDefined(); expect(obsidianProvider?.supportedVideoSizes).toEqual([ "1280x720", @@ -238,6 +241,17 @@ describe("Models API", () => { ]); expect(googleVertexProvider?.supportsVideoAudio).toBe(true); expect(googleVertexProvider?.supportsVideoWithoutAudio).toBe(true); + expect(quartzProvider?.pricing.per_second).toBeDefined(); + expect(quartzProvider?.supportedVideoSizes).toEqual([ + "1280x720", + "720x1280", + "1920x1080", + "1080x1920", + "3840x2160", + "2160x3840", + ]); + expect(quartzProvider?.supportsVideoAudio).toBe(true); + expect(quartzProvider?.supportsVideoWithoutAudio).toBe(true); expect(avalancheProvider?.pricing.per_second).toBeDefined(); expect(avalancheProvider?.supportedVideoSizes).toEqual([ "1920x1080", diff --git a/apps/gateway/src/videos/videos.ts b/apps/gateway/src/videos/videos.ts index 1ec6ac5036..fd3780f336 100644 --- a/apps/gateway/src/videos/videos.ts +++ b/apps/gateway/src/videos/videos.ts @@ -748,7 +748,7 @@ function getVideoModel(model: string): { throw new HTTPException(400, { message: - "Unsupported video model. Use a video-capable model from /v1/models, optionally prefixed with a configured provider like openai/, avalanche/, obsidian/, or google-vertex/.", + "Unsupported video model. Use a video-capable model from /v1/models, optionally prefixed with a configured provider like openai/, avalanche/, obsidian/, google-vertex/, or quartz/.", }); } @@ -761,6 +761,10 @@ function isSoraVideoModelName(modelName: string): boolean { return modelName === "sora-2" || modelName === "sora-2-pro"; } +function isVertexCompatibleVideoProvider(providerId: string): boolean { + return providerId === "google-vertex" || providerId === "quartz"; +} + function getVideoProviderConstraintReasons( provider: ProviderModelMapping, videoSize: VideoSizeConfig, @@ -830,12 +834,12 @@ function getVideoProviderConstraintReasons( if ( !isSoraVideoModelName(provider.modelName) && inputMode === "frames" && - provider.providerId !== "google-vertex" && + !isVertexCompatibleVideoProvider(provider.providerId) && provider.providerId !== "avalanche" && provider.providerId !== "obsidian" ) { reasons.push( - "frame inputs are currently only supported through obsidian, google-vertex, or avalanche", + "frame inputs are currently only supported through obsidian, google-vertex, quartz, or avalanche", ); } @@ -850,10 +854,10 @@ function getVideoProviderConstraintReasons( return reasons; } - if (provider.providerId === "google-vertex") { + if (isVertexCompatibleVideoProvider(provider.providerId)) { if (provider.modelName !== "veo-3.1-generate-preview") { reasons.push( - "reference images are currently only supported on google-vertex/veo-3.1-generate-preview", + `reference images are currently only supported on ${provider.providerId}/veo-3.1-generate-preview`, ); } } else if (provider.providerId === "avalanche") { @@ -870,7 +874,7 @@ function getVideoProviderConstraintReasons( } } else { reasons.push( - "reference images are currently only supported through obsidian, google-vertex, or avalanche", + "reference images are currently only supported through obsidian, google-vertex, quartz, or avalanche", ); } @@ -1018,6 +1022,7 @@ function getVideoUpstreamModelName( case "avalanche": return getAvalancheVideoModelName(baseModelName); case "google-vertex": + case "quartz": return baseModelName; default: return baseModelName; @@ -1068,6 +1073,8 @@ function getDefaultVideoProviderBaseUrl(providerId: Provider): string | null { return "https://api.openai.com"; case "google-vertex": return "https://us-central1-aiplatform.googleapis.com"; + case "quartz": + return null; default: return null; } @@ -1106,19 +1113,13 @@ async function resolveProviderContext( baseModelName?: string, ): Promise { const defaultBaseUrl = getDefaultVideoProviderBaseUrl(providerId); - const sharedVertexProjectId = - providerId === "google-vertex" - ? getProviderEnvValue("google-vertex", "project") - : undefined; - const sharedVertexRegion = - providerId === "google-vertex" - ? (getProviderEnvValue( - "google-vertex", - "region", - undefined, - "us-central1", - ) ?? "us-central1") - : undefined; + const sharedVertexProjectId = isVertexCompatibleVideoProvider(providerId) + ? getProviderEnvValue(providerId, "project") + : undefined; + const sharedVertexRegion = isVertexCompatibleVideoProvider(providerId) + ? (getProviderEnvValue(providerId, "region", undefined, "us-central1") ?? + "us-central1") + : undefined; if (project.mode === "api-keys") { const providerKey = await findProviderKey( @@ -1142,10 +1143,9 @@ async function resolveProviderContext( }); } - if (providerId === "google-vertex" && !sharedVertexProjectId) { + if (isVertexCompatibleVideoProvider(providerId) && !sharedVertexProjectId) { throw new HTTPException(500, { - message: - "LLM_GOOGLE_CLOUD_PROJECT environment variable is required for google-vertex video generation", + message: `${providerId} project environment variable is required for video generation`, }); } @@ -1181,24 +1181,21 @@ async function resolveProviderContext( }); } - const vertexProjectId = - providerId === "google-vertex" - ? getProviderEnvValue("google-vertex", "project", env.configIndex) - : undefined; - const vertexRegion = - providerId === "google-vertex" - ? (getProviderEnvValue( - "google-vertex", - "region", - env.configIndex, - "us-central1", - ) ?? "us-central1") - : undefined; - - if (providerId === "google-vertex" && !vertexProjectId) { + const vertexProjectId = isVertexCompatibleVideoProvider(providerId) + ? getProviderEnvValue(providerId, "project", env.configIndex) + : undefined; + const vertexRegion = isVertexCompatibleVideoProvider(providerId) + ? (getProviderEnvValue( + providerId, + "region", + env.configIndex, + "us-central1", + ) ?? "us-central1") + : undefined; + + if (isVertexCompatibleVideoProvider(providerId) && !vertexProjectId) { throw new HTTPException(500, { - message: - "LLM_GOOGLE_CLOUD_PROJECT environment variable is required for google-vertex video generation", + message: `${providerId} project environment variable is required for video generation`, }); } @@ -1243,10 +1240,9 @@ async function resolveProviderContext( }); } - if (providerId === "google-vertex" && !sharedVertexProjectId) { + if (isVertexCompatibleVideoProvider(providerId) && !sharedVertexProjectId) { throw new HTTPException(500, { - message: - "LLM_GOOGLE_CLOUD_PROJECT environment variable is required for google-vertex video generation", + message: `${providerId} project environment variable is required for video generation`, }); } @@ -1287,24 +1283,21 @@ async function resolveProviderContext( }); } - const vertexProjectId = - providerId === "google-vertex" - ? getProviderEnvValue("google-vertex", "project", env.configIndex) - : undefined; - const vertexRegion = - providerId === "google-vertex" - ? (getProviderEnvValue( - "google-vertex", - "region", - env.configIndex, - "us-central1", - ) ?? "us-central1") - : undefined; + const vertexProjectId = isVertexCompatibleVideoProvider(providerId) + ? getProviderEnvValue(providerId, "project", env.configIndex) + : undefined; + const vertexRegion = isVertexCompatibleVideoProvider(providerId) + ? (getProviderEnvValue( + providerId, + "region", + env.configIndex, + "us-central1", + ) ?? "us-central1") + : undefined; - if (providerId === "google-vertex" && !vertexProjectId) { + if (isVertexCompatibleVideoProvider(providerId) && !vertexProjectId) { throw new HTTPException(500, { - message: - "LLM_GOOGLE_CLOUD_PROJECT environment variable is required for google-vertex video generation", + message: `${providerId} project environment variable is required for video generation`, }); } @@ -1343,8 +1336,8 @@ async function hasVideoProviderConfiguration( (providerKey.baseUrl ?? getProviderEnvValue(providerId, "baseUrl") ?? defaultBaseUrl) && - (providerId !== "google-vertex" || - Boolean(getProviderEnvValue("google-vertex", "project"))), + (!isVertexCompatibleVideoProvider(providerId) || + Boolean(getProviderEnvValue(providerId, "project"))), ); } @@ -1357,10 +1350,10 @@ async function hasVideoProviderConfiguration( getProviderEnv(providerId).configIndex, ) ?? defaultBaseUrl) && - (providerId !== "google-vertex" || + (!isVertexCompatibleVideoProvider(providerId) || Boolean( getProviderEnvValue( - "google-vertex", + providerId, "project", getProviderEnv(providerId).configIndex, ), @@ -1374,8 +1367,8 @@ async function hasVideoProviderConfiguration( (providerKey.baseUrl ?? getProviderEnvValue(providerId, "baseUrl") ?? defaultBaseUrl) && - (providerId !== "google-vertex" || - Boolean(getProviderEnvValue("google-vertex", "project"))), + (!isVertexCompatibleVideoProvider(providerId) || + Boolean(getProviderEnvValue(providerId, "project"))), ); } @@ -1387,10 +1380,10 @@ async function hasVideoProviderConfiguration( getProviderEnv(providerId).configIndex, ) ?? defaultBaseUrl) && - (providerId !== "google-vertex" || + (!isVertexCompatibleVideoProvider(providerId) || Boolean( getProviderEnvValue( - "google-vertex", + providerId, "project", getProviderEnv(providerId).configIndex, ), @@ -2830,19 +2823,19 @@ async function createGoogleVertexVideoJob( if (outputBucket && !storageProjectId) { throw new HTTPException(500, { message: - "GOOGLE_CLOUD_PROJECT environment variable is required for google-vertex video output storage", + "GOOGLE_CLOUD_PROJECT environment variable is required for vertex-compatible video output storage", }); } if (!vertexProjectId || !providerContext.vertexRegion) { throw new HTTPException(500, { message: - "Google Vertex video generation requires project and region metadata", + "Vertex-compatible video generation requires project and region metadata", }); } const upstreamModelName = getVideoUpstreamModelName( - "google-vertex", + providerContext.providerId, providerMapping.modelName, videoSize, referenceImages.length > 0 @@ -3000,6 +2993,7 @@ async function createUpstreamVideoJob( referenceImageInputs, ); case "google-vertex": + case "quartz": return await createGoogleVertexVideoJob( providerContext, providerMapping, @@ -3328,7 +3322,7 @@ videos.openapi(createVideo, async (c) => { } if ( - selectedProviderContext.providerId === "google-vertex" && + isVertexCompatibleVideoProvider(selectedProviderContext.providerId) && !getGoogleVertexVideoOutputBucket() && organization.retentionLevel === "none" ) { @@ -3350,7 +3344,7 @@ videos.openapi(createVideo, async (c) => { if (!nextProvider || requestedProvider) { throw new HTTPException(400, { message: - "Google Vertex video generation requires either GCS output storage or data retention to be enabled.", + "Vertex-compatible video generation requires either GCS output storage or data retention to be enabled.", }); } diff --git a/apps/playground/src/components/playground/video-page-client.tsx b/apps/playground/src/components/playground/video-page-client.tsx index 3ca2d78e2b..2eac7e587e 100644 --- a/apps/playground/src/components/playground/video-page-client.tsx +++ b/apps/playground/src/components/playground/video-page-client.tsx @@ -115,7 +115,9 @@ export default function VideoPageClient({ () => selectedModels.some( (modelId) => - modelId.includes("/") && !modelId.startsWith("google-vertex/"), + modelId.includes("/") && + !modelId.startsWith("google-vertex/") && + !modelId.startsWith("quartz/"), ), [selectedModels], ); diff --git a/apps/playground/src/lib/video-gen.ts b/apps/playground/src/lib/video-gen.ts index a9ce24103c..f85101c7b3 100644 --- a/apps/playground/src/lib/video-gen.ts +++ b/apps/playground/src/lib/video-gen.ts @@ -98,6 +98,7 @@ export function supportsVideoFrameInput(modelId: string): boolean { providerId === undefined || providerId === "obsidian" || providerId === "google-vertex" || + providerId === "quartz" || providerId === "avalanche" ); } @@ -107,7 +108,7 @@ export function supportsVideoReferenceInput(modelId: string): boolean { ? modelId.split("/", 2) : [undefined, modelId]; - if (providerId === "google-vertex") { + if (providerId === "google-vertex" || providerId === "quartz") { return rootModelId === "veo-3.1-generate-preview"; } @@ -177,6 +178,7 @@ function mappingSupportsVideoRequest( if ( inputMode === "frames" && mapping.providerId !== "google-vertex" && + mapping.providerId !== "quartz" && mapping.providerId !== "avalanche" && mapping.providerId !== "obsidian" ) { @@ -184,7 +186,10 @@ function mappingSupportsVideoRequest( } if (inputMode === "reference") { - if (mapping.providerId === "google-vertex") { + if ( + mapping.providerId === "google-vertex" || + mapping.providerId === "quartz" + ) { if (mapping.modelName !== "veo-3.1-generate-preview") { return false; } diff --git a/apps/ui/src/components/provider-keys/provider-logo.ts b/apps/ui/src/components/provider-keys/provider-logo.ts index aa65426f6f..fd79364091 100644 --- a/apps/ui/src/components/provider-keys/provider-logo.ts +++ b/apps/ui/src/components/provider-keys/provider-logo.ts @@ -9,6 +9,7 @@ export const providerLogoUrls: Partial< anthropic: ProviderIcons.anthropic, "google-ai-studio": ProviderIcons["google-ai-studio"], "google-vertex": ProviderIcons["google-vertex"], + quartz: ProviderIcons.quartz, "inference.net": ProviderIcons["inference.net"], "together.ai": ProviderIcons["together.ai"], mistral: ProviderIcons.mistral, diff --git a/apps/worker/src/services/video-jobs.ts b/apps/worker/src/services/video-jobs.ts index bc66d92ad0..5ca1a035a1 100644 --- a/apps/worker/src/services/video-jobs.ts +++ b/apps/worker/src/services/video-jobs.ts @@ -68,6 +68,10 @@ interface ResolvedVideoProviderContext { token: string; } +function isVertexCompatibleVideoProvider(providerId: string): boolean { + return providerId === "google-vertex" || providerId === "quartz"; +} + function getDeterministicHash(seed: string): number { let hash = 5381; @@ -99,6 +103,8 @@ function getDefaultVideoProviderBaseUrl(providerId: Provider): string | null { return "https://api.openai.com"; case "google-vertex": return "https://us-central1-aiplatform.googleapis.com"; + case "quartz": + return null; default: return null; } @@ -233,7 +239,7 @@ function getVideoProviderHeaders( job: VideoJobRecord, providerContext: ResolvedVideoProviderContext, ): Record { - if (job.usedProvider === "google-vertex") { + if (isVertexCompatibleVideoProvider(job.usedProvider)) { return {}; } @@ -1742,7 +1748,7 @@ async function fetchUpstreamStatus( : await fetchAvalancheStatus(job, providerContext); } - if (job.usedProvider === "google-vertex") { + if (isVertexCompatibleVideoProvider(job.usedProvider)) { return await fetchGoogleVertexStatus(job, providerContext); } @@ -1778,7 +1784,7 @@ async function fetchUpstreamContentMetadata( ): Promise | null> { if ( job.usedProvider === "avalanche" || - job.usedProvider === "google-vertex" + isVertexCompatibleVideoProvider(job.usedProvider) ) { return null; } diff --git a/packages/actions/src/get-provider-endpoint.ts b/packages/actions/src/get-provider-endpoint.ts index c01962fefd..c8466d0741 100644 --- a/packages/actions/src/get-provider-endpoint.ts +++ b/packages/actions/src/get-provider-endpoint.ts @@ -10,6 +10,55 @@ import { import type { ProviderKeyOptions } from "@llmgateway/db"; +function buildVertexCompatibleEndpoint( + provider: "google-vertex" | "quartz", + url: string, + modelName: string | undefined, + token: string | undefined, + stream: boolean | undefined, + configIndex: number | undefined, +): string { + const endpoint = stream ? "streamGenerateContent" : "generateContent"; + const model = modelName ?? "gemini-2.5-flash-lite"; + + if (model === "gemini-2.0-flash-lite" || model === "gemini-2.5-flash-lite") { + const baseEndpoint = `${url}/v1/publishers/google/models/${model}:${endpoint}`; + const queryParams = []; + if (token) { + queryParams.push(`key=${token}`); + } + if (stream) { + queryParams.push("alt=sse"); + } + return queryParams.length > 0 + ? `${baseEndpoint}?${queryParams.join("&")}` + : baseEndpoint; + } + + const projectId = getProviderEnvValue(provider, "project", configIndex); + const region = + getProviderEnvValue(provider, "region", configIndex, "global") ?? "global"; + + if (!projectId) { + const providerEnv = getProviderEnvConfig(provider); + throw new Error( + `${providerEnv?.required.project ?? "LLM_GOOGLE_CLOUD_PROJECT"} environment variable is required for Vertex-compatible model "${model}"`, + ); + } + + const baseEndpoint = `${url}/v1/projects/${projectId}/locations/${region}/publishers/google/models/${model}:${endpoint}`; + const queryParams = []; + if (token) { + queryParams.push(`key=${token}`); + } + if (stream) { + queryParams.push("alt=sse"); + } + return queryParams.length > 0 + ? `${baseEndpoint}?${queryParams.join("&")}` + : baseEndpoint; +} + /** * Get the endpoint URL for a provider API call */ @@ -77,6 +126,14 @@ export function getProviderEndpoint( case "google-vertex": url = "https://aiplatform.googleapis.com"; break; + case "quartz": + url = getProviderEnvValue("quartz", "baseUrl", configIndex); + if (!url) { + throw new Error( + "Quartz provider requires LLM_QUARTZ_BASE_URL environment variable", + ); + } + break; case "obsidian": url = getProviderEnvValue("obsidian", "baseUrl", configIndex); if (!url) { @@ -217,53 +274,16 @@ export function getProviderEndpoint( ? `${baseEndpoint}?${queryParams.join("&")}` : baseEndpoint; } - case "google-vertex": { - const endpoint = stream ? "streamGenerateContent" : "generateContent"; - const model = modelName ?? "gemini-2.5-flash-lite"; - - // Special handling for some models which require a non-global location - let baseEndpoint: string; - if ( - model === "gemini-2.0-flash-lite" || - model === "gemini-2.5-flash-lite" - ) { - baseEndpoint = `${url}/v1/publishers/google/models/${model}:${endpoint}`; - } else { - const projectId = getProviderEnvValue( - "google-vertex", - "project", - configIndex, - ); - - const region = - getProviderEnvValue( - "google-vertex", - "region", - configIndex, - "global", - ) ?? "global"; - - if (!projectId) { - const vertexEnv = getProviderEnvConfig("google-vertex"); - throw new Error( - `${vertexEnv?.required.project ?? "LLM_GOOGLE_CLOUD_PROJECT"} environment variable is required for Vertex model "${model}"`, - ); - } - - baseEndpoint = `${url}/v1/projects/${projectId}/locations/${region}/publishers/google/models/${model}:${endpoint}`; - } - - const queryParams = []; - if (token) { - queryParams.push(`key=${token}`); - } - if (stream) { - queryParams.push("alt=sse"); - } - return queryParams.length > 0 - ? `${baseEndpoint}?${queryParams.join("&")}` - : baseEndpoint; - } + case "google-vertex": + case "quartz": + return buildVertexCompatibleEndpoint( + provider, + url, + modelName, + token, + stream, + configIndex, + ); case "perplexity": return `${url}/chat/completions`; case "novita": diff --git a/packages/actions/src/get-provider-headers.ts b/packages/actions/src/get-provider-headers.ts index b4fc752bab..456b731f53 100644 --- a/packages/actions/src/get-provider-headers.ts +++ b/packages/actions/src/get-provider-headers.ts @@ -29,6 +29,7 @@ export function getProviderHeaders( } case "google-ai-studio": case "google-vertex": + case "quartz": return {}; case "obsidian": case "avalanche": diff --git a/packages/actions/src/models.spec.ts b/packages/actions/src/models.spec.ts index a79edf6b8a..f4faa520b9 100644 --- a/packages/actions/src/models.spec.ts +++ b/packages/actions/src/models.spec.ts @@ -430,11 +430,11 @@ describe("getCheapestModelForProvider", () => { ) { // Calculate expected prices const regularPrice = - (regularProvider.inputPrice + (regularProvider.outputPrice || 0)) / + (regularProvider.inputPrice + (regularProvider.outputPrice ?? 0)) / 2; const discountPrice = ((discountProvider.inputPrice + - (discountProvider.outputPrice || 0)) / + (discountProvider.outputPrice ?? 0)) / 2) * (discountProvider as ProviderModelMapping).discount!; diff --git a/packages/actions/src/prepare-request-body.ts b/packages/actions/src/prepare-request-body.ts index e4c74824c2..08ee36e262 100644 --- a/packages/actions/src/prepare-request-body.ts +++ b/packages/actions/src/prepare-request-body.ts @@ -1592,6 +1592,7 @@ export async function prepareRequestBody( } case "google-ai-studio": case "google-vertex": + case "quartz": case "obsidian": { delete requestBody.model; // Not used in body delete requestBody.stream; // Stream is handled via URL parameter diff --git a/packages/actions/src/validate-provider-key.ts b/packages/actions/src/validate-provider-key.ts index d083cf4939..cea06bca0b 100644 --- a/packages/actions/src/validate-provider-key.ts +++ b/packages/actions/src/validate-provider-key.ts @@ -191,7 +191,9 @@ export async function validateProviderKey( provider, baseUrl, effectiveModelId, // Pass model ID for providers that need it in the URL (e.g., aws-bedrock, azure) - provider === "google-ai-studio" || provider === "google-vertex" + provider === "google-ai-studio" || + provider === "google-vertex" || + provider === "quartz" ? token : undefined, false, // validation doesn't need streaming diff --git a/packages/models/src/models.ts b/packages/models/src/models.ts index e9cbd1497b..afab1ee5b3 100644 --- a/packages/models/src/models.ts +++ b/packages/models/src/models.ts @@ -360,7 +360,30 @@ export interface ModelDefinition { releasedAt?: Date; } -export const models = [ +function addQuartzProviderMappings( + modelDefinitions: readonly ModelDefinition[], +): ModelDefinition[] { + return modelDefinitions.map((model) => { + if ( + !model.providers.some( + (provider) => provider.providerId === "google-vertex", + ) + ) { + return model; + } + + return { + ...model, + providers: model.providers.flatMap((provider) => + provider.providerId === "google-vertex" + ? [provider, { ...provider, providerId: "quartz" }] + : [provider], + ), + }; + }); +} + +export const models = addQuartzProviderMappings([ ...llmgatewayModels, ...openaiModels, ...anthropicModels, @@ -377,4 +400,4 @@ export const models = [ ...bytedanceModels, ...nousresearchModels, ...zaiModels, -] as const satisfies ModelDefinition[]; +] as const satisfies ModelDefinition[]); diff --git a/packages/models/src/providers.ts b/packages/models/src/providers.ts index 172b6bf93a..6504961895 100644 --- a/packages/models/src/providers.ts +++ b/packages/models/src/providers.ts @@ -134,6 +134,27 @@ export const providers = [ website: "https://cloud.google.com/vertex-ai", announcement: null, }, + { + id: "quartz", + name: "Quartz", + description: + "Quartz is a Vertex-compatible provider for accessing Gemini and other Vertex-routed models.", + env: { + required: { + apiKey: "LLM_QUARTZ_API_KEY", + baseUrl: "LLM_QUARTZ_BASE_URL", + project: "LLM_QUARTZ_PROJECT", + }, + optional: { + region: "LLM_QUARTZ_REGION", + }, + }, + streaming: true, + cancellation: true, + color: "#4285f4", + website: null, + announcement: null, + }, { id: "obsidian", name: "Obsidian", diff --git a/packages/shared/src/components/provider-icons.tsx b/packages/shared/src/components/provider-icons.tsx index 4acaeb52a3..9cc9ef551e 100644 --- a/packages/shared/src/components/provider-icons.tsx +++ b/packages/shared/src/components/provider-icons.tsx @@ -1246,6 +1246,7 @@ export const ProviderIcons = { deepseek: DeepseekIcon, "google-ai-studio": GoogleStudioAIIcon, "google-vertex": GoogleVertexIcon, + quartz: GoogleVertexIcon, groq: GroqIcon, "inference.net": InferenceNetIcon, mistral: MistralIcon, @@ -1278,6 +1279,7 @@ export const providerLogoUrls: Partial< bytedance: ProviderIcons.bytedance, "google-ai-studio": ProviderIcons["google-ai-studio"], "google-vertex": ProviderIcons["google-vertex"], + quartz: ProviderIcons.quartz, "inference.net": ProviderIcons["inference.net"], "together.ai": ProviderIcons["together.ai"], mistral: ProviderIcons.mistral, From cbfa5bd1154930f467028c09b6b897676d727900 Mon Sep 17 00:00:00 2001 From: Luca Steeb Date: Thu, 26 Mar 2026 17:46:58 +0700 Subject: [PATCH 30/55] fix(models): restrict quartz support --- apps/gateway/src/models/models.spec.ts | 48 +++++++++++++------ apps/gateway/src/videos/videos.ts | 48 +++++++++---------- .../playground/video-page-client.tsx | 4 +- apps/playground/src/lib/video-gen.ts | 9 +--- apps/worker/src/services/video-jobs.ts | 12 ++--- packages/models/src/models.ts | 6 +++ 6 files changed, 70 insertions(+), 57 deletions(-) diff --git a/apps/gateway/src/models/models.spec.ts b/apps/gateway/src/models/models.spec.ts index 5cb212f4c0..7be91f63ef 100644 --- a/apps/gateway/src/models/models.spec.ts +++ b/apps/gateway/src/models/models.spec.ts @@ -220,9 +220,6 @@ describe("Models API", () => { const googleVertexProvider = videoModel.providers.find( (provider: any) => provider.providerId === "google-vertex", ); - const quartzProvider = videoModel.providers.find( - (provider: any) => provider.providerId === "quartz", - ); expect(obsidianProvider?.pricing.per_second).toBeDefined(); expect(obsidianProvider?.supportedVideoSizes).toEqual([ "1280x720", @@ -241,17 +238,6 @@ describe("Models API", () => { ]); expect(googleVertexProvider?.supportsVideoAudio).toBe(true); expect(googleVertexProvider?.supportsVideoWithoutAudio).toBe(true); - expect(quartzProvider?.pricing.per_second).toBeDefined(); - expect(quartzProvider?.supportedVideoSizes).toEqual([ - "1280x720", - "720x1280", - "1920x1080", - "1080x1920", - "3840x2160", - "2160x3840", - ]); - expect(quartzProvider?.supportsVideoAudio).toBe(true); - expect(quartzProvider?.supportsVideoWithoutAudio).toBe(true); expect(avalancheProvider?.pricing.per_second).toBeDefined(); expect(avalancheProvider?.supportedVideoSizes).toEqual([ "1920x1080", @@ -264,6 +250,40 @@ describe("Models API", () => { } }); + test("GET /v1/models should only expose quartz for supported image preview models", async () => { + const res = await app.request("/v1/models?include_deactivated=true"); + expect(res.status).toBe(200); + + const json = await res.json(); + + for (const modelId of [ + "gemini-3.1-flash-image-preview", + "gemini-3-pro-image-preview", + ]) { + const model = json.data.find((item: any) => item.id === modelId); + expect(model).toBeDefined(); + expect( + model.providers.some( + (provider: any) => provider.providerId === "quartz", + ), + ).toBe(true); + } + + for (const modelId of [ + "gemini-2.5-flash-image-preview", + "veo-3.1-generate-preview", + "veo-3.1-fast-generate-preview", + ]) { + const model = json.data.find((item: any) => item.id === modelId); + expect(model).toBeDefined(); + expect( + model.providers.some( + (provider: any) => provider.providerId === "quartz", + ), + ).toBe(false); + } + }); + test("GET /v1/models should include stability information for models", async () => { const res = await app.request("/v1/models?include_deactivated=true"); expect(res.status).toBe(200); diff --git a/apps/gateway/src/videos/videos.ts b/apps/gateway/src/videos/videos.ts index fd3780f336..8eac95a631 100644 --- a/apps/gateway/src/videos/videos.ts +++ b/apps/gateway/src/videos/videos.ts @@ -748,7 +748,7 @@ function getVideoModel(model: string): { throw new HTTPException(400, { message: - "Unsupported video model. Use a video-capable model from /v1/models, optionally prefixed with a configured provider like openai/, avalanche/, obsidian/, google-vertex/, or quartz/.", + "Unsupported video model. Use a video-capable model from /v1/models, optionally prefixed with a configured provider like openai/, avalanche/, obsidian/, or google-vertex/.", }); } @@ -761,8 +761,8 @@ function isSoraVideoModelName(modelName: string): boolean { return modelName === "sora-2" || modelName === "sora-2-pro"; } -function isVertexCompatibleVideoProvider(providerId: string): boolean { - return providerId === "google-vertex" || providerId === "quartz"; +function isGoogleVertexVideoProvider(providerId: string): boolean { + return providerId === "google-vertex"; } function getVideoProviderConstraintReasons( @@ -834,12 +834,12 @@ function getVideoProviderConstraintReasons( if ( !isSoraVideoModelName(provider.modelName) && inputMode === "frames" && - !isVertexCompatibleVideoProvider(provider.providerId) && + !isGoogleVertexVideoProvider(provider.providerId) && provider.providerId !== "avalanche" && provider.providerId !== "obsidian" ) { reasons.push( - "frame inputs are currently only supported through obsidian, google-vertex, quartz, or avalanche", + "frame inputs are currently only supported through obsidian, google-vertex, or avalanche", ); } @@ -854,7 +854,7 @@ function getVideoProviderConstraintReasons( return reasons; } - if (isVertexCompatibleVideoProvider(provider.providerId)) { + if (isGoogleVertexVideoProvider(provider.providerId)) { if (provider.modelName !== "veo-3.1-generate-preview") { reasons.push( `reference images are currently only supported on ${provider.providerId}/veo-3.1-generate-preview`, @@ -874,7 +874,7 @@ function getVideoProviderConstraintReasons( } } else { reasons.push( - "reference images are currently only supported through obsidian, google-vertex, quartz, or avalanche", + "reference images are currently only supported through obsidian, google-vertex, or avalanche", ); } @@ -1022,7 +1022,6 @@ function getVideoUpstreamModelName( case "avalanche": return getAvalancheVideoModelName(baseModelName); case "google-vertex": - case "quartz": return baseModelName; default: return baseModelName; @@ -1073,8 +1072,6 @@ function getDefaultVideoProviderBaseUrl(providerId: Provider): string | null { return "https://api.openai.com"; case "google-vertex": return "https://us-central1-aiplatform.googleapis.com"; - case "quartz": - return null; default: return null; } @@ -1113,10 +1110,10 @@ async function resolveProviderContext( baseModelName?: string, ): Promise { const defaultBaseUrl = getDefaultVideoProviderBaseUrl(providerId); - const sharedVertexProjectId = isVertexCompatibleVideoProvider(providerId) + const sharedVertexProjectId = isGoogleVertexVideoProvider(providerId) ? getProviderEnvValue(providerId, "project") : undefined; - const sharedVertexRegion = isVertexCompatibleVideoProvider(providerId) + const sharedVertexRegion = isGoogleVertexVideoProvider(providerId) ? (getProviderEnvValue(providerId, "region", undefined, "us-central1") ?? "us-central1") : undefined; @@ -1143,7 +1140,7 @@ async function resolveProviderContext( }); } - if (isVertexCompatibleVideoProvider(providerId) && !sharedVertexProjectId) { + if (isGoogleVertexVideoProvider(providerId) && !sharedVertexProjectId) { throw new HTTPException(500, { message: `${providerId} project environment variable is required for video generation`, }); @@ -1181,10 +1178,10 @@ async function resolveProviderContext( }); } - const vertexProjectId = isVertexCompatibleVideoProvider(providerId) + const vertexProjectId = isGoogleVertexVideoProvider(providerId) ? getProviderEnvValue(providerId, "project", env.configIndex) : undefined; - const vertexRegion = isVertexCompatibleVideoProvider(providerId) + const vertexRegion = isGoogleVertexVideoProvider(providerId) ? (getProviderEnvValue( providerId, "region", @@ -1193,7 +1190,7 @@ async function resolveProviderContext( ) ?? "us-central1") : undefined; - if (isVertexCompatibleVideoProvider(providerId) && !vertexProjectId) { + if (isGoogleVertexVideoProvider(providerId) && !vertexProjectId) { throw new HTTPException(500, { message: `${providerId} project environment variable is required for video generation`, }); @@ -1240,7 +1237,7 @@ async function resolveProviderContext( }); } - if (isVertexCompatibleVideoProvider(providerId) && !sharedVertexProjectId) { + if (isGoogleVertexVideoProvider(providerId) && !sharedVertexProjectId) { throw new HTTPException(500, { message: `${providerId} project environment variable is required for video generation`, }); @@ -1283,10 +1280,10 @@ async function resolveProviderContext( }); } - const vertexProjectId = isVertexCompatibleVideoProvider(providerId) + const vertexProjectId = isGoogleVertexVideoProvider(providerId) ? getProviderEnvValue(providerId, "project", env.configIndex) : undefined; - const vertexRegion = isVertexCompatibleVideoProvider(providerId) + const vertexRegion = isGoogleVertexVideoProvider(providerId) ? (getProviderEnvValue( providerId, "region", @@ -1295,7 +1292,7 @@ async function resolveProviderContext( ) ?? "us-central1") : undefined; - if (isVertexCompatibleVideoProvider(providerId) && !vertexProjectId) { + if (isGoogleVertexVideoProvider(providerId) && !vertexProjectId) { throw new HTTPException(500, { message: `${providerId} project environment variable is required for video generation`, }); @@ -1336,7 +1333,7 @@ async function hasVideoProviderConfiguration( (providerKey.baseUrl ?? getProviderEnvValue(providerId, "baseUrl") ?? defaultBaseUrl) && - (!isVertexCompatibleVideoProvider(providerId) || + (!isGoogleVertexVideoProvider(providerId) || Boolean(getProviderEnvValue(providerId, "project"))), ); } @@ -1350,7 +1347,7 @@ async function hasVideoProviderConfiguration( getProviderEnv(providerId).configIndex, ) ?? defaultBaseUrl) && - (!isVertexCompatibleVideoProvider(providerId) || + (!isGoogleVertexVideoProvider(providerId) || Boolean( getProviderEnvValue( providerId, @@ -1367,7 +1364,7 @@ async function hasVideoProviderConfiguration( (providerKey.baseUrl ?? getProviderEnvValue(providerId, "baseUrl") ?? defaultBaseUrl) && - (!isVertexCompatibleVideoProvider(providerId) || + (!isGoogleVertexVideoProvider(providerId) || Boolean(getProviderEnvValue(providerId, "project"))), ); } @@ -1380,7 +1377,7 @@ async function hasVideoProviderConfiguration( getProviderEnv(providerId).configIndex, ) ?? defaultBaseUrl) && - (!isVertexCompatibleVideoProvider(providerId) || + (!isGoogleVertexVideoProvider(providerId) || Boolean( getProviderEnvValue( providerId, @@ -2993,7 +2990,6 @@ async function createUpstreamVideoJob( referenceImageInputs, ); case "google-vertex": - case "quartz": return await createGoogleVertexVideoJob( providerContext, providerMapping, @@ -3322,7 +3318,7 @@ videos.openapi(createVideo, async (c) => { } if ( - isVertexCompatibleVideoProvider(selectedProviderContext.providerId) && + isGoogleVertexVideoProvider(selectedProviderContext.providerId) && !getGoogleVertexVideoOutputBucket() && organization.retentionLevel === "none" ) { diff --git a/apps/playground/src/components/playground/video-page-client.tsx b/apps/playground/src/components/playground/video-page-client.tsx index 2eac7e587e..3ca2d78e2b 100644 --- a/apps/playground/src/components/playground/video-page-client.tsx +++ b/apps/playground/src/components/playground/video-page-client.tsx @@ -115,9 +115,7 @@ export default function VideoPageClient({ () => selectedModels.some( (modelId) => - modelId.includes("/") && - !modelId.startsWith("google-vertex/") && - !modelId.startsWith("quartz/"), + modelId.includes("/") && !modelId.startsWith("google-vertex/"), ), [selectedModels], ); diff --git a/apps/playground/src/lib/video-gen.ts b/apps/playground/src/lib/video-gen.ts index f85101c7b3..a9ce24103c 100644 --- a/apps/playground/src/lib/video-gen.ts +++ b/apps/playground/src/lib/video-gen.ts @@ -98,7 +98,6 @@ export function supportsVideoFrameInput(modelId: string): boolean { providerId === undefined || providerId === "obsidian" || providerId === "google-vertex" || - providerId === "quartz" || providerId === "avalanche" ); } @@ -108,7 +107,7 @@ export function supportsVideoReferenceInput(modelId: string): boolean { ? modelId.split("/", 2) : [undefined, modelId]; - if (providerId === "google-vertex" || providerId === "quartz") { + if (providerId === "google-vertex") { return rootModelId === "veo-3.1-generate-preview"; } @@ -178,7 +177,6 @@ function mappingSupportsVideoRequest( if ( inputMode === "frames" && mapping.providerId !== "google-vertex" && - mapping.providerId !== "quartz" && mapping.providerId !== "avalanche" && mapping.providerId !== "obsidian" ) { @@ -186,10 +184,7 @@ function mappingSupportsVideoRequest( } if (inputMode === "reference") { - if ( - mapping.providerId === "google-vertex" || - mapping.providerId === "quartz" - ) { + if (mapping.providerId === "google-vertex") { if (mapping.modelName !== "veo-3.1-generate-preview") { return false; } diff --git a/apps/worker/src/services/video-jobs.ts b/apps/worker/src/services/video-jobs.ts index 5ca1a035a1..a37226c011 100644 --- a/apps/worker/src/services/video-jobs.ts +++ b/apps/worker/src/services/video-jobs.ts @@ -68,8 +68,8 @@ interface ResolvedVideoProviderContext { token: string; } -function isVertexCompatibleVideoProvider(providerId: string): boolean { - return providerId === "google-vertex" || providerId === "quartz"; +function isGoogleVertexVideoProvider(providerId: string): boolean { + return providerId === "google-vertex"; } function getDeterministicHash(seed: string): number { @@ -103,8 +103,6 @@ function getDefaultVideoProviderBaseUrl(providerId: Provider): string | null { return "https://api.openai.com"; case "google-vertex": return "https://us-central1-aiplatform.googleapis.com"; - case "quartz": - return null; default: return null; } @@ -239,7 +237,7 @@ function getVideoProviderHeaders( job: VideoJobRecord, providerContext: ResolvedVideoProviderContext, ): Record { - if (isVertexCompatibleVideoProvider(job.usedProvider)) { + if (isGoogleVertexVideoProvider(job.usedProvider)) { return {}; } @@ -1748,7 +1746,7 @@ async function fetchUpstreamStatus( : await fetchAvalancheStatus(job, providerContext); } - if (isVertexCompatibleVideoProvider(job.usedProvider)) { + if (isGoogleVertexVideoProvider(job.usedProvider)) { return await fetchGoogleVertexStatus(job, providerContext); } @@ -1784,7 +1782,7 @@ async function fetchUpstreamContentMetadata( ): Promise | null> { if ( job.usedProvider === "avalanche" || - isVertexCompatibleVideoProvider(job.usedProvider) + isGoogleVertexVideoProvider(job.usedProvider) ) { return null; } diff --git a/packages/models/src/models.ts b/packages/models/src/models.ts index afab1ee5b3..4fc1f3ed3b 100644 --- a/packages/models/src/models.ts +++ b/packages/models/src/models.ts @@ -360,11 +360,17 @@ export interface ModelDefinition { releasedAt?: Date; } +const quartzSupportedModelIds = new Set([ + "gemini-3.1-flash-image-preview", + "gemini-3-pro-image-preview", +]); + function addQuartzProviderMappings( modelDefinitions: readonly ModelDefinition[], ): ModelDefinition[] { return modelDefinitions.map((model) => { if ( + !quartzSupportedModelIds.has(model.id) || !model.providers.some( (provider) => provider.providerId === "google-vertex", ) From 5abe3ae07210c3b83521fa5e177ee2ffbebfef3a Mon Sep 17 00:00:00 2001 From: Luca Steeb Date: Thu, 26 Mar 2026 17:56:42 +0700 Subject: [PATCH 31/55] fix(models): add quartz pro preview --- apps/gateway/src/models/models.spec.ts | 3 ++- packages/models/src/models.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/gateway/src/models/models.spec.ts b/apps/gateway/src/models/models.spec.ts index 7be91f63ef..b79a3bee3b 100644 --- a/apps/gateway/src/models/models.spec.ts +++ b/apps/gateway/src/models/models.spec.ts @@ -250,7 +250,7 @@ describe("Models API", () => { } }); - test("GET /v1/models should only expose quartz for supported image preview models", async () => { + test("GET /v1/models should only expose quartz for supported models", async () => { const res = await app.request("/v1/models?include_deactivated=true"); expect(res.status).toBe(200); @@ -258,6 +258,7 @@ describe("Models API", () => { for (const modelId of [ "gemini-3.1-flash-image-preview", + "gemini-3.1-pro-preview", "gemini-3-pro-image-preview", ]) { const model = json.data.find((item: any) => item.id === modelId); diff --git a/packages/models/src/models.ts b/packages/models/src/models.ts index 4fc1f3ed3b..d7c5145ecd 100644 --- a/packages/models/src/models.ts +++ b/packages/models/src/models.ts @@ -362,6 +362,7 @@ export interface ModelDefinition { const quartzSupportedModelIds = new Set([ "gemini-3.1-flash-image-preview", + "gemini-3.1-pro-preview", "gemini-3-pro-image-preview", ]); From a4b32aaeb37980375b669a7653a8283bcd6baced Mon Sep 17 00:00:00 2001 From: Luca Steeb Date: Thu, 26 Mar 2026 18:00:30 +0700 Subject: [PATCH 32/55] refactor(models): inline quartz mappings --- AGENTS.md | 1 + packages/models/src/models.ts | 34 +---------- packages/models/src/models/google.ts | 86 ++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 32 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index fff0676633..1bc967d353 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -153,6 +153,7 @@ When creating a new package in `packages/`, include these config files. Copy the - Always use pnpm for package management - Use cookies for user-settings which are not saved in the database to ensure SSR works - Apply DRY principles for code reuse +- Exception: in `packages/models`, explicit duplication of model/provider mappings is acceptable and preferred over helper-based expansion. This is the only place in the repo where duplicating model definitions is OK. - No unnecessary code comments - Do not use broad try/catch in API handlers unless to check for specific errors; instead, let errors propagate and be handled by the global error handler diff --git a/packages/models/src/models.ts b/packages/models/src/models.ts index d7c5145ecd..e9cbd1497b 100644 --- a/packages/models/src/models.ts +++ b/packages/models/src/models.ts @@ -360,37 +360,7 @@ export interface ModelDefinition { releasedAt?: Date; } -const quartzSupportedModelIds = new Set([ - "gemini-3.1-flash-image-preview", - "gemini-3.1-pro-preview", - "gemini-3-pro-image-preview", -]); - -function addQuartzProviderMappings( - modelDefinitions: readonly ModelDefinition[], -): ModelDefinition[] { - return modelDefinitions.map((model) => { - if ( - !quartzSupportedModelIds.has(model.id) || - !model.providers.some( - (provider) => provider.providerId === "google-vertex", - ) - ) { - return model; - } - - return { - ...model, - providers: model.providers.flatMap((provider) => - provider.providerId === "google-vertex" - ? [provider, { ...provider, providerId: "quartz" }] - : [provider], - ), - }; - }); -} - -export const models = addQuartzProviderMappings([ +export const models = [ ...llmgatewayModels, ...openaiModels, ...anthropicModels, @@ -407,4 +377,4 @@ export const models = addQuartzProviderMappings([ ...bytedanceModels, ...nousresearchModels, ...zaiModels, -] as const satisfies ModelDefinition[]); +] as const satisfies ModelDefinition[]; diff --git a/packages/models/src/models/google.ts b/packages/models/src/models/google.ts index 74e64e1c6b..58f3adbbe0 100644 --- a/packages/models/src/models/google.ts +++ b/packages/models/src/models/google.ts @@ -695,6 +695,42 @@ export const googleModels = [ jsonOutput: true, jsonOutputSchema: true, }, + { + test: "skip", + providerId: "quartz", + modelName: "gemini-3.1-pro-preview", + inputPrice: 2 / 1e6, + outputPrice: 12 / 1e6, + cachedInputPrice: 0.2 / 1e6, + pricingTiers: [ + { + name: "Up to 200K", + upToTokens: 200000, + inputPrice: 2 / 1e6, + outputPrice: 12 / 1e6, + cachedInputPrice: 0.2 / 1e6, + }, + { + name: "Over 200K", + upToTokens: Infinity, + inputPrice: 4 / 1e6, + outputPrice: 18 / 1e6, + cachedInputPrice: 0.4 / 1e6, + }, + ], + requestPrice: 0, + contextSize: 1048576, + maxOutput: 65536, + reasoning: true, + reasoningMaxTokens: true, + streaming: true, + vision: true, + tools: true, + webSearch: true, + webSearchPrice: 0.014, + jsonOutput: true, + jsonOutputSchema: true, + }, ], }, { @@ -987,6 +1023,31 @@ export const googleModels = [ jsonOutput: true, jsonOutputSchema: true, }, + { + test: "skip", + providerId: "quartz", + modelName: "gemini-3.1-flash-image-preview", + inputPrice: 0.25 / 1e6, + outputPrice: 1.5 / 1e6, + imageInputPrice: 0.25 / 1e6, + imageInputTokensByResolution: { default: 560 }, + imageOutputPrice: 60 / 1e6, + imageOutputTokensByResolution: { + "0.5K": 747, + "1K": 1120, + "2K": 1680, + "4K": 2520, + default: 1120, + }, + requestPrice: 0, + contextSize: 65536, + maxOutput: 65536, + streaming: true, + vision: true, + tools: false, + jsonOutput: true, + jsonOutputSchema: true, + }, { test: "skip", providerId: "obsidian", @@ -1265,6 +1326,31 @@ export const googleModels = [ jsonOutput: true, jsonOutputSchema: true, }, + { + test: "skip", + providerId: "quartz", + modelName: "gemini-3-pro-image-preview", + inputPrice: 2 / 1e6, + outputPrice: 12 / 1e6, + cachedInputPrice: 0.2 / 1e6, + imageInputPrice: 2 / 1e6, + imageInputTokensByResolution: { default: 560 }, + imageOutputPrice: 120 / 1e6, + imageOutputTokensByResolution: { + "1K": 1120, + "2K": 1120, + "4K": 2000, + default: 1120, + }, + requestPrice: 0, + contextSize: 65536, + maxOutput: 32768, + streaming: true, + vision: true, + tools: false, + jsonOutput: true, + jsonOutputSchema: true, + }, { test: "skip", providerId: "obsidian", From 17e72ce8c073264e4b8af330a61db941a75cdf7b Mon Sep 17 00:00:00 2001 From: Luca Steeb Date: Thu, 26 Mar 2026 21:39:52 +0700 Subject: [PATCH 33/55] fix: disable vision for deepseek --- apps/ui/src/content/guides/autohand.md | 2 +- apps/ui/src/content/guides/openclaw.md | 2 +- packages/models/src/models/deepseek.ts | 12 +++++------- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/apps/ui/src/content/guides/autohand.md b/apps/ui/src/content/guides/autohand.md index a343cfe137..f07c1a92a0 100644 --- a/apps/ui/src/content/guides/autohand.md +++ b/apps/ui/src/content/guides/autohand.md @@ -63,7 +63,7 @@ You can use any model from the [models page](https://llmgateway.io/models). Popu | `o3` | Advanced reasoning tasks | | `gpt-5-mini` | Cost-effective, quick responses | | `gemini-2.5-flash` | Fast responses, good for high-volume | -| `deepseek-v3.1` | Open-source with vision and tools | +| `deepseek-v3.1` | Open-source with tool support | ## Autohand Features with LLM Gateway diff --git a/apps/ui/src/content/guides/openclaw.md b/apps/ui/src/content/guides/openclaw.md index a878cf5c92..61dc34b4cc 100644 --- a/apps/ui/src/content/guides/openclaw.md +++ b/apps/ui/src/content/guides/openclaw.md @@ -116,7 +116,7 @@ LLM Gateway uses root model IDs with smart routing—automatically selecting the | `gemini-2.5-flash` | Fast responses, good for high-volume | | `claude-haiku-4-5` | Cost-effective, quick responses | | `grok-3` | xAI flagship | -| `deepseek-v3.1` | Open-source with vision and tools | +| `deepseek-v3.1` | Open-source with tool support | For more details on routing behavior, see the [routing documentation](https://docs.llmgateway.io/features/routing). diff --git a/packages/models/src/models/deepseek.ts b/packages/models/src/models/deepseek.ts index 7edcf6c3e7..0850bd3029 100644 --- a/packages/models/src/models/deepseek.ts +++ b/packages/models/src/models/deepseek.ts @@ -76,8 +76,7 @@ export const deepseekModels = [ { id: "deepseek-v3.1", name: "DeepSeek V3.1", - description: - "Updated DeepSeek V3 with vision, tools, and improved performance.", + description: "Updated DeepSeek V3 with tools and improved performance.", family: "deepseek", releasedAt: new Date("2025-08-21"), providers: [ @@ -93,7 +92,7 @@ export const deepseekModels = [ contextSize: 128000, maxOutput: undefined, streaming: true, - vision: true, + vision: false, tools: true, jsonOutputSchema: false, jsonOutput: true, @@ -119,8 +118,7 @@ export const deepseekModels = [ { id: "deepseek-v3.2", name: "DeepSeek V3.2", - description: - "Latest DeepSeek V3 with vision, tools, and improved performance.", + description: "Latest DeepSeek V3 with tools and improved performance.", family: "deepseek", releasedAt: new Date("2025-09-29"), providers: [ @@ -135,7 +133,7 @@ export const deepseekModels = [ maxOutput: undefined, jsonOutput: true, streaming: true, - vision: true, + vision: false, tools: true, }, { @@ -149,7 +147,7 @@ export const deepseekModels = [ contextSize: 163800, maxOutput: undefined, streaming: true, - vision: true, + vision: false, tools: true, jsonOutputSchema: false, jsonOutput: true, From 25e9b242a9613a58285d5e8fe6d52e3058f1b763 Mon Sep 17 00:00:00 2001 From: Luca Steeb Date: Thu, 26 Mar 2026 22:13:44 +0700 Subject: [PATCH 34/55] feat: discount quartz models --- packages/models/src/models/google.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/models/src/models/google.ts b/packages/models/src/models/google.ts index 58f3adbbe0..7b6a3be68a 100644 --- a/packages/models/src/models/google.ts +++ b/packages/models/src/models/google.ts @@ -699,6 +699,7 @@ export const googleModels = [ test: "skip", providerId: "quartz", modelName: "gemini-3.1-pro-preview", + discount: 0.2, inputPrice: 2 / 1e6, outputPrice: 12 / 1e6, cachedInputPrice: 0.2 / 1e6, @@ -1027,6 +1028,7 @@ export const googleModels = [ test: "skip", providerId: "quartz", modelName: "gemini-3.1-flash-image-preview", + discount: 0.2, inputPrice: 0.25 / 1e6, outputPrice: 1.5 / 1e6, imageInputPrice: 0.25 / 1e6, @@ -1330,6 +1332,7 @@ export const googleModels = [ test: "skip", providerId: "quartz", modelName: "gemini-3-pro-image-preview", + discount: 0.2, inputPrice: 2 / 1e6, outputPrice: 12 / 1e6, cachedInputPrice: 0.2 / 1e6, From 39aa21747dc4956126052556f43d34edf5346820 Mon Sep 17 00:00:00 2001 From: Luca Steeb Date: Thu, 26 Mar 2026 22:42:51 +0700 Subject: [PATCH 35/55] feat: add azure gpt-5.4 mini/nano --- packages/models/src/models/openai.ts | 44 ++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/packages/models/src/models/openai.ts b/packages/models/src/models/openai.ts index 9ad2d69214..495acd0510 100644 --- a/packages/models/src/models/openai.ts +++ b/packages/models/src/models/openai.ts @@ -1456,6 +1456,28 @@ export const openaiModels = [ jsonOutputSchema: true, jsonOutput: true, }, + { + test: "skip", + providerId: "azure", + stability: "unstable" as const, + modelName: "gpt-5.4-mini", + inputPrice: 0.75 / 1e6, + outputPrice: 4.5 / 1e6, + cachedInputPrice: 0.075 / 1e6, + requestPrice: 0, + contextSize: 400000, + maxOutput: 128000, + streaming: true, + vision: true, + tools: true, + webSearch: true, + webSearchPrice: 0.01, + reasoning: true, + reasoningOutput: "omit", + supportsResponsesApi: true, + jsonOutputSchema: true, + jsonOutput: true, + }, ], }, { @@ -1486,6 +1508,28 @@ export const openaiModels = [ jsonOutputSchema: true, jsonOutput: true, }, + { + test: "skip", + providerId: "azure", + stability: "unstable" as const, + modelName: "gpt-5.4-nano", + inputPrice: 0.2 / 1e6, + outputPrice: 1.25 / 1e6, + cachedInputPrice: 0.02 / 1e6, + requestPrice: 0, + contextSize: 400000, + maxOutput: 128000, + streaming: true, + vision: true, + tools: true, + webSearch: true, + webSearchPrice: 0.01, + reasoning: true, + reasoningOutput: "omit", + supportsResponsesApi: true, + jsonOutputSchema: true, + jsonOutput: true, + }, ], }, { From e9820b1b8a4e3992e0d0be254518826b574c7b59 Mon Sep 17 00:00:00 2001 From: Luca Steeb Date: Thu, 26 Mar 2026 23:12:46 +0700 Subject: [PATCH 36/55] fix: clear unstable on azure gpt-5.4 --- packages/models/src/models/openai.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/models/src/models/openai.ts b/packages/models/src/models/openai.ts index 495acd0510..58ee7f3609 100644 --- a/packages/models/src/models/openai.ts +++ b/packages/models/src/models/openai.ts @@ -1459,7 +1459,6 @@ export const openaiModels = [ { test: "skip", providerId: "azure", - stability: "unstable" as const, modelName: "gpt-5.4-mini", inputPrice: 0.75 / 1e6, outputPrice: 4.5 / 1e6, @@ -1511,7 +1510,6 @@ export const openaiModels = [ { test: "skip", providerId: "azure", - stability: "unstable" as const, modelName: "gpt-5.4-nano", inputPrice: 0.2 / 1e6, outputPrice: 1.25 / 1e6, From ed726527c4d191bcb9c97df139d1b0dec69ba10d Mon Sep 17 00:00:00 2001 From: Luca Steeb Date: Thu, 26 Mar 2026 23:19:49 +0700 Subject: [PATCH 37/55] fix: track regional model usage --- .../src/services/stats-calculator.spec.ts | 120 ++++++++++++++++++ apps/worker/src/services/stats-calculator.ts | 27 ++-- apps/worker/src/services/sync-models.ts | 21 +-- 3 files changed, 150 insertions(+), 18 deletions(-) diff --git a/apps/worker/src/services/stats-calculator.spec.ts b/apps/worker/src/services/stats-calculator.spec.ts index 9ce4b01b90..81262df75e 100644 --- a/apps/worker/src/services/stats-calculator.spec.ts +++ b/apps/worker/src/services/stats-calculator.spec.ts @@ -267,6 +267,126 @@ describe("stats-calculator", () => { expect(claudeRecord?.cachedCount).toBe(0); // No cached requests for claude }); + it("should attribute region-suffixed logs to the matching regional mappings", async () => { + const previousMinuteStart = new Date("2024-01-01T12:29:00.000Z"); + + await db.insert(provider).values({ + id: "alibaba", + name: "Alibaba", + description: "Alibaba provider", + streaming: true, + cancellation: false, + color: "#ff6a00", + website: "https://www.alibabacloud.com", + status: "active", + }); + + await db.insert(model).values({ + id: "deepseek-v3.2", + name: "DeepSeek V3.2", + family: "deepseek", + status: "active", + }); + + await db.insert(modelProviderMapping).values([ + { + id: "mapping-3", + modelId: "deepseek-v3.2", + providerId: "alibaba", + modelName: "deepseek-v3.2:singapore", + region: "singapore", + status: "active", + }, + { + id: "mapping-4", + modelId: "deepseek-v3.2", + providerId: "alibaba", + modelName: "deepseek-v3.2:cn-beijing", + region: "cn-beijing", + status: "active", + }, + ]); + + await db.insert(log).values([ + { + id: "log-region-1", + requestId: "req-region-1", + organizationId: "org-1", + projectId: "proj-1", + apiKeyId: "key-1", + duration: 1200, + requestedModel: "alibaba/deepseek-v3.2:singapore", + requestedProvider: "alibaba", + usedModel: "alibaba/deepseek-v3.2:singapore", + usedProvider: "alibaba", + responseSize: 100, + hasError: false, + promptTokens: "100", + completionTokens: "80", + totalTokens: "180", + cost: 0.12, + unifiedFinishReason: "completed", + mode: "api-keys", + usedMode: "api-keys", + createdAt: new Date(previousMinuteStart.getTime() + 10000), + }, + { + id: "log-region-2", + requestId: "req-region-2", + organizationId: "org-1", + projectId: "proj-1", + apiKeyId: "key-1", + duration: 1500, + requestedModel: "alibaba/deepseek-v3.2:cn-beijing", + requestedProvider: "alibaba", + usedModel: "alibaba/deepseek-v3.2:cn-beijing", + usedProvider: "alibaba", + responseSize: 120, + hasError: false, + promptTokens: "120", + completionTokens: "90", + totalTokens: "210", + cost: 0.21, + unifiedFinishReason: "completed", + mode: "api-keys", + usedMode: "api-keys", + createdAt: new Date(previousMinuteStart.getTime() + 20000), + }, + ]); + + await calculateMinutelyHistory(); + + const deepseekHistory = (await db.select().from(modelHistory)).find( + (record) => record.modelId === "deepseek-v3.2", + ); + + expect(deepseekHistory).toBeTruthy(); + expect(deepseekHistory?.logsCount).toBe(2); + expect(deepseekHistory?.totalInputTokens).toBe(220); + expect(deepseekHistory?.totalOutputTokens).toBe(170); + expect(deepseekHistory?.totalCost).toBeCloseTo(0.33); + + const regionHistory = await db.select().from(modelProviderMappingHistory); + const singaporeHistory = regionHistory.find( + (record) => record.modelProviderMappingId === "mapping-3", + ); + const beijingHistory = regionHistory.find( + (record) => record.modelProviderMappingId === "mapping-4", + ); + + expect(singaporeHistory).toBeTruthy(); + expect(singaporeHistory?.logsCount).toBe(1); + expect(singaporeHistory?.totalInputTokens).toBe(100); + expect(singaporeHistory?.totalOutputTokens).toBe(80); + expect(singaporeHistory?.totalCost).toBeCloseTo(0.12); + + expect(beijingHistory).toBeTruthy(); + expect(beijingHistory?.logsCount).toBe(1); + expect(beijingHistory?.totalInputTokens).toBe(120); + expect(beijingHistory?.totalOutputTokens).toBe(90); + expect(beijingHistory?.totalCost).toBeCloseTo(0.21); + }); + it("should handle cached requests correctly by ignoring tokens but counting requests", async () => { const previousMinuteStart = new Date("2024-01-01T12:29:00.000Z"); diff --git a/apps/worker/src/services/stats-calculator.ts b/apps/worker/src/services/stats-calculator.ts index d6278480a0..4087ae2f25 100644 --- a/apps/worker/src/services/stats-calculator.ts +++ b/apps/worker/src/services/stats-calculator.ts @@ -20,6 +20,11 @@ const BACKFILL_DURATION_SECONDS = Number(process.env.BACKFILL_DURATION_SECONDS) || 300; const ONE_MINUTE_MS = 60 * 1000; +const usedModelWithRegionSql = sql`split_part(${log.usedModel}, '/', 2)`; +const usedBaseModelSql = sql`split_part(${usedModelWithRegionSql}, ':', 1)`; +const usedRegionSql = sql< + string | null +>`nullif(split_part(${usedModelWithRegionSql}, ':', 2), '')`; /** * Helper function to round any date to the start of its minute (00 seconds, 00 milliseconds) @@ -62,11 +67,11 @@ async function calculateModelHistoryForMinute(targetMinute: Date) { const minuteEnd = new Date(roundedTargetMinute.getTime() + ONE_MINUTE_MS); const database = db; - // Get logs from the specified minute, aggregated by model - // Note: usedModel field contains "provider/model" format, so we extract just the model part + // Get logs from the specified minute, aggregated by base model. + // Note: usedModel contains "provider/model[:region]" in logs. const modelStats = await database .select({ - modelId: sql`split_part(${log.usedModel}, '/', 2)`.as("modelId"), + modelId: usedBaseModelSql.as("modelId"), logsCount: sql`count(*)::int`.as("logsCount"), errorsCount: sql`sum(case when ${log.hasError} = true then 1 else 0 end)::int`.as( @@ -129,7 +134,7 @@ async function calculateModelHistoryForMinute(targetMinute: Date) { lt(log.createdAt, minuteEnd), ), ) - .groupBy(sql`split_part(${log.usedModel}, '/', 2)`); + .groupBy(usedBaseModelSql); // Get all active models to ensure we create entries for inactive ones too const allModels = await database @@ -238,12 +243,13 @@ async function calculateHistoryForMinute(targetMinute: Date) { const minuteEnd = new Date(roundedTargetMinute.getTime() + ONE_MINUTE_MS); const database = db; - // Get logs from the specified minute - // Note: usedModel field contains "provider/model" format, so we extract just the model part + // Get logs from the specified minute and normalize them back into the + // (base model, provider, region) tuple used by model_provider_mapping. const mappingStats = await database .select({ - modelId: sql`split_part(${log.usedModel}, '/', 2)`.as("modelId"), + modelId: usedBaseModelSql.as("modelId"), providerId: log.usedProvider, + region: usedRegionSql.as("region"), logsCount: sql`count(*)::int`.as("logsCount"), errorsCount: sql`sum(case when ${log.hasError} = true then 1 else 0 end)::int`.as( @@ -306,7 +312,7 @@ async function calculateHistoryForMinute(targetMinute: Date) { lt(log.createdAt, minuteEnd), ), ) - .groupBy(sql`split_part(${log.usedModel}, '/', 2)`, log.usedProvider); + .groupBy(usedBaseModelSql, log.usedProvider, usedRegionSql); // Get all active model-provider mappings to ensure we create entries for inactive ones too const allMappings = await database @@ -314,6 +320,7 @@ async function calculateHistoryForMinute(targetMinute: Date) { id: modelProviderMapping.id, // The mapping ID modelId: modelProviderMapping.modelId, // LLMGateway model name providerId: modelProviderMapping.providerId, + region: modelProviderMapping.region, }) .from(modelProviderMapping) .where(eq(modelProviderMapping.status, "active")); @@ -322,7 +329,7 @@ async function calculateHistoryForMinute(targetMinute: Date) { const activeMappingsMap = new Map(); for (const stat of mappingStats) { if (stat.modelId && stat.providerId) { - const key = `${stat.modelId}-${stat.providerId}`; + const key = `${stat.modelId}-${stat.providerId}-${stat.region ?? ""}`; activeMappingsMap.set(key, stat); } } @@ -337,7 +344,7 @@ async function calculateHistoryForMinute(targetMinute: Date) { } processedMappings.add(mapping.id); - const key = `${mapping.modelId}-${mapping.providerId}`; + const key = `${mapping.modelId}-${mapping.providerId}-${mapping.region ?? ""}`; const stat = activeMappingsMap.get(key); // Use actual stats if available, otherwise create zero stats diff --git a/apps/worker/src/services/sync-models.ts b/apps/worker/src/services/sync-models.ts index e90c9f4068..b8ed44ed98 100644 --- a/apps/worker/src/services/sync-models.ts +++ b/apps/worker/src/services/sync-models.ts @@ -300,6 +300,11 @@ export async function updateTimingAverages() { try { const database = db; + const usedModelWithRegionSql = sql`split_part(${log.usedModel}, '/', 2)`; + const usedBaseModelSql = sql`split_part(${usedModelWithRegionSql}, ':', 1)`; + const usedRegionSql = sql< + string | null + >`nullif(split_part(${usedModelWithRegionSql}, ':', 2), '')`; // Update provider averages const providerAverages = await database @@ -331,9 +336,7 @@ export async function updateTimingAverages() { // Update model averages const modelAverages = await database .select({ - modelId: sql`split_part(${log.usedModel}, '/', 2)`.as( - "modelId", - ), + modelId: usedBaseModelSql.as("modelId"), avgTimeToFirstToken: sql`avg(${log.timeToFirstToken})`.as( "avgTimeToFirstToken", ), @@ -344,7 +347,7 @@ export async function updateTimingAverages() { }) .from(log) .where(and(isNotNull(log.timeToFirstToken), eq(log.streamed, true))) - .groupBy(sql`split_part(${log.usedModel}, '/', 2)`); + .groupBy(usedBaseModelSql); for (const avg of modelAverages) { await database @@ -360,10 +363,9 @@ export async function updateTimingAverages() { // Update model-provider mapping averages const mappingAverages = await database .select({ - modelId: sql`split_part(${log.usedModel}, '/', 2)`.as( - "modelId", - ), + modelId: usedBaseModelSql.as("modelId"), providerId: log.usedProvider, + region: usedRegionSql.as("region"), avgTimeToFirstToken: sql`avg(${log.timeToFirstToken})`.as( "avgTimeToFirstToken", ), @@ -374,7 +376,7 @@ export async function updateTimingAverages() { }) .from(log) .where(and(isNotNull(log.timeToFirstToken), eq(log.streamed, true))) - .groupBy(sql`split_part(${log.usedModel}, '/', 2)`, log.usedProvider); + .groupBy(usedBaseModelSql, log.usedProvider, usedRegionSql); for (const avg of mappingAverages) { await database @@ -388,6 +390,9 @@ export async function updateTimingAverages() { and( eq(modelProviderMapping.modelId, avg.modelId), eq(modelProviderMapping.providerId, avg.providerId), + avg.region + ? eq(modelProviderMapping.region, avg.region) + : isNull(modelProviderMapping.region), ), ); } From 037b2dc8b9bd16e9f8d316abf0863701f7e45cfe Mon Sep 17 00:00:00 2001 From: Luca Steeb Date: Fri, 27 Mar 2026 00:21:51 +0700 Subject: [PATCH 38/55] fix: use weighted direct scores --- apps/gateway/src/chat/chat.ts | 55 +++++++++++-------- apps/gateway/src/fallback.spec.ts | 91 ++++++++++++++++++++++++++++--- 2 files changed, 115 insertions(+), 31 deletions(-) diff --git a/apps/gateway/src/chat/chat.ts b/apps/gateway/src/chat/chat.ts index 747ffb946e..23accbbc23 100644 --- a/apps/gateway/src/chat/chat.ts +++ b/apps/gateway/src/chat/chat.ts @@ -50,6 +50,7 @@ import { metricsKey, shortid, type tables, + type ProviderMetrics, } from "@llmgateway/db"; import { applyRedactions, @@ -1655,10 +1656,7 @@ chat.openapi(completions, async (c) => { // Fetch metrics for all providers (including deactivated) to include in routing metadata // This provides visibility into uptime/latency/throughput for all providers const baseModelId = (modelInfo as ModelDefinition).id; - let metricsMap: Map< - string, - { uptime?: number; averageLatency?: number; throughput?: number } - > = new Map(); + let metricsMap: Map = new Map(); if (baseModelId && usedProvider !== "custom") { const metricsCombinations = allModelProviders.map((p) => ({ @@ -1669,24 +1667,37 @@ chat.openapi(completions, async (c) => { metricsMap = await getProviderMetricsForCombinations(metricsCombinations); } - // Build provider scores for all providers (including deactivated) with default values for missing metrics - const allProviderScores = allModelProviders.map((p) => { - const metrics = metricsMap.get( - metricsKey(baseModelId, p.providerId, p.region), - ); - const price = (p.inputPrice ?? 0) + (p.outputPrice ?? 0); - const isSelected = - p.providerId === usedProvider && p.region === usedRegion; - return { - providerId: p.providerId, - region: p.region, - score: isSelected ? 1 : 0, - price, - uptime: metrics?.uptime ?? 0, - latency: metrics?.averageLatency ?? 0, - throughput: metrics?.throughput ?? 0, - }; - }); + // Even when the provider/region was chosen directly, show the real weighted + // scores for all candidates so the routing debug view stays informative. + const weightedScores = getCheapestFromAvailableProviders( + allModelProviders, + modelInfo as ModelDefinition & { + id: string; + output?: string[]; + }, + { + metricsMap, + isStreaming: stream, + }, + ); + + const allProviderScores = + weightedScores?.metadata.providerScores ?? + allModelProviders.map((p) => { + const metrics = metricsMap.get( + metricsKey(baseModelId, p.providerId, p.region), + ); + const price = (p.inputPrice ?? 0) + (p.outputPrice ?? 0); + return { + providerId: p.providerId, + region: p.region, + score: 0, + price, + uptime: metrics?.uptime ?? 0, + latency: metrics?.averageLatency ?? 0, + throughput: metrics?.throughput ?? 0, + }; + }); routingMetadata = { availableProviders: allModelProviders.map((p) => p.providerId), diff --git a/apps/gateway/src/fallback.spec.ts b/apps/gateway/src/fallback.spec.ts index 75802b34c3..bb332d11f8 100644 --- a/apps/gateway/src/fallback.spec.ts +++ b/apps/gateway/src/fallback.spec.ts @@ -162,21 +162,31 @@ describe("fallback and error status code handling", () => { modelId: string, providerId: string, routingUptime: number, + options?: { + region?: string; + routingLatency?: number; + routingThroughput?: number; + routingTotalRequests?: number; + }, ) { + const conditions = [ + eq(tables.modelProviderMapping.modelId, modelId), + eq(tables.modelProviderMapping.providerId, providerId), + ]; + + if (options?.region) { + conditions.push(eq(tables.modelProviderMapping.region, options.region)); + } + await db .update(tables.modelProviderMapping) .set({ routingUptime, - routingLatency: 100, - routingThroughput: 100, - routingTotalRequests: 100, + routingLatency: options?.routingLatency ?? 100, + routingThroughput: options?.routingThroughput ?? 100, + routingTotalRequests: options?.routingTotalRequests ?? 100, }) - .where( - and( - eq(tables.modelProviderMapping.modelId, modelId), - eq(tables.modelProviderMapping.providerId, providerId), - ), - ); + .where(and(...conditions)); } async function insertIamRules( @@ -778,6 +788,69 @@ describe("fallback and error status code handling", () => { }); describe("routing metadata in DB log entries", () => { + test("direct provider selection still records weighted regional scores", async () => { + await setupKeys("alibaba"); + await db + .update(tables.providerKey) + .set({ + options: { + alibaba_region: "singapore", + }, + }) + .where(eq(tables.providerKey.id, "provider-key-id")); + + await setRoutingMetrics("deepseek-v3.2", "alibaba", 100, { + region: "singapore", + routingLatency: 866, + routingThroughput: 1, + }); + await setRoutingMetrics("deepseek-v3.2", "alibaba", 100, { + region: "cn-beijing", + routingLatency: 1767, + routingThroughput: 0.5, + }); + + const res = await app.request("/v1/chat/completions", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer real-token", + }, + body: JSON.stringify({ + model: "alibaba/deepseek-v3.2", + messages: [{ role: "user", content: "Hello!" }], + }), + }); + + expect(res.status).toBe(200); + + const logs = await waitForLogs(1); + const singaporeScore = logs[0].routingMetadata?.providerScores?.find( + (score) => + score.providerId === "alibaba" && score.region === "singapore", + ); + const beijingScore = logs[0].routingMetadata?.providerScores?.find( + (score) => + score.providerId === "alibaba" && score.region === "cn-beijing", + ); + const defaultAlibabaScore = logs[0].routingMetadata?.providerScores?.find( + (score) => score.providerId === "alibaba" && !score.region, + ); + const positiveAlibabaScores = + logs[0].routingMetadata?.providerScores?.filter( + (score) => score.providerId === "alibaba" && score.score > 0, + ) ?? []; + + expect(logs[0].routingMetadata?.selectionReason).toBe( + "direct-provider-specified", + ); + expect(singaporeScore).toBeTruthy(); + expect(beijingScore).toBeTruthy(); + expect(defaultAlibabaScore?.score).toBeGreaterThan(0); + expect(singaporeScore?.score).not.toBe(1); + expect(positiveAlibabaScores.length).toBeGreaterThan(1); + }); + test("successful request stores routing metadata with selection reason in DB log", async () => { await setupKeys("openai"); From 76d5002fbf5f7fabc3c30ff1fffd10dbe47c5367 Mon Sep 17 00:00:00 2001 From: Luca Steeb Date: Fri, 27 Mar 2026 00:41:29 +0700 Subject: [PATCH 39/55] feat: add novita deepseek v3.2 --- packages/models/src/models/deepseek.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/models/src/models/deepseek.ts b/packages/models/src/models/deepseek.ts index 0850bd3029..da3bacea8e 100644 --- a/packages/models/src/models/deepseek.ts +++ b/packages/models/src/models/deepseek.ts @@ -152,6 +152,21 @@ export const deepseekModels = [ jsonOutputSchema: false, jsonOutput: true, }, + { + providerId: "novita", + modelName: "deepseek/deepseek-v3.2", + inputPrice: 0.269 / 1e6, + cachedInputPrice: 0.1345 / 1e6, + outputPrice: 0.4 / 1e6, + requestPrice: 0, + contextSize: 163840, + maxOutput: 65536, + reasoning: false, + streaming: true, + vision: false, + tools: true, + jsonOutput: true, + }, { providerId: "alibaba", modelName: "deepseek-v3.2", @@ -169,7 +184,7 @@ export const deepseekModels = [ requestPrice: 0, contextSize: 131072, maxOutput: 65536, - reasoning: true, + reasoning: false, streaming: true, vision: false, tools: true, From 961939d77d0b3ccc5dc658f171798ec61d16fc65 Mon Sep 17 00:00:00 2001 From: Luca Steeb Date: Fri, 27 Mar 2026 01:20:42 +0700 Subject: [PATCH 40/55] chore: hide free models by default --- packages/models/src/models/zai.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/models/src/models/zai.ts b/packages/models/src/models/zai.ts index 1d72257bd8..882bd7a820 100644 --- a/packages/models/src/models/zai.ts +++ b/packages/models/src/models/zai.ts @@ -352,6 +352,7 @@ export const zaiModels = [ description: "Free, fast GLM-4.5 model.", family: "glm", free: true, + stability: "unstable", releasedAt: new Date("2025-08-13"), providers: [ { @@ -580,6 +581,7 @@ export const zaiModels = [ description: "Free, lightweight GLM-4.7 model.", family: "glm", free: true, + stability: "unstable", releasedAt: new Date("2025-12-22"), providers: [ { @@ -835,6 +837,7 @@ export const zaiModels = [ "Ultra-fast, lightweight vision-language model (9B) for low-latency workloads.", family: "glm", free: true, + stability: "unstable", releasedAt: new Date("2025-12-08"), providers: [ { From 7554d469c1f15057d43b34a4130cafc5b5c47769 Mon Sep 17 00:00:00 2001 From: Luca Steeb Date: Fri, 27 Mar 2026 01:34:25 +0700 Subject: [PATCH 41/55] fix: stop inferring free selector models --- apps/playground/src/components/model-selector.tsx | 2 +- apps/ui/src/components/shared/model-search.tsx | 5 +---- packages/shared/src/components/model-selector.tsx | 2 +- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/apps/playground/src/components/model-selector.tsx b/apps/playground/src/components/model-selector.tsx index 2c03b65963..43a761f1da 100644 --- a/apps/playground/src/components/model-selector.tsx +++ b/apps/playground/src/components/model-selector.tsx @@ -645,7 +645,7 @@ export function ModelSelector({ : 0; switch (filters.priceRange) { case "free": - return price === 0 && requestPrice === 0; + return e.model.free === true && price === 0 && requestPrice === 0; case "low": return price > 0 && price <= 0.000001; case "medium": diff --git a/apps/ui/src/components/shared/model-search.tsx b/apps/ui/src/components/shared/model-search.tsx index 0323b920f6..38ddc5c938 100644 --- a/apps/ui/src/components/shared/model-search.tsx +++ b/apps/ui/src/components/shared/model-search.tsx @@ -144,10 +144,7 @@ export function ModelSearch({ providerName: provider?.name ?? String(mapping.providerId), createdAt, free: - (model.free ?? - (mapping.inputPrice !== null && - mapping.inputPrice !== undefined && - parseFloat(mapping.inputPrice) === 0)) && + model.free === true && (mapping.requestPrice === undefined || mapping.requestPrice === null || parseFloat(mapping.requestPrice) === 0), diff --git a/packages/shared/src/components/model-selector.tsx b/packages/shared/src/components/model-selector.tsx index 11abb6a396..3018cc36a6 100644 --- a/packages/shared/src/components/model-selector.tsx +++ b/packages/shared/src/components/model-selector.tsx @@ -557,7 +557,7 @@ export function ModelSelector({ const requestPrice = e.mapping.requestPrice ?? 0; switch (filters.priceRange) { case "free": - return price === 0 && requestPrice === 0; + return e.model.free === true && price === 0 && requestPrice === 0; case "low": return price > 0 && price <= 0.000001; case "medium": From bfa7cc3d0f9579df18f1dc6f2ddf4f4cfd358402 Mon Sep 17 00:00:00 2001 From: Luca Steeb Date: Fri, 27 Mar 2026 01:46:17 +0700 Subject: [PATCH 42/55] fix: respect model instability in selectors --- .../playground/src/components/model-selector.tsx | 16 ++++++---------- .../shared/src/components/model-selector.tsx | 16 ++++++---------- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/apps/playground/src/components/model-selector.tsx b/apps/playground/src/components/model-selector.tsx index 43a761f1da..d66c54838f 100644 --- a/apps/playground/src/components/model-selector.tsx +++ b/apps/playground/src/components/model-selector.tsx @@ -146,11 +146,8 @@ function isModelUnstable( mapping: ApiModelProviderMapping, model: ApiModel, ): boolean { - const providerStability = mapping.stability; - const modelStability = model.stability; - const effectiveStability = providerStability ?? modelStability; - return ( - effectiveStability === "unstable" || effectiveStability === "experimental" + return [mapping.stability, model.stability].some( + (stability) => stability === "unstable" || stability === "experimental", ); } @@ -604,12 +601,11 @@ export function ModelSelector({ e.model.stability !== "experimental" ); } - const providerStability = e.mapping?.stability; - const modelStability = e.model.stability; - const effectiveStability = providerStability ?? modelStability; return ( - effectiveStability !== "unstable" && - effectiveStability !== "experimental" + e.mapping?.stability !== "unstable" && + e.mapping?.stability !== "experimental" && + e.model.stability !== "unstable" && + e.model.stability !== "experimental" ); }); } diff --git a/packages/shared/src/components/model-selector.tsx b/packages/shared/src/components/model-selector.tsx index 3018cc36a6..68623d76cc 100644 --- a/packages/shared/src/components/model-selector.tsx +++ b/packages/shared/src/components/model-selector.tsx @@ -134,11 +134,8 @@ function isModelUnstable( mapping: ProviderModelMapping, model: ModelDefinition, ): boolean { - const providerStability = mapping.stability; - const modelStability = model.stability; - const effectiveStability = providerStability ?? modelStability; - return ( - effectiveStability === "unstable" || effectiveStability === "experimental" + return [mapping.stability, model.stability].some( + (stability) => stability === "unstable" || stability === "experimental", ); } @@ -520,12 +517,11 @@ export function ModelSelector({ e.model.stability !== "experimental" ); } - const providerStability = e.mapping?.stability; - const modelStability = e.model.stability; - const effectiveStability = providerStability ?? modelStability; return ( - effectiveStability !== "unstable" && - effectiveStability !== "experimental" + e.mapping?.stability !== "unstable" && + e.mapping?.stability !== "experimental" && + e.model.stability !== "unstable" && + e.model.stability !== "experimental" ); }); } From 7a0d153781c654e840b5a1b3d9e59405af92d2fb Mon Sep 17 00:00:00 2001 From: Luca Steeb Date: Fri, 27 Mar 2026 02:02:30 +0700 Subject: [PATCH 43/55] fix: show canonical attempt models --- .../activity/[logId]/log-detail-client.tsx | 18 ++++++++++++++- apps/ui/src/components/dashboard/log-card.tsx | 23 ++++++++++++++----- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/apps/ui/src/app/dashboard/[orgId]/[projectId]/activity/[logId]/log-detail-client.tsx b/apps/ui/src/app/dashboard/[orgId]/[projectId]/activity/[logId]/log-detail-client.tsx index 5f682b2ec8..9636fdf02f 100644 --- a/apps/ui/src/app/dashboard/[orgId]/[projectId]/activity/[logId]/log-detail-client.tsx +++ b/apps/ui/src/app/dashboard/[orgId]/[projectId]/activity/[logId]/log-detail-client.tsx @@ -156,6 +156,22 @@ function formatDuration(ms: number) { return `${(ms / 1000).toFixed(2)}s`; } +function getCanonicalAttemptModel( + log: Partial, + attempt: { provider: string; region?: string | null }, +) { + const fallbackModel = log.usedModel ?? log.requestedModel ?? "unknown"; + const canonicalModel = fallbackModel.includes("/") + ? fallbackModel.split("/").slice(1).join("/") + : fallbackModel; + const canonicalBaseModel = canonicalModel.split(":")[0] ?? canonicalModel; + const canonicalModelWithRegion = attempt.region + ? `${canonicalBaseModel}:${attempt.region}` + : canonicalBaseModel; + + return `${attempt.provider}/${canonicalModelWithRegion}`; +} + export function LogDetailClient({ initialData, orgId, @@ -473,7 +489,7 @@ export function LogDetailClient({ ) : ( )} - {attempt.provider}/{attempt.model} + {getCanonicalAttemptModel(log, attempt)} {attempt.status_code}{" "} diff --git a/apps/ui/src/components/dashboard/log-card.tsx b/apps/ui/src/components/dashboard/log-card.tsx index f5b9f76d19..334a07cd36 100644 --- a/apps/ui/src/components/dashboard/log-card.tsx +++ b/apps/ui/src/components/dashboard/log-card.tsx @@ -60,6 +60,22 @@ export function LogCard({ setIsExpanded(!isExpanded); }; + const getCanonicalAttemptModel = (attempt: { + provider: string; + region?: string | null; + }) => { + const fallbackModel = log.usedModel ?? log.requestedModel ?? "unknown"; + const canonicalModel = fallbackModel.includes("/") + ? fallbackModel.split("/").slice(1).join("/") + : fallbackModel; + const canonicalBaseModel = canonicalModel.split(":")[0] ?? canonicalModel; + const canonicalModelWithRegion = attempt.region + ? `${canonicalBaseModel}:${attempt.region}` + : canonicalBaseModel; + + return `${attempt.provider}/${canonicalModelWithRegion}`; + }; + // Format duration in ms to a readable format const formatDuration = (ms: number) => { if (ms < 1000) { @@ -429,12 +445,7 @@ export function LogCard({ ) : ( )} - {attempt.provider}/{attempt.model} - {attempt.region && ( - - ({attempt.region}) - - )} + {getCanonicalAttemptModel(attempt)} {attempt.status_code}{" "} From 3d2bbbe2e25c85220816b2c2883daad13e29b983 Mon Sep 17 00:00:00 2001 From: Luca Steeb Date: Fri, 27 Mar 2026 02:11:58 +0700 Subject: [PATCH 44/55] fix: store canonical attempt models --- AGENTS.md | 2 ++ apps/gateway/src/chat/chat.ts | 14 +++++------ apps/gateway/src/fallback.spec.ts | 9 ++++++++ apps/gateway/src/videos/videos.spec.ts | 3 +++ apps/gateway/src/videos/videos.ts | 8 +++---- .../activity/[logId]/log-detail-client.tsx | 23 +++++-------------- apps/ui/src/components/dashboard/log-card.tsx | 23 +++++-------------- 7 files changed, 37 insertions(+), 45 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 1bc967d353..a39aaade58 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,6 +38,8 @@ When you are done writing code features or bug fixes, ALWAYS commit your changes NOTE: these commands can only be run in the root directory of the repository, not in individual app directories. +Do not run test files or suites in parallel unless the repository instructions for that exact suite explicitly require it. Some gateway and worker tests share ports, databases, and process state, so parallel test runs can produce false failures. + - `pnpm test:unit` - Run unit tests (\*.spec.ts files) - `pnpm test:e2e` - Run end-to-end tests (\*.e2e.ts files) diff --git a/apps/gateway/src/chat/chat.ts b/apps/gateway/src/chat/chat.ts index 23accbbc23..e358fac99f 100644 --- a/apps/gateway/src/chat/chat.ts +++ b/apps/gateway/src/chat/chat.ts @@ -3076,7 +3076,7 @@ chat.openapi(completions, async (c) => { if (willRetryTimeout) { routingAttempts.push({ provider: usedProvider, - model: usedModel, + model: baseModelName, ...(usedRegion && { region: usedRegion }), status_code: 0, error_type: getErrorType(0), @@ -3366,7 +3366,7 @@ chat.openapi(completions, async (c) => { if (willRetryFetch) { routingAttempts.push({ provider: usedProvider, - model: usedModel, + model: baseModelName, ...(usedRegion && { region: usedRegion }), status_code: 0, error_type: getErrorType(0), @@ -3543,7 +3543,7 @@ chat.openapi(completions, async (c) => { if (willRetryHttpError) { routingAttempts.push({ provider: usedProvider, - model: usedModel, + model: baseModelName, ...(usedRegion && { region: usedRegion }), status_code: res.status, error_type: getErrorType(res.status), @@ -3666,7 +3666,7 @@ chat.openapi(completions, async (c) => { if (res && res.ok && usedProvider) { routingAttempts.push({ provider: usedProvider, - model: usedModel, + model: baseModelName, ...(usedRegion && { region: usedRegion }), status_code: res.status, error_type: "none", @@ -5873,7 +5873,7 @@ chat.openapi(completions, async (c) => { if (willRetryFetchNonStreaming) { routingAttempts.push({ provider: usedProvider, - model: usedModel, + model: baseModelName, ...(usedRegion && { region: usedRegion }), status_code: 0, error_type: getErrorType(0), @@ -6310,7 +6310,7 @@ chat.openapi(completions, async (c) => { if (willRetryHttpNonStreaming) { routingAttempts.push({ provider: usedProvider, - model: usedModel, + model: baseModelName, ...(usedRegion && { region: usedRegion }), status_code: res.status, error_type: getErrorType(res.status), @@ -6390,7 +6390,7 @@ chat.openapi(completions, async (c) => { if (res && res.ok && usedProvider) { routingAttempts.push({ provider: usedProvider, - model: usedModel, + model: baseModelName, ...(usedRegion && { region: usedRegion }), status_code: res.status, error_type: "none", diff --git a/apps/gateway/src/fallback.spec.ts b/apps/gateway/src/fallback.spec.ts index bb332d11f8..5311e770f6 100644 --- a/apps/gateway/src/fallback.spec.ts +++ b/apps/gateway/src/fallback.spec.ts @@ -848,6 +848,15 @@ describe("fallback and error status code handling", () => { expect(beijingScore).toBeTruthy(); expect(defaultAlibabaScore?.score).toBeGreaterThan(0); expect(singaporeScore?.score).not.toBe(1); + expect(logs[0].routingMetadata?.routing).toEqual([ + expect.objectContaining({ + provider: "alibaba", + model: "deepseek-v3.2", + region: "singapore", + status_code: 200, + succeeded: true, + }), + ]); expect(positiveAlibabaScores.length).toBeGreaterThan(1); }); diff --git a/apps/gateway/src/videos/videos.spec.ts b/apps/gateway/src/videos/videos.spec.ts index 1d9a612dfa..4762b57487 100644 --- a/apps/gateway/src/videos/videos.spec.ts +++ b/apps/gateway/src/videos/videos.spec.ts @@ -475,17 +475,20 @@ describe("videos", () => { expect( videoJob?.routingMetadata?.routing?.map((attempt) => ({ provider: attempt.provider, + model: attempt.model, succeeded: attempt.succeeded, status_code: attempt.status_code, })), ).toEqual([ { provider: "google-vertex", + model: "veo-3.1-generate-preview", succeeded: false, status_code: 500, }, { provider: "avalanche", + model: "veo-3.1-generate-preview", succeeded: true, status_code: 200, }, diff --git a/apps/gateway/src/videos/videos.ts b/apps/gateway/src/videos/videos.ts index 8eac95a631..f590fb263c 100644 --- a/apps/gateway/src/videos/videos.ts +++ b/apps/gateway/src/videos/videos.ts @@ -3272,7 +3272,7 @@ videos.openapi(createVideo, async (c) => { ) { routingAttempts.push({ provider: selectedProviderContext.providerId, - model: selectedUpstreamModelName, + model: modelInfo.id, status_code: 402, error_type: "insufficient_credits", succeeded: false, @@ -3325,7 +3325,7 @@ videos.openapi(createVideo, async (c) => { const statusCode = 400; routingAttempts.push({ provider: selectedProviderContext.providerId, - model: selectedUpstreamModelName, + model: modelInfo.id, status_code: statusCode, error_type: "client_error", succeeded: false, @@ -3397,7 +3397,7 @@ videos.openapi(createVideo, async (c) => { upstreamResponse = upstreamJob.upstreamResponse; routingAttempts.push({ provider: selectedProviderContext.providerId, - model: selectedUpstreamModelName, + model: modelInfo.id, status_code: 200, error_type: "none", succeeded: true, @@ -3407,7 +3407,7 @@ videos.openapi(createVideo, async (c) => { const statusCode = error instanceof HTTPException ? error.status : 0; routingAttempts.push({ provider: selectedProviderContext.providerId, - model: selectedUpstreamModelName, + model: modelInfo.id, status_code: statusCode, error_type: getErrorType(statusCode), succeeded: false, diff --git a/apps/ui/src/app/dashboard/[orgId]/[projectId]/activity/[logId]/log-detail-client.tsx b/apps/ui/src/app/dashboard/[orgId]/[projectId]/activity/[logId]/log-detail-client.tsx index 9636fdf02f..49c06e78ae 100644 --- a/apps/ui/src/app/dashboard/[orgId]/[projectId]/activity/[logId]/log-detail-client.tsx +++ b/apps/ui/src/app/dashboard/[orgId]/[projectId]/activity/[logId]/log-detail-client.tsx @@ -156,22 +156,6 @@ function formatDuration(ms: number) { return `${(ms / 1000).toFixed(2)}s`; } -function getCanonicalAttemptModel( - log: Partial, - attempt: { provider: string; region?: string | null }, -) { - const fallbackModel = log.usedModel ?? log.requestedModel ?? "unknown"; - const canonicalModel = fallbackModel.includes("/") - ? fallbackModel.split("/").slice(1).join("/") - : fallbackModel; - const canonicalBaseModel = canonicalModel.split(":")[0] ?? canonicalModel; - const canonicalModelWithRegion = attempt.region - ? `${canonicalBaseModel}:${attempt.region}` - : canonicalBaseModel; - - return `${attempt.provider}/${canonicalModelWithRegion}`; -} - export function LogDetailClient({ initialData, orgId, @@ -489,7 +473,12 @@ export function LogDetailClient({ ) : ( )} - {getCanonicalAttemptModel(log, attempt)} + {attempt.provider}/{attempt.model} + {attempt.region && ( + + ({attempt.region}) + + )} {attempt.status_code}{" "} diff --git a/apps/ui/src/components/dashboard/log-card.tsx b/apps/ui/src/components/dashboard/log-card.tsx index 334a07cd36..f5b9f76d19 100644 --- a/apps/ui/src/components/dashboard/log-card.tsx +++ b/apps/ui/src/components/dashboard/log-card.tsx @@ -60,22 +60,6 @@ export function LogCard({ setIsExpanded(!isExpanded); }; - const getCanonicalAttemptModel = (attempt: { - provider: string; - region?: string | null; - }) => { - const fallbackModel = log.usedModel ?? log.requestedModel ?? "unknown"; - const canonicalModel = fallbackModel.includes("/") - ? fallbackModel.split("/").slice(1).join("/") - : fallbackModel; - const canonicalBaseModel = canonicalModel.split(":")[0] ?? canonicalModel; - const canonicalModelWithRegion = attempt.region - ? `${canonicalBaseModel}:${attempt.region}` - : canonicalBaseModel; - - return `${attempt.provider}/${canonicalModelWithRegion}`; - }; - // Format duration in ms to a readable format const formatDuration = (ms: number) => { if (ms < 1000) { @@ -445,7 +429,12 @@ export function LogCard({ ) : ( )} - {getCanonicalAttemptModel(attempt)} + {attempt.provider}/{attempt.model} + {attempt.region && ( + + ({attempt.region}) + + )} {attempt.status_code}{" "} From 4bd3041923fb4309b0d5348555f35ef3a9a8e09f Mon Sep 17 00:00:00 2001 From: Luca Steeb Date: Fri, 27 Mar 2026 02:20:07 +0700 Subject: [PATCH 45/55] fix: drop synthetic region roots --- apps/gateway/src/fallback.spec.ts | 9 +++++---- packages/models/src/region-helpers.ts | 4 +++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/apps/gateway/src/fallback.spec.ts b/apps/gateway/src/fallback.spec.ts index 5311e770f6..5cddf530cc 100644 --- a/apps/gateway/src/fallback.spec.ts +++ b/apps/gateway/src/fallback.spec.ts @@ -833,9 +833,6 @@ describe("fallback and error status code handling", () => { (score) => score.providerId === "alibaba" && score.region === "cn-beijing", ); - const defaultAlibabaScore = logs[0].routingMetadata?.providerScores?.find( - (score) => score.providerId === "alibaba" && !score.region, - ); const positiveAlibabaScores = logs[0].routingMetadata?.providerScores?.filter( (score) => score.providerId === "alibaba" && score.score > 0, @@ -846,7 +843,11 @@ describe("fallback and error status code handling", () => { ); expect(singaporeScore).toBeTruthy(); expect(beijingScore).toBeTruthy(); - expect(defaultAlibabaScore?.score).toBeGreaterThan(0); + expect( + logs[0].routingMetadata?.providerScores?.some( + (score) => score.providerId === "alibaba" && !score.region, + ), + ).toBe(false); expect(singaporeScore?.score).not.toBe(1); expect(logs[0].routingMetadata?.routing).toEqual([ expect.objectContaining({ diff --git a/packages/models/src/region-helpers.ts b/packages/models/src/region-helpers.ts index 9d99be7e48..338afcf814 100644 --- a/packages/models/src/region-helpers.ts +++ b/packages/models/src/region-helpers.ts @@ -21,6 +21,8 @@ export function stripRegionFromModelName( * and can override pricing and other region-specific properties. * * Mappings without `regions` are returned as-is in a single-element array. + * Mappings with `regions` do not keep a synthetic root entry because routing + * always resolves to a concrete region-specific endpoint. */ export function expandProviderRegions( mapping: ProviderModelMapping, @@ -41,7 +43,7 @@ export function expandProviderRegions( modelName: `${base.modelName}:${id}`, })); - return [base, ...regionEntries]; + return regionEntries; } /** From d03c4993c397b2b0395bb6b4bd1b74b127cebab5 Mon Sep 17 00:00:00 2001 From: Luca Steeb Date: Fri, 27 Mar 2026 02:30:25 +0700 Subject: [PATCH 46/55] chore: discount azure gpt-5.4 --- packages/models/src/models/openai.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/models/src/models/openai.ts b/packages/models/src/models/openai.ts index 58ee7f3609..39df47deed 100644 --- a/packages/models/src/models/openai.ts +++ b/packages/models/src/models/openai.ts @@ -1463,6 +1463,7 @@ export const openaiModels = [ inputPrice: 0.75 / 1e6, outputPrice: 4.5 / 1e6, cachedInputPrice: 0.075 / 1e6, + discount: 0.3, requestPrice: 0, contextSize: 400000, maxOutput: 128000, @@ -1514,6 +1515,7 @@ export const openaiModels = [ inputPrice: 0.2 / 1e6, outputPrice: 1.25 / 1e6, cachedInputPrice: 0.02 / 1e6, + discount: 0.3, requestPrice: 0, contextSize: 400000, maxOutput: 128000, From 15968a709a12230702234df7a0f76024d1edcfb3 Mon Sep 17 00:00:00 2001 From: Luca Steeb Date: Fri, 27 Mar 2026 13:51:38 +0700 Subject: [PATCH 47/55] fix: format routing prices consistently --- .../activity/[logId]/log-detail-client.tsx | 19 +++++++++++++++++-- apps/ui/src/components/dashboard/log-card.tsx | 6 +++--- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/apps/ui/src/app/dashboard/[orgId]/[projectId]/activity/[logId]/log-detail-client.tsx b/apps/ui/src/app/dashboard/[orgId]/[projectId]/activity/[logId]/log-detail-client.tsx index 49c06e78ae..305df7414b 100644 --- a/apps/ui/src/app/dashboard/[orgId]/[projectId]/activity/[logId]/log-detail-client.tsx +++ b/apps/ui/src/app/dashboard/[orgId]/[projectId]/activity/[logId]/log-detail-client.tsx @@ -418,11 +418,16 @@ export function LogDetailClient({
{log.routingMetadata.providerScores.map((score) => (
{score.providerId} + {score.region && ( + + ({score.region}) + + )} {score.failed && ( @@ -437,18 +442,28 @@ export function LogDetailClient({ )} - + {score.score.toFixed(2)} {score.uptime !== undefined && ( {score.uptime?.toFixed(0)}% up )} + {score.throughput !== undefined && ( + + {score.throughput?.toFixed(0)}t/s + + )} {score.latency !== undefined && ( {score.latency?.toFixed(0)}ms )} + {score.price !== undefined && ( + + ${score.price.toFixed(10)} + + )}
))} diff --git a/apps/ui/src/components/dashboard/log-card.tsx b/apps/ui/src/components/dashboard/log-card.tsx index f5b9f76d19..7a7f8512bd 100644 --- a/apps/ui/src/components/dashboard/log-card.tsx +++ b/apps/ui/src/components/dashboard/log-card.tsx @@ -353,7 +353,7 @@ export function LogCard({
{log.routingMetadata.providerScores.map((score) => (
@@ -377,7 +377,7 @@ export function LogCard({ )} - + {score.score.toFixed(2)} {score.uptime !== undefined && ( @@ -396,7 +396,7 @@ export function LogCard({ )} {score.price !== undefined && ( - ${score.price.toFixed(6)} + ${score.price.toFixed(10)} )} {score.priority !== undefined && From f66256f53041f8a8d79901b342fa0b8af4047c12 Mon Sep 17 00:00:00 2001 From: Luca Steeb Date: Fri, 27 Mar 2026 14:01:50 +0700 Subject: [PATCH 48/55] fix: trim direct routing scores --- apps/gateway/src/chat/chat.ts | 253 ++++++++++++++++++------------ apps/gateway/src/fallback.spec.ts | 18 +-- 2 files changed, 164 insertions(+), 107 deletions(-) diff --git a/apps/gateway/src/chat/chat.ts b/apps/gateway/src/chat/chat.ts index e358fac99f..0f1deec23f 100644 --- a/apps/gateway/src/chat/chat.ts +++ b/apps/gateway/src/chat/chat.ts @@ -32,6 +32,7 @@ import { getCheapestFromAvailableProviders, getProviderEndpoint, getProviderHeaders, + getProviderSelectionPrice, prepareRequestBody, type RoutingMetadata, } from "@llmgateway/actions"; @@ -162,6 +163,89 @@ function filterRegionsByAvailableKeys( }); } +function resolveRegionFromProviderKey( + key: InferSelectModel, +): string | undefined { + const providerDef = providers.find((p) => p.id === key.provider) as + | ProviderDefinition + | undefined; + if (!providerDef?.regionConfig) { + return undefined; + } + const regionKey = providerDef.regionConfig.optionsKey; + const explicitRegion = key.options + ? (key.options as Record)[regionKey] + : undefined; + return explicitRegion ?? providerDef.regionConfig.defaultRegion; +} + +function filterEligibleModelProviders( + availableModelProviders: ProviderModelMapping[], + options: { + allProviderVariants: ProviderModelMapping[]; + availableProviders?: string[]; + providerLockedRegions?: Map; + webSearchTool?: WebSearchTool; + responseFormatType?: string; + hasImages: boolean; + reasoningEffort?: string; + }, +): ProviderModelMapping[] { + return availableModelProviders.filter((provider) => { + if ( + options.availableProviders && + !options.availableProviders.includes(provider.providerId) + ) { + return false; + } + + const lockedRegion = options.providerLockedRegions?.get( + provider.providerId, + ); + if (lockedRegion && provider.region && provider.region !== lockedRegion) { + return false; + } + + if (options.webSearchTool && provider.webSearch !== true) { + return false; + } + + if ( + options.responseFormatType === "json_object" || + options.responseFormatType === "json_schema" + ) { + if (provider.jsonOutput !== true) { + return false; + } + } + + if ( + options.responseFormatType === "json_schema" && + provider.jsonOutputSchema !== true + ) { + return false; + } + + if (options.hasImages && provider.vision !== true) { + return false; + } + + if (options.reasoningEffort !== undefined) { + return provider.reasoning === true; + } + + const hasNonReasoningAlternative = options.allProviderVariants.some( + (p) => p.providerId === provider.providerId && p.reasoning !== true, + ); + + if (hasNonReasoningAlternative && provider.reasoning === true) { + return false; + } + + return true; + }); +} + function usesGoogleQueryToken(provider: string): boolean { return ( provider === "google-ai-studio" || @@ -1512,66 +1596,17 @@ chat.openapi(completions, async (c) => { } } - // Filter model providers to only those available - // If web search is requested, also filter to providers that support it - // If JSON output is requested, also filter to providers that support it - const availableModelProviders = iamFilteredModelProviders.filter( - (provider) => { - if (!availableProviders.includes(provider.providerId)) { - return false; - } - // If the user has a DB key with a locked region, only allow that region - const lockedRegion = providerLockedRegions.get(provider.providerId); - if ( - lockedRegion && - provider.region && - provider.region !== lockedRegion - ) { - return false; - } - // If web search tool is requested, only include providers that support it - if (webSearchTool) { - if (provider.webSearch !== true) { - return false; - } - } - // If JSON output is requested, only include providers that support it - if ( - response_format?.type === "json_object" || - response_format?.type === "json_schema" - ) { - if (provider.jsonOutput !== true) { - return false; - } - } - // If JSON schema output is requested, also include providers that support it - if (response_format?.type === "json_schema") { - if (provider.jsonOutputSchema !== true) { - return false; - } - } - // If images are present in messages, only include providers that support vision - if (hasImages && provider.vision !== true) { - return false; - } - // If reasoning_effort is specified, only include providers with reasoning support - if (reasoning_effort !== undefined) { - if (provider.reasoning !== true) { - return false; - } - } - // If reasoning_effort is NOT specified, prefer non-reasoning providers - // by excluding reasoning providers when a non-reasoning alternative exists for same provider - if (reasoning_effort === undefined) { - const hasNonReasoningAlternative = modelInfo.providers.some( - (p) => - p.providerId === provider.providerId && p.reasoning !== true, - ); - if (hasNonReasoningAlternative && provider.reasoning === true) { - return false; - } - } - return true; + // Filter model providers to only those eligible for this request + const availableModelProviders = filterEligibleModelProviders( + iamFilteredModelProviders, + { + allProviderVariants: modelInfo.providers, + availableProviders, + providerLockedRegions, + webSearchTool, + responseFormatType: response_format?.type, + hasImages, + reasoningEffort: reasoning_effort, }, ); @@ -1653,13 +1688,52 @@ chat.openapi(completions, async (c) => { selectionReason = "fallback-first-available"; } - // Fetch metrics for all providers (including deactivated) to include in routing metadata - // This provides visibility into uptime/latency/throughput for all providers + let routingMetadataProviders = allModelProviders; + + if ( + selectionReason === "direct-provider-specified" && + requestedProvider && + requestedProvider !== "custom" + ) { + let directRegion = usedRegion; + if ( + !directRegion && + (project.mode === "api-keys" || project.mode === "hybrid") + ) { + const providerKey = await findProviderKey( + project.organizationId, + requestedProvider, + requestId, + ); + directRegion = providerKey + ? resolveRegionFromProviderKey(providerKey) + : undefined; + } + + const selectedDirectProvider = + allModelProviders.find( + (provider) => + provider.providerId === requestedProvider && + provider.modelName === usedModel && + provider.region === directRegion, + ) ?? + allModelProviders.find( + (provider) => + provider.providerId === requestedProvider && + provider.modelName === usedModel, + ); + + routingMetadataProviders = selectedDirectProvider + ? [selectedDirectProvider] + : []; + } + + // Fetch metrics for all eligible providers to include in routing metadata const baseModelId = (modelInfo as ModelDefinition).id; let metricsMap: Map = new Map(); if (baseModelId && usedProvider !== "custom") { - const metricsCombinations = allModelProviders.map((p) => ({ + const metricsCombinations = routingMetadataProviders.map((p) => ({ modelId: baseModelId, providerId: p.providerId, region: p.region, @@ -1667,32 +1741,32 @@ chat.openapi(completions, async (c) => { metricsMap = await getProviderMetricsForCombinations(metricsCombinations); } - // Even when the provider/region was chosen directly, show the real weighted - // scores for all candidates so the routing debug view stays informative. - const weightedScores = getCheapestFromAvailableProviders( - allModelProviders, - modelInfo as ModelDefinition & { - id: string; - output?: string[]; - }, - { - metricsMap, - isStreaming: stream, - }, - ); + const weightedScores = + selectionReason === "direct-provider-specified" + ? null + : getCheapestFromAvailableProviders( + routingMetadataProviders, + modelInfo as ModelDefinition & { + id: string; + output?: string[]; + }, + { + metricsMap, + isStreaming: stream, + }, + ); const allProviderScores = weightedScores?.metadata.providerScores ?? - allModelProviders.map((p) => { + routingMetadataProviders.map((p) => { const metrics = metricsMap.get( metricsKey(baseModelId, p.providerId, p.region), ); - const price = (p.inputPrice ?? 0) + (p.outputPrice ?? 0); return { providerId: p.providerId, region: p.region, - score: 0, - price, + score: selectionReason === "direct-provider-specified" ? 1 : 0, + price: getProviderSelectionPrice(p), uptime: metrics?.uptime ?? 0, latency: metrics?.averageLatency ?? 0, throughput: metrics?.throughput ?? 0, @@ -1700,7 +1774,7 @@ chat.openapi(completions, async (c) => { }); routingMetadata = { - availableProviders: allModelProviders.map((p) => p.providerId), + availableProviders: routingMetadataProviders.map((p) => p.providerId), selectedProvider: usedProvider, selectionReason, providerScores: allProviderScores, @@ -1816,23 +1890,6 @@ chat.openapi(completions, async (c) => { }); } - // Helper: resolve the locked region from a provider key's options - function resolveRegionFromProviderKey( - key: InferSelectModel, - ): string | undefined { - const providerDef = providers.find((p) => p.id === key.provider) as - | ProviderDefinition - | undefined; - if (!providerDef?.regionConfig) { - return undefined; - } - const regionKey = providerDef.regionConfig.optionsKey; - const explicitRegion = key.options - ? (key.options as Record)[regionKey] - : undefined; - return explicitRegion ?? providerDef.regionConfig.defaultRegion; - } - if (project.mode === "api-keys") { // Get the provider key from the database using cached helper function if (usedProvider === "custom" && customProviderName) { diff --git a/apps/gateway/src/fallback.spec.ts b/apps/gateway/src/fallback.spec.ts index 5cddf530cc..8680811c69 100644 --- a/apps/gateway/src/fallback.spec.ts +++ b/apps/gateway/src/fallback.spec.ts @@ -788,7 +788,7 @@ describe("fallback and error status code handling", () => { }); describe("routing metadata in DB log entries", () => { - test("direct provider selection still records weighted regional scores", async () => { + test("direct provider selection only records the direct region", async () => { await setupKeys("alibaba"); await db .update(tables.providerKey) @@ -833,22 +833,16 @@ describe("fallback and error status code handling", () => { (score) => score.providerId === "alibaba" && score.region === "cn-beijing", ); - const positiveAlibabaScores = - logs[0].routingMetadata?.providerScores?.filter( - (score) => score.providerId === "alibaba" && score.score > 0, - ) ?? []; - expect(logs[0].routingMetadata?.selectionReason).toBe( "direct-provider-specified", ); expect(singaporeScore).toBeTruthy(); - expect(beijingScore).toBeTruthy(); + expect(beijingScore).toBeFalsy(); expect( logs[0].routingMetadata?.providerScores?.some( (score) => score.providerId === "alibaba" && !score.region, ), ).toBe(false); - expect(singaporeScore?.score).not.toBe(1); expect(logs[0].routingMetadata?.routing).toEqual([ expect.objectContaining({ provider: "alibaba", @@ -858,7 +852,13 @@ describe("fallback and error status code handling", () => { succeeded: true, }), ]); - expect(positiveAlibabaScores.length).toBeGreaterThan(1); + expect(logs[0].routingMetadata?.providerScores).toEqual([ + expect.objectContaining({ + providerId: "alibaba", + region: "singapore", + score: 1, + }), + ]); }); test("successful request stores routing metadata with selection reason in DB log", async () => { From 044ac8be0a7079437d3fbfd4735c014f65d4a65d Mon Sep 17 00:00:00 2001 From: Luca Steeb Date: Fri, 27 Mar 2026 14:17:19 +0700 Subject: [PATCH 49/55] fix: route direct providers by region --- apps/gateway/src/chat/chat.ts | 214 +++++++++++++++++++++--------- apps/gateway/src/fallback.spec.ts | 58 ++++++++ 2 files changed, 206 insertions(+), 66 deletions(-) diff --git a/apps/gateway/src/chat/chat.ts b/apps/gateway/src/chat/chat.ts index 0f1deec23f..966f6874df 100644 --- a/apps/gateway/src/chat/chat.ts +++ b/apps/gateway/src/chat/chat.ts @@ -179,6 +179,21 @@ function resolveRegionFromProviderKey( return explicitRegion ?? providerDef.regionConfig.defaultRegion; } +function resolveExplicitRegionFromProviderKey( + key: InferSelectModel, +): string | undefined { + const providerDef = providers.find((p) => p.id === key.provider) as + | ProviderDefinition + | undefined; + if (!providerDef?.regionConfig) { + return undefined; + } + const regionKey = providerDef.regionConfig.optionsKey; + return key.options + ? (key.options as Record)[regionKey] + : undefined; +} + function filterEligibleModelProviders( availableModelProviders: ProviderModelMapping[], options: { @@ -1328,6 +1343,95 @@ chat.openapi(completions, async (c) => { usedModel = "custom"; } + // When a specific provider is requested and it has multiple mappings (for example, + // regional variants), pick the best eligible mapping up front so the request and + // any low-uptime fallback logic operate on the concrete provider-region pair. + if ( + usedProvider && + usedProvider !== "llmgateway" && + usedProvider !== "custom" + ) { + const sameProviderMappings = modelInfo.providers.filter( + (p) => p.providerId === usedProvider, + ); + + if (sameProviderMappings.length > 1) { + let lockedRegion = usedRegion; + + if ( + !lockedRegion && + (project.mode === "api-keys" || project.mode === "hybrid") + ) { + const providerKey = await findProviderKey( + project.organizationId, + usedProvider, + requestId, + ); + lockedRegion = providerKey + ? resolveExplicitRegionFromProviderKey(providerKey) + : undefined; + } + + const providerLockedRegions = lockedRegion + ? new Map([[usedProvider, lockedRegion]]) + : undefined; + const eligibleMappings = filterEligibleModelProviders( + sameProviderMappings, + { + allProviderVariants: modelInfo.providers, + providerLockedRegions, + webSearchTool, + responseFormatType: response_format?.type, + hasImages, + reasoningEffort: reasoning_effort, + }, + ); + + if (eligibleMappings.length > 0) { + let selectedMapping = eligibleMappings[0]; + + if (eligibleMappings.length > 1) { + const metricsCombinations = eligibleMappings.map((provider) => ({ + modelId: modelInfo.id, + providerId: provider.providerId, + region: provider.region, + })); + const metricsMap = + await getProviderMetricsForCombinations(metricsCombinations); + const bestRegionResult = getCheapestFromAvailableProviders( + eligibleMappings, + modelInfo as ModelDefinition & { + id: string; + output?: string[]; + }, + { + metricsMap, + isStreaming: stream, + }, + ); + + selectedMapping = bestRegionResult?.provider ?? eligibleMappings[0]; + } + + usedModel = selectedMapping.modelName; + usedRegion = selectedMapping.region; + } + } else if (sameProviderMappings.length === 1) { + usedModel = sameProviderMappings[0].modelName; + usedRegion ??= (sameProviderMappings[0] as ProviderModelMapping).region; + } + + if (!usedRegion) { + const firstRegionalMatch = sameProviderMappings.find( + (p) => (p as ProviderModelMapping).region, + ) as ProviderModelMapping | undefined; + if (firstRegionalMatch) { + usedRegion = firstRegionalMatch.region; + usedModel = firstRegionalMatch.modelName; + } + } + } + // Check uptime for specifically requested providers (not llmgateway or custom) // If uptime is below 80%, route to an alternative provider instead // Skip this fallback if X-No-Fallback header is set @@ -1506,51 +1610,6 @@ chat.openapi(completions, async (c) => { } } - // For models with multiple provider mappings for the same provider (e.g., routing models - // like grok-4-fast with reasoning and non-reasoning variants, or regional variants), - // select the correct variant based on request capabilities when a specific provider is already set - if ( - usedProvider && - usedProvider !== "llmgateway" && - usedProvider !== "custom" - ) { - const sameProviderMappings = modelInfo.providers.filter( - (p) => p.providerId === usedProvider, - ); - if (sameProviderMappings.length > 1) { - let selectedMapping: ProviderModelMapping | undefined; - if (reasoning_effort !== undefined) { - selectedMapping = sameProviderMappings.find( - (p) => (p as ProviderModelMapping).reasoning === true, - ) as ProviderModelMapping | undefined; - } else { - selectedMapping = sameProviderMappings.find( - (p) => (p as ProviderModelMapping).reasoning !== true, - ) as ProviderModelMapping | undefined; - } - if (selectedMapping) { - usedModel = selectedMapping.modelName; - usedRegion ??= selectedMapping.region; - } - } else if (sameProviderMappings.length === 1) { - // Single matching entry — use its modelName (may include :region suffix) - usedModel = sameProviderMappings[0].modelName; - usedRegion ??= (sameProviderMappings[0] as ProviderModelMapping).region; - } - // If region still unset, derive it from the first regional entry. - // This handles the case where the user selects "Alibaba Cloud" (no region) - // for a model that only exists in specific regions (e.g., cn-beijing only). - if (!usedRegion) { - const firstRegionalMatch = sameProviderMappings.find( - (p) => (p as ProviderModelMapping).region, - ) as ProviderModelMapping | undefined; - if (firstRegionalMatch) { - usedRegion = firstRegionalMatch.region; - usedModel = firstRegionalMatch.modelName; - } - } - } - if (!usedProvider) { if (iamFilteredModelProviders.length === 0) { throw new HTTPException(403, { @@ -1689,15 +1748,16 @@ chat.openapi(completions, async (c) => { } let routingMetadataProviders = allModelProviders; + let directProviderRegionWasExplicit = false; if ( selectionReason === "direct-provider-specified" && requestedProvider && requestedProvider !== "custom" ) { - let directRegion = usedRegion; + let explicitDirectRegion = requestedRegion; if ( - !directRegion && + !explicitDirectRegion && (project.mode === "api-keys" || project.mode === "hybrid") ) { const providerKey = await findProviderKey( @@ -1705,27 +1765,44 @@ chat.openapi(completions, async (c) => { requestedProvider, requestId, ); - directRegion = providerKey - ? resolveRegionFromProviderKey(providerKey) + explicitDirectRegion = providerKey + ? resolveExplicitRegionFromProviderKey(providerKey) : undefined; } - const selectedDirectProvider = - allModelProviders.find( - (provider) => - provider.providerId === requestedProvider && - provider.modelName === usedModel && - provider.region === directRegion, - ) ?? - allModelProviders.find( - (provider) => - provider.providerId === requestedProvider && - provider.modelName === usedModel, - ); + directProviderRegionWasExplicit = Boolean(explicitDirectRegion); + const providerLockedRegions = explicitDirectRegion + ? new Map([[requestedProvider, explicitDirectRegion]]) + : undefined; + routingMetadataProviders = filterEligibleModelProviders( + allModelProviders.filter( + (provider) => provider.providerId === requestedProvider, + ), + { + allProviderVariants: modelInfo.providers, + providerLockedRegions, + webSearchTool, + responseFormatType: response_format?.type, + hasImages, + reasoningEffort: reasoning_effort, + }, + ); - routingMetadataProviders = selectedDirectProvider - ? [selectedDirectProvider] - : []; + if (directProviderRegionWasExplicit) { + const selectedDirectProvider = + routingMetadataProviders.find( + (provider) => + provider.modelName === usedModel && + provider.region === usedRegion, + ) ?? + routingMetadataProviders.find( + (provider) => provider.modelName === usedModel, + ); + + routingMetadataProviders = selectedDirectProvider + ? [selectedDirectProvider] + : []; + } } // Fetch metrics for all eligible providers to include in routing metadata @@ -1742,7 +1819,8 @@ chat.openapi(completions, async (c) => { } const weightedScores = - selectionReason === "direct-provider-specified" + selectionReason === "direct-provider-specified" && + directProviderRegionWasExplicit ? null : getCheapestFromAvailableProviders( routingMetadataProviders, @@ -1765,7 +1843,11 @@ chat.openapi(completions, async (c) => { return { providerId: p.providerId, region: p.region, - score: selectionReason === "direct-provider-specified" ? 1 : 0, + score: + selectionReason === "direct-provider-specified" && + directProviderRegionWasExplicit + ? 1 + : 0, price: getProviderSelectionPrice(p), uptime: metrics?.uptime ?? 0, latency: metrics?.averageLatency ?? 0, diff --git a/apps/gateway/src/fallback.spec.ts b/apps/gateway/src/fallback.spec.ts index 8680811c69..d145390918 100644 --- a/apps/gateway/src/fallback.spec.ts +++ b/apps/gateway/src/fallback.spec.ts @@ -788,6 +788,64 @@ describe("fallback and error status code handling", () => { }); describe("routing metadata in DB log entries", () => { + test("direct provider selection picks the best available region", async () => { + await setupKeys("alibaba"); + + await setRoutingMetrics("deepseek-v3.2", "alibaba", 100, { + region: "singapore", + routingLatency: 1200, + routingThroughput: 10, + }); + await setRoutingMetrics("deepseek-v3.2", "alibaba", 100, { + region: "cn-beijing", + routingLatency: 900, + routingThroughput: 20, + }); + + const res = await app.request("/v1/chat/completions", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer real-token", + }, + body: JSON.stringify({ + model: "alibaba/deepseek-v3.2", + messages: [{ role: "user", content: "Hello!" }], + }), + }); + + expect(res.status).toBe(200); + + const logs = await waitForLogs(1); + const singaporeScore = logs[0].routingMetadata?.providerScores?.find( + (score) => + score.providerId === "alibaba" && score.region === "singapore", + ); + const beijingScore = logs[0].routingMetadata?.providerScores?.find( + (score) => + score.providerId === "alibaba" && score.region === "cn-beijing", + ); + + expect(logs[0].usedModel).toBe("alibaba/deepseek-v3.2:cn-beijing"); + expect(logs[0].routingMetadata?.selectionReason).toBe( + "direct-provider-specified", + ); + expect(singaporeScore).toBeTruthy(); + expect(beijingScore).toBeTruthy(); + expect(beijingScore?.score ?? Number.MAX_VALUE).toBeLessThan( + singaporeScore?.score ?? 0, + ); + expect(logs[0].routingMetadata?.routing).toEqual([ + expect.objectContaining({ + provider: "alibaba", + model: "deepseek-v3.2", + region: "cn-beijing", + status_code: 200, + succeeded: true, + }), + ]); + }); + test("direct provider selection only records the direct region", async () => { await setupKeys("alibaba"); await db From 29dbc9f652da0a8cc8359fe16f45400211e0ddba Mon Sep 17 00:00:00 2001 From: Luca Steeb Date: Fri, 27 Mar 2026 16:03:13 +0700 Subject: [PATCH 50/55] docs: add routing regions section --- apps/docs/content/features/routing.mdx | 41 ++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/apps/docs/content/features/routing.mdx b/apps/docs/content/features/routing.mdx index 97e30f816e..e6da8735b6 100644 --- a/apps/docs/content/features/routing.mdx +++ b/apps/docs/content/features/routing.mdx @@ -116,6 +116,47 @@ curl -X POST "https://api.llmgateway.io/v1/chat/completions" \ }' ``` +#### Regions + +Some providers expose the same model in multiple regions. In that case, LLMGateway supports two routing modes: + +- `provider/model` selects the best eligible region for that provider using the same routing inputs used elsewhere: recent uptime, throughput, latency, and price +- `provider/model:region` pins the request to one exact region + +```bash +# Let LLMGateway choose the best Alibaba region for DeepSeek V3.2 +curl -X POST "https://api.llmgateway.io/v1/chat/completions" \ + -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "alibaba/deepseek-v3.2", + "messages": [{"role": "user", "content": "Hello!"}] + }' + +# Force a specific Alibaba region +curl -X POST "https://api.llmgateway.io/v1/chat/completions" \ + -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "alibaba/deepseek-v3.2:cn-beijing", + "messages": [{"role": "user", "content": "Hello!"}] + }' +``` + +If your provider key stores an explicit region, that region acts like a lock and LLMGateway will only use that region for provider-specific requests. If no explicit region is configured on the provider key, provider-specific requests can still score all eligible regions for that provider. + +Routing metadata reflects this: + +- Dynamic provider-region selection shows all eligible regional scores that were considered +- Explicitly pinned regions show only the pinned region in the score list + + + Region-aware routing only compares regions that are actually available for the + current project mode and provider setup. In credits mode, that means only + regions backed by configured environment keys. In API keys and hybrid mode, an + explicit provider-key region restricts the request to that region. + + #### Low-Uptime Protection When you specify a provider explicitly, LLMGateway checks the provider's recent uptime (last 5 minutes). If the uptime falls below 90%, the system automatically routes your request to the best available alternative provider to ensure reliability. This protects your application from providers experiencing temporary issues. From 3b529d6c83907e06f8cf22f4e245ef78a33491d5 Mon Sep 17 00:00:00 2001 From: Luca Steeb Date: Fri, 27 Mar 2026 17:04:18 +0700 Subject: [PATCH 51/55] fix: gate chat image attachments --- .../src/components/playground/chat-ui.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/apps/playground/src/components/playground/chat-ui.tsx b/apps/playground/src/components/playground/chat-ui.tsx index 7c6108e618..8793db2613 100644 --- a/apps/playground/src/components/playground/chat-ui.tsx +++ b/apps/playground/src/components/playground/chat-ui.tsx @@ -635,9 +635,10 @@ export const ChatUI = ({ } > { void handlePromptSubmit(message.text ?? "", message.files); @@ -656,12 +657,14 @@ export const ChatUI = ({ - - - - - - + {supportsImages && ( + + + + + + + )} Date: Fri, 27 Mar 2026 17:24:42 +0700 Subject: [PATCH 52/55] fix: accept gemini edit params --- .../content/features/image-generation.mdx | 6 ++-- apps/gateway/src/api.spec.ts | 36 +++++++++++++++++++ apps/gateway/src/images/images.ts | 26 ++++++++------ 3 files changed, 55 insertions(+), 13 deletions(-) diff --git a/apps/docs/content/features/image-generation.mdx b/apps/docs/content/features/image-generation.mdx index 8c791b0c65..18276946af 100644 --- a/apps/docs/content/features/image-generation.mdx +++ b/apps/docs/content/features/image-generation.mdx @@ -122,7 +122,8 @@ The `/v1/images/edits` endpoint is OpenAI-compatible and supports a focused subs | `output_format` | enum | no | `png`, `jpeg`, or `webp` | | `output_compression` | integer | no | Compression level for `jpeg`/`webp` | | `quality` | enum | no | `low`, `medium`, `high`, or `auto` | -| `size` | enum | no | `auto`, `1024x1024`, `1536x1024`, `1024x1536` | +| `size` | string | no | Output size. Examples: `1024x1024`, `1536x1024`, `1K`, `2K`, `4K` | +| `aspect_ratio` | string | no | Aspect ratio override. Examples: `1:1`, `16:9`, `4:3`, `5:4` | `mask` is not supported yet on `/v1/images/edits`. @@ -142,8 +143,9 @@ curl -X POST "https://api.llmgateway.io/v1/images/edits" \ ], "prompt": "Add a watercolor effect to this image", "model": "gemini-3-pro-image-preview", + "aspect_ratio": "16:9", "quality": "high", - "size": "1024x1024" + "size": "4K" }' ``` diff --git a/apps/gateway/src/api.spec.ts b/apps/gateway/src/api.spec.ts index 4617576b22..69c72579a6 100644 --- a/apps/gateway/src/api.spec.ts +++ b/apps/gateway/src/api.spec.ts @@ -197,6 +197,42 @@ describe("api", () => { } }); + test("/v1/images/edits accepts Gemini size and aspect ratio", async () => { + await db.insert(tables.apiKey).values({ + id: "token-id-image-edits", + token: "real-token-image-edits", + projectId: "project-id", + description: "Test API Key", + createdBy: "user-id", + }); + + const res = await app.request("/v1/images/edits", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer real-token-image-edits", + }, + body: JSON.stringify({ + model: "invalid-image-model", + prompt: "Make it cinematic", + images: [ + { + image_url: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAJFBMVEX///////9MaXH///////////////////////////////////8ZR3RTAAAADHRSTlP+jgB78KRmvTse21aub7wnAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAc0lEQVR42l3PWRIDIQgE0G5Z1fvfN7hMKhO+5BWtgraqU933qWG1BkCg0jfkahcAyt4QQOiFKmJI+oWhezRwI0Zx1rzRZ44C7gRIMws8oKDFiT4QdHvBNMUL1LKu3KAnUu+fCWndp/98Xf6Xm1846+dZ/wNI2AJy5D7oXAAAAABJRU5ErkJggg==", + }, + ], + size: "4K", + aspect_ratio: "16:9", + }), + }); + + expect(res.status).toBe(400); + + const json = await res.json(); + expect(JSON.stringify(json)).not.toContain("Invalid enum value"); + expect(JSON.stringify(json)).not.toContain('"path":["size"]'); + }); + test("Reasoning effort error for unsupported model", async () => { await db.insert(tables.apiKey).values({ id: "token-id", diff --git a/apps/gateway/src/images/images.ts b/apps/gateway/src/images/images.ts index d6f44597bd..492ed08cc0 100644 --- a/apps/gateway/src/images/images.ts +++ b/apps/gateway/src/images/images.ts @@ -454,13 +454,16 @@ const imageEditsRequestSchema = z.object({ description: "Output quality for image models.", example: "high", }), - size: z - .enum(["auto", "1024x1024", "1536x1024", "1024x1536"]) - .optional() - .openapi({ - description: "Requested output image size.", - example: "1024x1024", - }), + size: z.string().optional().openapi({ + description: + "Requested output image size. Supported values depend on the model and provider.", + example: "1024x1024", + }), + aspect_ratio: z.string().optional().openapi({ + description: + "The aspect ratio of the edited images (e.g. '1:1', '16:9', '4:3', '5:4'). Takes precedence over size-derived defaults.", + example: "16:9", + }), }); type ImageEditsRequest = z.infer; @@ -469,7 +472,7 @@ const imageEditsResponseSchema = imageGenerationsResponseSchema.extend({ background: z.enum(["transparent", "opaque"]).optional(), output_format: z.enum(["png", "webp", "jpeg"]).optional(), quality: z.enum(["low", "medium", "high"]).optional(), - size: z.enum(["1024x1024", "1024x1536", "1536x1024"]).optional(), + size: z.string().optional(), usage: z .object({ input_tokens: z.number(), @@ -712,9 +715,9 @@ async function processImageEdit(c: Context, request: ImageEditsRequest) { }); const requestedSize = request.size === "auto" ? undefined : request.size; - const aspectRatio = requestedSize - ? sizeToAspectRatio(requestedSize) - : undefined; + const aspectRatio = + request.aspect_ratio ?? + (requestedSize ? sizeToAspectRatio(requestedSize) : undefined); const model = request.model === "auto" || !request.model @@ -755,6 +758,7 @@ async function processImageEdit(c: Context, request: ImageEditsRequest) { imageCount: imageUrls.length, n: request.n, size: request.size, + aspectRatio: request.aspect_ratio, quality: request.quality, outputFormat: request.output_format, }); From f3515f1fedd5c796eefbc0440a2265e6dcf6505f Mon Sep 17 00:00:00 2001 From: Luca Steeb Date: Fri, 27 Mar 2026 18:28:53 +0700 Subject: [PATCH 53/55] fix: avoid activity log prefetch --- .../[orgId]/[projectId]/activity/page.tsx | 60 +------------------ 1 file changed, 1 insertion(+), 59 deletions(-) diff --git a/apps/ui/src/app/dashboard/[orgId]/[projectId]/activity/page.tsx b/apps/ui/src/app/dashboard/[orgId]/[projectId]/activity/page.tsx index 11e07d32c1..21f590a784 100644 --- a/apps/ui/src/app/dashboard/[orgId]/[projectId]/activity/page.tsx +++ b/apps/ui/src/app/dashboard/[orgId]/[projectId]/activity/page.tsx @@ -1,72 +1,15 @@ import { RecentLogs } from "@/components/activity/recent-logs"; import { Card, CardContent } from "@/lib/components/card"; import { fetchModels, fetchProviders } from "@/lib/fetch-models"; -import { fetchServerData } from "@/lib/server-api"; - -import type { LogsData } from "@/types/activity"; export default async function ActivityPage({ params, - searchParams, }: { params: Promise<{ orgId: string; projectId: string }>; - searchParams?: Promise<{ - days?: string; - startDate?: string; - endDate?: string; - finishReason?: string; - unifiedFinishReason?: string; - provider?: string; - model?: string; - limit?: string; - }>; }) { const { orgId, projectId } = await params; - const searchParamsData = await searchParams; - - // Build query parameters for logs - same as client-side - const logsQueryParams: Record = { - orderBy: "createdAt_desc", - projectId, - limit: "10", - }; - - // Add optional filter parameters if they exist - if (searchParamsData?.startDate) { - logsQueryParams.startDate = searchParamsData.startDate; - } - if (searchParamsData?.endDate) { - logsQueryParams.endDate = searchParamsData.endDate; - } - if ( - searchParamsData?.finishReason && - searchParamsData.finishReason !== "all" - ) { - logsQueryParams.finishReason = searchParamsData.finishReason; - } - if ( - searchParamsData?.unifiedFinishReason && - searchParamsData.unifiedFinishReason !== "all" - ) { - logsQueryParams.unifiedFinishReason = searchParamsData.unifiedFinishReason; - } - if (searchParamsData?.provider && searchParamsData.provider !== "all") { - logsQueryParams.provider = searchParamsData.provider; - } - if (searchParamsData?.model && searchParamsData.model !== "all") { - logsQueryParams.model = searchParamsData.model; - } - - if (searchParamsData?.limit) { - logsQueryParams.limit = searchParamsData.limit; - } - const [initialLogsData, providers, models] = await Promise.all([ - fetchServerData("GET", "/logs", { - params: { - query: logsQueryParams, - }, - }), + const [providers, models] = await Promise.all([ fetchProviders(), fetchModels(), ]); @@ -98,7 +41,6 @@ export default async function ActivityPage({ Date: Fri, 27 Mar 2026 21:18:15 +0700 Subject: [PATCH 54/55] Revert "fix: avoid activity log prefetch" This reverts commit f3515f1fedd5c796eefbc0440a2265e6dcf6505f. --- .../[orgId]/[projectId]/activity/page.tsx | 60 ++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/apps/ui/src/app/dashboard/[orgId]/[projectId]/activity/page.tsx b/apps/ui/src/app/dashboard/[orgId]/[projectId]/activity/page.tsx index 21f590a784..11e07d32c1 100644 --- a/apps/ui/src/app/dashboard/[orgId]/[projectId]/activity/page.tsx +++ b/apps/ui/src/app/dashboard/[orgId]/[projectId]/activity/page.tsx @@ -1,15 +1,72 @@ import { RecentLogs } from "@/components/activity/recent-logs"; import { Card, CardContent } from "@/lib/components/card"; import { fetchModels, fetchProviders } from "@/lib/fetch-models"; +import { fetchServerData } from "@/lib/server-api"; + +import type { LogsData } from "@/types/activity"; export default async function ActivityPage({ params, + searchParams, }: { params: Promise<{ orgId: string; projectId: string }>; + searchParams?: Promise<{ + days?: string; + startDate?: string; + endDate?: string; + finishReason?: string; + unifiedFinishReason?: string; + provider?: string; + model?: string; + limit?: string; + }>; }) { const { orgId, projectId } = await params; + const searchParamsData = await searchParams; + + // Build query parameters for logs - same as client-side + const logsQueryParams: Record = { + orderBy: "createdAt_desc", + projectId, + limit: "10", + }; + + // Add optional filter parameters if they exist + if (searchParamsData?.startDate) { + logsQueryParams.startDate = searchParamsData.startDate; + } + if (searchParamsData?.endDate) { + logsQueryParams.endDate = searchParamsData.endDate; + } + if ( + searchParamsData?.finishReason && + searchParamsData.finishReason !== "all" + ) { + logsQueryParams.finishReason = searchParamsData.finishReason; + } + if ( + searchParamsData?.unifiedFinishReason && + searchParamsData.unifiedFinishReason !== "all" + ) { + logsQueryParams.unifiedFinishReason = searchParamsData.unifiedFinishReason; + } + if (searchParamsData?.provider && searchParamsData.provider !== "all") { + logsQueryParams.provider = searchParamsData.provider; + } + if (searchParamsData?.model && searchParamsData.model !== "all") { + logsQueryParams.model = searchParamsData.model; + } + + if (searchParamsData?.limit) { + logsQueryParams.limit = searchParamsData.limit; + } - const [providers, models] = await Promise.all([ + const [initialLogsData, providers, models] = await Promise.all([ + fetchServerData("GET", "/logs", { + params: { + query: logsQueryParams, + }, + }), fetchProviders(), fetchModels(), ]); @@ -41,6 +98,7 @@ export default async function ActivityPage({ Date: Fri, 27 Mar 2026 21:40:35 +0700 Subject: [PATCH 55/55] chore: migrations --- .../1774622168_youthful_the_hood.sql | 3 + .../migrations/meta/1774622168_snapshot.json | 12854 ++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + 3 files changed, 12864 insertions(+) create mode 100644 packages/db/migrations/1774622168_youthful_the_hood.sql create mode 100644 packages/db/migrations/meta/1774622168_snapshot.json diff --git a/packages/db/migrations/1774622168_youthful_the_hood.sql b/packages/db/migrations/1774622168_youthful_the_hood.sql new file mode 100644 index 0000000000..a3c385734c --- /dev/null +++ b/packages/db/migrations/1774622168_youthful_the_hood.sql @@ -0,0 +1,3 @@ +ALTER TABLE "model_provider_mapping" DROP CONSTRAINT "model_provider_mapping_model_id_provider_id_unique";--> statement-breakpoint +ALTER TABLE "model_provider_mapping" ADD COLUMN "region" text;--> statement-breakpoint +ALTER TABLE "model_provider_mapping" ADD CONSTRAINT "model_provider_mapping_model_id_provider_id_region_unique" UNIQUE("model_id","provider_id","region"); \ No newline at end of file diff --git a/packages/db/migrations/meta/1774622168_snapshot.json b/packages/db/migrations/meta/1774622168_snapshot.json new file mode 100644 index 0000000000..b688001114 --- /dev/null +++ b/packages/db/migrations/meta/1774622168_snapshot.json @@ -0,0 +1,12854 @@ +{ + "version": "8", + "dialect": "postgres", + "id": "6b54214e-73e8-49e1-9713-82c42efed32d", + "prevId": "7a4b4e17-49e5-4795-8d0f-0cd3584b0c19", + "ddl": [ + { + "isRlsEnabled": false, + "name": "account", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "api_key", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "api_key_hourly_model_stats", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "api_key_hourly_stats", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "api_key_iam_rule", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "audit_log", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "chat", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "discount", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "enterprise_contact_submission", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "follow_up_email", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "guardrail_config", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "guardrail_rule", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "guardrail_violation", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "installation", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "lock", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "log", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "message", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "model", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "model_history", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "model_provider_mapping", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "model_provider_mapping_history", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "organization", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "organization_action", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "passkey", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "payment_method", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "project", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "project_hourly_model_stats", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "project_hourly_stats", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "provider", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "provider_key", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "referral", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "session", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "transaction", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "user", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "user_organization", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "verification", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "video_job", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "webhook_delivery_log", + "entityType": "tables", + "schema": "public" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider_id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "access_token", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refresh_token", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id_token", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "access_token_expires_at", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refresh_token_expires_at", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scope", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "password", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "api_key" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "api_key" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "api_key" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "api_key" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "api_key" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": { + "value": "'active'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "api_key" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "usage_limit", + "entityType": "columns", + "schema": "public", + "table": "api_key" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'0'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "usage", + "entityType": "columns", + "schema": "public", + "table": "api_key" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "project_id", + "entityType": "columns", + "schema": "public", + "table": "api_key" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_by", + "entityType": "columns", + "schema": "public", + "table": "api_key" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "api_key_id", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "project_id", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hour_timestamp", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "used_model", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "used_provider", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "request_count", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "error_count", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "cache_count", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "streamed_count", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "non_streamed_count", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "completed_count", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "length_limit_count", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "content_filter_count", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "tool_calls_count", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "canceled_count", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "unknown_finish_count", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "client_error_count", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "gateway_error_count", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "upstream_error_count", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'0'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "input_tokens", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'0'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "output_tokens", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'0'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "total_tokens", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'0'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "reasoning_tokens", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'0'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "cached_tokens", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "cost", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "input_cost", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "output_cost", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "request_cost", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "data_storage_cost", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "discount_savings", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "image_input_cost", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "image_output_cost", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "video_output_cost", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "cached_input_cost", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "credits_request_count", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "api_keys_request_count", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "credits_cost", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "api_keys_cost", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "credits_data_storage_cost", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "api_keys_data_storage_cost", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "api_key_id", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "project_id", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hour_timestamp", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "request_count", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "error_count", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "cache_count", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "streamed_count", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "non_streamed_count", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "completed_count", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "length_limit_count", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "content_filter_count", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "tool_calls_count", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "canceled_count", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "unknown_finish_count", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "client_error_count", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "gateway_error_count", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "upstream_error_count", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'0'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "input_tokens", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'0'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "output_tokens", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'0'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "total_tokens", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'0'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "reasoning_tokens", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'0'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "cached_tokens", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "cost", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "input_cost", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "output_cost", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "request_cost", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "data_storage_cost", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "discount_savings", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "image_input_cost", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "image_output_cost", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "video_output_cost", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "cached_input_cost", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "credits_request_count", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "api_keys_request_count", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "credits_cost", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "api_keys_cost", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "credits_data_storage_cost", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "api_keys_data_storage_cost", + "entityType": "columns", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "api_key_iam_rule" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "api_key_iam_rule" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "api_key_iam_rule" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "api_key_id", + "entityType": "columns", + "schema": "public", + "table": "api_key_iam_rule" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "rule_type", + "entityType": "columns", + "schema": "public", + "table": "api_key_iam_rule" + }, + { + "type": "json", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "rule_value", + "entityType": "columns", + "schema": "public", + "table": "api_key_iam_rule" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'active'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "api_key_iam_rule" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "audit_log" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "audit_log" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "audit_log" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "audit_log" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "action", + "entityType": "columns", + "schema": "public", + "table": "audit_log" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "resource_type", + "entityType": "columns", + "schema": "public", + "table": "audit_log" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "resource_id", + "entityType": "columns", + "schema": "public", + "table": "audit_log" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "audit_log" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "chat" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "chat" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "chat" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "chat" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "chat" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "model", + "entityType": "columns", + "schema": "public", + "table": "chat" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": { + "value": "'active'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "chat" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": { + "value": "false", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "web_search", + "entityType": "columns", + "schema": "public", + "table": "chat" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "discount" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "discount" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "discount" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "discount" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider", + "entityType": "columns", + "schema": "public", + "table": "discount" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "model", + "entityType": "columns", + "schema": "public", + "table": "discount" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "discount_percent", + "entityType": "columns", + "schema": "public", + "table": "discount" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reason", + "entityType": "columns", + "schema": "public", + "table": "discount" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "public", + "table": "discount" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "enterprise_contact_submission" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "enterprise_contact_submission" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "enterprise_contact_submission" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "enterprise_contact_submission" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "enterprise_contact_submission" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "country", + "entityType": "columns", + "schema": "public", + "table": "enterprise_contact_submission" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "size", + "entityType": "columns", + "schema": "public", + "table": "enterprise_contact_submission" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "message", + "entityType": "columns", + "schema": "public", + "table": "enterprise_contact_submission" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "honeypot", + "entityType": "columns", + "schema": "public", + "table": "enterprise_contact_submission" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "client_timestamp_ms", + "entityType": "columns", + "schema": "public", + "table": "enterprise_contact_submission" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ip_address", + "entityType": "columns", + "schema": "public", + "table": "enterprise_contact_submission" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_agent", + "entityType": "columns", + "schema": "public", + "table": "enterprise_contact_submission" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'pending'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "spam_filter_status", + "entityType": "columns", + "schema": "public", + "table": "enterprise_contact_submission" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "rejection_reason", + "entityType": "columns", + "schema": "public", + "table": "enterprise_contact_submission" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "follow_up_email" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "follow_up_email" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "follow_up_email" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email_type", + "entityType": "columns", + "schema": "public", + "table": "follow_up_email" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sent_to", + "entityType": "columns", + "schema": "public", + "table": "follow_up_email" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "guardrail_config" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "guardrail_config" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "true", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "enabled", + "entityType": "columns", + "schema": "public", + "table": "guardrail_config" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": { + "type": "unknown", + "value": "'{\"prompt_injection\":{\"enabled\":true,\"action\":\"block\"},\"jailbreak\":{\"enabled\":true,\"action\":\"block\"},\"pii_detection\":{\"enabled\":true,\"action\":\"redact\"},\"secrets\":{\"enabled\":true,\"action\":\"block\"},\"file_types\":{\"enabled\":true,\"action\":\"block\"},\"document_leakage\":{\"enabled\":false,\"action\":\"warn\"}}'" + }, + "generated": null, + "identity": null, + "name": "system_rules", + "entityType": "columns", + "schema": "public", + "table": "guardrail_config" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "10", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "max_file_size_mb", + "entityType": "columns", + "schema": "public", + "table": "guardrail_config" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": { + "value": "'{image/jpeg,image/png,image/gif,image/webp}'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "allowed_file_types", + "entityType": "columns", + "schema": "public", + "table": "guardrail_config" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": { + "value": "'redact'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "pii_action", + "entityType": "columns", + "schema": "public", + "table": "guardrail_config" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "guardrail_config" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "guardrail_config" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "guardrail_rule" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "guardrail_rule" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "guardrail_rule" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "guardrail_rule" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "config", + "entityType": "columns", + "schema": "public", + "table": "guardrail_rule" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "100", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "priority", + "entityType": "columns", + "schema": "public", + "table": "guardrail_rule" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "true", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "enabled", + "entityType": "columns", + "schema": "public", + "table": "guardrail_rule" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'block'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "action", + "entityType": "columns", + "schema": "public", + "table": "guardrail_rule" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "guardrail_rule" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "guardrail_rule" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "guardrail_violation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "guardrail_violation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "log_id", + "entityType": "columns", + "schema": "public", + "table": "guardrail_violation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "rule_id", + "entityType": "columns", + "schema": "public", + "table": "guardrail_violation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "rule_name", + "entityType": "columns", + "schema": "public", + "table": "guardrail_violation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "category", + "entityType": "columns", + "schema": "public", + "table": "guardrail_violation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "action_taken", + "entityType": "columns", + "schema": "public", + "table": "guardrail_violation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "matched_pattern", + "entityType": "columns", + "schema": "public", + "table": "guardrail_violation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "matched_content", + "entityType": "columns", + "schema": "public", + "table": "guardrail_violation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "content_hash", + "entityType": "columns", + "schema": "public", + "table": "guardrail_violation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "api_key_id", + "entityType": "columns", + "schema": "public", + "table": "guardrail_violation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "model", + "entityType": "columns", + "schema": "public", + "table": "guardrail_violation" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "guardrail_violation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "installation" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "installation" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "installation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "uuid", + "entityType": "columns", + "schema": "public", + "table": "installation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "installation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "lock" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "lock" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "lock" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "public", + "table": "lock" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "request_id", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "project_id", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "api_key_id", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "duration", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "time_to_first_token", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "time_to_first_reasoning_token", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "requested_model", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "requested_provider", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "used_model", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "used_model_mapping", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "used_provider", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "response_size", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "content", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reasoning_content", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "json", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tools", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "json", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tool_choice", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "json", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tool_results", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "finish_reason", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "unified_finish_reason", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prompt_tokens", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "completion_tokens", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "total_tokens", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reasoning_tokens", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "cached_tokens", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "json", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "messages", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "real", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "temperature", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "max_tokens", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "real", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "top_p", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "real", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "frequency_penalty", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "real", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "presence_penalty", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reasoning_effort", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reasoning_max_tokens", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "effort", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "json", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "response_format", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": { + "value": "false", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "has_error", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "json", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "error_details", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "real", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "cost", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "real", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "input_cost", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "real", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "output_cost", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "real", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "cached_input_cost", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "real", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "request_cost", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "real", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "web_search_cost", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image_input_tokens", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image_output_tokens", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "real", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image_input_cost", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "real", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image_output_cost", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "real", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "video_output_cost", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "video_download_count", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_video_downloaded_at", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": { + "value": "false", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "estimated_cost", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "real", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "discount", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pricing_tier", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": { + "value": "false", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "canceled", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": { + "value": "false", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "streamed", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": { + "value": "false", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "cached", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "mode", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "used_mode", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "source", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "json", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "custom_headers", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "json", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "routing_metadata", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "processed_at", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "raw_request", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "raw_response", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "upstream_request", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "upstream_response", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "trace_id", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": { + "value": "false", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "data_retention_cleaned_up", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'0'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "data_storage_cost", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "json", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "params", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_agent", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "json", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "plugins", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "json", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "plugin_results", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": { + "value": "false", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "retried", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "retried_by_log_id", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "internal_content_filter", + "entityType": "columns", + "schema": "public", + "table": "log" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "message" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "message" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "message" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "chat_id", + "entityType": "columns", + "schema": "public", + "table": "message" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "message" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "content", + "entityType": "columns", + "schema": "public", + "table": "message" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "images", + "entityType": "columns", + "schema": "public", + "table": "message" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reasoning", + "entityType": "columns", + "schema": "public", + "table": "message" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tools", + "entityType": "columns", + "schema": "public", + "table": "message" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sequence", + "entityType": "columns", + "schema": "public", + "table": "message" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "model" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "model" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "model" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "released_at", + "entityType": "columns", + "schema": "public", + "table": "model" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'(empty)'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "model" + }, + { + "type": "json", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "type": "unknown", + "value": "'[]'" + }, + "generated": null, + "identity": null, + "name": "aliases", + "entityType": "columns", + "schema": "public", + "table": "model" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'(empty)'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "model" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "family", + "entityType": "columns", + "schema": "public", + "table": "model" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "false", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "free", + "entityType": "columns", + "schema": "public", + "table": "model" + }, + { + "type": "json", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "type": "unknown", + "value": "'[\"text\"]'" + }, + "generated": null, + "identity": null, + "name": "output", + "entityType": "columns", + "schema": "public", + "table": "model" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "false", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "image_input_required", + "entityType": "columns", + "schema": "public", + "table": "model" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'stable'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "stability", + "entityType": "columns", + "schema": "public", + "table": "model" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'active'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "model" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "logs_count", + "entityType": "columns", + "schema": "public", + "table": "model" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "errors_count", + "entityType": "columns", + "schema": "public", + "table": "model" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "client_errors_count", + "entityType": "columns", + "schema": "public", + "table": "model" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "gateway_errors_count", + "entityType": "columns", + "schema": "public", + "table": "model" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "upstream_errors_count", + "entityType": "columns", + "schema": "public", + "table": "model" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "cached_count", + "entityType": "columns", + "schema": "public", + "table": "model" + }, + { + "type": "real", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "avg_time_to_first_token", + "entityType": "columns", + "schema": "public", + "table": "model" + }, + { + "type": "real", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "avg_time_to_first_reasoning_token", + "entityType": "columns", + "schema": "public", + "table": "model" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "stats_updated_at", + "entityType": "columns", + "schema": "public", + "table": "model" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "model_history" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "model_history" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "model_history" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "model_id", + "entityType": "columns", + "schema": "public", + "table": "model_history" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "minute_timestamp", + "entityType": "columns", + "schema": "public", + "table": "model_history" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "logs_count", + "entityType": "columns", + "schema": "public", + "table": "model_history" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "errors_count", + "entityType": "columns", + "schema": "public", + "table": "model_history" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "client_errors_count", + "entityType": "columns", + "schema": "public", + "table": "model_history" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "gateway_errors_count", + "entityType": "columns", + "schema": "public", + "table": "model_history" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "upstream_errors_count", + "entityType": "columns", + "schema": "public", + "table": "model_history" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "cached_count", + "entityType": "columns", + "schema": "public", + "table": "model_history" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "total_input_tokens", + "entityType": "columns", + "schema": "public", + "table": "model_history" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "total_output_tokens", + "entityType": "columns", + "schema": "public", + "table": "model_history" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "total_tokens", + "entityType": "columns", + "schema": "public", + "table": "model_history" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "total_reasoning_tokens", + "entityType": "columns", + "schema": "public", + "table": "model_history" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "total_cached_tokens", + "entityType": "columns", + "schema": "public", + "table": "model_history" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "total_duration", + "entityType": "columns", + "schema": "public", + "table": "model_history" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "total_time_to_first_token", + "entityType": "columns", + "schema": "public", + "table": "model_history" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "total_time_to_first_reasoning_token", + "entityType": "columns", + "schema": "public", + "table": "model_history" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "total_cost", + "entityType": "columns", + "schema": "public", + "table": "model_history" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "model_id", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider_id", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "model_name", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "region", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "input_price", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "output_price", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "cached_input_price", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image_input_price", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "request_price", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "context_size", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "max_output", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "false", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "streaming", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "vision", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reasoning", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "false", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "reasoning_max_tokens", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reasoning_output", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tools", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "false", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "json_output", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "false", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "json_output_schema", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "false", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "web_search", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "web_search_price", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'0'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "discount", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'stable'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "stability", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "json", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "supported_parameters", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "test", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deprecated_at", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deactivated_at", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'active'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "logs_count", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "errors_count", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "client_errors_count", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "gateway_errors_count", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "upstream_errors_count", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "cached_count", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "real", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "avg_time_to_first_token", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "real", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "avg_time_to_first_reasoning_token", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "real", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "routing_uptime", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "real", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "routing_latency", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "real", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "routing_throughput", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "routing_total_requests", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "stats_updated_at", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping_history" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping_history" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping_history" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "model_id", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping_history" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider_id", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping_history" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "model_provider_mapping_id", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping_history" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "minute_timestamp", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping_history" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "logs_count", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping_history" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "errors_count", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping_history" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "client_errors_count", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping_history" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "gateway_errors_count", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping_history" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "upstream_errors_count", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping_history" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "cached_count", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping_history" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "total_input_tokens", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping_history" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "total_output_tokens", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping_history" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "total_tokens", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping_history" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "total_reasoning_tokens", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping_history" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "total_cached_tokens", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping_history" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "total_duration", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping_history" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "total_time_to_first_token", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping_history" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "total_time_to_first_reasoning_token", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping_history" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "total_cost", + "entityType": "columns", + "schema": "public", + "table": "model_provider_mapping_history" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "billing_email", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "billing_company", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "billing_address", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "billing_tax_id", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "billing_notes", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "stripe_customer_id", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "stripe_subscription_id", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'0'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "credits", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "false", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "auto_top_up_enabled", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": { + "value": "'10'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "auto_top_up_threshold", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": { + "value": "'10'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "auto_top_up_amount", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'free'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "plan", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "plan_expires_at", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "false", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "subscription_cancelled", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "trial_start_date", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "trial_end_date", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "false", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "is_trial_active", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'none'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "retention_level", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": { + "value": "'active'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'0'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "referral_earnings", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "payment_failure_count", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_payment_failure_at", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "false", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "is_personal", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'none'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "dev_plan", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'0'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "dev_plan_credits_used", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'0'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "dev_plan_credits_limit", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "dev_plan_billing_cycle_start", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "dev_plan_stripe_subscription_id", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "false", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "dev_plan_cancelled", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "dev_plan_expires_at", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "false", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "dev_plan_allow_all_models", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "organization_action" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "organization_action" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "organization_action" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "organization_action" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "organization_action" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "amount", + "entityType": "columns", + "schema": "public", + "table": "organization_action" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "organization_action" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "passkey" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "passkey" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "passkey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "passkey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "public_key", + "entityType": "columns", + "schema": "public", + "table": "passkey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "passkey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "credential_id", + "entityType": "columns", + "schema": "public", + "table": "passkey" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "counter", + "entityType": "columns", + "schema": "public", + "table": "passkey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "device_type", + "entityType": "columns", + "schema": "public", + "table": "passkey" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "backed_up", + "entityType": "columns", + "schema": "public", + "table": "passkey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "transports", + "entityType": "columns", + "schema": "public", + "table": "passkey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "aaguid", + "entityType": "columns", + "schema": "public", + "table": "passkey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "payment_method" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "payment_method" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "payment_method" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "stripe_payment_method_id", + "entityType": "columns", + "schema": "public", + "table": "payment_method" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "payment_method" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "payment_method" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "false", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "is_default", + "entityType": "columns", + "schema": "public", + "table": "payment_method" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "project" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "project" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "project" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "project" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "project" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "false", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "caching_enabled", + "entityType": "columns", + "schema": "public", + "table": "project" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "60", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "cache_duration_seconds", + "entityType": "columns", + "schema": "public", + "table": "project" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'hybrid'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "mode", + "entityType": "columns", + "schema": "public", + "table": "project" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": { + "value": "'active'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "project" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "project_id", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hour_timestamp", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "used_model", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "used_provider", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "request_count", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "error_count", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "cache_count", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "streamed_count", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "non_streamed_count", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "completed_count", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "length_limit_count", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "content_filter_count", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "tool_calls_count", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "canceled_count", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "unknown_finish_count", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "client_error_count", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "gateway_error_count", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "upstream_error_count", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'0'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "input_tokens", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'0'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "output_tokens", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'0'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "total_tokens", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'0'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "reasoning_tokens", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'0'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "cached_tokens", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "cost", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "input_cost", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "output_cost", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "request_cost", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "data_storage_cost", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "discount_savings", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "image_input_cost", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "image_output_cost", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "video_output_cost", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "cached_input_cost", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "credits_request_count", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "api_keys_request_count", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "credits_cost", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "api_keys_cost", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "credits_data_storage_cost", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "api_keys_data_storage_cost", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "project_id", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hour_timestamp", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "request_count", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "error_count", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "cache_count", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "streamed_count", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "non_streamed_count", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "completed_count", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "length_limit_count", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "content_filter_count", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "tool_calls_count", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "canceled_count", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "unknown_finish_count", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "client_error_count", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "gateway_error_count", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "upstream_error_count", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'0'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "input_tokens", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'0'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "output_tokens", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'0'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "total_tokens", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'0'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "reasoning_tokens", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'0'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "cached_tokens", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "cost", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "input_cost", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "output_cost", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "request_cost", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "data_storage_cost", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "discount_savings", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "image_input_cost", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "image_output_cost", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "video_output_cost", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "cached_input_cost", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "credits_request_count", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "api_keys_request_count", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "credits_cost", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "api_keys_cost", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "credits_data_storage_cost", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "api_keys_data_storage_cost", + "entityType": "columns", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "provider" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "provider" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "provider" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "provider" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "provider" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "streaming", + "entityType": "columns", + "schema": "public", + "table": "provider" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "cancellation", + "entityType": "columns", + "schema": "public", + "table": "provider" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "color", + "entityType": "columns", + "schema": "public", + "table": "provider" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "website", + "entityType": "columns", + "schema": "public", + "table": "provider" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "announcement", + "entityType": "columns", + "schema": "public", + "table": "provider" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'active'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "provider" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "logs_count", + "entityType": "columns", + "schema": "public", + "table": "provider" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "errors_count", + "entityType": "columns", + "schema": "public", + "table": "provider" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "client_errors_count", + "entityType": "columns", + "schema": "public", + "table": "provider" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "gateway_errors_count", + "entityType": "columns", + "schema": "public", + "table": "provider" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "upstream_errors_count", + "entityType": "columns", + "schema": "public", + "table": "provider" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "cached_count", + "entityType": "columns", + "schema": "public", + "table": "provider" + }, + { + "type": "real", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "avg_time_to_first_token", + "entityType": "columns", + "schema": "public", + "table": "provider" + }, + { + "type": "real", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "avg_time_to_first_reasoning_token", + "entityType": "columns", + "schema": "public", + "table": "provider" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "stats_updated_at", + "entityType": "columns", + "schema": "public", + "table": "provider" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "provider_key" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "provider_key" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "provider_key" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "provider_key" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider", + "entityType": "columns", + "schema": "public", + "table": "provider_key" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "provider_key" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "base_url", + "entityType": "columns", + "schema": "public", + "table": "provider_key" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "options", + "entityType": "columns", + "schema": "public", + "table": "provider_key" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": { + "value": "'active'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "provider_key" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "provider_key" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "referral" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "referral" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "referral" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "referrer_organization_id", + "entityType": "columns", + "schema": "public", + "table": "referral" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "referred_organization_id", + "entityType": "columns", + "schema": "public", + "table": "referral" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ip_address", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_agent", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "transaction" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "transaction" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "transaction" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "transaction" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "transaction" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "amount", + "entityType": "columns", + "schema": "public", + "table": "transaction" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "credit_amount", + "entityType": "columns", + "schema": "public", + "table": "transaction" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'USD'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "currency", + "entityType": "columns", + "schema": "public", + "table": "transaction" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'completed'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "transaction" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "stripe_payment_intent_id", + "entityType": "columns", + "schema": "public", + "table": "transaction" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "stripe_invoice_id", + "entityType": "columns", + "schema": "public", + "table": "transaction" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "transaction" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "related_transaction_id", + "entityType": "columns", + "schema": "public", + "table": "transaction" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refund_reason", + "entityType": "columns", + "schema": "public", + "table": "transaction" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "false", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "email_verified", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "false", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "onboarding_completed", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "false", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "newsletter_subscribed", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "user_organization" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "user_organization" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "user_organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "user_organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "user_organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'owner'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "user_organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "identifier", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "video_job" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "request_id", + "entityType": "columns", + "schema": "public", + "table": "video_job" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "video_job" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "video_job" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "video_job" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "project_id", + "entityType": "columns", + "schema": "public", + "table": "video_job" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "api_key_id", + "entityType": "columns", + "schema": "public", + "table": "video_job" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "mode", + "entityType": "columns", + "schema": "public", + "table": "video_job" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "used_mode", + "entityType": "columns", + "schema": "public", + "table": "video_job" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "model", + "entityType": "columns", + "schema": "public", + "table": "video_job" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "requested_provider", + "entityType": "columns", + "schema": "public", + "table": "video_job" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "used_provider", + "entityType": "columns", + "schema": "public", + "table": "video_job" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "used_model", + "entityType": "columns", + "schema": "public", + "table": "video_job" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider_config_index", + "entityType": "columns", + "schema": "public", + "table": "video_job" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "upstream_id", + "entityType": "columns", + "schema": "public", + "table": "video_job" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prompt", + "entityType": "columns", + "schema": "public", + "table": "video_job" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'queued'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "video_job" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "progress", + "entityType": "columns", + "schema": "public", + "table": "video_job" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "error", + "entityType": "columns", + "schema": "public", + "table": "video_job" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "content_url", + "entityType": "columns", + "schema": "public", + "table": "video_job" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "storage_provider", + "entityType": "columns", + "schema": "public", + "table": "video_job" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "storage_bucket", + "entityType": "columns", + "schema": "public", + "table": "video_job" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "storage_object_path", + "entityType": "columns", + "schema": "public", + "table": "video_job" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "storage_uri", + "entityType": "columns", + "schema": "public", + "table": "video_job" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "storage_expires_at", + "entityType": "columns", + "schema": "public", + "table": "video_job" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "content_type", + "entityType": "columns", + "schema": "public", + "table": "video_job" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "completed_at", + "entityType": "columns", + "schema": "public", + "table": "video_job" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "public", + "table": "video_job" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_polled_at", + "entityType": "columns", + "schema": "public", + "table": "video_job" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "next_poll_at", + "entityType": "columns", + "schema": "public", + "table": "video_job" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "0", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "poll_attempt_count", + "entityType": "columns", + "schema": "public", + "table": "video_job" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "callback_url", + "entityType": "columns", + "schema": "public", + "table": "video_job" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "callback_secret", + "entityType": "columns", + "schema": "public", + "table": "video_job" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'none'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "callback_status", + "entityType": "columns", + "schema": "public", + "table": "video_job" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "callback_event_id", + "entityType": "columns", + "schema": "public", + "table": "video_job" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "callback_event_type", + "entityType": "columns", + "schema": "public", + "table": "video_job" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "callback_delivered_at", + "entityType": "columns", + "schema": "public", + "table": "video_job" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "result_logged_at", + "entityType": "columns", + "schema": "public", + "table": "video_job" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "routing_metadata", + "entityType": "columns", + "schema": "public", + "table": "video_job" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "upstream_create_response", + "entityType": "columns", + "schema": "public", + "table": "video_job" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "upstream_status_response", + "entityType": "columns", + "schema": "public", + "table": "video_job" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "webhook_delivery_log" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "webhook_delivery_log" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "webhook_delivery_log" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "video_job_id", + "entityType": "columns", + "schema": "public", + "table": "webhook_delivery_log" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "event_id", + "entityType": "columns", + "schema": "public", + "table": "webhook_delivery_log" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "event_type", + "entityType": "columns", + "schema": "public", + "table": "webhook_delivery_log" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "target_url", + "entityType": "columns", + "schema": "public", + "table": "webhook_delivery_log" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "1", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "attempt", + "entityType": "columns", + "schema": "public", + "table": "webhook_delivery_log" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "'pending'", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "webhook_delivery_log" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_tried_at", + "entityType": "columns", + "schema": "public", + "table": "webhook_delivery_log" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": { + "value": "now()", + "type": "unknown" + }, + "generated": null, + "identity": null, + "name": "next_retry_at", + "entityType": "columns", + "schema": "public", + "table": "webhook_delivery_log" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "delivered_at", + "entityType": "columns", + "schema": "public", + "table": "webhook_delivery_log" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "request_headers", + "entityType": "columns", + "schema": "public", + "table": "webhook_delivery_log" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "request_body", + "entityType": "columns", + "schema": "public", + "table": "webhook_delivery_log" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "response_status", + "entityType": "columns", + "schema": "public", + "table": "webhook_delivery_log" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "response_body", + "entityType": "columns", + "schema": "public", + "table": "webhook_delivery_log" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "error", + "entityType": "columns", + "schema": "public", + "table": "webhook_delivery_log" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "account_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "account" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "project_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "api_key_project_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "api_key" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "created_by", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "api_key_created_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "api_key" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "api_key_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "hour_timestamp", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "api_key_hourly_model_stats_api_key_id_hour_timestamp_idx", + "entityType": "indexes", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "project_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "hour_timestamp", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "api_key_hourly_model_stats_project_id_hour_timestamp_idx", + "entityType": "indexes", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "hour_timestamp", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "api_key_hourly_model_stats_hour_timestamp_idx", + "entityType": "indexes", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "api_key_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "hour_timestamp", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "api_key_hourly_stats_api_key_id_hour_timestamp_idx", + "entityType": "indexes", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "project_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "hour_timestamp", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "api_key_hourly_stats_project_id_hour_timestamp_idx", + "entityType": "indexes", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "hour_timestamp", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "api_key_hourly_stats_hour_timestamp_idx", + "entityType": "indexes", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "api_key_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "api_key_iam_rule_api_key_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "api_key_iam_rule" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "rule_type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "api_key_iam_rule_rule_type_idx", + "entityType": "indexes", + "schema": "public", + "table": "api_key_iam_rule" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "api_key_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "api_key_iam_rule_api_key_id_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "api_key_iam_rule" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "audit_log_organization_id_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "audit_log" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "audit_log_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "audit_log" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "action", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "audit_log_action_idx", + "entityType": "indexes", + "schema": "public", + "table": "audit_log" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "resource_type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "audit_log_resource_type_idx", + "entityType": "indexes", + "schema": "public", + "table": "audit_log" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "chat_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "chat" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "discount_organization_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "discount" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "provider", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "discount_provider_idx", + "entityType": "indexes", + "schema": "public", + "table": "discount" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "model", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "discount_model_idx", + "entityType": "indexes", + "schema": "public", + "table": "discount" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "enterprise_contact_submission_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "enterprise_contact_submission" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "email", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "enterprise_contact_submission_email_idx", + "entityType": "indexes", + "schema": "public", + "table": "enterprise_contact_submission" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "spam_filter_status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "enterprise_contact_submission_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "enterprise_contact_submission" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "follow_up_email_organization_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "follow_up_email" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "guardrail_config_organization_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "guardrail_config" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "guardrail_rule_organization_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "guardrail_rule" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "priority", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "guardrail_rule_priority_idx", + "entityType": "indexes", + "schema": "public", + "table": "guardrail_rule" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "guardrail_violation_org_created_idx", + "entityType": "indexes", + "schema": "public", + "table": "guardrail_violation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "rule_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "guardrail_violation_rule_created_idx", + "entityType": "indexes", + "schema": "public", + "table": "guardrail_violation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "project_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "log_project_id_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "log" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "used_model", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "used_provider", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "log_created_at_used_model_used_provider_idx", + "entityType": "indexes", + "schema": "public", + "table": "log" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "data_retention_cleaned_up = false", + "with": "", + "method": "btree", + "concurrently": false, + "name": "log_data_retention_pending_idx", + "entityType": "indexes", + "schema": "public", + "table": "log" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "project_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "used_model", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "log_project_id_used_model_idx", + "entityType": "indexes", + "schema": "public", + "table": "log" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "processed_at IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "log_processed_at_null_idx", + "entityType": "indexes", + "schema": "public", + "table": "log" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "chat_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "message_chat_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "message" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "model_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "model" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "minute_timestamp", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "model_history_minute_timestamp_idx", + "entityType": "indexes", + "schema": "public", + "table": "model_history" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "model_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "minute_timestamp", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "model_history_model_id_minute_timestamp_idx", + "entityType": "indexes", + "schema": "public", + "table": "model_history" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "model_provider_mapping_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "minute_timestamp", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "model_provider_mapping_history_minute_timestamp_idx", + "entityType": "indexes", + "schema": "public", + "table": "model_provider_mapping_history" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "minute_timestamp", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "provider_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "model_provider_mapping_history_minute_timestamp_provider_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "model_provider_mapping_history" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "minute_timestamp", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "model_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "model_provider_mapping_history_minute_timestamp_model_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "model_provider_mapping_history" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "model_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "minute_timestamp", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "model_provider_mapping_history_model_id_minute_timestamp_idx", + "entityType": "indexes", + "schema": "public", + "table": "model_provider_mapping_history" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "provider_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "model_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "minute_timestamp", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "model_provider_mapping_history_id_ts_idx", + "entityType": "indexes", + "schema": "public", + "table": "model_provider_mapping_history" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "organization_action_organization_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "organization_action" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "passkey_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "passkey" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "payment_method_organization_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "payment_method" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "project_organization_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "project" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "project_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "hour_timestamp", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "project_hourly_model_stats_project_id_hour_timestamp_idx", + "entityType": "indexes", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "hour_timestamp", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "project_hourly_model_stats_hour_timestamp_idx", + "entityType": "indexes", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "used_model", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "hour_timestamp", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "project_hourly_model_stats_used_model_hour_timestamp_idx", + "entityType": "indexes", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "used_provider", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "used_model", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "hour_timestamp", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "project_hourly_model_stats_p_m_time_idx", + "entityType": "indexes", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "hour_timestamp", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "project_hourly_stats_hour_timestamp_idx", + "entityType": "indexes", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "provider_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "provider" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "provider_key_organization_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "provider_key" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "referrer_organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "referral_referrer_organization_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "referral" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "referred_organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "referral_referred_organization_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "referral" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "session_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "session" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "transaction_organization_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "transaction" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "user_organization_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "user_organization" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "user_organization_organization_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "user_organization" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "project_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "video_job_project_id_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "video_job" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "next_poll_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "video_job_status_next_poll_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "video_job" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "upstream_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "video_job_upstream_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "video_job" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "callback_status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "video_job_callback_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "video_job" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "video_job_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "webhook_delivery_log_video_job_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "webhook_delivery_log" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "next_retry_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "webhook_delivery_log_status_next_retry_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "webhook_delivery_log" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "account_user_id_user_id_fk", + "entityType": "fks", + "schema": "public", + "table": "account" + }, + { + "nameExplicit": false, + "columns": [ + "project_id" + ], + "schemaTo": "public", + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "api_key_project_id_project_id_fk", + "entityType": "fks", + "schema": "public", + "table": "api_key" + }, + { + "nameExplicit": false, + "columns": [ + "created_by" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "api_key_created_by_user_id_fk", + "entityType": "fks", + "schema": "public", + "table": "api_key" + }, + { + "nameExplicit": false, + "columns": [ + "api_key_id" + ], + "schemaTo": "public", + "tableTo": "api_key", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "api_key_iam_rule_api_key_id_api_key_id_fk", + "entityType": "fks", + "schema": "public", + "table": "api_key_iam_rule" + }, + { + "nameExplicit": false, + "columns": [ + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "audit_log_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "audit_log" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "audit_log_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "audit_log" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "chat_user_id_user_id_fk", + "entityType": "fks", + "schema": "public", + "table": "chat" + }, + { + "nameExplicit": false, + "columns": [ + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "discount_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "discount" + }, + { + "nameExplicit": false, + "columns": [ + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "follow_up_email_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "follow_up_email" + }, + { + "nameExplicit": false, + "columns": [ + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "guardrail_config_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "guardrail_config" + }, + { + "nameExplicit": false, + "columns": [ + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "guardrail_rule_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "guardrail_rule" + }, + { + "nameExplicit": false, + "columns": [ + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "guardrail_violation_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "guardrail_violation" + }, + { + "nameExplicit": false, + "columns": [ + "chat_id" + ], + "schemaTo": "public", + "tableTo": "chat", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "message_chat_id_chat_id_fk", + "entityType": "fks", + "schema": "public", + "table": "message" + }, + { + "nameExplicit": false, + "columns": [ + "model_id" + ], + "schemaTo": "public", + "tableTo": "model", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "model_provider_mapping_model_id_model_id_fk", + "entityType": "fks", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "nameExplicit": false, + "columns": [ + "provider_id" + ], + "schemaTo": "public", + "tableTo": "provider", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "model_provider_mapping_provider_id_provider_id_fk", + "entityType": "fks", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "nameExplicit": false, + "columns": [ + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "organization_action_organization_id_organization_id_fk", + "entityType": "fks", + "schema": "public", + "table": "organization_action" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "passkey_user_id_user_id_fk", + "entityType": "fks", + "schema": "public", + "table": "passkey" + }, + { + "nameExplicit": false, + "columns": [ + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "payment_method_organization_id_organization_id_fk", + "entityType": "fks", + "schema": "public", + "table": "payment_method" + }, + { + "nameExplicit": false, + "columns": [ + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "project_organization_id_organization_id_fk", + "entityType": "fks", + "schema": "public", + "table": "project" + }, + { + "nameExplicit": false, + "columns": [ + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "provider_key_organization_id_organization_id_fk", + "entityType": "fks", + "schema": "public", + "table": "provider_key" + }, + { + "nameExplicit": false, + "columns": [ + "referrer_organization_id" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "referral_referrer_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "referral" + }, + { + "nameExplicit": false, + "columns": [ + "referred_organization_id" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "referral_referred_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "referral" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "session_user_id_user_id_fk", + "entityType": "fks", + "schema": "public", + "table": "session" + }, + { + "nameExplicit": false, + "columns": [ + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "transaction_organization_id_organization_id_fk", + "entityType": "fks", + "schema": "public", + "table": "transaction" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "user_organization_user_id_user_id_fk", + "entityType": "fks", + "schema": "public", + "table": "user_organization" + }, + { + "nameExplicit": false, + "columns": [ + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "user_organization_organization_id_organization_id_fk", + "entityType": "fks", + "schema": "public", + "table": "user_organization" + }, + { + "nameExplicit": false, + "columns": [ + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "video_job_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "video_job" + }, + { + "nameExplicit": false, + "columns": [ + "project_id" + ], + "schemaTo": "public", + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "video_job_project_id_project_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "video_job" + }, + { + "nameExplicit": false, + "columns": [ + "api_key_id" + ], + "schemaTo": "public", + "tableTo": "api_key", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "video_job_api_key_id_api_key_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "video_job" + }, + { + "nameExplicit": false, + "columns": [ + "video_job_id" + ], + "schemaTo": "public", + "tableTo": "video_job", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "webhook_delivery_log_video_job_id_video_job_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "webhook_delivery_log" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "account_pkey", + "schema": "public", + "table": "account", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "api_key_pkey", + "schema": "public", + "table": "api_key", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "api_key_hourly_model_stats_pkey", + "schema": "public", + "table": "api_key_hourly_model_stats", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "api_key_hourly_stats_pkey", + "schema": "public", + "table": "api_key_hourly_stats", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "api_key_iam_rule_pkey", + "schema": "public", + "table": "api_key_iam_rule", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "audit_log_pkey", + "schema": "public", + "table": "audit_log", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "chat_pkey", + "schema": "public", + "table": "chat", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "discount_pkey", + "schema": "public", + "table": "discount", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "enterprise_contact_submission_pkey", + "schema": "public", + "table": "enterprise_contact_submission", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "follow_up_email_pkey", + "schema": "public", + "table": "follow_up_email", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "guardrail_config_pkey", + "schema": "public", + "table": "guardrail_config", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "guardrail_rule_pkey", + "schema": "public", + "table": "guardrail_rule", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "guardrail_violation_pkey", + "schema": "public", + "table": "guardrail_violation", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "installation_pkey", + "schema": "public", + "table": "installation", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "lock_pkey", + "schema": "public", + "table": "lock", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "log_pkey", + "schema": "public", + "table": "log", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "message_pkey", + "schema": "public", + "table": "message", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "model_pkey", + "schema": "public", + "table": "model", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "model_history_pkey", + "schema": "public", + "table": "model_history", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "model_provider_mapping_pkey", + "schema": "public", + "table": "model_provider_mapping", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "model_provider_mapping_history_pkey", + "schema": "public", + "table": "model_provider_mapping_history", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "organization_pkey", + "schema": "public", + "table": "organization", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "organization_action_pkey", + "schema": "public", + "table": "organization_action", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "passkey_pkey", + "schema": "public", + "table": "passkey", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "payment_method_pkey", + "schema": "public", + "table": "payment_method", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "project_pkey", + "schema": "public", + "table": "project", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "project_hourly_model_stats_pkey", + "schema": "public", + "table": "project_hourly_model_stats", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "project_hourly_stats_pkey", + "schema": "public", + "table": "project_hourly_stats", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "provider_pkey", + "schema": "public", + "table": "provider", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "provider_key_pkey", + "schema": "public", + "table": "provider_key", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "referral_pkey", + "schema": "public", + "table": "referral", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "session_pkey", + "schema": "public", + "table": "session", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "transaction_pkey", + "schema": "public", + "table": "transaction", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "user_pkey", + "schema": "public", + "table": "user", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "user_organization_pkey", + "schema": "public", + "table": "user_organization", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "verification_pkey", + "schema": "public", + "table": "verification", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "video_job_pkey", + "schema": "public", + "table": "video_job", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "webhook_delivery_log_pkey", + "schema": "public", + "table": "webhook_delivery_log", + "entityType": "pks" + }, + { + "nameExplicit": false, + "columns": [ + "api_key_id", + "hour_timestamp", + "used_model", + "used_provider" + ], + "nullsNotDistinct": false, + "name": "api_key_hourly_model_stats_api_key_id_hour_timestamp_used_model_used_provider_unique", + "entityType": "uniques", + "schema": "public", + "table": "api_key_hourly_model_stats" + }, + { + "nameExplicit": false, + "columns": [ + "api_key_id", + "hour_timestamp" + ], + "nullsNotDistinct": false, + "name": "api_key_hourly_stats_api_key_id_hour_timestamp_unique", + "entityType": "uniques", + "schema": "public", + "table": "api_key_hourly_stats" + }, + { + "nameExplicit": true, + "columns": [ + "organization_id", + "provider", + "model" + ], + "nullsNotDistinct": false, + "name": "discount_org_provider_model_unique", + "entityType": "uniques", + "schema": "public", + "table": "discount" + }, + { + "nameExplicit": false, + "columns": [ + "organization_id", + "email_type" + ], + "nullsNotDistinct": false, + "name": "follow_up_email_organization_id_email_type_unique", + "entityType": "uniques", + "schema": "public", + "table": "follow_up_email" + }, + { + "nameExplicit": false, + "columns": [ + "model_id", + "minute_timestamp" + ], + "nullsNotDistinct": false, + "name": "model_history_model_id_minute_timestamp_unique", + "entityType": "uniques", + "schema": "public", + "table": "model_history" + }, + { + "nameExplicit": false, + "columns": [ + "model_id", + "provider_id", + "region" + ], + "nullsNotDistinct": false, + "name": "model_provider_mapping_model_id_provider_id_region_unique", + "entityType": "uniques", + "schema": "public", + "table": "model_provider_mapping" + }, + { + "nameExplicit": false, + "columns": [ + "model_provider_mapping_id", + "minute_timestamp" + ], + "nullsNotDistinct": false, + "name": "model_provider_mapping_history_model_provider_mapping_id_minute_timestamp_unique", + "entityType": "uniques", + "schema": "public", + "table": "model_provider_mapping_history" + }, + { + "nameExplicit": false, + "columns": [ + "project_id", + "hour_timestamp", + "used_model", + "used_provider" + ], + "nullsNotDistinct": false, + "name": "project_hourly_model_stats_project_id_hour_timestamp_used_model_used_provider_unique", + "entityType": "uniques", + "schema": "public", + "table": "project_hourly_model_stats" + }, + { + "nameExplicit": false, + "columns": [ + "project_id", + "hour_timestamp" + ], + "nullsNotDistinct": false, + "name": "project_hourly_stats_project_id_hour_timestamp_unique", + "entityType": "uniques", + "schema": "public", + "table": "project_hourly_stats" + }, + { + "nameExplicit": false, + "columns": [ + "organization_id", + "name" + ], + "nullsNotDistinct": false, + "name": "provider_key_organization_id_name_unique", + "entityType": "uniques", + "schema": "public", + "table": "provider_key" + }, + { + "nameExplicit": false, + "columns": [ + "token" + ], + "nullsNotDistinct": false, + "name": "api_key_token_unique", + "schema": "public", + "table": "api_key", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "organization_id" + ], + "nullsNotDistinct": false, + "name": "guardrail_config_organization_id_key", + "schema": "public", + "table": "guardrail_config", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "uuid" + ], + "nullsNotDistinct": false, + "name": "installation_uuid_unique", + "schema": "public", + "table": "installation", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "key" + ], + "nullsNotDistinct": false, + "name": "lock_key_unique", + "schema": "public", + "table": "lock", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "stripe_customer_id" + ], + "nullsNotDistinct": false, + "name": "organization_stripe_customer_id_key", + "schema": "public", + "table": "organization", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "stripe_subscription_id" + ], + "nullsNotDistinct": false, + "name": "organization_stripe_subscription_id_key", + "schema": "public", + "table": "organization", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "dev_plan_stripe_subscription_id" + ], + "nullsNotDistinct": false, + "name": "organization_dev_plan_stripe_subscription_id_key", + "schema": "public", + "table": "organization", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "referred_organization_id" + ], + "nullsNotDistinct": false, + "name": "referral_referred_organization_id_key", + "schema": "public", + "table": "referral", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "token" + ], + "nullsNotDistinct": false, + "name": "session_token_unique", + "schema": "public", + "table": "session", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "email" + ], + "nullsNotDistinct": false, + "name": "user_email_unique", + "schema": "public", + "table": "user", + "entityType": "uniques" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 931ef010d6..f5293f546c 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -757,6 +757,13 @@ "when": 1774300855725, "tag": "1774300855_dizzy_blindfold", "breakpoints": true + }, + { + "idx": 108, + "version": "8", + "when": 1774622168105, + "tag": "1774622168_youthful_the_hood", + "breakpoints": true } ] } \ No newline at end of file