Fix: openrouter func ctx resp

This commit is contained in:
Grail Finder
2025-11-25 11:40:37 +03:00
parent fc963f86c9
commit 01da37b397
3 changed files with 70 additions and 23 deletions

35
bot.go
View File

@@ -44,6 +44,7 @@ var (
ragger *rag.RAG ragger *rag.RAG
chunkParser ChunkParser chunkParser ChunkParser
lastToolCall *models.FuncCall lastToolCall *models.FuncCall
lastToolCallID string // Store the ID of the most recent tool call
//nolint:unused // TTS_ENABLED conditionally uses this //nolint:unused // TTS_ENABLED conditionally uses this
orator extra.Orator orator extra.Orator
asr extra.STT asr extra.STT
@@ -290,6 +291,8 @@ func sendMsgToLLM(body io.Reader) {
openAIToolChan <- chunk.ToolChunk openAIToolChan <- chunk.ToolChunk
if chunk.FuncName != "" { if chunk.FuncName != "" {
lastToolCall.Name = chunk.FuncName lastToolCall.Name = chunk.FuncName
// Store the tool call ID for the response
lastToolCallID = chunk.ToolID
} }
interrupt: interrupt:
if interruptResp { // read bytes, so it would not get into beginning of the next req if interruptResp { // read bytes, so it would not get into beginning of the next req
@@ -492,14 +495,40 @@ func findCall(msg, toolCall string, tv *tview.TextView) {
f, ok := fnMap[fc.Name] f, ok := fnMap[fc.Name]
if !ok { if !ok {
m := fc.Name + " is not implemented" m := fc.Name + " is not implemented"
chatRound(m, cfg.ToolRole, tv, false, false) // Create tool response message with the proper tool_call_id
toolResponseMsg := models.RoleMsg{
Role: cfg.ToolRole,
Content: m,
ToolCallID: lastToolCallID, // Use the stored tool call ID
}
chatBody.Messages = append(chatBody.Messages, toolResponseMsg)
// Clear the stored tool call ID after using it
lastToolCallID = ""
// Trigger the assistant to continue processing with the new tool response
// by calling chatRound with empty content to continue the assistant's response
chatRound("", cfg.AssistantRole, tv, false, false)
return return
} }
resp := f(fc.Args) resp := f(fc.Args)
toolMsg := fmt.Sprintf("tool response: %+v", string(resp)) toolMsg := string(resp) // Remove the "tool response: " prefix and %+v formatting
fmt.Fprintf(tv, "%s[-:-:b](%d) <%s>: [-:-:-]\n%s\n", fmt.Fprintf(tv, "%s[-:-:b](%d) <%s>: [-:-:-]\n%s\n",
"\n", len(chatBody.Messages), cfg.ToolRole, toolMsg) "\n", len(chatBody.Messages), cfg.ToolRole, toolMsg)
chatRound(toolMsg, cfg.ToolRole, tv, false, false)
// Create tool response message with the proper tool_call_id
toolResponseMsg := models.RoleMsg{
Role: cfg.ToolRole,
Content: toolMsg,
ToolCallID: lastToolCallID, // Use the stored tool call ID
}
chatBody.Messages = append(chatBody.Messages, toolResponseMsg)
// Clear the stored tool call ID after using it
lastToolCallID = ""
// Trigger the assistant to continue processing with the new tool response
// by calling chatRound with empty content to continue the assistant's response
chatRound("", cfg.AssistantRole, tv, false, false)
} }
func chatToTextSlice(showSys bool) []string { func chatToTextSlice(showSys bool) []string {

14
llm.go
View File

@@ -160,11 +160,14 @@ func (op OpenAIer) ParseChunk(data []byte) (*models.TextChunk, error) {
Chunk: llmchunk.Choices[len(llmchunk.Choices)-1].Delta.Content, Chunk: llmchunk.Choices[len(llmchunk.Choices)-1].Delta.Content,
} }
if len(llmchunk.Choices[len(llmchunk.Choices)-1].Delta.ToolCalls) > 0 { if len(llmchunk.Choices[len(llmchunk.Choices)-1].Delta.ToolCalls) > 0 {
resp.ToolChunk = llmchunk.Choices[len(llmchunk.Choices)-1].Delta.ToolCalls[0].Function.Arguments toolCall := llmchunk.Choices[len(llmchunk.Choices)-1].Delta.ToolCalls[0]
fname := llmchunk.Choices[len(llmchunk.Choices)-1].Delta.ToolCalls[0].Function.Name resp.ToolChunk = toolCall.Function.Arguments
fname := toolCall.Function.Name
if fname != "" { if fname != "" {
resp.FuncName = fname resp.FuncName = fname
} }
// Capture the tool call ID if available
resp.ToolID = toolCall.ID
} }
if llmchunk.Choices[len(llmchunk.Choices)-1].FinishReason == "stop" { if llmchunk.Choices[len(llmchunk.Choices)-1].FinishReason == "stop" {
if resp.Chunk != "" { if resp.Chunk != "" {
@@ -471,11 +474,14 @@ func (or OpenRouterChat) ParseChunk(data []byte) (*models.TextChunk, error) {
// Handle tool calls similar to OpenAIer // Handle tool calls similar to OpenAIer
if len(llmchunk.Choices[len(llmchunk.Choices)-1].Delta.ToolCalls) > 0 { if len(llmchunk.Choices[len(llmchunk.Choices)-1].Delta.ToolCalls) > 0 {
resp.ToolChunk = llmchunk.Choices[len(llmchunk.Choices)-1].Delta.ToolCalls[0].Function.Arguments toolCall := llmchunk.Choices[len(llmchunk.Choices)-1].Delta.ToolCalls[0]
fname := llmchunk.Choices[len(llmchunk.Choices)-1].Delta.ToolCalls[0].Function.Name resp.ToolChunk = toolCall.Function.Arguments
fname := toolCall.Function.Name
if fname != "" { if fname != "" {
resp.FuncName = fname resp.FuncName = fname
} }
// Capture the tool call ID if available
resp.ToolID = toolCall.ID
} }
if resp.ToolChunk != "" { if resp.ToolChunk != "" {
resp.ToolResp = true resp.ToolResp = true

View File

@@ -9,6 +9,7 @@ import (
) )
type FuncCall struct { type FuncCall struct {
ID string `json:"id,omitempty"`
Name string `json:"name"` Name string `json:"name"`
Args map[string]string `json:"args"` Args map[string]string `json:"args"`
} }
@@ -39,6 +40,7 @@ type ToolDeltaFunc struct {
} }
type ToolDeltaResp struct { type ToolDeltaResp struct {
ID string `json:"id,omitempty"`
Index int `json:"index"` Index int `json:"index"`
Function ToolDeltaFunc `json:"function"` Function ToolDeltaFunc `json:"function"`
} }
@@ -70,6 +72,7 @@ type TextChunk struct {
Finished bool Finished bool
ToolResp bool ToolResp bool
FuncName string FuncName string
ToolID string
} }
type TextContentPart struct { type TextContentPart struct {
@@ -89,6 +92,7 @@ type RoleMsg struct {
Role string `json:"role"` Role string `json:"role"`
Content string `json:"-"` Content string `json:"-"`
ContentParts []interface{} `json:"-"` ContentParts []interface{} `json:"-"`
ToolCallID string `json:"tool_call_id,omitempty"` // For tool response messages
hasContentParts bool // Flag to indicate which content type to marshal hasContentParts bool // Flag to indicate which content type to marshal
} }
@@ -99,9 +103,11 @@ func (m RoleMsg) MarshalJSON() ([]byte, error) {
aux := struct { aux := struct {
Role string `json:"role"` Role string `json:"role"`
Content []interface{} `json:"content"` Content []interface{} `json:"content"`
ToolCallID string `json:"tool_call_id,omitempty"`
}{ }{
Role: m.Role, Role: m.Role,
Content: m.ContentParts, Content: m.ContentParts,
ToolCallID: m.ToolCallID,
} }
return json.Marshal(aux) return json.Marshal(aux)
} else { } else {
@@ -109,9 +115,11 @@ func (m RoleMsg) MarshalJSON() ([]byte, error) {
aux := struct { aux := struct {
Role string `json:"role"` Role string `json:"role"`
Content string `json:"content"` Content string `json:"content"`
ToolCallID string `json:"tool_call_id,omitempty"`
}{ }{
Role: m.Role, Role: m.Role,
Content: m.Content, Content: m.Content,
ToolCallID: m.ToolCallID,
} }
return json.Marshal(aux) return json.Marshal(aux)
} }
@@ -123,10 +131,12 @@ func (m *RoleMsg) UnmarshalJSON(data []byte) error {
var structured struct { var structured struct {
Role string `json:"role"` Role string `json:"role"`
Content []interface{} `json:"content"` Content []interface{} `json:"content"`
ToolCallID string `json:"tool_call_id,omitempty"`
} }
if err := json.Unmarshal(data, &structured); err == nil && len(structured.Content) > 0 { if err := json.Unmarshal(data, &structured); err == nil && len(structured.Content) > 0 {
m.Role = structured.Role m.Role = structured.Role
m.ContentParts = structured.Content m.ContentParts = structured.Content
m.ToolCallID = structured.ToolCallID
m.hasContentParts = true m.hasContentParts = true
return nil return nil
} }
@@ -135,12 +145,14 @@ func (m *RoleMsg) UnmarshalJSON(data []byte) error {
var simple struct { var simple struct {
Role string `json:"role"` Role string `json:"role"`
Content string `json:"content"` Content string `json:"content"`
ToolCallID string `json:"tool_call_id,omitempty"`
} }
if err := json.Unmarshal(data, &simple); err != nil { if err := json.Unmarshal(data, &simple); err != nil {
return err return err
} }
m.Role = simple.Role m.Role = simple.Role
m.Content = simple.Content m.Content = simple.Content
m.ToolCallID = simple.ToolCallID
m.hasContentParts = false m.hasContentParts = false
return nil return nil
} }