Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,12 @@ prompt: |
metadata:
enable_thinking: "false"

# Optional: OpenAI-standard reasoning effort, sent on every request as
# "reasoning_effort" ("none"/"low"/"medium"/"high"). Unlike metadata.enable_thinking,
# this works even when the model's chat template has no enable_thinking toggle
# (e.g. LFM2.5) — so it's the reliable way to turn a reasoning model's thinking off:
reasoning_effort: "none"

# Optional: agent behavior
agent_options:
iterations: 10
Expand Down
68 changes: 36 additions & 32 deletions chat/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,15 @@ type Session struct {
agentMu sync.Mutex
agentStart map[string]time.Time // sub-agent ID -> spawn time, for elapsed

agentManager *cogito.AgentManager
agentDefs []cogito.AgentDefinition
agentModels map[string]bool // models configured per agent type (for the LLM-model guard)
agentLogs *agentLogStore // per-sub-agent activity log (for the agent_logs tool)
llmModel string
apiKey string
baseURL string
metadata map[string]string // global per-request metadata; merged with per-agent overrides
agentManager *cogito.AgentManager
agentDefs []cogito.AgentDefinition
agentModels map[string]bool // models configured per agent type (for the LLM-model guard)
agentLogs *agentLogStore // per-sub-agent activity log (for the agent_logs tool)
llmModel string
apiKey string
baseURL string
metadata map[string]string // global per-request metadata; merged with per-agent overrides
reasoningEffort string // OpenAI reasoning_effort sent on every request (e.g. "none")

configurator *manage.Configurator
reloadMu sync.Mutex
Expand Down Expand Up @@ -151,7 +152,8 @@ func CommandTransport(cmd string, args []string, env ...string) mcp.Transport {
// NewSession creates a new chat session
func NewSession(ctx context.Context, cfg types.Config, callbacks Callbacks, transports ...mcp.Transport) (*Session, error) {
var llm cogito.LLM = clients.NewOpenAILLMWithOptions(cfg.Model, cfg.APIKey, cfg.BaseURL, clients.OpenAIOptions{
Metadata: cfg.Metadata,
Metadata: cfg.Metadata,
ReasoningEffort: cfg.ReasoningEffort,
})

// Session tracing: wrap the LLM so every call is appended to the transcript.
Expand Down Expand Up @@ -185,27 +187,28 @@ func NewSession(ctx context.Context, cfg types.Config, callbacks Callbacks, tran
}

s := &Session{
ctx: ctx,
llm: llm,
clients: clients,
fragment: cogito.NewEmptyFragment(),
messages: []openai.ChatCompletionMessage{},
callbacks: callbacks,
cogitoOptions: cfg.AgentOptions,
compaction: cfg.Compaction,
allowedTools: make(map[string]bool),
agentStart: make(map[string]time.Time),
agentManager: agentManager,
agentLogs: newAgentLogStore(),
llmModel: cfg.Model,
apiKey: cfg.APIKey,
baseURL: cfg.BaseURL,
metadata: cfg.Metadata,
mcpClient: client,
cfgClients: map[string]*mcp.ClientSession{},
cfgServers: map[string]types.MCPServer{},
configurator: manage.New(plugin.BaseDir(), config.WritablePath()),
tracer: tracer,
ctx: ctx,
llm: llm,
clients: clients,
fragment: cogito.NewEmptyFragment(),
messages: []openai.ChatCompletionMessage{},
callbacks: callbacks,
cogitoOptions: cfg.AgentOptions,
compaction: cfg.Compaction,
allowedTools: make(map[string]bool),
agentStart: make(map[string]time.Time),
agentManager: agentManager,
agentLogs: newAgentLogStore(),
llmModel: cfg.Model,
apiKey: cfg.APIKey,
baseURL: cfg.BaseURL,
metadata: cfg.Metadata,
reasoningEffort: cfg.ReasoningEffort,
mcpClient: client,
cfgClients: map[string]*mcp.ClientSession{},
cfgServers: map[string]types.MCPServer{},
configurator: manage.New(plugin.BaseDir(), config.WritablePath()),
tracer: tracer,
}
for _, name := range cfg.AllowedTools {
s.allowedTools[name] = true
Expand Down Expand Up @@ -492,8 +495,9 @@ func (s *Session) SendMessage(text string) (string, error) {
// metadata is this agent type's override; overlay it on the global
// session metadata (per-key: agent wins, global-only keys inherited).
return clients.NewOpenAILLMWithOptions(chosen, s.apiKey, s.baseURL, clients.OpenAIOptions{
Temperature: temperature,
Metadata: mergeMetadata(s.metadata, metadata),
Temperature: temperature,
Metadata: mergeMetadata(s.metadata, metadata),
ReasoningEffort: s.reasoningEffort,
})
}),
cogito.WithAgentSpawnCallback(func(a *cogito.AgentState) {
Expand Down
108 changes: 108 additions & 0 deletions chat/session_reasoning_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package chat_test

import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"sync"
"testing"

"github.com/mudler/nib/chat"
"github.com/mudler/nib/types"
"github.com/mudler/xlog"
)

// reasoningEffortCapturingOpenAI records the "reasoning_effort" of every request
// and replies with a single stop message (streaming + non-streaming paths).
func reasoningEffortCapturingOpenAI(record func(string)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req struct {
Stream bool `json:"stream"`
ReasoningEffort string `json:"reasoning_effort"`
}
body, _ := io.ReadAll(r.Body)
_ = json.Unmarshal(body, &req)
record(req.ReasoningEffort)

if !req.Stream {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"id": "fake", "object": "chat.completion", "model": "fake",
"choices": []any{map[string]any{
"index": 0, "message": map[string]any{"role": "assistant", "content": "ok"},
"finish_reason": "stop",
}},
})
return
}
w.Header().Set("Content-Type", "text/event-stream")
fl, _ := w.(http.Flusher)
emit := func(delta map[string]any, finish string) {
choice := map[string]any{"index": 0, "delta": delta}
if finish != "" {
choice["finish_reason"] = finish
}
b, _ := json.Marshal(map[string]any{
"id": "fake", "object": "chat.completion.chunk", "model": "fake",
"choices": []any{choice},
})
w.Write([]byte("data: "))
w.Write(b)
w.Write([]byte("\n\n"))
if fl != nil {
fl.Flush()
}
}
emit(map[string]any{"content": "ok"}, "")
emit(map[string]any{}, "stop")
w.Write([]byte("data: [DONE]\n\n"))
if fl != nil {
fl.Flush()
}
}
}

// TestSessionSendsConfiguredReasoningEffort proves Config.ReasoningEffort reaches
// the wire as the OpenAI "reasoning_effort" field — the lever that disables a
// reasoning model's thinking when its template has no enable_thinking toggle.
func TestSessionSendsConfiguredReasoningEffort(t *testing.T) {
xlog.SetLogger(xlog.NewLogger(xlog.LogLevel("error"), ""))

var mu sync.Mutex
var last string
srv := httptest.NewServer(reasoningEffortCapturingOpenAI(func(s string) {
mu.Lock()
last = s
mu.Unlock()
}))
defer srv.Close()

cfg := types.Config{
Model: "fake-model",
APIKey: "fake-key",
BaseURL: srv.URL + "/v1",
LogLevel: "error",
ApprovalMode: "auto",
AgentOptions: types.AgentOptions{Iterations: 10, MaxAttempts: 3, MaxRetries: 3},
ReasoningEffort: "none",
}

session, err := chat.NewSession(context.Background(), cfg, chat.Callbacks{})
if err != nil {
t.Fatalf("NewSession: %v", err)
}
defer session.Close()

if _, err := session.SendMessage("hi"); err != nil {
t.Fatalf("SendMessage: %v", err)
}

mu.Lock()
got := last
mu.Unlock()
if got != "none" {
t.Fatalf("request reasoning_effort = %q, want none", got)
}
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ require (
github.com/charmbracelet/glamour v1.0.0
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
github.com/modelcontextprotocol/go-sdk v1.0.0
github.com/mudler/cogito v0.10.1-0.20260604212312-77024e447c46
github.com/mudler/cogito v0.10.1-0.20260605093203-0563a9999d90
github.com/mudler/xlog v0.0.1
github.com/sashabaranov/go-openai v1.41.2
golang.org/x/term v0.36.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/mudler/cogito v0.10.1-0.20260604212312-77024e447c46 h1:lZGsJnVZSXE6J8BJq4frtmpFU+JYc4zBzP5YCULGZYU=
github.com/mudler/cogito v0.10.1-0.20260604212312-77024e447c46/go.mod h1:6sfja3lcu2nWRzEc0wwqGNu/eCG3EWgij+8s7xyUeQ4=
github.com/mudler/cogito v0.10.1-0.20260605093203-0563a9999d90 h1:GKLjvHS0yrhTdVoZw8Zu8s2mo0hA6/V72Gto+wDNZ+I=
github.com/mudler/cogito v0.10.1-0.20260605093203-0563a9999d90/go.mod h1:6sfja3lcu2nWRzEc0wwqGNu/eCG3EWgij+8s7xyUeQ4=
github.com/mudler/xlog v0.0.1 h1:yR3/wszd3ZM6u1n96YITJZ4yUcDgqHSwvQmzUJa+8vg=
github.com/mudler/xlog v0.0.1/go.mod h1:39f5vcd05Qd6GWKM8IjyHNQ7AmOx3ZM0YfhfIGhC18U=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
Expand Down
16 changes: 11 additions & 5 deletions types/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,17 @@ type Config struct {
// LocalAI use it for per-request flags, e.g. {"enable_thinking": "false"}
// to disable reasoning. Applied to the main session and inherited by
// sub-agents (see AgentTypeConfig.Metadata for per-agent overrides).
Metadata map[string]string `yaml:"metadata,omitempty"`
MCPServers map[string]MCPServer `yaml:"mcp_servers"`
AgentOptions AgentOptions `yaml:"agent_options"`
Compaction CompactionConfig `yaml:"compaction"`
Agents []AgentTypeConfig `yaml:"agents"`
Metadata map[string]string `yaml:"metadata,omitempty"`
// ReasoningEffort sets the OpenAI "reasoning_effort" on every request
// ("none"/"low"/"medium"/"high"). Unlike Metadata.enable_thinking, this binds
// even when the model's chat template has no enable_thinking toggle (e.g.
// LFM2.5), so it's the reliable way to disable a reasoning model's thinking
// ("none"). Empty leaves the field unset.
ReasoningEffort string `yaml:"reasoning_effort,omitempty"`
MCPServers map[string]MCPServer `yaml:"mcp_servers"`
AgentOptions AgentOptions `yaml:"agent_options"`
Compaction CompactionConfig `yaml:"compaction"`
Agents []AgentTypeConfig `yaml:"agents"`

PromptFragments []string `yaml:"prompt_fragments"`
Skills []Skill `yaml:"skills"`
Expand Down
Loading