Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
4ec45d4
feat(ai-gateway): add NVIDIA direct BYOK support
lambertjosh Jul 27, 2026
44d5234
fix(ai-gateway): strip gateway-only fields NVIDIA rejects
lambertjosh Jul 27, 2026
cbff1c9
refactor(ai-gateway): address NVIDIA BYOK review feedback
lambertjosh Jul 28, 2026
126a7b2
fix(ai-gateway): keep stripping include_reasoning for NVIDIA
lambertjosh Jul 28, 2026
df1c045
refactor(ai-gateway): keep NVIDIA handling out of shared model settings
lambertjosh Jul 28, 2026
b0efb8b
fix(ai-gateway): preserve NVIDIA model capabilities
lambertjosh Jul 28, 2026
e4a2cd7
refactor(ai-gateway): simplify NVIDIA BYOK metadata
lambertjosh Jul 28, 2026
9464aae
fix(ai-gateway): preserve NVIDIA catalog on empty sync
lambertjosh Jul 29, 2026
8b84db1
refactor(ai-gateway): source NVIDIA models from models.dev
lambertjosh Jul 29, 2026
29561fc
refactor(ai-gateway): derive NVIDIA reasoning from models.dev
lambertjosh Jul 29, 2026
99938e9
Merge origin/main into research-nvidia-byok-support
chrarnoldus Jul 29, 2026
1381bc1
Merge remote-tracking branch 'origin/main' into research-nvidia-byok-…
chrarnoldus Jul 30, 2026
56ae955
refactor(ai-gateway): simplify NVIDIA BYOK filtering
chrarnoldus Jul 30, 2026
91a6571
fix(ai-gateway): strip NVIDIA include reasoning field
chrarnoldus Jul 30, 2026
6d30087
refactor(ai-gateway): minimize NVIDIA model sync
chrarnoldus Jul 30, 2026
fd5fd32
Merge branch 'main' into research-nvidia-byok-support
chrarnoldus Jul 30, 2026
9783e7c
Rename
chrarnoldus Jul 30, 2026
739723d
Simplify
chrarnoldus Jul 30, 2026
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 @@ -8,6 +8,7 @@ import kimiCoding from './kimi-coding';
import martian from './martian';
import morph from './morph';
import neuralwatt from './neurowatt';
import nvidiaByok from './nvidia-byok';
import ollamaCloud from './ollama-cloud';
import openCodeGo from './opencode-go';
import orcarouter from './orcarouter';
Expand All @@ -26,6 +27,7 @@ export default [
martian,
morph,
neuralwatt,
nvidiaByok,
ollamaCloud,
openCodeGo,
orcarouter,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const DIRECT_BYOK_PROVIDERS_META = {
martian: 'Martian',
'morph-byok': 'Morph BYOK',
neuralwatt: 'Neuralwatt',
'nvidia-byok': 'NVIDIA',
'ollama-cloud': 'Ollama Cloud',
'opencode-go': 'OpenCode Go',
orcarouter: 'OrcaRouter',
Expand Down
48 changes: 48 additions & 0 deletions apps/web/src/lib/ai-gateway/providers/direct-byok/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,27 @@ jest.mock('./direct-byok-definitions', () => ({
default_ai_sdk_provider: 'openai-compatible',
transformRequest: jest.fn(),
},
{
id: 'nvidia-byok',
base_url: 'https://integrate.api.nvidia.com/v1',
models: jest.fn(async () => [
{
id: 'deepseek-ai/deepseek-v4-flash',
name: 'DeepSeek V4 Flash',
context_length: 4096,
max_completion_tokens: 1024,
supported_parameters: ['max_tokens', 'temperature', 'tools', 'reasoning'],
variants: {
none: { reasoning: { enabled: false, effort: 'none' } },
high: { reasoning: { enabled: true, effort: 'high' } },
max: { reasoning: { enabled: true, effort: 'max' } },
},
},
]),
supported_chat_apis: ['chat_completions'],
default_ai_sdk_provider: 'openai-compatible',
transformRequest: jest.fn(),
},
],
}));

Expand Down Expand Up @@ -88,4 +109,31 @@ describe('getDirectByokModel', () => {
expect(directByokProviders[0].models).toHaveBeenCalledTimes(1);
expect(directByokProviders[1].models).not.toHaveBeenCalled();
});

