feat(aiproxy): add Z.AI / Zhipu provider with dual OpenAI and Anthropic API modes (#25131)

Register zhipu as a public OpenAI-compat provider with GLM catalog seeds,
Anthropic passthrough routing, and v4-style base URL path handling.
This commit is contained in:
Zexi Li
2026-07-13 11:25:22 +08:00
committed by GitHub
parent afde072512
commit 40b3c82ab4
14 changed files with 186 additions and 23 deletions
+43
View File
@@ -51,6 +51,7 @@ climc ai-test-chat
| `DASHSCOPE_API_KEY` | 通义千问(`provider=aliyun` |
| `MIMO_API_KEY` | 小米 MiMo`provider=xiaomi` |
| `MOONSHOT_API_KEY` | Moonshot / Kimi`provider=moonshot` |
| `ZHIPU_API_KEY` / `ZAI_API_KEY` | Z.AI / 智谱(`provider=zhipu` |
| `ANTHROPIC_API_KEY` | Anthropic 直通 |
| `DEEPSEEK_API_KEY` | DeepSeekAnthropic 兼容场景) |
| `AIPROXY_TEST_SKIP_STREAM` | `1` 跳过流式;`0` 强制流式 |
@@ -112,6 +113,48 @@ climc ai-test-chat --provider moonshot --model kimi-k2.6 --api-key "$MOONSHOT_AP
其它 catalog 模型:`kimi-k2.7-code``kimi-k2.5``moonshot-v1-8k` 等(id 形如 `moonshot-kimi-k2.6`)。
### Z.AI / 智谱(zhipu
控制台展示为 **Z.AI**`provider_key` 固定为 **`zhipu`**。上游 OpenAI 兼容 base 为 `https://open.bigmodel.cn/api/paas/v4`Anthropic 兼容 base 为 `https://open.bigmodel.cn/api/anthropic`。支持 `config.api_mode=openai|anthropic`(与 DeepSeek 类似的双 API 模式)。
**OpenAI 兼容(默认 `api_mode=openai`**
```bash
export ZHIPU_API_KEY='你的智谱 API Key'
climc ai-test-chat --provider zhipu --model glm-5.2 --api-key "$ZHIPU_API_KEY"
```
**Anthropic 兼容(`config.api_mode=anthropic`**aiproxy 将 Anthropic SDK 请求直通 `https://open.bigmodel.cn/api/anthropic/v1/messages``base_url` 可仍填 OpenAI 默认,由 aiproxy 自动切换)。
```bash
climc ai-test-anthropic --provider zhipu --model glm-5.2 \
--api-key "$ZHIPU_API_KEY" --upstream-base-url https://open.bigmodel.cn/api/anthropic
```
创建 provider 示例:
```json
{
"generate_name": "my-zhipu",
"provider_key": "zhipu",
"secret": "<zhipu-api-key>",
"config": {
"base_url": "https://open.bigmodel.cn/api/paas/v4",
"api_mode": "openai"
}
}
```
其它 catalog 模型:`glm-5.1``glm-5-turbo``glm-4.7``glm-4.7-flash` 等(id 形如 `zhipu-glm-5.2`)。
非交互:
```bash
export AIPROXY_TEST_PROVIDER=zhipu AIPROXY_TEST_MODEL=glm-5.2
export AIPROXY_TEST_API_KEY="$ZHIPU_API_KEY"
climc ai-test-chat
```
### Anthropic Messages API
数据面 **`POST /ai/anthropic/v1/messages`**,认证为 `Authorization: Bearer <virtual_key>`**不是**上游 Anthropic/DeepSeek API Key)。
+4
View File
@@ -41,6 +41,8 @@ func DefaultModelForProvider(providerKey string) string {
return "kimi-k2.6"
case api.ProviderKeyDeepseek:
return "deepseek-v4-flash"
case api.ProviderKeyZhipu:
return "glm-5.2"
case api.ProviderKeyOpenAI:
return "gpt-4o-mini"
case api.ProviderKeyAnthropic:
@@ -58,6 +60,8 @@ func DefaultPromptForProvider(providerKey string) string {
return "用一句话介绍小米 MiMo"
case api.ProviderKeyMoonshot:
return "用一句话介绍 Kimi"
case api.ProviderKeyZhipu:
return "用一句话介绍 Z.AI GLM"
default:
return "用一句话介绍这个模型"
}
+5
View File
@@ -75,6 +75,11 @@ func resolveApiKeyFromEnv(providerKey string) string {
return os.Getenv("MIMO_API_KEY")
case "moonshot":
return os.Getenv("MOONSHOT_API_KEY")
case "zhipu":
if v := os.Getenv("ZHIPU_API_KEY"); v != "" {
return v
}
return os.Getenv("ZAI_API_KEY")
case "anthropic":
return os.Getenv("ANTHROPIC_API_KEY")
case "openai":
+14
View File
@@ -254,6 +254,8 @@ func catalogSeedModelsForProvider(providerKey string) []catalogSeedModel {
return xiaomiMimoSeedModels()
case api.ProviderKeyMoonshot:
return moonshotKimiSeedModels()
case api.ProviderKeyZhipu:
return zhipuGLMSeedModels()
default:
return nil
}
@@ -358,3 +360,15 @@ func moonshotKimiSeedModels() []catalogSeedModel {
{ModelKey: "moonshot-v1-128k-vision-preview", Description: "Moonshot v1 128K Vision"},
}
}
func zhipuGLMSeedModels() []catalogSeedModel {
return []catalogSeedModel{
{ModelKey: "glm-5.2", Description: "Z.AI GLM-5.2 flagship; 1M context; long-horizon agents"},
{ModelKey: "glm-5.1", Description: "Z.AI GLM-5.1; long-horizon tasks"},
{ModelKey: "glm-5-turbo", Description: "Z.AI GLM-5-Turbo; cost-efficient"},
{ModelKey: "glm-4.7", Description: "Z.AI GLM-4.7; general chat and coding"},
{ModelKey: "glm-4.7-flash", Description: "Z.AI GLM-4.7-Flash; free tier"},
{ModelKey: "glm-4.6", Description: "Z.AI GLM-4.6; 200K context"},
{ModelKey: "glm-5v-turbo", Description: "Z.AI GLM-5V-Turbo; vision"},
}
}
+1
View File
@@ -14,6 +14,7 @@ func TestCatalogSeedModelsForPublicProviders(t *testing.T) {
api.ProviderKeyGemini,
api.ProviderKeyMoonshot,
api.ProviderKeyXiaomi,
api.ProviderKeyZhipu,
}
for _, pk := range publicKeys {
if !api.HasDefaultPublicBaseURL(pk) {
@@ -123,6 +123,32 @@ func TestGetAdapterCustomAnthropicPassthrough(t *testing.T) {
}
}
func TestGetAdapterZhipuAnthropicPassthrough(t *testing.T) {
adapter, err := GetAdapter("zhipu", "anthropic")
if err != nil {
t.Fatal(err)
}
if !adapter.AnthropicStreamPassthrough() {
t.Fatal("expected passthrough stream for zhipu anthropic mode")
}
body := jsonutils.NewDict()
body.Set("model", jsonutils.NewString("glm-5.2"))
body.Set("max_tokens", jsonutils.NewInt(100))
user := jsonutils.NewDict()
user.Set("role", jsonutils.NewString("user"))
user.Set("content", jsonutils.NewString("hi"))
body.Set("messages", jsonutils.NewArray(user))
req, err := adapter.BuildUpstreamRequest(testChatCtx("zhipu", "https://open.bigmodel.cn/api/anthropic", "zhipu-key", "glm-5.2"), body, false)
if err != nil {
t.Fatal(err)
}
want := "https://open.bigmodel.cn/api/anthropic/v1/messages"
if req.URL != want {
t.Fatalf("url: %s, want %s", req.URL, want)
}
}
func TestGetAdapterBlocksGemini(t *testing.T) {
if _, err := GetAdapter("gemini", ""); err == nil {
t.Fatal("expected gemini to be unsupported")
+13 -16
View File
@@ -137,10 +137,7 @@ func ChatCompletionsURL(baseURL string) string {
if strings.HasSuffix(base, "/chat/completions") {
return base
}
if strings.HasSuffix(base, "/v2") {
return base + "/chat/completions"
}
if strings.HasSuffix(base, "/v1") {
if hasAPIVersionPathSuffix(base) {
return base + "/chat/completions"
}
return base + "/v1/chat/completions"
@@ -152,10 +149,7 @@ func CompletionsURL(baseURL string) string {
if strings.HasSuffix(base, "/completions") {
return base
}
if strings.HasSuffix(base, "/v2") {
return base + "/completions"
}
if strings.HasSuffix(base, "/v1") {
if hasAPIVersionPathSuffix(base) {
return base + "/completions"
}
return base + "/v1/completions"
@@ -167,10 +161,7 @@ func EmbeddingsURL(baseURL string) string {
if strings.HasSuffix(base, "/embeddings") {
return base
}
if strings.HasSuffix(base, "/v2") {
return base + "/embeddings"
}
if strings.HasSuffix(base, "/v1") {
if hasAPIVersionPathSuffix(base) {
return base + "/embeddings"
}
return base + "/v1/embeddings"
@@ -182,15 +173,21 @@ func ImagesGenerationsURL(baseURL string) string {
if strings.HasSuffix(base, "/images/generations") {
return base
}
if strings.HasSuffix(base, "/v2") {
return base + "/images/generations"
}
if strings.HasSuffix(base, "/v1") {
if hasAPIVersionPathSuffix(base) {
return base + "/images/generations"
}
return base + "/v1/images/generations"
}
func hasAPIVersionPathSuffix(base string) bool {
idx := strings.LastIndex(base, "/")
if idx < 0 {
return false
}
seg := base[idx+1:]
return len(seg) >= 2 && seg[0] == 'v' && seg[1] >= '0' && seg[1] <= '9'
}
// BearerAuthHeaders returns standard OpenAI bearer auth headers.
func BearerAuthHeaders(apiKey string) map[string]string {
return map[string]string{
@@ -23,3 +23,11 @@ func TestChatCompletionsURLMoonshot(t *testing.T) {
t.Fatalf("ChatCompletionsURL() = %q, want %q", got, want)
}
}
func TestChatCompletionsURLZhipu(t *testing.T) {
got := ChatCompletionsURL("https://open.bigmodel.cn/api/paas/v4")
want := "https://open.bigmodel.cn/api/paas/v4/chat/completions"
if got != want {
t.Fatalf("ChatCompletionsURL() = %q, want %q", got, want)
}
}
+4 -1
View File
@@ -79,7 +79,10 @@ func (e *Error) Error() string {
// BaseURL is the origin + optional path prefix (e.g. https://dashscope.aliyuncs.com/compatible-mode).
func ChatCompletionsURL(baseURL string) string {
base := strings.TrimRight(strings.TrimSpace(baseURL), "/")
if strings.HasSuffix(base, "/v1") {
if strings.HasSuffix(base, "/chat/completions") {
return base
}
if strings.HasSuffix(base, "/v1") || hasAPIVersionPathSuffix(base) {
return base + "/chat/completions"
}
return base + "/v1/chat/completions"
@@ -25,6 +25,7 @@ func TestModelsURL(t *testing.T) {
{"https://api.openai.com/v1", "https://api.openai.com/v1/models"},
{"https://generativelanguage.googleapis.com/v1beta", "https://generativelanguage.googleapis.com/v1beta/models"},
{"https://api.deepseek.com/anthropic", "https://api.deepseek.com/anthropic/v1/models"},
{"https://open.bigmodel.cn/api/paas/v4", "https://open.bigmodel.cn/api/paas/v4/models"},
}
for _, tc := range cases {
if got := ModelsURL(tc.base); got != tc.want {
+18 -6
View File
@@ -83,14 +83,26 @@ func (c *SAiProviderConfig) EffectiveBaseURL(providerKey string) string {
return base
}
pk := strings.ToLower(strings.TrimSpace(providerKey))
if pk != ProviderKeyDeepseek {
switch pk {
case ProviderKeyDeepseek:
base = strings.TrimRight(base, "/")
if strings.HasSuffix(strings.ToLower(base), "/anthropic") {
return base
}
return base + "/anthropic"
case ProviderKeyZhipu:
base = strings.TrimRight(base, "/")
if strings.HasSuffix(strings.ToLower(base), "/api/anthropic") {
return base
}
openaiDefault := strings.TrimRight(DefaultPublicBaseURL(ProviderKeyZhipu), "/")
if base == "" || strings.EqualFold(base, openaiDefault) {
return DefaultZhipuAnthropicBaseURL()
}
return base
default:
return base
}
base = strings.TrimRight(base, "/")
if strings.HasSuffix(strings.ToLower(base), "/anthropic") {
return base
}
return base + "/anthropic"
}
// String implements gotypes.ISerializable for sqlchemy JSON/compound columns.
@@ -40,6 +40,9 @@ func TestSupportsDualAPIMode(t *testing.T) {
if !SupportsDualAPIMode(ProviderKeyCustom) {
t.Fatal("custom should support dual api mode")
}
if !SupportsDualAPIMode(ProviderKeyZhipu) {
t.Fatal("zhipu should support dual api mode")
}
if SupportsDualAPIMode(ProviderKeyOpenAI) {
t.Fatal("openai should not support dual api mode")
}
@@ -100,4 +103,40 @@ func TestHasDefaultPublicBaseURL(t *testing.T) {
if DefaultPublicBaseURL(ProviderKeyMoonshot) != "https://api.moonshot.cn" {
t.Fatalf("moonshot default base = %q", DefaultPublicBaseURL(ProviderKeyMoonshot))
}
if !HasDefaultPublicBaseURL(ProviderKeyZhipu) {
t.Fatal("zhipu should have default base url")
}
if DefaultPublicBaseURL(ProviderKeyZhipu) != "https://open.bigmodel.cn/api/paas/v4" {
t.Fatalf("zhipu default base = %q", DefaultPublicBaseURL(ProviderKeyZhipu))
}
}
func TestEffectiveBaseURLZhipuAnthropic(t *testing.T) {
cfg := &SAiProviderConfig{
BaseURL: "https://open.bigmodel.cn/api/paas/v4",
APIMode: ProviderAPIModeAnthropic,
}
got := cfg.EffectiveBaseURL(ProviderKeyZhipu)
want := DefaultZhipuAnthropicBaseURL()
if got != want {
t.Fatalf("EffectiveBaseURL() = %q, want %q", got, want)
}
}
func TestEffectiveBaseURLZhipuAnthropicFallback(t *testing.T) {
cfg := &SAiProviderConfig{APIMode: ProviderAPIModeAnthropic}
got := cfg.EffectiveBaseURL(ProviderKeyZhipu)
want := DefaultZhipuAnthropicBaseURL()
if got != want {
t.Fatalf("EffectiveBaseURL() = %q, want %q", got, want)
}
}
func TestEffectiveBaseURLZhipuOpenAI(t *testing.T) {
cfg := &SAiProviderConfig{}
got := cfg.EffectiveBaseURL(ProviderKeyZhipu)
want := "https://open.bigmodel.cn/api/paas/v4"
if got != want {
t.Fatalf("EffectiveBaseURL() = %q, want %q", got, want)
}
}
+7
View File
@@ -42,11 +42,18 @@ func DefaultPublicBaseURL(providerKey string) string {
return "https://api.xiaomimimo.com"
case ProviderKeyMoonshot:
return "https://api.moonshot.cn"
case ProviderKeyZhipu:
return "https://open.bigmodel.cn/api/paas/v4"
default:
return ""
}
}
// DefaultZhipuAnthropicBaseURL is the upstream base for Zhipu Claude-compatible API.
func DefaultZhipuAnthropicBaseURL() string {
return "https://open.bigmodel.cn/api/anthropic"
}
// HasDefaultPublicBaseURL reports whether provider_key has a built-in public base URL.
func HasDefaultPublicBaseURL(providerKey string) bool {
return DefaultPublicBaseURL(providerKey) != ""
+3
View File
@@ -47,6 +47,7 @@ const (
ProviderKeyVLLM = "vllm"
ProviderKeyXai = "xai"
ProviderKeyXiaomi = "xiaomi"
ProviderKeyZhipu = "zhipu"
)
// OpenAICompatProviderKeys are catalog keys routed through openai.NewCompat.
@@ -67,6 +68,7 @@ var OpenAICompatProviderKeys = []string{
ProviderKeyOllama,
ProviderKeyXiaomi,
ProviderKeyMoonshot,
ProviderKeyZhipu,
}
var nativeMessagesAdapterProviderKeys = map[string]struct{}{
@@ -105,6 +107,7 @@ const (
var DualAPIProviderKeys = map[string]struct{}{
ProviderKeyCustom: {},
ProviderKeyDeepseek: {},
ProviderKeyZhipu: {},
}
// IsCustomProviderKey reports whether provider_key is the user-defined custom gateway type.