You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit da8825f
Browse filesBrowse the repository at this point in the historyBrowse files
A bare backend name was read as a model name, and the CLI's failures said nothing (#56)
Two defects that hid each other. Both found by installing 0.1.2 from PyPI into a
clean venv and driving it against a real `claude` v2.1.220.
**`split_spec` only consulted BACKENDS when the spec contained a slash.** A bare
backend name fell through to "assume the default backend, keep the whole string
as the model", so:
split_spec("claude-cli") -> ("claude-cli", "claude-cli")
split_spec("mock") -> ("claude-cli", "mock")
`--model claude-cli` therefore shelled out to `claude -p --model claude-cli` and
was refused by the CLI on every call (3/3), while `grapharc models --check` went
on reporting the backend `usable` and `grapharc models claude-cli` printed
`model: claude-cli` without complaint — two commands whose job is to say whether
a spec will work, both saying yes about one that never did.
`--model mock` was the worse half: it named the *paid* subscription backend and
spawned the real binary, so the double `models --check` describes as "scripted
test double; never reaches a provider" reached for one. It happened to fail
before billing only because `mock` is not a model name.
This is the same "silently folded into a model name … fails much later with a
confusing error" failure `split_spec` already refuses for a mistyped backend
*with* a slash; it just could not see the case without one.
A bare backend name now resolves to that backend. `claude-cli` takes its own
default model and `mock` takes the scripted double (which ignores the model
segment entirely). `openrouter`, `openai` and `ollama` front catalogues rather
than a model, so a bare name there is refused with a spelling that works rather
than a guess about what to bill someone for. Slash forms and bare *model* names
are untouched.
**The gateway read the wrong stream.** `claude -p` fails with a non-zero exit, an
empty stderr, and its whole explanation in the JSON envelope on stdout:
{"is_error": true, "result": "There's an issue with the selected model
(claude-cli). It may not exist or you may not have access to it."}
`_invoke_cli` reported `proc.stderr`, so the error was `claude -p exited 1: ` —
a sentence that stops at the colon. That is how the bug above presented itself:
as no message at all. The one string that would have diagnosed it in seconds was
captured, held in `proc.stdout`, and discarded. Note the code already parses this
shape a few lines below, but only on the `returncode == 0` path, and the CLI sets
`is_error` *and* exits non-zero, so it took the branch that ignores stdout.
stdout is read first now, falling back to stderr when it is not the documented
envelope. The recovered text also feeds `_cli_failure`, which classifies
transient-vs-deterministic and was previously classifying from `""`.
Tests: every new test was confirmed to fail against the old code and pass against
the new — including the mock one, which asserts no subprocess is *created* rather
than just checking the returned type, since the type was what was wrong and a
refactor could fix the type and still shell out. `BARE_BACKEND_MODEL["claude-cli"]`
is a second copy of the model class's default so the registry need not import a
backend to split a string, and a test pins the two together the way CI already
pins `__version__` to the packaged version.
Verified: `grapharc demo stage1 --model claude-cli` now completes (8 nodes,
target_met, exit 0) where it previously died with an empty error; `--model mock`
resolves to the double with no subprocess; `--model openrouter` exits 2 with an
example. Full suite green on 3.12 and 3.13; ruff clean.
Co-authored-by: Claude <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: README.md
+2-1Lines changed: 2 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -529,7 +529,8 @@ Re-derived on 2026-07-28 by running each item, not by reading the commit log.
529
529
-**A planning round is an envelope, not a measurement.** A `round` event used to carry the planner's `tokens` and the round's `duration_ms`, both of which `metrics`, `cost` and `replay` add on top of node totals — and the planner's spend was already reported by its own `plan` event, so it was counted twice, and a round's duration encloses the plan plus every node it ran. Neither is on the event now; both are on its `state_delta` as `round_tokens` / `round_iterations` / `round_duration_ms`, where no reader sums them. `RoundRecord.iterations` also holds a figure now rather than always `0`.
530
530
-**The Claude CLI backend is completion-only.** Tool calling and structured output need one of the OpenAI-wire backends: `openrouter`, `openai`, or a local `ollama`.
531
531
-**A session turn is synchronous**, and a runner claim is a claim rather than a lease — nothing reclaims a session whose runner died holding it.
532
-
-**A bare model spec resolves to the paid `claude-cli` backend.**`--model mock` does not reach the scripted double; it becomes the model name `mock` on the subscription backend. Only the slash form (`mock/anything`) reaches the double. A mistyped backend *with* a slash is rejected properly, exit 2.
532
+
-*Closed:* a **bare backend name was read as a model name**, because `split_spec` only consulted the backend list when the spec contained a slash. `--model claude-cli` — the backend `models --check` reports as `usable` — shelled out to `claude -p --model claude-cli` and was refused by the CLI on *every* call, and `--model mock` named the paid subscription backend and spawned the real binary, so the double documented as "never reaches a provider" reached for one. A bare backend name now resolves to that backend (`claude-cli` to its own default model, `mock` to the scripted double, which ignores the model segment anyway); `openrouter`, `openai` and `ollama` front catalogues rather than a model, so those are refused with an example spelling instead of a guess about what to bill you for. The slash forms and bare *model* names are unchanged.
533
+
-*Closed:* a failing `claude -p`**reported no reason at all**. The CLI exits non-zero with an empty stderr and puts its explanation in the JSON envelope on stdout; the gateway read only stderr, so the error was `claude -p exited 1: ` — a sentence that stops at the colon. Since the wrong-model bug above presented itself exactly that way, the two hid each other. stdout is read first now, and the recovered text also feeds the transient-vs-deterministic classifier, which was previously deciding from `""`.
533
534
-**`.env` is found by walking up parent directories; `grapharc.toml` is not.** The config layer refuses an upward search on purpose — a run must not be governed by a file you did not know about. The credential loader predates that decision and still searches upward, so the thing that *spends money* is discovered more eagerly than the thing that *constrains* it.
534
535
-**`grapharc run` has no budget unless you give it one.** Set any of `--max-tokens`, `--max-iterations`, `--max-seconds`, or `--max-concurrency`; without them each dimension is unlimited and the gate admits a topology of any worst-case cost.
0 commit comments