dsh-opencode-go 0.1.24 → 0.2.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 (87) hide show
  1. package/README.en.md +59 -7
  2. package/README.md +164 -79
  3. package/THIRD_PARTY_NOTICES.md +1 -1
  4. package/assets/icon.png +0 -0
  5. package/docs/advanced.en.md +15 -22
  6. package/docs/advanced.md +15 -22
  7. package/docs/assets/logo-dark.png +0 -0
  8. package/docs/assets/logo-light.png +0 -0
  9. package/docs/development.md +47 -119
  10. package/docs/independent-package.md +4 -2
  11. package/docs/usage-analytics.en.md +108 -0
  12. package/docs/usage-analytics.md +108 -0
  13. package/docs/verification.md +25 -928
  14. package/lib/client.js +3430 -2473
  15. package/lib/index.js +2650 -2440
  16. package/lib/types/accounts.d.ts +96 -31
  17. package/lib/types/adapter.d.ts +45 -48
  18. package/lib/types/analytics.d.ts +345 -0
  19. package/lib/types/catalog.d.ts +112 -6
  20. package/lib/types/client/AnalyticsDialog.d.ts +25 -0
  21. package/lib/types/client/Section.d.ts +51 -7
  22. package/lib/types/client/UsagePill.d.ts +6 -4
  23. package/lib/types/client/controller.d.ts +176 -0
  24. package/lib/types/client/index.d.ts +9 -12
  25. package/lib/types/client/{locales.d.ts → presentation.d.ts} +124 -0
  26. package/lib/types/config.d.ts +9 -14
  27. package/lib/types/contracts.d.ts +283 -0
  28. package/lib/types/conversion.d.ts +147 -0
  29. package/lib/types/index.d.ts +7 -39
  30. package/lib/types/sdk-types.d.ts +5 -54
  31. package/lib/types/transport.d.ts +63 -0
  32. package/lib/vendor-licenses.txt +36 -36
  33. package/package.json +76 -77
  34. package/docs/fork-review-2026-09-25.md +0 -191
  35. package/docs/native-call-comparison-v020-rc2.md +0 -116
  36. package/lib/types/account-manager.d.ts +0 -25
  37. package/lib/types/account-operations.d.ts +0 -31
  38. package/lib/types/account-policy.d.ts +0 -16
  39. package/lib/types/account-recovery.d.ts +0 -2
  40. package/lib/types/account-selection.d.ts +0 -30
  41. package/lib/types/account-service.d.ts +0 -12
  42. package/lib/types/accounts-contract.d.ts +0 -38
  43. package/lib/types/call-trace.d.ts +0 -54
  44. package/lib/types/catalog-manager.d.ts +0 -20
  45. package/lib/types/client/AccountsCard.d.ts +0 -19
  46. package/lib/types/client/ModelEditor.d.ts +0 -20
  47. package/lib/types/client/accounts-controller.d.ts +0 -71
  48. package/lib/types/client/hint-mark.d.ts +0 -17
  49. package/lib/types/client/host-compat.d.ts +0 -7
  50. package/lib/types/client/host-primitives.d.ts +0 -11
  51. package/lib/types/client/section-controller.d.ts +0 -202
  52. package/lib/types/client/settings.d.ts +0 -18
  53. package/lib/types/client/staged-form.d.ts +0 -201
  54. package/lib/types/client/usage.d.ts +0 -4
  55. package/lib/types/config-contract.d.ts +0 -70
  56. package/lib/types/conversion/config.d.ts +0 -4
  57. package/lib/types/conversion/context.d.ts +0 -56
  58. package/lib/types/conversion/image-offload.d.ts +0 -13
  59. package/lib/types/conversion/image-pool.d.ts +0 -24
  60. package/lib/types/conversion/index.d.ts +0 -5
  61. package/lib/types/conversion/replay.d.ts +0 -66
  62. package/lib/types/conversion/stream.d.ts +0 -44
  63. package/lib/types/gateway-diagnostics.d.ts +0 -23
  64. package/lib/types/gateway-error.d.ts +0 -2
  65. package/lib/types/gateway-url.d.ts +0 -6
  66. package/lib/types/google-fetch.d.ts +0 -6
  67. package/lib/types/host-settings.d.ts +0 -11
  68. package/lib/types/json-response.d.ts +0 -14
  69. package/lib/types/metadata-cache.d.ts +0 -12
  70. package/lib/types/model-metadata.d.ts +0 -21
  71. package/lib/types/models-contract.d.ts +0 -66
  72. package/lib/types/models.d.ts +0 -13
  73. package/lib/types/protocol-contract.d.ts +0 -5
  74. package/lib/types/protocol-policy.d.ts +0 -4
  75. package/lib/types/provider-identity.d.ts +0 -4
  76. package/lib/types/proxy-url.d.ts +0 -2
  77. package/lib/types/proxy.d.ts +0 -11
  78. package/lib/types/reasoning.d.ts +0 -40
  79. package/lib/types/remote-contract.d.ts +0 -3
  80. package/lib/types/remotes.d.ts +0 -3
  81. package/lib/types/request-budget.d.ts +0 -12
  82. package/lib/types/request-control.d.ts +0 -2
  83. package/lib/types/settings-bridge.d.ts +0 -22
  84. package/lib/types/usage-contract.d.ts +0 -35
  85. package/lib/types/usage-display.d.ts +0 -4
  86. package/lib/types/usage.d.ts +0 -27
  87. /package/lib/types/{google-generative-ai.d.ts → google.d.ts} +0 -0
@@ -1,140 +1,68 @@
1
- # Build and compatibility environments
1
+ # Development
2
2
 
3
- The root package contains one coherent DSH 0.1.6-alpha.1 development environment, the plugin's runtime dependencies, build tools, and core test tools. The published runtime peer ranges remain unchanged. `tests/hosts/*` are independent fixture packages, not npm workspaces. Their dependencies and lockfiles are never installed by root `npm install`, `npm ci`, or `npm pack`.
3
+ The development baseline is DSH `0.2.1-rc.5`, Cordis `4.0.5` and Schemastery `3.18.5`. Runtime DSH peers require `>=0.2.1-rc.5`; Cordis requires `~4.0.5`. Install the locked dependencies with `npm ci`.
4
4
 
5
5
  ## Commands
6
6
 
7
7
  | Command | Purpose |
8
8
  | --- | --- |
9
- | `npm ci` | Install the locked development dependencies without rebuilding the plugin |
10
- | `npm run compile` | Type-check and regenerate the committed host, client, and declaration artifacts in `lib/` |
11
- | `npm run check:dist` | Rebuild in a temporary directory and compare every file against the shipped `lib/` without changing it |
12
- | `npm run test:ci` | Verify shipped artifacts match source, then run the core suite |
13
- | `npm test` | Rebuild, then run the core Vitest suite |
14
- | `npm run test:compat` | Pack once and test the installed artifact across every supported host fixture |
15
- | `npm run test:compat -- v015-rc1 v017-rc2 v021-alpha1` | Test only the named fixtures; unknown names fail |
16
- | `npm run test:install` | Test npm Git installation and fresh pnpm 11.7.0 Git/tarball installs with no script approvals, plus consumer type resolution |
17
- | `npm run test:install -- npm` / `-- pnpm` | Run just one package manager; unknown names fail |
18
- | `npm run verify` | Run core, compatibility, and installation checks |
9
+ | `npm run typecheck` | Check the host and browser TypeScript projects |
10
+ | `npm run compile` | Rebuild the host bundle, browser factory and declarations in `lib/` |
11
+ | `npm run check:dist` | Compare an independent rebuild with every shipped artifact |
12
+ | `npm test` | Run the Vitest suite |
13
+ | `npm run test:ci` | Check artifacts, then run the Vitest suite |
14
+ | `npm run verify:installed -- /path/to/consumer` | Exercise a built package through the consumer's real Cordis resolver; defaults to this checkout |
15
+ | `npm run verify:headless -- /path/to/consumer` | Exercise a consumer containing the official DSH CLI and the installed plugin, with an isolated profile and loopback gateway |
16
+ | `npm run verify` | Run type checks, artifact comparison, the complete Vitest suite and installed-package smoke |
19
17
 
