Skip to content

fix(llm): normalize native tool call metadata#337

Merged
ModerRAS merged 2 commits into
masterfrom
codex/fix-agent-tool-error-loop
May 10, 2026
Merged

fix(llm): normalize native tool call metadata#337
ModerRAS merged 2 commits into
masterfrom
codex/fix-agent-tool-error-loop

Conversation

@ModerRAS

@ModerRAS ModerRAS commented May 10, 2026

Copy link
Copy Markdown
Owner

Summary

Why

The previous fix handled null tool call IDs, but streaming updates can produce an empty encoded tool call ID. That empty ID is then stored in AssistantChatMessage/ToolChatMessage and can fail during the next OpenAI SDK request with errors like Empty encoded value, which bubbles out of the LLMAgent process as AI Agent 执行失败 instead of being handled in the tool-call loop.

Tests

  • dotnet test TelegramSearchBot.LLM.Test/TelegramSearchBot.LLM.Test.csproj --filter OpenAIToolCallNormalizationTests
  • dotnet test TelegramSearchBot.Test/TelegramSearchBot.Test.csproj --filter AgentIntegrationTests
  • dotnet test TelegramSearchBot.LLM.Test/TelegramSearchBot.LLM.Test.csproj
  • dotnet test

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of missing or invalid AI tool-call identifiers with automatic fallback generation
    • Enhanced error resilience for malformed tool argument data
    • Safer processing of tool argument data with graceful error recovery
  • Tests

    • Added test coverage for tool-call normalization and edge case handling

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 10, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@ModerRAS has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 46 minutes and 55 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e005008d-9779-42fe-9520-5738f17675b6

📥 Commits

Reviewing files that changed from the base of the PR and between 10bb57a and 009db40.

📒 Files selected for processing (4)
  • TelegramSearchBot.LLM.Test/Service/AI/LLM/OpenAIToolCallNormalizationTests.cs
  • TelegramSearchBot.LLM/Service/AI/LLM/OpenAIService.cs
  • TelegramSearchBot.LLM/TelegramSearchBot.LLM.csproj
  • TelegramSearchBot.Test/Service/Vector/VectorPerformanceTests.cs
📝 Walkthrough

Walkthrough

OpenAIService adds internal helper methods to normalize tool-call metadata and arguments, applies them throughout streaming tool-call construction/display/execution, and enables test visibility via InternalsVisibleTo. Tests validate the normalization and safe deserialization behavior.

Changes

Tool-Call Normalization

Layer / File(s) Summary
Assembly Configuration
TelegramSearchBot.LLM/TelegramSearchBot.LLM.csproj
InternalsVisibleTo directive grants TelegramSearchBot.LLM.Test access to internal members.
Normalization Helper Methods
TelegramSearchBot.LLM/Service/AI/LLM/OpenAIService.cs
Internal static methods added: NormalizeToolCallId() (generates fallback IDs starting with call_), NormalizeToolCallName() (defaults to "unknown"), NormalizeToolCallArguments() (normalizes to {} when empty), and DeserializeToolArgumentsForDisplay() (safe deserialization returning empty dict on error).
Service Integration
TelegramSearchBot.LLM/Service/AI/LLM/OpenAIService.cs
Native streaming tool-call construction uses normalization helpers for IDs, names, and arguments. Tool-call display formatter uses safe deserialization. Tool execution loop normalizes arguments before parsing.
Test Suite
TelegramSearchBot.LLM.Test/Service/AI/LLM/OpenAIToolCallNormalizationTests.cs
Five xUnit tests cover empty ID fallback generation, non-empty ID trimming, empty argument normalization to {}, non-string value conversion for display, and invalid JSON error handling.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

Poem

🐰 Tool calls normalized with care,
Fallback IDs float through the air,
Empty args become {} bright,
Safe parsing—no crashes tonight! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly describes the main change: normalizing native tool call metadata in the LLM service to fix errors during agent tool-call handling.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/fix-agent-tool-error-loop

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
TelegramSearchBot.LLM/Service/AI/LLM/OpenAIService.cs (1)

1098-1100: ⚡ Quick win

Use the tolerant argument parser for execution path too.

Line 1099 still uses strict string-dictionary deserialization, while display parsing is now tolerant. Reusing the same conversion strategy here would avoid avoidable per-tool parse failures on non-string argument payloads.

