diff --git a/src/server/__tests__/provider-runtime-env.test.ts b/src/server/__tests__/provider-runtime-env.test.ts new file mode 100644 index 00000000..72ec5859 --- /dev/null +++ b/src/server/__tests__/provider-runtime-env.test.ts @@ -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 { + 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', + }) + }) +}) diff --git a/src/server/services/providerRuntimeEnv.ts b/src/server/services/providerRuntimeEnv.ts new file mode 100644 index 00000000..979d27f5 --- /dev/null +++ b/src/server/services/providerRuntimeEnv.ts @@ -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 { + 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 { + return PROVIDER_PRESETS.find((preset) => preset.id === presetId)?.defaultEnv ?? {} +} + +function omitAuthEnv(env: Record): Record { + 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 { + return PROVIDER_PRESETS.find((preset) => preset.id === presetId)?.modelContextWindows ?? {} +} + +export function buildProviderAuthEnv( + provider: SavedProvider, + presetDefaultEnv: Record, + needsProxy: boolean, +): Record { + 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(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 { + 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 | 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, + configDir: string, + options?: { serverPort?: number }, +): Record { + const activeProviderEnv = readActiveProviderManagedEnv(configDir, options) + if (!activeProviderEnv) { + return settingsEnv + } + + const cleanedEnv = { ...settingsEnv } + for (const key of getManagedEnvKeys()) { + delete cleanedEnv[key] + } + return { + ...cleanedEnv, + ...activeProviderEnv, + } +} diff --git a/src/server/services/providerService.ts b/src/server/services/providerService.ts index 7fffd31a..cb4d05af 100644 --- a/src/server/services/providerService.ts +++ b/src/server/services/providerService.ts @@ -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 { - 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 { - return PROVIDER_PRESETS.find((preset) => preset.id === presetId)?.defaultEnv ?? {} -} - -function omitAuthEnv(env: Record): Record { - 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 { - return PROVIDER_PRESETS.find((preset) => preset.id === presetId)?.modelContextWindows ?? {} -} - -function buildProviderAuthEnv( - provider: SavedProvider, - presetDefaultEnv: Record, - needsProxy: boolean, -): Record { - 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(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 { - 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> { diff --git a/src/utils/managedEnv.ts b/src/utils/managedEnv.ts index be1d788f..db39f798 100644 --- a/src/utils/managedEnv.ts +++ b/src/utils/managedEnv.ts @@ -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 { const ccHahaSettings = join(getClaudeConfigHomeDir(), 'cc-haha', 'settings.json') const raw = readFileSync(ccHahaSettings, 'utf-8') const parsed = JSON.parse(raw) as { env?: Record } - return normalizeLegacyDeepSeekManagedEnv(parsed.env ?? {}).env + const settingsEnv = normalizeLegacyDeepSeekManagedEnv(parsed.env ?? {}).env + return mergeActiveProviderManagedEnv(settingsEnv, getClaudeConfigHomeDir()) } catch { - return {} + return mergeActiveProviderManagedEnv({}, getClaudeConfigHomeDir()) } }