Skip to content

Commit d47b8e7

Browse files
committed
feat: project rate limit resource
1 parent c8824de commit d47b8e7

11 files changed

+428
-14
lines changed

docs/resources/project_rate_limit.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
# generated by https://github.com/hashicorp/terraform-plugin-docs
3+
page_title: "openai_project_rate_limit Resource - terraform-provider-openai"
4+
subcategory: ""
5+
description: |-
6+
Manage rate limits per model for projects. Rate limits may be configured to be equal to or lower than the organization's rate limits.
7+
---
8+
9+
# openai_project_rate_limit (Resource)
10+
11+
Manage rate limits per model for projects. Rate limits may be configured to be equal to or lower than the organization's rate limits.
12+
13+
## Example Usage
14+
15+
```terraform
16+
resource "openai_project_rate_limit" "example" {
17+
project_id = "proj_000000000000000000000000"
18+
model = "o1-preview"
19+
20+
max_requests_per_1_minute = 2
21+
max_tokens_per_1_minute = 75000
22+
}
23+
```
24+
25+
<!-- schema generated by tfplugindocs -->
26+
## Schema
27+
28+
### Required
29+
30+
- `model` (String) The model to set rate limits for.
31+
- `project_id` (String) The ID of the project.
32+
33+
### Optional
34+
35+
- `batch_1_day_max_input_tokens` (Number) The maximum batch input tokens per day. Only present for relevant models.
36+
- `max_audio_megabytes_per_1_minute` (Number) The maximum audio megabytes per minute. Only present for relevant models.
37+
- `max_images_per_1_minute` (Number) The maximum images per minute. Only present for relevant models.
38+
- `max_requests_per_1_day` (Number) The maximum requests per day. Only present for relevant models.
39+
- `max_requests_per_1_minute` (Number) The maximum requests per minute.
40+
- `max_tokens_per_1_minute` (Number) The maximum tokens per minute.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
resource "openai_project_rate_limit" "example" {
2+
project_id = "proj_000000000000000000000000"
3+
model = "o1-preview"
4+
5+
max_requests_per_1_minute = 2
6+
max_tokens_per_1_minute = 75000
7+
}

internal/provider/data_source_project.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ func (d *ProjectDataSource) Read(ctx context.Context, req datasource.ReadRequest
7676
}
7777

7878
if err := data.Fill(*httpResp.JSON200); err != nil {
79-
resp.Diagnostics.AddError("API Error", fmt.Sprintf("Unable to unmarshal response: %s", err))
79+
resp.Diagnostics.AddError("API Error", fmt.Sprintf("Unable to fill data: %s", err))
8080
return
8181
}
8282

internal/provider/data_source_projects.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ func (d *ProjectsDataSource) Read(ctx context.Context, req datasource.ReadReques
122122
}
123123

124124
if err := data.Fill(projects); err != nil {
125-
resp.Diagnostics.AddError("API Error", fmt.Sprintf("Unable to unmarshal response: %s", err))
125+
resp.Diagnostics.AddError("API Error", fmt.Sprintf("Unable to fill data: %s", err))
126126
return
127127
}
128128

