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:
parent
e8afd31b28
commit
22f26d5710
5 changed files with 100 additions and 19 deletions
|
|
@ -28,7 +28,8 @@ type AgentInstance struct {
|
||||||
Tools *tools.ToolRegistry
|
Tools *tools.ToolRegistry
|
||||||
Subagents *config.SubagentsConfig
|
Subagents *config.SubagentsConfig
|
||||||
SkillsFilter []string
|
SkillsFilter []string
|
||||||
Candidates []providers.FallbackCandidate
|
Candidates []providers.FallbackCandidate
|
||||||
|
ReasoningEffort string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAgentInstance creates an agent instance from config.
|
// NewAgentInstance creates an agent instance from config.
|
||||||
|
|
@ -84,20 +85,21 @@ func NewAgentInstance(
|
||||||
candidates := providers.ResolveCandidates(modelCfg, defaults.Provider)
|
candidates := providers.ResolveCandidates(modelCfg, defaults.Provider)
|
||||||
|
|
||||||
return &AgentInstance{
|
return &AgentInstance{
|
||||||
ID: agentID,
|
ID: agentID,
|
||||||
Name: agentName,
|
Name: agentName,
|
||||||
Model: model,
|
Model: model,
|
||||||
Fallbacks: fallbacks,
|
Fallbacks: fallbacks,
|
||||||
Workspace: workspace,
|
Workspace: workspace,
|
||||||
MaxIterations: maxIter,
|
MaxIterations: maxIter,
|
||||||
ContextWindow: defaults.MaxTokens,
|
ContextWindow: defaults.MaxTokens,
|
||||||
Provider: provider,
|
Provider: provider,
|
||||||
Sessions: sessionsManager,
|
Sessions: sessionsManager,
|
||||||
ContextBuilder: contextBuilder,
|
ContextBuilder: contextBuilder,
|
||||||
Tools: toolsRegistry,
|
Tools: toolsRegistry,
|
||||||
Subagents: subagents,
|
Subagents: subagents,
|
||||||
SkillsFilter: skillsFilter,
|
SkillsFilter: skillsFilter,
|
||||||
Candidates: candidates,
|
Candidates: candidates,
|
||||||
|
ReasoningEffort: defaults.ReasoningEffort,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -472,6 +472,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
|
||||||
"tools_count": len(providerToolDefs),
|
"tools_count": len(providerToolDefs),
|
||||||
"max_tokens": 8192,
|
"max_tokens": 8192,
|
||||||
"temperature": 0.7,
|
"temperature": 0.7,
|
||||||
|
"reasoning_effort": agent.ReasoningEffort,
|
||||||
"system_prompt_len": len(messages[0].Content),
|
"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,
|
fbResult, fbErr := al.fallback.Execute(ctx, agent.Candidates,
|
||||||
func(ctx context.Context, provider, model string) (*providers.LLMResponse, error) {
|
func(ctx context.Context, provider, model string) (*providers.LLMResponse, error) {
|
||||||
return agent.Provider.Chat(ctx, messages, providerToolDefs, model, map[string]interface{}{
|
return agent.Provider.Chat(ctx, messages, providerToolDefs, model, map[string]interface{}{
|
||||||
"max_tokens": 8192,
|
"max_tokens": 8192,
|
||||||
"temperature": 0.7,
|
"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 fbResult.Response, nil
|
||||||
}
|
}
|
||||||
return agent.Provider.Chat(ctx, messages, providerToolDefs, agent.Model, map[string]interface{}{
|
return agent.Provider.Chat(ctx, messages, providerToolDefs, agent.Model, map[string]interface{}{
|
||||||
"max_tokens": 8192,
|
"max_tokens": 8192,
|
||||||
"temperature": 0.7,
|
"temperature": 0.7,
|
||||||
|
"reasoning_effort": agent.ReasoningEffort,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,7 @@ type AgentDefaults struct {
|
||||||
MaxTokens int `json:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"`
|
MaxTokens int `json:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"`
|
||||||
Temperature float64 `json:"temperature" env:"PICOCLAW_AGENTS_DEFAULTS_TEMPERATURE"`
|
Temperature float64 `json:"temperature" env:"PICOCLAW_AGENTS_DEFAULTS_TEMPERATURE"`
|
||||||
MaxToolIterations int `json:"max_tool_iterations" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"`
|
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 {
|
type ChannelsConfig struct {
|
||||||
|
|
|
||||||
|
|
@ -268,6 +268,12 @@ func buildCodexParams(messages []Message, tools []ToolDefinition, model string,
|
||||||
params.Instructions = openai.Opt(defaultCodexInstructions)
|
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 {
|
if len(tools) > 0 || enableWebSearch {
|
||||||
params.Tools = translateToolsForCodex(tools, enableWebSearch)
|
params.Tools = translateToolsForCodex(tools, enableWebSearch)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
func TestBuildCodexParams_DefaultWebSearchEnabled(t *testing.T) {
|
||||||
params := buildCodexParams([]Message{{Role: "user", Content: "Hi"}}, nil, "gpt-4o", map[string]interface{}{}, true)
|
params := buildCodexParams([]Message{{Role: "user", Content: "Hi"}}, nil, "gpt-4o", map[string]interface{}{}, true)
|
||||||
if len(params.Tools) != 1 {
|
if len(params.Tools) != 1 {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue