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
1 change: 1 addition & 0 deletions ENVIRONMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ Manage shared web env var additions and rotations with `pnpm web:env set <VARIAB
### AI Providers

- `OPENROUTER_API_KEY` - Primary OpenRouter API key for model inference through the AI gateway; provider definition in `apps/web/src/lib/ai-gateway/providers/provider-definitions.ts` pointing to `https://openrouter.ai/api/v1`. `[SECRET]`
- `POOLSIDE_FREE_API_KEY` - Poolside API key used as the Vercel AI Gateway BYOK credential for the free Laguna model. `[SECRET]`
- `OPENAI_API_KEY` - OpenAI API key; used in `apps/web/src/lib/ai-gateway/embeddings/embedding-providers.ts` for the `text-embedding-3-small` embedding model, and as a provider config in `apps/web/src/lib/config.server.ts`. `[SECRET]`
- `MISTRAL_API_KEY` - Mistral API key; used in `apps/web/src/lib/ai-gateway/embeddings/embedding-providers.ts` for `codestral-embed-2505` and `mistral-embed` embeddings, in the FIM completions proxy at `apps/web/src/app/api/fim/completions/route.ts` (routes Mistral Codestral vs. La Plateforme keys), and as a provider config in `apps/web/src/lib/config.server.ts`. `[SECRET]`
- `STREAMLAKE_API_KEY` - StreamLake API key for model inference through the AI gateway; provider definition in `apps/web/src/lib/ai-gateway/providers/provider-definitions.ts`. `[SECRET]`
Expand Down
68 changes: 67 additions & 1 deletion apps/web/src/lib/ai-gateway/providers/vercel/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, it, expect } from '@jest/globals';
import { afterAll, beforeEach, describe, it, expect } from '@jest/globals';
import {
applyVercelSettings,
convertProviderOptions,
getAnthropicProviderOptionsForVercel,
getVercelInferenceProvidersExcludingIgnored,
Expand All @@ -9,6 +10,71 @@ import {
import { getRandomNumber } from '@/lib/ai-gateway/getRandomNumber';
import type { GatewayRequest } from '@/lib/ai-gateway/providers/openrouter/types';

describe('applyVercelSettings', () => {
const originalPoolsideFreeApiKey = process.env.POOLSIDE_FREE_API_KEY;

beforeEach(() => {
process.env.POOLSIDE_FREE_API_KEY = 'poolside-free-api-key';
});

afterAll(() => {
if (originalPoolsideFreeApiKey === undefined) {
delete process.env.POOLSIDE_FREE_API_KEY;
} else {
process.env.POOLSIDE_FREE_API_KEY = originalPoolsideFreeApiKey;
}
});

it('uses the Poolside free API key for Laguna when the user has no BYOK', async () => {
const request: GatewayRequest = {
kind: 'chat_completions',
body: {
model: 'poolside/laguna-s-2.1:free',
messages: [{ role: 'user', content: 'hello' }],
},
};

await applyVercelSettings('poolside/laguna-s-2.1:free', request, null);

expect(request.body.providerOptions?.gateway?.byok).toEqual({
poolside: [{ apiKey: 'poolside-free-api-key' }],
});
});

it('does not configure Poolside BYOK when the free API key is unset', async () => {
delete process.env.POOLSIDE_FREE_API_KEY;
const request: GatewayRequest = {
kind: 'chat_completions',
body: {
model: 'poolside/laguna-s-2.1:free',
messages: [{ role: 'user', content: 'hello' }],
},
};

await applyVercelSettings('poolside/laguna-s-2.1:free', request, null);

expect(request.body.providerOptions?.gateway?.byok).toBeUndefined();
});

it('keeps user BYOK authoritative for Laguna', async () => {
const request: GatewayRequest = {
kind: 'chat_completions',
body: {
model: 'poolside/laguna-s-2.1:free',
messages: [{ role: 'user', content: 'hello' }],
},
};

await applyVercelSettings('poolside/laguna-s-2.1:free', request, [
{ providerId: 'openai', decryptedAPIKey: 'user-api-key' },
]);

expect(request.body.providerOptions?.gateway?.byok).toEqual({
openai: [{ apiKey: 'user-api-key' }],
});
});
});

describe('getAnthropicProviderOptionsForVercel', () => {
it('maps chat completion verbosity to Anthropic effort', () => {
const request: GatewayRequest = {
Expand Down
22 changes: 13 additions & 9 deletions apps/web/src/lib/ai-gateway/providers/vercel/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ import {
getVercelModelsFromRedis,
} from '@/lib/ai-gateway/providers/gateway-models-cache';
import type { AnthropicProviderOptions } from '@ai-sdk/anthropic';
import { getEnvVariable } from '@/lib/dotenvx';

const POOLSIDE_FREE_MODEL_ID = 'poolside/laguna-s-2.1:free';

type VercelRoutingPercentages = {
paid: number;
Expand Down Expand Up @@ -94,11 +97,6 @@ export async function shouldRouteToVercel(
request: GatewayRequest,
randomSeed: string
) {
// BYOK in the Vercel AI Gateway was not working for Laguna models.
if (requestedModel.includes('laguna')) {
return false;
}

console.debug('[shouldRouteToVercel] randomizing user to either OpenRouter or Vercel');
const percentages = await getVercelRoutingPercentages();
const routingPercentage = (await isFreeModel(requestedModel))
Expand Down Expand Up @@ -270,10 +268,16 @@ export async function applyVercelSettings(
const vercelInferenceProviders = requestToMutate.body.provider?.ignore?.length
? await getCachedVercelInferenceProviderIdsForModel(vercelModelId)
: null;
requestToMutate.body.providerOptions = convertProviderOptions(
requestToMutate,
vercelInferenceProviders
);
const providerOptions = convertProviderOptions(requestToMutate, vercelInferenceProviders);
if (requestedModel === POOLSIDE_FREE_MODEL_ID && providerOptions.gateway) {
const apiKey = getEnvVariable('POOLSIDE_FREE_API_KEY');
if (apiKey) {
providerOptions.gateway.byok = {
poolside: [{ apiKey }],
};
}
}
requestToMutate.body.providerOptions = providerOptions;
}

if (requestToMutate.body.providerOptions) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ describe('mapModelIdToVercel', () => {
['mistralai/mistral-medium-3-5', 'mistral/mistral-medium-3.5'],
['mistralai/mistral-small-2603', 'mistral/mistral-small'],
['mistralai/pixtral-large-2411', 'mistral/pixtral-large'],
['poolside/laguna-s-2.1:free', 'poolside/laguna-s-2.1-free'],
['qwen/qwen3-14b', 'alibaba/qwen-3-14b'],
['qwen/qwen3-235b-a22b', 'alibaba/qwen-3-235b'],
['qwen/qwen3-30b-a3b', 'alibaba/qwen-3-30b'],
Expand Down Expand Up @@ -90,10 +91,6 @@ describe('mapModelIdToVercel', () => {
expect(mapModelIdToVercel('openai/gpt-oss-20b')).toBe('openai/gpt-oss-20b');
});

it('leaves the OpenRouter-only Poolside model unchanged', () => {
expect(mapModelIdToVercel('poolside/laguna-s-2.1:free')).toBe('poolside/laguna-s-2.1:free');
});

it('leaves a model with an unknown provider prefix unchanged', () => {
expect(mapModelIdToVercel('deepseek/deepseek-v3.2')).toBe('deepseek/deepseek-v3.2');
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const vercelModelIdMapping: Record<string, string | undefined> = {
'mistralai/mistral-medium-3-5': 'mistral/mistral-medium-3.5',
'mistralai/mistral-small-2603': 'mistral/mistral-small',
'mistralai/pixtral-large-2411': 'mistral/pixtral-large',
'poolside/laguna-s-2.1:free': 'poolside/laguna-s-2.1-free',
'qwen/qwen3-14b': 'alibaba/qwen-3-14b',
'qwen/qwen3-235b-a22b': 'alibaba/qwen-3-235b',
'qwen/qwen3-30b-a3b': 'alibaba/qwen-3-30b',
Expand Down
Loading