Merge fix/cli-provider-request-timeout: honour request_timeout for CLI providers with clear timeout errors and fallback
This commit is contained in:
commit
1e13c55272
5 changed files with 71 additions and 1 deletions
|
|
@ -7,12 +7,14 @@ import (
|
|||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ClaudeCliProvider implements LLMProvider using the claude CLI as a subprocess.
|
||||
type ClaudeCliProvider struct {
|
||||
command string
|
||||
workspace string
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// NewClaudeCliProvider creates a new Claude CLI provider.
|
||||
|
|
@ -23,10 +25,25 @@ func NewClaudeCliProvider(workspace string) *ClaudeCliProvider {
|
|||
}
|
||||
}
|
||||
|
||||
// NewClaudeCliProviderWithTimeout creates a new Claude CLI provider with a request timeout.
|
||||
func NewClaudeCliProviderWithTimeout(workspace string, timeout time.Duration) *ClaudeCliProvider {
|
||||
return &ClaudeCliProvider{
|
||||
command: "claude",
|
||||
workspace: workspace,
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
// Chat implements LLMProvider.Chat by executing the claude CLI.
|
||||
func (p *ClaudeCliProvider) Chat(
|
||||
ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]any,
|
||||
) (*LLMResponse, error) {
|
||||
if p.timeout > 0 {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(ctx, p.timeout)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
prompt := p.buildStdinPrompt(messages, tools)
|
||||
|
||||
args := []string{"-p", "--output-format", "json", "--dangerously-skip-permissions", "--no-chrome"}
|
||||
|
|
@ -46,6 +63,9 @@ func (p *ClaudeCliProvider) Chat(
|
|||
cmd.Stderr = &stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
return nil, fmt.Errorf("claude cli timed out after %s: %w", p.timeout, context.DeadlineExceeded)
|
||||
}
|
||||
stderrStr := strings.TrimSpace(stderr.String())
|
||||
stdoutStr := strings.TrimSpace(stdout.String())
|
||||
switch {
|
||||
|
|
|
|||
|
|
@ -8,12 +8,14 @@ import (
|
|||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CodexCliProvider implements LLMProvider by wrapping the codex CLI as a subprocess.
|
||||
type CodexCliProvider struct {
|
||||
command string
|
||||
workspace string
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// NewCodexCliProvider creates a new Codex CLI provider.
|
||||
|
|
@ -24,6 +26,15 @@ func NewCodexCliProvider(workspace string) *CodexCliProvider {
|
|||
}
|
||||
}
|
||||
|
||||
// NewCodexCliProviderWithTimeout creates a new Codex CLI provider with a request timeout.
|
||||
func NewCodexCliProviderWithTimeout(workspace string, timeout time.Duration) *CodexCliProvider {
|
||||
return &CodexCliProvider{
|
||||
command: "codex",
|
||||
workspace: workspace,
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
// Chat implements LLMProvider.Chat by executing the codex CLI in non-interactive mode.
|
||||
func (p *CodexCliProvider) Chat(
|
||||
ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]any,
|
||||
|
|
@ -32,6 +43,12 @@ func (p *CodexCliProvider) Chat(
|
|||
return nil, fmt.Errorf("codex command not configured")
|
||||
}
|
||||
|
||||
if p.timeout > 0 {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(ctx, p.timeout)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
prompt := p.buildPrompt(messages, tools)
|
||||
|
||||
args := []string{
|
||||
|
|
@ -69,6 +86,9 @@ func (p *CodexCliProvider) Chat(
|
|||
}
|
||||
|
||||
if err != nil {
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
return nil, fmt.Errorf("codex cli timed out after %s: %w", p.timeout, context.DeadlineExceeded)
|
||||
}
|
||||
if ctx.Err() == context.Canceled {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package providers
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
|
@ -114,7 +115,7 @@ func ClassifyError(err error, provider, model string) *FailoverError {
|
|||
}
|
||||
|
||||
// Context deadline exceeded: treat as timeout, always fallback.
|
||||
if err == context.DeadlineExceeded {
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return &FailoverError{
|
||||
Reason: FailoverTimeout,
|
||||
Provider: provider,
|
||||
|
|
|
|||
|
|
@ -180,6 +180,9 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
|
|||
if workspace == "" {
|
||||
workspace = "."
|
||||
}
|
||||
if cfg.RequestTimeout > 0 {
|
||||
return NewClaudeCliProviderWithTimeout(workspace, time.Duration(cfg.RequestTimeout)*time.Second), modelID, nil
|
||||
}
|
||||
return NewClaudeCliProvider(workspace), modelID, nil
|
||||
|
||||
case "codex-cli", "codexcli":
|
||||
|
|
@ -187,6 +190,9 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
|
|||
if workspace == "" {
|
||||
workspace = "."
|
||||
}
|
||||
if cfg.RequestTimeout > 0 {
|
||||
return NewCodexCliProviderWithTimeout(workspace, time.Duration(cfg.RequestTimeout)*time.Second), modelID, nil
|
||||
}
|
||||
return NewCodexCliProvider(workspace), modelID, nil
|
||||
|
||||
case "gemini-cli", "geminicli":
|
||||
|
|
@ -194,6 +200,9 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
|
|||
if workspace == "" {
|
||||
workspace = "."
|
||||
}
|
||||
if cfg.RequestTimeout > 0 {
|
||||
return NewGeminiCliProviderWithTimeout(workspace, time.Duration(cfg.RequestTimeout)*time.Second), modelID, nil
|
||||
}
|
||||
return NewGeminiCliProvider(workspace), modelID, nil
|
||||
|
||||
case "github-copilot", "copilot":
|
||||
|
|
|
|||
|
|
@ -7,12 +7,14 @@ import (
|
|||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GeminiCliProvider implements LLMProvider using the gemini CLI as a subprocess.
|
||||
type GeminiCliProvider struct {
|
||||
command string
|
||||
workspace string
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// NewGeminiCliProvider creates a new Gemini CLI provider.
|
||||
|
|
@ -23,10 +25,25 @@ func NewGeminiCliProvider(workspace string) *GeminiCliProvider {
|
|||
}
|
||||
}
|
||||
|
||||
// NewGeminiCliProviderWithTimeout creates a new Gemini CLI provider with a request timeout.
|
||||
func NewGeminiCliProviderWithTimeout(workspace string, timeout time.Duration) *GeminiCliProvider {
|
||||
return &GeminiCliProvider{
|
||||
command: "gemini",
|
||||
workspace: workspace,
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
// Chat implements LLMProvider.Chat by executing the gemini CLI.
|
||||
func (p *GeminiCliProvider) Chat(
|
||||
ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]any,
|
||||
) (*LLMResponse, error) {
|
||||
if p.timeout > 0 {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(ctx, p.timeout)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
prompt := p.buildPrompt(messages, tools)
|
||||
|
||||
// --prompt "" triggers non-interactive stdin mode; the empty string is appended to stdin input.
|
||||
|
|
@ -46,6 +63,9 @@ func (p *GeminiCliProvider) Chat(
|
|||
cmd.Stderr = &stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
return nil, fmt.Errorf("gemini cli timed out after %s: %w", p.timeout, context.DeadlineExceeded)
|
||||
}
|
||||
stderrStr := strings.TrimSpace(stderr.String())
|
||||
stdoutStr := strings.TrimSpace(stdout.String())
|
||||
switch {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue