@vymalo/opencode-ratelimit 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  OpenCode plugin that makes OpenAI-compatible providers **rate-limit aware**. It reads the IETF draft-03 rate-limit response headers emitted by [Envoy Gateway](https://gateway.envoyproxy.io/docs/tasks/traffic/global-rate-limit/)'s global rate limiting (`x-ratelimit-limit` / `-remaining` / `-reset`), proactively pauses new requests when the quota is exhausted, and backs off + retries on HTTP `429` — so your OpenCode client cooperates with the gateway instead of hammering it.
4
4
 
5
- Auth-agnostic by design: it runs as an OpenCode `config` hook and only wraps the provider's `fetch`. It never reads or sets `Authorization`, so it composes with `@vymalo/opencode-oauth2`, static API keys, or no auth at all.
5
+ Auth-agnostic by design: it observes OpenCode's HTTP request/response hooks and never reads or sets `Authorization`, so it composes with `@vymalo/opencode-oauth2`, static API keys, or no auth at all.
6
+
7
+ > **Requires OpenCode v2.** This release targets the v2 plugin API and does not run on v1. Migrating from `0.17.x`? See [Migrating from v1](#migrating-from-v1).
6
8
 
7
9
  ## Why use this
8
10
 
@@ -10,14 +12,14 @@ OpenCode has no built-in client-side rate-limit handling. If your inference sits
10
12
 
11
13
  ## How it works
12
14
 
13
- OpenCode's plugin API has **no post-response hook**, so the only way to observe response status and headers is to inject a custom `fetch` onto the provider's `options.fetch` during the `config` hook (OpenCode forwards it to the AI SDK provider). This plugin wraps that fetch:
15
+ Four registrations on OpenCode's native request lifecycle:
14
16
 
15
- 1. **Pre-request gate** — if the last response said `remaining: 0`, hold new requests until `x-ratelimit-reset` elapses.
16
- 2. **Send** the request through the underlying fetch (or any fetch a prior plugin already installed).
17
- 3. **Read** the rate-limit headers from the response and update per-provider state.
18
- 4. **On `429`** — wait the reset window (`x-ratelimit-reset`, or `Retry-After` as a fallback) and retry, up to `maxRetries`.
17
+ 1. **`provider.transform`** — read which providers opted in via `settings.rateLimit`. Reads only; it never mutates a provider.
18
+ 2. **`http.request`** — pre-request gate. If the last response said `remaining: 0`, hold new requests until `x-ratelimit-reset` elapses.
19
+ 3. **`http.response`** — read the rate-limit headers and update per-bucket state.
20
+ 4. **`retry`** — on a `429`, return the reset window (`x-ratelimit-reset`, or `Retry-After` as a fallback) as the host's retry delay, up to `maxRetries`; or decline the retry so the `429` surfaces immediately.
19
21
 
20
- The `Response` is returned untouched (no `.clone()`), so the streaming body reaches OpenCode intact.
22
+ The response body is never read — only `status` and `headers` — so the streaming body reaches OpenCode intact. Because `retry` hands the delay to OpenCode, the plugin never blocks a request thread on a `429`; the host sleeps, cancellably, and counts the attempts.
21
23
 
22
24
  > **Where the headers live.** Envoy attaches its rate-limit `BackendTrafficPolicy` to a specific route — in practice `/v1/chat/completions`, **not** `/v1/models`. So the plugin sees the quota on the inference calls that matter, and a `curl` of `/v1/models` may show no `x-ratelimit-*` at all. The `limit` header is also commonly *multi-policy*, e.g. `x-ratelimit-limit: 200, 200;w=60, 200000;w=60, 50000000;w=2592000` — the parser takes the first token as `limit`, while `remaining`/`reset` already track whichever bucket is closest to its cap (which is all the throttle logic needs).
23
25
 
@@ -29,23 +31,21 @@ npm install @vymalo/opencode-ratelimit
29
31
 
30
32
  ## Usage
31
33
 
32
- A provider opts in via `options.meta.rateLimit`. All fields are optional:
34
+ A provider opts in via `settings.rateLimit`. All fields are optional:
33
35
 
34
36
  ```json
35
37
  {
36
- "plugin": ["@vymalo/opencode-ratelimit"],
37
- "provider": {
38
+ "plugins": ["@vymalo/opencode-ratelimit"],
39
+ "providers": {
38
40
  "my-provider": {
39
- "npm": "@ai-sdk/openai-compatible",
40
- "options": {
41
+ "package": "aisdk:@ai-sdk/openai-compatible",
42
+ "settings": {
41
43
  "baseURL": "https://api.example.com/v1",
42
- "meta": {
43
- "rateLimit": {
44
- "enabled": true,
45
- "maxWaitMs": 0,
46
- "maxRetries": 5,
47
- "headerPrefix": "x-ratelimit"
48
- }
44
+ "rateLimit": {
45
+ "enabled": true,
46
+ "maxWaitMs": 0,
47
+ "maxRetries": 5,
48
+ "headerPrefix": "x-ratelimit"
49
49
  }
50
50
  }
51
51
  }
@@ -53,6 +53,21 @@ A provider opts in via `options.meta.rateLimit`. All fields are optional:
53
53
  }
54
54
  ```
55
55
 
56
+ The plugin's own options (currently just `logLevel`) go on its `plugins` entry:
57
+
58
+ ```json
59
+ {
60
+ "plugins": [
61
+ { "package": "@vymalo/opencode-ratelimit", "options": { "logLevel": "debug" } }
62
+ ]
63
+ }
64
+ ```
65
+
66
+ > **`logLevel` is a gate, not an override.** Records go to OpenCode's own logger, which applies the
67
+ > host's level too — and Effect's default minimum is `Info`. So `"logLevel": "debug"` alone will not
68
+ > surface `debug`/`trace` records; the host has to permit them as well
69
+ > (`OPENCODE_LOG_LEVEL=DEBUG`). Both gates must be open.
70
+
56
71
  | Field | Default | Meaning |
57
72
  | --- | --- | --- |
58
73
  | `enabled` | `true` | Set `false` to opt a provider out while keeping the block. Omitting the whole `rateLimit` block also opts out. |
@@ -62,14 +77,15 @@ A provider opts in via `options.meta.rateLimit`. All fields are optional:
62
77
  | `headerPrefix` | `x-ratelimit` | Lowercased prefix of the draft-03 triple. Remap for gateways that use e.g. `ratelimit-*`. |
63
78
  | `tiers` | — | Optional array of policy bands keyed by reset magnitude. Replaces flat `maxWaitMs`/`maxRetries` with per-window behavior. See [Tiered policies](#tiered-policies). |
64
79
 
65
- If you also use `@vymalo/opencode-oauth2`, list it **before** this plugin in `plugin` (config hooks run in registration order). It is only a soft recommendation — this plugin's fetch wrapping is auth-independent.
80
+ Ordering in `plugins` does not matter for this plugin. Each v2 plugin registers against a different domain, and this one touches neither auth nor the provider definition.
66
81
 
67
82
  ### Tiered policies
68
83
 
69
84
  A single `maxWaitMs` can't tell a **per-minute burst reset** (≤60s — worth waiting through) from a **monthly-budget reset** (days away — should error fast): both arrive as the same `x-ratelimit-reset`. The discriminator is the reset's *magnitude*, and `tiers` lets you act on it. Each tier matches resets up to `maxResetSeconds` (first match wins; ascending, catch-all last):
70
85
 
71
86
  ```jsonc
72
- "meta": { "rateLimit": {
87
+ // providers.<id>.settings
88
+ "rateLimit": {
73
89
  "scope": "model",
74
90
  "tiers": [
75
91
  { "maxResetSeconds": 120, "action": "wait", "maxWaitMs": 0, "maxRetries": 3 }, // ≤2min: wait it out
@@ -92,18 +108,18 @@ Notes:
92
108
 
93
109
  ### Per-model scope
94
110
 
95
- If your gateway buckets per model (e.g. Envoy `BackendTrafficPolicy` selectors that include the model), `scope: "model"` (the default) keys cooldown state per `(provider, model)` — so exhausting one model's bucket never blocks requests to a sibling model that still has quota. The model is read from the request body (`@ai-sdk/openai-compatible` sends a JSON body with `model`); when it can't be parsed the bucket falls back to provider-wide. Use `scope: "provider"` if your gateway buckets per provider/account.
111
+ If your gateway buckets per model (e.g. Envoy `BackendTrafficPolicy` selectors that include the model), `scope: "model"` (the default) keys cooldown state per `(provider, model)` — so exhausting one model's bucket never blocks requests to a sibling model that still has quota. The model comes straight off the hook event, so there is no body parsing and no fallback case. Use `scope: "provider"` if your gateway buckets per provider/account.
96
112
 
97
113
  ## Caveats
98
114
 
99
115
  ### Long waits can be cut short by OpenCode's own timeouts
100
116
 
101
- OpenCode wraps the injected `fetch` with its own `headerTimeout` / `chunkTimeout` logic, and our pre-request gate and `429` backoff wait **inside** that fetch. With the default `maxWaitMs: 0`, a 55-second reset window means a 55-second wait — which OpenCode may abort if it exceeds the header timeout. When that happens the wait is cancelled cleanly (a `ratelimit_wait_aborted` event is logged and the abort is propagated as a normal cancellation).
117
+ The pre-request gate holds the request inside OpenCode's own request path, so it competes with the provider's `headerTimeout` / `chunkTimeout`. With the default `maxWaitMs: 0`, a 55-second reset window means a 55-second hold — which OpenCode may abort if it exceeds the header timeout. When that happens the wait is cancelled cleanly (a `ratelimit_wait_aborted` event is logged and the abort is propagated as a normal cancellation). The `429` backoff is the host's own retry delay and is not subject to this.
102
118
 
103
119
  If you expect long waits, raise the provider's `headerTimeout` (and `chunkTimeout`) above your largest reset window:
104
120
 
105
121
  ```json
106
- "options": { "headerTimeout": 90000, "chunkTimeout": 90000 }
122
+ "settings": { "headerTimeout": 90000, "chunkTimeout": 90000 }
107
123
  ```
108
124
 
109
125
  ### State is in-memory only
@@ -138,3 +154,22 @@ The `./lib` subpath exports the testable internals for embedders: `parseRateLimi
138
154
  ## License
139
155
 
140
156
  MIT
157
+
158
+ ## Migrating from v1
159
+
160
+ `0.18.0` targets the OpenCode **v2** plugin API; `0.17.x` was the last v1 release. See
161
+ [ADR-0019](../../docs/adr/0019-opencode-v2-cutover.md) for why there is no dual-support build.
162
+
163
+ | v1 | v2 |
164
+ | --- | --- |
165
+ | `"plugin": [...]` | `"plugins": [...]` |
166
+ | `"provider": { ... }` | `"providers": { ... }` |
167
+ | `"npm": "@ai-sdk/openai-compatible"` | `"package": "aisdk:@ai-sdk/openai-compatible"` |
168
+ | `options.baseURL` | `settings.baseURL` |
169
+ | `options.meta.rateLimit` | `settings.rateLimit` |
170
+ | `options.headerTimeout` / `chunkTimeout` | `settings.headerTimeout` / `chunkTimeout` |
171
+ | `pluginConfig.ratelimit.logLevel` | the plugin's own `options.logLevel` |
172
+ | host `config.logLevel` drove the log level | `options.logLevel`, else `OPENCODE_LOG_LEVEL` |
173
+
174
+ The `rateLimit` block's own fields — `enabled`, `scope`, `maxWaitMs`, `maxRetries`,
175
+ `headerPrefix`, `tiers` — are unchanged, as is every `ratelimit_*` event name.
package/dist/config.d.ts CHANGED
@@ -14,7 +14,7 @@ export declare const DEFAULT_SCOPE: RateLimitScope;
14
14
  * - **Tiered**: a `tiers` array, each entry `{ maxResetSeconds?, action?,
15
15
  * maxWaitMs?, maxRetries? }`, matched by reset magnitude.
16
16
  */
17
- export declare function parseRateLimitOptions(providerOptions: Record<string, unknown> | undefined): RateLimitOptions | null;
17
+ export declare function parseRateLimitOptions(providerSettings: Record<string, unknown> | undefined): RateLimitOptions | null;
18
18
  /**
19
19
  * Select the policy tier for an observed reset. `tiers` is pre-sorted ascending
20
20
  * with a catch-all last, so the first tier whose bound covers `resetSeconds`
package/dist/config.js CHANGED
@@ -2,7 +2,6 @@ export const DEFAULT_MAX_WAIT_MS = 0;
2
2
  export const DEFAULT_MAX_RETRIES = 5;
3
3
  export const DEFAULT_HEADER_PREFIX = "x-ratelimit";
4
4
  export const DEFAULT_SCOPE = "model";
5
- const META_KEY = "meta";
6
5
  const RATE_LIMIT_KEY = "rateLimit";
7
6
  function asRecord(value) {
8
7
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -84,15 +83,13 @@ function normalizeTiers(tiers) {
84
83
  * - **Tiered**: a `tiers` array, each entry `{ maxResetSeconds?, action?,
85
84
  * maxWaitMs?, maxRetries? }`, matched by reset magnitude.
86
85
  */
87
- export function parseRateLimitOptions(providerOptions) {
88
- if (!providerOptions) {
86
+ export function parseRateLimitOptions(providerSettings) {
87
+ if (!providerSettings) {
89
88
  return null;
90
89
  }
91
- const meta = asRecord(providerOptions[META_KEY]);
92
- if (!meta) {
93
- return null;
94
- }
95
- const rateLimit = asRecord(meta[RATE_LIMIT_KEY]);
90
+ // v2 providers have no free-form `options.meta`; `Provider.Settings` is an
91
+ // open struct, so the opt-in lives directly at `settings.rateLimit`.
92
+ const rateLimit = asRecord(providerSettings[RATE_LIMIT_KEY]);
96
93
  if (!rateLimit) {
97
94
  return null;
98
95
  }
@@ -1 +1 @@
1
- {"mappings":"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","names":[],"sources":["../src/config.ts"],"version":3,"file":"config.js","sourceRoot":""}
1
+ {"mappings":"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","names":[],"sources":["../src/config.ts"],"version":3,"file":"config.js","sourceRoot":""}
package/dist/lib.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- export { createOpencodeRatelimitPlugin, OpencodeRatelimitPlugin, type OpenCodePluginFactoryOptions } from "./opencode.js";
1
+ export { createOpencodeRatelimitPlugin, OpencodeRatelimitPlugin, type OpenCodePluginFactoryOptions, PLUGIN_ID } from "./opencode.js";
2
2
  export { DEFAULT_HEADER_PREFIX, DEFAULT_MAX_RETRIES, DEFAULT_MAX_WAIT_MS, DEFAULT_SCOPE, parseRateLimitOptions, selectTier } from "./config.js";
3
3
  export { parseRateLimit } from "./headers.js";
4
- export { createJsonConsoleLogger, DEFAULT_LOG_LEVEL, fromOpenCodeLogLevel, type LogFields, type Logger, type LogLevel } from "./logging.js";
5
- export { createProviderState, DEFAULT_BACKOFF_MS, type InstallConfigInput, installRateLimiter, makeRateLimitFetch, type ProviderConfigLike, type ProviderRateState, type RateLimitDeps, type RateStateStore } from "./plugin.js";
4
+ export { captureHostContext, createEffectLogger, createJsonConsoleLogger, DEFAULT_LOG_LEVEL, type LogFields, type Logger, type LogLevel, resolveLogLevel } from "./logging.js";
5
+ export { type BucketRef, collectOptIns, createProviderState, createRateLimiter, DEFAULT_BACKOFF_MS, type ProviderRateState, type RateLimitDeps, type RateLimiter, type RateStateStore, type RetryDecision } from "./plugin.js";
6
6
  export type { RateLimitAction, RateLimitOptions, RateLimitScope, RateLimitSnapshot, RateLimitTier } from "./types.js";
package/dist/lib.js CHANGED
@@ -1,7 +1,7 @@
1
- export { createOpencodeRatelimitPlugin, OpencodeRatelimitPlugin } from "./opencode.js";
1
+ export { createOpencodeRatelimitPlugin, OpencodeRatelimitPlugin, PLUGIN_ID } from "./opencode.js";
2
2
  export { DEFAULT_HEADER_PREFIX, DEFAULT_MAX_RETRIES, DEFAULT_MAX_WAIT_MS, DEFAULT_SCOPE, parseRateLimitOptions, selectTier } from "./config.js";
3
3
  export { parseRateLimit } from "./headers.js";
4
- export { createJsonConsoleLogger, DEFAULT_LOG_LEVEL, fromOpenCodeLogLevel } from "./logging.js";
5
- export { createProviderState, DEFAULT_BACKOFF_MS, installRateLimiter, makeRateLimitFetch } from "./plugin.js";
4
+ export { captureHostContext, createEffectLogger, createJsonConsoleLogger, DEFAULT_LOG_LEVEL, resolveLogLevel } from "./logging.js";
5
+ export { collectOptIns, createProviderState, createRateLimiter, DEFAULT_BACKOFF_MS } from "./plugin.js";
6
6
 
7
7
  //# sourceMappingURL=lib.js.map
package/dist/lib.js.map CHANGED
@@ -1 +1 @@
1
- {"mappings":"AAAA,SACE,+BACA,+BAEK;AAEP,SACE,uBACA,qBACA,qBACA,eACA,uBACA,kBACK;AAEP,SAAS,sBAAsB;AAE/B,SACE,yBACA,mBACA,4BAIK;AAEP,SACE,qBACA,oBAEA,oBACA,0BAKK","names":[],"sources":["../src/lib.ts"],"version":3,"file":"lib.js","sourceRoot":""}
1
+ {"mappings":"AAAA,SACE,+BACA,yBAEA,iBACK;AAEP,SACE,uBACA,qBACA,qBACA,eACA,uBACA,kBACK;AAEP,SAAS,sBAAsB;AAE/B,SACE,oBACA,oBACA,yBACA,mBAIA,uBACK;AAEP,SAEE,eACA,qBACA,mBACA,0BAMK","names":[],"sources":["../src/lib.ts"],"version":3,"file":"lib.js","sourceRoot":""}
package/dist/logging.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type Context as EffectContext, Effect } from "effect";
1
2
  import type { LogLevel } from "./types.js";
2
3
  export type { LogLevel } from "./types.js";
3
4
  export declare const LOG_LEVEL_PRIORITY: Record<LogLevel, number>;
@@ -13,4 +14,37 @@ export interface Logger {
13
14
  error(event: string, fields?: LogFields): void;
14
15
  }
15
16
  export declare function createJsonConsoleLogger(minLevel?: LogLevel): Logger;
16
- export declare function fromOpenCodeLogLevel(value: unknown): LogLevel | undefined;
17
+ /**
18
+ * Capture the host's Effect context from inside the plugin's own fiber. The
19
+ * returned value is what {@link createEffectLogger} needs to put a log record
20
+ * on the host's logger rather than on a default one.
21
+ *
22
+ * OpenCode installs its file logger as a `Logger.layer(...)` on the runtime
23
+ * that runs plugin setup, and Effect carries loggers as fiber-level references
24
+ * — so the context captured here is the only route to that sink. A bare
25
+ * `Effect.runFork` would log to Effect's default logger (the terminal) instead,
26
+ * which is exactly what ADR-0014 forbids.
27
+ */
28
+ export declare const captureHostContext: Effect.Effect<EffectContext.Context<never>>;
29
+ /**
30
+ * Structured logger that writes through the host's Effect logger — the v2
31
+ * replacement for v1's `client.app.log`.
32
+ *
33
+ * ADR-0014 (no plugin writes to the user's TUI) is preserved: OpenCode's own
34
+ * logger is a `Logger.toFile` sink over the shared `opencode-<channel>.log`,
35
+ * so records land in the host log stream and never on the terminal. The event
36
+ * name becomes the log message and `fields` become log annotations, which
37
+ * OpenCode's logfmt formatter flattens to `key=value` — the same shape the v1
38
+ * `extra` payload produced.
39
+ *
40
+ * Host-side severity filtering is Effect's job (`OPENCODE_LOG_LEVEL`); the
41
+ * `getMinLevel` gate here is only this plugin's own `logLevel` option, applied
42
+ * before the record is built so a filtered-out call costs nothing.
43
+ */
44
+ export declare function createEffectLogger(context: EffectContext.Context<never>, getMinLevel: () => LogLevel): Logger;
45
+ /**
46
+ * Resolve this plugin's log level from its own `logLevel` option, falling back
47
+ * to the host's `OPENCODE_LOG_LEVEL` env var. v2 gives plugins no `config`
48
+ * object, so the v1 `config.logLevel` channel is gone.
49
+ */
50
+ export declare function resolveLogLevel(value: unknown): LogLevel | undefined;
package/dist/logging.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { Effect } from "effect";
1
2
  export const LOG_LEVEL_PRIORITY = {
2
3
  trace: 5,
3
4
  debug: 10,
@@ -51,13 +52,73 @@ export function createJsonConsoleLogger(minLevel = DEFAULT_LOG_LEVEL) {
51
52
  error: (event, fields) => write("error", event, fields)
52
53
  };
53
54
  }
54
- export function fromOpenCodeLogLevel(value) {
55
+ /**
56
+ * Capture the host's Effect context from inside the plugin's own fiber. The
57
+ * returned value is what {@link createEffectLogger} needs to put a log record
58
+ * on the host's logger rather than on a default one.
59
+ *
60
+ * OpenCode installs its file logger as a `Logger.layer(...)` on the runtime
61
+ * that runs plugin setup, and Effect carries loggers as fiber-level references
62
+ * — so the context captured here is the only route to that sink. A bare
63
+ * `Effect.runFork` would log to Effect's default logger (the terminal) instead,
64
+ * which is exactly what ADR-0014 forbids.
65
+ */
66
+ export const captureHostContext = Effect.withFiber((fiber) => Effect.succeed(fiber.context));
67
+ /**
68
+ * Structured logger that writes through the host's Effect logger — the v2
69
+ * replacement for v1's `client.app.log`.
70
+ *
71
+ * ADR-0014 (no plugin writes to the user's TUI) is preserved: OpenCode's own
72
+ * logger is a `Logger.toFile` sink over the shared `opencode-<channel>.log`,
73
+ * so records land in the host log stream and never on the terminal. The event
74
+ * name becomes the log message and `fields` become log annotations, which
75
+ * OpenCode's logfmt formatter flattens to `key=value` — the same shape the v1
76
+ * `extra` payload produced.
77
+ *
78
+ * Host-side severity filtering is Effect's job (`OPENCODE_LOG_LEVEL`); the
79
+ * `getMinLevel` gate here is only this plugin's own `logLevel` option, applied
80
+ * before the record is built so a filtered-out call costs nothing.
81
+ */
82
+ export function createEffectLogger(context, getMinLevel) {
83
+ // `VYMALO_PLUGIN_CONSOLE_LOG=1` mirrors to stdout for local debugging. Off by
84
+ // default precisely because stdout is the user's terminal (ADR-0014).
85
+ const consoleAll = /^(1|true|yes|on)$/i.test(process.env.VYMALO_PLUGIN_CONSOLE_LOG ?? "");
86
+ const fallback = createJsonConsoleLogger("trace");
87
+ const write = (level, event, fields) => {
88
+ if (LOG_LEVEL_PRIORITY[level] < LOG_LEVEL_PRIORITY[getMinLevel()]) {
89
+ return;
90
+ }
91
+ if (consoleAll) {
92
+ fallback[level](event, fields);
93
+ }
94
+ const redacted = redactFields(fields);
95
+ // Effect has no `trace` severity below `debug`; fold the most-verbose tier
96
+ // onto debug, as the v1 adapter did for the host log API.
97
+ const record = level === "trace" ? Effect.logDebug(event) : level === "debug" ? Effect.logDebug(event) : level === "info" ? Effect.logInfo(event) : level === "warn" ? Effect.logWarning(event) : Effect.logError(event);
98
+ const annotated = redacted ? Effect.annotateLogs(record, redacted) : record;
99
+ Effect.runFork(Effect.provideContext(annotated, context));
100
+ };
101
+ return {
102
+ trace: (event, fields) => write("trace", event, fields),
103
+ debug: (event, fields) => write("debug", event, fields),
104
+ info: (event, fields) => write("info", event, fields),
105
+ warn: (event, fields) => write("warn", event, fields),
106
+ error: (event, fields) => write("error", event, fields)
107
+ };
108
+ }
109
+ /**
110
+ * Resolve this plugin's log level from its own `logLevel` option, falling back
111
+ * to the host's `OPENCODE_LOG_LEVEL` env var. v2 gives plugins no `config`
112
+ * object, so the v1 `config.logLevel` channel is gone.
113
+ */
114
+ export function resolveLogLevel(value) {
55
115
  if (typeof value !== "string") {
56
116
  return undefined;
57
117
  }
58
118
  switch (value.toUpperCase()) {
59
119
  // DEBUG unlocks the most-verbose `trace` tier (host has no finer level).
60
120
  case "DEBUG": return "trace";
121
+ case "TRACE": return "trace";
61
122
  case "INFO": return "info";
62
123
  case "WARN": return "warn";
63
124
  case "ERROR": return "error";
@@ -1 +1 @@
1
- {"mappings":"AAIA,OAAO,MAAM,qBAA+C;CAC1D,OAAO;CACP,OAAO;CACP,MAAM;CACN,MAAM;CACN,OAAO;AACT;AAEA,OAAO,MAAM,oBAA8B;AAc3C,SAAS,aAAa,QAA2C;CAC/D,IAAI,CAAC,QAAQ;EACX,OAAO;CACT;CACA,MAAM,WAAsB,CAAC;CAC7B,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAAG;EACjD,IAAI,uCAAuC,KAAK,GAAG,GAAG;GACpD,SAAS,OAAO;GAChB;EACF;EACA,SAAS,OAAO;CAClB;CACA,OAAO;AACT;AAEA,OAAO,SAAS,wBAAwB,WAAqB,mBAA2B;CACtF,MAAM,cAAc,mBAAmB;CAEvC,MAAM,SAAS,OAAiB,OAAe,WAA6B;EAC1E,IAAI,mBAAmB,SAAS,aAAa;GAC3C;EACF;EACA,MAAM,UAAU;GACd,IAAI,IAAI,KAAK,CAAC,CAAC,YAAY;GAC3B;GACA;GACA,GAAI,aAAa,MAAM,KAAK,CAAC;EAC/B;EACA,MAAM,OAAO,KAAK,UAAU,OAAO;EACnC,IAAI,UAAU,SAAS;GACrB,QAAQ,MAAM,IAAI;GAClB;EACF;EACA,IAAI,UAAU,QAAQ;GACpB,QAAQ,KAAK,IAAI;GACjB;EACF;EACA,QAAQ,IAAI,IAAI;CAClB;CAEA,OAAO;EACL,QAAQ,OAAO,WAAW,MAAM,SAAS,OAAO,MAAM;EACtD,QAAQ,OAAO,WAAW,MAAM,SAAS,OAAO,MAAM;EACtD,OAAO,OAAO,WAAW,MAAM,QAAQ,OAAO,MAAM;EACpD,OAAO,OAAO,WAAW,MAAM,QAAQ,OAAO,MAAM;EACpD,QAAQ,OAAO,WAAW,MAAM,SAAS,OAAO,MAAM;CACxD;AACF;AAEA,OAAO,SAAS,qBAAqB,OAAsC;CACzE,IAAI,OAAO,UAAU,UAAU;EAC7B,OAAO;CACT;CACA,QAAQ,MAAM,YAAY,GAA1B;;EAEE,KAAK,SACH,OAAO;EACT,KAAK,QACH,OAAO;EACT,KAAK,QACH,OAAO;EACT,KAAK,SACH,OAAO;EACT,SACE,OAAO;CACX;AACF","names":[],"sources":["../src/logging.ts"],"version":3,"file":"logging.js","sourceRoot":""}
1
+ {"mappings":"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","names":[],"sources":["../src/logging.ts"],"version":3,"file":"logging.js","sourceRoot":""}
@@ -1,11 +1,36 @@
1
- import type { Plugin } from "@opencode-ai/plugin";
1
+ import { Plugin } from "@opencode/plugin/effect";
2
2
  import { type Logger } from "./logging.js";
3
+ import { type RateLimiter } from "./plugin.js";
4
+ /** Stable plugin id. v2 requires one and keys storage/registrations by it. */
5
+ export declare const PLUGIN_ID = "vymalo.ratelimit";
3
6
  export interface OpenCodePluginFactoryOptions {
7
+ /** Override the logger (tests). Defaults to the host's Effect logger. */
4
8
  logger?: Logger;
5
- fetchImpl?: typeof fetch;
9
+ /** Defaults to `Date.now`. */
6
10
  now?: () => number;
11
+ /** Defaults to a real `setTimeout`-based, abort-aware sleep. */
7
12
  sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
13
+ /** Observe the limiter the plugin built (tests). */
14
+ onLimiter?: (limiter: RateLimiter) => void;
8
15
  }
9
- export declare function createOpencodeRatelimitPlugin(factoryOptions?: OpenCodePluginFactoryOptions): Plugin;
10
- export declare const OpencodeRatelimitPlugin: Plugin;
16
+ /**
17
+ * Build the OpenCode v2 plugin.
18
+ *
19
+ * Three registrations, one per lifecycle seam:
20
+ *
21
+ * - `provider.transform` reads the `settings.rateLimit` opt-in set. It only
22
+ * *reads* — the limiter needs to know which providers it manages, and a
23
+ * transform is the seam that reruns on every provider reload. Transform
24
+ * callbacks must stay synchronous and cheap, which this is.
25
+ * - `session.hook("http.request")` is the pre-request gate.
26
+ * - `session.hook("http.response")` reads the `x-ratelimit-*` triple.
27
+ * - `session.hook("retry")` decides whether a 429 waits or surfaces.
28
+ *
29
+ * v1 had to do all of this inside one injected `provider.options.fetch`,
30
+ * because OpenCode exposed no post-response hook. v2's native HTTP seams make
31
+ * the fetch wrapper unnecessary — and the `retry` hook hands the backoff sleep
32
+ * to the host, so the plugin no longer blocks a request thread on a 429.
33
+ */
34
+ export declare function createOpencodeRatelimitPlugin(factoryOptions?: OpenCodePluginFactoryOptions): Plugin.Plugin;
35
+ export declare const OpencodeRatelimitPlugin: Plugin.Plugin;
11
36
  export default OpencodeRatelimitPlugin;
package/dist/opencode.js CHANGED
@@ -1,63 +1,82 @@
1
- import { createJsonConsoleLogger, DEFAULT_LOG_LEVEL, fromOpenCodeLogLevel, LOG_LEVEL_PRIORITY } from "./logging.js";
2
- import { installRateLimiter } from "./plugin.js";
3
- const PLUGIN_SERVICE_NAME = "opencode-ratelimit-plugin";
1
+ import { Plugin } from "@opencode/plugin/effect";
2
+ import { Effect } from "effect";
3
+ import { captureHostContext, createEffectLogger, DEFAULT_LOG_LEVEL, resolveLogLevel } from "./logging.js";
4
+ import { collectOptIns, createRateLimiter } from "./plugin.js";
5
+ /** Stable plugin id. v2 requires one and keys storage/registrations by it. */
6
+ export const PLUGIN_ID = "vymalo.ratelimit";
4
7
  /**
5
- * Pipe plugin logs through OpenCode's `client.app.log` so they show up in the
6
- * host's structured log stream, with the JSON console as a reliable fallback.
7
- * Mirrors the pattern used by `@vymalo/opencode-oauth2` and `-models-info`.
8
+ * Build the OpenCode v2 plugin.
9
+ *
10
+ * Three registrations, one per lifecycle seam:
11
+ *
12
+ * - `provider.transform` reads the `settings.rateLimit` opt-in set. It only
13
+ * *reads* — the limiter needs to know which providers it manages, and a
14
+ * transform is the seam that reruns on every provider reload. Transform
15
+ * callbacks must stay synchronous and cheap, which this is.
16
+ * - `session.hook("http.request")` is the pre-request gate.
17
+ * - `session.hook("http.response")` reads the `x-ratelimit-*` triple.
18
+ * - `session.hook("retry")` decides whether a 429 waits or surfaces.
19
+ *
20
+ * v1 had to do all of this inside one injected `provider.options.fetch`,
21
+ * because OpenCode exposed no post-response hook. v2's native HTTP seams make
22
+ * the fetch wrapper unnecessary — and the `retry` hook hands the backoff sleep
23
+ * to the host, so the plugin no longer blocks a request thread on a 429.
8
24
  */
9
- function createOpenCodeLogger(client, getMinLevel) {
10
- const fallback = createJsonConsoleLogger("debug");
11
- // OpenCode already captures plugin logs via client.app.log (and filters them
12
- // by its own log level). No plugin in this suite may write to the user's
13
- // TUI (ADR-0014, superseding ADR-0013's narrower per-plugin scope) — even a
14
- // warn/error about the caller's own throttled request stays off the
15
- // terminal and goes only to the host log, so only mirror to the JSON
16
- // console when explicitly requested; set VYMALO_PLUGIN_CONSOLE_LOG=1 to
17
- // restore full console output.
18
- const consoleAll = /^(1|true|yes|on)$/i.test(process.env.VYMALO_PLUGIN_CONSOLE_LOG ?? "");
19
- const write = (level, event, fields) => {
20
- if (LOG_LEVEL_PRIORITY[level] < LOG_LEVEL_PRIORITY[getMinLevel()]) {
21
- return;
22
- }
23
- if (consoleAll) {
24
- fallback[level](event, fields);
25
- }
26
- // The host's log API has no `trace` level — fold it onto `debug` so the most
27
- // verbose tier still reaches the structured stream (it's already gated above
28
- // by `getMinLevel`, which DEBUG maps to `trace`).
29
- const hostLevel = level === "trace" ? "debug" : level;
30
- void client.app.log({ body: {
31
- service: PLUGIN_SERVICE_NAME,
32
- level: hostLevel,
33
- message: event,
34
- extra: fields
35
- } }).catch(() => {
36
- /* best-effort */
37
- });
38
- };
39
- return {
40
- trace: (event, fields) => write("trace", event, fields),
41
- debug: (event, fields) => write("debug", event, fields),
42
- info: (event, fields) => write("info", event, fields),
43
- warn: (event, fields) => write("warn", event, fields),
44
- error: (event, fields) => write("error", event, fields)
45
- };
46
- }
47
25
  export function createOpencodeRatelimitPlugin(factoryOptions = {}) {
48
- return async ({ client }) => {
49
- let currentLogLevel = DEFAULT_LOG_LEVEL;
50
- const logger = factoryOptions.logger ?? createOpenCodeLogger(client, () => currentLogLevel);
51
- return { config: async (config) => {
52
- currentLogLevel = fromOpenCodeLogLevel(config.logLevel) ?? DEFAULT_LOG_LEVEL;
53
- installRateLimiter(config, {
26
+ return Plugin.define({
27
+ id: PLUGIN_ID,
28
+ effect: Effect.fn(function* (ctx) {
29
+ // v2 gives plugins no `config` object, so the v1 `config.logLevel`
30
+ // channel is gone: take this plugin's own `logLevel` option first, then
31
+ // the host's `OPENCODE_LOG_LEVEL`.
32
+ const logLevel = resolveLogLevel(ctx.options.logLevel) ?? resolveLogLevel(process.env.OPENCODE_LOG_LEVEL) ?? DEFAULT_LOG_LEVEL;
33
+ let logger = factoryOptions.logger;
34
+ if (!logger) {
35
+ const hostContext = yield* captureHostContext;
36
+ logger = createEffectLogger(hostContext, () => logLevel);
37
+ }
38
+ const limiter = createRateLimiter({
54
39
  logger,
55
- fetchImpl: factoryOptions.fetchImpl,
56
40
  now: factoryOptions.now,
57
41
  sleep: factoryOptions.sleep
58
42
  });
59
- } };
60
- };
43
+ factoryOptions.onLimiter?.(limiter);
44
+ const log = logger;
45
+ yield* ctx.provider.transform((editor) => {
46
+ limiter.configure(collectOptIns(editor.list().map((record) => ({
47
+ id: record.provider.id,
48
+ settings: record.provider.settings
49
+ })), log));
50
+ });
51
+ yield* ctx.session.hook("http.request", (event) => Effect.promise(() => limiter.acquire({
52
+ providerId: event.model.providerID,
53
+ modelID: event.model.id
54
+ }, event.request.signal)));
55
+ yield* ctx.session.hook("http.response", (event) => Effect.sync(() => {
56
+ limiter.observe({
57
+ providerId: event.model.providerID,
58
+ modelID: event.model.id
59
+ }, {
60
+ status: event.response.status,
61
+ headers: event.response.headers
62
+ });
63
+ }));
64
+ yield* ctx.session.hook("retry", (event) => Effect.sync(() => {
65
+ const decision = limiter.decideRetry({
66
+ providerId: event.model.providerID,
67
+ modelID: event.model.id
68
+ }, {
69
+ status: event.error.status,
70
+ retryAfterMs: event.error.retryAfterMs
71
+ }, event.attempt);
72
+ // Only speak up about a 429 we manage; leave every other failure to
73
+ // the host's own retry policy.
74
+ if (decision.retry) {
75
+ event.decision = decision;
76
+ }
77
+ }));
78
+ })
79
+ });
61
80
  }
62
81
  export const OpencodeRatelimitPlugin = createOpencodeRatelimitPlugin();
63
82
  export default OpencodeRatelimitPlugin;
@@ -1 +1 @@
1
- {"mappings":"AAEA,SACE,yBACA,mBACA,sBAEA,0BAGK;AACP,SAAkC,0BAA0B;AAE5D,MAAM,sBAAsB;;;;;;AAgB5B,SAAS,qBAAqB,QAA+B,aAAqC;CAChG,MAAM,WAAW,wBAAwB,OAAO;;;;;;;;CAQhD,MAAM,aAAa,qBAAqB,KAAK,QAAQ,IAAI,6BAA6B,EAAE;CAExF,MAAM,SAAS,OAAiB,OAAe,WAAuB;EACpE,IAAI,mBAAmB,SAAS,mBAAmB,YAAY,IAAI;GACjE;EACF;EACA,IAAI,YAAY;GACd,SAAS,MAAM,CAAC,OAAO,MAAM;EAC/B;;;;EAIA,MAAM,YAAY,UAAU,UAAU,UAAU;EAChD,KAAK,OAAO,IACT,IAAI,EACH,MAAM;GACJ,SAAS;GACT,OAAO;GACP,SAAS;GACT,OAAO;EACT,EACF,CAAC,CAAC,CACD,YAAY;;EAEb,CAAC;CACL;CAEA,OAAO;EACL,QAAQ,OAAO,WAAW,MAAM,SAAS,OAAO,MAAM;EACtD,QAAQ,OAAO,WAAW,MAAM,SAAS,OAAO,MAAM;EACtD,OAAO,OAAO,WAAW,MAAM,QAAQ,OAAO,MAAM;EACpD,OAAO,OAAO,WAAW,MAAM,QAAQ,OAAO,MAAM;EACpD,QAAQ,OAAO,WAAW,MAAM,SAAS,OAAO,MAAM;CACxD;AACF;AAEA,OAAO,SAAS,8BACd,iBAA+C,CAAC,GACxC;CACR,OAAO,OAAO,EAAE,aAAa;EAC3B,IAAI,kBAA4B;EAChC,MAAM,SAAS,eAAe,UAAU,qBAAqB,cAAc,eAAe;EAE1F,OAAO,EACL,QAAQ,OAAO,WAA2B;GACxC,kBAAkB,qBAAqB,OAAO,QAAQ,KAAK;GAC3D,mBAAmB,QAA8B;IAC/C;IACA,WAAW,eAAe;IAC1B,KAAK,eAAe;IACpB,OAAO,eAAe;GACxB,CAAC;EACH,EACF;CACF;AACF;AAEA,OAAO,MAAM,0BAAkC,8BAA8B;AAE7E,eAAe","names":[],"sources":["../src/opencode.ts"],"version":3,"file":"opencode.js","sourceRoot":""}
1
+ {"mappings":"AAAA,SAAS,cAAc;AACvB,SAAS,cAAc;AAEvB,SACE,oBACA,oBACA,mBAGA,uBACK;AACP,SAAS,eAAe,yBAA2C;;AAGnE,OAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;AA+BzB,OAAO,SAAS,8BACd,iBAA+C,CAAC,GACjC;CACf,OAAO,OAAO,OAAO;EACnB,IAAI;EACJ,QAAQ,OAAO,GAAG,WAAW,KAAK;;;;GAIhC,MAAM,WACJ,gBAAgB,IAAI,QAAQ,QAAQ,KACpC,gBAAgB,QAAQ,IAAI,kBAAkB,KAC9C;GAEF,IAAI,SAAS,eAAe;GAC5B,IAAI,CAAC,QAAQ;IACX,MAAM,cAAc,OAAO;IAC3B,SAAS,mBAAmB,mBAAmB,QAAQ;GACzD;GAEA,MAAM,UAAU,kBAAkB;IAChC;IACA,KAAK,eAAe;IACpB,OAAO,eAAe;GACxB,CAAC;GACD,eAAe,YAAY,OAAO;GAElC,MAAM,MAAM;GAEZ,OAAO,IAAI,SAAS,WAAW,WAAW;IACxC,QAAQ,UACN,cACE,OAAO,KAAK,CAAC,CAAC,KAAK,YAAY;KAC7B,IAAI,OAAO,SAAS;KACpB,UAAU,OAAO,SAAS;IAC5B,EAAE,GACF,GACF,CACF;GACF,CAAC;GAED,OAAO,IAAI,QAAQ,KAAK,iBAAiB,UACvC,OAAO,cACL,QAAQ,QACN;IAAE,YAAY,MAAM,MAAM;IAAY,SAAS,MAAM,MAAM;GAAG,GAC9D,MAAM,QAAQ,MAChB,CACF,CACF;GAEA,OAAO,IAAI,QAAQ,KAAK,kBAAkB,UACxC,OAAO,WAAW;IAChB,QAAQ,QACN;KAAE,YAAY,MAAM,MAAM;KAAY,SAAS,MAAM,MAAM;IAAG,GAC9D;KAAE,QAAQ,MAAM,SAAS;KAAQ,SAAS,MAAM,SAAS;IAAQ,CACnE;GACF,CAAC,CACH;GAEA,OAAO,IAAI,QAAQ,KAAK,UAAU,UAChC,OAAO,WAAW;IAChB,MAAM,WAAW,QAAQ,YACvB;KAAE,YAAY,MAAM,MAAM;KAAY,SAAS,MAAM,MAAM;IAAG,GAC9D;KAAE,QAAQ,MAAM,MAAM;KAAQ,cAAc,MAAM,MAAM;IAAa,GACrE,MAAM,OACR;;;IAGA,IAAI,SAAS,OAAO;KAClB,MAAM,WAAW;IACnB;GACF,CAAC,CACH;EACF,CAAC;CACH,CAAC;AACH;AAEA,OAAO,MAAM,0BAAyC,8BAA8B;AAEpF,eAAe","names":[],"sources":["../src/opencode.ts"],"version":3,"file":"opencode.js","sourceRoot":""}
package/dist/plugin.d.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  import type { Logger } from "./logging.js";
2
- import type { RateLimitOptions } from "./types.js";
2
+ import type { RateLimitOptions, RateLimitSnapshot } from "./types.js";
3
3
  /** Fallback backoff when a 429 carries no `x-ratelimit-reset` and no `Retry-After`. */
4
4
  export declare const DEFAULT_BACKOFF_MS = 1e3;
5
5
  /**
6
6
  * Mutable, in-memory rate-limit state for a single bucket (a provider, or a
7
7
  * `(provider, model)` pair when `scope: "model"`). Lives for the process
8
- * lifetime in the provider's state store. Never persisted — a reset window is
8
+ * lifetime in the limiter's state store. Never persisted — a reset window is
9
9
  * measured in seconds, so survival across process boots is pointless.
10
10
  */
11
11
  export interface ProviderRateState {
@@ -25,6 +25,13 @@ export interface ProviderRateState {
25
25
  * back to its own sleep, so it never waits past its required time / `maxWaitMs`.
26
26
  */
27
27
  cooldownPromiseUntilMs?: number;
28
+ /**
29
+ * Last snapshot read from this bucket's responses. The v2 `retry` hook sees a
30
+ * `SessionError` (status + `retryAfterMs`) but no response headers, so the
31
+ * reset window it needs for tier selection and backoff is carried here from
32
+ * the `http.response` hook that observed it.
33
+ */
34
+ lastSnapshot?: RateLimitSnapshot;
28
35
  }
29
36
  export declare function createProviderState(): ProviderRateState;
30
37
  export interface RateLimitDeps {
@@ -33,36 +40,59 @@ export interface RateLimitDeps {
33
40
  now?: () => number;
34
41
  /** Defaults to a real `setTimeout`-based, abort-aware sleep. */
35
42
  sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
36
- /** Underlying fetch when a provider has no pre-existing `options.fetch`. Defaults to `globalThis.fetch`. */
37
- fetchImpl?: typeof fetch;
38
- }
39
- export interface ProviderConfigLike {
40
- options?: Record<string, unknown>;
41
- }
42
- export interface InstallConfigInput {
43
- provider?: Record<string, ProviderConfigLike | undefined>;
44
43
  }
45
44
  /** A per-bucket state store (key = provider id, or `${providerId}\0${model}`). */
46
45
  export type RateStateStore = Map<string, ProviderRateState>;
46
+ /** What the `retry` hook should do with a failed request. */
47
+ export type RetryDecision = {
48
+ retry: false;
49
+ } | {
50
+ retry: true;
51
+ delay: number;
52
+ };
53
+ /** Identifies the bucket a hook event belongs to. */
54
+ export interface BucketRef {
55
+ providerId: string;
56
+ modelID?: string;
57
+ }
47
58
  /**
48
- * Walk every provider in the assembled OpenCode config; for each one that has
49
- * opted in via `options.meta.rateLimit`, wrap its `options.fetch` with a
50
- * rate-limit-aware fetch. Synchronous — all real work happens lazily inside the
51
- * wrapper at request time.
59
+ * The rate-limit engine, shaped for OpenCode v2's three request-lifecycle
60
+ * seams rather than v1's single `fetch` wrapper:
61
+ *
62
+ * - {@link RateLimiter.acquire} is the pre-request gate (`http.request`): it
63
+ * waits out a cooldown a previous `"wait"` tier armed.
64
+ * - {@link RateLimiter.observe} reads the rate-limit headers off a response
65
+ * (`http.response`) and arms or clears the gate for the *next* callers.
66
+ * - {@link RateLimiter.decideRetry} answers the `retry` hook: a `"wait"` tier
67
+ * returns a delay for the host to sleep, an `"error"` tier declines the retry
68
+ * so the 429 surfaces immediately.
69
+ *
70
+ * The host owns the retry loop in v2, so the plugin never sleeps on a 429 — it
71
+ * returns a delay and OpenCode waits (cancellably). The plugin still sleeps in
72
+ * `acquire`, because there is no host hook for "hold this request back".
52
73
  */
53
- export declare function installRateLimiter(input: InstallConfigInput, deps: RateLimitDeps): void;
74
+ export interface RateLimiter {
75
+ /** Replace the opted-in provider set. Called from the provider transform. */
76
+ configure(entries: ReadonlyMap<string, RateLimitOptions>): void;
77
+ /** Resolved options for a provider, or `undefined` when it has not opted in. */
78
+ options(providerId: string): RateLimitOptions | undefined;
79
+ acquire(ref: BucketRef, signal?: AbortSignal): Promise<void>;
80
+ observe(ref: BucketRef, response: {
81
+ status: number;
82
+ headers: Headers;
83
+ }): void;
84
+ decideRetry(ref: BucketRef, error: {
85
+ status?: number;
86
+ retryAfterMs?: number;
87
+ }, attempt: number): RetryDecision;
88
+ }
54
89
  /**
55
- * Build the fetch wrapper for one provider. The wrapper:
56
- * 1. Resolves the bucket (provider, or `(provider, model)` under `scope: "model"`).
57
- * 2. Pre-request gate: if that bucket's cooldown is armed, waits until it clears.
58
- * 3. Sends the request via the underlying fetch.
59
- * 4. Reads the rate-limit headers; selects the policy tier by reset magnitude.
60
- * On `remaining: 0` it arms the gate for the next callers (only when the
61
- * matched tier is `"wait"`).
62
- * 5. On a 429: a `"wait"` tier waits the reset window and retries (up to the
63
- * tier's `maxRetries`); an `"error"` tier surfaces the 429 immediately.
64
- *
65
- * The Response is returned untouched (no `.clone()`) — we only read `status`
66
- * and `headers`, so the body stream is delivered to OpenCode intact.
90
+ * Read each provider's `settings.rateLimit` opt-in. v2 has no free-form
91
+ * `options.meta` on a provider — `Provider.Settings` is an open struct, so the
92
+ * opt-in moves from v1's `options.meta.rateLimit` to `settings.rateLimit`.
67
93
  */
68
- export declare function makeRateLimitFetch(providerId: string, opts: RateLimitOptions, store: RateStateStore, deps: RateLimitDeps, delegate?: typeof fetch): typeof fetch;
94
+ export declare function collectOptIns(providers: readonly {
95
+ id: string;
96
+ settings?: Record<string, unknown>;
97
+ }[], logger: Logger): Map<string, RateLimitOptions>;
98
+ export declare function createRateLimiter(deps: RateLimitDeps): RateLimiter;
package/dist/plugin.js CHANGED
@@ -9,143 +9,114 @@ export function createProviderState() {
9
9
  };
10
10
  }
11
11
  /**
12
- * Walk every provider in the assembled OpenCode config; for each one that has
13
- * opted in via `options.meta.rateLimit`, wrap its `options.fetch` with a
14
- * rate-limit-aware fetch. Synchronous — all real work happens lazily inside the
15
- * wrapper at request time.
12
+ * Read each provider's `settings.rateLimit` opt-in. v2 has no free-form
13
+ * `options.meta` on a provider — `Provider.Settings` is an open struct, so the
14
+ * opt-in moves from v1's `options.meta.rateLimit` to `settings.rateLimit`.
16
15
  */
17
- export function installRateLimiter(input, deps) {
18
- const providers = input.provider;
19
- deps.logger.trace("ratelimit_install_start", { providerCount: providers ? Object.keys(providers).length : 0 });
20
- if (!providers) {
21
- deps.logger.info("ratelimit_plugin_initialized", { providerCount: 0 });
22
- return;
23
- }
24
- let enabledCount = 0;
25
- for (const [providerId, providerConfig] of Object.entries(providers)) {
26
- if (!providerConfig) {
27
- continue;
28
- }
29
- deps.logger.trace("ratelimit_provider_optin_check", {
30
- providerId,
31
- hasOptions: Boolean(providerConfig.options)
32
- });
33
- const opts = parseRateLimitOptions(providerConfig.options);
16
+ export function collectOptIns(providers, logger) {
17
+ const out = new Map();
18
+ for (const provider of providers) {
19
+ const opts = parseRateLimitOptions(provider.settings);
34
20
  if (!opts) {
35
- deps.logger.debug("ratelimit_provider_skipped", {
36
- providerId,
21
+ logger.debug("ratelimit_provider_skipped", {
22
+ providerId: provider.id,
37
23
  reason: "not_opted_in"
38
24
  });
39
25
  continue;
40
26
  }
41
- deps.logger.trace("ratelimit_provider_optin_resolved", {
42
- providerId,
43
- scope: opts.scope,
44
- headerPrefix: opts.headerPrefix,
45
- tiers: opts.tiers.length
46
- });
47
- const options = providerConfig.options ??= {};
48
- // Compose with any fetch a prior plugin already installed (capture it now,
49
- // not at call time, so we delegate to the original — not to ourselves).
50
- const delegate = typeof options.fetch === "function" ? options.fetch : undefined;
51
- deps.logger.trace("ratelimit_fetch_wrapped", {
52
- providerId,
53
- composedWithExistingFetch: Boolean(delegate)
54
- });
55
- const store = new Map();
56
- options.fetch = makeRateLimitFetch(providerId, opts, store, deps, delegate);
57
- enabledCount += 1;
58
- deps.logger.info("ratelimit_provider_enabled", {
59
- providerId,
27
+ out.set(provider.id, opts);
28
+ logger.info("ratelimit_provider_enabled", {
29
+ providerId: provider.id,
60
30
  scope: opts.scope,
61
31
  tiers: opts.tiers.length,
62
32
  headerPrefix: opts.headerPrefix
63
33
  });
64
34
  }
65
- deps.logger.info("ratelimit_plugin_initialized", { providerCount: enabledCount });
35
+ logger.info("ratelimit_plugin_initialized", { providerCount: out.size });
36
+ return out;
66
37
  }
67
- /**
68
- * Build the fetch wrapper for one provider. The wrapper:
69
- * 1. Resolves the bucket (provider, or `(provider, model)` under `scope: "model"`).
70
- * 2. Pre-request gate: if that bucket's cooldown is armed, waits until it clears.
71
- * 3. Sends the request via the underlying fetch.
72
- * 4. Reads the rate-limit headers; selects the policy tier by reset magnitude.
73
- * On `remaining: 0` it arms the gate for the next callers (only when the
74
- * matched tier is `"wait"`).
75
- * 5. On a 429: a `"wait"` tier waits the reset window and retries (up to the
76
- * tier's `maxRetries`); an `"error"` tier surfaces the 429 immediately.
77
- *
78
- * The Response is returned untouched (no `.clone()`) — we only read `status`
79
- * and `headers`, so the body stream is delivered to OpenCode intact.
80
- */
81
- export function makeRateLimitFetch(providerId, opts, store, deps, delegate) {
38
+ export function createRateLimiter(deps) {
82
39
  const now = deps.now ?? Date.now;
83
40
  const sleep = deps.sleep ?? defaultSleep;
84
- const underlying = delegate ?? deps.fetchImpl ?? globalThis.fetch;
85
41
  const { logger } = deps;
86
- const isRequest = (value) => typeof Request !== "undefined" && value instanceof Request;
87
- const wrapped = async (input, init) => {
88
- // Honor an abort signal whether it rides on `init` or on a `Request` input.
89
- const signal = init?.signal ?? (isRequest(input) ? input.signal : undefined) ?? undefined;
90
- const model = opts.scope === "model" ? await modelFromRequest(input, init) : undefined;
91
- if (opts.scope === "model") {
92
- logger.trace("ratelimit_model_resolved", {
93
- providerId,
94
- model,
95
- matched: model !== undefined
96
- });
97
- }
98
- const key = model ? `${providerId}\u0000${model}` : providerId;
42
+ let configured = new Map();
43
+ const store = new Map();
44
+ const optionsFor = (providerId) => configured.get(providerId);
45
+ const bucket = (ref, opts) => {
46
+ const model = opts.scope === "model" ? ref.modelID : undefined;
47
+ const key = model ? `${ref.providerId}\u0000${model}` : ref.providerId;
99
48
  let state = store.get(key);
100
- const bucketExisted = state !== undefined;
49
+ const existed = state !== undefined;
101
50
  if (!state) {
102
51
  state = createProviderState();
103
52
  store.set(key, state);
104
53
  }
105
- logger.trace("ratelimit_fetch_invoked", {
106
- providerId,
107
- scope: opts.scope,
54
+ return {
55
+ state,
56
+ key,
108
57
  model,
109
- bucketKey: key,
110
- bucketExisted,
111
- cooldownUntilMs: state.cooldownUntilMs
112
- });
113
- // 2. Pre-request gate — wait out a cooldown a previous "wait" tier armed.
114
- if (state.cooldownUntilMs > now()) {
58
+ existed
59
+ };
60
+ };
61
+ return {
62
+ configure(entries) {
63
+ configured = new Map(entries);
64
+ // Drop state for providers that are no longer opted in so a reload does
65
+ // not leave an orphaned cooldown gating a provider nobody manages.
66
+ for (const key of [...store.keys()]) {
67
+ const providerId = key.split("\0", 1)[0] ?? key;
68
+ if (!configured.has(providerId)) {
69
+ store.delete(key);
70
+ }
71
+ }
72
+ },
73
+ options: optionsFor,
74
+ async acquire(ref, signal) {
75
+ const opts = optionsFor(ref.providerId);
76
+ if (!opts) {
77
+ return;
78
+ }
79
+ const { state, key, model, existed } = bucket(ref, opts);
80
+ logger.trace("ratelimit_acquire", {
81
+ providerId: ref.providerId,
82
+ scope: opts.scope,
83
+ model,
84
+ bucketKey: key,
85
+ bucketExisted: existed,
86
+ cooldownUntilMs: state.cooldownUntilMs
87
+ });
88
+ if (state.cooldownUntilMs <= now()) {
89
+ return;
90
+ }
115
91
  const waitMs = clampWait(state.cooldownUntilMs - now(), state.cooldownMaxWaitMs);
116
92
  logger.trace("ratelimit_cooldown_gate_armed", {
117
- providerId,
93
+ providerId: ref.providerId,
118
94
  model,
119
95
  cooldownUntilMs: state.cooldownUntilMs,
120
96
  remainingMs: state.cooldownUntilMs - now(),
121
97
  clampedWaitMs: waitMs,
122
98
  maxWaitMs: state.cooldownMaxWaitMs
123
99
  });
124
- if (waitMs > 0) {
125
- logger.info("ratelimit_throttle_wait", {
126
- providerId,
127
- model,
128
- waitMs
129
- });
130
- await waitGate(state, sleep, now, state.cooldownMaxWaitMs, signal, logger, providerId, model, "pre_request");
100
+ if (waitMs <= 0) {
101
+ return;
131
102
  }
132
- }
133
- // 3-5. Attempt loop.
134
- let attempt = 0;
135
- for (;;) {
136
- // A Request body is single-use; send a fresh clone each attempt so a
137
- // wait-tier retry doesn't fail with "body already used". (The original is
138
- // never sent directly, so each clone has an unconsumed body.)
139
- const attemptInput = isRequest(input) ? input.clone() : input;
140
- logger.trace("ratelimit_underlying_fetch", {
141
- providerId,
103
+ logger.info("ratelimit_throttle_wait", {
104
+ providerId: ref.providerId,
142
105
  model,
143
- attempt
106
+ waitMs
144
107
  });
145
- const response = await underlying(attemptInput, init);
146
- const snapshot = readSnapshot(response, opts, now(), logger, providerId);
108
+ await waitGate(state, sleep, now, state.cooldownMaxWaitMs, signal, logger, ref.providerId, model, "pre_request");
109
+ },
110
+ observe(ref, response) {
111
+ const opts = optionsFor(ref.providerId);
112
+ if (!opts) {
113
+ return;
114
+ }
115
+ const { state, model } = bucket(ref, opts);
116
+ const snapshot = readSnapshot(response.headers, opts, now(), logger, ref.providerId);
117
+ state.lastSnapshot = snapshot;
147
118
  logger.trace("ratelimit_snapshot_parsed", {
148
- providerId,
119
+ providerId: ref.providerId,
149
120
  model,
150
121
  status: response.status,
151
122
  limit: snapshot.limit,
@@ -155,56 +126,89 @@ export function makeRateLimitFetch(providerId, opts, store, deps, delegate) {
155
126
  retryAfterMs: snapshot.retryAfterMs
156
127
  });
157
128
  logger.debug("ratelimit_quota", {
158
- providerId,
129
+ providerId: ref.providerId,
159
130
  model,
160
131
  remaining: snapshot.remaining,
161
132
  limit: snapshot.limit,
162
133
  resetSeconds: snapshot.resetSeconds
163
134
  });
164
- if (response.status !== 429) {
165
- // Arm the gate for the NEXT callers when the window is exhausted — but
166
- // only under a "wait" tier. An "error" tier wants the next request to
167
- // hit a real 429 and be surfaced, so we leave the gate unarmed.
168
- if (snapshot.remaining !== undefined && snapshot.remaining <= 0 && snapshot.resetAtMs !== undefined) {
169
- const resetForTier = effectiveResetSeconds(snapshot);
170
- const tier = selectTier(opts.tiers, resetForTier);
171
- logger.trace("ratelimit_exhausted_tier_selected", {
172
- providerId,
135
+ const resetForTier = effectiveResetSeconds(snapshot);
136
+ const tier = selectTier(opts.tiers, resetForTier);
137
+ if (response.status === 429) {
138
+ // The host will ask `decideRetry` what THIS caller does. Arm the gate so
139
+ // every OTHER concurrent caller throttles too (v1 got this from the
140
+ // shared wrapper state); an "error" tier clears it so the fail-fast is
141
+ // actually fast.
142
+ if (tier.action === "wait") {
143
+ const waitMs = clampWait(computeBackoff(snapshot, now()), tier.maxWaitMs);
144
+ state.cooldownUntilMs = now() + waitMs;
145
+ state.cooldownMaxWaitMs = tier.maxWaitMs;
146
+ logger.trace("ratelimit_cooldown_set", {
147
+ providerId: ref.providerId,
173
148
  model,
174
- resetSeconds: resetForTier,
175
- maxResetSeconds: tier.maxResetSeconds,
176
- action: tier.action,
149
+ cooldownUntilMs: state.cooldownUntilMs,
177
150
  maxWaitMs: tier.maxWaitMs,
178
- maxRetries: tier.maxRetries
151
+ source: "429_backoff"
152
+ });
153
+ } else {
154
+ state.cooldownUntilMs = 0;
155
+ state.cooldownMaxWaitMs = 0;
156
+ logger.trace("ratelimit_cooldown_cleared", {
157
+ providerId: ref.providerId,
158
+ model,
159
+ source: "429_error_tier"
179
160
  });
180
- if (tier.action === "wait") {
181
- state.cooldownUntilMs = snapshot.resetAtMs;
182
- state.cooldownMaxWaitMs = tier.maxWaitMs;
183
- logger.trace("ratelimit_cooldown_set", {
184
- providerId,
185
- model,
186
- cooldownUntilMs: state.cooldownUntilMs,
187
- maxWaitMs: tier.maxWaitMs,
188
- source: "exhausted"
189
- });
190
- } else {
191
- // Error tier → don't arm, and drop any stale cooldown a prior
192
- // "wait" window left so the next request hits a real 429 at once.
193
- state.cooldownUntilMs = 0;
194
- state.cooldownMaxWaitMs = 0;
195
- logger.trace("ratelimit_cooldown_cleared", {
196
- providerId,
197
- model,
198
- source: "exhausted_error_tier"
199
- });
200
- }
201
161
  }
202
- return response;
162
+ return;
163
+ }
164
+ // Arm the gate for the NEXT callers when the window is exhausted — but
165
+ // only under a "wait" tier. An "error" tier wants the next request to hit
166
+ // a real 429 and be surfaced, so we leave the gate unarmed.
167
+ if (snapshot.remaining === undefined || snapshot.remaining > 0 || snapshot.resetAtMs === undefined) {
168
+ return;
203
169
  }
170
+ logger.trace("ratelimit_exhausted_tier_selected", {
171
+ providerId: ref.providerId,
172
+ model,
173
+ resetSeconds: resetForTier,
174
+ maxResetSeconds: tier.maxResetSeconds,
175
+ action: tier.action,
176
+ maxWaitMs: tier.maxWaitMs,
177
+ maxRetries: tier.maxRetries
178
+ });
179
+ if (tier.action === "wait") {
180
+ state.cooldownUntilMs = snapshot.resetAtMs;
181
+ state.cooldownMaxWaitMs = tier.maxWaitMs;
182
+ logger.trace("ratelimit_cooldown_set", {
183
+ providerId: ref.providerId,
184
+ model,
185
+ cooldownUntilMs: state.cooldownUntilMs,
186
+ maxWaitMs: tier.maxWaitMs,
187
+ source: "exhausted"
188
+ });
189
+ return;
190
+ }
191
+ state.cooldownUntilMs = 0;
192
+ state.cooldownMaxWaitMs = 0;
193
+ logger.trace("ratelimit_cooldown_cleared", {
194
+ providerId: ref.providerId,
195
+ model,
196
+ source: "exhausted_error_tier"
197
+ });
198
+ },
199
+ decideRetry(ref, error, attempt) {
200
+ const opts = optionsFor(ref.providerId);
201
+ if (!opts || error.status !== 429) {
202
+ return { retry: false };
203
+ }
204
+ const { state, model } = bucket(ref, opts);
205
+ // Prefer what the response hook saw; fall back to the error's own
206
+ // `retryAfterMs` when this failure never produced an observed response.
207
+ const snapshot = state.lastSnapshot ?? (error.retryAfterMs !== undefined ? { retryAfterMs: error.retryAfterMs } : {});
204
208
  const resetForTier = effectiveResetSeconds(snapshot);
205
209
  const tier = selectTier(opts.tiers, resetForTier);
206
210
  logger.trace("ratelimit_429_tier_selected", {
207
- providerId,
211
+ providerId: ref.providerId,
208
212
  model,
209
213
  attempt,
210
214
  resetSeconds: resetForTier,
@@ -214,100 +218,50 @@ export function makeRateLimitFetch(providerId, opts, store, deps, delegate) {
214
218
  maxRetries: tier.maxRetries
215
219
  });
216
220
  if (tier.action === "error") {
217
- // Drop any cooldown a prior "wait" tier armed, so the fail-fast is
218
- // actually fast — later requests must not sleep a stale window first.
219
- state.cooldownUntilMs = 0;
220
- state.cooldownMaxWaitMs = 0;
221
- logger.trace("ratelimit_cooldown_cleared", {
222
- providerId,
223
- model,
224
- source: "429_error_tier"
225
- });
226
221
  logger.warn("ratelimit_failfast", {
227
- providerId,
222
+ providerId: ref.providerId,
228
223
  model,
229
224
  resetSeconds: snapshot.resetSeconds
230
225
  });
231
- return response;
226
+ return { retry: false };
232
227
  }
233
228
  if (attempt >= tier.maxRetries) {
234
229
  logger.trace("ratelimit_retries_exhausted", {
235
- providerId,
230
+ providerId: ref.providerId,
236
231
  model,
237
232
  attempt,
238
233
  maxRetries: tier.maxRetries
239
234
  });
240
235
  logger.error("ratelimit_giveup", {
241
- providerId,
236
+ providerId: ref.providerId,
242
237
  model,
243
238
  attempts: attempt
244
239
  });
245
- return response;
240
+ return { retry: false };
246
241
  }
247
242
  const backoffMs = computeBackoff(snapshot, now());
248
243
  const waitMs = clampWait(backoffMs, tier.maxWaitMs);
249
244
  logger.trace("ratelimit_429_backoff_computed", {
250
- providerId,
245
+ providerId: ref.providerId,
251
246
  model,
252
247
  attempt: attempt + 1,
253
248
  rawBackoffMs: backoffMs,
254
249
  clampedWaitMs: waitMs,
255
250
  maxWaitMs: tier.maxWaitMs
256
251
  });
257
- state.cooldownUntilMs = now() + waitMs;
258
- state.cooldownMaxWaitMs = tier.maxWaitMs;
259
- logger.trace("ratelimit_cooldown_set", {
260
- providerId,
261
- model,
262
- cooldownUntilMs: state.cooldownUntilMs,
263
- maxWaitMs: tier.maxWaitMs,
264
- source: "429_backoff"
265
- });
266
252
  logger.warn("ratelimit_429_backoff", {
267
- providerId,
253
+ providerId: ref.providerId,
268
254
  model,
269
255
  attempt: attempt + 1,
270
256
  waitMs,
271
257
  resetSeconds: snapshot.resetSeconds
272
258
  });
273
- await waitGate(state, sleep, now, tier.maxWaitMs, signal, logger, providerId, model, "backoff");
274
- attempt += 1;
259
+ return {
260
+ retry: true,
261
+ delay: waitMs
262
+ };
275
263
  }
276
264
  };
277
- return wrapped;
278
- }
279
- /**
280
- * Best-effort extraction of the `model` from an OpenAI-compatible request.
281
- * The AI SDK calls `fetch(url, { body: "<json>" })`, so the common path reads a
282
- * JSON string body synchronously (non-destructive). It also supports the
283
- * `fetch(new Request(...))` shape by reading a clone of the Request body, so a
284
- * Request-style caller doesn't silently collapse to the provider-wide bucket.
285
- * Anything unparseable → `undefined` → caller falls back to the provider bucket.
286
- */
287
- async function modelFromRequest(input, init) {
288
- const fromInit = modelFromBody(init?.body);
289
- if (fromInit !== undefined) {
290
- return fromInit;
291
- }
292
- if (typeof Request !== "undefined" && input instanceof Request) {
293
- try {
294
- return modelFromBody(await input.clone().text());
295
- } catch {
296
- return undefined;
297
- }
298
- }
299
- return undefined;
300
- }
301
- function modelFromBody(body) {
302
- if (typeof body !== "string") {
303
- return undefined;
304
- }
305
- try {
306
- const parsed = JSON.parse(body);
307
- return typeof parsed.model === "string" && parsed.model.length > 0 ? parsed.model : undefined;
308
- } catch {
309
- return undefined;
310
- }
311
265
  }
312
266
  /**
313
267
  * Reset magnitude (seconds) used for tier selection: prefer `x-ratelimit-reset`,
@@ -324,9 +278,9 @@ function effectiveResetSeconds(snapshot) {
324
278
  }
325
279
  return undefined;
326
280
  }
327
- function readSnapshot(response, opts, nowMs, logger, providerId) {
281
+ function readSnapshot(headers, opts, nowMs, logger, providerId) {
328
282
  try {
329
- return parseRateLimit(response.headers, opts.headerPrefix, nowMs);
283
+ return parseRateLimit(headers, opts.headerPrefix, nowMs);
330
284
  } catch (error) {
331
285
  logger.debug("ratelimit_header_parse_failed", {
332
286
  providerId,
@@ -1 +1 @@
1
- {"mappings":"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","names":[],"sources":["../src/plugin.ts"],"version":3,"file":"plugin.js","sourceRoot":""}
1
+ {"mappings":"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","names":[],"sources":["../src/plugin.ts"],"version":3,"file":"plugin.js","sourceRoot":""}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vymalo/opencode-ratelimit",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "OpenCode plugin that makes OpenAI-compatible providers rate-limit aware: it reads Envoy Gateway / IETF draft-03 rate-limit response headers (x-ratelimit-limit/remaining/reset), proactively throttles requests when the quota is exhausted, and backs off and retries on HTTP 429.",
5
5
  "license": "MIT",
6
6
  "author": "vymalo contributors",
@@ -16,6 +16,7 @@
16
16
  "keywords": [
17
17
  "opencode",
18
18
  "opencode-plugin",
19
+ "opencode-v2",
19
20
  "rate-limit",
20
21
  "throttle",
21
22
  "429",
@@ -47,11 +48,15 @@
47
48
  "access": "public"
48
49
  },
49
50
  "dependencies": {
50
- "@opencode-ai/plugin": "1.15.10"
51
+ "@opencode/plugin": "2.2.6"
51
52
  },
52
53
  "devDependencies": {
53
- "vite": "^8.2.1",
54
- "vitest": "^4.1.7"
54
+ "effect": "4.1.0",
55
+ "vite": "^8.3.0",
56
+ "vitest": "^5.0.1"
57
+ },
58
+ "peerDependencies": {
59
+ "effect": "^4.0.0"
55
60
  },
56
61
  "scripts": {
57
62
  "build": "node ../../scripts/build-package.mjs",