internal/provider/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ func (p *OpenAIProvider) Configure(ctx context.Context, req provider.ConfigureRe
110110
func (p *OpenAIProvider) Resources(ctx context.Context) []func() resource.Resource {
111111
return []func() resource.Resource{
112112
NewInviteResource,
113+
NewProjectRateLimitResource,
113114
NewProjectResource,
114115
NewProjectServiceAccountResource,
115116
NewProjectUserResource,

internal/provider/resource_project.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ func (r *ProjectResource) Create(ctx context.Context, req resource.CreateRequest
8787
}
8888

8989
if err := data.Fill(*httpResp.JSON201); err != nil {
90-
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to unmarshal response: %s", err))
90+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to fill data: %s", err))
9191
return
9292
}
9393

@@ -119,7 +119,7 @@ func (r *ProjectResource) Read(ctx context.Context, req resource.ReadRequest, re
119119
}
120120

121121
if err := data.Fill(*httpResp.JSON200); err != nil {
122-
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to unmarshal response: %s", err))
122+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to fill data: %s", err))
123123
return
124124
}
125125

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
package provider
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
8+
"github.com/hashicorp/terraform-plugin-framework/resource"
9+
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
10+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
11+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
12+
"github.com/hashicorp/terraform-plugin-framework/types"
13+
"github.com/jianyuan/terraform-provider-openai/internal/apiclient"
14+
"github.com/jianyuan/terraform-provider-openai/internal/ptr"
15+
)
16+
17+
type ProjectRateLimitResourceModel struct {
18+
ProjectId types.String `tfsdk:"project_id"`
19+
Model types.String `tfsdk:"model"`
20+
MaxRequestsPer1Minute types.Int64 `tfsdk:"max_requests_per_1_minute"`
21+
MaxTokensPer1Minute types.Int64 `tfsdk:"max_tokens_per_1_minute"`
22+
MaxImagesPer1Minute types.Int64 `tfsdk:"max_images_per_1_minute"`
23+
MaxAudioMegabytesPer1Minute types.Int64 `tfsdk:"max_audio_megabytes_per_1_minute"`
24+
MaxRequestsPer1Day types.Int64 `tfsdk:"max_requests_per_1_day"`
25+
Batch1DayMaxInputTokens types.Int64 `tfsdk:"batch_1_day_max_input_tokens"`
26+
}
27+
28+
func (m *ProjectRateLimitResourceModel) Fill(rl apiclient.ProjectRateLimit) error {
29+
m.Model = types.StringValue(rl.Model)
30+
m.MaxRequestsPer1Minute = types.Int64Value(int64(rl.MaxRequestsPer1Minute))
31+
m.MaxTokensPer1Minute = types.Int64Value(int64(rl.MaxTokensPer1Minute))
32+
if !m.MaxImagesPer1Minute.IsNull() {
33+
if rl.MaxImagesPer1Minute == nil {
34+
m.MaxImagesPer1Minute = types.Int64Null()
35+
} else {
36+
m.MaxImagesPer1Minute = types.Int64Value(int64(*rl.MaxImagesPer1Minute))
37+
}
38+
}
39+
if !m.MaxAudioMegabytesPer1Minute.IsNull() {
40+
if rl.MaxAudioMegabytesPer1Minute == nil {
41+
m.MaxAudioMegabytesPer1Minute = types.Int64Null()
42+
} else {
43+
m.MaxAudioMegabytesPer1Minute = types.Int64Value(int64(*rl.MaxAudioMegabytesPer1Minute))
44+
}
45+
}
46+
if !m.MaxRequestsPer1Day.IsNull() {
47+
if rl.MaxRequestsPer1Day == nil {
48+
m.MaxRequestsPer1Day = types.Int64Null()
49+
} else {
50+
m.MaxRequestsPer1Day = types.Int64Value(int64(*rl.MaxRequestsPer1Day))
51+
}
52+
}
53+
if !m.Batch1DayMaxInputTokens.IsNull() {
54+
if rl.Batch1DayMaxInputTokens == nil {
55+
m.Batch1DayMaxInputTokens = types.Int64Null()
56+
} else {
57+
m.Batch1DayMaxInputTokens = types.Int64Value(int64(*rl.Batch1DayMaxInputTokens))
58+
}
59+
}
60+
return nil
61+
}
62+
63+
var _ resource.Resource = &ProjectRateLimitResource{}
64+
65+
func NewProjectRateLimitResource() resource.Resource {
66+
return &ProjectRateLimitResource{}
67+
}
68+
69+
type ProjectRateLimitResource struct {
70+
baseResource
71+
}
72+
73+
func (r *ProjectRateLimitResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
74+
resp.TypeName = req.ProviderTypeName + "_project_rate_limit"
75+
}
76+
77+
func (r *ProjectRateLimitResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
78+
resp.Schema = schema.Schema{
79+
MarkdownDescription: "Manage rate limits per model for projects. Rate limits may be configured to be equal to or lower than the organization's rate limits.",
80+
81+
Attributes: map[string]schema.Attribute{
82+
"project_id": schema.StringAttribute{
83+
MarkdownDescription: "The ID of the project.",
84+
Required: true,
85+
PlanModifiers: []planmodifier.String{
86+
stringplanmodifier.RequiresReplace(),
87+
},
88+
},
89+
"model": schema.StringAttribute{
90+
MarkdownDescription: "The model to set rate limits for.",
91+
Required: true,
92+
PlanModifiers: []planmodifier.String{
93+
stringplanmodifier.RequiresReplace(),
94+
},
95+
},
96+
"max_requests_per_1_minute": schema.Int64Attribute{
97+
MarkdownDescription: "The maximum requests per minute.",
98+
Optional: true,
99+
},
100+
"max_tokens_per_1_minute": schema.Int64Attribute{
101+
MarkdownDescription: "The maximum tokens per minute.",
102+
Optional: true,
103+
},
104+
"max_images_per_1_minute": schema.Int64Attribute{
105+
MarkdownDescription: "The maximum images per minute. Only present for relevant models.",
106+
Optional: true,
107+
},
108+
"max_audio_megabytes_per_1_minute": schema.Int64Attribute{
109+
MarkdownDescription: "The maximum audio megabytes per minute. Only present for relevant models.",
110+
Optional: true,
111+
},
112+
"max_requests_per_1_day": schema.Int64Attribute{
113+
MarkdownDescription: "The maximum requests per day. Only present for relevant models.",
114+
Optional: true,
115+
},
116+
"batch_1_day_max_input_tokens": schema.Int64Attribute{
117+
MarkdownDescription: "The maximum batch input tokens per day. Only present for relevant models.",
118+
Optional: true,
119+
},
120+
},
121+
}
122+
}
123+
124+
func (r *ProjectRateLimitResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
125+
var data ProjectRateLimitResourceModel
126+
127+
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
128+
129+
if resp.Diagnostics.HasError() {
130+
return
131+
}
132+
133+
body := apiclient.ProjectRateLimitUpdateRequest{}
134+
if !data.MaxRequestsPer1Minute.IsNull() {
135+
body.MaxRequestsPer1Minute = ptr.Ptr(int(data.MaxRequestsPer1Minute.ValueInt64()))
136+
}
137+
if !data.MaxTokensPer1Minute.IsNull() {
138+
body.MaxTokensPer1Minute = ptr.Ptr(int(data.MaxTokensPer1Minute.ValueInt64()))
139+
}
140+
if !data.MaxImagesPer1Minute.IsNull() {
141+
body.MaxImagesPer1Minute = ptr.Ptr(int(data.MaxImagesPer1Minute.ValueInt64()))
142+
}
143+
if !data.MaxAudioMegabytesPer1Minute.IsNull() {
144+
body.MaxAudioMegabytesPer1Minute = ptr.Ptr(int(data.MaxAudioMegabytesPer1Minute.ValueInt64()))
145+
}
146+
if !data.MaxRequestsPer1Day.IsNull() {
147+
body.MaxRequestsPer1Day = ptr.Ptr(int(data.MaxRequestsPer1Day.ValueInt64()))
148+
}
149+
if !data.Batch1DayMaxInputTokens.IsNull() {
150+
body.Batch1DayMaxInputTokens = ptr.Ptr(int(data.Batch1DayMaxInputTokens.ValueInt64()))
151+
}
152+
153+
httpResp, err := r.client.UpdateProjectRateLimitsWithResponse(
154+
ctx,
155+
data.ProjectId.ValueString(),
156+
fmt.Sprintf("rl-%s", data.Model.ValueString()),
157+
body,
158+
)
159+
160+
if err != nil {
161+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create, got error: %s", err))
162+
return
163+
}
164+
165+
if httpResp.StatusCode() != http.StatusOK {
166+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create, got status code %d: %s", httpResp.StatusCode(), string(httpResp.Body)))
167+
return
168+
}
169+
170+
if httpResp.JSON200 == nil {
171+
resp.Diagnostics.AddError("Client Error", "Unable to create, got empty response")
172+
return
173+
}
174+
175+
if err := data.Fill(*httpResp.JSON200); err != nil {
176+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to fill data: %s", err))
177+
return
178+
}
179+
180+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
181+
}
182+
183+
func (r *ProjectRateLimitResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
184+
var data ProjectRateLimitResourceModel
185+
186+
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
187+
188+
if resp.Diagnostics.HasError() {
189+
return
190+
}
191+
192+
params := &apiclient.ListProjectRateLimitsParams{
193+
Limit: ptr.Ptr(100),
194+
}
195+
196+
out:
197+
for {
198+
httpResp, err := r.client.ListProjectRateLimitsWithResponse(
199+
ctx,
200+
data.ProjectId.ValueString(),
201+
params,
202+
)
203+
204+
if err != nil {
205+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read, got error: %s", err))
206+
return
207+
}
208+
209+
if httpResp.StatusCode() != http.StatusOK {
210+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read, got status code %d: %s", httpResp.StatusCode(), string(httpResp.Body)))
211+
return
212+
}
213+
214+
if httpResp.JSON200 == nil {
215+
resp.Diagnostics.AddError("Client Error", "Unable to read, got empty response")
216+
return
217+
}
218+
219+
for _, rl := range httpResp.JSON200.Data {
220+
if rl.Model != data.Model.ValueString() {
221+
continue
222+
}
223+
224+
if err := data.Fill(rl); err != nil {
225+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to fill data: %s", err))
226+
return
227+
}
228+
229+
break out
230+
}
231+
}
232+
233+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
234+
}
235+
236+
func (r *ProjectRateLimitResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
237+
var data ProjectRateLimitResourceModel
238+
239+
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
240+
241+
if resp.Diagnostics.HasError() {
242+
return
243+
}
244+
245+
body := apiclient.ProjectRateLimitUpdateRequest{}
246+
if !data.MaxRequestsPer1Minute.IsNull() {
247+
body.MaxRequestsPer1Minute = ptr.Ptr(int(data.MaxRequestsPer1Minute.ValueInt64()))
248+
}
249+
if !data.MaxTokensPer1Minute.IsNull() {
250+
body.MaxTokensPer1Minute = ptr.Ptr(int(data.MaxTokensPer1Minute.ValueInt64()))
251+
}
252+
if !data.MaxImagesPer1Minute.IsNull() {
253+
body.MaxImagesPer1Minute = ptr.Ptr(int(data.MaxImagesPer1Minute.ValueInt64()))
254+
}
255+
if !data.MaxAudioMegabytesPer1Minute.IsNull() {
256+
body.MaxAudioMegabytesPer1Minute = ptr.Ptr(int(data.MaxAudioMegabytesPer1Minute.ValueInt64()))
257+
}
258+
if !data.MaxRequestsPer1Day.IsNull() {
259+
body.MaxRequestsPer1Day = ptr.Ptr(int(data.MaxRequestsPer1Day.ValueInt64()))
260+
}
261+
if !data.Batch1DayMaxInputTokens.IsNull() {
262+
body.Batch1DayMaxInputTokens = ptr.Ptr(int(data.Batch1DayMaxInputTokens.ValueInt64()))
263+
}
264+
265+
httpResp, err := r.client.UpdateProjectRateLimitsWithResponse(
266+
ctx,
267+
data.ProjectId.ValueString(),
268+
fmt.Sprintf("rl-%s", data.Model.ValueString()),
269+
body,
270+
)
271+
272+
if err != nil {
273+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update, got error: %s", err))
274+
return
275+
}
276+
277+
if httpResp.StatusCode() != http.StatusOK {
278+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update, got status code %d: %s", httpResp.StatusCode(), string(httpResp.Body)))
279+
return
280+
}
281+
282+
if httpResp.JSON200 == nil {
283+
resp.Diagnostics.AddError("Client Error", "Unable to update, got empty response")
284+
return
285+
}
286+
287+
if err := data.Fill(*httpResp.JSON200); err != nil {
288+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to fill data: %s", err))
289+
return
290+
}
291+
292+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
293+
}
294+
295+
func (r *ProjectRateLimitResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
296+
resp.Diagnostics.AddWarning("Delete not supported", "This resource does not support deletion.")
297+
}

0 commit comments

Comments
 (0)