diff --git a/apps/web/src/app/api/public/leaderboard-model-provider-usage/route.ts b/apps/web/src/app/api/public/leaderboard-model-provider-usage/route.ts new file mode 100644 index 0000000000..bcc7248e18 --- /dev/null +++ b/apps/web/src/app/api/public/leaderboard-model-provider-usage/route.ts @@ -0,0 +1,174 @@ +import { z } from 'zod'; + +import { normalizePublicInferenceProvider } from '@/lib/public-inference-provider'; +import { + createPublicSnowflakeReport, + publicSnowflakeReportOptions, +} from '@/lib/public-snowflake-report'; +import { LEADERBOARD_MODEL_PROVIDER_USAGE_REDIS_KEY } from '@/lib/redis-keys'; + +const MINIMUM_TOKENS = 10_000_000; +const MAXIMUM_ERROR_RATE = 0.5; + +const LEADERBOARD_MODEL_PROVIDER_USAGE_QUERY = ` +select + mu.requested_model as "model" + , mu.inference_provider as "provider" + , sum(mu.total_cost_microdollars) as "sum_cost" + , sum(mu.total_input_tokens) + sum(mu.total_output_tokens) as "sum_tokens" + , sum(mu.total_input_tokens) as "sum_input_tokens" + , sum(mu.total_cache_hit_tokens) as "sum_cache_hit_tokens" + , sum(mu.request_count) as "sum_request_count" + , sum(mu.error_count) as "sum_error_count" +from kilo_dw.dbt_prod.microdollar_usage_daily as mu +where + mu.usage_date >= dateadd(week, -1, current_date()) + and mu.usage_date < current_date() + and mu.provider not in ('custom', 'direct-byok') + and mu.total_output_tokens > 0 + and mu.is_user_byok = false + and mu.requested_model not ilike '%mercury-edit%' +group by 1, 2 +order by 4 desc; +`; + +const modelProviderUsageSchema = z.array( + z.object({ + model: z.string().min(1), + provider: z.string().min(1), + sumTokens: z.number(), + costPerRequest: z.number(), + costPerMillionTokens: z.number(), + cacheRatio: z.number(), + errorRate: z.number(), + percentageOfModel: z.number(), + }) +); + +type ModelProviderUsage = z.infer[number]; + +type AggregatedUsage = { + model: string; + provider: string; + sumCost: number; + sumTokens: number; + sumInputTokens: number; + sumCacheHitTokens: number; + sumRequestCount: number; + sumErrorCount: number; +}; + +function normalizeModel(model: string): string { + const withoutTrailingSlashes = model.replace(/\/+$/, ''); + const slashIndex = withoutTrailingSlashes.lastIndexOf('/'); + const withoutProvider = withoutTrailingSlashes.startsWith('openrouter/') + ? withoutTrailingSlashes + : slashIndex >= 0 + ? withoutTrailingSlashes.slice(slashIndex + 1) + : withoutTrailingSlashes; + const colonIndex = withoutProvider.indexOf(':'); + + if (colonIndex < 0 || withoutProvider.slice(colonIndex) === ':free') { + return withoutProvider; + } + + return withoutProvider.slice(0, colonIndex); +} + +function ratio(numerator: number, denominator: number): number { + return denominator === 0 ? 0 : numerator / denominator; +} + +function parseAndAggregateUsage(rows: string[][]): ModelProviderUsage[] { + const usageByModelAndProvider = new Map(); + + for (const row of rows) { + const [rawModel, rawProvider, ...rawAggregates] = row; + const [sumCost, sumTokens, sumInputTokens, sumCacheHitTokens, sumRequestCount, sumErrorCount] = + rawAggregates.map(Number); + const model = normalizeModel(rawModel?.trim() || ''); + const provider = normalizePublicInferenceProvider(rawProvider?.trim() || 'unknown'); + + if ( + !model || + ![ + sumCost, + sumTokens, + sumInputTokens, + sumCacheHitTokens, + sumRequestCount, + sumErrorCount, + ].every(Number.isFinite) + ) { + throw new Error('Snowflake returned an invalid leaderboard model provider usage row'); + } + + if (provider.key === 'other') { + continue; + } + + const aggregationKey = `${model}\0${provider.key}`; + const existing = usageByModelAndProvider.get(aggregationKey); + + if (existing) { + existing.sumCost += sumCost; + existing.sumTokens += sumTokens; + existing.sumInputTokens += sumInputTokens; + existing.sumCacheHitTokens += sumCacheHitTokens; + existing.sumRequestCount += sumRequestCount; + existing.sumErrorCount += sumErrorCount; + } else { + usageByModelAndProvider.set(aggregationKey, { + model, + provider: provider.name, + sumCost, + sumTokens, + sumInputTokens, + sumCacheHitTokens, + sumRequestCount, + sumErrorCount, + }); + } + } + + const aggregatedUsage = [...usageByModelAndProvider.values()]; + const totalTokensByModel = new Map(); + + for (const usage of aggregatedUsage) { + totalTokensByModel.set( + usage.model, + (totalTokensByModel.get(usage.model) ?? 0) + usage.sumTokens + ); + } + + return aggregatedUsage + .filter(usage => usage.sumTokens >= MINIMUM_TOKENS) + .sort( + (left, right) => + right.sumTokens - left.sumTokens || + left.model.localeCompare(right.model) || + left.provider.localeCompare(right.provider) + ) + .map(usage => ({ + model: usage.model, + provider: usage.provider, + sumTokens: usage.sumTokens, + costPerRequest: ratio(usage.sumCost, usage.sumRequestCount - usage.sumErrorCount) / 1e6, + costPerMillionTokens: ratio(usage.sumCost, usage.sumTokens), + cacheRatio: ratio(usage.sumCacheHitTokens, usage.sumInputTokens), + errorRate: ratio(usage.sumErrorCount, usage.sumRequestCount), + percentageOfModel: + ratio(usage.sumTokens, totalTokensByModel.get(usage.model) ?? 0) * 100, + })).filter(usage => usage.errorRate < MAXIMUM_ERROR_RATE); +} + +export const GET = createPublicSnowflakeReport({ + cacheKey: LEADERBOARD_MODEL_PROVIDER_USAGE_REDIS_KEY, + errorMessage: 'Failed to fetch leaderboard model provider usage', + parseRows: parseAndAggregateUsage, + query: LEADERBOARD_MODEL_PROVIDER_USAGE_QUERY, + schema: modelProviderUsageSchema, + source: 'public-leaderboard-model-provider-usage-api', +}); + +export const OPTIONS = publicSnowflakeReportOptions; diff --git a/apps/web/src/app/api/public/leaderboard-model-usage/route.ts b/apps/web/src/app/api/public/leaderboard-model-usage/route.ts new file mode 100644 index 0000000000..5c438a1eba --- /dev/null +++ b/apps/web/src/app/api/public/leaderboard-model-usage/route.ts @@ -0,0 +1,72 @@ +import { z } from 'zod'; + +import { + createPublicSnowflakeReport, + publicSnowflakeReportOptions, +} from '@/lib/public-snowflake-report'; +import { LEADERBOARD_MODEL_USAGE_REDIS_KEY } from '@/lib/redis-keys'; + +const LEADERBOARD_MODEL_USAGE_QUERY = ` +select + to_char(mu.usage_date, 'YYYY-MM-DD') as usage_date + , coalesce(mu.requested_model, mu.model) as "model" + , case + when mu.feature ilike '%claw%' then 'kiloclaw' + when mu.mode ilike '%code%' then 'code' + when mu.mode ilike '%review%' then 'review' + when mu.mode ilike '%plan%' then 'plan' + when mu.mode ilike '%ask%' then 'ask' + when mu.mode ilike '%debug%' then 'debug' + else null + end as mode + , sum(coalesce(mu.total_input_tokens, 0)) + sum(coalesce(mu.total_output_tokens, 0)) as "tokens" +from kilo_dw.dbt_prod.microdollar_usage_daily as mu +where + mu.usage_date >= dateadd(week, -1, current_date()) + and mu.usage_date < current_date() + and mu.total_input_tokens > 0 + and mu.provider != 'custom' +group by 1, 2, 3 +order by 1 desc, 4 desc; +`; + +const modelUsageSchema = z.array( + z.object({ + usageDate: z.string(), + model: z.string().min(1), + mode: z.enum(['kiloclaw', 'code', 'review', 'plan', 'ask', 'debug']).nullable(), + tokens: z.number(), + }) +); + +type ModelUsage = z.infer[number]; + +function parseModelUsage(rows: string[][]): ModelUsage[] { + return rows.map(row => { + const [usageDate, model, modeValue, tokenValue] = row; + const tokens = Number(tokenValue); + const mode = modeValue || null; + + if (!usageDate || !model || !Number.isFinite(tokens)) { + throw new Error('Snowflake returned an invalid model usage row'); + } + + return modelUsageSchema.element.parse({ + usageDate, + model, + mode, + tokens, + }); + }); +} + +export const GET = createPublicSnowflakeReport({ + cacheKey: LEADERBOARD_MODEL_USAGE_REDIS_KEY, + errorMessage: 'Failed to fetch leaderboard model usage', + parseRows: parseModelUsage, + query: LEADERBOARD_MODEL_USAGE_QUERY, + schema: modelUsageSchema, + source: 'public-leaderboard-model-usage-api', +}); + +export const OPTIONS = publicSnowflakeReportOptions; diff --git a/apps/web/src/app/api/public/leaderboard-provider-race/route.test.ts b/apps/web/src/app/api/public/leaderboard-provider-race/route.test.ts new file mode 100644 index 0000000000..a35c35a24f --- /dev/null +++ b/apps/web/src/app/api/public/leaderboard-provider-race/route.test.ts @@ -0,0 +1,109 @@ +import { describe, test, expect, jest } from '@jest/globals'; +import type { resolveSnowflakeConfig } from '@/lib/snowflake'; +import type { GET as RouteGet } from './route'; + +jest.mock('@sentry/nextjs', () => ({ captureException: jest.fn() })); + +jest.mock('@/lib/redis', () => ({ + redisClient: { + get: jest.fn<() => Promise>().mockResolvedValue(null), + set: jest.fn<() => Promise>().mockResolvedValue('OK'), + }, +})); + +jest.mock('@/lib/snowflake', () => ({ + resolveSnowflakeConfig: jest.fn(), + executeSnowflakeStatement: jest.fn(), +})); + +type SnowflakeConfig = ReturnType; + +const FAKE_CONFIG = { accountHost: 'test.snowflakecomputing.com' } as SnowflakeConfig; + +// The route builds a module-level in-process cache (1h TTL) at import time, so +// each test loads it in an isolated module registry to keep that cache from +// leaking between cases. +async function loadGet(opts: { + config?: SnowflakeConfig; + rows?: string[][]; + statementError?: Error; +}): Promise { + let GET!: typeof RouteGet; + await jest.isolateModulesAsync(async () => { + const snowflake = await import('@/lib/snowflake'); + const config = opts.config === undefined ? FAKE_CONFIG : opts.config; + jest.mocked(snowflake.resolveSnowflakeConfig).mockReturnValue(config); + if (opts.statementError) { + jest.mocked(snowflake.executeSnowflakeStatement).mockRejectedValue(opts.statementError); + } else { + jest.mocked(snowflake.executeSnowflakeStatement).mockResolvedValue(opts.rows ?? []); + } + ({ GET } = await import('./route')); + }); + return GET; +} + +describe('GET /api/public/leaderboard-provider-race', () => { + test('returns 503 when Snowflake is not configured', async () => { + const GET = await loadGet({ config: null }); + + const response = await GET(); + + expect(response.status).toBe(503); + expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*'); + }); + + test('returns 502 when the Snowflake query fails (e.g. is_open_weights backfill not landed)', async () => { + const GET = await loadGet({ statementError: new Error('invalid identifier IS_OPEN_WEIGHTS') }); + + const response = await GET(); + + expect(response.status).toBe(502); + }); + + test('maps explicit is_open_weights to booleans and NULL/unknown to null', async () => { + const GET = await loadGet({ + rows: [ + ['2026-07-14', 'Anthropic', 'false', '12345'], + ['2026-07-14', 'Alibaba', 'true', '6789'], + // The SQL API returns NULL cells as null at runtime despite the + // string[] row type; unmapped models land here. + ['2026-07-14', 'other', null, '999'] as unknown as string[], + // A future model_dim mapping gap must not be silently bucketed as closed. + ['2026-07-14', 'Mystery', 'unexpected', '111'], + ], + }); + + const response = await GET(); + + expect(response.status).toBe(200); + expect(await response.json()).toEqual([ + { weekStart: '2026-07-14', provider: 'Anthropic', isOpenWeights: false, tokens: 12345 }, + { weekStart: '2026-07-14', provider: 'Alibaba', isOpenWeights: true, tokens: 6789 }, + { weekStart: '2026-07-14', provider: 'other', isOpenWeights: null, tokens: 999 }, + { weekStart: '2026-07-14', provider: 'Mystery', isOpenWeights: null, tokens: 111 }, + ]); + expect(response.headers.get('Cache-Control')).toContain('public'); + expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*'); + }); + + test('returns 502 when a row is malformed', async () => { + const GET = await loadGet({ rows: [['2026-07-14', 'Anthropic', 'true', 'not-a-number']] }); + + const response = await GET(); + + expect(response.status).toBe(502); + }); +}); + +describe('OPTIONS /api/public/leaderboard-provider-race', () => { + test('returns 204 with CORS headers', async () => { + const { OPTIONS } = await import('./route'); + + const response = OPTIONS(); + + expect(response.status).toBe(204); + expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*'); + expect(response.headers.get('Access-Control-Allow-Methods')).toBe('GET'); + }); +}); diff --git a/apps/web/src/app/api/public/leaderboard-provider-race/route.ts b/apps/web/src/app/api/public/leaderboard-provider-race/route.ts new file mode 100644 index 0000000000..89c1fdd025 --- /dev/null +++ b/apps/web/src/app/api/public/leaderboard-provider-race/route.ts @@ -0,0 +1,91 @@ +import { z } from 'zod'; + +import { + createPublicSnowflakeReport, + publicSnowflakeReportOptions, +} from '@/lib/public-snowflake-report'; +import { LEADERBOARD_PROVIDER_RACE_REDIS_KEY } from '@/lib/redis-keys'; + +// Weekly token volume per model lab, from a fixed start date through the most +// recent complete week. Grouped at week x model_provider_company x +// is_open_weights so a single payload drives both the per-lab "race" view and +// an open-weight vs proprietary toggle. model_provider_company and +// is_open_weights are maintained in the dbt model_dim seed (kilocode-dbt), so +// the lab mapping lives in one place rather than being re-derived here. +// The partial current week is excluded so every returned week is complete. +// Infrastructure-artifact BYOK model ids are excluded here (not in the base +// dbt model, which stays complete): URI/path ids (s3://, gs://, ...), ckpt: +// checkpoint refs, and HuggingFace class names (e.g. AtlasForCausalLM). These +// are customers pointing Kilo at their own checkpoints/endpoints - single-org +// noise that cannot be attributed to a model lab and would otherwise dominate +// the "other" bucket. +const LEADERBOARD_PROVIDER_RACE_QUERY = ` +select + to_char(date_trunc('week', ud.usage_date), 'YYYY-MM-DD') as week_start + , ud.model_provider_company as provider + , ud.is_open_weights + , sum(ud.total_tokens) as tokens +from kilo_dw.dbt_prod.usage_daily as ud +where + ud.usage_date >= '2025-07-01' + and date_trunc('week', ud.usage_date) < date_trunc('week', current_date()) + and ud.total_tokens > 0 + and not ( + ud.model like '%://%' + or ud.model ilike 'ckpt:%' + or ud.model ilike '%ForCausalLM' + ) +group by 1, 2, 3 +order by 1, 4 desc; +`; + +const providerRaceSchema = z.array( + z.object({ + weekStart: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), + provider: z.string().min(1), + isOpenWeights: z.boolean().nullable(), + tokens: z.number(), + }) +); + +type ProviderRace = z.infer; + +// is_open_weights is maintained in the dbt model_dim seed and is NULL for +// unmapped models (the ones bucketed as provider = 'other'). Only the explicit +// 'true'/'false' strings map to a boolean; anything else (NULL from the SQL API, +// or a future mapping gap) becomes null so the client can distinguish "unknown" +// from a confirmed closed-weight lab instead of silently bucketing gaps as closed. +function parseOpenWeights(raw: string): boolean | null { + if (raw === 'true') return true; + if (raw === 'false') return false; + return null; +} + +function parseProviderRace(rows: string[][]): ProviderRace { + return rows.map(row => { + const [weekStart, provider, rawOpenWeights, rawTokens] = row; + const tokens = Number(rawTokens); + + if (!weekStart || !provider || !Number.isFinite(tokens)) { + throw new Error('Snowflake returned an invalid provider race row'); + } + + return providerRaceSchema.element.parse({ + weekStart, + provider, + isOpenWeights: parseOpenWeights(rawOpenWeights), + tokens, + }); + }); +} + +export const GET = createPublicSnowflakeReport({ + cacheKey: LEADERBOARD_PROVIDER_RACE_REDIS_KEY, + errorMessage: 'Failed to fetch leaderboard provider race', + parseRows: parseProviderRace, + query: LEADERBOARD_PROVIDER_RACE_QUERY, + schema: providerRaceSchema, + source: 'public-leaderboard-provider-race-api', +}); + +export const OPTIONS = publicSnowflakeReportOptions; diff --git a/apps/web/src/lib/public-inference-provider.ts b/apps/web/src/lib/public-inference-provider.ts new file mode 100644 index 0000000000..1f35ef6ed9 --- /dev/null +++ b/apps/web/src/lib/public-inference-provider.ts @@ -0,0 +1,71 @@ +const PROVIDER_ALIASES: Record = { + amazonbedrock: 'bedrock', + custom: 'other', + directbyok: 'other', + googleaistudio: 'google', + martian: 'stealth', + seed: 'bytedance', + togetherai: 'together', + unknown: 'other', + vertex: 'google', + vertexanthropic: 'google', +}; + +const PROVIDER_NAMES: Record = { + ai21: 'AI21', + aionlabs: 'Aion Labs', + akashml: 'Akash ML', + arceeai: 'Arcee AI', + atlascloud: 'Atlas Cloud', + bedrock: 'Amazon Bedrock', + bytedance: 'ByteDance', + dekallm: 'DekaLLM', + deepinfra: 'DeepInfra', + deepseek: 'DeepSeek', + digitalocean: 'DigitalOcean', + fireworks: 'Fireworks AI', + friendli: 'Friendli AI', + gmicloud: 'GMI Cloud', + google: 'Google', + inception: 'Inception', + inceptron: 'Inceptron', + ionet: 'IO.net', + minimax: 'MiniMax', + modelrun: 'ModelRun', + moonshotai: 'Moonshot AI', + nexagi: 'Nex AGI', + nextbit: 'NextBit', + novita: 'Novita AI', + nvidia: 'NVIDIA', + openai: 'OpenAI', + openinference: 'OpenInference', + other: 'Other', + sambanova: 'SambaNova', + siliconflow: 'SiliconFlow', + stealth: 'Stealth', + stepfun: 'StepFun', + streamlake: 'StreamLake', + together: 'Together AI', + unknown: 'Unknown', + wandb: 'Weights & Biases', + xai: 'SpaceXAI', + zai: 'Z.ai', +}; + +export type NormalizedPublicInferenceProvider = { + key: string; + name: string; +}; + +export function normalizePublicInferenceProvider( + provider: string +): NormalizedPublicInferenceProvider { + const normalizedKey = provider.toLowerCase().replace(/[^a-z0-9]/g, ''); + const key = PROVIDER_ALIASES[normalizedKey] ?? normalizedKey; + const fallbackName = provider + .trim() + .toLowerCase() + .replace(/(^|[\s_-])\w/g, character => character.toUpperCase()); + + return { key, name: PROVIDER_NAMES[key] ?? fallbackName }; +} diff --git a/apps/web/src/lib/public-snowflake-report.ts b/apps/web/src/lib/public-snowflake-report.ts new file mode 100644 index 0000000000..6fd64be7f2 --- /dev/null +++ b/apps/web/src/lib/public-snowflake-report.ts @@ -0,0 +1,138 @@ +import { captureException } from '@sentry/nextjs'; +import { NextResponse } from 'next/server'; +import type { ZodType } from 'zod'; + +import { createCachedFetch } from '@/lib/cached-fetch'; +import { redisClient } from '@/lib/redis'; +import type { RedisKey } from '@/lib/redis-keys'; +import { executeSnowflakeStatement, resolveSnowflakeConfig } from '@/lib/snowflake'; + +const CORS_HEADERS = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET', + 'Access-Control-Allow-Headers': 'Content-Type', +}; + +// Single source of truth for every cache layer in front of Snowflake: the +// Vercel edge cache, the in-process cache, and the Redis cache all expire after +// one hour so the data served stays consistent across layers. +const CACHE_TTL_SECONDS = 3600; +const IN_MEMORY_CACHE_TTL_MS = CACHE_TTL_SECONDS * 1000; + +type PublicSnowflakeReportOptions = { + cacheKey: RedisKey; + errorMessage: string; + parseRows: (rows: string[][]) => Usage; + query: string; + schema: ZodType; + source: string; +}; + +function successResponse(usage: Usage): NextResponse { + return NextResponse.json(usage, { + headers: { + ...CORS_HEADERS, + 'Cache-Control': `public, s-maxage=${CACHE_TTL_SECONDS}, stale-while-revalidate=${CACHE_TTL_SECONDS}`, + }, + }); +} + +/** + * Read the report from Redis, falling back to Snowflake on a miss and + * repopulating Redis. Throws on Snowflake failure so the in-process cache keeps + * serving the last-known-good value (or `null` when nothing has been cached). + */ +async function fetchReport( + options: PublicSnowflakeReportOptions +): Promise { + try { + const cached = await redisClient.get(options.cacheKey); + if (cached !== null) { + return options.schema.parse(JSON.parse(cached)); + } + } catch (error) { + captureException(error, { + tags: { source: options.source, operation: 'redis-read' }, + }); + } + + const config = resolveSnowflakeConfig(); + if (!config) { + return null; + } + + try { + const rows = await executeSnowflakeStatement({ + config, + statement: options.query, + timeoutSeconds: 30, + }); + const usage = options.schema.parse(options.parseRows(rows)); + + try { + await redisClient.set(options.cacheKey, JSON.stringify(usage), { + ex: CACHE_TTL_SECONDS, + }); + } catch (error) { + captureException(error, { + tags: { source: options.source, operation: 'redis-write' }, + }); + } + + return usage; + } catch (error) { + captureException(error, { + tags: { source: options.source }, + }); + throw error; + } +} + +/** + * Builds a cached GET handler for a public Snowflake-backed report. + * + * Caching cascades through three layers, all expiring after one hour: the + * Vercel edge cache (`s-maxage`), an in-process `createCachedFetch` so warm + * instances avoid Redis entirely, and the Redis cache in front of Snowflake. + * The in-process cache is created once per report (module scope) and stores + * pure data, so it is safe to share across requests. + */ +export function createPublicSnowflakeReport(options: PublicSnowflakeReportOptions) { + const getCachedReport = createCachedFetch( + () => fetchReport(options), + IN_MEMORY_CACHE_TTL_MS, + null + ); + + return async function GET(): Promise { + if (!resolveSnowflakeConfig()) { + return NextResponse.json( + { error: 'Snowflake is not configured' }, + { + status: 503, + headers: { ...CORS_HEADERS, 'Cache-Control': 'no-store' }, + } + ); + } + + const usage = await getCachedReport(); + if (usage === null) { + return NextResponse.json( + { error: options.errorMessage }, + { + status: 502, + headers: { ...CORS_HEADERS, 'Cache-Control': 'no-store' }, + } + ); + } + + return successResponse(usage); + }; +} + +export function publicSnowflakeReportOptions(): NextResponse { + return new NextResponse(null, { + status: 204, + headers: CORS_HEADERS, + }); +} diff --git a/apps/web/src/lib/redis-keys.ts b/apps/web/src/lib/redis-keys.ts index 3b091624aa..b059efae40 100644 --- a/apps/web/src/lib/redis-keys.ts +++ b/apps/web/src/lib/redis-keys.ts @@ -55,6 +55,12 @@ export const codingPlanUsageRedisKey = (input: { `coding-plan-usage:v1:${input.userId}:${input.subscriptionId}:${input.planId}:${input.providerId}:${input.inventoryId}` ); +export const LEADERBOARD_MODEL_PROVIDER_USAGE_REDIS_KEY = redisKey( + 'public-api:leaderboard-model-provider-usage' +); +export const LEADERBOARD_MODEL_USAGE_REDIS_KEY = redisKey('public-api:leaderboard-model-usage'); +export const LEADERBOARD_PROVIDER_RACE_REDIS_KEY = redisKey('public-api:leaderboard-provider-race'); + export const REQUEST_LOGGING_OPT_INS_REDIS_KEY = redisKey('ai-gateway:request-logging-opt-ins'); export const abuseRulesClassificationRedisKey = (identityKey: string) =>