diff --git a/Makefile b/Makefile index 501c676..7d026c4 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ REGISTRY_HOSTNAME=registry.terraform.io NAMESPACE=mkdev-me NAME=openai BINARY=terraform-provider-${NAME} -VERSION=1.2.0-rc.1 +VERSION=1.2.0 OS_ARCH=darwin_arm64 default: install diff --git a/docs/data-sources/organization_user.md b/docs/data-sources/organization_user.md index 1f757e5..d4fe4d8 100644 --- a/docs/data-sources/organization_user.md +++ b/docs/data-sources/organization_user.md @@ -22,7 +22,7 @@ description: |- ### Read-Only -- `created` (Number) The Unix timestamp when the user was added to the organization +- `added_at` (Number) The Unix timestamp when the user was added to the organization - `id` (String) The ID of this resource. - `name` (String) The name of the user - `role` (String) The role of the user in the organization (owner, member, or reader) diff --git a/docs/data-sources/organization_users.md b/docs/data-sources/organization_users.md index c1a1252..e437376 100644 --- a/docs/data-sources/organization_users.md +++ b/docs/data-sources/organization_users.md @@ -46,7 +46,7 @@ output "all_users_count" { Read-Only: -- `created` (Number) +- `added_at` (Number) - `email` (String) - `id` (String) - `name` (String) diff --git a/docs/data-sources/project.md b/docs/data-sources/project.md index a6eee0b..b53497a 100644 --- a/docs/data-sources/project.md +++ b/docs/data-sources/project.md @@ -28,7 +28,7 @@ output "project_id" { # Use project data to set variables locals { project_active = data.openai_project.production.status == "active" - project_title = data.openai_project.production.title + project_name = data.openai_project.production.name } ``` @@ -45,9 +45,7 @@ locals { ### Read-Only -- `archived_at` (Number) Timestamp when the project was archived (null if not archived) -- `created` (Number) Timestamp when the project was created +- `created_at` (Number) Timestamp when the project was created - `id` (String) The ID of this resource. -- `is_initial` (Boolean) Whether this is the initial project +- `name` (String) The name of the project - `status` (String) The status of the project -- `title` (String) The title of the project diff --git a/docs/data-sources/projects.md b/docs/data-sources/projects.md index 6ab1dde..da89427 100644 --- a/docs/data-sources/projects.md +++ b/docs/data-sources/projects.md @@ -42,9 +42,7 @@ output "project_count" { Read-Only: -- `archived_at` (Number) -- `created` (Number) +- `created_at` (Number) - `id` (String) -- `is_initial` (Boolean) +- `name` (String) - `status` (String) -- `title` (String) diff --git a/docs/resources/organization_user.md b/docs/resources/organization_user.md index d87cf67..0250773 100644 --- a/docs/resources/organization_user.md +++ b/docs/resources/organization_user.md @@ -41,7 +41,7 @@ resource "openai_organization_user" "organization_users" { ### Read-Only -- `created` (Number) The Unix timestamp when the user was added to the organization +- `added_at` (Number) The Unix timestamp when the user was added to the organization - `email` (String) The email address of the user - `id` (String) The ID of this resource. - `name` (String) The name of the user diff --git a/docs/resources/project.md b/docs/resources/project.md index 9a6fb96..69c520d 100644 --- a/docs/resources/project.md +++ b/docs/resources/project.md @@ -15,12 +15,12 @@ description: |- ```terraform # Create a new OpenAI project resource "openai_project" "development" { - title = "Development Project" + name = "Development Project" } # Create a production project resource "openai_project" "production" { - title = "Production API Services" + name = "Production API Services" } # Output the project ID @@ -35,7 +35,7 @@ output "dev_project_id" { ### Required -- `title` (String) The title of the project +- `name` (String) The name of the project ### Optional @@ -43,10 +43,9 @@ output "dev_project_id" { ### Read-Only -- `archived_at` (Number) Timestamp when the project was archived, if applicable -- `created` (Number) Timestamp when the project was created +- `archived_at` (String) Timestamp when the project was archived, if applicable +- `created_at` (String) Timestamp when the project was created - `id` (String) The ID of this resource. -- `is_initial` (Boolean) Whether this is the initial project - `status` (String) Status of the project (active, archived, etc.) diff --git a/docs/resources/rate_limit.md b/docs/resources/rate_limit.md index 776d92a..c67e75c 100644 --- a/docs/resources/rate_limit.md +++ b/docs/resources/rate_limit.md @@ -10,7 +10,44 @@ description: |- Manages rate limits for an OpenAI model in a project. Note that rate limits cannot be truly deleted via the API, so this resource will reset rate limits to defaults when removed. This resource requires an admin API key with the api.management.read scope for full functionality, but will gracefully handle permission errors to allow operations to continue. +## Example Usage +```terraform +# Set rate limits for GPT-4o model in a project +resource "openai_rate_limit" "gpt4o_limits" { + project_id = "proj_abc123" + model = "gpt-4o" + + max_requests_per_minute = 500 + max_tokens_per_minute = 30000 +} + +# Set rate limits for GPT-4o-mini with additional constraints +resource "openai_rate_limit" "gpt4o_mini_limits" { + project_id = "proj_abc123" + model = "gpt-4o-mini" + + max_requests_per_minute = 1000 + max_tokens_per_minute = 60000 + max_requests_per_1_day = 10000 +} + +# Set rate limits for DALL-E 3 image generation +resource "openai_rate_limit" "dalle3_limits" { + project_id = "proj_abc123" + model = "dall-e-3" + + max_images_per_minute = 5 +} + +# Set rate limits for batch processing +resource "openai_rate_limit" "batch_limits" { + project_id = "proj_abc123" + model = "gpt-4o" + + batch_1_day_max_input_tokens = 1000000 +} +``` ## Schema @@ -23,7 +60,6 @@ Manages rate limits for an OpenAI model in a project. Note that rate limits cann ### Optional - `batch_1_day_max_input_tokens` (Number) Maximum number of input tokens per day for batch processing. -- `ignore_rate_limit_warning` (Boolean) Set to true to acknowledge that OpenAI rate limits cannot be truly deleted and will be reset to defaults on removal. - `max_audio_megabytes_per_1_minute` (Number) Maximum audio megabytes per minute. - `max_images_per_minute` (Number) Maximum number of images per minute. - `max_requests_per_1_day` (Number) Maximum number of requests per day. diff --git a/examples/data-sources/openai_project/data-source.tf b/examples/data-sources/openai_project/data-source.tf index c4ed594..d3451d6 100644 --- a/examples/data-sources/openai_project/data-source.tf +++ b/examples/data-sources/openai_project/data-source.tf @@ -13,5 +13,5 @@ output "project_id" { # Use project data to set variables locals { project_active = data.openai_project.production.status == "active" - project_title = data.openai_project.production.title + project_name = data.openai_project.production.name } diff --git a/examples/resources/openai_project/resource.tf b/examples/resources/openai_project/resource.tf index 97f49a2..0076f4a 100644 --- a/examples/resources/openai_project/resource.tf +++ b/examples/resources/openai_project/resource.tf @@ -1,11 +1,11 @@ # Create a new OpenAI project resource "openai_project" "development" { - title = "Development Project" + name = "Development Project" } # Create a production project resource "openai_project" "production" { - title = "Production API Services" + name = "Production API Services" } # Output the project ID diff --git a/examples/resources/openai_rate_limit/resource.tf b/examples/resources/openai_rate_limit/resource.tf new file mode 100644 index 0000000..6d552d6 --- /dev/null +++ b/examples/resources/openai_rate_limit/resource.tf @@ -0,0 +1,34 @@ +# Set rate limits for GPT-4o model in a project +resource "openai_rate_limit" "gpt4o_limits" { + project_id = "proj_abc123" + model = "gpt-4o" + + max_requests_per_minute = 500 + max_tokens_per_minute = 30000 +} + +# Set rate limits for GPT-4o-mini with additional constraints +resource "openai_rate_limit" "gpt4o_mini_limits" { + project_id = "proj_abc123" + model = "gpt-4o-mini" + + max_requests_per_minute = 1000 + max_tokens_per_minute = 60000 + max_requests_per_1_day = 10000 +} + +# Set rate limits for DALL-E 3 image generation +resource "openai_rate_limit" "dalle3_limits" { + project_id = "proj_abc123" + model = "dall-e-3" + + max_images_per_minute = 5 +} + +# Set rate limits for batch processing +resource "openai_rate_limit" "batch_limits" { + project_id = "proj_abc123" + model = "gpt-4o" + + batch_1_day_max_input_tokens = 1000000 +} diff --git a/internal/client/client.go b/internal/client/client.go index 1d9b853..793f744 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -141,13 +141,12 @@ func (c *OpenAIClient) SetTimeout(timeout time.Duration) { // Project represents a project in OpenAI type Project struct { - ID string `json:"id"` Object string `json:"object"` - Created int64 `json:"created"` - Status string `json:"status"` + ID string `json:"id"` + Name string `json:"name"` + CreatedAt *int64 `json:"created_at"` ArchivedAt *int64 `json:"archived_at"` - IsInitial bool `json:"is_initial"` - Title string `json:"title"` + Status string `json:"status"` } // ProjectUser represents a user associated with a project @@ -189,11 +188,6 @@ type APIKey struct { LastUsedAt *int64 `json:"last_used_at,omitempty"` } -// CreateProjectRequest represents the request to create a project -type CreateProjectRequest struct { - Title string `json:"title"` -} - // Error represents an error from the OpenAI API type Error struct { Message string `json:"message"` @@ -559,64 +553,23 @@ type AssistantFunction struct { Parameters json.RawMessage `json:"parameters"` } -// UserInfo represents the nested user details in an organization user response -type UserInfo struct { - ID string `json:"id"` - Object string `json:"object"` - Email string `json:"email"` - Name string `json:"name"` -} - -// OrganizationUser represents an OpenAI organization user as returned by the API -type OrganizationUser struct { - Object string `json:"object"` - Created int64 `json:"created"` - IsDefault bool `json:"is_default"` - IsScaleTierAuthorizedPurchaser bool `json:"is_scale_tier_authorized_purchaser"` - IsScimManaged bool `json:"is_scim_managed"` - IsServiceAccount bool `json:"is_service_account"` - Role string `json:"role"` - User UserInfo `json:"user"` -} - -// User provides a flattened view of OrganizationUser for easier access -// This is a convenience wrapper used internally +// User represents an OpenAI user type User struct { - ID string `json:"id"` - Object string `json:"object"` - Email string `json:"email"` - Name string `json:"name"` - Role string `json:"role"` - Created int64 `json:"created"` - IsDefault bool `json:"is_default"` - IsScaleTierAuthorizedPurchaser bool `json:"is_scale_tier_authorized_purchaser"` - IsScimManaged bool `json:"is_scim_managed"` - IsServiceAccount bool `json:"is_service_account"` -} - -// ToUser converts an OrganizationUser to a flattened User struct -func (ou *OrganizationUser) ToUser() *User { - return &User{ - ID: ou.User.ID, - Object: ou.Object, - Email: ou.User.Email, - Name: ou.User.Name, - Role: ou.Role, - Created: ou.Created, - IsDefault: ou.IsDefault, - IsScaleTierAuthorizedPurchaser: ou.IsScaleTierAuthorizedPurchaser, - IsScimManaged: ou.IsScimManaged, - IsServiceAccount: ou.IsServiceAccount, - } + ID string `json:"id"` + Object string `json:"object"` + Email string `json:"email"` + Name string `json:"name"` + Role string `json:"role"` + AddedAt int64 `json:"added_at"` } // UsersResponse represents the response from the list users API type UsersResponse struct { - Object string `json:"object"` - Data []OrganizationUser `json:"data"` - FirstID string `json:"first_id"` - LastID string `json:"last_id"` - HasMore bool `json:"has_more"` + Object string `json:"object"` + Data []User `json:"data"` + FirstID string `json:"first_id"` + LastID string `json:"last_id"` + HasMore bool `json:"has_more"` } // ListUsers retrieves a list of users in the organization @@ -693,13 +646,12 @@ func (c *OpenAIClient) FindUserByEmail(email string) (*User, bool, error) { return nil, false, nil } - // Return the first matching user (convert from OrganizationUser to User) - orgUser := usersResponse.Data[0] - user := orgUser.ToUser() + // Return the first matching user + user := usersResponse.Data[0] // Double check that the email matches exactly (case insensitive) if strings.EqualFold(user.Email, email) { - return user, true, nil + return &user, true, nil } // No exact match found @@ -725,14 +677,13 @@ func (c *OpenAIClient) GetUser(userID string) (*User, bool, error) { return nil, false, fmt.Errorf("error getting user: %w", err) } - // Parse the response into OrganizationUser (new API format) - var orgUser OrganizationUser - if err := json.Unmarshal(respBody, &orgUser); err != nil { + // Parse the response + var user User + if err := json.Unmarshal(respBody, &user); err != nil { return nil, false, fmt.Errorf("error decoding user response: %w", err) } - // Convert to flattened User struct - return orgUser.ToUser(), true, nil + return &user, true, nil } // UpdateUserRole updates a user's role @@ -754,14 +705,13 @@ func (c *OpenAIClient) UpdateUserRole(userID string, role string) (*User, error) return nil, fmt.Errorf("error updating user role: %w", err) } - // Parse the response into OrganizationUser (new API format) - var orgUser OrganizationUser - if err := json.Unmarshal(respBody, &orgUser); err != nil { + // Parse the response + var user User + if err := json.Unmarshal(respBody, &user); err != nil { return nil, fmt.Errorf("error decoding user response: %w", err) } - // Convert to flattened User struct - return orgUser.ToUser(), nil + return &user, nil } // DeleteUser removes a user from the organization @@ -1111,15 +1061,15 @@ func (c *OpenAIClient) ListProjects(limit int, includeArchived bool, after strin return &resp, nil } -// CreateProject creates a new project with the given title -func (c *OpenAIClient) CreateProject(title string) (*Project, error) { +// CreateProject creates a new project with the given name +func (c *OpenAIClient) CreateProject(name string) (*Project, error) { // Create the request body requestBody := map[string]interface{}{ - "title": title, + "name": name, } // Debug information - fmt.Printf("Creating project with title: %s\n", title) + fmt.Printf("Creating project with name: %s\n", name) fmt.Printf("Request body: %+v\n", requestBody) // Use the exact endpoint from the curl command that works @@ -1166,11 +1116,11 @@ func (c *OpenAIClient) GetProject(id string) (*Project, error) { return &project, nil } -// UpdateProject updates an existing project with the given title -func (c *OpenAIClient) UpdateProject(id, title string) (*Project, error) { +// UpdateProject updates an existing project with the given name +func (c *OpenAIClient) UpdateProject(id, name string) (*Project, error) { // Create the request body requestBody := map[string]interface{}{ - "title": title, + "name": name, } // Use the exact endpoint structure consistent with the curl command @@ -1352,426 +1302,117 @@ func (c *OpenAIClient) CreateRateLimit(projectID, resourceType, limitType string return &rateLimit, nil } -// GetRateLimit retrieves information about a specific rate limit. +// GetRateLimit retrieves information about a specific rate limit by model name or rate limit ID. +// It lists all rate limits for the project and finds the matching one. // // Parameters: // - projectID: The ID of the project the rate limit belongs to -// - rateLimitID: The ID of the rate limit to retrieve +// - modelOrRateLimitID: Either a model name (e.g., "gpt-4o") or rate limit ID (e.g., "rl-gpt-4o") // // Returns: // - A RateLimit object with details about the requested rate limit // - An error if the operation failed or the rate limit doesn't exist func (c *OpenAIClient) GetRateLimit(projectID, modelOrRateLimitID string) (*RateLimit, error) { - // Debug: Input parameters - fmt.Printf("[DEBUG] GetRateLimit called with:\n") - fmt.Printf(" - Project ID: %s\n", projectID) - fmt.Printf(" - Model or Rate Limit ID: %s\n", modelOrRateLimitID) - - // Step 1: List all rate limits for the project - fmt.Printf("[DEBUG] Listing all rate limits for project %s\n", projectID) rateLimits, err := c.ListRateLimits(projectID) if err != nil { - fmt.Printf("[ERROR] Failed to list rate limits: %v\n", err) return nil, fmt.Errorf("failed to list rate limits: %w", err) } - // Log the rate limits we got for debugging - fmt.Printf("[DEBUG] Found %d rate limits for project\n", len(rateLimits.Data)) - for i, rl := range rateLimits.Data { - fmt.Printf("[DEBUG] Rate limit #%d: ID=%s, Model=%s\n", i+1, rl.ID, rl.Model) - } - - // Determine what we're looking for - model or rate limit ID - model := "" - searchID := modelOrRateLimitID - - if !strings.HasPrefix(modelOrRateLimitID, "rl-") { - // If it doesn't have "rl-" prefix, it's a model name - model = modelOrRateLimitID - searchID = "rl-" + modelOrRateLimitID - fmt.Printf("[DEBUG] Searching by MODEL: %s (ID would be %s)\n", model, searchID) - } else { - // It has the prefix, treat it as an ID but also extract the model - // Try to extract model name from the ID - parts := strings.Split(modelOrRateLimitID[3:], "-") - if len(parts) >= 1 { - model = parts[0] - if len(parts) > 1 { - // For multi-part model names like "gpt-4o-mini" - if !containsProjectSuffix(parts[len(parts)-1]) { - model = strings.Join(parts, "-") - } else { - model = strings.Join(parts[:len(parts)-1], "-") - } - } - } - fmt.Printf("[DEBUG] Searching by ID: %s (extracted model: %s)\n", searchID, model) - } - - // SIMPLE MATCHING STRATEGY: - // 1. Try exact ID match first - for _, rl := range rateLimits.Data { - if rl.ID == searchID { - fmt.Printf("[DEBUG] Found exact ID match: %s (Model: %s)\n", rl.ID, rl.Model) - return &rl, nil - } + // Normalize the search - if it's a rate limit ID, extract the model name + searchModel := modelOrRateLimitID + if strings.HasPrefix(modelOrRateLimitID, "rl-") { + searchModel = strings.TrimPrefix(modelOrRateLimitID, "rl-") } - // 2. Try ID prefix match (accounts for project-specific suffixes) - for _, rl := range rateLimits.Data { - if strings.HasPrefix(rl.ID, searchID) || rl.ID == searchID { - fmt.Printf("[DEBUG] Found ID prefix match: %s (Model: %s)\n", rl.ID, rl.Model) - return &rl, nil + // Search for exact model match first + for i := range rateLimits.Data { + if rateLimits.Data[i].Model == searchModel { + return &rateLimits.Data[i], nil } } - // 3. Try model name match as fallback - if model != "" { - for _, rl := range rateLimits.Data { - if rl.Model == model { - fmt.Printf("[DEBUG] Found by model name match: %s (ID: %s)\n", rl.Model, rl.ID) - return &rl, nil - } + // Try exact ID match + for i := range rateLimits.Data { + if rateLimits.Data[i].ID == modelOrRateLimitID { + return &rateLimits.Data[i], nil } } - // 4. Last resort: try partial model matching for compound model names - if model != "" && strings.Contains(model, "-") { - modelBase := strings.Split(model, "-")[0] - for _, rl := range rateLimits.Data { - if strings.HasPrefix(rl.Model, modelBase) { - fmt.Printf("[DEBUG] Found by partial model name match: %s matches base %s (ID: %s)\n", - rl.Model, modelBase, rl.ID) - return &rl, nil - } - } - } - - // Not found after all attempts - fmt.Printf("[ERROR] Rate limit not found for ID/model: %s\n", modelOrRateLimitID) - return nil, fmt.Errorf("API error: Project with ID '%s' not found or rate limit '%s' does not exist", - projectID, modelOrRateLimitID) -} - -// Helper function to check if a string looks like a project suffix -// (typically 8 or fewer characters at the end of a rate limit ID) -func containsProjectSuffix(s string) bool { - return len(s) <= 8 && len(s) > 0 + return nil, fmt.Errorf("rate limit not found for model/ID '%s' in project '%s'", modelOrRateLimitID, projectID) } // UpdateRateLimit modifies an existing rate limit for a project. +// Uses POST to /v1/organization/projects/{project_id}/rate_limits/{rate_limit_id} func (c *OpenAIClient) UpdateRateLimit(projectID, modelOrRateLimitID string, maxRequestsPerMinute, maxTokensPerMinute, maxImagesPerMinute, batch1DayMaxInputTokens, maxAudioMegabytesPer1Minute, maxRequestsPer1Day *int) (*RateLimit, error) { - // Debug: Input parameters - fmt.Printf("[UPDATE-RL-DEBUG] ========== UpdateRateLimit DEBUG ==========\n") - fmt.Printf("[UPDATE-RL-DEBUG] ProjectID: %s\n", projectID) - fmt.Printf("[UPDATE-RL-DEBUG] ModelOrRateLimitID: %s\n", modelOrRateLimitID) - - // Step 1: List all rate limits and find the one we want to update - fmt.Printf("[UPDATE-RL-DEBUG] Listing all rate limits to find target rate limit\n") - rateLimits, err := c.ListRateLimits(projectID) + // First, find the rate limit to get its ID + targetRateLimit, err := c.GetRateLimit(projectID, modelOrRateLimitID) if err != nil { - fmt.Printf("[UPDATE-RL-DEBUG] Failed to list rate limits: %v\n", err) - return nil, fmt.Errorf("failed to list rate limits: %w", err) - } - - // Search logic similar to GetRateLimit - model := "" - searchID := modelOrRateLimitID - - if !strings.HasPrefix(modelOrRateLimitID, "rl-") { - // If it doesn't have "rl-" prefix, it's a model name - model = modelOrRateLimitID - searchID = "rl-" + modelOrRateLimitID - fmt.Printf("[UPDATE-RL-DEBUG] Searching by MODEL: %s (ID would be %s)\n", model, searchID) - } else { - // It has the prefix, treat it as an ID but also extract the model - // Try to extract model name from the ID - parts := strings.Split(modelOrRateLimitID[3:], "-") - if len(parts) >= 1 { - model = parts[0] - if len(parts) > 1 { - // For multi-part model names like "gpt-4o-mini" - if !containsProjectSuffix(parts[len(parts)-1]) { - model = strings.Join(parts, "-") - } else { - model = strings.Join(parts[:len(parts)-1], "-") - } - } - } - fmt.Printf("[UPDATE-RL-DEBUG] Searching by ID: %s (extracted model: %s)\n", searchID, model) - } - - // Find the target rate limit - var targetRateLimit *RateLimit - - // 1. Try exact ID match first - for _, rl := range rateLimits.Data { - if rl.ID == searchID { - fmt.Printf("[UPDATE-RL-DEBUG] Found exact ID match: %s (Model: %s)\n", rl.ID, rl.Model) - targetRateLimit = &rl - break - } - } - - // 2. Try ID prefix match (accounts for project-specific suffixes) - if targetRateLimit == nil { - for _, rl := range rateLimits.Data { - if strings.HasPrefix(rl.ID, searchID+"-") || rl.ID == searchID { - fmt.Printf("[UPDATE-RL-DEBUG] Found ID prefix match: %s (Model: %s)\n", rl.ID, rl.Model) - targetRateLimit = &rl - break - } - } - } - - // 3. Try model name match as fallback - if targetRateLimit == nil && model != "" { - for _, rl := range rateLimits.Data { - if rl.Model == model { - fmt.Printf("[UPDATE-RL-DEBUG] Found by model name match: %s (ID: %s)\n", rl.Model, rl.ID) - targetRateLimit = &rl - break - } - } - } - - // 4. Last resort: try partial model matching for compound model names - if targetRateLimit == nil && model != "" && strings.Contains(model, "-") { - modelBase := strings.Split(model, "-")[0] - for _, rl := range rateLimits.Data { - if strings.HasPrefix(rl.Model, modelBase) { - fmt.Printf("[UPDATE-RL-DEBUG] Found by partial model name match: %s matches base %s (ID: %s)\n", - rl.Model, modelBase, rl.ID) - targetRateLimit = &rl - break - } - } - } - - if targetRateLimit == nil { - fmt.Printf("[UPDATE-RL-DEBUG] Rate limit not found for ID/model: %s\n", modelOrRateLimitID) - fmt.Printf("[UPDATE-RL-DEBUG] ========== END UpdateRateLimit DEBUG ==========\n\n") - return nil, fmt.Errorf("API error: Project with ID '%s' not found or rate limit '%s' does not exist", - projectID, modelOrRateLimitID) + return nil, fmt.Errorf("failed to find rate limit: %w", err) } - // Now we have the target rate limit, construct the URL for update using the CORRECT ID - effectiveRateLimitID := targetRateLimit.ID - fmt.Printf("[UPDATE-RL-DEBUG] Found rate limit to update: %s (Model: %s)\n", effectiveRateLimitID, targetRateLimit.Model) - - // Construct the direct path with the rate limit ID included - path := fmt.Sprintf("/organization/projects/%s/rate_limits/%s", projectID, effectiveRateLimitID) - fmt.Printf("[UPDATE-RL-DEBUG] Using path for update: %s\n", path) + // Construct the API path + path := fmt.Sprintf("/v1/organization/projects/%s/rate_limits/%s", projectID, targetRateLimit.ID) // Create the request body with only non-nil fields + // Note: API uses "max_requests_per_1_minute" format (with _1_) req := make(map[string]interface{}) - // Add the required "name" field - use the model name from the target rate limit - req["name"] = targetRateLimit.Model - fmt.Printf("[UPDATE-RL-DEBUG] Setting name: %s\n", targetRateLimit.Model) - if maxRequestsPerMinute != nil { req["max_requests_per_1_minute"] = *maxRequestsPerMinute - fmt.Printf("[UPDATE-RL-DEBUG] Setting max_requests_per_1_minute: %d\n", *maxRequestsPerMinute) } if maxTokensPerMinute != nil { req["max_tokens_per_1_minute"] = *maxTokensPerMinute - fmt.Printf("[UPDATE-RL-DEBUG] Setting max_tokens_per_1_minute: %d\n", *maxTokensPerMinute) } if maxImagesPerMinute != nil { req["max_images_per_1_minute"] = *maxImagesPerMinute - fmt.Printf("[UPDATE-RL-DEBUG] Setting max_images_per_1_minute: %d\n", *maxImagesPerMinute) } if batch1DayMaxInputTokens != nil { req["batch_1_day_max_input_tokens"] = *batch1DayMaxInputTokens - fmt.Printf("[UPDATE-RL-DEBUG] Setting batch_1_day_max_input_tokens: %d\n", *batch1DayMaxInputTokens) } if maxAudioMegabytesPer1Minute != nil { req["max_audio_megabytes_per_1_minute"] = *maxAudioMegabytesPer1Minute - fmt.Printf("[UPDATE-RL-DEBUG] Setting max_audio_megabytes_per_1_minute: %d\n", *maxAudioMegabytesPer1Minute) } if maxRequestsPer1Day != nil { req["max_requests_per_1_day"] = *maxRequestsPer1Day - fmt.Printf("[UPDATE-RL-DEBUG] Setting max_requests_per_1_day: %d\n", *maxRequestsPer1Day) - } - - // Log the request for debugging - reqJson, _ := json.Marshal(req) - fmt.Printf("[UPDATE-RL-DEBUG] Request body: %s\n", string(reqJson)) - fmt.Printf("[UPDATE-RL-DEBUG] Rate limit ID after processing: %s\n", effectiveRateLimitID) - - // Debug: Output the actual API key being used (first/last 4 chars) - apiKeyToUse := c.APIKey - maskedKey := "" - if len(apiKeyToUse) > 8 { - maskedKey = apiKeyToUse[:4] + "..." + apiKeyToUse[len(apiKeyToUse)-4:] - } else { - maskedKey = "***" } - fmt.Printf("[UPDATE-RL-DEBUG] Using API key: %s\n", maskedKey) - // Send POST request to update the rate limit - OpenAI API requires POST, not PUT for this endpoint - fmt.Printf("[UPDATE-RL-DEBUG] Using default API key for request\n") + // Send POST request to update the rate limit body, err := c.doRequest(http.MethodPost, path, req) if err != nil { - fmt.Printf("[UPDATE-RL-DEBUG] Update rate limit request failed: %v\n", err) - fmt.Printf("[UPDATE-RL-DEBUG] ========== END UpdateRateLimit DEBUG ==========\n\n") return nil, err } - // Debug: Log the response - fmt.Printf("[UPDATE-RL-DEBUG] Response from OpenAI API: %s\n", string(body)) - // Parse the response var rateLimit RateLimit if err := json.Unmarshal(body, &rateLimit); err != nil { - fmt.Printf("[UPDATE-RL-DEBUG] Failed to unmarshal rate limit response: %v\n", err) - fmt.Printf("[UPDATE-RL-DEBUG] ========== END UpdateRateLimit DEBUG ==========\n\n") return nil, fmt.Errorf("failed to unmarshal rate limit response: %v", err) } - // Set the requested values for fields that might not be included in the response - if maxRequestsPerMinute != nil { - rateLimit.MaxRequestsPer1Minute = *maxRequestsPerMinute - } - if maxTokensPerMinute != nil { - rateLimit.MaxTokensPer1Minute = *maxTokensPerMinute - } - if maxImagesPerMinute != nil { - rateLimit.MaxImagesPer1Minute = *maxImagesPerMinute - } - if batch1DayMaxInputTokens != nil { - rateLimit.Batch1DayMaxInputTokens = *batch1DayMaxInputTokens - } - if maxAudioMegabytesPer1Minute != nil { - rateLimit.MaxAudioMegabytesPer1Minute = *maxAudioMegabytesPer1Minute - } - if maxRequestsPer1Day != nil { - rateLimit.MaxRequestsPer1Day = *maxRequestsPer1Day - } - - fmt.Printf("[UPDATE-RL-DEBUG] Final rate limit object: %+v\n", rateLimit) - fmt.Printf("[UPDATE-RL-DEBUG] ========== END UpdateRateLimit DEBUG ==========\n\n") return &rateLimit, nil } -// DeleteRateLimit removes a rate limit from a project. -// This will allow the project to operate without this specific limitation. +// DeleteRateLimit resets a rate limit to default values. +// Note: OpenAI doesn't support DELETE operations on rate limits. +// This function resets the rate limit to organization default values. // // Parameters: // - projectID: The ID of the project the rate limit belongs to -// - rateLimitID: The ID of the rate limit to delete +// - modelOrRateLimitID: Either a model name or rate limit ID // // Returns: // - An error if the operation failed func (c *OpenAIClient) DeleteRateLimit(projectID, modelOrRateLimitID string) error { - // Note: OpenAI doesn't support DELETE operations on rate limits. - // Instead we "reset" them to default values. - fmt.Printf("[DELETE-RL-DEBUG] ========== DeleteRateLimit DEBUG ==========\n") - fmt.Printf("[DELETE-RL-DEBUG] ProjectID: %s\n", projectID) - fmt.Printf("[DELETE-RL-DEBUG] ModelOrRateLimitID: %s\n", modelOrRateLimitID) - - // Step 1: List all rate limits and find the one we want to reset - fmt.Printf("[DELETE-RL-DEBUG] Listing all rate limits to find target rate limit\n") - rateLimits, err := c.ListRateLimits(projectID) + // Find the rate limit to get its ID and model + targetRateLimit, err := c.GetRateLimit(projectID, modelOrRateLimitID) if err != nil { - fmt.Printf("[DELETE-RL-DEBUG] Failed to list rate limits: %v\n", err) - return fmt.Errorf("failed to list rate limits: %w", err) - } - - // Search logic similar to GetRateLimit - model := "" - searchID := modelOrRateLimitID - - if !strings.HasPrefix(modelOrRateLimitID, "rl-") { - // If it doesn't have "rl-" prefix, it's a model name - model = modelOrRateLimitID - searchID = "rl-" + modelOrRateLimitID - fmt.Printf("[DELETE-RL-DEBUG] Searching by MODEL: %s (ID would be %s)\n", model, searchID) - } else { - // It has the prefix, treat it as an ID but also extract the model - // Try to extract model name from the ID - parts := strings.Split(modelOrRateLimitID[3:], "-") - if len(parts) >= 1 { - model = parts[0] - if len(parts) > 1 { - // For multi-part model names like "gpt-4o-mini" - if !containsProjectSuffix(parts[len(parts)-1]) { - model = strings.Join(parts, "-") - } else { - model = strings.Join(parts[:len(parts)-1], "-") - } - } - } - fmt.Printf("[DELETE-RL-DEBUG] Searching by ID: %s (extracted model: %s)\n", searchID, model) - } - - // Find the target rate limit - var targetRateLimit *RateLimit - - // 1. Try exact ID match first - for _, rl := range rateLimits.Data { - if rl.ID == searchID { - fmt.Printf("[DELETE-RL-DEBUG] Found exact ID match: %s (Model: %s)\n", rl.ID, rl.Model) - targetRateLimit = &rl - break - } - } - - // 2. Try ID prefix match (accounts for project-specific suffixes) - if targetRateLimit == nil { - for _, rl := range rateLimits.Data { - if strings.HasPrefix(rl.ID, searchID+"-") || rl.ID == searchID { - fmt.Printf("[DELETE-RL-DEBUG] Found ID prefix match: %s (Model: %s)\n", rl.ID, rl.Model) - targetRateLimit = &rl - break - } - } - } - - // 3. Try model name match as fallback - if targetRateLimit == nil && model != "" { - for _, rl := range rateLimits.Data { - if rl.Model == model { - fmt.Printf("[DELETE-RL-DEBUG] Found by model name match: %s (ID: %s)\n", rl.Model, rl.ID) - targetRateLimit = &rl - break - } - } - } - - // 4. Last resort: try partial model matching for compound model names - if targetRateLimit == nil && model != "" && strings.Contains(model, "-") { - modelBase := strings.Split(model, "-")[0] - for _, rl := range rateLimits.Data { - if strings.HasPrefix(rl.Model, modelBase) { - fmt.Printf("[DELETE-RL-DEBUG] Found by partial model name match: %s matches base %s (ID: %s)\n", - rl.Model, modelBase, rl.ID) - targetRateLimit = &rl - break - } - } - } - - if targetRateLimit == nil { - fmt.Printf("[DELETE-RL-DEBUG] Rate limit not found for ID/model: %s\n", modelOrRateLimitID) - fmt.Printf("[DELETE-RL-DEBUG] ========== END DeleteRateLimit DEBUG ==========\n\n") - return fmt.Errorf("API error: Project with ID '%s' not found or rate limit '%s' does not exist", - projectID, modelOrRateLimitID) + return fmt.Errorf("failed to find rate limit: %w", err) } - // Now we have the target rate limit, construct the URL for update - effectiveRateLimitID := targetRateLimit.ID - effectiveModel := targetRateLimit.Model - fmt.Printf("[DELETE-RL-DEBUG] Found rate limit to reset: %s (Model: %s)\n", effectiveRateLimitID, effectiveModel) - - // Construct the direct path with the rate limit ID included - path := fmt.Sprintf("/organization/projects/%s/rate_limits/%s", projectID, effectiveRateLimitID) - fmt.Printf("[DELETE-RL-DEBUG] Using path for reset: %s\n", path) + // Construct the API path + path := fmt.Sprintf("/v1/organization/projects/%s/rate_limits/%s", projectID, targetRateLimit.ID) // Get default values for this model - defaultValues := getDefaultRateLimitValues(effectiveModel) - fmt.Printf("[DELETE-RL-DEBUG] Default values for model %s: %+v\n", effectiveModel, defaultValues) + defaultValues := getDefaultRateLimitValues(targetRateLimit.Model) // Create the request body with default values req := map[string]interface{}{ @@ -1793,33 +1434,9 @@ func (c *OpenAIClient) DeleteRateLimit(projectID, modelOrRateLimitID string) err req["max_requests_per_1_day"] = defaultValues.MaxRequestsPer1Day } - // Log the request for debugging - reqJson, _ := json.Marshal(req) - fmt.Printf("[DELETE-RL-DEBUG] Request body with default values: %s\n", string(reqJson)) - - // Debug: Output the actual API key being used (first/last 4 chars) - apiKeyToUse := c.APIKey - maskedKey := "" - if len(apiKeyToUse) > 8 { - maskedKey = apiKeyToUse[:4] + "..." + apiKeyToUse[len(apiKeyToUse)-4:] - } else { - maskedKey = "***" - } - fmt.Printf("[DELETE-RL-DEBUG] Using API key: %s\n", maskedKey) - // Send POST request to reset the rate limit to default values - fmt.Printf("[DELETE-RL-DEBUG] Using default API key for request\n") - body, err := c.doRequest(http.MethodPost, path, req) - if err != nil { - fmt.Printf("[DELETE-RL-DEBUG] Reset rate limit request failed: %v\n", err) - fmt.Printf("[DELETE-RL-DEBUG] ========== END DeleteRateLimit DEBUG ==========\n\n") - return err - } - - // Debug: Log the response - fmt.Printf("[DELETE-RL-DEBUG] Response from OpenAI API: %s\n", string(body)) - fmt.Printf("[DELETE-RL-DEBUG] ========== END DeleteRateLimit DEBUG ==========\n\n") - return nil + _, err = c.doRequest(http.MethodPost, path, req) + return err } // AddProjectUser adds a user to a project. @@ -2720,43 +2337,18 @@ func (c *OpenAIClient) DeleteInvite(inviteID string) error { // ListRateLimits retrieves all rate limits for a specific project. func (c *OpenAIClient) ListRateLimits(projectID string) (*RateLimitListResponse, error) { - // Use consistent URL format with other methods, avoiding the absolute URL approach - url := fmt.Sprintf("/organization/projects/%s/rate_limits", projectID) - - // Debug info about the request - fmt.Printf("\n\n[LIST-RL-DEBUG] ========== LIST RATE LIMITS DEBUG ==========\n") - fmt.Printf("[LIST-RL-DEBUG] ProjectID: %s\n", projectID) - maskedKey := "*****" - if len(c.APIKey) > 5 { - maskedKey = c.APIKey[:5] + "*****" - } - fmt.Printf("[LIST-RL-DEBUG] Using API key (masked): %s\n", maskedKey) - fmt.Printf("[LIST-RL-DEBUG] Client API URL: %s\n", c.APIURL) - fmt.Printf("[LIST-RL-DEBUG] Organization ID: %s\n", c.OrganizationID) - fmt.Printf("[LIST-RL-DEBUG] API URL: %s\n", url) + url := fmt.Sprintf("/v1/organization/projects/%s/rate_limits", projectID) respBody, err := c.doRequest("GET", url, nil) if err != nil { - fmt.Printf("[LIST-RL-DEBUG] Error listing rate limits: %v\n", err) - fmt.Printf("[LIST-RL-DEBUG] ========== END LIST RATE LIMITS DEBUG ==========\n\n") return nil, fmt.Errorf("failed to list rate limits: %w", err) } var response RateLimitListResponse if err := json.Unmarshal(respBody, &response); err != nil { - fmt.Printf("[LIST-RL-DEBUG] Failed to parse rate limits response: %v\n", err) - fmt.Printf("[LIST-RL-DEBUG] ========== END LIST RATE LIMITS DEBUG ==========\n\n") return nil, fmt.Errorf("failed to parse rate limits response: %w", err) } - // Print all rate limits for debugging - fmt.Printf("[LIST-RL-DEBUG] Successfully listed %d rate limits\n", len(response.Data)) - for i, rl := range response.Data { - fmt.Printf("[LIST-RL-DEBUG] Rate Limit #%d - ID: %s, Model: %s, MaxRequests: %d, MaxTokens: %d\n", - i+1, rl.ID, rl.Model, rl.MaxRequestsPer1Minute, rl.MaxTokensPer1Minute) - } - fmt.Printf("[LIST-RL-DEBUG] ========== END LIST RATE LIMITS DEBUG ==========\n\n") - return &response, nil } diff --git a/internal/provider/data_source_openai_organization_user.go b/internal/provider/data_source_openai_organization_user.go index b284076..6eb2fac 100644 --- a/internal/provider/data_source_openai_organization_user.go +++ b/internal/provider/data_source_openai_organization_user.go @@ -38,7 +38,7 @@ func dataSourceOpenAIOrganizationUser() *schema.Resource { Computed: true, Description: "The role of the user in the organization (owner, member, or reader)", }, - "created": { + "added_at": { Type: schema.TypeInt, Computed: true, Description: "The Unix timestamp when the user was added to the organization", @@ -114,8 +114,8 @@ func dataSourceOpenAIOrganizationUserRead(ctx context.Context, d *schema.Resourc return diag.FromErr(fmt.Errorf("error setting role: %s", err)) } - if err := d.Set("created", user.Created); err != nil { - return diag.FromErr(fmt.Errorf("error setting created: %s", err)) + if err := d.Set("added_at", user.AddedAt); err != nil { + return diag.FromErr(fmt.Errorf("error setting added_at: %s", err)) } return nil diff --git a/internal/provider/data_source_openai_organization_users.go b/internal/provider/data_source_openai_organization_users.go index aa5ca48..31e1faa 100644 --- a/internal/provider/data_source_openai_organization_users.go +++ b/internal/provider/data_source_openai_organization_users.go @@ -58,7 +58,7 @@ func dataSourceOpenAIOrganizationUsers() *schema.Resource { Computed: true, Description: "The role of the user in the organization (owner, member, or reader)", }, - "created": { + "added_at": { Type: schema.TypeInt, Computed: true, Description: "The Unix timestamp when the user was added to the organization", @@ -100,12 +100,12 @@ func dataSourceOpenAIOrganizationUsersRead(ctx context.Context, d *schema.Resour // Create a list with just this one user users := []map[string]interface{}{ { - "id": user.ID, - "object": user.Object, - "email": user.Email, - "name": user.Name, - "role": user.Role, - "created": user.Created, + "id": user.ID, + "object": user.Object, + "email": user.Email, + "name": user.Name, + "role": user.Role, + "added_at": user.AddedAt, }, } @@ -144,16 +144,14 @@ func dataSourceOpenAIOrganizationUsersRead(ctx context.Context, d *schema.Resour } // Add users from this page to the collection - for _, orgUser := range resp.Data { - // Convert to flattened user for consistent field access - user := orgUser.ToUser() + for _, user := range resp.Data { u := map[string]interface{}{ - "id": user.ID, - "object": user.Object, - "email": user.Email, - "name": user.Name, - "role": user.Role, - "created": user.Created, + "id": user.ID, + "object": user.Object, + "email": user.Email, + "name": user.Name, + "role": user.Role, + "added_at": user.AddedAt, } allUsers = append(allUsers, u) } diff --git a/internal/provider/data_source_openai_project.go b/internal/provider/data_source_openai_project.go index 21bfa61..64ad61d 100644 --- a/internal/provider/data_source_openai_project.go +++ b/internal/provider/data_source_openai_project.go @@ -14,13 +14,11 @@ import ( // ProjectResponse represents the API response for an OpenAI project type ProjectResponse struct { - ID string `json:"id"` - Object string `json:"object"` - Title string `json:"title"` - Created int64 `json:"created"` - Status string `json:"status"` - ArchivedAt *int64 `json:"archived_at"` - IsInitial bool `json:"is_initial"` + ID string `json:"id"` + Object string `json:"object"` + Name string `json:"name"` + CreatedAt int `json:"created_at"` + Status string `json:"status"` } // dataSourceOpenAIProject returns a schema.Resource that represents a data source for an OpenAI project. @@ -40,31 +38,21 @@ func dataSourceOpenAIProject() *schema.Resource { Sensitive: true, Description: "Admin API key for authentication. If not provided, the provider's default API key will be used.", }, - "title": { + "name": { Type: schema.TypeString, Computed: true, - Description: "The title of the project", + Description: "The name of the project", }, "status": { Type: schema.TypeString, Computed: true, Description: "The status of the project", }, - "created": { + "created_at": { Type: schema.TypeInt, Computed: true, Description: "Timestamp when the project was created", }, - "archived_at": { - Type: schema.TypeInt, - Computed: true, - Description: "Timestamp when the project was archived (null if not archived)", - }, - "is_initial": { - Type: schema.TypeBool, - Computed: true, - Description: "Whether this is the initial project", - }, }, } } @@ -146,21 +134,13 @@ func dataSourceOpenAIProjectRead(ctx context.Context, d *schema.ResourceData, m } // Set the project details in the schema - if err := d.Set("title", project.Title); err != nil { + if err := d.Set("name", project.Name); err != nil { return diag.FromErr(err) } if err := d.Set("status", project.Status); err != nil { return diag.FromErr(err) } - if err := d.Set("created", project.Created); err != nil { - return diag.FromErr(err) - } - if project.ArchivedAt != nil { - if err := d.Set("archived_at", *project.ArchivedAt); err != nil { - return diag.FromErr(err) - } - } - if err := d.Set("is_initial", project.IsInitial); err != nil { + if err := d.Set("created_at", project.CreatedAt); err != nil { return diag.FromErr(err) } diff --git a/internal/provider/data_source_openai_projects.go b/internal/provider/data_source_openai_projects.go index 119e57b..919111e 100644 --- a/internal/provider/data_source_openai_projects.go +++ b/internal/provider/data_source_openai_projects.go @@ -45,31 +45,21 @@ func dataSourceOpenAIProjects() *schema.Resource { Computed: true, Description: "The ID of the project", }, - "title": { + "name": { Type: schema.TypeString, Computed: true, - Description: "The title of the project", + Description: "The name of the project", }, "status": { Type: schema.TypeString, Computed: true, Description: "The status of the project", }, - "created": { + "created_at": { Type: schema.TypeInt, Computed: true, Description: "Timestamp when the project was created", }, - "archived_at": { - Type: schema.TypeInt, - Computed: true, - Description: "Timestamp when the project was archived", - }, - "is_initial": { - Type: schema.TypeBool, - Computed: true, - Description: "Whether this is the initial project", - }, }, }, Description: "List of available projects", @@ -206,13 +196,9 @@ func dataSourceOpenAIProjectsRead(ctx context.Context, d *schema.ResourceData, m for _, project := range allProjects { projectMap := map[string]interface{}{ "id": project.ID, - "title": project.Title, + "name": project.Name, "status": project.Status, - "created": project.Created, - "is_initial": project.IsInitial, - } - if project.ArchivedAt != nil { - projectMap["archived_at"] = *project.ArchivedAt + "created_at": project.CreatedAt, } projects = append(projects, projectMap) } diff --git a/internal/provider/resource_openai_organization_user.go b/internal/provider/resource_openai_organization_user.go index 5f2c501..203acb4 100644 --- a/internal/provider/resource_openai_organization_user.go +++ b/internal/provider/resource_openai_organization_user.go @@ -46,7 +46,7 @@ func resourceOpenAIOrganizationUser() *schema.Resource { Computed: true, Description: "The name of the user", }, - "created": { + "added_at": { Type: schema.TypeInt, Computed: true, Description: "The Unix timestamp when the user was added to the organization", @@ -98,8 +98,8 @@ func resourceOpenAIOrganizationUserCreate(ctx context.Context, d *schema.Resourc if err := d.Set("name", user.Name); err != nil { return diag.FromErr(fmt.Errorf("error setting name: %s", err)) } - if err := d.Set("created", user.Created); err != nil { - return diag.FromErr(fmt.Errorf("error setting created: %s", err)) + if err := d.Set("added_at", user.AddedAt); err != nil { + return diag.FromErr(fmt.Errorf("error setting added_at: %s", err)) } return nil @@ -141,8 +141,8 @@ func resourceOpenAIOrganizationUserRead(ctx context.Context, d *schema.ResourceD if err := d.Set("role", user.Role); err != nil { return diag.FromErr(fmt.Errorf("error setting role: %s", err)) } - if err := d.Set("created", user.Created); err != nil { - return diag.FromErr(fmt.Errorf("error setting created: %s", err)) + if err := d.Set("added_at", user.AddedAt); err != nil { + return diag.FromErr(fmt.Errorf("error setting added_at: %s", err)) } return nil @@ -173,8 +173,8 @@ func resourceOpenAIOrganizationUserUpdate(ctx context.Context, d *schema.Resourc if err := d.Set("name", user.Name); err != nil { return diag.FromErr(fmt.Errorf("error setting name: %s", err)) } - if err := d.Set("created", user.Created); err != nil { - return diag.FromErr(fmt.Errorf("error setting created: %s", err)) + if err := d.Set("added_at", user.AddedAt); err != nil { + return diag.FromErr(fmt.Errorf("error setting added_at: %s", err)) } } diff --git a/internal/provider/resource_openai_project.go b/internal/provider/resource_openai_project.go index ccd2874..267dd7c 100644 --- a/internal/provider/resource_openai_project.go +++ b/internal/provider/resource_openai_project.go @@ -34,13 +34,13 @@ func resourceOpenAIProject() *schema.Resource { Delete: schema.DefaultTimeout(20 * time.Minute), }, Schema: map[string]*schema.Schema{ - "title": { + "name": { Type: schema.TypeString, Required: true, - Description: "The title of the project", + Description: "The name of the project", }, - "created": { - Type: schema.TypeInt, + "created_at": { + Type: schema.TypeString, Computed: true, Description: "Timestamp when the project was created", }, @@ -50,15 +50,10 @@ func resourceOpenAIProject() *schema.Resource { Description: "Status of the project (active, archived, etc.)", }, "archived_at": { - Type: schema.TypeInt, + Type: schema.TypeString, Computed: true, Description: "Timestamp when the project was archived, if applicable", }, - "is_initial": { - Type: schema.TypeBool, - Computed: true, - Description: "Whether this is the initial project", - }, }, } return resource @@ -72,12 +67,12 @@ func resourceOpenAIProjectCreate(ctx context.Context, d *schema.ResourceData, me return diag.FromErr(err) } - title := d.Get("title").(string) + name := d.Get("name").(string) - log.Printf("[DEBUG] Creating OpenAI project with title: %s", title) + log.Printf("[DEBUG] Creating OpenAI project with name: %s", name) // Create the project using the OpenAI API - project, err := c.CreateProject(title) + project, err := c.CreateProject(name) if err != nil { return diag.Errorf("error creating project: %s", err) } @@ -105,32 +100,35 @@ func resourceOpenAIProjectRead(ctx context.Context, d *schema.ResourceData, meta return diag.Errorf("error reading project: %s", err) } - log.Printf("[DEBUG] Successfully retrieved project from API: %s (status: %s)", project.Title, project.Status) + log.Printf("[DEBUG] Successfully retrieved project from API: %s (status: %s)", project.Name, project.Status) // Set basic fields - if err := d.Set("title", project.Title); err != nil { - return diag.Errorf("error setting title: %s", err) + if err := d.Set("name", project.Name); err != nil { + return diag.Errorf("error setting name: %s", err) } - if err := d.Set("status", project.Status); err != nil { - return diag.Errorf("error setting status: %s", err) + if project.Status != "" { + if err := d.Set("status", project.Status); err != nil { + return diag.Errorf("error setting status: %s", err) + } + log.Printf("[DEBUG] Set status to: %s", project.Status) } - log.Printf("[DEBUG] Set status to: %s", project.Status) - if err := d.Set("created", project.Created); err != nil { - return diag.Errorf("error setting created: %s", err) + // Handle Unix timestamps for created_at and archived_at + if project.CreatedAt != nil { + createdTime := time.Unix(int64(*project.CreatedAt), 0) + if err := d.Set("created_at", createdTime.Format(time.RFC3339)); err != nil { + return diag.Errorf("error setting created_at: %s", err) + } + log.Printf("[DEBUG] Set created_at to: %s", createdTime.Format(time.RFC3339)) } - log.Printf("[DEBUG] Set created to: %d", project.Created) if project.ArchivedAt != nil { - if err := d.Set("archived_at", *project.ArchivedAt); err != nil { + archivedTime := time.Unix(int64(*project.ArchivedAt), 0) + if err := d.Set("archived_at", archivedTime.Format(time.RFC3339)); err != nil { return diag.Errorf("error setting archived_at: %s", err) } - log.Printf("[DEBUG] Set archived_at to: %d", *project.ArchivedAt) - } - - if err := d.Set("is_initial", project.IsInitial); err != nil { - return diag.Errorf("error setting is_initial: %s", err) + log.Printf("[DEBUG] Set archived_at to: %s", archivedTime.Format(time.RFC3339)) } log.Printf("[DEBUG] OpenAI project read complete for ID: %s", projectID) @@ -145,12 +143,13 @@ func resourceOpenAIProjectUpdate(ctx context.Context, d *schema.ResourceData, me return diag.FromErr(err) } - title := d.Get("title").(string) + name := d.Get("name").(string) - log.Printf("[DEBUG] Updating OpenAI project with ID: %s, title: %s", d.Id(), title) + log.Printf("[DEBUG] Updating OpenAI project with ID: %s, name: %s", d.Id(), name) // Update the project using the OpenAI API - _, err = c.UpdateProject(d.Id(), title) + // Note: The API uses POST for updates, not PATCH + _, err = c.UpdateProject(d.Id(), name) if err != nil { return diag.Errorf("error updating project: %s", err) } @@ -196,32 +195,34 @@ func resourceOpenAIProjectImport(ctx context.Context, d *schema.ResourceData, me return nil, fmt.Errorf("error reading project during import: %s", err) } - log.Printf("[DEBUG] Successfully retrieved project from API: %s (status: %s)", project.Title, project.Status) + log.Printf("[DEBUG] Successfully retrieved project from API: %s (status: %s)", project.Name, project.Status) // Set all fields - if err := d.Set("title", project.Title); err != nil { - return nil, fmt.Errorf("error setting title: %s", err) + if err := d.Set("name", project.Name); err != nil { + return nil, fmt.Errorf("error setting name: %s", err) } - if err := d.Set("created", project.Created); err != nil { - return nil, fmt.Errorf("error setting created: %s", err) + if project.CreatedAt != nil { + createdTime := time.Unix(int64(*project.CreatedAt), 0) + if err := d.Set("created_at", createdTime.Format(time.RFC3339)); err != nil { + return nil, fmt.Errorf("error setting created_at: %s", err) + } + log.Printf("[DEBUG] Set created_at to: %s", createdTime.Format(time.RFC3339)) } - log.Printf("[DEBUG] Set created to: %d", project.Created) - if err := d.Set("status", project.Status); err != nil { - return nil, fmt.Errorf("error setting status: %s", err) + if project.Status != "" { + if err := d.Set("status", project.Status); err != nil { + return nil, fmt.Errorf("error setting status: %s", err) + } + log.Printf("[DEBUG] Set status to: %s", project.Status) } - log.Printf("[DEBUG] Set status to: %s", project.Status) if project.ArchivedAt != nil { - if err := d.Set("archived_at", *project.ArchivedAt); err != nil { + archivedTime := time.Unix(int64(*project.ArchivedAt), 0) + if err := d.Set("archived_at", archivedTime.Format(time.RFC3339)); err != nil { return nil, fmt.Errorf("error setting archived_at: %s", err) } - log.Printf("[DEBUG] Set archived_at to: %d", *project.ArchivedAt) - } - - if err := d.Set("is_initial", project.IsInitial); err != nil { - return nil, fmt.Errorf("error setting is_initial: %s", err) + log.Printf("[DEBUG] Set archived_at to: %s", archivedTime.Format(time.RFC3339)) } log.Printf("[DEBUG] OpenAI project import complete for ID: %s", projectID) diff --git a/internal/provider/resource_openai_project_test.go b/internal/provider/resource_openai_project_test.go index 1d9a75e..f4141f6 100644 --- a/internal/provider/resource_openai_project_test.go +++ b/internal/provider/resource_openai_project_test.go @@ -13,7 +13,6 @@ func TestAccResourceOpenAIProject_basic(t *testing.T) { var projectID string projectName := "tf-acc-test-project" - projectDesc := "Terraform acceptance test project" resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -21,13 +20,11 @@ func TestAccResourceOpenAIProject_basic(t *testing.T) { CheckDestroy: testAccCheckOpenAIProjectDestroy, Steps: []resource.TestStep{ { - Config: testAccResourceOpenAIProjectBasic(projectName, projectDesc), + Config: testAccResourceOpenAIProjectBasic(projectName), Check: resource.ComposeTestCheckFunc( testAccCheckOpenAIProjectExists("openai_project.test", &projectID), resource.TestCheckResourceAttr("openai_project.test", "name", projectName), - resource.TestCheckResourceAttr("openai_project.test", "description", projectDesc), resource.TestCheckResourceAttrSet("openai_project.test", "created_at"), - resource.TestCheckResourceAttrSet("openai_project.test", "updated_at"), ), }, { @@ -44,9 +41,7 @@ func TestAccResourceOpenAIProject_update(t *testing.T) { var projectID string projectName := "tf-acc-test-project" - projectDesc := "Terraform acceptance test project" projectNameUpdated := "tf-acc-test-project-updated" - projectDescUpdated := "Terraform acceptance test project updated" resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -54,45 +49,17 @@ func TestAccResourceOpenAIProject_update(t *testing.T) { CheckDestroy: testAccCheckOpenAIProjectDestroy, Steps: []resource.TestStep{ { - Config: testAccResourceOpenAIProjectBasic(projectName, projectDesc), + Config: testAccResourceOpenAIProjectBasic(projectName), Check: resource.ComposeTestCheckFunc( testAccCheckOpenAIProjectExists("openai_project.test", &projectID), resource.TestCheckResourceAttr("openai_project.test", "name", projectName), - resource.TestCheckResourceAttr("openai_project.test", "description", projectDesc), ), }, { - Config: testAccResourceOpenAIProjectBasic(projectNameUpdated, projectDescUpdated), + Config: testAccResourceOpenAIProjectBasic(projectNameUpdated), Check: resource.ComposeTestCheckFunc( testAccCheckOpenAIProjectExists("openai_project.test", &projectID), resource.TestCheckResourceAttr("openai_project.test", "name", projectNameUpdated), - resource.TestCheckResourceAttr("openai_project.test", "description", projectDescUpdated), - ), - }, - }, - }) -} - -func TestAccResourceOpenAIProject_withUsageLimits(t *testing.T) { - t.Skip("Skipping until properly implemented and OpenAI API credentials are configured for tests") - - var projectID string - projectName := "tf-acc-test-project-limits" - projectDesc := "Terraform acceptance test project with usage limits" - - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviderFactories, - CheckDestroy: testAccCheckOpenAIProjectDestroy, - Steps: []resource.TestStep{ - { - Config: testAccResourceOpenAIProjectWithUsageLimits(projectName, projectDesc, 100.0, 1000000), - Check: resource.ComposeTestCheckFunc( - testAccCheckOpenAIProjectExists("openai_project.test", &projectID), - resource.TestCheckResourceAttr("openai_project.test", "name", projectName), - resource.TestCheckResourceAttr("openai_project.test", "description", projectDesc), - resource.TestCheckResourceAttr("openai_project.test", "usage_limits.0.max_budget", "100"), - resource.TestCheckResourceAttr("openai_project.test", "usage_limits.0.max_tokens", "1000000"), ), }, }, @@ -141,25 +108,10 @@ func testAccCheckOpenAIProjectDestroy(s *terraform.State) error { return nil } -func testAccResourceOpenAIProjectBasic(name, description string) string { - return fmt.Sprintf(` -resource "openai_project" "test" { - name = "%s" - description = "%s" -} -`, name, description) -} - -func testAccResourceOpenAIProjectWithUsageLimits(name, description string, maxBudget float64, maxTokens int) string { +func testAccResourceOpenAIProjectBasic(name string) string { return fmt.Sprintf(` resource "openai_project" "test" { - name = "%s" - description = "%s" - - usage_limits { - max_budget = %f - max_tokens = %d - } + name = "%s" } -`, name, description, maxBudget, maxTokens) +`, name) } diff --git a/internal/provider/resource_openai_rate_limit.go b/internal/provider/resource_openai_rate_limit.go index eb951c3..a6769bb 100644 --- a/internal/provider/resource_openai_rate_limit.go +++ b/internal/provider/resource_openai_rate_limit.go @@ -74,12 +74,6 @@ func resourceOpenAIRateLimit() *schema.Resource { Computed: true, Description: "The ID of the rate limit.", }, - "ignore_rate_limit_warning": { - Type: schema.TypeBool, - Optional: true, - Default: false, - Description: "Set to true to acknowledge that OpenAI rate limits cannot be truly deleted and will be reset to defaults on removal.", - }, }, } } @@ -280,165 +274,68 @@ func resourceOpenAIRateLimitRead(ctx context.Context, d *schema.ResourceData, me return diag.FromErr(fmt.Errorf("error getting OpenAI client: %w", err)) } - // The ID in Terraform may have a suffix, but we save it to maintain consistency rateLimitID := d.Id() projectID := d.Get("project_id").(string) model := d.Get("model").(string) - // Enhanced debug logging - fmt.Printf("\n\n[TF-READ-DEBUG] ========== TERRAFORM READ RATE LIMIT DEBUG ==========\n") - fmt.Printf("[TF-READ-DEBUG] Resource ID from Terraform state (d.Id()): %s\n", rateLimitID) - fmt.Printf("[TF-READ-DEBUG] Project ID: %s\n", projectID) - fmt.Printf("[TF-READ-DEBUG] Model from config: %s\n", model) - if rateLimitID == "" { d.SetId("") - fmt.Printf("[TF-READ-DEBUG] Rate limit ID is empty, clearing resource from state\n") - fmt.Printf("[TF-READ-DEBUG] ========== END TERRAFORM READ RATE LIMIT DEBUG ==========\n\n") return diag.Diagnostics{} } - // Use the provider's API key - fmt.Printf("[TF-READ-DEBUG] Using default API key from provider configuration\n") - - // IMPORTANT: First try with just the model name, as the OpenAI API actually uses model names - // not the IDs with project suffixes that we generate for Terraform state management - fmt.Printf("[TF-READ-DEBUG] First trying GetRateLimitWithKey with model name: %s\n", model) + // Try to get rate limit by model name first, then by ID rateLimit, err := c.GetRateLimit(projectID, model) - - // If we can't find it with the model name, try with the full ID as fallback if err != nil { - fmt.Printf("[TF-READ-DEBUG] Failed to get rate limit with model name: %v\n", err) - fmt.Printf("[TF-READ-DEBUG] Now trying with full rate limit ID: %s\n", rateLimitID) - rateLimit, err = c.GetRateLimit(projectID, rateLimitID) if err != nil { - // Log the complete error for debugging - fmt.Printf("[TF-READ-DEBUG] Also failed with full ID: %v\n", err) - fmt.Printf("[TF-READ-DEBUG] Error type: %T\n", err) - // Handle various error cases if responseHasStatusCode(err, 404) || strings.Contains(err.Error(), "not found") { - fmt.Printf("[TF-READ-DEBUG] Resource not found, removing from Terraform state\n") d.SetId("") - fmt.Printf("[TF-READ-DEBUG] ========== END TERRAFORM READ RATE LIMIT DEBUG ==========\n\n") return nil } else if strings.Contains(err.Error(), "No project found") { - fmt.Printf("[TF-READ-DEBUG] Project not found, removing from Terraform state: %s\n", projectID) d.SetId("") - fmt.Printf("[TF-READ-DEBUG] ========== END TERRAFORM READ RATE LIMIT DEBUG ==========\n\n") return nil } else if strings.Contains(err.Error(), "insufficient permissions") || strings.Contains(err.Error(), "do not have permission") { - // Always create a warning, never an error for permission issues - warning := diag.Diagnostic{ - Severity: diag.Warning, - Summary: "Permission error reading rate limit", - Detail: fmt.Sprintf("Permission error: %s. The resource will remain in the Terraform state, but the actual values might differ from what's shown.", err), + tflog.Warn(ctx, fmt.Sprintf("Permission error when reading rate limit: %v", err)) + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Warning, + Summary: "Permission error reading rate limit", + Detail: fmt.Sprintf("Permission error: %s. The resource will remain in the Terraform state, but the actual values might differ from what's shown.", err), + }, } - - // Just log the warning and continue - tflog.Warn(ctx, fmt.Sprintf("Permission error when reading rate limit: %v. Continuing with Terraform operation.", err)) - fmt.Printf("[TF-READ-DEBUG] Permission error, returning warning\n") - fmt.Printf("[TF-READ-DEBUG] ========== END TERRAFORM READ RATE LIMIT DEBUG ==========\n\n") - - // Always return only the warning, never an error for permission issues - return diag.Diagnostics{warning} - } else { - // For any other error, return it to Terraform so it can handle drift properly - fmt.Printf("[TF-READ-DEBUG] Unhandled error, returning to Terraform\n") - fmt.Printf("[TF-READ-DEBUG] ========== END TERRAFORM READ RATE LIMIT DEBUG ==========\n\n") - return diag.Errorf("Error reading rate limit from OpenAI API: %s", err) } + return diag.Errorf("Error reading rate limit from OpenAI API: %s", err) } } - // If we got here, we successfully found the rate limit - fmt.Printf("[TF-READ-DEBUG] Successfully read rate limit: ID=%s, Model=%s, MaxReq=%d, MaxTokens=%d\n", - rateLimit.ID, rateLimit.Model, rateLimit.MaxRequestsPer1Minute, rateLimit.MaxTokensPer1Minute) - + // Set all rate limit values in state if err := d.Set("model", rateLimit.Model); err != nil { - fmt.Printf("[TF-READ-DEBUG] Error setting model: %v\n", err) - fmt.Printf("[TF-READ-DEBUG] ========== END TERRAFORM READ RATE LIMIT DEBUG ==========\n\n") return diag.FromErr(err) } if err := d.Set("max_requests_per_minute", rateLimit.MaxRequestsPer1Minute); err != nil { - fmt.Printf("[TF-READ-DEBUG] Error setting max_requests_per_minute: %v\n", err) - fmt.Printf("[TF-READ-DEBUG] ========== END TERRAFORM READ RATE LIMIT DEBUG ==========\n\n") return diag.FromErr(err) } if err := d.Set("max_tokens_per_minute", rateLimit.MaxTokensPer1Minute); err != nil { - fmt.Printf("[TF-READ-DEBUG] Error setting max_tokens_per_minute: %v\n", err) - fmt.Printf("[TF-READ-DEBUG] ========== END TERRAFORM READ RATE LIMIT DEBUG ==========\n\n") return diag.FromErr(err) } if err := d.Set("max_images_per_minute", rateLimit.MaxImagesPer1Minute); err != nil { - fmt.Printf("[TF-READ-DEBUG] Error setting max_images_per_minute: %v\n", err) - fmt.Printf("[TF-READ-DEBUG] ========== END TERRAFORM READ RATE LIMIT DEBUG ==========\n\n") return diag.FromErr(err) } - - // For fields that may not be returned by the API for certain models, - // only update the state if the API returned a non-zero value - - // If batch_1_day_max_input_tokens is 0 in the API response but non-zero in our state, - // keep the existing state value - oldBatch1DayMaxInputTokens := d.Get("batch_1_day_max_input_tokens").(int) - if rateLimit.Batch1DayMaxInputTokens > 0 || oldBatch1DayMaxInputTokens == 0 { - if err := d.Set("batch_1_day_max_input_tokens", rateLimit.Batch1DayMaxInputTokens); err != nil { - fmt.Printf("[TF-READ-DEBUG] Error setting batch_1_day_max_input_tokens: %v\n", err) - fmt.Printf("[TF-READ-DEBUG] ========== END TERRAFORM READ RATE LIMIT DEBUG ==========\n\n") - return diag.FromErr(err) - } + if err := d.Set("batch_1_day_max_input_tokens", rateLimit.Batch1DayMaxInputTokens); err != nil { + return diag.FromErr(err) } - - // If max_audio_megabytes_per_1_minute is 0 in the API response but non-zero in our state, - // keep the existing state value - oldMaxAudioMegabytesPer1Minute := d.Get("max_audio_megabytes_per_1_minute").(int) - if rateLimit.MaxAudioMegabytesPer1Minute > 0 || oldMaxAudioMegabytesPer1Minute == 0 { - if err := d.Set("max_audio_megabytes_per_1_minute", rateLimit.MaxAudioMegabytesPer1Minute); err != nil { - fmt.Printf("[TF-READ-DEBUG] Error setting max_audio_megabytes_per_1_minute: %v\n", err) - fmt.Printf("[TF-READ-DEBUG] ========== END TERRAFORM READ RATE LIMIT DEBUG ==========\n\n") - return diag.FromErr(err) - } + if err := d.Set("max_audio_megabytes_per_1_minute", rateLimit.MaxAudioMegabytesPer1Minute); err != nil { + return diag.FromErr(err) } - - // If max_requests_per_1_day is 0 in the API response but non-zero in our state, - // keep the existing state value - oldMaxRequestsPer1Day := d.Get("max_requests_per_1_day").(int) - if rateLimit.MaxRequestsPer1Day > 0 || oldMaxRequestsPer1Day == 0 { - if err := d.Set("max_requests_per_1_day", rateLimit.MaxRequestsPer1Day); err != nil { - fmt.Printf("[TF-READ-DEBUG] Error setting max_requests_per_1_day: %v\n", err) - fmt.Printf("[TF-READ-DEBUG] ========== END TERRAFORM READ RATE LIMIT DEBUG ==========\n\n") - return diag.FromErr(err) - } - } else { - fmt.Printf("[TF-READ-DEBUG] Preserving max_requests_per_1_day value %d in state (API returned 0)\n", - oldMaxRequestsPer1Day) + if err := d.Set("max_requests_per_1_day", rateLimit.MaxRequestsPer1Day); err != nil { + return diag.FromErr(err) } - if err := d.Set("rate_limit_id", rateLimit.ID); err != nil { - fmt.Printf("[TF-READ-DEBUG] Error setting rate_limit_id: %v\n", err) - fmt.Printf("[TF-READ-DEBUG] ========== END TERRAFORM READ RATE LIMIT DEBUG ==========\n\n") - return diag.FromErr(fmt.Errorf("failed to set rate_limit_id: %v", err)) + return diag.FromErr(err) } - // Keep the current resource ID d.SetId(rateLimit.ID) - fmt.Printf("[TF-READ-DEBUG] Set resource ID to: %s\n", rateLimit.ID) - - // Log the values for debugging - fmt.Printf("[TF-READ-DEBUG] Rate limit values being set in Terraform state:\n") - fmt.Printf(" - max_requests_per_minute: %d\n", rateLimit.MaxRequestsPer1Minute) - fmt.Printf(" - max_tokens_per_minute: %d\n", rateLimit.MaxTokensPer1Minute) - fmt.Printf(" - max_images_per_minute: %d\n", rateLimit.MaxImagesPer1Minute) - fmt.Printf(" - batch_1_day_max_input_tokens: %d (preserved: %t)\n", - rateLimit.Batch1DayMaxInputTokens, rateLimit.Batch1DayMaxInputTokens == 0 && oldBatch1DayMaxInputTokens > 0) - fmt.Printf(" - max_audio_megabytes_per_1_minute: %d (preserved: %t)\n", - rateLimit.MaxAudioMegabytesPer1Minute, rateLimit.MaxAudioMegabytesPer1Minute == 0 && oldMaxAudioMegabytesPer1Minute > 0) - fmt.Printf(" - max_requests_per_1_day: %d (preserved: %t)\n", - rateLimit.MaxRequestsPer1Day, rateLimit.MaxRequestsPer1Day == 0 && oldMaxRequestsPer1Day > 0) - fmt.Printf("[TF-READ-DEBUG] ========== END TERRAFORM READ RATE LIMIT DEBUG ==========\n\n") - return diag.Diagnostics{} } @@ -711,117 +608,27 @@ func getAPIKeyType(apiKey string) string { // Since OpenAI API doesn't support true deletion of rate limits, this function resets the rate limit // to the default values based on the comprehensive model defaults we have compiled. func resourceOpenAIRateLimitDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - tflog.Info(ctx, "START: Deleting OpenAI rate limit resource") - fmt.Printf("\n\n[TF-DELETE-DEBUG] ========== TERRAFORM DELETE RATE LIMIT DEBUG ==========\n") - fmt.Printf("[TF-DELETE-DEBUG] Resource ID: %s\n", d.Id()) - - // Get the client from the provider configuration - // Fix: Use GetOpenAIClientWithAdminKey helper function for admin operations - client, err := GetOpenAIClientWithAdminKey(meta) + c, err := GetOpenAIClientWithAdminKey(meta) if err != nil { return diag.FromErr(fmt.Errorf("error getting OpenAI client: %w", err)) } - fmt.Printf("[TF-DELETE-DEBUG] Client API URL: %s\n", client.APIURL) - var diags diag.Diagnostics - - // Extract the model and project ID from the resource state model := d.Get("model").(string) projectID := d.Get("project_id").(string) - fmt.Printf("[TF-DELETE-DEBUG] Model from state: %s\n", model) - fmt.Printf("[TF-DELETE-DEBUG] Project ID from state: %s\n", projectID) - - // Get the rate limit ID directly from the resource ID - resourceID := d.Id() - fmt.Printf("[TF-DELETE-DEBUG] Resource ID (full): %s\n", resourceID) - - // Use the provider's API key - fmt.Printf("[TF-DELETE-DEBUG] Using provider's API key\n") - - // Try to get the existing rate limit before deleting - fmt.Printf("[TF-DELETE-DEBUG] Checking if rate limit exists before deletion\n") - existingRateLimit, getRLErr := client.GetRateLimit(projectID, model) - if getRLErr != nil { - fmt.Printf("[TF-DELETE-DEBUG] Error getting existing rate limit: %v\n", getRLErr) - // Try with the full ID as fallback - existingRateLimit, getRLErr = client.GetRateLimit(projectID, resourceID) - if getRLErr != nil { - fmt.Printf("[TF-DELETE-DEBUG] Also failed with full ID: %v\n", getRLErr) - } - } - - if existingRateLimit != nil { - fmt.Printf("[TF-DELETE-DEBUG] Found existing rate limit: ID=%s, Model=%s, MaxReq=%d, MaxTokens=%d\n", - existingRateLimit.ID, existingRateLimit.Model, existingRateLimit.MaxRequestsPer1Minute, existingRateLimit.MaxTokensPer1Minute) - } else { - fmt.Printf("[TF-DELETE-DEBUG] Rate limit not found or nil response\n") - } - // Attempt to delete the rate limit - Use the model name since that's what the API expects - fmt.Printf("[TF-DELETE-DEBUG] Calling DeleteRateLimitWithKey with: projectID=%s, model=%s\n", - projectID, model) - - err = client.DeleteRateLimit(projectID, model) + // Delete (reset to defaults) the rate limit + err = c.DeleteRateLimit(projectID, model) if err != nil { - fmt.Printf("[TF-DELETE-DEBUG] Error deleting rate limit: %v\n", err) - fmt.Printf("[TF-DELETE-DEBUG] Error type: %T\n", err) - fmt.Printf("[TF-DELETE-DEBUG] Error contains 'permission': %v\n", strings.Contains(err.Error(), "permission")) - fmt.Printf("[TF-DELETE-DEBUG] Error contains '403': %v\n", strings.Contains(err.Error(), "403")) - - // Try with the full ID as fallback - fmt.Printf("[TF-DELETE-DEBUG] Trying again with full resource ID: %s\n", resourceID) - err = client.DeleteRateLimit(projectID, resourceID) - if err != nil { - fmt.Printf("[TF-DELETE-DEBUG] Also failed with full ID: %v\n", err) - - if strings.Contains(err.Error(), "permission") || strings.Contains(err.Error(), "403") { - // Special handling for permission errors - fmt.Printf("[TF-DELETE-DEBUG] Got permission/403 error, checking if we should handle differently\n") - - // Check if the error is specifically a permission error for deletion - if strings.Contains(err.Error(), "permission to delete") { - // Get the rate limit again to see if it still exists with original values - fmt.Printf("[TF-DELETE-DEBUG] Checking if rate limit still exists after deletion attempt\n") - rateLimit, getRLErr := client.GetRateLimit(projectID, model) - if getRLErr == nil && rateLimit != nil { - // The rate limit still exists, but we can consider it "deleted" from Terraform's perspective - fmt.Printf("[TF-DELETE-DEBUG] Rate limit still exists but marking as deleted in Terraform state\n") - fmt.Printf("[TF-DELETE-DEBUG] ========== END TERRAFORM DELETE RATE LIMIT DEBUG ==========\n\n") - return diags - } - } - } - - // Convert the error to diagnostics - diags = append(diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: "Error deleting OpenAI rate limit", - Detail: fmt.Sprintf("Error deleting OpenAI rate limit: %s", err), - }) - fmt.Printf("[TF-DELETE-DEBUG] Added error diagnostic\n") - fmt.Printf("[TF-DELETE-DEBUG] ========== END TERRAFORM DELETE RATE LIMIT DEBUG ==========\n\n") - return diags + // Handle permission errors gracefully + if strings.Contains(err.Error(), "permission") || strings.Contains(err.Error(), "403") { + tflog.Warn(ctx, fmt.Sprintf("Permission error deleting rate limit: %v", err)) + // Still remove from state as we can't manage it + return nil } + return diag.FromErr(fmt.Errorf("error deleting rate limit: %w", err)) } - fmt.Printf("[TF-DELETE-DEBUG] Rate limit deletion successful\n") - - // Verify if rate limit was actually deleted/reset by checking the API again - fmt.Printf("[TF-DELETE-DEBUG] Verifying rate limit state after deletion\n") - verifyRateLimit, verifyErr := client.GetRateLimit(projectID, model) - if verifyErr != nil { - fmt.Printf("[TF-DELETE-DEBUG] Error verifying rate limit state: %v\n", verifyErr) - } else if verifyRateLimit != nil { - fmt.Printf("[TF-DELETE-DEBUG] Rate limit still exists after deletion (expected, since it's reset to defaults)\n") - fmt.Printf("[TF-DELETE-DEBUG] Updated rate limit details: ID=%s, Model=%s, MaxReq=%d, MaxTokens=%d\n", - verifyRateLimit.ID, verifyRateLimit.Model, verifyRateLimit.MaxRequestsPer1Minute, verifyRateLimit.MaxTokensPer1Minute) - } else { - fmt.Printf("[TF-DELETE-DEBUG] Rate limit not found after deletion (unexpected)\n") - } - - fmt.Printf("[TF-DELETE-DEBUG] ========== END TERRAFORM DELETE RATE LIMIT DEBUG ==========\n\n") - tflog.Info(ctx, "END: Deleted OpenAI rate limit resource") - return diags + return nil } // Helper function to get min of two integers diff --git a/modules/projects/README.md b/modules/projects/README.md index db10f7a..8c57667 100644 --- a/modules/projects/README.md +++ b/modules/projects/README.md @@ -85,10 +85,8 @@ output "project_ids" { | `list_mode` | Whether to retrieve all projects instead of working with a single project | `bool` | `false` | no | | `name` | Name of the project | `string` | `null` | yes, when create_project is true | | `project_id` | ID of the project to use in data source mode | `string` | `null` | yes, when create_project is false and list_mode is false | -| `is_default` | Whether this project should be the default project | `bool` | `false` | no | | `openai_admin_key` | OpenAI Admin API key | `string` | `null` | no | -| `rate_limits` | Rate limits for the project | `list(object)` | `[]` | no | -| `users` | Users to add to the project | `list(object)` | `[]` | no | +| `organization_id` | OpenAI Organization ID (org-xxxx) | `string` | `""` | no | ## Outputs @@ -100,7 +98,6 @@ output "project_ids" { | `project_name` | The name of the project | | `project_status` | The status of the project | | `project_created_at` | When the project was created | -| `project_usage_limits` | Usage limits for the project | ### List Mode (list_mode = true) diff --git a/modules/projects/main.tf b/modules/projects/main.tf index 9671810..a1d8e81 100644 --- a/modules/projects/main.tf +++ b/modules/projects/main.tf @@ -10,9 +10,8 @@ terraform { # Create an OpenAI project resource "openai_project" "project" { - provider = openai - name = var.name - is_default = var.is_default + provider = openai + name = var.name } # Outputs from the actual project resource @@ -31,7 +30,3 @@ output "created_at" { value = openai_project.project.created_at } -output "is_default" { - description = "Whether the OpenAI project is the default project" - value = openai_project.project.is_default -} diff --git a/modules/projects/outputs.tf b/modules/projects/outputs.tf index ca90466..ad890af 100644 --- a/modules/projects/outputs.tf +++ b/modules/projects/outputs.tf @@ -20,10 +20,6 @@ output "project_created_at" { value = var.list_mode ? null : (var.create_project ? one(openai_project.project[*].created_at) : one(data.openai_project.project[*].created_at)) } -output "project_usage_limits" { - description = "Usage limits for the project (only in single project mode)" - value = var.list_mode ? null : (var.create_project ? null : one(data.openai_project.project[*].usage_limits)) -} # List mode outputs output "projects" { diff --git a/modules/projects/variables.tf b/modules/projects/variables.tf index 4adc1c8..16d2cac 100644 --- a/modules/projects/variables.tf +++ b/modules/projects/variables.tf @@ -41,29 +41,3 @@ variable "organization_id" { default = "" } -variable "rate_limits" { - description = "Rate limits for the project" - type = list(object({ - model = string - max_requests_per_minute = optional(number) - max_tokens_per_minute = optional(number) - max_images_per_minute = optional(number) - batch_1_day_max_input_tokens = optional(number) - })) - default = [] -} - -variable "users" { - description = "Users to add to the project" - type = list(object({ - user_id = string - role = string - })) - default = [] -} - -variable "is_default" { - description = "Whether this project should be the default project" - type = bool - default = false -}