Injecting Tool Results Back Into an Agent Loop Across Six Backends: Where Formatting Breaks
Compare how six different LLM backends expect tool results to be formatted when returned to an agent loop, and learn how to build a normalization shim that seamlessly supports Claude, GPT, DeepSeek, Qwen, GLM, and Kimi through a single API aggregator.
The Problem with Hand-Rolled Agent Loops
When you build an agent loop manually, you typically call a model, parse its tool call request, execute the tool, and then feed the tool result back into the conversation. This last step—injecting the tool result—is where many implementations break when switching between backends.
Each model family has its own conventions for representing tool calls and results. The differences are subtle but critical: some expect a dedicated tool role message, others use a function role, some require the result to be wrapped in a specific object, and some embed it differently within the message history.
Backend-Specific Expectations
Here's how the six backends commonly exposed through an aggregator tend to differ when handling tool results:
- Claude (Anthropic) – Expects tool results as a
toolresultcontent block within ausermessage. The result must reference thetooluse_idfrom the original tool call. Multiple tool results can be batched in one user message. - GPT (OpenAI) – Uses a
toolrole message with atoolcallidfield and the result as a string incontent. This is the most common pattern but is not universal. - DeepSeek – Similar to OpenAI's format but may have slight variations in field naming or required fields depending on the API version. Often expects
role: "tool"withtoolcallidandcontent. - Qwen (Alibaba) – Generally follows OpenAI's convention but may require the tool result to be a JSON string or have additional metadata. Some versions expect a
functionrole instead. - GLM (Zhipu) – Typically uses a
toolrole but might require the result to be wrapped in afunctionobject or include anamefield. - Kimi (Moonshot) – Often aligns with OpenAI's format but may have differences in how it handles multiple tool results or streaming responses.
These differences mean that code written for one backend will likely fail or produce incorrect behavior when pointed at another.
Building a Normalization Shim
To support all six backends with a single agent loop, you need a normalization layer that translates a canonical internal representation into each backend's expected format. Here's a conceptual approach:
- Define a canonical tool result structure – For example, an object with
toolcallid,name, andresult(string or JSON). - Detect the target backend – Based on the model name or a configuration flag, determine which formatting rules to apply.
- Transform the canonical structure – Write backend-specific adapters that convert the canonical tool result into the exact message shape required.
- Inject into the conversation – Append the transformed message to the message history in the correct position (usually after the assistant's tool call message).
Example Adapter Logic
- For Claude: Create a
usermessage with atoolresultcontent block containingtooluse_idandcontent. - For GPT/DeepSeek/Kimi: Create a
toolrole message withtoolcallidandcontent. - For Qwen/GLM: Check documentation for exact requirements; often a
toolrole with additionalnamefield or afunctionrole.
Handling Tool Call IDs
A critical detail is the tool call ID. When the model requests a tool call, it provides an ID that must be referenced in the result. Claude uses tooluseid; OpenAI uses toolcallid. Your shim must map between these consistently. If the backend does not provide an ID (rare), you may need to generate one and hope the backend accepts it.
Streaming and Partial Results
If you're streaming responses, tool results are usually injected after the stream completes. However, some backends may expect the tool result to be part of the same stream or have specific ordering requirements. Always inject tool results as a new message after the assistant's message that contains the tool call.
Testing Across Backends
The only reliable way to ensure your shim works is to test each backend with a simple tool (e.g., a calculator or echo function) and verify that the model correctly incorporates the result into its next response. Use the aggregator to switch models easily and compare behavior.
Cost Considerations
When using the aggregator, you pay the official price multiplied by 1.3 for each API call, including those that inject tool results. If you're a key contributor, you receive credits at official price × 1.1 (or × 1.2 for premium models) in USDC. This means that while testing across multiple backends, your effective cost per call is higher than official pricing, so optimize your loop to minimize unnecessary calls.
Conclusion
Injecting tool results correctly is essential for a functional agent loop. By understanding the formatting differences across backends and implementing a normalization shim, you can write once and run everywhere—without rewriting your agent logic for each model. Always refer to the latest API documentation for each backend, as formats can change.