gateway: a signed thinking block with no text goes to Messages with its empty thinking, not without the field (#1447, Edward4226)
The second refusal in #1447: OpenCode Go's Claude models answer a
thinking block with a signature and no text,
{"type":"thinking","signature":"abc123"}, with 400 "messages.1.content.0.
thinking.thinking: Field required". Messages requires the field, but
aBlock's Thinking was omitempty, so buildAnthropic dropped it whenever a
signed part had no text: Claude Code's own thinking when its display is
omitted ("thinking": "" with a real signature), and Gemini's signed
parts, translated to an Anthropic upstream.
aBlock now always writes "thinking" on a thinking block (MarshalJSON);
every other block keeps leaving its empty fields out. Only aBlock had
this: the Anthropic reply encoders already write the field as a map.
A separate change from the prefill fix (af1e477d): a different field
and a different refusal.
TestSignedThinkingWithNoTextKeepsTheField fails on the old code ("thinking
block = map[signature:abc123 type:thinking]") and passes now.
Run: gofmt -l (clean), go vet -tags nogui ./internal/gateway on darwin,
GOOS=linux and GOOS=windows, go test -tags nogui ./internal/gateway
./internal/provider ./internal/plugin under a scratch HOME (all ok).
Not run for real: the owner's OpenCode Go subscription is inactive (403).
Which request produced the reporter's messages.25 error from Codex isn't
known: Responses input carries no signatures, so it wasn't this path for
a Codex client's own turns.
This commit is contained in:
1 parent
916c1cc7ea
commit
12c0550ae7
2 files changed
+54
No files matched your search
@@ -44,6 +44,22 @@ type aBlock struct {
|
||||
CacheControl map[string]string `json:"cache_control,omitempty"`
|
||||
}
|
||||
|
||||
// MarshalJSON writes a thinking block's text even when it is empty.
|
||||
// Messages requires the field: a signed block with no text (Claude Code's
|
||||
// thinking when its display is omitted) sent without it is refused,
|
||||
// "messages.N.content.0.thinking.thinking: Field required" (#1447). Every
|
||||
// other block keeps its omitempty fields.
|
||||
func (b aBlock) MarshalJSON() ([]byte, error) {
|
||||
type plain aBlock
|
||||
if b.Type != "thinking" {
|
||||
return json.Marshal(plain(b))
|
||||
}
|
||||
return json.Marshal(struct {
|
||||
plain
|
||||
Thinking string `json:"thinking"`
|
||||
}{plain(b), b.Thinking})
|
||||
}
|
||||
|
||||
// ephemeral marks a prompt-cache breakpoint.
|
||||
var ephemeral = map[string]string{"type": "ephemeral"}
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A signed thinking block with no text goes with "thinking": "", the
|
||||
// field Messages requires. Without it OpenCode Go's Claude models answer
|
||||
// "messages.1.content.0.thinking.thinking: Field required" (#1447).
|
||||
func TestSignedThinkingWithNoTextKeepsTheField(t *testing.T) {
|
||||
r := &Request{Messages: []Message{
|
||||
{Role: "user", Parts: []Part{{Kind: Text, Text: "hi"}}},
|
||||
{Role: "assistant", Parts: []Part{{Kind: Thinking, Signature: "abc123"}, {Kind: Text, Text: "hello"}}},
|
||||
{Role: "user", Parts: []Part{{Kind: Text, Text: "go on"}}},
|
||||
}}
|
||||
var out struct {
|
||||
Messages []struct {
|
||||
Content []map[string]any `json:"content"`
|
||||
} `json:"messages"`
|
||||
}
|
||||
body := buildAnthropic(r, "claude-haiku-5-5")
|
||||
if err := json.Unmarshal(body, &out); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
think := out.Messages[1].Content[0]
|
||||
if v, ok := think["thinking"]; !ok || v != "" || think["signature"] != "abc123" {
|
||||
t.Fatalf("thinking block = %v: a signed block with no text must carry \"thinking\": \"\"", think)
|
||||
}
|
||||
// Other blocks keep leaving their empty fields out.
|
||||
if text := out.Messages[1].Content[1]; len(text) != 2 || text["text"] != "hello" {
|
||||
t.Fatalf("text block = %v", text)
|
||||
}
|
||||
if strings.Count(string(body), `"thinking":`) != 1 {
|
||||
t.Fatalf("only the thinking block names thinking: %s", body)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user