From 8b8a9e5a7aed647eef77df0a50d47b2df94ebe1c Mon Sep 17 00:00:00 2001 From: yetone Date: Mon, 28 Sep 2026 00:39:22 +0800 Subject: [PATCH] =?UTF-8?q?gateway:=20a=20passed-through=20request's=20rea?= =?UTF-8?q?soning=20effort=20is=20fitted=20to=20the=20model's=20levels,=20?= =?UTF-8?q?and=20one=20asking=20"none"=20that=20the=20upstream=20refuses?= =?UTF-8?q?=20with=20the=20levels=20it=20takes=20is=20asked=20again=20at?= =?UTF-8?q?=20low=20=E2=80=94=20Qoder's=20permission=20check=20asks=20"non?= =?UTF-8?q?e",=20which=20Command=20Code=20turned=20away?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/gateway/gateway.go | 62 ++++++++++++++++++++++++++++++++ internal/gateway/gateway_test.go | 23 ++++++++++++ 2 files changed, 85 insertions(+) diff --git a/internal/gateway/gateway.go b/internal/gateway/gateway.go index 3efa2756..d95266e0 100644 --- a/internal/gateway/gateway.go +++ b/internal/gateway/gateway.go @@ -932,10 +932,28 @@ func (s *Server) passthrough(w http.ResponseWriter, r *http.Request, p provider. case provider.Anthropic: body = thinkingOffUnlessAsked(body) } + // the effort as the agent sent it, fitted to the model's levels: Qoder's + // permission check asks "none", which Command Code turns away + asked := bodyEffort(proto, body) + if e := fitEffort(asked, p.Efforts(model)); asked != "" && e != asked { + body = withBodyEffort(proto, body, e) + } res, err := s.forward(r.Context(), p, proto, pathOf(proto), p.Prepare(body), r.Header) if err != nil { return writeError(w, proto, 502, p.Name+": "+err.Error()), err.Error(), true } + if e := bodyEffort(proto, body); (e == "none" || e == "minimal") && res.StatusCode == http.StatusBadRequest { + // a model whose levels magpie doesn't know, refusing no reasoning + // with the levels it takes: asked again at the lowest of them + b, _ := io.ReadAll(io.LimitReader(res.Body, 1<<20)) + res.Body.Close() + res.Body = io.NopCloser(bytes.NewReader(b)) + if effortLevelsNamed.Match(b) { + if res, err = s.forward(r.Context(), p, proto, pathOf(proto), p.Prepare(withBodyEffort(proto, body, "low")), r.Header); err != nil { + return writeError(w, proto, 502, p.Name+": "+err.Error()), err.Error(), true + } + } + } if proto == provider.Anthropic && res.StatusCode == http.StatusBadRequest { // a model that always thinks refuses thinking turned off: asked // again with it left to the model @@ -1468,6 +1486,50 @@ func conversationID(in http.Header, body []byte) string { return "magpie-" + hex.EncodeToString(sum[:12]) } +// effortLevelsNamed is an error that lists the reasoning levels a model +// takes, as one refusing "none" does: Command Code's `expected one of +// "low"|"medium"|"high"|"xhigh"|"max"`. +var effortLevelsNamed = regexp.MustCompile(`(?i)\blow\b\W+(?:medium|high)\b`) + +// bodyEffort is the reasoning effort a Chat or Responses request asks for. +func bodyEffort(proto provider.Protocol, body []byte) string { + var v struct { + ReasoningEffort string `json:"reasoning_effort"` + Reasoning *struct { + Effort string `json:"effort"` + } `json:"reasoning"` + } + if json.Unmarshal(body, &v) != nil { + return "" + } + switch { + case proto == provider.Chat: + return v.ReasoningEffort + case proto == provider.Responses && v.Reasoning != nil: + return v.Reasoning.Effort + } + return "" +} + +// withBodyEffort asks a Chat or Responses request for effort instead, +// keeping the rest of its reasoning settings. +func withBodyEffort(proto provider.Protocol, body []byte, effort string) []byte { + switch proto { + case provider.Chat: + return withFields(body, map[string]any{"reasoning_effort": effort}) + case provider.Responses: + var v struct { + Reasoning map[string]any `json:"reasoning"` + } + if json.Unmarshal(body, &v) != nil || v.Reasoning == nil { + return body + } + v.Reasoning["effort"] = effort + return withFields(body, map[string]any{"reasoning": v.Reasoning}) + } + return body +} + // withFields sets top-level fields, keeping every other field as it was. func withFields(body []byte, fields map[string]any) []byte { dec := json.NewDecoder(bytes.NewReader(body)) diff --git a/internal/gateway/gateway_test.go b/internal/gateway/gateway_test.go index f463692c..85bfe655 100644 --- a/internal/gateway/gateway_test.go +++ b/internal/gateway/gateway_test.go @@ -561,6 +561,29 @@ func TestAnthropicPassthroughModelThatAlwaysThinks(t *testing.T) { } } +// Qoder asks "none" for its permission checks, which Command Code turns +// away with the levels it takes: asked again at the lowest, once +func TestPassthroughEffortNoneRefused(t *testing.T) { + f := &fake{t: t, ctype: "application/json", reply: `{"id":"c1","choices":[]}`} + f.refuse = func(b []byte) (int, string) { + if bytes.Contains(b, []byte(`"reasoning_effort":"none"`)) { + return 400, `{"error":{"message":"Command Code: Invalid option: expected one of \"low\"|\"medium\"|\"high\"|\"xhigh\"|\"max\""}}` + } + return 0, "" + } + setup(t, provider.Chat, f) + code, body := post(t, "/v1/chat/completions", `{"model":"m1","messages":[{"role":"user","content":"ok?"}],"reasoning_effort":"none"}`) + if code != 200 || f.calls != 2 || !bytes.Contains(f.got, []byte(`"reasoning_effort":"low"`)) || !bytes.Contains(f.got, []byte(`"ok?"`)) { + t.Fatalf("%d %s after %d calls, last sent %s", code, body, f.calls, f.got) + } + // another 400 is the agent's to see + f.calls = 0 + f.refuse = func([]byte) (int, string) { return 400, `{"error":{"message":"bad request"}}` } + if code, _ := post(t, "/v1/chat/completions", `{"model":"m1","messages":[],"reasoning_effort":"none"}`); code != 400 || f.calls != 1 { + t.Errorf("%d after %d calls", code, f.calls) + } +} + // effort without thinking asks for no reasoning on a Chat upstream func TestAnthropicEffortWithoutThinking(t *testing.T) { f := &fake{t: t, reply: sse(`data: {"id":"c1","choices":[{"delta":{"content":"T"},"finish_reason":"stop"}]}`, `data: [DONE]`)}