fix(ai): replay Claude assistant turns verbatim so thinking blocks keep their signature

Claude Sonnet 5.5 는 응답에 thinking 블록을 포함한다(adaptive thinking, 비활성화 불가).
어댑터가 어시스턴트 턴을 자체 구조체로 재직렬화하면서 thinking 본문과 signature 를
떨어뜨려 두 번째 턴에서 400(thinking.thinking: Field required)이 났다(2026-10-15 C-G1
실측). 응답 content 를 json.RawMessage 로 보관해 그대로 재전송한다. 테스트에 thinking
블록 재전송 검증을 추가.
This commit is contained in:
seon committed 2026-10-15 22:12:16 +09:00
1 parent 684e70ba60
commit f671e2419f
2 files changed
+16 -8

No files matched your search

+11 -7
View File
@@ -135,11 +135,11 @@ type request struct {
}
type response struct {
ID string `json:"id"`
Type string `json:"type"`
Model string `json:"model"`
StopReason string `json:"stop_reason"`
Content []block `json:"content"`
ID string `json:"id"`
Type string `json:"type"`
Model string `json:"model"`
StopReason string `json:"stop_reason"`
Content json.RawMessage `json:"content"` // kept verbatim: thinking blocks (with their signature) must be replayed unchanged
Usage struct {
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
@@ -275,8 +275,10 @@ func (c *Client) CreateResponse(ctx context.Context, req openai.Request) (*opena
out.ID = fmt.Sprintf("msg_local_%d", c.seq)
c.mu.Unlock()
}
var blocks []block
_ = json.Unmarshal(r.Content, &blocks)
var text strings.Builder
for _, bl := range r.Content {
for _, bl := range blocks {
switch bl.Type {
case "text":
text.WriteString(bl.Text)
@@ -296,7 +298,9 @@ func (c *Client) CreateResponse(ctx context.Context, req openai.Request) (*opena
out.Usage.OutputTokens = r.Usage.OutputTokens
out.Usage.TotalTokens = out.Usage.InputTokens + out.Usage.OutputTokens
// remember the conversation for the next turn (assistant content kept verbatim so tool_use ids line up)
// remember the conversation for the next turn: the assistant content is replayed as the raw JSON the API returned,
// so tool_use ids and thinking blocks (adaptive thinking is always on for Claude 5.5; a thinking block replayed
// without its text/signature is rejected with "thinking.thinking: Field required") stay intact
full := append(append([]message{}, msgs...), message{Role: "assistant", Content: r.Content})
c.mu.Lock()
c.conv[out.ID] = conversation{system: body.System, msgs: full}