feat(llm): exercise a tool-call round trip in ocr llm test - #1394
Conversation
|
🔍 OpenCodeReview found 2 issue(s) in this PR.
|
102c53a to
555e65d
Compare
chaojixinren
left a comment
There was a problem hiding this comment.
The approach makes sense. Please address the multi-call handling issue noted inline and add a regression test before merging. Otherwise, make test and make check both pass locally.
| if tc := findTestToolCall(resp, task.Tool.Name); tc != nil { | ||
| toolCalled = true | ||
| messages = append(messages, | ||
| llm.NewToolCallMessage(resp.VisibleContent(), resp.ToolCalls(), resp.Native(), resp.ReasoningContent()), |
There was a problem hiding this comment.
This replays all tool calls but only returns a result for the first one. If the model returns multiple calls, a working provider may reject the next request.
Could you return a result for every call and add a regression test?
There was a problem hiding this comment.
Thanks you were right, and it was worse than one missed result. I reproduced the old construction against a response with three calls: it replayed all three and answered one, leaving call_2 and call_3 unanswered. Both supported families reject that, so a perfectly healthy provider would have returned a 400 on the second request indistinguishable from the provider rejection this command exists to detect. The command would have blamed the endpoint for its own malformed request.
Fixed in toolResultMessages, which answers every call in the replayed turn:
- a call to the configured self-test tool gets the configured result, including a repeated call under a different id;
- any other call gets
Error: this tool is not available in ocr llm test and was not executed.an honest non-execution rather than an invented outcome, and non-empty because some providers reject an empty result.
I considered the narrower alternative of replaying only the self-test call so there would be exactly one call and one result, and rejected it for two reasons. It sends a turn the model did not produce; and it would not reliably work anyway, because buildAnthropicParams reuses the whole native assistant message (internal/llm/client.go:1364), so a filtered slice would not be what goes on the wire. Replaying the real turn and answering all of it is both more faithful and more portable.
Regression test as requested: TestSecondTurnPairsEveryToolCall builds the second request exactly as runLLMTest does and asserts every replayed tool call is answered exactly once, with a mix of offered, unknown and repeated calls. TestToolResultMessages_AnswersEveryCall covers the mapping directly.
Two related notes while I was in here:
- The Anthropic path hardcodes
is_error=falsewhen building tool-result blocks (client.go:1343), so the unknown-tool result above cannot be marked as an error on that path. Only the text conveys it. Fixing that means changing the shared result helper, which felt out of scope for this PR happy to follow up separately if you would like it. - Re-verified live against Vertex with
google/gemini-3.8-flashafter the change: on this branch without fix(llm): replay tool_call extra_content and surface provider error bodies #1393 the command still correctly fails at the post-tool-call turn, which is the behaviour the PR is for.
make test and make check equivalents pass here too, with the caveat already in the description that I have not run the full suite to completion on this machine.
A single request never covers the turn after a tool call, so a provider that rejects that turn passed the connectivity test while failing every review. Refs alibaba#1357
555e65d to
1905683
Compare
|
One further question while looking at this, which I would rather ask than decide unilaterally. The round trip is still optional. The first request does not force tool choice, so a model that ignores the prompt produces no tool call, and the command prints Setting The reason I have not done it is the Anthropic trade-off: Happy to add it to this PR, keep it as a follow-up, or leave it as-is. |
Thanks, the multi-call fix addresses my concern, and the regression test covers the mixed and repeated calls. On forced tool choice, let's keep the current behavior in this PR and discuss stricter verification separately. Preserving the configured Anthropic thinking/signature replay path is valuable, and the explicit |
|
Understood, thanks — leaving forced tool choice out of this PR and keeping the Happy to open follow-ups for stricter verification and for Nothing further from me on this PR. |
|
@NanaseInori review |
# Conflicts: # cmd/opencodereview/llm_cmd_test.go
Description
ocr llm testsends one request with no tools. That never covers the turn that follows a tool call, which is where an OpenAI-compatible provider carrying its own tool-call metadata rejects the conversation.That gap is what made #1357 confusing to report. On Vertex AI with
google/gemini-3.8-flashthe test said:while every single review failed at the first
file_read. The configuration was fine; the request shape the test used was simply not the one that breaks.This makes the connectivity test exercise the same shape a review does: offer a trivial tool, and when the model calls it, send the result back as a second turn.
LlmConversationgains an optionaltool, so the task config decides what is offered and what canned result comes back. A task without one behaves exactly as before.task.jsondefinesocr_selftest, which takes a short string and returns a fixed acknowledgement.runLLMTestoffers the tool, and on a call replays it throughNewToolCallMessage+NewToolResultMessagefor the second request.When the model does not call the tool, the test reports the round trip as unverified rather than passing silently. A model declining the tool is not evidence that the provider handles the turn, and quietly treating it as success would rebuild the blind spot this is meant to remove.
This is independent of the
extra_contentfix in #1393 and does not depend on it. It is the detection half: #1393 stops the failure, this stops it from hiding.The pattern is not specific to Gemini. #156 was a misleading connectivity result from the same command, fixed in #162 by adding the explicit
(empty response)and✓ Connection test successfullines this builds on. #571 reportedllm testpassing whileocr scanreturned 401. #700 reports a provider rejectingcache_controlwith a 400 on every review, and notes in passing: "BTW:ocr llm testwas successful". None of those are fixed by this change — each has a different trigger, and a tool round trip would not catch them — but they are three separate reports of the probe clearing while real work fails, which is the shape this narrows.How Has This Been Tested?
make test— not run in full; see notego testpasses oninternal/config/testconnectionandcmd/opencodereview, withgo vetandgofmt -sclean. I have not run the whole suite to completion on this machine —TestRangeDiffDetectsRenameininternal/difffails here for me with an unmodified tree as well, caused by a stray directory in my checkout — so I have not claimed a pass I did not see.Live against Vertex AI (
aiplatform.googleapis.com/.../endpoints/openapi/chat/completions,google/gemini-3.8-flash), which is the endpoint from #1357. This branch is cut frommainand deliberately does not contain the #1393 fix, so it shows the detection working on a genuinely broken setup:ocr llm test✓ Connection test successful— the false passError: llm request after tool call failed: POST ".../chat/completions": 400 Bad Request✓ Connection test successful+✓ Tool-call round trip verifiedIn the passing run the model's answer quotes the tool's return value back — "With connectivity confirmed (
ocr_selftest ok), I am open-code-review…" — so the second turn really did carry the tool result rather than the model inventing an answer.Unit tests cover the reporting contract (verified / unverified / nothing offered, and that the unverified line carries no success mark), the mapping from the configured tool onto
llm.ToolDef, a task that defines no tool, and picking the self-test call out of a response that also contains other tool calls.Type of Change
Behaviour change worth calling out:
ocr llm testnow makes up to two requests instead of one, and can now fail where it previously passed. That is the intent — it fails only where a review would also fail.Checklist
go fmt,go vet)AI/LLM disclosure: I used Claude (Opus 5) to help refine this change. I reviewed and understand every line, ran the live Vertex verification myself, and will answer review comments myself.
Related Issues
Refs #1357