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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user