From a02076dcb8ee119ecdb93fa99489ee0c82d657c2 Mon Sep 17 00:00:00 2001 From: yetone Date: Thu, 1 Oct 2026 20:05:33 +0800 Subject: [PATCH] =?UTF-8?q?claude:=20a=20Claude=20account's=20usage=20and?= =?UTF-8?q?=20model=20test=20read=20Claude=20Code's=20answer=20with=20its?= =?UTF-8?q?=20verbose=20output=20on=20(KevinXC=20on=20Discord:=20=E6=96=B0?= =?UTF-8?q?=E7=89=88=E6=9C=AC=20claude=20=E9=A2=9D=E5=BA=A6=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E5=A4=B1=E8=B4=A5=E4=BA=86;=20the=20card=20said=20usa?= =?UTF-8?q?ge=20couldn't=20be=20read,=20its=20tooltip=20began=20[{"type":"?= =?UTF-8?q?system","subtype":"init")=20=E2=80=94=20with=20Claude=20Code's?= =?UTF-8?q?=20own=20"Verbose=20output"=20turned=20on=20(its=20/config,=20k?= =?UTF-8?q?ept=20in=20.claude.json,=20which=20--setting-sources=20""=20lea?= =?UTF-8?q?ves=20in=20force),=20claude=20-p=20--output-format=20json=20pri?= =?UTF-8?q?nts=20every=20message=20as=20a=20JSON=20array,=20the=20system?= =?UTF-8?q?=20init=20first=20and=20the=20result=20last,=20and=20magpie=20r?= =?UTF-8?q?ead=20only=20the=20one-object=20form:=20/usage's=20output=20was?= =?UTF-8?q?=20taken=20for=20an=20error=20and=20shown=20as=20one,=20and=20a?= =?UTF-8?q?=20model=20test's=20is=5Ferror=20was=20missed.=20The=20result?= =?UTF-8?q?=20is=20now=20the=20array's=20last=20"result"=20message=20when?= =?UTF-8?q?=20it=20is=20an=20array;=20the=20one-object=20form=20reads=20as?= =?UTF-8?q?=20before.=20Arguments,=20prompts=20and=20headers=20are=20uncha?= =?UTF-8?q?nged.=20TestClaudeVerboseOutput=20fails=20without=20the=20chang?= =?UTF-8?q?e;=20the=20real=20Claude=20Code=202.1.286=20with=20verbose=20on?= =?UTF-8?q?,=20in=20a=20sandbox=20config=20directory,=20gives=20an=20array?= =?UTF-8?q?=20that=20now=20reads=20to=20its=20/usage=20text.=20go=20vet,?= =?UTF-8?q?=20go=20build=20-tags=20nogui=20(darwin,=20GOOS=3Dlinux),=20GOO?= =?UTF-8?q?S=3Dwindows=20go=20build,=20go=20test=20-tags=20nogui=20./inter?= =?UTF-8?q?nal/gateway=20./internal/provider=20pass.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/gateway/claude_warmup.go | 41 +++++++++++++++++++------- internal/gateway/claude_warmup_test.go | 25 ++++++++++++++++ 2 files changed, 56 insertions(+), 10 deletions(-) diff --git a/internal/gateway/claude_warmup.go b/internal/gateway/claude_warmup.go index 8d86beae..e8143c6c 100644 --- a/internal/gateway/claude_warmup.go +++ b/internal/gateway/claude_warmup.go @@ -53,11 +53,7 @@ func askClaude(ctx context.Context, configDir, model string) error { var out, stderr bytes.Buffer cmd.Stdout, cmd.Stderr = &out, &stderr runErr := cmd.Run() - var res struct { - IsError bool `json:"is_error"` - Result string `json:"result"` - } - if json.Unmarshal(out.Bytes(), &res) == nil && res.IsError { + if res, ok := claudeResult(out.Bytes()); ok && res.IsError { return errors.New("Claude Code: " + clip(res.Result)) } if runErr != nil { @@ -98,11 +94,7 @@ func claudeUsage(ctx context.Context) (string, error) { var out, stderr bytes.Buffer cmd.Stdout, cmd.Stderr = &out, &stderr runErr := cmd.Run() - var res struct { - IsError bool `json:"is_error"` - Result string `json:"result"` - } - if err := json.Unmarshal(out.Bytes(), &res); err == nil { + if res, ok := claudeResult(out.Bytes()); ok { if res.IsError { return "", errors.New("Claude Code: " + clip(res.Result)) } @@ -118,6 +110,35 @@ func claudeUsage(ctx context.Context) (string, error) { return "", errors.New("Claude Code: " + clip(msg)) } +// claudeRun is the result Claude Code prints for a -p run with +// --output-format json. +type claudeRun struct { + Type string `json:"type"` + IsError bool `json:"is_error"` + Result string `json:"result"` +} + +// claudeResult reads the result from what claude -p --output-format json +// printed: one object, or, when the user turned on Claude Code's verbose +// output (its own config, which --setting-sources leaves on), an array of +// every message, the result last. +func claudeResult(b []byte) (claudeRun, bool) { + var res claudeRun + if json.Unmarshal(b, &res) == nil { + return res, true + } + var all []claudeRun + if json.Unmarshal(b, &all) != nil { + return claudeRun{}, false + } + for i := len(all) - 1; i >= 0; i-- { + if all[i].Type == "result" { + return all[i], true + } + } + return claudeRun{}, false +} + func claudeUsageArgs() []string { return []string{"-p", "/usage", "--output-format", "json", "--tools", "", "--strict-mcp-config", "--setting-sources", "", "--no-session-persistence"} diff --git a/internal/gateway/claude_warmup_test.go b/internal/gateway/claude_warmup_test.go index 6517e6c8..9e7539ba 100644 --- a/internal/gateway/claude_warmup_test.go +++ b/internal/gateway/claude_warmup_test.go @@ -115,3 +115,28 @@ func TestClaudeUsageRunsClaudeCode(t *testing.T) { } } } + +// With Claude Code's verbose output on (its /config, kept in .claude.json), +// claude -p --output-format json prints every message as an array, the +// system init first and the result last (KevinXC on Discord: usage +// couldn't be read; the tooltip began [{"type":"system","subtype":"init") +func TestClaudeVerboseOutput(t *testing.T) { + text := "Current session: 13% used" + b, _ := json.Marshal([]map[string]any{ + {"type": "system", "subtype": "init", "tools": []string{}}, + {"type": "result", "subtype": "success", "is_error": false, "result": text}, + }) + fakeWarmClaude(t, string(b), 0) + got, err := claudeUsage(context.Background()) + if err != nil || got != text { + t.Fatalf("usage %q %v", got, err) + } + b, _ = json.Marshal([]map[string]any{ + {"type": "system", "subtype": "init"}, + {"type": "result", "is_error": true, "result": "Invalid API key ยท Please run /login"}, + }) + fakeWarmClaude(t, string(b), 1) + if err := warmClaude(context.Background(), ""); err == nil || !strings.Contains(err.Error(), "Please run /login") || strings.Contains(err.Error(), "init") { + t.Fatalf("got %v", err) + } +}