diff --git a/apps/web/src/app/api/fim/completions/route.ts b/apps/web/src/app/api/fim/completions/route.ts index c0f0ccef14c..156fe908b0b 100644 --- a/apps/web/src/app/api/fim/completions/route.ts +++ b/apps/web/src/app/api/fim/completions/route.ts @@ -5,7 +5,7 @@ import z from 'zod'; import { captureException, setTag, startInactiveSpan } from '@sentry/nextjs'; import type { MicrodollarUsageContext } from '@/lib/ai-gateway/processUsage.types'; import { validateFeatureHeader, FEATURE_HEADER } from '@/lib/feature-detection'; -import { isFreeModel } from '@/lib/ai-gateway/models'; +import { isFreeModel } from '@/lib/ai-gateway/is-free-model'; import { sentryRootSpan } from '@/lib/getRootSpan'; import { getUserFromAuth } from '@/lib/user.server'; import { diff --git a/apps/web/src/app/api/openrouter/[...path]/route.ts b/apps/web/src/app/api/openrouter/[...path]/route.ts index 0bf31dfa2b1..80bd2075987 100644 --- a/apps/web/src/app/api/openrouter/[...path]/route.ts +++ b/apps/web/src/app/api/openrouter/[...path]/route.ts @@ -24,12 +24,12 @@ import { setTag, startInactiveSpan } from '@sentry/nextjs'; import { getUserFromAuth } from '@/lib/user.server'; import { sentryRootSpan } from '@/lib/getRootSpan'; import { - isFreeModel, isDeadFreeModel, isExcludedForFeature, isKiloExclusiveFreeModel, isKiloStealthModel, } from '@/lib/ai-gateway/models'; +import { isFreeModel } from '@/lib/ai-gateway/is-free-model'; import { accountForMicrodollarUsage, captureProxyError, diff --git a/apps/web/src/app/api/openrouter/embeddings/route.ts b/apps/web/src/app/api/openrouter/embeddings/route.ts index 799bdca4a57..04675f441e2 100644 --- a/apps/web/src/app/api/openrouter/embeddings/route.ts +++ b/apps/web/src/app/api/openrouter/embeddings/route.ts @@ -8,7 +8,7 @@ import { debugSaveProxyRequest } from '@/lib/debugUtils'; import { captureException, setTag, startInactiveSpan } from '@sentry/nextjs'; import { getUserFromAuth } from '@/lib/user.server'; import { sentryRootSpan } from '@/lib/getRootSpan'; -import { isFreeModel } from '@/lib/ai-gateway/models'; +import { isFreeModel } from '@/lib/ai-gateway/is-free-model'; import { captureProxyError, checkOrganizationModelRestrictions, diff --git a/apps/web/src/lib/ai-gateway/is-free-model.ts b/apps/web/src/lib/ai-gateway/is-free-model.ts new file mode 100644 index 00000000000..c19ede168aa --- /dev/null +++ b/apps/web/src/lib/ai-gateway/is-free-model.ts @@ -0,0 +1,23 @@ +import { KILO_AUTO_FREE_MODEL } from '@/lib/ai-gateway/auto-model'; +import { isKiloExclusiveFreeModel, isOpenRouterStealthModel } from '@/lib/ai-gateway/models'; + +/** + * Returns true if `model` should be treated as free for the requesting user + * this request. + * + * Server-only: future implementations will consult external state (e.g. a + * Redis-backed membership set for partner-funded preview model ids) to + * answer for models that are conditionally free. Lives outside `models.ts` + * so client bundles importing the model-id constants + * (`PRIMARY_DEFAULT_MODEL`, `preferredModels`, …) from `models.ts` don't + * transitively pull in those server-only dependencies. + */ +export async function isFreeModel(model: string): Promise { + return ( + isKiloExclusiveFreeModel(model) || + model === KILO_AUTO_FREE_MODEL.id || + (model ?? '').endsWith(':free') || + model === 'openrouter/free' || + isOpenRouterStealthModel(model ?? '') + ); +} diff --git a/apps/web/src/lib/ai-gateway/models.test.ts b/apps/web/src/lib/ai-gateway/models.test.ts index 8ddc2928af3..c0df8f0ed53 100644 --- a/apps/web/src/lib/ai-gateway/models.test.ts +++ b/apps/web/src/lib/ai-gateway/models.test.ts @@ -1,5 +1,6 @@ import { describe, test, expect } from '@jest/globals'; -import { autoFreeModels, isFreeModel, kiloExclusiveModels } from './models'; +import { autoFreeModels, kiloExclusiveModels } from './models'; +import { isFreeModel } from './is-free-model'; import { getInferenceProvider } from './providers/kilo-exclusive-model'; describe('isFreeModel', () => { diff --git a/apps/web/src/lib/ai-gateway/models.ts b/apps/web/src/lib/ai-gateway/models.ts index 2b2b9d09f1d..fed0d76ecd1 100644 --- a/apps/web/src/lib/ai-gateway/models.ts +++ b/apps/web/src/lib/ai-gateway/models.ts @@ -58,16 +58,6 @@ export const preferredModels = [ GLM_CURRENT_MODEL_ID, ]; -export async function isFreeModel(model: string): Promise { - return ( - isKiloExclusiveFreeModel(model) || - model === KILO_AUTO_FREE_MODEL.id || - (model ?? '').endsWith(':free') || - model === 'openrouter/free' || - isOpenRouterStealthModel(model ?? '') - ); -} - export function isPdfSupportingModel(model: string): boolean { return isClaudeModel(model) || isOpenAiModel(model) || isGrok4Model(model); } @@ -97,7 +87,7 @@ export function isKiloStealthModel(model: string): boolean { return kiloExclusiveModels.some(m => m.public_id === model && m.flags.includes('stealth')); } -function isOpenRouterStealthModel(model: string): boolean { +export function isOpenRouterStealthModel(model: string): boolean { return model.startsWith('openrouter/') && (model.endsWith('-alpha') || model.endsWith('-beta')); } diff --git a/apps/web/src/lib/ai-gateway/processUsage.ts b/apps/web/src/lib/ai-gateway/processUsage.ts index ea96a9ff4bb..54a46a2c214 100644 --- a/apps/web/src/lib/ai-gateway/processUsage.ts +++ b/apps/web/src/lib/ai-gateway/processUsage.ts @@ -21,7 +21,8 @@ import { eq, sql } from 'drizzle-orm'; import { sentryRootSpan } from '../getRootSpan'; import { ingestOrganizationTokenUsage } from '@/lib/organizations/organization-usage'; import type { ProviderId } from '@/lib/ai-gateway/providers/types'; -import { findKiloExclusiveModel, isFreeModel, isKiloStealthModel } from '@/lib/ai-gateway/models'; +import { findKiloExclusiveModel, isKiloStealthModel } from '@/lib/ai-gateway/models'; +import { isFreeModel } from '@/lib/ai-gateway/is-free-model'; import { sentryLogger } from '@/lib/utils.server'; import { maybeIssueKiloPassBonusFromUsageThreshold } from '@/lib/kilo-pass/usage-triggered-bonus'; import { getEffectiveKiloPassThreshold } from '@/lib/kilo-pass/threshold'; diff --git a/apps/web/src/lib/ai-gateway/providers/openrouter/index.ts b/apps/web/src/lib/ai-gateway/providers/openrouter/index.ts index 629994f1b60..3c7e39b7728 100644 --- a/apps/web/src/lib/ai-gateway/providers/openrouter/index.ts +++ b/apps/web/src/lib/ai-gateway/providers/openrouter/index.ts @@ -1,9 +1,9 @@ import { - isFreeModel, isPdfSupportingModel, kiloExclusiveModels, preferredModels, } from '@/lib/ai-gateway/models'; +import { isFreeModel } from '@/lib/ai-gateway/is-free-model'; import PROVIDERS from '@/lib/ai-gateway/providers/provider-definitions'; import type { OpenRouterModel } from '@/lib/organizations/organization-types'; import { diff --git a/apps/web/src/lib/bot/agent-runner.ts b/apps/web/src/lib/bot/agent-runner.ts index 58cc3ecbf42..14a3d348e54 100644 --- a/apps/web/src/lib/bot/agent-runner.ts +++ b/apps/web/src/lib/bot/agent-runner.ts @@ -29,7 +29,7 @@ import { formatGitLabRepositoriesForPrompt, getGitLabRepositoryContext, } from '@/lib/slack-bot/gitlab-repository-context'; -import { isFreeModel } from '@/lib/ai-gateway/models'; +import { isFreeModel } from '@/lib/ai-gateway/is-free-model'; import { generateApiToken } from '@/lib/tokens'; import { captureException } from '@sentry/nextjs'; import { createOpenAICompatible } from '@ai-sdk/openai-compatible';