diff --git a/package.json b/package.json index fa59be8..4645bf4 100644 --- a/package.json +++ b/package.json @@ -253,37 +253,19 @@ "opencode-go", "opencode-go-anthropic", "opencode-zen", - "opencode-zen-anthropic", - "china-coding", - "china-standard", - "china-anthropic", - "international-coding", - "international-standard", - "international-anthropic" + "opencode-zen-anthropic" ], "enumItemLabels": [ "%opencode-for-copilot.config.endpoint.opencodeGo.label%", "%opencode-for-copilot.config.endpoint.opencodeGoAnthropic.label%", "%opencode-for-copilot.config.endpoint.opencodeZen.label%", - "%opencode-for-copilot.config.endpoint.opencodeZenAnthropic.label%", - "%opencode-for-copilot.config.endpoint.chinaCoding.label%", - "%opencode-for-copilot.config.endpoint.chinaStandard.label%", - "%opencode-for-copilot.config.endpoint.chinaAnthropic.label%", - "%opencode-for-copilot.config.endpoint.internationalCoding.label%", - "%opencode-for-copilot.config.endpoint.internationalStandard.label%", - "%opencode-for-copilot.config.endpoint.internationalAnthropic.label%" + "%opencode-for-copilot.config.endpoint.opencodeZenAnthropic.label%" ], "markdownEnumDescriptions": [ "%opencode-for-copilot.config.endpoint.opencodeGo.description%", "%opencode-for-copilot.config.endpoint.opencodeGoAnthropic.description%", "%opencode-for-copilot.config.endpoint.opencodeZen.description%", - "%opencode-for-copilot.config.endpoint.opencodeZenAnthropic.description%", - "%opencode-for-copilot.config.endpoint.chinaCoding.description%", - "%opencode-for-copilot.config.endpoint.chinaStandard.description%", - "%opencode-for-copilot.config.endpoint.chinaAnthropic.description%", - "%opencode-for-copilot.config.endpoint.internationalCoding.description%", - "%opencode-for-copilot.config.endpoint.internationalStandard.description%", - "%opencode-for-copilot.config.endpoint.internationalAnthropic.description%" + "%opencode-for-copilot.config.endpoint.opencodeZenAnthropic.description%" ], "markdownDescription": "%opencode-for-copilot.config.endpoint.description%" }, diff --git a/package.nls.json b/package.nls.json index 78f587c..9bc4c97 100644 --- a/package.nls.json +++ b/package.nls.json @@ -17,26 +17,14 @@ "opencode-for-copilot.walkthrough.title": "OpenCode for Copilot Chat", "opencode-for-copilot.walkthrough.description": "Set up OpenCode Go models in Copilot Chat.", "opencode-for-copilot.walkthrough.setApiKey.title": "Set your OpenCode API key", - "opencode-for-copilot.walkthrough.setApiKey.description": "Subscribe to [OpenCode Go](https://opencode.ai/docs/go) and [get your API key](command:opencode-for-copilot.getApiKey) from opencode.ai, or use a GLM Coding Plan token from bigmodel.cn.\n[Set Go API Key](command:opencode-for-copilot.setGoApiKey)", - "opencode-for-copilot.walkthrough.showModels.title": "Show GLM models", - "opencode-for-copilot.walkthrough.showModels.description": "If the models are hidden, open VS Code's Language Models manager and show the GLM models.\n[Open Language Models](command:workbench.action.chat.manage)", + "opencode-for-copilot.walkthrough.setApiKey.description": "Subscribe to [OpenCode Go](https://opencode.ai/docs/go) or [OpenCode Zen](https://opencode.ai/docs/zen) and [get your API key](command:opencode-for-copilot.getApiKey) from opencode.ai.\n[Set Go API Key](command:opencode-for-copilot.setGoApiKey)", + "opencode-for-copilot.walkthrough.showModels.title": "Show OpenCode models", + "opencode-for-copilot.walkthrough.showModels.description": "If the models are hidden, open VS Code's Language Models manager and show the OpenCode models.\n[Open Language Models](command:workbench.action.chat.manage)", "opencode-for-copilot.walkthrough.advancedSettings.title": "Try advanced settings", "opencode-for-copilot.walkthrough.advancedSettings.description": "Customize providers and model names, configure the vision proxy, enable experimental settings, and more in the extension settings.\n[Open settings](command:opencode-for-copilot.openSettings)", - "opencode-for-copilot.config.title": "GLM Copilot", - "opencode-for-copilot.config.baseUrl.description": "Optional API base URL override. When non-empty, this value has the highest priority and overrides the `endpoint` preset. Leave empty to use the selected `endpoint` preset. Default resolved endpoint is OpenCode Go: `https://opencode.ai/zen/go/v1` (OpenAI protocol) — switch to a Zhipu/Z.ai preset to use the official GLM endpoints.", - "opencode-for-copilot.config.endpoint.description": "Single-value endpoint selector. Each option uniquely resolves to a base URL and wire protocol (OpenAI `/chat/completions` or Anthropic `/v1/messages`). The OpenCode Go options serve the curated open coding model catalogue from https://opencode.ai/docs/go; the China/International options serve the official Zhipu/Z.ai GLM endpoints. Replaces the older `region` + `apiMode` + `apiProtocol` trio. A non-empty `baseUrl` still overrides this preset.", - "opencode-for-copilot.config.endpoint.chinaCoding.label": "China · Coding Plan (OpenAI)", - "opencode-for-copilot.config.endpoint.chinaCoding.description": "Domestic BigModel Coding Plan via OpenAI `/chat/completions` at `https://open.bigmodel.cn/api/coding/paas/v4`. Default.", - "opencode-for-copilot.config.endpoint.chinaStandard.label": "China · Standard API (OpenAI)", - "opencode-for-copilot.config.endpoint.chinaStandard.description": "Domestic BigModel pay-as-you-go API via OpenAI `/chat/completions` at `https://open.bigmodel.cn/api/paas/v4`.", - "opencode-for-copilot.config.endpoint.chinaAnthropic.label": "China · Coding Plan (Anthropic)", - "opencode-for-copilot.config.endpoint.chinaAnthropic.description": "Domestic BigModel Coding Plan via Anthropic `/v1/messages` at `https://open.bigmodel.cn/api/anthropic`. May bypass stricter rate limits on certain models.", - "opencode-for-copilot.config.endpoint.internationalCoding.label": "International · Coding Plan (OpenAI)", - "opencode-for-copilot.config.endpoint.internationalCoding.description": "International Z.ai Coding Plan via OpenAI `/chat/completions` at `https://api.z.ai/api/coding/paas/v4`.", - "opencode-for-copilot.config.endpoint.internationalStandard.label": "International · Standard API (OpenAI)", - "opencode-for-copilot.config.endpoint.internationalStandard.description": "International Z.ai pay-as-you-go API via OpenAI `/chat/completions` at `https://api.z.ai/api/paas/v4`.", - "opencode-for-copilot.config.endpoint.internationalAnthropic.label": "International · Coding Plan (Anthropic)", - "opencode-for-copilot.config.endpoint.internationalAnthropic.description": "International Z.ai Coding Plan via Anthropic `/v1/messages` at `https://api.z.ai/api/anthropic`.", + "opencode-for-copilot.config.title": "OpenCode for Copilot", + "opencode-for-copilot.config.baseUrl.description": "Optional API base URL override. When non-empty, this value has the highest priority and overrides the `endpoint` preset. Leave empty to use the selected OpenCode endpoint preset.", + "opencode-for-copilot.config.endpoint.description": "OpenCode endpoint selector. Each option resolves to a base URL and wire protocol (OpenAI `/chat/completions` or Anthropic `/v1/messages`). Go serves the curated subscription catalogue; Zen serves pay-as-you-go models. A non-empty `baseUrl` overrides this preset.", "opencode-for-copilot.config.endpoint.opencodeGo.label": "OpenCode Go (OpenAI)", "opencode-for-copilot.config.endpoint.opencodeGo.description": "OpenCode Go subscription via OpenAI `/chat/completions` at `https://opencode.ai/zen/go/v1`. Serves GLM-5.2/5.1, Kimi K2.7/K2.6, DeepSeek V4 Pro/Flash, and MiMo V2.5/V2.5 Pro. Default. Get your API key at https://opencode.ai/auth.", "opencode-for-copilot.config.endpoint.opencodeGoAnthropic.label": "OpenCode Go (Anthropic)", @@ -46,12 +34,12 @@ "opencode-for-copilot.config.endpoint.opencodeZenAnthropic.label": "OpenCode Zen (Anthropic)", "opencode-for-copilot.config.endpoint.opencodeZenAnthropic.description": "OpenCode Zen pay-as-you-go via Anthropic `/v1/messages` at `https://opencode.ai/zen`. Serves Claude (Opus, Sonnet, Haiku) and Qwen. Get your API key at https://opencode.ai/auth.", "opencode-for-copilot.config.maxTokens.description": "Maximum number of output tokens per request. Set to 0 to use the API default. Useful for controlling costs.", - "opencode-for-copilot.config.experimental.stabilizeToolList.description": "**Experimental**: improve GLM context-cache hit rate by pre-activating available tools.\n- When the enabled tools list changes across turns, this may improve GLM context-cache hit rate.\n- Requests will include more function definitions, so input tokens may increase. Cache-hit input tokens are billed at a lower price, but still count toward usage.\n- This may add internal preflight tool calls to the current Copilot chat history. If you switch to another model in the same conversation, that model provider may reject or mishandle the replayed history. Start a new chat if model switching behaves unexpectedly.\n\nUse [Configure Tools](command:workbench.action.chat.configureTools) to **view and manage** your tool list:\n\n- 64 or fewer enabled tools: usually no need to enable this unless the tool list still changes across turns.\n- More than 128 enabled tools: not recommended. GLM supports at most 128 functions in one `tools` request. Consider disabling tools you rarely use.", - "opencode-for-copilot.config.debugMode.description": "Controls what diagnostic information GLM Copilot writes. Token usage is always reported to Copilot when the API returns it.\n\n- **Minimal** - Token usage only. No diagnostic logs or request dumps.\n- **Metadata** - Privacy-safe diagnostic metadata (request hashes, prefix overlap, tool schema changes). Does not contain prompt text. View with [`GLM: Show Logs`](command:opencode-for-copilot.showLogs).\n- **Verbose** - Complete request payloads written to disk for local debugging. **Warning: contains sensitive prompt content.** View with [`GLM: Open Request Dumps Folder`](command:opencode-for-copilot.openRequestDumpsFolder).", + "opencode-for-copilot.config.experimental.stabilizeToolList.description": "**Experimental**: improve context-cache hit rate by pre-activating available tools.\n- When the enabled tools list changes across turns, this may improve context-cache hit rate.\n- Requests will include more function definitions, so input tokens may increase. Cache-hit input tokens are billed at a lower price, but still count toward usage.\n- This may add internal preflight tool calls to the current Copilot chat history. If you switch to another model in the same conversation, that model provider may reject or mishandle the replayed history. Start a new chat if model switching behaves unexpectedly.\n\nUse [Configure Tools](command:workbench.action.chat.configureTools) to **view and manage** your tool list:\n\n- 64 or fewer enabled tools: usually no need to enable this unless the tool list still changes across turns.\n- More than 128 enabled tools: not recommended. The API supports at most 128 functions in one `tools` request. Consider disabling tools you rarely use.", + "opencode-for-copilot.config.debugMode.description": "Controls what diagnostic information OpenCode for Copilot writes. Token usage is always reported to Copilot when the API returns it.\n\n- **Minimal** - Token usage only. No diagnostic logs or request dumps.\n- **Metadata** - Privacy-safe diagnostic metadata (request hashes, prefix overlap, tool schema changes). Does not contain prompt text. View with [`OpenCode: Show Logs`](command:opencode-for-copilot.showLogs).\n- **Verbose** - Complete request payloads written to disk for local debugging. **Warning: contains sensitive prompt content.** View with [`OpenCode: Open Request Dumps Folder`](command:opencode-for-copilot.openRequestDumpsFolder).", "opencode-for-copilot.config.debugMode.minimal.label": "Minimal", "opencode-for-copilot.config.debugMode.minimal.description": "Token usage only. No diagnostic logs or dumps.", "opencode-for-copilot.config.debugMode.metadata.label": "Metadata", - "opencode-for-copilot.config.debugMode.metadata.description": "Privacy-safe metadata. Safe to share publicly. View with `GLM: Show Logs`.", + "opencode-for-copilot.config.debugMode.metadata.description": "Privacy-safe metadata. Safe to share publicly. View with `OpenCode: Show Logs`.", "opencode-for-copilot.config.debugMode.verbose.label": "Verbose", "opencode-for-copilot.config.debugMode.verbose.description": "Contains sensitive prompt content. For local debugging only.", "opencode-for-copilot.config.ponytailMode.description": "Injects a lazy-senior-dev system instruction before each request. The model is asked to verify the need for code, check the codebase/stdlib/dependencies, and prefer deletion over addition.", @@ -60,19 +48,19 @@ "opencode-for-copilot.config.ponytailMode.lite.label": "Lite", "opencode-for-copilot.config.ponytailMode.lite.description": "Gentle reminder to reuse existing code and avoid unnecessary dependencies.", "opencode-for-copilot.config.ponytailMode.full.label": "Full", - "opencode-for-copilot.config.ponytailMode.full.description": "Full Ponytail ladder: YAGNI → codebase → stdlib → platform → dependencies → one-liner → minimum code. Default.", + "opencode-for-copilot.config.ponytailMode.full.description": "Full Ponytail ladder: YAGNI → codebase → stdlib → platform → dependencies → one-liner → minimum code. Default.", "opencode-for-copilot.config.ponytailMode.ultra.label": "Ultra", "opencode-for-copilot.config.ponytailMode.ultra.description": "Aggressive minimalism. Question over-engineering and justify every character.", - "opencode-for-copilot.config.codeSimplifier.description": "Enable the Code Simplifier autonomous refinement agent (on by default). The model proactively reviews recently modified code and simplifies it for clarity, consistency, and maintainability — reducing nesting, improving naming, and eliminating redundancy while preserving all original behaviour. When enabled, Ponytail is automatically lowered to Lite for compatibility.", + "opencode-for-copilot.config.codeSimplifier.description": "Enable the Code Simplifier autonomous refinement agent (on by default). The model proactively reviews recently modified code and simplifies it for clarity, consistency, and maintainability — reducing nesting, improving naming, and eliminating redundancy while preserving all original behaviour. When enabled, Ponytail is automatically lowered to Lite for compatibility.", "opencode-for-copilot.config.stripThinkTags.description": "Strip leaked reasoning tags ( Müd, ``, ``) from model output. Some models leak these into content instead of routing them through the thinking channel.", "opencode-for-copilot.config.stripThinkTags.auto.label": "Auto", "opencode-for-copilot.config.stripThinkTags.auto.description": "Strip think tags only for models known to leak them (MiniMax M2 family).", "opencode-for-copilot.config.stripThinkTags.always.label": "Always", "opencode-for-copilot.config.stripThinkTags.always.description": "Strip think tags for all models.", "opencode-for-copilot.config.stripThinkTags.never.label": "Never", - "opencode-for-copilot.config.stripThinkTags.never.description": "Never strip think tags — show raw model output.", - "opencode-for-copilot.config.modelIdOverrides.description": "Override the API model ID sent for any built-in or custom GLM model. Defaults are prefilled with official GLM IDs; add custom model keys only when the endpoint expects a different API model name.", - "opencode-for-copilot.config.customModels.description": "Additional GLM-compatible models shown in the Copilot model picker. Use either a string model ID or an object with `id`, optional display `name`, token limits, `toolCalling`, and `thinking`. Custom model IDs override built-in IDs in the picker and lookup. Image attachments are still converted through the existing Vision Proxy; custom models do not bypass the proxy or use native vision.", + "opencode-for-copilot.config.stripThinkTags.never.description": "Never strip think tags — show raw model output.", + "opencode-for-copilot.config.modelIdOverrides.description": "Override the API model ID sent for any built-in or custom model. Defaults are prefilled with OpenCode catalogue IDs; add custom model keys only when the endpoint expects a different API model name.", + "opencode-for-copilot.config.customModels.description": "Additional OpenCode-compatible models shown in the Copilot model picker. Use either a string model ID or an object with `id`, optional display `name`, token limits, `toolCalling`, and `thinking`. Custom model IDs override built-in IDs in the picker and lookup. Image attachments are still converted through the existing Vision Proxy; custom models do not bypass the proxy or use native vision.", "opencode-for-copilot.config.customModels.string.description": "Custom model ID. Defaults are applied for name, token limits, tool calling, thinking, and Vision Proxy image support.", "opencode-for-copilot.config.customModels.id.description": "Model ID shown in VS Code and used for requests unless `modelIdOverrides` maps it to another API model ID.", "opencode-for-copilot.config.customModels.name.description": "Display name. Defaults to the model ID.", @@ -80,13 +68,13 @@ "opencode-for-copilot.config.customModels.maxOutputTokens.description": "Maximum output tokens shown to Copilot. Defaults to 131072.", "opencode-for-copilot.config.customModels.toolCalling.description": "Whether this custom model supports tool calling. Defaults to true.", "opencode-for-copilot.config.customModels.thinking.description": "Whether this custom model supports thinking mode. Defaults to true.", - "opencode-for-copilot.config.visionModel.description": "Compatibility setting managed by [Configure Vision Proxy](command:opencode-for-copilot.setVisionModel). Stores the selected VS Code vision model as `vendor/id` when that source is active. In automatic mode, GLM-4.6V-Flash is tried first and this VS Code model is used as fallback. Legacy bare model IDs are read for compatibility.", - "opencode-for-copilot.config.visionPrompt.description": "Prompt sent to the vision proxy model when describing image attachments before forwarding them to GLM.\n\n[Configure Vision Proxy](command:opencode-for-copilot.setVisionModel)", + "opencode-for-copilot.config.visionModel.description": "Compatibility setting managed by [Configure Vision Proxy](command:opencode-for-copilot.setVisionModel). Stores the selected VS Code vision model as `vendor/id` when that source is active. In automatic mode, an OpenCode vision model is tried first and this VS Code model is used as fallback. Legacy bare model IDs are read for compatibility.", + "opencode-for-copilot.config.visionPrompt.description": "Prompt sent to the vision proxy model when describing image attachments before forwarding them to the chat model.\n\n[Configure Vision Proxy](command:opencode-for-copilot.setVisionModel)", "opencode-for-copilot.config.agentRoles.description": "Models used by the pipeline's agent roles: research (parallel research step) and review (final code review).", "opencode-for-copilot.config.agentRoles.research.description": "Models used for the parallel research step. Areas are assigned round-robin across the listed models, so multiple models run research concurrently.", "opencode-for-copilot.config.agentRoles.review.description": "Models used for the final code review step.", - "opencode-for-copilot.config.agentRoles.implementFallback.description": "Models tried in order after the chat-selected implementer is unavailable (e.g. provider outage, quota exhausted). When unset, the audited free models are used as a default safety net — out-of-the-box users get fallback with zero config.", - "opencode-for-copilot.config.rules.description": "User-defined rules, injected as a `### USER RULES` block at the top of the system message for every coding request (main-agent and background). Each entry becomes one bullet — use them to enforce project conventions like `Always use TypeScript rather than JavaScript`, `Keep responses concise`, or `Return curly braces on the next line`. Rules are stripped entirely on utility requests (chat-title, git-commit, etc.) so they don't pollute the prompt cache. Empty / whitespace-only entries are dropped automatically.", - "opencode-for-copilot.config.allowExtraTools.description": "**Experimental:** let the agent swarm call tools beyond its curated whitelist.\n\nBy default, research / review / implement agents only see the curated set of built-in VS Code tools (read_file, apply_patch, runTests, …). This keeps prompts small and under GLM's 128-tool cap. When enabled, MCP-discovered tools and other Copilot-registered external tools the user has set up are also forwarded to the agents — useful for database, browser, or ticketing MCP servers.\n\n- Off (default): only curated built-in tools reach the swarm.\n- On: curated tools first, then extras appended after them. The 128-tool hard cap still applies; curated tools are always kept before extras when the list is truncated.\n\nRead-only extras are also pulled into the research / review pool when their name looks read-only (`read`, `query`, `search`, `list`, `fetch`, `get`, `resolve`, `describe`). Mutators stay in the implementer pool only.", - "opencode-for-copilot.config.auditFreeModelProbeMs.description": "Per-model probe timeout (milliseconds) for the agent swarm's free-tier audit. When `opencode-for-copilot.agentRoles` is NOT set, each `@swarm` run starts by probing every free OpenCode model in parallel and ranking the responders by latency; this setting bounds a single probe. The audit's wall-clock is the *slowest* probe (all probes run in parallel), so the audit adds at most this much to the start of each run.\n\nDefault 6000ms (6s). Tune down on slow connections to truncate the audit — if a probe doesn't complete in time, the model is marked ✗(`timeout`) and the swarm routes through the rest. Minimum 500ms, maximum 30000ms. The audit is skipped entirely when you pin `agentRoles.*`, in which case this setting has no effect." -} + "opencode-for-copilot.config.agentRoles.implementFallback.description": "Models tried in order after the chat-selected implementer is unavailable (e.g. provider outage, quota exhausted). When unset, the audited free models are used as a default safety net — out-of-the-box users get fallback with zero config.", + "opencode-for-copilot.config.rules.description": "User-defined rules, injected as a `### USER RULES` block at the top of the system message for every coding request (main-agent and background). Each entry becomes one bullet — use them to enforce project conventions like `Always use TypeScript rather than JavaScript`, `Keep responses concise`, or `Return curly braces on the next line`. Rules are stripped entirely on utility requests (chat-title, git-commit, etc.) so they don't pollute the prompt cache. Empty / whitespace-only entries are dropped automatically.", + "opencode-for-copilot.config.allowExtraTools.description": "**Experimental:** let the agent swarm call tools beyond its curated whitelist.\n\nBy default, research / review / implement agents only see the curated set of built-in VS Code tools (read_file, apply_patch, runTests, …). This keeps prompts small and under the 128-tool cap. When enabled, MCP-discovered tools and other Copilot-registered external tools the user has set up are also forwarded to the agents — useful for database, browser, or ticketing MCP servers.\n\n- Off (default): only curated built-in tools reach the swarm.\n- On: curated tools first, then extras appended after them. The 128-tool hard cap still applies; curated tools are always kept before extras when the list is truncated.\n\nRead-only extras are also pulled into the research / review pool when their name looks read-only (`read`, `query`, `search`, `list`, `fetch`, `get`, `resolve`, `describe`). Mutators stay in the implementer pool only.", + "opencode-for-copilot.config.auditFreeModelProbeMs.description": "Per-model probe timeout (milliseconds) for the agent swarm's free-tier audit. When `opencode-for-copilot.agentRoles` is NOT set, each `@swarm` run starts by probing every free OpenCode model in parallel and ranking the responders by latency; this setting bounds a single probe. The audit's wall-clock is the *slowest* probe (all probes run in parallel), so the audit adds at most this much to the start of each run.\n\nDefault 6000ms (6s). Tune down on slow connections to truncate the audit — if a probe doesn't complete in time, the model is marked ✗(`timeout`) and the swarm routes through the rest. Minimum 500ms, maximum 30000ms. The audit is skipped entirely when you pin `agentRoles.*`, in which case this setting has no effect." +} \ No newline at end of file diff --git a/package.nls.zh-cn.json b/package.nls.zh-cn.json index cdec31c..95d6952 100644 --- a/package.nls.zh-cn.json +++ b/package.nls.zh-cn.json @@ -18,26 +18,14 @@ "opencode-for-copilot.walkthrough.title": "OpenCode for Copilot Chat", "opencode-for-copilot.walkthrough.description": "在 Copilot Chat 中配置 OpenCode Go 模型。", "opencode-for-copilot.walkthrough.setApiKey.title": "设置你的 OpenCode API Key", - "opencode-for-copilot.walkthrough.setApiKey.description": "订阅 [OpenCode Go](https://opencode.ai/docs/go) 并从 opencode.ai [获取 API Key](command:opencode-for-copilot.getApiKey),或使用 bigmodel.cn 的 GLM Coding Plan Token。\n[设置 Go API Key](command:opencode-for-copilot.setGoApiKey)", - "opencode-for-copilot.walkthrough.showModels.title": "显示 GLM 模型", - "opencode-for-copilot.walkthrough.showModels.description": "如果模型被隐藏,打开 VS Code 的语言模型管理器并显示 GLM 模型。\n[打开语言模型管理](command:workbench.action.chat.manage)", + "opencode-for-copilot.walkthrough.setApiKey.description": "订阅 [OpenCode Go](https://opencode.ai/docs/go) 或 [OpenCode Zen](https://opencode.ai/docs/zen),并从 opencode.ai [获取 API Key](command:opencode-for-copilot.getApiKey)。\n[设置 Go API Key](command:opencode-for-copilot.setGoApiKey)", + "opencode-for-copilot.walkthrough.showModels.title": "显示 OpenCode 模型", + "opencode-for-copilot.walkthrough.showModels.description": "如果模型被隐藏,打开 VS Code 的语言模型管理器并显示 OpenCode 模型。\n[打开语言模型管理](command:workbench.action.chat.manage)", "opencode-for-copilot.walkthrough.advancedSettings.title": "尝试高级设置", "opencode-for-copilot.walkthrough.advancedSettings.description": "可以在插件设置中自定义提供方与模型名称、自定义视觉代理、开启实验性设置等。\n[打开设置](command:opencode-for-copilot.openSettings)", - "opencode-for-copilot.config.title": "GLM 助手", - "opencode-for-copilot.config.baseUrl.description": "可选的 API 基础 URL 覆盖项。非空时优先级最高,会覆盖 `endpoint` 预设;留空时使用选定的 `endpoint` 预设。默认解析到 OpenCode Go:`https://opencode.ai/zen/go/v1`(OpenAI 协议)——如需使用官方 GLM 端点,请切换到 Zhipu/Z.ai 预设。", - "opencode-for-copilot.config.endpoint.description": "单值 endpoint 选择器。每个选项唯一对应一个基础 URL 和线路协议(OpenAI `/chat/completions` 或 Anthropic `/v1/messages`)。OpenCode Go 选项提供 https://opencode.ai/docs/go 上的精选开放编程模型;国内/国际选项提供官方 Zhipu/Z.ai GLM 端点。取代旧的 `region` + `apiMode` + `apiProtocol` 三件套。`baseUrl` 非空时仍会覆盖此预设。", - "opencode-for-copilot.config.endpoint.chinaCoding.label": "国内 · Coding Plan(OpenAI)", - "opencode-for-copilot.config.endpoint.chinaCoding.description": "国内 BigModel Coding Plan,走 OpenAI `/chat/completions`:`https://open.bigmodel.cn/api/coding/paas/v4`。默认值。", - "opencode-for-copilot.config.endpoint.chinaStandard.label": "国内 · 通用 API(OpenAI)", - "opencode-for-copilot.config.endpoint.chinaStandard.description": "国内 BigModel 按量付费通用 API,走 OpenAI `/chat/completions`:`https://open.bigmodel.cn/api/paas/v4`。", - "opencode-for-copilot.config.endpoint.chinaAnthropic.label": "国内 · Coding Plan(Anthropic)", - "opencode-for-copilot.config.endpoint.chinaAnthropic.description": "国内 BigModel Coding Plan,走 Anthropic `/v1/messages`:`https://open.bigmodel.cn/api/anthropic`。可能绕过某些模型的更严格速率限制。", - "opencode-for-copilot.config.endpoint.internationalCoding.label": "国际 · Coding Plan(OpenAI)", - "opencode-for-copilot.config.endpoint.internationalCoding.description": "国际 Z.ai Coding Plan,走 OpenAI `/chat/completions`:`https://api.z.ai/api/coding/paas/v4`。", - "opencode-for-copilot.config.endpoint.internationalStandard.label": "国际 · 通用 API(OpenAI)", - "opencode-for-copilot.config.endpoint.internationalStandard.description": "国际 Z.ai 按量付费通用 API,走 OpenAI `/chat/completions`:`https://api.z.ai/api/paas/v4`。", - "opencode-for-copilot.config.endpoint.internationalAnthropic.label": "国际 · Coding Plan(Anthropic)", - "opencode-for-copilot.config.endpoint.internationalAnthropic.description": "国际 Z.ai Coding Plan,走 Anthropic `/v1/messages`:`https://api.z.ai/api/anthropic`。", + "opencode-for-copilot.config.title": "OpenCode for Copilot", + "opencode-for-copilot.config.baseUrl.description": "可选的 API 基础 URL 覆盖项。非空时优先级最高,会覆盖 `endpoint` 预设;留空时使用选定的 OpenCode endpoint 预设。", + "opencode-for-copilot.config.endpoint.description": "OpenCode endpoint 选择器。每个选项对应一个基础 URL 和线路协议(OpenAI `/chat/completions` 或 Anthropic `/v1/messages`)。Go 提供订阅精选模型;Zen 提供按量付费模型。`baseUrl` 非空时仍会覆盖此预设。", "opencode-for-copilot.config.endpoint.opencodeGo.label": "OpenCode Go(OpenAI)", "opencode-for-copilot.config.endpoint.opencodeGo.description": "OpenCode Go 订阅,走 OpenAI `/chat/completions`:`https://opencode.ai/zen/go/v1`。提供 GLM-5.2/5.1、Kimi K2.7/K2.6、DeepSeek V4 Pro/Flash、MiMo V2.5/V2.5 Pro。默认值。在 https://opencode.ai/auth 获取 API Key。", "opencode-for-copilot.config.endpoint.opencodeGoAnthropic.label": "OpenCode Go(Anthropic)", @@ -47,12 +35,12 @@ "opencode-for-copilot.config.endpoint.opencodeZenAnthropic.label": "OpenCode Zen(Anthropic)", "opencode-for-copilot.config.endpoint.opencodeZenAnthropic.description": "OpenCode Zen 按量付费,走 Anthropic `/v1/messages`:`https://opencode.ai/zen`。提供 Claude(Opus、Sonnet、Haiku)和 Qwen。在 https://opencode.ai/auth 获取 API Key。", "opencode-for-copilot.config.maxTokens.description": "每次请求的最大输出 Token 数,设为 0 则使用 API 默认值,可用于控制成本。", - "opencode-for-copilot.config.experimental.stabilizeToolList.description": "**实验性功能**:通过预先激活可用的工具来提升 GLM 上下文缓存命中率。\n- 当已启用工具列表跨轮次变化时,这可能提高 GLM 上下文缓存命中率;\n- 请求中将包含更多函数工具定义,input tokens 可能增加,虽然缓存命中的 input tokens 单价更低,但仍会计入用量;\n- 此设置可能会在当前 Copilot 对话历史中加入内部预检工具调用。在同一对话中切换到其他模型时,部分模型提供方可能无法正确重放这段历史;如果切换模型后请求异常,请新建对话后重试。\n\n通过 [配置工具](command:workbench.action.chat.configureTools) **查看和管理**工具列表:\n\n- 64 个或更少已启用工具时通常无需开启,除非工具列表仍在跨轮次变化;\n- 超过 128 个已启用工具时不建议开启:GLM 单次 `tools` 请求最多支持 128 个 functions。考虑禁用部分不常用工具。", - "opencode-for-copilot.config.debugMode.description": "控制 GLM Copilot 写入的诊断信息量。API 返回 token 用量时会始终上报给 Copilot。\n\n- **基本** - 仅上报 token 用量,不输出诊断日志或请求 dump。\n- **元数据** - 隐私安全的诊断元数据(请求哈希、前缀重合度、工具定义变更)。不含提示词原文。使用 [`GLM: 显示日志`](command:opencode-for-copilot.showLogs) 查看。\n- **详细** - 将完整请求体写入磁盘,供本地调试。**警告:包含敏感的提示词内容。** 使用 [`GLM: 打开请求 Dump 目录`](command:opencode-for-copilot.openRequestDumpsFolder) 浏览。", + "opencode-for-copilot.config.experimental.stabilizeToolList.description": "**实验性功能**:通过预先激活可用的工具来提升上下文缓存命中率。\n- 当已启用工具列表跨轮次变化时,这可能提高上下文缓存命中率;\n- 请求中将包含更多函数工具定义,input tokens 可能增加,虽然缓存命中的 input tokens 单价更低,但仍会计入用量;\n- 此设置可能会在当前 Copilot 对话历史中加入内部预检工具调用。在同一对话中切换到其他模型时,部分模型提供方可能无法正确重放这段历史;如果切换模型后请求异常,请新建对话后重试。\n\n通过 [配置工具](command:workbench.action.chat.configureTools) **查看和管理**工具列表:\n\n- 64 个或更少已启用工具时通常无需开启,除非工具列表仍在跨轮次变化;\n- 超过 128 个已启用工具时不建议开启:API 单次 `tools` 请求最多支持 128 个 functions。考虑禁用部分不常用工具。", + "opencode-for-copilot.config.debugMode.description": "控制 OpenCode for Copilot 写入的诊断信息量。API 返回 token 用量时会始终上报给 Copilot。\n\n- **基本** - 仅上报 token 用量,不输出诊断日志或请求 dump。\n- **元数据** - 隐私安全的诊断元数据(请求哈希、前缀重合度、工具定义变更)。不含提示词原文。使用 [`OpenCode: 显示日志`](command:opencode-for-copilot.showLogs) 查看。\n- **详细** - 将完整请求体写入磁盘,供本地调试。**警告:包含敏感的提示词内容。** 使用 [`OpenCode: 打开请求 Dump 目录`](command:opencode-for-copilot.openRequestDumpsFolder) 浏览。", "opencode-for-copilot.config.debugMode.minimal.label": "基本", "opencode-for-copilot.config.debugMode.minimal.description": "仅上报 token 用量,不输出诊断日志或 dump。", "opencode-for-copilot.config.debugMode.metadata.label": "元数据", - "opencode-for-copilot.config.debugMode.metadata.description": "隐私安全的元数据,可安全公开分享。使用 `GLM: 显示日志` 查看。", + "opencode-for-copilot.config.debugMode.metadata.description": "隐私安全的元数据,可安全公开分享。使用 `OpenCode: 显示日志` 查看。", "opencode-for-copilot.config.debugMode.verbose.label": "详细", "opencode-for-copilot.config.debugMode.verbose.description": "包含敏感的提示词内容,仅供本地调试。", "opencode-for-copilot.config.ponytailMode.description": "在每次请求前注入“懒惰资深开发者”系统提示。模型会先在代码库、标准库、已安装依赖中查找可用方案,避免过度编码。", @@ -72,8 +60,8 @@ "opencode-for-copilot.config.stripThinkTags.always.description": "对所有模型剥离思考标签。", "opencode-for-copilot.config.stripThinkTags.never.label": "从不", "opencode-for-copilot.config.stripThinkTags.never.description": "从不剥离思考标签——显示原始模型输出。", - "opencode-for-copilot.config.modelIdOverrides.description": "覆盖任意内置或自定义 GLM 模型实际发送到 API 的模型 ID。默认预填官方 GLM ID;只有 endpoint 需要不同 API 模型名时才需要为自定义模型添加映射。", - "opencode-for-copilot.config.customModels.description": "额外显示在 Copilot 模型选择器里的 GLM 兼容模型。可以写字符串模型 ID,也可以写对象:`id`、可选显示名 `name`、token 上限、`toolCalling`、`thinking`。自定义模型 ID 与内置 ID 冲突时会覆盖内置模型的 picker 与查找结果。图片附件仍会先经过现有视觉代理转成文本;自定义模型不会绕过 proxy,也不启用 native vision。", + "opencode-for-copilot.config.modelIdOverrides.description": "覆盖任意内置或自定义模型实际发送到 API 的模型 ID。默认预填 OpenCode 目录 ID;只有 endpoint 需要不同 API 模型名时才需要为自定义模型添加映射。", + "opencode-for-copilot.config.customModels.description": "额外显示在 Copilot 模型选择器里的 OpenCode 兼容模型。可以写字符串模型 ID,也可以写对象:`id`、可选显示名 `name`、token 上限、`toolCalling`、`thinking`。自定义模型 ID 与内置 ID 冲突时会覆盖内置模型的 picker 与查找结果。图片附件仍会先经过现有视觉代理转成文本;自定义模型不会绕过 proxy,也不启用 native vision。", "opencode-for-copilot.config.customModels.string.description": "自定义模型 ID。显示名、token 上限、工具调用、思考模式和视觉代理图片支持都会使用默认值。", "opencode-for-copilot.config.customModels.id.description": "显示在 VS Code 中的模型 ID;除非 `modelIdOverrides` 将它映射到其他 API 模型 ID,否则请求也使用这个 ID。", "opencode-for-copilot.config.customModels.name.description": "显示名称,默认等于模型 ID。", @@ -81,8 +69,8 @@ "opencode-for-copilot.config.customModels.maxOutputTokens.description": "展示给 Copilot 的最大输出 token 数,默认 131072。", "opencode-for-copilot.config.customModels.toolCalling.description": "该自定义模型是否支持工具调用,默认 true。", "opencode-for-copilot.config.customModels.thinking.description": "该自定义模型是否支持思考模式,默认 true。", - "opencode-for-copilot.config.visionModel.description": "由[配置视觉代理](command:opencode-for-copilot.setVisionModel)管理的兼容设置;当选择 VS Code 视觉模型来源时,以 `vendor/id` 保存所选模型。自动模式会先尝试 GLM-4.6V-Flash,再将此 VS Code 视觉模型作为回退。旧版裸模型 ID 仍会兼容读取。", - "opencode-for-copilot.config.visionPrompt.description": "在将图片附件转发给 GLM 之前,发送给视觉代理模型的提示词。\n\n[配置视觉代理](command:opencode-for-copilot.setVisionModel)", + "opencode-for-copilot.config.visionModel.description": "由[配置视觉代理](command:opencode-for-copilot.setVisionModel)管理的兼容设置;当选择 VS Code 视觉模型来源时,以 `vendor/id` 保存所选模型。自动模式会先尝试 OpenCode 视觉模型,再将此 VS Code 视觉模型作为回退。旧版裸模型 ID 仍会兼容读取。", + "opencode-for-copilot.config.visionPrompt.description": "在将图片附件转发给聊天模型之前,发送给视觉代理模型的提示词。\n\n[配置视觉代理](command:opencode-for-copilot.setVisionModel)", "opencode-for-copilot.config.agentRoles.description": "流水线各代理角色使用的模型:research(并行研究步骤)与review(最终代码审查)。", "opencode-for-copilot.config.agentRoles.research.description": "并行研究步骤使用的模型。研究领域按轮询分配,配置多个模型可让研究真正并行执行。", "opencode-for-copilot.config.agentRoles.review.description": "最终代码审查步骤使用的模型。", diff --git a/src/client/consts.ts b/src/client/consts.ts index 72e1223..1874725 100644 --- a/src/client/consts.ts +++ b/src/client/consts.ts @@ -12,23 +12,18 @@ export const API_PROVIDER_HTTP_ERROR_LINKS: Readonly< Record>>> > = { 401: { - glm: { + opencode: { labelKey: 'error.action.createApiKey', - url: EXTERNAL_URLS.glm.apiKeys, + url: EXTERNAL_URLS.opencode.apiKeys, }, }, 402: { - glm: { + opencode: { labelKey: 'error.action.viewUsage', - url: EXTERNAL_URLS.glm.usage, - }, - }, - '5xx': { - glm: { - labelKey: 'error.action.checkGLMStatus', - url: EXTERNAL_URLS.glm.status, + url: EXTERNAL_URLS.opencode.usage, }, }, + '5xx': {}, }; /** @@ -59,14 +54,14 @@ export const GLM_BUSINESS_ERROR_CODES: Readonly = { - 'open.bigmodel.cn': 'api.z.ai', - 'api.z.ai': 'open.bigmodel.cn', -}; - -function resolveFailoverBaseUrl(baseUrl: string): string | undefined { - try { - const url = new URL(baseUrl); - const altHost = FAILOVER_HOST_MAP[url.hostname]; - if (!altHost) { - return undefined; - } - url.hostname = altHost; - return url.toString(); - } catch { - return undefined; - } -} diff --git a/src/client/error/index.ts b/src/client/error/index.ts index c365275..c6b93ac 100644 --- a/src/client/error/index.ts +++ b/src/client/error/index.ts @@ -1,4 +1,4 @@ -import { isOfficialGLMBaseUrl, isOpencodeBaseUrl } from '../../endpoint'; +import { isOpencodeBaseUrl } from '../../endpoint'; import { t } from '../../i18n'; import { safeStringify } from '../../json'; import { @@ -205,7 +205,7 @@ function getHttpErrorMessage(params: { toolCount?: number; }): string { const { status, baseUrl, businessCode, serverMessage, toolCount } = params; - const isOfficialGlm = isOfficialGLMBaseUrl(baseUrl); + const isManagedOpencode = isOpencodeBaseUrl(baseUrl); // OpenCode Zen-only model + Go-subscription key → 401 "Insufficient balance". // The raw server message is just a billing link; explain that the model @@ -224,7 +224,7 @@ function getHttpErrorMessage(params: { // 1) 已知业务错误码 → 使用官方错误表对应的精确文案。GLM 的服务端消息通常 // 是 `[code][detail][request_id]` 包裹格式,detail 里包含动态参数 // (如重置时间),比模板更准确,所以优先透出 detail。 - if (businessCode && isOfficialGlm) { + if (businessCode && isManagedOpencode) { const definition = GLM_BUSINESS_ERROR_CODES[businessCode]; if (definition) { return formatGlmBusinessMessage(definition.messageKey, businessCode, serverMessage); @@ -233,7 +233,7 @@ function getHttpErrorMessage(params: { // 2) 官方端点 + 未知业务码 → 仍然剥离 GLM 包裹格式中的 request_id 噪音, // 只透出 [code][detail] 部分给用户。 - if (isOfficialGlm && serverMessage) { + if (isManagedOpencode && serverMessage) { const detail = extractGlmMessageDetail(serverMessage); if (detail) { return businessCode ? `${detail} (code ${businessCode})` : `${detail} (HTTP ${status})`; @@ -450,7 +450,7 @@ function getGlmBusinessCodeAction( baseUrl: string, actionUrls: ErrorActionUrls, ): readonly ErrorActionLink[] | undefined { - if (!businessCode || !isOfficialGLMBaseUrl(baseUrl)) { + if (!businessCode || !isOpencodeBaseUrl(baseUrl)) { return undefined; } const definition = GLM_BUSINESS_ERROR_CODES[businessCode]; @@ -556,7 +556,7 @@ function escapeBoldText(value: string): string { } function identifyApiProvider(baseUrl: string): ApiProviderId | undefined { - return isOfficialGLMBaseUrl(baseUrl) ? 'glm' : undefined; + return isOpencodeBaseUrl(baseUrl) ? 'opencode' : undefined; } function getHttpErrorLinkStatusKey(status: number): HttpErrorLinkStatusKey | undefined { diff --git a/src/client/types.ts b/src/client/types.ts index f093628..b2831e9 100644 --- a/src/client/types.ts +++ b/src/client/types.ts @@ -22,7 +22,7 @@ export interface HttpErrorLinkDefinition { url: string; } -export type ApiProviderId = 'glm'; +export type ApiProviderId = 'opencode'; export type HttpErrorLinkStatusKey = 401 | 402 | '5xx'; export type GLMRequestErrorKind = 'http' | 'network' | 'unknown'; diff --git a/src/config.ts b/src/config.ts index 1dd1992..1ea043b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,80 +1,63 @@ import vscode from 'vscode'; import { CONFIG_SECTION, LEGACY_CONFIG_SECTION, MODELS } from './consts'; import { - deriveEndpointPreset, - normalizeBaseUrl, - resolveEndpointApiKeyUrl, - resolveEndpointBaseUrl, - resolveEndpointProtocol, - resolvePlanDefaultEndpoint, - type OpencodePlan + normalizeBaseUrl, + normalizeLegacyEndpointPreset, + resolveEndpointApiKeyUrl, + resolveEndpointBaseUrl, + resolveEndpointProtocol, + resolvePlanDefaultEndpoint, + type OpencodePlan, } from './endpoint'; -import { - getDynamicModels -} from './provider/opencode-models'; +import { getDynamicModels } from './provider/opencode-models'; import type { PonytailMode } from './provider/ponytail'; import type { - ApiMode, - ApiProtocol, - ApiRegion, - CustomModelConfig, - EndpointPreset, - ModelDefinition, + ApiProtocol, + CustomModelConfig, + EndpointPreset, + ModelDefinition, } from './types'; export type DebugMode = 'minimal' | 'metadata' | 'verbose'; -const DEFAULT_API_MODE: ApiMode = 'coding-plan'; -const DEFAULT_API_REGION: ApiRegion = 'china'; const DEFAULT_API_PROTOCOL: ApiProtocol = 'openai'; -const CUSTOM_MODEL_DETAIL = 'Custom GLM-compatible model'; +const CUSTOM_MODEL_DETAIL = 'Custom OpenCode-compatible model'; const CUSTOM_MODEL_MAX_INPUT_TOKENS = 200_000; const CUSTOM_MODEL_MAX_OUTPUT_TOKENS = 131_072; +const UTILITY_GO_DEFAULT_API_MODEL = 'deepseek-v4-flash'; +const UTILITY_ZEN_DEFAULT_API_MODEL = 'deepseek-v4-flash-free'; + /** - * Get GLM API base URL from settings. + * OpenCode API base URL. * * Resolution order: - * 1. `baseUrl` override (highest priority — covers advanced/proxy use cases) - * 2. `endpoint` preset (new single-value selector) - * 3. Legacy (region, apiMode, apiProtocol) tuple — transparently mapped to - * a preset so existing user settings keep working without migration. + * 1. `baseUrl` override (proxy / custom gateway) + * 2. `endpoint` preset (including legacy GLM preset strings mapped to OpenCode) + * 3. Active OpenCode plan default (`opencode-go` / `opencode-zen`) */ export function getBaseUrl(): string { const override = getBaseUrlOverride(); if (override) { return override; } - - const preset = getEndpoint(); - return resolveEndpointBaseUrl(preset); + return resolveEndpointBaseUrl(getEndpoint()); } export function getBaseUrlOverride(): string | undefined { const config = vscode.workspace.getConfiguration(CONFIG_SECTION); const value = config.get('baseUrl', ''); - // Guard against non-string values in settings.json that would crash normalizeBaseUrl().trim() const normalized = normalizeBaseUrl(typeof value === 'string' ? value : ''); return normalized || undefined; } -export function getApiMode(): ApiMode { - const config = vscode.workspace.getConfiguration(CONFIG_SECTION); - return normalizeApiMode(config.get('apiMode'), DEFAULT_API_MODE) ?? DEFAULT_API_MODE; -} - -export function getRegion(): ApiRegion { - const config = vscode.workspace.getConfiguration(CONFIG_SECTION); - return normalizeApiRegion(config.get('region'), DEFAULT_API_REGION) ?? DEFAULT_API_REGION; -} - /** - * Get the single-value endpoint preset. + * Active endpoint preset. * * Resolution order: - * 1. Explicit `endpoint` setting (always wins) - * 2. Explicitly configured legacy (region, apiMode, apiProtocol) tuple - * 3. The active OpenCode plan's default endpoint (`opencode-go` / `opencode-zen`) + * 1. Explicit `endpoint` setting (legacy GLM values are migrated on read) + * 2. Legacy `apiProtocol=anthropic` without `endpoint` → plan's Anthropic preset + * 3. Active OpenCode plan default */ export function getEndpoint(): EndpointPreset { const config = vscode.workspace.getConfiguration(CONFIG_SECTION); @@ -82,13 +65,12 @@ export function getEndpoint(): EndpointPreset { if (explicit) { return explicit; } - if (hasExplicitLegacyEndpointConfig()) { - return deriveEndpointFromLegacy(); + if (hasExplicitLegacyApiProtocol()) { + return getOpencodePlan() === 'zen' ? 'opencode-zen-anthropic' : 'opencode-go-anthropic'; } return resolvePlanDefaultEndpoint(getOpencodePlan()); } -/** Which OpenCode plan's key and model catalog to use — `opencodePlan` setting. */ export function getOpencodePlan(): OpencodePlan { const config = vscode.workspace.getConfiguration(CONFIG_SECTION); return config.get('opencodePlan') === 'zen' ? 'zen' : 'go'; @@ -112,7 +94,6 @@ const LEGACY_SETTING_KEYS = [ 'ponytailMode', 'codeSimplifier', 'stripThinkTags', - // Legacy keys no longer contributed but still read for backward compatibility. 'apiKey', 'region', 'apiMode', @@ -123,13 +104,6 @@ const LEGACY_SETTING_KEYS = [ const SETTINGS_MIGRATION_KEY = 'opencode-for-copilot.settingsMigratedFromLegacy.version'; const SETTINGS_MIGRATION_VERSION = 1; -/** - * One-time copy of user-set `glm-copilot.*` values into the new - * `opencode-for-copilot.*` section. The old section is shared with the - * upstream GLM extension (same origin); reading it live would couple the two - * extensions' configuration. After this runs, the legacy section is never - * read again — both extensions can coexist with independent settings. - */ export async function migrateLegacySettings(context: vscode.ExtensionContext): Promise { if (context.globalState.get(SETTINGS_MIGRATION_KEY, 0) >= SETTINGS_MIGRATION_VERSION) { return; @@ -158,90 +132,47 @@ export async function migrateLegacySettings(context: vscode.ExtensionContext): P await context.globalState.update(SETTINGS_MIGRATION_KEY, SETTINGS_MIGRATION_VERSION); } -function hasExplicitLegacyEndpointConfig(): boolean { +function hasExplicitLegacyApiProtocol(): boolean { const config = vscode.workspace.getConfiguration(CONFIG_SECTION); - return (['region', 'apiMode', 'apiProtocol'] as const).some((key) => { - const inspection = config.inspect(key); - return ( - inspection?.globalValue !== undefined || - inspection?.workspaceValue !== undefined || - inspection?.workspaceFolderValue !== undefined - ); - }); + const inspection = config.inspect('apiProtocol'); + return ( + inspection?.globalValue !== undefined || + inspection?.workspaceValue !== undefined || + inspection?.workspaceFolderValue !== undefined + ); } -/** - * Get the wire protocol implied by the active endpoint preset. - * - * `baseUrl` override does not change the protocol — users pointing at a - * custom gateway still pick the protocol shape explicitly via `endpoint`. - */ export function getApiProtocol(): ApiProtocol { const preset = getEndpoint(); - const protocol = resolveEndpointProtocol(preset); - // Preserve the legacy explicit `apiProtocol` override path: when a user has - // NOT set the new `endpoint` but DID set `apiProtocol`, that intent still - // wins so custom-baseUrl users keep their chosen protocol shape. - const config = vscode.workspace.getConfiguration(CONFIG_SECTION); - const explicitEndpoint = normalizeEndpointPreset(config.get('endpoint')); - if (explicitEndpoint) { - return protocol; - } - const legacyProtocol = normalizeApiProtocol(config.get('apiProtocol'), protocol); - return legacyProtocol ?? protocol; + return resolveEndpointProtocol(preset); } export function getApiKeyUrl(): string { - // Follows the same resolution as getEndpoint(): explicit preset, then - // explicitly-configured legacy tuple, then the active OpenCode plan. return resolveEndpointApiKeyUrl(getEndpoint()); } -function deriveEndpointFromLegacy(): EndpointPreset { - const region = getRegion(); - const apiMode = getApiMode(); - const apiProtocol = getApiProtocolLegacy(); - return deriveEndpointPreset(region, apiMode, apiProtocol); -} - -function getApiProtocolLegacy(): ApiProtocol { - const config = vscode.workspace.getConfiguration(CONFIG_SECTION); - return ( - normalizeApiProtocol(config.get('apiProtocol'), DEFAULT_API_PROTOCOL) ?? - DEFAULT_API_PROTOCOL - ); -} - function normalizeEndpointPreset(value: unknown): EndpointPreset | undefined { - if ( - value === 'china-coding' || - value === 'china-standard' || - value === 'china-anthropic' || - value === 'international-coding' || - value === 'international-standard' || - value === 'international-anthropic' || - value === 'opencode-go' || - value === 'opencode-go-anthropic' || - value === 'opencode-zen' || - value === 'opencode-zen-anthropic' - ) { - return value; - } - return undefined; + return normalizeLegacyEndpointPreset(value); } -/** - * Resolve the API model ID to send to the endpoint. - * - * Users can override model IDs via the `modelIdOverrides` setting object - * (e.g. for third-party API proxies). Falls back to the VS Code model ID - * when no override is configured. - */ export function getApiModelId(vscodeModelId: string): string { const override = getModelIdOverrides()[vscodeModelId]?.trim(); + if (isUtilityModelId(vscodeModelId)) { + const plan = getOpencodePlan(); + if (plan === 'zen') { + if (override && override !== UTILITY_GO_DEFAULT_API_MODEL) { + return override; + } + return UTILITY_ZEN_DEFAULT_API_MODEL; + } + } return override || vscodeModelId; } +function isUtilityModelId(modelId: string): boolean { + return modelId === 'copilot-utility' || modelId === 'copilot-utility-small'; +} + export function getModelIdOverrides(): Record { const config = vscode.workspace.getConfiguration(CONFIG_SECTION); const raw = config.get>('modelIdOverrides'); @@ -273,14 +204,8 @@ export function getCustomModels(): ModelDefinition[] { return [...byId.values()]; } -/** - * Dynamic model list override. When set by `refreshDynamicModels()`, this is - * used instead of the static `MODELS` array. This lets us serve live model - * lists from the OpenCode API while keeping the static array as a fallback. - */ let dynamicModelsOverride: readonly ModelDefinition[] | undefined; -/** Build the merged model map: dynamic/static base + custom overrides. */ function buildModelMap(): Map { const source = dynamicModelsOverride ?? MODELS; const byId = new Map(source.map((model) => [model.id, model])); @@ -290,21 +215,10 @@ function buildModelMap(): Map { return byId; } -/** - * Synchronous model list — used by the model picker, request handler, and tests. - * Returns dynamic models if available, otherwise falls back to static MODELS. - */ export function listProviderModels(): ModelDefinition[] { return [...buildModelMap().values()]; } -/** - * Asynchronously refresh the model list from the OpenCode API. - * Updates `dynamicModelsOverride` so the next call to - * `listProviderModels()` returns fresh data. - * - * On network failure the existing list (or static fallback) stays in place. - */ export async function refreshDynamicModels(): Promise { const customModels = getCustomModels(); const fallback = dynamicModelsOverride ?? MODELS; @@ -315,42 +229,25 @@ export function findModelDefinition(modelId: string): ModelDefinition | undefine return buildModelMap().get(modelId); } -/** - * Get the configured max output tokens limit. - * Returns `undefined` when set to 0 (API default — no limit). - */ export function getMaxTokens(): number | undefined { const config = vscode.workspace.getConfiguration(CONFIG_SECTION); const value = config.get('maxTokens', 0); - // Guard against Infinity (e.g. from misconfiguration) which would satisfy - // value > 0 but produce an invalid API request. return Number.isFinite(value) && value > 0 ? value : undefined; } -/** - * Diagnostic mode. `verbose` also enables metadata logs. - * - * The legacy boolean `debug` setting is still read as a fallback so old - * settings keep working even if migration cannot update every scope. - */ export function getDebugMode(): DebugMode { const config = vscode.workspace.getConfiguration(CONFIG_SECTION); const mode = getConfiguredDebugMode(config); - if (mode) return mode; - + if (mode) { + return mode; + } return config.get('debug', false) ? 'metadata' : 'minimal'; } -/** - * Whether to log privacy-preserving diagnostic debug information. - */ export function getDebugLoggingEnabled(): boolean { return getDebugMode() !== 'minimal'; } -/** - * Whether to write full GLM request payloads to disk. - */ export function getRequestDumpEnabled(): boolean { return getDebugMode() === 'verbose'; } @@ -360,10 +257,6 @@ export function getStabilizeToolListEnabled(): boolean { return config.get('experimental.stabilizeToolList', false); } -/** - * Prefix model picker names with their provider (e.g. "Kimi · Kimi K3") so - * models stay identifiable when the picker truncates long names. - */ export function getShowProviderPrefix(): boolean { const config = vscode.workspace.getConfiguration(CONFIG_SECTION); return config.get('showProviderPrefix', false); @@ -371,15 +264,6 @@ export function getShowProviderPrefix(): boolean { export type StripThinkTagsMode = 'auto' | 'always' | 'never'; -/** - * Controls stripping of leaked think tags (``, ``, `deliberation`) - * from model output. Some models (MiniMax M2, DeepSeek) leak these into content - * instead of routing them through `reasoning_content`. - * - * - `auto` (default): strip only for models known to leak (MiniMax M2 family) - * - `always`: strip for all models - * - `never`: never strip - */ export function getStripThinkTagsMode(): StripThinkTagsMode { const config = vscode.workspace.getConfiguration(CONFIG_SECTION); const value = config.get('stripThinkTags', 'auto'); @@ -395,8 +279,6 @@ export function getPonytailMode(): PonytailMode { const config = vscode.workspace.getConfiguration(CONFIG_SECTION); const value = config.get('ponytailMode'); const raw = normalizePonytailMode(value) ?? DEFAULT_PONYTAIL_MODE; - // Code Simplifier's proactive review conflicts with Ponytail full/ultra's - // "be brief, never volunteer." Downgrade to lite so both coexist cleanly. if (getCodeSimplifierEnabled() && (raw === 'full' || raw === 'ultra')) { return 'lite'; } @@ -410,7 +292,6 @@ function normalizePonytailMode(value: unknown): PonytailMode | undefined { return undefined; } -/** Whether the Code Simplifier autonomous refinement agent is enabled. */ export function getCodeSimplifierEnabled(): boolean { const config = vscode.workspace.getConfiguration(CONFIG_SECTION); return config.get('codeSimplifier', false); @@ -489,24 +370,6 @@ function normalizeDebugMode(value: unknown): DebugMode | undefined { return undefined; } -function normalizeApiMode(value: unknown, fallback: ApiMode | undefined): ApiMode | undefined { - return value === 'coding-plan' || value === 'standard' ? value : fallback; -} - -function normalizeApiProtocol( - value: unknown, - fallback: ApiProtocol | undefined, -): ApiProtocol | undefined { - return value === 'openai' || value === 'anthropic' ? value : fallback; -} - -function normalizeApiRegion( - value: unknown, - fallback: ApiRegion | undefined, -): ApiRegion | undefined { - return value === 'china' || value === 'international' ? value : fallback; -} - function normalizeCustomModel(entry: unknown): ModelDefinition | undefined { const model = readCustomModelConfig(entry); if (!model) { @@ -540,11 +403,9 @@ function readCustomModelConfig(entry: unknown): CustomModelConfig | undefined { if (typeof entry === 'string') { return { id: entry }; } - if (!entry || typeof entry !== 'object' || Array.isArray(entry)) { return undefined; } - return entry as CustomModelConfig; } diff --git a/src/consts.ts b/src/consts.ts index cdd4968..b5130c0 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -20,16 +20,9 @@ export const CONFIG_SECTION = 'opencode-for-copilot'; export const LEGACY_CONFIG_SECTION = 'glm-copilot'; export const EXTERNAL_URLS = { - glm: { - apiKeys: 'https://www.bigmodel.cn/usercenter/proj-mgmt/apikeys', - usage: 'https://www.bigmodel.cn/usercenter/resourcepack', - status: 'https://docs.bigmodel.cn/cn/api/status-code/status-code-v4', - // 1113 账户欠费、402 余额不足 等场景的充值入口。 - topUp: 'https://www.bigmodel.cn/usercenter/proj-mgmt/resourcepack', - // 1309 GLM Coding Plan 套餐到期、1311 套餐未包含模型 等场景的续订入口。 - codingPlan: 'https://bigmodel.cn/claude-code', - // 1313 公平使用策略被限制时的解除入口(个人中心-编程套餐总览)。 - fairUsePolicy: 'https://www.bigmodel.cn/usercenter/valuepack', + opencode: { + apiKeys: 'https://opencode.ai/auth', + usage: 'https://opencode.ai/auth', }, } as const; diff --git a/src/endpoint.ts b/src/endpoint.ts index 9f5ae03..9f2610f 100644 --- a/src/endpoint.ts +++ b/src/endpoint.ts @@ -1,24 +1,6 @@ -import type { ApiMode, ApiRegion, EndpointPreset } from './types'; - -export const GLM_CN_API_HOST = 'open.bigmodel.cn'; -export const GLM_CN_LEGACY_API_HOST = 'dev.bigmodel.cn'; -export const GLM_INTERNATIONAL_API_HOST = 'api.z.ai'; - -export const GLM_CN_CODING_BASE_URL = `https://${GLM_CN_API_HOST}/api/coding/paas/v4`; -export const GLM_CN_GENERAL_BASE_URL = `https://${GLM_CN_API_HOST}/api/paas/v4`; -export const GLM_CN_ANTHROPIC_BASE_URL = `https://${GLM_CN_API_HOST}/api/anthropic`; -export const GLM_INTERNATIONAL_CODING_BASE_URL = `https://${GLM_INTERNATIONAL_API_HOST}/api/coding/paas/v4`; -export const GLM_INTERNATIONAL_GENERAL_BASE_URL = `https://${GLM_INTERNATIONAL_API_HOST}/api/paas/v4`; -export const GLM_INTERNATIONAL_ANTHROPIC_BASE_URL = `https://${GLM_INTERNATIONAL_API_HOST}/api/anthropic`; +import type { EndpointPreset } from './types'; // ---- OpenCode Go (https://opencode.ai/docs/go) ---- -// -// OpenCode Go is a low-cost subscription that serves a curated set of open -// coding models behind a single API key. The OpenAI-compatible endpoint is -// reached at `…/v1/chat/completions` and the Anthropic-compatible endpoint at -// `…/v1/messages`. Because the client appends `/chat/completions` (OpenAI) or -// `/v1/messages` (Anthropic) to the base URL, the two presets use different -// base URLs so the final request URLs line up exactly with the docs. export const OPENCODE_GO_API_HOST = 'opencode.ai'; export const OPENCODE_GO_OPENAI_BASE_URL = `https://${OPENCODE_GO_API_HOST}/zen/go/v1`; export const OPENCODE_GO_ANTHROPIC_BASE_URL = `https://${OPENCODE_GO_API_HOST}/zen/go`; @@ -27,48 +9,24 @@ export const OPENCODE_GO_API_KEY_URL = 'https://opencode.ai/auth'; export const OPENCODE_GO_USAGE_CONSOLE_URL = 'https://opencode.ai/auth'; // ---- OpenCode Zen (https://opencode.ai/docs/zen) ---- -// -// OpenCode Zen is a pay-as-you-go AI gateway that serves a curated set of -// coding models. The OpenAI-compatible endpoint is at `…/v1/chat/completions` -// and the Anthropic-compatible endpoint at `…/v1/messages`. Because the client -// appends `/chat/completions` (OpenAI) or `/v1/messages` (Anthropic), the two -// presets need different base URLs so the final request URLs line up exactly. export const OPENCODE_ZEN_OPENAI_BASE_URL = `https://${OPENCODE_GO_API_HOST}/zen/v1`; export const OPENCODE_ZEN_ANTHROPIC_BASE_URL = `https://${OPENCODE_GO_API_HOST}/zen`; export const OPENCODE_ZEN_API_KEY_URL = 'https://opencode.ai/auth'; -/** - * Default endpoint preset — domestic Coding Plan over the OpenAI protocol. - */ -export const DEFAULT_ENDPOINT_PRESET: EndpointPreset = 'china-coding'; - -export const GLM_CN_CODING_API_KEY_URL = 'https://bigmodel.cn/coding-plan/personal/overview'; -export const GLM_CN_GENERAL_API_KEY_URL = 'https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys'; -export const GLM_INTERNATIONAL_CODING_API_KEY_URL = 'https://z.ai/manage-apikey/subscription'; -export const GLM_INTERNATIONAL_GENERAL_API_KEY_URL = 'https://z.ai/manage-apikey/apikey-list'; +/** OpenCode plans with separate API keys and model catalogs. */ +export type OpencodePlan = 'go' | 'zen'; -export type OfficialGLMPlatform = 'zhipu' | 'zai'; +const LEGACY_ENDPOINT_PRESETS: Readonly> = { + 'china-coding': 'opencode-go', + 'china-standard': 'opencode-go', + 'china-anthropic': 'opencode-go-anthropic', + 'international-coding': 'opencode-zen', + 'international-standard': 'opencode-zen', + 'international-anthropic': 'opencode-zen-anthropic', +}; -/** - * Resolve the base URL for a single `endpoint` preset value. - * - * The preset encodes region + mode + protocol in one enum, removing the - * combinatorial confusion of the legacy region/apiMode/apiProtocol trio. - */ export function resolveEndpointBaseUrl(preset: EndpointPreset): string { switch (preset) { - case 'china-coding': - return GLM_CN_CODING_BASE_URL; - case 'china-standard': - return GLM_CN_GENERAL_BASE_URL; - case 'china-anthropic': - return GLM_CN_ANTHROPIC_BASE_URL; - case 'international-coding': - return GLM_INTERNATIONAL_CODING_BASE_URL; - case 'international-standard': - return GLM_INTERNATIONAL_GENERAL_BASE_URL; - case 'international-anthropic': - return GLM_INTERNATIONAL_ANTHROPIC_BASE_URL; case 'opencode-go': return OPENCODE_GO_OPENAI_BASE_URL; case 'opencode-go-anthropic': @@ -80,23 +38,8 @@ export function resolveEndpointBaseUrl(preset: EndpointPreset): string { } } -/** - * Resolve the "request an API key" landing page for a single preset value. - */ export function resolveEndpointApiKeyUrl(preset: EndpointPreset): string { switch (preset) { - case 'china-coding': - return GLM_CN_CODING_API_KEY_URL; - case 'china-standard': - return GLM_CN_GENERAL_API_KEY_URL; - case 'china-anthropic': - return GLM_CN_CODING_API_KEY_URL; - case 'international-coding': - return GLM_INTERNATIONAL_CODING_API_KEY_URL; - case 'international-standard': - return GLM_INTERNATIONAL_GENERAL_API_KEY_URL; - case 'international-anthropic': - return GLM_INTERNATIONAL_CODING_API_KEY_URL; case 'opencode-go': case 'opencode-go-anthropic': return OPENCODE_GO_API_KEY_URL; @@ -106,76 +49,28 @@ export function resolveEndpointApiKeyUrl(preset: EndpointPreset): string { } } -/** - * The wire protocol implied by a preset value. - */ export function resolveEndpointProtocol(preset: EndpointPreset): 'openai' | 'anthropic' { - return preset === 'china-anthropic' || - preset === 'international-anthropic' || - preset === 'opencode-go-anthropic' || - preset === 'opencode-zen-anthropic' + return preset === 'opencode-go-anthropic' || preset === 'opencode-zen-anthropic' ? 'anthropic' : 'openai'; } -/** - * Map the legacy (region, apiMode, apiProtocol) tuple onto the closest - * `endpoint` preset. Used to migrate existing user settings transparently. - * - * `apiProtocol === "anthropic"` wins over `apiMode` because the protocol - * uniquely implies the Anthropic endpoint path, while `apiMode` only varies - * the OpenAI-style path. - */ -export function deriveEndpointPreset( - region: ApiRegion, - apiMode: ApiMode, - apiProtocol: 'openai' | 'anthropic', -): EndpointPreset { - if (apiProtocol === 'anthropic') { - return region === 'international' ? 'international-anthropic' : 'china-anthropic'; - } - if (region === 'international') { - return apiMode === 'standard' ? 'international-standard' : 'international-coding'; - } - return apiMode === 'standard' ? 'china-standard' : 'china-coding'; -} - -export function resolveApiKeyUrl(apiMode: ApiMode, region: ApiRegion): string { - if (region === 'international') { - return apiMode === 'standard' - ? GLM_INTERNATIONAL_GENERAL_API_KEY_URL - : GLM_INTERNATIONAL_CODING_API_KEY_URL; - } - return apiMode === 'standard' ? GLM_CN_GENERAL_API_KEY_URL : GLM_CN_CODING_API_KEY_URL; -} - -export function identifyOfficialGLMPlatform(baseUrl: string): OfficialGLMPlatform | undefined { - try { - const host = new URL(baseUrl).hostname.toLowerCase(); - if (host === GLM_INTERNATIONAL_API_HOST) { - return 'zai'; - } - if (host === GLM_CN_API_HOST || host === GLM_CN_LEGACY_API_HOST) { - return 'zhipu'; - } - return undefined; - } catch { +/** Map legacy GLM endpoint preset strings onto the nearest OpenCode preset. */ +export function normalizeLegacyEndpointPreset(value: unknown): EndpointPreset | undefined { + if (typeof value !== 'string') { return undefined; } + if ( + value === 'opencode-go' || + value === 'opencode-go-anthropic' || + value === 'opencode-zen' || + value === 'opencode-zen-anthropic' + ) { + return value; + } + return LEGACY_ENDPOINT_PRESETS[value]; } -export function isOfficialGLMBaseUrl(baseUrl: string): boolean { - return identifyOfficialGLMPlatform(baseUrl) !== undefined; -} - -/** - * Whether a base URL points at the OpenCode Go subscription endpoint. - * - * OpenCode Go is intentionally NOT classified as an "official GLM" platform: - * it does not accept GLM-specific request flags (e.g. `tool_stream`) and its - * error model differs from the Zhipu/Z.ai business error codes. Pricing for - * OpenCode Go is USD, resolved separately in `getPricingCurrencyForBaseUrl`. - */ export function isOpencodeBaseUrl(baseUrl: string): boolean { try { return new URL(normalizeBaseUrl(baseUrl)).hostname.toLowerCase() === OPENCODE_GO_API_HOST; @@ -184,10 +79,11 @@ export function isOpencodeBaseUrl(baseUrl: string): boolean { } } -/** The two OpenCode plans with separate API keys and model catalogs. */ -export type OpencodePlan = 'go' | 'zen'; +/** OpenCode-managed hosts where the extension may tune helper requests. */ +export function isManagedEndpointBaseUrl(baseUrl: string): boolean { + return isOpencodeBaseUrl(baseUrl); +} -/** Which OpenCode plan a base URL serves — `undefined` for non-OpenCode hosts. */ export function resolveOpencodePlanForBaseUrl(baseUrl: string): OpencodePlan | undefined { if (!isOpencodeBaseUrl(baseUrl)) { return undefined; @@ -204,7 +100,6 @@ export function resolveOpencodePlanForBaseUrl(baseUrl: string): OpencodePlan | u } } -/** Endpoint preset used when nothing is explicitly configured. */ export function resolvePlanDefaultEndpoint(plan: OpencodePlan): EndpointPreset { return plan === 'zen' ? 'opencode-zen' : 'opencode-go'; } diff --git a/src/provider/index.ts b/src/provider/index.ts index 1b4cb7e..e66d03d 100644 --- a/src/provider/index.ts +++ b/src/provider/index.ts @@ -10,11 +10,7 @@ import { refreshDynamicModels, } from '../config'; import { API_KEY_GO_SECRET, API_KEY_SECRET, API_KEY_ZEN_SECRET, CONFIG_SECTION } from '../consts'; -import { - isOpencodeBaseUrl, - OPENCODE_GO_USAGE_CONSOLE_URL, - type OpencodePlan, -} from '../endpoint'; +import { OPENCODE_GO_USAGE_CONSOLE_URL, isOpencodeBaseUrl } from '../endpoint'; import { t } from '../i18n'; import { logger } from '../logger'; import { createCacheDiagnosticsRecorder, dumpProviderInput } from './debug'; @@ -29,7 +25,6 @@ import { resolveConversationSegment } from './segment'; import { streamChatCompletion } from './stream'; import { estimateTokenCount } from './tokens'; import { processToolFlow } from './tools/flow'; -import { formatGLMPlanUsageForLog, queryGLMPlanUsage, supportsGLMPlanUsage } from './usage'; import { createVisionService } from './vision'; // ---- Request deduplication for utility kinds ---- @@ -184,36 +179,19 @@ export class GLMChatProvider implements vscode.LanguageModelChatProvider { } async queryUsage(): Promise { - const apiKey = await this.authManager.getApiKey(); - if (!apiKey) { - void vscode.window.showWarningMessage(t('usage.notConfigured')); - return; - } - const baseUrl = getBaseUrl(); - // OpenCode Go does not expose the GLM monitor API; point subscribers at - // the OpenCode console where Go usage limits are tracked. - if (isOpencodeBaseUrl(baseUrl)) { - void vscode.window.showInformationMessage(t('usage.opencodeConsole')); - await vscode.env.openExternal(vscode.Uri.parse(OPENCODE_GO_USAGE_CONSOLE_URL)); - return; - } - if (!supportsGLMPlanUsage(baseUrl)) { + if (!isOpencodeBaseUrl(baseUrl)) { void vscode.window.showWarningMessage(t('usage.unsupportedBaseUrl')); return; } - - logger.show(); - logger.info(t('usage.queryStarted')); - try { - const usage = await queryGLMPlanUsage(baseUrl, apiKey); - logger.info(formatGLMPlanUsageForLog(usage)); - void vscode.window.showInformationMessage(t('usage.querySucceeded')); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - logger.warn('Failed to query GLM Coding Plan usage', error); - void vscode.window.showErrorMessage(t('usage.queryFailed', message)); + const plan = getOpencodePlan(); + const hasKey = await this.authManager.hasPlanApiKey(plan); + if (!hasKey) { + void vscode.window.showWarningMessage(t('usage.notConfigured')); + return; } + void vscode.window.showInformationMessage(t('usage.opencodeConsole')); + await vscode.env.openExternal(vscode.Uri.parse(OPENCODE_GO_USAGE_CONSOLE_URL)); } async hasApiKey(): Promise { diff --git a/src/provider/pricing/currency.ts b/src/provider/pricing/currency.ts index 3fe3361..38d592b 100644 --- a/src/provider/pricing/currency.ts +++ b/src/provider/pricing/currency.ts @@ -1,25 +1,7 @@ -import { identifyOfficialGLMPlatform, isOpencodeBaseUrl, normalizeBaseUrl } from '../../endpoint'; +import { isOpencodeBaseUrl, normalizeBaseUrl } from '../../endpoint'; import type { PricingCurrency } from '../../types'; -/** - * The GLM domestic and international endpoints expose different currencies. - * There is no stable balance endpoint in the OpenAI-compatible API path, so - * model-picker pricing uses the endpoint host instead of probing account state. - * - * OpenCode Go (https://opencode.ai/docs/go) is billed in USD regardless of the - * user's region, so it always resolves to USD. - */ +/** OpenCode Go and Zen expose USD pricing in the model picker. */ export function getPricingCurrencyForBaseUrl(baseUrl: string): PricingCurrency | undefined { - const normalized = normalizeBaseUrl(baseUrl); - if (isOpencodeBaseUrl(normalized)) { - return 'USD'; - } - const platform = identifyOfficialGLMPlatform(normalized); - if (platform === 'zhipu') { - return 'CNY'; - } - if (platform === 'zai') { - return 'USD'; - } - return undefined; + return isOpencodeBaseUrl(normalizeBaseUrl(baseUrl)) ? 'USD' : undefined; } diff --git a/src/provider/request.ts b/src/provider/request.ts index 860d27c..1f7ab44 100644 --- a/src/provider/request.ts +++ b/src/provider/request.ts @@ -2,17 +2,22 @@ import vscode from 'vscode'; import { AuthManager } from '../auth'; import { GLMClient } from '../client'; import { - findModelDefinition, - getApiModelId, - getApiProtocol, - getBaseUrl, - getBaseUrlOverride, - getCodeSimplifierEnabled, - getMaxTokens, - getPonytailMode, - getRules, + findModelDefinition, + getApiModelId, + getApiProtocol, + getBaseUrl, + getBaseUrlOverride, + getCodeSimplifierEnabled, + getMaxTokens, + getOpencodePlan, + getPonytailMode, + getRules, } from '../config'; -import { isOfficialGLMBaseUrl, resolveEndpointBaseUrl, resolveEndpointProtocol } from '../endpoint'; +import { + isManagedEndpointBaseUrl, + resolveEndpointBaseUrl, + resolveEndpointProtocol, +} from '../endpoint'; import { t } from '../i18n'; import type { ApiProtocol, GLMRequest, ModelDefinition, PricingCurrency } from '../types'; import { injectCodeSimplifierSystemMessage } from './code-simplifier'; @@ -22,7 +27,7 @@ import { getConfiguredThinkingEffort, type ModelConfigurationOptions } from './m import { injectPonytailSystemMessage } from './ponytail'; import { getPricingCurrencyForBaseUrl } from './pricing/currency'; import type { ReplayMarkerMetadata } from './replay'; -import { shouldForceThinkingNone, type RequestKind } from './routing'; +import { resolveRequestMaxTokens, shouldForceThinkingNone, type RequestKind } from './routing'; import { injectRulesSystemMessage } from './rules'; import type { ConversationSegment } from './segment'; import { REQUEST_KINDS_ELIGIBLE_FOR_TOOL_TRIMMING } from './tools/consts'; @@ -53,6 +58,22 @@ export function clearClientCache(): void { clientCache.clear(); } +/** Utility models follow the active OpenCode plan endpoint. */ +function resolvePinnedEndpoint( + modelId: string | undefined, + preset: ModelDefinition['endpointPreset'], +): ModelDefinition['endpointPreset'] { + if (!preset?.startsWith('opencode-')) { + return preset; + } + const isUtility = + modelId === 'copilot-utility' || modelId === 'copilot-utility-small'; + if (isUtility && getOpencodePlan() === 'zen') { + return preset === 'opencode-go-anthropic' ? 'opencode-zen-anthropic' : 'opencode-zen'; + } + return preset; +} + export interface PreparedChatRequest { client: GLMClient; request: GLMRequest; @@ -103,7 +124,7 @@ export async function prepareChatRequest({ // pinned protocol is respected so the request shape matches what the model // expects. Unpinned models (e.g. GLM-5.2) follow the active endpoint preset. const baseUrlOverride = getBaseUrlOverride(); - const modelEndpoint = modelDef?.endpointPreset; + const modelEndpoint = resolvePinnedEndpoint(modelInfo.id, modelDef?.endpointPreset); let baseUrl: string; let apiProtocol: ApiProtocol; if (baseUrlOverride) { @@ -124,7 +145,7 @@ export async function prepareChatRequest({ } const client = getCachedClient(baseUrl, apiKey, apiProtocol); const isThinkingModel = modelDef?.capabilities.thinking ?? false; - const maxTokens = getMaxTokens(); + const maxTokens = resolveRequestMaxTokens(requestKind, getMaxTokens()); const apiModelId = getApiModelId(modelInfo.id); const visionResolution = await resolveImageMessages(messages, token, getVisionDescriber); @@ -163,17 +184,16 @@ export async function prepareChatRequest({ stream_options: { include_usage: true }, tools, tool_choice: tools && tools.length > 0 ? ('auto' as const) : undefined, - // tool_stream: tools && tools.length > 0 ? true : undefined, - tool_stream: tools && tools.length > 0 && isOfficialGLMBaseUrl(baseUrl) ? true : undefined, max_tokens: maxTokens, }; const configuredThinkingEffort = getConfiguredThinkingEffort( options as ModelConfigurationOptions, ); - // Only force helper requests into disabled thinking on the official API. - // Custom endpoints keep their configured effort to preserve pre-#137 request shape. - const forceNoneThinking = shouldForceThinkingNone(requestKind) && isOfficialGLMBaseUrl(baseUrl); + const forceNoneThinking = + shouldForceThinkingNone(requestKind) && + !baseUrlOverride && + isManagedEndpointBaseUrl(baseUrl); const thinkingEffort = forceNoneThinking ? 'none' : configuredThinkingEffort; const supportsReasoningEffort = modelDef?.supportsReasoningEffort ?? false; const request: GLMRequest = { diff --git a/src/provider/routing/classifier.ts b/src/provider/routing/classifier.ts index fed9efb..092c0ea 100644 --- a/src/provider/routing/classifier.ts +++ b/src/provider/routing/classifier.ts @@ -55,6 +55,21 @@ export function shouldForceThinkingNone(requestKind: RequestKind): boolean { return REQUEST_KINDS_WITH_FORCED_NONE_THINKING.has(requestKind); } +const HELPER_MAX_OUTPUT_TOKENS = 512; + +export function resolveRequestMaxTokens( + requestKind: RequestKind, + configuredMax: number | undefined, +): number | undefined { + if (!shouldForceThinkingNone(requestKind)) { + return configuredMax; + } + if (configuredMax === undefined) { + return HELPER_MAX_OUTPUT_TOKENS; + } + return Math.min(configuredMax, HELPER_MAX_OUTPUT_TOKENS); +} + export function classifyProviderRequest(input: { messages: readonly vscode.LanguageModelChatRequestMessage[]; tools?: readonly vscode.LanguageModelChatTool[]; diff --git a/src/provider/routing/index.ts b/src/provider/routing/index.ts index 7a5ca71..0b4f19e 100644 --- a/src/provider/routing/index.ts +++ b/src/provider/routing/index.ts @@ -3,6 +3,7 @@ export { classifyProviderRequest, formatModelFields, formatRequestLogLine, + resolveRequestMaxTokens, shouldForceThinkingNone, type RequestKind, } from './classifier'; diff --git a/src/provider/usage.ts b/src/provider/usage.ts index e7b696a..2de2e76 100644 --- a/src/provider/usage.ts +++ b/src/provider/usage.ts @@ -1,194 +1,4 @@ -import { identifyOfficialGLMPlatform, normalizeBaseUrl } from '../endpoint'; - -const USAGE_TIMEOUT_MS = 15_000; - -export interface GLMPlanUsageResult { - platform: 'ZAI' | 'ZHIPU'; - baseDomain: string; - startTime: string; - endTime: string; - modelUsage: unknown; - toolUsage: unknown; - quotaLimit: unknown; -} - -export function supportsGLMPlanUsage(baseUrl: string): boolean { - return identifyOfficialGLMPlatform(baseUrl) !== undefined; -} - -export async function queryGLMPlanUsage( - baseUrl: string, - authToken: string, -): Promise { - const platform = identifyOfficialGLMPlatform(baseUrl); - if (!platform) { - throw new Error('Unsupported GLM baseUrl'); - } - - const baseDomain = getBaseDomain(baseUrl); - const { startTime, endTime } = createUsageWindow(); - const queryParams = new URLSearchParams({ startTime, endTime }); - // Combine a manual controller with the timeout so that if any one request - // fails, the remaining in-flight requests are cancelled instead of being - // orphaned (they would otherwise keep consuming connections/quota). - const controller = new AbortController(); - const signal = AbortSignal.any([controller.signal, AbortSignal.timeout(USAGE_TIMEOUT_MS)]); - - try { - const [modelUsage, toolUsage, quotaLimit] = await Promise.all([ - queryUsageEndpoint( - `${baseDomain}/api/monitor/usage/model-usage?${queryParams}`, - authToken, - signal, - ), - queryUsageEndpoint( - `${baseDomain}/api/monitor/usage/tool-usage?${queryParams}`, - authToken, - signal, - ), - queryUsageEndpoint(`${baseDomain}/api/monitor/usage/quota/limit`, authToken, signal).then( - processQuotaLimit, - ), - ]); - - return { - platform: platform === 'zai' ? 'ZAI' : 'ZHIPU', - baseDomain, - startTime, - endTime, - modelUsage, - toolUsage, - quotaLimit, - }; - } finally { - // Always abort the controller so the AbortSignal is torn down - // and doesn't hold references — previously only aborted on error. - controller.abort(); - } -} - -export function formatGLMPlanUsageForLog(result: GLMPlanUsageResult): string { - return [ - `GLM Coding Plan usage`, - `platform=${result.platform}`, - `baseDomain=${result.baseDomain}`, - `window=${result.startTime} -> ${result.endTime}`, - `modelUsage=${JSON.stringify(result.modelUsage, null, 2)}`, - `toolUsage=${JSON.stringify(result.toolUsage, null, 2)}`, - `quotaLimit=${JSON.stringify(result.quotaLimit, null, 2)}`, - ].join('\n'); -} - -function getBaseDomain(baseUrl: string): string { - const parsed = new URL(normalizeBaseUrl(baseUrl)); - return `${parsed.protocol}//${parsed.host}`; -} - -function createUsageWindow(now = new Date()): { - startTime: string; - endTime: string; -} { - const start = new Date( - now.getFullYear(), - now.getMonth(), - now.getDate() - 1, - now.getHours(), - 0, - 0, - 0, - ); - const end = new Date( - now.getFullYear(), - now.getMonth(), - now.getDate(), - now.getHours(), - 59, - 59, - 999, - ); - return { - startTime: formatDateTime(start), - endTime: formatDateTime(end), - }; -} - -function formatDateTime(date: Date): string { - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - const hours = String(date.getHours()).padStart(2, '0'); - const minutes = String(date.getMinutes()).padStart(2, '0'); - const seconds = String(date.getSeconds()).padStart(2, '0'); - return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; -} - -async function queryUsageEndpoint( - url: string, - authToken: string, - signal: AbortSignal, -): Promise { - const response = await fetch(url, { - method: 'GET', - headers: { - Authorization: authToken, - 'Accept-Language': 'en-US,en', - 'Content-Type': 'application/json', - }, - signal, - }); - const text = await response.text(); - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${truncate(text)}`); - } - - if (!text.trim()) { - return {}; - } - - try { - const parsed = JSON.parse(text) as { data?: unknown }; - return parsed.data ?? parsed; - } catch { - return text; - } -} - -function processQuotaLimit(data: unknown): unknown { - if (!isRecord(data) || !Array.isArray(data.limits)) { - return data; - } - - return { - ...data, - limits: data.limits.map((item) => { - if (!isRecord(item)) { - return item; - } - if (item.type === 'TOKENS_LIMIT') { - return { - type: 'Token usage (5 hours)', - percentage: item.percentage, - }; - } - if (item.type === 'TIME_LIMIT') { - return { - type: 'MCP usage (1 month)', - percentage: item.percentage, - currentUsage: item.currentValue, - total: item.usage, - usageDetails: item.usageDetails, - }; - } - return item; - }), - }; -} - -function isRecord(value: unknown): value is Record { - return typeof value === 'object' && value !== null; -} - -function truncate(value: string): string { - const singleLine = value.replace(/\s+/g, ' ').trim(); - return singleLine.length > 500 ? `${singleLine.slice(0, 500)}...` : singleLine; +/** OpenCode usage is tracked in the web console — no monitor API in this extension. */ +export function supportsOpenCodeUsageConsole(baseUrl: string): boolean { + return baseUrl.includes('opencode.ai'); } diff --git a/src/provider/vision/consts.ts b/src/provider/vision/consts.ts index fa084f1..04bacc2 100644 --- a/src/provider/vision/consts.ts +++ b/src/provider/vision/consts.ts @@ -1,5 +1,8 @@ -/** Built-in GLM multimodal model used first when the vision proxy is automatic. */ -export const DEFAULT_GLM_VISION_MODEL_ID = 'glm-4.6v-flash'; +/** Default OpenCode vision model for automatic image description. */ +export const DEFAULT_OPENCODE_VISION_MODEL_ID = 'glm-5.2'; + +/** @deprecated Use {@link DEFAULT_OPENCODE_VISION_MODEL_ID}. */ +export const DEFAULT_GLM_VISION_MODEL_ID = DEFAULT_OPENCODE_VISION_MODEL_ID; /** Default VS Code model ID used when GLM vision falls back to Copilot/VS Code models. */ export const DEFAULT_VISION_MODEL_ID = 'oswe-vscode-prime'; diff --git a/src/provider/vision/service.ts b/src/provider/vision/service.ts index a2b757b..a15860a 100644 --- a/src/provider/vision/service.ts +++ b/src/provider/vision/service.ts @@ -1,9 +1,13 @@ import vscode from 'vscode'; import type { AuthManager } from '../../auth'; -import { getApiModelId, getApiProtocol, getBaseUrl } from '../../config'; -import { GLM_CN_CODING_BASE_URL } from '../../endpoint'; +import { getApiModelId, getBaseUrl, getBaseUrlOverride, getEndpoint } from '../../config'; +import { + OPENCODE_GO_OPENAI_BASE_URL, + OPENCODE_ZEN_OPENAI_BASE_URL, + resolveEndpointBaseUrl, +} from '../../endpoint'; import { t } from '../../i18n'; -import { DEFAULT_GLM_VISION_MODEL_ID } from './consts'; +import { DEFAULT_OPENCODE_VISION_MODEL_ID } from './consts'; import { logAutomaticGLMVisionFallback, logAutomaticGLMVisionModelSelected, @@ -81,7 +85,7 @@ export function createVisionService( } } - const config = createAutomaticGLMVisionConfig(); + const config = createAutomaticOpenCodeVisionConfig(); const apiKey = await authManager.getApiKeyForEndpoint(config.url); const primary = createEndpointVisionDescriber(config, apiKey); logAutomaticGLMVisionModelSelected(primary.id, config.url); @@ -125,22 +129,34 @@ class AutomaticVisionDescriber implements VisionDescriber { } } -function createAutomaticGLMVisionConfig(): VisionProxyConfig { - // When the main chat protocol is Anthropic, the vision proxy still needs to use - // the OpenAI-compatible endpoint because Anthropic vision endpoint availability - // may differ. Fall back to the Coding Plan endpoint for vision descriptions. - const protocol = getApiProtocol(); - const visionBaseUrl = protocol === 'anthropic' ? GLM_CN_CODING_BASE_URL : getBaseUrl(); +function createAutomaticOpenCodeVisionConfig(): VisionProxyConfig { + // Always use the OpenAI-compatible OpenCode base URL — even when main chat + // uses the Anthropic wire protocol. + const visionBaseUrl = resolveOpenCodeVisionBaseUrl(); return { providerFamily: 'openai-compatible', apiType: 'chat-completions', url: `${visionBaseUrl}/chat/completions`, - modelId: getApiModelId(DEFAULT_GLM_VISION_MODEL_ID), + modelId: getApiModelId(DEFAULT_OPENCODE_VISION_MODEL_ID), updatedAt: Date.now(), }; } +function resolveOpenCodeVisionBaseUrl(): string { + if (getBaseUrlOverride()) { + return getBaseUrl(); + } + const preset = getEndpoint(); + if (preset === 'opencode-go-anthropic') { + return OPENCODE_GO_OPENAI_BASE_URL; + } + if (preset === 'opencode-zen-anthropic') { + return OPENCODE_ZEN_OPENAI_BASE_URL; + } + return resolveEndpointBaseUrl(preset); +} + function isCancelledVisionError(error: unknown): boolean { return isVisionProxyError(error) && error.code === 'cancelled'; } diff --git a/src/runtime/diagnostics.ts b/src/runtime/diagnostics.ts index 09bd80c..07e7442 100644 --- a/src/runtime/diagnostics.ts +++ b/src/runtime/diagnostics.ts @@ -1,12 +1,12 @@ import { execFileSync } from 'node:child_process'; import vscode from 'vscode'; import { - getApiMode, getApiProtocol, getBaseUrl, getCodeSimplifierEnabled, getDebugMode, getEndpoint, + getOpencodePlan, getPonytailMode, } from '../config'; import { CONFIG_SECTION } from '../consts'; @@ -63,7 +63,7 @@ export function buildRuntimeDiagnosticsReport(context: vscode.ExtensionContext): '## Configuration', `- Endpoint preset: ${getEndpoint()}`, `- Base URL: ${getBaseUrl()}`, - `- API mode: ${getApiMode()}`, + `- OpenCode plan: ${getOpencodePlan()}`, `- API protocol: ${getApiProtocol()}`, `- Debug mode: ${getDebugMode()}`, `- Ponytail mode: ${getPonytailMode()}`, diff --git a/src/types.ts b/src/types.ts index f937605..76db667 100644 --- a/src/types.ts +++ b/src/types.ts @@ -95,30 +95,15 @@ export interface StreamCallbacks { // ---- Configuration types ---- -export type ApiMode = 'coding-plan' | 'standard'; - -export type ApiRegion = 'china' | 'international'; - export type ApiProtocol = 'openai' | 'anthropic'; /** - * Single-value endpoint selector that folds the legacy - * (region, apiMode, apiProtocol) tuple into one enum. - * - * Each value uniquely resolves to a base URL + wire protocol, so users pick - * exactly what they want from one dropdown instead of three interacting ones. + * OpenCode endpoint preset — each value resolves to one base URL + wire protocol. * - * The `opencode-go*` values target the OpenCode Go subscription - * (https://opencode.ai/docs/go), which serves a curated set of open coding - * models behind a single API key from `https://opencode.ai/auth`. + * Go presets: https://opencode.ai/docs/go + * Zen presets: https://opencode.ai/docs/zen */ export type EndpointPreset = - | 'china-coding' - | 'china-standard' - | 'china-anthropic' - | 'international-coding' - | 'international-standard' - | 'international-anthropic' | 'opencode-go' | 'opencode-go-anthropic' | 'opencode-zen' diff --git a/test/client/error.test.ts b/test/client/error.test.ts index 57946cf..048939b 100644 --- a/test/client/error.test.ts +++ b/test/client/error.test.ts @@ -3,7 +3,7 @@ import { GLMRequestError, createHttpError, createUserFacingError } from '../../s import { OPENCODE_GO_OPENAI_BASE_URL } from '../../src/endpoint'; import type { GLMRequest } from '../../src/types'; -const GLM_ANTHROPIC_BASE_URL = 'https://open.bigmodel.cn/api/anthropic'; +const OPENCODE_ANTHROPIC_BASE_URL = 'https://opencode.ai/zen/go'; const PROXY_BASE_URL = 'https://proxy.example.com/v1'; function buildRequest(overrides: Partial = {}): GLMRequest { @@ -43,7 +43,7 @@ describe('createHttpError', () => { ); const error = await createHttpError(response, { - baseUrl: GLM_ANTHROPIC_BASE_URL, + baseUrl: OPENCODE_ANTHROPIC_BASE_URL, request: buildRequest(), }); @@ -70,7 +70,7 @@ describe('createHttpError', () => { }); const error = await createHttpError(response, { - baseUrl: GLM_ANTHROPIC_BASE_URL, + baseUrl: OPENCODE_ANTHROPIC_BASE_URL, request: buildRequest(), }); @@ -117,7 +117,7 @@ describe('createHttpError', () => { }); const error = await createHttpError(response, { - baseUrl: GLM_ANTHROPIC_BASE_URL, + baseUrl: OPENCODE_ANTHROPIC_BASE_URL, request: buildRequest(), }); @@ -152,7 +152,7 @@ describe('createHttpError', () => { }); const error = await createHttpError(response, { - baseUrl: GLM_ANTHROPIC_BASE_URL, + baseUrl: OPENCODE_ANTHROPIC_BASE_URL, request: buildRequest(), }); @@ -170,7 +170,7 @@ describe('createHttpError', () => { }); const error = await createHttpError(response, { - baseUrl: GLM_ANTHROPIC_BASE_URL, + baseUrl: OPENCODE_ANTHROPIC_BASE_URL, request: buildRequest(), }); @@ -188,7 +188,7 @@ describe('createHttpError', () => { }); const error = await createHttpError(response, { - baseUrl: GLM_ANTHROPIC_BASE_URL, + baseUrl: OPENCODE_ANTHROPIC_BASE_URL, request: buildRequest(), }); @@ -242,7 +242,7 @@ describe('createUserFacingError', () => { }); const httpError = await createHttpError(response, { - baseUrl: GLM_ANTHROPIC_BASE_URL, + baseUrl: OPENCODE_ANTHROPIC_BASE_URL, request: buildRequest(), }); const facing = createUserFacingError(httpError); diff --git a/test/config.test.ts b/test/config.test.ts index 7b73ff9..d4be1cb 100644 --- a/test/config.test.ts +++ b/test/config.test.ts @@ -1,29 +1,24 @@ import * as vscode from 'vscode'; import { beforeEach, describe, expect, it } from 'vitest'; import { - findModelDefinition, - getApiKeyUrl, - getApiModelId, - getApiProtocol, - getBaseUrl, - getCustomModels, - getEndpoint, - listProviderModels, - migrateLegacySettings, + findModelDefinition, + getApiKeyUrl, + getApiModelId, + getApiProtocol, + getBaseUrl, + getCustomModels, + getEndpoint, + listProviderModels, + migrateLegacySettings, } from '../src/config'; import { MODELS } from '../src/consts'; import { - GLM_CN_ANTHROPIC_BASE_URL, - GLM_CN_CODING_BASE_URL, - GLM_INTERNATIONAL_ANTHROPIC_BASE_URL, - GLM_INTERNATIONAL_CODING_API_KEY_URL, - GLM_INTERNATIONAL_CODING_BASE_URL, - GLM_INTERNATIONAL_GENERAL_API_KEY_URL, - GLM_INTERNATIONAL_GENERAL_BASE_URL, - OPENCODE_GO_ANTHROPIC_BASE_URL, - OPENCODE_GO_API_KEY_URL, - OPENCODE_GO_OPENAI_BASE_URL, - OPENCODE_ZEN_OPENAI_BASE_URL + OPENCODE_GO_ANTHROPIC_BASE_URL, + OPENCODE_GO_API_KEY_URL, + OPENCODE_GO_OPENAI_BASE_URL, + OPENCODE_ZEN_ANTHROPIC_BASE_URL, + OPENCODE_ZEN_API_KEY_URL, + OPENCODE_ZEN_OPENAI_BASE_URL, } from '../src/endpoint'; import { __clearConfigurationValues, __setConfigurationValue } from './support/vscode.mock'; @@ -33,7 +28,6 @@ describe('legacy settings migration (glm-copilot -> opencode-for-copilot)', () = }); it('copies user-set legacy values to the new section exactly once', async () => { - // Simulates an upgrading user: values live in the old shared section. __setConfigurationValue('glm-copilot.endpoint', 'china-anthropic'); __setConfigurationValue('glm-copilot.maxTokens', 8192); const store = new Map(); @@ -46,10 +40,8 @@ describe('legacy settings migration (glm-copilot -> opencode-for-copilot)', () = await migrateLegacySettings(context); - expect(getEndpoint()).toBe('china-anthropic'); - expect(getBaseUrl()).toBe(GLM_CN_ANTHROPIC_BASE_URL); - // User-set values in the new section must never be overwritten by a - // stale legacy value. + expect(getEndpoint()).toBe('opencode-go-anthropic'); + expect(getBaseUrl()).toBe(OPENCODE_GO_ANTHROPIC_BASE_URL); expect(vscode.workspace.getConfiguration('opencode-for-copilot').get('maxTokens')).toBe(8192); }); }); @@ -64,24 +56,8 @@ describe('configuration helpers', () => { expect(getApiKeyUrl()).toBe(OPENCODE_GO_API_KEY_URL); }); - it('uses apiMode and region presets when baseUrl is empty', () => { - __setConfigurationValue('opencode-for-copilot.apiMode', 'standard'); - __setConfigurationValue('opencode-for-copilot.region', 'international'); - - expect(getBaseUrl()).toBe(GLM_INTERNATIONAL_GENERAL_BASE_URL); - expect(getApiKeyUrl()).toBe(GLM_INTERNATIONAL_GENERAL_API_KEY_URL); - }); - - it('lets non-empty baseUrl override apiMode and region presets', () => { - __setConfigurationValue('opencode-for-copilot.apiMode', 'standard'); - __setConfigurationValue('opencode-for-copilot.region', 'international'); - __setConfigurationValue('opencode-for-copilot.baseUrl', ' https://proxy.example.com/v1/// '); - - expect(getBaseUrl()).toBe('https://proxy.example.com/v1'); - }); - it('lets non-empty baseUrl override the endpoint preset', () => { - __setConfigurationValue('opencode-for-copilot.endpoint', 'international-anthropic'); + __setConfigurationValue('opencode-for-copilot.endpoint', 'opencode-zen-anthropic'); __setConfigurationValue('opencode-for-copilot.baseUrl', 'https://proxy.example.com/v1'); expect(getBaseUrl()).toBe('https://proxy.example.com/v1'); @@ -107,28 +83,21 @@ describe('endpoint preset selection', () => { expect(getBaseUrl()).toBe(OPENCODE_ZEN_OPENAI_BASE_URL); }); - it('respects an explicit endpoint preset', () => { - __setConfigurationValue('opencode-for-copilot.endpoint', 'international-anthropic'); - - expect(getEndpoint()).toBe('international-anthropic'); - expect(getBaseUrl()).toBe(GLM_INTERNATIONAL_ANTHROPIC_BASE_URL); - expect(getApiProtocol()).toBe('anthropic'); - expect(getApiKeyUrl()).toBe(GLM_INTERNATIONAL_CODING_API_KEY_URL); - }); - - it('resolves china-anthropic preset to the CN Anthropic endpoint', () => { + it('migrates legacy china-anthropic preset to OpenCode Go Anthropic', () => { __setConfigurationValue('opencode-for-copilot.endpoint', 'china-anthropic'); - expect(getBaseUrl()).toBe(GLM_CN_ANTHROPIC_BASE_URL); + expect(getEndpoint()).toBe('opencode-go-anthropic'); + expect(getBaseUrl()).toBe(OPENCODE_GO_ANTHROPIC_BASE_URL); expect(getApiProtocol()).toBe('anthropic'); }); - it('resolves international-coding preset', () => { + it('migrates legacy international-coding preset to OpenCode Zen', () => { __setConfigurationValue('opencode-for-copilot.endpoint', 'international-coding'); - expect(getBaseUrl()).toBe(GLM_INTERNATIONAL_CODING_BASE_URL); + expect(getEndpoint()).toBe('opencode-zen'); + expect(getBaseUrl()).toBe(OPENCODE_ZEN_OPENAI_BASE_URL); expect(getApiProtocol()).toBe('openai'); - expect(getApiKeyUrl()).toBe(GLM_INTERNATIONAL_CODING_API_KEY_URL); + expect(getApiKeyUrl()).toBe(OPENCODE_ZEN_API_KEY_URL); }); it('resolves the opencode-go preset to the OpenCode Go OpenAI endpoint', () => { @@ -149,33 +118,37 @@ describe('endpoint preset selection', () => { expect(getApiKeyUrl()).toBe(OPENCODE_GO_API_KEY_URL); }); - it('falls back to legacy tuple when endpoint is unset (backward compat)', () => { - // Mimics an existing user who upgraded and has not migrated yet. - __setConfigurationValue('opencode-for-copilot.region', 'international'); - __setConfigurationValue('opencode-for-copilot.apiMode', 'standard'); - __setConfigurationValue('opencode-for-copilot.apiProtocol', 'openai'); + it('resolves the opencode-zen-anthropic preset', () => { + __setConfigurationValue('opencode-for-copilot.endpoint', 'opencode-zen-anthropic'); - expect(getEndpoint()).toBe('international-standard'); - expect(getBaseUrl()).toBe(GLM_INTERNATIONAL_GENERAL_BASE_URL); - expect(getApiKeyUrl()).toBe(GLM_INTERNATIONAL_GENERAL_API_KEY_URL); + expect(getEndpoint()).toBe('opencode-zen-anthropic'); + expect(getBaseUrl()).toBe(OPENCODE_ZEN_ANTHROPIC_BASE_URL); + expect(getApiProtocol()).toBe('anthropic'); + expect(getApiKeyUrl()).toBe(OPENCODE_ZEN_API_KEY_URL); }); - it('legacy apiProtocol=anthropic + international region now resolves to the international Anthropic endpoint (regression)', () => { - __setConfigurationValue('opencode-for-copilot.region', 'international'); + it('uses legacy apiProtocol=anthropic when endpoint is unset', () => { __setConfigurationValue('opencode-for-copilot.apiProtocol', 'anthropic'); - expect(getEndpoint()).toBe('international-anthropic'); - expect(getBaseUrl()).toBe(GLM_INTERNATIONAL_ANTHROPIC_BASE_URL); + expect(getEndpoint()).toBe('opencode-go-anthropic'); + expect(getBaseUrl()).toBe(OPENCODE_GO_ANTHROPIC_BASE_URL); expect(getApiProtocol()).toBe('anthropic'); }); - it('endpoint preset takes precedence over legacy tuple', () => { - __setConfigurationValue('opencode-for-copilot.endpoint', 'china-coding'); - __setConfigurationValue('opencode-for-copilot.region', 'international'); - __setConfigurationValue('opencode-for-copilot.apiMode', 'standard'); + it('uses zen anthropic preset when plan is zen and apiProtocol=anthropic', () => { + __setConfigurationValue('opencode-for-copilot.opencodePlan', 'zen'); + __setConfigurationValue('opencode-for-copilot.apiProtocol', 'anthropic'); + + expect(getEndpoint()).toBe('opencode-zen-anthropic'); + expect(getBaseUrl()).toBe(OPENCODE_ZEN_ANTHROPIC_BASE_URL); + }); + + it('endpoint preset takes precedence over legacy apiProtocol', () => { + __setConfigurationValue('opencode-for-copilot.endpoint', 'opencode-go'); + __setConfigurationValue('opencode-for-copilot.apiProtocol', 'anthropic'); - expect(getEndpoint()).toBe('china-coding'); - expect(getBaseUrl()).toBe(GLM_CN_CODING_BASE_URL); + expect(getEndpoint()).toBe('opencode-go'); + expect(getBaseUrl()).toBe(OPENCODE_GO_OPENAI_BASE_URL); }); it('normalizes custom model strings and objects', () => { @@ -199,14 +172,6 @@ describe('endpoint preset selection', () => { expect(models[0]).toMatchObject({ id: 'team-coder', name: 'team-coder', - maxInputTokens: 200_000, - maxOutputTokens: 131_072, - capabilities: { - toolCalling: true, - imageInput: true, - thinking: true, - }, - requiresThinkingParam: true, }); expect(models[1]).toMatchObject({ id: 'custom-no-tools', @@ -215,38 +180,32 @@ describe('endpoint preset selection', () => { maxOutputTokens: 456, capabilities: { toolCalling: false, - imageInput: true, thinking: false, }, - requiresThinkingParam: false, }); }); +}); - it('lets custom model IDs override built-in model lookup and picker registry', () => { - __setConfigurationValue('opencode-for-copilot.customModels', [ - { - id: 'glm-5.2', - name: 'Local GLM-5.2', - maxInputTokens: 42, - thinking: false, - }, - ]); +describe('model registry helpers', () => { + beforeEach(() => { + __clearConfigurationValues(); + }); - const models = listProviderModels(); + it('finds built-in models by id', () => { + const model = findModelDefinition('glm-5.2'); + expect(model).toBeDefined(); + expect(model?.id).toBe('glm-5.2'); + }); - expect(models).toHaveLength(MODELS.length); - expect(findModelDefinition('glm-5.2')).toMatchObject({ - id: 'glm-5.2', - name: 'Local GLM-5.2', - maxInputTokens: 42, - capabilities: { - imageInput: true, - thinking: false, - }, - }); + it('lists provider models including custom models', () => { + __setConfigurationValue('opencode-for-copilot.customModels', ['team-coder']); + + const models = listProviderModels(); + expect(models.some((m) => m.id === 'team-coder')).toBe(true); + expect(models.length).toBeGreaterThanOrEqual(MODELS.length); }); - it('supports modelIdOverrides for arbitrary built-in or custom model IDs', () => { + it('applies modelIdOverrides for API model IDs', () => { __setConfigurationValue('opencode-for-copilot.modelIdOverrides', { 'glm-5.2': 'upstream-glm-5.2', 'team-coder': 'provider-team-coder', diff --git a/test/endpoint.test.ts b/test/endpoint.test.ts index 21fd182..89f5756 100644 --- a/test/endpoint.test.ts +++ b/test/endpoint.test.ts @@ -1,18 +1,9 @@ import { describe, expect, it } from 'vitest'; import { - GLM_CN_ANTHROPIC_BASE_URL, - GLM_CN_API_HOST, - GLM_CN_CODING_API_KEY_URL, - GLM_CN_CODING_BASE_URL, - GLM_CN_GENERAL_API_KEY_URL, - GLM_CN_GENERAL_BASE_URL, - GLM_CN_LEGACY_API_HOST, - GLM_INTERNATIONAL_ANTHROPIC_BASE_URL, - GLM_INTERNATIONAL_API_HOST, - GLM_INTERNATIONAL_CODING_API_KEY_URL, - GLM_INTERNATIONAL_CODING_BASE_URL, - GLM_INTERNATIONAL_GENERAL_API_KEY_URL, - GLM_INTERNATIONAL_GENERAL_BASE_URL, + isManagedEndpointBaseUrl, + isOpencodeBaseUrl, + normalizeBaseUrl, + normalizeLegacyEndpointPreset, OPENCODE_GO_ANTHROPIC_BASE_URL, OPENCODE_GO_API_HOST, OPENCODE_GO_API_KEY_URL, @@ -20,53 +11,21 @@ import { OPENCODE_ZEN_ANTHROPIC_BASE_URL, OPENCODE_ZEN_API_KEY_URL, OPENCODE_ZEN_OPENAI_BASE_URL, - deriveEndpointPreset, - identifyOfficialGLMPlatform, - isOfficialGLMBaseUrl, - isOpencodeBaseUrl, - normalizeBaseUrl, - resolveApiKeyUrl, resolveEndpointApiKeyUrl, resolveEndpointBaseUrl, resolveEndpointProtocol, resolveOpencodePlanForBaseUrl, + resolvePlanDefaultEndpoint, } from '../src/endpoint'; describe('endpoint helpers', () => { - it('resolves API key pages from apiMode and region', () => { - expect(resolveApiKeyUrl('coding-plan', 'china')).toBe(GLM_CN_CODING_API_KEY_URL); - expect(resolveApiKeyUrl('standard', 'china')).toBe(GLM_CN_GENERAL_API_KEY_URL); - expect(resolveApiKeyUrl('coding-plan', 'international')).toBe( - GLM_INTERNATIONAL_CODING_API_KEY_URL, - ); - expect(resolveApiKeyUrl('standard', 'international')).toBe( - GLM_INTERNATIONAL_GENERAL_API_KEY_URL, - ); - }); - it('normalizes trailing slashes and surrounding whitespace', () => { - expect(normalizeBaseUrl(' https://open.bigmodel.cn/api/paas/v4/// ')).toBe( - 'https://open.bigmodel.cn/api/paas/v4', - ); - }); - - it('identifies official GLM platforms by host', () => { - expect(identifyOfficialGLMPlatform(`https://${GLM_INTERNATIONAL_API_HOST}/api/paas/v4`)).toBe( - 'zai', - ); - expect(identifyOfficialGLMPlatform(`https://${GLM_CN_API_HOST}/api/paas/v4`)).toBe('zhipu'); - expect(identifyOfficialGLMPlatform(`https://${GLM_CN_LEGACY_API_HOST}/api/paas/v4`)).toBe( - 'zhipu', + expect(normalizeBaseUrl(' https://opencode.ai/zen/go/v1/// ')).toBe( + 'https://opencode.ai/zen/go/v1', ); }); - it('does not classify custom or invalid URLs as official', () => { - expect(identifyOfficialGLMPlatform('https://proxy.example.com/v1')).toBeUndefined(); - expect(identifyOfficialGLMPlatform('not a url')).toBeUndefined(); - expect(isOfficialGLMBaseUrl('https://proxy.example.com/v1')).toBe(false); - }); - - it('identifies OpenCode URLs without treating them as official GLM', () => { + it('identifies OpenCode URLs', () => { expect(isOpencodeBaseUrl(OPENCODE_GO_OPENAI_BASE_URL)).toBe(true); expect(isOpencodeBaseUrl(OPENCODE_GO_ANTHROPIC_BASE_URL)).toBe(true); expect(isOpencodeBaseUrl(OPENCODE_ZEN_OPENAI_BASE_URL)).toBe(true); @@ -74,15 +33,25 @@ describe('endpoint helpers', () => { expect(isOpencodeBaseUrl(`https://${OPENCODE_GO_API_HOST}/zen/go/v1/chat/completions`)).toBe( true, ); - // OpenCode is a separate platform — it must NOT be classified as official - // GLM (no tool_stream, no GLM business error codes, no Zhipu/Z.ai links). - expect(identifyOfficialGLMPlatform(OPENCODE_GO_OPENAI_BASE_URL)).toBeUndefined(); - expect(isOfficialGLMBaseUrl(OPENCODE_GO_OPENAI_BASE_URL)).toBe(false); - expect(identifyOfficialGLMPlatform(OPENCODE_ZEN_OPENAI_BASE_URL)).toBeUndefined(); - expect(isOfficialGLMBaseUrl(OPENCODE_ZEN_OPENAI_BASE_URL)).toBe(false); - expect(isOpencodeBaseUrl('https://api.z.ai/api/paas/v4')).toBe(false); + expect(isOpencodeBaseUrl('https://proxy.example.com/v1')).toBe(false); expect(isOpencodeBaseUrl('not a url')).toBe(false); }); + + it('treats managed endpoints as OpenCode hosts only', () => { + expect(isManagedEndpointBaseUrl(OPENCODE_GO_OPENAI_BASE_URL)).toBe(true); + expect(isManagedEndpointBaseUrl('https://open.bigmodel.cn/api/paas/v4')).toBe(false); + }); + + it('resolves OpenCode plan from base URL path', () => { + expect(resolveOpencodePlanForBaseUrl(OPENCODE_GO_OPENAI_BASE_URL)).toBe('go'); + expect(resolveOpencodePlanForBaseUrl(OPENCODE_ZEN_OPENAI_BASE_URL)).toBe('zen'); + expect(resolveOpencodePlanForBaseUrl('https://proxy.example.com/v1')).toBeUndefined(); + }); + + it('maps plan to default endpoint preset', () => { + expect(resolvePlanDefaultEndpoint('go')).toBe('opencode-go'); + expect(resolvePlanDefaultEndpoint('zen')).toBe('opencode-zen'); + }); }); describe('resolveOpencodePlanForBaseUrl', () => { @@ -115,36 +84,14 @@ describe('resolveOpencodePlanForBaseUrl', () => { }); describe('endpoint preset resolver', () => { - it('resolves every preset to its official base URL', () => { - expect(resolveEndpointBaseUrl('china-coding')).toBe(GLM_CN_CODING_BASE_URL); - expect(resolveEndpointBaseUrl('china-standard')).toBe(GLM_CN_GENERAL_BASE_URL); - expect(resolveEndpointBaseUrl('china-anthropic')).toBe(GLM_CN_ANTHROPIC_BASE_URL); - expect(resolveEndpointBaseUrl('international-coding')).toBe(GLM_INTERNATIONAL_CODING_BASE_URL); - expect(resolveEndpointBaseUrl('international-standard')).toBe( - GLM_INTERNATIONAL_GENERAL_BASE_URL, - ); - expect(resolveEndpointBaseUrl('international-anthropic')).toBe( - GLM_INTERNATIONAL_ANTHROPIC_BASE_URL, - ); + it('resolves every OpenCode preset to its base URL', () => { expect(resolveEndpointBaseUrl('opencode-go')).toBe(OPENCODE_GO_OPENAI_BASE_URL); expect(resolveEndpointBaseUrl('opencode-go-anthropic')).toBe(OPENCODE_GO_ANTHROPIC_BASE_URL); expect(resolveEndpointBaseUrl('opencode-zen')).toBe(OPENCODE_ZEN_OPENAI_BASE_URL); expect(resolveEndpointBaseUrl('opencode-zen-anthropic')).toBe(OPENCODE_ZEN_ANTHROPIC_BASE_URL); }); - it('resolves every preset to its API key management page', () => { - expect(resolveEndpointApiKeyUrl('china-coding')).toBe(GLM_CN_CODING_API_KEY_URL); - expect(resolveEndpointApiKeyUrl('china-standard')).toBe(GLM_CN_GENERAL_API_KEY_URL); - expect(resolveEndpointApiKeyUrl('china-anthropic')).toBe(GLM_CN_CODING_API_KEY_URL); - expect(resolveEndpointApiKeyUrl('international-coding')).toBe( - GLM_INTERNATIONAL_CODING_API_KEY_URL, - ); - expect(resolveEndpointApiKeyUrl('international-standard')).toBe( - GLM_INTERNATIONAL_GENERAL_API_KEY_URL, - ); - expect(resolveEndpointApiKeyUrl('international-anthropic')).toBe( - GLM_INTERNATIONAL_CODING_API_KEY_URL, - ); + it('resolves every preset to the OpenCode API key page', () => { expect(resolveEndpointApiKeyUrl('opencode-go')).toBe(OPENCODE_GO_API_KEY_URL); expect(resolveEndpointApiKeyUrl('opencode-go-anthropic')).toBe(OPENCODE_GO_API_KEY_URL); expect(resolveEndpointApiKeyUrl('opencode-zen')).toBe(OPENCODE_ZEN_API_KEY_URL); @@ -152,12 +99,6 @@ describe('endpoint preset resolver', () => { }); it('maps each preset to its implied wire protocol', () => { - expect(resolveEndpointProtocol('china-coding')).toBe('openai'); - expect(resolveEndpointProtocol('china-standard')).toBe('openai'); - expect(resolveEndpointProtocol('china-anthropic')).toBe('anthropic'); - expect(resolveEndpointProtocol('international-coding')).toBe('openai'); - expect(resolveEndpointProtocol('international-standard')).toBe('openai'); - expect(resolveEndpointProtocol('international-anthropic')).toBe('anthropic'); expect(resolveEndpointProtocol('opencode-go')).toBe('openai'); expect(resolveEndpointProtocol('opencode-go-anthropic')).toBe('anthropic'); expect(resolveEndpointProtocol('opencode-zen')).toBe('openai'); @@ -165,34 +106,18 @@ describe('endpoint preset resolver', () => { }); }); -describe('legacy endpoint derivation', () => { - it('maps the default tuple to china-coding', () => { - expect(deriveEndpointPreset('china', 'coding-plan', 'openai')).toBe('china-coding'); +describe('legacy endpoint preset migration', () => { + it('maps legacy GLM preset strings onto OpenCode presets', () => { + expect(normalizeLegacyEndpointPreset('china-coding')).toBe('opencode-go'); + expect(normalizeLegacyEndpointPreset('china-standard')).toBe('opencode-go'); + expect(normalizeLegacyEndpointPreset('china-anthropic')).toBe('opencode-go-anthropic'); + expect(normalizeLegacyEndpointPreset('international-coding')).toBe('opencode-zen'); + expect(normalizeLegacyEndpointPreset('international-standard')).toBe('opencode-zen'); + expect(normalizeLegacyEndpointPreset('international-anthropic')).toBe('opencode-zen-anthropic'); }); - it('maps standard mode on both regions', () => { - expect(deriveEndpointPreset('china', 'standard', 'openai')).toBe('china-standard'); - expect(deriveEndpointPreset('international', 'standard', 'openai')).toBe( - 'international-standard', - ); - }); - - it('maps international coding plan', () => { - expect(deriveEndpointPreset('international', 'coding-plan', 'openai')).toBe( - 'international-coding', - ); - }); - - it('lets apiProtocol=anthropic override apiMode and picks the right region (regression)', () => { - // Previously region was ignored under anthropic protocol; now it must - // resolve to the matching regional Anthropic endpoint. - expect(deriveEndpointPreset('china', 'coding-plan', 'anthropic')).toBe('china-anthropic'); - expect(deriveEndpointPreset('china', 'standard', 'anthropic')).toBe('china-anthropic'); - expect(deriveEndpointPreset('international', 'coding-plan', 'anthropic')).toBe( - 'international-anthropic', - ); - expect(deriveEndpointPreset('international', 'standard', 'anthropic')).toBe( - 'international-anthropic', - ); + it('passes through current OpenCode preset strings', () => { + expect(normalizeLegacyEndpointPreset('opencode-go')).toBe('opencode-go'); + expect(normalizeLegacyEndpointPreset('opencode-zen-anthropic')).toBe('opencode-zen-anthropic'); }); }); diff --git a/test/provider/models.test.ts b/test/provider/models.test.ts index 9592b44..886cd34 100644 --- a/test/provider/models.test.ts +++ b/test/provider/models.test.ts @@ -180,7 +180,7 @@ describe('model metadata helpers', () => { expect(custom).toMatchObject({ id: 'team-coder', name: 'team-coder', - detail: 'Custom GLM-compatible model', + detail: 'Custom OpenCode-compatible model', capabilities: { toolCalling: true, imageInput: true, diff --git a/test/runtime/commands.test.ts b/test/runtime/commands.test.ts index 4955770..6ea44ce 100644 --- a/test/runtime/commands.test.ts +++ b/test/runtime/commands.test.ts @@ -1,17 +1,12 @@ import { beforeEach, describe, expect, it } from 'vitest'; import * as vscode from 'vscode'; -import { - GLM_CN_CODING_API_KEY_URL, - GLM_CN_GENERAL_API_KEY_URL, - GLM_INTERNATIONAL_CODING_API_KEY_URL, - GLM_INTERNATIONAL_GENERAL_API_KEY_URL, -} from '../../src/endpoint'; +import { OPENCODE_GO_API_KEY_URL, OPENCODE_ZEN_API_KEY_URL } from '../../src/endpoint'; import { registerCommands } from '../../src/runtime/commands'; import { - __clearConfigurationValues, - __getOpenedExternal, - __resetCommandState, - __setConfigurationValue, + __clearConfigurationValues, + __getOpenedExternal, + __resetCommandState, + __setConfigurationValue, } from '../support/vscode.mock'; describe('runtime commands', () => { @@ -21,13 +16,12 @@ describe('runtime commands', () => { }); it.each([ - ['coding-plan', 'china', GLM_CN_CODING_API_KEY_URL], - ['standard', 'china', GLM_CN_GENERAL_API_KEY_URL], - ['coding-plan', 'international', GLM_INTERNATIONAL_CODING_API_KEY_URL], - ['standard', 'international', GLM_INTERNATIONAL_GENERAL_API_KEY_URL], - ])('opens the API key page for %s/%s', async (apiMode, region, expectedUrl) => { - __setConfigurationValue('opencode-for-copilot.apiMode', apiMode); - __setConfigurationValue('opencode-for-copilot.region', region); + ['opencode-go', OPENCODE_GO_API_KEY_URL], + ['opencode-go-anthropic', OPENCODE_GO_API_KEY_URL], + ['opencode-zen', OPENCODE_ZEN_API_KEY_URL], + ['opencode-zen-anthropic', OPENCODE_ZEN_API_KEY_URL], + ])('opens the API key page for endpoint %s', async (endpoint, expectedUrl) => { + __setConfigurationValue('opencode-for-copilot.endpoint', endpoint); registerCommands({ subscriptions: [] } as unknown as vscode.ExtensionContext); await vscode.commands.executeCommand('opencode-for-copilot.getApiKey');