Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/ankurmay1988_opencode-provider-bridge.json
T

1 line
33 KiB
JSON

{"repo": "ankurmay1988/opencode-provider-bridge", "files": {"src/opencodeConfig.ts": {"e018cb52441485b1eb5ebeb47f11b494c637c81c": {"t": 1785596380.6951513, "n": 12447, "h": "269f9bce0f8987a2", "c": "// =============================================================================\n// opencodeConfig.ts — Provider & Model Discovery\n// =============================================================================\n//\n// DISCOVERY TIERS (tried in order until one succeeds):\n//\n// TIER 1 — SDK (preferred)\n// Connects to a running opencode server via @opencode-ai/sdk.\n// Returns live provider configs with API keys, all model names,\n// capabilities (toolcall, vision, reasoning), context limits, etc.\n// Requires: opencode CLI/desktop running on :4096.\n//\n// TIER 2 — models.dev + auth.json (fallback)\n// Fetches https://models.dev/api.json (public provider/model catalog)\n// and intersects it with the user's auth.json credentials.\n// Returns model names + capabilities from the catalog for any\n// provider the user has credentials for (nvidia, vultr, etc.).\n// Requires: internet access for the catalog fetch.\n//\n// TIER 3 — auth.json only (bare fallback)\n// Creates a single placeholder model per provider with no metadata.\n// The provider shows up in VS Code's picker but the user won't\n// see model names, context limits, or capabilities.\n// Requires: nothing — works fully offline.\n//\n// WHY NOT JUST models.dev?\n// models.dev alone gives model metadata but no API keys (those are\n// in auth.json). models.dev + auth.json together work as well as\n// the SDK — we just lose live config (custom baseURLs, overrides).\n// We removed models.dev earlier but added it back because without\n// it Tier 3 shows only \"default\" model names — useless to the user.\n// =============================================================================\n\nimport * as fs from 'fs';\nimport * as os from 'os';\nimport * as path from 'path';\n\nimport type { Model, Provider } from '@opencode-ai/sdk';\n\nimport { createOpencodeClient } from '@opencode-ai/sdk';\nimport { log as logger } from './logger.js';\n\nconst PKG_NAME = 'opencode-provider-bridge';\n\n// ---------------------------------------------------------------------------\n// PUBLIC TYPES\n// ---------------------------------------------------------------------------\n\nexport interface ProviderCredential {\n type: 'api' | 'oauth';\n key?: string;\n access?: string;\n}\n\nexport interface OpencodeAuth {\n [providerId: string]: ProviderCredential;\n}\n\nexport interface ModelsDevModel {\n id: string;\n name: string;\n family: string;\n /** Exact API base URL from opencode's model registry (SDK Model.api.url). */\n apiUrl?: string;\n /** npm package from opencode's model registry (e.g. @ai-sdk/openai-compatible). */\n apiNpm?: string;\n tool_call?: boolean;\n reasoning?: boolean;\n attachment?: boolean;\n modalities?: { input: string[]; output: string[] };\n limit?: { context?: number; output?: number };\n}\n\nexport interface ModelsDevProvider {\n id: string;\n name: string;\n api?: string;\n env?: string[];\n npm?: string;\n models: Record<string, ModelsDevModel>;\n}\n\nexport interface ModelsDevCatalog {\n [providerId: string]: ModelsDevProvider;\n}\n\nexport type ProviderEntry = {\n provider: ModelsDevProvider;\n credential: ProviderCredential;\n models: [string, ModelsDevModel][];\n};\n\n// ---------------------------------------------------------------------------\n// CONSTANTS\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_SDK_PORT = 4096;\nconst MODELS_DEV_URL = 'https://models.dev/api.json';\n\nconst AUTH_FILE_PATHS = [\n path.join(os.homedir(), '.local', 'share', 'opencode', 'auth.json'),\n path.join(os.homedir(), '.opencode', 'auth.json'),\n path.join(os.homedir(), '.config', 'opencode', 'auth.json'),\n];\n\n/**\n * Hardcoded entries for opencode-managed providers (Zen, Go).\n * These may not appear in the public models.dev catalog so we keep\n * them here as a last-resort fallback.\n */\nconst KNOWN_PROVIDERS: Record<string, { name: string; api: string }> = {\n opencode: { name: 'OpenCode Zen', api: 'https://opencode.ai/zen/v1' },\n 'opencode-go': { name: 'OpenCode Go', api: 'https://opencode.ai/go/v1' },\n};\n\n// ---------------------------------------------------------------------------\n// SDK-TO-LOCAL TYPE MAPPERS\n// ---------------------------------------------------------------------------\n\nfunction sdkModelToDevModel(model: Model): ModelsDevModel {\n return {\n id: model.id,\n name: model.name,\n family: model.providerID,\n apiUrl: model.api?.url, // exact endpoint from opencode's registry\n apiNpm: model.api?.npm, // exact npm package from opencode's registry\n tool_call: model.capabilities.toolcall,\n reasoning: model.capabilities.reasoning,\n attachment: model.capabilities.attachment,\n modalities: {\n input: Object.entries(model.capabilities.input)\n .filter(([, v]) => v).map(([k]) => k),\n output: Object.entries(model.capabilities.output)\n .filter(([, v]) => v).map(([k]) => k),\n },\n limit: {\n context: model.limit.context,\n output: model.limit.output,\n },\n };\n}\n\nfunction sdkProviderToEntry(sp: Provider): ProviderEntry | null {\n const models: [string, ModelsDevModel][] = [];\n\n for (const [rawId, model] of Object.entries(sp.models)) {\n models.push([rawId, sdkModelToDevModel(model)]);\n }\n\n if (models.length === 0) {return null;}\n\n const baseURL = sp.options?.baseURL as string | undefined;\n const apiURL = sp.options?.api as string | undefined;\n\n return {\n provider: {\n id: sp.id,\n name: sp.name,\n api: baseURL ?? apiURL,\n env: sp.env,\n models: Object.fromEntries(models),\n },\n credential: {\n type: 'api',\n key: sp.key,\n },\n models,\n };\n}\n\n// ---------------------------------------------------------------------------\n// TIER 1: SDK-BASED DISCOVERY\n// ---------------------------------------------------------------------------\n\n/**\n * Try SDK discovery on a given port.\n * @param port — if set, connects to localhost:{port}; if omitted, uses default 4096\n * @param logPrefix — optional label for log lines (e.g. \"retry\")\n */\nexport async function trySdkProviders(port?: number, logPrefix?: string): Promise<Map<string, ProviderEntry> | null> {\n const tag = logPrefix ?? 'TIER 1';\n const log = (msg: string) => logger(` ${msg}`);\n const url = `http://localhost:${port ?? DEFAULT_SDK_PORT}`;\n logger(`${tag}: SDK discovery → ${url}`);\n\n try {\n const client = createOpencodeClient({ baseUrl: url });\n logger(`${tag}: client created, calling config.providers()...`);\n\n const result = await client.config.providers() as { data?: { providers: Provider[] } };\n const providers = result?.data?.providers;\n logger(`${tag}: returned ${providers?.length ?? 0} provider(s)`);\n\n if (!providers?.length) {\n logger(`${tag}: no providers returned.`);\n return null;\n }\n\n for (const sp of providers) {\n const modelCount = Object.keys(sp.models).length;\n logger(`${tag}: [${sp.id}] \"${sp.name}\" source=${sp.source} hasKey=${!!sp.key} models=${modelCount}`);\n }\n\n const configured = new Map<string, ProviderEntry>();\n for (const sp of providers) {\n const entry = sdkProviderToEntry(sp);\n if (!entry) {\n logger(`${tag}: Skipping \"${sp.id}\": 0 models`);\n continue;\n }\n // If the SDK didn't return an API endpoint, check known providers\n if (!entry.provider.api) {\n const known = KNOWN_PROVIDERS[sp.id];\n if (known) {entry.provider.api = known.api;}\n }\n configured.set(sp.id, entry);\n const keyStatus = sp.key ? 'keyed' : 'no key';\n logger(`${tag}: Included \"${sp.id}\": ${entry.models.length} models (${keyStatus})`);\n }\n\n logger(`${tag}: done — ${configured.size} provider(s)`);\n return configured.size > 0 ? configured : null;\n } catch (err) {\n logger(`${tag} failed: ${(err as Error).message}`);\n return null;\n }\n}\n\n// ---------------------------------------------------------------------------\n// TIER 2: MODELS.DEV + AUTH.JSON\n// ---------------------------------------------------------------------------\n\nexport function readOpencodeAuth(): OpencodeAuth {\n for (const p of AUTH_FILE_PATHS) {\n try {\n const raw = fs.readFileSync(p, 'utf-8');\n return JSON.parse(raw);\n } catch { continue; }\n }\n return {};\n}\n\n/**\n * Fetch the public models.dev catalog.\n * Non-blocking — if the fetch fails we return {} and move to Tier 3.\n */\nasync function fetchModelsCatalogger(): Promise<ModelsDevCatalog> {\n logger(` TIER 2: fetching models.dev catalog...`);\n try {\n const resp = await fetch(MODELS_DEV_URL);\n if (!resp.ok) {\n logger(` models.dev returned HTTP ${resp.status}`);\n return {};\n }\n const catalog = await resp.json() as ModelsDevCatalog;\n const count = Object.keys(catalog).length;\n logger(` models.dev loaded: ${count} provider(s) in catalog`);\n return catalog;\n } catch (err) {\n logger(` models.dev fetch failed: ${(err as Error).message}`);\n return {};\n }\n}\n\n/**\n * Intersect auth.json credentials with the models.dev catalog.\n * Every provider with a credential AND a catalog entry gets included\n * with its full model list (names, capabilities, limits).\n */\nfunction filterModelsForProviders(\n catalog: ModelsDevCatalog,\n auth: OpencodeAuth,\n): Map<string, ProviderEntry> | null {\n const log = (msg: string) => logger(` ${msg}`);\n const result = new Map<string, ProviderEntry>();\n\n for (const [providerId, credential] of Object.entries(auth)) {\n const providerInfo = catalog[providerId];\n if (!providerInfo) {\n logger(` \"${providerId}\" NOT in models.dev catalog — skipping`);\n continue;\n }\n\n const supportedModels = Object.entries(providerInfo.models);\n if (supportedModels.length === 0) {\n logger(` \"${providerId}\" has 0 models in catalog — skipping`);\n continue;\n }\n\n result.set(providerId, {\n provider: providerInfo,\n credential,\n models: supportedModels,\n });\n\n logger(` Included \"${providerId}\": ${supportedModels.length} models from catalog`);\n }\n\n return result.size > 0 ? result : null;\n}\n\n// ---------------------------------------------------------------------------\n// TIER 3: AUTH.JSON ONLY (BARE FALLBACK)\n// ---------------------------------------------------------------------------\n\n/**\n * Every provider in auth.json gets a single placeholder model.\n * No model metadata — the user sees the provider name but all models\n * appear as \"default\". This is the last resort when SDK and models.dev\n * are both unavailable.\n */\nfunction makeBareFallback(auth: OpencodeAuth): Map<string, ProviderEntry> | null {\n const log = (msg: string) => logger(` ${msg}`);\n const result = new Map<string, ProviderEntry>();\n\n for (const [providerId, credential] of Object.entries(auth)) {\n // For known providers (Zen, Go) use hardcoded name + API URL\n const known = KNOWN_PROVIDERS[providerId];\n const name = known?.name ?? providerId;\n const api = known?.api ?? undefined;\n\n result.set(providerId, {\n provider: { id: providerId, name, api, models: {} },\n credential,\n models: [['default', {\n id: `${providerId}/default`,\n name,\n family: providerId,\n tool_call: true,\n }]],\n });\n\n logger(` Included \"${providerId}\" (${name}) — bare fallback, 1 placeholder model`);\n }\n\n return result.size > 0 ? result : null;\n}\n\n// ---------------------------------------------------------------------------\n// PUBLIC ENTRY POINT\n// ---------------------------------------------------------------------------\n\n/**\n * Fallback discovery (Tiers 2 + 3) — used when the SDK is unavailable.\n * Does NOT try the SDK path; that's handled by extension.ts with server mgmt.\n */\nexport async function fallbackProviders(): Promise<Map<string, ProviderEntry>> {\n const [auth, catalog] = await Promise.all([\n Promise.resolve().then(() => readOpencodeAuth()),\n fetchModelsCatalogger(),\n ]);\n const ids = Object.keys(auth);\n logger(` auth.json: ${ids.length} provider(s) - ${ids.join(', ') || '(none)'}`);\n\n // Tier 2: models.dev + auth.json\n const catalogResult = filterModelsForProviders(catalog, auth);\n if (catalogResult) {return catalogResult;}\n\n // Tier 3: bare fallback\n logger(` TIER 3: bare fallback - no catalog available`);\n const bareResult = makeBareFallback(auth);\n if (bareResult) {return bareResult;}\n\n logger(` No providers found.`);\n return new Map();\n}\n"}}, "src/provider.ts": {"e018cb52441485b1eb5ebeb47f11b494c637c81c": {"t": 1785596398.2057233, "n": 20170, "h": "05d827f666fdfbb1", "c": "import * as vscode from 'vscode';\n\nimport type { ModelMessage, ToolSet } from 'ai';\nimport { ModelsDevModel, ModelsDevProvider } from './opencodeConfig.js';\nimport { extractTextFromToolResult, simplifySchema } from './providerUtils.js';\nimport { jsonSchema, streamText, tool } from 'ai';\n\nimport { createAnthropic } from '@ai-sdk/anthropic';\nimport { createGoogleGenerativeAI } from '@ai-sdk/google';\nimport { createOpenAICompatible } from '@ai-sdk/openai-compatible';\nimport { createVerboseFetch } from './verboseFetch.js';\nimport { log } from './logger.js';\n\nconst VERBOSE_FETCH = createVerboseFetch(globalThis.fetch);\n\nconst REASONING_CONTENT_MIME = 'application/vnd.opencode-bridge.reasoning';\n\nexport class OpencodeModelProvider implements vscode.LanguageModelChatProvider {\n private readonly onDidChangeEmitter = new vscode.EventEmitter<void>();\n readonly onDidChangeLanguageModelChatInformation = this.onDidChangeEmitter.event;\n\n lastUsage: { prompt: number; completion: number } | null = null;\n\n /** OpenAI-compatible provider instance (for models returning OpenAI SSE). */\n private openaiProvider: ReturnType<typeof createOpenAICompatible> | null = null;\n\n /** Anthropic provider instance (for models returning Anthropic SSE, e.g. Qwen). */\n private anthropicProvider: ReturnType<typeof createAnthropic> | null = null;\n\n /** Google Generative AI provider instance (for Gemini models). */\n private googleProvider: ReturnType<typeof createGoogleGenerativeAI> | null = null;\n\n /** Cache of simplified tool input schemas keyed by tool name. */\n private toolSchemaCache = new Map<string, Record<string, unknown>>();\n\n /**\n * Cross-turn cache mapping toolCallId → toolName.\n * Populated when processing assistant tool-call parts so that when a\n * subsequent tool-result part arrives (which lacks a name property in\n * VS Code's LanguageModelToolResultPart), we can still report the\n * correct tool name to the AI SDK.\n */\n private toolCallNameCache = new Map<string, string>();\n\n constructor(\n readonly providerInfo: ModelsDevProvider,\n private apiKey: string,\n private enabledModels: Map<string, ModelsDevModel>,\n ) {}\n\n get hasApiKey(): boolean {\n return this.apiKey.length > 0;\n }\n\n setApiKey(key: string): void {\n this.apiKey = key;\n this.openaiProvider = null;\n this.anthropicProvider = null;\n this.googleProvider = null;\n }\n\n fireChange(): void {\n this.onDidChangeEmitter.fire();\n }\n\n /** Base URL for this provider's API. */\n private getBaseUrl(): string {\n const knownApis: Record<string, string> = {\n opencode: 'https://opencode.ai/zen/v1',\n 'opencode-go': 'https://opencode.ai/go/v1',\n };\n return this.providerInfo.api || knownApis[this.providerInfo.id] || `https://api.${this.providerInfo.id}.com/v1`;\n }\n\n /**\n * Returns the correct LanguageModelV3 for a given model, routing to\n * the appropriate AI SDK based on the model's `apiNpm` metadata from\n * the opencode provider registry.\n *\n * The opencode SDK returns per-model metadata that tells us exactly\n * which AI SDK package to use (e.g. `@ai-sdk/openai-compatible`,\n * `@ai-sdk/anthropic`, `@ai-sdk/google`). This is the authoritative\n * source of truth:\n *\n * @ai-sdk/openai-compatible → sends OpenAI-format requests\n * to /v1/chat/completions\n * @ai-sdk/anthropic → sends Anthropic-format requests\n * to /v1/messages\n * @ai-sdk/google → sends Google Generative AI requests\n * to /v1/models/{model}\n *\n * The per-model apiUrl (if set) is used as the base URL for the\n * provider (except for Google models, where the provider-level\n * URL is used since the SDK constructs the model path internally).\n * When it's not set (fallback tiers), `getBaseUrl()` is used instead.\n */\n private getLanguageModel(modelId: string) {\n const modelMeta = this.enabledModels.get(modelId);\n const apiUrl = modelMeta?.apiUrl ?? '';\n const apiNpm = modelMeta?.apiNpm ?? '';\n\n // ── Verbose routing diagnostics ────────────────────────────────\n log(`[opencode-provider-bridge] ROUTE model=\"${modelId}\" apiUrl=\"${apiUrl}\" apiNpm=\"${apiNpm}\" provider=\"${this.providerInfo.id}\"`, 'debug');\n log(`[opencode-provider-bridge] ROUTE modelMeta: ${JSON.stringify({\n id: modelMeta?.id,\n name: modelMeta?.name,\n family: modelMeta?.family,\n apiUrl: modelMeta?.apiUrl,\n apiNpm: modelMeta?.apiNpm,\n tool_call: modelMeta?.tool_call,\n reasoning: modelMeta?.reasoning,\n modalities: modelMeta?.modalities,\n limits: modelMeta?.limit,\n }, null, 2)}`, 'debug');\n\n // Use per-model apiUrl if available, otherwise fall back to provider-level URL\n const baseUrl = (apiUrl || this.getBaseUrl()).replace(/\\/$/, '');\n\n if (apiNpm === '@ai-sdk/google') {\n // Google SDK constructs URLs like:\n // {baseURL}/models/{modelId}:streamGenerateContent?alt=sse\n // The per-model apiUrl already includes the model-specific path\n // (e.g. /zen/v1/models/gemini-3.1-pro), so we use the provider-level\n // base URL and let the SDK append the model path.\n const googleBaseUrl = this.getBaseUrl().replace(/\\/$/, '');\n log(`[opencode-provider-bridge] ROUTE → Google SDK (@ ${googleBaseUrl})`, 'debug');\n if (!this.googleProvider) {\n this.googleProvider = createGoogleGenerativeAI({\n name: this.providerInfo.id,\n baseURL: googleBaseUrl,\n apiKey: this.apiKey,\n fetch: VERBOSE_FETCH,\n });\n }\n return this.googleProvider(modelId);\n }\n\n if (apiNpm === '@ai-sdk/anthropic' || apiUrl.includes('/messages')) {\n log(`[opencode-provider-bridge] ROUTE → Anthropic SDK (@ ${baseUrl})`, 'debug');\n if (!this.anthropicProvider) {\n this.anthropicProvider = createAnthropic({\n name: this.providerInfo.id,\n baseURL: baseUrl,\n authToken: this.apiKey,\n fetch: VERBOSE_FETCH,\n });\n }\n return this.anthropicProvider(modelId);\n }\n\n // Default: OpenAI-compatible (for @ai-sdk/openai-compatible or when apiNpm is unknown)\n log(`[opencode-provider-bridge] ROUTE → OpenAI-compatible SDK (@ ${baseUrl})`, 'debug');\n if (!this.openaiProvider) {\n this.openaiProvider = createOpenAICompatible({\n name: this.providerInfo.id,\n baseURL: baseUrl,\n apiKey: this.apiKey,\n fetch: VERBOSE_FETCH,\n });\n }\n return this.openaiProvider(modelId);\n }\n\n provideLanguageModelChatInformation(\n _options: vscode.PrepareLanguageModelChatModelOptions,\n _token: vscode.CancellationToken,\n ): vscode.ProviderResult<vscode.LanguageModelChatInformation[]> {\n const models: vscode.LanguageModelChatInformation[] = [];\n\n for (const [modelId, modelMeta] of this.enabledModels) {\n log(`[opencode-provider-bridge] REGISTER model=\"${modelId}\" name=\"${modelMeta.name ?? modelId}\" apiUrl=\"${modelMeta.apiUrl ?? '(none)'}\" apiNpm=\"${modelMeta.apiNpm ?? '(none)'}\" tool_call=${modelMeta.tool_call} reasoning=${modelMeta.reasoning} ctx=${modelMeta.limit?.context ?? '?'}`, 'debug');\n\n models.push({\n id: modelId,\n name: modelMeta.name || modelId,\n family: modelMeta.family || 'unknown',\n version: '1.0.0',\n maxInputTokens: modelMeta.limit?.context ?? 128000,\n maxOutputTokens: modelMeta.limit?.output ?? 4096,\n capabilities: {\n imageInput: !!(modelMeta.modalities?.input?.includes('image')),\n toolCalling: modelMeta.tool_call ?? false,\n },\n });\n }\n\n log(`[opencode-provider-bridge] Registered ${models.length} models for provider \"${this.providerInfo.name}\"`, 'info');\n return models;\n }\n\n async provideLanguageModelChatResponse(\n model: vscode.LanguageModelChatInformation,\n messages: readonly vscode.LanguageModelChatRequestMessage[],\n options: vscode.ProvideLanguageModelChatResponseOptions,\n progress: vscode.Progress<vscode.LanguageModelResponsePart>,\n token: vscode.CancellationToken,\n ): Promise<void> {\n let currentReasoning = '';\n let reasoningEnded = false;\n\n const coreMessages = this.toModelMessages(messages);\n log(`[opencode-provider-bridge] Converted ${coreMessages.length} messages for model=${model.id}`, 'debug');\n log(`[opencode-provider-bridge] FULL MESSAGES:\\n${JSON.stringify(coreMessages, null, 2)}`, 'debug');\n\n const tools: ToolSet = {};\n if (options.tools?.length) {\n log(`[opencode-provider-bridge] Building ${options.tools.length} tool(s)`, 'debug');\n for (const t of options.tools) {\n let params = this.toolSchemaCache.get(t.name);\n if (!params) {\n params = simplifySchema(t.inputSchema, t.name);\n this.toolSchemaCache.set(t.name, params);\n log(`[opencode-provider-bridge] Tool: ${t.name} simplified`, 'debug');\n }\n\n tools[t.name] = tool({\n description: t.description ?? '',\n inputSchema: jsonSchema(params),\n });\n }\n }\n\n const abortController = new AbortController();\n token.onCancellationRequested(() => abortController.abort());\n\n const toolChoice = options.toolMode === vscode.LanguageModelChatToolMode.Required\n ? 'required'\n : 'auto';\n\n const languageModel = this.getLanguageModel(model.id);\n\n log(`[opencode-provider-bridge] Calling streamText model=${model.id} tools=${Object.keys(tools).length}`, 'info');\n\n // Progress type assertion: LanguageModelThinkingPart is available in the\n // VS Code 1.120+ runtime even if @types/vscode doesn't include it in\n // LanguageModelResponsePart yet.\n const report = progress.report.bind(progress) as (part: unknown) => void;\n\n try {\n const result = streamText({\n model: languageModel,\n messages: coreMessages as any,\n tools: Object.keys(tools).length > 0 ? tools : undefined,\n toolChoice: Object.keys(tools).length > 0 ? toolChoice : undefined,\n abortSignal: abortController.signal,\n });\n\n let hasText = false;\n let hasToolCall = false;\n\n for await (const part of result.fullStream) {\n switch (part.type) {\n case 'text-delta':\n hasText = true;\n report(new vscode.LanguageModelTextPart(part.text));\n break;\n\n case 'reasoning-delta':\n currentReasoning += part.text;\n // Report each reasoning chunk immediately so VS Code's UI\n // shows thinking/reasoning content in real-time.\n report(new vscode.LanguageModelThinkingPart(part.text));\n break;\n\n case 'reasoning-start':\n // Signal VS Code that reasoning/thinking has started.\n // This triggers the thinking animation in the chat response.\n report(new vscode.LanguageModelThinkingPart(''));\n break;\n\n case 'reasoning-end':\n reasoningEnded = true;\n // Signal VS Code that reasoning is complete.\n // This closes the thinking animation in the chat response.\n report(new vscode.LanguageModelThinkingPart('', undefined, { vscode_reasoning_done: true }));\n break;\n\n case 'tool-call':\n hasToolCall = true;\n log(`[opencode-provider-bridge] TOOL_OUT: ${part.toolName} id=${part.toolCallId}`, 'debug');\n report(new vscode.LanguageModelToolCallPart(part.toolCallId, part.toolName, part.input as Record<string, unknown>));\n break;\n\n case 'tool-result':\n // Render tool execution result in the chat UI so the user can see\n // what the tool returned between the model's response.\n if (part.output) {\n const resultText = extractTextFromToolResult(part.output);\n if (resultText) {\n report(new vscode.LanguageModelToolResultPart(\n part.toolCallId,\n [new vscode.LanguageModelTextPart(resultText)],\n ));\n }\n }\n break;\n\n case 'finish': {\n const { totalUsage } = part;\n if (totalUsage) {\n this.lastUsage = {\n prompt: totalUsage.inputTokens ?? 0,\n completion: totalUsage.outputTokens ?? 0,\n };\n }\n break;\n }\n\n case 'error':\n log(`[opencode-provider-bridge] Stream error: ${part.error}`, 'error');\n throw part.error instanceof Error\n ? part.error\n : new Error(String(part.error));\n }\n }\n\n // Report final aggregated reasoning for multi-turn context.\n // The real-time chunks above already rendered the thinking content;\n // this provides the complete text as data for subsequent turns.\n if (currentReasoning && !reasoningEnded) {\n report(new vscode.LanguageModelThinkingPart(\n '',\n undefined,\n { _completeThinking: currentReasoning, vscode_reasoning_done: true },\n ));\n } else if (currentReasoning && reasoningEnded) {\n // reasoning-end already emitted vscode_reasoning_done.\n // Only emit _completeThinking as data to avoid double-triggering the UI.\n report(new vscode.LanguageModelDataPart(\n new TextEncoder().encode(JSON.stringify({\n _completeThinking: currentReasoning,\n })),\n REASONING_CONTENT_MIME,\n ));\n }\n\n if (this.lastUsage) {\n report(new vscode.LanguageModelDataPart(\n new TextEncoder().encode(JSON.stringify({\n prompt_tokens: this.lastUsage.prompt,\n completion_tokens: this.lastUsage.completion,\n total_tokens: this.lastUsage.prompt + this.lastUsage.completion,\n })),\n 'usage',\n ));\n }\n\n if (!hasText && !hasToolCall && !currentReasoning) {\n log(`[opencode-provider-bridge] Empty response — reporting minimal text to prevent Copilot Unknown error`, 'warn');\n report(new vscode.LanguageModelTextPart(''));\n }\n } catch (err) {\n if (err instanceof vscode.LanguageModelError) {throw err;}\n\n // Classify common error types\n const message = (err as Error).message?.toLowerCase() ?? '';\n const statusCode = (err as any).status ?? (err as any).statusCode ?? 0;\n\n if (statusCode === 429 || message.includes('rate limit') || message.includes('too many')) {\n throw vscode.LanguageModelError.Blocked(\n `${this.providerInfo.name}: rate limited. Please wait and try again.`,\n );\n }\n if (statusCode === 401 || statusCode === 403 || message.includes('unauthorized') || message.includes('invalid api key')) {\n throw vscode.LanguageModelError.NotFound(\n `${this.providerInfo.name}: invalid API key. Use \"OpenCode Bridge: Set API Key\" to update it.`,\n );\n }\n if (statusCode === 402 || message.includes('quota') || message.includes('insufficient_quota')) {\n throw new vscode.LanguageModelError(\n `${this.providerInfo.name}: quota exceeded. Please check your plan and billing.`,\n );\n }\n\n throw new vscode.LanguageModelError(\n `${this.providerInfo.name} request failed: ${(err as Error).message}`,\n );\n }\n }\n\n async provideTokenCount(\n _model: vscode.LanguageModelChatInformation,\n text: string | vscode.LanguageModelChatRequestMessage,\n _token: vscode.CancellationToken,\n ): Promise<number> {\n const serialized = typeof text === 'string' ? text : JSON.stringify(text.content);\n return Math.max(1, Math.ceil(serialized.length / 4));\n }\n\n private toModelMessages(\n messages: readonly vscode.LanguageModelChatRequestMessage[],\n ): ModelMessage[] {\n const result: ModelMessage[] = [];\n const SystemRole = (vscode.LanguageModelChatMessageRole as any).System;\n\n for (const msg of messages) {\n const textParts: string[] = [];\n const toolCallParts: Array<{\n toolCallId: string;\n toolName: string;\n input: unknown;\n }> = [];\n let toolCallId: string | undefined;\n let toolResultContent: string | undefined;\n let toolResultName: string | undefined;\n let reasoningContent = '';\n\n for (const part of msg.content) {\n if (part instanceof vscode.LanguageModelTextPart) {\n textParts.push(part.value);\n } else if (part instanceof vscode.LanguageModelToolCallPart) {\n log(`[opencode-provider-bridge] convert: ToolCall name=${part.name} id=${part.callId}`, 'debug');\n // Cache the name so subsequent ToolResultPart conversions can look it up.\n this.toolCallNameCache.set(part.callId, part.name);\n toolCallParts.push({\n toolCallId: part.callId,\n toolName: part.name,\n input: part.input,\n });\n } else if (part instanceof vscode.LanguageModelToolResultPart) {\n log(`[opencode-provider-bridge] convert: ToolResult id=${part.callId}`, 'debug');\n toolCallId = part.callId;\n // LanguageModelToolResultPart has no `.name` property in VS Code's\n // API. Look up the tool name from the cache (populated when the\n // preceding assistant message's tool-call was processed). Fall\n // back to 'unknown' for debugging visibility.\n toolResultName = this.toolCallNameCache.get(part.callId) ?? (part as any).name ?? undefined;\n log(`[opencode-provider-bridge] convert: ToolResult id=${part.callId} resolvedName=\"${toolResultName ?? '(still undefined)'}\"`, 'debug');\n const isStringContent = typeof (part as any).content === 'string';\n toolResultContent = isStringContent\n ? (part as any).content as string\n : (part.content as Array<unknown>)\n .filter((c): c is vscode.LanguageModelTextPart =>\n c instanceof vscode.LanguageModelTextPart,\n )\n .map((c) => c.value)\n .join('\\n');\n } else if (part instanceof vscode.LanguageModelDataPart) {\n if (part.mimeType === REASONING_CONTENT_MIME) {\n const decoded = new TextDecoder().decode(part.data);\n reasoningContent += decoded;\n }\n } else if (part instanceof vscode.LanguageModelThinkingPart) {\n reasoningContent += part.value ?? '';\n }\n }\n\n if (msg.role === vscode.LanguageModelChatMessageRole.User) {\n if (textParts.length > 0) {\n result.push({ role: 'user', content: textParts.join('\\n') });\n }\n if (toolCallId && toolResultContent !== undefined) {\n const resolvedName = toolResultName ?? 'unknown';\n if (resolvedName === 'unknown') {\n log(`[opencode-provider-bridge] WARNING: toolName for callId=${toolCallId} is still 'unknown' — cache miss`, 'warn');\n }\n result.push({\n role: 'tool',\n content: [\n {\n type: 'tool-result',\n toolCallId,\n toolName: resolvedName,\n output: { type: 'text', value: toolResultContent },\n },\n ],\n });\n }\n } else if (SystemRole !== undefined && msg.role === SystemRole) {\n const text = textParts.join('\\n');\n if (text) {\n result.push({ role: 'system', content: text });\n }\n } else {\n const text = textParts.join('\\n');\n const contentParts: Array<Record<string, unknown>> = [];\n\n if (text) {\n contentParts.push({ type: 'text', text });\n }\n for (const tc of toolCallParts) {\n contentParts.push({ type: 'tool-call', toolCallId: tc.toolCallId, toolName: tc.toolName, input: tc.input });\n }\n if (reasoningContent) {\n contentParts.push({ type: 'reasoning', text: reasoningContent });\n }\n\n result.push({\n role: 'assistant',\n content: contentParts.length > 0 ? contentParts : text,\n } as ModelMessage);\n }\n }\n\n return result;\n }\n}\n"}}}}