gateway: a passed-through request's reasoning effort is fitted to the model's levels, and one asking "none" that the upstream refuses with the levels it takes is asked again at low — Qoder's permission check asks "none", which Command Code turned away
This commit is contained in:
2 files changed
+85
No files matched your search
@@ -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))
|
||||
|
||||
@@ -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]`)}
|
||||
|
||||
Reference in new issue
Block a user