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
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export {
OpenRouterSearchResponse,
OpenRouterProvider,
OpenRouterProvidersResponse,
OpenRouterApiProvider,
OpenRouterApiProvidersResponse,
NormalizedProvider,
NormalizedOpenRouterResponse,
} from '@kilocode/db/schema-types';
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
OpenRouterProvider,
} from '@/lib/ai-gateway/providers/openrouter/openrouter-types';
import {
OpenRouterApiProvidersResponse,
OpenRouterProvidersResponse,
OpenRouterSearchResponse,
} from '@/lib/ai-gateway/providers/openrouter/openrouter-types';
Expand All @@ -19,6 +20,17 @@ import PROVIDERS from '@/lib/ai-gateway/providers/provider-definitions';
import type { Provider } from '@/lib/ai-gateway/providers/types';
import type { StoredModel } from '@/lib/ai-gateway/providers/vercel/types';
import { EndpointsSchema, ModelsSchema } from '@/lib/ai-gateway/providers/vercel/types';
import { redisSet } from '@/lib/redis';

// Redis keys for the latest synced data. Consumers are not wired up yet —
// these mirror what is persisted in the `models_by_provider` table so the
// hot path can eventually avoid a database round-trip.
export const SYNC_PROVIDERS_REDIS_KEYS = {
providers: 'ai-gateway.sync-providers.providers',
openrouter: 'ai-gateway.sync-providers.openrouter',
vercel: 'ai-gateway.sync-providers.vercel',
openrouterProviders: 'ai-gateway.sync-providers.openrouter-providers',
} as const;

async function fetchGatewayModels(gateway: Provider) {
const headers = {
Expand Down Expand Up @@ -401,12 +413,76 @@ async function syncProviders() {
return result;
}

async function fetchOpenRouterApiProviders(): Promise<OpenRouterApiProvidersResponse> {
console.log('Fetching OpenRouter providers from public API endpoint...');

const response = await fetch('https://openrouter.ai/api/v1/providers', {
method: 'GET',
headers: {
'HTTP-Referer': 'https://kilocode.ai',
'X-Title': 'Kilo Code',
},
});

if (!response.ok) {
throw new Error(
`Failed to fetch OpenRouter API providers: ${response.status} ${response.statusText}`
);
}

const parsed = OpenRouterApiProvidersResponse.parse(await response.json());
console.log(`Found ${parsed.data.length} providers from /api/v1/providers`);
return parsed;
}

// Best-effort mirror of the synced data into Redis. Consumers are not wired
// up yet; failures are logged but never block the DB write.
async function mirrorToRedis(values: {
providers: NormalizedOpenRouterResponse;
openrouter: Record<string, StoredModel>;
vercel: Record<string, StoredModel>;
openrouterProviders: OpenRouterApiProvidersResponse | null;
}): Promise<void> {
const entries: [string, unknown][] = [
[SYNC_PROVIDERS_REDIS_KEYS.providers, values.providers],
[SYNC_PROVIDERS_REDIS_KEYS.openrouter, values.openrouter],
[SYNC_PROVIDERS_REDIS_KEYS.vercel, values.vercel],
];
if (values.openrouterProviders) {
entries.push([SYNC_PROVIDERS_REDIS_KEYS.openrouterProviders, values.openrouterProviders]);
}
await Promise.all(
entries.map(async ([key, value]) => {
try {
await redisSet(key, JSON.stringify(value));
} catch (err) {
console.error(`[syncAndStoreProviders] Failed to write ${key} to Redis:`, err);
}
})
);
}

export async function syncAndStoreProviders() {
const startTime = performance.now();

const openrouter_data = await fetchGatewayModels(PROVIDERS.OPENROUTER);
const vercel_data = await fetchGatewayModels(PROVIDERS.VERCEL_AI_GATEWAY);

// Best-effort: no readers yet, and a temporary failure here must not
// prevent the primary (openrouter/vercel/providers) snapshot from refreshing.
let openrouter_providers: OpenRouterApiProvidersResponse | null = null;
try {
const fetched = await fetchOpenRouterApiProviders();
if (fetched.data.length < 10) {
throw new Error(
`Suspicious: total number of OpenRouter API providers is ${fetched.data.length} < 10`
);
}
openrouter_providers = fetched;
} catch (err) {
console.error('[syncAndStoreProviders] Failed to fetch /api/v1/providers:', err);
}

const providers = await syncProviders();

if (providers.total_providers < 10) {
Expand All @@ -420,12 +496,23 @@ export async function syncAndStoreProviders() {
const result = await db.transaction(async tx => {
const results = await tx
.insert(modelsByProvider)
.values({ data: providers, openrouter: openrouter_data, vercel: vercel_data })
.values({
data: providers,
openrouter: openrouter_data,
vercel: vercel_data,
})
.returning();
await tx.delete(modelsByProvider).where(lt(modelsByProvider.id, results[0].id));
return results[0];
});

await mirrorToRedis({
providers,
openrouter: openrouter_data,
vercel: vercel_data,
openrouterProviders: openrouter_providers,
});

return {
id: result.id,
generated_at: result.data.generated_at,
Expand Down
19 changes: 19 additions & 0 deletions packages/db/src/schema-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,25 @@ export const OpenRouterProvidersResponse = z.union([
z.array(OpenRouterProvider),
]);

// Response shape for the public OpenRouter API endpoint GET /api/v1/providers.
// Distinct from the frontend `all-providers` endpoint above: this returns a
// smaller, stable public view used by external API consumers.
export type OpenRouterApiProvider = z.infer<typeof OpenRouterApiProvider>;
export const OpenRouterApiProvider = z.object({
name: z.string(),
slug: z.string(),
privacy_policy_url: z.string().nullable(),
terms_of_service_url: z.string().nullable(),
status_page_url: z.string().nullable(),
headquarters: z.string().nullable(),
datacenters: z.array(z.string()).nullable(),
});

export type OpenRouterApiProvidersResponse = z.infer<typeof OpenRouterApiProvidersResponse>;
export const OpenRouterApiProvidersResponse = z.object({
data: z.array(OpenRouterApiProvider),
});

export type NormalizedProvider = z.infer<typeof NormalizedProvider>;
export const NormalizedProvider = z.object({
name: z.string(),
Expand Down
Loading