feat: add nvidia provider
Browse files- src/commands/login/login.tsx +30 -4
- src/components/NvidiaLoginFlow.tsx +106 -0
- src/hooks/useApiKeyVerification.ts +39 -4
- src/server/config/providerPresets.json +19 -0
- src/services/api/client.ts +15 -3
- src/services/api/nvidiaClient.ts +246 -0
- src/services/voice/doubaoSTT.ts +34 -0
- src/setup.ts +3 -0
- src/utils/auth.ts +33 -3
- src/utils/config.ts +5 -1
- src/utils/model/model.ts +5 -0
- src/utils/model/modelOptions.ts +27 -0
- src/utils/model/modelStrings.ts +18 -0
- src/utils/model/providers.ts +23 -2
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 |
-
//
|
| 92 |
const { getAPIProvider } = await import('../../utils/model/providers.js')
|
| 93 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
-
:
|
|
|
|
|
|
|
| 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 |
/**
|