Skip to content

Commit 2f38c3d

Browse files
authored
Enable timeout configuration (#21)
1 parent 8f10808 commit 2f38c3d

File tree

4 files changed

+116
-72
lines changed

4 files changed

+116
-72
lines changed

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,4 @@ provider "openai" {
4545
- `api_key` (String, Sensitive) Project API key (sk-proj...) for authentication. Note: Use project keys, not admin keys.
4646
- `api_url` (String) The URL for OpenAI API. If not set, the OPENAI_API_URL environment variable will be used, or the default value of 'https://api.openai.com/v1'.
4747
- `organization` (String) The Organization ID for OpenAI API operations. If not set, the OPENAI_ORGANIZATION environment variable will be used.
48+
- `timeout` (Number) Timeout in seconds for API operations. If not set, the OPENAI_TIMEOUT environment variable will be used, or the default value of 300 seconds (5 minutes).

internal/client/client.go

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type OpenAIClient struct {
2222
OrganizationID string
2323
APIURL string
2424
HTTPClient *http.Client
25+
Timeout time.Duration // Timeout for all requests
2526
}
2627

2728
// NewClient creates a new instance of the OpenAI client
@@ -57,17 +58,87 @@ func NewClient(apiKey, organizationID, apiURL string) *OpenAIClient {
5758
MaxIdleConnsPerHost: 10,
5859
}
5960

60-
return &OpenAIClient{
61+
defaultTimeout := 60 * time.Second
62+
63+
client := &OpenAIClient{
6164
APIKey: apiKey,
6265
OrganizationID: organizationID,
6366
APIURL: apiURL,
6467
HTTPClient: &http.Client{
6568
Transport: transport,
66-
Timeout: 60 * time.Second, // Increased timeout for API operations
69+
Timeout: defaultTimeout,
70+
},
71+
Timeout: defaultTimeout,
72+
}
73+
74+
return client
75+
}
76+
77+
// ClientConfig contains configuration options for the OpenAI client
78+
type ClientConfig struct {
79+
APIKey string
80+
OrganizationID string
81+
APIURL string
82+
Timeout time.Duration // Timeout for all operations
83+
}
84+
85+
// NewClientWithConfig creates a new instance of the OpenAI client with custom configuration
86+
func NewClientWithConfig(config ClientConfig) *OpenAIClient {
87+
// Set default API URL if not provided
88+
if config.APIURL == "" {
89+
config.APIURL = "https://api.openai.com"
90+
}
91+
92+
// Ensure the URL doesn't end with a slash
93+
config.APIURL = strings.TrimSuffix(config.APIURL, "/")
94+
95+
// Set default timeout if not provided
96+
if config.Timeout == 0 {
97+
config.Timeout = 60 * time.Second
98+
}
99+
100+
// Debug: Print the client configuration
101+
fmt.Printf("DEBUG: Creating new OpenAI client with API URL: %s\n", config.APIURL)
102+
fmt.Printf("DEBUG: Organization ID: %s\n", config.OrganizationID)
103+
fmt.Printf("DEBUG: API key provided: %v\n", config.APIKey != "")
104+
fmt.Printf("DEBUG: Timeout: %v\n", config.Timeout)
105+
106+
// Create a custom transport with specific timeouts and DNS configuration
107+
dialer := &net.Dialer{
108+
Timeout: 180 * time.Second,
109+
KeepAlive: 180 * time.Second,
110+
DualStack: true,
111+
}
112+
113+
transport := &http.Transport{
114+
Proxy: http.ProxyFromEnvironment,
115+
DialContext: dialer.DialContext,
116+
ForceAttemptHTTP2: true,
117+
MaxIdleConns: 100,
118+
IdleConnTimeout: 90 * time.Second,
119+
TLSHandshakeTimeout: 10 * time.Second,
120+
ExpectContinueTimeout: 1 * time.Second,
121+
MaxIdleConnsPerHost: 10,
122+
}
123+
124+
return &OpenAIClient{
125+
APIKey: config.APIKey,
126+
OrganizationID: config.OrganizationID,
127+
APIURL: config.APIURL,
128+
HTTPClient: &http.Client{
129+
Transport: transport,
130+
Timeout: config.Timeout,
67131
},
132+
Timeout: config.Timeout,
68133
}
69134
}
70135

136+
// SetTimeout updates the timeout for the client
137+
func (c *OpenAIClient) SetTimeout(timeout time.Duration) {
138+
c.Timeout = timeout
139+
c.HTTPClient.Timeout = timeout
140+
}
141+
71142
// Project represents a project in OpenAI
72143
type Project struct {
73144
Object string `json:"object"`

internal/provider/provider.go

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"log"
66
"net/url"
7+
"time"
78

89
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
910
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
@@ -55,7 +56,13 @@ func GetOpenAIClientWithProjectKey(m interface{}) (*client.OpenAIClient, error)
5556
// If project API key is available, create a new client with it
5657
if c.ProjectAPIKey != "" {
5758
log.Printf("[DEBUG] Using project API key for request")
58-
return client.NewClient(c.ProjectAPIKey, c.OpenAIClient.OrganizationID, c.OpenAIClient.APIURL), nil
59+
config := client.ClientConfig{
60+
APIKey: c.ProjectAPIKey,
61+
OrganizationID: c.OpenAIClient.OrganizationID,
62+
APIURL: c.OpenAIClient.APIURL,
63+
Timeout: c.OpenAIClient.Timeout,
64+
}
65+
return client.NewClientWithConfig(config), nil
5966
}
6067
// Fall back to the default client if no project key
6168
log.Printf("[DEBUG] No project API key available, using default client")
@@ -80,7 +87,13 @@ func GetOpenAIClientWithAdminKey(m interface{}) (*client.OpenAIClient, error) {
8087
// If admin API key is available, create a new client with it
8188
if c.AdminAPIKey != "" {
8289
log.Printf("[DEBUG] Using admin API key for request")
83-
return client.NewClient(c.AdminAPIKey, c.OpenAIClient.OrganizationID, c.OpenAIClient.APIURL), nil
90+
config := client.ClientConfig{
91+
APIKey: c.AdminAPIKey,
92+
OrganizationID: c.OpenAIClient.OrganizationID,
93+
APIURL: c.OpenAIClient.APIURL,
94+
Timeout: c.OpenAIClient.Timeout,
95+
}
96+
return client.NewClientWithConfig(config), nil
8497
}
8598
// Fall back to the project API key if no admin key
8699
log.Printf("[DEBUG] No admin API key available, using project API key")
@@ -327,6 +340,12 @@ func Provider() *schema.Provider {
327340
DefaultFunc: schema.EnvDefaultFunc("OPENAI_API_URL", "https://api.openai.com/v1"),
328341
Description: "The URL for OpenAI API. If not set, the OPENAI_API_URL environment variable will be used, or the default value of 'https://api.openai.com/v1'.",
329342
},
343+
"timeout": {
344+
Type: schema.TypeInt,
345+
Optional: true,
346+
DefaultFunc: schema.EnvDefaultFunc("OPENAI_TIMEOUT", 300),
347+
Description: "Timeout in seconds for API operations. If not set, the OPENAI_TIMEOUT environment variable will be used, or the default value of 300 seconds (5 minutes).",
348+
},
330349
},
331350
ResourcesMap: resourceMap,
332351
DataSourcesMap: map[string]*schema.Resource{
@@ -400,27 +419,37 @@ func providerConfigure(ctx context.Context, d *schema.ResourceData) (interface{}
400419
adminKey := d.Get("admin_key").(string)
401420
organization := d.Get("organization").(string)
402421
apiURL := d.Get("api_url").(string)
422+
timeout := d.Get("timeout").(int)
403423

404424
if apiURL == "" {
405425
apiURL = defaultAPIURL
406426
}
407427

408428
log.Printf("[DEBUG] Configuring provider with base URL: %s", apiURL)
409429
log.Printf("[DEBUG] Organization ID: %s", organization)
430+
log.Printf("[DEBUG] Timeout: %d seconds", timeout)
410431

411432
// Validate base URL
412433
baseURL, err := url.Parse(apiURL)
413434
if err != nil {
414435
return nil, diag.Errorf("invalid API URL: %v", err)
415436
}
416437

438+
// Create client configuration with timeout
439+
config := client.ClientConfig{
440+
APIKey: apiKey,
441+
OrganizationID: organization,
442+
APIURL: baseURL.String(),
443+
Timeout: time.Duration(timeout) * time.Second,
444+
}
445+
417446
// Initialize OpenAI client with project API key by default
418447
// The embedded client should use the project API key for standard operations
419-
client := &OpenAIClient{
420-
OpenAIClient: client.NewClient(apiKey, organization, baseURL.String()),
448+
providerClient := &OpenAIClient{
449+
OpenAIClient: client.NewClientWithConfig(config),
421450
ProjectAPIKey: apiKey,
422451
AdminAPIKey: adminKey,
423452
}
424453

425-
return client, nil
454+
return providerClient, nil
426455
}

internal/provider/resource_openai_model_response.go

Lines changed: 8 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package provider
22

33
import (
4-
"bytes"
54
"context"
65
"encoding/json"
76
"fmt"
@@ -250,76 +249,20 @@ func resourceOpenAIModelResponseCreate(ctx context.Context, d *schema.ResourceDa
250249
fmt.Printf("[RESOURCE-DEBUG] Added store=true because stop sequences were provided\n")
251250
}
252251

253-
// Usar directamente HTTP
254-
fmt.Printf("[RESOURCE-DEBUG] Using direct HTTP approach\n")
252+
// Use the client's DoRequest method
253+
fmt.Printf("[RESOURCE-DEBUG] Using client DoRequest with timeout: %v\n", providerClient.Timeout)
255254

256-
// Create complete URL safely
257-
baseURL := providerClient.APIURL
258-
fmt.Printf("[RESOURCE-DEBUG] Original baseURL: %s\n", baseURL)
259-
260-
// Define the url variable
261-
var url string
262-
263-
// Normalize: If base URL already contains /v1, don't add it again
264-
if strings.HasSuffix(baseURL, "/v1") {
265-
if !strings.HasSuffix(baseURL, "/") {
266-
baseURL += "/"
267-
}
268-
url = baseURL + "responses"
269-
fmt.Printf("[RESOURCE-DEBUG] URL with /v1 in base: %s\n", url)
270-
} else {
271-
// Standard case - need to add /v1
272-
if !strings.HasSuffix(baseURL, "/") {
273-
baseURL += "/"
274-
}
275-
url = baseURL + "v1/responses"
276-
fmt.Printf("[RESOURCE-DEBUG] Standard URL: %s\n", url)
277-
}
278-
279-
// Marshal the request body
280-
jsonBody, err := json.Marshal(requestBody)
281-
if err != nil {
282-
fmt.Printf("[RESOURCE-DEBUG] Error marshaling request: %s\n", err)
283-
return diag.Errorf("Error marshaling request: %s", err)
284-
}
285-
286-
// Create the HTTP request
287-
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonBody))
288-
if err != nil {
289-
fmt.Printf("[RESOURCE-DEBUG] Error creating request: %s\n", err)
290-
return diag.Errorf("Error creating request: %s", err)
291-
}
292-
293-
// Set headers
294-
req.Header.Set("Content-Type", "application/json")
295-
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", providerClient.APIKey))
296-
if providerClient.OrganizationID != "" {
297-
req.Header.Set("OpenAI-Organization", providerClient.OrganizationID)
298-
}
299-
300-
// Make the HTTP request
301-
fmt.Printf("[RESOURCE-DEBUG] Sending request to: %s\n", req.URL.String())
302-
resp, err := providerClient.HTTPClient.Do(req)
255+
// Use the /v1/responses endpoint
256+
path := "/v1/responses"
257+
258+
// Make the request using the client's method
259+
respBody, err := providerClient.DoRequest("POST", path, requestBody)
303260
if err != nil {
304261
fmt.Printf("[RESOURCE-DEBUG] Error making request: %s\n", err)
305262
return diag.Errorf("Error making request: %s", err)
306263
}
307-
defer resp.Body.Close()
308-
309-
// Read the response body
310-
respBody, err := io.ReadAll(resp.Body)
311-
if err != nil {
312-
fmt.Printf("[RESOURCE-DEBUG] Error reading response: %s\n", err)
313-
return diag.Errorf("Error reading response: %s", err)
314-
}
315-
316-
// Verify the status code
317-
if resp.StatusCode >= 400 {
318-
fmt.Printf("[RESOURCE-DEBUG] Error response: %d - %s\n", resp.StatusCode, string(respBody))
319-
return diag.Errorf("API error: status code %d, body: %s", resp.StatusCode, string(respBody))
320-
}
321264

322-
// Parse response as a map instead of using client.ModelResponse
265+
// Parse response as a map
323266
var responseMap map[string]interface{}
324267
if err := json.Unmarshal(respBody, &responseMap); err != nil {
325268
fmt.Printf("[RESOURCE-DEBUG] Error parsing response: %s\n", err)

0 commit comments

Comments
 (0)