@danypops/jittor 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -4
- package/docs/PROVIDER_RESEARCH.md +21 -2
- package/extension/src/benchmark-tui.ts +6 -4
- package/extension/src/footer.ts +38 -22
- package/extension/src/index.ts +73 -9
- package/extension/src/tui.ts +29 -2
- package/extension/src/usage.ts +24 -15
- package/package.json +5 -1
- package/src/adapters/openrouter-benchmark-index-source.ts +2 -1
- package/src/adapters/sqlite-metric-store.ts +43 -2
- package/src/cli.ts +139 -10
- package/src/client.ts +12 -44
- package/src/constants.ts +31 -4
- package/src/daemon.ts +47 -46
- package/src/db.ts +13 -30
- package/src/domain/model-observation.ts +41 -19
- package/src/domain/model-ranking-service.ts +3 -2
- package/src/domain/model-ranking.ts +31 -12
- package/src/domain/task-cost.ts +70 -0
- package/src/domain/task-focus.ts +65 -0
- package/src/domain/usage.ts +80 -56
- package/src/log.ts +28 -0
- package/src/ports/metric-store.ts +30 -0
- package/src/providers/anthropic-contracts.ts +22 -11
- package/src/providers/google-adc-auth.ts +63 -0
- package/src/providers/google-vertex-budget-contracts.ts +181 -0
- package/src/providers/google-vertex-budget.ts +127 -0
- package/src/providers/google-vertex-contracts.ts +14 -2
- package/src/providers/telemetry-sources.ts +35 -0
- package/src/service.ts +80 -22
- package/src/state.ts +31 -57
- package/src/version.ts +2 -14
package/README.md
CHANGED
|
@@ -35,9 +35,11 @@ SQLite runs in WAL mode with versioned migrations, JSON validation, bounded quer
|
|
|
35
35
|
|
|
36
36
|
Operations currently include bounded metric recording/query/pruning, benchmark refresh/status/query, context assessment, routing control, telemetry polling, and service checkpointing. Every operation is exposed through the authenticated typed client; benchmark operations also have CLI parity.
|
|
37
37
|
|
|
38
|
-
Provider adapters currently include official OpenRouter key/usage/model telemetry and an explicitly experimental Codex subscription adapter. The Codex adapter follows the pinned open-source CLI `/wham/usage` payload and `x-codex-*` response-header contracts, accepts additional metered limits, and fails closed on malformed windows or impossible percentages. File credentials must be explicitly configured and private (`0600`); Jittor reads only the access token and account ID, never refreshes credentials, and never logs or persists OAuth secrets. Anthropic has no personal-account polling endpoint (its Admin/Rate Limits API is documented as unavailable for individual accounts), so Jittor instead reads the official `anthropic-ratelimit-*` response headers Pi observes on every Messages API call and fails closed on schema drift the same way. Google Vertex AI has neither a personal polling endpoint nor a documented remaining-quota response header, so Jittor never fabricates a Vertex budget bar; it instead classifies Vertex's `google.rpc.Status` failure shape (quota, authentication, invalid-request, overload, transport, unknown) from Pi's bounded, content-free `errorMessage` and records only a bounded failure-count metric.
|
|
38
|
+
Provider adapters currently include official OpenRouter key/usage/model telemetry and an explicitly experimental Codex subscription adapter. The Codex adapter follows the pinned open-source CLI `/wham/usage` payload and `x-codex-*` response-header contracts, accepts additional metered limits, and fails closed on malformed windows or impossible percentages. File credentials must be explicitly configured and private (`0600`); Jittor reads only the access token and account ID, never refreshes credentials, and never logs or persists OAuth secrets. Anthropic has no personal-account polling endpoint (its Admin/Rate Limits API is documented as unavailable for individual accounts), so Jittor instead reads the official `anthropic-ratelimit-*` response headers Pi observes on every Messages API call and fails closed on schema drift the same way. Google Vertex AI has neither a personal polling endpoint nor a documented remaining-quota response header, so Jittor never fabricates a Vertex budget bar; it instead classifies Vertex's `google.rpc.Status` failure shape (quota, authentication, invalid-request, overload, transport, unknown) from Pi's bounded, content-free `errorMessage` and records only a bounded failure-count metric. Because no budget signal can ever exist for this provider, the footer's `budget` segment is omitted entirely for it rather than showing a permanent `?` placeholder that could never resolve; the `?` placeholder is reserved for providers that can report a budget but simply haven't yet (router not ready, or telemetry not observed on the first turn).
|
|
39
39
|
|
|
40
|
-
The
|
|
40
|
+
The third-party `anthropic-vertex` provider (Anthropic Claude models served through Google Vertex, e.g. via `@twogiants/pi-anthropic-vertex`) is tracked separately from both of the above: it reuses Pi's own Anthropic Messages stream implementation with Anthropic's official `@anthropic-ai/vertex-sdk` client, so its wire shape is Anthropic's, but its quota accounting is Google's. Jittor applies Google Vertex's failure classification to it (real-world reports confirm its 429s still carry GCP's own quota-exceeded shape even through Anthropic's own SDK) and, best-effort, also checks for genuine Anthropic rate-limit response headers on it, since it is unverified whether this specific passthrough ever forwards them. Either way, every metric is tagged `anthropic-vertex`, never blended into direct Anthropic's `anthropic` source or Pi's unrelated native `google-vertex` provider, since each represents a different account/quota pool. Its footer budget (labeled `vtok`/`vreq` when headers are observed) stays `null` (may still resolve) rather than `undefined` (provably impossible) until it's confirmed one way or the other.
|
|
41
|
+
|
|
42
|
+
The native Pi extension preflights input and every provider turn, applies model/thinking decisions, records response headers and finalized usage through the daemon, and blocks requests when required telemetry is unsafe. It follows Pi's current authenticated model/provider and synchronizes Pi's available models before every decision, so unavailable catalog routes are never selected. Its responsive integrated footer groups repository and model identity with cumulative usage, a color-coded context-window bar, and current-provider budget telemetry. Codex shows the active model's bounded quota as a draining remaining-budget bar with reset and freshness information. OpenRouter uses the same drain semantics when its official key telemetry exposes a configured limit and remaining balance; keys without a limit remain honest text-only spend and never receive a fabricated denominator. Anthropic shows the same drain semantics from its most-restrictive-in-effect token bucket, falling back to the request bucket when no token telemetry has been observed yet. During Pi compaction, the context bar drains against a learned median duration estimated from the last few completed compactions (bounded to the most recent 20 samples, requiring at least 3 before trusting it), in exact sync with a countdown ("compact ~Ns left") — never a count-up, never a fabricated total. Until enough evidence exists, the bar does not drain at all (there is no real rate to drain against) and no timer text is shown; the bar itself simply blinks in place at its starting fill once per render tick so compaction never looks stalled without claiming knowledge it doesn't have. Run `jittor compaction estimate [--json]` to inspect the current estimate and its confidence directly. Unknown and stale telemetry are marked explicitly. Run `/jittor` for the consolidated Settings TUI (its default action), or `/jittor status` for detailed burn pressure, freshness, route state, and confirmed emergency-halt/override controls.
|
|
41
43
|
|
|
42
44
|
Jittor currently registers no model-callable native tools, so Pi's native model `content` versus renderer `details` contract is explicitly not applicable. Daemon JSON, CLI `--json`, human CLI output, command notifications, panels, and the footer remain separate bounded channels. See [`docs/OUTPUT_CHANNELS.md`](docs/OUTPUT_CHANNELS.md) for the conformance matrix and the requirements that apply if a native tool is introduced later.
|
|
43
45
|
|
|
@@ -66,6 +68,12 @@ Jittor observes finalized Codex assistant errors through Pi's public message lif
|
|
|
66
68
|
|
|
67
69
|
`/usage` is its own top-level command, separate from `/jittor`. Bare `/usage` opens a colored Unicode cumulative graph with X/Y axes, per-provider/model series, and explicit **Hourly**, **Daily**, **Weekly**, **Monthly**, and **Quarterly** periods; `/usage cost` opens the same graph showing aggregated USD spend instead of tokens, reusing the `cost` metric already recorded content-free on every finalized Pi assistant message (no new instrumentation). Left/Right or Tab/Shift+Tab changes the time frame, `v` toggles between the token and cost views, and `r` refreshes.
|
|
68
70
|
|
|
71
|
+
The graph fetches metrics per distinct provider/model scope (`jittor metrics distinct-scopes`, bounded to 40 scopes, 250 rows each) rather than one flat "most recent rows" query. A flat query lets one heavy, long-running session monopolize the entire row budget with its own most recent activity, silently hiding every other provider from the chart no matter which time frame is selected, since the query would never reach back far enough in time to see anything else. Fetching per scope guarantees every active provider/model gets its own fair share of the query budget instead.
|
|
72
|
+
|
|
73
|
+
### Cost per Papyrus task
|
|
74
|
+
|
|
75
|
+
Jittor observes Papyrus's task-focus lifecycle in real time over a shared Pi extension event bus (`papyrus.task-focus.v1`) -- Papyrus never depends on Jittor, it only broadcasts which task is currently focused. Every token/cost metric Jittor already records on a finalized Pi assistant message is tagged with the currently focused task's id the moment it is recorded (no time-window estimation, no new instrumentation). A paused or cleared focus stops tagging; spend recorded with nothing focused is reported separately as unattributed, never dropped or folded into an invented task. Run `jittor metrics cost-by-task --since <ms> --until <ms> [--json]` for a bounded per-task breakdown of cost and input/output/cache tokens.
|
|
76
|
+
|
|
69
77
|
Series are colored with a categorical palette chosen to avoid this UI's own status colors ("success"/"warning"/"error" already mean something specific elsewhere in this panel, so reusing them for arbitrary model identity would make a model's bar segment look like a warning or a failure) and instead reuses the theme's syntax-highlighting roles, which are already tuned by theme authors to stay mutually distinguishable on screen — the same design problem as a categorical data palette. Once more series are active than there are hues, a series reuses a hue in bold rather than repeating an indistinguishable color. Multiple models active within the same cumulative time frame are rendered as one bar stacked by color, not separate bars.
|
|
70
78
|
|
|
71
79
|
Token-budget thresholds are optional and must be configured by the user; Jittor never infers a token allowance from Codex or another provider's subscription percentage. Configure or clear one period with `/usage budget <hourly|daily|weekly|monthly|quarterly> <positive-tokens|off>`, and inspect all of them with `/usage budget`. A configured budget appears as a horizontal threshold on the cumulative graph with explicit remaining or **OVER BUDGET** state; the cost view does not yet support a budget threshold. These private settings persist in `$XDG_CONFIG_HOME/jittor/extension.json` (or `~/.config/jittor/extension.json`).
|
|
@@ -84,9 +92,9 @@ jittor benchmarks list --source openrouter-models [--model provider/model] [--di
|
|
|
84
92
|
|
|
85
93
|
Only complete snapshots are queryable. Query output reports both completeness and freshness. See [`docs/BENCHMARK_SOURCES.md`](docs/BENCHMARK_SOURCES.md) for source authority, provenance, conflict, and redistribution rules.
|
|
86
94
|
|
|
87
|
-
Jittor separately records content-free local model observations from Pi's public lifecycle: TTFT, wall latency, output throughput, token/cache/cost efficiency, provider retries, tool-loop counts, failures, and
|
|
95
|
+
Jittor separately records content-free local model observations from Pi's public lifecycle: TTFT, wall latency, output throughput, token/cache/cost efficiency, provider retries, tool-loop counts, failures, and two independent classifications derived only from bounded tool names: domain (subject matter, e.g. `coding`) and type (activity, e.g. `research`, `planning`) -- a run can be domain=coding and type=research at once. Prompts, responses, tool arguments/results, credentials, and project paths are never retained. `/jittor outcome accepted` or `/jittor outcome rejected` attaches explicit outcome evidence to the latest completed local run; runtime completion alone is not treated as quality success. Robust aggregates report sample size, median, p90, median absolute deviation, recency, and confidence without merging local observations into external benchmark facts.
|
|
88
96
|
|
|
89
|
-
The ranking operation accepts an explicit bounded candidate set and never adds identities found only in evidence. It scores
|
|
97
|
+
The ranking operation accepts an explicit bounded candidate set and never adds identities found only in evidence. It scores quality (both a domain-specific dimension, e.g. `quality-coding`, and a type-specific dimension, e.g. `quality-type-planning`, each optional and additive over the universal `quality-general` fallback), cost, latency, context, and local reliability with bounded user weights, budget-pressure adjustment, component confidence, freshness, provenance, and deterministic tie-breaking. Missing evidence remains unknown and lowers confidence. Run `/jittor benchmarks [coding|general] [research|planning|general]` (either order, either or both omitted) for the responsive recommendation panel. Because the released Pi extension API does not expose the exact `/scoped-models` set, the current adapter labels candidates `available-models`; the panel says **ADVISORY** and offers no selection action. Automatic route ordering is allowed only for `exact-session` authority and then narrows/reorders routes already present in the supplied candidate set.
|
|
90
98
|
|
|
91
99
|
### Context pressure
|
|
92
100
|
|
|
@@ -102,6 +110,9 @@ Every daemon operation is reachable from the CLI through the authenticated typed
|
|
|
102
110
|
jittor metrics record --source <s> --scope <s> --metric <s> --value <number|null> --unit <unit> [--observed-at <ms>] [--attributes <json>] [--json]
|
|
103
111
|
jittor metrics query [--source <s>] [--scope <s>] [--metric <s>] [--since <ms>] [--until <ms>] [--limit <n>] [--order asc|desc] [--json]
|
|
104
112
|
jittor metrics prune --before <ms> [--json]
|
|
113
|
+
jittor metrics distinct-scopes --source <s> --since <ms> --until <ms> [--limit 1..40] [--json]
|
|
114
|
+
jittor metrics cost-by-task --since <ms> --until <ms> [--json]
|
|
115
|
+
jittor metrics prune --before <ms> [--force] [--json] # force required if before is newer than 24h ago
|
|
105
116
|
jittor service checkpoint [--json]
|
|
106
117
|
jittor telemetry poll [--json]
|
|
107
118
|
jittor compaction estimate [--json]
|
|
@@ -183,7 +183,20 @@ anthropic-priority-output-tokens-limit / -remaining / -reset (Priority Tier onl
|
|
|
183
183
|
|
|
184
184
|
Unlike Anthropic and OpenRouter, Vertex does not document a per-response rate-limit or remaining-quota header for `generateContent`/Messages-compatible calls. Quota is configured and reported at the Google Cloud project/region level (Service Usage / Quota APIs, Cloud Console "Quotas & System Limits"), which is an account-configuration surface, not a response header Jittor could read before a request is throttled — the same class of limitation Amazon Bedrock has (see Papyrus doc `jittor-provider-survey-which-additional-apis-to-support-0wma`, Tier 2). Failures instead surface as a `google.rpc.Status` shape, `{error: {code, message, status, details[]}}`, with `status` one of the canonical gRPC codes (`RESOURCE_EXHAUSTED`, `PERMISSION_DENIED`, `UNAUTHENTICATED`, `UNAVAILABLE`, `DEADLINE_EXCEEDED`, `INVALID_ARGUMENT`, ...), sometimes with a `google.rpc.RetryInfo.retryDelay` or `google.rpc.QuotaFailure` detail.
|
|
185
185
|
|
|
186
|
-
Jittor therefore does not fabricate a remaining-budget bar for Vertex. It classifies the bounded, content-free `errorMessage` string Pi already exposes for every provider (the same source `classifyCodexFailure` reads) into a failure kind and transience, and records only a bounded failure-count metric (`source: "google-vertex", scope: "failure", metric: <kind>, unit: "count"`) — never a `ratio` metric implying a known remaining fraction. This is an honest degradation: Jittor surfaces *that* and *what kind of* capacity/auth/request pressure Pi is seeing, without claiming to know how much budget remains.
|
|
186
|
+
Jittor therefore does not fabricate a remaining-budget bar for Vertex from the response path alone. It classifies the bounded, content-free `errorMessage` string Pi already exposes for every provider (the same source `classifyCodexFailure` reads) into a failure kind and transience, and records only a bounded failure-count metric (`source: "google-vertex", scope: "failure", metric: <kind>, unit: "count"`) — never a `ratio` metric implying a known remaining fraction. This is an honest degradation: Jittor surfaces *that* and *what kind of* capacity/auth/request pressure Pi is seeing, without claiming to know how much budget remains.
|
|
187
|
+
|
|
188
|
+
### A real hot(ish)-path signal exists once quota moves to individual per-user GCP projects
|
|
189
|
+
|
|
190
|
+
The "no telemetry" conclusion above is specific to a *shared* GCP project, where Google has nothing to key a per-user signal on. Several organizations are migrating Vertex/Claude-on-Vertex access from one shared project to one GCP project per individual user specifically so usage/cost can be attributed per person — and that migration changes the honest answer, because Cloud Billing budgets support **project-scoped access** without any Cloud Billing account IAM role: `resourcemanager.projects.get` + `billing.resourcebudgets.read`/`billing.resourceCosts.get` on the project alone are enough (verified against `docs.cloud.google.com/billing/docs/how-to/budget-api-access-control` and `.../billing/docs/how-to/budgets`, fetched 2026-07-23) — matching a "passwordless/keyless", ADC-only individual-project auth model with no static service-account key.
|
|
191
|
+
|
|
192
|
+
Two API surfaces matter here, and they answer different questions:
|
|
193
|
+
|
|
194
|
+
- **`billingAccounts.budgets.get`/`.list`** (REST, project-scoped per above) returns only the budget's *policy* — its cap (`amount`), alert `thresholdRules`, and filters. It does **not** return current spend. Resolving which billing account owns a given project first requires `projects.getBillingInfo` (`cloudbilling.googleapis.com`, also just `resourcemanager.projects.get`/Project Viewer).
|
|
195
|
+
- **Cloud Billing's own programmatic budget notifications**, delivered over Pub/Sub, are the real signal: Google's docs state notifications are "sent to the Pub/Sub topic **multiple times per day** with the current status of your budget" (not only on threshold crossings), each message carrying real dollar figures — `costAmount`, `budgetAmount`, `costIntervalStart`, `currencyCode`, and (only once actually exceeded) `alertThresholdExceeded`/`forecastThresholdExceeded` (schema verified against `docs.cloud.google.com/billing/docs/how-to/budgets-programmatic-notifications#notification-format` and the worked fixture in `.../billing/docs/how-to/listen-to-notifications`, fetched 2026-07-23). Since Jittor is a local, loopback-only daemon with no public inbound endpoint, it **pulls** (never pushes) this topic via a Pub/Sub pull subscription (`pubsub.googleapis.com/v1/{subscription}:pull`), authenticated the same ADC way.
|
|
196
|
+
|
|
197
|
+
This is still not a fully real-time signal, and the docs are explicit about both caveats Jittor must preserve rather than paper over: (1) "Budgets use estimated Cloud Billing data which is subject to change until your invoice is finalized", and (2) "Pub/Sub only provides at-least-once delivery. You might receive a message multiple times, and messages might arrive out of order." Jittor's `GoogleVertexBudgetTelemetryAdapter` (`src/providers/google-vertex-budget.ts`) selects the freshest pulled message by its own Pub/Sub `publishTime`, fails closed (throws) on any message that doesn't match the documented schema, and reports this signal at a lower confidence (`0.6`, vs. `0.8` for Codex's header-derived windows) than a per-response header would earn. `spend`/`cap` are recorded as real USD metrics and their `spend-fraction` ratio is left unclamped (so a genuine over-cap soft-quota period stays visible); only the policy-facing `BudgetWindow.usedFraction` is clamped to `1.0`, since a known-to-be-≥100% real number floored to the window's documented `[0,1]` invariant is not the same thing as fabricating one from nothing.
|
|
198
|
+
|
|
199
|
+
Setting this up requires one-time configuration outside Jittor for each individual project: create a Pub/Sub topic, connect it to the project's budget (Project Owner/Editor role, plus Pub/Sub Admin on whichever project holds the topic), and create a pull subscription on it. Jittor only consumes the subscription; it does not provision any of this GCP-side configuration itself.
|
|
187
200
|
|
|
188
201
|
## Normalized Jittor model
|
|
189
202
|
|
|
@@ -267,7 +280,13 @@ Required safeguards: hysteresis, cooldown, maximum delay, minimum telemetry fres
|
|
|
267
280
|
### Google Vertex AI
|
|
268
281
|
|
|
269
282
|
- Google Cloud/Gemini API 429 `RESOURCE_EXHAUSTED` error reports and `google.rpc.Status`/`QuotaFailure`/`RetryInfo` detail shapes, cross-checked across multiple live incident reports (fetched 2026-07-21); no official Vertex response header for remaining quota was found
|
|
270
|
-
-
|
|
283
|
+
- Amazon Bedrock documents the same account-level-quota-not-header pattern as a cross-check on the general "cloud-vendor AI quota lives at the account/project layer, not a response header" shape
|
|
284
|
+
- https://docs.cloud.google.com/billing/docs/how-to/budget-api-access-control (project-scoped `GetBudget`/`ListBudgets` permissions, fetched 2026-07-23)
|
|
285
|
+
- https://docs.cloud.google.com/billing/docs/how-to/budgets ("Project-scoped billing permissions" / single-project access section, fetched 2026-07-23)
|
|
286
|
+
- https://docs.cloud.google.com/billing/docs/reference/rest/v1/projects/getBillingInfo (fetched 2026-07-23)
|
|
287
|
+
- https://docs.cloud.google.com/billing/docs/how-to/budgets-programmatic-notifications#notification-format (fetched 2026-07-23)
|
|
288
|
+
- https://docs.cloud.google.com/billing/docs/how-to/listen-to-notifications (worked notification fixture used verbatim in `test/google-vertex-budget.test.ts`, fetched 2026-07-23)
|
|
289
|
+
- https://docs.cloud.google.com/billing/docs/reference/budget/rest/v1/billingAccounts.budgets (Budget resource schema — confirms no current-spend field, fetched 2026-07-23)
|
|
271
290
|
|
|
272
291
|
### Pi
|
|
273
292
|
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
MODEL_RANKING_DEFAULT_QUALITY_WEIGHT,
|
|
10
10
|
MODEL_RANKING_DEFAULT_RELIABILITY_WEIGHT,
|
|
11
11
|
} from "../../src/constants.ts";
|
|
12
|
-
import type {
|
|
12
|
+
import type { ModelTaskDomain, ModelTaskType } from "../../src/domain/model-observation.ts";
|
|
13
13
|
import type { ModelCandidate, ModelRankingResult, RankedModel, UtilityComponentName } from "../../src/domain/model-ranking.ts";
|
|
14
14
|
|
|
15
15
|
export interface BenchmarkPanelClient {
|
|
@@ -52,7 +52,7 @@ export function renderBenchmarkView(result: ModelRankingResult, currentIdentity:
|
|
|
52
52
|
theme.fg("borderMuted", "─".repeat(safeWidth)),
|
|
53
53
|
theme.bold("Jittor Benchmark Recommendations"),
|
|
54
54
|
result.scopeAuthority === "exact-session" ? "Scope: exact session" : "Scope: available models · ADVISORY (exact session scope unavailable)",
|
|
55
|
-
`
|
|
55
|
+
`Domain: ${result.domain} · Type: ${result.type} · evidence ${result.completeness}`,
|
|
56
56
|
reason,
|
|
57
57
|
...shown.flatMap((item, index) => candidateLines(item, index, currentIdentity)),
|
|
58
58
|
...(result.ranked.length > shown.length ? [` … ${result.ranked.length - shown.length} more candidates omitted`] : []),
|
|
@@ -68,13 +68,15 @@ export async function showBenchmarkPanel(
|
|
|
68
68
|
client: BenchmarkPanelClient,
|
|
69
69
|
candidates: ModelCandidate[],
|
|
70
70
|
currentIdentity: string,
|
|
71
|
-
|
|
71
|
+
domain: ModelTaskDomain,
|
|
72
|
+
type: ModelTaskType,
|
|
72
73
|
): Promise<void> {
|
|
73
74
|
for (;;) {
|
|
74
75
|
const result = await client.call("models.rank", {
|
|
75
76
|
candidates,
|
|
76
77
|
scopeAuthority: "available-models",
|
|
77
|
-
|
|
78
|
+
domain,
|
|
79
|
+
type,
|
|
78
80
|
budgetPressure: 0,
|
|
79
81
|
weights: {
|
|
80
82
|
quality: MODEL_RANKING_DEFAULT_QUALITY_WEIGHT,
|
package/extension/src/footer.ts
CHANGED
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
FOOTER_CONTEXT_ACCENT_FRACTION,
|
|
8
8
|
FOOTER_CONTEXT_ERROR_FRACTION,
|
|
9
9
|
FOOTER_COMPACTION_BLINK_HALF_PERIOD_MS,
|
|
10
|
-
FOOTER_COMPACTION_DRAIN_STEP_MS,
|
|
11
10
|
FOOTER_CONTEXT_WARNING_FRACTION,
|
|
12
11
|
FOOTER_WIDE_TERMINAL_WIDTH,
|
|
13
12
|
MILLISECONDS_PER_DAY,
|
|
@@ -140,19 +139,18 @@ function fillColor(fraction: number | null): FooterColor {
|
|
|
140
139
|
/**
|
|
141
140
|
* Once a learned median duration is available (see estimateCompactionDuration / the
|
|
142
141
|
* `compaction.estimate` daemon operation), the bar drains against that real estimate: fraction
|
|
143
|
-
* counts down linearly from 1 to 0 over estimatedMs
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
142
|
+
* counts down linearly from 1 to 0 over estimatedMs, exactly in step with the countdown shown in
|
|
143
|
+
* compactionStatusText — same elapsed/estimatedMs ratio drives both. Until then — cold start, or
|
|
144
|
+
* the estimate fetch has not resolved yet — there is no real duration to drain against, so the
|
|
145
|
+
* fill holds steady at the fraction observed when compaction started; the blink alone (see
|
|
146
|
+
* compactionBarGlyph) communicates liveness without fabricating a rate.
|
|
147
147
|
*/
|
|
148
148
|
function compactionFraction(progress: CompactionProgress, width: number, now: number): number {
|
|
149
149
|
if (progress.confidence === "learned" && typeof progress.estimatedMs === "number" && progress.estimatedMs > 0) {
|
|
150
150
|
const elapsed = Math.max(0, now - progress.startedAt);
|
|
151
151
|
return Math.max(0, Math.min(1, 1 - (elapsed / progress.estimatedMs)));
|
|
152
152
|
}
|
|
153
|
-
|
|
154
|
-
const drained = Math.floor(Math.max(0, now - progress.startedAt) / FOOTER_COMPACTION_DRAIN_STEP_MS);
|
|
155
|
-
return Math.max(0, initialFilled - drained) / width;
|
|
153
|
+
return Math.min(1, Math.max(0, progress.initialFraction));
|
|
156
154
|
}
|
|
157
155
|
|
|
158
156
|
/**
|
|
@@ -166,18 +164,30 @@ export function compactionBlinkOn(startedAt: number, now: number, halfPeriodMs =
|
|
|
166
164
|
return Math.floor(elapsed / halfPeriodMs) % 2 === 0;
|
|
167
165
|
}
|
|
168
166
|
|
|
169
|
-
|
|
170
|
-
|
|
167
|
+
/**
|
|
168
|
+
* The compaction signal lives in the bar itself: it blinks between its normal draining fill and a
|
|
169
|
+
* blank track of the same width, rather than a separate indicator glyph next to it. Off-phase
|
|
170
|
+
* intentionally renders identically to the "no data" empty track (dim, all "░") so the bar reads
|
|
171
|
+
* as a single blinking element, not a bar plus a decoration.
|
|
172
|
+
*/
|
|
173
|
+
function compactionBarGlyph(progress: CompactionProgress, theme: FooterTheme, width: number, now: number): string {
|
|
174
|
+
if (!compactionBlinkOn(progress.startedAt, now)) return theme.fg("dim", "░".repeat(width));
|
|
175
|
+
const fraction = compactionFraction(progress, width, now);
|
|
176
|
+
return theme.fg("accent", progressBar(fraction, width));
|
|
171
177
|
}
|
|
172
178
|
|
|
173
|
-
/**
|
|
174
|
-
|
|
175
|
-
|
|
179
|
+
/**
|
|
180
|
+
* A countdown, never a count-up: once a learned estimate exists it reports seconds remaining,
|
|
181
|
+
* ticking down toward zero in step with the draining bar. Before that (cold start, no estimate
|
|
182
|
+
* yet) there is nothing true to count down from, so this reports nothing at all rather than a
|
|
183
|
+
* fabricated elapsed count or a guessed total — the blinking, non-draining bar is the only signal.
|
|
184
|
+
*/
|
|
185
|
+
function compactionStatusText(progress: CompactionProgress, now: number): string | undefined {
|
|
176
186
|
if (progress.confidence === "learned" && typeof progress.estimatedMs === "number" && progress.estimatedMs > 0) {
|
|
177
187
|
const remainingSeconds = Math.max(0, Math.ceil((progress.estimatedMs - (now - progress.startedAt)) / MILLISECONDS_PER_SECOND));
|
|
178
|
-
return `compact
|
|
188
|
+
return `compact ~${remainingSeconds}s left`;
|
|
179
189
|
}
|
|
180
|
-
return
|
|
190
|
+
return undefined;
|
|
181
191
|
}
|
|
182
192
|
|
|
183
193
|
function contextSegment(
|
|
@@ -190,8 +200,9 @@ function contextSegment(
|
|
|
190
200
|
): string {
|
|
191
201
|
const w = barWidth(width);
|
|
192
202
|
if (compaction) {
|
|
193
|
-
const
|
|
194
|
-
|
|
203
|
+
const bar = compactionBarGlyph(compaction, theme, w, now);
|
|
204
|
+
const statusText = compactionStatusText(compaction, now);
|
|
205
|
+
return statusText === undefined ? `ctx ${bar}` : `ctx ${bar} ${statusText}`;
|
|
195
206
|
}
|
|
196
207
|
const usage = context.getContextUsage();
|
|
197
208
|
const window = usage?.contextWindow ?? context.model?.contextWindow ?? 0;
|
|
@@ -211,8 +222,7 @@ function minimalContextSegment(
|
|
|
211
222
|
): string {
|
|
212
223
|
const w = barWidth(width);
|
|
213
224
|
if (compaction) {
|
|
214
|
-
|
|
215
|
-
return `ctx ${theme.fg("accent", progressBar(fraction, w))} ${compactionBlinkGlyph(compaction, now)}`;
|
|
225
|
+
return `ctx ${compactionBarGlyph(compaction, theme, w, now)}`;
|
|
216
226
|
}
|
|
217
227
|
const percent = context.getContextUsage()?.percent;
|
|
218
228
|
const fraction = percent === null || percent === undefined ? null : percent / 100;
|
|
@@ -228,7 +238,13 @@ function resetLabel(resetsAt: number | undefined, now: number): string | undefin
|
|
|
228
238
|
return `resets in ${Math.max(1, Math.ceil(remaining / MILLISECONDS_PER_MINUTE))}m`;
|
|
229
239
|
}
|
|
230
240
|
|
|
231
|
-
|
|
241
|
+
/**
|
|
242
|
+
* `undefined` means no budget signal is possible for this provider at all (see buildFooterBudget);
|
|
243
|
+
* the segment is omitted entirely rather than showing a placeholder that could never resolve.
|
|
244
|
+
* `null` means not known yet but might resolve, which still earns the `?` placeholder.
|
|
245
|
+
*/
|
|
246
|
+
function budgetSegment(budget: ProviderBudget | null | undefined, theme: FooterTheme, width: number, compact: boolean, now: number): string | undefined {
|
|
247
|
+
if (budget === undefined) return undefined;
|
|
232
248
|
const w = barWidth(width);
|
|
233
249
|
if (!budget) return `budget ${theme.fg("dim", progressBar(null, w))} ?`;
|
|
234
250
|
const stale = budget.observedAt !== undefined && now - budget.observedAt > TELEMETRY_STALE_AFTER_MS;
|
|
@@ -288,7 +304,7 @@ export function renderFooterLines(
|
|
|
288
304
|
context: FooterContext,
|
|
289
305
|
footerData: FooterData,
|
|
290
306
|
theme: FooterTheme,
|
|
291
|
-
providerBudget: ProviderBudget | null,
|
|
307
|
+
providerBudget: ProviderBudget | null | undefined,
|
|
292
308
|
thinkingLevel: string,
|
|
293
309
|
width: number,
|
|
294
310
|
now = Date.now(),
|
|
@@ -325,7 +341,7 @@ export function renderFooterLines(
|
|
|
325
341
|
}
|
|
326
342
|
|
|
327
343
|
export interface IntegratedFooterState {
|
|
328
|
-
providerBudget: ProviderBudget | null;
|
|
344
|
+
providerBudget: ProviderBudget | null | undefined;
|
|
329
345
|
compaction?: CompactionProgress;
|
|
330
346
|
requestRender?: () => void;
|
|
331
347
|
}
|
package/extension/src/index.ts
CHANGED
|
@@ -10,12 +10,14 @@ import {
|
|
|
10
10
|
MILLISECONDS_PER_MINUTE,
|
|
11
11
|
MILLISECONDS_PER_SECOND,
|
|
12
12
|
PAPYRUS_CONTEXT_INJECTION_CHANNEL,
|
|
13
|
+
PAPYRUS_TASK_FOCUS_CHANNEL,
|
|
13
14
|
CONTEXT_EVENT_DEDUP_LIMIT,
|
|
14
15
|
} from "../../src/constants.ts";
|
|
15
16
|
import { CodexRecoveryPolicy, classifyCodexFailure, type CodexFailureKind, type CodexFailureMetadata } from "../../src/domain/codex-recovery.ts";
|
|
16
17
|
import { CompactionTelemetry, papyrusContextMetric, validatePapyrusContextInjection } from "../../src/domain/context-telemetry.ts";
|
|
18
|
+
import { applyTaskFocusEvent, validateTaskFocusEvent } from "../../src/domain/task-focus.ts";
|
|
17
19
|
import type { MetricObservation, StoredMetricObservation } from "../../src/domain/metric.ts";
|
|
18
|
-
import { classifyTaskFromTools, modelRunMetrics,
|
|
20
|
+
import { classifyTaskFromTools, modelRunMetrics, TASK_DOMAINS, TASK_TYPES, type ModelRunObservation, type ModelTaskDomain, type ModelTaskType } from "../../src/domain/model-observation.ts";
|
|
19
21
|
import type { ModelCandidate } from "../../src/domain/model-ranking.ts";
|
|
20
22
|
import { USAGE_PERIODS, type UsagePeriod } from "../../src/domain/usage.ts";
|
|
21
23
|
import type { PolicyDecision, Route } from "../../src/policy.ts";
|
|
@@ -228,7 +230,8 @@ async function applyDecision(
|
|
|
228
230
|
return halt(ctx, `Jittor could not apply any authenticated Pi route after ${decision.route.provider}/${decision.route.model} became unavailable`);
|
|
229
231
|
}
|
|
230
232
|
|
|
231
|
-
|
|
233
|
+
/** taskId, when a Papyrus task is currently focused in this session, tags the metric for real-time cost-per-task correlation without any new instrumentation surface. */
|
|
234
|
+
function assistantUsageMetrics(message: unknown, observedAt: number, taskId: string | null = null): MetricObservation[] {
|
|
232
235
|
if (typeof message !== "object" || message === null || Array.isArray(message)) return [];
|
|
233
236
|
const value = message as Record<string, unknown>;
|
|
234
237
|
if (value["role"] !== "assistant" || typeof value["usage"] !== "object" || value["usage"] === null) return [];
|
|
@@ -236,7 +239,7 @@ function assistantUsageMetrics(message: unknown, observedAt: number): MetricObse
|
|
|
236
239
|
const provider = typeof value["provider"] === "string" ? value["provider"] : "unknown";
|
|
237
240
|
const model = typeof value["model"] === "string" ? value["model"] : "unknown";
|
|
238
241
|
const scope = `${provider}:${model}`;
|
|
239
|
-
const attributes = { provider, model };
|
|
242
|
+
const attributes = { provider, model, ...(taskId === null ? {} : { taskId }) };
|
|
240
243
|
const metrics: MetricObservation[] = [];
|
|
241
244
|
for (const [field, metric] of [["input", "input-tokens"], ["output", "output-tokens"], ["cacheRead", "cache-read-tokens"], ["cacheWrite", "cache-write-tokens"]] as const) {
|
|
242
245
|
const amount = usage[field];
|
|
@@ -274,6 +277,21 @@ export function registerJittorExtension(
|
|
|
274
277
|
// Reject malformed or stale cross-extension observations without retaining payloads.
|
|
275
278
|
}
|
|
276
279
|
});
|
|
280
|
+
// Real-time cost-per-task correlation: Jittor observes Papyrus's task-focus broadcasts (Papyrus
|
|
281
|
+
// never depends on Jittor) and tags newly recorded token/cost metrics with the currently focused
|
|
282
|
+
// task id. Scoped to this Pi session: a focus change in a different concurrent session must not
|
|
283
|
+
// affect this one's attribution.
|
|
284
|
+
let currentSessionId: string | undefined;
|
|
285
|
+
let focusedTaskId: string | null = null;
|
|
286
|
+
const stopPapyrusTaskFocus = pi.events?.on?.(PAPYRUS_TASK_FOCUS_CHANNEL, (payload) => {
|
|
287
|
+
try {
|
|
288
|
+
const event = validateTaskFocusEvent(payload);
|
|
289
|
+
if (event.sessionId !== undefined && event.sessionId !== currentSessionId) return;
|
|
290
|
+
focusedTaskId = applyTaskFocusEvent(event);
|
|
291
|
+
} catch {
|
|
292
|
+
// Reject malformed or stale cross-extension events without retaining payloads or crashing the extension.
|
|
293
|
+
}
|
|
294
|
+
});
|
|
277
295
|
const recoveryPolicy = new CodexRecoveryPolicy({
|
|
278
296
|
baseDelayMs: CODEX_RECOVERY_BASE_DELAY_MS,
|
|
279
297
|
maxDelayMs: CODEX_RECOVERY_MAX_DELAY_MS,
|
|
@@ -285,6 +303,11 @@ export function registerJittorExtension(
|
|
|
285
303
|
let recoveryCooldown: { until: number; attempt: number; failureKind: CodexFailureKind } | undefined;
|
|
286
304
|
let lastCodexResponse: CodexFailureMetadata = {};
|
|
287
305
|
let lastGoogleVertexResponse: GoogleVertexFailureMetadata = {};
|
|
306
|
+
// The third-party "anthropic-vertex" provider (Anthropic Claude via Google Vertex) is tracked
|
|
307
|
+
// separately from "google-vertex" (Pi's own, unrelated native Vertex provider): different code
|
|
308
|
+
// path, different account/quota pool, and its metrics must stay distinguishable -- see
|
|
309
|
+
// google-vertex-contracts.ts and anthropic-contracts.ts.
|
|
310
|
+
let lastAnthropicVertexResponse: GoogleVertexFailureMetadata = {};
|
|
288
311
|
const cancelRecovery = (resetPolicy: boolean): void => {
|
|
289
312
|
if (recoveryTimer !== undefined) recoveryRuntime.clearTimeout(recoveryTimer);
|
|
290
313
|
recoveryTimer = undefined;
|
|
@@ -417,13 +440,25 @@ export function registerJittorExtension(
|
|
|
417
440
|
ctx.ui.notify("No active Pi model is available for benchmark recommendations.", "warning");
|
|
418
441
|
return;
|
|
419
442
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
443
|
+
// Domain (subject matter, e.g. coding) and type (activity, e.g. research/planning) are
|
|
444
|
+
// independent axes -- each positional word is classified against whichever axis it
|
|
445
|
+
// belongs to, in either order, so "/jittor benchmarks coding research" and
|
|
446
|
+
// "/jittor benchmarks research coding" both work; an unmatched word is a usage error.
|
|
447
|
+
const requested = action.split(/\s+/).slice(1);
|
|
448
|
+
let requestedDomain: ModelTaskDomain | undefined;
|
|
449
|
+
let requestedType: ModelTaskType | undefined;
|
|
450
|
+
let malformed = requested.length > 2;
|
|
451
|
+
for (const word of requested) {
|
|
452
|
+
if (TASK_DOMAINS.includes(word as ModelTaskDomain) && requestedDomain === undefined) requestedDomain = word as ModelTaskDomain;
|
|
453
|
+
else if (TASK_TYPES.includes(word as ModelTaskType) && requestedType === undefined) requestedType = word as ModelTaskType;
|
|
454
|
+
else malformed = true;
|
|
455
|
+
}
|
|
456
|
+
if (malformed) {
|
|
457
|
+
ctx.ui.notify("Usage: /jittor benchmarks [coding|general] [research|planning|general]", "warning");
|
|
423
458
|
return;
|
|
424
459
|
}
|
|
425
460
|
const candidates = benchmarkCandidatesFromPi(ctx.modelRegistry.getAvailable() as PiRouteModel[], pi.getThinkingLevel());
|
|
426
|
-
await showBenchmarkPanel(ctx, client, candidates, `${ctx.model.provider}/${ctx.model.id}`,
|
|
461
|
+
await showBenchmarkPanel(ctx, client, candidates, `${ctx.model.provider}/${ctx.model.id}`, requestedDomain ?? "general", requestedType ?? "general");
|
|
427
462
|
return;
|
|
428
463
|
}
|
|
429
464
|
if (action === "outcome accepted" || action === "outcome rejected") {
|
|
@@ -534,6 +569,8 @@ export function registerJittorExtension(
|
|
|
534
569
|
});
|
|
535
570
|
|
|
536
571
|
pi.on("session_start", async (_event, ctx) => {
|
|
572
|
+
currentSessionId = ctx.sessionManager.getSessionId();
|
|
573
|
+
focusedTaskId = null;
|
|
537
574
|
finishCompactionUi();
|
|
538
575
|
compactionTelemetry = new CompactionTelemetry();
|
|
539
576
|
activeLocalRun = undefined;
|
|
@@ -541,6 +578,7 @@ export function registerJittorExtension(
|
|
|
541
578
|
cancelRecovery(true);
|
|
542
579
|
lastCodexResponse = {};
|
|
543
580
|
lastGoogleVertexResponse = {};
|
|
581
|
+
lastAnthropicVertexResponse = {};
|
|
544
582
|
ctx.ui.setStatus("jittor", undefined);
|
|
545
583
|
showFooter(ctx);
|
|
546
584
|
try {
|
|
@@ -614,9 +652,11 @@ export function registerJittorExtension(
|
|
|
614
652
|
});
|
|
615
653
|
|
|
616
654
|
pi.on("turn_start", async (event, ctx) => {
|
|
655
|
+
currentSessionId = ctx.sessionManager.getSessionId();
|
|
617
656
|
compactionTelemetry.observeTurn();
|
|
618
657
|
lastCodexResponse = {};
|
|
619
658
|
lastGoogleVertexResponse = {};
|
|
659
|
+
lastAnthropicVertexResponse = {};
|
|
620
660
|
activeLocalRun = {
|
|
621
661
|
runId: `local-${event.timestamp}-${++localRunSequence}`,
|
|
622
662
|
startedAt: event.timestamp,
|
|
@@ -664,6 +704,22 @@ export function registerJittorExtension(
|
|
|
664
704
|
}
|
|
665
705
|
}
|
|
666
706
|
}
|
|
707
|
+
if (ctx.model?.provider === "anthropic-vertex") {
|
|
708
|
+
// Best-effort only: unverified whether this passthrough ever forwards Anthropic's own
|
|
709
|
+
// rate-limit headers. If it doesn't, hasAnthropicRateLimitHeaders is false and nothing is
|
|
710
|
+
// recorded -- the same honest default as every other unconfirmed signal in this file.
|
|
711
|
+
const headers = new Headers(event.headers);
|
|
712
|
+
if (hasAnthropicRateLimitHeaders(headers)) {
|
|
713
|
+
try {
|
|
714
|
+
await recordMetrics(client, parseAnthropicRateLimitHeaders(headers, Date.now(), "anthropic-vertex").metrics);
|
|
715
|
+
} catch {
|
|
716
|
+
if (enforcement.isEnabled()) ctx.ui.notify(`Jittor detected Anthropic-on-Vertex telemetry schema drift. ${RECOVERY_GUIDANCE}.`, "error");
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
// Well-evidenced regardless of headers: GCP's own quota system fronts this transport, so the
|
|
720
|
+
// same failure classification as google-vertex applies -- see google-vertex-contracts.ts.
|
|
721
|
+
lastAnthropicVertexResponse = { status: event.status, ...(header(event.headers, "retry-after") ? { retryAfter: header(event.headers, "retry-after") } : {}) };
|
|
722
|
+
}
|
|
667
723
|
if (ctx.model?.provider === "google-vertex") {
|
|
668
724
|
lastGoogleVertexResponse = { status: event.status, ...(header(event.headers, "retry-after") ? { retryAfter: header(event.headers, "retry-after") } : {}) };
|
|
669
725
|
}
|
|
@@ -697,7 +753,7 @@ export function registerJittorExtension(
|
|
|
697
753
|
provider: value["provider"],
|
|
698
754
|
model: value["model"],
|
|
699
755
|
thinking: pi.getThinkingLevel(),
|
|
700
|
-
|
|
756
|
+
...classifyTaskFromTools(active.toolNames),
|
|
701
757
|
startedAt: active.startedAt,
|
|
702
758
|
firstTokenAt: active.firstTokenAt,
|
|
703
759
|
completedAt,
|
|
@@ -733,7 +789,14 @@ export function registerJittorExtension(
|
|
|
733
789
|
}
|
|
734
790
|
lastGoogleVertexResponse = {};
|
|
735
791
|
}
|
|
736
|
-
|
|
792
|
+
if (event.message.role === "assistant" && event.message.provider === "anthropic-vertex") {
|
|
793
|
+
if (event.message.stopReason === "error") {
|
|
794
|
+
const failure = classifyGoogleVertexFailure(event.message.errorMessage, lastAnthropicVertexResponse);
|
|
795
|
+
await recordMetrics(client, googleVertexFailureMetrics(failure, Date.now(), "anthropic-vertex")).catch(() => undefined);
|
|
796
|
+
}
|
|
797
|
+
lastAnthropicVertexResponse = {};
|
|
798
|
+
}
|
|
799
|
+
const metrics = assistantUsageMetrics(event.message, Date.now(), focusedTaskId);
|
|
737
800
|
if (metrics.length > 0) {
|
|
738
801
|
const amount = (name: string): number => metrics.filter((metric) => metric.metric === name && typeof metric.value === "number").reduce((sum, metric) => sum + (metric.value ?? 0), 0);
|
|
739
802
|
compactionTelemetry.observeProviderUsage({ input: amount("input-tokens"), output: amount("output-tokens"), cacheRead: amount("cache-read-tokens"), cacheWrite: amount("cache-write-tokens") });
|
|
@@ -746,6 +809,7 @@ export function registerJittorExtension(
|
|
|
746
809
|
finishCompactionUi();
|
|
747
810
|
if (compactionTelemetry.hasOpenCompaction()) await recordMetrics(client, [compactionTelemetry.abort(Date.now(), "session-shutdown")]).catch(() => undefined);
|
|
748
811
|
stopPapyrusContext?.();
|
|
812
|
+
stopPapyrusTaskFocus?.();
|
|
749
813
|
cancelRecovery(true);
|
|
750
814
|
lastCodexResponse = {};
|
|
751
815
|
activeLocalRun = undefined;
|
package/extension/src/tui.ts
CHANGED
|
@@ -63,7 +63,15 @@ function windowName(seconds: number): string {
|
|
|
63
63
|
return `${Math.round(seconds / 60)}m`;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
/**
|
|
67
|
+
* `null` means "not known yet, but this provider can report a budget once data arrives" -- router
|
|
68
|
+
* not ready, or a supported provider whose telemetry hasn't been observed yet; the footer shows a
|
|
69
|
+
* placeholder that may resolve. `undefined` means "no budget signal is possible for this provider
|
|
70
|
+
* at all" (e.g. google-vertex, which has no documented rate-limit or quota header/endpoint Jittor
|
|
71
|
+
* could ever read -- see google-vertex-contracts.ts); the footer omits the segment entirely rather
|
|
72
|
+
* than showing a `?` that can never resolve.
|
|
73
|
+
*/
|
|
74
|
+
export function buildFooterBudget(status: RouterStatus, metrics: StoredMetricObservation[]): ProviderBudget | null | undefined {
|
|
67
75
|
if (!status.ready || !status.currentRoute) return null;
|
|
68
76
|
if (status.currentRoute.provider === "openai-codex") {
|
|
69
77
|
const codex = codexWindowForModel(metrics, status.currentRoute.model);
|
|
@@ -90,6 +98,25 @@ export function buildFooterBudget(status: RouterStatus, metrics: StoredMetricObs
|
|
|
90
98
|
...(Number.isFinite(resetsAt) && resetsAt > 0 ? { resetsAt } : {}),
|
|
91
99
|
};
|
|
92
100
|
}
|
|
101
|
+
if (status.currentRoute.provider === "anthropic-vertex") {
|
|
102
|
+
// Best-effort only (see index.ts): these metrics only exist if Anthropic-style rate-limit
|
|
103
|
+
// headers were actually observed on this passthrough, which is unverified. Labeled distinctly
|
|
104
|
+
// ("vtok"/"vreq") from direct Anthropic's "tok"/"req" since they are a different account/quota
|
|
105
|
+
// pool even if the header shape is identical. If nothing was ever observed, this stays null
|
|
106
|
+
// (may still resolve later), not undefined (never possible) -- unlike google-vertex, this
|
|
107
|
+
// provider's transport has not been shown to structurally lack the signal.
|
|
108
|
+
const anthropicVertex = latest(metrics, (row) => row.source === "anthropic-vertex" && row.metric === "used-fraction" && row.scope === "tokens" && typeof row.value === "number")
|
|
109
|
+
?? latest(metrics, (row) => row.source === "anthropic-vertex" && row.metric === "used-fraction" && row.scope === "requests" && typeof row.value === "number");
|
|
110
|
+
if (!anthropicVertex || typeof anthropicVertex.value !== "number") return null;
|
|
111
|
+
const resetsAt = Number(anthropicVertex.attributes["resetsAt"]);
|
|
112
|
+
return {
|
|
113
|
+
kind: "bounded",
|
|
114
|
+
label: anthropicVertex.scope === "tokens" ? "vtok" : "vreq",
|
|
115
|
+
remainingFraction: 1 - anthropicVertex.value,
|
|
116
|
+
observedAt: anthropicVertex.observedAt,
|
|
117
|
+
...(Number.isFinite(resetsAt) && resetsAt > 0 ? { resetsAt } : {}),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
93
120
|
if (status.currentRoute.provider === "openrouter") {
|
|
94
121
|
const openRouter = latest(metrics, (row) => row.source === "openrouter" && row.metric === "usage" && typeof row.value === "number");
|
|
95
122
|
const remaining = latest(metrics, (row) => row.source === "openrouter" && row.metric === "remaining-fraction" && typeof row.value === "number");
|
|
@@ -106,7 +133,7 @@ export function buildFooterBudget(status: RouterStatus, metrics: StoredMetricObs
|
|
|
106
133
|
if (!openRouter || typeof openRouter.value !== "number") return null;
|
|
107
134
|
return { kind: "unbounded", label: "spend", valueText: `$${openRouter.value.toFixed(3)}`, observedAt: openRouter.observedAt };
|
|
108
135
|
}
|
|
109
|
-
return
|
|
136
|
+
return undefined;
|
|
110
137
|
}
|
|
111
138
|
|
|
112
139
|
export function formatFooterStatus(status: RouterStatus, metrics: StoredMetricObservation[]): string {
|
package/extension/src/usage.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
-
import { HUMAN_TEXT_FIELD_MAX_CHARACTERS, USAGE_CHART_HEIGHT,
|
|
4
|
-
import type { StoredMetricObservation } from "../../src/domain/metric.ts";
|
|
3
|
+
import { HUMAN_TEXT_FIELD_MAX_CHARACTERS, USAGE_CHART_HEIGHT, USAGE_MAX_DISTINCT_SCOPES, USAGE_RENDER_MAX_SERIES, USAGE_Y_AXIS_WIDTH } from "../../src/constants.ts";
|
|
5
4
|
import {
|
|
6
5
|
buildCostGraph,
|
|
7
6
|
buildUsageGraph,
|
|
7
|
+
resolveUsageWindow,
|
|
8
8
|
USAGE_PERIODS,
|
|
9
9
|
usagePeriod,
|
|
10
|
-
usagePeriodStart,
|
|
11
10
|
type CostGraph,
|
|
11
|
+
type UsageAggregateRow,
|
|
12
12
|
type UsageGraph,
|
|
13
13
|
type UsagePeriod,
|
|
14
14
|
} from "../../src/domain/usage.ts";
|
|
@@ -254,15 +254,23 @@ export function renderCostGraph(chart: CostGraph, width: number, theme: UsageThe
|
|
|
254
254
|
export type UsageViewKind = "tokens" | "cost";
|
|
255
255
|
const USAGE_VIEWS: UsageViewKind[] = ["tokens", "cost"];
|
|
256
256
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
257
|
+
/**
|
|
258
|
+
* One bounded round trip: the daemon discovers distinct scopes (still capped at
|
|
259
|
+
* USAGE_MAX_DISTINCT_SCOPES -- more scopes than that is still a real, honestly-reported
|
|
260
|
+
* truncation) and SQL-side aggregates every matching observation into (scope, metric, bucket)
|
|
261
|
+
* sums for the exact window this panel renders. Replaces a per-scope fetch of up to
|
|
262
|
+
* USAGE_PER_SCOPE_QUERY_LIMIT raw rows each, which fixed a *different* problem (one heavy scope
|
|
263
|
+
* starving *other* scopes out of a shared row budget) but could still silently truncate a single
|
|
264
|
+
* heavy scope's *own* older history within the same window -- a real incident: a scope logging
|
|
265
|
+
* tens of thousands of rows a week had its "weekly" chart built from a few minutes of its most
|
|
266
|
+
* recent rows alone. Aggregation has no such failure mode: result size scales with (scopes x
|
|
267
|
+
* metrics x buckets), never with raw event count.
|
|
268
|
+
*/
|
|
269
|
+
async function loadPiMetrics(client: JittorPanelClient, window: ReturnType<typeof resolveUsageWindow>): Promise<{ rows: UsageAggregateRow[]; truncated: boolean }> {
|
|
270
|
+
const result = await client.call("metrics.usage_series", {
|
|
271
|
+
source: "pi", since: window.start, until: window.end, bucketSizeMs: window.bucketSizeMs, bucketCount: window.bucketCount, scopeLimit: USAGE_MAX_DISTINCT_SCOPES,
|
|
272
|
+
}) as { rows: UsageAggregateRow[]; truncated: boolean };
|
|
273
|
+
return result;
|
|
266
274
|
}
|
|
267
275
|
|
|
268
276
|
export async function showUsagePanel(
|
|
@@ -276,11 +284,12 @@ export async function showUsagePanel(
|
|
|
276
284
|
let viewIndex = Math.max(0, USAGE_VIEWS.indexOf(initialView));
|
|
277
285
|
for (;;) {
|
|
278
286
|
const period = USAGE_PERIODS[periodIndex]!.id;
|
|
287
|
+
const window = resolveUsageWindow(period, now);
|
|
279
288
|
// One bounded query serves both views: token and cost metrics share the same "pi" source rows.
|
|
280
|
-
const { rows, truncated } = await loadPiMetrics(client,
|
|
289
|
+
const { rows, truncated } = await loadPiMetrics(client, window);
|
|
281
290
|
const view = USAGE_VIEWS[viewIndex]!;
|
|
282
|
-
const tokenChart = buildUsageGraph(rows, { period,
|
|
283
|
-
const costChart = buildCostGraph(rows, { period,
|
|
291
|
+
const tokenChart = buildUsageGraph(rows, window, { period, truncated });
|
|
292
|
+
const costChart = buildCostGraph(rows, window, { period, truncated });
|
|
284
293
|
const tokenBudget = budgets.getUsageTokenBudget(period);
|
|
285
294
|
const renderActive = (width: number, theme: UsageTheme): string[] =>
|
|
286
295
|
view === "tokens" ? renderUsageGraph(tokenChart, width, theme, tokenBudget) : renderCostGraph(costChart, width, theme);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/jittor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Just-in-Time Token Optimizing Router for Pi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": ["pi-package", "llm-router", "token-budget"],
|
|
@@ -22,6 +22,10 @@
|
|
|
22
22
|
"@earendil-works/pi-tui": "*",
|
|
23
23
|
"typebox": "*"
|
|
24
24
|
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@danypops/daemon-kit": "^0.3.1",
|
|
27
|
+
"google-auth-library": "^10.9.0"
|
|
28
|
+
},
|
|
25
29
|
"devDependencies": {
|
|
26
30
|
"bun-types": "latest"
|
|
27
31
|
},
|
|
@@ -83,7 +83,8 @@ export class OpenRouterBenchmarkIndexSource implements BenchmarkSource {
|
|
|
83
83
|
return [
|
|
84
84
|
validateBenchmarkObservation({ ...common, dimension: "quality-coding", value: requiredNumber(row["coding_index"], "coding index"), unit: "ratio", methodology }),
|
|
85
85
|
validateBenchmarkObservation({ ...common, dimension: "quality-general", value: requiredNumber(row["intelligence_index"], "intelligence index"), unit: "ratio", methodology }),
|
|
86
|
-
|
|
86
|
+
// agentic_index measures tool-use/agentic execution style, an activity (type), not a subject-matter domain.
|
|
87
|
+
validateBenchmarkObservation({ ...common, dimension: "quality-type-planning", value: requiredNumber(row["agentic_index"], "agentic index"), unit: "ratio", methodology }),
|
|
87
88
|
validateBenchmarkObservation({ ...common, dimension: "price-input", value: price(pricing["prompt"], "prompt pricing"), unit: "usd", methodology: { ...methodology, basis: "OpenRouter USD per input token" } }),
|
|
88
89
|
validateBenchmarkObservation({ ...common, dimension: "price-output", value: price(pricing["completion"], "completion pricing"), unit: "usd", methodology: { ...methodology, basis: "OpenRouter USD per output token" } }),
|
|
89
90
|
];
|