opencode-plugboard 0.13.0 → 0.15.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 +2 -2
- package/index.ts +19 -6
- package/package.json +5 -1
- package/src/anthropic/route.ts +2 -0
- package/src/cache.ts +165 -0
- package/src/config.ts +97 -1
- package/src/contract.ts +6 -0
- package/src/gemini/route.ts +2 -0
- package/src/limits.ts +144 -0
- package/src/log.ts +98 -31
- package/src/metrics.ts +100 -104
- package/src/models.ts +51 -3
- package/src/openai/fanout.ts +5 -0
- package/src/openai/route.ts +3 -1
- package/src/responses/route.ts +2 -0
- package/src/runtime.ts +51 -4
package/src/runtime.ts
CHANGED
|
@@ -24,7 +24,9 @@
|
|
|
24
24
|
* limits (src/session/prune.ts). Both start from request handling (the store's `register`);
|
|
25
25
|
* - the metrics registry (src/metrics.ts) and the runtime start time, so counters and uptime
|
|
26
26
|
* outlive any one generation. `processMetrics` / `processStatus` read them for `/metrics`,
|
|
27
|
-
* `/v1/status` and the discovery pages
|
|
27
|
+
* `/v1/status` and the discovery pages;
|
|
28
|
+
* - the request limiter's queues and rate windows (src/limits.ts), so a cap holds across
|
|
29
|
+
* generations. `processLimits` reads them.
|
|
28
30
|
*
|
|
29
31
|
* Registration is explicit: `registerGeneration` returns whether the generation was accepted. A
|
|
30
32
|
* rejected one (different `listen`, options that differ from the live ones, foreign runtime) never
|
|
@@ -48,8 +50,11 @@ import { setTimeout as delay } from "node:timers/promises"
|
|
|
48
50
|
import { Cause, Effect, Stream } from "effect"
|
|
49
51
|
import stableStringify from "safe-stable-stringify"
|
|
50
52
|
import { raceAbort } from "./abort.js"
|
|
51
|
-
import {
|
|
53
|
+
import { cachedCaller, createCacheStore, type CacheStore } from "./cache.js"
|
|
54
|
+
import type { PlugboardOptions } from "./config.js"
|
|
55
|
+
import { isPlugError, type CatalogModel, type ModelCaller, type PlugError } from "./contract.js"
|
|
52
56
|
import { errorMessage, log, logError, outsideRequest, setLogFormat } from "./log.js"
|
|
57
|
+
import { createLimitState, type LimitState } from "./limits.js"
|
|
53
58
|
import { createMetrics, type Metrics, type RuntimeSnapshot } from "./metrics.js"
|
|
54
59
|
import { ensureAppDir } from "./session/history.js"
|
|
55
60
|
import type { HistoryPort } from "./session/host.js"
|
|
@@ -107,8 +112,8 @@ const BACKEND_POLL_MS = 25
|
|
|
107
112
|
const BIND_RETRY_MS = 15_000
|
|
108
113
|
const BIND_REMINDER_MS = 5 * 60_000
|
|
109
114
|
|
|
110
|
-
/** 7: the options fingerprint comes from safe-stable-stringify, so it must not be compared with an older one. 6: the listener retries a failed bind (new fields on `Listener`). 5: adds the metrics registry and the start time. 4: the history sweep state and the session store's register hook. 3: confirmed directories and rejected generations. */
|
|
111
|
-
const VERSION =
|
|
115
|
+
/** 10: the response cache store (`src/cache.ts`) lives here too. 9: the request limiter state (`src/limits.ts`) lives here, so the concurrency caps and rate windows survive a generation. 8: `Metrics.render` is async and the registry is @prometheus-io/client's. 7: the options fingerprint comes from safe-stable-stringify, so it must not be compared with an older one. 6: the listener retries a failed bind (new fields on `Listener`). 5: adds the metrics registry and the start time. 4: the history sweep state and the session store's register hook. 3: confirmed directories and rejected generations. */
|
|
116
|
+
const VERSION = 10
|
|
112
117
|
const KEY = Symbol.for("opencode-plugboard.runtime")
|
|
113
118
|
|
|
114
119
|
interface Listener {
|
|
@@ -158,8 +163,12 @@ interface Runtime {
|
|
|
158
163
|
readonly sweep: { nextAt: number; running: boolean }
|
|
159
164
|
/** Process-wide counters, shared by every generation (see src/metrics.ts). */
|
|
160
165
|
readonly metrics: Metrics
|
|
166
|
+
/** Concurrency queues and requests-per-minute windows (see src/limits.ts). */
|
|
167
|
+
readonly limits: LimitState
|
|
161
168
|
/** When this runtime was created, epoch ms. */
|
|
162
169
|
readonly startedAt: number
|
|
170
|
+
/** The response cache store (src/cache.ts) and the fingerprint of the config and `apps` policy it was built for. */
|
|
171
|
+
cache?: { readonly fingerprint: string; readonly store: CacheStore }
|
|
163
172
|
listener?: Listener
|
|
164
173
|
/** Present while a fiber is subscribed to the server event bus. */
|
|
165
174
|
pump?: Pump
|
|
@@ -171,6 +180,7 @@ const REJECTED: Registration = { accepted: false, dispose() {}, confirmed: () =>
|
|
|
171
180
|
const detachedSessions = createSessionStore()
|
|
172
181
|
const detachedMetrics = createMetrics()
|
|
173
182
|
const detachedStartedAt = Date.now()
|
|
183
|
+
const detachedLimits = createLimitState()
|
|
174
184
|
let foreignVersionLogged = false
|
|
175
185
|
|
|
176
186
|
/** The process runtime, created on first use; undefined when a different version already owns the process. */
|
|
@@ -186,6 +196,7 @@ function runtime(): Runtime | undefined {
|
|
|
186
196
|
rejected: new Map(),
|
|
187
197
|
sweep: { nextAt: 0, running: false },
|
|
188
198
|
metrics: createMetrics(),
|
|
199
|
+
limits: createLimitState(),
|
|
189
200
|
startedAt: Date.now(),
|
|
190
201
|
}
|
|
191
202
|
slot[KEY] = created
|
|
@@ -261,6 +272,42 @@ export const sessionStore = (): SessionStore => runtime()?.sessions ?? detachedS
|
|
|
261
272
|
/** The metrics registry shared by every generation in the process (a detached one under a foreign-version runtime). */
|
|
262
273
|
export const processMetrics = (): Metrics => runtime()?.metrics ?? detachedMetrics
|
|
263
274
|
|
|
275
|
+
/** The request limiter's state shared by every generation in the process (detached under a foreign-version runtime). */
|
|
276
|
+
export const processLimits = (): LimitState => runtime()?.limits ?? detachedLimits
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Wraps `caller` with the response cache when option `cache` is set (a no-op without it). The store
|
|
280
|
+
* lives on the runtime, so it survives plugin generations. It is replaced when the cache option or
|
|
281
|
+
* any policy input (`apps`, `models`, `aliases`, `fallbacks`) differs from the one it was built for:
|
|
282
|
+
* the key does not cover policy, and a hit skips the session caller's scope checks. The store is
|
|
283
|
+
* resolved on the first call, not when the wrapper is built: a rejected generation is built too but
|
|
284
|
+
* never serves, and must not replace the live store. Under a foreign-version runtime the store is
|
|
285
|
+
* private to the wrapper.
|
|
286
|
+
*/
|
|
287
|
+
export function withResponseCache(caller: ModelCaller, options: PlugboardOptions): ModelCaller {
|
|
288
|
+
const config = options.cache
|
|
289
|
+
if (!config) return caller
|
|
290
|
+
let wrapped: ModelCaller | undefined
|
|
291
|
+
const build = (): ModelCaller => {
|
|
292
|
+
const rt = runtime()
|
|
293
|
+
const fingerprint = canonicalJson({
|
|
294
|
+
cache: config,
|
|
295
|
+
apps: options.apps,
|
|
296
|
+
models: options.models,
|
|
297
|
+
aliases: options.aliases,
|
|
298
|
+
fallbacks: options.fallbacks,
|
|
299
|
+
})
|
|
300
|
+
let held = rt?.cache
|
|
301
|
+
if (!held || held.fingerprint !== fingerprint) {
|
|
302
|
+
held = { fingerprint, store: createCacheStore({ ttlMs: config.ttlSeconds * 1000, maxEntries: config.maxEntries, maxBytes: config.maxBytes }) }
|
|
303
|
+
if (rt) rt.cache = held
|
|
304
|
+
}
|
|
305
|
+
const count = options.metrics ? (app: string, result: "hit" | "miss") => processMetrics().cache(app, result) : undefined
|
|
306
|
+
return cachedCaller(caller, config, held.store, count)
|
|
307
|
+
}
|
|
308
|
+
return { stream: (request, context) => (wrapped ??= build()).stream(request, context) }
|
|
309
|
+
}
|
|
310
|
+
|
|
264
311
|
/** What `/v1/status` and the discovery pages report about the process, besides the model catalog. */
|
|
265
312
|
export interface RuntimeStatus extends RuntimeSnapshot {
|
|
266
313
|
readonly runtimeVersion: number
|