fix(model):修复模型测试未使用代理的问题
This commit is contained in:
1 parent
586439a19a
commit
d348c72504
2 files changed
+131
-135
No files matched your search
@@ -1,121 +1,126 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
|
||||
"github.com/qianfree/team-api/api/admin/v1"
|
||||
v1 "github.com/qianfree/team-api/api/admin/v1"
|
||||
relaycommon "github.com/qianfree/team-api/relay/common"
|
||||
"github.com/qianfree/team-api/relay/constant"
|
||||
)
|
||||
|
||||
// sendTestRequest 发送最小测试请求到上游供应商
|
||||
func sendTestRequest(ctx context.Context, channelType int, baseURL, apiKey, modelName string) testResult {
|
||||
func sendTestRequest(ctx context.Context, channelType int, baseURL, apiKey, modelName string, useProxy bool) testResult {
|
||||
baseURL = strings.TrimSuffix(baseURL, "/")
|
||||
|
||||
switch constant.ProviderType(channelType) {
|
||||
case constant.ProviderOpenAI:
|
||||
return testOpenAI(ctx, baseURL, apiKey, modelName)
|
||||
return testOpenAI(ctx, baseURL, apiKey, modelName, useProxy)
|
||||
case constant.ProviderClaude:
|
||||
return testClaude(ctx, baseURL, apiKey, modelName)
|
||||
return testClaude(ctx, baseURL, apiKey, modelName, useProxy)
|
||||
case constant.ProviderGemini:
|
||||
return testGemini(ctx, baseURL, apiKey, modelName)
|
||||
return testGemini(ctx, baseURL, apiKey, modelName, useProxy)
|
||||
case constant.ProviderZhipu:
|
||||
return testZhipu(ctx, baseURL, apiKey, modelName)
|
||||
return testZhipu(ctx, baseURL, apiKey, modelName, useProxy)
|
||||
default:
|
||||
// 尝试 OpenAI 兼容格式
|
||||
return testOpenAI(ctx, baseURL, apiKey, modelName)
|
||||
return testOpenAI(ctx, baseURL, apiKey, modelName, useProxy)
|
||||
}
|
||||
}
|
||||
|
||||
func testOpenAI(ctx context.Context, baseURL, apiKey, modelName string) testResult {
|
||||
// doTestHTTPRequest 发送测试HTTP请求,根据渠道设置决定是否使用代理
|
||||
func doTestHTTPRequest(ctx context.Context, method, reqURL string, headers map[string]string, body []byte, useProxy bool) (*http.Response, error) {
|
||||
client := relaycommon.NewPooledClient(30, useProxy)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, reqURL, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建请求失败: %w", err)
|
||||
}
|
||||
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
return client.Do(req)
|
||||
}
|
||||
|
||||
// executeTest 封装通用的测试请求执行和结果处理逻辑
|
||||
func executeTest(ctx context.Context, provider, method, reqURL string, headers map[string]string, body []byte, useProxy bool) testResult {
|
||||
maskedHdrs := maskHeaders(headers)
|
||||
bodyStr := string(body)
|
||||
|
||||
g.Log().Infof(ctx, "[ChannelTest] %s | 发送请求 | %s %s | 代理: %v", provider, method, reqURL, useProxy)
|
||||
|
||||
resp, err := doTestHTTPRequest(ctx, method, reqURL, headers, body, useProxy)
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "[ChannelTest] %s | 请求失败 | %s | 错误: %v", provider, reqURL, err)
|
||||
return testResult{
|
||||
Error: fmt.Sprintf("请求失败: %v", err),
|
||||
Request: buildReqDetail(method, reqURL, maskedHdrs, bodyStr),
|
||||
}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
respStr := string(respBody)
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
g.Log().Warningf(ctx, "[ChannelTest] %s | 测试失败 | HTTP %d | 响应: %s", provider, resp.StatusCode, truncateStr(respStr, 200))
|
||||
return testResult{
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("HTTP %d: %s", resp.StatusCode, truncateStr(respStr, 500)),
|
||||
Response: truncateStr(respStr, 500),
|
||||
Request: buildReqDetail(method, reqURL, maskedHdrs, bodyStr),
|
||||
}
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "[ChannelTest] %s | 测试成功 | HTTP %d | 响应: %s", provider, resp.StatusCode, truncateStr(respStr, 200))
|
||||
return testResult{
|
||||
Success: true,
|
||||
Response: truncateStr(respStr, 500),
|
||||
Request: buildReqDetail(method, reqURL, maskedHdrs, bodyStr),
|
||||
}
|
||||
}
|
||||
|
||||
func testOpenAI(ctx context.Context, baseURL, apiKey, modelName string, useProxy bool) testResult {
|
||||
reqBody := map[string]any{
|
||||
"model": modelName,
|
||||
"max_tokens": 5,
|
||||
"messages": []map[string]string{{"role": "user", "content": "hi"}},
|
||||
}
|
||||
bodyJSON, _ := json.Marshal(reqBody)
|
||||
|
||||
reqURL := baseURL + "/v1/chat/completions"
|
||||
|
||||
client := g.Client().SetTimeout(30 * time.Second)
|
||||
client.SetHeader("Authorization", "Bearer "+apiKey)
|
||||
client.SetHeader("Content-Type", "application/json")
|
||||
|
||||
resp, err := client.DoRequest(ctx, "POST", reqURL, reqBody)
|
||||
if err != nil {
|
||||
return testResult{
|
||||
Error: fmt.Sprintf("请求失败: %v", err),
|
||||
Request: buildReqDetail("POST", reqURL, map[string]string{"Authorization": maskKey(apiKey), "Content-Type": "application/json"}, string(bodyJSON)),
|
||||
}
|
||||
}
|
||||
defer resp.Close()
|
||||
|
||||
respBody := string(resp.ReadAll())
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return testResult{
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("HTTP %d: %s", resp.StatusCode, truncateStr(respBody, 500)),
|
||||
Response: truncateStr(respBody, 500),
|
||||
Request: buildReqDetail("POST", reqURL, map[string]string{"Authorization": maskKey(apiKey), "Content-Type": "application/json"}, string(bodyJSON)),
|
||||
}
|
||||
}
|
||||
|
||||
return testResult{
|
||||
Success: true,
|
||||
Response: truncateStr(respBody, 500),
|
||||
Request: buildReqDetail("POST", reqURL, map[string]string{"Authorization": maskKey(apiKey), "Content-Type": "application/json"}, string(bodyJSON)),
|
||||
headers := map[string]string{
|
||||
"Authorization": "Bearer " + apiKey,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
return executeTest(ctx, "OpenAI", "POST", reqURL, headers, bodyJSON, useProxy)
|
||||
}
|
||||
|
||||
func testClaude(ctx context.Context, baseURL, apiKey, modelName string) testResult {
|
||||
func testClaude(ctx context.Context, baseURL, apiKey, modelName string, useProxy bool) testResult {
|
||||
reqBody := map[string]any{
|
||||
"model": modelName,
|
||||
"max_tokens": 5,
|
||||
"messages": []map[string]string{{"role": "user", "content": "hi"}},
|
||||
}
|
||||
bodyJSON, _ := json.Marshal(reqBody)
|
||||
|
||||
reqURL := baseURL + "/v1/messages"
|
||||
|
||||
client := g.Client().SetTimeout(30 * time.Second)
|
||||
client.SetHeader("x-api-key", apiKey)
|
||||
client.SetHeader("anthropic-version", "2023-06-01")
|
||||
client.SetHeader("Content-Type", "application/json")
|
||||
|
||||
resp, err := client.DoRequest(ctx, "POST", reqURL, reqBody)
|
||||
if err != nil {
|
||||
return testResult{
|
||||
Error: fmt.Sprintf("请求失败: %v", err),
|
||||
Request: buildReqDetail("POST", reqURL, map[string]string{"x-api-key": maskKey(apiKey), "anthropic-version": "2023-06-01", "Content-Type": "application/json"}, string(bodyJSON)),
|
||||
}
|
||||
}
|
||||
defer resp.Close()
|
||||
|
||||
respBody := string(resp.ReadAll())
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return testResult{
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("HTTP %d: %s", resp.StatusCode, truncateStr(respBody, 500)),
|
||||
Response: truncateStr(respBody, 500),
|
||||
Request: buildReqDetail("POST", reqURL, map[string]string{"x-api-key": maskKey(apiKey), "anthropic-version": "2023-06-01", "Content-Type": "application/json"}, string(bodyJSON)),
|
||||
}
|
||||
}
|
||||
|
||||
return testResult{
|
||||
Success: true,
|
||||
Response: truncateStr(respBody, 500),
|
||||
Request: buildReqDetail("POST", reqURL, map[string]string{"x-api-key": maskKey(apiKey), "anthropic-version": "2023-06-01", "Content-Type": "application/json"}, string(bodyJSON)),
|
||||
headers := map[string]string{
|
||||
"x-api-key": apiKey,
|
||||
"anthropic-version": "2023-06-01",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
return executeTest(ctx, "Claude", "POST", reqURL, headers, bodyJSON, useProxy)
|
||||
}
|
||||
|
||||
func testGemini(ctx context.Context, baseURL, apiKey, modelName string) testResult {
|
||||
func testGemini(ctx context.Context, baseURL, apiKey, modelName string, useProxy bool) testResult {
|
||||
reqBody := map[string]any{
|
||||
"contents": []map[string]any{
|
||||
{"role": "user", "parts": []map[string]string{{"text": "hi"}}},
|
||||
@@ -125,79 +130,27 @@ func testGemini(ctx context.Context, baseURL, apiKey, modelName string) testResu
|
||||
},
|
||||
}
|
||||
bodyJSON, _ := json.Marshal(reqBody)
|
||||
|
||||
reqURL := fmt.Sprintf("%s/v1beta/models/%s:generateContent", baseURL, modelName)
|
||||
|
||||
client := g.Client().SetTimeout(30 * time.Second)
|
||||
client.SetHeader("x-goog-api-key", apiKey)
|
||||
client.SetHeader("Content-Type", "application/json")
|
||||
|
||||
resp, err := client.DoRequest(ctx, "POST", reqURL, reqBody)
|
||||
if err != nil {
|
||||
return testResult{
|
||||
Error: fmt.Sprintf("请求失败: %v", err),
|
||||
Request: buildReqDetail("POST", reqURL, map[string]string{"x-goog-api-key": maskKey(apiKey), "Content-Type": "application/json"}, string(bodyJSON)),
|
||||
}
|
||||
}
|
||||
defer resp.Close()
|
||||
|
||||
respBody := string(resp.ReadAll())
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return testResult{
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("HTTP %d: %s", resp.StatusCode, truncateStr(respBody, 500)),
|
||||
Response: truncateStr(respBody, 500),
|
||||
Request: buildReqDetail("POST", reqURL, map[string]string{"x-goog-api-key": maskKey(apiKey), "Content-Type": "application/json"}, string(bodyJSON)),
|
||||
}
|
||||
}
|
||||
|
||||
return testResult{
|
||||
Success: true,
|
||||
Response: truncateStr(respBody, 500),
|
||||
Request: buildReqDetail("POST", reqURL, map[string]string{"x-goog-api-key": maskKey(apiKey), "Content-Type": "application/json"}, string(bodyJSON)),
|
||||
headers := map[string]string{
|
||||
"x-goog-api-key": apiKey,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
return executeTest(ctx, "Gemini", "POST", reqURL, headers, bodyJSON, useProxy)
|
||||
}
|
||||
|
||||
func testZhipu(ctx context.Context, baseURL, apiKey, modelName string) testResult {
|
||||
func testZhipu(ctx context.Context, baseURL, apiKey, modelName string, useProxy bool) testResult {
|
||||
reqBody := map[string]any{
|
||||
"model": modelName,
|
||||
"max_tokens": 5,
|
||||
"messages": []map[string]string{{"role": "user", "content": "hi"}},
|
||||
}
|
||||
bodyJSON, _ := json.Marshal(reqBody)
|
||||
|
||||
reqURL := baseURL + "/api/paas/v4/chat/completions"
|
||||
|
||||
client := g.Client().SetTimeout(30 * time.Second)
|
||||
client.SetHeader("Authorization", "Bearer "+apiKey)
|
||||
client.SetHeader("Content-Type", "application/json")
|
||||
|
||||
resp, err := client.DoRequest(ctx, "POST", reqURL, reqBody)
|
||||
if err != nil {
|
||||
return testResult{
|
||||
Error: fmt.Sprintf("请求失败: %v", err),
|
||||
Request: buildReqDetail("POST", reqURL, map[string]string{"Authorization": maskKey(apiKey), "Content-Type": "application/json"}, string(bodyJSON)),
|
||||
}
|
||||
}
|
||||
defer resp.Close()
|
||||
|
||||
respBody := string(resp.ReadAll())
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return testResult{
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("HTTP %d: %s", resp.StatusCode, truncateStr(respBody, 500)),
|
||||
Response: truncateStr(respBody, 500),
|
||||
Request: buildReqDetail("POST", reqURL, map[string]string{"Authorization": maskKey(apiKey), "Content-Type": "application/json"}, string(bodyJSON)),
|
||||
}
|
||||
}
|
||||
|
||||
return testResult{
|
||||
Success: true,
|
||||
Response: truncateStr(respBody, 500),
|
||||
Request: buildReqDetail("POST", reqURL, map[string]string{"Authorization": maskKey(apiKey), "Content-Type": "application/json"}, string(bodyJSON)),
|
||||
headers := map[string]string{
|
||||
"Authorization": "Bearer " + apiKey,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
return executeTest(ctx, "Zhipu", "POST", reqURL, headers, bodyJSON, useProxy)
|
||||
}
|
||||
|
||||
func buildReqDetail(method, url string, headers map[string]string, body string) *v1.ChannelTestReqDetail {
|
||||
@@ -209,6 +162,25 @@ func buildReqDetail(method, url string, headers map[string]string, body string)
|
||||
}
|
||||
}
|
||||
|
||||
// maskHeaders 对请求头中的敏感信息进行脱敏
|
||||
func maskHeaders(headers map[string]string) map[string]string {
|
||||
masked := make(map[string]string, len(headers))
|
||||
for k, v := range headers {
|
||||
lower := strings.ToLower(k)
|
||||
if strings.Contains(lower, "key") || strings.Contains(lower, "auth") {
|
||||
// 保留 "Bearer " 前缀
|
||||
if strings.HasPrefix(v, "Bearer ") {
|
||||
masked[k] = "Bearer " + maskKey(strings.TrimPrefix(v, "Bearer "))
|
||||
} else {
|
||||
masked[k] = maskKey(v)
|
||||
}
|
||||
} else {
|
||||
masked[k] = v
|
||||
}
|
||||
}
|
||||
return masked
|
||||
}
|
||||
|
||||
// maskKey 对 API Key 进行脱敏处理
|
||||
func maskKey(key string) string {
|
||||
if len(key) <= 8 {
|
||||
|
||||
@@ -2,8 +2,11 @@ package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
|
||||
"github.com/qianfree/team-api/api/admin/v1"
|
||||
"github.com/qianfree/team-api/internal/dao"
|
||||
"github.com/qianfree/team-api/internal/logic/common"
|
||||
@@ -22,12 +25,13 @@ func (s *sAdmin) TestChannel(ctx context.Context, req *v1.ChannelTestReq) (*v1.C
|
||||
BaseURL string `json:"base_url"`
|
||||
TestModel string `json:"test_model"`
|
||||
Status string `json:"status"`
|
||||
Settings string `json:"settings"`
|
||||
}
|
||||
|
||||
var ch *channelRow
|
||||
err := dao.ChnChannels.Ctx(ctx).
|
||||
Where("id", channelID).
|
||||
Fields("id, name, type, base_url, test_model, status").
|
||||
Fields("id, name, type, base_url, test_model, status, settings").
|
||||
Scan(&ch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -69,9 +73,20 @@ func (s *sAdmin) TestChannel(ctx context.Context, req *v1.ChannelTestReq) (*v1.C
|
||||
return nil, common.NewBadRequestError("解密 API Key 失败")
|
||||
}
|
||||
|
||||
// 解析渠道设置中的 use_proxy
|
||||
useProxy := false
|
||||
if ch.Settings != "" {
|
||||
var settings struct {
|
||||
UseProxy bool `json:"use_proxy"`
|
||||
}
|
||||
if json.Unmarshal([]byte(ch.Settings), &settings) == nil {
|
||||
useProxy = settings.UseProxy
|
||||
}
|
||||
}
|
||||
|
||||
// 发送最小测试请求
|
||||
startTime := time.Now()
|
||||
result := sendTestRequest(ctx, ch.Type, ch.BaseURL, apiKey, testModel)
|
||||
result := sendTestRequest(ctx, ch.Type, ch.BaseURL, apiKey, testModel, useProxy)
|
||||
latencyMs := time.Since(startTime).Milliseconds()
|
||||
|
||||
// 更新健康度
|
||||
@@ -81,6 +96,15 @@ func (s *sAdmin) TestChannel(ctx context.Context, req *v1.ChannelTestReq) (*v1.C
|
||||
relay.UpdateHealthScoreDirect(ctx, channelID, false, float64(latencyMs))
|
||||
}
|
||||
|
||||
// 记录测试结果日志
|
||||
if result.Success {
|
||||
g.Log().Infof(ctx, "[ChannelTest] 渠道 %s (%d) 测试成功 | 模型: %s | 延迟: %dms | 代理: %v",
|
||||
ch.Name, channelID, testModel, latencyMs, useProxy)
|
||||
} else {
|
||||
g.Log().Warningf(ctx, "[ChannelTest] 渠道 %s (%d) 测试失败 | 模型: %s | 延迟: %dms | 代理: %v | 错误: %s",
|
||||
ch.Name, channelID, testModel, latencyMs, useProxy, result.Error)
|
||||
}
|
||||
|
||||
return &v1.ChannelTestRes{
|
||||
Success: result.Success,
|
||||
Latency: latencyMs,
|
||||
|
||||
Reference in new issue
Block a user