20
- The checks use loopback gateways and local fixtures, not paid inference or an installed user profile. Compatibility and installation checks need registry access; installation checks also require Git. Temporary consumers are created outside the repository and removed on success or failure. Tests never implicitly skip a missing host environment.
18
+ `test:ci` runs `check:dist` followed by `npm test`. `verify` runs `typecheck`, `test:ci` and `verify:installed`. CI uses `npm ci --strict-peer-deps` and `npm run verify` on Linux and Windows with Node 22.19 and 24. Inference smoke checks use synthetic credentials and a loopback gateway. `verify:headless` additionally requires the official DSH CLI in the supplied consumer; it creates an isolated home and profile.
21
19
 
22
- ## Build before distribution
20
+ ## Test suite
23
21
 
24
- The default Git branch ships `lib/` so a GitHub URL, an npm release, and an npm tarball all supply ready-to-load code. There are no `prepare`, `prepack`, `prepublish`, or dependency installation hooks. The explicit build command is named `compile` because npm also treats a script named `build` as a reason to prepare a Git dependency and install its development dependencies. `prepublishOnly` checks artifact freshness when a maintainer publishes to npm; it does not execute during dependency installation.
22
+ Vitest discovers exactly 12 domain files:
25
23
 
26
- After source or build-tool changes, run `npm run compile` and commit the regenerated `lib/` with those changes. `npm run check:dist` checks missing, changed, and extra artifacts against an independent rebuild. CI uses `npm run test:ci` so it checks the committed files before running tests, instead of silently replacing stale files first. CSS module filenames are repository-relative, CSS export keys are sorted, and declaration line endings are fixed to LF so artifacts are independent of the checkout path and operating system.
27
-
28
- The installation regression creates a fresh repository containing the shipped artifacts but no `src/`, `scripts/`, `tests/`, or `node_modules/`, then installs it with both package managers. Each installed package must expose its host and client entrypoints, declarations, build metadata, and importable host module, and must not declare scripts that trigger Git preparation or dependency installation hooks. Fresh pnpm consumers install both the Git artifact and a packed npm artifact with `strictDepBuilds=true`, separate empty stores and isolated user configuration. They receive no `allowBuilds` entries, including Google/protobuf skips. The fixtures assert that the private SDK alias, Google SDK and protobufjs are absent, and type-check exported model types without access to development dependencies. The cold Git identity changes for each temporary repository. Consumer profiles and their trust settings are never modified by these tests.
29
-
30
- ## Installed-package matrix
31
-
32
- `scripts/compatibility-hosts.mjs` lists ten host generations, including DSH `0.2.0-rc.2` and `0.2.1-alpha.1`. Each `tests/hosts/<id>` has a private manifest and its own lockfile. DSH packages and their required DSH peer/dependency closure are pinned to that host generation, together with compatible Cordis packages. This prevents a broad upstream peer range from silently selecting a later host generation. The 0.2 fixtures also install their real `dsh-llm-pi-ai` adapter and public pi-ai dependency alongside this plugin.
33
-
34
- The runner performs these steps for each host:
35
-
36
- 1. Copy the manifest and lockfile into a fresh consumer outside the checkout.
37
- 2. Run `npm ci --strict-peer-deps` to validate that host's dependency contracts.
38
- 3. Install the same plugin tarball used by every other host.
39
- 4. Check that the host's pinned packages retained their versions and the plugin is an actual installed directory.
40
- 5. Copy the fixtures into the consumer and run them in separate Node processes.
41
-
42
- Local compatibility runs are serial by default. Set `DSH_COMPAT_CONCURRENCY` to a positive integer to run that many isolated hosts at once; CI uses four workers and prefers its restored npm download cache, fetching missing packages as needed. Each host keeps its own dependency installation and fixture processes, and all workers consume the same tarball. A failed host does not skip queued hosts or delete files still used by running checks: the runner collects every result before cleanup. Logs are grouped by host, and the console and GitHub job summary report each result and elapsed time.
43
-
44
- All host JavaScript imports use ordinary Node resolution. The fixtures do not redirect DSH imports to alternate packages or load the plugin from the source checkout. The client fixture uses the consumer's React, store, and UI primitives, with a CSS loader for published host styles. It verifies the installed browser factory, settings registration, catalog injection, rendering, CSS, advanced reset after an invalid draft, and cleanup. Host fixtures retain text streaming, real image processing, history/offload, limits, reasoning, live settings, and profile-bundle checks. Each host also runs all four wire protocols through prompt and tool declaration checks, a streamed tool call, JSON-restored assistant replay, tool-result continuation, and a one-shot request with no tools. Google continuation also checks retained tool signatures and separate signed text Parts, including signature-only empty text.
45
-
46
- ## Architecture and ownership
47
-
48
- `src/index.ts` composes the host services. `src/config-contract.ts` is the browser-safe configuration vocabulary; both faces import it instead of maintaining separate interfaces. Version-dependent settings attachment lives in `src/host-settings.ts` and `src/client/host-compat.ts`. `src/settings-bridge.ts` owns section/profile selection and the applied/rejected/unknown write verdict: modern acknowledgments are authoritative, while older hosts require a committed-state check.
49
-
50
- `GoCatalogManager` owns one configuration-keyed catalog per mount. Inference, model discovery and the settings model service share that catalog directly. The inference adapter retains its standalone catalog fallback for public callers. A superseded catalog cannot send a late route-refresh notification.
51
-
52
- Model information reads capture the plugin configuration without querying account generations or serializing host settings. Only request preparation and direct dispatch capture an account generation, before asynchronous discovery; prepared dispatch reuses that generation. Account revision checks remain fresh so a manual A → C → A selection still invalidates an older request's account adoption.
53
-
54
- The browser sends typed `opencodeGoAccounts.execute` commands for account additions, removals, selection, naming, ordering, automatic switching and key replacement, including staged key drafts. `GoAccountManager` serializes these commands with recovery; both use `accountOperationPatch` to finish the same durable intention. A client-generated addition ID makes RPC retries idempotent. The UI owns drafts, busy state and quota display; the host owns credentials and revision-guarded metadata writes. Add account operations to the shared command contract and host manager, rather than creating another browser write path.
55
-
56
- `src/account-policy.ts` decides whether a failed request may try another account and whether a gateway key should be remembered as rejected. Error classification stays in `src/gateway-error.ts`; stream lifecycle and dispatch stay in `src/adapter.ts`. Both terminal stream errors and thrown errors use the same policy, which blocks fallback after any output, positive token usage or cancellation.
57
-
58
- ## Failure boundaries and resource policy
59
-
60
- `src/request-budget.ts` assigns a default per-request output allowance of 65,536 tokens only to the exact model ID `deepseek-v4.1-flash`, bounded by its resolved output capacity. Other models keep the SDK's default allowance and existing caller-cap behavior; similarly named models do not inherit the rule. Explicit per-model `maxTokens` settings continue to cap caller values. Both model resolution paths report `defaultMaxTokens` when a model has the DeepSeek default or an explicit settings override; otherwise the field is omitted. Dispatch uses the same function on its captured configuration, so the host can include the known default allowance in compaction budgeting. SDK context clamping can reduce the serialized allowance. Anthropic token-budget reasoning retains its existing behavior. HTTP diagnostics report the serialized `max_tokens`, rather than assuming the pre-SDK value is final.
61
-
62
- Failed or aborted SDK events without any measured input, output, cache or total tokens omit the usage chunk. DSH's assembler then leaves attempt usage absent, preserving the previous context-pressure sample. Positive usage, including cache-only usage, is retained for accounting. Explicit context-overflow codes or messages in HTTP 400/413/422 responses map to `CONTEXT_WINDOW_EXCEEDED`, including codes the SDK discarded. Opaque errors retain `INVALID_REQUEST`; their diagnostics include the actual output allowance, configured context window and compaction/settings guidance. The host owns recovery and retry limits.
63
-
64
- The gateway uses HTTP 401 for model errors, subscription limits and authentication failures. `gateway-error.ts` classifies captured `error.type`/`error.code` first. Untyped quota text is admitted only on HTTP 401/402/403/429; untyped 5xx keeps `SERVER` (504 keeps `TIMEOUT`) even when diagnostics quote earlier quota failures. A generic 401/403 never proves a key is invalid. Only `INVALID_CREDENTIAL` is remembered as a gateway key rejection; quota failures can switch accounts before output but do not blacklist credentials. HTTP 200 stream errors continue through the stream mapper's fallback classification. Retry-After remains in bounded HTTP evidence for diagnosis; the host owns retry scheduling.
65
-
66
- Discovery and credential/image preparation have a default 60-second deadline. Dispatch has a separate default 30-minute deadline spanning discovery, fallback and streaming. The existing idle watchdog still limits an outstanding stream read. Every external wait observes cancellation, including services that ignore their signal; canceling a catalog waiter does not abort the shared refresh. Prepared capability resolution has its own preparation deadline, and the dispatch timer starts when the prepared stream is consumed.
67
-
68
- Image count preflight does not estimate encoded bytes on modern hosts. Four shared permits and a 32-task queue bound preparation across simultaneous calls; exact encoded sizes are checked incrementally with unknown sizes contributing zero. A shared 128 MiB lease budget charges each prepared raw image plus occurrence-weighted base64 bytes. Adapter leases last through streaming and release on completion, failure or consumer stop; standalone conversion only holds its lease during conversion. Queue/budget admission fails with `IMAGE_RESOURCE_BUSY`, without account switching. This logical budget excludes decoder RSS, JSON/SDK copies and retained history. A returned raw image exists before its charge is checked, so four active native operations can still produce transient overshoot. Failure aborts queued work. An in-flight service that ignores cancellation retains its permit until it really settles, so cancellation cannot accidentally admit unbounded native work. A permanently stuck service can therefore exhaust these slots and needs host/service recovery. DSH 0.1.5 retains its transient projection policy; later hosts retain durable `IMAGE_OFFLOAD_REQUIRED` retries.
69
-
70
- `StagedForm` submits settings set/unset operations in one `scope.mutate` with the captured revision. A refused batch leaves drafts and does not write secrets. Successful settings drafts clear even if a separate secret write fails, allowing an idempotent retry. A newer draft typed during a save is retained. Settings and credentials remain separate stores, without a cross-store transaction.
71
-
72
- Account additions and generated-account removals first confirm a durable `accountOperations` intention in the account's own section/profile. The hidden field stores operation ID, kind and account metadata, never a key. The Host reconciles it at startup, settings/credential connection and committed updates. Already stored addition keys gain their metadata; additions without keys remain visible and can be repaired or removed. Deletion intentions finish credential removal before atomically removing metadata and the intention. Revision guards preserve concurrent edits and later selections; a refused store retains the intention, with at most four passes per trigger and a 30-second deadline per operation. Closing or replacing the connection cancels its waits so a new service can resume. Credential deletion requires an exact generated ID/reference pairing; external or shared legacy references keep their values. Recovery acts only while its service connection is current. Cancelling a wait cannot roll back an already accepted storage write. It cannot recover a key that was never stored, provide isolation against arbitrary simultaneous writes in other profiles, or repair operations predating the journal. Manually sharing a generated reference across profiles also shares its deletion lifecycle.
73
-
74
- A nonempty gateway listing with no valid model IDs is a discovery failure and preserves served membership. A confirmed empty list clears it. Missing models on a cold or failed listing report `DISCOVERY_FAILED`, while confirmed absence reports `UNKNOWN_MODEL` through the adapter. Mixed valid/invalid rows use valid IDs and expose a warning in `sources.listing.warning`, the settings page and Host logs. The warning includes counts and at most eight row positions, never malformed row contents, and clears after a clean listing. The choice favors usable partial data while making potential membership loss visible. Public disk metadata still does not establish gateway membership after a restart.
75
-
76
- The public `onCallTrace` observer receives one content-free summary per dispatch: random call ID, model, elapsed and stage milliseconds, attempt count, outcome and final code. A bounded `attemptDetails` array includes each attempt's stages, outcome/code, HTTP status, selected redacted upstream request ID, first text/reasoning/tool-argument delta latency and image pool diagnostics. Queue wait is summed across image tasks and may exceed wall-clock image-stage time. HTTP evidence files carry the same call ID and attempt number. There is no stable account identity in this trace. The plugin forwards it to the host debug logger. Observer failures cannot alter inference. Compile-time `never` checking and runtime rejection guard the SDK event union; unknown host roles are explicitly rejected. The open peer range is an installation policy, while the installed matrix is the finite evidence of compatibility.
77
-
78
- ## Reasoning intent
79
-
80
- `src/protocol-contract.ts` restricts the experimental `protocolOverrides` surface to DeepSeek V4.1 Flash → Responses. `src/protocol-policy.ts` resolves the wire API, base URL and fresh protocol compatibility profile together on the captured request config, before capacity overrides. The cached catalog remains unmodified. Effort choices stay those advertised by metadata; an unexpected native toggle/budget or source protocol is rejected before inference. Protocol errors do not trigger a second-protocol retry. The installed-host transcript matrix exercises the override as a Chat Completions-declared model through the real Responses SDK, including tool-result continuation and restored replay; focused tests also switch existing histories in both directions and retain only valid native reasoning state.
81
-
82
- For latency comparisons, fix the account, proxy/network, prompt, model, reasoning effort, output cap and stable `x-opencode-session` header. Interleave protocols and report cold and warm samples separately. Measure first body-text delta, total time, output/reasoning tokens and failures; the existing `GoCallTrace.firstOutputMs` includes reasoning and tool deltas and is not body-text TTFT. Local fixtures establish compatibility, not live gateway speed or its underlying provider. Run paid live benchmarks only when requested.
83
-
84
- `scripts/benchmark-protocols.mjs` compares the plugin already installed in the macOS desktop profile using the app's actual peer packages. It requires explicit `--live`, performs 20 bounded synthetic calls with a 4,096-token output cap, and writes JSON results under `opencode-protocol-benchmark-<runId>` without changing settings or stored credentials. Automatic fallback is disabled in memory. The output includes first and repeated samples, reasoning/output tokens, route headers, and failures; first-session samples do not prove an upstream cache miss. Run its offline observer/statistics check with `node scripts/benchmark-protocols.mjs --self-test`.
85
-
86
- `scripts/benchmark-latency.mjs` compares direct gateway fetches with the desktop-installed adapter using byte-identical SDK request bodies and headers, including the selected credential and a fresh shared session header. Run its `--self-test` offline; live runs require the desktop executable with `ELECTRON_RUN_AS_NODE=1`, `--expose-internals`, explicit `--live`, and `--session /absolute/path/to/a/single-step/session.v4.jsonl.zstd`. The `zstd` CLI reads the journal's concatenated frames. The default run uses four alternating pairs of the reconstructed request and two pairs containing only its human message, with a 1,024-token output cap. Use `--control-pairs 0 --fresh-context` to insert an inert prefix per pair and observe cache misses/hits, or `--control-pairs 0 --concurrent-title` to replay the recorded title request concurrently with each main call. The latter adds one 64-token auxiliary generation per main request when that was the recorded cap. Original prompt content, raw headers and credentials remain in memory; reports contain hashes, counters, usage, gateway route IDs, content-free adapter traces and timings only. Undici diagnostic channels measure socket submission, upload completion and response headers; a common SSE observer measures wire thinking/text before SDK parsing, which is timed independently at the adapter's consumer. Socket connection events describe process-level activity and are only attributable during these isolated runs. Automatic retries/account switching are disabled, and a failed main request stops the run. Profile and credential hashes are checked afterward. These runs exercise the real installed adapter and Host packages outside the UI; they cannot recover historical gateway queue time or desktop IPC latency.
87
-
88
- ```sh
89
- ELECTRON_RUN_AS_NODE=1 '/Applications/DeepSeek Harness.app/Contents/MacOS/DeepSeek Harness' scripts/benchmark-protocols.mjs --live
90
- ```
91
-
92
- Use `--profile`, `--resources`, or `--out` to change the desktop profile, application resources directory, or report directory. The selected account's credential is read from the environment or the user's DSH credential store and stays in memory.
93
-
94
- The model catalog records whether controls are a switch, adjustable effort, or a token budget, independently of pi-ai's wire compatibility hints. `src/reasoning.ts` owns the picker choices and translates a validated selection into Default, Off, On, an explicit effort, or a token budget. Model resolution declares no `defaultEffort`: an omitted user choice remains omitted through the DSH runtime.
95
-
96
- Before the SDK sends a payload, the shared `onPayload` hook removes automatic thinking controls for Default. For On it retains the SDK's native enable/budget but removes synthetic effort fields. Off and explicit effort keep the SDK's serialization. Other output configuration, rendering and history options are retained, and payload edits do not mutate shared catalog models. Switches keep their previous opaque `high` ID for durable selection compatibility and display it as On. The verified MiMo Flash control override stays in the same reasoning module.
97
-
98
- Token budgets use pi-ai's `thinkingBudgets` override; the SDK still handles the wire field, context ceiling, and answer room. Only SDK-supported budget protocols expose the control; adaptive Anthropic models retain effort controls. Models declaring both effort and budget keep their efforts and can add numeric budget choices. Standard budget presets retain the historical `minimal`/`low`/`medium`/`high` IDs. Additional presets use durable `budget:N` IDs and are configured in `modelLimits[modelId].thinkingBudgets`. Catalog bounds reach the settings page through the models RPC; numeric choices and request settings are captured together before dispatch.
99
-
100
- ## Bundled pi-ai implementation
101
-
102
- The development dependency `opencode-go-pi-ai` is an npm alias for `@earendil-works/pi-ai@1.2.0`. esbuild retains the four supported protocol implementations and the OpenCode Go model table, and discards unrelated provider implementations. Public SDK types are flattened into `lib/types/sdk-types.d.ts`; consumers do not need the private alias to resolve them. OpenAI 7.19.0 and Anthropic 0.129.0 remain runtime dependencies at the same versions pi-ai uses. Google 2.21.0 and its required HTTP dependencies are bundled. Bundled code and type licenses ship in `lib/vendor-licenses.txt`.
103
-
104
- The published package does not depend on pi-ai or Google/protobuf packages, so their install hooks never run in consumers, including when using Gemini. This avoids the pnpm 11 script-approval interruption that triggered issue #43 without changing the host's policy. An npm consumer separately includes public pi-ai 0.85.1 and a peer-only probe, plus OpenAI 6.40.0 and Anthropic 0.124.0. It verifies that the peer retains pi-ai 0.85.1, the consumer retains both older transport SDKs, and the plugin resolves its own OpenAI 7.19.0 and Anthropic 0.129.0. Other plugins' dependencies still follow the user's host trust settings.
105
-
106
- `src/google-generative-ai.ts` reuses pi-ai's native Google message conversion, tool declarations, streaming events, usage and thought-signature replay. A request-scoped `AsyncLocalStorage` binding carries the captured diagnostic/proxy fetch and Harness User-Agent without replacing global fetch. `scripts/google-fetch-bridge.mjs` substitutes one HTTP fetch call in the bundled Google SDK, with an exact 2.21.0 version check and a single-match source check. It also pins pi-ai 1.2.0's Google client factory to `vertexai: false`, so host `GOOGLE_GENAI_USE_VERTEXAI` / `GOOGLE_GENAI_USE_ENTERPRISE` settings cannot change the gateway's Gemini route. The stream patch keeps Parts within a chunk separate and starts a new block when a later chunk carries a different signature, preserving signed text, thinking and empty text during replay. Continuing deltas still accumulate within their Part, including signatures sent on the first or final delta. Each adapter substitution requires the pinned pi-ai version and exactly one source match. The environment and other Google clients retain their own backend selection. esbuild and Vitest use the same substitutions; review the bridge before changing either SDK pin. The remaining SDK serialization, stream parsing, cancellation and retry handling stay upstream code. Gemini 4 Argon postdates pi-ai 1.2.0, so the wrapper selects its discrete thinking levels explicitly. Default removes Google's nested `thinkingConfig`, while Low/Medium/High retain the metadata-declared levels.
107
-
108
- The default public base is `https://opencode.ai/inference/go/v1` for model discovery, every inference protocol and usage requests. `src/gateway-url.ts` migrates the exact legacy official base `https://opencode.ai/zen/go/v1` before request paths are constructed. Settings schemas contain only data: the modern Host rebuilds volatile fields and serializes the form again, which drops rehydrated transform callbacks and prevents the desktop page from loading. `readConfig` normalizes live values, and `src/host-settings.ts` normalizes profile writes and rewrites saved aliases on attachment and document changes, with revision checks to preserve concurrent edits. Legacy runtime reads also normalize immediately while the saved migration completes. URL validation, direct catalog construction and metadata conversion apply the migration, including callers that supply raw configuration. The plugin never needs to reach the legacy gateway, even if it stops serving all routes. Custom gateways retain their configured addresses. Directory availability alone does not verify inference routing; a desktop follow-up confirmed the canonical Google route with a bounded real generation and an actual chat response.
109
-
110
- The [pi-ai 1.2.0 changelog](https://github.com/earendil-works/pi/blob/v1.2.0/packages/ai/CHANGELOG.md) adds per-key OpenRouter catalogs and provider refresh base URLs, fixes Mistral stream timeouts, and retries Anthropic preferred strict-tool requests once without strict mode when grammar limits are exceeded. Its Google adapter is unchanged from 1.1.0 and still rejects custom fetch. Model/compatibility types now come from shared schema modules; the public declaration bundle must retain those definitions without leaking the private SDK alias.
111
-
112
- DSH requests still convert through `toPiContext`, preserving the existing host input and durable replay contracts. Immediately before calling a provider directly, the adapter applies pi-ai's `normalizeContext`. In 1.1.0, direct providers require `TranscriptContext`; only `Models` streaming entry points normalize legacy Context automatically. Normalizing at this call site preserves the system prompt and tool declarations on both text and image paths without tying the SDK format to the DSH version.
113
-
114
- This upgrade does not enable developer messages, tool-change blocks, or deferred tool loading. Their existing `UNSUPPORTED_CONTENT` errors remain explicit. The minimum DSH version and peer ranges stay unchanged; new host fixtures verify runtime compatibility rather than expanding the supported content vocabulary.
115
-
116
- There is one explicit npm prerelease exception: DSH evaluates plugin ranges with prereleases included, whereas npm does not generally accept a future prerelease under `>=0.1.5-rc.1`. Only the tarball-install step uses `--legacy-peer-deps` after the host has passed strict installation. This is not a project `.npmrc` policy and does not affect root installs. The post-install version assertions ensure npm did not replace pinned host packages. Changing the plugin's published version policy is a separate decision.
24
+ | Test file | Coverage |
25
+ | --- | --- |
26
+ | `plugin.spec.ts` | Cordis mounting/unloading, native SettingsForms/profile writes, hot configuration, RPC and public exports |
27
+ | `accounts.spec.ts` | Account CRUD, 20-account cap, serial writes, failed-write cleanup, fallback/promotion, stale calls and five-minute cooldown |
28
+ | `catalog.spec.ts` | Discovery, model limits/visibility, online metadata, ETag/disk cache, endpoint isolation and bounded transport |
29
+ | `adapter.spec.ts` | Requests, capacity limits, session/attribution headers, immutable request snapshots, cancellation and timeouts |
30
+ | `protocols.spec.ts` | Reasoning levels, protocol overrides, OpenAI/Anthropic/Google serialization and signed Google parts |
31
+ | `conversion.spec.ts` | Independent tool messages, replay signatures, context conversion, durable image offload, per-request budgets and stream contracts |
32
+ | `usage.spec.ts` | Official quota reads, current-account selection, native gateway access and safe error responses |
33
+ | `analytics.spec.ts` | Session folding, totals, context/peak pricing, Go/Go Plus limits, memory cache freshness, scan limits and cancellation |
34
+ | `transport.spec.ts` | Bounded JSON/gzip, classified/redacted HTTP errors, real CONNECT/SOCKS5 proxies and SDK bridge matching |
35
+ | `settings.client.spec.tsx` | Page configuration, typed Save/Reset/refused writes, locale changes, registration and subscription/cancellation lifecycle |
36
+ | `accounts-models.client.spec.tsx` | AccountsCard forms/status/quota, drag/keyboard ordering, model visibility, metadata failures and refresh races |
37
+ | `usage.client.spec.tsx` | Conversation pill, AnalyticsDialog, pricing presentation, polling/visibility, accessibility and themes |
117
38
 
118
- DSH 0.2.1-alpha.1 uses Cordis 4.0.5-alpha.1 and Schemastery 3.18.5-alpha.1 and no longer publishes runtime invariant plugins. Its fixture pins that dependency generation without the removed package. The plugin does not import invariant entrypoints or replace the former composer statistics row; its quota pill uses the retained `conversation.input.right` list slot. The installed client check verifies that the published composer still declares that slot, and profile checks validate proxy writes, rejection, clearing and reset through each modern host's real settings service.
39
+ All test imports target current modules directly. `vitest.config.ts` has no aliases or compatibility resolver. Its Google SDK loader is the same version-guarded transport patch used by the production build. The UI tests load the published DSH primitives and the installed locale bundle.
119
40
 
120
- ## Updating a fixture
41
+ `tests/setup.ts` gives each test a temporary `DSH_HOME`, provides synthetic models.dev metadata, allows loopback HTTP fixtures and explicit `.invalid` destinations through the local proxy fixture, and rejects other network requests. `tests/support/` contains five shared helpers: current host/account services, ConfigForm/React fixtures, metadata data, loopback gateways and proxies. Fixtures clean up contexts, sockets, timers and temporary homes.
121
42
 
122
- Add or update the entry in `scripts/compatibility-hosts.mjs`, its private manifest, and its lockfile together. Pin each DSH package reached through dependencies and required peers to the intended host generation; do not upgrade an old host merely to satisfy a newer package. Keep compatible Cordis versions and the client fixture's React, DOM, CSS, and UI dependencies explicit.
43
+ ## Modules
123
44
 
124
- Generate a lockfile in a clean temporary directory containing that fixture's manifest:
45
+ The source contains 17 implementation modules, plus CSS and its ambient declaration:
125
46
 
126
- ```sh
127
- npm install --package-lock-only --ignore-scripts --strict-peer-deps
128
- ```
47
+ | Domain | Modules |
48
+ | --- | --- |
49
+ | Host composition and settings | `index.ts`, `config.ts` |
50
+ | Inference and model discovery | `adapter.ts`, `catalog.ts`, `conversion.ts`, `google.ts`, `sdk-types.ts` |
51
+ | Account commands and official quota | `accounts.ts` |
52
+ | Local usage analytics and plan pricing | `analytics.ts` |
53
+ | Browser-safe configuration and RPC contracts | `contracts.ts` |
54
+ | HTTP, proxy transport and redacted errors | `transport.ts` |
55
+ | Browser registration and state | `client/index.ts`, `client/controller.ts` |
56
+ | UI and presentation | `client/Section.tsx`, `client/UsagePill.tsx`, `client/AnalyticsDialog.tsx`, `client/presentation.ts` |
129
57
 
130
- Copy the resulting lockfile back to its fixture directory. Generating it outside the checkout prevents an existing root `node_modules` from influencing resolution. Then run the selected compatibility check and the full verification before release. Root development dependency changes require the same strict resolution discipline.
58
+ The host uses `settings.configure()` and volatile configuration fields; the browser binds the official `configForms` contract. The page keeps text drafts until Save, model capacities stay typed objects throughout editing, and immediate switches retain their existing behavior. The accounts card owns all credential entry and replacement.
131
59
 
132
- After changing the root lockfile, verify a clean `npm ci --strict-peer-deps` and `npm run test:ci` with each supported Node version's bundled npm. Node 22.19.0 ships npm 10.9.3; running Node 22 with a newer npm does not check that install contract. In this dependency tree, npm 10 requires the optional esbuild peer used by Vite under Vitest to be recorded, even when npm 11 can install without that record. Keep its platform packages in the lockfile as well.
60
+ Account commands and automatic primary-account promotion share one in-memory serial queue. Credential writes settle before the next command starts. Metadata changes use a single revision-guarded write; no WAL, startup recovery or conflict retry loop is maintained. Only credentials owned by the removed generated account are deleted. Request generations prevent late fallback outcomes from replacing a newer user selection. Credential, quota and rate-limit failures can try the next account only before output or positive usage, and rejected gateway keys cool down for five minutes. Changing a key or explicitly selecting an account clears its rejection state.
133
61
 
134
- The GitHub workflow runs core and Git-installation checks on Linux and Windows with Node 22.19.0 and 24. Core tests, npm installs, and pnpm installs run as independent jobs, so one slow installation does not consume another check's time allowance. Installation jobs need no root `npm ci`: their runner uses only Node built-ins, and the fresh Git fixture supplies committed artifacts and must not need build dependencies or plugin build approval. The complete installed-host matrix runs separately on Linux/Node 24.
62
+ Analytics folds session events, prices them using the current catalog (including context tiers and peak/off-peak pricing), and includes Go / Go Plus monthly limits. A bounded in-memory session cache reuses results for at most five seconds; restarting the process clears it. Reports retain coverage, truncation, pricing availability and cancellation. Analytics does not write fold files.
135
63
 
136
- Each isolated installation consumer installs the root's pinned TypeScript version solely to check the plugin's public declarations. That check resolves its compiler from the consumer, so it also runs when the checkout has no `node_modules`. Bundled license text uses LF regardless of the source checkout's line endings, keeping Windows artifact checks reproducible.
64
+ Image count limits are checked before reads. Each request prepares up to four images concurrently and checks its own encoded-payload budget. The host's `requiredImageOffload` / `projectOffloadedImages` protocol owns durable offload marks. There is no plugin-wide preparation queue or payload pool. HTTP failure classification, credential redaction, context-capacity hints, stream deadlines and cancellation remain active; internal call tracing and diagnostic file capture are removed.
137
65
 
138
- Installation jobs reuse the npm download cache and a separate pnpm store, preferring cached packages while still fetching missing data. They never restore the checkout's `node_modules` or `lib/`. CI keeps the npm cache, isolated consumers, and the cached pnpm store under `RUNNER_TEMP`, on the runner's work volume. `DSH_PNPM_STORE_DIR` selects the pnpm store; local runs use the system temporary directory and the default package caches unless overridden.
66
+ ## Distribution
139
67
 
140
- Every Git-installation run reports fixture preparation, installation, artifact verification, cleanup, and total elapsed time. GitHub job summaries include a phase table. CI also enables npm's internal timing records and uploads the `*-timing.json` files for seven days, for diagnosing dependency resolution and extraction; a timeout may prevent the still-running process from writing its final timing file. The 15-minute job limit remains unchanged.
68
+ The `.` and `./client` exports continue to point to the built host entry and browser factory. Rebuild `lib/` with source changes, then run `npm run check:dist`. Builds do not execute during dependency installation. The Google transport bridge remains pinned to the existing SDK versions; the telemetry dependency is fixed at `1.2.0` to retain the original private SDK behavior and shipped license attribution.
@@ -6,10 +6,12 @@ The Host depends on DSH services through peer dependencies so Cordis service cla
6
6
 
7
7
  The package's bundle keeps the configuration entry `id: opencode-go` so existing settings remain attached. Its LLM provider route is independently named `dsh-opencode-go`, shown as `DSH OpenCode Go`, and can coexist with the generic adapter's `opencode-go` route. The SDK retains its native `opencode-go` identity for transport behavior; replay metadata records the DSH route and an optional `sdkProvider` to restore the native identity. The original fork's `llm-opencode-go` row can be disabled by a profile patch. Plugin registration does not select the user's default model or replace another provider automatically. Saved session/preset provider selections must be updated explicitly.
8
8
 
9
- Published `@deepseek-ai/dsh-llm-pi-ai` versions `0.1.5-rc.2` and `0.1.6-alpha.1` do not export `./conversion`. This package therefore carries the request-history, stream, and replay conversion modules under `src/conversion`, with the upstream MIT license. It does not import unpublished files or bundle the whole adapter. Protocol transport remains implemented by pi-ai. Conversion maintenance is now this project's responsibility; a future public conversion library can replace these modules once published and tested.
9
+ The plugin owns request-history, stream and replay conversion in `src/conversion.ts`, with the upstream MIT license. It consumes only published host APIs from DSH `0.2.1-rc.5` and later. Protocol transport remains implemented by the pinned private pi-ai SDK, with Google adapter wrappers in `src/google.ts` and the request-local fetch binding in `src/transport.ts`.
10
10
 
11
11
  The build uses esbuild and lightningcss, with Rollup to flatten the exported SDK declarations. The host bundles the required pi-ai implementation and only the OpenCode Go model catalog. DSH services, Undici, OpenAI and Anthropic SDKs stay external; DSH class identities remain shared. The browser factory follows DSH's `window.__ModuleLoader__.load` format, embeds CSS Modules, and allows only the declared platform module imports. A package-level builder is sufficient for this single external plugin; extracting a general DSH build-tool package would require a separate release and is not a prerequisite for installation.
12
12
 
13
- Multi-version host fixtures have independent manifests and lockfiles, outside the root npm dependency graph. Consumer installation uses prebuilt artifacts and does not run a plugin build. Compatibility verification installs one generated tarball into isolated consumers and uses their ordinary module resolution; it does not substitute selected host modules into the root process. See [development and verification](development.md).
13
+ The minimum supported Host is DSH `0.2.1-rc.5`, with Cordis `~4.0.5`. Development dependencies and the lockfile pin that DSH release, Cordis `4.0.5` and Schemastery `3.18.5`. Consumer installation uses the prebuilt artifacts and does not run a plugin build.
14
+
15
+ Verification uses 12 domain-focused Vitest files against published rc.5 APIs, separate host/client type checks, a byte-for-byte independent rebuild and Cordis's ordinary package resolver. Tests import the 17 current implementation modules directly. `npm run verify:installed -- /path/to/consumer` exercises a consumer's resolved package; `npm run verify:headless -- /path/to/consumer` additionally exercises the official CLI with a temporary profile and loopback gateway. The default `verify` includes the complete test suite. See [development](development.md) and [verification](verification.md) for commands and scope.
14
16
 
15
17
  The initial source derives from the user's working copy of DeepSeek Harness on branch `oh-my-dsh`, whose HEAD was `2c8555b323f3b49eac743bafd527f9f3238bf548`, including the uncommitted OpenCode Go settings changes present when this project was created. Runtime conversion modules retain their published behavior. The original checkout is unchanged by this extraction.
@@ -0,0 +1,108 @@
1
+ # Usage analytics notes
2
+
3
+ [Back to README](../README.en.md) · [简体中文](usage-analytics.md)
4
+
5
+ This page documents what the `dsh-opencode-go` usage dashboard counts, how it prices requests, and what it deliberately leaves out.
6
+
7
+ ## Entry points
8
+
9
+ - the **Usage analytics** row at the bottom of the pill's expanded panel, styled like the account row above it;
10
+ - the **Usage analytics** card on the settings page: the same folding card the model and advanced sections use. Unfolded it has two rows — **Dashboard**, which opens the dashboard, and **Theme**, which picks the style. Folded, its trailing note names the style in force.
11
+
12
+ ## Data source
13
+
14
+ The Host reads DSH's `ctx.sessionQuery`:
15
+
16
+ - `listSessions()` lists sessions newest-first;
17
+ - `readSession(id)` returns the complete event log, including closed sessions;
18
+ - only events are scanned; the dashboard never parses compressed logs on disk or makes a session live. After an app restart, the Host reads session events again; the plugin does not keep an on-disk fold cache.
19
+
20
+ The browser receives aggregate numbers only: no session ids, titles, content, working directories, or credentials.
21
+
22
+ Session scanning and aggregation run locally, but model and pricing metadata may refresh from `https://models.dev/api.json`. That request carries no conversation content, account keys or per-request analytics. Normal inference and quota requests still reach your configured model gateway; there is no additional telemetry or analytics service.
23
+
24
+ ## What is counted
25
+
26
+ ### Included
27
+
28
+ - `assistant/message` events on the `dsh-opencode-go` route (one settled model request each);
29
+ - `compaction/summary` events on the same route (the summarization request);
30
+ - the pre-0.1.16 `opencode-go` route, but only for requests before 2026-09-30 UTC. Later `opencode-go` records belong to DSH's native route and are excluded.
31
+
32
+ ### Excluded
33
+
34
+ - DSH's native OpenCode Go route and every other provider;
35
+ - fork-inherited prefixes (sequence numbers strictly below `inheritedEventCount`; the boundary slot belongs to this session, matching the host's half-open `isOwnSeq`);
36
+ - model calls that never enter the session log (for example some title generation);
37
+ - model calls that cannot be attributed to this plugin's route (the log lacks a routing header or model name) — the footer counts them as unattributed activity, without tokens or cost;
38
+ - failed requests whose gateway charge is not represented by a usage record; they appear only as unsettled attempts.
39
+
40
+ ### Request time and range
41
+
42
+ The request start comes from the matching `step/start` event; when that key is absent (legacy or damaged logs) the session's most recent `step/start` is used, and the message event time is the fallback only when a session recorded no start at all. The legacy-route judgment follows the request start time. Summaries use their own event time. The range is half-open `[from, to)` and days are bucketed in the browser's IANA timezone.
43
+
44
+ ### Tokens
45
+
46
+ - input, output, cache read, and cache write are tracked separately;
47
+ - billed input = input + cache read + cache write (pi-ai's input excludes caches);
48
+ - usage degrades per field: a present-but-malformed component zeroes only itself while the valid components still count; a request counts as missing usage only when input and output are both missing or malformed, and missing usage is never zero-filled into the estimate;
49
+ - cache hit rate = cache read / (input + cache read + cache write): output tokens never enter the denominator, matching the repository's existing session statistic; a zero denominator shows 0.0%.
50
+
51
+ ### Estimate
52
+
53
+ - current models.dev rates for `opencode-go` (USD per 1M tokens) — all history is estimated at the rates in force at query time (models.dev publishes no historical rates);
54
+ - the highest matching context tier prices the whole request;
55
+ - DeepSeek V4.1 Flash and V4 Pro double during the documented peak window;
56
+ - a model whose upstream cost block is missing or malformed is treated as unpriced: it adds no cost, is counted separately, and its cost cell shows an **unpriced** tag; such models were previously priced at zero silently — the new display is louder but honest. A genuinely free model with a published, legal (including all-zero) cost block still prices at zero;
57
+ - the lead sentence only states an amount when at least one request was priced: with `pricedRequests === 0` it puts `—` where the amount would go (the same meaning as the KPI's `-`), so "cannot be priced" is never rendered as `$0.00`;
58
+ - when the price table fails or times out (at most a 4-second wait) the last successful table is used; with no usable table at all, every model that appeared is listed as unpriced;
59
+ - amounts accumulate as integer micro-USD; this is an estimate, not an invoice.
60
+
61
+ ## Model detail
62
+
63
+ Each model row shows only the display name plus requests, input, output, cache read, cost, and cost share; the invocation id and the Go / Go Plus monthly limits are no longer rendered (the snapshot data remains in `src/analytics.ts`).
64
+
65
+ The cost cell has three states: priced requests render the estimate (even when some requests in the same row are unpriced); `pricedRequests === 0` with `unpricedRequests > 0` renders the **unpriced** tag; a row whose requests all lack usage (both pricing counters are zero) renders `—`, meaning the row carries no usable data rather than "free". The share cell renders `—` whenever there is no amount, and the composition note's "also free or unpriced" line only lists genuinely unpriced models, no longer mixing in missing-usage rows.
66
+
67
+ The official `/usage` five-hour, weekly, and monthly percentages remain authoritative; the dashboard never derives them from the estimate. The quota row now shows only the window name, percentage, bar, reset time and the near-limit tick — the "plenty left / near limit" wording and the remaining-percentage line are gone.
68
+
69
+ The daily view is pointer-interactive: hovering a calendar cell (or within 5px of one) or any point on the trend shows that day's estimate, request count and tokens, highlights the same bucket in both views and draws a vertical guide. Selection is computed from the distance to the nearest cell: moving more than 5px away from every cell — into the gaps between cells — clears it immediately instead of holding the previous bucket, and leaving the chart area clears it too. Per-model, per-day breakdowns are not part of the current contract, so the tooltip reports the day's totals.
70
+
71
+ The calendar is laid out densely by calendar date across the selected range (a 30-day range always fills 30 cells, capped at 366): dates missing from `byDay` become zero-value cells, so every cell always sits under its true weekday instead of drifting when activity skips dates. Days without activity are hoverable too and report $0.00 / 0 requests; the trend shares this dense series, so the hover index matches in both views. The daily average is computed over the range's calendar days (including days without activity), the same denominator as the KPI band.
72
+
73
+ ## Themes and the pill
74
+
75
+ - Four styles: **Silent Black**, **Graphite** (default), **Sea Salt**, and **Warm Sand**. Each has a dark and a light variant that follows DSH's color scheme.
76
+ - The only switch is the **Theme** row inside the settings page's Usage analytics card, where each option previews its own surface, accent and text colors. The choice is stored in the browser, not in the settings document.
77
+ - The pill popover and the dashboard no longer carry a switch; they render whatever style is selected.
78
+ - The pill no longer carries the `Go ·` prefix; with multiple accounts it shows the active account's full name, truncated with an ellipsis when long, with the complete name kept in the button title and accessible label while the expanded panel still offers every account. Account switching is unchanged, and pill rebuilds triggered by account or connection changes no longer close an open dashboard.
79
+ - The settings page keeps DSH's own appearance and is not affected by these themes.
80
+
81
+ ## Scan limits and performance
82
+
83
+ - by default, at most **500 sessions**, **500,000 events**, and **12 seconds**, read with up to eight concurrent session reads (falling back to serial for a Host that cannot read concurrently);
84
+ - exceeding a limit marks the report incomplete with its cause (session slice / event budget / time budget); the footer also reports how many sessions were left unread when the budget or cap stopped the scan, without implying the range was fully covered;
85
+ - **Scan more history** raises the session cap to 1,000 then 2,000; the choice is stored in the browser and survives reopening the panel;
86
+ - session folds are cached in memory under an LRU policy for at most five seconds; live sessions are re-read on closing, and restarting clears the cache. Eviction happens after scanning, while each report uses its own scan results;
87
+ - the price table is fetched in parallel with the scan, waiting at most 4 seconds;
88
+ - the footer splits list / scan / pricing timings;
89
+ - within one open, the window end is taken once at open time, so re-entering the same range does not rescan; **Refresh** forces a new scan;
90
+ - without a reusable in-memory cache, the scan reads target sessions under those budgets; sessions are read again after restarting or after the five-second reuse window;
91
+ - a range already seen in this open renders its previous report immediately and revalidates in the background, explicitly labeled as the previous scan while loading;
92
+ - closing the dialog aborts the running scan; there is no background polling.
93
+
94
+ ## Query and report contracts
95
+
96
+ `src/contracts.ts` validates query dates, timezone and scan limits. Report types and aggregation live in `src/analytics.ts`. RPC checks the response envelope without manually revalidating every aggregate field. The host and client ship in the same package.
97
+
98
+ ## Known limitations
99
+
100
+ - session request headers carry no account identity, so history cannot be split per account; the dashboard totals every saved account;
101
+ - a request that fails or is canceled before usage may still be billed by the gateway but adds no cost;
102
+ - summaries count as requests, so the request total includes them;
103
+ - device or gateway clock skew can move a request across a peak boundary or a day boundary;
104
+ - models.dev rates and the official plan page can drift; the footer shows the price update time.
105
+
106
+ ## Verification
107
+
108
+ Phase one checks peak/off-peak and tiered pricing, event folding, five-second memory reuse, Go / Go Plus limit data, and the full AnalyticsDialog rendering and interactions. Checks use synthetic sessions and fake query services without reading real user logs. Existing test paths and removed internal contracts will be migrated together in phase two.
@@ -0,0 +1,108 @@
1
+ # 用量统计说明
2
+
3
+ [返回 README](../README.md) · [English](usage-analytics.en.md)
4
+
5
+ 本页说明 `dsh-opencode-go` 用量看板统计什么、如何计价,以及哪些数据不会被计入。
6
+
7
+ ## 入口
8
+
9
+ - 胶囊展开面板底部的「用量统计」行,样式与上方的账号行一致;
10
+ - 设置页的「用量统计」卡片:与「可用模型」「高级设置」同一种折叠卡片,点击展开后分两栏——第一栏「看板」打开看板,第二栏「主题」切换风格。折叠时右侧显示当前风格名。
11
+
12
+ ## 数据来源
13
+
14
+ 看板由 Host 侧读取 DSH 的 `ctx.sessionQuery`:
15
+
16
+ - `listSessions()` 获取会话列表(最新优先);
17
+ - `readSession(id)` 读取完整事件日志,包含已关闭会话;
18
+ - 只扫描事件,不解析磁盘上的压缩日志,也不把会话加载为实时。重启后由宿主重新读取会话事件,不使用插件磁盘折叠缓存。
19
+
20
+ 浏览器只收到聚合后的数字,不包含会话 ID、标题、正文、工作目录或任何密钥。
21
+
22
+ 会话扫描和统计在本地完成,但模型与价格元数据会从 `https://models.dev/api.json` 联网刷新;该请求不携带会话正文、账号密钥或逐条统计记录。正常对话与额度查询仍访问你配置的模型网关,没有额外的遥测或分析服务。
23
+
24
+ ## 统计口径
25
+
26
+ ### 计入的请求
27
+
28
+ - `dsh-opencode-go` 路由下的 `assistant/message`(每次已结算的模型请求);
29
+ - 同一路由下的 `compaction/summary`(上下文压缩的摘要请求);
30
+ - `opencode-go` 旧路由,仅限 0.1.16 改名(2026-09-30 UTC)之前的请求;之后的 `opencode-go` 视为 DSH 原生路由,不计入。
31
+
32
+ ### 不计入
33
+
34
+ - DSH 原生 OpenCode Go 路由与其他 provider;
35
+ - fork 继承的前缀(严格低于 `inheritedEventCount` 的序号;边界槽位属于本会话,与宿主 `isOwnSeq` 的半开区间一致);
36
+ - 未写入会话日志的模型调用(例如部分会话标题生成);
37
+ - 无法归属到插件路由的模型调用(日志缺少路由头或模型名)——页脚以"未归属活动"单独计数,不计 token 与金额;
38
+ - 已失败的请求中网关可能已经计费、但日志里没有 usage 的调用(这类只会计入"未结算尝试")。
39
+
40
+ ### 请求时间
41
+
42
+ 优先取同一 `turn:step` 的 `step/start` 时间(请求开始);该键缺失(遗留或损坏的日志)时取本会话最近一次 `step/start`;整段会话没有任何开始事件时才退回消息事件时间,旧路由判定随请求开始时间自然修正。压缩摘要使用事件时间。区间为半开区间 `[from, to)`,按浏览器传入的 IANA 时区分桶。
43
+
44
+ ### token
45
+
46
+ - 输入、输出、缓存读、缓存写分别统计;
47
+ - 计费输入 = 输入 + 缓存读 + 缓存写(pi-ai 的 input 不含缓存);
48
+ - usage 按字段降级:存在的分量畸形只把该分量记 0,其余有效分量照常入账;仅当输入与输出同时缺失或畸形时,整条请求计入"缺用量",不补零到金额。
49
+ - 缓存命中率 = 缓存读 / (输入 + 缓存读 + 缓存写):输出 token 不计入分母,与仓库既有的会话统计口径一致;分母为 0 时显示 0.0%。
50
+
51
+ ### 金额
52
+
53
+ - 使用 models.dev 中 `opencode-go` 的当前价格(USD / 1M tokens)——全部历史请求都按查询时刻的价目估算(models.dev 不提供历史价);
54
+ - 有上下文阶梯的模型,按计费输入选择最高命中的阈值;
55
+ - DeepSeek V4.1 Flash / V4 Pro 在峰时整体 ×2;
56
+ - 上游缺少价目块或价目块损坏的模型按"未计价"处理:不计金额、单独计数,费用列显示 `未计价` 标签;此前这类模型被静默按 0 价计费,现在更显眼但更诚实。上游发布了合法价目(含全零)的真免费模型仍按 0 价计费;
57
+ - 首句只在一份报告里存在可计价请求时才给出金额:`pricedRequests === 0` 时用「—」代替金额(与 KPI 区的 `-` 同义),不会把"无法计价"写成 `$0.00`;
58
+ - 价目表拉取失败或超时(至多等 4 秒)时回退到最近一次成功的价目表;完全没有任何可用价目时,出现过的模型全部列为未计价;
59
+ - 金额以整数微美元累计,页面显示为美元;这是估算,不是账单。
60
+
61
+ ## 模型明细
62
+
63
+ 模型行只显示模型名称,以及请求、输入、输出、缓存读、花费与花费占比;调用名与 Go / Go Plus 月限额不再出现在看板上(限额快照数据仍保留在 `src/analytics.ts`)。
64
+
65
+ 费用列分三态:有计价请求时显示估算金额(哪怕同一模型还有部分请求未计价);`pricedRequests === 0` 且 `unpricedRequests > 0` 时显示 `未计价` 标签;全部请求都缺用量(两个计价计数器均为 0)时显示 `—`,表示这一行没有任何可用数据,而不是"免费"。占比列在无金额时同样显示 `—`;Composition 的"另有免费或未计价模型"只列真正未计价的模型,缺用量模型不再混入。
66
+
67
+ 官方 `/usage` 的 5 小时 / 每周 / 每月百分比仍是额度权威值;看板不根据金额反推百分比。额度栏只显示窗口名称、百分比、进度、重置时间与临近上限的阈值刻线,不再重复"充足 / 接近上限 / 余量"等文字。
68
+
69
+ 按天区域支持指针悬停:把鼠标移到日历格子上(或靠近格子 5px 以内)或折线上的任意一天,会显示该日的估算金额、请求数与 Token,同时在日历与折线上高亮同一天并画出竖向参考线。判定按"指针到最近格子的距离"计算:移到格子之间的空白处超过 5px 就立即取消选中,不会保留上一格的状态;指针离开图表区域同样清除。按模型、按天的逐日拆分不在当前契约内,悬停只呈现当日合计。
70
+
71
+ 日历按所选区间的每一个日历日稠密排布(30 天区间固定 30 格,上限 366 格),`byDay` 里缺失的日期补成零值格,所以每一格永远落在它真实的星期列下,不会因为中间空档而错位。没有活动的日子同样可以悬停,显示 $0.00 / 0 次;趋势图与日历共用这份稠密序列,悬停索引一一对应。"日均"按区间日历天数计算(分母包含没有活动的日子),与 KPI 区的日均为同一口径。
72
+
73
+ ## 主题与胶囊
74
+
75
+ - 四套主题:**寂静黑**、**石墨**(默认)、**海盐青**、**暖砂**;每套都有深色与浅色版本,跟随 DSH 的明暗模式。
76
+ - 唯一的切换入口是设置页「用量统计」展开后的「主题」栏,每套风格用一张小样显示自己的表面色、强调色与文字色;偏好保存在浏览器本地(不写入设置文档)。
77
+ - 胶囊面板和看板不再提供切换,只按所选风格渲染。
78
+ - 胶囊不再带 `Go ·` 前缀;多账号时直接显示当前账号全名,名字过长时以省略号截断,完整名字保留在按钮标题与读屏标签中,展开面板仍可选择全部账号;账号切换逻辑不变。账号或连接变化引发的胶囊界面重建不再合上打开中的看板。
79
+ - 设置页保持 DSH 自己的外观,不受这套主题影响。
80
+
81
+ ## 扫描上限与性能
82
+
83
+ - 默认最多扫描 **500 个会话**、**500,000 个事件**、**12 秒**,会话日志最多 8 路并发读取(宿主不支持并发时可退回串行);
84
+ - 任一上限触发时,报告标记"不完整"并区分成因(会话数切片 / 事件预算 / 时间预算);页脚另报"预算或上限截止时未读取的会话"数,不暗示范围已完整覆盖;
85
+ - 可点击 **扫描更多历史** 将会话上限提升到 1000 / 2000;该选择保存在浏览器本地,重新打开面板后保留;
86
+ - 会话折叠结果仅在进程内按 LRU 缓存,最多复用 5 秒;live 会话关闭时重新读取,重启后重新扫描。逐出在每轮扫描结束后进行,当轮报告使用本轮实际读到的结果,不因逐出丢数;
87
+ - 价目表在扫描开始时并行拉取,至多等 4 秒;
88
+ - 页脚分列显示列表 / 扫描 / 定价耗时;
89
+ - 同一次打开内区间终点只取打开时刻一次,重复进入同一区间不会重复扫描,**刷新**按钮强制重新扫描;
90
+ - 没有可复用内存缓存时,扫描读取目标会话并遵守上述预算;重启或超过 5 秒复用窗口后重新读取;
91
+ - 本次打开已看过的区间再次选中时,立即渲染上次的报告并在后台重新校验,加载期间明确标注"以下为上一轮结果";
92
+ - 关闭弹窗会中止进行中的扫描,不后台轮询。
93
+
94
+ ## 查询与报告契约
95
+
96
+ 查询范围在 `src/contracts.ts` 中校验,限制日期、时区和扫描上限。报告类型与聚合逻辑统一位于 `src/analytics.ts`;RPC 只做轻量响应结构检查,不再对每个响应字段重复执行手工校验。宿主与客户端随同一个包发布。
97
+
98
+ ## 已知限制
99
+
100
+ - 会话日志的 `request/header` 不含账号信息,因此历史数据无法按账号拆分;看板展示所有已保存账号的合计。
101
+ - 请求若在产生 usage 前失败/取消,可能已被网关计费,但不会产生金额。
102
+ - 压缩摘要与普通消息都按请求计入;"请求"数因此包含摘要调用。
103
+ - 设备时间或网关时钟偏差会影响峰时边界与按天分桶。
104
+ - models.dev 价格与官方套餐页可能短期不同步;页面显示价格更新时间。
105
+
106
+ ## 验证
107
+
108
+ 第 1 阶段已检查峰谷和阶梯计费、会话事件折叠、5 秒内存缓存、Go / Go Plus 限额数据,以及完整 AnalyticsDialog 的渲染与交互。检查使用合成会话与假 query 服务,不读取真实用户日志。现有测试文件的模块路径与已移除内部实现将在第 2 阶段统一整理。