open-code-review/internal/llm/providers.go
Mountain Ghost. W 74a2ac1c1c
Some checks are pending
CI / test (push) Waiting to run
Deploy Pages / build (push) Waiting to run
Deploy Pages / deploy (push) Blocked by required conditions
feat(llm): add z-ai-coding provider for GLM Coding Plan endpoint (#258)
* feat(llm): add z-ai-coding provider for GLM Coding Plan endpoint

Z.AI (智谱) subscribers to the GLM Coding Plan must route requests through
the dedicated coding endpoint (https://open.bigmodel.cn/api/coding/paas/v4)
for them to be billed against the subscription quota. The existing z-ai
provider points at the generic pay-as-you-go endpoint
(https://open.bigmodel.cn/api/paas/v4), so Coding Plan keys silently drain
the wallet balance instead of consuming the plan quota, surfacing as a
spurious "1113 余额不足" error even when the plan is barely used.

Add a dedicated z-ai-coding provider following the existing *-tokenplan
pattern (dashscope/dashscope-tokenplan, tencent-tokenhub/hy-tokenplan).
It reuses Z_AI_API_KEY — the same key authenticates against both endpoints,
so selecting this provider is all that's needed to activate the plan. The
model list is restricted to the models officially supported by the Coding
Plan to avoid selecting a non-plan model that falls back to wallet billing.

- internal/llm/providers.go: register z-ai-coding preset
- extensions/vscode/src/shared/providers.ts: mirror the preset (kept in
  sync with the Go registry per the file header)
- internal/llm/providers_test.go: update the sorted provider list assertion

Co-Authored-By: Oz <oz-agent@warp.dev>

* docs(pages): add Z.AI GLM Coding Plan config tip to docs page

Subscribers to the Z.AI (Zhipu) GLM Coding Plan must route requests through
the dedicated coding endpoint (https://open.bigmodel.cn/api/coding/paas/v4)
to bill against the plan quota. The default z-ai preset points at the generic
pay-as-you-go endpoint, so coding-plan keys silently drain the wallet and
surface a spurious "1113 余额不足" error — a recurring trap for new users.

Add a provider-specific callout at the end of the Docs config section showing
the one-line fix that works today on any released version:

  ocr config set providers.z-ai.url https://open.bigmodel.cn/api/coding/paas/v4

This complements the z-ai-coding provider added in the previous commit: the
provider gives a native first-class option going forward, while this doc tip
rescues users already running released builds. Copy/localized for zh/en/ja.

Co-Authored-By: Oz <oz-agent@warp.dev>

* fix(llm): address review feedback for z-ai-coding provider

- Switch z-ai-coding to a dedicated Z_AI_CODING_API_KEY env var instead
  of reusing Z_AI_API_KEY, matching the tokenplan-provider convention so
  pay-as-you-go and Coding Plan keys can be configured independently
- Remove comment blocks from the z-ai-coding presets (Go and TS) to keep
  the registry as plain data consistent with the other entries
- Revert pages/ changes (i18n + DocsPage.tsx); provider docs are out of
  scope for a provider-registration PR

Co-Authored-By: Oz <oz-agent@warp.dev>

---------

Co-authored-by: mountainwu <mountainwu@kuainiugroup.com>
Co-authored-by: Oz <oz-agent@warp.dev>
2026-07-02 19:23:17 +08:00

269 lines
5.9 KiB
Go

package llm
import (
"sort"
"strings"
)
// Provider holds the preset configuration for a known LLM provider.
type Provider struct {
Name string
DisplayName string
Protocol string // "anthropic" or "openai"
BaseURL string
AuthHeader string // Anthropic-only; empty for OpenAI-compatible
EnvVar string // environment variable name for API key fallback
Models []string
}
var registry = []Provider{
{
Name: "anthropic",
DisplayName: "Anthropic Claude API",
Protocol: "anthropic",
BaseURL: "https://api.anthropic.com",
AuthHeader: "x-api-key",
EnvVar: "ANTHROPIC_API_KEY",
Models: []string{
"claude-opus-4-8",
"claude-opus-4-7",
"claude-opus-4-6",
"claude-sonnet-4-6",
},
},
{
Name: "openai",
DisplayName: "OpenAI API",
Protocol: "openai",
BaseURL: "https://api.openai.com/v1",
EnvVar: "OPENAI_API_KEY",
Models: []string{
"gpt-5.5",
"gpt-5.4",
"gpt-5.4-mini",
},
},
{
Name: "dashscope",
DisplayName: "Alibaba DashScope API",
Protocol: "openai",
BaseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
EnvVar: "DASHSCOPE_API_KEY",
Models: []string{
"qwen3.7-max",
"qwen3.7-plus",
"qwen3.6-plus",
"qwen3.6-flash",
"deepseek-v4-pro",
"deepseek-v4-flash",
"kimi-k2.7-code",
"glm-5.2",
"MiniMax-M2.5",
},
},
{
Name: "dashscope-tokenplan",
DisplayName: "Alibaba DashScope Token Plan API",
Protocol: "openai",
BaseURL: "https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1",
EnvVar: "DASHSCOPE_TOKENPLAN_KEY",
Models: []string{
"qwen3.7-max",
"qwen3.7-plus",
"qwen3.6-plus",
"qwen3.6-flash",
"deepseek-v4-pro",
"deepseek-v4-flash",
"kimi-k2.7-code",
"kimi-k2.6",
"kimi-k2.5",
"glm-5.2",
"glm-5.1",
"glm-5",
"MiniMax-M2.5",
},
},
{
Name: "volcengine",
DisplayName: "Volcano Engine Ark API",
Protocol: "openai",
BaseURL: "https://ark.cn-beijing.volces.com/api/v3",
EnvVar: "ARK_API_KEY",
Models: []string{
"doubao-seed-evolving",
"doubao-seed-2-1-pro-260628",
"doubao-seed-2-1-turbo-260628",
"doubao-seed-2-0-lite-260428",
"doubao-seed-2-0-mini-260428",
"doubao-seed-2-0-pro-260215",
},
},
{
Name: "deepseek",
DisplayName: "DeepSeek API",
Protocol: "openai",
BaseURL: "https://api.deepseek.com",
EnvVar: "DEEPSEEK_API_KEY",
Models: []string{
"deepseek-v4-pro",
"deepseek-v4-flash",
},
},
{
Name: "tencent-tokenhub",
DisplayName: "Tencent TokenHub API",
Protocol: "openai",
BaseURL: "https://tokenhub.tencentmaas.com/v1",
EnvVar: "TENCENT_TOKENHUB_API_KEY",
Models: []string{
"hy3-preview",
"deepseek-v4-pro",
"deepseek-v4-flash",
"glm-5.2",
"glm-5.1",
"glm-5",
"glm-5-turbo",
"kimi-k2.7-code",
"kimi-k2.6",
"kimi-k2.5",
"minimax-m3",
"minimax-m2.7",
"minimax-m2.5",
},
},
{
Name: "hy-tokenplan",
DisplayName: "Tencent Hunyuan Token Plan API",
Protocol: "openai",
BaseURL: "https://api.lkeap.cloud.tencent.com/plan/v3",
EnvVar: "TENCENT_HUNYUAN_TOKENPLAN_KEY",
Models: []string{
"hy3-preview",
},
},
{
Name: "kimi",
DisplayName: "Kimi Moonshot API",
Protocol: "openai",
BaseURL: "https://api.moonshot.cn/v1",
EnvVar: "MOONSHOT_API_KEY",
Models: []string{
"kimi-k2.7-code",
"kimi-k2.7-code-highspeed",
"kimi-k2.6",
"kimi-k2.5",
},
},
{
Name: "z-ai",
DisplayName: "Z.AI API",
Protocol: "openai",
BaseURL: "https://open.bigmodel.cn/api/paas/v4",
EnvVar: "Z_AI_API_KEY",
Models: []string{
"glm-5.2",
"glm-5.1",
"glm-5",
"glm-5-turbo",
"glm-4.7",
},
},
{
Name: "z-ai-coding",
DisplayName: "Z.AI Coding Plan API",
Protocol: "openai",
BaseURL: "https://open.bigmodel.cn/api/coding/paas/v4",
EnvVar: "Z_AI_CODING_API_KEY",
Models: []string{
"glm-5.2",
"glm-5.1",
"glm-5-turbo",
"glm-4.7",
},
},
{
Name: "mimo",
DisplayName: "Xiaomi MiMo API",
Protocol: "openai",
BaseURL: "https://api.xiaomimimo.com/v1",
EnvVar: "MIMO_API_KEY",
Models: []string{
"mimo-v2.5-pro",
"mimo-v2.5",
},
},
{
Name: "minimax",
DisplayName: "MiniMax API",
Protocol: "openai",
BaseURL: "https://api.minimaxi.com/v1",
EnvVar: "MINIMAX_API_KEY",
Models: []string{
"MiniMax-M3",
"MiniMax-M2.7",
"MiniMax-M2.7-highspeed",
"MiniMax-M2.5",
"MiniMax-M2.5-highspeed",
},
},
{
Name: "baidu-qianfan",
DisplayName: "Baidu Qianfan API",
Protocol: "openai",
BaseURL: "https://qianfan.baidubce.com/v2",
EnvVar: "QIANFAN_API_KEY",
Models: []string{
"ernie-5.1",
"ernie-5.0",
"ernie-x1.1",
"ernie-x1-turbo-32k-preview",
"deepseek-v4-pro",
"deepseek-v4-flash",
"glm-5.2",
"glm-5.1",
"glm-5",
"kimi-k2.6",
},
},
}
var registryMap map[string]Provider
func init() {
registryMap = make(map[string]Provider, len(registry))
for _, p := range registry {
registryMap[strings.ToLower(p.Name)] = p
}
}
// LookupProvider returns the preset provider by name.
// The returned Provider has its own copy of the Models slice.
func LookupProvider(name string) (Provider, bool) {
p, ok := registryMap[strings.ToLower(strings.TrimSpace(name))]
if ok {
p = copyProvider(p)
}
return p, ok
}
// ListProviders returns all built-in providers sorted by provider name.
// Each returned Provider has its own copy of the Models slice in registry order.
func ListProviders() []Provider {
out := make([]Provider, len(registry))
for i, p := range registry {
out[i] = copyProvider(p)
}
sort.Slice(out, func(i, j int) bool {
return out[i].Name < out[j].Name
})
return out
}
func copyProvider(p Provider) Provider {
if p.Models != nil {
models := make([]string, len(p.Models))
copy(models, p.Models)
p.Models = models
}
return p
}