fix(channel-monitor): build zhipu probe URL from the configured endpoint

The zhipu adapter always appended the official /api/paas/v4/chat/completions
path, so probes broke whenever the endpoint was not the official root:

- OpenAI-compatible relay (e.g. https://relay.example.com): requested
  /api/paas/v4/chat/completions, which relays do not serve -> 404
- Coding Plan base (https://open.bigmodel.cn/api/coding/paas/v4): produced
  .../coding/paas/v4/api/paas/v4/chat/completions -> 404

Resolve the path from the endpoint instead: append /chat/completions when it
already carries /paas/v4, keep the official path for bigmodel.cn / z.ai
roots, and fall back to /v1/chat/completions (same as kimi/deepseek/minimax)
for everything else.

Fixes #7067

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
hezhaoqian
2026-10-06 13:09:51 +08:00
co-authored by Claude Opus 5.5
parent b8dece9000
commit 0173861a5a
2 changed files with 51 additions and 1 deletions
@@ -297,7 +297,7 @@ func callProvider(ctx context.Context, provider, endpoint, apiKey, model, prompt
return "", "", 0, err
}
headers := mergeHeaders(adapter.buildHeaders(apiKey), opts)
full := joinURL(endpoint, adapter.buildPath(model))
full := joinURL(endpoint, monitorRequestPath(provider, endpoint, adapter, model))
respBytes, status, err := postRawJSON(ctx, full, body, headers)
if err != nil {
return "", "", status, err
@@ -556,6 +556,37 @@ func postRawJSON(ctx context.Context, fullURL string, payload []byte, headers ma
return respBody, resp.StatusCode, nil
}
// monitorRequestPath 返回探测请求路径。智谱按 endpoint 区分:
// - 已带 /paas/v4(含 Coding Plan 的 /api/coding/paas/v4):只追加 /chat/completions
// - 官方域名根地址:/api/paas/v4/chat/completions
// - 中转站 / 本站网关:只暴露 OpenAI 兼容的 /v1/chat/completions,与 Kimi / DeepSeek 一致
func monitorRequestPath(provider, endpoint string, adapter providerAdapter, model string) string {
if provider != MonitorProviderZhipu {
return adapter.buildPath(model)
}
u, err := url.Parse(strings.TrimSpace(endpoint))
if err != nil {
return providerOpenAIPath
}
if strings.Contains(u.EscapedPath(), "/paas/v4") {
return "/chat/completions"
}
if isZhipuOfficialHost(u) {
return adapter.buildPath(model)
}
return providerOpenAIPath
}
func isZhipuOfficialHost(u *url.URL) bool {
host := strings.ToLower(u.Hostname())
for _, official := range []string{"bigmodel.cn", "z.ai"} {
if host == official || strings.HasSuffix(host, "."+official) {
return true
}
}
return false
}
// joinURL 保留 base 的上游路径前缀,并避免重复追加已有的 API 路径前缀。
// 使用 EscapedPath 匹配完整路径段,避免把 hostname 或编码斜杠当作路径。
func joinURL(base, path string) string {
@@ -84,6 +84,9 @@ func TestCallProvider_BasePath(t *testing.T) {
{"openai responses version", MonitorProviderOpenAI, MonitorAPIModeResponses, "/relay/v1", "/relay/v1/responses"},
{"gemini version", MonitorProviderGemini, "", "/relay/v1beta", "/relay/v1beta/models/test-model:generateContent"},
{"zhipu version", MonitorProviderZhipu, "", "/api/paas/v4", "/api/paas/v4/chat/completions"},
{"zhipu coding plan", MonitorProviderZhipu, "", "/api/coding/paas/v4", "/api/coding/paas/v4/chat/completions"},
{"zhipu relay origin", MonitorProviderZhipu, "", "", "/v1/chat/completions"},
{"zhipu relay version", MonitorProviderZhipu, "", "/relay/v1", "/relay/v1/chat/completions"},
} {
t.Run(tc.name, func(t *testing.T) {
_, _, status, err := callProvider(context.Background(), tc.provider,
@@ -94,3 +97,19 @@ func TestCallProvider_BasePath(t *testing.T) {
})
}
}
func TestMonitorRequestPath_Zhipu(t *testing.T) {
adapter := providerAdapters[MonitorProviderZhipu]
for endpoint, want := range map[string]string{
"https://open.bigmodel.cn": providerZhipuPath,
"https://api.z.ai": providerZhipuPath,
"https://open.bigmodel.cn/api/coding/paas/v4": "/chat/completions",
"https://birdapi.up.railway.app": providerOpenAIPath,
"https://notbigmodel.cn": providerOpenAIPath,
"https://relay.example.com/v1": providerOpenAIPath,
} {
require.Equal(t, want, monitorRequestPath(MonitorProviderZhipu, endpoint, adapter, "glm-5.3-flash"), endpoint)
}
require.Equal(t, providerOpenAIPath,
monitorRequestPath(MonitorProviderDeepseek, "https://birdapi.up.railway.app", providerAdapters[MonitorProviderDeepseek], "m"))
}