feat: expose acp thinking variants (#9064)

Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
This commit is contained in:
Mert Can Demir 2026-01-29 23:15:36 +03:00 committed by GitHub
parent cd4075faf6
commit fdd484d2c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 529 additions and 544 deletions

816
bun.lock

File diff suppressed because it is too large Load Diff

View File

@ -49,7 +49,7 @@
"dependencies": { "dependencies": {
"@actions/core": "1.11.1", "@actions/core": "1.11.1",
"@actions/github": "6.0.1", "@actions/github": "6.0.1",
"@agentclientprotocol/sdk": "0.12.0", "@agentclientprotocol/sdk": "0.13.0",
"@ai-sdk/amazon-bedrock": "3.0.73", "@ai-sdk/amazon-bedrock": "3.0.73",
"@ai-sdk/anthropic": "2.0.57", "@ai-sdk/anthropic": "2.0.57",
"@ai-sdk/azure": "2.0.91", "@ai-sdk/azure": "2.0.91",

View File

@ -26,6 +26,7 @@ import {
type ToolCallContent, type ToolCallContent,
type ToolKind, type ToolKind,
} from "@agentclientprotocol/sdk" } from "@agentclientprotocol/sdk"
import { Log } from "../util/log" import { Log } from "../util/log"
import { ACPSessionManager } from "./session" import { ACPSessionManager } from "./session"
import type { ACPConfig } from "./types" import type { ACPConfig } from "./types"
@ -40,6 +41,11 @@ import { LoadAPIKeyError } from "ai"
import type { Event, OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2" import type { Event, OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2"
import { applyPatch } from "diff" import { applyPatch } from "diff"
type ModeOption = { id: string; name: string; description?: string }
type ModelOption = { modelId: string; name: string }
const DEFAULT_VARIANT_VALUE = "default"
export namespace ACP { export namespace ACP {
const log = Log.create({ service: "acp-agent" }) const log = Log.create({ service: "acp-agent" })
@ -476,7 +482,7 @@ export namespace ACP {
sessionId, sessionId,
models: load.models, models: load.models,
modes: load.modes, modes: load.modes,
_meta: {}, _meta: load._meta,
} }
} catch (e) { } catch (e) {
const error = MessageV2.fromError(e, { const error = MessageV2.fromError(e, {
@ -529,7 +535,7 @@ export namespace ACP {
providerID: lastUser.model.providerID, providerID: lastUser.model.providerID,
modelID: lastUser.model.modelID, modelID: 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 result.modes.currentModeId = lastUser.agent
this.sessionManager.setMode(sessionId, lastUser.agent) this.sessionManager.setMode(sessionId, lastUser.agent)
} }
@ -956,27 +962,7 @@ export namespace ACP {
} }
} }
private async loadSessionMode(params: LoadSessionRequest) { private async loadAvailableModes(directory: string): Promise<ModeOption[]> {
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}`,
}))
})
const agents = await this.config.sdk.app const agents = await this.config.sdk.app
.agents( .agents(
{ {
@ -986,6 +972,56 @@ export namespace ACP {
) )
.then((resp) => resp.data!) .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)
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 }
}
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 = 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: true })
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 const commands = await this.config.sdk.command
.list( .list(
{ {
@ -1006,20 +1042,6 @@ export namespace ACP {
description: "compact the session", 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<string, Config.Mcp> = {} const mcpServers: Record<string, Config.Mcp> = {}
for (const server of params.mcpServers) { for (const server of params.mcpServers) {
if ("type" in server) { if ("type" in server) {
@ -1073,40 +1095,46 @@ export namespace ACP {
return { return {
sessionId, sessionId,
models: { models: {
currentModelId: `${model.providerID}/${model.modelID}`, currentModelId: formatModelIdWithVariant(model, currentVariant, availableVariants, true),
availableModels, availableModels,
}, },
modes: { modes,
availableModes, _meta: buildVariantMeta({
currentModeId, model,
}, variant: this.sessionManager.getVariant(sessionId),
_meta: {}, availableVariants,
}),
} }
} }
async unstable_setSessionModel(params: SetSessionModelRequest) { async unstable_setSessionModel(params: SetSessionModelRequest) {
const session = this.sessionManager.get(params.sessionId) const session = this.sessionManager.get(params.sessionId)
const providers = await this.sdk.config
.providers({ directory: session.cwd }, { throwOnError: true })
.then((x) => x.data!.providers)
const model = Provider.parseModel(params.modelId) const selection = parseModelSelection(params.modelId, providers)
this.sessionManager.setModel(session.id, selection.model)
this.sessionManager.setVariant(session.id, selection.variant)
this.sessionManager.setModel(session.id, { const entries = sortProvidersByName(providers)
providerID: model.providerID, const availableVariants = modelVariantsFromProviders(entries, selection.model)
modelID: model.modelID,
})
return { return {
_meta: {}, _meta: buildVariantMeta({
model: selection.model,
variant: selection.variant,
availableVariants,
}),
} }
} }
async setSessionMode(params: SetSessionModeRequest): Promise<SetSessionModeResponse | void> { async setSessionMode(params: SetSessionModeRequest): Promise<SetSessionModeResponse | void> {
this.sessionManager.get(params.sessionId) const session = this.sessionManager.get(params.sessionId)
await this.config.sdk.app const availableModes = await this.loadAvailableModes(session.cwd)
.agents({}, { throwOnError: true }) if (!availableModes.some((mode) => mode.id === params.modeId)) {
.then((x) => x.data) throw new Error(`Agent not found: ${params.modeId}`)
.then((agent) => { }
if (!agent) throw new Error(`Agent not found: ${params.modeId}`)
})
this.sessionManager.setMode(params.sessionId, params.modeId) this.sessionManager.setMode(params.sessionId, params.modeId)
} }
@ -1223,6 +1251,7 @@ export namespace ACP {
providerID: model.providerID, providerID: model.providerID,
modelID: model.modelID, modelID: model.modelID,
}, },
variant: this.sessionManager.getVariant(sessionID),
parts, parts,
agent, agent,
directory, directory,
@ -1434,4 +1463,105 @@ export namespace ACP {
} }
return result return result
} }
function sortProvidersByName<T extends { name: string }>(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<string, { variants?: Record<string, any> }> }>,
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 buildAvailableModels(
providers: Array<{ id: string; name: string; models: Record<string, any> }>,
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 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 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; models: Record<string, { variants?: Record<string, any> }> }>,
): { model: { providerID: string; modelID: string }; variant?: string } {
const parsed = Provider.parseModel(modelId)
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: 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("/")
const baseModelInfo = provider.models[baseModelId]
if (baseModelInfo?.variants && candidateVariant in baseModelInfo.variants) {
return {
model: { providerID: parsed.providerID, modelID: baseModelId },
variant: candidateVariant,
}
}
}
return { model: parsed, variant: undefined }
}
} }

View File

@ -96,6 +96,18 @@ export class ACPSessionManager {
return session 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) { setMode(sessionId: string, modeId: string) {
const session = this.get(sessionId) const session = this.get(sessionId)
session.modeId = modeId session.modeId = modeId

View File

@ -10,6 +10,7 @@ export interface ACPSessionState {
providerID: string providerID: string
modelID: string modelID: string
} }
variant?: string
modeId?: string modeId?: string
} }