diff --git a/AGENTS.md b/AGENTS.md index fff0676633..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) @@ -153,6 +155,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/apps/api/src/routes/admin.ts b/apps/api/src/routes/admin.ts index 1230ef8962..16ecbfa4c1 100644 --- a/apps/api/src/routes/admin.ts +++ b/apps/api/src/routes/admin.ts @@ -4370,6 +4370,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(), @@ -4598,6 +4599,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, @@ -4648,6 +4650,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/apps/api/src/routes/internal-models.ts b/apps/api/src/routes/internal-models.ts index e9e62cec4f..4b26d065e9 100644 --- a/apps/api/src/routes/internal-models.ts +++ b/apps/api/src/routes/internal-models.ts @@ -34,6 +34,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/api/src/routes/keys-provider.ts b/apps/api/src/routes/keys-provider.ts index 8eee47b763..e8bea31a4c 100644 --- a/apps/api/src/routes/keys-provider.ts +++ b/apps/api/src/routes/keys-provider.ts @@ -33,6 +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 + .enum(["singapore", "us-virginia", "cn-beijing"]) + .optional(), }) .nullable(), status: z.enum(["active", "inactive", "deleted"]).nullable(), @@ -60,6 +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 + .enum(["singapore", "us-virginia", "cn-beijing"]) + .optional(), }) .optional(), organizationId: z.string().min(1, "Organization ID is required"), 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/code/src/lib/api/v1.d.ts b/apps/code/src/lib/api/v1.d.ts index b17c941259..457f653e08 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; @@ -2931,6 +2945,7 @@ export interface paths { id: string; modelId: string; modelName: string; + region: string | null; providerId: string; providerName: string; status: string; @@ -3672,6 +3687,8 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; + /** @enum {string} */ + alibaba_region?: "singapore" | "us-virginia" | "cn-beijing"; } | null; /** @enum {string|null} */ status: "active" | "inactive" | "deleted" | null; @@ -3707,6 +3724,8 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; + /** @enum {string} */ + alibaba_region?: "singapore" | "us-virginia" | "cn-beijing"; }; organizationId: string; }; @@ -3735,6 +3754,8 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; + /** @enum {string} */ + alibaba_region?: "singapore" | "us-virginia" | "cn-beijing"; } | null; /** @enum {string|null} */ status: "active" | "inactive" | "deleted" | null; @@ -3851,6 +3872,8 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; + /** @enum {string} */ + alibaba_region?: "singapore" | "us-virginia" | "cn-beijing"; } | null; /** @enum {string|null} */ status: "active" | "inactive" | "deleted" | null; @@ -6983,6 +7006,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/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/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. 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/chat-helpers.e2e.ts b/apps/gateway/src/chat-helpers.e2e.ts index 760f4575ac..66c9c7b885 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 { @@ -28,11 +29,67 @@ 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; + } + // 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), + ); +} + // Parse TEST_PROVIDERS environment variable (filter by provider name) export const testProvidersEnv = process.env.TEST_PROVIDERS; export const specifiedProviders = testProvidersEnv @@ -104,11 +161,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; @@ -148,11 +203,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; @@ -166,12 +219,14 @@ 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); } - const providerModelId = `${provider.providerId}/${model.id}`; - return specifiedModels!.includes(providerModelId); + return matchesTestModel(provider.providerId, model.id, provider.region); }); }); @@ -192,14 +247,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; @@ -217,8 +276,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; } } @@ -243,8 +303,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 { @@ -259,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 }); @@ -281,7 +342,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; @@ -299,8 +364,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; } } @@ -330,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 35f21e4e6d..bdf01d7302 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 @@ -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 8601464822..966f6874df 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"; @@ -47,8 +48,10 @@ import { getProviderMetricsForCombinations, type InferSelectModel, isCachingEnabled, + metricsKey, shortid, type tables, + type ProviderMetrics, } from "@llmgateway/db"; import { applyRedactions, @@ -61,13 +64,18 @@ import { getModelStreamingSupport, hasMaxTokens, hasProviderEnvironmentToken, + hasRegionSpecificEnvKey, type ModelDefinition, models, type Provider, + type ProviderDefinition, type ProviderModelMapping, type ProviderRequestBody, providers, type WebSearchTool, + expandAllProviderRegions, + getRegionSpecificEnvValue, + stripRegionFromModelName, } from "@llmgateway/models"; import { completionsRequestSchema } from "./schemas/completions.js"; @@ -109,6 +117,7 @@ import { type RoutingAttempt, getErrorType, MAX_RETRIES, + providerRetryKey, selectNextProvider, shouldRetryRequest, } from "./tools/retry-with-fallback.js"; @@ -124,6 +133,146 @@ 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, + ); + }); +} + +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 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: { + 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" || + 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*/; @@ -216,12 +365,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(), }), @@ -447,6 +598,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 = @@ -460,10 +612,34 @@ 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), + }; + let allModelProviders = expandAllProviderRegions( + modelInfoResult.allModelProviders, + ); 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 && @@ -531,6 +707,63 @@ chat.openapi(completions, async (c) => { }); } + // 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 const organization = await findOrganizationById(project.organizationId); @@ -753,6 +986,7 @@ chat.openapi(completions, async (c) => { let usedProvider = requestedProvider; let usedModel: string = requestedModel; + let usedRegion: string | undefined = requestedRegion; let routingMetadata: RoutingMetadata | undefined; // Extract retention level for data storage cost calculation @@ -915,8 +1149,15 @@ chat.openapi(completions, async (c) => { } const candidateAllowedProviders = candidateIam.allowedProviders; + // Expand region entries into separate candidates for scoring + const expandedCandidateProviders = + project.mode === "credits" + ? filterRegionsByAvailableKeys( + expandAllProviderRegions(modelDef.providers), + ) + : 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 || @@ -926,10 +1167,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; } @@ -938,25 +1176,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; } @@ -964,16 +1196,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; } @@ -982,20 +1211,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; } @@ -1023,6 +1252,7 @@ chat.openapi(completions, async (c) => { const metricsCombinations = selectedProviders.map((p) => ({ modelId: selectedModel.id, providerId: p.providerId, + region: p.region, })); const metricsMap = await getProviderMetricsForCombinations(metricsCombinations); @@ -1036,6 +1266,7 @@ chat.openapi(completions, async (c) => { if (cheapestResult) { usedProvider = cheapestResult.provider.providerId; usedModel = cheapestResult.provider.modelName; + usedRegion = cheapestResult.provider.region; routingMetadata = { ...cheapestResult.metadata, ...(noFallback ? { noFallback: true } : {}), @@ -1075,7 +1306,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 @@ -1109,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 @@ -1125,17 +1448,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(`${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) { @@ -1160,12 +1487,15 @@ 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 if (webSearchTool) { - if ((provider as ProviderModelMapping).webSearch !== true) { + if (provider.webSearch !== true) { return false; } } @@ -1174,23 +1504,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; @@ -1198,13 +1523,22 @@ 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 const metricsCombinations = availableModelProviders.map((p) => ({ modelId: modelWithPricing.id, providerId: p.providerId, + region: p.region, })); const allMetricsMap = await getProviderMetricsForCombinations(metricsCombinations); @@ -1214,7 +1548,7 @@ chat.openapi(completions, async (c) => { const betterUptimeProviders = availableModelProviders.filter( (p) => { const providerMetrics = allMetricsMap.get( - `${modelWithPricing.id}:${p.providerId}`, + 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 @@ -1276,34 +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), 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; - if (reasoning_effort !== undefined) { - selectedMapping = sameProviderMappings.find( - (p) => (p as ProviderModelMapping).reasoning === true, - ); - } else { - selectedMapping = sameProviderMappings.find( - (p) => (p as ProviderModelMapping).reasoning !== true, - ); - } - if (selectedMapping) { - usedModel = selectedMapping.modelName; - } - } - } - if (!usedProvider) { if (iamFilteredModelProviders.length === 0) { throw new HTTPException(403, { @@ -1314,6 +1620,7 @@ chat.openapi(completions, async (c) => { if (iamFilteredModelProviders.length === 1) { usedProvider = iamFilteredModelProviders[0].providerId; usedModel = iamFilteredModelProviders[0].modelName; + usedRegion = iamFilteredModelProviders[0].region; } else { const providerIds = iamFilteredModelProviders.map((p) => p.providerId); const providerKeys = await findProviderKeysByProviders( @@ -1329,61 +1636,36 @@ chat.openapi(completions, async (c) => { .filter((p) => hasProviderEnvironmentToken(p.id as Provider)) .map((p) => p.id); - // 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 web search tool is requested, only include providers that support it - if (webSearchTool) { - if ((provider as ProviderModelMapping).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 as ProviderModelMapping).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) { - return false; - } - } - // If images are present in messages, only include providers that support vision - if (hasImages && (provider as ProviderModelMapping).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) { - 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 as ProviderModelMapping).reasoning !== true, - ); - if ( - hasNonReasoningAlternative && - (provider as ProviderModelMapping).reasoning === true - ) { - return false; - } + // 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); } - 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, }, ); @@ -1400,13 +1682,22 @@ 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 const metricsCombinations = availableModelProviders.map((p) => ({ modelId: modelWithPricing.id, providerId: p.providerId, + region: p.region, })); const metricsMap = await getProviderMetricsForCombinations(metricsCombinations); @@ -1420,6 +1711,7 @@ chat.openapi(completions, async (c) => { if (cheapestResult) { usedProvider = cheapestResult.provider.providerId; usedModel = cheapestResult.provider.modelName; + usedRegion = cheapestResult.provider.region; routingMetadata = { ...cheapestResult.metadata, ...(noFallback ? { noFallback: true } : {}), @@ -1427,10 +1719,12 @@ chat.openapi(completions, async (c) => { } else { usedProvider = availableModelProviders[0].providerId; usedModel = availableModelProviders[0].modelName; + usedRegion = availableModelProviders[0].region; } } else { usedProvider = availableModelProviders[0].providerId; usedModel = availableModelProviders[0].modelName; + usedRegion = availableModelProviders[0].region; } } } @@ -1453,39 +1747,116 @@ 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; + let directProviderRegionWasExplicit = false; + + if ( + selectionReason === "direct-provider-specified" && + requestedProvider && + requestedProvider !== "custom" + ) { + let explicitDirectRegion = requestedRegion; + if ( + !explicitDirectRegion && + (project.mode === "api-keys" || project.mode === "hybrid") + ) { + const providerKey = await findProviderKey( + project.organizationId, + requestedProvider, + requestId, + ); + explicitDirectRegion = providerKey + ? resolveExplicitRegionFromProviderKey(providerKey) + : undefined; + } + + 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, + }, + ); + + 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 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) => ({ + const metricsCombinations = routingMetadataProviders.map((p) => ({ modelId: baseModelId, providerId: p.providerId, + region: p.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 price = (p.inputPrice ?? 0) + (p.outputPrice ?? 0); - const isSelected = p.providerId === usedProvider; - return { - providerId: p.providerId, - score: isSelected ? 1 : 0, - price, - uptime: metrics?.uptime ?? 0, - latency: metrics?.averageLatency ?? 0, - throughput: metrics?.throughput ?? 0, - }; - }); + const weightedScores = + selectionReason === "direct-provider-specified" && + directProviderRegionWasExplicit + ? null + : getCheapestFromAvailableProviders( + routingMetadataProviders, + modelInfo as ModelDefinition & { + id: string; + output?: string[]; + }, + { + metricsMap, + isStreaming: stream, + }, + ); + + const allProviderScores = + weightedScores?.metadata.providerScores ?? + routingMetadataProviders.map((p) => { + const metrics = metricsMap.get( + metricsKey(baseModelId, p.providerId, p.region), + ); + return { + providerId: p.providerId, + region: p.region, + score: + selectionReason === "direct-provider-specified" && + directProviderRegionWasExplicit + ? 1 + : 0, + price: getProviderSelectionPrice(p), + uptime: metrics?.uptime ?? 0, + latency: metrics?.averageLatency ?? 0, + throughput: metrics?.throughput ?? 0, + }; + }); routingMetadata = { - availableProviders: allModelProviders.map((p) => p.providerId), + availableProviders: routingMetadataProviders.map((p) => p.providerId), selectedProvider: usedProvider, selectionReason, providerScores: allProviderScores, @@ -1495,10 +1866,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, @@ -1513,39 +1885,49 @@ chat.openapi(completions, async (c) => { ], }; } else { - finalModelInfo = + 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) { + finalModelInfo = { + ...rawFinalModelInfo, + providers: expandAllProviderRegions(rawFinalModelInfo.providers), + }; + } } // 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( - (p) => p.providerId === usedProvider && p.modelName === usedModel, + (p) => + p.providerId === usedProvider && + p.modelName === usedModel && + 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 let usedModelFormatted = formatUsedModelForDisplay( usedProvider, - baseModelName, + usedRegion ? `${baseModelName}:${usedRegion}` : baseModelName, customProviderName, ); // Store in LLMGateway format @@ -1559,7 +1941,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) { @@ -1580,7 +1962,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 - if ( project.mode === "credits" && (usedProvider === "custom" || usedProvider === "llmgateway") @@ -1618,6 +1999,14 @@ 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"); @@ -1657,6 +2046,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) { @@ -1675,6 +2072,19 @@ 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", + keyOptions: providerKey.options, + }); } else { // No API key available, fall back to credits // Check both regular credits AND dev plan credits @@ -1716,6 +2126,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, { @@ -1764,10 +2182,12 @@ 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.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 @@ -1775,6 +2195,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, { @@ -1785,17 +2208,44 @@ chat.openapi(completions, async (c) => { url = getProviderEndpoint( usedProvider, providerKey?.baseUrl ?? undefined, - usedModel, - usedProvider === "google-ai-studio" || usedProvider === "google-vertex" - ? usedToken - : undefined, + upstreamModelName, + usesGoogleQueryToken(usedProvider) ? usedToken : undefined, stream, supportsReasoning, hasExistingToolCalls, providerKey?.options ?? undefined, configIndex, isImageGeneration, + 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 ?? "none", + endpoint: url, + tokenSource: providerKey ? "db-provider-key" : "env-var", + }); } catch (error) { if (usedProvider === "llmgateway" && usedModel !== "custom") { throw new HTTPException(400, { @@ -2186,7 +2636,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.region === usedRegion, ); if ( @@ -2221,12 +2674,14 @@ 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.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.`, @@ -2247,10 +2702,12 @@ 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.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; @@ -2289,11 +2746,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 ( @@ -2362,7 +2815,7 @@ chat.openapi(completions, async (c) => { let requestBody: ProviderRequestBody = await prepareRequestBody( usedProvider, - usedModel, + upstreamModelName, messages as BaseMessage[], effectiveStream, temperature, @@ -2395,7 +2848,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.region === usedRegion, ); if ( providerMapping && @@ -2596,8 +3052,11 @@ 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); + failedProviderIds.add( + providerRetryKey(nextProvider.providerId, nextProvider.region), + ); // Don't consume a retry slot for context-resolution failures retryAttempt--; continue; @@ -2756,12 +3215,15 @@ 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), succeeded: false, }); - failedProviderIds.add(usedProvider); + failedProviderIds.add( + providerRetryKey(usedProvider, usedRegion), + ); continue; } @@ -3043,12 +3505,15 @@ 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), succeeded: false, }); - failedProviderIds.add(usedProvider); + failedProviderIds.add( + providerRetryKey(usedProvider, usedRegion), + ); continue; } @@ -3217,12 +3682,13 @@ 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), succeeded: false, }); - failedProviderIds.add(usedProvider); + failedProviderIds.add(providerRetryKey(usedProvider, usedRegion)); continue; } @@ -3246,6 +3712,7 @@ chat.openapi(completions, async (c) => { requested_provider: requestedProvider, used_model: baseModelName, used_provider: usedProvider, + ...(usedRegion && { used_region: usedRegion }), underlying_used_model: usedModel, }, }; @@ -3338,7 +3805,8 @@ 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", succeeded: true, @@ -4035,11 +4503,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, @@ -4208,9 +4672,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, @@ -4227,11 +4689,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) { @@ -4254,11 +4712,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 = @@ -4290,9 +4744,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, @@ -4354,6 +4806,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) { @@ -4656,9 +5109,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" || @@ -4795,9 +5246,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; @@ -4942,6 +5391,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, }, @@ -5082,11 +5532,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, @@ -5372,8 +5818,11 @@ 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); + failedProviderIds.add( + providerRetryKey(nextProvider.providerId, nextProvider.region), + ); // Don't consume a retry slot for context-resolution failures retryAttempt--; continue; @@ -5563,12 +6012,13 @@ 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), succeeded: false, }); - failedProviderIds.add(usedProvider); + failedProviderIds.add(providerRetryKey(usedProvider, usedRegion)); continue; } @@ -5999,12 +6449,13 @@ 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), succeeded: false, }); - failedProviderIds.add(usedProvider); + failedProviderIds.add(providerRetryKey(usedProvider, usedRegion)); continue; } @@ -6036,6 +6487,7 @@ chat.openapi(completions, async (c) => { requested_provider: requestedProvider, used_model: baseModelName, used_provider: usedProvider, + ...(usedRegion && { used_region: usedRegion }), underlying_used_model: usedModel, }, }); @@ -6077,7 +6529,8 @@ 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", succeeded: true, @@ -6305,11 +6758,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, @@ -6440,6 +6889,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 @@ -6486,9 +6936,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-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/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-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/gateway/src/chat/tools/resolve-provider-context.ts b/apps/gateway/src/chat/tools/resolve-provider-context.ts index 8f76c20f8d..e9d29f47fc 100644 --- a/apps/gateway/src/chat/tools/resolve-provider-context.ts +++ b/apps/gateway/src/chat/tools/resolve-provider-context.ts @@ -10,18 +10,21 @@ import { getProviderHeaders, prepareRequestBody, } from "@llmgateway/actions"; +import { logger } from "@llmgateway/logger"; import { type BaseMessage, + getRegionSpecificEnvValue, + getProviderEnvVar, hasMaxTokens, type ModelDefinition, type OpenAIRequestBody, type OpenAIToolInput, type Provider, - type ProviderModelMapping, type ProviderRequestBody, providers, type ToolChoiceType, type WebSearchTool, + stripRegionFromModelName, } from "@llmgateway/models"; import { getProviderEnv } from "./get-provider-env.js"; @@ -50,6 +53,7 @@ export interface ProviderContext { frequency_penalty: number | undefined; presence_penalty: number | undefined; headers: Record; + usedRegion: string | undefined; } export interface OriginalRequestParams { @@ -123,7 +127,7 @@ export function formatUsedModelForDisplay( * 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, @@ -132,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, @@ -203,25 +212,60 @@ 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.region === usedRegion, ); + // --- 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 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: ${modelRegions.join(", ")}`, + }); + } + } + + // Override token with region-specific env var if available (credits/hybrid mode) + if (usedRegion && !providerKey) { + 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; + } + } + } + // --- 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( usedProvider as Provider, providerKey?.baseUrl ?? undefined, - usedModel, - usedProvider === "google-ai-studio" || usedProvider === "google-vertex" + upstreamModelName, + usedProvider === "google-ai-studio" || + usedProvider === "google-vertex" || + usedProvider === "quartz" ? usedToken : undefined, options.stream, @@ -230,8 +274,19 @@ export async function resolveProviderContext( providerKey?.options ?? undefined, configIndex, isImageGeneration, + 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}`, @@ -249,8 +304,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; @@ -285,13 +339,11 @@ 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 = providerMappingForSelected.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; } } } @@ -303,7 +355,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, @@ -389,5 +441,6 @@ export async function resolveProviderContext( frequency_penalty, presence_penalty, headers, + usedRegion, }; } diff --git a/apps/gateway/src/chat/tools/retry-with-fallback.ts b/apps/gateway/src/chat/tools/retry-with-fallback.ts index 4d16108e7b..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; @@ -56,23 +57,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/chat/tools/transform-response-to-openai.ts b/apps/gateway/src/chat/tools/transform-response-to-openai.ts index 3dd4e565b5..d4bea3c950 100644 --- a/apps/gateway/src/chat/tools/transform-response-to-openai.ts +++ b/apps/gateway/src/chat/tools/transform-response-to-openai.ts @@ -81,12 +81,14 @@ export function transformResponseToOpenai( showUpgradeMessage = false, annotations: Annotation[] | null = null, routing: RoutingAttempt[] | null = null, + usedRegion?: string | undefined, ) { let transformedResponse = json; switch (usedProvider) { case "google-ai-studio": case "google-vertex": + case "quartz": case "obsidian": { transformedResponse = { id: `chatcmpl-${Date.now()}`, @@ -144,6 +146,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 +195,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 +268,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 +330,7 @@ export function transformResponseToOpenai( requested_provider: requestedProvider, used_model: baseModelName, used_provider: usedProvider, + ...(usedRegion && { used_region: usedRegion }), underlying_used_model: usedModel, ...(routing && { routing }), }, @@ -365,6 +371,7 @@ export function transformResponseToOpenai( requested_provider: requestedProvider, used_model: baseModelName, used_provider: usedProvider, + ...(usedRegion && { used_region: usedRegion }), underlying_used_model: usedModel, ...(routing && { routing }), }, @@ -391,6 +398,7 @@ export function transformResponseToOpenai( requested_provider: requestedProvider, used_model: baseModelName, used_provider: usedProvider, + ...(usedRegion && { used_region: usedRegion }), underlying_used_model: usedModel, ...(routing && { routing }), }; @@ -494,6 +502,7 @@ export function transformResponseToOpenai( requested_provider: requestedProvider, used_model: baseModelName, used_provider: usedProvider, + ...(usedRegion && { used_region: usedRegion }), underlying_used_model: usedModel, ...(routing && { routing }), }; @@ -581,6 +590,7 @@ export function transformResponseToOpenai( requested_provider: requestedProvider, used_model: baseModelName, used_provider: usedProvider, + ...(usedRegion && { used_region: usedRegion }), underlying_used_model: usedModel, ...(routing && { routing }), }; @@ -668,6 +678,7 @@ export function transformResponseToOpenai( requested_provider: requestedProvider, used_model: baseModelName, used_provider: usedProvider, + ...(usedRegion && { used_region: usedRegion }), underlying_used_model: usedModel, ...(routing && { routing }), }; @@ -756,6 +767,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 +821,7 @@ export function transformResponseToOpenai( requested_provider: requestedProvider, used_model: baseModelName, used_provider: usedProvider, + ...(usedRegion && { used_region: usedRegion }), underlying_used_model: usedModel, ...(routing && { routing }), }; 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/fallback.spec.ts b/apps/gateway/src/fallback.spec.ts index 75802b34c3..d145390918 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,137 @@ 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 + .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", + ); + expect(logs[0].routingMetadata?.selectionReason).toBe( + "direct-provider-specified", + ); + expect(singaporeScore).toBeTruthy(); + expect(beijingScore).toBeFalsy(); + expect( + logs[0].routingMetadata?.providerScores?.some( + (score) => score.providerId === "alibaba" && !score.region, + ), + ).toBe(false); + expect(logs[0].routingMetadata?.routing).toEqual([ + expect.objectContaining({ + provider: "alibaba", + model: "deepseek-v3.2", + region: "singapore", + status_code: 200, + succeeded: true, + }), + ]); + 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 () => { await setupKeys("openai"); 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, }); diff --git a/apps/gateway/src/lib/costs.ts b/apps/gateway/src/lib/costs.ts index b3c323f6c2..1ffe39d201 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 { @@ -328,6 +345,7 @@ export async function calculateCosts( const isGoogleProvider = provider === "google-ai-studio" || provider === "google-vertex" || + provider === "quartz" || provider === "obsidian"; const totalOutputTokens = isGoogleProvider ? calculatedCompletionTokens @@ -403,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..b79a3bee3b 100644 --- a/apps/gateway/src/models/models.spec.ts +++ b/apps/gateway/src/models/models.spec.ts @@ -250,6 +250,41 @@ describe("Models API", () => { } }); + 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); + + const json = await res.json(); + + 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); + 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.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 1ec6ac5036..f590fb263c 100644 --- a/apps/gateway/src/videos/videos.ts +++ b/apps/gateway/src/videos/videos.ts @@ -761,6 +761,10 @@ function isSoraVideoModelName(modelName: string): boolean { return modelName === "sora-2" || modelName === "sora-2-pro"; } +function isGoogleVertexVideoProvider(providerId: string): boolean { + return providerId === "google-vertex"; +} + function getVideoProviderConstraintReasons( provider: ProviderModelMapping, videoSize: VideoSizeConfig, @@ -830,7 +834,7 @@ function getVideoProviderConstraintReasons( if ( !isSoraVideoModelName(provider.modelName) && inputMode === "frames" && - provider.providerId !== "google-vertex" && + !isGoogleVertexVideoProvider(provider.providerId) && provider.providerId !== "avalanche" && provider.providerId !== "obsidian" ) { @@ -850,10 +854,10 @@ function getVideoProviderConstraintReasons( return reasons; } - if (provider.providerId === "google-vertex") { + if (isGoogleVertexVideoProvider(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") { @@ -1106,19 +1110,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 = isGoogleVertexVideoProvider(providerId) + ? getProviderEnvValue(providerId, "project") + : undefined; + const sharedVertexRegion = isGoogleVertexVideoProvider(providerId) + ? (getProviderEnvValue(providerId, "region", undefined, "us-central1") ?? + "us-central1") + : undefined; if (project.mode === "api-keys") { const providerKey = await findProviderKey( @@ -1142,10 +1140,9 @@ async function resolveProviderContext( }); } - if (providerId === "google-vertex" && !sharedVertexProjectId) { + if (isGoogleVertexVideoProvider(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 +1178,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 = isGoogleVertexVideoProvider(providerId) + ? getProviderEnvValue(providerId, "project", env.configIndex) + : undefined; + const vertexRegion = isGoogleVertexVideoProvider(providerId) + ? (getProviderEnvValue( + providerId, + "region", + env.configIndex, + "us-central1", + ) ?? "us-central1") + : undefined; + + if (isGoogleVertexVideoProvider(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 +1237,9 @@ async function resolveProviderContext( }); } - if (providerId === "google-vertex" && !sharedVertexProjectId) { + if (isGoogleVertexVideoProvider(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 +1280,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 = isGoogleVertexVideoProvider(providerId) + ? getProviderEnvValue(providerId, "project", env.configIndex) + : undefined; + const vertexRegion = isGoogleVertexVideoProvider(providerId) + ? (getProviderEnvValue( + providerId, + "region", + env.configIndex, + "us-central1", + ) ?? "us-central1") + : undefined; - if (providerId === "google-vertex" && !vertexProjectId) { + if (isGoogleVertexVideoProvider(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 +1333,8 @@ async function hasVideoProviderConfiguration( (providerKey.baseUrl ?? getProviderEnvValue(providerId, "baseUrl") ?? defaultBaseUrl) && - (providerId !== "google-vertex" || - Boolean(getProviderEnvValue("google-vertex", "project"))), + (!isGoogleVertexVideoProvider(providerId) || + Boolean(getProviderEnvValue(providerId, "project"))), ); } @@ -1357,10 +1347,10 @@ async function hasVideoProviderConfiguration( getProviderEnv(providerId).configIndex, ) ?? defaultBaseUrl) && - (providerId !== "google-vertex" || + (!isGoogleVertexVideoProvider(providerId) || Boolean( getProviderEnvValue( - "google-vertex", + providerId, "project", getProviderEnv(providerId).configIndex, ), @@ -1374,8 +1364,8 @@ async function hasVideoProviderConfiguration( (providerKey.baseUrl ?? getProviderEnvValue(providerId, "baseUrl") ?? defaultBaseUrl) && - (providerId !== "google-vertex" || - Boolean(getProviderEnvValue("google-vertex", "project"))), + (!isGoogleVertexVideoProvider(providerId) || + Boolean(getProviderEnvValue(providerId, "project"))), ); } @@ -1387,10 +1377,10 @@ async function hasVideoProviderConfiguration( getProviderEnv(providerId).configIndex, ) ?? defaultBaseUrl) && - (providerId !== "google-vertex" || + (!isGoogleVertexVideoProvider(providerId) || Boolean( getProviderEnvValue( - "google-vertex", + providerId, "project", getProviderEnv(providerId).configIndex, ), @@ -2830,19 +2820,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 @@ -3282,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, @@ -3328,14 +3318,14 @@ videos.openapi(createVideo, async (c) => { } if ( - selectedProviderContext.providerId === "google-vertex" && + isGoogleVertexVideoProvider(selectedProviderContext.providerId) && !getGoogleVertexVideoOutputBucket() && organization.retentionLevel === "none" ) { const statusCode = 400; routingAttempts.push({ provider: selectedProviderContext.providerId, - model: selectedUpstreamModelName, + model: modelInfo.id, status_code: statusCode, error_type: "client_error", succeeded: false, @@ -3350,7 +3340,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.", }); } @@ -3407,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, @@ -3417,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/playground/src/components/model-selector.tsx b/apps/playground/src/components/model-selector.tsx index f222dbfbb1..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", ); } @@ -460,17 +457,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}` @@ -595,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" ); }); } @@ -636,7 +641,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": @@ -718,13 +723,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 +811,9 @@ export function ModelSelector({ selectedProviderDef ?? getProviderForModel(selectedModel, providers) )?.name} + {selectedMapping?.region && ( + ({selectedMapping.region}) + )} @@ -1138,7 +1149,7 @@ export function ModelSelector({ ? getProviderIcon(provider.id) : null; const entryKey = `${mapping!.providerId}-${model.id}-${mapping!.modelName}`; - const providerModelValue = `${mapping!.providerId}/${model.id}`; + const providerModelValue = `${mapping!.providerId}/${mapping!.region ? mapping!.modelName : model.id}`; const disabled = isOptionDisabled?.(providerModelValue) ?? false; const disabledReason = @@ -1212,6 +1223,11 @@ export function ModelSelector({ {disabledReason ?? provider?.name} + {!disabledReason && mapping?.region && ( + + ({mapping.region}) + + )} @@ -1275,6 +1291,11 @@ export function ModelSelector({
{previewEntry.provider?.name ?? "Auto-select provider"} + {previewEntry.mapping?.region && ( + + ({previewEntry.mapping.region}) + + )}
{previewEntry.model.family} family @@ -1798,6 +1819,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/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 && ( + + + + + + + )} {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)} + + )}
))} @@ -474,6 +489,11 @@ export function LogDetailClient({ )} {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 73f886ab7c..7a7f8512bd 100644 --- a/apps/ui/src/components/dashboard/log-card.tsx +++ b/apps/ui/src/components/dashboard/log-card.tsx @@ -353,11 +353,16 @@ export function LogCard({
{log.routingMetadata.providerScores.map((score) => (
{score.providerId} + {score.region && ( + + ({score.region}) + + )} {score.failed && ( @@ -372,7 +377,7 @@ export function LogCard({ )} - + {score.score.toFixed(2)} {score.uptime !== undefined && ( @@ -391,7 +396,7 @@ export function LogCard({ )} {score.price !== undefined && ( - ${score.price.toFixed(6)} + ${score.price.toFixed(10)} )} {score.priority !== undefined && @@ -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/all-models.tsx b/apps/ui/src/components/models/all-models.tsx index 24a431f04e..9420772acc 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 && ( @@ -1786,7 +1791,7 @@ export function AllModels({
{modelsWithProviders.map((model) => ( 1 + ? tiers && tiers.length > 1 : detail.aggregated.hasTieredPricing; return ; } @@ -560,9 +562,11 @@ function renderRowValue( const summary = getProviderPricingSummary(selectedProvider, "outputPrice") ?? detail.aggregated.outputPrice; + const tiers = selectedProvider + ? selectedProvider.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 b459c29683..fac95c2a86 100644 --- a/apps/ui/src/components/models/model-provider-card.tsx +++ b/apps/ui/src/components/models/model-provider-card.tsx @@ -136,6 +136,11 @@ export function ModelProviderCard({

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

{shouldShowStabilityWarning(providerStability) && ( @@ -497,25 +502,63 @@ 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; + 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 && ( @@ -526,42 +569,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 - - )} -
- ))} + )} +
+ ))} +
-
- )} + ); + })()} {provider.perSecondPrice && Object.keys(provider.perSecondPrice).length > 0 && (
diff --git a/apps/ui/src/components/models/playground-model-selector.tsx b/apps/ui/src/components/models/playground-model-selector.tsx index 7563691bf3..f61e4a1dc0 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}` @@ -308,6 +316,12 @@ export function ModelSelector({ getProviderForModel(selectedModel, providers) )?.name } + {(() => { + const mapping = selectedModel.providers.find( + (p) => p.providerId === selectedProviderId, + ); + return mapping?.region ? ` (${mapping.region})` : null; + })()} )}
@@ -515,7 +529,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" @@ -545,6 +561,11 @@ export function ModelSelector({ {!rootOnly && ( {provider?.name} + {mapping.region && ( + + ({mapping.region}) + + )} )}
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 07fec4bffe..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 @@ -60,6 +60,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(); @@ -68,6 +69,13 @@ export function CreateProviderKeyDialog({ const createMutation = api.useMutation("post", "/keys/provider"); + const selectedProviderDef = providers.find( + (p) => p.id === selectedProvider, + ) as ProviderDefinition | undefined; + + const effectiveRegion = + (selectedRegion || selectedProviderDef?.regionConfig?.defaultRegion) ?? ""; + const availableProviders = providers.filter( (provider) => provider.id !== "llmgateway", ); @@ -126,13 +134,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; - }; + options?: Record; organizationId: string; } = { provider: selectedProvider, @@ -150,6 +152,14 @@ export function CreateProviderKeyDialog({ aws_bedrock_region_prefix: awsBedrockRegionPrefix, }; } + // Include region in options for providers that support it + if (selectedProviderDef?.regionConfig && effectiveRegion) { + payload.options = { + ...payload.options, + [selectedProviderDef.regionConfig.optionsKey]: effectiveRegion, + }; + } + if (selectedProvider === "azure") { if (!azureResource) { toast({ @@ -186,6 +196,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", + }); + }, }, ); }; @@ -202,6 +221,7 @@ export function CreateProviderKeyDialog({ setAzureApiVersion("2024-10-21"); setAzureDeploymentType("ai-foundry"); setAzureValidationModel("gpt-4o-mini"); + setSelectedRegion(""); }, 300); }; @@ -228,7 +248,10 @@ export function CreateProviderKeyDialog({
{ + setSelectedProvider(value); + setSelectedRegion(""); + }} value={selectedProvider} providers={availableProviders} loading={false} @@ -387,6 +410,28 @@ export function CreateProviderKeyDialog({ )} + {selectedProviderDef?.regionConfig && ( +
+ + +

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

+
+ )} + {selectedProvider === "custom" && ( <>
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/ui/src/components/shared/model-card.tsx b/apps/ui/src/components/shared/model-card.tsx index 144ee5d912..dc2864f92f 100644 --- a/apps/ui/src/components/shared/model-card.tsx +++ b/apps/ui/src/components/shared/model-card.tsx @@ -134,7 +134,7 @@ 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?.length ?? 0) > 1 && (

Tiered pricing available

diff --git a/apps/ui/src/components/shared/model-search.tsx b/apps/ui/src/components/shared/model-search.tsx index 34fb75a673..d6215cf658 100644 --- a/apps/ui/src/components/shared/model-search.tsx +++ b/apps/ui/src/components/shared/model-search.tsx @@ -154,10 +154,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/apps/ui/src/content/guides/autohand.md b/apps/ui/src/content/guides/autohand.md index a932498a19..0ab666768c 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 Code 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/apps/ui/src/lib/api/v1.d.ts b/apps/ui/src/lib/api/v1.d.ts index b17c941259..457f653e08 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; @@ -2931,6 +2945,7 @@ export interface paths { id: string; modelId: string; modelName: string; + region: string | null; providerId: string; providerName: string; status: string; @@ -3672,6 +3687,8 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; + /** @enum {string} */ + alibaba_region?: "singapore" | "us-virginia" | "cn-beijing"; } | null; /** @enum {string|null} */ status: "active" | "inactive" | "deleted" | null; @@ -3707,6 +3724,8 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; + /** @enum {string} */ + alibaba_region?: "singapore" | "us-virginia" | "cn-beijing"; }; organizationId: string; }; @@ -3735,6 +3754,8 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; + /** @enum {string} */ + alibaba_region?: "singapore" | "us-virginia" | "cn-beijing"; } | null; /** @enum {string|null} */ status: "active" | "inactive" | "deleted" | null; @@ -3851,6 +3872,8 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; + /** @enum {string} */ + alibaba_region?: "singapore" | "us-virginia" | "cn-beijing"; } | null; /** @enum {string|null} */ status: "active" | "inactive" | "deleted" | null; @@ -6983,6 +7006,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 7c48ee1b8e..a123592db9 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/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 c9d6e58ac6..b8ed44ed98 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,18 +98,24 @@ export async function syncProvidersAndModels() { }); if (modelDef.providers && modelDef.providers.length > 0) { - for (const mapping of modelDef.providers) { - const mappings = await database - .select() - .from(modelProviderMapping) - .where( - and( - eq(modelProviderMapping.modelId, modelDef.id), - eq(modelProviderMapping.providerId, mapping.providerId), - ), - ) - .limit(1); - const existingMapping = mappings[0]; + const expandedProviders = expandAllProviderRegions(modelDef.providers); + for (const mapping of expandedProviders) { + const mappingRegion = mapping.region; + 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 @@ -113,6 +124,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 +209,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() @@ -287,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 @@ -318,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", ), @@ -331,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 @@ -347,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", ), @@ -361,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 @@ -375,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), ), ); } diff --git a/apps/worker/src/services/video-jobs.ts b/apps/worker/src/services/video-jobs.ts index bc66d92ad0..a37226c011 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 isGoogleVertexVideoProvider(providerId: string): boolean { + return providerId === "google-vertex"; +} + function getDeterministicHash(seed: string): number { let hash = 5381; @@ -233,7 +237,7 @@ function getVideoProviderHeaders( job: VideoJobRecord, providerContext: ResolvedVideoProviderContext, ): Record { - if (job.usedProvider === "google-vertex") { + if (isGoogleVertexVideoProvider(job.usedProvider)) { return {}; } @@ -1742,7 +1746,7 @@ async function fetchUpstreamStatus( : await fetchAvalancheStatus(job, providerContext); } - if (job.usedProvider === "google-vertex") { + if (isGoogleVertexVideoProvider(job.usedProvider)) { return await fetchGoogleVertexStatus(job, providerContext); } @@ -1778,7 +1782,7 @@ async function fetchUpstreamContentMetadata( ): Promise | null> { if ( job.usedProvider === "avalanche" || - job.usedProvider === "google-vertex" + isGoogleVertexVideoProvider(job.usedProvider) ) { return null; } 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 diff --git a/ee/admin/src/lib/api/v1.d.ts b/ee/admin/src/lib/api/v1.d.ts index b17c941259..457f653e08 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; @@ -2931,6 +2945,7 @@ export interface paths { id: string; modelId: string; modelName: string; + region: string | null; providerId: string; providerName: string; status: string; @@ -3672,6 +3687,8 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; + /** @enum {string} */ + alibaba_region?: "singapore" | "us-virginia" | "cn-beijing"; } | null; /** @enum {string|null} */ status: "active" | "inactive" | "deleted" | null; @@ -3707,6 +3724,8 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; + /** @enum {string} */ + alibaba_region?: "singapore" | "us-virginia" | "cn-beijing"; }; organizationId: string; }; @@ -3735,6 +3754,8 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; + /** @enum {string} */ + alibaba_region?: "singapore" | "us-virginia" | "cn-beijing"; } | null; /** @enum {string|null} */ status: "active" | "inactive" | "deleted" | null; @@ -3851,6 +3872,8 @@ export interface paths { /** @enum {string} */ azure_deployment_type?: "openai" | "ai-foundry"; azure_validation_model?: string; + /** @enum {string} */ + alibaba_region?: "singapore" | "us-virginia" | "cn-beijing"; } | null; /** @enum {string|null} */ status: "active" | "inactive" | "deleted" | null; @@ -6983,6 +7006,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/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/packages/actions/src/get-cheapest-from-available-providers.ts b/packages/actions/src/get-cheapest-from-available-providers.ts index 7a3cdfedae..df9fb03b8f 100644 --- a/packages/actions/src/get-cheapest-from-available-providers.ts +++ b/packages/actions/src/get-cheapest-from-available-providers.ts @@ -1,12 +1,11 @@ +import { type ProviderMetrics, metricsKey } from "@llmgateway/db"; import { getProviderDefinition, - type ProviderModelMapping, type AvailableModelProvider, type ModelWithPricing, + type ProviderModelMapping, } 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; @@ -210,12 +210,10 @@ 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.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 @@ -258,12 +256,17 @@ export function getCheapestFromAvailableProviders< for (const provider of stableProviders) { const providerInfo = modelWithPricing.providers.find( - (p) => p.providerId === provider.providerId, + (p) => + p.providerId === provider.providerId && p.region === provider.region, ); const price = getProviderSelectionPrice(providerInfo, videoPricing); - 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, @@ -376,6 +379,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, @@ -405,6 +409,7 @@ function selectByPriceOnly( const providerPrices: Array<{ providerId: string; + region?: string; price: number; effectivePrice: number; priority: number; @@ -412,7 +417,8 @@ function selectByPriceOnly( for (const provider of stableProviders) { const providerInfo = modelWithPricing.providers.find( - (p) => p.providerId === provider.providerId, + (p) => + p.providerId === provider.providerId && p.region === provider.region, ); const totalPrice = getProviderSelectionPrice(providerInfo, videoPricing); @@ -423,6 +429,7 @@ function selectByPriceOnly( providerPrices.push({ providerId: provider.providerId, + region: provider.region, price: totalPrice, effectivePrice, priority, @@ -440,6 +447,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/actions/src/get-provider-endpoint.ts b/packages/actions/src/get-provider-endpoint.ts index 63fee6d014..c8466d0741 100644 --- a/packages/actions/src/get-provider-endpoint.ts +++ b/packages/actions/src/get-provider-endpoint.ts @@ -1,5 +1,7 @@ import { models, + providers, + type ProviderDefinition, type ProviderModelMapping, type ProviderId, getProviderEnvValue, @@ -8,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 */ @@ -22,6 +73,7 @@ export function getProviderEndpoint( providerKeyOptions?: ProviderKeyOptions, configIndex?: number, imageGenerations?: boolean, + region?: string, ): string { let modelName = model; if (model && model !== "custom") { @@ -37,6 +89,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 { @@ -61,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) { @@ -99,14 +172,17 @@ export function getProviderEndpoint( case "moonshot": url = "https://api.moonshot.ai"; break; - case "alibaba": + case "alibaba": { + const alibabaBaseUrl = + regionBaseUrl ?? "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; @@ -198,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 b93e611f53..cea06bca0b 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 + ] ?? providerDef?.regionConfig?.defaultRegion) + : 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,19 +176,41 @@ 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, 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 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, 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 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/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 68cfd33dc7..4dca00fe1b 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -426,6 +426,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"; } export const providerKey = pgTable( @@ -534,6 +535,7 @@ export const log = pgTable( selectionReason?: string; providerScores?: Array<{ providerId: string; + region?: string; score: number; uptime?: number; latency?: number; @@ -550,6 +552,7 @@ export const log = pgTable( routing?: Array<{ provider: string; model: string; + region?: string; status_code: number; error_type: string; succeeded: boolean; @@ -682,6 +685,7 @@ export const videoJob = pgTable( selectionReason?: string; providerScores?: Array<{ providerId: string; + region?: string; score: number; uptime?: number; latency?: number; @@ -698,6 +702,7 @@ export const videoJob = pgTable( routing?: Array<{ provider: string; model: string; + region?: string; status_code: number; error_type: string; succeeded: boolean; @@ -981,6 +986,7 @@ export const modelProviderMapping = pgTable( .notNull() .references(() => provider.id, { onDelete: "cascade" }), modelName: text().notNull(), + region: text(), inputPrice: decimal(), outputPrice: decimal(), cachedInputPrice: decimal(), @@ -1030,7 +1036,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 5a92af2d0d..e9cbd1497b 100644 --- a/packages/models/src/models.ts +++ b/packages/models/src/models.ts @@ -47,6 +47,63 @@ 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. + * When absent, falls back to the mapping-level inputPrice. + */ + inputPrice?: number; + /** + * Price per output token in USD for this region. + * When absent, falls back to the mapping-level outputPrice. + */ + 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[]; + /** + * 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; + /** + * 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. + */ + maxOutput?: number; +} + export interface ProviderModelMapping { providerId: (typeof providers)[number]["id"]; modelName: string; @@ -199,6 +256,19 @@ export interface ProviderModelMapping { * When true, requests are routed to a provider-specific image generation endpoint. */ imageGenerations?: boolean; + /** + * 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[]; /** * Whether this model uses a dedicated video generation API. * When true, requests are routed to a provider-specific video generation endpoint. diff --git a/packages/models/src/models/alibaba.ts b/packages/models/src/models/alibaba.ts index d6169070de..fa4f54f4a1 100644 --- a/packages/models/src/models/alibaba.ts +++ b/packages/models/src/models/alibaba.ts @@ -15,9 +15,17 @@ export const alibabaModels = [ discount: 0.2, inputPrice: 1.6 / 1e6, outputPrice: 6.4 / 1e6, + regions: [ + { id: "singapore" }, + { + id: "cn-beijing", + inputPrice: 0.345 / 1e6, + outputPrice: 1.377 / 1e6, + }, + ], requestPrice: 0, - contextSize: 131072, - maxOutput: 32000, + contextSize: 32768, + maxOutput: 8192, streaming: true, vision: true, tools: true, @@ -39,9 +47,18 @@ export const alibabaModels = [ discount: 0.2, inputPrice: 1.6 / 1e6, outputPrice: 6.4 / 1e6, + regions: [ + { id: "singapore" }, + { + 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,6 +96,60 @@ export const alibabaModels = [ cachedInputPrice: 0.24 / 1e6, }, ], + regions: [ + { id: "singapore" }, + { + 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 128K", + upToTokens: 128000, + inputPrice: 0.115 / 1e6, + outputPrice: 0.287 / 1e6, + cachedInputPrice: 0.023 / 1e6, + }, + { + name: "128K-256K", + upToTokens: 256000, + inputPrice: 0.345 / 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, + }, + ], + }, + ], requestPrice: 0, contextSize: 131072, maxOutput: 32000, @@ -119,6 +190,38 @@ export const alibabaModels = [ cachedInputPrice: 0.24 / 1e6, }, ], + regions: [ + { id: "singapore" }, + { + id: "cn-beijing", + inputPrice: 0.115 / 1e6, + outputPrice: 0.287 / 1e6, + cachedInputPrice: 0.023 / 1e6, + pricingTiers: [ + { + name: "Up to 128K", + upToTokens: 128000, + inputPrice: 0.115 / 1e6, + outputPrice: 0.287 / 1e6, + cachedInputPrice: 0.023 / 1e6, + }, + { + name: "128K-256K", + upToTokens: 256000, + inputPrice: 0.345 / 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, + }, + ], + }, + ], requestPrice: 0, contextSize: 1000000, maxOutput: 32000, @@ -143,20 +246,54 @@ export const alibabaModels = [ inputPrice: 0.05 / 1e6, outputPrice: 0.4 / 1e6, cachedInputPrice: 0.01 / 1e6, - pricingTiers: [ + regions: [ { - name: "Up to 256K", - upToTokens: 256000, - inputPrice: 0.05 / 1e6, - outputPrice: 0.4 / 1e6, - cachedInputPrice: 0.01 / 1e6, + id: "singapore", + 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, + }, + ], }, { - name: "Over 256K", - upToTokens: Infinity, - inputPrice: 0.25 / 1e6, - outputPrice: 2.0 / 1e6, - cachedInputPrice: 0.05 / 1e6, + id: "cn-beijing", + inputPrice: 0.022 / 1e6, + outputPrice: 0.216 / 1e6, + cachedInputPrice: 0.0044 / 1e6, + pricingTiers: [ + { + name: "Up to 128K", + upToTokens: 128000, + inputPrice: 0.022 / 1e6, + outputPrice: 0.216 / 1e6, + cachedInputPrice: 0.0044 / 1e6, + }, + { + name: "128K-256K", + upToTokens: 256000, + inputPrice: 0.087 / 1e6, + outputPrice: 0.861 / 1e6, + cachedInputPrice: 0.017 / 1e6, + }, + { + name: "256K-1M", + upToTokens: 1000000, + inputPrice: 0.173 / 1e6, + outputPrice: 1.721 / 1e6, + cachedInputPrice: 0.035 / 1e6, + }, + ], }, ], requestPrice: 0, @@ -205,6 +342,14 @@ export const alibabaModels = [ discount: 0.2, inputPrice: 0.05 / 1e6, outputPrice: 0.2 / 1e6, + regions: [ + { id: "singapore" }, + { + id: "cn-beijing", + inputPrice: 0.044 / 1e6, + outputPrice: 0.087 / 1e6, + }, + ], requestPrice: 0, contextSize: 1000000, maxOutput: 8192, @@ -1103,6 +1248,28 @@ export const alibabaModels = [ discount: 0.2, inputPrice: 0.6 / 1e6, outputPrice: 3.6 / 1e6, + regions: [ + { id: "singapore" }, + { + id: "cn-beijing", + 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, contextSize: 262144, maxOutput: 65536, @@ -1250,6 +1417,14 @@ export const alibabaModels = [ discount: 0.2, inputPrice: 0.8 / 1e6, outputPrice: 2.4 / 1e6, + regions: [ + { id: "singapore" }, + { + id: "cn-beijing", + inputPrice: 0.23 / 1e6, + outputPrice: 0.574 / 1e6, + }, + ], requestPrice: 0, contextSize: 131072, maxOutput: 8192, @@ -1274,8 +1449,9 @@ 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, maxOutput: 8192, @@ -1300,34 +1476,111 @@ export const alibabaModels = [ inputPrice: 0.3 / 1e6, outputPrice: 1.5 / 1e6, cachedInputPrice: 0.06 / 1e6, - pricingTiers: [ + regions: [ { - name: "Up to 32K", - upToTokens: 32000, - inputPrice: 0.3 / 1e6, - outputPrice: 1.5 / 1e6, - cachedInputPrice: 0.06 / 1e6, + id: "singapore", + 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, + }, + ], }, { - name: "32K-128K", - upToTokens: 128000, - inputPrice: 0.5 / 1e6, - outputPrice: 2.5 / 1e6, - cachedInputPrice: 0.1 / 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, + }, + ], }, { - 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: "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.216 / 1e6, + outputPrice: 0.861 / 1e6, + cachedInputPrice: 0.043 / 1e6, + }, + { + name: "128K-256K", + upToTokens: 256000, + inputPrice: 0.359 / 1e6, + outputPrice: 1.434 / 1e6, + cachedInputPrice: 0.072 / 1e6, + }, + { + name: "256K-1M", + upToTokens: 1000000, + inputPrice: 0.717 / 1e6, + outputPrice: 3.584 / 1e6, + cachedInputPrice: 0.143 / 1e6, + }, + ], }, ], requestPrice: 0, @@ -1354,27 +1607,90 @@ export const alibabaModels = [ inputPrice: 0.2 / 1e6, outputPrice: 1.6 / 1e6, cachedInputPrice: 0.04 / 1e6, - pricingTiers: [ + regions: [ { - name: "Up to 32K", - upToTokens: 32000, - inputPrice: 0.2 / 1e6, - outputPrice: 1.6 / 1e6, - cachedInputPrice: 0.04 / 1e6, + id: "singapore", + 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: "128K-256K", + upToTokens: 256000, + inputPrice: 0.6 / 1e6, + outputPrice: 4.8 / 1e6, + cachedInputPrice: 0.12 / 1e6, + }, + ], }, { - name: "32K-128K", - upToTokens: 128000, - inputPrice: 0.3 / 1e6, - outputPrice: 2.4 / 1e6, - cachedInputPrice: 0.06 / 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, + }, + ], }, { - name: "Over 128K", - upToTokens: Infinity, - inputPrice: 0.6 / 1e6, - outputPrice: 4.8 / 1e6, - cachedInputPrice: 0.12 / 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.15 / 1e6, + cachedInputPrice: 0.043 / 1e6, + }, + { + name: "128K-256K", + upToTokens: 256000, + inputPrice: 0.43 / 1e6, + outputPrice: 4.301 / 1e6, + cachedInputPrice: 0.086 / 1e6, + }, + ], }, ], requestPrice: 0, @@ -1401,27 +1717,90 @@ export const alibabaModels = [ inputPrice: 0.05 / 1e6, outputPrice: 0.4 / 1e6, cachedInputPrice: 0.01 / 1e6, - pricingTiers: [ + regions: [ { - name: "Up to 32K", - upToTokens: 32000, - inputPrice: 0.05 / 1e6, - outputPrice: 0.4 / 1e6, - cachedInputPrice: 0.01 / 1e6, + id: "singapore", + 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: "128K-256K", + upToTokens: 256000, + inputPrice: 0.12 / 1e6, + outputPrice: 0.96 / 1e6, + cachedInputPrice: 0.024 / 1e6, + }, + ], }, { - name: "32K-128K", - upToTokens: 128000, - inputPrice: 0.075 / 1e6, - outputPrice: 0.6 / 1e6, - cachedInputPrice: 0.015 / 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, + }, + ], }, { - name: "Over 128K", - upToTokens: Infinity, - inputPrice: 0.12 / 1e6, - outputPrice: 0.96 / 1e6, - cachedInputPrice: 0.024 / 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.043 / 1e6, + outputPrice: 0.43 / 1e6, + cachedInputPrice: 0.0086 / 1e6, + }, + { + name: "128K-256K", + upToTokens: 256000, + inputPrice: 0.086 / 1e6, + outputPrice: 0.859 / 1e6, + cachedInputPrice: 0.017 / 1e6, + }, + ], }, ], requestPrice: 0, @@ -1550,27 +1929,90 @@ export const alibabaModels = [ inputPrice: 1.2 / 1e6, outputPrice: 6.0 / 1e6, cachedInputPrice: 0.24 / 1e6, - pricingTiers: [ + regions: [ { - name: "Up to 32K", - upToTokens: 32000, - inputPrice: 1.2 / 1e6, - outputPrice: 6.0 / 1e6, - cachedInputPrice: 0.24 / 1e6, + id: "singapore", + 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: "128K-252K", + upToTokens: 252000, + inputPrice: 3.0 / 1e6, + outputPrice: 15.0 / 1e6, + cachedInputPrice: 0.6 / 1e6, + }, + ], }, { - name: "32K-128K", - upToTokens: 128000, - inputPrice: 2.4 / 1e6, - outputPrice: 12.0 / 1e6, - cachedInputPrice: 0.48 / 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, + }, + ], }, { - name: "Over 128K", - upToTokens: Infinity, - inputPrice: 3.0 / 1e6, - outputPrice: 15.0 / 1e6, - cachedInputPrice: 0.6 / 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.574 / 1e6, + outputPrice: 2.294 / 1e6, + cachedInputPrice: 0.115 / 1e6, + }, + { + name: "128K-252K", + upToTokens: 252000, + inputPrice: 1.004 / 1e6, + outputPrice: 4.014 / 1e6, + cachedInputPrice: 0.201 / 1e6, + }, + ], }, ], requestPrice: 0, diff --git a/packages/models/src/models/deepseek.ts b/packages/models/src/models/deepseek.ts index d3a3be5b08..da3bacea8e 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,11 +147,49 @@ export const deepseekModels = [ contextSize: 163800, maxOutput: undefined, streaming: true, - vision: true, + vision: false, tools: true, 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", + 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, + reasoning: false, + streaming: true, + vision: false, + tools: true, + jsonOutput: true, + }, { providerId: "bytedance", modelName: "deepseek-v3-2-251201", diff --git a/packages/models/src/models/google.ts b/packages/models/src/models/google.ts index 74e64e1c6b..7b6a3be68a 100644 --- a/packages/models/src/models/google.ts +++ b/packages/models/src/models/google.ts @@ -695,6 +695,43 @@ export const googleModels = [ jsonOutput: true, jsonOutputSchema: true, }, + { + test: "skip", + providerId: "quartz", + modelName: "gemini-3.1-pro-preview", + discount: 0.2, + 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 +1024,32 @@ export const googleModels = [ jsonOutput: true, jsonOutputSchema: true, }, + { + 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, + 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 +1328,32 @@ export const googleModels = [ jsonOutput: true, jsonOutputSchema: true, }, + { + test: "skip", + providerId: "quartz", + modelName: "gemini-3-pro-image-preview", + discount: 0.2, + 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", 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/openai.ts b/packages/models/src/models/openai.ts index 9ad2d69214..39df47deed 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", + modelName: "gpt-5.4-mini", + inputPrice: 0.75 / 1e6, + outputPrice: 4.5 / 1e6, + cachedInputPrice: 0.075 / 1e6, + discount: 0.3, + 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", + modelName: "gpt-5.4-nano", + inputPrice: 0.2 / 1e6, + outputPrice: 1.25 / 1e6, + cachedInputPrice: 0.02 / 1e6, + discount: 0.3, + 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, + }, ], }, { diff --git a/packages/models/src/models/zai.ts b/packages/models/src/models/zai.ts index f323d460df..882bd7a820 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", @@ -322,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: [ { @@ -453,6 +484,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", @@ -520,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: [ { @@ -629,6 +691,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", @@ -745,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: [ { diff --git a/packages/models/src/provider.ts b/packages/models/src/provider.ts index e32b90c87b..648f06f03f 100644 --- a/packages/models/src/provider.ts +++ b/packages/models/src/provider.ts @@ -103,3 +103,50 @@ 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] + ); +} + +/** + * 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, + region: string, +): boolean { + const baseEnvVar = getProviderEnvVar(provider); + if (!baseEnvVar) { + return false; + } + const regionSuffix = region.toUpperCase().replace(/-/g, "_"); + 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; +} diff --git a/packages/models/src/providers.ts b/packages/models/src/providers.ts index 14c41a8729..6504961895 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 = [ @@ -117,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", @@ -223,12 +261,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", diff --git a/packages/models/src/region-helpers.ts b/packages/models/src/region-helpers.ts new file mode 100644 index 0000000000..338afcf814 --- /dev/null +++ b/packages/models/src/region-helpers.ts @@ -0,0 +1,58 @@ +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 + * 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, +): ProviderModelMapping[] { + if (!mapping.regions || mapping.regions.length === 0) { + return [mapping]; + } + + const { regions: _, ...base } = mapping; + + 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 regionEntries; +} + +/** + * 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..7530deee4c 100644 --- a/packages/models/src/types.ts +++ b/packages/models/src/types.ts @@ -319,9 +319,12 @@ export interface ModelWithPricing { providerId: string; inputPrice?: number; outputPrice?: number; + perSecondPrice?: Record; supportedParameters?: string[]; modelName: string; discount?: number; + region?: string; + stability?: string; }>; } @@ -329,6 +332,7 @@ export interface ModelWithPricing { export interface AvailableModelProvider { providerId: string; modelName: string; + region?: string; } // Function type definitions diff --git a/packages/shared/src/components/model-selector.tsx b/packages/shared/src/components/model-selector.tsx index 11abb6a396..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" ); }); } @@ -557,7 +553,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": 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, 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 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