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 @@ -9,7 +9,6 @@ import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';

type SyncResult = {
id: number;
generated_at: string;
total_providers: number;
total_models: number;
Expand Down Expand Up @@ -42,8 +41,8 @@ export function SyncProvidersContent() {

<p className="text-muted-foreground">
Fetches provider and model data from OpenRouter and the Vercel AI Gateway, then stores the
result in the database. This runs automatically via cron but can be triggered manually here.
Mainly intended for local development use.
result in Redis. This runs automatically via cron but can be triggered manually here. Mainly
intended for local development use.
</p>

<Card>
Expand All @@ -70,7 +69,6 @@ export function SyncProvidersContent() {
<div className="rounded-lg border p-4 text-sm">
<p className="font-medium">Last sync result</p>
<ul className="text-muted-foreground mt-2 space-y-1">
<li>Row ID: {lastResult.id}</li>
<li>Generated at: {new Date(lastResult.generated_at).toLocaleString()}</li>
<li>Providers: {lastResult.total_providers}</li>
<li>Models: {lastResult.total_models}</li>
Expand Down
19 changes: 7 additions & 12 deletions apps/web/src/app/api/openrouter/models-by-provider/route.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,19 @@
import { connection, NextResponse } from 'next/server';
import { MODELS_BY_PROVIDER_ADMIN_URL, modelsByProvider } from '@kilocode/db/schema';
import { desc } from 'drizzle-orm';
import { db } from '@/lib/drizzle';
import { MODELS_BY_PROVIDER_ADMIN_URL } from '@kilocode/db/schema';
import { redisGet } from '@/lib/redis';
import { GATEWAY_METADATA_REDIS_KEYS } from '@/lib/redis-keys';

export async function GET() {
await connection();

const result = await db
.select()
.from(modelsByProvider)
.orderBy(desc(modelsByProvider.id))
.limit(1);

if (!result || result.length === 0) {
const raw = await redisGet(GATEWAY_METADATA_REDIS_KEYS.allProviders);
if (raw === null) {
throw new Error(
'No models data found in database. Use the admin panel at ' + MODELS_BY_PROVIDER_ADMIN_URL
'No models data found in Redis. Use the admin panel at ' + MODELS_BY_PROVIDER_ADMIN_URL
);
}

return NextResponse.json(result[0].data, {
return NextResponse.json(JSON.parse(raw), {
headers: {
'Cache-Control': `max-age=0, s-maxage=60`,
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { modelsByProvider } from '@kilocode/db/schema';
import { db } from '@/lib/drizzle';
import { normalizeModelId } from '@/lib/ai-gateway/model-utils';
import type { NormalizedOpenRouterResponse } from '@/lib/ai-gateway/providers/openrouter/openrouter-types';
import { desc } from 'drizzle-orm';
import { redisGet } from '@/lib/redis';
import { GATEWAY_METADATA_REDIS_KEYS } from '@/lib/redis-keys';

export type ModelIdToProviderSlugsIndex = ReadonlyMap<string, ReadonlySet<string>>;

Expand Down Expand Up @@ -85,22 +84,18 @@ export function createModelsByProviderIndexLoader(options: ProviderIndexLoaderOp
};
}

export async function fetchLatestModelsByProviderSnapshotFromDb(): Promise<
export async function fetchLatestModelsByProviderSnapshotFromRedis(): Promise<
NormalizedOpenRouterResponse | undefined
> {
const result = await db
.select({ data: modelsByProvider.data })
.from(modelsByProvider)
.orderBy(desc(modelsByProvider.id))
.limit(1);

return result[0]?.data;
const raw = await redisGet(GATEWAY_METADATA_REDIS_KEYS.allProviders);
if (raw === null) return undefined;
return JSON.parse(raw) as NormalizedOpenRouterResponse;
}

const DEFAULT_TTL_MS = 30_000;

const defaultLoader = createModelsByProviderIndexLoader({
fetchSnapshot: fetchLatestModelsByProviderSnapshotFromDb,
fetchSnapshot: fetchLatestModelsByProviderSnapshotFromRedis,
ttlMs: DEFAULT_TTL_MS,
nowMs: () => Date.now(),
});
Expand Down
32 changes: 11 additions & 21 deletions apps/web/src/lib/ai-gateway/providers/openrouter/sync-providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@ import {
OpenRouterProvidersResponse,
OpenRouterSearchResponse,
} from '@/lib/ai-gateway/providers/openrouter/openrouter-types';
import { modelsByProvider } from '@kilocode/db/schema';
import { db } from '@/lib/drizzle';
import { lt } from 'drizzle-orm';
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';
Expand Down Expand Up @@ -297,7 +294,14 @@ async function mirrorToRedis(values: {
if (values.openrouterProviders) {
entries.push([GATEWAY_METADATA_REDIS_KEYS.openrouterProviders, values.openrouterProviders]);
}
await Promise.all(entries.map(([key, value]) => redisSet(key, JSON.stringify(value))));
const results = await Promise.all(
entries.map(([key, value]) => redisSet(key, JSON.stringify(value)))
);
if (results.some(ok => !ok)) {
throw new Error(
'mirrorToRedis: one or more Redis writes did not persist (is REDIS_URL configured?)'
);
}
}

export async function syncAndStoreProviders() {
Expand All @@ -323,19 +327,6 @@ export async function syncAndStoreProviders() {
throw new Error(`Suspicious: total number of models is ${providers.total_models} < 100`);
}

const result = await db.transaction(async tx => {
const results = await tx
.insert(modelsByProvider)
.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({
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Sync can silently succeed without storing a snapshot

redisSet() returns false when REDIS_URL is unset, and mirrorToRedis() ignores those return values. Now that the SQL write is gone, this mutation can report success while persisting nothing, which leaves /api/openrouter/models-by-provider and the Redis-backed readers with no data immediately afterward. Consider failing the sync when any Redis write returns false, or keeping the existing SQL fallback until Redis is guaranteed in every environment.

providers,
openrouter: openrouter_data,
Expand All @@ -344,10 +335,9 @@ export async function syncAndStoreProviders() {
});

return {
id: result.id,
generated_at: result.data.generated_at,
total_models: result.data.total_models,
total_providers: result.data.total_providers,
generated_at: providers.generated_at,
total_models: providers.total_models,
total_providers: providers.total_providers,
time: performance.now() - startTime,
};
}
10 changes: 10 additions & 0 deletions packages/db/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2421,6 +2421,16 @@ export const contributor_champion_sync_state = pgTable('contributor_champion_syn

export type ContributorChampionSyncState = typeof contributor_champion_sync_state.$inferSelect;

/**
* @deprecated Obsolete. The canonical storage for the provider/model
* metadata this table used to hold is now Redis (see
* `GATEWAY_METADATA_REDIS_KEYS` in `apps/web/src/lib/redis-keys.ts`).
*
* No code writes to this table anymore. The definition is kept only so the
* Drizzle schema still matches the existing database; the table can be
* dropped in a future migration once we're confident nothing external
* depends on it.
*/
export const modelsByProvider = pgTable('models_by_provider', {
id: serial().notNull().primaryKey(),
data: jsonb('data').$type<NormalizedOpenRouterResponse>().notNull(),
Expand Down
Loading