chenbhao commited on
Commit
eb16a76
·
1 Parent(s): 3596123

feat: add nvidia provider

Browse files
src/commands/login/login.tsx CHANGED
@@ -25,6 +25,7 @@ import { OpenAILoginFlow } from '../../components/OpenAILoginFlow.js'
25
  import { OpenRouterLoginFlow } from '../../components/OpenRouterLoginFlow.js'
26
  import { LocalLoginFlow } from '../../components/LocalLoginFlow.js'
27
  import { OpenCodeLoginFlow } from '../../components/OpenCodeLoginFlow.js'
 
28
 
29
  import { useMainLoopModel } from '../../hooks/useMainLoopModel.js';
30
 
@@ -48,7 +49,7 @@ import {
48
  import { resetUserCache } from '../../utils/user.js';
49
 
50
  // TODO
51
- type AuthProviderChoice = 'anthropic' | 'openai' | 'openrouter' | 'local' | 'opencode'
52
 
53
  /* 第一层: 入口函数 call()
54
  * CLI 执行 /login 真正被调用的函数
@@ -88,18 +89,26 @@ export async function call(
88
  void checkAndDisableAutoModeIfNeeded(appState.toolPermissionContext, context.setAppState, appState.fastMode);
89
  }
90
 
91
- // For OpenCode provider, pre-fetch models so /model shows them immediately
92
  const { getAPIProvider } = await import('../../utils/model/providers.js')
93
- if (getAPIProvider() === 'opencode') {
 
94
  const { fetchOpencodeModels } = await import('../../services/api/opencodeClient.js')
95
  await fetchOpencodeModels()
 
 
 
96
  }
97
 
 
 
 
 
98
  // Increment authVersion to trigger re-fetching of auth-dependent data
99
  context.setAppState(prev => ({
100
  ...prev,
101
  authVersion: prev.authVersion + 1,
102
- mainLoopModel: 'big-pickle',
103
  mainLoopModelForSession: null,
104
  }));
105
  }
@@ -187,6 +196,18 @@ export function Login(props: {
187
  ),
188
  value: 'local',
189
  },
 
 
 
 
 
 
 
 
 
 
 
 
190
  ],
191
  [],
192
  )
@@ -235,6 +256,11 @@ export function Login(props: {
235
  onDone={onFlowDone}
236
  startingMessage="Configure local model server (Ollama, LM Studio, vLLM, etc.)."
237
  />
 
 
 
 
 
238
  ) : (
239
  <ConsoleOAuthFlow
240
  onDone={onFlowDone}
 
25
  import { OpenRouterLoginFlow } from '../../components/OpenRouterLoginFlow.js'
26
  import { LocalLoginFlow } from '../../components/LocalLoginFlow.js'
27
  import { OpenCodeLoginFlow } from '../../components/OpenCodeLoginFlow.js'
28
+ import { NvidiaLoginFlow } from '../../components/NvidiaLoginFlow.js'
29
 
30
  import { useMainLoopModel } from '../../hooks/useMainLoopModel.js';
31
 
 
49
  import { resetUserCache } from '../../utils/user.js';
50
 
51
  // TODO
52
+ type AuthProviderChoice = 'anthropic' | 'openai' | 'openrouter' | 'local' | 'opencode' | 'nvidia'
53
 
54
  /* 第一层: 入口函数 call()
55
  * CLI 执行 /login 真正被调用的函数
 
89
  void checkAndDisableAutoModeIfNeeded(appState.toolPermissionContext, context.setAppState, appState.fastMode);
90
  }
91
 
92
+ // Pre-fetch models for providers that support dynamic model listing
93
  const { getAPIProvider } = await import('../../utils/model/providers.js')
94
+ const provider = getAPIProvider()
95
+ if (provider === 'opencode') {
96
  const { fetchOpencodeModels } = await import('../../services/api/opencodeClient.js')
97
  await fetchOpencodeModels()
98
+ } else if (provider === 'nvidia') {
99
+ const { fetchNvidiaModels } = await import('../../services/api/nvidiaClient.js')
100
+ await fetchNvidiaModels()
101
  }
102
 
103
+ // Clear cached model strings so they re-initialize with the new provider
104
+ const { clearModelStrings } = await import('../../utils/model/modelStrings.js')
105
+ clearModelStrings()
106
+
107
  // Increment authVersion to trigger re-fetching of auth-dependent data
108
  context.setAppState(prev => ({
109
  ...prev,
110
  authVersion: prev.authVersion + 1,
111
+ mainLoopModel: provider === 'nvidia' ? null : 'big-pickle',
112
  mainLoopModelForSession: null,
113
  }));
114
  }
 
196
  ),
197
  value: 'local',
198
  },
199
+ {
200
+ label: (
201
+ <Text>
202
+ NVIDIA{' '}
203
+ <Text dimColor={true}>
204
+ NVIDIA NIM API key from build.nvidia.com
205
+ </Text>
206
+ {'\n'}
207
+ </Text>
208
+ ),
209
+ value: 'nvidia',
210
+ },
211
  ],
212
  [],
213
  )
 
256
  onDone={onFlowDone}
257
  startingMessage="Configure local model server (Ollama, LM Studio, vLLM, etc.)."
258
  />
259
+ ) : selectedProvider === 'nvidia' ? (
260
+ <NvidiaLoginFlow
261
+ onDone={onFlowDone}
262
+ startingMessage="Better-Clawd can use NVIDIA with your NVIDIA API key."
263
+ />
264
  ) : (
265
  <ConsoleOAuthFlow
266
  onDone={onFlowDone}
src/components/NvidiaLoginFlow.tsx ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from 'react'
2
+ import { useState, useEffect } from 'react'
3
+ import { Box, Text } from '../ink.js'
4
+ import { saveNvidiaApiKey, getNvidiaApiKey } from '../utils/auth.js'
5
+ import { Spinner } from './Spinner.js'
6
+ import TextInput from './TextInput.js'
7
+
8
+ type NvidiaLoginFlowProps = {
9
+ onDone: () => void
10
+ startingMessage?: string
11
+ }
12
+
13
+ export function NvidiaLoginFlow({
14
+ onDone,
15
+ startingMessage,
16
+ }: NvidiaLoginFlowProps): React.ReactNode {
17
+ const [isBusy, setIsBusy] = useState(false)
18
+ const [status, setStatus] = useState<string | null>(null)
19
+ const [inputValue, setInputValue] = useState('')
20
+ const [cursorOffset, setCursorOffset] = useState(0)
21
+ const [existingKey, setExistingKey] = useState<string | null>(null)
22
+
23
+ useEffect(() => {
24
+ const key = getNvidiaApiKey()
25
+ if (key) {
26
+ setExistingKey(key)
27
+ }
28
+ }, [])
29
+
30
+ async function handleSubmit(value?: string): Promise<void> {
31
+ if (!value && !existingKey) {
32
+ setStatus('Please enter an API key or press Esc to cancel')
33
+ return
34
+ }
35
+
36
+ const trimmed = value?.trim() || ''
37
+ const keyToSave = trimmed || existingKey || ''
38
+
39
+ if (!keyToSave) {
40
+ setStatus('Please enter an API key or press Esc to cancel')
41
+ return
42
+ }
43
+
44
+ setIsBusy(true)
45
+ setStatus(null)
46
+ try {
47
+ await saveNvidiaApiKey(keyToSave)
48
+ onDone()
49
+ } catch (error) {
50
+ setStatus(error instanceof Error ? error.message : String(error))
51
+ } finally {
52
+ setIsBusy(false)
53
+ }
54
+ }
55
+
56
+ if (isBusy) {
57
+ return (
58
+ <Box flexDirection="column" gap={1}>
59
+ <Box>
60
+ <Spinner />
61
+ <Text>Configuring NVIDIA login for Better-Clawd...</Text>
62
+ </Box>
63
+ <Text dimColor={true}>
64
+ NVIDIA GPU-accelerated models are accessed via the NVIDIA API
65
+ catalog at `https://integrate.api.nvidia.com/v1`.
66
+ </Text>
67
+ </Box>
68
+ )
69
+ }
70
+
71
+ return (
72
+ <Box flexDirection="column" gap={1}>
73
+ <Text>
74
+ {startingMessage ??
75
+ 'Better-Clawd can use NVIDIA with your NVIDIA API key.'}
76
+ </Text>
77
+ <Text dimColor={true}>
78
+ Paste your NVIDIA API key to use models from the NVIDIA API catalog
79
+ via the OpenAI-compatible endpoint.
80
+ </Text>
81
+ <Box>
82
+ <Text>Paste your NVIDIA API key:</Text>
83
+ <TextInput
84
+ value={inputValue}
85
+ onChange={setInputValue}
86
+ onSubmit={handleSubmit}
87
+ onExit={() => {
88
+ setInputValue('')
89
+ setCursorOffset(0)
90
+ }}
91
+ cursorOffset={cursorOffset}
92
+ onChangeCursorOffset={setCursorOffset}
93
+ columns={72}
94
+ mask="*"
95
+ focus={true}
96
+ placeholder={existingKey || undefined}
97
+ />
98
+ </Box>
99
+ {status ? <Text color="error">{status}</Text> : null}
100
+ <Text dimColor={true}>
101
+ Press <Text bold={true}>Enter</Text> to save, or <Text bold={true}>Esc</Text>{' '}
102
+ to cancel.
103
+ </Text>
104
+ </Box>
105
+ )
106
+ }
src/hooks/useApiKeyVerification.ts CHANGED
@@ -62,13 +62,23 @@ export function useApiKeyVerification(): ApiKeyVerificationResult {
62
  }
63
  return 'missing'
64
  }
65
-
66
  // Check OpenCode Zen authentication
67
  // OpenCode Zen uses free models by default, no API key required
68
  if (authProvider === 'opencode') {
69
  return 'valid'
70
  }
71
-
 
 
 
 
 
 
 
 
 
 
72
  // Anthropic authentication
73
  if (!isAnthropicAuthEnabled() || isClaudeAISubscriber()) {
74
  return 'valid'
@@ -142,7 +152,20 @@ export function useApiKeyVerification(): ApiKeyVerificationResult {
142
  setStatus('valid')
143
  return
144
  }
145
-
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  // Anthropic authentication
147
  if (!isAnthropicAuthEnabled() || isClaudeAISubscriber()) {
148
  setStatus('valid')
@@ -227,7 +250,19 @@ export function useApiKeyVerification(): ApiKeyVerificationResult {
227
  setStatus('valid')
228
  return
229
  }
230
-
 
 
 
 
 
 
 
 
 
 
 
 
231
  // Anthropic authentication
232
  if (!isAnthropicAuthEnabled() || isClaudeAISubscriber()) {
233
  setStatus('valid')
 
62
  }
63
  return 'missing'
64
  }
65
+
66
  // Check OpenCode Zen authentication
67
  // OpenCode Zen uses free models by default, no API key required
68
  if (authProvider === 'opencode') {
69
  return 'valid'
70
  }
71
+
72
+ // Check NVIDIA authentication
73
+ if (authProvider === 'nvidia') {
74
+ const config = getGlobalConfig()
75
+ const hasApiKey = !!config.nvidiaApiKey
76
+ if (hasApiKey) {
77
+ return 'valid'
78
+ }
79
+ return 'missing'
80
+ }
81
+
82
  // Anthropic authentication
83
  if (!isAnthropicAuthEnabled() || isClaudeAISubscriber()) {
84
  return 'valid'
 
152
  setStatus('valid')
153
  return
154
  }
155
+
156
+ // Check NVIDIA authentication
157
+ if (authProvider === 'nvidia') {
158
+ const { getGlobalConfig: getConfig } = await import('../utils/config.js')
159
+ const freshConfig = getConfig()
160
+ const hasApiKey = !!freshConfig.nvidiaApiKey
161
+ if (hasApiKey) {
162
+ setStatus('valid')
163
+ } else {
164
+ setStatus('missing')
165
+ }
166
+ return
167
+ }
168
+
169
  // Anthropic authentication
170
  if (!isAnthropicAuthEnabled() || isClaudeAISubscriber()) {
171
  setStatus('valid')
 
250
  setStatus('valid')
251
  return
252
  }
253
+
254
+ // Check NVIDIA authentication
255
+ if (authProvider === 'nvidia') {
256
+ const config = getGlobalConfig()
257
+ const hasApiKey = !!config.nvidiaApiKey
258
+ if (hasApiKey) {
259
+ setStatus('valid')
260
+ } else {
261
+ setStatus('missing')
262
+ }
263
+ return
264
+ }
265
+
266
  // Anthropic authentication
267
  if (!isAnthropicAuthEnabled() || isClaudeAISubscriber()) {
268
  setStatus('valid')
src/server/config/providerPresets.json CHANGED
@@ -210,6 +210,25 @@
210
  "ANTHROPIC_AUTH_TOKEN": "ollama"
211
  }
212
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
  {
214
  "id": "custom",
215
  "name": "Custom",
 
210
  "ANTHROPIC_AUTH_TOKEN": "ollama"
211
  }
212
  },
213
+ {
214
+ "id": "nvidia",
215
+ "name": "NVIDIA NIM",
216
+ "baseUrl": "https://integrate.api.nvidia.com/v1",
217
+ "apiFormat": "openai_chat",
218
+ "defaultModels": {
219
+ "main": "nvidia/llama-3.1-nemotron-70b-instruct",
220
+ "haiku": "nvidia/llama-3.1-nemotron-70b-instruct",
221
+ "sonnet": "nvidia/llama-3.1-nemotron-70b-instruct",
222
+ "opus": "nvidia/llama-3.1-nemotron-70b-instruct"
223
+ },
224
+ "needsApiKey": true,
225
+ "websiteUrl": "https://build.nvidia.com",
226
+ "apiKeyUrl": "https://build.nvidia.com",
227
+ "authStrategy": "api_key",
228
+ "defaultEnv": {
229
+ "NVIDIA_BASE_URL": "https://integrate.api.nvidia.com/v1"
230
+ }
231
+ },
232
  {
233
  "id": "custom",
234
  "name": "Custom",
src/services/api/client.ts CHANGED
@@ -26,6 +26,7 @@ import {
26
  } from 'src/utils/model/providers.js'
27
  import { getProxyFetchOptions } from 'src/utils/proxy.js'
28
  import { createOpenCodeFetchOverride, fetchOpencodeModels } from './opencodeClient.js'
 
29
  import {
30
  getIsNonInteractiveSession,
31
  getSessionId,
@@ -155,7 +156,13 @@ export async function getAnthropicClient({
155
  opencodeFetchOverride = createOpenCodeFetchOverride(resolvedModel)
156
  }
157
 
158
- const resolvedFetch = buildFetch(fetchOverride || opencodeFetchOverride, source)
 
 
 
 
 
 
159
 
160
  const ARGS = {
161
  defaultHeaders,
@@ -366,6 +373,11 @@ export async function getAnthropicClient({
366
  clientConfig.apiKey = 'opencode-zen'
367
  }
368
 
 
 
 
 
 
369
  return new Anthropic(clientConfig)
370
  }
371
 
@@ -375,8 +387,8 @@ async function configureApiKeyHeaders(
375
  ): Promise<void> {
376
  const provider = getAPIProvider()
377
 
378
- // Skip for OpenRouter, OpenAI, Local, and OpenCode - they use apiKey parameter instead
379
- if (provider === 'openrouter' || provider === 'openai' || provider === 'local' || provider === 'opencode') {
380
  return
381
  }
382
 
 
26
  } from 'src/utils/model/providers.js'
27
  import { getProxyFetchOptions } from 'src/utils/proxy.js'
28
  import { createOpenCodeFetchOverride, fetchOpencodeModels } from './opencodeClient.js'
29
+ import { createNvidiaFetchOverride } from './nvidiaClient.js'
30
  import {
31
  getIsNonInteractiveSession,
32
  getSessionId,
 
156
  opencodeFetchOverride = createOpenCodeFetchOverride(resolvedModel)
157
  }
158
 
159
+ // For NVIDIA provider, use custom fetch to convert Anthropic format to OpenAI-compatible format
160
+ let nvidiaFetchOverride: ClientOptions['fetch'] | undefined
161
+ if (provider === 'nvidia') {
162
+ nvidiaFetchOverride = createNvidiaFetchOverride()
163
+ }
164
+
165
+ const resolvedFetch = buildFetch(fetchOverride || opencodeFetchOverride || nvidiaFetchOverride, source)
166
 
167
  const ARGS = {
168
  defaultHeaders,
 
373
  clientConfig.apiKey = 'opencode-zen'
374
  }
375
 
376
+ // Handle NVIDIA - uses custom fetch override to convert Anthropic format to OpenAI format
377
+ if (provider === 'nvidia') {
378
+ clientConfig.apiKey = 'nvidia-nim'
379
+ }
380
+
381
  return new Anthropic(clientConfig)
382
  }
383
 
 
387
  ): Promise<void> {
388
  const provider = getAPIProvider()
389
 
390
+ // Skip for OpenRouter, OpenAI, Local, OpenCode, and NVIDIA - they use apiKey parameter instead
391
+ if (provider === 'openrouter' || provider === 'openai' || provider === 'local' || provider === 'opencode' || provider === 'nvidia') {
392
  return
393
  }
394
 
src/services/api/nvidiaClient.ts ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getNvidiaApiKey } from '../../utils/auth.js'
2
+ import { getNvidiaBaseUrl } from '../../utils/model/providers.js'
3
+ import {
4
+ convertAnthropicMessagesToOpenAI,
5
+ convertAnthropicToolsToOpenAI,
6
+ convertOpenAIStreamToAnthropic,
7
+ type AnthropicMessage,
8
+ } from './copilotClient.js'
9
+
10
+ /**
11
+ * NVIDIA NIM API uses the OpenAI-compatible `/v1/chat/completions` protocol.
12
+ * This fetch override intercepts Anthropic Messages API calls and translates
13
+ * them to the OpenAI format that NVIDIA expects.
14
+ */
15
+
16
+ const NVIDIA_MODEL = process.env.NVIDIA_MODEL || 'nvidia/llama-3.1-nemotron-70b-instruct'
17
+
18
+ function normalizeBaseUrl(url: string): string {
19
+ return url.replace(/\/$/, '')
20
+ }
21
+
22
+ function chatCompletionsUrl(base: string): string {
23
+ const b = normalizeBaseUrl(base)
24
+ if (b.endsWith('/v1')) {
25
+ return `${b}/chat/completions`
26
+ }
27
+ return `${b}/v1/chat/completions`
28
+ }
29
+
30
+ export function createNvidiaFetchOverride(): (input: RequestInfo | URL, init?: RequestInit) => Promise<Response> {
31
+ const baseUrl = getNvidiaBaseUrl()
32
+ const apiKey = getNvidiaApiKey()
33
+ const endpoint = chatCompletionsUrl(baseUrl)
34
+
35
+ return async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
36
+ const url = input instanceof URL ? input.href : typeof input === 'string' ? input : input.url
37
+
38
+ // Only intercept Messages API calls
39
+ if (!url.includes('/messages') && !url.includes('/v1/')) {
40
+ return fetch(input, init)
41
+ }
42
+
43
+ // Stub out token counting and model listing
44
+ if (url.includes('/count_tokens') || url.includes('/models')) {
45
+ return new Response(JSON.stringify({ input_tokens: 0 }), {
46
+ status: 200,
47
+ headers: { 'Content-Type': 'application/json' },
48
+ })
49
+ }
50
+
51
+ let anthropicBody: Record<string, unknown> = {}
52
+ if (init?.body) {
53
+ try {
54
+ anthropicBody = JSON.parse(
55
+ typeof init.body === 'string' ? init.body : new TextDecoder().decode(init.body as ArrayBuffer),
56
+ )
57
+ } catch {
58
+ return fetch(input, init)
59
+ }
60
+ }
61
+
62
+ const systemBlocks = anthropicBody.system as
63
+ | Array<{ type: string; text: string }>
64
+ | string
65
+ | undefined
66
+ let systemPrompt = ''
67
+ if (typeof systemBlocks === 'string') {
68
+ systemPrompt = systemBlocks
69
+ } else if (Array.isArray(systemBlocks)) {
70
+ systemPrompt = systemBlocks
71
+ .filter(b => b.type === 'text')
72
+ .map(b => b.text)
73
+ .join('\n\n')
74
+ }
75
+
76
+ const anthropicMessages = (anthropicBody.messages || []) as AnthropicMessage[]
77
+ const openaiMessages = convertAnthropicMessagesToOpenAI(anthropicMessages, systemPrompt)
78
+
79
+ const anthropicTools = (anthropicBody.tools || []) as Array<{
80
+ name: string
81
+ description?: string
82
+ input_schema?: Record<string, unknown>
83
+ }>
84
+ const openaiTools = anthropicTools.length > 0 ? convertAnthropicToolsToOpenAI(anthropicTools) : undefined
85
+
86
+ const isStreaming = anthropicBody.stream === true
87
+
88
+ // Use the model from the user's selection, falling back to the env var / default
89
+ const selectedModel = (anthropicBody.model as string) || NVIDIA_MODEL
90
+
91
+ const requestBody: Record<string, unknown> = {
92
+ model: selectedModel,
93
+ messages: openaiMessages,
94
+ stream: isStreaming,
95
+ }
96
+
97
+ if (anthropicBody.max_tokens) {
98
+ requestBody.max_tokens = anthropicBody.max_tokens
99
+ }
100
+
101
+ if (openaiTools && openaiTools.length > 0) {
102
+ requestBody.tools = openaiTools
103
+ requestBody.tool_choice = 'auto'
104
+ }
105
+
106
+ const headers: Record<string, string> = {
107
+ 'Content-Type': 'application/json',
108
+ 'User-Agent': 'claude-code/2.1.88',
109
+ 'HTTP-Referer': 'https://claude.ai/',
110
+ 'X-Title': 'Better-Clawd',
111
+ 'X-BILLING-INVOKE-ORIGIN': 'Better-Clawd',
112
+ }
113
+ if (apiKey) {
114
+ headers.Authorization = `Bearer ${apiKey}`
115
+ }
116
+
117
+ const nvidiaResponse = await fetch(endpoint, {
118
+ method: 'POST',
119
+ headers,
120
+ body: JSON.stringify(requestBody),
121
+ signal: init?.signal,
122
+ })
123
+
124
+ if (!nvidiaResponse.ok) {
125
+ return nvidiaResponse
126
+ }
127
+
128
+ if (!isStreaming) {
129
+ const data = (await nvidiaResponse.json()) as {
130
+ id: string
131
+ choices: Array<{
132
+ message: {
133
+ role: string
134
+ content: string | null
135
+ tool_calls?: Array<{
136
+ id: string
137
+ function: { name: string; arguments: string }
138
+ }>
139
+ }
140
+ finish_reason: string
141
+ }>
142
+ usage?: { prompt_tokens: number; completion_tokens: number }
143
+ }
144
+
145
+ const choice = data.choices[0]
146
+ const anthropicContent: Array<{
147
+ type: string
148
+ text?: string
149
+ id?: string
150
+ name?: string
151
+ input?: unknown
152
+ }> = []
153
+
154
+ if (choice?.message?.content) {
155
+ anthropicContent.push({ type: 'text', text: choice.message.content })
156
+ }
157
+
158
+ if (choice?.message?.tool_calls) {
159
+ for (const tc of choice.message.tool_calls) {
160
+ anthropicContent.push({
161
+ type: 'tool_use',
162
+ id: tc.id,
163
+ name: tc.function.name,
164
+ input: JSON.parse(tc.function.arguments || '{}'),
165
+ })
166
+ }
167
+ }
168
+
169
+ const anthropicResponse = {
170
+ id: data.id || `msg_nvidia_${Date.now()}`,
171
+ type: 'message',
172
+ role: 'assistant',
173
+ content: anthropicContent,
174
+ model: selectedModel,
175
+ stop_reason: choice?.finish_reason === 'tool_calls' ? 'tool_use' : 'end_turn',
176
+ usage: {
177
+ input_tokens: data.usage?.prompt_tokens || 0,
178
+ output_tokens: data.usage?.completion_tokens || 0,
179
+ },
180
+ }
181
+
182
+ return new Response(JSON.stringify(anthropicResponse), {
183
+ status: 200,
184
+ headers: { 'Content-Type': 'application/json' },
185
+ })
186
+ }
187
+
188
+ // Streaming response
189
+ if (!nvidiaResponse.body) {
190
+ return nvidiaResponse
191
+ }
192
+
193
+ const transformStream = convertOpenAIStreamToAnthropic(nvidiaResponse.body, selectedModel)
194
+
195
+ return new Response(transformStream, {
196
+ status: 200,
197
+ headers: {
198
+ 'Content-Type': 'text/event-stream',
199
+ 'Cache-Control': 'no-cache',
200
+ Connection: 'keep-alive',
201
+ },
202
+ })
203
+ }
204
+ }
205
+
206
+ let cachedNvidiaModels: string[] | null = null
207
+
208
+ export function getCachedNvidiaModels(): string[] {
209
+ return cachedNvidiaModels || []
210
+ }
211
+
212
+ /**
213
+ * Fetch available models from the NVIDIA API catalog.
214
+ */
215
+ export async function fetchNvidiaModels(apiKey?: string): Promise<string[]> {
216
+ const baseUrl = getNvidiaBaseUrl()
217
+ const key = apiKey || getNvidiaApiKey()
218
+ const normalizedBase = normalizeBaseUrl(baseUrl)
219
+ const modelsUrl = normalizedBase.endsWith('/v1')
220
+ ? `${normalizedBase}/models`
221
+ : `${normalizedBase}/v1/models`
222
+
223
+ const headers: Record<string, string> = {}
224
+ if (key) {
225
+ headers.Authorization = `Bearer ${key}`
226
+ }
227
+
228
+ try {
229
+ const res = await fetch(modelsUrl, { headers, signal: AbortSignal.timeout(20_000) })
230
+ if (!res.ok) {
231
+ cachedNvidiaModels = []
232
+ return []
233
+ }
234
+ const json = (await res.json()) as { data?: Array<{ id: string }> }
235
+ if (json.data && Array.isArray(json.data)) {
236
+ const modelIds = json.data.map((m: { id: string }) => m.id)
237
+ cachedNvidiaModels = modelIds
238
+ return modelIds
239
+ }
240
+ cachedNvidiaModels = []
241
+ return []
242
+ } catch {
243
+ cachedNvidiaModels = []
244
+ return []
245
+ }
246
+ }
src/services/voice/doubaoSTT.ts ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // @generated stub from scan-missing-imports
2
+ // 该文件自动生成,对应 ant-internal 的 feature() gated 模块。
3
+ // 所有外部 build 的代码路径在 DCE 后都不会真的执行这里的代码,这只是
4
+ // bun build resolver 的占位符。
5
+ const __target = function noop() {}
6
+ const __handler: ProxyHandler<any> = {
7
+ get(_t, prop) {
8
+ if (prop === '__esModule') return true
9
+ if (prop === 'default') return new Proxy(__target, __handler)
10
+ if (prop === Symbol.toPrimitive) return () => undefined
11
+ if (prop === Symbol.iterator) return function* () {}
12
+ if (prop === Symbol.asyncIterator) return async function* () {}
13
+ if (prop === 'then') return undefined
14
+ return new Proxy(__target, __handler)
15
+ },
16
+ apply() {
17
+ return new Proxy(__target, __handler)
18
+ },
19
+ construct() {
20
+ return new Proxy(__target, __handler)
21
+ },
22
+ }
23
+ const stub: any = new Proxy(__target, __handler)
24
+ export default stub
25
+ export const __stubMissing = true
26
+ // 兼容常见的命名导出 —— 没列在这里的也会通过 default Proxy 兜底
27
+ export const createCachedMCState = stub
28
+ export const isCachedMicrocompactEnabled = stub
29
+ export const isModelSupportedForCacheEditing = stub
30
+ export const getCachedMCConfig = stub
31
+ export const markToolsSentToAPI = stub
32
+ export const resetCachedMCState = stub
33
+ export const checkProtectedNamespace = stub
34
+ export const getCoordinatorUserContext = stub
src/setup.ts CHANGED
@@ -400,6 +400,9 @@ export async function setup(
400
  } else if (configuredAuthProvider === 'opencode') {
401
  const { fetchOpencodeModels } = await import('./services/api/opencodeClient.js')
402
  void fetchOpencodeModels()
 
 
 
403
  }
