Refactor server: extract shared helpers, fix tool-call ordering and finish_reason mapping

- Extract callUpstream helper to deduplicate ~30 lines between chatCompletions and responses
- Move HTML templates to internal/server/templates/ via go:embed (server.go 1749→1192 lines)
- Consolidate 4 copies of firstNonEmpty into util.FirstNonEmpty
- Extract chatStreamChunk named type shared by aggregateStream and aggregateResponsesStream
- Fix tool-call ordering: iterate sorted map keys instead of sequential 0..N
- Map upstream finish_reason to Responses API status (length/content_filter → incomplete)
- Surface /v1/models errors as 401/502 instead of silently returning empty 200
- Add Secure cookie flag via isTLSRequest helper
- forEachSSEChunk: use sseDataPayload parser, drop redundant json.Valid, distinguish bufio.ErrTooLong
- Fix StreamIdleTimout typo → StreamIdleTimeout
- sso.go: single Read → io.ReadAll(io.LimitReader), explicit unknown error fallback
This commit is contained in:
2026-08-20 15:46:47 +08:00
parent 2517b4f730
commit 9aba511abe
10 changed files with 824 additions and 765 deletions
+33 -61
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"io"
"net/http"
"sort"
"time"
"git.misaka.ren/M1saka/zhanlu_proxy/internal/openai"
@@ -46,42 +47,11 @@ func (s *Server) responses(w http.ResponseWriter, r *http.Request) {
return
}
if !creds.HasAPIKey() {
creds, err = s.provisionCredentials(r.Context(), creds)
if err != nil {
writeOpenAIError(w, http.StatusBadGateway, "zhanlu api key provisioning failed: "+err.Error(), "auth_error", "zhanlu_provision_failed")
return
}
s.cfg.Credentials = creds
_ = s.st.SaveCredentials(creds)
}
modelBaseURL := firstNonEmpty(creds.ModelBaseURL, s.cfg.MobileModelBaseURL)
client, err := s.zhanluClientWithBase(modelBaseURL)
if err != nil {
writeOpenAIError(w, http.StatusInternalServerError, err.Error(), "sign_error", "signer_init_failed")
return
}
resp, err := client.ChatCompletions(r.Context(), creds.APIKey, upstreamBody)
if err != nil {
msg := "zhanlu upstream request failed"
if s.cfg.Debug {
msg = redactSensitive(err.Error())
}
writeOpenAIError(w, http.StatusBadGateway, msg, "upstream_error", "zhanlu_request_failed")
s.record(chatReq.Model, clientWantsStream, nil, "upstream_error", start)
resp, ok := s.callUpstream(w, r, creds, chatReq.Model, upstreamBody, clientWantsStream, start)
if !ok {
return
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
b, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
msg := fmt.Sprintf("zhanlu upstream returned %d", resp.StatusCode)
if s.cfg.Debug && len(b) > 0 {
msg += ": " + string(b)
}
writeOpenAIError(w, http.StatusBadGateway, msg, "upstream_error", "zhanlu_bad_status")
s.record(chatReq.Model, clientWantsStream, nil, "upstream_error", start)
return
}
if clientWantsStream {
usage, status := s.proxyResponsesStream(w, resp, chatReq.Model, meta)
@@ -110,27 +80,7 @@ func (s *Server) aggregateResponsesStream(w http.ResponseWriter, resp *http.Resp
}
toolCalls := map[int]*toolCall{}
err := forEachSSEChunk(resp.Body, func(chunk []byte) error {
var event struct {
ID string `json:"id"`
Choices []struct {
Delta struct {
Content string `json:"content"`
ReasoningContent string `json:"reasoning_content"`
Reasoning string `json:"reasoning"`
ToolCalls []struct {
Index int `json:"index"`
ID string `json:"id"`
Type string `json:"type"`
Function struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
} `json:"function"`
} `json:"tool_calls"`
} `json:"delta"`
FinishReason *string `json:"finish_reason"`
} `json:"choices"`
Usage any `json:"usage"`
}
var event chatStreamChunk
if err := json.Unmarshal(chunk, &event); err != nil {
return err
}
@@ -170,7 +120,14 @@ func (s *Server) aggregateResponsesStream(w http.ResponseWriter, resp *http.Resp
}
_ = id
_ = finishReason
// Map the upstream finish_reason onto the Responses API status: a
// length/content_filter cutoff means the response is incomplete, not
// completed. Default ("stop"/"tool_calls") stays "completed".
status := "completed"
switch finishReason {
case "length", "content_filter":
status = "incomplete"
}
respID := "resp_" + randomRequestID()
output := []any{}
@@ -195,11 +152,14 @@ func (s *Server) aggregateResponsesStream(w http.ResponseWriter, resp *http.Resp
}
if len(toolCalls) > 0 {
indices := make([]int, 0, len(toolCalls))
for i := range toolCalls {
indices = append(indices, i)
}
sort.Ints(indices)
ordered := make([]*toolCall, 0, len(toolCalls))
for i := 0; i < len(toolCalls); i++ {
if call := toolCalls[i]; call != nil {
ordered = append(ordered, call)
}
for _, i := range indices {
ordered = append(ordered, toolCalls[i])
}
for _, tc := range ordered {
output = append(output, map[string]any{
@@ -221,7 +181,7 @@ func (s *Server) aggregateResponsesStream(w http.ResponseWriter, resp *http.Resp
result := map[string]any{
"id": respID, "object": "response", "created_at": time.Now().Unix(),
"model": model, "status": "completed", "output": output,
"model": model, "status": status, "output": output,
"parallel_tool_calls": meta.ParallelToolCalls,
"tool_choice": meta.ToolChoice,
"tools": meta.Tools,
@@ -254,6 +214,7 @@ type responsesStreamState struct {
fullReasoning string
usage any
status string
finishReason string
// tool call tracking
tools map[int]*streamToolCall
@@ -504,9 +465,16 @@ func (st *responsesStreamState) finish(model string) {
})
}
// Map the upstream finish_reason onto the Responses API status, mirroring
// the non-streaming path: a length/content_filter cutoff is "incomplete".
respStatus := "completed"
switch st.finishReason {
case "length", "content_filter":
respStatus = "incomplete"
}
completedResp := map[string]any{
"id": st.respID, "object": "response", "created_at": time.Now().Unix(),
"model": model, "status": "completed", "output": finalOutput,
"model": model, "status": respStatus, "output": finalOutput,
"parallel_tool_calls": st.meta.ParallelToolCalls,
"tool_choice": st.meta.ToolChoice,
"tools": st.meta.Tools,
@@ -546,6 +514,7 @@ func (s *Server) proxyResponsesStream(w http.ResponseWriter, resp *http.Response
} `json:"function"`
} `json:"tool_calls"`
} `json:"delta"`
FinishReason *string `json:"finish_reason"`
} `json:"choices"`
}
if jErr := json.Unmarshal([]byte(payload), &evt); jErr == nil {
@@ -557,6 +526,9 @@ func (s *Server) proxyResponsesStream(w http.ResponseWriter, resp *http.Response
}
if len(evt.Choices) > 0 {
delta := evt.Choices[0].Delta
if evt.Choices[0].FinishReason != nil {
st.finishReason = *evt.Choices[0].FinishReason
}
reasoningDelta := delta.ReasoningContent + delta.Reasoning
if reasoningDelta != "" {
st.handleReasoning(reasoningDelta)