Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 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
102 changes: 102 additions & 0 deletions apps/web/src/lib/ai-gateway/providers/direct-byok/nvidia-byok.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import type { TransformRequestContext } from '@/lib/ai-gateway/providers/types';
import type { DirectByokModel } from './types';
import nvidiaByok from './nvidia-byok';

const SUPER_MODEL_ID = 'nvidia/nemotron-3-super-120b-a12b';
const SUPER_MODEL: DirectByokModel = {
id: SUPER_MODEL_ID,
name: 'Nemotron 3 Super',
flags: ['reasoning'],
context_length: 262144,
max_completion_tokens: 262144,
variants: {
none: { reasoning: { enabled: false, effort: 'none' } },
high: { reasoning: { enabled: true, effort: 'high' } },
},
};

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

nvidiaByok.transformRequest({ request } as TransformRequestContext, model);
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' },
safety_identifier: 'user-hash',
prompt_cache_key: 'task-hash',
temperature: 0.5,
});

expect(body).toMatchObject({
model: SUPER_MODEL_ID,
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('reasoning_effort');
expect(body).not.toHaveProperty('safety_identifier');
expect(body).not.toHaveProperty('prompt_cache_key');
});

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('preserves an explicit reasoning effort', () => {
expect(
transform({
model: SUPER_MODEL_ID,
reasoning_effort: 'high',
reasoning: { effort: 'low' },
})
).toHaveProperty('reasoning_effort', 'high');
expect(
transform({ model: SUPER_MODEL_ID, reasoning_effort: 'unsupported' })
).not.toHaveProperty('reasoning_effort');
});

test('strips efforts not supported by the selected model', () => {
const gptOssModel: DirectByokModel = {
id: 'openai/gpt-oss-120b',
name: 'GPT-OSS-120B',
flags: ['reasoning'],
context_length: 128000,
max_completion_tokens: 8192,
variants: {
low: { reasoning: { enabled: true, effort: 'low' } },
medium: { reasoning: { enabled: true, effort: 'medium' } },
high: { reasoning: { enabled: true, effort: 'high' } },
},
};

expect(transform({ reasoning_effort: 'medium' }, gptOssModel)).toHaveProperty(
'reasoning_effort',
'medium'
);
expect(transform({ reasoning_effort: 'max' }, gptOssModel)).not.toHaveProperty(
'reasoning_effort'
);
expect(transform({ reasoning: { enabled: false } }, gptOssModel)).not.toHaveProperty(
'reasoning_effort'
);
});
});
55 changes: 55 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,55 @@
import { cachedEnhancedDirectByokModelList } from '@/lib/ai-gateway/providers/direct-byok/model-list';
import type { DirectByokProvider } from '@/lib/ai-gateway/providers/direct-byok/types';
import { ReasoningEffortSchema } from '@kilocode/db/schema-types';

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, model) {
const { request } = context;
if (request.kind !== 'chat_completions') {
return;
}

const reasoningEffort =
request.body.reasoning?.enabled === false
? 'none'
: (request.body.reasoning_effort ?? request.body.reasoning?.effort);
const parsedReasoningEffort = ReasoningEffortSchema.safeParse(reasoningEffort);
Comment thread
chrarnoldus marked this conversation as resolved.
Outdated
const supportedReasoningEfforts = new Set(
Object.values(model.variants ?? {}).flatMap(variant =>
variant.reasoning?.effort ? [variant.reasoning.effort] : []
)
);
if (
parsedReasoningEffort.success &&
supportedReasoningEfforts.has(parsedReasoningEffort.data)
) {
request.body.reasoning_effort = parsedReasoningEffort.data;
} 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.
},
models: cachedEnhancedDirectByokModelList({
providerId: 'nvidia-byok',
recommendedModels: [
{
id: 'nvidia/nemotron-3-super-120b-a12b',
name: 'Nemotron 3 Super 120B A12B',
flags: ['reasoning'],
context_length: 262144,
max_completion_tokens: 262144,
},
],
}),
} satisfies DirectByokProvider;
Original file line number Diff line number Diff line change
Expand Up @@ -221,10 +221,68 @@ describe('parseModelsDevProviderModels', () => {
},
},
'alibaba-token-plan',
new Set(['available', 'provider-only'])
{ availableModelIds: new Set(['available', 'provider-only']) }
);

expect(models.map(model => model.id)).toEqual(['available']);
expect(models[0].context_length).toBe(128_000);
});

