Repository navigation
Description
[v2] Plugin API: no way to read token usage in 2.0.x — DCP-style context-pruning plugins are dead; please re-expose it
On opencode 2.0.16 (verified on the binary bundled in the OpenChamber 2.0.16 AppImage, opencode serve), the plugin API no longer exposes any token-usage data, while the HTTP API still does. This makes token-aware plugins — most notably @tarquinen/opencode-dcp (dynamic context pruning: nudges + auto-compression at context-limit thresholds) — non-functional: its token counter reads ~0, so isContextOverLimits() never goes true and no nudge or auto-compression ever triggers at any context % (even past 100k tokens). Tracked upstream at Tarquinen/opencode-dynamic-context-pruning#618 (comment 2026-09-25).
What the 2.0.16 plugin API exposes (probe-verified)
A probe plugin ({id, setup} export) inspecting the API surface found:
| API | 2.0.16 shape | token data? |
|---|---|---|
ctx.session.context({sessionID}) |
flat minimal entries: id/time/type/text (+description/outcome); observed types user, idle, synthetic |
❌ no assistant entries at all, no tokens, no content |
native event.messages in the session.context hook |
id/role/content only |
❌ no tokens/usage fields |
ctx.session.* methods |
hook, create, get, switchAgent, switchModel, prompt, generate, command, synthetic, interrupt, update, move, wait, context |
no messages() method |
chat.message / chat.transform hooks |
absent from the 2.0.16 binary | — |
For comparison, on 2.0.12 session.context() returned rich entries (assistant entries with content + tokens), which is the shape DCP 3.2.0 was built against (peer dep @opencode-ai/plugin >=1.18.29).
The data still exists — HTTP API asymmetry
On the same 2.0.16 server:
GET /api/session/:id/messagereturns assistant entries withcontentandtokens: {input, output, reasoning, cache:{read, write}}(e.g. a real turn:{"input":611,"output":6275,"cache":{"read":110651}});GET /api/session/:idreturns session-level aggregatetokens.
So usage is fully tracked server-side; only the plugin surface lost access to it.
What breaks (concrete)
- DCP:
getCurrentTokenUsage()sumsmsg.info.tokensover the projected view → all zero →contextLimitAnchors/turnNudgeAnchors/iterationNudgeAnchorsinstorage/plugin/dcp/<session>.jsonstay empty forever; manual/dcp-compressstill works (different code path). - Any other plugin doing token budgeting, context-% gating, or usage-based routing (e.g. model escalation at N tokens).
- Plugins can currently only estimate usage (local tokenizer over
event.messagescontent), which is workable but inaccurate (no cache accounting, no reasoning tokens) and expensive to recompute per hook.
Ask
Re-expose token usage to the plugin API. Ranked options:
ctx.session.messages({sessionID})returning the same per-message shape asGET /api/session/:id/message(assistant entries withcontent+tokens) — restores the 2.0.12 contract DCP and other plugins were built against.- Richer
ctx.session.context()— include assistant entries withtokens(and ideallycontent) alongside the currentuser/idle/syntheticentries. - Session aggregate only — expose the
GET /api/session/:idaggregatetokensthrough the plugin API (e.g. onctx.session.get({sessionID})); enough for context-limit nudging (what DCP's nudge layer needs), though per-message usage is still needed for fine-grained pruning decisions. - Or simply add
tokens/usageto the nativeevent.messagesobjects in thesession.contexthook.
Any of these un-breaks the plugin; option 1 (or 2) is the drop-in fix.
Minimal repro
export default {
id: "token-probe",
setup: (ctx) => {
ctx.session.hook("context", async (event) => {
const entries = await ctx.session.context({ sessionID: event.sessionID })
console.log(
"entry types:", [...new Set(entries.map((e) => e.type))],
"entry keys:", Object.keys(entries[0] ?? {}),
"native msg keys:", Object.keys(event.messages[0] ?? {}),
"session methods:", Object.keys(ctx.session),
)
// 2.0.16: types=[user,idle,synthetic], keys=[id,time,type,text],
// native keys=[id,role,content] — no tokens anywhere
})
},
}Run against any session with assistant turns on 2.0.16 (opencode serve, auth opencode:$PW), and compare GET /api/session/:id + .../message from the same server.
References
- V2 plugin support (OpenCode 2.x) Tarquinen/opencode-dynamic-context-pruning#618 — "V2 plugin support (OpenCode 2.x)" (closed by author as fixed in 3.2.0; 2.0.16 comment documents the regression)
- OpenCode 2.x: embedded plugin is inert — v1 plugin API removed; port proposed (code in fork branch) zdk/lowfat#20, opencode 2.0.16: context event tools have no execute() — plugin inert; port to tool.hook execute.before (+ snip is a wrapper, not a pipe filter) carson2222/opencode-smartsnip#4 — sibling 2.0.16 plugin-API breakages (tool
executeremoval), filed the same day for context - @tarquinen/opencode-dcp 3.2.0 source:
lib/v2/messages.ts(project()),lib/token-utils.ts(getCurrentTokenUsage)
Verification
- I searched existing issues before filing: [FEATURE] Session metrics and lifecycle hooks for plugin analytics #23454 and feat: real-time output speed (tok/s) indicator — expose statusline/usage-delta to plugins #48067 (both CLOSED) are the closest prior requests.
This request is distinct: opencode 2.0.x REMOVED token data from the plugin API —session.context()now returns minimal entries (id/time/type/text; no assistant entries, no tokens) and there is nosession.messages()— so existing plugins built against the 2.0.12-era API (DCP 3.2.0) are non-functional.
System
- OS: Garuda Linux (Arch-based), x86_64
- opencode: 2.0.16 (OpenChamber AppImage bundled binary,
opencode serve) - Provider: @opencode/ai/providers/openai-compatible -> llama-swap / llama-server v1 (http://192.168.2.215:11436/v1)
Activity
[-][v2] Plugin API: no way to read token usage in 2.0.x — DCP-style context-pruning plugins are dead; please re-expose it[/-][+][FEATURE]: [v2] Plugin API: no way to read token usage in 2.0.x — DCP-style context-pruning plugins are dead; please re-expose it[/+]github-actions commentedon Sep 25, 2026
Thanks for updating the issue with the requested Feature Request title, verification checkbox, and system details.
famewolf commentedon Sep 25, 2026
Closing this as no longer needed — the practical blocker it described has a working path.
Why it's no longer needed: DCP (dynamic-context-pruning) 3.2.0 now works on 2.0.16. Its token-usage path is the plugin-SDK REST call
client.session.messages, which returns per-messagetokenson 2.0.16 even though the synchronouscontextevent no longer exposes token fields. I've verified this live in a real session: DCP performed 16 compression nudges and pruned ~280k tokens end-to-end on 2.0.16. So the original premise — "DCP-style context-pruning plugins are dead on 2.0.x because there's no token signal" — no longer holds: the signal is reachable via the REST path.What's actually lost in 2.0.x: only a synchronous token field on the
contextevent itself. That's a nice-to-have (lets a plugin read tokens inline without a REST round-trip), not a functional blocker — the REST path fully covers the use case.If you'd still like the sync field re-exposed on the
contextevent for convenience, I'm happy to re-open or spin a smaller issue scoped to that. Otherwise closing.