Clarify OpenAI client timeout precedence

This commit is contained in:
2026-07-26 17:42:44 +00:00
parent d86b65adad
commit eb6dfb19b0
5 changed files with 65 additions and 30 deletions

View File

@@ -36,7 +36,6 @@ type OpenAICompatibleConfig struct {
type OpenAICompatibleClient struct {
baseURL string
defaultModel string
timeout time.Duration
httpClient *http.Client
}
@@ -67,7 +66,6 @@ func NewOpenAICompatibleClient(cfg OpenAICompatibleConfig) (*OpenAICompatibleCli
return &OpenAICompatibleClient{
baseURL: strings.TrimRight(baseURL, "/"),
defaultModel: cfg.Model,
timeout: timeout,
httpClient: client,
}, nil
}
@@ -116,19 +114,20 @@ func (c *OpenAICompatibleClient) Generate(ctx context.Context, req domain.Genera
httpReq.Header.Set("Authorization", "Bearer "+apiKey)
}
effectiveTimeout := c.timeout
if req.Target.TimeoutSeconds > 0 {
effectiveTimeout = time.Duration(req.Target.TimeoutSeconds) * time.Second
} else if req.TargetPresence.TimeoutSeconds {
effectiveTimeout = 0
}
httpClient := c.httpClient
if httpClient == nil {
httpClient = &http.Client{Timeout: effectiveTimeout}
} else if httpClient.Timeout != effectiveTimeout {
httpClient = &http.Client{Timeout: defaults.LLMRequestTimeoutDefault}
}
if req.Target.TimeoutSeconds > 0 {
effectiveTimeout := time.Duration(req.Target.TimeoutSeconds) * time.Second
if httpClient.Timeout != effectiveTimeout {
cloned := *httpClient
cloned.Timeout = effectiveTimeout
httpClient = &cloned
}
} else if req.TargetPresence.TimeoutSeconds && httpClient.Timeout != 0 {
cloned := *httpClient
cloned.Timeout = effectiveTimeout
cloned.Timeout = 0
httpClient = &cloned
}

View File

@@ -31,9 +31,6 @@ func TestNewOpenAICompatibleClientDoesNotMutateSuppliedZeroTimeoutClient(t *test
if client.httpClient == supplied {
t.Fatal("expected constructed client to use a cloned HTTP client")
}
if client.httpClient.Timeout != client.timeout {
t.Fatalf("expected cloned client timeout %v, got %v", client.timeout, client.httpClient.Timeout)
}
if client.httpClient.Timeout <= 0 {
t.Fatalf("expected constructed client to use a positive default timeout, got %v", client.httpClient.Timeout)
}
@@ -674,8 +671,9 @@ func TestOpenAICompatibleClientExplicitZeroTimeoutDisablesClientTimeout(t *testi
defer ts.Close()
client, err := NewOpenAICompatibleClient(OpenAICompatibleConfig{
BaseURL: ts.URL + "/v1",
Timeout: time.Nanosecond,
BaseURL: ts.URL + "/v1",
Timeout: time.Nanosecond,
HTTPClient: &http.Client{Timeout: time.Nanosecond},
})
if err != nil {
t.Fatal(err)
@@ -718,6 +716,34 @@ func TestOpenAICompatibleClientOmittedTimeoutUsesClientTimeout(t *testing.T) {
}
}
func TestOpenAICompatibleClientSuppliedHTTPClientTimeoutOverridesConfigTimeout(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(25 * time.Millisecond)
_, _ = w.Write([]byte(`{"choices":[{"message":{"content":"ok"}}]}`))
}))
defer ts.Close()
client, err := NewOpenAICompatibleClient(OpenAICompatibleConfig{
BaseURL: ts.URL + "/v1",
Model: "model",
Timeout: 5 * time.Millisecond,
HTTPClient: &http.Client{Timeout: 100 * time.Millisecond},
})
if err != nil {
t.Fatal(err)
}
resp, err := client.Generate(context.Background(), domain.GenerateRequest{
Prompt: domain.RenderedPrompt{Messages: []domain.RenderedMessage{{Role: "user", Content: "hi"}}},
})
if err != nil {
t.Fatalf("expected supplied client timeout to allow the request, got %v", err)
}
if resp.Content != "ok" {
t.Fatalf("unexpected response content: %q", resp.Content)
}
}
func TestOpenAICompatibleClientRejectsInvalidExtraParamsBeforeProviderCall(t *testing.T) {
tests := []struct {
name string
@@ -1011,9 +1037,10 @@ func TestOpenAICompatibleClientRequestTimeoutOverride(t *testing.T) {
defer ts.Close()
client, err := NewOpenAICompatibleClient(OpenAICompatibleConfig{
BaseURL: ts.URL + "/v1",
Model: "m",
Timeout: 50 * time.Millisecond,
BaseURL: ts.URL + "/v1",
Model: "m",
Timeout: 5 * time.Millisecond,
HTTPClient: &http.Client{Timeout: 50 * time.Millisecond},
})
if err != nil {
t.Fatal(err)