fix(llmloop): guard nil tool-call arguments map to prevent panic#393
Conversation
Some OpenAI-compatible gateways emit "arguments": null for tool calls.
json.Unmarshal("null", &args) succeeds and sets the map to nil (JSON
null nils maps regardless of prior value), so the code_comment path
override (args["path"] = newPath) panicked with "assignment to entry
in nil map", killing the per-file subtask.
- internal/llmloop: parse arguments through a shared parseToolArgs
helper that always returns a non-nil map, covering both the known-tool
and dynamic-tool paths.
- internal/llm: the Anthropic history-replay path had the same hazard --
null arguments reset the pre-initialized argsMap to nil, serializing
tool_use input as JSON null, which the API rejects.
Fixes alibaba#382
|
✅ OpenCodeReview: No comments generated. Looks good to me. |
lizhengfeng101
left a comment
There was a problem hiding this comment.
Review
Solid bug fix — root cause analysis is accurate, the fix is minimal and defensive, and test coverage is thorough. A few minor suggestions below, none blocking.
1. Consider unifying the nil-guard logic across packages
loop.go extracts parseToolArgs as a helper, but client.go uses an inline if argsMap == nil check. Since they're in different packages, direct reuse isn't trivial, but it's worth either:
- Promoting
parseToolArgsto a shared location (e.g.internal/llm), or - At minimum, cross-referencing the two sites in comments so future maintainers know both exist.
Not a blocker — just a maintainability note.
2. Trim the comment in client.go:689-693
The three-line comment says one thing in three ways. Suggest condensing:
// null arguments → empty map; Anthropic API rejects null input (#382).
argsMap = map[string]any{}3. Tests are excellent
The table-driven cases cover the exact panic repro (null args on code_comment), normal path-override behavior, malformed JSON, empty strings, and the dynamic-tool nil-args path. argsCapturingProvider is a clean, focused mock. Nothing missing here.
Summary
Low-risk, well-tested defensive fix. The graceful degradation (null args → existing "'comments' array is required" error instead of panic) is the right call. LGTM — approve after the optional comment cleanup.
Review feedback on alibaba#393: the two null-arguments guards now reference each other instead of sharing a helper; a 2-line guard does not justify a cross-package export.
|
Thanks for the review @lizhengfeng101! Both comment nits addressed in bb3928c: trimmed the client.go comment to your suggested wording (plus a pointer to |
Problem
ocr scan(and potentiallyreview) crashes withpanic: assignment to entry in nil mapatinternal/llmloop/loop.goinexecuteToolCall, killing the per-file subtask:Reported with GLM via an OpenAI-compatible endpoint; reproduces at any
--concurrency, but only on files where the model emits the offending tool call — matching the reporter's partial-failure sessions ("5 completed, 4 failed").Root cause
Some OpenAI-compatible gateways emit
"arguments": nullfor tool calls.json.Unmarshal([]byte("null"), &args)returns nil error and leaves the map nil. Thecode_commentpath-override then writes into it:This is the only nil-map write in the tool-call handling surface (audited every
Function.Argumentsunmarshal in the repo), but the dynamic-tool path has the same latent hazard: providers receive a nilargsmap and would panic if they ever write to it.Fix
internal/llmloop/loop.go— both argument-unmarshal sites inexecuteToolCallnow go through a sharedparseToolArgshelper that always returns a non-nil map, so every downstream consumer (ParseComments, providerExecute, thecode_commentpath override) is safe. A null-argscode_commentcall now degrades gracefully to the existing "'comments' array is required" tool-error result, which the model can react to — instead of killing the subtask.internal/llm/client.go— the Anthropic history-replay path had the same latent hazard:argsMapis pre-initialized, butjson.Unmarshal([]byte("null"), &argsMap)resets it to nil (JSONnullsets maps to nil regardless of prior value), sotool_useinput would serialize as JSONnull, which the API rejects. Guarded the same way.Intentional behavior note: for dynamic/MCP tools called with
arguments: null, the wire payload to the MCP server changes from"arguments": nullto"arguments": {}— the standards-conformant encoding (JSON-Schematype: objectvalidators rejectnull).Tests
TestExecuteToolCall_ArgumentsEdgeCases(internal/llmloop):nullargs oncode_comment(the exact panic: assignment to entry in nil map #382 repro — panics before this fix),{}, valid args (path-override behavior preserved), empty string, malformed JSON, andnullargs on a dynamic tool (assertsExecutereceives a non-nil map).TestBuildAnthropicParams_NullToolCallArguments(internal/llm):null/empty/{}arguments →tool_useinput is always a non-nil object (fails on the nil map before the fix).Both tests verified RED before the fix, GREEN after:
make check,make test(-race),make coverage(81%+, ≥80% gate) andgo build ./cmd/opencodereviewall pass.Fixes #382