fix(nvidia-nim): enable reasoning template kwargs (#1893)

* fix(nvidia-nim): enable reasoning template kwargs

* fix(nim): preserve explicit provider selections

* fix(nim): limit env-only startup precedence
This commit is contained in:
JATMN
2026-07-09 09:05:50 +08:00
committed by GitHub
parent cde6e090d3
commit de9729500b
4 changed files with 419 additions and 4 deletions
+178
View File
@@ -7993,6 +7993,81 @@ test('DeepSeek sends thinking toggle and normalized reasoning effort', async ()
expect(requestBody?.store).toBeUndefined()
})
test('NVIDIA NIM DeepSeek sends chat template thinking kwargs', async () => {
process.env.OPENAI_BASE_URL = 'https://integrate.api.nvidia.com/v1'
process.env.NVIDIA_API_KEY = 'nvapi-test'
let requestBody: Record<string, unknown> | undefined
globalThis.fetch = (async (_input, init) => {
requestBody = JSON.parse(String(init?.body))
return new Response(
JSON.stringify({
id: 'chatcmpl-1',
model: 'deepseek-ai/deepseek-v4-pro',
choices: [
{ message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' },
],
usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 },
}),
{ headers: { 'Content-Type': 'application/json' } },
)
}) as unknown as FetchType
const client = createOpenAIShimClient({
reasoningEffort: 'xhigh',
}) as OpenAIShimClient
await client.beta.messages.create({
model: 'deepseek-ai/deepseek-v4-pro',
system: 'test',
messages: [{ role: 'user', content: 'hi' }],
max_tokens: 64,
stream: false,
thinking: { type: 'enabled' },
})
expect(requestBody?.thinking).toEqual({ type: 'enabled' })
expect(requestBody?.reasoning_effort).toBe('max')
expect(requestBody?.chat_template_kwargs).toEqual({
thinking: true,
enable_thinking: true,
})
})
test('NVIDIA NIM DeepSeek omits chat template thinking kwargs when thinking is disabled', async () => {
process.env.OPENAI_BASE_URL = 'https://integrate.api.nvidia.com/v1'
process.env.NVIDIA_API_KEY = 'nvapi-test'
let requestBody: Record<string, unknown> | undefined
globalThis.fetch = (async (_input, init) => {
requestBody = JSON.parse(String(init?.body))
return new Response(
JSON.stringify({
id: 'chatcmpl-1',
model: 'deepseek-ai/deepseek-v4-pro',
choices: [
{ message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' },
],
}),
{ headers: { 'Content-Type': 'application/json' } },
)
}) as unknown as FetchType
const client = createOpenAIShimClient({
reasoningEffort: 'xhigh',
}) as OpenAIShimClient
await client.beta.messages.create({
model: 'deepseek-ai/deepseek-v4-pro?thinking=disabled',
system: 'test',
messages: [{ role: 'user', content: 'hi' }],
max_tokens: 64,
stream: false,
})
expect(requestBody?.thinking).toBeUndefined()
expect(requestBody?.reasoning_effort).toBeUndefined()
expect(requestBody?.chat_template_kwargs).toBeUndefined()
})
test('DeepSeek omits thinking controls when the Anthropic-side request does not set them', async () => {
process.env.OPENAI_BASE_URL = 'https://api.deepseek.com/v1'
process.env.OPENAI_API_KEY = 'sk-deepseek'
@@ -8565,6 +8640,109 @@ test('Z.AI GLM-5.2: per-turn thinking overrides model-query default', async () =
expect(requestBody?.reasoning_effort).toBe('high')
})
test('NVIDIA NIM Z.AI GLM sends chat template thinking kwargs', async () => {
process.env.OPENAI_BASE_URL = 'https://integrate.api.nvidia.com/v1'
process.env.NVIDIA_API_KEY = 'nvapi-test'
let requestBody: Record<string, unknown> | undefined
globalThis.fetch = (async (_input, init) => {
requestBody = JSON.parse(String(init?.body))
return new Response(
JSON.stringify({
id: 'chatcmpl-1',
model: 'z-ai/glm-5.2',
choices: [
{ message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' },
],
}),
{ headers: { 'Content-Type': 'application/json' } },
)
}) as unknown as FetchType
const client = createOpenAIShimClient({
reasoningEffort: 'xhigh',
}) as OpenAIShimClient
await client.beta.messages.create({
model: 'z-ai/glm-5.2',
messages: [{ role: 'user', content: 'hi' }],
max_tokens: 64,
stream: false,
})
expect(requestBody?.thinking).toEqual({ type: 'enabled' })
expect(requestBody?.reasoning_effort).toBe('max')
expect(requestBody?.chat_template_kwargs).toEqual({
thinking: true,
enable_thinking: true,
})
})
test('NVIDIA NIM Z.AI GLM omits chat template thinking kwargs without a reasoning request', async () => {
process.env.OPENAI_BASE_URL = 'https://integrate.api.nvidia.com/v1'
process.env.NVIDIA_API_KEY = 'nvapi-test'
let requestBody: Record<string, unknown> | undefined
globalThis.fetch = (async (_input, init) => {
requestBody = JSON.parse(String(init?.body))
return new Response(
JSON.stringify({
id: 'chatcmpl-1',
model: 'z-ai/glm-5.2',
choices: [
{ message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' },
],
}),
{ headers: { 'Content-Type': 'application/json' } },
)
}) as unknown as FetchType
const client = createOpenAIShimClient({}) as OpenAIShimClient
await client.beta.messages.create({
model: 'z-ai/glm-5.2',
messages: [{ role: 'user', content: 'hi' }],
max_tokens: 64,
stream: false,
})
expect(requestBody?.thinking).toBeUndefined()
expect(requestBody?.reasoning_effort).toBeUndefined()
expect(requestBody?.chat_template_kwargs).toBeUndefined()
})
test('NVIDIA NIM Z.AI GLM omits chat template thinking kwargs when thinking is disabled', async () => {
process.env.OPENAI_BASE_URL = 'https://integrate.api.nvidia.com/v1'
process.env.NVIDIA_API_KEY = 'nvapi-test'
let requestBody: Record<string, unknown> | undefined
globalThis.fetch = (async (_input, init) => {
requestBody = JSON.parse(String(init?.body))
return new Response(
JSON.stringify({
id: 'chatcmpl-1',
model: 'z-ai/glm-5.2',
choices: [
{ message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' },
],
}),
{ headers: { 'Content-Type': 'application/json' } },
)
}) as unknown as FetchType
const client = createOpenAIShimClient({
reasoningEffort: 'xhigh',
}) as OpenAIShimClient
await client.beta.messages.create({
model: 'z-ai/glm-5.2?thinking=disabled',
messages: [{ role: 'user', content: 'hi' }],
max_tokens: 64,
stream: false,
})
expect(requestBody?.thinking).toEqual({ type: 'disabled' })
expect(requestBody?.reasoning_effort).toBeUndefined()
expect(requestBody?.chat_template_kwargs).toBeUndefined()
})
test('Z.AI GLM-5.2: streaming requests with tools send tool_stream', async () => {
process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4'
process.env.OPENAI_API_KEY = 'sk-zai-test'
+43
View File
@@ -364,6 +364,47 @@ export function hasMistralApiHost(baseUrl: string | undefined): boolean {
}
}
function hasNvidiaNimApiHost(baseUrl: string | undefined): boolean {
if (!baseUrl) return false
try {
return new URL(baseUrl).hostname.toLowerCase() === 'integrate.api.nvidia.com'
} catch {
return false
}
}
function setNvidiaNimChatTemplateThinking(body: Record<string, unknown>): void {
const existing = body.chat_template_kwargs
const kwargs =
existing && typeof existing === 'object' && !Array.isArray(existing)
? { ...(existing as Record<string, unknown>) }
: {}
kwargs.thinking = true
kwargs.enable_thinking = true
body.chat_template_kwargs = kwargs
}
function maybeSetNvidiaNimChatTemplateThinking(
body: Record<string, unknown>,
baseUrl: string | undefined,
reasoningRequestPlan: {
thinkingType?: string
reasoningEffort?: string
},
): void {
if (!hasNvidiaNimApiHost(baseUrl)) return
if (
reasoningRequestPlan.thinkingType !== 'enabled' &&
!reasoningRequestPlan.reasoningEffort
) {
return
}
setNvidiaNimChatTemplateThinking(body)
}
function formatRetryAfterHint(response: Response): string {
const ra = response.headers.get('retry-after')
return ra ? ` (Retry-After: ${ra})` : ''
@@ -3756,6 +3797,7 @@ class OpenAIShimMessages {
if (reasoningRequestPlan.reasoningEffort) {
body.reasoning_effort = reasoningRequestPlan.reasoningEffort
}
maybeSetNvidiaNimChatTemplateThinking(body, request.baseUrl, reasoningRequestPlan)
}
if (reasoningRequestPlan.wireFormat === 'zai_compatible') {
@@ -3769,6 +3811,7 @@ class OpenAIShimMessages {
} else {
delete body.reasoning_effort
}
maybeSetNvidiaNimChatTemplateThinking(body, request.baseUrl, reasoningRequestPlan)
}
// Route/model strip rules are authoritative even when compatibility
+115
View File
@@ -507,6 +507,121 @@ test('buildStartupEnvFromProfile preserves explicit OpenAI-compatible env withou
assert.equal(isDefaultStartupProviderEnv(env), false)
})
test('buildStartupEnvFromProfile preserves concrete env-only NIM setup over stale profile', async () => {
const processEnv = {
OPENAI_BASE_URL: 'https://integrate.api.nvidia.com/v1',
OPENAI_MODEL: 'qwen/qwen3.5-397b-a17b',
NVIDIA_API_KEY: 'nvapi-live',
NVIDIA_NIM: '1',
}
const env = await buildStartupEnvFromProfile({
persisted: profile('openai', {
OPENAI_BASE_URL: 'https://integrate.api.nvidia.com/v1',
OPENAI_MODEL: 'z-ai/glm-5.2',
NVIDIA_API_KEY: 'nvapi-stale',
NVIDIA_NIM: '1',
}),
processEnv,
})
assert.notEqual(env, processEnv)
assert.equal(env.CLAUDE_CODE_USE_OPENAI, '1')
assert.equal(env.CLAUDE_CODE_PROVIDER_ROUTE_ID, 'nvidia-nim')
assert.equal(env.OPENAI_MODEL, 'qwen/qwen3.5-397b-a17b')
assert.equal(env.OPENAI_BASE_URL, 'https://integrate.api.nvidia.com/v1')
assert.equal(env.NVIDIA_API_KEY, 'nvapi-live')
assert.equal(env.NVIDIA_NIM, '1')
assert.equal(resolveActiveRouteIdFromEnv(env), 'nvidia-nim')
})
test('buildStartupEnvFromProfile does not activate non-NIM env-only OpenAI-compatible setup', async () => {
const env = await buildStartupEnvFromProfile({
persisted: null,
processEnv: {
OPENAI_BASE_URL: 'https://openrouter.ai/api/v1',
OPENAI_MODEL: 'openrouter/zhipu/glm-5.2',
OPENAI_API_KEY: 'sk-live',
},
})
assert.equal(env.CLAUDE_CODE_USE_OPENAI, '1')
assert.equal(env.CLAUDE_CODE_PROVIDER_ROUTE_ID, undefined)
assert.equal(env.OPENAI_BASE_URL, 'https://opengateway.gitlawb.com/v1')
assert.equal(env.OPENAI_MODEL, 'mimo-v2.5-pro')
assert.equal(env.OPENAI_API_KEY, undefined)
assert.equal(resolveActiveRouteIdFromEnv(env), 'gitlawb-opengateway')
assert.equal(isDefaultStartupProviderEnv(env), true)
})
test('buildStartupEnvFromProfile documents no-flag Gemini env does not beat concrete NIM setup', async () => {
const processEnv = {
GEMINI_API_KEY: 'gemini-live',
GEMINI_MODEL: 'gemini-2.5-flash',
OPENAI_BASE_URL: 'https://integrate.api.nvidia.com/v1',
OPENAI_MODEL: 'qwen/qwen3.5-397b-a17b',
NVIDIA_API_KEY: 'nvapi-live',
NVIDIA_NIM: '1',
}
const env = await buildStartupEnvFromProfile({
persisted: null,
processEnv,
})
assert.notEqual(env, processEnv)
assert.equal(env.CLAUDE_CODE_USE_OPENAI, '1')
assert.equal(env.CLAUDE_CODE_USE_GEMINI, undefined)
assert.equal(env.CLAUDE_CODE_PROVIDER_ROUTE_ID, 'nvidia-nim')
assert.equal(env.GEMINI_API_KEY, undefined)
assert.equal(env.OPENAI_MODEL, 'qwen/qwen3.5-397b-a17b')
assert.equal(resolveActiveRouteIdFromEnv(env), 'nvidia-nim')
})
test('buildStartupEnvFromProfile preserves explicit OpenAI opt-out over concrete env-only NIM setup', async () => {
const processEnv: NodeJS.ProcessEnv = {
CLAUDE_CODE_USE_OPENAI: '0',
OPENAI_BASE_URL: 'https://integrate.api.nvidia.com/v1',
OPENAI_MODEL: 'qwen/qwen3.5-397b-a17b',
NVIDIA_API_KEY: 'nvapi-live',
NVIDIA_NIM: '1',
}
const env = await buildStartupEnvFromProfile({
persisted: null,
processEnv,
})
assert.equal(env, processEnv)
assert.equal(env.CLAUDE_CODE_USE_OPENAI, '0')
assert.equal(env.CLAUDE_CODE_PROVIDER_ROUTE_ID, undefined)
assert.equal(isDefaultStartupProviderEnv(env), false)
})
test('buildStartupEnvFromProfile preserves explicit Gemini selection over concrete env-only NIM setup', async () => {
const processEnv: NodeJS.ProcessEnv = {
CLAUDE_CODE_USE_GEMINI: '1',
GEMINI_API_KEY: 'gemini-live',
GEMINI_MODEL: 'gemini-2.5-flash',
OPENAI_BASE_URL: 'https://integrate.api.nvidia.com/v1',
OPENAI_MODEL: 'qwen/qwen3.5-397b-a17b',
NVIDIA_API_KEY: 'nvapi-live',
NVIDIA_NIM: '1',
}
const env = await buildStartupEnvFromProfile({
persisted: null,
processEnv,
})
assert.equal(env, processEnv)
assert.equal(env.CLAUDE_CODE_USE_GEMINI, '1')
assert.equal(env.CLAUDE_CODE_USE_OPENAI, undefined)
assert.equal(env.CLAUDE_CODE_PROVIDER_ROUTE_ID, undefined)
assert.equal(resolveActiveRouteIdFromEnv(env), 'gemini')
assert.equal(isDefaultStartupProviderEnv(env), false)
})
test('buildStartupEnvFromProfile respects an explicit CLAUDE_CODE_USE_OPENAI=0 opt-out (issue #1245)', async () => {
const env = await buildStartupEnvFromProfile({
persisted: null,
+83 -4
View File
@@ -25,6 +25,7 @@ import {
getRouteDefaultBaseUrl,
getRouteDefaultModel,
normalizeXiaomiMimoBaseUrl,
resolveRouteCredentialValue,
resolveRouteIdFromBaseUrl,
} from '../integrations/routeMetadata.js'
import {
@@ -1237,6 +1238,28 @@ export function hasExplicitProviderSelection(
)
}
function hasExplicitNonOpenAIProviderSelection(
processEnv: NodeJS.ProcessEnv = process.env,
): boolean {
return (
isEnvTruthy(processEnv.CLAUDE_CODE_USE_GITHUB) ||
isEnvTruthy(processEnv.CLAUDE_CODE_USE_GEMINI) ||
isEnvTruthy(processEnv.CLAUDE_CODE_USE_MISTRAL) ||
isEnvTruthy(processEnv.CLAUDE_CODE_USE_BEDROCK) ||
isEnvTruthy(processEnv.CLAUDE_CODE_USE_VERTEX) ||
isEnvTruthy(processEnv.CLAUDE_CODE_USE_FOUNDRY)
)
}
function hasExplicitOpenAICompatibleOptOut(
processEnv: NodeJS.ProcessEnv = process.env,
): boolean {
return (
processEnv.CLAUDE_CODE_USE_OPENAI !== undefined &&
!isEnvTruthy(processEnv.CLAUDE_CODE_USE_OPENAI)
)
}
function hasConcreteProviderSelection(
processEnv: NodeJS.ProcessEnv = process.env,
): boolean {
@@ -1300,6 +1323,38 @@ function hasConcreteProviderSelection(
)
}
function getConcreteOpenAICompatibleEnvRouteId(
processEnv: NodeJS.ProcessEnv = process.env,
): string | null {
const openAIBaseUrl =
sanitizeProviderConfigValue(processEnv.OPENAI_BASE_URL) ??
sanitizeProviderConfigValue(processEnv.OPENAI_API_BASE)
const openAIModel = normalizeProfileModel(
sanitizeProviderConfigValue(processEnv.OPENAI_MODEL),
)
const openAICredential = resolveOpenAICredentialEnvSelection(processEnv)
const openAIRouteId = resolveRouteIdFromBaseUrl(openAIBaseUrl)
const routeCredential = sanitizeApiKey(
resolveRouteCredentialValue({
routeId: openAIRouteId ?? 'custom',
baseUrl: openAIBaseUrl,
processEnv,
}),
)
const authHeaderValue = sanitizeApiKey(processEnv.OPENAI_AUTH_HEADER_VALUE)
if (
openAIBaseUrl !== undefined &&
openAIModel !== undefined &&
(openAICredential?.kind === 'usable' ||
routeCredential !== undefined ||
authHeaderValue !== undefined)
) {
return openAIRouteId ?? 'custom'
}
return null
}
export function selectAutoProfile(
recommendedOllamaModel: string | null,
): ProviderProfile {
@@ -1857,6 +1912,7 @@ export async function buildLaunchEnv(options: {
'FIREWORKS_API_KEY',
'AIMLAPI_API_KEY',
'MIMO_API_KEY',
'NVIDIA_API_KEY',
'VENICE_API_KEY',
] as const) {
// AI/ML API accepts the generic OPENAI_API_KEY, so it does not need an
@@ -1866,6 +1922,9 @@ export async function buildLaunchEnv(options: {
if (dedicatedKey === 'AIMLAPI_API_KEY' && effectiveOpenAIRouteId !== 'aimlapi') {
continue
}
if (dedicatedKey === 'NVIDIA_API_KEY' && effectiveOpenAIRouteId !== 'nvidia-nim') {
continue
}
const dedicatedValue =
(dedicatedKey === 'AIMLAPI_API_KEY' && openAICredential?.kind === 'usable'
? sanitizeApiKey(openAICredential.value)
@@ -1876,6 +1935,12 @@ export async function buildLaunchEnv(options: {
env[dedicatedKey] = dedicatedValue
}
}
if (effectiveOpenAIRouteId === 'nvidia-nim') {
const nvidiaNimFlag = processEnv.NVIDIA_NIM || persistedEnv.NVIDIA_NIM
if (nvidiaNimFlag) {
env.NVIDIA_NIM = nvidiaNimFlag
}
}
const customHeaders = shellCustomHeaders || persistedCustomHeaders
if (customHeaders) {
env.ANTHROPIC_CUSTOM_HEADERS = customHeaders
@@ -1926,6 +1991,23 @@ export async function buildStartupEnvFromProfile(options?: {
return processEnv
}
const concreteOpenAIRouteId = getConcreteOpenAICompatibleEnvRouteId(processEnv)
if (
concreteOpenAIRouteId === 'nvidia-nim' &&
!isEnvTruthy(processEnv.CLAUDE_CODE_USE_OPENAI) &&
!hasExplicitOpenAICompatibleOptOut(processEnv) &&
!hasExplicitNonOpenAIProviderSelection(processEnv)
) {
return buildLaunchEnv({
profile: 'openai',
persisted: null,
goal:
options?.goal ??
normalizeRecommendationGoal(processEnv.OPENCLAUDE_PROFILE_GOAL),
processEnv,
})
}
// If startup already has a concrete provider selection, keep trusting it.
// This prevents legacy profiles or the fresh-install default from becoming
// a silent third precedence layer over explicit env/flags.
@@ -1943,10 +2025,7 @@ export async function buildStartupEnvFromProfile(options?: {
// injecting the default Opengateway profile — otherwise the fallback
// re-enables OpenAI and the startup validator reports a spurious missing
// OPENAI_API_KEY warning (#1245).
if (
processEnv.CLAUDE_CODE_USE_OPENAI !== undefined &&
!isEnvTruthy(processEnv.CLAUDE_CODE_USE_OPENAI)
) {
if (hasExplicitOpenAICompatibleOptOut(processEnv)) {
return processEnv
}