feat(llm): add ChatTemplateKwargs to CompletionRequest for provider-specific template params

- Add ChatTemplateKwargs field to llm.CompletionRequest
- Propagate kwargs through agent loop in both Run() and RunStream()
- Pass kwargs to llama.cpp client chat request
- Fix tool schema marshaling to include type/function wrapper
- Fix stream indentation logic in RunStream with responseBuilder
- Remove indirect marker from uuid dependency
This commit is contained in:
Victor Hugo Vargas Servin 2026-07-03 14:22:36 -07:00
parent 0054ca793c
commit 1a8f1557f6
4 changed files with 58 additions and 42 deletions

2
go.mod
View file

@ -4,4 +4,4 @@ go 1.26
require gopkg.in/yaml.v3 v3.0.1 require gopkg.in/yaml.v3 v3.0.1
require github.com/google/uuid v1.6.0 // indirect require github.com/google/uuid v1.6.0

View file

@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"iter" "iter"
"strings"
"time" "time"
"github.com/VictorVargas/rony-llm-agent/pkg/llm" "github.com/VictorVargas/rony-llm-agent/pkg/llm"
@ -40,6 +41,7 @@ type Config struct {
Approver Approver Approver Approver
OnIteration OnIterationHook OnIteration OnIterationHook
ToolTimeout time.Duration ToolTimeout time.Duration
ChatTemplateKwargs map[string]any // passed to the LLM provider (e.g. Qwen enable_thinking)
} }
// Iteration represents a single cycle of the agent loop. // Iteration represents a single cycle of the agent loop.
@ -88,6 +90,7 @@ func (l *Loop) Run(ctx context.Context, input string) (Response, error) {
resp, err := l.cfg.LLM.Generate(ctx, llm.CompletionRequest{ resp, err := l.cfg.LLM.Generate(ctx, llm.CompletionRequest{
Messages: messages, Messages: messages,
Tools: l.getToolSchemas(), Tools: l.getToolSchemas(),
ChatTemplateKwargs: l.cfg.ChatTemplateKwargs,
}) })
if err != nil { if err != nil {
return Response{}, fmt.Errorf("LLM generate failed: %w", err) return Response{}, fmt.Errorf("LLM generate failed: %w", err)
@ -145,9 +148,11 @@ func (l *Loop) RunStream(ctx context.Context, input string) iter.Seq2[llm.Stream
stream := l.cfg.LLM.Stream(ctx, llm.CompletionRequest{ stream := l.cfg.LLM.Stream(ctx, llm.CompletionRequest{
Messages: messages, Messages: messages,
Tools: l.getToolSchemas(), Tools: l.getToolSchemas(),
ChatTemplateKwargs: l.cfg.ChatTemplateKwargs,
}) })
var hasToolCalls bool var hasToolCalls bool
var responseBuilder strings.Builder
for chunk, err := range stream { for chunk, err := range stream {
if err != nil { if err != nil {
yield(llm.StreamChunk{}, err) yield(llm.StreamChunk{}, err)
@ -173,6 +178,7 @@ func (l *Loop) RunStream(ctx context.Context, input string) iter.Seq2[llm.Stream
} }
if !hasToolCalls && chunk.Delta != "" { if !hasToolCalls && chunk.Delta != "" {
responseBuilder.WriteString(chunk.Delta)
if !yield(chunk, nil) { if !yield(chunk, nil) {
return return
} }
@ -184,10 +190,8 @@ func (l *Loop) RunStream(ctx context.Context, input string) iter.Seq2[llm.Stream
} }
} }
if iterations >= l.cfg.MaxIters {
yield(llm.StreamChunk{}, fmt.Errorf("max iterations (%d) reached", l.cfg.MaxIters)) yield(llm.StreamChunk{}, fmt.Errorf("max iterations (%d) reached", l.cfg.MaxIters))
} }
}
} }
func (l *Loop) buildInitialMessages(input string) []llm.Message { func (l *Loop) buildInitialMessages(input string) []llm.Message {
@ -201,7 +205,16 @@ func (l *Loop) buildInitialMessages(input string) []llm.Message {
func (l *Loop) getToolSchemas() []json.RawMessage { func (l *Loop) getToolSchemas() []json.RawMessage {
var schemas []json.RawMessage var schemas []json.RawMessage
for _, tool := range l.cfg.Tools.List() { for _, tool := range l.cfg.Tools.List() {
schemas = append(schemas, tool.InputSchema) def := map[string]interface{}{
"type": "function",
"function": map[string]interface{}{
"name": tool.Name,
"description": tool.Description,
"parameters": json.RawMessage(tool.InputSchema),
},
}
data, _ := json.Marshal(def)
schemas = append(schemas, data)
} }
return schemas return schemas
} }

View file

@ -176,6 +176,7 @@ func (c *Client) buildRequest(req llm.CompletionRequest, stream bool) (io.Reader
Model: req.Model, Model: req.Model,
Messages: messages, Messages: messages,
Stream: stream, Stream: stream,
ChatTemplateKwargs: req.ChatTemplateKwargs,
} }
if len(tools) > 0 { if len(tools) > 0 {
openReq.Tools = tools openReq.Tools = tools
@ -242,6 +243,7 @@ type llamaChatRequest struct {
TopP float32 `json:"top_p,omitempty"` TopP float32 `json:"top_p,omitempty"`
Stop []string `json:"stop,omitempty"` Stop []string `json:"stop,omitempty"`
Stream bool `json:"stream"` Stream bool `json:"stream"`
ChatTemplateKwargs map[string]any `json:"chat_template_kwargs,omitempty"`
} }
type llamaMessage struct { type llamaMessage struct {

View file

@ -74,6 +74,7 @@ type CompletionRequest struct {
MaxTokens *int `json:"max_tokens,omitempty"` MaxTokens *int `json:"max_tokens,omitempty"`
Stop []string `json:"stop,omitempty"` Stop []string `json:"stop,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"` Metadata map[string]string `json:"metadata,omitempty"`
ChatTemplateKwargs map[string]any `json:"chat_template_kwargs,omitempty"` // model-specific chat template params, e.g. Qwen enable_thinking
} }
// CompletionResponse is returned from an LLM provider. // CompletionResponse is returned from an LLM provider.