feat: add reasoning_effort support for Codex provider

Allow controlling OpenAI reasoning effort via the
agents.defaults.reasoning_effort config field or the
PICOCLAW_AGENTS_DEFAULTS_REASONING_EFFORT env var. The value
is passed through the options map and applied to the Responses
API ReasoningParam when using the Codex provider. Non-Codex
providers ignore the option.

Valid levels: none, minimal, low, medium, high, xhigh.
This commit is contained in:
Zi Jie Sng 2026-02-19 11:27:55 +08:00
parent e8afd31b28
commit 22f26d5710
No known key found for this signature in database
GPG key ID: C6746FA87F0B063A
5 changed files with 100 additions and 19 deletions

View file

@ -28,7 +28,8 @@ type AgentInstance struct {
Tools *tools.ToolRegistry
Subagents *config.SubagentsConfig
SkillsFilter []string
Candidates []providers.FallbackCandidate
Candidates []providers.FallbackCandidate
ReasoningEffort string
}
// NewAgentInstance creates an agent instance from config.
@ -84,20 +85,21 @@ func NewAgentInstance(
candidates := providers.ResolveCandidates(modelCfg, defaults.Provider)
return &AgentInstance{
ID: agentID,
Name: agentName,
Model: model,
Fallbacks: fallbacks,
Workspace: workspace,
MaxIterations: maxIter,
ContextWindow: defaults.MaxTokens,
Provider: provider,
Sessions: sessionsManager,
ContextBuilder: contextBuilder,
Tools: toolsRegistry,
Subagents: subagents,
SkillsFilter: skillsFilter,
Candidates: candidates,
ID: agentID,
Name: agentName,
Model: model,
Fallbacks: fallbacks,
Workspace: workspace,
MaxIterations: maxIter,
ContextWindow: defaults.MaxTokens,
Provider: provider,
Sessions: sessionsManager,
ContextBuilder: contextBuilder,
Tools: toolsRegistry,
Subagents: subagents,
SkillsFilter: skillsFilter,
Candidates: candidates,
ReasoningEffort: defaults.ReasoningEffort,
}
}

View file

@ -472,6 +472,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
"tools_count": len(providerToolDefs),
"max_tokens": 8192,
"temperature": 0.7,
"reasoning_effort": agent.ReasoningEffort,
"system_prompt_len": len(messages[0].Content),
})
@ -492,8 +493,9 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
fbResult, fbErr := al.fallback.Execute(ctx, agent.Candidates,
func(ctx context.Context, provider, model string) (*providers.LLMResponse, error) {
return agent.Provider.Chat(ctx, messages, providerToolDefs, model, map[string]interface{}{
"max_tokens": 8192,
"temperature": 0.7,
"max_tokens": 8192,
"temperature": 0.7,
"reasoning_effort": agent.ReasoningEffort,
})
},
)
@ -508,8 +510,9 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
return fbResult.Response, nil
}
return agent.Provider.Chat(ctx, messages, providerToolDefs, agent.Model, map[string]interface{}{
"max_tokens": 8192,
"temperature": 0.7,
"max_tokens": 8192,
"temperature": 0.7,
"reasoning_effort": agent.ReasoningEffort,
})
}

View file

@ -149,6 +149,7 @@ type AgentDefaults struct {
MaxTokens int `json:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"`
Temperature float64 `json:"temperature" env:"PICOCLAW_AGENTS_DEFAULTS_TEMPERATURE"`
MaxToolIterations int `json:"max_tool_iterations" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"`
ReasoningEffort string `json:"reasoning_effort,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_REASONING_EFFORT"`
}
type ChannelsConfig struct {

View file

@ -268,6 +268,12 @@ func buildCodexParams(messages []Message, tools []ToolDefinition, model string,
params.Instructions = openai.Opt(defaultCodexInstructions)
}
if re, ok := options["reasoning_effort"].(string); ok && re != "" {
params.Reasoning = responses.ReasoningParam{
Effort: responses.ReasoningEffort(re),
}
}
if len(tools) > 0 || enableWebSearch {
params.Tools = translateToolsForCodex(tools, enableWebSearch)
}

View file

@ -142,6 +142,75 @@ func TestBuildCodexParams_StoreIsFalse(t *testing.T) {
}
}
func TestBuildCodexParams_NilOptions(t *testing.T) {
// Passing nil options should not panic.
params := buildCodexParams([]Message{{Role: "user", Content: "Hi"}}, nil, "gpt-4o", nil, false)
if params.Model != "gpt-4o" {
t.Errorf("Model = %q, want %q", params.Model, "gpt-4o")
}
if params.Reasoning.Effort != "" {
t.Errorf("Reasoning.Effort = %q, want empty", params.Reasoning.Effort)
}
}
func TestBuildCodexParams_MissingReasoningEffortKey(t *testing.T) {
// Options map without reasoning_effort should produce zero-value Reasoning.
params := buildCodexParams([]Message{{Role: "user", Content: "Hi"}}, nil, "o3", map[string]interface{}{
"max_tokens": 4096,
}, false)
if params.Reasoning.Effort != "" {
t.Errorf("Reasoning.Effort = %q, want empty", params.Reasoning.Effort)
}
}
func TestBuildCodexParams_ReasoningEffort(t *testing.T) {
params := buildCodexParams([]Message{{Role: "user", Content: "Hi"}}, nil, "o3", map[string]interface{}{
"reasoning_effort": "medium",
}, false)
if params.Reasoning.Effort != responses.ReasoningEffortMedium {
t.Errorf("Reasoning.Effort = %q, want %q", params.Reasoning.Effort, responses.ReasoningEffortMedium)
}
// Verify the JSON serialization includes the reasoning field.
b, err := json.Marshal(params)
if err != nil {
t.Fatalf("json.Marshal: %v", err)
}
var raw map[string]interface{}
if err := json.Unmarshal(b, &raw); err != nil {
t.Fatalf("json.Unmarshal: %v", err)
}
reasoning, ok := raw["reasoning"].(map[string]interface{})
if !ok {
t.Fatalf("reasoning field missing or wrong type in JSON: %s", string(b))
}
if reasoning["effort"] != "medium" {
t.Errorf("JSON reasoning.effort = %v, want %q", reasoning["effort"], "medium")
}
}
func TestBuildCodexParams_ReasoningEffortEmpty(t *testing.T) {
params := buildCodexParams([]Message{{Role: "user", Content: "Hi"}}, nil, "o3", map[string]interface{}{
"reasoning_effort": "",
}, false)
if params.Reasoning.Effort != "" {
t.Errorf("Reasoning.Effort = %q, want empty", params.Reasoning.Effort)
}
// When empty, reasoning should not appear in JSON.
b, err := json.Marshal(params)
if err != nil {
t.Fatalf("json.Marshal: %v", err)
}
var raw map[string]interface{}
if err := json.Unmarshal(b, &raw); err != nil {
t.Fatalf("json.Unmarshal: %v", err)
}
if _, ok := raw["reasoning"]; ok {
t.Errorf("reasoning field should be absent in JSON when empty, got: %s", string(b))
}
}
func TestBuildCodexParams_DefaultWebSearchEnabled(t *testing.T) {
params := buildCodexParams([]Message{{Role: "user", Content: "Hi"}}, nil, "gpt-4o", map[string]interface{}{}, true)
if len(params.Tools) != 1 {