From 87bb97355f0c8034ac465416c18d79120093dd42 Mon Sep 17 00:00:00 2001 From: Mert Can Demir Date: Sat, 17 Jan 2026 12:52:58 +0300 Subject: [PATCH 1/7] chore: upgrade acp sdk and implement session config options --- bun.lock | 6 +- packages/opencode/package.json | 2 +- packages/opencode/src/acp/agent.ts | 589 ++++++++++++++++++++++++--- packages/opencode/src/acp/session.ts | 12 + packages/opencode/src/acp/types.ts | 1 + packages/opencode/src/flag/flag.ts | 1 + 6 files changed, 555 insertions(+), 56 deletions(-) diff --git a/bun.lock b/bun.lock index e71d700e1cb..d1a4e0c8f4b 100644 --- a/bun.lock +++ b/bun.lock @@ -260,7 +260,7 @@ "dependencies": { "@actions/core": "1.11.1", "@actions/github": "6.0.1", - "@agentclientprotocol/sdk": "0.5.1", + "@agentclientprotocol/sdk": "0.13.0", "@ai-sdk/amazon-bedrock": "3.0.73", "@ai-sdk/anthropic": "2.0.57", "@ai-sdk/azure": "2.0.91", @@ -548,7 +548,7 @@ "@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="], - "@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.5.1", "", { "dependencies": { "zod": "^3.0.0" } }, "sha512-9bq2TgjhLBSUSC5jE04MEe+Hqw8YePzKghhYZ9QcjOyonY3q2oJfX6GoSO83hURpEnsqEPIrex6VZN3+61fBJg=="], + "@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.13.0", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Z6/Fp4cXLbYdMXr5AK752JM5qG2VKb6ShM0Ql6FimBSckMmLyK54OA20UhPYoH4C37FSFwUTARuwQOwQUToYrw=="], "@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@3.0.73", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.57", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-EAAGJ/dfbAZaqIhK3w52hq6cftSLZwXdC6uHKh8Cls1T0N4MxS6ykDf54UyFO3bZWkQxR+Mdw1B3qireGOxtJQ=="], @@ -3964,8 +3964,6 @@ "@actions/http-client/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], - "@agentclientprotocol/sdk/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.57", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-DREpYqW2pylgaj69gZ+K8u92bo9DaMgFdictYnY+IwYeY3bawQ4zI7l/o1VkDsBDljAx8iYz5lPURwVZNu+Xpg=="], "@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.5", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.9.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Ogt4Zi9hEbIP17oQMd68qYOHUzmH47UkK7q7Gl55iIm9oKt27MUGrC5JfpMroeHjdkOliOA4Qt3NQ1xMq/nrlA=="], diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 757e6efde90..bb889d7af33 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -49,7 +49,7 @@ "dependencies": { "@actions/core": "1.11.1", "@actions/github": "6.0.1", - "@agentclientprotocol/sdk": "0.5.1", + "@agentclientprotocol/sdk": "0.13.0", "@ai-sdk/amazon-bedrock": "3.0.73", "@ai-sdk/anthropic": "2.0.57", "@ai-sdk/azure": "2.0.91", diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index f8792393c60..e05874a8fcb 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -18,6 +18,13 @@ import { type ToolCallContent, type ToolKind, } from "@agentclientprotocol/sdk" +import type { + SessionConfigOption, + SessionConfigOptionCategory, + SessionConfigSelectOption, + SetSessionConfigOptionRequest, + SetSessionConfigOptionResponse, +} from "@agentclientprotocol/sdk/dist/schema/index.js" import { Log } from "../util/log" import { ACPSessionManager } from "./session" import type { ACPConfig, ACPSessionState } from "./types" @@ -27,11 +34,17 @@ import { Installation } from "@/installation" import { MessageV2 } from "@/session/message-v2" import { Config } from "@/config/config" import { Todo } from "@/session/todo" +import { Flag } from "@/flag/flag" import { z } from "zod" import { LoadAPIKeyError } from "ai" import type { OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2" import { applyPatch } from "diff" +const DEFAULT_VARIANT_VALUE = "default" + +type ModeOption = { id: string; name: string; description?: string } +type ModelOption = { modelId: string; name: string } + export namespace ACP { const log = Log.create({ service: "acp-agent" }) @@ -48,6 +61,7 @@ export namespace ACP { private config: ACPConfig private sdk: OpencodeClient private sessionManager + private configOptionsSupported = false constructor(connection: AgentSideConnection, config: ACPConfig) { this.connection = connection @@ -349,6 +363,7 @@ export namespace ACP { async initialize(params: InitializeRequest): Promise { log.info("initialize", { protocolVersion: params.protocolVersion }) + this.configOptionsSupported = detectConfigOptionsSupport(params) const authMethod: AuthMethod = { description: "Run `opencode auth login` in the terminal", @@ -388,6 +403,10 @@ export namespace ACP { } } + private shouldUseConfigOptionsFallback() { + return !this.configOptionsSupported + } + async authenticate(_params: AuthenticateRequest) { throw new Error("Authentication not implemented") } @@ -415,7 +434,8 @@ export namespace ACP { sessionId, models: load.models, modes: load.modes, - _meta: {}, + configOptions: load.configOptions, + _meta: load._meta, } } catch (e) { const error = MessageV2.fromError(e, { @@ -670,27 +690,26 @@ export namespace ACP { } } - private async loadSessionMode(params: LoadSessionRequest) { - const directory = params.cwd - const model = await defaultModel(this.config, directory) - const sessionId = params.sessionId - - const providers = await this.sdk.config.providers({ directory }).then((x) => x.data!.providers) - const entries = providers.sort((a, b) => { - const nameA = a.name.toLowerCase() - const nameB = b.name.toLowerCase() - if (nameA < nameB) return -1 - if (nameA > nameB) return 1 - return 0 - }) - const availableModels = entries.flatMap((provider) => { - const models = Provider.sort(Object.values(provider.models)) - return models.map((model) => ({ - modelId: `${provider.id}/${model.id}`, - name: `${provider.name}/${model.name}`, - })) - }) + private async sendConfigOptionsUpdate(sessionId: string, configOptions: SessionConfigOption[]) { + const updates = ["config_option_update", "config_options_update"] as const + await Promise.all( + updates.map(async (sessionUpdate) => { + await this.connection + .sessionUpdate({ + sessionId, + update: { + sessionUpdate, + configOptions, + }, + } as unknown as Parameters[0]) + .catch((err) => { + log.error("failed to send config options update", { error: err, sessionUpdate }) + }) + }), + ) + } + private async loadAvailableModes(directory: string): Promise { const agents = await this.config.sdk.app .agents( { @@ -700,6 +719,57 @@ export namespace ACP { ) .then((resp) => resp.data!) + return agents + .filter((agent) => agent.mode !== "subagent" && !agent.hidden) + .map((agent) => ({ + id: agent.name, + name: agent.name, + description: agent.description, + })) + } + + private async resolveModeState( + directory: string, + sessionId: string, + ): Promise<{ availableModes: ModeOption[]; currentModeId?: string }> { + const availableModes = await this.loadAvailableModes(directory) + let currentModeId = this.sessionManager.get(sessionId).modeId + if (!currentModeId && availableModes.length) { + const defaultAgentName = await AgentModule.defaultAgent() + currentModeId = availableModes.find((mode) => mode.name === defaultAgentName)?.id ?? availableModes[0].id + this.sessionManager.setMode(sessionId, currentModeId) + } + + return { availableModes, currentModeId } + } + + private async loadSessionMode(params: LoadSessionRequest) { + const directory = params.cwd + const model = await defaultModel(this.config, directory) + const sessionId = params.sessionId + + const fallbackEnabled = this.shouldUseConfigOptionsFallback() + + const providers = await this.sdk.config.providers({ directory }).then((x) => x.data!.providers) + const entries = sortProvidersByName(providers) + const availableVariants = modelVariantsFromProviders(entries, model) + const currentVariant = this.sessionManager.getVariant(sessionId) + if (currentVariant && !availableVariants.includes(currentVariant)) { + this.sessionManager.setVariant(sessionId, undefined) + } + const availableModels = buildAvailableModels(entries, { includeVariants: fallbackEnabled }) + const { availableModes, currentModeId } = await this.resolveModeState(directory, sessionId) + const baseModelId = `${model.providerID}/${model.modelID}` + const configOptions = buildConfigOptions({ + availableModes, + currentModeId, + availableModels, + currentModelId: baseModelId, + availableVariants, + currentVariant: this.sessionManager.getVariant(sessionId), + }) + const modeState = currentModeId ? { availableModes, currentModeId } : undefined + const commands = await this.config.sdk.command .list( { @@ -720,20 +790,6 @@ export namespace ACP { description: "compact the session", }) - const availableModes = agents - .filter((agent) => agent.mode !== "subagent" && !agent.hidden) - .map((agent) => ({ - id: agent.name, - name: agent.name, - description: agent.description, - })) - - const defaultAgentName = await AgentModule.defaultAgent() - const currentModeId = availableModes.find((m) => m.name === defaultAgentName)?.id ?? availableModes[0].id - - // Persist the default mode so prompt() uses it immediately - this.sessionManager.setMode(sessionId, currentModeId) - const mcpServers: Record = {} for (const server of params.mcpServers) { if ("type" in server) { @@ -784,44 +840,201 @@ export namespace ACP { }) }, 0) + setTimeout(() => { + void this.sendConfigOptionsUpdate(sessionId, configOptions) + }, 0) + return { sessionId, models: { - currentModelId: `${model.providerID}/${model.modelID}`, + currentModelId: formatModelIdWithVariant(model, currentVariant, availableVariants, fallbackEnabled), availableModels, }, - modes: { - availableModes, - currentModeId, - }, - _meta: {}, + modes: modeState, + configOptions: configOptions.length ? configOptions : undefined, + _meta: buildVariantMeta({ + model, + variant: this.sessionManager.getVariant(sessionId), + availableVariants, + }), } } - async setSessionModel(params: SetSessionModelRequest) { + async unstable_setSessionModel(params: SetSessionModelRequest) { const session = this.sessionManager.get(params.sessionId) - const model = Provider.parseModel(params.modelId) + const fallbackEnabled = this.shouldUseConfigOptionsFallback() + + const providers = await this.sdk.config + .providers({ directory: session.cwd }, { throwOnError: true }) + .then((x) => x.data!.providers) + const entries = sortProvidersByName(providers) + + const selection = fallbackEnabled + ? parseModelSelection(params.modelId, entries) + : { model: Provider.parseModel(params.modelId), variant: undefined } + const model = selection.model this.sessionManager.setModel(session.id, { providerID: model.providerID, modelID: model.modelID, }) + if (fallbackEnabled) { + this.sessionManager.setVariant(session.id, selection.variant) + } + const availableVariants = modelVariantsFromProviders(entries, model) + const currentVariant = this.sessionManager.getVariant(session.id) + if (currentVariant && !availableVariants.includes(currentVariant)) { + this.sessionManager.setVariant(session.id, undefined) + } + + const availableModels = buildAvailableModels(entries, { includeVariants: fallbackEnabled }) + const { availableModes, currentModeId } = await this.resolveModeState(session.cwd, session.id) + const baseModelId = `${model.providerID}/${model.modelID}` + const configOptions = buildConfigOptions({ + availableModes, + currentModeId, + availableModels, + currentModelId: baseModelId, + availableVariants, + currentVariant: this.sessionManager.getVariant(session.id), + }) + await this.sendConfigOptionsUpdate(session.id, configOptions) return { - _meta: {}, + _meta: buildVariantMeta({ + model, + variant: this.sessionManager.getVariant(session.id), + availableVariants, + }), + } + } + + async unstable_setSessionConfigOption( + params: SetSessionConfigOptionRequest, + ): Promise { + const session = this.sessionManager.get(params.sessionId) + const directory = session.cwd + const fallbackEnabled = this.shouldUseConfigOptionsFallback() + let model = session.model ?? (await defaultModel(this.config, directory)) + if (!session.model) { + this.sessionManager.setModel(session.id, model) + } + + const providers = await this.sdk.config + .providers({ directory }, { throwOnError: true }) + .then((x) => x.data!.providers) + const entries = sortProvidersByName(providers) + const availableModels = buildAvailableModels(entries, { includeVariants: fallbackEnabled }) + const { availableModes, currentModeId } = await this.resolveModeState(directory, session.id) + + let availableVariants = modelVariantsFromProviders(entries, model) + if (params.configId === "variant" && availableVariants.length === 0) { + throw RequestError.invalidParams({ configId: params.configId }, "No variants available") + } + + switch (params.configId) { + case "variant": { + let nextVariant: string | undefined + if (params.value === DEFAULT_VARIANT_VALUE) { + nextVariant = undefined + } else if (availableVariants.includes(params.value)) { + nextVariant = params.value + } else { + throw RequestError.invalidParams({ value: params.value }, "Unsupported variant value") + } + + const previousVariant = this.sessionManager.getVariant(session.id) + if (previousVariant !== nextVariant) { + this.sessionManager.setVariant(session.id, nextVariant) + } + break + } + case "mode": { + if (!availableModes.some((mode) => mode.id === params.value)) { + throw RequestError.invalidParams({ value: params.value }, "Unsupported mode value") + } + this.sessionManager.setMode(session.id, params.value) + break + } + case "model": { + if (!availableModels.some((option) => option.modelId === params.value)) { + throw RequestError.invalidParams({ value: params.value }, "Unsupported model value") + } + const selection = fallbackEnabled + ? parseModelSelection(params.value, entries) + : { model: Provider.parseModel(params.value), variant: undefined } + const nextModel = selection.model + model = { providerID: nextModel.providerID, modelID: nextModel.modelID } + this.sessionManager.setModel(session.id, model) + if (fallbackEnabled) { + this.sessionManager.setVariant(session.id, selection.variant) + } + + availableVariants = modelVariantsFromProviders(entries, model) + const currentVariant = this.sessionManager.getVariant(session.id) + if (currentVariant && !availableVariants.includes(currentVariant)) { + this.sessionManager.setVariant(session.id, undefined) + } + break + } + default: + throw RequestError.invalidParams({ configId: params.configId }, "Unsupported config option") + } + + const currentMode = this.sessionManager.get(session.id).modeId ?? currentModeId + const currentModelId = `${model.providerID}/${model.modelID}` + const configOptions = buildConfigOptions({ + availableModes, + currentModeId: currentMode, + availableModels, + currentModelId, + availableVariants, + currentVariant: this.sessionManager.getVariant(session.id), + }) + + await this.sendConfigOptionsUpdate(session.id, configOptions) + + return { + configOptions, + _meta: buildVariantMeta({ + model, + variant: this.sessionManager.getVariant(session.id), + availableVariants, + }), } } async setSessionMode(params: SetSessionModeRequest): Promise { - this.sessionManager.get(params.sessionId) - await this.config.sdk.app - .agents({}, { throwOnError: true }) - .then((x) => x.data) - .then((agent) => { - if (!agent) throw new Error(`Agent not found: ${params.modeId}`) - }) + const session = this.sessionManager.get(params.sessionId) + const availableModes = await this.loadAvailableModes(session.cwd) + if (!availableModes.some((mode) => mode.id === params.modeId)) { + throw new Error(`Agent not found: ${params.modeId}`) + } this.sessionManager.setMode(params.sessionId, params.modeId) + + const fallbackEnabled = this.shouldUseConfigOptionsFallback() + const model = session.model ?? (await defaultModel(this.config, session.cwd)) + if (!session.model) { + this.sessionManager.setModel(session.id, model) + } + + const providers = await this.sdk.config + .providers({ directory: session.cwd }, { throwOnError: true }) + .then((x) => x.data!.providers) + const entries = sortProvidersByName(providers) + const availableVariants = modelVariantsFromProviders(entries, model) + const availableModels = buildAvailableModels(entries, { includeVariants: fallbackEnabled }) + const configOptions = buildConfigOptions({ + availableModes, + currentModeId: params.modeId, + availableModels, + currentModelId: `${model.providerID}/${model.modelID}`, + availableVariants, + currentVariant: this.sessionManager.getVariant(session.id), + }) + + await this.sendConfigOptionsUpdate(session.id, configOptions) } async prompt(params: PromptRequest) { @@ -834,6 +1047,34 @@ export namespace ACP { if (!current) { this.sessionManager.setModel(session.id, model) } + const previousVariant = this.sessionManager.getVariant(sessionID) + const requestedVariant = (params._meta as { opencode?: { variant?: string } } | null)?.opencode?.variant + if (typeof requestedVariant === "string") { + const providers = await this.sdk.config + .providers({ directory }, { throwOnError: true }) + .then((x) => x.data!.providers) + const entries = sortProvidersByName(providers) + const availableVariants = modelVariantsFromProviders(entries, model) + if (availableVariants.includes(requestedVariant)) { + this.sessionManager.setVariant(sessionID, requestedVariant) + } else { + this.sessionManager.setVariant(sessionID, undefined) + } + const nextVariant = this.sessionManager.getVariant(sessionID) + if (nextVariant !== previousVariant) { + const availableModels = buildAvailableModels(entries, { includeVariants: this.shouldUseConfigOptionsFallback() }) + const { availableModes, currentModeId } = await this.resolveModeState(directory, sessionID) + const configOptions = buildConfigOptions({ + availableModes, + currentModeId, + availableModels, + currentModelId: `${model.providerID}/${model.modelID}`, + availableVariants, + currentVariant: nextVariant, + }) + await this.sendConfigOptionsUpdate(sessionID, configOptions) + } + } const agent = session.modeId ?? (await AgentModule.defaultAgent()) const parts: Array< @@ -913,6 +1154,7 @@ export namespace ACP { providerID: model.providerID, modelID: model.modelID, }, + variant: this.sessionManager.getVariant(sessionID), parts, agent, directory, @@ -1124,4 +1366,249 @@ export namespace ACP { } return result } + + function modelVariantsFromProviders( + providers: Array<{ id: string; models: Record }> }>, + model: { providerID: string; modelID: string }, + ): string[] { + const provider = providers.find((entry) => entry.id === model.providerID) + if (!provider) return [] + const modelInfo = provider.models[model.modelID] + if (!modelInfo?.variants) return [] + return Object.keys(modelInfo.variants) + } + + function sortProvidersByName(providers: T[]): T[] { + return [...providers].sort((a, b) => { + const nameA = a.name.toLowerCase() + const nameB = b.name.toLowerCase() + if (nameA < nameB) return -1 + if (nameA > nameB) return 1 + return 0 + }) + } + + function buildAvailableModels( + providers: Array<{ id: string; name: string; models: Record }>, + options: { includeVariants?: boolean } = {}, + ): ModelOption[] { + const includeVariants = options.includeVariants ?? false + return providers.flatMap((provider) => { + const models = Provider.sort(Object.values(provider.models) as any) + return models.flatMap((model) => { + const base: ModelOption = { + modelId: `${provider.id}/${model.id}`, + name: `${provider.name}/${model.name}`, + } + if (!includeVariants || !model.variants) return [base] + const variants = Object.keys(model.variants).filter((variant) => variant !== DEFAULT_VARIANT_VALUE) + const variantOptions = variants.map((variant) => ({ + modelId: `${provider.id}/${model.id}/${variant}`, + name: `${provider.name}/${model.name} (${variant})`, + })) + return [base, ...variantOptions] + }) + }) + } + + function buildConfigOptions(input: { + availableModes: ModeOption[] + currentModeId?: string + availableModels: ModelOption[] + currentModelId: string + availableVariants: string[] + currentVariant?: string + }): SessionConfigOption[] { + const configOptions: SessionConfigOption[] = [] + + const modeOption = buildModeConfigOption({ + availableModes: input.availableModes, + currentModeId: input.currentModeId, + }) + if (modeOption) configOptions.push(modeOption) + + const modelOption = buildModelConfigOption({ + availableModels: input.availableModels, + currentModelId: input.currentModelId, + }) + if (modelOption) configOptions.push(modelOption) + + const variantOption = buildVariantConfigOption({ + modelId: input.currentModelId, + availableVariants: input.availableVariants, + currentVariant: input.currentVariant, + }) + if (variantOption) configOptions.push(variantOption) + + return configOptions + } + + function buildModeConfigOption(input: { + availableModes: ModeOption[] + currentModeId?: string + }): SessionConfigOption | undefined { + if (!input.availableModes.length || !input.currentModeId) return undefined + + const options: SessionConfigSelectOption[] = input.availableModes.map((mode) => ({ + name: mode.name, + value: mode.id, + description: mode.description ?? undefined, + })) + const category: SessionConfigOptionCategory = "mode" + + return { + id: "mode", + name: "Mode", + type: "select", + currentValue: input.currentModeId, + options, + category, + } + } + + function buildModelConfigOption(input: { + availableModels: ModelOption[] + currentModelId: string + }): SessionConfigOption | undefined { + if (!input.availableModels.length) return undefined + + const options: SessionConfigSelectOption[] = input.availableModels.map((model) => ({ + name: model.name, + value: model.modelId, + })) + const category: SessionConfigOptionCategory = "model" + + return { + id: "model", + name: "Model", + type: "select", + currentValue: input.currentModelId, + options, + category, + } + } + + function buildVariantConfigOptions(input: { + modelId: string + availableVariants: string[] + currentVariant?: string + }): SessionConfigOption[] { + const option = buildVariantConfigOption(input) + return option ? [option] : [] + } + + function buildVariantConfigOption(input: { + modelId: string + availableVariants: string[] + currentVariant?: string + }): SessionConfigOption | undefined { + const variants = input.availableVariants.filter((variant) => variant !== DEFAULT_VARIANT_VALUE) + if (variants.length === 0) return undefined + + const selectedVariant = + input.currentVariant && variants.includes(input.currentVariant) ? input.currentVariant : undefined + const options: SessionConfigSelectOption[] = [ + { + name: "Default", + value: DEFAULT_VARIANT_VALUE, + }, + ...variants.map((variant) => ({ + name: variant, + value: variant, + })), + ] + const category: SessionConfigOptionCategory = "thought_level" + + return { + id: "variant", + name: "Thinking Level", + type: "select", + currentValue: selectedVariant ?? DEFAULT_VARIANT_VALUE, + options, + category, + _meta: buildVariantConfigMeta({ + modelId: input.modelId, + currentVariant: selectedVariant, + availableVariants: variants, + }), + } + } + + function buildVariantConfigMeta(input: { + modelId: string + currentVariant?: string + availableVariants: string[] + }) { + return { + opencode: { + modelId: input.modelId, + currentVariant: input.currentVariant ?? null, + availableVariants: input.availableVariants, + hasVariants: input.availableVariants.length > 0, + }, + } + } + + function buildVariantMeta(input: { + model: { providerID: string; modelID: string } + variant?: string + availableVariants: string[] + }) { + return { + opencode: { + modelId: `${input.model.providerID}/${input.model.modelID}`, + variant: input.variant ?? null, + availableVariants: input.availableVariants, + }, + } + } + + function detectConfigOptionsSupport(params: InitializeRequest): boolean { + const override = Flag.OPENCODE_ACP_CONFIG_OPTIONS_FALLBACK?.toLowerCase() + if (override) { + if (["1", "true", "on", "force", "fallback"].includes(override)) return false + if (["0", "false", "off", "disable", "disabled"].includes(override)) return true + } + // Default to fallback unless client explicitly signals config options support. + return params.clientCapabilities?._meta?.["config_options"] === true + } + + function formatModelIdWithVariant( + model: { providerID: string; modelID: string }, + variant: string | undefined, + availableVariants: string[], + includeVariant: boolean, + ) { + const base = `${model.providerID}/${model.modelID}` + if (!includeVariant || !variant || !availableVariants.includes(variant)) return base + return `${base}/${variant}` + } + + function parseModelSelection( + modelId: string, + providers: Array<{ id: string; name: string; models: Record }>, + ) { + const parsed = Provider.parseModel(modelId) + const provider = providers.find((entry) => entry.id === parsed.providerID) + if (!provider) return { model: { providerID: parsed.providerID, modelID: parsed.modelID }, variant: undefined } + + if (provider.models[parsed.modelID]) { + return { model: { providerID: parsed.providerID, modelID: parsed.modelID }, variant: undefined } + } + + const segments = parsed.modelID.split("/") + if (segments.length > 1) { + const candidateVariant = segments[segments.length - 1] + const baseModelId = segments.slice(0, -1).join("/") + if (provider.models[baseModelId]) { + const baseModel = { providerID: parsed.providerID, modelID: baseModelId } + const availableVariants = modelVariantsFromProviders(providers, baseModel) + if (availableVariants.includes(candidateVariant)) { + return { model: baseModel, variant: candidateVariant } + } + } + } + + return { model: { providerID: parsed.providerID, modelID: parsed.modelID }, variant: undefined } + } } diff --git a/packages/opencode/src/acp/session.ts b/packages/opencode/src/acp/session.ts index 70b65834705..28acee23182 100644 --- a/packages/opencode/src/acp/session.ts +++ b/packages/opencode/src/acp/session.ts @@ -92,6 +92,18 @@ export class ACPSessionManager { return session } + getVariant(sessionId: string) { + const session = this.get(sessionId) + return session.variant + } + + setVariant(sessionId: string, variant?: string) { + const session = this.get(sessionId) + session.variant = variant + this.sessions.set(sessionId, session) + return session + } + setMode(sessionId: string, modeId: string) { const session = this.get(sessionId) session.modeId = modeId diff --git a/packages/opencode/src/acp/types.ts b/packages/opencode/src/acp/types.ts index 42b23091237..de8ac508122 100644 --- a/packages/opencode/src/acp/types.ts +++ b/packages/opencode/src/acp/types.ts @@ -10,6 +10,7 @@ export interface ACPSessionState { providerID: string modelID: string } + variant?: string modeId?: string } diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index 4cdb549096a..184facc8ba1 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -18,6 +18,7 @@ export namespace Flag { OPENCODE_DISABLE_CLAUDE_CODE || truthy("OPENCODE_DISABLE_CLAUDE_CODE_PROMPT") export const OPENCODE_DISABLE_CLAUDE_CODE_SKILLS = OPENCODE_DISABLE_CLAUDE_CODE || truthy("OPENCODE_DISABLE_CLAUDE_CODE_SKILLS") + export const OPENCODE_ACP_CONFIG_OPTIONS_FALLBACK = process.env["OPENCODE_ACP_CONFIG_OPTIONS_FALLBACK"] export const OPENCODE_FAKE_VCS = process.env["OPENCODE_FAKE_VCS"] export const OPENCODE_CLIENT = process.env["OPENCODE_CLIENT"] ?? "cli" export const OPENCODE_SERVER_PASSWORD = process.env["OPENCODE_SERVER_PASSWORD"] From ab54916931444461305edf3446b04a73404a80e7 Mon Sep 17 00:00:00 2001 From: Mert Can Demir Date: Sat, 17 Jan 2026 12:55:16 +0300 Subject: [PATCH 2/7] chore: upgrade bun to 1.3.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f1d6c4fead1..8db569de1e3 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "AI-powered development tool", "private": true, "type": "module", - "packageManager": "bun@1.3.5", + "packageManager": "bun@1.3.6", "scripts": { "dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts", "typecheck": "bun turbo typecheck", From c510b55e9e89c4992d98b6f17022946a3a0b0571 Mon Sep 17 00:00:00 2001 From: Mert Can Demir Date: Sat, 17 Jan 2026 13:52:29 +0300 Subject: [PATCH 3/7] refactor: reorganize helper functions in acp agent module --- packages/opencode/src/acp/agent.ts | 130 ++++++++++++++--------------- 1 file changed, 61 insertions(+), 69 deletions(-) diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index e05874a8fcb..2cb5685f4d8 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -40,11 +40,11 @@ import { LoadAPIKeyError } from "ai" import type { OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2" import { applyPatch } from "diff" -const DEFAULT_VARIANT_VALUE = "default" - type ModeOption = { id: string; name: string; description?: string } type ModelOption = { modelId: string; name: string } +const DEFAULT_VARIANT_VALUE = "default" + export namespace ACP { const log = Log.create({ service: "acp-agent" }) @@ -1367,6 +1367,16 @@ export namespace ACP { return result } + function sortProvidersByName(providers: T[]): T[] { + return [...providers].sort((a, b) => { + const nameA = a.name.toLowerCase() + const nameB = b.name.toLowerCase() + if (nameA < nameB) return -1 + if (nameA > nameB) return 1 + return 0 + }) + } + function modelVariantsFromProviders( providers: Array<{ id: string; models: Record }> }>, model: { providerID: string; modelID: string }, @@ -1378,16 +1388,6 @@ export namespace ACP { return Object.keys(modelInfo.variants) } - function sortProvidersByName(providers: T[]): T[] { - return [...providers].sort((a, b) => { - const nameA = a.name.toLowerCase() - const nameB = b.name.toLowerCase() - if (nameA < nameB) return -1 - if (nameA > nameB) return 1 - return 0 - }) - } - function buildAvailableModels( providers: Array<{ id: string; name: string; models: Record }>, options: { includeVariants?: boolean } = {}, @@ -1411,6 +1411,55 @@ export namespace ACP { }) } + function detectConfigOptionsSupport(params: InitializeRequest): boolean { + const override = Flag.OPENCODE_ACP_CONFIG_OPTIONS_FALLBACK?.toLowerCase() + if (override) { + if (["1", "true", "on", "force", "fallback"].includes(override)) return false + if (["0", "false", "off", "disable", "disabled"].includes(override)) return true + } + // Default to fallback unless client explicitly signals config options support. + return params.clientCapabilities?._meta?.["config_options"] === true + } + + function formatModelIdWithVariant( + model: { providerID: string; modelID: string }, + variant: string | undefined, + availableVariants: string[], + includeVariant: boolean, + ) { + const base = `${model.providerID}/${model.modelID}` + if (!includeVariant || !variant || !availableVariants.includes(variant)) return base + return `${base}/${variant}` + } + + function parseModelSelection( + modelId: string, + providers: Array<{ id: string; name: string; models: Record }>, + ) { + const parsed = Provider.parseModel(modelId) + const provider = providers.find((entry) => entry.id === parsed.providerID) + if (!provider) return { model: { providerID: parsed.providerID, modelID: parsed.modelID }, variant: undefined } + + if (provider.models[parsed.modelID]) { + return { model: { providerID: parsed.providerID, modelID: parsed.modelID }, variant: undefined } + } + + const segments = parsed.modelID.split("/") + if (segments.length > 1) { + const candidateVariant = segments[segments.length - 1] + const baseModelId = segments.slice(0, -1).join("/") + if (provider.models[baseModelId]) { + const baseModel = { providerID: parsed.providerID, modelID: baseModelId } + const availableVariants = modelVariantsFromProviders(providers, baseModel) + if (availableVariants.includes(candidateVariant)) { + return { model: baseModel, variant: candidateVariant } + } + } + } + + return { model: { providerID: parsed.providerID, modelID: parsed.modelID }, variant: undefined } + } + function buildConfigOptions(input: { availableModes: ModeOption[] currentModeId?: string @@ -1488,15 +1537,6 @@ export namespace ACP { } } - function buildVariantConfigOptions(input: { - modelId: string - availableVariants: string[] - currentVariant?: string - }): SessionConfigOption[] { - const option = buildVariantConfigOption(input) - return option ? [option] : [] - } - function buildVariantConfigOption(input: { modelId: string availableVariants: string[] @@ -1563,52 +1603,4 @@ export namespace ACP { } } - function detectConfigOptionsSupport(params: InitializeRequest): boolean { - const override = Flag.OPENCODE_ACP_CONFIG_OPTIONS_FALLBACK?.toLowerCase() - if (override) { - if (["1", "true", "on", "force", "fallback"].includes(override)) return false - if (["0", "false", "off", "disable", "disabled"].includes(override)) return true - } - // Default to fallback unless client explicitly signals config options support. - return params.clientCapabilities?._meta?.["config_options"] === true - } - - function formatModelIdWithVariant( - model: { providerID: string; modelID: string }, - variant: string | undefined, - availableVariants: string[], - includeVariant: boolean, - ) { - const base = `${model.providerID}/${model.modelID}` - if (!includeVariant || !variant || !availableVariants.includes(variant)) return base - return `${base}/${variant}` - } - - function parseModelSelection( - modelId: string, - providers: Array<{ id: string; name: string; models: Record }>, - ) { - const parsed = Provider.parseModel(modelId) - const provider = providers.find((entry) => entry.id === parsed.providerID) - if (!provider) return { model: { providerID: parsed.providerID, modelID: parsed.modelID }, variant: undefined } - - if (provider.models[parsed.modelID]) { - return { model: { providerID: parsed.providerID, modelID: parsed.modelID }, variant: undefined } - } - - const segments = parsed.modelID.split("/") - if (segments.length > 1) { - const candidateVariant = segments[segments.length - 1] - const baseModelId = segments.slice(0, -1).join("/") - if (provider.models[baseModelId]) { - const baseModel = { providerID: parsed.providerID, modelID: baseModelId } - const availableVariants = modelVariantsFromProviders(providers, baseModel) - if (availableVariants.includes(candidateVariant)) { - return { model: baseModel, variant: candidateVariant } - } - } - } - - return { model: { providerID: parsed.providerID, modelID: parsed.modelID }, variant: undefined } - } } From 5311624e92f4e3edf0bf35978334f1d62421e731 Mon Sep 17 00:00:00 2001 From: Mert Can Demir Date: Sat, 17 Jan 2026 15:15:47 +0300 Subject: [PATCH 4/7] refactor: remove unused ACP config options fallback flag --- packages/opencode/src/acp/agent.ts | 432 +++-------------------------- packages/opencode/src/flag/flag.ts | 2 +- 2 files changed, 38 insertions(+), 396 deletions(-) diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index 2cb5685f4d8..3d7fcf6b18a 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -18,13 +18,7 @@ import { type ToolCallContent, type ToolKind, } from "@agentclientprotocol/sdk" -import type { - SessionConfigOption, - SessionConfigOptionCategory, - SessionConfigSelectOption, - SetSessionConfigOptionRequest, - SetSessionConfigOptionResponse, -} from "@agentclientprotocol/sdk/dist/schema/index.js" + import { Log } from "../util/log" import { ACPSessionManager } from "./session" import type { ACPConfig, ACPSessionState } from "./types" @@ -34,7 +28,6 @@ import { Installation } from "@/installation" import { MessageV2 } from "@/session/message-v2" import { Config } from "@/config/config" import { Todo } from "@/session/todo" -import { Flag } from "@/flag/flag" import { z } from "zod" import { LoadAPIKeyError } from "ai" import type { OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2" @@ -61,7 +54,6 @@ export namespace ACP { private config: ACPConfig private sdk: OpencodeClient private sessionManager - private configOptionsSupported = false constructor(connection: AgentSideConnection, config: ACPConfig) { this.connection = connection @@ -363,7 +355,6 @@ export namespace ACP { async initialize(params: InitializeRequest): Promise { log.info("initialize", { protocolVersion: params.protocolVersion }) - this.configOptionsSupported = detectConfigOptionsSupport(params) const authMethod: AuthMethod = { description: "Run `opencode auth login` in the terminal", @@ -403,10 +394,6 @@ export namespace ACP { } } - private shouldUseConfigOptionsFallback() { - return !this.configOptionsSupported - } - async authenticate(_params: AuthenticateRequest) { throw new Error("Authentication not implemented") } @@ -434,7 +421,6 @@ export namespace ACP { sessionId, models: load.models, modes: load.modes, - configOptions: load.configOptions, _meta: load._meta, } } catch (e) { @@ -690,25 +676,6 @@ export namespace ACP { } } - private async sendConfigOptionsUpdate(sessionId: string, configOptions: SessionConfigOption[]) { - const updates = ["config_option_update", "config_options_update"] as const - await Promise.all( - updates.map(async (sessionUpdate) => { - await this.connection - .sessionUpdate({ - sessionId, - update: { - sessionUpdate, - configOptions, - }, - } as unknown as Parameters[0]) - .catch((err) => { - log.error("failed to send config options update", { error: err, sessionUpdate }) - }) - }), - ) - } - private async loadAvailableModes(directory: string): Promise { const agents = await this.config.sdk.app .agents( @@ -748,8 +715,6 @@ export namespace ACP { const model = await defaultModel(this.config, directory) const sessionId = params.sessionId - const fallbackEnabled = this.shouldUseConfigOptionsFallback() - const providers = await this.sdk.config.providers({ directory }).then((x) => x.data!.providers) const entries = sortProvidersByName(providers) const availableVariants = modelVariantsFromProviders(entries, model) @@ -757,17 +722,8 @@ export namespace ACP { if (currentVariant && !availableVariants.includes(currentVariant)) { this.sessionManager.setVariant(sessionId, undefined) } - const availableModels = buildAvailableModels(entries, { includeVariants: fallbackEnabled }) + const availableModels = buildAvailableModels(entries, { includeVariants: true }) const { availableModes, currentModeId } = await this.resolveModeState(directory, sessionId) - const baseModelId = `${model.providerID}/${model.modelID}` - const configOptions = buildConfigOptions({ - availableModes, - currentModeId, - availableModels, - currentModelId: baseModelId, - availableVariants, - currentVariant: this.sessionManager.getVariant(sessionId), - }) const modeState = currentModeId ? { availableModes, currentModeId } : undefined const commands = await this.config.sdk.command @@ -840,18 +796,13 @@ export namespace ACP { }) }, 0) - setTimeout(() => { - void this.sendConfigOptionsUpdate(sessionId, configOptions) - }, 0) - return { sessionId, models: { - currentModelId: formatModelIdWithVariant(model, currentVariant, availableVariants, fallbackEnabled), + currentModelId: formatModelIdWithVariant(model, currentVariant, availableVariants, true), availableModels, }, modes: modeState, - configOptions: configOptions.length ? configOptions : undefined, _meta: buildVariantMeta({ model, variant: this.sessionManager.getVariant(sessionId), @@ -862,144 +813,21 @@ export namespace ACP { async unstable_setSessionModel(params: SetSessionModelRequest) { const session = this.sessionManager.get(params.sessionId) - - const fallbackEnabled = this.shouldUseConfigOptionsFallback() - const providers = await this.sdk.config .providers({ directory: session.cwd }, { throwOnError: true }) .then((x) => x.data!.providers) - const entries = sortProvidersByName(providers) - const selection = fallbackEnabled - ? parseModelSelection(params.modelId, entries) - : { model: Provider.parseModel(params.modelId), variant: undefined } - const model = selection.model - - this.sessionManager.setModel(session.id, { - providerID: model.providerID, - modelID: model.modelID, - }) - if (fallbackEnabled) { - this.sessionManager.setVariant(session.id, selection.variant) - } - const availableVariants = modelVariantsFromProviders(entries, model) - const currentVariant = this.sessionManager.getVariant(session.id) - if (currentVariant && !availableVariants.includes(currentVariant)) { - this.sessionManager.setVariant(session.id, undefined) - } - - const availableModels = buildAvailableModels(entries, { includeVariants: fallbackEnabled }) - const { availableModes, currentModeId } = await this.resolveModeState(session.cwd, session.id) - const baseModelId = `${model.providerID}/${model.modelID}` - const configOptions = buildConfigOptions({ - availableModes, - currentModeId, - availableModels, - currentModelId: baseModelId, - availableVariants, - currentVariant: this.sessionManager.getVariant(session.id), - }) - await this.sendConfigOptionsUpdate(session.id, configOptions) - - return { - _meta: buildVariantMeta({ - model, - variant: this.sessionManager.getVariant(session.id), - availableVariants, - }), - } - } - - async unstable_setSessionConfigOption( - params: SetSessionConfigOptionRequest, - ): Promise { - const session = this.sessionManager.get(params.sessionId) - const directory = session.cwd - const fallbackEnabled = this.shouldUseConfigOptionsFallback() - let model = session.model ?? (await defaultModel(this.config, directory)) - if (!session.model) { - this.sessionManager.setModel(session.id, model) - } + const { model, variant } = parseModelSelection(params.modelId, providers) + this.sessionManager.setModel(session.id, model) + this.sessionManager.setVariant(session.id, variant) - const providers = await this.sdk.config - .providers({ directory }, { throwOnError: true }) - .then((x) => x.data!.providers) const entries = sortProvidersByName(providers) - const availableModels = buildAvailableModels(entries, { includeVariants: fallbackEnabled }) - const { availableModes, currentModeId } = await this.resolveModeState(directory, session.id) - - let availableVariants = modelVariantsFromProviders(entries, model) - if (params.configId === "variant" && availableVariants.length === 0) { - throw RequestError.invalidParams({ configId: params.configId }, "No variants available") - } - - switch (params.configId) { - case "variant": { - let nextVariant: string | undefined - if (params.value === DEFAULT_VARIANT_VALUE) { - nextVariant = undefined - } else if (availableVariants.includes(params.value)) { - nextVariant = params.value - } else { - throw RequestError.invalidParams({ value: params.value }, "Unsupported variant value") - } - - const previousVariant = this.sessionManager.getVariant(session.id) - if (previousVariant !== nextVariant) { - this.sessionManager.setVariant(session.id, nextVariant) - } - break - } - case "mode": { - if (!availableModes.some((mode) => mode.id === params.value)) { - throw RequestError.invalidParams({ value: params.value }, "Unsupported mode value") - } - this.sessionManager.setMode(session.id, params.value) - break - } - case "model": { - if (!availableModels.some((option) => option.modelId === params.value)) { - throw RequestError.invalidParams({ value: params.value }, "Unsupported model value") - } - const selection = fallbackEnabled - ? parseModelSelection(params.value, entries) - : { model: Provider.parseModel(params.value), variant: undefined } - const nextModel = selection.model - model = { providerID: nextModel.providerID, modelID: nextModel.modelID } - this.sessionManager.setModel(session.id, model) - if (fallbackEnabled) { - this.sessionManager.setVariant(session.id, selection.variant) - } - - availableVariants = modelVariantsFromProviders(entries, model) - const currentVariant = this.sessionManager.getVariant(session.id) - if (currentVariant && !availableVariants.includes(currentVariant)) { - this.sessionManager.setVariant(session.id, undefined) - } - break - } - default: - throw RequestError.invalidParams({ configId: params.configId }, "Unsupported config option") - } - - const currentMode = this.sessionManager.get(session.id).modeId ?? currentModeId - const currentModelId = `${model.providerID}/${model.modelID}` - const configOptions = buildConfigOptions({ - availableModes, - currentModeId: currentMode, - availableModels, - currentModelId, - availableVariants, - currentVariant: this.sessionManager.getVariant(session.id), - }) - - await this.sendConfigOptionsUpdate(session.id, configOptions) + const availableVariants = modelVariantsFromProviders(entries, model) return { - configOptions, _meta: buildVariantMeta({ model, - variant: this.sessionManager.getVariant(session.id), + variant, availableVariants, }), } @@ -1012,29 +840,6 @@ export namespace ACP { throw new Error(`Agent not found: ${params.modeId}`) } this.sessionManager.setMode(params.sessionId, params.modeId) - - const fallbackEnabled = this.shouldUseConfigOptionsFallback() - const model = session.model ?? (await defaultModel(this.config, session.cwd)) - if (!session.model) { - this.sessionManager.setModel(session.id, model) - } - - const providers = await this.sdk.config - .providers({ directory: session.cwd }, { throwOnError: true }) - .then((x) => x.data!.providers) - const entries = sortProvidersByName(providers) - const availableVariants = modelVariantsFromProviders(entries, model) - const availableModels = buildAvailableModels(entries, { includeVariants: fallbackEnabled }) - const configOptions = buildConfigOptions({ - availableModes, - currentModeId: params.modeId, - availableModels, - currentModelId: `${model.providerID}/${model.modelID}`, - availableVariants, - currentVariant: this.sessionManager.getVariant(session.id), - }) - - await this.sendConfigOptionsUpdate(session.id, configOptions) } async prompt(params: PromptRequest) { @@ -1047,34 +852,6 @@ export namespace ACP { if (!current) { this.sessionManager.setModel(session.id, model) } - const previousVariant = this.sessionManager.getVariant(sessionID) - const requestedVariant = (params._meta as { opencode?: { variant?: string } } | null)?.opencode?.variant - if (typeof requestedVariant === "string") { - const providers = await this.sdk.config - .providers({ directory }, { throwOnError: true }) - .then((x) => x.data!.providers) - const entries = sortProvidersByName(providers) - const availableVariants = modelVariantsFromProviders(entries, model) - if (availableVariants.includes(requestedVariant)) { - this.sessionManager.setVariant(sessionID, requestedVariant) - } else { - this.sessionManager.setVariant(sessionID, undefined) - } - const nextVariant = this.sessionManager.getVariant(sessionID) - if (nextVariant !== previousVariant) { - const availableModels = buildAvailableModels(entries, { includeVariants: this.shouldUseConfigOptionsFallback() }) - const { availableModes, currentModeId } = await this.resolveModeState(directory, sessionID) - const configOptions = buildConfigOptions({ - availableModes, - currentModeId, - availableModels, - currentModelId: `${model.providerID}/${model.modelID}`, - availableVariants, - currentVariant: nextVariant, - }) - await this.sendConfigOptionsUpdate(sessionID, configOptions) - } - } const agent = session.modeId ?? (await AgentModule.defaultAgent()) const parts: Array< @@ -1411,16 +1188,6 @@ export namespace ACP { }) } - function detectConfigOptionsSupport(params: InitializeRequest): boolean { - const override = Flag.OPENCODE_ACP_CONFIG_OPTIONS_FALLBACK?.toLowerCase() - if (override) { - if (["1", "true", "on", "force", "fallback"].includes(override)) return false - if (["0", "false", "off", "disable", "disabled"].includes(override)) return true - } - // Default to fallback unless client explicitly signals config options support. - return params.clientCapabilities?._meta?.["config_options"] === true - } - function formatModelIdWithVariant( model: { providerID: string; modelID: string }, variant: string | undefined, @@ -1432,175 +1199,50 @@ export namespace ACP { return `${base}/${variant}` } + function buildVariantMeta(input: { + model: { providerID: string; modelID: string } + variant?: string + availableVariants: string[] + }) { + return { + opencode: { + modelId: `${input.model.providerID}/${input.model.modelID}`, + variant: input.variant ?? null, + availableVariants: input.availableVariants, + }, + } + } + function parseModelSelection( modelId: string, - providers: Array<{ id: string; name: string; models: Record }>, - ) { + providers: Array<{ id: string; models: Record }> }>, + ): { model: { providerID: string; modelID: string }; variant?: string } { const parsed = Provider.parseModel(modelId) - const provider = providers.find((entry) => entry.id === parsed.providerID) - if (!provider) return { model: { providerID: parsed.providerID, modelID: parsed.modelID }, variant: undefined } + const provider = providers.find((p) => p.id === parsed.providerID) + if (!provider) { + return { model: parsed, variant: undefined } + } + // Check if modelID exists directly if (provider.models[parsed.modelID]) { - return { model: { providerID: parsed.providerID, modelID: parsed.modelID }, variant: undefined } + return { model: parsed, variant: undefined } } + // Try to extract variant from end of modelID (e.g., "claude-sonnet-4/high" -> model: "claude-sonnet-4", variant: "high") const segments = parsed.modelID.split("/") if (segments.length > 1) { const candidateVariant = segments[segments.length - 1] const baseModelId = segments.slice(0, -1).join("/") - if (provider.models[baseModelId]) { - const baseModel = { providerID: parsed.providerID, modelID: baseModelId } - const availableVariants = modelVariantsFromProviders(providers, baseModel) - if (availableVariants.includes(candidateVariant)) { - return { model: baseModel, variant: candidateVariant } + const baseModelInfo = provider.models[baseModelId] + if (baseModelInfo?.variants && candidateVariant in baseModelInfo.variants) { + return { + model: { providerID: parsed.providerID, modelID: baseModelId }, + variant: candidateVariant, } } } - return { model: { providerID: parsed.providerID, modelID: parsed.modelID }, variant: undefined } - } - - function buildConfigOptions(input: { - availableModes: ModeOption[] - currentModeId?: string - availableModels: ModelOption[] - currentModelId: string - availableVariants: string[] - currentVariant?: string - }): SessionConfigOption[] { - const configOptions: SessionConfigOption[] = [] - - const modeOption = buildModeConfigOption({ - availableModes: input.availableModes, - currentModeId: input.currentModeId, - }) - if (modeOption) configOptions.push(modeOption) - - const modelOption = buildModelConfigOption({ - availableModels: input.availableModels, - currentModelId: input.currentModelId, - }) - if (modelOption) configOptions.push(modelOption) - - const variantOption = buildVariantConfigOption({ - modelId: input.currentModelId, - availableVariants: input.availableVariants, - currentVariant: input.currentVariant, - }) - if (variantOption) configOptions.push(variantOption) - - return configOptions - } - - function buildModeConfigOption(input: { - availableModes: ModeOption[] - currentModeId?: string - }): SessionConfigOption | undefined { - if (!input.availableModes.length || !input.currentModeId) return undefined - - const options: SessionConfigSelectOption[] = input.availableModes.map((mode) => ({ - name: mode.name, - value: mode.id, - description: mode.description ?? undefined, - })) - const category: SessionConfigOptionCategory = "mode" - - return { - id: "mode", - name: "Mode", - type: "select", - currentValue: input.currentModeId, - options, - category, - } - } - - function buildModelConfigOption(input: { - availableModels: ModelOption[] - currentModelId: string - }): SessionConfigOption | undefined { - if (!input.availableModels.length) return undefined - - const options: SessionConfigSelectOption[] = input.availableModels.map((model) => ({ - name: model.name, - value: model.modelId, - })) - const category: SessionConfigOptionCategory = "model" - - return { - id: "model", - name: "Model", - type: "select", - currentValue: input.currentModelId, - options, - category, - } - } - - function buildVariantConfigOption(input: { - modelId: string - availableVariants: string[] - currentVariant?: string - }): SessionConfigOption | undefined { - const variants = input.availableVariants.filter((variant) => variant !== DEFAULT_VARIANT_VALUE) - if (variants.length === 0) return undefined - - const selectedVariant = - input.currentVariant && variants.includes(input.currentVariant) ? input.currentVariant : undefined - const options: SessionConfigSelectOption[] = [ - { - name: "Default", - value: DEFAULT_VARIANT_VALUE, - }, - ...variants.map((variant) => ({ - name: variant, - value: variant, - })), - ] - const category: SessionConfigOptionCategory = "thought_level" - - return { - id: "variant", - name: "Thinking Level", - type: "select", - currentValue: selectedVariant ?? DEFAULT_VARIANT_VALUE, - options, - category, - _meta: buildVariantConfigMeta({ - modelId: input.modelId, - currentVariant: selectedVariant, - availableVariants: variants, - }), - } - } - - function buildVariantConfigMeta(input: { - modelId: string - currentVariant?: string - availableVariants: string[] - }) { - return { - opencode: { - modelId: input.modelId, - currentVariant: input.currentVariant ?? null, - availableVariants: input.availableVariants, - hasVariants: input.availableVariants.length > 0, - }, - } - } - - function buildVariantMeta(input: { - model: { providerID: string; modelID: string } - variant?: string - availableVariants: string[] - }) { - return { - opencode: { - modelId: `${input.model.providerID}/${input.model.modelID}`, - variant: input.variant ?? null, - availableVariants: input.availableVariants, - }, - } + return { model: parsed, variant: undefined } } } diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index 184facc8ba1..92d3359650f 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -18,7 +18,7 @@ export namespace Flag { OPENCODE_DISABLE_CLAUDE_CODE || truthy("OPENCODE_DISABLE_CLAUDE_CODE_PROMPT") export const OPENCODE_DISABLE_CLAUDE_CODE_SKILLS = OPENCODE_DISABLE_CLAUDE_CODE || truthy("OPENCODE_DISABLE_CLAUDE_CODE_SKILLS") - export const OPENCODE_ACP_CONFIG_OPTIONS_FALLBACK = process.env["OPENCODE_ACP_CONFIG_OPTIONS_FALLBACK"] + export const OPENCODE_FAKE_VCS = process.env["OPENCODE_FAKE_VCS"] export const OPENCODE_CLIENT = process.env["OPENCODE_CLIENT"] ?? "cli" export const OPENCODE_SERVER_PASSWORD = process.env["OPENCODE_SERVER_PASSWORD"] From 109bb9ec3f4cc21bb7bb119afc1c42d0663cd3ad Mon Sep 17 00:00:00 2001 From: Mert Can Demir Date: Sun, 18 Jan 2026 00:52:41 +0300 Subject: [PATCH 5/7] refactor: simplify mode and model state resolution logic --- package.json | 2 +- packages/opencode/src/acp/agent.ts | 40 +++++++++++++++++++----------- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 8db569de1e3..f1d6c4fead1 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "AI-powered development tool", "private": true, "type": "module", - "packageManager": "bun@1.3.6", + "packageManager": "bun@1.3.5", "scripts": { "dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts", "typecheck": "bun turbo typecheck", diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index 3d7fcf6b18a..859a6dd5a97 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -700,12 +700,16 @@ export namespace ACP { sessionId: string, ): Promise<{ availableModes: ModeOption[]; currentModeId?: string }> { const availableModes = await this.loadAvailableModes(directory) - let currentModeId = this.sessionManager.get(sessionId).modeId - if (!currentModeId && availableModes.length) { - const defaultAgentName = await AgentModule.defaultAgent() - currentModeId = availableModes.find((mode) => mode.name === defaultAgentName)?.id ?? availableModes[0].id - this.sessionManager.setMode(sessionId, currentModeId) - } + const currentModeId = + this.sessionManager.get(sessionId).modeId || + (await (async () => { + if (!availableModes.length) return undefined + const defaultAgentName = await AgentModule.defaultAgent() + const resolvedModeId = + availableModes.find((mode) => mode.name === defaultAgentName)?.id ?? availableModes[0].id + this.sessionManager.setMode(sessionId, resolvedModeId) + return resolvedModeId + })()) return { availableModes, currentModeId } } @@ -723,8 +727,14 @@ export namespace ACP { this.sessionManager.setVariant(sessionId, undefined) } const availableModels = buildAvailableModels(entries, { includeVariants: true }) - const { availableModes, currentModeId } = await this.resolveModeState(directory, sessionId) - const modeState = currentModeId ? { availableModes, currentModeId } : undefined + const modeState = await this.resolveModeState(directory, sessionId) + const currentModeId = modeState.currentModeId + const modes = currentModeId + ? { + availableModes: modeState.availableModes, + currentModeId, + } + : undefined const commands = await this.config.sdk.command .list( @@ -802,7 +812,7 @@ export namespace ACP { currentModelId: formatModelIdWithVariant(model, currentVariant, availableVariants, true), availableModels, }, - modes: modeState, + modes, _meta: buildVariantMeta({ model, variant: this.sessionManager.getVariant(sessionId), @@ -817,17 +827,17 @@ export namespace ACP { .providers({ directory: session.cwd }, { throwOnError: true }) .then((x) => x.data!.providers) - const { model, variant } = parseModelSelection(params.modelId, providers) - this.sessionManager.setModel(session.id, model) - this.sessionManager.setVariant(session.id, variant) + const selection = parseModelSelection(params.modelId, providers) + this.sessionManager.setModel(session.id, selection.model) + this.sessionManager.setVariant(session.id, selection.variant) const entries = sortProvidersByName(providers) - const availableVariants = modelVariantsFromProviders(entries, model) + const availableVariants = modelVariantsFromProviders(entries, selection.model) return { _meta: buildVariantMeta({ - model, - variant, + model: selection.model, + variant: selection.variant, availableVariants, }), } From 546a460e6ada0db9d1a22075608c735afc0674c0 Mon Sep 17 00:00:00 2001 From: Mert Can Demir Date: Sun, 18 Jan 2026 13:38:49 +0300 Subject: [PATCH 6/7] chore: remove trailing whitespace in flag.ts --- packages/opencode/src/flag/flag.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index 92d3359650f..4cdb549096a 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -18,7 +18,6 @@ export namespace Flag { OPENCODE_DISABLE_CLAUDE_CODE || truthy("OPENCODE_DISABLE_CLAUDE_CODE_PROMPT") export const OPENCODE_DISABLE_CLAUDE_CODE_SKILLS = OPENCODE_DISABLE_CLAUDE_CODE || truthy("OPENCODE_DISABLE_CLAUDE_CODE_SKILLS") - export const OPENCODE_FAKE_VCS = process.env["OPENCODE_FAKE_VCS"] export const OPENCODE_CLIENT = process.env["OPENCODE_CLIENT"] ?? "cli" export const OPENCODE_SERVER_PASSWORD = process.env["OPENCODE_SERVER_PASSWORD"] From 8ee7fc0250a1338df86a6ab53a9862e3a99a0127 Mon Sep 17 00:00:00 2001 From: Mert Can Demir Date: Sun, 18 Jan 2026 13:40:30 +0300 Subject: [PATCH 7/7] fix: add optional chaining for modes in agent restoration --- packages/opencode/src/acp/agent.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index 779827b026c..858465c7d5e 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -518,7 +518,7 @@ export namespace ACP { const lastUser = messages?.findLast((m) => m.info.role === "user")?.info if (lastUser?.role === "user") { result.models.currentModelId = `${lastUser.model.providerID}/${lastUser.model.modelID}` - if (result.modes.availableModes.some((m) => m.id === lastUser.agent)) { + if (result.modes?.availableModes.some((m) => m.id === lastUser.agent)) { result.modes.currentModeId = lastUser.agent } }