404
 
405
  // If permission mode is set to bypass, verify we're in a safe environment
 
400
  } else if (configuredAuthProvider === 'opencode') {
401
  const { fetchOpencodeModels } = await import('./services/api/opencodeClient.js')
402
  void fetchOpencodeModels()
403
+ } else if (configuredAuthProvider === 'nvidia') {
404
+ const { fetchNvidiaModels } = await import('./services/api/nvidiaClient.js')
405
+ void fetchNvidiaModels()
406
  }
407
 
408
  // If permission mode is set to bypass, verify we're in a safe environment
src/utils/auth.ts CHANGED
@@ -307,7 +307,7 @@ export function getOpenRouterApiKeyWithSource(): {
307
  : { key: null, source: 'none' }
308
  }
309
 
310
- export function getConfiguredAuthProvider(): 'anthropic' | 'openrouter' | 'openai' | 'local' | 'opencode' | null {
311
  // First try to get from cache for performance
312
  const storedProvider = getGlobalConfig().authProvider
313
  if (storedProvider) {
@@ -319,13 +319,13 @@ export function getConfiguredAuthProvider(): 'anthropic' | 'openrouter' | 'opena
319
  }
320
 
321
  // Read authProvider directly from file to bypass cache
322
- export function getConfiguredAuthProviderFromFile(): 'anthropic' | 'openrouter' | 'openai' | 'local' | 'opencode' | null {
323
  try {
324
  // eslint-disable-next-line @typescript-eslint/no-require-imports
325
  const { readFileSync } = require('fs') as typeof import('fs')
326
  const raw = readFileSync(getGlobalClaudeFile(), 'utf8')
327
  const config = JSON.parse(raw) as {
328
- authProvider?: 'anthropic' | 'openrouter' | 'openai' | 'local' | 'opencode'
329
  }
330
 
331
  if (config.authProvider) {
@@ -430,6 +430,36 @@ export async function saveOpenRouterApiKey(apiKey: string): Promise<void> {
430
  clearStoredProviderCache()
431
  }
432
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
433
  export async function saveOpenCodeApiKey(apiKey: string, modelName?: string): Promise<void> {
434
  saveGlobalConfig(current => ({
435
  ...current,
 
307
  : { key: null, source: 'none' }
308
  }
309
 
310
+ export function getConfiguredAuthProvider(): 'anthropic' | 'openrouter' | 'openai' | 'local' | 'opencode' | 'nvidia' | null {
311
  // First try to get from cache for performance
312
  const storedProvider = getGlobalConfig().authProvider
313
  if (storedProvider) {
 
319
  }
320
 
321
  // Read authProvider directly from file to bypass cache
322
+ export function getConfiguredAuthProviderFromFile(): 'anthropic' | 'openrouter' | 'openai' | 'local' | 'opencode' | 'nvidia' | null {
323
  try {
324
  // eslint-disable-next-line @typescript-eslint/no-require-imports
325
  const { readFileSync } = require('fs') as typeof import('fs')
326
  const raw = readFileSync(getGlobalClaudeFile(), 'utf8')
327
  const config = JSON.parse(raw) as {
328
+ authProvider?: 'anthropic' | 'openrouter' | 'openai' | 'local' | 'opencode' | 'nvidia'
329
  }
330
 
331
  if (config.authProvider) {
 
430
  clearStoredProviderCache()
431
  }
432
 
433
+ export function getNvidiaApiKey(): string | null {
434
+ if (process.env.NVIDIA_API_KEY) {
435
+ return process.env.NVIDIA_API_KEY
436
+ }
437
+
438
+ const config = getGlobalConfig()
439
+ return config.nvidiaApiKey || null
440
+ }
441
+
442
+ export async function saveNvidiaApiKey(apiKey: string): Promise<void> {
443
+ if (!apiKey || typeof apiKey !== 'string') {
444
+ throw new Error('Invalid API key: API key must be a non-empty string.')
445
+ }
446
+
447
+ const trimmedKey = apiKey.trim()
448
+
449
+ if (!trimmedKey) {
450
+ throw new Error('Invalid API key: API key cannot be empty or whitespace only.')
451
+ }
452
+
453
+ saveGlobalConfig(current => ({
454
+ ...current,
455
+ authProvider: 'nvidia',
456
+ nvidiaApiKey: trimmedKey,
457
+ }))
458
+ // Clear provider cache so it will be re-read on next access
459
+ const { clearStoredProviderCache } = await import('./model/providers.js')
460
+ clearStoredProviderCache()
461
+ }
462
+
463
  export async function saveOpenCodeApiKey(apiKey: string, modelName?: string): Promise<void> {
464
  saveGlobalConfig(current => ({
465
  ...current,
src/utils/config.ts CHANGED
@@ -187,7 +187,7 @@ export type GlobalConfig = {
187
  apiKeyHelper?: string
188
 
189
  // Authentication provider configuration
190
- authProvider?: 'anthropic' | 'openai' | 'openrouter' | 'local' | 'opencode'
191
 
192
  // OpenRouter API key
193
  openRouterApiKey?: string
@@ -203,6 +203,9 @@ export type GlobalConfig = {
203
  // OpenCode Zen configuration
204
  openCodeApiKey?: string
205
  openCodeModelName?: string
 
 
 
206
 
207
  projects?: Record<string, ProjectConfig>
208
  numStartups: number
@@ -663,6 +666,7 @@ export const GLOBAL_CONFIG_KEYS = [
663
  'openAiAccessToken',
664
  'localBaseUrl',
665
  'localModelName',
 
666
  'installMethod',
667
  'autoUpdates',
668
  'autoUpdatesProtectedForNative',
 
187
  apiKeyHelper?: string
188
 
189
  // Authentication provider configuration
190
+ authProvider?: 'anthropic' | 'openai' | 'openrouter' | 'local' | 'opencode' | 'nvidia'
191
 
192
  // OpenRouter API key
193
  openRouterApiKey?: string
 
203
  // OpenCode Zen configuration
204
  openCodeApiKey?: string
205
  openCodeModelName?: string
206
+
207
+ // NVIDIA API key
208
+ nvidiaApiKey?: string
209
 
210
  projects?: Record<string, ProjectConfig>
211
  numStartups: number
 
666
  'openAiAccessToken',
667
  'localBaseUrl',
668
  'localModelName',
669
+ 'nvidiaApiKey',
670
  'installMethod',
671
  'autoUpdates',
672
  'autoUpdatesProtectedForNative',
src/utils/model/model.ts CHANGED
@@ -277,6 +277,11 @@ export function getDefaultMainLoopModelSetting(): ModelName | ModelAlias {
277
  return 'big-pickle'
278
  }
279
 
 
 
 
 
 
280
  // Ants default to defaultModel from flag config, or Opus 1M if not configured
281
  if (process.env.USER_TYPE === 'ant') {
282
  return (
 
277
  return 'big-pickle'
278
  }
279
 
280
+ // Check if using NVIDIA provider
281
+ if (apiProvider === 'nvidia') {
282
+ return getModelStrings().sonnet46
283
+ }
284
+
285
  // Ants default to defaultModel from flag config, or Opus 1M if not configured
286
  if (process.env.USER_TYPE === 'ant') {
287
  return (
src/utils/model/modelOptions.ts CHANGED
@@ -499,6 +499,33 @@ function getModelOptionsBase(fastMode = false): ModelOption[] {
499
  return [getDefaultOptionForUser(fastMode)]
500
  }
501
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
502
  // OpenCode Zen: Fetch models dynamically from API
503
  // If no API key, show only free models
504
  // If API key is provided, show all models except free ones
 
499
  return [getDefaultOptionForUser(fastMode)]
500
  }
501
 
502
+ // NVIDIA API: Show dynamically fetched models from the NVIDIA API catalog
503
+ if (getAPIProvider() === 'nvidia') {
504
+ // Trigger background fetch of NVIDIA models (non-blocking)
505
+ // This will populate the cache for subsequent calls
506
+ void import('../../services/api/nvidiaClient.js').then(({ fetchNvidiaModels }) => {
507
+ fetchNvidiaModels()
508
+ })
509
+
510
+ const { getCachedNvidiaModels } = require('../../services/api/nvidiaClient.js') as {
511
+ getCachedNvidiaModels: () => string[]
512
+ }
513
+ const models = getCachedNvidiaModels()
514
+
515
+ if (models && models.length > 0) {
516
+ return [
517
+ getDefaultOptionForUser(fastMode),
518
+ ...models.map(m => ({
519
+ value: m,
520
+ label: m,
521
+ description: 'NVIDIA NIM model',
522
+ })),
523
+ ]
524
+ }
525
+
526
+ return [getDefaultOptionForUser(fastMode)]
527
+ }
528
+
529
  // OpenCode Zen: Fetch models dynamically from API
530
  // If no API key, show only free models
531
  // If API key is provided, show all models except free ones
src/utils/model/modelStrings.ts CHANGED
@@ -50,6 +50,15 @@ function getBuiltinModelStrings(provider: APIProvider): ModelStrings {
50
  return out as ModelStrings
51
  }
52
 
 
 
 
 
 
 
 
 
 
53
  const out = {} as ModelStrings
54
  for (const key of MODEL_KEYS) {
55
  out[key] = ALL_MODEL_CONFIGS[key][provider]
@@ -191,3 +200,12 @@ export async function ensureModelStringsInitialized(): Promise<void> {
191
  // For Bedrock, wait for the profile fetch
192
  await updateBedrockModelStrings()
193
  }
 
 
 
 
 
 
 
 
 
 
50
  return out as ModelStrings
51
  }
52
 
53
+ if (provider === 'nvidia') {
54
+ const out = getBuiltinModelStrings('firstParty') as Record<string, string>
55
+ out.haiku45 = process.env.NVIDIA_HAIKU_MODEL || 'nvidia/llama-3.1-nemotron-70b-instruct'
56
+ out.sonnet45 = process.env.NVIDIA_SONNET_MODEL || 'nvidia/llama-3.1-nemotron-70b-instruct'
57
+ out.sonnet46 = process.env.NVIDIA_SONNET_MODEL || 'nvidia/llama-3.1-nemotron-70b-instruct'
58
+ out.opus46 = process.env.NVIDIA_OPUS_MODEL || 'nvidia/llama-3.1-nemotron-70b-instruct'
59
+ return out as ModelStrings
60
+ }
61
+
62
  const out = {} as ModelStrings
63
  for (const key of MODEL_KEYS) {
64
  out[key] = ALL_MODEL_CONFIGS[key][provider]
 
200
  // For Bedrock, wait for the profile fetch
201
  await updateBedrockModelStrings()
202
  }
203
+
204
+ /**
205
+ * Clear cached model strings so the next call to getModelStrings()
206
+ * re-initializes from the current provider. Call this after changing
207
+ * the auth provider (e.g., after /login).
208
+ */
209
+ export function clearModelStrings(): void {
210
+ setModelStringsState(null as unknown as ModelStrings)
211
+ }
src/utils/model/providers.ts CHANGED
@@ -8,6 +8,7 @@ export type APIProvider =
8
  | 'openai'
9
  | 'local'
10
  | 'opencode'
 
11
  | 'bedrock'
12
  | 'vertex'
13
  | 'foundry'
@@ -53,6 +54,9 @@ function getStoredProviderPreference(): APIProvider | null {
53
  case 'opencode':
54
  result = 'opencode'
55
  break
 
 
 
56
  case 'anthropic':
57
  result = 'firstParty'
58
  break
@@ -90,6 +94,8 @@ function getExplicitProviderOverride(): APIProvider | null {
90
  return 'local'
91
  case 'opencode':
92
  return 'opencode'
 
 
93
  case 'bedrock':
94
  return 'bedrock'
95
  case 'vertex':
@@ -126,6 +132,19 @@ export function isOpenRouterConfigured(): boolean {
126
  return getStoredProviderPreference() === 'openrouter'
127
  }
128
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  export function isOpenAIConfigured(): boolean {
130
  if (
131
  getExplicitProviderOverride() === 'openai' ||
@@ -199,7 +218,9 @@ export function getAPIProvider(): APIProvider | null {
199
  ? 'foundry'
200
  : isOpencodeConfigured()
201
  ? 'opencode'
202
- : isOpenAIConfigured()
 
 
203
  ? 'openai'
204
  : isOpenRouterConfigured()
205
  ? 'openrouter'
@@ -213,7 +234,7 @@ export function getAPIProviderForStatsig(): AnalyticsMetadata_I_VERIFIED_THIS_IS
213
  export function isAnthropicCompatibleProvider(
214
  provider: APIProvider = getAPIProvider(),
215
  ): boolean {
216
- return provider !== 'openai' && provider !== 'opencode'
217
  }
218
 
219
  /**
 
8
  | 'openai'
9
  | 'local'
10
  | 'opencode'
11
+ | 'nvidia'
12
  | 'bedrock'
13
  | 'vertex'
14
  | 'foundry'
 
54
  case 'opencode':
55
  result = 'opencode'
56
  break
57
+ case 'nvidia':
58
+ result = config.nvidiaApiKey ? 'nvidia' : null
59
+ break
60
  case 'anthropic':
61
  result = 'firstParty'
62
  break
 
94
  return 'local'
95
  case 'opencode':
96
  return 'opencode'
97
+ case 'nvidia':
98
+ return 'nvidia'
99
  case 'bedrock':
100
  return 'bedrock'
101
  case 'vertex':
 
132
  return getStoredProviderPreference() === 'openrouter'
133
  }
134
 
135
+ export function isNvidiaConfigured(): boolean {
136
+ if (getExplicitProviderOverride() === 'nvidia' || Boolean(process.env.NVIDIA_API_KEY)) {
137
+ return true
138
+ }
139
+
140
+ // Check config file
141
+ return getStoredProviderPreference() === 'nvidia'
142
+ }
143
+
144
+ export function getNvidiaBaseUrl(): string {
145
+ return process.env.NVIDIA_BASE_URL ?? 'https://integrate.api.nvidia.com/v1'
146
+ }
147
+
148
  export function isOpenAIConfigured(): boolean {
149
  if (
150
  getExplicitProviderOverride() === 'openai' ||
 
218
  ? 'foundry'
219
  : isOpencodeConfigured()
220
  ? 'opencode'
221
+ : isNvidiaConfigured()
222
+ ? 'nvidia'
223
+ : isOpenAIConfigured()
224
  ? 'openai'
225
  : isOpenRouterConfigured()
226
  ? 'openrouter'
 
234
  export function isAnthropicCompatibleProvider(
235
  provider: APIProvider = getAPIProvider(),
236
  ): boolean {
237
+ return provider !== 'openai' && provider !== 'opencode' && provider !== 'nvidia'
238
  }
239
 
240
  /**