fix(gateway): 修复 Qwen3.8 推理参数并增加上游自愈

统一 Qwen3.8 固定思考、推理档位、预算互斥与温度下限规则,并覆盖 Chat Completions 和原生 Responses 请求。

新增安全参数纠正、有限重试和并发安全的进程内 LRU 学习;补充共享行为向量、httptest 回归与真实模型验收用例。

验证:go test ./... -count=1 通过,gofmt 检查通过;真实 Qwen3.8 流式、非流式及缓存重学习验收通过。
This commit is contained in:
2026-08-01 22:50:45 +08:00
parent 50a13de70b
commit 9c093974a1
9 changed files with 1547 additions and 33 deletions
+53 -18
View File
@@ -18,7 +18,8 @@ import (
)
type OpenAIClient struct {
HTTPClient *http.Client
HTTPClient *http.Client
Corrections *ParameterCorrectionCache
}
func (c OpenAIClient) Run(ctx context.Context, request Request) (Response, error) {
@@ -56,6 +57,7 @@ func (c OpenAIClient) Run(ctx context.Context, request Request) (Response, error
applyOpenAIChatReasoningParams(body, request.Candidate)
body = FilterOpenAIChatRequestBody(body)
} else if request.Kind == "responses" {
applyOpenAIResponsesReasoningParams(body, request.Candidate)
body = FilterOpenAIResponsesRequestBody(body)
if _, hasInput := body["input"]; !hasInput {
if messages, hasMessages := request.Body["messages"]; hasMessages {
@@ -73,31 +75,64 @@ func (c OpenAIClient) Run(ctx context.Context, request Request) (Response, error
normalizeOpenAIImageRequestBody(endpointKind, body, request.OriginalBody)
stream := openAIEndpointSupportsStream(endpointKind) && (request.Stream || boolValue(body, "stream"))
ensureOpenAIStreamUsage(body, endpointKind, stream)
raw, contentType, err := openAIRequestPayload(endpointKind, body, request.Candidate)
if err != nil {
return Response{}, err
}
upstreamEndpoint := joinURL(openAIBaseURL(endpointKind, request.Candidate), endpoint)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, upstreamEndpoint, bytes.NewReader(raw))
if err != nil {
return Response{}, err
}
req.Header.Set("Content-Type", contentType)
req.Header.Set("Authorization", "Bearer "+apiKey)
responseStartedAt := time.Now()
if err := notifySubmissionStarted(request); err != nil {
return Response{}, err
correctionScope := newParameterCorrectionScope(request, endpointKind)
correctionEnabled := endpointKind == "chat.completions" || endpointKind == "responses"
if correctionEnabled {
c.Corrections.apply(correctionScope, body)
}
resp, err := httpClient(request.HTTPClient, c.HTTPClient).Do(req)
if err != nil {
return Response{}, &ClientError{Code: "network", Message: err.Error(), Retryable: true}
provisionalRules := make([]parameterCorrectionRule, 0, 2)
seenCorrectionErrors := make(map[string]struct{})
var resp *http.Response
for correctionAttempt := 0; ; correctionAttempt++ {
raw, contentType, payloadErr := openAIRequestPayload(endpointKind, body, request.Candidate)
if payloadErr != nil {
return Response{}, payloadErr
}
req, requestErr := http.NewRequestWithContext(ctx, http.MethodPost, upstreamEndpoint, bytes.NewReader(raw))
if requestErr != nil {
return Response{}, requestErr
}
req.Header.Set("Content-Type", contentType)
req.Header.Set("Authorization", "Bearer "+apiKey)
if err := notifySubmissionStarted(request); err != nil {
return Response{}, err
}
resp, requestErr = httpClient(request.HTTPClient, c.HTTPClient).Do(req)
if requestErr != nil {
return Response{}, &ClientError{Code: "network", Message: requestErr.Error(), Retryable: true}
}
if err := notifyResponseReceived(request); err != nil {
_ = resp.Body.Close()
return Response{}, err
}
if !correctionEnabled || (resp.StatusCode != http.StatusBadRequest && resp.StatusCode != http.StatusUnprocessableEntity) {
break
}
_, _, upstreamErr := decodeHTTPResponseForProtocol(resp, openAIWireProtocol(endpointKind))
if upstreamErr == nil {
break
}
fingerprint := fmt.Sprintf("%d:%s", resp.StatusCode, strings.ToLower(upstreamErr.Error()))
if _, repeated := seenCorrectionErrors[fingerprint]; repeated || correctionAttempt >= 2 {
return Response{}, annotateResponseError(upstreamErr, requestIDFromParameterError(upstreamErr), responseStartedAt, time.Now())
}
seenCorrectionErrors[fingerprint] = struct{}{}
rule, safe := deriveParameterCorrection(upstreamErr, body, request.Candidate)
if !safe || !applyParameterCorrectionRule(body, rule) {
return Response{}, annotateResponseError(upstreamErr, requestIDFromParameterError(upstreamErr), responseStartedAt, time.Now())
}
c.Corrections.invalidate(correctionScope, rule.Param)
provisionalRules = append(provisionalRules, rule)
}
if err := notifyResponseReceived(request); err != nil {
return Response{}, err
if len(provisionalRules) > 0 && resp.StatusCode >= 200 && resp.StatusCode < 300 {
c.Corrections.commit(correctionScope, provisionalRules)
}
requestID := requestIDFromHTTPResponse(resp)
var result map[string]any
var wire *WireResponse
var err error
upstreamResponseID := ""
nativeStreamDelta := openAIWireStreamDelta(request.StreamDelta, openAIWireProtocol(endpointKind), resp)
if request.Kind == "responses" && protocol == ProtocolOpenAIResponses && stream {