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
19 changes: 19 additions & 0 deletions internal/executor/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,24 @@ type OpenCodeConfig struct {

// ServerCommand is the command to start the server (default: "opencode serve")
ServerCommand string `yaml:"server_command,omitempty"`

// RequestTimeout is the maximum time to wait for OpenCode HTTP responses.
// Applies to session creation, message send, and SSE response headers.
// Default: "10m"
RequestTimeout string `yaml:"request_timeout,omitempty"`
}

// EffectiveRequestTimeout returns the OpenCode HTTP request timeout.
// Falls back to 10m when empty or invalid.
func (c *OpenCodeConfig) EffectiveRequestTimeout() time.Duration {
if c == nil || c.RequestTimeout == "" {
return 10 * time.Minute
}
d, err := time.ParseDuration(c.RequestTimeout)
if err != nil || d <= 0 {
return 10 * time.Minute
}
return d
}

// DefaultBackendConfig returns default backend configuration.
Expand All @@ -669,6 +687,7 @@ func DefaultBackendConfig() *BackendConfig {
Provider: "anthropic",
AutoStartServer: true,
ServerCommand: "opencode serve",
RequestTimeout: "10m",
},
ModelRouting: DefaultModelRoutingConfig(),
Timeout: DefaultTimeoutConfig(),
Expand Down
2 changes: 1 addition & 1 deletion internal/executor/backend_opencode.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func NewOpenCodeBackend(config *OpenCodeConfig) *OpenCodeBackend {
config: config,
log: logging.WithComponent("executor.opencode"),
httpClient: &http.Client{
Timeout: 10 * time.Minute, // Long timeout for AI operations
Timeout: config.EffectiveRequestTimeout(),
},
}
}
Expand Down
13 changes: 11 additions & 2 deletions internal/executor/backend_opencode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ func TestNewOpenCodeBackend(t *testing.T) {
config *OpenCodeConfig
expectServerURL string
expectModel string
expectTimeout string
}{
{
name: "nil config uses defaults",
config: nil,
expectServerURL: "http://127.0.0.1:4096",
expectModel: "anthropic/claude-sonnet-4",
expectTimeout: "10m0s",
},
{
name: "empty server URL uses default",
Expand All @@ -34,15 +36,18 @@ func TestNewOpenCodeBackend(t *testing.T) {
},
expectServerURL: "http://127.0.0.1:4096",
expectModel: "custom-model",
expectTimeout: "10m0s",
},
{
name: "custom config",
config: &OpenCodeConfig{
ServerURL: "http://localhost:5000",
Model: "anthropic/claude-opus-4",
ServerURL: "http://localhost:5000",
Model: "anthropic/claude-opus-4",
RequestTimeout: "20m",
},
expectServerURL: "http://localhost:5000",
expectModel: "anthropic/claude-opus-4",
expectTimeout: "20m0s",
},
{
name: "empty model uses default",
Expand All @@ -52,6 +57,7 @@ func TestNewOpenCodeBackend(t *testing.T) {
},
expectServerURL: "http://localhost:4096",
expectModel: "anthropic/claude-sonnet-4",
expectTimeout: "10m0s",
},
}

Expand All @@ -67,6 +73,9 @@ func TestNewOpenCodeBackend(t *testing.T) {
if backend.config.Model != tt.expectModel {
t.Errorf("Model = %q, want %q", backend.config.Model, tt.expectModel)
}
if backend.httpClient.Timeout.String() != tt.expectTimeout {
t.Errorf("http timeout = %q, want %q", backend.httpClient.Timeout.String(), tt.expectTimeout)
}
})
}
}
Expand Down
27 changes: 27 additions & 0 deletions internal/executor/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ func TestOpenCodeConfig(t *testing.T) {
Provider: "anthropic",
AutoStartServer: true,
ServerCommand: "opencode serve --port 5000",
RequestTimeout: "20m",
}

if config.ServerURL != "http://localhost:5000" {
Expand All @@ -127,6 +128,32 @@ func TestOpenCodeConfig(t *testing.T) {
if !config.AutoStartServer {
t.Error("AutoStartServer should be true")
}
if config.RequestTimeout != "20m" {
t.Errorf("RequestTimeout = %q, want 20m", config.RequestTimeout)
}
}

func TestOpenCodeConfigEffectiveRequestTimeout(t *testing.T) {
tests := []struct {
name string
config *OpenCodeConfig
want string
}{
{name: "nil config fallback", config: nil, want: "10m0s"},
{name: "empty timeout fallback", config: &OpenCodeConfig{}, want: "10m0s"},
{name: "configured timeout used", config: &OpenCodeConfig{RequestTimeout: "20m"}, want: "20m0s"},
{name: "invalid timeout falls back", config: &OpenCodeConfig{RequestTimeout: "nope"}, want: "10m0s"},
{name: "zero timeout falls back", config: &OpenCodeConfig{RequestTimeout: "0s"}, want: "10m0s"},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.config.EffectiveRequestTimeout().String()
if got != tt.want {
t.Fatalf("EffectiveRequestTimeout() = %q, want %q", got, tt.want)
}
})
}
}

func TestResolveModel(t *testing.T) {
Expand Down
Loading