mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
Integrate direct TUI provider env recovery into local main
Local main had advanced independently with OpenAI OAuth URL handling, while the detached worktree fixed stale cc-haha provider env for direct TUI launches. Merge the verified worktree commit so source and desktop-terminal TUI paths both derive provider runtime env from the active provider index. Constraint: Local main is the canonical checkout for this repository Rejected: git merge --ff-only 3a42ca20 | local main and the detached worktree had one commit of independent divergence Confidence: high Scope-risk: moderate Directive: Keep provider runtime env changes represented on local main before any release or PR work 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 after the local-main merge
This commit is contained in:
commit
671358656b
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