Repository navigation
fix(tools): stop retrying broken tool-call arguments with identical requests - #80
Merged
Merged
Conversation
…equests
A decision whose tool-call arguments did not parse was retried with the
same request, which cannot fix a deterministic cause, and the final
error ("unexpected end of JSON input") did not say why.
The LocalAI SSE reader now:
- accepts SSE lines up to 16 MiB (a tool call sent in one chunk
overflowed the 64KB scanner default);
- logs a chunk that does not decode, and ends a stream that dropped a
chunk and produced a tool call with ErrStreamInterrupted;
- ends a body without [DONE] and without a finish_reason with
ErrStreamInterrupted instead of Done;
- reports the output cap it sent on the Done event (and in LLMReply).
Both decision paths now:
- treat empty arguments as {};
- on finish_reason=length, fail at once with a
*ToolArgumentsTruncatedError when the completion stopped below the
cap (the context window ran out), else raise the cap once;
- on malformed JSON, append the call (with arguments {} so chat
templates that parse history still accept it) and a tool reply that
quotes the parse error and the malformed text to the next request,
and fail with ErrToolArgumentsInvalid.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
A nib session on a local LocalAI (llama.cpp,
n_ctx100k, ~14 tok/s) failed with:The prompt was about 73k tokens. The model then streamed a long tool call until prompt + output reached
n_ctx. llama.cpp stopped withfinish_reason=length, the call's arguments were cut, andjson.Unmarshalfailed. cogito then re-sent the identical request, and each attempt took about half an hour. The final error did not say why.The decision loops treated every argument parse failure the same way and retried with an identical request. That cannot fix a deterministic cause, and the stream reader could also produce cut arguments by itself.
Changes
Errors (
tool_args.go). New sentinels forerrors.Is:ErrStreamInterruptedErrToolArgumentsTruncatedErrToolArgumentsInvalid*ToolArgumentsTruncatedErrorcarriesPromptTokens,CompletionTokens,MaxTokens,ToolName,ArgumentsBytesandReasoningBytes, so a caller can tell "the window ran out" from "the cap was reached", and a long tool call from runaway reasoning. The final decision errors keep their text and wrap the cause.LocalAI stream reader (
clients/localai_client.go):[DONE]and without afinish_reasonis anErrStreamInterruptederror, notDone. Before, a cut connection looked like a finished reply, and the decision parsed partial arguments.data:line that does not decode was silently dropped. It is now logged. If the stream carried a tool call, it ends withErrStreamInterrupted, because the dropped line may have held part of the arguments.StreamEvent(onDone) andLLMReplycarry theMaxTokensthe client sent, so the decision can compare it with the usage.Decisions (
tools.go, streaming and non-streaming):{}.finish_reason, the argument length and the last 80 characters.finish_reason=lengthwith a cut tool call:*ToolArgumentsTruncatedErrorand does not re-send.{}, because llama.cpp chat templates parse earlier tool-call arguments as JSON and would reject the correction request. After the attempts, the error wrapsErrToolArgumentsInvalid.ErrStreamInterrupted.Not changed:
clients/openai_client.go. go-openai returnsio.EOFboth after[DONE]and on a cut connection, so an interruption cannot be detected cleanly there. A comment explains this.Tests
clients/localai_stream_integrity_test.go: a stream ending without[DONE]orfinish_reasonis interrupted; with afinish_reasonit isDone; a 200KB single chunk is accepted; a malformed tool-call chunk is not silently dropped.decision_tool_args_test.go:{}call with the tool message, and every echoed tool call carries valid JSON arguments.ErrToolArgumentsInvalid.ErrStreamInterrupted.All tests were written first and failed before the change.
make test: 74 passed, 0 failed. Plaingo test .in the root is flaky onagent_test.go:206("should spawn agent in background"), which fails 2 of 3 runs on unchangedmaintoo.🤖 Generated with Claude Code