From 42c2e6958db4e870f21ce0b60b7522975cd8757f Mon Sep 17 00:00:00 2001 From: pdfinn Date: Fri, 20 Mar 2026 13:59:13 +0700 Subject: [PATCH] fix: extract tool args and fix tool result history for OpenAI path Mirror of infernode llmclient fixes for the Go llm9p server: 1. Call extractToolArgs on OpenAI tool_call arguments before building TOOL: lines. The Anthropic path already did this; the OpenAI path passed raw {"args":"value"} JSON through to the agent layer. 2. Handle user-role messages with StructuredContent (tool results) in buildChatMessages. Previously these were emitted as plain {"role":"user","content":"tool results submitted"}, losing the actual results and breaking role alternation. Now expanded into individual {"role":"tool"} messages via rebuildToolResultMessages. Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/llm/openai_client.go | 58 ++++++++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/internal/llm/openai_client.go b/internal/llm/openai_client.go index 2456562aafcea23f5e13bd495b7e7fc83466d6d7..8b93913effade0dd6f0d67b111cc7f81ac1b59f9 100644 --- a/internal/llm/openai_client.go +++ b/internal/llm/openai_client.go @@ -356,10 +356,17 @@ func buildChatMessages(systemPrompt string, msgs []Message) []openai.ChatComplet for _, msg := range msgs { switch msg.Role { case "user": - apiMsgs = append(apiMsgs, openai.ChatCompletionMessage{ - Role: openai.ChatMessageRoleUser, - Content: msg.Content, - }) + if msg.StructuredContent != "" { + // User message with structured content = tool results. + // Expand into individual {"role":"tool"} messages. + toolMsgs := rebuildToolResultMessages(msg) + apiMsgs = append(apiMsgs, toolMsgs...) + } else { + apiMsgs = append(apiMsgs, openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleUser, + Content: msg.Content, + }) + } case "assistant": // Check if this message has structured content with tool calls if msg.StructuredContent != "" { @@ -424,6 +431,46 @@ func rebuildAssistantToolMessage(msg Message) openai.ChatCompletionMessage { return apiMsg } +// rebuildToolResultMessages converts a user-role message with Anthropic-format +// tool_result structured content into OpenAI-format {"role":"tool"} messages. +func rebuildToolResultMessages(msg Message) []openai.ChatCompletionMessage { + type rawBlock struct { + Type string `json:"type"` + ToolUseID string `json:"tool_use_id"` + Content string `json:"content"` + } + + var blocks []rawBlock + if err := json.Unmarshal([]byte(msg.StructuredContent), &blocks); err != nil { + // Fallback to plain user message + return []openai.ChatCompletionMessage{{ + Role: openai.ChatMessageRoleUser, + Content: msg.Content, + }} + } + + var msgs []openai.ChatCompletionMessage + for _, b := range blocks { + if b.Type != "tool_result" { + continue + } + msgs = append(msgs, openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleTool, + Content: b.Content, + ToolCallID: b.ToolUseID, + }) + } + + if len(msgs) == 0 { + // No tool_result blocks found; fallback to plain user message + return []openai.ChatCompletionMessage{{ + Role: openai.ChatMessageRoleUser, + Content: msg.Content, + }} + } + return msgs +} + // Ask sends a prompt to the LLM and returns the response. func (c *OpenAIClient) Ask(ctx context.Context, prompt string) (string, error) { c.mu.Lock() @@ -760,8 +807,9 @@ func (c *OpenAIClient) AskWithRequest(ctx context.Context, req AskRequest) (AskR } for _, tc := range toolCalls { + args := extractToolArgs(json.RawMessage(tc.Function.Arguments)) toolCallEntries = append(toolCallEntries, struct{ id, name, args string }{ - tc.ID, tc.Function.Name, tc.Function.Arguments, + tc.ID, tc.Function.Name, args, }) inputJSON := tc.Function.Arguments if inputJSON == "" {