From f14b27dc3e59cd440ce06b1649f6f857fc0d395b Mon Sep 17 00:00:00 2001 From: kapelame Date: Tue, 4 Aug 2026 18:34:34 +0800 Subject: [PATCH] fix: parse OpenCode text response events --- .../internal/agent/opencode/parser.go | 42 +++++++++++++++++ .../internal/agent/opencode/parser_test.go | 46 +++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/apps/parsar-daemon/internal/agent/opencode/parser.go b/apps/parsar-daemon/internal/agent/opencode/parser.go index ce25b247..4db599b6 100644 --- a/apps/parsar-daemon/internal/agent/opencode/parser.go +++ b/apps/parsar-daemon/internal/agent/opencode/parser.go @@ -36,6 +36,7 @@ func (t *translator) Translate(line []byte) (translation, error) { var head struct { Type string `json:"type"` Properties json.RawMessage `json:"properties"` + Part json.RawMessage `json:"part"` } if err := json.Unmarshal(line, &head); err != nil || head.Type == "" { if t.plainBuf.Len() > 0 { @@ -48,9 +49,14 @@ func (t *translator) Translate(line []byte) (translation, error) { switch head.Type { case "message.part.delta": return t.translatePartDelta(head.Properties) + case "text": + return t.translateTextPart(head.Part) case "message.updated", "message.updated.1": t.captureUsage(head.Properties) return translation{}, nil + case "step_finish": + t.capturePartUsage(head.Part) + return translation{}, nil default: t.captureGenericUsage(line) return translation{}, nil @@ -76,6 +82,25 @@ func (t *translator) translatePartDelta(raw json.RawMessage) (translation, error return translation{Envelopes: []proto.Envelope{env}}, nil } +func (t *translator) translateTextPart(raw json.RawMessage) (translation, error) { + var p struct { + Type string `json:"type"` + Text string `json:"text"` + } + if err := json.Unmarshal(raw, &p); err != nil { + return translation{}, fmt.Errorf("opencode: parse text part: %w", err) + } + if p.Text == "" || (p.Type != "" && p.Type != "text") { + return translation{}, nil + } + t.deltaBuf.WriteString(p.Text) + env, err := proto.NewEnvelope(proto.TypeDelta, t.runID, proto.DeltaPayload{Delta: p.Text, Sequence: t.seq.Add(1)}) + if err != nil { + return translation{}, err + } + return translation{Envelopes: []proto.Envelope{env}}, nil +} + func (t *translator) captureUsage(raw json.RawMessage) { var p struct { Info usageInfo `json:"info"` @@ -85,6 +110,13 @@ func (t *translator) captureUsage(raw json.RawMessage) { } } +func (t *translator) capturePartUsage(raw json.RawMessage) { + var p usageInfo + if err := json.Unmarshal(raw, &p); err == nil { + t.mergeUsage(p) + } +} + func (t *translator) captureGenericUsage(raw json.RawMessage) { var p struct { Info usageInfo `json:"info"` @@ -112,9 +144,19 @@ type usageTokens struct { CacheRead int32 `json:"cacheRead"` CacheWrite int32 `json:"cacheWrite"` Total int32 `json:"total"` + Cache struct { + Read int32 `json:"read"` + Write int32 `json:"write"` + } `json:"cache"` } func (t *translator) mergeUsage(info usageInfo) { + if info.Tokens.CacheRead == 0 { + info.Tokens.CacheRead = info.Tokens.Cache.Read + } + if info.Tokens.CacheWrite == 0 { + info.Tokens.CacheWrite = info.Tokens.Cache.Write + } if info.Tokens.Input != 0 { t.usage.InputTokens = info.Tokens.Input } diff --git a/apps/parsar-daemon/internal/agent/opencode/parser_test.go b/apps/parsar-daemon/internal/agent/opencode/parser_test.go index 14f85751..1128bba2 100644 --- a/apps/parsar-daemon/internal/agent/opencode/parser_test.go +++ b/apps/parsar-daemon/internal/agent/opencode/parser_test.go @@ -33,6 +33,27 @@ func TestTranslatePartDeltaEmitsDeltaAndDone(t *testing.T) { } } +func TestTranslateTextEventEmitsDeltaAndDone(t *testing.T) { + tr := opencode.NewTranslatorForTest("run-text") + tx, err := tr.Translate([]byte(`{"type":"text","timestamp":1785838824775,"sessionID":"ses_1","part":{"id":"prt_1","messageID":"msg_1","sessionID":"ses_1","type":"text","text":"OK"}}`)) + if err != nil { + t.Fatalf("Translate: %v", err) + } + if len(tx.Envelopes) != 1 || tx.Envelopes[0].Type != proto.TypeDelta { + t.Fatalf("delta envelopes = %#v", tx.Envelopes) + } + delta := decodePayload[proto.DeltaPayload](t, tx.Envelopes[0]) + if delta.Delta != "OK" || delta.Sequence == 0 { + t.Fatalf("delta payload = %#v", delta) + } + + envs := tr.TerminalEnvelopes(nil, "", false) + done := decodePayload[proto.DonePayload](t, envs[len(envs)-1]) + if done.Content != "OK" { + t.Fatalf("done content = %q", done.Content) + } +} + func TestTranslateCapturesUsage(t *testing.T) { tr := opencode.NewTranslatorForTest("run-u") _, err := tr.Translate([]byte(`{"type":"message.updated","properties":{"info":{"cost":0.25,"tokens":{"input":10,"output":7,"reasoning":3,"cacheRead":2,"cacheWrite":1,"total":23}}}}`)) @@ -58,6 +79,31 @@ func TestTranslateCapturesUsage(t *testing.T) { } } +func TestTranslateStepFinishCapturesUsage(t *testing.T) { + tr := opencode.NewTranslatorForTest("run-step-finish") + _, err := tr.Translate([]byte(`{"type":"step_finish","part":{"type":"step-finish","tokens":{"total":12576,"input":11803,"output":645,"reasoning":0,"cache":{"write":4,"read":128}},"cost":0.00432258}}`)) + if err != nil { + t.Fatalf("Translate: %v", err) + } + envs := tr.TerminalEnvelopes(nil, "", false) + var got *proto.UsagePayload + for _, env := range envs { + if env.Type == proto.TypeUsage { + payload := decodePayload[proto.UsagePayload](t, env) + got = &payload + } + } + if got == nil { + t.Fatalf("usage env missing: %#v", envs) + } + if got.InputTokens != 11803 || got.OutputTokens != 645 || got.CostUSD != 0.00432258 { + t.Fatalf("usage = %#v", got) + } + if got.Raw["total_tokens"] != float64(12576) || got.Raw["cache_read_tokens"] != float64(128) || got.Raw["cache_write_tokens"] != float64(4) { + t.Fatalf("usage raw = %#v", got.Raw) + } +} + func TestPlainOutputFallsBackToDelta(t *testing.T) { tr := opencode.NewTranslatorForTest("run-p") _, _ = tr.Translate([]byte("plain output"))