mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
Prevent stale provider proxy env in direct TUI launches
Direct TUI launches read cc-haha managed settings without the desktop host's session-env injection, so a stale proxy URL could survive after switching to an Anthropic-compatible provider. The provider runtime env calculation now has one shared source of truth and CLI config loading recomputes managed provider env from the active provider index before applying it. Constraint: Direct source-based TUI usage must keep working with .env when no desktop provider is configured Rejected: Inject desktop runtime only from the embedded terminal shell | native terminals and source TUI launches would still read stale managed settings Confidence: high Scope-risk: moderate Directive: Keep provider env derivation shared between ProviderService and CLI managedEnv loading Tested: bun test src/server/__tests__/provider-runtime-env.test.ts src/server/__tests__/providers.test.ts src/server/__tests__/conversation-service.test.ts Tested: bun run check:server Not-tested: Live provider request from embedded terminal TUI
This commit is contained in:
parent
10d75852bb
commit
3a42ca2067
105
src/server/__tests__/provider-runtime-env.test.ts
Normal file
105
src/server/__tests__/provider-runtime-env.test.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
|
||||
import * as fs from 'fs/promises'
|
||||
import * as os from 'os'
|
||||
import * as path from 'path'
|
||||
|
||||
import {
|
||||
mergeActiveProviderManagedEnv,
|
||||
readActiveProviderManagedEnv,
|
||||
} from '../services/providerRuntimeEnv.js'
|
||||
|
||||
let tmpDir: string
|
||||
|
||||
async function writeJson(filePath: string, value: unknown): Promise<void> {
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
||||
await fs.writeFile(filePath, JSON.stringify(value, null, 2), 'utf-8')
|
||||
}
|
||||
|
||||
describe('providerRuntimeEnv', () => {
|
||||
beforeEach(async () => {
|
||||
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'provider-runtime-env-'))
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await fs.rm(tmpDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('derives native Anthropic provider env from the active provider index', async () => {
|
||||
await writeJson(path.join(tmpDir, 'cc-haha', 'providers.json'), {
|
||||
activeId: 'provider-1',
|
||||
providers: [
|
||||
{
|
||||
id: 'provider-1',
|
||||
presetId: 'custom',
|
||||
name: 'Active Provider',
|
||||
apiKey: 'sk-active',
|
||||
authStrategy: 'auth_token',
|
||||
baseUrl: 'https://api.example.com/anthropic',
|
||||
apiFormat: 'anthropic',
|
||||
models: {
|
||||
main: 'active-main',
|
||||
haiku: '',
|
||||
sonnet: 'active-sonnet',
|
||||
opus: '',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const env = readActiveProviderManagedEnv(tmpDir)
|
||||
|
||||
expect(env).toMatchObject({
|
||||
ANTHROPIC_BASE_URL: 'https://api.example.com/anthropic',
|
||||
ANTHROPIC_API_KEY: '',
|
||||
ANTHROPIC_AUTH_TOKEN: 'sk-active',
|
||||
ANTHROPIC_MODEL: 'active-main',
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'active-main',
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: 'active-sonnet',
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: 'active-main',
|
||||
})
|
||||
})
|
||||
|
||||
test('active provider env overrides stale proxy settings while preserving unrelated env', async () => {
|
||||
await writeJson(path.join(tmpDir, 'cc-haha', 'providers.json'), {
|
||||
activeId: 'provider-1',
|
||||
providers: [
|
||||
{
|
||||
id: 'provider-1',
|
||||
presetId: 'custom',
|
||||
name: 'Sub2API',
|
||||
apiKey: 'sk-sub2api',
|
||||
authStrategy: 'auth_token',
|
||||
baseUrl: 'https://sub2api.example.com',
|
||||
apiFormat: 'anthropic',
|
||||
models: {
|
||||
main: 'gpt-5.5',
|
||||
haiku: 'gpt-5.5',
|
||||
sonnet: 'gpt-5.5',
|
||||
opus: 'gpt-5.5',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const env = mergeActiveProviderManagedEnv(
|
||||
{
|
||||
ANTHROPIC_BASE_URL: 'http://127.0.0.1:3456/proxy',
|
||||
ANTHROPIC_API_KEY: 'proxy-managed',
|
||||
ANTHROPIC_MODEL: 'deepseek-v4-pro',
|
||||
DISABLE_AUTOUPDATER: '1',
|
||||
},
|
||||
tmpDir,
|
||||
)
|
||||
|
||||
expect(env).toMatchObject({
|
||||
ANTHROPIC_BASE_URL: 'https://sub2api.example.com',
|
||||
ANTHROPIC_API_KEY: '',
|
||||
ANTHROPIC_AUTH_TOKEN: 'sk-sub2api',
|
||||
ANTHROPIC_MODEL: 'gpt-5.5',
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'gpt-5.5',
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: 'gpt-5.5',
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: 'gpt-5.5',
|
||||
DISABLE_AUTOUPDATER: '1',
|
||||
})
|
||||
})
|
||||
})
|
||||
274
src/server/services/providerRuntimeEnv.ts
Normal file
274
src/server/services/providerRuntimeEnv.ts
Normal file
@ -0,0 +1,274 @@
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
|
||||
import { MODEL_CONTEXT_WINDOWS_ENV_KEY } from '../../utils/model/modelContextWindows.js'
|
||||
import { PROVIDER_PRESETS } from '../config/providerPresets.js'
|
||||
import type {
|
||||
ApiFormat,
|
||||
ProviderAuthStrategy,
|
||||
ProvidersIndex,
|
||||
SavedProvider,
|
||||
} from '../types/provider.js'
|
||||
import {
|
||||
ATTRIBUTION_HEADER_ENV_KEY,
|
||||
attributionHeaderEnvForModel,
|
||||
} from './attributionHeaderPolicy.js'
|
||||
import {
|
||||
OPENAI_CODEX_OAUTH_FILE_ENV_KEY,
|
||||
OPENAI_OAUTH_PROVIDER_ENV_KEY,
|
||||
buildOpenAIOfficialRuntimeEnv,
|
||||
isOpenAIOfficialProviderId,
|
||||
} from './openaiOfficialProvider.js'
|
||||
|
||||
export const MANAGED_PROVIDER_ENV_KEYS = [
|
||||
'ANTHROPIC_BASE_URL',
|
||||
'ANTHROPIC_API_KEY',
|
||||
'ANTHROPIC_AUTH_TOKEN',
|
||||
'ANTHROPIC_MODEL',
|
||||
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
||||
'ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES',
|
||||
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
||||
'ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES',
|
||||
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
||||
'ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES',
|
||||
'CLAUDE_CODE_AUTO_COMPACT_WINDOW',
|
||||
ATTRIBUTION_HEADER_ENV_KEY,
|
||||
MODEL_CONTEXT_WINDOWS_ENV_KEY,
|
||||
OPENAI_OAUTH_PROVIDER_ENV_KEY,
|
||||
OPENAI_CODEX_OAUTH_FILE_ENV_KEY,
|
||||
] as const
|
||||
|
||||
const CUSTOM_PROVIDER_MODEL_CAPABILITIES = 'thinking,effort,adaptive_thinking,max_effort'
|
||||
const AUTH_ENV_KEYS = new Set(['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN'])
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return !!value && typeof value === 'object' && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function isProviderModels(value: unknown): value is SavedProvider['models'] {
|
||||
return (
|
||||
isRecord(value) &&
|
||||
typeof value.main === 'string' &&
|
||||
typeof value.haiku === 'string' &&
|
||||
typeof value.sonnet === 'string' &&
|
||||
typeof value.opus === 'string'
|
||||
)
|
||||
}
|
||||
|
||||
function isSavedProvider(value: unknown): value is SavedProvider {
|
||||
if (!isRecord(value)) return false
|
||||
const runtimeKind = value.runtimeKind
|
||||
return (
|
||||
typeof value.id === 'string' &&
|
||||
typeof value.presetId === 'string' &&
|
||||
typeof value.name === 'string' &&
|
||||
typeof value.apiKey === 'string' &&
|
||||
typeof value.baseUrl === 'string' &&
|
||||
(
|
||||
runtimeKind === undefined ||
|
||||
runtimeKind === 'anthropic_compatible' ||
|
||||
runtimeKind === 'openai_oauth'
|
||||
) &&
|
||||
isProviderModels(value.models)
|
||||
)
|
||||
}
|
||||
|
||||
export function normalizeModelMapping(models: SavedProvider['models']): SavedProvider['models'] {
|
||||
const main = models.main.trim()
|
||||
return {
|
||||
main,
|
||||
haiku: models.haiku.trim() || main,
|
||||
sonnet: models.sonnet.trim() || main,
|
||||
opus: models.opus.trim() || main,
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeSavedProvider(provider: SavedProvider): SavedProvider {
|
||||
return {
|
||||
...provider,
|
||||
apiFormat: provider.apiFormat ?? 'anthropic',
|
||||
runtimeKind: provider.runtimeKind ?? 'anthropic_compatible',
|
||||
models: normalizeModelMapping(provider.models),
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeProvidersIndex(value: unknown): ProvidersIndex | null {
|
||||
if (!isRecord(value) || !Array.isArray(value.providers)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const { activeProviderId: legacyActiveProviderId, ...rest } = value
|
||||
const providers = value.providers
|
||||
.filter(isSavedProvider)
|
||||
.map((provider) => normalizeSavedProvider(provider))
|
||||
const rawActiveId =
|
||||
typeof value.activeId === 'string'
|
||||
? value.activeId
|
||||
: typeof legacyActiveProviderId === 'string'
|
||||
? legacyActiveProviderId
|
||||
: null
|
||||
const activeId = rawActiveId && (
|
||||
providers.some((provider) => provider.id === rawActiveId) ||
|
||||
isOpenAIOfficialProviderId(rawActiveId)
|
||||
)
|
||||
? rawActiveId
|
||||
: null
|
||||
|
||||
return {
|
||||
...rest,
|
||||
schemaVersion: typeof value.schemaVersion === 'number' ? value.schemaVersion : 1,
|
||||
activeId,
|
||||
providers,
|
||||
}
|
||||
}
|
||||
|
||||
export function getPresetDefaultEnv(presetId: string): Record<string, string> {
|
||||
return PROVIDER_PRESETS.find((preset) => preset.id === presetId)?.defaultEnv ?? {}
|
||||
}
|
||||
|
||||
function omitAuthEnv(env: Record<string, string>): Record<string, string> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(env).filter(([key]) => !AUTH_ENV_KEYS.has(key.toUpperCase())),
|
||||
)
|
||||
}
|
||||
|
||||
export function getPresetAuthStrategy(presetId: string): ProviderAuthStrategy {
|
||||
return PROVIDER_PRESETS.find((preset) => preset.id === presetId)?.authStrategy ?? 'auth_token'
|
||||
}
|
||||
|
||||
function getPresetModelContextWindows(presetId: string): Record<string, number> {
|
||||
return PROVIDER_PRESETS.find((preset) => preset.id === presetId)?.modelContextWindows ?? {}
|
||||
}
|
||||
|
||||
export function buildProviderAuthEnv(
|
||||
provider: SavedProvider,
|
||||
presetDefaultEnv: Record<string, string>,
|
||||
needsProxy: boolean,
|
||||
): Record<string, string> {
|
||||
if (needsProxy) {
|
||||
return { ANTHROPIC_API_KEY: 'proxy-managed' }
|
||||
}
|
||||
|
||||
const strategy = provider.authStrategy ?? getPresetAuthStrategy(provider.presetId)
|
||||
const key = provider.apiKey || presetDefaultEnv.ANTHROPIC_AUTH_TOKEN || presetDefaultEnv.ANTHROPIC_API_KEY || ''
|
||||
|
||||
switch (strategy) {
|
||||
case 'api_key':
|
||||
return key ? { ANTHROPIC_API_KEY: key } : {}
|
||||
case 'auth_token':
|
||||
case 'auth_token_empty_api_key':
|
||||
return {
|
||||
ANTHROPIC_API_KEY: '',
|
||||
...(key ? { ANTHROPIC_AUTH_TOKEN: key } : {}),
|
||||
}
|
||||
case 'dual_same_token':
|
||||
return key ? { ANTHROPIC_API_KEY: key, ANTHROPIC_AUTH_TOKEN: key } : {}
|
||||
case 'dual_dummy':
|
||||
return { ANTHROPIC_API_KEY: 'dummy', ANTHROPIC_AUTH_TOKEN: 'dummy' }
|
||||
}
|
||||
}
|
||||
|
||||
export function getManagedEnvKeys(): string[] {
|
||||
const keys = new Set<string>(MANAGED_PROVIDER_ENV_KEYS)
|
||||
for (const preset of PROVIDER_PRESETS) {
|
||||
for (const key of Object.keys(preset.defaultEnv ?? {})) {
|
||||
keys.add(key)
|
||||
}
|
||||
}
|
||||
return [...keys]
|
||||
}
|
||||
|
||||
export function buildProviderManagedEnv(
|
||||
provider: SavedProvider,
|
||||
options?: { proxyPath?: string; serverPort?: number },
|
||||
): Record<string, string> {
|
||||
if (provider.runtimeKind === 'openai_oauth') {
|
||||
return buildOpenAIOfficialRuntimeEnv()
|
||||
}
|
||||
|
||||
const apiFormat: ApiFormat = provider.apiFormat ?? 'anthropic'
|
||||
const needsProxy = apiFormat !== 'anthropic'
|
||||
const proxyPath = options?.proxyPath ?? '/proxy'
|
||||
const serverPort = options?.serverPort ?? 3456
|
||||
const baseUrl = needsProxy
|
||||
? `http://127.0.0.1:${serverPort}${proxyPath}`
|
||||
: provider.baseUrl
|
||||
|
||||
const models = normalizeModelMapping(provider.models)
|
||||
const modelContextWindows = {
|
||||
...getPresetModelContextWindows(provider.presetId),
|
||||
...(provider.modelContextWindows ?? {}),
|
||||
}
|
||||
|
||||
const presetDefaultEnv = getPresetDefaultEnv(provider.presetId)
|
||||
const customProviderCapabilityEnv =
|
||||
provider.presetId === 'custom'
|
||||
? {
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES: CUSTOM_PROVIDER_MODEL_CAPABILITIES,
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES: CUSTOM_PROVIDER_MODEL_CAPABILITIES,
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES: CUSTOM_PROVIDER_MODEL_CAPABILITIES,
|
||||
}
|
||||
: {}
|
||||
|
||||
return {
|
||||
...omitAuthEnv(presetDefaultEnv),
|
||||
...customProviderCapabilityEnv,
|
||||
...(provider.autoCompactWindow !== undefined && {
|
||||
CLAUDE_CODE_AUTO_COMPACT_WINDOW: String(provider.autoCompactWindow),
|
||||
}),
|
||||
...(Object.keys(modelContextWindows).length > 0 && {
|
||||
[MODEL_CONTEXT_WINDOWS_ENV_KEY]: JSON.stringify(modelContextWindows),
|
||||
}),
|
||||
ANTHROPIC_BASE_URL: baseUrl,
|
||||
...buildProviderAuthEnv(provider, presetDefaultEnv, needsProxy),
|
||||
ANTHROPIC_MODEL: models.main,
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: models.haiku,
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: models.sonnet,
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: models.opus,
|
||||
...attributionHeaderEnvForModel(models.main),
|
||||
}
|
||||
}
|
||||
|
||||
export function readActiveProviderManagedEnv(
|
||||
configDir: string,
|
||||
options?: { serverPort?: number },
|
||||
): Record<string, string> | null {
|
||||
try {
|
||||
const raw = fs.readFileSync(path.join(configDir, 'cc-haha', 'providers.json'), 'utf-8')
|
||||
const index = normalizeProvidersIndex(JSON.parse(raw))
|
||||
if (!index?.activeId) return null
|
||||
|
||||
if (isOpenAIOfficialProviderId(index.activeId)) {
|
||||
return buildOpenAIOfficialRuntimeEnv()
|
||||
}
|
||||
|
||||
const provider = index.providers.find((entry) => entry.id === index.activeId)
|
||||
if (!provider) return null
|
||||
|
||||
return buildProviderManagedEnv(provider, {
|
||||
serverPort: options?.serverPort,
|
||||
})
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function mergeActiveProviderManagedEnv(
|
||||
settingsEnv: Record<string, string>,
|
||||
configDir: string,
|
||||
options?: { serverPort?: number },
|
||||
): Record<string, string> {
|
||||
const activeProviderEnv = readActiveProviderManagedEnv(configDir, options)
|
||||
if (!activeProviderEnv) {
|
||||
return settingsEnv
|
||||
}
|
||||
|
||||
const cleanedEnv = { ...settingsEnv }
|
||||
for (const key of getManagedEnvKeys()) {
|
||||
delete cleanedEnv[key]
|
||||
}
|
||||
return {
|
||||
...cleanedEnv,
|
||||
...activeProviderEnv,
|
||||
}
|
||||
}
|
||||
@ -17,17 +17,8 @@ import { anthropicToOpenaiResponses } from '../proxy/transform/anthropicToOpenai
|
||||
import { openaiChatToAnthropic } from '../proxy/transform/openaiChatToAnthropic.js'
|
||||
import { openaiResponsesToAnthropic } from '../proxy/transform/openaiResponsesToAnthropic.js'
|
||||
import type { AnthropicRequest, AnthropicResponse } from '../proxy/transform/types.js'
|
||||
import { PROVIDER_PRESETS } from '../config/providerPresets.js'
|
||||
import { MODEL_CONTEXT_WINDOWS_ENV_KEY } from '../../utils/model/modelContextWindows.js'
|
||||
import {
|
||||
ATTRIBUTION_HEADER_ENV_KEY,
|
||||
attributionHeaderEnvForModel,
|
||||
} from './attributionHeaderPolicy.js'
|
||||
import {
|
||||
OPENAI_CODEX_OAUTH_FILE_ENV_KEY,
|
||||
OPENAI_OFFICIAL_PROVIDER,
|
||||
OPENAI_OAUTH_PROVIDER_ENV_KEY,
|
||||
buildOpenAIOfficialRuntimeEnv,
|
||||
isOpenAIOfficialProviderId,
|
||||
} from './openaiOfficialProvider.js'
|
||||
import { hahaOpenAIOAuthService } from './hahaOpenAIOAuthService.js'
|
||||
@ -35,6 +26,15 @@ import {
|
||||
CURRENT_PROVIDER_INDEX_SCHEMA_VERSION,
|
||||
ensurePersistentStorageUpgraded,
|
||||
} from './persistentStorageMigrations.js'
|
||||
import {
|
||||
buildProviderAuthEnv,
|
||||
buildProviderManagedEnv,
|
||||
getManagedEnvKeys,
|
||||
getPresetAuthStrategy,
|
||||
getPresetDefaultEnv,
|
||||
normalizeModelMapping,
|
||||
normalizeProvidersIndex,
|
||||
} from './providerRuntimeEnv.js'
|
||||
import type {
|
||||
SavedProvider,
|
||||
ProvidersIndex,
|
||||
@ -47,173 +47,11 @@ import type {
|
||||
ProviderAuthStrategy,
|
||||
} from '../types/provider.js'
|
||||
|
||||
const MANAGED_ENV_KEYS = [
|
||||
'ANTHROPIC_BASE_URL',
|
||||
'ANTHROPIC_API_KEY',
|
||||
'ANTHROPIC_AUTH_TOKEN',
|
||||
'ANTHROPIC_MODEL',
|
||||
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
||||
'ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES',
|
||||
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
||||
'ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES',
|
||||
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
||||
'ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES',
|
||||
'CLAUDE_CODE_AUTO_COMPACT_WINDOW',
|
||||
ATTRIBUTION_HEADER_ENV_KEY,
|
||||
MODEL_CONTEXT_WINDOWS_ENV_KEY,
|
||||
OPENAI_OAUTH_PROVIDER_ENV_KEY,
|
||||
OPENAI_CODEX_OAUTH_FILE_ENV_KEY,
|
||||
] as const
|
||||
|
||||
const CUSTOM_PROVIDER_MODEL_CAPABILITIES = 'thinking,effort,adaptive_thinking,max_effort'
|
||||
|
||||
const DEFAULT_INDEX: ProvidersIndex = {
|
||||
schemaVersion: CURRENT_PROVIDER_INDEX_SCHEMA_VERSION,
|
||||
activeId: null,
|
||||
providers: [],
|
||||
}
|
||||
const AUTH_ENV_KEYS = new Set(['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN'])
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return !!value && typeof value === 'object' && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function isProviderModels(value: unknown): value is SavedProvider['models'] {
|
||||
return (
|
||||
isRecord(value) &&
|
||||
typeof value.main === 'string' &&
|
||||
typeof value.haiku === 'string' &&
|
||||
typeof value.sonnet === 'string' &&
|
||||
typeof value.opus === 'string'
|
||||
)
|
||||
}
|
||||
|
||||
function isSavedProvider(value: unknown): value is SavedProvider {
|
||||
if (!isRecord(value)) return false
|
||||
const runtimeKind = value.runtimeKind
|
||||
return (
|
||||
typeof value.id === 'string' &&
|
||||
typeof value.presetId === 'string' &&
|
||||
typeof value.name === 'string' &&
|
||||
typeof value.apiKey === 'string' &&
|
||||
typeof value.baseUrl === 'string' &&
|
||||
(
|
||||
runtimeKind === undefined ||
|
||||
runtimeKind === 'anthropic_compatible' ||
|
||||
runtimeKind === 'openai_oauth'
|
||||
) &&
|
||||
isProviderModels(value.models)
|
||||
)
|
||||
}
|
||||
|
||||
function normalizeModelMapping(models: SavedProvider['models']): SavedProvider['models'] {
|
||||
const main = models.main.trim()
|
||||
return {
|
||||
main,
|
||||
haiku: models.haiku.trim() || main,
|
||||
sonnet: models.sonnet.trim() || main,
|
||||
opus: models.opus.trim() || main,
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeSavedProvider(provider: SavedProvider): SavedProvider {
|
||||
return {
|
||||
...provider,
|
||||
apiFormat: provider.apiFormat ?? 'anthropic',
|
||||
runtimeKind: provider.runtimeKind ?? 'anthropic_compatible',
|
||||
models: normalizeModelMapping(provider.models),
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeProvidersIndex(value: unknown): ProvidersIndex | null {
|
||||
if (!isRecord(value) || !Array.isArray(value.providers)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const { activeProviderId: _legacyActiveProviderId, ...rest } = value
|
||||
const providers = value.providers
|
||||
.filter(isSavedProvider)
|
||||
.map((provider) => normalizeSavedProvider(provider))
|
||||
const rawActiveId =
|
||||
typeof value.activeId === 'string'
|
||||
? value.activeId
|
||||
: typeof _legacyActiveProviderId === 'string'
|
||||
? _legacyActiveProviderId
|
||||
: null
|
||||
const activeId = rawActiveId && (
|
||||
providers.some((provider) => provider.id === rawActiveId) ||
|
||||
isOpenAIOfficialProviderId(rawActiveId)
|
||||
)
|
||||
? rawActiveId
|
||||
: null
|
||||
|
||||
return {
|
||||
...rest,
|
||||
schemaVersion: CURRENT_PROVIDER_INDEX_SCHEMA_VERSION,
|
||||
activeId,
|
||||
providers,
|
||||
}
|
||||
}
|
||||
|
||||
function getPresetDefaultEnv(presetId: string): Record<string, string> {
|
||||
return PROVIDER_PRESETS.find((preset) => preset.id === presetId)?.defaultEnv ?? {}
|
||||
}
|
||||
|
||||
function omitAuthEnv(env: Record<string, string>): Record<string, string> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(env).filter(([key]) => !AUTH_ENV_KEYS.has(key.toUpperCase())),
|
||||
)
|
||||
}
|
||||
|
||||
function getPresetAuthStrategy(presetId: string): ProviderAuthStrategy {
|
||||
return PROVIDER_PRESETS.find((preset) => preset.id === presetId)?.authStrategy ?? 'auth_token'
|
||||
}
|
||||
|
||||
function getPresetModelContextWindows(presetId: string): Record<string, number> {
|
||||
return PROVIDER_PRESETS.find((preset) => preset.id === presetId)?.modelContextWindows ?? {}
|
||||
}
|
||||
|
||||
function buildProviderAuthEnv(
|
||||
provider: SavedProvider,
|
||||
presetDefaultEnv: Record<string, string>,
|
||||
needsProxy: boolean,
|
||||
): Record<string, string> {
|
||||
if (needsProxy) {
|
||||
return { ANTHROPIC_API_KEY: 'proxy-managed' }
|
||||
}
|
||||
|
||||
const strategy = provider.authStrategy ?? getPresetAuthStrategy(provider.presetId)
|
||||
const key = provider.apiKey || presetDefaultEnv.ANTHROPIC_AUTH_TOKEN || presetDefaultEnv.ANTHROPIC_API_KEY || ''
|
||||
|
||||
switch (strategy) {
|
||||
case 'api_key':
|
||||
return key ? { ANTHROPIC_API_KEY: key } : {}
|
||||
case 'auth_token':
|
||||
return {
|
||||
ANTHROPIC_API_KEY: '',
|
||||
...(key ? { ANTHROPIC_AUTH_TOKEN: key } : {}),
|
||||
}
|
||||
case 'auth_token_empty_api_key':
|
||||
return {
|
||||
ANTHROPIC_API_KEY: '',
|
||||
...(key ? { ANTHROPIC_AUTH_TOKEN: key } : {}),
|
||||
}
|
||||
case 'dual_same_token':
|
||||
return key ? { ANTHROPIC_API_KEY: key, ANTHROPIC_AUTH_TOKEN: key } : {}
|
||||
case 'dual_dummy':
|
||||
return { ANTHROPIC_API_KEY: 'dummy', ANTHROPIC_AUTH_TOKEN: 'dummy' }
|
||||
}
|
||||
}
|
||||
|
||||
function getManagedEnvKeys(): string[] {
|
||||
const keys = new Set<string>(MANAGED_ENV_KEYS)
|
||||
for (const preset of PROVIDER_PRESETS) {
|
||||
for (const key of Object.keys(preset.defaultEnv ?? {})) {
|
||||
keys.add(key)
|
||||
}
|
||||
}
|
||||
return [...keys]
|
||||
}
|
||||
|
||||
export class ProviderService {
|
||||
private static serverPort = 3456
|
||||
@ -402,48 +240,10 @@ export class ProviderService {
|
||||
provider: SavedProvider,
|
||||
options?: { proxyPath?: string },
|
||||
): Record<string, string> {
|
||||
if (provider.runtimeKind === 'openai_oauth') {
|
||||
return buildOpenAIOfficialRuntimeEnv()
|
||||
}
|
||||
|
||||
const needsProxy = provider.apiFormat != null && provider.apiFormat !== 'anthropic'
|
||||
const proxyPath = options?.proxyPath ?? '/proxy'
|
||||
const baseUrl = needsProxy
|
||||
? `http://127.0.0.1:${ProviderService.serverPort}${proxyPath}`
|
||||
: provider.baseUrl
|
||||
|
||||
const modelContextWindows = {
|
||||
...getPresetModelContextWindows(provider.presetId),
|
||||
...(provider.modelContextWindows ?? {}),
|
||||
}
|
||||
|
||||
const presetDefaultEnv = getPresetDefaultEnv(provider.presetId)
|
||||
const customProviderCapabilityEnv =
|
||||
provider.presetId === 'custom'
|
||||
? {
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES: CUSTOM_PROVIDER_MODEL_CAPABILITIES,
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES: CUSTOM_PROVIDER_MODEL_CAPABILITIES,
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES: CUSTOM_PROVIDER_MODEL_CAPABILITIES,
|
||||
}
|
||||
: {}
|
||||
|
||||
return {
|
||||
...omitAuthEnv(presetDefaultEnv),
|
||||
...customProviderCapabilityEnv,
|
||||
...(provider.autoCompactWindow !== undefined && {
|
||||
CLAUDE_CODE_AUTO_COMPACT_WINDOW: String(provider.autoCompactWindow),
|
||||
}),
|
||||
...(Object.keys(modelContextWindows).length > 0 && {
|
||||
[MODEL_CONTEXT_WINDOWS_ENV_KEY]: JSON.stringify(modelContextWindows),
|
||||
}),
|
||||
ANTHROPIC_BASE_URL: baseUrl,
|
||||
...buildProviderAuthEnv(provider, presetDefaultEnv, needsProxy),
|
||||
ANTHROPIC_MODEL: provider.models.main,
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: provider.models.haiku,
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: provider.models.sonnet,
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: provider.models.opus,
|
||||
...attributionHeaderEnvForModel(provider.models.main),
|
||||
}
|
||||
return buildProviderManagedEnv(provider, {
|
||||
proxyPath: options?.proxyPath,
|
||||
serverPort: ProviderService.serverPort,
|
||||
})
|
||||
}
|
||||
|
||||
async getProviderRuntimeEnv(id: string): Promise<Record<string, string>> {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { isRemoteManagedSettingsEligible } from '../services/remoteManagedSettings/syncCache.js'
|
||||
import { mergeActiveProviderManagedEnv } from '../server/services/providerRuntimeEnv.js'
|
||||
import { clearCACertsCache } from './caCerts.js'
|
||||
import { getGlobalConfig } from './config.js'
|
||||
import { getClaudeConfigHomeDir, isEnvTruthy } from './envUtils.js'
|
||||
@ -104,9 +105,10 @@ function getCcHahaSettingsEnv(): Record<string, string> {
|
||||
const ccHahaSettings = join(getClaudeConfigHomeDir(), 'cc-haha', 'settings.json')
|
||||
const raw = readFileSync(ccHahaSettings, 'utf-8')
|
||||
const parsed = JSON.parse(raw) as { env?: Record<string, string> }
|
||||
return normalizeLegacyDeepSeekManagedEnv(parsed.env ?? {}).env
|
||||
const settingsEnv = normalizeLegacyDeepSeekManagedEnv(parsed.env ?? {}).env
|
||||
return mergeActiveProviderManagedEnv(settingsEnv, getClaudeConfigHomeDir())
|
||||
} catch {
|
||||
return {}
|
||||
return mergeActiveProviderManagedEnv({}, getClaudeConfigHomeDir())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user