import type { ProviderAccount, ProviderProtocol } from '../shared/providers/types'; import { getProviderBackendConfig, getProviderDefaultModel, getProviderDefinition, } from '../shared/providers/registry'; import { getProviderService } from '../services/providers/provider-service'; import { getApiKey } from '../utils/secure-storage'; import { resolveOpencodeProviderId, sanitizeOpencodeProviderId, } from '../../shared/opencode-provider-id'; import { NIANCODE_USER_MODEL_ACCOUNT_ID, normalizeImportedUserModelId, selectUserModelRuntimeAccounts, } from '../../shared/user-model-config'; import { getImportedModelProfile } from '../../shared/imported-model-profile'; const OPENCODE_CONFIG_SCHEMA = 'https://opencode.ai/config.json'; const OPENCODE_ENV_PREFIX = 'NIANCODE_OPENCODE'; const OPENAI_COMPATIBLE_PACKAGE = '@ai-sdk/openai-compatible'; const OPENAI_PACKAGE = '@ai-sdk/openai'; const ANTHROPIC_PACKAGE = '@ai-sdk/anthropic'; const WORKS_SQUARE_AI_GATEWAY_CREDENTIAL_MODE = 'works_square_ai_gateway'; const OPENCODE_BUILTIN_PROVIDER_IDS = new Set([ 'anthropic', 'openai', 'google', 'openrouter', 'moonshot', 'deepseek', ]); export interface OpencodeProviderEntry { npm?: string; name?: string; options?: Record; models?: Record; } export interface OpencodeMcpServerEntry { type: 'local'; command: string[]; enabled: boolean; env?: Record; } type OpencodeModelModality = 'text' | 'audio' | 'image' | 'pdf'; export interface OpencodeModelLimit { context: number; output: number; } export interface OpencodeProviderModelEntry { name: string; modalities?: { input: OpencodeModelModality[]; output: OpencodeModelModality[]; }; limit?: OpencodeModelLimit; } export interface OpencodeRuntimeConfig { '$schema': string; enabled_providers: string[]; permission: { external_directory: 'ask'; }; provider: Record; mcp?: Record; model?: string; small_model?: string; } export interface OpencodeRuntimeConfigResult { config: OpencodeRuntimeConfig; env: Record; } export interface OpencodeRuntimeConfigSummary { model: string | null; smallModel: string | null; providerIds: string[]; enabledProviderIds: string[]; providerCount: number; providers: OpencodeProviderSummary[]; mcpServerIds: string[]; mcpServerCount: number; mcpServers: OpencodeMcpServerSummary[]; } export interface OpencodeProviderSummary { id: string; name?: string; npm?: string; baseURL?: string; modelIds: string[]; imageInputModelIds?: string[]; modelLimits?: Record; hasApiKey: boolean; apiKeyEnv?: string; headerNames: string[]; } export interface OpencodeMcpServerSummary { id: string; type: string; enabled: boolean; command: string[]; envKeys: string[]; } export type ResolveOpencodeProviderApiKey = ( account: ProviderAccount, opencodeProviderId: string, ) => Promise; export interface BuildOpencodeRuntimeConfigOptions { accounts: ProviderAccount[]; defaultAccountId?: string | null; resolveApiKey: ResolveOpencodeProviderApiKey; mcpServers?: Record; } function envVarNameForAccount(accountId: string): string { const suffix = sanitizeOpencodeProviderId(accountId) .replace(/-/g, '_') .toUpperCase(); return `${OPENCODE_ENV_PREFIX}_${suffix}_API_KEY`; } function normalizeBaseUrl(baseUrl: string | undefined): string | undefined { const trimmed = baseUrl?.trim().replace(/\/+$/, ''); return trimmed || undefined; } function normalizeRuntimeBaseUrlForAccount( account: ProviderAccount, baseUrl: string | undefined, ): string | undefined { if (!baseUrl || account.metadata?.worksSquareCredentialMode !== WORKS_SQUARE_AI_GATEWAY_CREDENTIAL_MODE) { return baseUrl; } try { const parsed = new URL(baseUrl); if (parsed.pathname === '' || parsed.pathname === '/') { parsed.pathname = '/v1'; return parsed.toString().replace(/\/+$/, ''); } } catch { return baseUrl; } return baseUrl; } function headersForAccount( account: ProviderAccount, apiKeyEnvVarName: string | undefined, ): Record | undefined { const headers = { ...(account.headers ?? {}) }; if ( apiKeyEnvVarName && account.metadata?.worksSquareCredentialMode === WORKS_SQUARE_AI_GATEWAY_CREDENTIAL_MODE && !Object.keys(headers).some((key) => key.toLowerCase() === 'authorization') ) { headers.Authorization = `Bearer {env:${apiKeyEnvVarName}}`; } return Object.keys(headers).length > 0 ? headers : undefined; } function protocolForAccount(account: ProviderAccount): ProviderProtocol | undefined { return account.apiProtocol ?? getProviderBackendConfig(account.vendorId)?.api; } function packageForAccount(account: ProviderAccount): string | undefined { const hasExplicitRuntimeShape = Boolean(account.baseUrl || account.apiProtocol); if ( !hasExplicitRuntimeShape && OPENCODE_BUILTIN_PROVIDER_IDS.has(account.vendorId) ) { return undefined; } const protocol = protocolForAccount(account); if (protocol === 'openai-responses') { return OPENAI_PACKAGE; } if (protocol === 'anthropic-messages') { return ANTHROPIC_PACKAGE; } return OPENAI_COMPATIBLE_PACKAGE; } function baseUrlForAccount(account: ProviderAccount): string | undefined { return normalizeRuntimeBaseUrlForAccount(account, normalizeBaseUrl( account.baseUrl ?? getProviderBackendConfig(account.vendorId)?.baseUrl ?? getProviderDefinition(account.vendorId)?.defaultBaseUrl, )); } function normalizeModelIdForAccount( rawModel: string | undefined, opencodeProviderId: string, account: ProviderAccount, ): string | undefined { const trimmed = rawModel?.trim(); if (!trimmed) { return undefined; } const providerPrefix = `${opencodeProviderId}/`; const modelId = trimmed.startsWith(providerPrefix) ? trimmed.slice(providerPrefix.length) : trimmed; return account.id === NIANCODE_USER_MODEL_ACCOUNT_ID ? normalizeImportedUserModelId(modelId) : modelId; } function modelIdsForAccount(account: ProviderAccount, opencodeProviderId: string): string[] { const modelIds: string[] = []; const seen = new Set(); const rawModels = [ account.model ?? getProviderDefaultModel(account.vendorId), ...(account.fallbackModels ?? []), ]; for (const rawModel of rawModels) { const modelId = normalizeModelIdForAccount(rawModel, opencodeProviderId, account); if (!modelId || seen.has(modelId)) { continue; } seen.add(modelId); modelIds.push(modelId); } return modelIds; } function modelEntryForAccount(account: ProviderAccount, modelId: string): OpencodeProviderModelEntry { const entry: OpencodeProviderModelEntry = { name: modelId }; if (account.id !== NIANCODE_USER_MODEL_ACCOUNT_ID) return entry; const profile = getImportedModelProfile(modelId); if (profile?.modalities) { entry.modalities = { input: [...profile.modalities.input], output: [...profile.modalities.output], }; } if (profile?.limit) { entry.limit = { ...profile.limit }; } return entry; } function modelEntryHasImageInput(model: OpencodeProviderModelEntry | undefined): boolean { return model?.modalities?.input.includes('image') ?? false; } function accountNeedsApiKey(account: ProviderAccount): boolean { return account.authMode !== 'local' && account.vendorId !== 'ollama'; } function sortAccountsForSelection( accounts: ProviderAccount[], defaultAccountId?: string | null, ): ProviderAccount[] { return [...accounts].sort((left, right) => { if (left.id === defaultAccountId) return -1; if (right.id === defaultAccountId) return 1; return 0; }); } export async function buildOpencodeRuntimeConfig( options: BuildOpencodeRuntimeConfigOptions, ): Promise { const config: OpencodeRuntimeConfig = { '$schema': OPENCODE_CONFIG_SCHEMA, enabled_providers: [], permission: { external_directory: 'ask', }, provider: {}, }; if (options.mcpServers && Object.keys(options.mcpServers).length > 0) { config.mcp = options.mcpServers; } const env: Record = {}; let selectedModel: string | undefined; for (const account of sortAccountsForSelection( selectUserModelRuntimeAccounts(options.accounts), options.defaultAccountId, )) { if (!account.enabled) { continue; } const opencodeProviderId = resolveOpencodeProviderId(account); if (config.provider[opencodeProviderId]) { continue; } const apiKey = await options.resolveApiKey(account, opencodeProviderId); if (!apiKey && accountNeedsApiKey(account)) { continue; } const providerEntry: OpencodeProviderEntry = {}; const providerPackage = packageForAccount(account); if (providerPackage) { providerEntry.npm = providerPackage; providerEntry.name = account.label; } const entryOptions: Record = {}; const baseURL = providerPackage ? baseUrlForAccount(account) : normalizeBaseUrl(account.baseUrl); if (baseURL) { entryOptions.baseURL = baseURL; } let envVarName: string | undefined; if (apiKey) { envVarName = envVarNameForAccount(account.id); env[envVarName] = apiKey; entryOptions.apiKey = `{env:${envVarName}}`; } const headers = headersForAccount(account, envVarName); if (headers) { entryOptions.headers = headers; } if (Object.keys(entryOptions).length > 0) { providerEntry.options = entryOptions; } const modelIds = modelIdsForAccount(account, opencodeProviderId); if (modelIds.length > 0) { providerEntry.models = Object.fromEntries( modelIds.map((modelId) => [modelId, modelEntryForAccount(account, modelId)]), ); } const primaryModelId = modelIds[0]; config.provider[opencodeProviderId] = providerEntry; const modelRef = primaryModelId ? `${opencodeProviderId}/${primaryModelId}` : undefined; if (modelRef && (account.id === options.defaultAccountId || !selectedModel)) { selectedModel = modelRef; } } if (selectedModel) { config.model = selectedModel; } config.enabled_providers = Object.keys(config.provider); return { config, env }; } async function resolveNianCodeApiKey( account: ProviderAccount, _opencodeProviderId: string, ): Promise { return await getApiKey(account.id); } export interface BuildOpencodeRuntimeConfigFromNianCodeProvidersOptions { mcpServers?: Record; } export async function buildOpencodeRuntimeConfigFromNianCodeProviders( options: BuildOpencodeRuntimeConfigFromNianCodeProvidersOptions = {}, ): Promise { const providerService = getProviderService(); const [accounts, defaultAccountId] = await Promise.all([ providerService.listAccounts(), providerService.getDefaultAccountId(), ]); return buildOpencodeRuntimeConfig({ accounts, defaultAccountId, resolveApiKey: resolveNianCodeApiKey, mcpServers: options.mcpServers, }); } export function summarizeOpencodeRuntimeConfig( result: OpencodeRuntimeConfigResult, ): OpencodeRuntimeConfigSummary { const providerIds = Object.keys(result.config.provider); const mcp = result.config.mcp ?? {}; const mcpServerIds = Object.keys(mcp); const mcpServers = mcpServerIds.map((serverId) => { const server = mcp[serverId]; return { id: serverId, type: server.type, enabled: server.enabled, command: [...server.command], envKeys: Object.keys(server.env ?? {}).sort(), }; }); const providers = providerIds.map((providerId) => { const provider = result.config.provider[providerId]; const options = provider?.options ?? {}; const rawApiKey = typeof options.apiKey === 'string' ? options.apiKey : undefined; const apiKeyEnv = rawApiKey?.match(/^\{env:([^}]+)\}$/)?.[1]; const headers = options.headers && typeof options.headers === 'object' && !Array.isArray(options.headers) ? options.headers as Record : {}; const models = provider?.models ?? {}; const modelLimits: Record = Object.fromEntries( Object.entries(models).flatMap(([modelId, model]) => ( model.limit ? [[modelId, { ...model.limit }] as const] : [] )), ); return { id: providerId, ...(provider?.name ? { name: provider.name } : {}), ...(provider?.npm ? { npm: provider.npm } : {}), ...(typeof options.baseURL === 'string' ? { baseURL: options.baseURL } : {}), modelIds: Object.keys(models), imageInputModelIds: Object.entries(models).flatMap(([modelId, model]) => ( modelEntryHasImageInput(model) ? [modelId] : [] )), ...(Object.keys(modelLimits).length > 0 ? { modelLimits } : {}), hasApiKey: Boolean(rawApiKey), ...(apiKeyEnv ? { apiKeyEnv } : {}), headerNames: Object.keys(headers), }; }); return { model: result.config.model ?? null, smallModel: result.config.small_model ?? null, providerIds, enabledProviderIds: result.config.enabled_providers ?? providerIds, providerCount: providerIds.length, providers, mcpServerIds, mcpServerCount: mcpServerIds.length, mcpServers, }; } export async function buildOpencodeRuntimeConfigSummaryFromNianCodeProviders( options: BuildOpencodeRuntimeConfigFromNianCodeProvidersOptions = {}, ): Promise { return summarizeOpencodeRuntimeConfig(await buildOpencodeRuntimeConfigFromNianCodeProviders(options)); }