From 0900bbeccecb8b64df55ee47c576622d72313427 Mon Sep 17 00:00:00 2001 From: zenyr Date: Sun, 4 Oct 2026 22:04:37 +0900 Subject: [PATCH 1/3] feat(quota): register as a native quota provider on v7 and v8 hosts Advertise the quota_provider capability and serve quota.identifier, quota.describe, and quota.fetch so CLIProxyAPI v7.2.159+ (including v8) reports OpenCode Go credentials as quota-capable and serves their rolling, weekly, and monthly windows through /v0/management/quota/fetch. The quota RPC method names and wire shapes are declared locally, so the SDK stays on v7.2.138 and the plugin keeps reporting schema version 3. Older hosts ignore the unknown capability and keep loading the plugin. The separate quota page stays for hosts and Management Center builds without generic plugin quota rendering. Co-Authored-By: Claude Opus 5.5 (1M context) --- README.md | 1 + internal/plugin/native_quota.go | 113 +++++++++++++++++++++++++ internal/plugin/native_quota_test.go | 118 +++++++++++++++++++++++++++ internal/plugin/plugin.go | 4 + 4 files changed, 236 insertions(+) create mode 100644 internal/plugin/native_quota.go create mode 100644 internal/plugin/native_quota_test.go diff --git a/README.md b/README.md index b62a393..a4fb787 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ This plugin exposes OpenCode Go as a single provider (`opencode-go`) backed by a - **Dynamic Catalog Discovery**: Fetches remote model catalogs with local fallback and custom route overrides. - **Multi-Key Auth Scheduling**: Pools multiple API keys with CLIProxyAPI's native scheduler for rotation, retries, and error cooldowns across all protocols. - **OpenCode Go Quota Page**: Management Center includes a separate `OpenCode Go Quota` page. Page load lists credentials without contacting OpenCode; each card is refreshed manually and independently, and quota values do not affect routing or CPA's native quota page. +- **Native Quota Provider**: On CLIProxyAPI `v7.2.159+` (including v8), the plugin also registers as a quota provider, so each OpenCode Go credential reports `supports_quota` in `/v0/management/auth-files` and its rolling, weekly, and monthly windows are served by `POST /v0/management/quota/fetch`. Quota reset is unsupported. Older hosts ignore the capability and keep using the separate page. ## Requirements diff --git a/internal/plugin/native_quota.go b/internal/plugin/native_quota.go new file mode 100644 index 0000000..c9eb20e --- /dev/null +++ b/internal/plugin/native_quota.go @@ -0,0 +1,113 @@ +package plugin + +import ( + "context" + "encoding/json" + "fmt" + "strings" +) + +// Native quota RPC (CLIProxyAPI v7.2.159+), declared locally so the plugin +// keeps its v7.2.138 schema version and still loads on older hosts, which +// ignore the quota_provider capability. +const ( + methodQuotaIdentifier = "quota.identifier" + methodQuotaDescribe = "quota.describe" + methodQuotaFetch = "quota.fetch" + methodQuotaReset = "quota.reset" +) + +const quotaDisplayName = "OpenCode Go" + +type nativeQuotaFetchRequest struct { + Provider string `json:"provider"` + Attributes map[string]string `json:"attributes,omitempty"` +} + +type nativeQuotaDescribeResponse struct { + SupportedProviders []string `json:"supported_providers,omitempty"` + DisplayName string `json:"display_name,omitempty"` + SupportsReset bool `json:"supports_reset,omitempty"` +} + +type nativeQuotaSubscription struct { + Plan string `json:"plan,omitempty"` +} + +type nativeQuotaBucket struct { + Window string `json:"window,omitempty"` + RemainingFraction float64 `json:"remainingFraction"` + ResetTime string `json:"resetTime,omitempty"` +} + +type nativeQuotaGroup struct { + DisplayName string `json:"displayName,omitempty"` + Buckets []nativeQuotaBucket `json:"buckets,omitempty"` +} + +type nativeQuotaFetchResponse struct { + Subscription *nativeQuotaSubscription `json:"subscription,omitempty"` + Groups []nativeQuotaGroup `json:"groups,omitempty"` +} + +func (m *Manager) handleNativeQuota(method string, request []byte) []byte { + switch method { + case methodQuotaIdentifier: + return okEnvelope(map[string]string{"identifier": ProviderID}) + case methodQuotaDescribe: + return okEnvelope(nativeQuotaDescribeResponse{SupportedProviders: []string{ProviderID}, DisplayName: quotaDisplayName}) + case methodQuotaFetch: + var req nativeQuotaFetchRequest + if json.Unmarshal(request, &req) != nil { + return ErrEnvelope("invalid_request", "malformed quota fetch request body") + } + resp, err := m.fetchNativeQuota(context.Background(), req) + if err != nil { + return ErrEnvelope("quota_failure", err.Error()) + } + return okEnvelope(resp) + default: + return ErrEnvelope("unsupported", "OpenCode Go does not support quota resets") + } +} + +// fetchNativeQuota uses the credential the host selected, never a config key. +func (m *Manager) fetchNativeQuota(ctx context.Context, req nativeQuotaFetchRequest) (nativeQuotaFetchResponse, error) { + if req.Provider != ProviderID { + return nativeQuotaFetchResponse{}, fmt.Errorf("unsupported quota provider") + } + key := strings.TrimSpace(req.Attributes["api_key"]) + if key == "" { + return nativeQuotaFetchResponse{}, fmt.Errorf("quota credential has no api key") + } + m.mu.RLock() + baseURL, timeout := m.cfg.BaseURL, m.cfg.RequestTimeout + m.mu.RUnlock() + usage, err := fetchQuota(ctx, m.bridge, baseURL, timeout, key) + if err != nil { + return nativeQuotaFetchResponse{}, err + } + return nativeQuotaFromUsage(usage) +} + +func nativeQuotaFromUsage(usage quotaUsage) (nativeQuotaFetchResponse, error) { + windows := []struct { + name string + window quotaWindow + }{{"rolling", usage.Rolling}, {"weekly", usage.Weekly}, {"monthly", usage.Monthly}} + buckets := make([]nativeQuotaBucket, 0, len(windows)) + for _, w := range windows { + if w.window.Status == "" && w.window.ResetsAt == "" { + continue + } + percent := min(max(w.window.Percent, 0), 100) + buckets = append(buckets, nativeQuotaBucket{Window: w.name, RemainingFraction: float64(100-percent) / 100, ResetTime: w.window.ResetsAt}) + } + if len(buckets) == 0 { + return nativeQuotaFetchResponse{}, fmt.Errorf("quota response has no readings") + } + return nativeQuotaFetchResponse{ + Subscription: &nativeQuotaSubscription{Plan: "Go"}, + Groups: []nativeQuotaGroup{{DisplayName: quotaDisplayName, Buckets: buckets}}, + }, nil +} diff --git a/internal/plugin/native_quota_test.go b/internal/plugin/native_quota_test.go new file mode 100644 index 0000000..52c59c0 --- /dev/null +++ b/internal/plugin/native_quota_test.go @@ -0,0 +1,118 @@ +package plugin + +import ( + "encoding/json" + "net/http" + "strings" + "testing" + + "github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginabi" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi" + + "opencode-go-cliproxyapi/internal/config" +) + +func TestNativeQuotaRegistrationKeepsLegacySchema(t *testing.T) { + var registration struct { + SchemaVersion uint32 `json:"schema_version"` + Capabilities map[string]any `json:"capabilities"` + } + decodeResult(t, mustHandle(t, NewManager(nil), pluginabi.MethodPluginRegister, lifecycleRequestBody(testValidYAML)), ®istration) + if registration.SchemaVersion != pluginabi.SchemaVersion { + t.Fatalf("schema_version = %d, want %d", registration.SchemaVersion, pluginabi.SchemaVersion) + } + if registration.Capabilities["quota_provider"] != true || registration.Capabilities["management_api"] != true { + t.Fatalf("capabilities = %+v", registration.Capabilities) + } +} + +func TestNativeQuotaIdentifierAndDescribe(t *testing.T) { + m := NewManager(nil) + var id struct{ Identifier string } + decodeResult(t, mustHandle(t, m, methodQuotaIdentifier, []byte(`{}`)), &id) + if id.Identifier != ProviderID { + t.Fatalf("identifier = %q", id.Identifier) + } + var desc nativeQuotaDescribeResponse + decodeResult(t, mustHandle(t, m, methodQuotaDescribe, []byte(`{}`)), &desc) + if len(desc.SupportedProviders) != 1 || desc.SupportedProviders[0] != ProviderID || desc.SupportsReset { + t.Fatalf("describe = %+v", desc) + } +} + +func TestNativeQuotaFetchUsesSelectedCredential(t *testing.T) { + const selected = "native-selected-secret" + f := &fakeCaller{responder: func(method string, payload []byte) ([]byte, error) { + if method != pluginabi.MethodHostHTTPDo { + return hostOK(map[string]any{}), nil + } + var wire struct { + URL string `json:"url"` + Headers http.Header `json:"headers"` + } + if err := json.Unmarshal(payload, &wire); err != nil || wire.URL != "https://quota.test/v1/usage" || wire.Headers.Get("Authorization") != "Bearer "+selected { + t.Fatalf("bad quota request: %s", payload) + } + return hostOK(pluginapi.HTTPResponse{StatusCode: http.StatusOK, Body: []byte(`{"usage":{"rolling":{"status":"ok","percent":0,"resetsAt":"2026-10-04T15:26:49.000Z"},"weekly":{"status":"ok","percent":4,"resetsAt":"2026-10-05T00:00:00.000Z"},"monthly":{"status":"ok","percent":41,"resetsAt":"2026-10-26T10:20:31.000Z"}}}`)}), nil + }} + m := NewManager(NewHostBridge(f.call)) + m.cfg = config.Config{BaseURL: "https://quota.test/v1", RequestTimeout: config.DefaultRequestTimeout, APIKeys: []config.APIKey{{Value: "config-default-key"}}} + var got nativeQuotaFetchResponse + decodeResult(t, mustHandle(t, m, methodQuotaFetch, []byte(`{"auth_index":"1","provider":"opencode-go","attributes":{"api_key":"`+selected+`"}}`)), &got) + if got.Subscription == nil || got.Subscription.Plan != "Go" || len(got.Groups) != 1 { + t.Fatalf("response = %+v", got) + } + want := []nativeQuotaBucket{ + {Window: "rolling", RemainingFraction: 1, ResetTime: "2026-10-04T15:26:49.000Z"}, + {Window: "weekly", RemainingFraction: 0.96, ResetTime: "2026-10-05T00:00:00.000Z"}, + {Window: "monthly", RemainingFraction: 0.59, ResetTime: "2026-10-26T10:20:31.000Z"}, + } + buckets := got.Groups[0].Buckets + if len(buckets) != len(want) { + t.Fatalf("buckets = %+v", buckets) + } + for i := range want { + if buckets[i] != want[i] { + t.Fatalf("bucket %d = %+v, want %+v", i, buckets[i], want[i]) + } + } +} + +func TestNativeQuotaSkipsMissingWindowsAndClampsPercent(t *testing.T) { + got, err := nativeQuotaFromUsage(quotaUsage{Weekly: quotaWindow{Status: "ok", Percent: 130}}) + if err != nil || len(got.Groups[0].Buckets) != 1 || got.Groups[0].Buckets[0] != (nativeQuotaBucket{Window: "weekly", RemainingFraction: 0}) { + t.Fatalf("response = %+v err=%v", got, err) + } + if _, err := nativeQuotaFromUsage(quotaUsage{}); err == nil { + t.Fatal("empty usage did not fail") + } +} + +func TestNativeQuotaFailuresAreRedacted(t *testing.T) { + const key = "native-error-secret" + f := &fakeCaller{responder: func(string, []byte) ([]byte, error) { + return hostOK(pluginapi.HTTPResponse{StatusCode: http.StatusUnauthorized, Body: []byte(key + " upstream body")}), nil + }} + m := NewManager(NewHostBridge(f.call)) + m.cfg = config.Config{BaseURL: "https://quota.test/v1", RequestTimeout: config.DefaultRequestTimeout} + for name, body := range map[string]string{ + "upstream": `{"provider":"opencode-go","attributes":{"api_key":"` + key + `"}}`, + "provider": `{"provider":"codex","attributes":{"api_key":"` + key + `"}}`, + "no key": `{"provider":"opencode-go"}`, + "json": `{`, + } { + t.Run(name, func(t *testing.T) { + resp := string(mustHandle(t, m, methodQuotaFetch, []byte(body))) + if !strings.Contains(resp, `"ok":false`) || strings.Contains(resp, key) || strings.Contains(resp, "upstream body") { + t.Fatalf("response = %s", resp) + } + }) + } +} + +func TestNativeQuotaResetIsUnsupported(t *testing.T) { + resp := string(mustHandle(t, NewManager(nil), methodQuotaReset, []byte(`{}`))) + if !strings.Contains(resp, `"ok":false`) || !strings.Contains(resp, "unsupported") { + t.Fatalf("response = %s", resp) + } +} diff --git a/internal/plugin/plugin.go b/internal/plugin/plugin.go index 057605c..43598cc 100644 --- a/internal/plugin/plugin.go +++ b/internal/plugin/plugin.go @@ -128,6 +128,8 @@ func (m *Manager) HandleCall(method string, request []byte) (resp []byte, err er return m.registerManagement(request) case pluginabi.MethodManagementHandle: return m.handleManagement(request) + case methodQuotaIdentifier, methodQuotaDescribe, methodQuotaFetch, methodQuotaReset: + return m.handleNativeQuota(method, request), nil case pluginabi.MethodExecutorIdentifier: return okEnvelope(map[string]string{"identifier": ProviderID}), nil case pluginabi.MethodExecutorCountTokens: @@ -160,6 +162,7 @@ type capabilities struct { ExecutorInputFormats []string `json:"executor_input_formats,omitempty"` ExecutorOutputFormats []string `json:"executor_output_formats,omitempty"` ManagementAPI bool `json:"management_api"` + QuotaProvider bool `json:"quota_provider"` } type registrationResult struct { @@ -242,6 +245,7 @@ func registrationEnvelope() []byte { ExecutorInputFormats: formats, ExecutorOutputFormats: formats, ManagementAPI: true, + QuotaProvider: true, }, }) } From 8add15638745bca0de7b02ec0b74f9cc9b44e0b1 Mon Sep 17 00:00:00 2001 From: zenyr Date: Sun, 4 Oct 2026 22:12:39 +0900 Subject: [PATCH 2/3] fix(quota): bound quota lookups and pin the legacy schema in tests Cap quota upstream lookups at 30 seconds for both the native provider and the separate page, since the default request timeout is minutes long. Assert schema version 3 literally and check the fetch wire keys, so an SDK bump that breaks older hosts fails the tests. Add the native quota provider to the release notes. Co-Authored-By: Claude Opus 5.5 (1M context) --- RELEASE_NOTES.md | 4 ++++ internal/plugin/native_quota_test.go | 29 +++++++++++++++++++++++++--- internal/plugin/quota.go | 12 +++++++++++- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index fd0d512..24bb764 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -2,6 +2,10 @@ Full OpenAI Codex CLI support across all models: multi-agent namespaces, custom programmatic tools (`exec`), tool call results, and reasoning efforts are fully functional. +### Features + +- Register as a native CLIProxyAPI quota provider: on hosts `v7.2.159+` (including v8), OpenCode Go credentials report `supports_quota` in `/v0/management/auth-files` and serve rolling, weekly, and monthly windows through `POST /v0/management/quota/fetch`. The plugin keeps schema version 3, so older v7 hosts still load it and keep using the separate `OpenCode Go Quota` page. Quota lookups are now capped at 30 seconds. + ### Bug Fixes - Fix missing Responses stream item lifecycle completion events (output_text.done, content_part.done, function_call_arguments.done, output_item.done) prior to response.completed, resolving dropped assistant output and tool calls in OpenAI Codex CLI and strict Responses clients. diff --git a/internal/plugin/native_quota_test.go b/internal/plugin/native_quota_test.go index 52c59c0..33bdea9 100644 --- a/internal/plugin/native_quota_test.go +++ b/internal/plugin/native_quota_test.go @@ -5,6 +5,7 @@ import ( "net/http" "strings" "testing" + "time" "github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginabi" "github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi" @@ -18,8 +19,9 @@ func TestNativeQuotaRegistrationKeepsLegacySchema(t *testing.T) { Capabilities map[string]any `json:"capabilities"` } decodeResult(t, mustHandle(t, NewManager(nil), pluginabi.MethodPluginRegister, lifecycleRequestBody(testValidYAML)), ®istration) - if registration.SchemaVersion != pluginabi.SchemaVersion { - t.Fatalf("schema_version = %d, want %d", registration.SchemaVersion, pluginabi.SchemaVersion) + // Hosts before v7.2.159 reject plugins whose schema is newer than 3. + if registration.SchemaVersion != 3 { + t.Fatalf("schema_version = %d, want 3", registration.SchemaVersion) } if registration.Capabilities["quota_provider"] != true || registration.Capabilities["management_api"] != true { t.Fatalf("capabilities = %+v", registration.Capabilities) @@ -57,8 +59,15 @@ func TestNativeQuotaFetchUsesSelectedCredential(t *testing.T) { }} m := NewManager(NewHostBridge(f.call)) m.cfg = config.Config{BaseURL: "https://quota.test/v1", RequestTimeout: config.DefaultRequestTimeout, APIKeys: []config.APIKey{{Value: "config-default-key"}}} + raw := mustHandle(t, m, methodQuotaFetch, []byte(`{"auth_index":"1","provider":"opencode-go","attributes":{"api_key":"`+selected+`"},"host_callback_id":"cb-1"}`)) + var wire map[string]any + decodeResult(t, raw, &wire) + bucket := wire["groups"].([]any)[0].(map[string]any)["buckets"].([]any)[1].(map[string]any) + if wire["subscription"].(map[string]any)["plan"] != "Go" || bucket["window"] != "weekly" || bucket["remainingFraction"] != 0.96 || bucket["resetTime"] != "2026-10-05T00:00:00.000Z" { + t.Fatalf("wire response = %+v", wire) + } var got nativeQuotaFetchResponse - decodeResult(t, mustHandle(t, m, methodQuotaFetch, []byte(`{"auth_index":"1","provider":"opencode-go","attributes":{"api_key":"`+selected+`"}}`)), &got) + decodeResult(t, raw, &got) if got.Subscription == nil || got.Subscription.Plan != "Go" || len(got.Groups) != 1 { t.Fatalf("response = %+v", got) } @@ -110,6 +119,20 @@ func TestNativeQuotaFailuresAreRedacted(t *testing.T) { } } +func TestQuotaTimeoutIsBounded(t *testing.T) { + for in, want := range map[time.Duration]time.Duration{ + 0: maxQuotaTimeout, + config.DefaultRequestTimeout: maxQuotaTimeout, + 5 * time.Second: 5 * time.Second, + maxQuotaTimeout: maxQuotaTimeout, + -time.Second: maxQuotaTimeout, + } { + if got := quotaTimeout(in); got != want { + t.Fatalf("quotaTimeout(%s) = %s, want %s", in, got, want) + } + } +} + func TestNativeQuotaResetIsUnsupported(t *testing.T) { resp := string(mustHandle(t, NewManager(nil), methodQuotaReset, []byte(`{}`))) if !strings.Contains(resp, `"ok":false`) || !strings.Contains(resp, "unsupported") { diff --git a/internal/plugin/quota.go b/internal/plugin/quota.go index cf069f4..e92a66f 100644 --- a/internal/plugin/quota.go +++ b/internal/plugin/quota.go @@ -109,11 +109,21 @@ func quotaJSON(v any) (pluginapi.ManagementResponse, error) { return pluginapi.ManagementResponse{Headers: http.Header{"Content-Type": []string{"application/json"}}, Body: body}, nil } +// maxQuotaTimeout bounds quota lookups, which serve interactive management calls. +const maxQuotaTimeout = 30 * time.Second + +func quotaTimeout(requestTimeout time.Duration) time.Duration { + if requestTimeout <= 0 || requestTimeout > maxQuotaTimeout { + return maxQuotaTimeout + } + return requestTimeout +} + func fetchQuota(ctx context.Context, bridge *HostBridge, baseURL string, timeout time.Duration, key string) (quotaUsage, error) { if bridge == nil { return quotaUsage{}, fmt.Errorf("quota bridge unavailable") } - ctx, cancel := context.WithTimeout(ctx, timeout) + ctx, cancel := context.WithTimeout(ctx, quotaTimeout(timeout)) defer cancel() resp, err := bridge.Do(ctx, pluginapi.HTTPRequest{Method: http.MethodGet, URL: strings.TrimRight(baseURL, "/") + "/usage", Headers: http.Header{"Authorization": []string{"Bearer " + key}, "Accept": []string{"application/json"}}}) if err != nil || resp.StatusCode != http.StatusOK { From 97548372f8bee987b3c631e1d7a7286bdf66663f Mon Sep 17 00:00:00 2001 From: zenyr Date: Sun, 4 Oct 2026 22:47:06 +0900 Subject: [PATCH 3/3] fix(quota): report the rolling window as 5h OpenCode Go's rolling limit spans five hours. Reporting it as 5h lets Management Center label and order it like other five-hour windows. Document the v8 Management API quota route as well. Co-Authored-By: Claude Opus 5.5 (1M context) --- README.md | 2 +- internal/plugin/native_quota.go | 3 ++- internal/plugin/native_quota_test.go | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a4fb787..fb5096d 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ This plugin exposes OpenCode Go as a single provider (`opencode-go`) backed by a - **Dynamic Catalog Discovery**: Fetches remote model catalogs with local fallback and custom route overrides. - **Multi-Key Auth Scheduling**: Pools multiple API keys with CLIProxyAPI's native scheduler for rotation, retries, and error cooldowns across all protocols. - **OpenCode Go Quota Page**: Management Center includes a separate `OpenCode Go Quota` page. Page load lists credentials without contacting OpenCode; each card is refreshed manually and independently, and quota values do not affect routing or CPA's native quota page. -- **Native Quota Provider**: On CLIProxyAPI `v7.2.159+` (including v8), the plugin also registers as a quota provider, so each OpenCode Go credential reports `supports_quota` in `/v0/management/auth-files` and its rolling, weekly, and monthly windows are served by `POST /v0/management/quota/fetch`. Quota reset is unsupported. Older hosts ignore the capability and keep using the separate page. +- **Native Quota Provider**: On CLIProxyAPI `v7.2.159+` (including v8), the plugin also registers as a quota provider, so each OpenCode Go credential reports `supports_quota` in `/v0/management/auth-files` and its rolling (reported as the `5h` window), weekly, and monthly windows are served by `POST /v0/management/quota/fetch` (v8 Management API: `POST /v8/management/plugins/opencode-go-cliproxyapi/quota`). Quota reset is unsupported. Older hosts ignore the capability and keep using the separate page. ## Requirements diff --git a/internal/plugin/native_quota.go b/internal/plugin/native_quota.go index c9eb20e..c703742 100644 --- a/internal/plugin/native_quota.go +++ b/internal/plugin/native_quota.go @@ -91,10 +91,11 @@ func (m *Manager) fetchNativeQuota(ctx context.Context, req nativeQuotaFetchRequ } func nativeQuotaFromUsage(usage quotaUsage) (nativeQuotaFetchResponse, error) { + // OpenCode Go's rolling window spans five hours. windows := []struct { name string window quotaWindow - }{{"rolling", usage.Rolling}, {"weekly", usage.Weekly}, {"monthly", usage.Monthly}} + }{{"5h", usage.Rolling}, {"weekly", usage.Weekly}, {"monthly", usage.Monthly}} buckets := make([]nativeQuotaBucket, 0, len(windows)) for _, w := range windows { if w.window.Status == "" && w.window.ResetsAt == "" { diff --git a/internal/plugin/native_quota_test.go b/internal/plugin/native_quota_test.go index 33bdea9..9e24b41 100644 --- a/internal/plugin/native_quota_test.go +++ b/internal/plugin/native_quota_test.go @@ -72,7 +72,7 @@ func TestNativeQuotaFetchUsesSelectedCredential(t *testing.T) { t.Fatalf("response = %+v", got) } want := []nativeQuotaBucket{ - {Window: "rolling", RemainingFraction: 1, ResetTime: "2026-10-04T15:26:49.000Z"}, + {Window: "5h", RemainingFraction: 1, ResetTime: "2026-10-04T15:26:49.000Z"}, {Window: "weekly", RemainingFraction: 0.96, ResetTime: "2026-10-05T00:00:00.000Z"}, {Window: "monthly", RemainingFraction: 0.59, ResetTime: "2026-10-26T10:20:31.000Z"}, }