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