import { execCli } from '@main/utils/childProcess'; import { resolveInteractiveShellEnv } from '@main/utils/shellEnv'; import { createLogger } from '@shared/utils/logger'; import { createDefaultCliExtensionCapabilities } from '@shared/utils/providerExtensionCapabilities'; import { resolveGeminiRuntimeAuth } from './geminiRuntimeAuth'; import { buildProviderAwareCliEnv } from './providerAwareCliEnv'; import { providerConnectionService } from './ProviderConnectionService'; import type { CliProviderId, CliProviderStatus } from '@shared/types'; const logger = createLogger('ClaudeMultimodelBridgeService'); const PROVIDER_STATUS_TIMEOUT_MS = 10_000; const PROVIDER_MODELS_TIMEOUT_MS = 10_000; interface RuntimeExtensionCapabilityResponse { status?: 'supported' | 'read-only' | 'unsupported'; ownership?: 'shared' | 'provider-scoped'; reason?: string | null; } interface RuntimeExtensionCapabilitiesResponse { plugins?: RuntimeExtensionCapabilityResponse; mcp?: RuntimeExtensionCapabilityResponse; skills?: RuntimeExtensionCapabilityResponse; apiKeys?: RuntimeExtensionCapabilityResponse; } interface ProviderStatusCommandResponse { schemaVersion?: number; providers?: Record< string, { supported?: boolean; authenticated?: boolean; authMethod?: string | null; verificationState?: 'verified' | 'unknown' | 'offline' | 'error'; canLoginFromUi?: boolean; statusMessage?: string | null; capabilities?: { teamLaunch?: boolean; oneShot?: boolean; extensions?: RuntimeExtensionCapabilitiesResponse; }; backend?: { kind?: string; label?: string; endpointLabel?: string | null; projectId?: string | null; authMethodDetail?: string | null; } | null; } >; } interface ProviderModelsCommandResponse { schemaVersion?: number; providers?: Record< string, { models?: (string | { id?: string; label?: string; description?: string })[]; } >; } interface UnifiedRuntimeStatusResponse { schemaVersion?: number; providers?: Record< string, { supported?: boolean; authenticated?: boolean; authMethod?: string | null; verificationState?: 'verified' | 'unknown' | 'offline' | 'error'; canLoginFromUi?: boolean; statusMessage?: string | null; detailMessage?: string | null; selectedBackendId?: string | null; resolvedBackendId?: string | null; availableBackends?: { id?: string; label?: string; description?: string; selectable?: boolean; recommended?: boolean; available?: boolean; statusMessage?: string | null; detailMessage?: string | null; }[]; externalRuntimeDiagnostics?: { id?: string; label?: string; detected?: boolean; statusMessage?: string | null; detailMessage?: string | null; }[]; models?: (string | { id?: string; label?: string; description?: string })[]; capabilities?: { teamLaunch?: boolean; oneShot?: boolean; extensions?: RuntimeExtensionCapabilitiesResponse; }; backend?: { kind?: string; label?: string; endpointLabel?: string | null; projectId?: string | null; authMethodDetail?: string | null; } | null; } >; } const ORDERED_PROVIDER_IDS: CliProviderId[] = ['anthropic', 'codex', 'gemini']; function extractJsonObject(raw: string): T { const trimmed = raw.trim(); try { return JSON.parse(trimmed) as T; } catch { const start = trimmed.indexOf('{'); const end = trimmed.lastIndexOf('}'); if (start >= 0 && end > start) { return JSON.parse(trimmed.slice(start, end + 1)) as T; } throw new Error('No JSON object found in CLI output'); } } function createDefaultProviderStatus(providerId: CliProviderId): CliProviderStatus { return { providerId, displayName: providerId === 'anthropic' ? 'Anthropic' : providerId === 'codex' ? 'Codex' : 'Gemini', supported: false, authenticated: false, authMethod: null, verificationState: 'unknown', modelVerificationState: 'idle', statusMessage: null, models: [], modelAvailability: [], canLoginFromUi: true, capabilities: { teamLaunch: false, oneShot: false, extensions: createDefaultCliExtensionCapabilities(), }, selectedBackendId: null, resolvedBackendId: null, availableBackends: [], externalRuntimeDiagnostics: [], backend: null, connection: null, }; } function mapRuntimeExtensionCapabilities( capabilities?: RuntimeExtensionCapabilitiesResponse ): CliProviderStatus['capabilities']['extensions'] { const defaults = createDefaultCliExtensionCapabilities(); return { plugins: { ...defaults.plugins, status: capabilities?.plugins?.status ?? defaults.plugins.status, ownership: capabilities?.plugins?.ownership ?? defaults.plugins.ownership, reason: capabilities?.plugins?.reason ?? defaults.plugins.reason, }, mcp: { ...defaults.mcp, status: capabilities?.mcp?.status ?? defaults.mcp.status, ownership: capabilities?.mcp?.ownership ?? defaults.mcp.ownership, reason: capabilities?.mcp?.reason ?? defaults.mcp.reason, }, skills: { ...defaults.skills, status: capabilities?.skills?.status ?? defaults.skills.status, ownership: capabilities?.skills?.ownership ?? defaults.skills.ownership, reason: capabilities?.skills?.reason ?? defaults.skills.reason, }, apiKeys: { ...defaults.apiKeys, status: capabilities?.apiKeys?.status ?? defaults.apiKeys.status, ownership: capabilities?.apiKeys?.ownership ?? defaults.apiKeys.ownership, reason: capabilities?.apiKeys?.reason ?? defaults.apiKeys.reason, }, }; } function extractModelIds( models: (string | { id?: string; label?: string; description?: string })[] | undefined ): string[] { if (!models) { return []; } return models.flatMap((model) => { if (typeof model === 'string') { return [model]; } if (typeof model?.id === 'string' && model.id.trim().length > 0) { return [model.id.trim()]; } return []; }); } export class ClaudeMultimodelBridgeService { private async buildCliEnv( binaryPath: string ): Promise>> { return buildProviderAwareCliEnv({ binaryPath }); } private async buildProviderCliEnv( binaryPath: string, providerId: CliProviderId ): Promise>> { return buildProviderAwareCliEnv({ binaryPath, providerId }); } private isUnifiedRuntimeUnsupported(error: unknown): boolean { const message = error instanceof Error ? error.message : String(error); const lower = message.toLowerCase(); return ( lower.includes('unknown command') || lower.includes('unknown option') || lower.includes('no such command') || lower.includes('did you mean') || lower.includes('runtime status') ); } private mapRuntimeProviderStatus( providerId: CliProviderId, runtimeStatus: NonNullable[string] | undefined ): CliProviderStatus { const provider = createDefaultProviderStatus(providerId); if (!runtimeStatus) { return provider; } return { ...provider, supported: runtimeStatus.supported === true, authenticated: runtimeStatus.authenticated === true, authMethod: runtimeStatus.authMethod ?? null, verificationState: runtimeStatus.verificationState ?? 'unknown', statusMessage: runtimeStatus.statusMessage ?? null, canLoginFromUi: runtimeStatus.canLoginFromUi !== false, capabilities: { teamLaunch: runtimeStatus.capabilities?.teamLaunch === true, oneShot: runtimeStatus.capabilities?.oneShot === true, extensions: mapRuntimeExtensionCapabilities(runtimeStatus.capabilities?.extensions), }, selectedBackendId: runtimeStatus.selectedBackendId ?? null, resolvedBackendId: runtimeStatus.resolvedBackendId ?? null, availableBackends: runtimeStatus.availableBackends?.map((backend) => ({ id: backend.id ?? 'unknown', label: backend.label ?? backend.id ?? 'Unknown', description: backend.description ?? '', selectable: backend.selectable !== false, recommended: backend.recommended === true, available: backend.available === true, statusMessage: backend.statusMessage ?? null, detailMessage: backend.detailMessage ?? null, })) ?? [], externalRuntimeDiagnostics: runtimeStatus.externalRuntimeDiagnostics?.map((diagnostic) => ({ id: diagnostic.id ?? 'unknown', label: diagnostic.label ?? diagnostic.id ?? 'Unknown', detected: diagnostic.detected === true, statusMessage: diagnostic.statusMessage ?? null, detailMessage: diagnostic.detailMessage ?? null, })) ?? [], models: extractModelIds(runtimeStatus.models), backend: runtimeStatus.backend?.kind ? { kind: runtimeStatus.backend.kind, label: runtimeStatus.backend.label ?? runtimeStatus.backend.kind, endpointLabel: runtimeStatus.backend.endpointLabel ?? null, projectId: runtimeStatus.backend.projectId ?? null, authMethodDetail: runtimeStatus.backend.authMethodDetail ?? null, } : null, }; } private applyConnectionIssue( provider: CliProviderStatus, connectionIssues: Partial> ): CliProviderStatus { const issue = connectionIssues[provider.providerId]; if (!issue) { return provider; } return { ...provider, authenticated: false, authMethod: null, verificationState: 'error', statusMessage: issue, backend: null, }; } private applyConnectionIssues( providers: CliProviderStatus[], connectionIssues: Partial> ): CliProviderStatus[] { return providers.map((provider) => this.applyConnectionIssue(provider, connectionIssues)); } async getProviderStatus( binaryPath: string, providerId: CliProviderId ): Promise { await resolveInteractiveShellEnv(); const { env, connectionIssues } = await this.buildCliEnv(binaryPath); try { const { stdout } = await execCli( binaryPath, ['runtime', 'status', '--json', '--provider', providerId], { timeout: PROVIDER_STATUS_TIMEOUT_MS, env, } ); const parsed = extractJsonObject(stdout); return providerConnectionService.enrichProviderStatus( this.applyConnectionIssue( this.mapRuntimeProviderStatus(providerId, parsed.providers?.[providerId]), connectionIssues ) ); } catch (error) { if (!this.isUnifiedRuntimeUnsupported(error)) { logger.warn( `Provider-scoped runtime status unavailable for ${providerId}, falling back to full probe: ${ error instanceof Error ? error.message : String(error) }` ); } } const providers = await this.getProviderStatuses(binaryPath); return ( providers.find((provider) => provider.providerId === providerId) ?? createDefaultProviderStatus(providerId) ); } private async buildGeminiStatus(binaryPath: string): Promise { const provider = createDefaultProviderStatus('gemini'); const { env } = await this.buildProviderCliEnv(binaryPath, 'gemini'); try { const { stdout } = await execCli( binaryPath, ['model', 'list', '--json', '--provider', 'all'], { timeout: PROVIDER_MODELS_TIMEOUT_MS, env, } ); const parsed = extractJsonObject(stdout); const models = extractModelIds(parsed.providers?.gemini?.models); if (models.length > 0) { provider.supported = true; provider.models = models; provider.capabilities = { teamLaunch: true, oneShot: true, extensions: createDefaultCliExtensionCapabilities(), }; } } catch (error) { logger.warn( `Gemini model list unavailable: ${error instanceof Error ? error.message : String(error)}` ); } const authState = await resolveGeminiRuntimeAuth(env); if (authState.authenticated) { provider.authenticated = true; provider.authMethod = authState.authMethod === 'adc_authorized_user' || authState.authMethod === 'adc_service_account' ? `gemini_${authState.authMethod}` : authState.authMethod; provider.verificationState = 'verified'; provider.statusMessage = null; if (authState.authMethod === 'cli_oauth_personal') { provider.backend = { kind: 'cli', label: 'Gemini CLI', endpointLabel: 'Code Assist (cloudcode-pa.googleapis.com/v1internal)', projectId: authState.projectId, authMethodDetail: authState.authMethod, }; } return provider; } provider.statusMessage = authState.statusMessage ?? 'Set GEMINI_API_KEY or Google ADC to use Gemini.'; return provider; } async getProviderStatuses( binaryPath: string, onUpdate?: (providers: CliProviderStatus[]) => void ): Promise { await resolveInteractiveShellEnv(); const { env, connectionIssues } = await this.buildCliEnv(binaryPath); try { const { stdout } = await execCli(binaryPath, ['runtime', 'status', '--json'], { timeout: PROVIDER_STATUS_TIMEOUT_MS, env, }); const parsed = extractJsonObject(stdout); const providers = await providerConnectionService.enrichProviderStatuses( this.applyConnectionIssues( ORDERED_PROVIDER_IDS.map((providerId) => this.mapRuntimeProviderStatus(providerId, parsed.providers?.[providerId]) ), connectionIssues ) ); onUpdate?.(providers); return providers; } catch (error) { if (!this.isUnifiedRuntimeUnsupported(error)) { logger.warn( `Unified runtime status unavailable, falling back to legacy probes: ${ error instanceof Error ? error.message : String(error) }` ); } } const [statusResult, modelsResult] = await Promise.allSettled([ execCli(binaryPath, ['auth', 'status', '--json', '--provider', 'all'], { timeout: PROVIDER_STATUS_TIMEOUT_MS, env, }), execCli(binaryPath, ['model', 'list', '--json', '--provider', 'all'], { timeout: PROVIDER_MODELS_TIMEOUT_MS, env, }), ]); const providers = new Map( ORDERED_PROVIDER_IDS.map((providerId) => [ providerId, createDefaultProviderStatus(providerId), ]) ); if (statusResult.status === 'fulfilled') { try { const parsed = extractJsonObject(statusResult.value.stdout); for (const providerId of ORDERED_PROVIDER_IDS.filter((id) => id !== 'gemini')) { const runtimeStatus = parsed.providers?.[providerId]; if (!runtimeStatus) continue; providers.set(providerId, { ...providers.get(providerId)!, supported: runtimeStatus.supported === true, authenticated: runtimeStatus.authenticated === true, authMethod: runtimeStatus.authMethod ?? null, verificationState: runtimeStatus.verificationState ?? 'unknown', statusMessage: runtimeStatus.statusMessage ?? null, canLoginFromUi: runtimeStatus.canLoginFromUi !== false, capabilities: { teamLaunch: runtimeStatus.capabilities?.teamLaunch === true, oneShot: runtimeStatus.capabilities?.oneShot === true, extensions: mapRuntimeExtensionCapabilities(runtimeStatus.capabilities?.extensions), }, backend: runtimeStatus.backend?.kind ? { kind: runtimeStatus.backend.kind, label: runtimeStatus.backend.label ?? runtimeStatus.backend.kind, endpointLabel: runtimeStatus.backend.endpointLabel ?? null, projectId: runtimeStatus.backend.projectId ?? null, authMethodDetail: runtimeStatus.backend.authMethodDetail ?? null, } : null, }); onUpdate?.(ORDERED_PROVIDER_IDS.map((id) => providers.get(id)!)); } } catch (error) { logger.warn( `Failed to parse provider auth status JSON: ${ error instanceof Error ? error.message : String(error) }` ); } } else { const message = statusResult.reason instanceof Error ? statusResult.reason.message : String(statusResult.reason); logger.warn(`Provider auth status unavailable: ${message}`); for (const providerId of ORDERED_PROVIDER_IDS) { providers.set(providerId, { ...providers.get(providerId)!, statusMessage: 'Provider status not supported by current claude-multimodel build', }); onUpdate?.(ORDERED_PROVIDER_IDS.map((id) => providers.get(id)!)); } } if (modelsResult.status === 'fulfilled') { try { const parsed = extractJsonObject(modelsResult.value.stdout); for (const providerId of ORDERED_PROVIDER_IDS.filter((id) => id !== 'gemini')) { const runtimeModels = extractModelIds(parsed.providers?.[providerId]?.models); if (runtimeModels.length === 0) continue; providers.set(providerId, { ...providers.get(providerId)!, models: runtimeModels, }); onUpdate?.(ORDERED_PROVIDER_IDS.map((id) => providers.get(id)!)); } } catch (error) { logger.warn( `Failed to parse provider models JSON: ${ error instanceof Error ? error.message : String(error) }` ); } } providers.set('gemini', await this.buildGeminiStatus(binaryPath)); onUpdate?.(ORDERED_PROVIDER_IDS.map((id) => providers.get(id)!)); const enrichedProviders = await providerConnectionService.enrichProviderStatuses( this.applyConnectionIssues( ORDERED_PROVIDER_IDS.map((providerId) => providers.get(providerId)!), connectionIssues ) ); onUpdate?.(enrichedProviders); return enrichedProviders; } }