test('excludes explicit capability mismatches while accepting missing metadata', () => {
const model = (id: string, overrides: Record<string, unknown> = {}) => ({
id,
name: id,
tool_call: true,
modalities: { input: ['text'], output: ['text'] },
...overrides,
});
const models = parseModelsDevProviderModels(
{
models: {
chat: model('nvidia/chat', {
reasoning: true,
reasoning_options: [{ type: 'effort', values: ['none', 'high', 'max'] }],
limit: { context: 128000 },
}),
vision: model('nvidia/vision', {
modalities: { input: ['text', 'image'], output: ['text'] },
}),
unknownCapabilities: { id: 'nvidia/unknown' },
unavailable: model('nvidia/unavailable'),
noTools: model('nvidia/no-tools', { tool_call: false }),
noTextInput: model('nvidia/no-text-input', {
modalities: { input: ['image'], output: ['text'] },
}),
},
},
'nvidia-byok',
{
availableModelIds: new Set([
'nvidia/chat',
'nvidia/vision',
'nvidia/unknown',
'nvidia/no-tools',
'nvidia/no-text-input',
]),
}
);

expect(models).toEqual([
expect.objectContaining({
id: 'nvidia/chat',
context_length: 128000,
flags: ['reasoning'],
variants: {
none: { reasoning: { enabled: false, effort: 'none' } },
high: { reasoning: { enabled: true, effort: 'high' } },
max: { reasoning: { enabled: true, effort: 'max' } },
},
}),
expect.objectContaining({
id: 'nvidia/vision',
input_modalities: ['text', 'image'],
}),
expect.objectContaining({ id: 'nvidia/unknown' }),
]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ const ModelsDevModelSchema = z.object({
output: z.array(ModalitySchema).optional(),
})
.optional(),
tool_call: z.boolean().optional(),
});

const ModelsDevProviderSchema = z.object({
Expand All @@ -84,6 +85,14 @@ type RawModel = {
variants?: OpenCodeSettings['variants'];
};

type ModelsDevProviderOptions = {
availableModelIds?: ReadonlySet<string>;
};

type ModelsDevFetcherOptions = {
availableModelsUrl?: string;
};

type SyncContext = {
getModelsDevCatalog(): Promise<ModelsDevCatalog>;
};
Expand Down Expand Up @@ -178,15 +187,17 @@ function addAnthropicVariantVerbosity(
export function parseModelsDevProviderModels(
entry: unknown,
providerId: DirectUserByokInferenceProviderId,
availableModelIds?: ReadonlySet<string>
options: ModelsDevProviderOptions = {}
): RawModel[] {
const provider = ModelsDevProviderSchema.parse(entry);
return Object.values(provider.models)
.filter(
model =>
model.status !== 'deprecated' &&
(!model.modalities?.output || model.modalities.output.includes('text')) &&
(!availableModelIds || availableModelIds.has(model.id))
(!options.availableModelIds || options.availableModelIds.has(model.id)) &&
model.tool_call !== false &&
(!model.modalities?.input || model.modalities.input.includes('text'))
)
.map(model => {
const modelId = `${providerId}/${model.id}`.toLowerCase();
Expand All @@ -209,7 +220,7 @@ export function parseModelsDevProviderModels(
function modelsDevFetcher(
providerId: DirectUserByokInferenceProviderId,
catalogKey: string,
availableModelsUrl?: string
options: ModelsDevFetcherOptions = {}
): ProviderFetcher {
return {
providerId,
Expand All @@ -219,21 +230,23 @@ function modelsDevFetcher(
if (!entry) {
throw new Error(`models.dev catalog missing ${catalogKey} entry`);
}
if (!availableModelsUrl) {
return parseModelsDevProviderModels(entry, providerId);
}
const response = await fetch(availableModelsUrl);
if (!response.ok) {
throw new Error(
`Failed to fetch ${providerId} available models: ${response.status} ${response.statusText}`
let availableModelIds: ReadonlySet<string> | undefined;
if (options.availableModelsUrl) {
const response = await fetch(options.availableModelsUrl);
if (!response.ok) {
throw new Error(
`Failed to fetch ${providerId} available models: ${response.status} ${response.statusText}`
);
}
availableModelIds = new Set(
OpenAICompatibleModelsResponseSchema.parse(await response.json()).data.map(
model => model.id
)
);
}
const availableModelIds = new Set(
OpenAICompatibleModelsResponseSchema.parse(await response.json()).data.map(
model => model.id
)
);
return parseModelsDevProviderModels(entry, providerId, availableModelIds);
return parseModelsDevProviderModels(entry, providerId, {
availableModelIds,
});
},
};
}
Expand All @@ -254,6 +267,9 @@ const FETCHERS: ReadonlyArray<ProviderFetcher> = [
label: 'Neuralwatt',
url: 'https://api.neuralwatt.com/v1/models',
}),
modelsDevFetcher('nvidia-byok', 'nvidia', {
availableModelsUrl: 'https://integrate.api.nvidia.com/v1/models',
}),
openAICompatibleFetcher({
providerId: 'chutes-byok',
label: 'Chutes',
Expand Down Expand Up @@ -291,8 +307,12 @@ const FETCHERS: ReadonlyArray<ProviderFetcher> = [
}),
modelsDevFetcher('alibaba-token-plan', 'alibaba-token-plan'),
modelsDevFetcher('zai-coding', 'zai-coding-plan'),
modelsDevFetcher('ollama-cloud', 'ollama-cloud', 'https://ollama.com/v1/models'),
modelsDevFetcher('opencode-go', 'opencode-go', 'https://opencode.ai/zen/go/v1/models'),
modelsDevFetcher('ollama-cloud', 'ollama-cloud', {
availableModelsUrl: 'https://ollama.com/v1/models',
}),
modelsDevFetcher('opencode-go', 'opencode-go', {
availableModelsUrl: 'https://opencode.ai/zen/go/v1/models',
}),
modelsDevFetcher('xiaomi-token-plan-ams', 'xiaomi-token-plan-ams'),
modelsDevFetcher('xiaomi-token-plan-sgp', 'xiaomi-token-plan-sgp'),
];
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/lib/ai-gateway/providers/direct-byok/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export type DirectByokProvider = {
models: () => Promise<ReadonlyArray<DirectByokModel>>;
supported_chat_apis: ReadonlyArray<GatewayChatApiKind>;
default_ai_sdk_provider: CustomLlmProvider;
transformRequest(context: TransformRequestContext): void;
transformRequest(context: TransformRequestContext, model: DirectByokModel): void;
};

export const COMPATIBLE_USER_AGENT = 'Kilo-Code/5.12';
2 changes: 1 addition & 1 deletion apps/web/src/lib/ai-gateway/providers/get-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ async function checkDirectBYOK(
supportedChatApis: directByok.supported_chat_apis,
async transformRequest(context) {
context.request.body.model = directByokModel.id;
directByok.transformRequest(context);
directByok.transformRequest(context, directByokModel);
},
} satisfies Provider,
userByok,
Expand Down
Loading