diff --git a/apps/web/package.json b/apps/web/package.json index 02f6875c16..b8ba19b3a2 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -39,6 +39,7 @@ "@chat-adapter/state-memory": "^4.20.1", "@chat-adapter/state-redis": "^4.20.1", "google-auth-library": "^10.4.1", + "@kilocode/ai-gateway": "workspace:*", "@kilocode/db": "workspace:*", "@kilocode/encryption": "workspace:*", "@kilocode/kiloclaw-secret-catalog": "workspace:*", diff --git a/apps/web/src/app/admin/alerting/use-add-model-search.ts b/apps/web/src/app/admin/alerting/use-add-model-search.ts index 3e55660db6..858e4ca53e 100644 --- a/apps/web/src/app/admin/alerting/use-add-model-search.ts +++ b/apps/web/src/app/admin/alerting/use-add-model-search.ts @@ -4,7 +4,7 @@ import { useMemo } from 'react'; import { useQuery } from '@tanstack/react-query'; import type { ModelOption } from '@/app/admin/alerting/types'; import { OpenRouterModelsResponseSchema } from '@/lib/organizations/organization-types'; -import { normalizeModelId } from '@/lib/ai-gateway/model-utils'; +import { normalizeModelId } from '@kilocode/ai-gateway/model-utils'; import { z } from 'zod'; type AddModelSearchResult = { diff --git a/apps/web/src/app/admin/components/SafetyIdentifierHashGenerator.tsx b/apps/web/src/app/admin/components/SafetyIdentifierHashGenerator.tsx index 352020593b..c99f8b5a3f 100644 --- a/apps/web/src/app/admin/components/SafetyIdentifierHashGenerator.tsx +++ b/apps/web/src/app/admin/components/SafetyIdentifierHashGenerator.tsx @@ -3,7 +3,7 @@ import { useRef, useState } from 'react'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; -import { kilologHash } from '@/lib/ai-gateway/kilologHash'; +import { kilologHash } from '@kilocode/ai-gateway/kilolog-hash'; export function SafetyIdentifierHashGenerator() { const [id, setId] = useState(''); diff --git a/apps/web/src/app/admin/gateway/RoutingContent.tsx b/apps/web/src/app/admin/gateway/RoutingContent.tsx index 6688c759e7..e8c3f8dd82 100644 --- a/apps/web/src/app/admin/gateway/RoutingContent.tsx +++ b/apps/web/src/app/admin/gateway/RoutingContent.tsx @@ -9,7 +9,7 @@ import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { Label } from '@/components/ui/label'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { DEFAULT_VERCEL_PERCENTAGE, NOTE_MAX_LENGTH } from '@/lib/ai-gateway/gateway-config'; +import { DEFAULT_VERCEL_PERCENTAGE, NOTE_MAX_LENGTH } from '@kilocode/ai-gateway/gateway-config'; export function RoutingContent() { const trpc = useTRPC(); diff --git a/apps/web/src/app/api/openrouter/[...path]/route.ts b/apps/web/src/app/api/openrouter/[...path]/route.ts index b44f032b62..0b66c36c44 100644 --- a/apps/web/src/app/api/openrouter/[...path]/route.ts +++ b/apps/web/src/app/api/openrouter/[...path]/route.ts @@ -78,7 +78,7 @@ import { getToolsAvailable, getToolsUsed, } from '@/lib/ai-gateway/o11y/api-metrics.server'; -import { normalizeModelId } from '@/lib/ai-gateway/model-utils'; +import { normalizeModelId } from '@kilocode/ai-gateway/model-utils'; import { isForbiddenFreeModel } from '@/lib/ai-gateway/forbidden-free-models'; import { isCloudflareIP } from '@/lib/cloudflare-ip'; import { isKiloAutoModel } from '@/lib/ai-gateway/kilo-auto'; diff --git a/apps/web/src/app/api/openrouter/embeddings/route.ts b/apps/web/src/app/api/openrouter/embeddings/route.ts index 5575c38534..5b5cf80b89 100644 --- a/apps/web/src/app/api/openrouter/embeddings/route.ts +++ b/apps/web/src/app/api/openrouter/embeddings/route.ts @@ -30,7 +30,7 @@ import { type AnonymousUserContext, } from '@/lib/anonymous'; import { emitApiMetricsForResponse } from '@/lib/ai-gateway/o11y/api-metrics.server'; -import { normalizeModelId } from '@/lib/ai-gateway/model-utils'; +import { normalizeModelId } from '@kilocode/ai-gateway/model-utils'; import { buildUpstreamBody, type EmbeddingProxyRequest, diff --git a/apps/web/src/components/organizations/OrganizationProvidersAndModelsConfigurationCard.tsx b/apps/web/src/components/organizations/OrganizationProvidersAndModelsConfigurationCard.tsx index ae27214c9d..03b21c3bde 100644 --- a/apps/web/src/components/organizations/OrganizationProvidersAndModelsConfigurationCard.tsx +++ b/apps/web/src/components/organizations/OrganizationProvidersAndModelsConfigurationCard.tsx @@ -13,7 +13,7 @@ import { AvailableModelsDialog } from './providers-and-models/AvailableModelsDia import { useOrganizationConfiguration } from './providers-and-models/useOrganizationConfiguration'; import { useOpenRouterModelsAndProviders } from '@/app/api/openrouter/hooks'; import type { ProviderSelection } from '@/components/models/util'; -import { normalizeModelId } from '@/lib/ai-gateway/model-utils'; +import { normalizeModelId } from '@kilocode/ai-gateway/model-utils'; type OrganizationProvidersAndModelsConfigurationCardProps = { organizationId: string; diff --git a/apps/web/src/components/organizations/providers-and-models/OrganizationProvidersAndModelsPage.tsx b/apps/web/src/components/organizations/providers-and-models/OrganizationProvidersAndModelsPage.tsx index 2cb3b52d62..103a92f030 100644 --- a/apps/web/src/components/organizations/providers-and-models/OrganizationProvidersAndModelsPage.tsx +++ b/apps/web/src/components/organizations/providers-and-models/OrganizationProvidersAndModelsPage.tsx @@ -7,7 +7,7 @@ import { useUpdateOrganizationSettings, } from '@/app/api/organizations/hooks'; import { useOpenRouterModelsAndProviders } from '@/app/api/openrouter/hooks'; -import { normalizeModelId } from '@/lib/ai-gateway/model-utils'; +import { normalizeModelId } from '@kilocode/ai-gateway/model-utils'; import { useRoleTesting } from '@/contexts/RoleTestingContext'; import { OrganizationContextProvider } from '../OrganizationContext'; import { OrganizationPageHeader } from '../OrganizationPageHeader'; diff --git a/apps/web/src/components/organizations/providers-and-models/allowLists.domain.ts b/apps/web/src/components/organizations/providers-and-models/allowLists.domain.ts index b30ed29e61..1c32e1f442 100644 --- a/apps/web/src/components/organizations/providers-and-models/allowLists.domain.ts +++ b/apps/web/src/components/organizations/providers-and-models/allowLists.domain.ts @@ -1,4 +1,4 @@ -import { normalizeModelId } from '@/lib/ai-gateway/model-utils'; +import { normalizeModelId } from '@kilocode/ai-gateway/model-utils'; export type OpenRouterModelSlugSnapshot = { slug: string; diff --git a/apps/web/src/components/organizations/providers-and-models/useProvidersAndModelsAllowListsState.ts b/apps/web/src/components/organizations/providers-and-models/useProvidersAndModelsAllowListsState.ts index 63ea775035..4d3d6472b1 100644 --- a/apps/web/src/components/organizations/providers-and-models/useProvidersAndModelsAllowListsState.ts +++ b/apps/web/src/components/organizations/providers-and-models/useProvidersAndModelsAllowListsState.ts @@ -1,5 +1,5 @@ import { useCallback, useMemo, useReducer } from 'react'; -import { normalizeModelId } from '@/lib/ai-gateway/model-utils'; +import { normalizeModelId } from '@kilocode/ai-gateway/model-utils'; import { buildModelProvidersIndex, canonicalizeDenyList, diff --git a/apps/web/src/lib/ai-gateway/gateway-config.ts b/apps/web/src/lib/ai-gateway/gateway-config.ts index a5ef6d7db3..b743116baa 100644 --- a/apps/web/src/lib/ai-gateway/gateway-config.ts +++ b/apps/web/src/lib/ai-gateway/gateway-config.ts @@ -1,44 +1,9 @@ -import * as z from 'zod'; - -export const DEFAULT_VERCEL_PERCENTAGE = 50; - -const vercelRoutingPercentage = z.number().int().min(0).max(100); - -export const NOTE_MAX_LENGTH = 500; - -const note = z.string().max(NOTE_MAX_LENGTH); - -export const GatewayConfigSchema = z.object({ - vercel_routing_percentage: vercelRoutingPercentage.nullable(), - updated_at: z.string().nullable(), - updated_by: z.string().nullable(), - updated_by_email: z.string().nullable(), - note: note.nullable().default(null), -}); - -export type GatewayConfig = z.infer; - -export const DEFAULT_GATEWAY_CONFIG: GatewayConfig = { - vercel_routing_percentage: null, - updated_at: null, - updated_by: null, - updated_by_email: null, - note: null, -}; - -/** - * Schema for parsing just the percentage from Redis (used on the hot path). - * - * `vercel_routing_percentage` is nullable because clearing the override in - * the admin UI persists an explicit `null`. Callers should treat `null` as - * "no override, use DEFAULT_VERCEL_PERCENTAGE". - */ -export const GatewayPercentageSchema = z.object({ - vercel_routing_percentage: vercelRoutingPercentage.nullable(), -}); - -/** Schema for the admin set-mutation input. */ -export const GatewayConfigInputSchema = z.object({ - vercel_routing_percentage: vercelRoutingPercentage.nullable(), - note: note.nullable(), -}); +export { + DEFAULT_GATEWAY_CONFIG, + DEFAULT_VERCEL_PERCENTAGE, + GatewayConfigInputSchema, + GatewayConfigSchema, + GatewayPercentageSchema, + NOTE_MAX_LENGTH, + type GatewayConfig, +} from '@kilocode/ai-gateway/gateway-config'; diff --git a/apps/web/src/lib/ai-gateway/handleRequestLogging.ts b/apps/web/src/lib/ai-gateway/handleRequestLogging.ts index c12babb435..3cc4f295aa 100644 --- a/apps/web/src/lib/ai-gateway/handleRequestLogging.ts +++ b/apps/web/src/lib/ai-gateway/handleRequestLogging.ts @@ -3,7 +3,7 @@ import { db } from '@/lib/drizzle'; import { logExceptInTest } from '@/lib/utils.server'; import { after } from 'next/server'; import type { GatewayRequest } from '@/lib/ai-gateway/providers/openrouter/types'; -import { kilologHash } from '@/lib/ai-gateway/kilologHash'; +import { kilologHash } from '@kilocode/ai-gateway/kilolog-hash'; import { createHash } from 'crypto'; import { redisSet } from '@/lib/redis'; import { requestLogRedisKey } from '@/lib/redis-keys'; diff --git a/apps/web/src/lib/ai-gateway/kilologHash.ts b/apps/web/src/lib/ai-gateway/kilologHash.ts index b6e7acab07..4303063c9c 100644 --- a/apps/web/src/lib/ai-gateway/kilologHash.ts +++ b/apps/web/src/lib/ai-gateway/kilologHash.ts @@ -1,7 +1 @@ -export async function kilologHash(id: string): Promise { - const encoder = new TextEncoder(); - const data = encoder.encode('kilolog|' + id); - const hashBuffer = await crypto.subtle.digest('SHA-256', data); - const hashArray = Array.from(new Uint8Array(hashBuffer)); - return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); -} +export { kilologHash } from '@kilocode/ai-gateway/kilolog-hash'; diff --git a/apps/web/src/lib/ai-gateway/model-utils.ts b/apps/web/src/lib/ai-gateway/model-utils.ts index d8f9cfe72d..c99d9c9b71 100644 --- a/apps/web/src/lib/ai-gateway/model-utils.ts +++ b/apps/web/src/lib/ai-gateway/model-utils.ts @@ -1,12 +1 @@ -/** - * Shared model utilities that can be used on both client and server. - * Keep this file free of server-only dependencies. - */ - -/** - * Normalize a model ID by removing the `:free`, `:exacto`, etc. suffixes if present. - */ -export function normalizeModelId(modelId: string): string { - const colonIndex = modelId.indexOf(':'); - return colonIndex >= 0 ? modelId.substring(0, colonIndex) : modelId; -} +export { normalizeModelId } from '@kilocode/ai-gateway/model-utils'; diff --git a/apps/web/src/lib/ai-gateway/providers/openrouter/index.ts b/apps/web/src/lib/ai-gateway/providers/openrouter/index.ts index 7076f27ec5..643e6e4e27 100644 --- a/apps/web/src/lib/ai-gateway/providers/openrouter/index.ts +++ b/apps/web/src/lib/ai-gateway/providers/openrouter/index.ts @@ -18,7 +18,7 @@ import { import { AUTO_MODELS } from '@/lib/ai-gateway/kilo-auto'; // Re-export from shared module for backwards compatibility -export { normalizeModelId } from '@/lib/ai-gateway/model-utils'; +export { normalizeModelId } from '@kilocode/ai-gateway/model-utils'; function buildAutoModels(): OpenRouterModel[] { return AUTO_MODELS.map(m => ({ diff --git a/apps/web/src/lib/ai-gateway/providers/openrouter/models-by-provider-index.server.ts b/apps/web/src/lib/ai-gateway/providers/openrouter/models-by-provider-index.server.ts index 40ecd1139a..d9e2a84469 100644 --- a/apps/web/src/lib/ai-gateway/providers/openrouter/models-by-provider-index.server.ts +++ b/apps/web/src/lib/ai-gateway/providers/openrouter/models-by-provider-index.server.ts @@ -1,6 +1,6 @@ import { modelsByProvider } from '@kilocode/db/schema'; import { db } from '@/lib/drizzle'; -import { normalizeModelId } from '@/lib/ai-gateway/model-utils'; +import { normalizeModelId } from '@kilocode/ai-gateway/model-utils'; import type { NormalizedOpenRouterResponse } from '@/lib/ai-gateway/providers/openrouter/openrouter-types'; import { desc } from 'drizzle-orm'; diff --git a/apps/web/src/lib/ai-gateway/providers/vercel/index.ts b/apps/web/src/lib/ai-gateway/providers/vercel/index.ts index 62d2ab3eb0..6da15c6c81 100644 --- a/apps/web/src/lib/ai-gateway/providers/vercel/index.ts +++ b/apps/web/src/lib/ai-gateway/providers/vercel/index.ts @@ -22,7 +22,7 @@ import { createCachedFetch } from '@/lib/cached-fetch'; import { GatewayPercentageSchema, DEFAULT_VERCEL_PERCENTAGE, -} from '@/lib/ai-gateway/gateway-config'; +} from '@kilocode/ai-gateway/gateway-config'; import { VERCEL_ROUTING_REDIS_KEY } from '@/lib/redis-keys'; import { getRandomNumber } from '@/lib/ai-gateway/getRandomNumber'; import { getVercelModels } from '@/lib/ai-gateway/providers/gateway-models-cache'; diff --git a/apps/web/src/lib/model-allow.server.ts b/apps/web/src/lib/model-allow.server.ts index 66a6b59794..f098d0655a 100644 --- a/apps/web/src/lib/model-allow.server.ts +++ b/apps/web/src/lib/model-allow.server.ts @@ -1,5 +1,5 @@ import 'server-only'; -import { normalizeModelId } from '@/lib/ai-gateway/model-utils'; +import { normalizeModelId } from '@kilocode/ai-gateway/model-utils'; import { getProviderSlugsForModel } from '@/lib/ai-gateway/providers/openrouter/models-by-provider-index.server'; export type ProviderAwareAllowPredicate = (modelId: string) => Promise; diff --git a/apps/web/src/routers/admin-alerting-router.ts b/apps/web/src/routers/admin-alerting-router.ts index df8c93f36a..0da726b741 100644 --- a/apps/web/src/routers/admin-alerting-router.ts +++ b/apps/web/src/routers/admin-alerting-router.ts @@ -1,7 +1,7 @@ import { adminProcedure, createTRPCRouter } from '@/lib/trpc/init'; import { z } from 'zod'; import { fetchO11yJson, O11yRequestError } from '@/lib/ai-gateway/o11y-client'; -import { normalizeModelId } from '@/lib/ai-gateway/model-utils'; +import { normalizeModelId } from '@kilocode/ai-gateway/model-utils'; import { TRPCError } from '@trpc/server'; const AlertingConfigSchema = z.object({ diff --git a/apps/web/src/routers/admin/gateway-config-router.ts b/apps/web/src/routers/admin/gateway-config-router.ts index 81782c1a87..7b53356b1e 100644 --- a/apps/web/src/routers/admin/gateway-config-router.ts +++ b/apps/web/src/routers/admin/gateway-config-router.ts @@ -1,12 +1,12 @@ import { adminProcedure, createTRPCRouter } from '@/lib/trpc/init'; import { redisGet, redisSet } from '@/lib/redis'; import { - GatewayConfigSchema, - GatewayConfigInputSchema, DEFAULT_GATEWAY_CONFIG, -} from '@/lib/ai-gateway/gateway-config'; + GatewayConfigInputSchema, + GatewayConfigSchema, +} from '@kilocode/ai-gateway/gateway-config'; import { VERCEL_ROUTING_REDIS_KEY } from '@/lib/redis-keys'; -import type { GatewayConfig } from '@/lib/ai-gateway/gateway-config'; +import type { GatewayConfig } from '@kilocode/ai-gateway/gateway-config'; import { TRPCError } from '@trpc/server'; async function readConfig(): Promise { diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 110ce722c0..64df15f071 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -10,6 +10,8 @@ "forceConsistentCasingInFileNames": true, "paths": { "@/*": ["./src/*"], + "@kilocode/ai-gateway": ["../../packages/ai-gateway/src/index.ts"], + "@kilocode/ai-gateway/*": ["../../packages/ai-gateway/src/*"], "@kilocode/db": ["../../packages/db/src/index.ts"], "@kilocode/db/*": ["../../packages/db/src/*"], "@kilocode/encryption": ["../../packages/encryption/src/index.ts"], diff --git a/packages/ai-gateway/package.json b/packages/ai-gateway/package.json new file mode 100644 index 0000000000..6aaf04966b --- /dev/null +++ b/packages/ai-gateway/package.json @@ -0,0 +1,26 @@ +{ + "name": "@kilocode/ai-gateway", + "version": "0.0.1", + "private": true, + "type": "module", + "exports": { + ".": "./src/index.ts", + "./adapters": "./src/adapters/index.ts", + "./gateway-config": "./src/gateway-config.ts", + "./http": "./src/http/index.ts", + "./kilolog-hash": "./src/kilolog-hash.ts", + "./model-utils": "./src/model-utils.ts", + "./observability": "./src/observability/index.ts" + }, + "scripts": { + "typecheck": "tsgo --noEmit", + "lint": "pnpm -w exec oxlint --config .oxlintrc.json packages/ai-gateway/src" + }, + "dependencies": { + "zod": "catalog:" + }, + "devDependencies": { + "@typescript/native-preview": "catalog:", + "typescript": "catalog:" + } +} diff --git a/packages/ai-gateway/src/adapters/index.ts b/packages/ai-gateway/src/adapters/index.ts new file mode 100644 index 0000000000..22e88a03ce --- /dev/null +++ b/packages/ai-gateway/src/adapters/index.ts @@ -0,0 +1,15 @@ +export type BackgroundTask = Promise | (() => unknown); + +export type BackgroundScheduler = (task: BackgroundTask) => void; + +export const runDetached: BackgroundScheduler = task => { + const run = typeof task === 'function' ? task : () => task; + queueMicrotask(() => { + void Promise.resolve().then(run); + }); +}; + +export type KeyValueStore = { + get(key: string): Promise; + set(key: string, value: string, ttlSeconds?: number): Promise; +}; diff --git a/packages/ai-gateway/src/gateway-config.ts b/packages/ai-gateway/src/gateway-config.ts new file mode 100644 index 0000000000..891c5771d9 --- /dev/null +++ b/packages/ai-gateway/src/gateway-config.ts @@ -0,0 +1,36 @@ +import * as z from 'zod'; + +export const DEFAULT_VERCEL_PERCENTAGE = 50; + +const vercelRoutingPercentage = z.number().int().min(0).max(100); + +export const NOTE_MAX_LENGTH = 500; + +const note = z.string().max(NOTE_MAX_LENGTH); + +export const GatewayConfigSchema = z.object({ + vercel_routing_percentage: vercelRoutingPercentage.nullable(), + updated_at: z.string().nullable(), + updated_by: z.string().nullable(), + updated_by_email: z.string().nullable(), + note: note.nullable().default(null), +}); + +export type GatewayConfig = z.infer; + +export const DEFAULT_GATEWAY_CONFIG: GatewayConfig = { + vercel_routing_percentage: null, + updated_at: null, + updated_by: null, + updated_by_email: null, + note: null, +}; + +export const GatewayPercentageSchema = z.object({ + vercel_routing_percentage: vercelRoutingPercentage.nullable(), +}); + +export const GatewayConfigInputSchema = z.object({ + vercel_routing_percentage: vercelRoutingPercentage.nullable(), + note: note.nullable(), +}); diff --git a/packages/ai-gateway/src/http/index.ts b/packages/ai-gateway/src/http/index.ts new file mode 100644 index 0000000000..9ca79b19e3 --- /dev/null +++ b/packages/ai-gateway/src/http/index.ts @@ -0,0 +1,31 @@ +export type JsonResponseInit = ResponseInit & { + headers?: HeadersInit; +}; + +export function jsonResponse(body: unknown, init: JsonResponseInit = {}): Response { + const headers = new Headers(init.headers); + if (!headers.has('content-type')) { + headers.set('content-type', 'application/json'); + } + return new Response(JSON.stringify(body), { ...init, headers }); +} + +export function getSafeProxyOutputHeaders(response: Response): Headers { + const outputHeaders = new Headers(); + + for (const headerKey of ['date', 'content-type', 'request-id']) { + const value = response.headers.get(headerKey); + if (value) outputHeaders.set(headerKey, value); + } + outputHeaders.set('Content-Encoding', 'identity'); + + return outputHeaders; +} + +export function wrapInSafeResponse(response: Response): Response { + return new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers: getSafeProxyOutputHeaders(response), + }); +} diff --git a/packages/ai-gateway/src/index.ts b/packages/ai-gateway/src/index.ts new file mode 100644 index 0000000000..4794eddbfc --- /dev/null +++ b/packages/ai-gateway/src/index.ts @@ -0,0 +1,6 @@ +export * from './adapters'; +export * from './gateway-config'; +export * from './http'; +export * from './kilolog-hash'; +export * from './model-utils'; +export * from './observability'; diff --git a/packages/ai-gateway/src/kilolog-hash.ts b/packages/ai-gateway/src/kilolog-hash.ts new file mode 100644 index 0000000000..b6e7acab07 --- /dev/null +++ b/packages/ai-gateway/src/kilolog-hash.ts @@ -0,0 +1,7 @@ +export async function kilologHash(id: string): Promise { + const encoder = new TextEncoder(); + const data = encoder.encode('kilolog|' + id); + const hashBuffer = await crypto.subtle.digest('SHA-256', data); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); +} diff --git a/packages/ai-gateway/src/model-utils.ts b/packages/ai-gateway/src/model-utils.ts new file mode 100644 index 0000000000..5d1c642163 --- /dev/null +++ b/packages/ai-gateway/src/model-utils.ts @@ -0,0 +1,7 @@ +/** + * Normalize a model ID by removing the `:free`, `:exacto`, etc. suffixes if present. + */ +export function normalizeModelId(modelId: string): string { + const colonIndex = modelId.indexOf(':'); + return colonIndex >= 0 ? modelId.substring(0, colonIndex) : modelId; +} diff --git a/packages/ai-gateway/src/observability/index.ts b/packages/ai-gateway/src/observability/index.ts new file mode 100644 index 0000000000..7f70c7717e --- /dev/null +++ b/packages/ai-gateway/src/observability/index.ts @@ -0,0 +1,25 @@ +export type GatewayLogContext = Record; + +export type GatewayLogger = { + debug(message: string, context?: GatewayLogContext): void; + info(message: string, context?: GatewayLogContext): void; + warn(message: string, context?: GatewayLogContext): void; + error(message: string, context?: GatewayLogContext): void; +}; + +export type GatewayTelemetry = { + captureException(error: unknown, context?: GatewayLogContext): void; + captureMessage(message: string, context?: GatewayLogContext): void; +}; + +export const noopGatewayLogger: GatewayLogger = { + debug() {}, + info() {}, + warn() {}, + error() {}, +}; + +export const noopGatewayTelemetry: GatewayTelemetry = { + captureException() {}, + captureMessage() {}, +}; diff --git a/packages/ai-gateway/tsconfig.json b/packages/ai-gateway/tsconfig.json new file mode 100644 index 0000000000..ba86999f33 --- /dev/null +++ b/packages/ai-gateway/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "es2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["esnext", "dom", "dom.iterable"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "isolatedModules": true, + "resolveJsonModule": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 04df5eba8d..a94f05a784 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -466,6 +466,9 @@ importers: '@chat-adapter/state-redis': specifier: ^4.20.1 version: 4.20.1 + '@kilocode/ai-gateway': + specifier: workspace:* + version: link:../../packages/ai-gateway '@kilocode/db': specifier: workspace:* version: link:../../packages/db @@ -669,7 +672,7 @@ importers: version: 14.25.1 drizzle-orm: specifier: 'catalog:' - version: 0.45.1(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(bun-types@1.3.11)(pg@8.20.0) + version: 0.45.1(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(bun-types@1.3.12)(pg@8.20.0) event-source-polyfill: specifier: ^1.0.31 version: 1.0.31 @@ -873,11 +876,24 @@ importers: specifier: 'catalog:' version: 5.9.3 + packages/ai-gateway: + dependencies: + zod: + specifier: 'catalog:' + version: 4.3.6 + devDependencies: + '@typescript/native-preview': + specifier: 'catalog:' + version: 7.0.0-dev.20260319.1 + typescript: + specifier: 'catalog:' + version: 5.9.3 + packages/db: dependencies: drizzle-orm: specifier: 'catalog:' - version: 0.45.1(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(bun-types@1.3.11)(pg@8.20.0) + version: 0.45.1(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(bun-types@1.3.12)(pg@8.20.0) pg: specifier: ^8.20.0 version: 8.20.0 @@ -948,7 +964,7 @@ importers: version: 8.9.0 drizzle-orm: specifier: 'catalog:' - version: 0.45.1(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(bun-types@1.3.11)(pg@8.20.0) + version: 0.45.1(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(bun-types@1.3.12)(pg@8.20.0) stripe: specifier: 'catalog:' version: 19.3.0(@types/node@25.5.0) @@ -1013,7 +1029,7 @@ importers: version: link:../../packages/worker-utils drizzle-orm: specifier: 'catalog:' - version: 0.45.1(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(bun-types@1.3.11)(pg@8.20.0) + version: 0.45.1(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(bun-types@1.3.12)(pg@8.20.0) hono: specifier: ^4.12.7 version: 4.12.8 @@ -1056,7 +1072,7 @@ importers: version: 8.0.3 drizzle-orm: specifier: 'catalog:' - version: 0.45.1(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(bun-types@1.3.11)(pg@8.20.0) + version: 0.45.1(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(bun-types@1.3.12)(pg@8.20.0) jsonwebtoken: specifier: 'catalog:' version: 9.0.3 @@ -1170,7 +1186,7 @@ importers: version: 11.13.0(typescript@5.9.3) drizzle-orm: specifier: 'catalog:' - version: 0.45.1(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(bun-types@1.3.11)(pg@8.20.0) + version: 0.45.1(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(bun-types@1.3.12)(pg@8.20.0) jsonwebtoken: specifier: 'catalog:' version: 9.0.3 @@ -1237,7 +1253,7 @@ importers: version: 11.13.0(typescript@5.9.3) drizzle-orm: specifier: 'catalog:' - version: 0.45.1(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(bun-types@1.3.11)(pg@8.20.0) + version: 0.45.1(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(bun-types@1.3.12)(pg@8.20.0) hono: specifier: ^4.12.7 version: 4.12.8 @@ -1293,7 +1309,7 @@ importers: devDependencies: '@types/bun': specifier: latest - version: 1.3.11 + version: 1.3.12 '@types/node': specifier: ^20.19.37 version: 20.19.37 @@ -1308,7 +1324,7 @@ importers: devDependencies: '@types/bun': specifier: latest - version: 1.3.11 + version: 1.3.12 '@types/node': specifier: ^20.19.37 version: 20.19.37 @@ -1501,7 +1517,7 @@ importers: version: 11.13.0(typescript@5.9.3) drizzle-orm: specifier: 'catalog:' - version: 0.45.1(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(bun-types@1.3.11)(pg@8.20.0) + version: 0.45.1(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(bun-types@1.3.12)(pg@8.20.0) hono: specifier: ^4.12.7 version: 4.12.8 @@ -1587,7 +1603,7 @@ importers: version: 8.2.0 drizzle-orm: specifier: 'catalog:' - version: 0.45.1(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(bun-types@1.3.11)(pg@8.20.0) + version: 0.45.1(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(bun-types@1.3.12)(pg@8.20.0) zod: specifier: 'catalog:' version: 4.3.6 @@ -1689,7 +1705,7 @@ importers: version: link:../../packages/worker-utils drizzle-orm: specifier: 'catalog:' - version: 0.45.1(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(bun-types@1.3.11)(pg@8.20.0) + version: 0.45.1(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(bun-types@1.3.12)(pg@8.20.0) hono: specifier: ^4.12.7 version: 4.12.8 @@ -1735,7 +1751,7 @@ importers: version: 4.1.0 drizzle-orm: specifier: 'catalog:' - version: 0.45.1(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(bun-types@1.3.11)(pg@8.20.0) + version: 0.45.1(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(bun-types@1.3.12)(pg@8.20.0) jose: specifier: 'catalog:' version: 6.2.1 @@ -1769,7 +1785,7 @@ importers: version: link:../../packages/db drizzle-orm: specifier: 'catalog:' - version: 0.45.1(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(bun-types@1.3.11)(pg@8.20.0) + version: 0.45.1(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(bun-types@1.3.12)(pg@8.20.0) node-html-markdown: specifier: ^2.0.0 version: 2.0.0 @@ -1800,7 +1816,7 @@ importers: version: link:../../packages/worker-utils drizzle-orm: specifier: 'catalog:' - version: 0.45.1(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(bun-types@1.3.11)(pg@8.20.0) + version: 0.45.1(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(bun-types@1.3.12)(pg@8.20.0) expo-server-sdk: specifier: ^6.1.0 version: 6.1.0(patch_hash=7850520582b5b394397b35d1ea195192fe78589d8a6a748fe15177b818c4ed0b) @@ -1843,7 +1859,7 @@ importers: version: link:../../packages/worker-utils drizzle-orm: specifier: 'catalog:' - version: 0.45.1(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(bun-types@1.3.11)(pg@8.20.0) + version: 0.45.1(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(bun-types@1.3.12)(pg@8.20.0) hono: specifier: ^4.12.7 version: 4.12.8 @@ -1880,7 +1896,7 @@ importers: version: link:../../packages/db drizzle-orm: specifier: 'catalog:' - version: 0.45.1(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(bun-types@1.3.11)(pg@8.20.0) + version: 0.45.1(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(bun-types@1.3.12)(pg@8.20.0) workers-tagged-logger: specifier: 'catalog:' version: 1.0.0 @@ -1911,7 +1927,7 @@ importers: version: link:../../packages/db drizzle-orm: specifier: 'catalog:' - version: 0.45.1(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(bun-types@1.3.11)(pg@8.20.0) + version: 0.45.1(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(bun-types@1.3.12)(pg@8.20.0) zod: specifier: 'catalog:' version: 4.3.6 @@ -1942,7 +1958,7 @@ importers: version: 0.0.22 drizzle-orm: specifier: 'catalog:' - version: 0.45.1(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(bun-types@1.3.11)(pg@8.20.0) + version: 0.45.1(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(bun-types@1.3.12)(pg@8.20.0) hono: specifier: ^4.12.7 version: 4.12.8 @@ -1994,7 +2010,7 @@ importers: version: 10.0.1 drizzle-orm: specifier: 'catalog:' - version: 0.45.1(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(bun-types@1.3.11)(pg@8.20.0) + version: 0.45.1(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(bun-types@1.3.12)(pg@8.20.0) hono: specifier: ^4.12.7 version: 4.12.8 @@ -7302,6 +7318,9 @@ packages: '@types/bun@1.3.11': resolution: {integrity: sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg==} + '@types/bun@1.3.12': + resolution: {integrity: sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -8459,6 +8478,9 @@ packages: bun-types@1.3.11: resolution: {integrity: sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg==} + bun-types@1.3.12: + resolution: {integrity: sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA==} + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -21384,6 +21406,10 @@ snapshots: dependencies: bun-types: 1.3.11 + '@types/bun@1.3.12': + dependencies: + bun-types: 1.3.12 + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -22655,6 +22681,10 @@ snapshots: dependencies: '@types/node': 25.5.0 + bun-types@1.3.12: + dependencies: + '@types/node': 25.5.0 + bytes@3.1.2: {} cac@6.7.14: {} @@ -23518,12 +23548,12 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(bun-types@1.3.11)(pg@8.20.0): + drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(bun-types@1.3.12)(pg@8.20.0): optionalDependencies: '@cloudflare/workers-types': 4.20260313.1 '@opentelemetry/api': 1.9.0 '@types/pg': 8.18.0 - bun-types: 1.3.11 + bun-types: 1.3.12 pg: 8.20.0 dset@3.1.4: {}