Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/admin/alerting/use-add-model-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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('');
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/admin/gateway/RoutingContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/api/openrouter/[...path]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/api/openrouter/embeddings/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
53 changes: 9 additions & 44 deletions apps/web/src/lib/ai-gateway/gateway-config.ts
Original file line number Diff line number Diff line change
@@ -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<typeof GatewayConfigSchema>;

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';
2 changes: 1 addition & 1 deletion apps/web/src/lib/ai-gateway/handleRequestLogging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
8 changes: 1 addition & 7 deletions apps/web/src/lib/ai-gateway/kilologHash.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1 @@
export async function kilologHash(id: string): Promise<string> {
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';
13 changes: 1 addition & 12 deletions apps/web/src/lib/ai-gateway/model-utils.ts
Original file line number Diff line number Diff line change
@@ -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';
2 changes: 1 addition & 1 deletion apps/web/src/lib/ai-gateway/providers/openrouter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 => ({
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/lib/ai-gateway/providers/vercel/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/lib/model-allow.server.ts
Original file line number Diff line number Diff line change
@@ -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<boolean>;
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/routers/admin-alerting-router.ts
Original file line number Diff line number Diff line change
@@ -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({
Expand Down
8 changes: 4 additions & 4 deletions apps/web/src/routers/admin/gateway-config-router.ts
Original file line number Diff line number Diff line change
@@ -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<GatewayConfig> {
Expand Down
2 changes: 2 additions & 0 deletions apps/web/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
26 changes: 26 additions & 0 deletions packages/ai-gateway/package.json
Original file line number Diff line number Diff line change
@@ -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:"
}
}
15 changes: 15 additions & 0 deletions packages/ai-gateway/src/adapters/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export type BackgroundTask = Promise<unknown> | (() => 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<string | null>;
set(key: string, value: string, ttlSeconds?: number): Promise<void>;
};
36 changes: 36 additions & 0 deletions packages/ai-gateway/src/gateway-config.ts
Original file line number Diff line number Diff line change
@@ -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<typeof GatewayConfigSchema>;

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(),
});
31 changes: 31 additions & 0 deletions packages/ai-gateway/src/http/index.ts
Original file line number Diff line number Diff line change
@@ -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),
});
}
6 changes: 6 additions & 0 deletions packages/ai-gateway/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
7 changes: 7 additions & 0 deletions packages/ai-gateway/src/kilolog-hash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export async function kilologHash(id: string): Promise<string> {
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('');
}
7 changes: 7 additions & 0 deletions packages/ai-gateway/src/model-utils.ts
Original file line number Diff line number Diff line change
@@ -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;
}
25 changes: 25 additions & 0 deletions packages/ai-gateway/src/observability/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export type GatewayLogContext = Record<string, unknown>;

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() {},
};
Loading
Loading