@vymalo/opencode-ratelimit 0.16.1 → 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 +59 -24
- package/dist/config.d.ts +1 -1
- package/dist/config.js +5 -8
- package/dist/config.js.map +1 -1
- package/dist/lib.d.ts +3 -3
- package/dist/lib.js +3 -3
- package/dist/lib.js.map +1 -1
- package/dist/logging.d.ts +35 -1
- package/dist/logging.js +62 -1
- package/dist/logging.js.map +1 -1
- package/dist/opencode.d.ts +29 -4
- package/dist/opencode.js +72 -50
- package/dist/opencode.js.map +1 -1
- package/dist/plugin.d.ts +58 -28
- package/dist/plugin.js +158 -204
- package/dist/plugin.js.map +1 -1
- package/package.json +9 -4
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
|
|
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
|
-
|
|
15
|
+
Four registrations on OpenCode's native request lifecycle:
|
|
14
16
|
|
|
15
|
-
1.
|
|
16
|
-
2.
|
|
17
|
-
3.
|
|
18
|
-
4.
|
|
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
|
|
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 `
|
|
34
|
+
A provider opts in via `settings.rateLimit`. All fields are optional:
|
|
33
35
|
|
|
34
36
|
```json
|
|
35
37
|
{
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
+
"plugins": ["@vymalo/opencode-ratelimit"],
|
|
39
|
+
"providers": {
|
|
38
40
|
"my-provider": {
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
+
"package": "aisdk:@ai-sdk/openai-compatible",
|
|
42
|
+
"settings": {
|
|
41
43
|
"baseURL": "https://api.example.com/v1",
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
"
|
|
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(
|
|
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(
|
|
88
|
-
if (!
|
|
86
|
+
export function parseRateLimitOptions(providerSettings) {
|
|
87
|
+
if (!providerSettings) {
|
|
89
88
|
return null;
|
|
90
89
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
}
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"mappings":"AAEA,OAAO,MAAM,sBAAsB;AACnC,OAAO,MAAM,sBAAsB;AACnC,OAAO,MAAM,wBAAwB;AACrC,OAAO,MAAM,gBAAgC;AAE7C,MAAM,
|
|
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,
|
|
5
|
-
export {
|
|
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,
|
|
5
|
-
export {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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";
|
package/dist/logging.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"mappings":"
|
|
1
|
+
{"mappings":"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","names":[],"sources":["../src/logging.ts"],"version":3,"file":"logging.js","sourceRoot":""}
|
package/dist/opencode.d.ts
CHANGED
|
@@ -1,11 +1,36 @@
|
|
|
1
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
10
|
-
|
|
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,60 +1,82 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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). Mirroring every event to stdout on top of that just
|
|
13
|
-
// floods the terminal, so only mirror warn/error to the JSON console by
|
|
14
|
-
// default; set VYMALO_PLUGIN_CONSOLE_LOG=1 to restore full console output.
|
|
15
|
-
const consoleAll = /^(1|true|yes|on)$/i.test(process.env.VYMALO_PLUGIN_CONSOLE_LOG ?? "");
|
|
16
|
-
const write = (level, event, fields) => {
|
|
17
|
-
if (LOG_LEVEL_PRIORITY[level] < LOG_LEVEL_PRIORITY[getMinLevel()]) {
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
if (consoleAll || level === "warn" || level === "error") {
|
|
21
|
-
fallback[level](event, fields);
|
|
22
|
-
}
|
|
23
|
-
// The host's log API has no `trace` level — fold it onto `debug` so the most
|
|
24
|
-
// verbose tier still reaches the structured stream (it's already gated above
|
|
25
|
-
// by `getMinLevel`, which DEBUG maps to `trace`).
|
|
26
|
-
const hostLevel = level === "trace" ? "debug" : level;
|
|
27
|
-
void client.app.log({ body: {
|
|
28
|
-
service: PLUGIN_SERVICE_NAME,
|
|
29
|
-
level: hostLevel,
|
|
30
|
-
message: event,
|
|
31
|
-
extra: fields
|
|
32
|
-
} }).catch(() => {
|
|
33
|
-
/* best-effort */
|
|
34
|
-
});
|
|
35
|
-
};
|
|
36
|
-
return {
|
|
37
|
-
trace: (event, fields) => write("trace", event, fields),
|
|
38
|
-
debug: (event, fields) => write("debug", event, fields),
|
|
39
|
-
info: (event, fields) => write("info", event, fields),
|
|
40
|
-
warn: (event, fields) => write("warn", event, fields),
|
|
41
|
-
error: (event, fields) => write("error", event, fields)
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
25
|
export function createOpencodeRatelimitPlugin(factoryOptions = {}) {
|
|
45
|
-
return
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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({
|
|
51
39
|
logger,
|
|
52
|
-
fetchImpl: factoryOptions.fetchImpl,
|
|
53
40
|
now: factoryOptions.now,
|
|
54
41
|
sleep: factoryOptions.sleep
|
|
55
42
|
});
|
|
56
|
-
|
|
57
|
-
|
|
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
|
+
});
|
|
58
80
|
}
|
|
59
81
|
export const OpencodeRatelimitPlugin = createOpencodeRatelimitPlugin();
|
|
60
82
|
export default OpencodeRatelimitPlugin;
|
package/dist/opencode.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"mappings":"
|
|
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
|
|
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
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
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
|
|
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
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
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
|
|
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
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const model = opts.scope === "model" ?
|
|
91
|
-
|
|
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
|
|
49
|
+
const existed = state !== undefined;
|
|
101
50
|
if (!state) {
|
|
102
51
|
state = createProviderState();
|
|
103
52
|
store.set(key, state);
|
|
104
53
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
54
|
+
return {
|
|
55
|
+
state,
|
|
56
|
+
key,
|
|
108
57
|
model,
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
|
|
106
|
+
waitMs
|
|
144
107
|
});
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
175
|
-
maxResetSeconds: tier.maxResetSeconds,
|
|
176
|
-
action: tier.action,
|
|
149
|
+
cooldownUntilMs: state.cooldownUntilMs,
|
|
177
150
|
maxWaitMs: tier.maxWaitMs,
|
|
178
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
274
|
-
|
|
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(
|
|
281
|
+
function readSnapshot(headers, opts, nowMs, logger, providerId) {
|
|
328
282
|
try {
|
|
329
|
-
return parseRateLimit(
|
|
283
|
+
return parseRateLimit(headers, opts.headerPrefix, nowMs);
|
|
330
284
|
} catch (error) {
|
|
331
285
|
logger.debug("ratelimit_header_parse_failed", {
|
|
332
286
|
providerId,
|
package/dist/plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"mappings":"AAAA,SAAS,uBAAuB,kBAAkB;AAClD,SAAS,sBAAsB;;AAK/B,OAAO,MAAM,qBAAqB;
|
|
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.
|
|
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
|
|
51
|
+
"@opencode/plugin": "2.2.6"
|
|
51
52
|
},
|
|
52
53
|
"devDependencies": {
|
|
53
|
-
"
|
|
54
|
-
"
|
|
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",
|