imbue-mngr-opencode-usage 0.1.0__tar.gz
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.
- imbue_mngr_opencode_usage-0.1.0/.gitignore +364 -0
- imbue_mngr_opencode_usage-0.1.0/CHANGELOG.md +13 -0
- imbue_mngr_opencode_usage-0.1.0/PKG-INFO +61 -0
- imbue_mngr_opencode_usage-0.1.0/README.md +51 -0
- imbue_mngr_opencode_usage-0.1.0/UNABRIDGED_CHANGELOG.md +13 -0
- imbue_mngr_opencode_usage-0.1.0/changelog/.gitkeep +0 -0
- imbue_mngr_opencode_usage-0.1.0/conftest.py +7 -0
- imbue_mngr_opencode_usage-0.1.0/imbue/mngr_opencode_usage/__init__.py +0 -0
- imbue_mngr_opencode_usage-0.1.0/imbue/mngr_opencode_usage/aggregate_usage_source_test.py +56 -0
- imbue_mngr_opencode_usage-0.1.0/imbue/mngr_opencode_usage/conftest.py +3 -0
- imbue_mngr_opencode_usage-0.1.0/imbue/mngr_opencode_usage/cross_boundary_test.py +21 -0
- imbue_mngr_opencode_usage-0.1.0/imbue/mngr_opencode_usage/plugin.py +90 -0
- imbue_mngr_opencode_usage-0.1.0/imbue/mngr_opencode_usage/plugin_test.py +76 -0
- imbue_mngr_opencode_usage-0.1.0/imbue/mngr_opencode_usage/resources/__init__.py +0 -0
- imbue_mngr_opencode_usage-0.1.0/imbue/mngr_opencode_usage/resources/mngr_opencode_usage_plugin.ts +116 -0
- imbue_mngr_opencode_usage-0.1.0/imbue/mngr_opencode_usage/resources/mngr_opencode_usage_plugin_test.py +143 -0
- imbue_mngr_opencode_usage-0.1.0/imbue/mngr_opencode_usage/test_ratchets.py +278 -0
- imbue_mngr_opencode_usage-0.1.0/pyproject.toml +71 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
**/__pycache__/
|
|
3
|
+
**/*.py[codz]
|
|
4
|
+
**/*$py.class
|
|
5
|
+
|
|
6
|
+
# macOS Finder metadata
|
|
7
|
+
**/.DS_Store
|
|
8
|
+
|
|
9
|
+
# C extensions
|
|
10
|
+
**/*.so
|
|
11
|
+
|
|
12
|
+
# Distribution / packaging
|
|
13
|
+
**/.Python
|
|
14
|
+
**/build/
|
|
15
|
+
**/develop-eggs/
|
|
16
|
+
**/dist/
|
|
17
|
+
**/downloads/
|
|
18
|
+
**/eggs/
|
|
19
|
+
**/.eggs/
|
|
20
|
+
**/lib/
|
|
21
|
+
**/lib64/
|
|
22
|
+
**/parts/
|
|
23
|
+
**/sdist/
|
|
24
|
+
**/var/
|
|
25
|
+
**/wheels/
|
|
26
|
+
**/share/python-wheels/
|
|
27
|
+
**/*.egg-info/
|
|
28
|
+
**/.installed.cfg
|
|
29
|
+
**/*.egg
|
|
30
|
+
**/MANIFEST
|
|
31
|
+
|
|
32
|
+
# PyInstaller
|
|
33
|
+
# Usually these files are written by a python script from a template
|
|
34
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
35
|
+
**/*.manifest
|
|
36
|
+
**/*.spec
|
|
37
|
+
|
|
38
|
+
# Installer logs
|
|
39
|
+
**/pip-log.txt
|
|
40
|
+
**/pip-delete-this-directory.txt
|
|
41
|
+
|
|
42
|
+
# Unit test / coverage reports
|
|
43
|
+
**/htmlcov/
|
|
44
|
+
**/.tox/
|
|
45
|
+
**/.nox/
|
|
46
|
+
**/.coverage
|
|
47
|
+
**/.coverage.*
|
|
48
|
+
**/.cache
|
|
49
|
+
**/nosetests.xml
|
|
50
|
+
**/coverage.xml
|
|
51
|
+
**/*.cover
|
|
52
|
+
**/*.py.cover
|
|
53
|
+
**/.hypothesis/
|
|
54
|
+
**/.pytest_cache/
|
|
55
|
+
**/cover/
|
|
56
|
+
|
|
57
|
+
# Translations
|
|
58
|
+
**/*.mo
|
|
59
|
+
**/*.pot
|
|
60
|
+
|
|
61
|
+
# Django stuff:
|
|
62
|
+
**/*.log
|
|
63
|
+
**/local_settings.py
|
|
64
|
+
**/db.sqlite3
|
|
65
|
+
**/db.sqlite3-journal
|
|
66
|
+
|
|
67
|
+
# Flask stuff:
|
|
68
|
+
**/instance/
|
|
69
|
+
**/.webassets-cache
|
|
70
|
+
|
|
71
|
+
# Scrapy stuff:
|
|
72
|
+
**/.scrapy
|
|
73
|
+
|
|
74
|
+
# Sphinx documentation
|
|
75
|
+
**/docs/_build/
|
|
76
|
+
|
|
77
|
+
# PyBuilder
|
|
78
|
+
**/.pybuilder/
|
|
79
|
+
**/target/
|
|
80
|
+
|
|
81
|
+
# Jupyter Notebook
|
|
82
|
+
**/.ipynb_checkpoints
|
|
83
|
+
|
|
84
|
+
# IPython
|
|
85
|
+
**/profile_default/
|
|
86
|
+
**/ipython_config.py
|
|
87
|
+
|
|
88
|
+
# pyenv
|
|
89
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
90
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
91
|
+
# .python-version
|
|
92
|
+
|
|
93
|
+
# pipenv
|
|
94
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
95
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
96
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
97
|
+
# install all needed dependencies.
|
|
98
|
+
#Pipfile.lock
|
|
99
|
+
|
|
100
|
+
# UV
|
|
101
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
102
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
103
|
+
# commonly ignored for libraries.
|
|
104
|
+
#uv.lock
|
|
105
|
+
|
|
106
|
+
# poetry
|
|
107
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
108
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
109
|
+
# commonly ignored for libraries.
|
|
110
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
111
|
+
#poetry.lock
|
|
112
|
+
#poetry.toml
|
|
113
|
+
|
|
114
|
+
# pdm
|
|
115
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
116
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
117
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
118
|
+
#pdm.lock
|
|
119
|
+
#pdm.toml
|
|
120
|
+
**/.pdm-python
|
|
121
|
+
**/.pdm-build/
|
|
122
|
+
|
|
123
|
+
# pixi
|
|
124
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
125
|
+
#pixi.lock
|
|
126
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
127
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
128
|
+
**/.pixi
|
|
129
|
+
|
|
130
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
131
|
+
**/__pypackages__/
|
|
132
|
+
|
|
133
|
+
# Celery stuff
|
|
134
|
+
**/celerybeat-schedule
|
|
135
|
+
**/celerybeat.pid
|
|
136
|
+
|
|
137
|
+
# SageMath parsed files
|
|
138
|
+
**/*.sage.py
|
|
139
|
+
|
|
140
|
+
# Environments
|
|
141
|
+
**/.env
|
|
142
|
+
**/.envrc
|
|
143
|
+
**/.venv
|
|
144
|
+
**/.venv-*
|
|
145
|
+
**/env/
|
|
146
|
+
**/venv/
|
|
147
|
+
**/ENV/
|
|
148
|
+
**/env.bak/
|
|
149
|
+
**/venv.bak/
|
|
150
|
+
|
|
151
|
+
# Spyder project settings
|
|
152
|
+
**/.spyderproject
|
|
153
|
+
**/.spyproject
|
|
154
|
+
|
|
155
|
+
# Rope project settings
|
|
156
|
+
**/.ropeproject
|
|
157
|
+
|
|
158
|
+
# mkdocs documentation
|
|
159
|
+
**/site
|
|
160
|
+
|
|
161
|
+
# mypy
|
|
162
|
+
**/.mypy_cache/
|
|
163
|
+
**/.dmypy.json
|
|
164
|
+
**/dmypy.json
|
|
165
|
+
|
|
166
|
+
# Pyre type checker
|
|
167
|
+
**/.pyre/
|
|
168
|
+
|
|
169
|
+
# pytype static type analyzer
|
|
170
|
+
**/.pytype/
|
|
171
|
+
|
|
172
|
+
# Cython debug symbols
|
|
173
|
+
**/cython_debug/
|
|
174
|
+
|
|
175
|
+
# PyCharm
|
|
176
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
177
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
178
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
179
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
180
|
+
**/.idea/
|
|
181
|
+
|
|
182
|
+
# Abstra
|
|
183
|
+
# Abstra is an AI-powered process automation framework.
|
|
184
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
185
|
+
# Learn more at https://abstra.io/docs
|
|
186
|
+
**/.abstra/
|
|
187
|
+
|
|
188
|
+
# Visual Studio Code
|
|
189
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
190
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
191
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
192
|
+
# you could uncomment the following to ignore the entire vscode folder
|
|
193
|
+
# .vscode/
|
|
194
|
+
|
|
195
|
+
# Ruff stuff:
|
|
196
|
+
**/.ruff_cache/
|
|
197
|
+
|
|
198
|
+
# PyPI configuration file
|
|
199
|
+
**/.pypirc
|
|
200
|
+
|
|
201
|
+
# Cursor
|
|
202
|
+
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
|
203
|
+
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
|
204
|
+
# refer to https://docs.cursor.com/context/ignore-files
|
|
205
|
+
**/.cursorignore
|
|
206
|
+
**/.cursorindexingignore
|
|
207
|
+
|
|
208
|
+
# Marimo
|
|
209
|
+
**/marimo/_static/
|
|
210
|
+
**/marimo/_lsp/
|
|
211
|
+
**/__marimo__/
|
|
212
|
+
|
|
213
|
+
# task folders for local custom claude workflow (at any depth, incl. the
|
|
214
|
+
# root-level `dev` project's `dev/_tasks/`, which `*/*/_tasks/` missed)
|
|
215
|
+
**/_tasks/
|
|
216
|
+
**/.sesskey
|
|
217
|
+
|
|
218
|
+
# Node.js
|
|
219
|
+
**/node_modules/
|
|
220
|
+
|
|
221
|
+
# Frontend build artifacts
|
|
222
|
+
**/frontend-dist/
|
|
223
|
+
|
|
224
|
+
# Claude Code local settings (session-specific permissions)
|
|
225
|
+
**/.claude/settings.local.json
|
|
226
|
+
|
|
227
|
+
# Local Claude settings backup files
|
|
228
|
+
**/.claude/*.bak
|
|
229
|
+
|
|
230
|
+
# Claude worktress
|
|
231
|
+
**/.claude/worktrees
|
|
232
|
+
|
|
233
|
+
# Agent definitions deployed by mngr_subagent_proxy at session start
|
|
234
|
+
**/.claude/agents/
|
|
235
|
+
|
|
236
|
+
# Local mngr settings (not committed)
|
|
237
|
+
**/.mngr/settings.local.toml
|
|
238
|
+
|
|
239
|
+
# Local working / scratch markdown (not committed)
|
|
240
|
+
**/*.local.md
|
|
241
|
+
|
|
242
|
+
# Local working / scratch shell scripts (not committed)
|
|
243
|
+
# (covers user notification scripts like scripts/notify_user.local.sh, too)
|
|
244
|
+
**/*.local.sh
|
|
245
|
+
|
|
246
|
+
# Local-only throwaway scripts (e.g. one-off test harnesses).
|
|
247
|
+
**/scripts/*.local.sh
|
|
248
|
+
|
|
249
|
+
# Local working / scratch Python + text (not committed; e.g. one-off validation
|
|
250
|
+
# harnesses or probe scripts). Mirrors the *.local.md / *.local.sh convention.
|
|
251
|
+
**/*.local.py
|
|
252
|
+
**/*.local.txt
|
|
253
|
+
|
|
254
|
+
# Test output files (slow tests, coverage summaries)
|
|
255
|
+
**/.test_output/
|
|
256
|
+
|
|
257
|
+
# TMR output directories (reports + test outputs)
|
|
258
|
+
**/tmr_*/
|
|
259
|
+
**/tmr-report/
|
|
260
|
+
|
|
261
|
+
# Active session marker file (used to detect interrupted sessions)
|
|
262
|
+
**/.claude/active
|
|
263
|
+
|
|
264
|
+
# we stick the image build artifacts here
|
|
265
|
+
**/.mngr/dev/build/
|
|
266
|
+
**/.mngr/dev/secrets/
|
|
267
|
+
|
|
268
|
+
# Offload caches and local files
|
|
269
|
+
**/.offload/**
|
|
270
|
+
**/test-results
|
|
271
|
+
|
|
272
|
+
# Minds deploy-time build artifacts.
|
|
273
|
+
# Ignore contents so we can un-ignore the committed `template/` subtree: git
|
|
274
|
+
# won't descend into a fully-ignored directory, so negations must live below
|
|
275
|
+
# the contents pattern rather than below the directory pattern.
|
|
276
|
+
**/.minds/*
|
|
277
|
+
# The template directory is committed: it defines the expected keys for each
|
|
278
|
+
# <service>.sh file, with empty values. `scripts/push_modal_secrets.py` uses
|
|
279
|
+
# it to check that per-env .sh files declare every template key.
|
|
280
|
+
!/.minds/template/
|
|
281
|
+
!/.minds/template/**
|
|
282
|
+
# Vault ACL policies for the minds tiers. The HCL is non-sensitive and is
|
|
283
|
+
# the source of truth for the policies pushed to Vault via
|
|
284
|
+
# `vault policy write <name> .minds/policies/<name>.hcl`.
|
|
285
|
+
!/.minds/policies/
|
|
286
|
+
!/.minds/policies/**
|
|
287
|
+
|
|
288
|
+
# Autofix session artifacts
|
|
289
|
+
**/.autofix/
|
|
290
|
+
**/.reviewer/logs/
|
|
291
|
+
**/.reviewer/outputs/
|
|
292
|
+
**/.reviewer/settings.local.json
|
|
293
|
+
**/.reviewer/.stop_hook_consecutive_blocks
|
|
294
|
+
**/.reviewer/conversation-issues.jsonl
|
|
295
|
+
**/.reviews/
|
|
296
|
+
|
|
297
|
+
# Stuck agent tracking (stop hook escape valve)
|
|
298
|
+
**/.claude/blocked_stop_commits
|
|
299
|
+
|
|
300
|
+
# Scheduled tasks lock file
|
|
301
|
+
**/.claude/scheduled_tasks.lock
|
|
302
|
+
|
|
303
|
+
# Demo recordings (asciinema .cast, .txt, and scripts)
|
|
304
|
+
**/.demos/
|
|
305
|
+
|
|
306
|
+
# for git worktrees from other repos
|
|
307
|
+
**/.external_worktrees/
|
|
308
|
+
|
|
309
|
+
# ignore coverage files generated within subprojects
|
|
310
|
+
**/cov.xml
|
|
311
|
+
|
|
312
|
+
# ignore demos
|
|
313
|
+
**/recordings
|
|
314
|
+
apps/minds_workspace_server/package-lock.json
|
|
315
|
+
**/*.local.md
|
|
316
|
+
|
|
317
|
+
# Generated by the _generate-dockerignore justfile recipe before each
|
|
318
|
+
# test-offload* run. The recipe strips this self-reference so offload's
|
|
319
|
+
# copy_untracked_files ships the generated file into the build context.
|
|
320
|
+
/.dockerignore
|
|
321
|
+
|
|
322
|
+
# Transient offload image-cache marker written by test-offload runs; not
|
|
323
|
+
# checked in (unlike the committed .offload-*-cache-key files which pin CI
|
|
324
|
+
# image versions).
|
|
325
|
+
/.offload-image-cache
|
|
326
|
+
|
|
327
|
+
# Generated by _generate-release-dockerfile from Dockerfile + .release.extras.
|
|
328
|
+
/libs/mngr/imbue/mngr/resources/Dockerfile.release
|
|
329
|
+
**/.reviewer/autofix/issues/
|
|
330
|
+
.reviewer/logs/
|
|
331
|
+
|
|
332
|
+
# Tailwind Play CDN JS for the minds desktop client. Fetched once via
|
|
333
|
+
# `just minds-tailwind` (plain curl, no build) and shipped with the
|
|
334
|
+
# distributable. Not checked in because it's a ~300KB blob we can always
|
|
335
|
+
# re-fetch.
|
|
336
|
+
/apps/minds/imbue/minds/desktop_client/static/tailwind.js
|
|
337
|
+
|
|
338
|
+
# macOS Finder metadata
|
|
339
|
+
**/.DS_Store
|
|
340
|
+
**/.env.swp
|
|
341
|
+
# Vim swap files for any file (`.foo.swp`, `.foo.swo`, etc.) -- generated
|
|
342
|
+
# while a file is open in vim. Never checked in.
|
|
343
|
+
**/.*.sw[a-p]
|
|
344
|
+
|
|
345
|
+
# `minds env deploy` writes a recover-target file at the monorepo root
|
|
346
|
+
# on failed deploys; `minds env recover` deletes it after rolling back.
|
|
347
|
+
# Never checked in. Per-env naming
|
|
348
|
+
# (``.minds-deploy-recover-target-<env-name>.json``) means each in-flight
|
|
349
|
+
# deploy has its own file, so concurrent deploys against different envs
|
|
350
|
+
# don't trip over each other.
|
|
351
|
+
**/.minds-deploy-*.json
|
|
352
|
+
# Per-env deploy lock files (kernel-level ``flock`` targets). Created
|
|
353
|
+
# by ``hold_deploy_lock`` in ``apps/minds/imbue/minds/envs/recover.py``
|
|
354
|
+
# and held for the entire duration of ``minds env deploy`` /
|
|
355
|
+
# ``minds env recover`` so two concurrent invocations against the same
|
|
356
|
+
# env serialize. The file itself is empty and cheap to leave around.
|
|
357
|
+
**/.minds-deploy-lock-*.lock
|
|
358
|
+
**/.antigravitycli/
|
|
359
|
+
|
|
360
|
+
# Playwright MCP scratch artifacts (mock screenshots, page snapshots)
|
|
361
|
+
**/.playwright-mcp/
|
|
362
|
+
|
|
363
|
+
# tmr (task-runner) orchestration scratch report
|
|
364
|
+
**/tmr-report/
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog - mngr_opencode_usage
|
|
2
|
+
|
|
3
|
+
A concise, human-friendly summary of changes for the `mngr_opencode_usage` library. Entries are categorized using the [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) categories: Added, Changed, Deprecated, Removed, Fixed, Security.
|
|
4
|
+
|
|
5
|
+
For the full, unedited changelog entries, see [UNABRIDGED_CHANGELOG.md](UNABRIDGED_CHANGELOG.md).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [v0.1.0] - 2026-06-16
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Added: New package `imbue-mngr-opencode-usage` providing cost/usage tracking for opencode agents in `mngr usage`. Installs a second in-process opencode plugin (alongside the lifecycle one) that appends one `cost_snapshot` event per assistant message (opencode's reported per-message cost, plus tokens and the provider-qualified model) to `events/opencode/usage/events.jsonl`. A reader hookimpl claims the `opencode` source and aggregates session-incrementally. After provisioning, an opencode agent's spend shows up in `mngr usage` like Claude's.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: imbue-mngr-opencode-usage
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: OpenCode usage data provider for `mngr usage` (per-agent in-process plugin that captures per-message cost + tokens)
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Requires-Dist: imbue-mngr-opencode==0.2.16
|
|
7
|
+
Requires-Dist: imbue-mngr-usage==0.1.5
|
|
8
|
+
Requires-Dist: imbue-mngr==0.2.16
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# imbue-mngr-opencode-usage
|
|
12
|
+
|
|
13
|
+
OpenCode data provider for `mngr usage`. Single responsibility: install a second
|
|
14
|
+
in-process TypeScript plugin into each OpenCode agent so each assistant message
|
|
15
|
+
appends one event to `$MNGR_AGENT_STATE_DIR/events/opencode/usage/events.jsonl`.
|
|
16
|
+
The `mngr usage` CLI walks those events files itself (see `imbue-mngr-usage`).
|
|
17
|
+
|
|
18
|
+
## How the pieces fit
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
mngr_opencode_usage's on_after_provisioning hookimpl
|
|
22
|
+
└─→ installs <config_dir>/plugin/mngr_opencode_usage_plugin.ts
|
|
23
|
+
(alongside mngr_opencode's lifecycle plugin; OpenCode auto-loads plugin/*.ts)
|
|
24
|
+
|
|
25
|
+
OpenCode serve process, on each assistant message.updated
|
|
26
|
+
└─→ mngr_opencode_usage_plugin.ts
|
|
27
|
+
└─→ events/opencode/usage/events.jsonl (append one cost_snapshot event)
|
|
28
|
+
|
|
29
|
+
mngr usage
|
|
30
|
+
└─→ aggregate_usage_source hookimpl (claims the "opencode" source)
|
|
31
|
+
└─→ aggregate_session_incremental: sum each session's messages
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
All file I/O goes through `host.write_text_file`, so provisioning works for local
|
|
35
|
+
**and** remote agents.
|
|
36
|
+
|
|
37
|
+
## What gets captured
|
|
38
|
+
|
|
39
|
+
OpenCode computes cost and tokens per message and exposes them on each assistant
|
|
40
|
+
`message.updated` event. The writer emits, per message, a self-contained event
|
|
41
|
+
carrying:
|
|
42
|
+
|
|
43
|
+
- `cost.total_cost_usd` -- OpenCode's own per-message cost (provenance is
|
|
44
|
+
**REPORTED**; no estimation needed).
|
|
45
|
+
- `tokens` -- `{input, output, cache_read, cache_creation}` (reasoning folded
|
|
46
|
+
into output), for auditability.
|
|
47
|
+
- `model` -- the provider-qualified `<providerID>/<modelID>`.
|
|
48
|
+
- `session_id` + `message_id` -- the reader sums per session, keeping the freshest
|
|
49
|
+
event per `message_id` so a streaming message's re-fires collapse to its final
|
|
50
|
+
reading.
|
|
51
|
+
|
|
52
|
+
## Why session-incremental
|
|
53
|
+
|
|
54
|
+
OpenCode reports each message's **own** cost, not a session running total. So the
|
|
55
|
+
writer is stateless -- it appends one self-contained event per message and keeps
|
|
56
|
+
nothing in memory. The reader (`aggregate_session_incremental`) recomputes each
|
|
57
|
+
session's total from the append-only log, which makes it robust across
|
|
58
|
+
`mngr stop`/`start` (a fresh serve process with empty memory loses no history).
|
|
59
|
+
|
|
60
|
+
`cost_mode` is `API_KEY`: OpenCode bills against a real provider key, so the cost
|
|
61
|
+
is real spend, never an imputed subscription value.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# imbue-mngr-opencode-usage
|
|
2
|
+
|
|
3
|
+
OpenCode data provider for `mngr usage`. Single responsibility: install a second
|
|
4
|
+
in-process TypeScript plugin into each OpenCode agent so each assistant message
|
|
5
|
+
appends one event to `$MNGR_AGENT_STATE_DIR/events/opencode/usage/events.jsonl`.
|
|
6
|
+
The `mngr usage` CLI walks those events files itself (see `imbue-mngr-usage`).
|
|
7
|
+
|
|
8
|
+
## How the pieces fit
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
mngr_opencode_usage's on_after_provisioning hookimpl
|
|
12
|
+
└─→ installs <config_dir>/plugin/mngr_opencode_usage_plugin.ts
|
|
13
|
+
(alongside mngr_opencode's lifecycle plugin; OpenCode auto-loads plugin/*.ts)
|
|
14
|
+
|
|
15
|
+
OpenCode serve process, on each assistant message.updated
|
|
16
|
+
└─→ mngr_opencode_usage_plugin.ts
|
|
17
|
+
└─→ events/opencode/usage/events.jsonl (append one cost_snapshot event)
|
|
18
|
+
|
|
19
|
+
mngr usage
|
|
20
|
+
└─→ aggregate_usage_source hookimpl (claims the "opencode" source)
|
|
21
|
+
└─→ aggregate_session_incremental: sum each session's messages
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
All file I/O goes through `host.write_text_file`, so provisioning works for local
|
|
25
|
+
**and** remote agents.
|
|
26
|
+
|
|
27
|
+
## What gets captured
|
|
28
|
+
|
|
29
|
+
OpenCode computes cost and tokens per message and exposes them on each assistant
|
|
30
|
+
`message.updated` event. The writer emits, per message, a self-contained event
|
|
31
|
+
carrying:
|
|
32
|
+
|
|
33
|
+
- `cost.total_cost_usd` -- OpenCode's own per-message cost (provenance is
|
|
34
|
+
**REPORTED**; no estimation needed).
|
|
35
|
+
- `tokens` -- `{input, output, cache_read, cache_creation}` (reasoning folded
|
|
36
|
+
into output), for auditability.
|
|
37
|
+
- `model` -- the provider-qualified `<providerID>/<modelID>`.
|
|
38
|
+
- `session_id` + `message_id` -- the reader sums per session, keeping the freshest
|
|
39
|
+
event per `message_id` so a streaming message's re-fires collapse to its final
|
|
40
|
+
reading.
|
|
41
|
+
|
|
42
|
+
## Why session-incremental
|
|
43
|
+
|
|
44
|
+
OpenCode reports each message's **own** cost, not a session running total. So the
|
|
45
|
+
writer is stateless -- it appends one self-contained event per message and keeps
|
|
46
|
+
nothing in memory. The reader (`aggregate_session_incremental`) recomputes each
|
|
47
|
+
session's total from the append-only log, which makes it robust across
|
|
48
|
+
`mngr stop`/`start` (a fresh serve process with empty memory loses no history).
|
|
49
|
+
|
|
50
|
+
`cost_mode` is `API_KEY`: OpenCode bills against a real provider key, so the cost
|
|
51
|
+
is real spend, never an imputed subscription value.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Unabridged Changelog - mngr_opencode_usage
|
|
2
|
+
|
|
3
|
+
Full, unedited changelog entries consolidated nightly from individual files in `libs/mngr_opencode_usage/changelog/`.
|
|
4
|
+
|
|
5
|
+
For a concise summary, see [CHANGELOG.md](CHANGELOG.md).
|
|
6
|
+
|
|
7
|
+
## 2026-06-16
|
|
8
|
+
|
|
9
|
+
Corrected the package version back to 0.1.0 (it had been bumped to 0.1.1, but that version was never actually released).
|
|
10
|
+
|
|
11
|
+
## 2026-06-16
|
|
12
|
+
|
|
13
|
+
New package `imbue-mngr-opencode-usage`: cost/usage tracking for OpenCode agents in `mngr usage`. It installs a second in-process OpenCode plugin (alongside the lifecycle one) that appends one `cost_snapshot` event per assistant message -- OpenCode's own per-message cost (reported, not estimated) plus tokens and the provider-qualified model -- to `events/opencode/usage/events.jsonl`. A reader hookimpl claims the `opencode` source and aggregates it with the session-incremental strategy (sum each session's messages). After provisioning an OpenCode agent, its spend now shows up in `mngr usage` like Claude's.
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Unit tests for the OpenCode ``aggregate_usage_source`` reader hookimpl."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from imbue.mngr_opencode_usage.plugin import aggregate_usage_source
|
|
8
|
+
from imbue.mngr_usage.api import parse_usage_events
|
|
9
|
+
from imbue.mngr_usage.data_types import CostMode
|
|
10
|
+
from imbue.mngr_usage.data_types import CostProvenance
|
|
11
|
+
|
|
12
|
+
_NOW = 2_800_000_000
|
|
13
|
+
_SINCE = 10**12
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _message_event(session_id: str, message_id: str, second: int, total_cost_usd: float) -> dict[str, Any]:
|
|
17
|
+
return {
|
|
18
|
+
"timestamp": f"2056-01-01T00:00:{second:02d}.000000000Z",
|
|
19
|
+
"session_id": session_id,
|
|
20
|
+
"message_id": message_id,
|
|
21
|
+
"event_id": f"evt-{message_id}-{second}",
|
|
22
|
+
"cost": {"total_cost_usd": total_cost_usd},
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_opencode_hookimpl_sums_per_message_as_reported() -> None:
|
|
27
|
+
snapshot = aggregate_usage_source(
|
|
28
|
+
source_name="opencode",
|
|
29
|
+
agents_events={
|
|
30
|
+
"agent-1": parse_usage_events(
|
|
31
|
+
[
|
|
32
|
+
_message_event("s1", "m1", 1, 0.10),
|
|
33
|
+
_message_event("s1", "m2", 2, 0.25),
|
|
34
|
+
],
|
|
35
|
+
"opencode",
|
|
36
|
+
)
|
|
37
|
+
},
|
|
38
|
+
since_seconds=_SINCE,
|
|
39
|
+
now=_NOW,
|
|
40
|
+
)
|
|
41
|
+
assert snapshot is not None
|
|
42
|
+
assert snapshot.source_name == "opencode"
|
|
43
|
+
record = snapshot.sessions[0]
|
|
44
|
+
assert record.cost.total_cost_usd == 0.35
|
|
45
|
+
assert record.cost_provenance == CostProvenance.REPORTED
|
|
46
|
+
assert record.cost_mode == CostMode.API_KEY
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_opencode_hookimpl_declines_non_opencode_sources() -> None:
|
|
50
|
+
snapshot = aggregate_usage_source(
|
|
51
|
+
source_name="claude",
|
|
52
|
+
agents_events={"agent-1": parse_usage_events([_message_event("s1", "m1", 1, 0.10)], "claude")},
|
|
53
|
+
since_seconds=_SINCE,
|
|
54
|
+
now=_NOW,
|
|
55
|
+
)
|
|
56
|
+
assert snapshot is None
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Guard the TS-writer <-> Python-reader source-name seam.
|
|
2
|
+
|
|
3
|
+
The writer (`.ts`) and the reader (`plugin.py`) agree on the source name only by
|
|
4
|
+
hand-synced string literals across the language boundary. A rename on one side
|
|
5
|
+
would silently disable usage tracking, so pin the invariant here.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import importlib.resources
|
|
11
|
+
|
|
12
|
+
from imbue.mngr_opencode_usage import resources as _resources
|
|
13
|
+
from imbue.mngr_opencode_usage.plugin import _OPENCODE_USAGE_SOURCE_NAME
|
|
14
|
+
from imbue.mngr_opencode_usage.plugin import _USAGE_PLUGIN_FILENAME
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_writer_ts_emits_the_source_the_reader_claims() -> None:
|
|
18
|
+
writer_ts = importlib.resources.files(_resources).joinpath(_USAGE_PLUGIN_FILENAME).read_text()
|
|
19
|
+
# The writer emits `source: "<name>/usage"`; the reader strips "/usage" and
|
|
20
|
+
# claims "<name>". Assert the writer's literal matches the reader's constant.
|
|
21
|
+
assert f'"{_OPENCODE_USAGE_SOURCE_NAME}/usage"' in writer_ts
|