pi: a model that always thinks has off hidden on the Messages API, and its Chinese refusal of thinking off is asked again (#699, liuweifeng: 走 Anthropic Messages API 的 always-thinks 模型被删掉 thinkingLevelMap 的 off,Pi 仍提供 off 导致上游 400) — piThinkingLevels left off out of thinkingLevelMap for every model on anthropic-messages, which is right for Claude (Pi offers a level left out, and Claude takes thinking disabled whatever its levels) but had Pi offer off for ZCode's GLM-5.3-Flash too, whose levels are low/high/max and which answers thinking disabled with 400 "该模型始终支持思考,不可关闭". Now off is left out only for a model that thinks only when asked: gateway.ThinksOnlyWhenAsked, the gateway's own anthropicModel (the test that decides it leaves a Claude's request as sent and turns another vendor's thinking off unless asked), plus a relay's opus-5.5 spelling. Another vendor's model there gets "off": null unless none is among its levels (GLM-5.2's none still maps off to it), as on Chat and Responses. The gateway's alwaysThinks, which asks a refused thinking-off request again with thinking left to the model, didn't know the Chinese wording, so the 400 reached Pi (and any agent whose request the gateway turned off); it now matches 始终…思考 / 思考…不可关闭. OpenCode's variants, dsh's levels and omp's efforts name only the model's own levels, off only for none, so they don't offer a level the model refuses; omp can't hide its off, and the gateway's retry now covers it. TestPiOffOnMessagesAPI (GLM-5.3-Flash off null and not offered; GLM-5.2 off=none; Claude and opus-5.5 off left out) and TestAnthropicPassthroughAlwaysThinksInChinese (asked again once without thinking) fail on the old code and pass. Pi's models.json generated in a temp HOME with PI_CODING_AGENT_DIR in it: zc/GLM-5.3-Flash has "off": null, low/high/max; claude-sonnet-5 has no off key. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestDevinSignedInOnlyInMagpie, a timing test that passes alone. Not tried with a real ZCode account.
This commit is contained in:
1 parent
89d305229b
commit
de290249bb
4 files changed
+80
-11
No files matched your search
@@ -311,7 +311,7 @@ func piModelJSON(m catalog.Model, gw string, native bool) map[string]any {
|
||||
if m.Images {
|
||||
e["input"] = []string{"text", "image"}
|
||||
}
|
||||
if levels := piThinkingLevels(m.Efforts, e["api"] == "anthropic-messages"); levels != nil {
|
||||
if levels := piThinkingLevels(m.Efforts, e["api"] == "anthropic-messages" && gateway.ThinksOnlyWhenAsked(m.ID)); levels != nil {
|
||||
e["thinkingLevelMap"] = levels
|
||||
}
|
||||
// without it Pi takes every model for a 128K one, and compacts
|
||||
@@ -354,11 +354,14 @@ func openCodeVariants(efforts []string) map[string]any {
|
||||
// turned away (#243). Pi's off is the model's none when it takes one. A
|
||||
// model without none has off hidden — Pi's off asks a Responses model for
|
||||
// effort none and leaves a Chat model on the vendor's default thinking —
|
||||
// except on Anthropic's Messages API, where Pi's off sends thinking
|
||||
// disabled, which Claude takes whatever its levels; there off is left out,
|
||||
// for Pi to offer as before. A model whose levels magpie doesn't know gets
|
||||
// no map: Pi's own defaults, as before.
|
||||
func piThinkingLevels(efforts []string, anthropic bool) map[string]any {
|
||||
// except a Claude on Anthropic's Messages API (claudeOff), where Pi's off
|
||||
// sends thinking disabled, which Claude takes whatever its levels; there
|
||||
// off is left out, for Pi to offer as before. Another vendor's model there
|
||||
// keeps off hidden: without none it always thinks, and ZCode's
|
||||
// GLM-5.3-Flash turned Pi's off away ("该模型始终支持思考,不可关闭", #699),
|
||||
// as a level left out is one Pi offers. A model whose levels magpie doesn't
|
||||
// know gets no map: Pi's own defaults, as before.
|
||||
func piThinkingLevels(efforts []string, claudeOff bool) map[string]any {
|
||||
if len(efforts) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -374,7 +377,7 @@ func piThinkingLevels(efforts []string, anthropic bool) map[string]any {
|
||||
levels[l] = nil
|
||||
}
|
||||
}
|
||||
if levels["off"] == nil && anthropic {
|
||||
if levels["off"] == nil && claudeOff {
|
||||
delete(levels, "off")
|
||||
}
|
||||
return levels
|
||||
|
||||
@@ -3,6 +3,8 @@ package agent
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/yetone/magpie/internal/catalog"
|
||||
)
|
||||
|
||||
// Every one of Pi's levels is in the map, the model's own mapped and the
|
||||
@@ -36,3 +38,39 @@ func TestPiThinkingLevels(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #699: ZCode's GLM-5.3-Flash, on Anthropic's Messages API, always thinks
|
||||
// (its levels low, high and max, no none) and turns thinking disabled away
|
||||
// ("该模型始终支持思考,不可关闭"); its entry left off out, which Pi offers,
|
||||
// so off is null for it. A Claude there still has off left for Pi to offer,
|
||||
// and a vendor's model that takes none maps off to it.
|
||||
func TestPiOffOnMessagesAPI(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
id string
|
||||
efforts []string
|
||||
off any // the map's off; "left out" when it has none
|
||||
offered []string
|
||||
}{
|
||||
{"zcode/GLM-5.3-Flash", []string{"low", "high", "max"}, nil, []string{"low", "high", "max"}},
|
||||
{"zcode/GLM-5.2", []string{"none", "high", "max"}, "none", []string{"off", "high", "max"}},
|
||||
{"anth/claude-sonnet-5", []string{"low", "medium", "high", "xhigh", "max"}, "left out", []string{"off", "low", "medium", "high", "xhigh", "max"}},
|
||||
{"relay/opus-5.5", []string{"low", "high", "max"}, "left out", []string{"off", "low", "high", "max"}},
|
||||
} {
|
||||
m := catalog.Model{ID: c.id, Name: c.id, Efforts: c.efforts, APIs: []string{"anthropic"}}
|
||||
e := piModelJSON(m, "http://127.0.0.1:1", true)
|
||||
if e["api"] != "anthropic-messages" {
|
||||
t.Fatalf("%s on %v", c.id, e["api"])
|
||||
}
|
||||
levels := e["thinkingLevelMap"].(map[string]any)
|
||||
off, set := levels["off"]
|
||||
if !set {
|
||||
off = "left out"
|
||||
}
|
||||
if off != c.off {
|
||||
t.Errorf("%s: off is %v, want %v", c.id, off, c.off)
|
||||
}
|
||||
if got := piSupported(e); !reflect.DeepEqual(got, c.offered) {
|
||||
t.Errorf("%s: Pi offers %v, want %v", c.id, got, c.offered)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -202,10 +202,20 @@ func thinkingOffUnlessAsked(body []byte) []byte {
|
||||
var anthropicModel = regexp.MustCompile(`(?i)(?:^|[/.:-])claude-`)
|
||||
|
||||
// alwaysThinks is a vendor refusing to turn a model's thinking off: Z.ai's
|
||||
// GLM-5.3 answers 1210, "…always engages in thinking…", and DashScope's own
|
||||
// glm-5.3 answers "The value of the enable_thinking parameter is restricted
|
||||
// to True."
|
||||
var alwaysThinks = regexp.MustCompile(`(?i)always engages in thinking|thinking (?:can ?not|can't) be (?:disabled|turned off)|enable_thinking[^"]{0,60}restricted to true`)
|
||||
// GLM-5.3 answers 1210, "…always engages in thinking…", or, in Chinese
|
||||
// (ZCode's GLM-5.3-Flash), "该模型始终支持思考,不可关闭" (#699); DashScope's
|
||||
// own glm-5.3 answers "The value of the enable_thinking parameter is
|
||||
// restricted to True."
|
||||
var alwaysThinks = regexp.MustCompile(`(?i)always engages in thinking|thinking (?:can ?not|can't) be (?:disabled|turned off)|enable_thinking[^"]{0,60}restricted to true|始终(?:支持|开启|启用)?思考|思考[^"]{0,20}(?:不可|无法|不能)关闭`)
|
||||
|
||||
// ThinksOnlyWhenAsked is a model that takes thinking turned off on the
|
||||
// Messages API whatever its levels: one of Anthropic's own (anthropicModel,
|
||||
// or a relay's opus-5.5). Another vendor's model there takes it off only
|
||||
// when none is among its levels; without it, it always thinks, and GLM-5.3
|
||||
// turns thinking disabled away (#699).
|
||||
func ThinksOnlyWhenAsked(model string) bool {
|
||||
return anthropicModel.MatchString(model) || claudeVersion.MatchString(strings.ToLower(model))
|
||||
}
|
||||
|
||||
// withoutThinkingOff is body with its thinking left to the model, when it
|
||||
// says thinking is off; false when it doesn't.
|
||||
|
||||
@@ -589,6 +589,24 @@ func TestAnthropicPassthroughDashScopeThinkingRestricted(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// #699: ZCode's GLM-5.3-Flash refuses thinking turned off in Chinese
|
||||
// ("该模型始终支持思考,不可关闭"), which Pi's off sent and the agent got as
|
||||
// a 400: asked again with thinking left to the model, once
|
||||
func TestAnthropicPassthroughAlwaysThinksInChinese(t *testing.T) {
|
||||
f := &fake{t: t, ctype: "application/json", reply: `{"id":"msg","type":"message","content":[]}`}
|
||||
f.refuse = func(b []byte) (int, string) {
|
||||
if bytes.Contains(b, []byte(`"disabled"`)) {
|
||||
return 400, `{"type":"error","error":{"type":"invalid_request_error","message":"该模型始终支持思考,不可关闭"}}`
|
||||
}
|
||||
return 0, ""
|
||||
}
|
||||
setup(t, provider.Anthropic, f)
|
||||
code, body := post(t, "/v1/messages", `{"model":"m1","max_tokens":5,"messages":[{"role":"user","content":"title?"}],"thinking":{"type":"disabled"}}`)
|
||||
if code != 200 || f.calls != 2 || bytes.Contains(f.got, []byte("thinking")) || !bytes.Contains(f.got, []byte(`"title?"`)) {
|
||||
t.Fatalf("%d %s after %d calls, last sent %s", code, body, f.calls, f.got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenRouterMandatoryReasoningRetries(t *testing.T) {
|
||||
const refusal = `{"error":{"message":"OpenRouter: Reasoning is mandatory for this endpoint and cannot be disabled.","type":"invalid_request_error"},"type":"error"}`
|
||||
for _, tc := range []struct {
|
||||
|
||||
Reference in new issue
Block a user