feat(api): 统一官方兼容接口响应协议

兼容接口现在以入口协议作为最终响应协议,同协议保留官方 Wire 响应,跨协议统一转换成功、任务状态与错误结构。

同时修正异步提交状态边界,持久化兼容公开任务标识和官方提交响应,并新增迁移、流式响应及协议契约测试。

验证:go vet ./...;go test ./...;govulncheck ./...;pnpm lint;pnpm test;pnpm build;pnpm audit --audit-level high;pnpm openapi;全部 CI 脚本。
This commit is contained in:
2026-07-22 15:34:59 +08:00
parent 42e8b517fd
commit e07a997aa9
32 changed files with 2436 additions and 591 deletions
+75 -3
View File
@@ -1,6 +1,7 @@
package clients
import (
"bufio"
"bytes"
"context"
"encoding/json"
@@ -24,18 +25,35 @@ func (c GeminiClient) Run(ctx context.Context, request Request) (Response, error
}
body := geminiBody(request)
raw, _ := json.Marshal(body)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, geminiURL(request.Candidate.BaseURL, upstreamModelName(request.Candidate), apiKey), bytes.NewReader(raw))
endpoint := geminiURL(request.Candidate.BaseURL, upstreamModelName(request.Candidate), apiKey)
if request.Stream {
endpoint = geminiStreamURL(request.Candidate.BaseURL, upstreamModelName(request.Candidate), apiKey)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(raw))
if err != nil {
return Response{}, err
}
req.Header.Set("Content-Type", "application/json")
responseStartedAt := time.Now()
if err := notifySubmissionStarted(request); err != nil {
return Response{}, err
}
resp, err := httpClient(request.HTTPClient, c.HTTPClient).Do(req)
if err != nil {
return Response{}, &ClientError{Code: "network", Message: err.Error(), Retryable: true}
}
if err := notifyResponseReceived(request); err != nil {
return Response{}, err
}
requestID := requestIDFromHTTPResponse(resp)
result, err := decodeHTTPResponse(resp)
var result map[string]any
var wire *WireResponse
if request.Stream && resp.StatusCode >= 200 && resp.StatusCode < 300 {
wire = &WireResponse{Protocol: ProtocolGeminiGenerateContent, StatusCode: resp.StatusCode, Headers: compatibleResponseHeaders(resp.Header)}
result, err = decodeGeminiStreamResponse(resp, request.StreamDelta, wire)
} else {
result, wire, err = decodeHTTPResponseForProtocol(resp, ProtocolGeminiGenerateContent)
}
responseFinishedAt := time.Now()
if err != nil {
return Response{}, annotateResponseError(err, requestID, responseStartedAt, responseFinishedAt)
@@ -52,10 +70,20 @@ func (c GeminiClient) Run(ctx context.Context, request Request) (Response, error
ResponseStartedAt: responseStartedAt,
ResponseFinishedAt: responseFinishedAt,
ResponseDurationMS: responseDurationMS(responseStartedAt, responseFinishedAt),
UpstreamProtocol: ProtocolGeminiGenerateContent,
Wire: wire,
}, nil
}
func geminiURL(baseURL string, model string, apiKey string) string {
return geminiActionURL(baseURL, model, "generateContent", apiKey, false)
}
func geminiStreamURL(baseURL string, model string, apiKey string) string {
return geminiActionURL(baseURL, model, "streamGenerateContent", apiKey, true)
}
func geminiActionURL(baseURL string, model string, action string, apiKey string, stream bool) string {
base := strings.TrimRight(strings.TrimSpace(baseURL), "/")
if base == "" {
base = "https://generativelanguage.googleapis.com"
@@ -67,7 +95,51 @@ func geminiURL(baseURL string, model string, apiKey string) string {
base += "/v1beta"
}
escapedModel := url.PathEscape(model)
return fmt.Sprintf("%s/models/%s:generateContent?key=%s", base, escapedModel, url.QueryEscape(apiKey))
endpoint := fmt.Sprintf("%s/models/%s:%s?key=%s", base, escapedModel, action, url.QueryEscape(apiKey))
if stream {
endpoint += "&alt=sse"
}
return endpoint
}
func decodeGeminiStreamResponse(resp *http.Response, onDelta StreamDelta, wire *WireResponse) (map[string]any, error) {
defer resp.Body.Close()
scanner := bufio.NewScanner(resp.Body)
scanner.Buffer(make([]byte, 64*1024), 16*1024*1024)
result := map[string]any{}
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if !strings.HasPrefix(line, "data:") {
continue
}
data := strings.TrimSpace(strings.TrimPrefix(line, "data:"))
if data == "" || data == "[DONE]" {
continue
}
var event map[string]any
if err := json.Unmarshal([]byte(data), &event); err != nil {
return nil, &ClientError{Code: "invalid_response", Message: err.Error(), StatusCode: resp.StatusCode, Retryable: false, Wire: wire}
}
result = event
if onDelta != nil {
if err := onDelta(StreamDeltaEvent{
Text: geminiText(event),
Event: event,
WireProtocol: ProtocolGeminiGenerateContent,
WireStatusCode: resp.StatusCode,
WireHeaders: compatibleResponseHeaders(resp.Header),
}); err != nil {
return nil, err
}
}
}
if err := scanner.Err(); err != nil {
return nil, &ClientError{Code: "network", Message: err.Error(), Retryable: true, Wire: wire}
}
if len(result) == 0 {
return nil, &ClientError{Code: "invalid_response", Message: "gemini stream returned no events", StatusCode: resp.StatusCode, Retryable: false, Wire: wire}
}
return result, nil
}
func geminiBody(request Request) map[string]any {