Skip to content
Open
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
4 changes: 2 additions & 2 deletions apps/web/src/app/api/openrouter/[...path]/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { getBalanceAndOrgSettings } from '@/lib/organizations/organization-usage
import { classifyAbuse } from '@/lib/ai-gateway/abuse-service';
import { getProvider } from '@/lib/ai-gateway/providers/get-provider';
import { upstreamRequest } from '@/lib/ai-gateway/providers/upstream-request';
import { getOpenRouterModels } from '@/lib/ai-gateway/providers/gateway-models-cache';
import { getOpenRouterModelsFromRedis } from '@/lib/ai-gateway/providers/gateway-models-cache';
import { emitApiMetricsForResponse } from '@/lib/ai-gateway/o11y/api-metrics.server';
import { accountForMicrodollarUsage } from '@/lib/ai-gateway/llm-proxy-helpers';
import { redisClient } from '@/lib/redis';
Expand Down Expand Up @@ -86,7 +86,7 @@ const mockedGetBalanceAndOrgSettings = jest.mocked(getBalanceAndOrgSettings);
const mockedClassifyAbuse = jest.mocked(classifyAbuse);
const mockedGetProvider = jest.mocked(getProvider);
const mockedUpstreamRequest = jest.mocked(upstreamRequest);
const mockedGetOpenRouterModels = jest.mocked(getOpenRouterModels);
const mockedGetOpenRouterModels = jest.mocked(getOpenRouterModelsFromRedis);
const mockedEmitApiMetricsForResponse = jest.mocked(emitApiMetricsForResponse);
const mockedAccountForMicrodollarUsage = jest.mocked(accountForMicrodollarUsage);
const mockedRedisGet = jest.mocked(redisClient.get);
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/lib/ai-gateway/auto-model/resolution.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, expect, it, jest } from '@jest/globals';

jest.mock('@/lib/ai-gateway/providers/gateway-models-cache', () => ({
getOpenRouterModels: jest.fn(async () => new Set<string>()),
getOpenRouterModelsFromRedis: jest.fn(async () => new Set<string>()),
}));