test('uses per-model metadata before generic model-name defaults', async () => {
const { getBYOKforUser } = await import('@/lib/ai-gateway/byok');
const { getModelVariants } = await import('@/lib/ai-gateway/providers/model-settings');
jest.mocked(getBYOKforUser).mockResolvedValue([{ providerId: 'nvidia-byok' }] as never);
jest.mocked(getModelVariants).mockReturnValue({
xhigh: { reasoning: { enabled: true, effort: 'xhigh' } },
});
const { getDirectByokModelsForUser } = await import('.');

const models = await getDirectByokModelsForUser('user-id');

expect(models).toEqual([
expect.objectContaining({
id: 'nvidia-byok/deepseek-ai/deepseek-v4-flash',
supported_parameters: ['max_tokens', 'temperature', 'tools', 'reasoning'],
opencode: {
ai_sdk_provider: 'openai-compatible',
variants: {
none: { reasoning: { enabled: false, effort: 'none' } },
high: { reasoning: { enabled: true, effort: 'high' } },
max: { reasoning: { enabled: true, effort: 'max' } },
},
},
}),
]);
});
});
10 changes: 8 additions & 2 deletions apps/web/src/lib/ai-gateway/providers/direct-byok/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,19 @@ function convertModel(
is_moderated: false,
},
per_request_limits: null,
supported_parameters: ['max_tokens', 'temperature', 'tools', 'reasoning', 'include_reasoning'],
supported_parameters: model.supported_parameters ?? [
'max_tokens',
'temperature',
'tools',
'reasoning',
'include_reasoning',
],
default_parameters: {},
preferredIndex: model.flags?.includes('recommended') ? preferredIndex : undefined,
hasUserByokAvailable: true,
opencode: {
ai_sdk_provider: getAiSdkProvider(id, provider.id) ?? provider.default_ai_sdk_provider,
variants: getModelVariants(id),
variants: model.variants ?? getModelVariants(id),
} satisfies OpenCodeSettings,
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import type { TransformRequestContext } from '@/lib/ai-gateway/providers/types';
import nvidiaByok from './nvidia-byok';

const SUPER_MODEL_ID = 'nvidia/nemotron-3-super-120b-a12b';
const ULTRA_MODEL_ID = 'nvidia/nemotron-3-ultra-550b-a55b';

function transform(body: Record<string, unknown>) {
const request = {
kind: 'chat_completions',
body: {
messages: [{ role: 'user', content: 'Hello' }],
...body,
},
} as TransformRequestContext['request'];

nvidiaByok.transformRequest({ request } as TransformRequestContext);
return request.body;
}

describe('NVIDIA direct BYOK', () => {
test('removes gateway-only request fields', () => {
const body = transform({
model: SUPER_MODEL_ID,
provider: { order: ['nvidia'] },
providerOptions: { gateway: {} },
transforms: ['middle-out'],
reasoning: { effort: 'low' },
include_reasoning: true,
safety_identifier: 'user-hash',
prompt_cache_key: 'task-hash',
temperature: 0.5,
});

expect(body).toMatchObject({
model: SUPER_MODEL_ID,
reasoning_effort: 'low',
temperature: 0.5,
});
expect(body).not.toHaveProperty('provider');
expect(body).not.toHaveProperty('providerOptions');
expect(body).not.toHaveProperty('transforms');
expect(body).not.toHaveProperty('reasoning');
expect(body).not.toHaveProperty('include_reasoning');
expect(body).not.toHaveProperty('safety_identifier');
expect(body).not.toHaveProperty('prompt_cache_key');
});

test('uses the documented reasoning efforts for Super and Ultra', () => {
expect(transform({ model: SUPER_MODEL_ID, reasoning: { effort: 'low' } })).toHaveProperty(
'reasoning_effort',
'low'
);
expect(transform({ model: ULTRA_MODEL_ID, reasoning: { effort: 'medium' } })).toHaveProperty(
'reasoning_effort',
'medium'
);
});

test('translates an explicit reasoning disable to the documented none effort', () => {
expect(transform({ model: SUPER_MODEL_ID, reasoning: { enabled: false } })).toHaveProperty(
'reasoning_effort',
'none'
);
});

test('strips reasoning controls for models without verified reasoning support', () => {
// gpt-oss and Llama endpoints return 400 for efforts they do not accept.
expect(
transform({ model: 'openai/gpt-oss-120b', reasoning_effort: 'none' })
).not.toHaveProperty('reasoning_effort');
expect(
transform({ model: 'meta/llama-3.1-8b-instruct', reasoning_effort: 'max' })
).not.toHaveProperty('reasoning_effort');
});

test('keeps documented efforts for gpt-oss models', () => {
expect(transform({ model: 'openai/gpt-oss-120b', reasoning_effort: 'medium' })).toHaveProperty(
'reasoning_effort',
'medium'
);
});

test('preserves valid explicit effort and removes unsupported efforts', () => {
expect(
transform({
model: SUPER_MODEL_ID,
reasoning_effort: 'high',
reasoning: { effort: 'low' },
})
).toHaveProperty('reasoning_effort', 'high');
expect(
transform({
model: SUPER_MODEL_ID,
reasoning_effort: 'medium',
})
).not.toHaveProperty('reasoning_effort');
});
});
48 changes: 48 additions & 0 deletions apps/web/src/lib/ai-gateway/providers/direct-byok/nvidia-byok.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { cachedEnhancedDirectByokModelList } from '@/lib/ai-gateway/providers/direct-byok/model-list';
import type { DirectByokProvider } from '@/lib/ai-gateway/providers/direct-byok/types';
import { getNvidiaReasoningEfforts } from '@/lib/ai-gateway/providers/nvidia';

