فهرست منبع

feat: expose acp thinking variants (#9064)

Co-authored-by: Aiden Cline <[email protected]>
Mert Can Demir 2 ماه پیش
والد
کامیت
fdd484d2c1
5فایلهای تغییر یافته به همراه402 افزوده شده و 259 حذف شده
  1. 202 202
      bun.lock
  2. 1 1
      packages/opencode/package.json
  3. 186 56
      packages/opencode/src/acp/agent.ts
  4. 12 0
      packages/opencode/src/acp/session.ts
  5. 1 0
      packages/opencode/src/acp/types.ts

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 202 - 202
bun.lock


+ 1 - 1
packages/opencode/package.json

@@ -49,7 +49,7 @@
   "dependencies": {
     "@actions/core": "1.11.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/anthropic": "2.0.57",
     "@ai-sdk/azure": "2.0.91",

+ 186 - 56
packages/opencode/src/acp/agent.ts

@@ -26,6 +26,7 @@ import {
   type ToolCallContent,
   type ToolKind,
 } from "@agentclientprotocol/sdk"
+
 import { Log } from "../util/log"
 import { ACPSessionManager } from "./session"
 import type { ACPConfig } from "./types"
@@ -40,6 +41,11 @@ import { LoadAPIKeyError } from "ai"
 import type { Event, OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2"
 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 {
   const log = Log.create({ service: "acp-agent" })
 
@@ -476,7 +482,7 @@ export namespace ACP {
           sessionId,
           models: load.models,
           modes: load.modes,
-          _meta: {},
+          _meta: load._meta,
         }
       } catch (e) {
         const error = MessageV2.fromError(e, {
@@ -529,7 +535,7 @@ export namespace ACP {
             providerID: lastUser.model.providerID,
             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
             this.sessionManager.setMode(sessionId, lastUser.agent)
           }
@@ -956,27 +962,7 @@ 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 loadAvailableModes(directory: string): Promise<ModeOption[]> {
       const agents = await this.config.sdk.app
         .agents(
           {
@@ -986,6 +972,56 @@ 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)
+      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
         .list(
           {
@@ -1006,20 +1042,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<string, Config.Mcp> = {}
       for (const server of params.mcpServers) {
         if ("type" in server) {
@@ -1073,40 +1095,46 @@ export namespace ACP {
       return {
         sessionId,
         models: {
-          currentModelId: `${model.providerID}/${model.modelID}`,
+          currentModelId: formatModelIdWithVariant(model, currentVariant, availableVariants, true),
           availableModels,
         },
-        modes: {
-          availableModes,
-          currentModeId,
-        },
-        _meta: {},
+        modes,
+        _meta: buildVariantMeta({
+          model,
+          variant: this.sessionManager.getVariant(sessionId),
+          availableVariants,
+        }),
       }
     }
 
     async unstable_setSessionModel(params: SetSessionModelRequest) {
       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, {
-        providerID: model.providerID,
-        modelID: model.modelID,
-      })
+      const entries = sortProvidersByName(providers)
+      const availableVariants = modelVariantsFromProviders(entries, selection.model)
 
       return {
-        _meta: {},
+        _meta: buildVariantMeta({
+          model: selection.model,
+          variant: selection.variant,
+          availableVariants,
+        }),
       }
     }
 
     async setSessionMode(params: SetSessionModeRequest): Promise<SetSessionModeResponse | void> {
-      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)
     }
 
@@ -1223,6 +1251,7 @@ export namespace ACP {
             providerID: model.providerID,
             modelID: model.modelID,
           },
+          variant: this.sessionManager.getVariant(sessionID),
           parts,
           agent,
           directory,
@@ -1434,4 +1463,105 @@ export namespace ACP {
     }
     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 }
+  }
 }

+ 12 - 0
packages/opencode/src/acp/session.ts

@@ -96,6 +96,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

+ 1 - 0
packages/opencode/src/acp/types.ts

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

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است