jest.mock('@/lib/kiloclaw/setup-promo', () => ({
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/lib/ai-gateway/auto-model/resolution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import {
findKiloExclusiveModel,
isKiloExclusiveFreeModel,
} from '@/lib/ai-gateway/models';
import { getOpenRouterModels } from '@/lib/ai-gateway/providers/gateway-models-cache';
import { getOpenRouterModelsFromRedis } from '@/lib/ai-gateway/providers/gateway-models-cache';
import PROVIDERS from '@/lib/ai-gateway/providers/provider-definitions';
import type { ProviderId } from '@/lib/ai-gateway/providers/types';
import {
Expand Down Expand Up @@ -79,7 +79,7 @@ function resolveMode(modeHeader: string | null, featureHeader: FeatureValue | nu
export async function getAutoFreeCandidates(
apiKind: GatewayRequest['kind'] | null
): Promise<ReadonlyArray<string>> {
const openRouterModels = await getOpenRouterModels();
const openRouterModels = await getOpenRouterModelsFromRedis();
const candidates = new Set<string>();
for (const model of autoFreeModels) {
if (isKiloExclusiveFreeModel(model)) {
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/lib/ai-gateway/byok/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ import {
import { isCodestralModel } from '@/lib/ai-gateway/providers/mistral';
import { mapModelIdToVercel } from '@/lib/ai-gateway/providers/vercel/mapModelIdToVercel';
import type { BYOKResult } from '@/lib/ai-gateway/providers/types';
import { getVercelModelsMetadata } from '@/lib/ai-gateway/providers/gateway-models-cache';
import { getVercelModelsMetadataFromDatabase } from '@/lib/ai-gateway/providers/gateway-models-cache';
import type { OpenRouterModel } from '@/lib/organizations/organization-types';
import { isKiloExclusiveModel } from '@/lib/ai-gateway/models';

export async function getModelUserByokProviders(modelId: string): Promise<UserByokProviderId[]> {
const vercelModelMetadata = await getVercelModelsMetadata();
const vercelModelMetadata = await getVercelModelsMetadataFromDatabase();
if (Object.keys(vercelModelMetadata).length === 0) {
console.error('[getModelUserByokProviders] no Vercel model metadata for model %s', modelId);
return [];
Expand Down
30 changes: 18 additions & 12 deletions apps/web/src/lib/ai-gateway/providers/gateway-models-cache.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,42 @@
import { StoredModelSchema, type StoredModel } from '@kilocode/db';
import { modelsByProvider, StoredModelSchema, type StoredModel } from '@kilocode/db';
import { desc } from 'drizzle-orm';
import * as z from 'zod';
import { redisClient } from '@/lib/redis';
import { createCachedFetch } from '@/lib/cached-fetch';
import { GATEWAY_METADATA_REDIS_KEYS, vercelInferenceProvidersRedisKey } from '@/lib/redis-keys';
import type { RedisKey } from '@/lib/redis-keys';
import { readDb } from '@/lib/drizzle';

export type StoredModelMap = Record<string, StoredModel>;

const StoredModelMapSchema = z.record(z.string(), StoredModelSchema);

function createStoredModelsFetcher(redisKey: RedisKey, name: string) {
function createStoredModelsFromDatabaseFetcher(provider: 'openrouter' | 'vercel', name: string) {
return createCachedFetch<StoredModelMap>(
async () => {
const raw = JSON.parse((await redisClient.get<string>(redisKey)) ?? 'null');
if (!raw || typeof raw !== 'object' || Object.keys(raw).length === 0) {
console.debug(`[getGatewayModels] no ${name} models found in Redis`);
const [row] = await readDb
.select({ models: modelsByProvider[provider] })
.from(modelsByProvider)
.orderBy(desc(modelsByProvider.id))
.limit(1);
if (!row?.models || Object.keys(row.models).length === 0) {
console.debug(`[getGatewayModels] no ${name} models found in the database`);
return {};
}
return StoredModelMapSchema.parse(raw);
return StoredModelMapSchema.parse(row.models);
},
600_000,
{}
);
}

export const getVercelModelsMetadata = createStoredModelsFetcher(
GATEWAY_METADATA_REDIS_KEYS.vercelModels,
export const getVercelModelsMetadataFromDatabase = createStoredModelsFromDatabaseFetcher(
'vercel',
'Vercel'
);

export const getOpenRouterModelsMetadata = createStoredModelsFetcher(
GATEWAY_METADATA_REDIS_KEYS.openrouterModels,
export const getOpenRouterModelsMetadataFromDatabase = createStoredModelsFromDatabaseFetcher(
'openrouter',
'OpenRouter'
);

Expand Down Expand Up @@ -95,12 +101,12 @@ function createModelIdsFetcher(redisKey: RedisKey, name: string) {
);
}

export const getVercelModels = createModelIdsFetcher(
export const getVercelModelsFromRedis = createModelIdsFetcher(
GATEWAY_METADATA_REDIS_KEYS.vercelModelIds,
'Vercel'
);

export const getOpenRouterModels = createModelIdsFetcher(
export const getOpenRouterModelsFromRedis = createModelIdsFetcher(
GATEWAY_METADATA_REDIS_KEYS.openrouterModelIds,
'OpenRouter'
);
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { isFableModel } from '@/lib/ai-gateway/providers/anthropic.constants';
import { KILO_AUTO_EFFICIENT_MODEL } from '@/lib/ai-gateway/auto-model';

jest.mock('@/lib/ai-gateway/providers/gateway-models-cache', () => ({
getOpenRouterModelsMetadata: jest.fn(() => Promise.resolve({})),
getOpenRouterModelsMetadataFromDatabase: jest.fn(() => Promise.resolve({})),
}));

const originalFetch = global.fetch;
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/lib/ai-gateway/providers/openrouter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { isForbiddenFreeModel } from '@/lib/ai-gateway/forbidden-free-models';
import { getGatewayOpenCodeSettings } from '@/lib/ai-gateway/providers/model-settings';
import { AUTO_MODELS, type AutoModel } from '@/lib/ai-gateway/auto-model';
import { ATTRIBUTION_HEADERS } from '@/lib/ai-gateway/providers/openrouter/attribution-headers';
import { getOpenRouterModelsMetadata } from '@/lib/ai-gateway/providers/gateway-models-cache';
import { getOpenRouterModelsMetadataFromDatabase } from '@/lib/ai-gateway/providers/gateway-models-cache';
import { getPreferredProviderOrder } from '@/lib/ai-gateway/providers/apply-provider-specific-logic';
import { normalizeInferenceProviderId } from '@/lib/ai-gateway/providers/openrouter/inference-provider-id';
import { getTerminalBenchSummaries, terminalBenchFor } from '@/lib/model-stats/terminal-bench';
Expand Down Expand Up @@ -127,7 +127,7 @@ const unavailableModels = [

async function enhancedModelList(models: OpenRouterModel[]) {
const autoModels = buildAutoModels();
const endpointsMetadata = await getOpenRouterModelsMetadata();
const endpointsMetadata = await getOpenRouterModelsMetadataFromDatabase();
const summaries = await getTerminalBenchSummaries();
const enhancedModels = await Promise.all(
models
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,8 @@ async function syncProviders(
return result;
}

const MODEL_METADATA_REDIS_TTL_SECONDS = 7 * 24 * 60 * 60;

async function mirrorToRedis(values: {
providers: NormalizedOpenRouterResponse;
openrouter: Record<string, StoredModel>;
Expand All @@ -411,7 +413,16 @@ async function mirrorToRedis(values: {
entries.push([GATEWAY_METADATA_REDIS_KEYS.openrouterProviders, values.openrouterProviders]);
}
await Promise.all([
...entries.map(([key, value]) => redisClient.set(key, JSON.stringify(value))),
...entries.map(([key, value]) => {
const serializedValue = JSON.stringify(value);
if (
key === GATEWAY_METADATA_REDIS_KEYS.openrouterModels ||
key === GATEWAY_METADATA_REDIS_KEYS.vercelModels
) {
return redisClient.set(key, serializedValue, { ex: MODEL_METADATA_REDIS_TTL_SECONDS });
}
return redisClient.set(key, serializedValue);
}),
mirrorVercelInferenceProvidersToRedis(values.vercel),
]);
}
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/lib/ai-gateway/providers/vercel/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { VERCEL_ROUTING_REDIS_KEY } from '@/lib/redis-keys';
import { getRandomNumber } from '@/lib/ai-gateway/getRandomNumber';
import {
getCachedVercelInferenceProviderIdsForModel,
getVercelModels,
getVercelModelsFromRedis,
} from '@/lib/ai-gateway/providers/gateway-models-cache';
import type { AnthropicProviderOptions } from '@ai-sdk/anthropic';
import { isDeepseekModel } from '@/lib/ai-gateway/providers/deepseek';
Expand Down Expand Up @@ -104,7 +104,7 @@ export async function shouldRouteToVercel(
return false;
}

const vercelModels = await getVercelModels();
const vercelModels = await getVercelModelsFromRedis();
const vercelModelId = mapModelIdToVercel(requestedModel, isReasoningExplicitlyDisabled(request));
if (!vercelModels.has(vercelModelId)) {
console.debug(`[shouldRouteToVercel] model not found in Vercel model list`);
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/lib/cached-fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* Returns the cached value immediately if it's younger than `ttlMs`,
* otherwise calls `fetcher` to refresh. If the fetcher throws (e.g.
* Redis timeout), returns the last-known-good cached value, or
* a network timeout), returns the last-known-good cached value, or
* `defaultValue` if nothing has been cached yet.
*/
export function createCachedFetch<T>(fetcher: () => Promise<T>, ttlMs: number, defaultValue: T) {
Expand Down
13 changes: 7 additions & 6 deletions apps/web/src/routers/byok-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ import {
VercelUserByokInferenceProviderIdSchema,
} from '@/lib/ai-gateway/providers/openrouter/inference-provider-id';
import {
getVercelModelsMetadata,
getOpenRouterModelsMetadata,
getVercelModelsMetadataFromDatabase,
getOpenRouterModelsMetadataFromDatabase,
} from '@/lib/ai-gateway/providers/gateway-models-cache';
import { createGateway, generateText } from 'ai';
import PROVIDERS from '@/lib/ai-gateway/providers/provider-definitions';
Expand Down Expand Up @@ -88,19 +88,20 @@ async function testCodestralApiKey(apiKey: string): Promise<{ success: boolean;

async function fetchSupportedModels(): Promise<Record<string, string[]>> {
const [vercelModelMetadata, openRouterModelMetadata] = await Promise.all([
getVercelModelsMetadata(),
getOpenRouterModelsMetadata(),
getVercelModelsMetadataFromDatabase(),
getOpenRouterModelsMetadataFromDatabase(),
]);

if (Object.keys(vercelModelMetadata).length === 0) {
throw new Error(
'No Vercel model metadata in Redis, use the admin panel at ' + MODELS_BY_PROVIDER_ADMIN_URL
'No Vercel model metadata in the database, use the admin panel at ' +
MODELS_BY_PROVIDER_ADMIN_URL
);
}

if (Object.keys(openRouterModelMetadata).length === 0) {
throw new Error(
'No OpenRouter model metadata in Redis, use the admin panel at ' +
'No OpenRouter model metadata in the database, use the admin panel at ' +
MODELS_BY_PROVIDER_ADMIN_URL
);
}
Expand Down