export default {
id: 'nvidia-byok',
base_url: 'https://integrate.api.nvidia.com/v1',
supported_chat_apis: ['chat_completions'],
default_ai_sdk_provider: 'openai-compatible',
transformRequest(context) {
const { request } = context;
if (request.kind !== 'chat_completions') {
return;
}

const model = request.body.model;
const reasoningEfforts = getNvidiaReasoningEfforts(model);
if (reasoningEfforts) {
const requestedEffort =
request.body.reasoning?.enabled === false
? 'none'
: (request.body.reasoning_effort ?? request.body.reasoning?.effort);
const supportedEffort = reasoningEfforts.find(effort => effort === requestedEffort);

if (supportedEffort) {
(request.body as { reasoning_effort?: string }).reasoning_effort = supportedEffort;
} else {
delete request.body.reasoning_effort;
}
} else {
delete request.body.reasoning_effort;
}

// NVIDIA rejects these with `Validation: Unsupported parameter(s)`.
delete request.body.provider;
Comment thread
chrarnoldus marked this conversation as resolved.
delete request.body.providerOptions;
delete request.body.transforms;
delete request.body.reasoning;
delete request.body.safety_identifier;
delete request.body.prompt_cache_key;
Comment thread
chrarnoldus marked this conversation as resolved.
// Older clients may still send this untyped OpenRouter field.
delete (request.body as { include_reasoning?: boolean }).include_reasoning;
Comment thread
chrarnoldus marked this conversation as resolved.
Outdated
},
models: cachedEnhancedDirectByokModelList({
providerId: 'nvidia-byok',
recommendedModels: [],
Comment thread
chrarnoldus marked this conversation as resolved.
Outdated
}),
} satisfies DirectByokProvider;
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import {
parseNvidiaProviderModels,
parseModelsDevProviderModels,
parseOpenAICompatibleProviderModels,
} from './sync-direct-byok';
import { DirectByokModelArraySchema } from './types';

describe('parseOpenAICompatibleProviderModels', () => {
test('parses Morph OpenAI-compatible model metadata', () => {
Expand Down Expand Up @@ -122,3 +124,66 @@ describe('parseModelsDevProviderModels', () => {
]);
});
});

describe('parseNvidiaProviderModels', () => {
const model = (id: string, overrides: Record<string, unknown> = {}) => ({
id,
name: id,
tool_call: true,
modalities: { input: ['text'], output: ['text'] },
...overrides,
});

test('keeps models.dev tool-calling text models', () => {
const catalog = {
chat: model('nvidia/chat'),
vision: model('nvidia/vision', {
modalities: { input: ['text', 'image'], output: ['text'] },
}),
deprecated: model('nvidia/deprecated', { status: 'deprecated' }),
noTools: model('nvidia/no-tools', { tool_call: false }),
noTextInput: model('nvidia/no-text-input', {
modalities: { input: ['image'], output: ['text'] },
}),
noTextOutput: model('nvidia/no-text-output', {
modalities: { input: ['text'], output: ['image'] },
}),
unavailable: model('google/gemma-2-2b-it'),
};

expect(parseNvidiaProviderModels({ models: catalog })).toEqual([
expect.objectContaining({ id: 'nvidia/chat', variants: {} }),
expect.objectContaining({
id: 'nvidia/vision',
input_modalities: ['text', 'image'],
variants: {},
}),
expect.objectContaining({ id: 'google/gemma-2-2b-it', variants: {} }),
]);
});

test('maps documented reasoning efforts into model metadata', () => {
const efforts = {
'nvidia/nemotron-3-super-120b-a12b': ['none', 'low', 'high'],
'nvidia/nemotron-3-ultra-550b-a55b': ['none', 'medium', 'high'],
'deepseek-ai/deepseek-v4-flash': ['none', 'high', 'max'],
'deepseek-ai/deepseek-v4-pro': ['none', 'high', 'max'],
'openai/gpt-oss-20b': ['low', 'medium', 'high'],
'openai/gpt-oss-120b': ['low', 'medium', 'high'],
};
const ids = Object.keys(efforts);
const models = Object.fromEntries(ids.map(id => [id, model(id)]));
const synced = parseNvidiaProviderModels({ models }).map(item => ({
...item,
name: item.name ?? item.id,
context_length: item.context_length ?? 200_000,
max_completion_tokens: item.max_completion_tokens ?? 32_000,
}));
const parsed = DirectByokModelArraySchema.parse(JSON.parse(JSON.stringify(synced)));

expect(
Object.fromEntries(parsed.map(item => [item.id, Object.keys(item.variants ?? {})]))
).toEqual(efforts);
expect(parsed.every(item => item.supported_parameters?.includes('reasoning'))).toBe(true);
});
});
Loading