Conversation
Problem ------- When Anthropic ends a generation because the configured `max_tokens` cap was reached, `AnthropicPromptDriver` returned the partial text as an ordinary `Message` (and the stream path yielded ordinary deltas) with no indication that the output was cut off. Callers saw a truncated answer that was indistinguishable from a complete one — a silent failure mode for downstream parsing, structured output, and long summarization tasks. Root cause ---------- Both `try_run` and `try_stream` ignored the API's `stop_reason`. The sync path logged the full response at DEBUG only, and the stream path consumed `message_delta` events purely for usage accounting, discarding `event.delta.stop_reason`. Approach -------- Inspect `stop_reason` in both paths and emit a single `logger.warning` only when it equals `max_tokens`. Normal terminations (`end_turn`, `tool_use`, `stop_sequence`, absent) stay silent. No exception is raised, no content is altered, and caller-specified `max_tokens` is still honoured exactly — this is a diagnostic signal, not a behaviour change. Verification ------------ Parameterized tests cover `max_tokens`, `end_turn`, `tool_use`, and a `None` stop reason across both the sync and stream paths, asserting the warning fires exactly once for `max_tokens` and never otherwise. `pytest tests/unit/drivers/prompt/test_anthropic_prompt_driver.py` reports 94 passed. Restoring only `griptape/drivers/prompt/anthropic_prompt_driver.py` to its pre-fix state while keeping the new tests fails 4 nodes (the two `max_tokens` cases across sync and stream, each doubled by the file's class-level parametrization), so the tests genuinely cover this change. `ruff format --check` reports both touched files already formatted; `ruff check` reports only the pre-existing `CPY001` (missing copyright notice) that the base commit already reports for the same two files. The Anthropic client is mocked; no live API call was made. Impact ------ Operators get a log line pointing at the real cause of a truncated completion. The record is emitted on the shared griptape logger — the module's `logger` is `logging.getLogger(Defaults.logging_config.logger_name)`, whose name resolves to `griptape`, not to a module-specific name — so it follows whatever handler and level are configured for `griptape`. A caller who intentionally sets a low `max_tokens` will therefore see this warning on every call and can only silence it by silencing griptape warnings as a whole. No API surface, return type, or token accounting changes.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Describe your changes
The symptom
When Anthropic ends a generation because the configured
max_tokenscap was reached,AnthropicPromptDriverreturns the partial text as an ordinaryMessage, and the streaming path yields ordinary deltas. There is no signal — not in the return value, not in the logs at default level — that the answer was cut off.The returned
Messagecarries no stop-reason field, so a caller cannot recover it either:A truncated answer is indistinguishable from a complete one. For structured output, long summarization, and anything that parses the model's text, that is a bad way to fail.
The root cause
Both paths in
griptape/drivers/prompt/anthropic_prompt_driver.pyate825448bee212745aa58aa09f628caa5f8a0f9efignore the API'sstop_reason.try_rundumps the whole response at DEBUG and then discards it:try_streamconsumesmessage_deltaevents purely for usage accounting, droppingevent.delta.stop_reason:Both fields exist on the installed SDK. Checked against
anthropic1.4.0 rather than assumed:What this change does
Inspects
stop_reasonin both paths and emits a singlelogger.warningonly when it equalsmax_tokens. Every other termination —end_turn,tool_use,stop_sequence, absent — stays silent.No exception is raised, no content is altered, no return type changes, and caller-specified
max_tokensis still honoured exactly. This is a diagnostic signal, not a behaviour change.Full diff: 2 files changed, 40 insertions(+), zero deletions — 4 added lines in the driver, 36 in its test file.
The trade-off a reviewer should decide on
The module-level logger in this file is shared, not module-specific:
I resolved it at runtime rather than reasoning about it:
So this record lands on the single
griptapelogger. Consequence, stated plainly: a caller who deliberately sets a lowmax_tokenswill see this warning on every single call, and can only silence it by silencing griptape warnings as a whole. There is no per-driver filter.I chose the shared logger to stay consistent with the rest of the file rather than change the repo's logging convention inside a bug fix. If you would prefer any of these, say the word and I will rework the branch:
logging.getLogger(__name__)for this record so it can be filtered per driver;logger.debuginstead oflogger.warning;stop_reasononMessagerather than logging at all (a public API change, so probably its own discussion).Issue ticket number and link
Closes #2333 (#2333)
Testing
Python 3.12.12, ruff 0.16.6,
anthropic1.4.0, run from a clean checkout of this branch on top ofe825448.Targeted unit tests — 78 passed on the base commit, 94 on this branch:
Revert proof. Restoring only
griptape/drivers/prompt/anthropic_prompt_driver.pyto its state ate825448while keeping the new tests:Restoring the fix returns it to
94 passed in 0.93s.Being precise about what the new tests are. This branch adds 16 test nodes (94 − 78). Only 4 of them are regression tests — the two
max_tokenscases in the sync path and the two in the stream path, each doubled by the file's class-level parametrization; those are the 4 listed above. The other 12 pass against the unpatched source too: thetest_try_run_does_not_warn_for_normal_stop_reasonsfamily (6 nodes) and the three non-max_tokensstream parameters (6 nodes). They are guards proving the warning does not fire on normal terminations, not proof that the fix works. I would rather say that than let a "16 new tests" number imply more than it does.End-to-end symptom check, same script as in the linked issue (client patched, no network), run on both refs:
Note the returned
Messageis unchanged in both — only the log record is new.Formatting:
Lint — 2 findings, both
CPY001, and the base commit reports the same 2 for the same 2 files. No new findings:Whitespace:
git diff --check e825448is clean (exit 0).Full unit suite, to show nothing else moved.
--continue-on-collection-errorsis needed in my environment because 29 test modules fail to import for lack of optional extras (boto3,pypdf, and similar), which would otherwise abort the run before a single test executed:(Wall-clock times are omitted from those two lines because they swing with machine load and prove nothing.)
Those 82 failures and 248 errors are pre-existing on
e825448in my environment (missing optional extras), not caused by this branch. I sorted theFAILED/ERRORnode-id lines from both runs — 330 lines each — and diffed them: identical. The only delta is +16 passing nodes, which are the new test cases.What was not verified
Stated plainly, because understating is safer than overstating:
No live Anthropic API call. The client is mocked in every test and in the repro. The shapes this fix depends on (
Message.stop_reason,RawMessageDeltaEvent.delta.stop_reason) were confirmed against the installedanthropic1.4.0 type definitions, as pasted above — but not against the wire. If the API ever reports truncation differently from what the SDK types declare, this would not catch it.Only 4 of the 7 documented stop reasons are exercised. The SDK's
Literalincludesstop_sequence,pause_turn,refusal, andmodel_context_window_exceeded; my tests covermax_tokens,end_turn,tool_use, andNone. The untested ones take the same silent path asend_turn, but I did not add nodes for them. Notablymodel_context_window_exceededis arguably also a truncation a caller would want to know about — I deliberately scoped this PR tomax_tokensand did not touch it.make checkwas run, but with drifting tool versions. All four legs —check/format,check/lint,check/types(pyright),check/spell(typos) — were run repo-wide one825448and on this branch. Each of the four outputs is byte-identical between the two refs:Those absolute counts are large because my local tools are newer than the versions
uv.lockpins (ruff 0.16.6 vs 0.15.15, pyright 1.1.413 vs 1.1.410, typos 1.50.1 vs 1.47.0), so they are not what CI will report — the pre-existing findings are an artifact of that drift. The load-bearing part is the base-to-branch delta, which is zero, and neither file this PR touches is in the 55-file "would reformat" set.make check/formatalso runsmdformat --check .github/ docs/;mdformatis not installed here, but this branch touches neither directory.No wording review. I have not tried to match the warning text to any house style; if you want different wording or a different level, it is a one-line change.
One interpreter, one OS. Python 3.12.12 on macOS only. The
>=3.10, <4matrix and Windows were not exercised.The full unit suite is not green to begin with in this environment, so "no regressions" here means "the failure set is byte-identical to base", not "everything passes".
No integration tests.
make test/integrationexecutes documentation code blocks and needs live provider credentials. Not run.