Proposed refactor
-                                    var argsJson = NormalizeToolCallArguments(toolCall.FunctionArguments?.ToString());
-                                    var argsDict = JsonConvert.DeserializeObject<Dictionary<string, string>>(argsJson)
-                                        ?? new Dictionary<string, string>();
+                                    var argsDict = DeserializeToolArgumentsForDisplay(
+                                        toolCall.FunctionArguments?.ToString());
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@TelegramSearchBot.LLM/Service/AI/LLM/OpenAIService.cs` around lines 1098 -
1100, The current execution path deserializes argsJson into
Dictionary<string,string> strictly which fails for non-string payloads; change
the conversion in OpenAIService.cs (around
NormalizeToolCallArguments/toolCall.FunctionArguments usage) to use the same
tolerant parser used for display parsing: normalize arguments with
NormalizeToolCallArguments, then deserialize into a Dictionary<string,object>
(or parse to JObject) and convert values to strings safely (e.g., ToString() or
a safe serializer) to build argsDict so non-string argument types won’t cause
exceptions during execution.
TelegramSearchBot.LLM.Test/Service/AI/LLM/OpenAIToolCallNormalizationTests.cs (1)

6-43: ⚡ Quick win

Add coverage for NormalizeToolCallName and null/whitespace argument inputs.

You added the helper in production code, but this test file currently doesn’t pin its behavior. Adding those cases would close the normalization test surface.

Suggested additional tests
+        [Fact]
+        public void NormalizeToolCallName_EmptyOrWhitespace_ReturnsUnknown() {
+            Assert.Equal("unknown", OpenAIService.NormalizeToolCallName(""));
+            Assert.Equal("unknown", OpenAIService.NormalizeToolCallName("   "));
+        }
+
+        [Fact]
+        public void NormalizeToolCallArguments_NullOrWhitespace_ReturnsEmptyJsonObject() {
+            Assert.Equal("{}", OpenAIService.NormalizeToolCallArguments(null));
+            Assert.Equal("{}", OpenAIService.NormalizeToolCallArguments("   "));
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@TelegramSearchBot.LLM.Test/Service/AI/LLM/OpenAIToolCallNormalizationTests.cs`
around lines 6 - 43, Add tests that cover the new NormalizeToolCallName behavior
and null/whitespace argument inputs: add a test that calls
OpenAIService.NormalizeToolCallName with an empty or whitespace string and
asserts it returns a non-empty fallback starting with "call_" and another test
that passes a trimmed name like " tool_1 " and expects "tool_1"; also add tests
for OpenAIService.NormalizeToolCallArguments (or the existing
NormalizeToolCallArguments overload) passing null and whitespace-only inputs and
assert they return "{}" (or an empty JSON object string) to mirror the existing
empty-string case. Reference the OpenAIService class and the
NormalizeToolCallName and NormalizeToolCallArguments method names when adding
these tests alongside the existing NormalizeToolCallId/Arguments tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@TelegramSearchBot.LLM.Test/Service/AI/LLM/OpenAIToolCallNormalizationTests.cs`:
- Around line 6-43: Add tests that cover the new NormalizeToolCallName behavior
and null/whitespace argument inputs: add a test that calls
OpenAIService.NormalizeToolCallName with an empty or whitespace string and
asserts it returns a non-empty fallback starting with "call_" and another test
that passes a trimmed name like " tool_1 " and expects "tool_1"; also add tests
for OpenAIService.NormalizeToolCallArguments (or the existing
NormalizeToolCallArguments overload) passing null and whitespace-only inputs and
assert they return "{}" (or an empty JSON object string) to mirror the existing
empty-string case. Reference the OpenAIService class and the
NormalizeToolCallName and NormalizeToolCallArguments method names when adding
these tests alongside the existing NormalizeToolCallId/Arguments tests.

In `@TelegramSearchBot.LLM/Service/AI/LLM/OpenAIService.cs`:
- Around line 1098-1100: The current execution path deserializes argsJson into
Dictionary<string,string> strictly which fails for non-string payloads; change
the conversion in OpenAIService.cs (around
NormalizeToolCallArguments/toolCall.FunctionArguments usage) to use the same
tolerant parser used for display parsing: normalize arguments with
NormalizeToolCallArguments, then deserialize into a Dictionary<string,object>
(or parse to JObject) and convert values to strings safely (e.g., ToString() or
a safe serializer) to build argsDict so non-string argument types won’t cause
exceptions during execution.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4e4c32d1-b565-44cf-802c-b8a07d365cc3

📥 Commits

Reviewing files that changed from the base of the PR and between ab44e8b and 10bb57a.

📒 Files selected for processing (3)
  • TelegramSearchBot.LLM.Test/Service/AI/LLM/OpenAIToolCallNormalizationTests.cs
  • TelegramSearchBot.LLM/Service/AI/LLM/OpenAIService.cs
  • TelegramSearchBot.LLM/TelegramSearchBot.LLM.csproj

@github-actions

github-actions Bot commented May 10, 2026

Copy link
Copy Markdown
Contributor

PR Check Report

Summary

Test Results

Platform Status Details
Ubuntu Passed Tests passed, artifacts uploaded
Windows Passed Tests passed, artifacts uploaded

Code Quality

  • Code formatting check
  • Security vulnerability scan
  • Dependency analysis
  • Code coverage collection

Test Artifacts

  • Test results artifacts count: 2
  • Code coverage uploaded to Codecov

Links


This report is auto-generated by GitHub Actions

@ModerRAS ModerRAS force-pushed the codex/fix-agent-tool-error-loop branch from 10bb57a to 38603eb Compare May 10, 2026 01:15
@ModerRAS ModerRAS merged commit 5f11d86 into master May 10, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant