/** * 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 { getProxyFetchOptions } from '../../utils/proxy.js' import { getManualNetworkProxyUrl, loadNetworkSettings, mergeLoopbackNoProxy, type NetworkSettings, } from './networkSettings.js' import { normalizeModelStringForAPI } from '../../utils/model/model.js' import type { SavedProvider, ProvidersIndex, CreateProviderInput, UpdateProviderInput, TestProviderInput, ProviderTestResult, ProviderTestStepResult, ApiFormat, ProviderAuthStrategy, } from '../types/provider.js' import { BUILT_IN_PROVIDER_IDS, } from '../types/provider.js' const DEFAULT_INDEX: ProvidersIndex = { schemaVersion: CURRENT_PROVIDER_INDEX_SCHEMA_VERSION, activeId: null, providers: [], providerOrder: [...BUILT_IN_PROVIDER_IDS], } function isPermutation(candidateIds: string[], expectedIds: string[]): boolean { const expectedSet = new Set(expectedIds) const candidateSet = new Set(candidateIds) return ( candidateIds.length === expectedIds.length && candidateSet.size === candidateIds.length && expectedIds.every((id) => candidateSet.has(id)) && candidateIds.every((id) => expectedSet.has(id)) ) } function savedProviderIds(providers: SavedProvider[]): string[] { return providers.map((provider) => provider.id) } function fullProviderOrderIds(providers: SavedProvider[]): string[] { return [ ...savedProviderIds(providers), ...BUILT_IN_PROVIDER_IDS, ] } function sortSavedProvidersByOrder(providers: SavedProvider[], providerOrder: string[]): SavedProvider[] { const byId = new Map(providers.map((provider) => [provider.id, provider])) return providerOrder .map((id) => byId.get(id)) .filter((provider): provider is SavedProvider => provider !== undefined) } function mergeSavedOrderIntoDisplayOrder(providerOrder: string[], savedOrder: string[]): string[] { const savedSet = new Set(savedOrder) const queue = [...savedOrder] return providerOrder.map((id) => { if (!savedSet.has(id)) return id return queue.shift() ?? id }) } function appendNewProviderToOrder(providerOrder: string[], providerId: string, existingProviders: SavedProvider[]): string[] { const existingProviderIds = new Set(existingProviders.map((provider) => provider.id)) const lastSavedIndex = providerOrder.reduce( (latest, id, index) => existingProviderIds.has(id) ? index : latest, -1, ) if (lastSavedIndex !== -1) { return [ ...providerOrder.slice(0, lastSavedIndex + 1), providerId, ...providerOrder.slice(lastSavedIndex + 1), ] } const firstBuiltInIndex = providerOrder.findIndex((id) => BUILT_IN_PROVIDER_IDS.includes(id as typeof BUILT_IN_PROVIDER_IDS[number])) if (firstBuiltInIndex === -1) return [...providerOrder, providerId] return [ ...providerOrder.slice(0, firstBuiltInIndex), providerId, ...providerOrder.slice(firstBuiltInIndex), ] } 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') } private async readIndex(): Promise { await ensurePersistentStorageUpgraded() return readRecoverableJsonFile({ filePath: this.getIndexPath(), label: 'providers index', defaultValue: DEFAULT_INDEX, normalize: normalizeProvidersIndex, }) } private async writeIndex(index: ProvidersIndex): Promise { 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> { return this.managedSettingsService.readSettings() } async getManagedSettings(): Promise> { return this.readSettings() } async updateManagedSettings(settings: Record): Promise { await this.managedSettingsService.updateSettings((current) => ({ settings: Object.assign({}, current, settings), result: undefined, })) } // --- CRUD --- async listProviders(): Promise<{ providers: SavedProvider[]; activeId: string | null; providerOrder: string[] }> { const index = await this.readIndex() return { providers: index.providers, activeId: index.activeId, providerOrder: index.providerOrder, } } async getProvider(id: string): Promise { 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 { 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.model1mSupport !== undefined && { model1mSupport: input.model1mSupport }), ...(input.autoCompactWindow !== undefined && { autoCompactWindow: input.autoCompactWindow }), ...(input.modelContextWindows !== undefined && { modelContextWindows: input.modelContextWindows }), toolSearchEnabled: input.toolSearchEnabled ?? true, ...(input.notes !== undefined && { notes: input.notes }), } index.providerOrder = appendNewProviderToOrder(index.providerOrder, provider.id, index.providers) index.providers.push(provider) await this.writeIndex(index) return provider } async updateProvider(id: string, input: UpdateProviderInput): Promise { 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) }), ...(input.model1mSupport !== undefined && input.model1mSupport !== null && { model1mSupport: input.model1mSupport }), ...(typeof input.autoCompactWindow === 'number' && { autoCompactWindow: input.autoCompactWindow }), ...(input.modelContextWindows !== undefined && input.modelContextWindows !== null && { modelContextWindows: input.modelContextWindows }), ...(input.toolSearchEnabled !== undefined && { toolSearchEnabled: input.toolSearchEnabled }), ...(input.notes !== undefined && { notes: input.notes }), } if (input.model1mSupport === null) { delete updated.model1mSupport } 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 { 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) index.providerOrder = index.providerOrder.filter((providerId) => providerId !== id) await this.writeIndex(index) } /** * Reorder providers to match the given display id order. * * New clients send a full display-order permutation including the built-in * official providers. Legacy clients may still send only saved provider ids; * in that case the saved providers are reordered inside the current display * order without moving the built-in official rows. */ async reorderProviders(orderedIds: string[]): Promise<{ providers: SavedProvider[]; providerOrder: string[] }> { const index = await this.readIndex() const currentSavedIds = savedProviderIds(index.providers) const isFullDisplayPermutation = isPermutation(orderedIds, fullProviderOrderIds(index.providers)) const isLegacySavedPermutation = isPermutation(orderedIds, currentSavedIds) if (!isFullDisplayPermutation && !isLegacySavedPermutation) { throw ApiError.badRequest('orderedIds must be a permutation of existing provider ids and built-in provider ids') } index.providerOrder = isFullDisplayPermutation ? orderedIds : mergeSavedOrderIntoDisplayOrder(index.providerOrder, orderedIds) index.providers = sortSavedProvidersByOrder(index.providers, index.providerOrder) await this.writeIndex(index) return { providers: index.providers, providerOrder: index.providerOrder, } } // --- Activation --- async activateProvider(id: string): Promise { 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 { 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 { return buildProviderManagedEnv(provider, { proxyPath: options?.proxyPath, serverPort: ProviderService.serverPort, }) } async getProviderRuntimeEnv(id: string): Promise> { const provider = await this.getProvider(id) return this.buildManagedEnv(provider, { proxyPath: `/proxy/providers/${provider.id}`, }) } private async syncToSettings(provider: SavedProvider): Promise { await this.managedSettingsService.updateSettings((settings) => { const existingEnv = (settings.env as Record) || {} 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 { await this.managedSettingsService.updateSettings((settings) => { const env = { ...((settings.env as Record) || {}) } for (const key of getManagedEnvKeys()) { delete env[key] } const nextSettings: Record = { ...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 } 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<{ id: string name: string baseUrl: string apiKey: string apiFormat: ApiFormat } | null> { if (providerId) { if (isOpenAIOfficialProviderId(providerId)) { return null } const provider = await this.getProvider(providerId) return { id: provider.id, name: provider.name, 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 { id: provider.id, name: provider.name, 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 { 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 { const format: ApiFormat = input.apiFormat ?? 'anthropic' const authStrategy = input.authStrategy ?? 'api_key' const base = input.baseUrl.replace(/\/+$/, '') const modelId = normalizeModelStringForAPI(input.modelId) 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, 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, 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 { const start = Date.now() try { const { url, headers, body } = buildDirectTestRequest(base, apiKey, modelId, format, authStrategy) const proxyOptions = getProxyFetchOptions({ proxyUrl: getManualNetworkProxyUrl(networkSettings), targetUrl: url, noProxy: mergeLoopbackNoProxy(process.env.no_proxy || process.env.NO_PROXY), }) 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 | null if (!response.ok) { let error = `HTTP ${response.status}` if (resBody?.error && typeof resBody.error === 'object') { error = ((resBody.error as Record).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 { 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), targetUrl: upstreamUrl, noProxy: mergeLoopbackNoProxy(process.env.no_proxy || process.env.NO_PROXY), }) // 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; body: Record } { 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, stream: false, 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 { 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 | 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).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 } }