Download src/server/services/providerService.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 26.4 kB
-
https://huggingface.co/chenbhao/codev/resolve/main/src/server/services/providerService.ts
- Command line
-
hf download hf://chenbhao/codev/src/server/services/providerService.ts
-
curl -L -o providerService.ts https://huggingface.co/chenbhao/codev/resolve/main/src/server/services/providerService.ts
26.4 kB
| /** | |
| * Provider Service β preset-based provider configuration | |
| * | |
| * Storage: ~/.claude/cc-haha/providers.json (lightweight index) | |
| * Active provider env vars written to ~/.claude/cc-haha/settings.json | |
| * (isolated from the original Claude Code's ~/.claude/settings.json) | |
| */ | |
| import * as fs from 'fs/promises' | |
| import * as path from 'path' | |
| import * as os from 'os' | |
| import { ApiError } from '../middleware/errorHandler.js' | |
| import { readRecoverableJsonFile } from './recoverableJsonFile.js' | |
| import { ManagedSettingsService } from './managedSettingsService.js' | |
| import { anthropicToOpenaiChat } from '../proxy/transform/anthropicToOpenaiChat.js' | |
| import { anthropicToOpenaiResponses } from '../proxy/transform/anthropicToOpenaiResponses.js' | |
| import { openaiChatToAnthropic } from '../proxy/transform/openaiChatToAnthropic.js' | |
| import { openaiResponsesToAnthropic } from '../proxy/transform/openaiResponsesToAnthropic.js' | |
| import type { AnthropicRequest, AnthropicResponse } from '../proxy/transform/types.js' | |
| import { | |
| OPENAI_OFFICIAL_PROVIDER, | |
| isOpenAIOfficialProviderId, | |
| } from './openaiOfficialProvider.js' | |
| import { hahaOpenAIOAuthService } from './hahaOpenAIOAuthService.js' | |
| import { | |
| CURRENT_PROVIDER_INDEX_SCHEMA_VERSION, | |
| ensurePersistentStorageUpgraded, | |
| } from './persistentStorageMigrations.js' | |
| import { | |
| buildProviderAuthEnv, | |
| buildProviderManagedEnv, | |
| getManagedEnvKeys, | |
| getPresetAuthStrategy, | |
| getPresetDefaultEnv, | |
| normalizeModelMapping, | |
| normalizeProvidersIndex, | |
| } from './providerRuntimeEnv.js' | |
| import { PROVIDER_PRESETS } from '../config/providerPresets.js' | |
| import { getProxyFetchOptions } from '../../utils/proxy.js' | |
| import { | |
| getManualNetworkProxyUrl, | |
| loadNetworkSettings, | |
| type NetworkSettings, | |
| } from './networkSettings.js' | |
| import type { | |
| SavedProvider, | |
| ProvidersIndex, | |
| CreateProviderInput, | |
| UpdateProviderInput, | |
| TestProviderInput, | |
| ProviderTestResult, | |
| ProviderTestStepResult, | |
| ApiFormat, | |
| ProviderAuthStrategy, | |
| } from '../types/provider.js' | |
| const DEFAULT_INDEX: ProvidersIndex = { | |
| schemaVersion: CURRENT_PROVIDER_INDEX_SCHEMA_VERSION, | |
| activeId: null, | |
| providers: [], | |
| } | |
| export class ProviderService { | |
| private static serverPort = 3456 | |
| private managedSettingsService = new ManagedSettingsService() | |
| static setServerPort(port: number): void { | |
| ProviderService.serverPort = port | |
| } | |
| static getServerPort(): number { | |
| return ProviderService.serverPort | |
| } | |
| private getConfigDir(): string { | |
| return process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude') | |
| } | |
| private getCcHahaDir(): string { | |
| return path.join(this.getConfigDir(), 'cc-haha') | |
| } | |
| private getIndexPath(): string { | |
| return path.join(this.getCcHahaDir(), 'providers.json') | |
| } | |
| /** | |
| * Path to the TUI's global config file (~/.claude.json). | |
| * Always at the home directory level, not under CLAUDE_CONFIG_DIR. | |
| */ | |
| private getTuiConfigPath(): string { | |
| return path.join(os.homedir(), '.claude.json') | |
| } | |
| /** | |
| * Auto-import the provider configured in the TUI's global config (~/.claude.json). | |
| * When the user configured a provider via /login in the TUI, this ensures the | |
| * desktop app sees and uses the same provider automatically. | |
| * | |
| * Only imports when no desktop provider is currently active, to avoid overriding | |
| * the user's desktop-specific choice. | |
| */ | |
| private async autoImportTuiProvider(): Promise<void> { | |
| const tuiConfigPath = this.getTuiConfigPath() | |
| let raw: string | |
| try { | |
| raw = await fs.readFile(tuiConfigPath, 'utf-8') | |
| } catch { | |
| return // No TUI config file | |
| } | |
| let tuiConfig: { | |
| authProvider?: string | |
| nvidiaApiKey?: string | |
| } | |
| try { | |
| tuiConfig = JSON.parse(raw) | |
| } catch { | |
| return // Invalid JSON | |
| } | |
| const providerId = tuiConfig.authProvider | |
| if (!providerId) return // No TUI provider configured | |
| // Read current index to check if already imported | |
| const index = await this.readIndex() | |
| if (index.activeId) return // already has a desktop provider active | |
| // Check if a TUI-imported provider already exists (e.g. from a previous import) | |
| const tuiProviderTag = `tui-${providerId}` | |
| const existing = index.providers.find(p => p.presetId === tuiProviderTag) | |
| if (existing) { | |
| index.activeId = existing.id | |
| await this.writeIndex(index) | |
| return | |
| } | |
| // Find the matching preset | |
| const preset = PROVIDER_PRESETS.find(p => p.id === providerId) | |
| if (!preset) return // No matching desktop preset for this TUI provider | |
| // Extract API key based on provider type | |
| let apiKey = '' | |
| if (providerId === 'nvidia') { | |
| apiKey = tuiConfig.nvidiaApiKey || '' | |
| if (!apiKey) return | |
| } | |
| const provider: SavedProvider = { | |
| id: crypto.randomUUID(), | |
| presetId: tuiProviderTag, | |
| name: `TUI: ${preset.name}`, | |
| apiKey, | |
| baseUrl: preset.baseUrl, | |
| apiFormat: preset.apiFormat, | |
| runtimeKind: 'anthropic_compatible', | |
| models: preset.defaultModels, | |
| } | |
| index.providers.push(provider) | |
| index.activeId = provider.id | |
| await this.writeIndex(index) | |
| } | |
| private async readIndex(): Promise<ProvidersIndex> { | |
| await ensurePersistentStorageUpgraded() | |
| return readRecoverableJsonFile({ | |
| filePath: this.getIndexPath(), | |
| label: 'providers index', | |
| defaultValue: DEFAULT_INDEX, | |
| normalize: normalizeProvidersIndex, | |
| }) | |
| } | |
| private async writeIndex(index: ProvidersIndex): Promise<void> { | |
| const filePath = this.getIndexPath() | |
| const dir = path.dirname(filePath) | |
| await fs.mkdir(dir, { recursive: true }) | |
| const tmpFile = `${filePath}.tmp.${Date.now()}` | |
| try { | |
| await fs.writeFile(tmpFile, JSON.stringify(index, null, 2) + '\n', 'utf-8') | |
| await fs.rename(tmpFile, filePath) | |
| } catch (err) { | |
| await fs.unlink(tmpFile).catch(() => {}) | |
| throw ApiError.internal(`Failed to write providers index: ${err}`) | |
| } | |
| } | |
| private async readSettings(): Promise<Record<string, unknown>> { | |
| return this.managedSettingsService.readSettings() | |
| } | |
| async getManagedSettings(): Promise<Record<string, unknown>> { | |
| return this.readSettings() | |
| } | |
| async updateManagedSettings(settings: Record<string, unknown>): Promise<void> { | |
| await this.managedSettingsService.updateSettings((current) => ({ | |
| settings: Object.assign({}, current, settings), | |
| result: undefined, | |
| })) | |
| } | |
| // --- CRUD --- | |
| async listProviders(): Promise<{ providers: SavedProvider[]; activeId: string | null }> { | |
| // Auto-import TUI provider so desktop follows the TUI's provider choice | |
| await this.autoImportTuiProvider() | |
| const index = await this.readIndex() | |
| return { providers: index.providers, activeId: index.activeId } | |
| } | |
| async getProvider(id: string): Promise<SavedProvider> { | |
| if (isOpenAIOfficialProviderId(id)) { | |
| return OPENAI_OFFICIAL_PROVIDER | |
| } | |
| const index = await this.readIndex() | |
| const provider = index.providers.find((p) => p.id === id) | |
| if (!provider) throw ApiError.notFound(`Provider not found: ${id}`) | |
| return provider | |
| } | |
| async addProvider(input: CreateProviderInput): Promise<SavedProvider> { | |
| const index = await this.readIndex() | |
| const provider: SavedProvider = { | |
| id: crypto.randomUUID(), | |
| presetId: input.presetId, | |
| name: input.name, | |
| apiKey: input.apiKey, | |
| ...(input.authStrategy !== undefined && { authStrategy: input.authStrategy }), | |
| baseUrl: input.baseUrl, | |
| apiFormat: input.apiFormat ?? 'anthropic', | |
| runtimeKind: input.runtimeKind ?? 'anthropic_compatible', | |
| models: normalizeModelMapping(input.models), | |
| ...(input.autoCompactWindow !== undefined && { autoCompactWindow: input.autoCompactWindow }), | |
| ...(input.modelContextWindows !== undefined && { modelContextWindows: input.modelContextWindows }), | |
| ...(input.notes !== undefined && { notes: input.notes }), | |
| } | |
| index.providers.push(provider) | |
| await this.writeIndex(index) | |
| return provider | |
| } | |
| async updateProvider(id: string, input: UpdateProviderInput): Promise<SavedProvider> { | |
| const index = await this.readIndex() | |
| const idx = index.providers.findIndex((p) => p.id === id) | |
| if (idx === -1) throw ApiError.notFound(`Provider not found: ${id}`) | |
| const existing = index.providers[idx] | |
| const updated: SavedProvider = { | |
| ...existing, | |
| ...(input.name !== undefined && { name: input.name }), | |
| ...(input.apiKey !== undefined && { apiKey: input.apiKey }), | |
| ...(input.authStrategy !== undefined && { authStrategy: input.authStrategy }), | |
| ...(input.baseUrl !== undefined && { baseUrl: input.baseUrl }), | |
| ...(input.apiFormat !== undefined && { apiFormat: input.apiFormat }), | |
| ...(input.runtimeKind !== undefined && { runtimeKind: input.runtimeKind }), | |
| ...(input.models !== undefined && { models: normalizeModelMapping(input.models) }), | |
| ...(typeof input.autoCompactWindow === 'number' && { autoCompactWindow: input.autoCompactWindow }), | |
| ...(input.modelContextWindows !== undefined && input.modelContextWindows !== null && { modelContextWindows: input.modelContextWindows }), | |
| ...(input.notes !== undefined && { notes: input.notes }), | |
| } | |
| if (input.autoCompactWindow === null) { | |
| delete updated.autoCompactWindow | |
| } | |
| if (input.modelContextWindows === null) { | |
| delete updated.modelContextWindows | |
| } | |
| index.providers[idx] = updated | |
| await this.writeIndex(index) | |
| if (index.activeId === id) { | |
| await this.syncToSettings(updated) | |
| } | |
| return updated | |
| } | |
| async deleteProvider(id: string): Promise<void> { | |
| const index = await this.readIndex() | |
| const idx = index.providers.findIndex((p) => p.id === id) | |
| if (idx === -1) throw ApiError.notFound(`Provider not found: ${id}`) | |
| if (index.activeId === id) { | |
| throw ApiError.conflict('Cannot delete the active provider. Switch to another provider first.') | |
| } | |
| index.providers.splice(idx, 1) | |
| await this.writeIndex(index) | |
| } | |
| // --- Activation --- | |
| async activateProvider(id: string): Promise<void> { | |
| const index = await this.readIndex() | |
| const provider = isOpenAIOfficialProviderId(id) | |
| ? OPENAI_OFFICIAL_PROVIDER | |
| : index.providers.find((p) => p.id === id) | |
| if (!provider) throw ApiError.notFound(`Provider not found: ${id}`) | |
| index.activeId = id | |
| await this.writeIndex(index) | |
| if (provider.runtimeKind === 'openai_oauth') { | |
| await this.syncToSettings(provider) | |
| } else if (provider.presetId === 'official') { | |
| await this.clearProviderFromSettings() | |
| } else { | |
| await this.syncToSettings(provider) | |
| } | |
| } | |
| async activateOfficial(): Promise<void> { | |
| const index = await this.readIndex() | |
| index.activeId = null | |
| await this.writeIndex(index) | |
| await this.clearProviderFromSettings() | |
| } | |
| // --- Settings sync --- | |
| private buildManagedEnv( | |
| provider: SavedProvider, | |
| options?: { proxyPath?: string }, | |
| ): Record<string, string> { | |
| return buildProviderManagedEnv(provider, { | |
| proxyPath: options?.proxyPath, | |
| serverPort: ProviderService.serverPort, | |
| }) | |
| } | |
| async getProviderRuntimeEnv(id: string): Promise<Record<string, string>> { | |
| const provider = await this.getProvider(id) | |
| return this.buildManagedEnv(provider, { | |
| proxyPath: `/proxy/providers/${provider.id}`, | |
| }) | |
| } | |
| private async syncToSettings(provider: SavedProvider): Promise<void> { | |
| await this.managedSettingsService.updateSettings((settings) => { | |
| const existingEnv = (settings.env as Record<string, string>) || {} | |
| const cleanedEnv = { ...existingEnv } | |
| for (const key of getManagedEnvKeys()) { | |
| delete cleanedEnv[key] | |
| } | |
| return { | |
| settings: { | |
| ...settings, | |
| env: { | |
| ...cleanedEnv, | |
| ...this.buildManagedEnv(provider), | |
| }, | |
| }, | |
| result: undefined, | |
| } | |
| }) | |
| } | |
| private async clearProviderFromSettings(): Promise<void> { | |
| await this.managedSettingsService.updateSettings((settings) => { | |
| const env = { ...((settings.env as Record<string, string>) || {}) } | |
| for (const key of getManagedEnvKeys()) { | |
| delete env[key] | |
| } | |
| const nextSettings: Record<string, unknown> = { | |
| ...settings, | |
| } | |
| if (Object.keys(env).length === 0) { | |
| delete nextSettings.env | |
| } else { | |
| nextSettings.env = env | |
| } | |
| return { | |
| settings: nextSettings, | |
| result: undefined, | |
| } | |
| }) | |
| } | |
| // --- Auth status --- | |
| /** | |
| * Check whether any usable auth exists: | |
| * 1. A cc-haha provider is active β has auth | |
| * 2. Original ~/.claude/settings.json has ANTHROPIC_AUTH_TOKEN or ANTHROPIC_API_KEY β has auth | |
| * 3. process.env already has ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN β has auth | |
| * 4. None of the above β needs setup | |
| */ | |
| async checkAuthStatus(): Promise<{ | |
| hasAuth: boolean | |
| source: 'cc-haha-provider' | 'openai-oauth' | 'original-settings' | 'env' | 'none' | |
| activeProvider?: string | |
| }> { | |
| // 1. Check cc-haha active provider | |
| const index = await this.readIndex() | |
| if (index.activeId) { | |
| if (isOpenAIOfficialProviderId(index.activeId)) { | |
| const tokens = await hahaOpenAIOAuthService.ensureFreshTokens() | |
| if (tokens?.accessToken && tokens.refreshToken) { | |
| return { | |
| hasAuth: true, | |
| source: 'openai-oauth', | |
| activeProvider: OPENAI_OFFICIAL_PROVIDER.name, | |
| } | |
| } | |
| return { | |
| hasAuth: false, | |
| source: 'none', | |
| activeProvider: OPENAI_OFFICIAL_PROVIDER.name, | |
| } | |
| } | |
| const provider = index.providers.find(p => p.id === index.activeId) | |
| if (provider) { | |
| const presetDefaultEnv = getPresetDefaultEnv(provider.presetId) | |
| const needsProxy = provider.apiFormat != null && provider.apiFormat !== 'anthropic' | |
| const authEnv = buildProviderAuthEnv(provider, presetDefaultEnv, needsProxy) | |
| if (Object.values(authEnv).some(value => value.length > 0)) { | |
| return { hasAuth: true, source: 'cc-haha-provider', activeProvider: provider.name } | |
| } | |
| } | |
| } | |
| // 2. Check process.env (covers .env file + inherited env) | |
| if (process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN) { | |
| return { hasAuth: true, source: 'env' } | |
| } | |
| // 3. Check original ~/.claude/settings.json | |
| try { | |
| const originalPath = path.join(this.getConfigDir(), 'settings.json') | |
| const raw = await fs.readFile(originalPath, 'utf-8') | |
| const settings = JSON.parse(raw) as { env?: Record<string, string> } | |
| const env = settings.env ?? {} | |
| if (env.ANTHROPIC_AUTH_TOKEN || env.ANTHROPIC_API_KEY) { | |
| return { hasAuth: true, source: 'original-settings' } | |
| } | |
| } catch { | |
| // File doesn't exist or invalid | |
| } | |
| return { hasAuth: false, source: 'none' } | |
| } | |
| // --- Proxy support --- | |
| async getProviderForProxy(providerId?: string): Promise<{ | |
| baseUrl: string | |
| apiKey: string | |
| apiFormat: ApiFormat | |
| } | null> { | |
| if (providerId) { | |
| if (isOpenAIOfficialProviderId(providerId)) { | |
| return null | |
| } | |
| const provider = await this.getProvider(providerId) | |
| return { | |
| baseUrl: provider.baseUrl, | |
| apiKey: provider.apiKey, | |
| apiFormat: provider.apiFormat ?? 'anthropic', | |
| } | |
| } | |
| const index = await this.readIndex() | |
| if (!index.activeId) return null | |
| if (isOpenAIOfficialProviderId(index.activeId)) { | |
| return null | |
| } | |
| const provider = await this.getProvider(index.activeId).catch(() => null) | |
| if (!provider) return null | |
| return { | |
| baseUrl: provider.baseUrl, | |
| apiKey: provider.apiKey, | |
| apiFormat: provider.apiFormat ?? 'anthropic', | |
| } | |
| } | |
| async getActiveProviderForProxy(): Promise<{ | |
| baseUrl: string | |
| apiKey: string | |
| apiFormat: ApiFormat | |
| } | null> { | |
| return this.getProviderForProxy() | |
| } | |
| // --- Test --- | |
| async testProvider( | |
| id: string, | |
| overrides?: { baseUrl?: string; modelId?: string; apiFormat?: ApiFormat; authStrategy?: ProviderAuthStrategy }, | |
| ): Promise<ProviderTestResult> { | |
| const provider = await this.getProvider(id) | |
| const baseUrl = overrides?.baseUrl || provider.baseUrl | |
| const modelId = overrides?.modelId || provider.models.main | |
| const apiFormat = overrides?.apiFormat ?? provider.apiFormat ?? 'anthropic' | |
| const authStrategy = overrides?.authStrategy ?? provider.authStrategy ?? getPresetAuthStrategy(provider.presetId) | |
| const presetDefaultEnv = getPresetDefaultEnv(provider.presetId) | |
| const apiKey = provider.apiKey | |
| || presetDefaultEnv.ANTHROPIC_AUTH_TOKEN | |
| || presetDefaultEnv.ANTHROPIC_API_KEY | |
| || (authStrategy === 'dual_dummy' ? 'dummy' : '') | |
| if (!baseUrl || !apiKey) { | |
| return { connectivity: { success: false, latencyMs: 0, error: 'Missing baseUrl or apiKey' } } | |
| } | |
| return this.testProviderConfig({ | |
| baseUrl, | |
| apiKey, | |
| modelId, | |
| authStrategy, | |
| apiFormat, | |
| }) | |
| } | |
| async testProviderConfig(input: TestProviderInput): Promise<ProviderTestResult> { | |
| const format: ApiFormat = input.apiFormat ?? 'anthropic' | |
| const authStrategy = input.authStrategy ?? 'api_key' | |
| const base = input.baseUrl.replace(/\/+$/, '') | |
| const networkSettings = await loadNetworkSettings() | |
| // ββ Step 1: Basic connectivity βββββββββββββββββββββββββββ | |
| // Directly call the upstream API to verify URL, key, and model. | |
| const step1 = await this.testConnectivity(base, input.apiKey, input.modelId, format, authStrategy, networkSettings) | |
| // If connectivity failed, no point running step 2 | |
| if (!step1.success) { | |
| return { connectivity: step1 } | |
| } | |
| // For native Anthropic format, no proxy pipeline to test | |
| if (format === 'anthropic') { | |
| return { connectivity: step1 } | |
| } | |
| // ββ Step 2: Full proxy pipeline ββββββββββββββββββββββββββ | |
| // Anthropic request β transform β upstream β transform back β validate | |
| const step2 = await this.testProxyPipeline(base, input.apiKey, input.modelId, format, networkSettings) | |
| return { connectivity: step1, proxy: step2 } | |
| } | |
| /** Step 1: Direct upstream call to verify connectivity, auth, and model. */ | |
| private async testConnectivity( | |
| base: string, | |
| apiKey: string, | |
| modelId: string, | |
| format: ApiFormat, | |
| authStrategy: ProviderAuthStrategy, | |
| networkSettings: NetworkSettings, | |
| ): Promise<ProviderTestStepResult> { | |
| const start = Date.now() | |
| try { | |
| const { url, headers, body } = buildDirectTestRequest(base, apiKey, modelId, format, authStrategy) | |
| const proxyOptions = getProxyFetchOptions({ proxyUrl: getManualNetworkProxyUrl(networkSettings) }) | |
| const response = await fetch(url, { | |
| method: 'POST', | |
| headers, | |
| body: JSON.stringify(body), | |
| signal: AbortSignal.timeout(networkSettings.aiRequestTimeoutMs), | |
| ...proxyOptions, | |
| }) | |
| const latencyMs = Date.now() - start | |
| const resBody = await response.json().catch(() => null) as Record<string, unknown> | null | |
| if (!response.ok) { | |
| let error = `HTTP ${response.status}` | |
| if (resBody?.error && typeof resBody.error === 'object') { | |
| error = ((resBody.error as Record<string, unknown>).message as string) || error | |
| } | |
| return { success: false, latencyMs, error, modelUsed: modelId, httpStatus: response.status } | |
| } | |
| // Validate response structure | |
| const valid = validateResponseBody(resBody, format) | |
| if (!valid.ok) { | |
| return { success: false, latencyMs, error: valid.error, modelUsed: modelId, httpStatus: response.status } | |
| } | |
| return { success: true, latencyMs, modelUsed: valid.model || modelId, httpStatus: response.status } | |
| } catch (err: unknown) { | |
| const latencyMs = Date.now() - start | |
| if (err instanceof DOMException && err.name === 'TimeoutError') { | |
| return { success: false, latencyMs, error: `Request timed out (${Math.round(networkSettings.aiRequestTimeoutMs / 1000)}s)`, modelUsed: modelId } | |
| } | |
| return { success: false, latencyMs, error: err instanceof Error ? err.message : String(err), modelUsed: modelId } | |
| } | |
| } | |
| /** Step 2: Full proxy pipeline β Anthropic β transform β upstream β transform back β validate. */ | |
| private async testProxyPipeline( | |
| base: string, | |
| apiKey: string, | |
| modelId: string, | |
| format: 'openai_chat' | 'openai_responses', | |
| networkSettings: NetworkSettings, | |
| ): Promise<ProviderTestStepResult> { | |
| const start = Date.now() | |
| try { | |
| // Build an Anthropic Messages API request (same shape as what CLI sends) | |
| const anthropicReq: AnthropicRequest = { | |
| model: modelId, | |
| max_tokens: 64, | |
| messages: [{ role: 'user', content: 'Say "ok" and nothing else.' }], | |
| } | |
| // Transform to OpenAI format | |
| let upstreamUrl: string | |
| let transformedBody: unknown | |
| if (format === 'openai_chat') { | |
| transformedBody = anthropicToOpenaiChat(anthropicReq) | |
| upstreamUrl = `${base}/v1/chat/completions` | |
| } else { | |
| transformedBody = anthropicToOpenaiResponses(anthropicReq) | |
| upstreamUrl = `${base}/v1/responses` | |
| } | |
| const proxyOptions = getProxyFetchOptions({ proxyUrl: getManualNetworkProxyUrl(networkSettings) }) | |
| // Call upstream with transformed request | |
| const response = await fetch(upstreamUrl, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` }, | |
| body: JSON.stringify(transformedBody), | |
| signal: AbortSignal.timeout(networkSettings.aiRequestTimeoutMs), | |
| ...proxyOptions, | |
| }) | |
| if (!response.ok) { | |
| const latencyMs = Date.now() - start | |
| const errText = await response.text().catch(() => '') | |
| return { success: false, latencyMs, modelUsed: modelId, httpStatus: response.status, | |
| error: `Upstream HTTP ${response.status}: ${errText.slice(0, 200)}` } | |
| } | |
| // Transform response back to Anthropic format | |
| const responseBody = await response.json() | |
| const anthropicRes = format === 'openai_chat' | |
| ? openaiChatToAnthropic(responseBody, modelId) | |
| : openaiResponsesToAnthropic(responseBody, modelId) | |
| const latencyMs = Date.now() - start | |
| // Validate the final Anthropic response | |
| if (anthropicRes.type !== 'message' || !Array.isArray(anthropicRes.content)) { | |
| return { success: false, latencyMs, modelUsed: modelId, | |
| error: 'Proxy transform produced invalid Anthropic response' } | |
| } | |
| return { success: true, latencyMs, modelUsed: anthropicRes.model || modelId, httpStatus: response.status } | |
| } catch (err: unknown) { | |
| const latencyMs = Date.now() - start | |
| if (err instanceof DOMException && err.name === 'TimeoutError') { | |
| return { success: false, latencyMs, error: `Proxy pipeline timed out (${Math.round(networkSettings.aiRequestTimeoutMs / 1000)}s)`, modelUsed: modelId } | |
| } | |
| return { success: false, latencyMs, error: err instanceof Error ? err.message : String(err), modelUsed: modelId } | |
| } | |
| } | |
| } | |
| // βββ Helpers βββββββββββββββββββββββββββββββββββββββββββββββ | |
| function buildDirectTestRequest( | |
| base: string, | |
| apiKey: string, | |
| modelId: string, | |
| format: ApiFormat, | |
| authStrategy: ProviderAuthStrategy, | |
| ): { url: string; headers: Record<string, string>; body: Record<string, unknown> } { | |
| const prompt = 'Say "ok" and nothing else.' | |
| if (format === 'openai_chat') { | |
| return { | |
| url: `${base}/v1/chat/completions`, | |
| headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` }, | |
| body: { model: modelId, max_tokens: 16, messages: [{ role: 'user', content: prompt }] }, | |
| } | |
| } | |
| if (format === 'openai_responses') { | |
| return { | |
| url: `${base}/v1/responses`, | |
| headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` }, | |
| body: { model: modelId, max_output_tokens: 16, input: [{ type: 'message', role: 'user', content: prompt }] }, | |
| } | |
| } | |
| // anthropic | |
| return { | |
| url: `${base}/v1/messages`, | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'anthropic-version': '2023-06-01', | |
| ...buildAnthropicAuthHeaders(apiKey, authStrategy), | |
| }, | |
| body: { model: modelId, max_tokens: 16, messages: [{ role: 'user', content: prompt }] }, | |
| } | |
| } | |
| function buildAnthropicAuthHeaders(apiKey: string, authStrategy: ProviderAuthStrategy): Record<string, string> { | |
| switch (authStrategy) { | |
| case 'api_key': | |
| return { 'x-api-key': apiKey } | |
| case 'auth_token': | |
| case 'auth_token_empty_api_key': | |
| return { Authorization: `Bearer ${apiKey}` } | |
| case 'dual_same_token': | |
| return { 'x-api-key': apiKey, Authorization: `Bearer ${apiKey}` } | |
| case 'dual_dummy': | |
| return { 'x-api-key': 'dummy', Authorization: 'Bearer dummy' } | |
| } | |
| } | |
| function validateResponseBody( | |
| body: Record<string, unknown> | null, | |
| format: ApiFormat, | |
| ): { ok: true; model?: string } | { ok: false; error: string } { | |
| if (!body) return { ok: false, error: 'Empty response β not a valid API endpoint' } | |
| if (body.error && typeof body.error === 'object') { | |
| return { ok: false, error: ((body.error as Record<string, unknown>).message as string) || 'Error in response body' } | |
| } | |
| if (format === 'openai_chat') { | |
| if (!Array.isArray(body.choices) || body.choices.length === 0) { | |
| return { ok: false, error: 'Response missing choices β not a valid Chat Completions endpoint' } | |
| } | |
| return { ok: true, model: (body.model as string) || undefined } | |
| } | |
| if (format === 'openai_responses') { | |
| if (!Array.isArray(body.output)) { | |
| return { ok: false, error: 'Response missing output β not a valid Responses API endpoint' } | |
| } | |
| return { ok: true, model: (body.model as string) || undefined } | |
| } | |
| // anthropic | |
| if (body.type !== 'message' || !Array.isArray(body.content)) { | |
| return { ok: false, error: 'Not a valid Anthropic Messages endpoint' } | |
| } | |
| return { ok: true, model: (body.model as string) || undefined } | |
| } | |