opencode-plugboard 0.21.1 → 0.22.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.
Files changed (3) hide show
  1. package/README.md +24 -2
  2. package/package.json +2 -1
  3. package/src/usage.ts +24 -23
package/README.md CHANGED
@@ -43,7 +43,7 @@ export PLUGBOARD_KEY_MY_APP=choose-a-long-random-string
43
43
  {
44
44
  "plugins": [
45
45
  {
46
- "package": "opencode-plugboard@0.21.1",
46
+ "package": "opencode-plugboard@0.22.0",
47
47
  "options": {
48
48
  "keys": { "my-app": "{env:PLUGBOARD_KEY_MY_APP}" },
49
49
  "models": ["anthropic/claude-sonnet-4-6", "github-copilot/*", "opencode-go/glm-5.3-flash"],
@@ -390,7 +390,7 @@ A server shared by a Discord bot, n8n, Open WebUI and a coding agent, with most
390
390
  {
391
391
  "plugins": [
392
392
  {
393
- "package": "opencode-plugboard@0.21.1",
393
+ "package": "opencode-plugboard@0.22.0",
394
394
  "options": {
395
395
  // Reachable from the LAN. Non-loopback hosts refuse to start without keys.
396
396
  "listen": { "host": "10.0.1.50", "port": 4010 },
@@ -460,6 +460,17 @@ One page per client, with the config, a chat, a tool call, structured output and
460
460
  | pydantic-ai | [pydantic-ai][client-pydantic-ai] | pass with `OpenAIChatModel` |
461
461
  | Open WebUI | [open-webui][client-open-webui] | pass over its HTTP API; no browser, no structured output in its UI |
462
462
  | n8n | [n8n][client-n8n] | pass with "Use Responses API" off |
463
+ | Claude Code | [claude-code][client-claude-code] | pass: text and a tool turn through `/v1/messages`; needs `maxTools` above 36 |
464
+ | Codex CLI | [codex-cli][client-codex-cli] | docs only, not run yet (Responses API) |
465
+ | Continue | [continue][client-continue] | docs only, not run yet |
466
+ | Kilo Code | [kilo-code][client-kilo-code] | docs only, not run yet |
467
+ | LibreChat | [librechat][client-librechat] | docs only, not run yet |
468
+ | Zed | [zed][client-zed] | docs only, not run yet |
469
+ | OpenAI Agents SDK (Python) | [openai-agents][client-openai-agents] | pass on Responses and Chat Completions: text, stream, tools, structured output |
470
+ | LiteLLM (as a client) | [litellm-client][client-litellm] | pass: text, stream, tools |
471
+ | Instructor | [instructor][client-instructor] | pass: TOOLS, JSON_SCHEMA and partial streaming modes |
472
+ | DSPy | [dspy][client-dspy] | pass: Predict, ChainOfThought, typed output |
473
+ | Aider | [aider][client-aider] | pass: one scripted `--message` run |
463
474
  | Anything else | [any-app][client-any-app] | base URL, key, model id |
464
475
 
465
476
  Each run drove the client's own SDK or API against a dev server at an earlier commit: a plain chat, a tool round trip, streaming, an image and structured output on `fast`, a Claude model and a ChatGPT model. A pass is not a clean sweep, and the notes say where it was not. The latest full run is in [compat/results/release-candidate.md][release-candidate].
@@ -538,4 +549,15 @@ Not affiliated with or endorsed by OpenAI, Anthropic, Google, GitHub or Microsof
538
549
  [client-pydantic-ai]: https://jjjermiah.github.io/opencode-plugboard/clients/pydantic-ai
539
550
  [client-open-webui]: https://jjjermiah.github.io/opencode-plugboard/clients/open-webui
540
551
  [client-n8n]: https://jjjermiah.github.io/opencode-plugboard/clients/n8n
552
+ [client-claude-code]: https://jjjermiah.github.io/opencode-plugboard/clients/claude-code
553
+ [client-codex-cli]: https://jjjermiah.github.io/opencode-plugboard/clients/codex-cli
554
+ [client-continue]: https://jjjermiah.github.io/opencode-plugboard/clients/continue
555
+ [client-kilo-code]: https://jjjermiah.github.io/opencode-plugboard/clients/kilo-code
556
+ [client-librechat]: https://jjjermiah.github.io/opencode-plugboard/clients/librechat
557
+ [client-zed]: https://jjjermiah.github.io/opencode-plugboard/clients/zed
558
+ [client-openai-agents]: https://jjjermiah.github.io/opencode-plugboard/clients/openai-agents
559
+ [client-litellm]: https://jjjermiah.github.io/opencode-plugboard/clients/litellm-client
560
+ [client-instructor]: https://jjjermiah.github.io/opencode-plugboard/clients/instructor
561
+ [client-dspy]: https://jjjermiah.github.io/opencode-plugboard/clients/dspy
562
+ [client-aider]: https://jjjermiah.github.io/opencode-plugboard/clients/aider
541
563
  [client-any-app]: https://jjjermiah.github.io/opencode-plugboard/clients/any-app
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-plugboard",
3
- "version": "0.21.1",
3
+ "version": "0.22.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Use the ChatGPT, Claude, Copilot and OpenCode Go logins in your OpenCode server from any app: an OpenCode V2 plugin serving OpenAI, Anthropic and Gemini compatible APIs.",
@@ -32,6 +32,7 @@
32
32
  "flake-hunt": "bun .github/scripts/flake-hunt.ts",
33
33
  "coverage-manifest": "bun .github/scripts/coverage-manifest.ts",
34
34
  "live-matrix": "bun test/live/matrix.ts",
35
+ "compat:python": "uv run --project compat/py python compat/py/clients/run_all.py",
35
36
  "build:tui": "bun .github/scripts/build-tui.ts",
36
37
  "prepack": "bun run build:tui",
37
38
  "smoke:pack": "bun .github/scripts/smoke-pack.ts",
package/src/usage.ts CHANGED
@@ -9,10 +9,11 @@
9
9
  * Where it lives: the live aggregate is in memory on the process runtime (`src/runtime.ts`), so it
10
10
  * survives a plugin generation, and each day is persisted to the plugin's host storage as JSON under
11
11
  * `usage/<app>/<YYYY-MM-DD>`. Every write comes from request handling (`withUsage` and the route's
12
- * request log point), never from plugin setup or a finalizer (docs/architecture.md, I23). A day is
13
- * written at most every `FLUSH_MS`: a change is persisted by the first request that finds the last
14
- * write that old, so the stored copy can lag a quiet period behind memory. Queries read memory first,
15
- * so a report is never stale.
12
+ * request log point), never from plugin setup or a finalizer (docs/architecture.md, I23). A change is
13
+ * written as it lands; while a write of that day is in flight, later changes wait for it and go out
14
+ * in the next write, so the stored copy keeps up with every change (#241: a debounce kept the request
15
+ * count of a burst in memory, and a restart dropped it). Queries read memory first, so a
16
+ * report is never stale.
16
17
  *
17
18
  * Retention: days older than `usage.retentionDays` are deleted from request handling, one bounded
18
19
  * page of keys at a time and at most once per `PRUNE_EVERY_MS`.
@@ -48,8 +49,6 @@ export const GENERATION_ROUTES: ReadonlySet<string> = new Set([
48
49
 
49
50
  const DAY_MS = 86_400_000
50
51
  const KEY_PREFIX = "usage/"
51
- /** A changed day is written at most this often. */
52
- const FLUSH_MS = 10_000
53
52
  /** Retention runs at most this often, and each run reads one page of keys. */
54
53
  const PRUNE_EVERY_MS = 10 * 60_000
55
54
  const PRUNE_PAGE = 200
@@ -417,7 +416,6 @@ interface Entry {
417
416
  /** Bumped by every change; `flushedVersion` is the version last persisted. */
418
417
  version: number
419
418
  flushedVersion: number
420
- flushedAt: number
421
419
  flushing?: Promise<void>
422
420
  }
423
421
 
@@ -517,28 +515,31 @@ export function createUsageRecorder(deps: UsageRecorderDeps): UsageRecorder {
517
515
  // Another request may have loaded the day while this one read storage: keep the first entry.
518
516
  const current = state.entries.get(key)
519
517
  if (current) return current
520
- const entry: Entry = { key, app, date, day: stored ?? emptyDay(), version: 0, flushedVersion: 0, flushedAt: 0 }
518
+ const entry: Entry = { key, app, date, day: stored ?? emptyDay(), version: 0, flushedVersion: 0 }
521
519
  state.entries.set(key, entry)
522
520
  return entry
523
521
  }
524
522
 
525
- /** Persists a changed day when the last write is older than `FLUSH_MS`. The JSON is taken now; a later change waits for the next write. */
526
- function flushIfDue(entry: Entry, at: number): void {
527
- if (entry.version === entry.flushedVersion || entry.flushing !== undefined || at - entry.flushedAt < FLUSH_MS) return
523
+ /**
524
+ * Persists a changed day. The JSON is taken now. A write already in flight is not raced: when it lands,
525
+ * a change made meanwhile is written next. A failed write is logged and the day stays changed, so the
526
+ * next change tries again.
527
+ */
528
+ function flushIfDue(entry: Entry): void {
529
+ if (entry.version === entry.flushedVersion || entry.flushing !== undefined) return
528
530
  const version = entry.version
529
531
  const json = JSON.stringify(entry.day)
530
- entry.flushedAt = at
531
- entry.flushing = storage
532
- .put(entry.key, json)
533
- .then(
534
- () => {
535
- entry.flushedVersion = version
536
- },
537
- (error) => logError("usage.flush_failed", { app: entry.app, date: entry.date, error: errorMessage(error) }),
538
- )
539
- .finally(() => {
532
+ entry.flushing = storage.put(entry.key, json).then(
533
+ () => {
534
+ entry.flushedVersion = version
540
535
  entry.flushing = undefined
541
- })
536
+ flushIfDue(entry)
537
+ },
538
+ (error) => {
539
+ entry.flushing = undefined
540
+ logError("usage.flush_failed", { app: entry.app, date: entry.date, error: errorMessage(error) })
541
+ },
542
+ )
542
543
  }
543
544
 
544
545
  /** Deletes the days older than the retention window: one page of keys per run, at most once per `PRUNE_EVERY_MS`. */
@@ -576,7 +577,7 @@ export function createUsageRecorder(deps: UsageRecorderDeps): UsageRecorder {
576
577
  if (!entry) return
577
578
  update(entry.day)
578
579
  entry.version++
579
- flushIfDue(entry, at)
580
+ flushIfDue(entry)
580
581
  maybePrune(at)
581
582
  })
582
583
  .catch((error) => logError("usage.record_failed", { app, error: errorMessage(error) }))