diff --git a/backend/internal/service/channel_monitor_checker.go b/backend/internal/service/channel_monitor_checker.go index a310288bf9..ddb5e78317 100644 --- a/backend/internal/service/channel_monitor_checker.go +++ b/backend/internal/service/channel_monitor_checker.go @@ -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 { diff --git a/backend/internal/service/channel_monitor_endpoint_test.go b/backend/internal/service/channel_monitor_endpoint_test.go index 91482dd84f..7bb95aaa19 100644 --- a/backend/internal/service/channel_monitor_endpoint_test.go +++ b/backend/internal/service/channel_monitor_endpoint_test.go @@ -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")) +}