feat: /login local model
Browse files- src/components/LocalLoginFlow.tsx +4 -7
- src/hooks/useApiKeyVerification.ts +118 -3
- src/services/api/client.ts +12 -2
- src/utils/auth.ts +89 -10
- src/utils/config.ts +21 -0
- src/utils/model/model.ts +12 -0
src/components/LocalLoginFlow.tsx
CHANGED
|
@@ -1,14 +1,11 @@
|
|
| 1 |
import * as React from 'react'
|
| 2 |
import { useState } from 'react'
|
| 3 |
-
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
| 4 |
import { saveLocalModelConfig } from '../utils/auth.js'
|
| 5 |
import { Box, Text } from '../ink.js'
|
| 6 |
import TextInput from './TextInput.js'
|
| 7 |
-
import { ConsoleOAuthFlow } from './ConsoleOAuthFlow.js'
|
| 8 |
-
import Dialog from './design-system/Dialog.js'
|
| 9 |
|
| 10 |
interface Props {
|
| 11 |
-
onDone:
|
| 12 |
startingMessage?: string
|
| 13 |
}
|
| 14 |
|
|
@@ -47,15 +44,15 @@ export function LocalLoginFlow({ onDone, startingMessage }: Props) {
|
|
| 47 |
try {
|
| 48 |
// 保存本地模型配置
|
| 49 |
await saveLocalModelConfig(finalUrl, finalModel)
|
| 50 |
-
onDone(
|
| 51 |
} catch (error) {
|
| 52 |
console.error('Failed to save local model config:', error)
|
| 53 |
-
onDone(
|
| 54 |
}
|
| 55 |
}
|
| 56 |
|
| 57 |
const handleCancel = () => {
|
| 58 |
-
onDone(
|
| 59 |
}
|
| 60 |
|
| 61 |
return (
|
|
|
|
| 1 |
import * as React from 'react'
|
| 2 |
import { useState } from 'react'
|
|
|
|
| 3 |
import { saveLocalModelConfig } from '../utils/auth.js'
|
| 4 |
import { Box, Text } from '../ink.js'
|
| 5 |
import TextInput from './TextInput.js'
|
|
|
|
|
|
|
| 6 |
|
| 7 |
interface Props {
|
| 8 |
+
onDone: () => void
|
| 9 |
startingMessage?: string
|
| 10 |
}
|
| 11 |
|
|
|
|
| 44 |
try {
|
| 45 |
// 保存本地模型配置
|
| 46 |
await saveLocalModelConfig(finalUrl, finalModel)
|
| 47 |
+
onDone()
|
| 48 |
} catch (error) {
|
| 49 |
console.error('Failed to save local model config:', error)
|
| 50 |
+
onDone()
|
| 51 |
}
|
| 52 |
}
|
| 53 |
|
| 54 |
const handleCancel = () => {
|
| 55 |
+
onDone()
|
| 56 |
}
|
| 57 |
|
| 58 |
return (
|
src/hooks/useApiKeyVerification.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import { useCallback, useState } from 'react'
|
| 2 |
import { getIsNonInteractiveSession } from '../bootstrap/state.js'
|
| 3 |
import { verifyApiKey } from '../services/api/claude.js'
|
| 4 |
import { getGlobalConfig } from '../utils/config.js'
|
|
@@ -8,8 +8,12 @@ import {
|
|
| 8 |
isAnthropicAuthEnabled,
|
| 9 |
isClaudeAISubscriber,
|
| 10 |
getConfiguredAuthProvider,
|
|
|
|
| 11 |
getOpenAIAuthTokens,
|
|
|
|
|
|
|
| 12 |
} from '../utils/auth.js'
|
|
|
|
| 13 |
|
| 14 |
export type VerificationStatus =
|
| 15 |
| 'loading'
|
|
@@ -26,7 +30,17 @@ export type ApiKeyVerificationResult = {
|
|
| 26 |
|
| 27 |
export function useApiKeyVerification(): ApiKeyVerificationResult {
|
| 28 |
const [status, setStatus] = useState<VerificationStatus>(() => {
|
| 29 |
-
const authProvider =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
// Check OpenAI authentication
|
| 32 |
if (authProvider === 'openai') {
|
|
@@ -67,8 +81,109 @@ export function useApiKeyVerification(): ApiKeyVerificationResult {
|
|
| 67 |
})
|
| 68 |
const [error, setError] = useState<Error | null>(null)
|
| 69 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
const verify = useCallback(async (): Promise<void> => {
|
| 71 |
-
const authProvider =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
// Check OpenAI authentication
|
| 74 |
if (authProvider === 'openai') {
|
|
|
|
| 1 |
+
import { useCallback, useEffect, useState } from 'react'
|
| 2 |
import { getIsNonInteractiveSession } from '../bootstrap/state.js'
|
| 3 |
import { verifyApiKey } from '../services/api/claude.js'
|
| 4 |
import { getGlobalConfig } from '../utils/config.js'
|
|
|
|
| 8 |
isAnthropicAuthEnabled,
|
| 9 |
isClaudeAISubscriber,
|
| 10 |
getConfiguredAuthProvider,
|
| 11 |
+
getConfiguredAuthProviderFromFile,
|
| 12 |
getOpenAIAuthTokens,
|
| 13 |
+
getLocalBaseUrl,
|
| 14 |
+
getLocalModelName,
|
| 15 |
} from '../utils/auth.js'
|
| 16 |
+
import { useAppState } from '../state/AppState.js'
|
| 17 |
|
| 18 |
export type VerificationStatus =
|
| 19 |
| 'loading'
|
|
|
|
| 30 |
|
| 31 |
export function useApiKeyVerification(): ApiKeyVerificationResult {
|
| 32 |
const [status, setStatus] = useState<VerificationStatus>(() => {
|
| 33 |
+
const authProvider = getConfiguredAuthProviderFromFile()
|
| 34 |
+
|
| 35 |
+
// Check Local authentication
|
| 36 |
+
if (authProvider === 'local') {
|
| 37 |
+
const baseUrl = getLocalBaseUrl()
|
| 38 |
+
const modelName = getLocalModelName()
|
| 39 |
+
if (baseUrl && modelName) {
|
| 40 |
+
return 'valid'
|
| 41 |
+
}
|
| 42 |
+
return 'missing'
|
| 43 |
+
}
|
| 44 |
|
| 45 |
// Check OpenAI authentication
|
| 46 |
if (authProvider === 'openai') {
|
|
|
|
| 81 |
})
|
| 82 |
const [error, setError] = useState<Error | null>(null)
|
| 83 |
|
| 84 |
+
// Watch authVersion changes to re-verify API key
|
| 85 |
+
const authVersion = useAppState(s => s.authVersion)
|
| 86 |
+
|
| 87 |
+
useEffect(() => {
|
| 88 |
+
// When authVersion changes, re-verify the API key
|
| 89 |
+
const reverify = async (): Promise<void> => {
|
| 90 |
+
const authProvider = getConfiguredAuthProviderFromFile()
|
| 91 |
+
console.log('[useApiKeyVerification] authVersion changed, authProvider:', authProvider)
|
| 92 |
+
|
| 93 |
+
// Check Local authentication
|
| 94 |
+
if (authProvider === 'local') {
|
| 95 |
+
// Force re-read config by reading directly from file
|
| 96 |
+
const baseUrl = getLocalBaseUrl()
|
| 97 |
+
const modelName = getLocalModelName()
|
| 98 |
+
console.log('[useApiKeyVerification] Local - baseUrl:', baseUrl, 'modelName:', modelName)
|
| 99 |
+
if (baseUrl && modelName) {
|
| 100 |
+
setStatus('valid')
|
| 101 |
+
} else {
|
| 102 |
+
setStatus('missing')
|
| 103 |
+
}
|
| 104 |
+
return
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
// Check OpenAI authentication
|
| 108 |
+
if (authProvider === 'openai') {
|
| 109 |
+
const { getGlobalConfig: getConfig } = await import('../utils/config.js')
|
| 110 |
+
const freshConfig = getConfig()
|
| 111 |
+
const hasApiKey = !!freshConfig.openAiApiKey
|
| 112 |
+
const hasAccessToken = !!freshConfig.openAiAccessToken
|
| 113 |
+
if (hasApiKey || hasAccessToken) {
|
| 114 |
+
setStatus('valid')
|
| 115 |
+
} else {
|
| 116 |
+
setStatus('missing')
|
| 117 |
+
}
|
| 118 |
+
return
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
// Check OpenRouter authentication
|
| 122 |
+
if (authProvider === 'openrouter') {
|
| 123 |
+
const { getGlobalConfig: getConfig } = await import('../utils/config.js')
|
| 124 |
+
const freshConfig = getConfig()
|
| 125 |
+
const hasApiKey = !!freshConfig.openRouterApiKey
|
| 126 |
+
if (hasApiKey) {
|
| 127 |
+
setStatus('valid')
|
| 128 |
+
} else {
|
| 129 |
+
setStatus('missing')
|
| 130 |
+
}
|
| 131 |
+
return
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
// Anthropic authentication
|
| 135 |
+
if (!isAnthropicAuthEnabled() || isClaudeAISubscriber()) {
|
| 136 |
+
setStatus('valid')
|
| 137 |
+
return
|
| 138 |
+
}
|
| 139 |
+
// Warm the apiKeyHelper cache (no-op if not configured), then read from
|
| 140 |
+
// all sources. getAnthropicApiKeyWithSource() reads the now-warm cache.
|
| 141 |
+
await getApiKeyFromApiKeyHelper(getIsNonInteractiveSession())
|
| 142 |
+
const { key: apiKey, source } = getAnthropicApiKeyWithSource()
|
| 143 |
+
if (!apiKey) {
|
| 144 |
+
if (source === 'apiKeyHelper') {
|
| 145 |
+
setStatus('error')
|
| 146 |
+
setError(new Error('API key helper did not return a valid key'))
|
| 147 |
+
return
|
| 148 |
+
}
|
| 149 |
+
const newStatus = 'missing'
|
| 150 |
+
setStatus(newStatus)
|
| 151 |
+
return
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
try {
|
| 155 |
+
const isValid = await verifyApiKey(apiKey, false)
|
| 156 |
+
const newStatus = isValid ? 'valid' : 'invalid'
|
| 157 |
+
setStatus(newStatus)
|
| 158 |
+
return
|
| 159 |
+
} catch (error) {
|
| 160 |
+
// This happens when there an error response from the API but it's not an invalid API key error
|
| 161 |
+
// In this case, we still mark the API key as invalid - but we also log the error so we can
|
| 162 |
+
// display it to the user to be more helpful
|
| 163 |
+
setError(error as Error)
|
| 164 |
+
const newStatus = 'error'
|
| 165 |
+
setStatus(newStatus)
|
| 166 |
+
return
|
| 167 |
+
}
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
void reverify()
|
| 171 |
+
}, [authVersion])
|
| 172 |
+
|
| 173 |
const verify = useCallback(async (): Promise<void> => {
|
| 174 |
+
const authProvider = getConfiguredAuthProviderFromFile()
|
| 175 |
+
|
| 176 |
+
// Check Local authentication
|
| 177 |
+
if (authProvider === 'local') {
|
| 178 |
+
const baseUrl = getLocalBaseUrl()
|
| 179 |
+
const modelName = getLocalModelName()
|
| 180 |
+
if (baseUrl && modelName) {
|
| 181 |
+
setStatus('valid')
|
| 182 |
+
} else {
|
| 183 |
+
setStatus('missing')
|
| 184 |
+
}
|
| 185 |
+
return
|
| 186 |
+
}
|
| 187 |
|
| 188 |
// Check OpenAI authentication
|
| 189 |
if (authProvider === 'openai') {
|
src/services/api/client.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
| 11 |
refreshGcpCredentialsIfNeeded,
|
| 12 |
getOpenRouterApiKey,
|
| 13 |
getOpenAIApiKey,
|
|
|
|
| 14 |
} from 'src/utils/auth.js'
|
| 15 |
import { getUserAgent } from 'src/utils/http.js'
|
| 16 |
import { getSmallFastModel } from 'src/utils/model/model.js'
|
|
@@ -335,6 +336,15 @@ export async function getAnthropicClient({
|
|
| 335 |
}
|
| 336 |
}
|
| 337 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 338 |
return new Anthropic(clientConfig)
|
| 339 |
}
|
| 340 |
|
|
@@ -344,8 +354,8 @@ async function configureApiKeyHeaders(
|
|
| 344 |
): Promise<void> {
|
| 345 |
const provider = getAPIProvider()
|
| 346 |
|
| 347 |
-
// Skip for OpenRouter and
|
| 348 |
-
if (provider === 'openrouter' || provider === 'openai') {
|
| 349 |
return
|
| 350 |
}
|
| 351 |
|
|
|
|
| 11 |
refreshGcpCredentialsIfNeeded,
|
| 12 |
getOpenRouterApiKey,
|
| 13 |
getOpenAIApiKey,
|
| 14 |
+
getLocalBaseUrl,
|
| 15 |
} from 'src/utils/auth.js'
|
| 16 |
import { getUserAgent } from 'src/utils/http.js'
|
| 17 |
import { getSmallFastModel } from 'src/utils/model/model.js'
|
|
|
|
| 336 |
}
|
| 337 |
}
|
| 338 |
|
| 339 |
+
// Handle Local
|
| 340 |
+
if (provider === 'local') {
|
| 341 |
+
const localBaseUrl = await getLocalBaseUrl()
|
| 342 |
+
if (localBaseUrl) {
|
| 343 |
+
clientConfig.baseURL = localBaseUrl
|
| 344 |
+
clientConfig.apiKey = 'local-model' // Use a dummy API key for local models
|
| 345 |
+
}
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
return new Anthropic(clientConfig)
|
| 349 |
}
|
| 350 |
|
|
|
|
| 354 |
): Promise<void> {
|
| 355 |
const provider = getAPIProvider()
|
| 356 |
|
| 357 |
+
// Skip for OpenRouter, OpenAI, and Local - they use apiKey parameter instead
|
| 358 |
+
if (provider === 'openrouter' || provider === 'openai' || provider === 'local') {
|
| 359 |
return
|
| 360 |
}
|
| 361 |
|
src/utils/auth.ts
CHANGED
|
@@ -49,6 +49,7 @@ import {
|
|
| 49 |
saveGlobalConfig,
|
| 50 |
} from './config.js'
|
| 51 |
import { logAntError, logForDebugging } from './debug.js'
|
|
|
|
| 52 |
import {
|
| 53 |
getClaudeConfigHomeDir,
|
| 54 |
isBareMode,
|
|
@@ -295,10 +296,8 @@ export function getOpenRouterApiKeyWithSource(): {
|
|
| 295 |
: { key: null, source: 'none' }
|
| 296 |
}
|
| 297 |
|
| 298 |
-
export function getConfiguredAuthProvider():
|
| 299 |
-
|
| 300 |
-
| 'openrouter'
|
| 301 |
-
| 'openai' {
|
| 302 |
const storedProvider = getGlobalConfig().authProvider
|
| 303 |
if (storedProvider) {
|
| 304 |
return storedProvider
|
|
@@ -315,6 +314,38 @@ export function getConfiguredAuthProvider():
|
|
| 315 |
}
|
| 316 |
}
|
| 317 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 318 |
export type OpenAIAuthTokens = {
|
| 319 |
accessToken: string
|
| 320 |
refreshToken?: string | null
|
|
@@ -394,6 +425,7 @@ export async function saveOpenRouterApiKey(apiKey: string): Promise<void> {
|
|
| 394 |
}
|
| 395 |
|
| 396 |
export async function saveLocalModelConfig(baseUrl: string, modelName: string): Promise<void> {
|
|
|
|
| 397 |
if (!baseUrl.trim()) {
|
| 398 |
throw new Error('Base URL cannot be empty')
|
| 399 |
}
|
|
@@ -405,16 +437,63 @@ export async function saveLocalModelConfig(baseUrl: string, modelName: string):
|
|
| 405 |
throw new Error('Invalid URL format')
|
| 406 |
}
|
| 407 |
|
| 408 |
-
saveGlobalConfig(current =>
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 414 |
|
| 415 |
// Clear provider cache so it will be re-read on next access
|
| 416 |
const { clearStoredProviderCache } = await import('./model/providers.js')
|
| 417 |
clearStoredProviderCache()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 418 |
}
|
| 419 |
|
| 420 |
function getCodexHomeDir(): string {
|
|
|
|
| 49 |
saveGlobalConfig,
|
| 50 |
} from './config.js'
|
| 51 |
import { logAntError, logForDebugging } from './debug.js'
|
| 52 |
+
import { getGlobalClaudeFile } from './env.js'
|
| 53 |
import {
|
| 54 |
getClaudeConfigHomeDir,
|
| 55 |
isBareMode,
|
|
|
|
| 296 |
: { key: null, source: 'none' }
|
| 297 |
}
|
| 298 |
|
| 299 |
+
export function getConfiguredAuthProvider(): 'anthropic' | 'openrouter' | 'openai' | 'local' {
|
| 300 |
+
// First try to get from cache for performance
|
|
|
|
|
|
|
| 301 |
const storedProvider = getGlobalConfig().authProvider
|
| 302 |
if (storedProvider) {
|
| 303 |
return storedProvider
|
|
|
|
| 314 |
}
|
| 315 |
}
|
| 316 |
|
| 317 |
+
// Read authProvider directly from file to bypass cache
|
| 318 |
+
export function getConfiguredAuthProviderFromFile(): 'anthropic' | 'openrouter' | 'openai' | 'local' {
|
| 319 |
+
try {
|
| 320 |
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
| 321 |
+
const { readFileSync } = require('fs') as typeof import('fs')
|
| 322 |
+
const raw = readFileSync(getGlobalClaudeFile(), 'utf8')
|
| 323 |
+
const config = JSON.parse(raw) as {
|
| 324 |
+
authProvider?: 'anthropic' | 'openrouter' | 'openai' | 'local'
|
| 325 |
+
}
|
| 326 |
+
console.log('[getConfiguredAuthProviderFromFile] Config from file:', config.authProvider)
|
| 327 |
+
|
| 328 |
+
if (config.authProvider) {
|
| 329 |
+
return config.authProvider
|
| 330 |
+
}
|
| 331 |
+
} catch (error) {
|
| 332 |
+
console.log('[getConfiguredAuthProviderFromFile] Error reading file:', error)
|
| 333 |
+
// Ignore errors, fall through to default
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
// Fallback to API provider
|
| 337 |
+
const provider = getAPIProvider()
|
| 338 |
+
console.log('[getConfiguredAuthProviderFromFile] Fallback to API provider:', provider)
|
| 339 |
+
switch (provider) {
|
| 340 |
+
case 'openrouter':
|
| 341 |
+
return 'openrouter'
|
| 342 |
+
case 'openai':
|
| 343 |
+
return 'openai'
|
| 344 |
+
default:
|
| 345 |
+
return 'anthropic'
|
| 346 |
+
}
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
export type OpenAIAuthTokens = {
|
| 350 |
accessToken: string
|
| 351 |
refreshToken?: string | null
|
|
|
|
| 425 |
}
|
| 426 |
|
| 427 |
export async function saveLocalModelConfig(baseUrl: string, modelName: string): Promise<void> {
|
| 428 |
+
console.log('[saveLocalModelConfig] Saving local model config:', baseUrl, modelName)
|
| 429 |
if (!baseUrl.trim()) {
|
| 430 |
throw new Error('Base URL cannot be empty')
|
| 431 |
}
|
|
|
|
| 437 |
throw new Error('Invalid URL format')
|
| 438 |
}
|
| 439 |
|
| 440 |
+
saveGlobalConfig(current => {
|
| 441 |
+
const newConfig = {
|
| 442 |
+
...current,
|
| 443 |
+
authProvider: 'local',
|
| 444 |
+
localBaseUrl: baseUrl,
|
| 445 |
+
localModelName: modelName,
|
| 446 |
+
}
|
| 447 |
+
console.log('[saveLocalModelConfig] New config to save:', newConfig)
|
| 448 |
+
return newConfig
|
| 449 |
+
})
|
| 450 |
|
| 451 |
// Clear provider cache so it will be re-read on next access
|
| 452 |
const { clearStoredProviderCache } = await import('./model/providers.js')
|
| 453 |
clearStoredProviderCache()
|
| 454 |
+
console.log('[saveLocalModelConfig] Config saved successfully')
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
export function getLocalModelName(): string | null {
|
| 458 |
+
try {
|
| 459 |
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
| 460 |
+
const { readFileSync } = require('fs') as typeof import('fs')
|
| 461 |
+
const raw = readFileSync(getGlobalClaudeFile(), 'utf8')
|
| 462 |
+
const config = JSON.parse(raw) as {
|
| 463 |
+
authProvider?: string
|
| 464 |
+
localModelName?: string
|
| 465 |
+
}
|
| 466 |
+
console.log('[getLocalModelName] Config from file:', config.authProvider, config.localModelName)
|
| 467 |
+
|
| 468 |
+
if (config.authProvider === 'local' && config.localModelName) {
|
| 469 |
+
return config.localModelName
|
| 470 |
+
}
|
| 471 |
+
return null
|
| 472 |
+
} catch (error) {
|
| 473 |
+
console.log('[getLocalModelName] Error reading file:', error)
|
| 474 |
+
return null
|
| 475 |
+
}
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
export function getLocalBaseUrl(): string | null {
|
| 479 |
+
try {
|
| 480 |
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
| 481 |
+
const { readFileSync } = require('fs') as typeof import('fs')
|
| 482 |
+
const raw = readFileSync(getGlobalClaudeFile(), 'utf8')
|
| 483 |
+
const config = JSON.parse(raw) as {
|
| 484 |
+
authProvider?: string
|
| 485 |
+
localBaseUrl?: string
|
| 486 |
+
}
|
| 487 |
+
console.log('[getLocalBaseUrl] Config from file:', config.authProvider, config.localBaseUrl)
|
| 488 |
+
|
| 489 |
+
if (config.authProvider === 'local' && config.localBaseUrl) {
|
| 490 |
+
return config.localBaseUrl
|
| 491 |
+
}
|
| 492 |
+
return null
|
| 493 |
+
} catch (error) {
|
| 494 |
+
console.log('[getLocalBaseUrl] Error reading file:', error)
|
| 495 |
+
return null
|
| 496 |
+
}
|
| 497 |
}
|
| 498 |
|
| 499 |
function getCodexHomeDir(): string {
|
src/utils/config.ts
CHANGED
|
@@ -185,6 +185,21 @@ export type GlobalConfig = {
|
|
| 185 |
* @deprecated Use settings.apiKeyHelper instead.
|
| 186 |
*/
|
| 187 |
apiKeyHelper?: string
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
projects?: Record<string, ProjectConfig>
|
| 189 |
numStartups: number
|
| 190 |
installMethod?: InstallMethod
|
|
@@ -626,6 +641,12 @@ export const DEFAULT_GLOBAL_CONFIG: GlobalConfig = createDefaultGlobalConfig()
|
|
| 626 |
|
| 627 |
export const GLOBAL_CONFIG_KEYS = [
|
| 628 |
'apiKeyHelper',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 629 |
'installMethod',
|
| 630 |
'autoUpdates',
|
| 631 |
'autoUpdatesProtectedForNative',
|
|
|
|
| 185 |
* @deprecated Use settings.apiKeyHelper instead.
|
| 186 |
*/
|
| 187 |
apiKeyHelper?: string
|
| 188 |
+
|
| 189 |
+
// Authentication provider configuration
|
| 190 |
+
authProvider?: 'anthropic' | 'openai' | 'openrouter' | 'local'
|
| 191 |
+
|
| 192 |
+
// OpenRouter API key
|
| 193 |
+
openRouterApiKey?: string
|
| 194 |
+
|
| 195 |
+
// OpenAI API configuration
|
| 196 |
+
openAiApiKey?: string
|
| 197 |
+
openAiAccessToken?: string
|
| 198 |
+
|
| 199 |
+
// Local model configuration
|
| 200 |
+
localBaseUrl?: string
|
| 201 |
+
localModelName?: string
|
| 202 |
+
|
| 203 |
projects?: Record<string, ProjectConfig>
|
| 204 |
numStartups: number
|
| 205 |
installMethod?: InstallMethod
|
|
|
|
| 641 |
|
| 642 |
export const GLOBAL_CONFIG_KEYS = [
|
| 643 |
'apiKeyHelper',
|
| 644 |
+
'authProvider',
|
| 645 |
+
'openRouterApiKey',
|
| 646 |
+
'openAiApiKey',
|
| 647 |
+
'openAiAccessToken',
|
| 648 |
+
'localBaseUrl',
|
| 649 |
+
'localModelName',
|
| 650 |
'installMethod',
|
| 651 |
'autoUpdates',
|
| 652 |
'autoUpdatesProtectedForNative',
|
src/utils/model/model.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
| 7 |
*/
|
| 8 |
import { getMainLoopModelOverride } from '../../bootstrap/state.js'
|
| 9 |
import {
|
|
|
|
| 10 |
getSubscriptionType,
|
| 11 |
isClaudeAISubscriber,
|
| 12 |
isMaxSubscriber,
|
|
@@ -170,12 +171,23 @@ export function getRuntimeMainLoopModel(params: {
|
|
| 170 |
* Get the default main loop model setting.
|
| 171 |
*
|
| 172 |
* This handles the built-in default:
|
|
|
|
| 173 |
* - Opus for Max and Team Premium users
|
| 174 |
* - Sonnet 4.6 for all other users (including Team Standard, Pro, Enterprise)
|
| 175 |
*
|
| 176 |
* @returns The default model setting to use
|
| 177 |
*/
|
| 178 |
export function getDefaultMainLoopModelSetting(): ModelName | ModelAlias {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
// Ants default to defaultModel from flag config, or Opus 1M if not configured
|
| 180 |
if (process.env.USER_TYPE === 'ant') {
|
| 181 |
return (
|
|
|
|
| 7 |
*/
|
| 8 |
import { getMainLoopModelOverride } from '../../bootstrap/state.js'
|
| 9 |
import {
|
| 10 |
+
getLocalModelName,
|
| 11 |
getSubscriptionType,
|
| 12 |
isClaudeAISubscriber,
|
| 13 |
isMaxSubscriber,
|
|
|
|
| 171 |
* Get the default main loop model setting.
|
| 172 |
*
|
| 173 |
* This handles the built-in default:
|
| 174 |
+
* - Local model name if configured as Local provider
|
| 175 |
* - Opus for Max and Team Premium users
|
| 176 |
* - Sonnet 4.6 for all other users (including Team Standard, Pro, Enterprise)
|
| 177 |
*
|
| 178 |
* @returns The default model setting to use
|
| 179 |
*/
|
| 180 |
export function getDefaultMainLoopModelSetting(): ModelName | ModelAlias {
|
| 181 |
+
// Check if using Local provider
|
| 182 |
+
if (getAPIProvider() === 'local') {
|
| 183 |
+
const localModelName = getLocalModelName()
|
| 184 |
+
if (localModelName) {
|
| 185 |
+
return localModelName
|
| 186 |
+
}
|
| 187 |
+
// Fallback to 'default' if no local model name is configured
|
| 188 |
+
return 'default'
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
// Ants default to defaultModel from flag config, or Opus 1M if not configured
|
| 192 |
if (process.env.USER_TYPE === 'ant') {
|
| 193 |
return (
|