@cat-factory/app 0.168.0 → 0.170.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 CHANGED
@@ -56,9 +56,10 @@ over the WebSocket. How that sync works is written up in
56
56
  ## Interface modes (basic / advanced)
57
57
 
58
58
  The SPA renders at one of two **interface tiers**. `basic` (the default) is the everyday
59
- surface: the power-user nav destinations are hidden and the run/pipeline options that only
60
- exist to override a workspace-level default are left at that default. `advanced` shows
61
- everything. The tier resolves in a fixed order, first match wins:
59
+ surface: the run/pipeline options that only exist to override a workspace-level default are
60
+ left at that default, and the nav is trimmed to the destinations that are the **only route**
61
+ to their capability. `advanced` shows everything. The tier resolves in a fixed order, first
62
+ match wins:
62
63
 
63
64
  1. **`NUXT_PUBLIC_UI_MODE`** (`basic` | `advanced`) — the deployment pin. Like
64
65
  `NUXT_PUBLIC_API_BASE` it is baked in at **build** time (`ssr: false`), and while it is
@@ -81,7 +82,15 @@ hoc where it can be avoided:
81
82
  - **A nav destination** declares `advanced: true` in `app/modular/nav-contributions.ts`. The
82
83
  shared `navSlotFilter` drops it in basic mode across all three shells (sidebar, command
83
84
  palette, toolbar), independently of its RBAC `gate` — both must pass. A consumer module's
84
- own contributions take the same flag.
85
+ own contributions take the same flag. The bar for setting it is **route count, not how
86
+ advanced the surface feels**: hiding the only way to reach a capability removes the
87
+ capability from the tier, while hiding a shortcut into a surface a basic destination also
88
+ opens removes nothing. So the flag belongs on a surface that sits beside the delivery path
89
+ (Sandbox, Kaizen), on a palette shortcut into a Workspace-settings tab, or on a knob the
90
+ Integrations hub already offers — and never on a sole route (the pipeline builder, the
91
+ fragment library, the infrastructure/PREnv windows, the operator + reports views).
92
+ `nav-contributions.spec.ts` pins the advanced set against a table of each item's
93
+ alternative route, so promoting one forces that claim to be written down.
85
94
  - **A less-used option inside a surface** reads `useUiModeStore().isAdvanced`. Hide, never
86
95
  disable, and only ever hide an OVERRIDE: what remains must be exactly the default the hidden
87
96
  field would have shown, so a basic-mode user never gets different behaviour from an advanced
@@ -4,10 +4,10 @@ import type { StepMetrics } from '~/types/execution'
4
4
  import {
5
5
  formatMs,
6
6
  formatTokens,
7
- freshPromptTokens,
8
7
  headroomColor,
9
8
  headroomRatio,
10
9
  pct,
10
+ totalInputTokens,
11
11
  transportRatio,
12
12
  } from '~/utils/observability'
13
13
 
@@ -23,12 +23,16 @@ defineEmits<{ inspect: [] }>()
23
23
  const { t } = useI18n()
24
24
 
25
25
  const m = computed(() => props.metrics)
26
- // The headline "↑" is FRESH (uncached) input, not the raw prompt-token sum: a long agentic
27
- // run re-sends its whole transcript every turn, so the raw sum is ~all cache reads and reads
28
- // as a blow-up. The cached prefix is shown separately (the green chip) so the two are distinct.
29
- const freshPrompt = computed(() =>
30
- freshPromptTokens(m.value.promptTokens, m.value.cachedPromptTokens ?? 0),
31
- )
26
+ // The headline "↑" is TOTAL input fresh + both cache classes the like-for-like measure of
27
+ // Claude Code's own context gauge, which counts the same buckets because a cached token still
28
+ // occupies the context window (see `totalInputTokens`). The three classes then render as the
29
+ // breakdown, because they are priced an order of magnitude apart in opposite directions: a read
30
+ // is ~0.1x base input, a write 1.25-2x. Volume in the headline, cost in the breakdown — leading
31
+ // with fresh made a 31M-token run read as a 685-token one.
32
+ const totalInput = computed(() => totalInputTokens(m.value))
33
+ const cacheRead = computed(() => m.value.cacheReadTokens ?? 0)
34
+ const cacheWrite = computed(() => m.value.cacheWriteTokens ?? 0)
35
+ const hasCache = computed(() => cacheRead.value > 0 || cacheWrite.value > 0)
32
36
  const headroom = computed(() => headroomRatio(m.value))
33
37
  const transport = computed(() => transportRatio(m.value))
34
38
  const headroomTone = computed(() => headroomColor(headroom.value, m.value.truncatedCalls > 0))
@@ -53,20 +57,9 @@ const headroomTone = computed(() => headroomColor(headroom.value, m.value.trunca
53
57
  <span class="text-slate-500">·</span>
54
58
  <span
55
59
  class="tabular-nums text-slate-400"
56
- :title="t('observability.metricsBar.promptCompletionTokens')"
60
+ :title="t('observability.metricsBar.inputCompletionTokens')"
57
61
  >
58
- {{ formatTokens(freshPrompt) }}↑ {{ formatTokens(m.completionTokens) }}↓
59
- </span>
60
- <span
61
- v-if="(m.cachedPromptTokens ?? 0) > 0"
62
- class="tabular-nums text-emerald-400/80"
63
- :title="t('observability.metricsBar.cachedTokensHint')"
64
- >
65
- {{
66
- t('observability.metricsBar.cachedTokens', {
67
- tokens: formatTokens(m.cachedPromptTokens ?? 0),
68
- })
69
- }}
62
+ {{ formatTokens(totalInput) }}↑ {{ formatTokens(m.completionTokens) }}↓
70
63
  </span>
71
64
  <div class="ms-auto flex items-center gap-1">
72
65
  <UBadge v-if="m.errors > 0" color="error" variant="subtle" size="sm">
@@ -83,6 +76,29 @@ const headroomTone = computed(() => headroomColor(headroom.value, m.value.trunca
83
76
  </div>
84
77
  </div>
85
78
 
79
+ <!-- input breakdown: the headline's three classes, priced an order of magnitude apart -->
80
+ <div v-if="hasCache" class="mt-1.5 flex items-center gap-1.5 text-[11px] tabular-nums">
81
+ <span class="text-slate-400" :title="t('observability.metricsBar.freshHint')">
82
+ {{ t('observability.metricsBar.fresh', { tokens: formatTokens(m.promptTokens) }) }}
83
+ </span>
84
+ <span v-if="cacheRead > 0" class="text-slate-600">·</span>
85
+ <span
86
+ v-if="cacheRead > 0"
87
+ class="text-emerald-400/80"
88
+ :title="t('observability.metricsBar.cacheReadHint')"
89
+ >
90
+ {{ t('observability.metricsBar.cacheRead', { tokens: formatTokens(cacheRead) }) }}
91
+ </span>
92
+ <span v-if="cacheWrite > 0" class="text-slate-600">·</span>
93
+ <span
94
+ v-if="cacheWrite > 0"
95
+ class="text-amber-400/80"
96
+ :title="t('observability.metricsBar.cacheWriteHint')"
97
+ >
98
+ {{ t('observability.metricsBar.cacheWrite', { tokens: formatTokens(cacheWrite) }) }}
99
+ </span>
100
+ </div>
101
+
86
102
  <!-- output-limit headroom -->
87
103
  <div v-if="headroom !== null" class="mt-2">
88
104
  <div class="flex items-center justify-between text-[11px]">
@@ -8,7 +8,7 @@ import type {
8
8
  WebSearchProvider,
9
9
  } from '~/types/execution'
10
10
  import { agentKindMeta } from '~/utils/catalog'
11
- import { formatMs, formatTokens, freshPromptTokens, pct } from '~/utils/observability'
11
+ import { formatMs, formatTokens, pct, totalInputTokens } from '~/utils/observability'
12
12
 
13
13
  // Drill-down overlay for a run's LLM activity. Opened via
14
14
  // `ui.openObservability(instanceId)` from a step surface; loads the full per-call
@@ -126,15 +126,18 @@ const totals = computed(() => {
126
126
  const upstreamMs = sum(c, (x) => x.upstreamMs)
127
127
  const overheadMs = sum(c, (x) => x.overheadMs)
128
128
  const total = upstreamMs + overheadMs
129
+ // The three input classes are orthogonal at the source, so they are simply summed —
130
+ // `promptTokens` IS the fresh figure and needs no heuristic to recover it; the headline is
131
+ // their TOTAL (see `totalInputTokens` — the like-for-like Claude Code context gauge).
129
132
  const promptTokens = sum(c, (x) => x.promptTokens)
130
- const cachedPromptTokens = sum(c, (x) => x.cachedPromptTokens)
133
+ const cacheReadTokens = sum(c, (x) => x.cacheReadTokens)
134
+ const cacheWriteTokens = sum(c, (x) => x.cacheWriteTokens)
131
135
  return {
132
136
  calls: c.length,
133
137
  promptTokens,
134
- cachedPromptTokens,
135
- // Fresh (uncached) input — the raw prompt-token sum is dominated by per-turn cache reads
136
- // on a long agentic run, so surface fresh vs cached rather than the misleading raw total.
137
- freshPromptTokens: freshPromptTokens(promptTokens, cachedPromptTokens),
138
+ cacheReadTokens,
139
+ cacheWriteTokens,
140
+ inputTokens: totalInputTokens({ promptTokens, cacheReadTokens, cacheWriteTokens }),
138
141
  completionTokens: sum(c, (x) => x.completionTokens),
139
142
  upstreamMs,
140
143
  overheadMs,
@@ -290,18 +293,47 @@ function exportJson() {
290
293
  {{ t('observability.summary.tokensInOut') }}
291
294
  </dt>
292
295
  <dd class="mt-0.5 tabular-nums text-slate-200">
293
- {{ formatTokens(totals.freshPromptTokens) }} /
294
- {{ formatTokens(totals.completionTokens) }}
296
+ <span :title="t('observability.summary.inputTokensHint')">
297
+ {{ formatTokens(totals.inputTokens) }} /
298
+ {{ formatTokens(totals.completionTokens) }}
299
+ </span>
295
300
  <span
296
- v-if="totals.cachedPromptTokens > 0"
297
- class="ms-1 text-[11px] text-emerald-400/80"
301
+ v-if="totals.cacheReadTokens > 0 || totals.cacheWriteTokens > 0"
302
+ class="mt-0.5 block text-[11px]"
298
303
  >
299
- ·
300
- {{
301
- t('observability.summary.cached', {
302
- tokens: formatTokens(totals.cachedPromptTokens),
303
- })
304
- }}
304
+ <span class="text-slate-400" :title="t('observability.summary.freshHint')">
305
+ {{
306
+ t('observability.summary.fresh', {
307
+ tokens: formatTokens(totals.promptTokens),
308
+ })
309
+ }}
310
+ </span>
311
+ <template v-if="totals.cacheReadTokens > 0">
312
+ <span class="text-slate-600"> · </span>
313
+ <span
314
+ class="text-emerald-400/80"
315
+ :title="t('observability.summary.cacheReadHint')"
316
+ >
317
+ {{
318
+ t('observability.summary.cacheRead', {
319
+ tokens: formatTokens(totals.cacheReadTokens),
320
+ })
321
+ }}
322
+ </span>
323
+ </template>
324
+ <template v-if="totals.cacheWriteTokens > 0">
325
+ <span class="text-slate-600"> · </span>
326
+ <span
327
+ class="text-amber-400/80"
328
+ :title="t('observability.summary.cacheWriteHint')"
329
+ >
330
+ {{
331
+ t('observability.summary.cacheWrite', {
332
+ tokens: formatTokens(totals.cacheWriteTokens),
333
+ })
334
+ }}
335
+ </span>
336
+ </template>
305
337
  </span>
306
338
  </dd>
307
339
  </div>
@@ -418,12 +450,14 @@ function exportJson() {
418
450
  <span
419
451
  :title="
420
452
  t('observability.call.tokensTitle', {
421
- prompt: c.promptTokens,
453
+ input: totalInputTokens(c),
454
+ fresh: c.promptTokens,
422
455
  completion: c.completionTokens,
423
456
  })
424
457
  "
425
458
  >
426
- {{ formatTokens(c.promptTokens) }}↑ {{ formatTokens(c.completionTokens) }}↓
459
+ {{ formatTokens(totalInputTokens(c)) }}↑
460
+ {{ formatTokens(c.completionTokens) }}↓
427
461
  </span>
428
462
  <span
429
463
  v-if="headroomOf(c) !== null"
@@ -467,11 +501,14 @@ function exportJson() {
467
501
  <span v-if="c.requestMaxTokens != null">{{
468
502
  t('observability.call.maxTokens', { value: c.requestMaxTokens })
469
503
  }}</span>
470
- <span v-if="c.cachedPromptTokens > 0" class="text-emerald-400">{{
471
- t('observability.call.promptCached', {
472
- cached: c.cachedPromptTokens,
473
- prompt: c.promptTokens,
474
- })
504
+ <span v-if="c.cacheReadTokens > 0 || c.cacheWriteTokens > 0">{{
505
+ t('observability.call.fresh', { tokens: c.promptTokens })
506
+ }}</span>
507
+ <span v-if="c.cacheReadTokens > 0" class="text-emerald-400">{{
508
+ t('observability.call.cacheRead', { tokens: c.cacheReadTokens })
509
+ }}</span>
510
+ <span v-if="c.cacheWriteTokens > 0" class="text-amber-400">{{
511
+ t('observability.call.cacheWrite', { tokens: c.cacheWriteTokens })
475
512
  }}</span>
476
513
  <span>{{
477
514
  t('observability.call.total', { duration: formatMs(c.totalMs) })
@@ -99,11 +99,36 @@ describe('navSlotFilter', () => {
99
99
  expect(kept).toContain('integrations-hub')
100
100
  expect(kept).toContain('workspace-settings')
101
101
  expect(kept).toContain('model-config')
102
- // ...and the authoring / operator surfaces don't.
103
- expect(kept).not.toContain('build-pipeline')
104
- expect(kept).not.toContain('fragments')
102
+ // ...and so does every destination that is the SOLE route to its capability, however deep
103
+ // it feels: authoring a flow, the standards library, the PREnv/runner plumbing, and the
104
+ // aggregate run-health / spend views.
105
+ expect(kept).toContain('build-pipeline')
106
+ expect(kept).toContain('fragments')
107
+ expect(kept).toContain('infrastructure')
108
+ expect(kept).toContain('environment-setup')
109
+ expect(kept).toContain('operator-dashboard')
110
+ expect(kept).toContain('reports')
111
+ // What drops is beside the delivery path (experimentation) or a shortcut into a surface
112
+ // basic mode already reaches another way.
105
113
  expect(kept).not.toContain('sandbox')
106
- expect(kept).not.toContain('operator-dashboard')
114
+ expect(kept).not.toContain('kaizen')
115
+ expect(kept).not.toContain('merge-thresholds')
116
+ })
117
+
118
+ it('never makes an advanced item the only route to its capability', () => {
119
+ // The tier's governing rule (see the catalog comment): every advanced destination either
120
+ // sits beside the delivery path, or is a shortcut into a surface a basic destination also
121
+ // opens. Spelled out here as a table so promoting an item to `advanced` forces an explicit
122
+ // claim about how basic mode still reaches it, rather than a silent capability loss.
123
+ const ALTERNATIVE_ROUTE: Record<string, string> = {
124
+ sandbox: 'none needed - experimentation surface, not on the delivery path',
125
+ kaizen: 'none needed - self-grading history, not on the delivery path',
126
+ 'merge-thresholds': 'workspace-settings -> Merge tab',
127
+ 'service-fragment-defaults': 'workspace-settings -> Service best practices tab',
128
+ 'local-models': 'integrations-hub -> Local runners',
129
+ }
130
+ const advanced = NAV_CONTRIBUTIONS.filter((i) => i.advanced).map((i) => i.id)
131
+ expect(advanced.sort()).toEqual(Object.keys(ALTERNATIVE_ROUTE).sort())
107
132
  })
108
133
 
109
134
  it('keeps the tier switch itself reachable in basic mode', () => {
@@ -119,15 +144,19 @@ describe('navSlotFilter', () => {
119
144
  })
120
145
 
121
146
  it('keeps the tier and the permission axes independent — both must pass', () => {
122
- // `build-pipeline` is advanced AND needs board.write: neither axis alone reveals it.
147
+ // `sandbox` is advanced AND needs integrations.manage: neither axis alone reveals it.
123
148
  const advancedOnly: NavGates = { ...NO_GATES, advancedMode: true }
124
- expect(ids(navSlotFilter(slots(), { gates: advancedOnly }))).not.toContain('build-pipeline')
149
+ expect(ids(navSlotFilter(slots(), { gates: advancedOnly }))).not.toContain('sandbox')
125
150
 
126
- const permissionOnly: NavGates = { ...NO_GATES, canWriteBoard: true, advancedMode: false }
127
- expect(ids(navSlotFilter(slots(), { gates: permissionOnly }))).not.toContain('build-pipeline')
151
+ const permissionOnly: NavGates = {
152
+ ...NO_GATES,
153
+ canManageIntegrations: true,
154
+ advancedMode: false,
155
+ }
156
+ expect(ids(navSlotFilter(slots(), { gates: permissionOnly }))).not.toContain('sandbox')
128
157
 
129
- const both: NavGates = { ...NO_GATES, canWriteBoard: true, advancedMode: true }
130
- expect(ids(navSlotFilter(slots(), { gates: both }))).toContain('build-pipeline')
158
+ const both: NavGates = { ...NO_GATES, canManageIntegrations: true, advancedMode: true }
159
+ expect(ids(navSlotFilter(slots(), { gates: both }))).toContain('sandbox')
131
160
  })
132
161
 
133
162
  it('leaves at least one sidebar destination in every basic-mode section it keeps', () => {
@@ -153,13 +153,24 @@ const S = (...s: NavSurface[]) => s as readonly NavSurface[]
153
153
  * with accounts enabled).
154
154
  * - one icon per destination across shells.
155
155
  *
156
- * `advanced: true` marks the power-user half, hidden in basic interface mode. The line
157
- * drawn here is "would a team shipping their first task open this?": connecting a repo or
158
- * an integration, picking models, and the workspace/account settings stay; AUTHORING the
159
- * machinery those defaults come from (the pipeline builder, the fragment library, merge
160
- * thresholds, service fragment defaults), the experimentation surfaces (sandbox, Kaizen),
161
- * the infrastructure/environment plumbing, per-user local models, and the operator-tier
162
- * dashboards do not.
156
+ * `advanced: true` marks the power-user half, hidden in basic interface mode. The line is
157
+ * drawn by ROUTE COUNT, not by how advanced a surface feels: hiding the only way to reach a
158
+ * capability removes the capability from the tier, while hiding a shortcut to a surface basic
159
+ * mode already reaches removes nothing. So an item is advanced only when both of these hold —
160
+ * it is not the sole route to its capability, and nothing on the delivery path needs it:
161
+ *
162
+ * - `sandbox` / `kaizen` — experimentation and self-grading surfaces, beside the delivery
163
+ * path rather than on it.
164
+ * - `merge-thresholds` / `service-fragment-defaults` — palette shortcuts into Workspace
165
+ * settings tabs (Merge, Service best practices), which basic mode reaches via
166
+ * `workspace-settings`.
167
+ * - `local-models` — a per-user endpoint knob the Integrations hub already offers.
168
+ *
169
+ * Everything else stays in basic BECAUSE it is a sole route: authoring a flow
170
+ * (`build-pipeline`), the standards/skills library (`fragments`, whose only other route is a
171
+ * button two levels into Workspace settings), the PREnv + runner plumbing (`infrastructure`,
172
+ * `environment-setup`), and the operator/cost views that are the only aggregate read of run
173
+ * health and spend (`operator-dashboard`, `reports`).
163
174
  */
164
175
  export const NAV_CONTRIBUTIONS: readonly NavContribution[] = [
165
176
  {
@@ -167,7 +178,6 @@ export const NAV_CONTRIBUTIONS: readonly NavContribution[] = [
167
178
  labelKey: 'nav.buildPipeline',
168
179
  icon: 'i-lucide-workflow',
169
180
  surfaces: S('sidebar', 'command'),
170
- advanced: true,
171
181
  gate: (g) => g.canWriteBoard,
172
182
  action: 'buildPipeline',
173
183
  testId: 'nav-build-pipeline',
@@ -253,7 +263,6 @@ export const NAV_CONTRIBUTIONS: readonly NavContribution[] = [
253
263
  labelKey: 'nav.infrastructure',
254
264
  icon: 'i-lucide-server-cog',
255
265
  surfaces: S('sidebar'),
256
- advanced: true,
257
266
  gate: (g) => g.infrastructureAvailable,
258
267
  action: 'infrastructure',
259
268
  testId: 'nav-infrastructure',
@@ -264,7 +273,6 @@ export const NAV_CONTRIBUTIONS: readonly NavContribution[] = [
264
273
  labelKey: 'nav.environmentSetup',
265
274
  icon: 'i-lucide-flask-conical',
266
275
  surfaces: S('sidebar'),
267
- advanced: true,
268
276
  gate: (g) => g.infrastructureAvailable,
269
277
  action: 'environmentSetup',
270
278
  testId: 'nav-environment-setup',
@@ -275,7 +283,6 @@ export const NAV_CONTRIBUTIONS: readonly NavContribution[] = [
275
283
  labelKey: 'nav.contextFragments',
276
284
  icon: 'i-lucide-book-marked',
277
285
  surfaces: S('sidebar', 'command'),
278
- advanced: true,
279
286
  gate: (g) => g.libraryAvailable && g.canManageSettings,
280
287
  action: 'fragmentLibrary',
281
288
  testId: 'nav-fragments',
@@ -381,7 +388,6 @@ export const NAV_CONTRIBUTIONS: readonly NavContribution[] = [
381
388
  labelKey: 'nav.operatorDashboard',
382
389
  icon: 'i-lucide-gauge',
383
390
  surfaces: S('sidebar'),
384
- advanced: true,
385
391
  gate: (g) => g.accountsEnabled && g.isAccountAdmin,
386
392
  action: 'operatorDashboard',
387
393
  testId: 'nav-operator-dashboard',
@@ -392,7 +398,6 @@ export const NAV_CONTRIBUTIONS: readonly NavContribution[] = [
392
398
  labelKey: 'nav.reports',
393
399
  icon: 'i-lucide-chart-column',
394
400
  surfaces: S('sidebar'),
395
- advanced: true,
396
401
  gate: (g) => g.accountsEnabled && g.isAccountAdmin,
397
402
  action: 'reports',
398
403
  testId: 'nav-reports',
@@ -1,28 +1,28 @@
1
1
  import { describe, it, expect } from 'vitest'
2
- import { freshPromptTokens } from './observability'
2
+ import { totalInputTokens } from './observability'
3
3
 
4
- describe('freshPromptTokens', () => {
5
- it('subtracts the cached prefix from the prompt-token sum', () => {
6
- expect(freshPromptTokens(1000, 300)).toBe(700)
4
+ describe('totalInputTokens', () => {
5
+ it('sums all three input classes, so the headline matches Claude Code’s context gauge', () => {
6
+ expect(
7
+ totalInputTokens({ promptTokens: 685, cacheReadTokens: 31_099_813, cacheWriteTokens: 0 }),
8
+ ).toBe(31_100_498)
7
9
  })
8
10
 
9
- it('reduces a near-fully-cached agentic run to its tiny fresh input (inclusive shape)', () => {
10
- // The 31M-cache-read shape from the investigation on the INCLUSIVE shape (subscription
11
- // harness / OpenAI-style): prompt tokens INCLUDE the cache reads, so subtracting the cached
12
- // subset leaves the few-hundred-token fresh figure, not tens of millions.
13
- expect(freshPromptTokens(31_100_498, 31_099_813)).toBe(685)
11
+ it('counts cache WRITES too they occupy the window like any other input token', () => {
12
+ expect(
13
+ totalInputTokens({ promptTokens: 100, cacheReadTokens: 900, cacheWriteTokens: 40 }),
14
+ ).toBe(1040)
14
15
  })
15
16
 
16
- it('treats promptTokens as already-fresh when cached exceeds it (Anthropic separate shape)', () => {
17
- // Anthropic via the LLM proxy reports cache reads SEPARATELY, so promptTokens is fresh-only
18
- // and cachedPromptTokens can far exceed it. The fresh figure is promptTokens itself — NOT 0
19
- // (the old subtract-and-clamp collapsed real fresh input to zero on this shape).
20
- expect(freshPromptTokens(685, 31_099_813)).toBe(685)
17
+ it('does NOT lead with the fresh figure on a cache-dominated run', () => {
18
+ // The regression this pins: leading with fresh made a ~31M-token run render as 685 tokens,
19
+ // discounting cache reads because their dollar cost is low. Volume is the thing being
20
+ // measured here, and a cached token costs the same context window as a fresh one.
21
+ const m = { promptTokens: 685, cacheReadTokens: 31_099_813, cacheWriteTokens: 0 }
22
+ expect(totalInputTokens(m)).toBeGreaterThan(m.promptTokens * 1000)
21
23
  })
22
24
 
23
- it('never returns negative, and treats a fully-cached inclusive rollup as 0 fresh', () => {
24
- expect(freshPromptTokens(500, 500)).toBe(0)
25
- // cached > prompt ⇒ separate shape ⇒ fresh = prompt (never a negative number).
26
- expect(freshPromptTokens(500, 600)).toBe(500)
25
+ it('degrades to the fresh count when an older snapshot carries no cache fields', () => {
26
+ expect(totalInputTokens({ promptTokens: 500 })).toBe(500)
27
27
  })
28
28
  })
@@ -12,31 +12,28 @@ export function formatTokens(n: number): string {
12
12
  }
13
13
 
14
14
  /**
15
- * FRESH (uncached) prompt tokens: the input tokens actually processed this call/rollup after
16
- * excluding the prefix served from the provider's cache. A long agentic run re-sends its whole
17
- * growing transcript every turn, so on the "inclusive" shape the raw `promptTokens` sum is
18
- * dominated by cache reads (often >99%) — showing THAT as "tokens burned" reads as a blow-up
19
- * when almost nothing fresh was processed. This surfaces the fresh figure alongside cached.
15
+ * TOTAL input tokens: fresh + cache read + cache write. This is the headline "↑" figure on
16
+ * every LLM surface, and it deliberately COUNTS THE CACHED CLASSES.
20
17
  *
21
- * `cachedPromptTokens` has PROVIDER-DEPENDENT semantics (see the field docs on `stepMetricsSchema`
22
- * / `llmCallMetricSchema`), so we can't blindly subtract:
23
- * - Inclusive shape (OpenAI/DeepSeek, and the subscription-CLI harness, which folds cache into
24
- * `promptTokens`): cached is a SUBSET of prompt fresh = prompt cached.
25
- * - Separate shape (Anthropic via the LLM proxy): cache reads are reported SEPARATELY and
26
- * `promptTokens` is ALREADY fresh-only, so cached can EXCEED prompt. Subtracting there would
27
- * wrongly collapse a real fresh input to 0 — when cached ≥ prompt, `promptTokens` itself IS
28
- * the fresh figure.
18
+ * That is the like-for-like measure of Claude Code's own context gauge, which sums exactly these
19
+ * buckets because a cached token still physically occupies the context window. Leading with the
20
+ * fresh figure instead (what this surface used to do, on the grounds that the raw sum "reads as a
21
+ * blow-up") discounts cache reads because their DOLLAR cost is low but the quota, latency and
22
+ * context-window cost is the whole thing an autonomous run burns, and hiding it is what let a
23
+ * ~31M-token run look like a 685-token one. See
24
+ * `docs/initiatives/token-burn-instrumentation.md`.
29
25
  *
30
- * NOTE: with only these two aggregates we cannot distinguish the separate shape while cached is
31
- * still prompt (there the subtraction under-counts fresh by the cache-read amount). A fully
32
- * exact split needs the wire contract to carry cache-read vs cache-write distinctly at the
33
- * source; this heuristic fixes the dominant (cached prompt) case and never returns negative.
26
+ * The three classes are still rendered as the breakdown beneath it, because they are priced an
27
+ * order of magnitude apart in opposite directions this makes the volume honest WITHOUT making
28
+ * the cost unreadable. The two fields are optional on an older snapshot, where absent reads as 0
29
+ * and the total degrades to the fresh count.
34
30
  */
35
- export function freshPromptTokens(promptTokens: number, cachedPromptTokens: number): number {
36
- // Separate shape: promptTokens is already fresh-only (cache reads counted separately).
37
- if (cachedPromptTokens > promptTokens) return promptTokens
38
- // Inclusive shape: cached is a subset of prompt.
39
- return promptTokens - cachedPromptTokens
31
+ export function totalInputTokens(m: {
32
+ promptTokens: number
33
+ cacheReadTokens?: number
34
+ cacheWriteTokens?: number
35
+ }): number {
36
+ return m.promptTokens + (m.cacheReadTokens ?? 0) + (m.cacheWriteTokens ?? 0)
40
37
  }
41
38
 
42
39
  /** Compact duration: 850 → "850ms", 1500 → "1.5s", 90_000 → "1m 30s". */
@@ -2906,13 +2906,23 @@
2906
2906
  "transportOverhead": "Transport-Overhead",
2907
2907
  "modelExecution": "Modellausführung",
2908
2908
  "truncated": "{count} abgeschnitten | {count} abgeschnitten",
2909
- "cached": "{tokens} gecacht"
2909
+ "inputTokensHint": "Eingabe- / Ausgabe-Tokens insgesamt. Die Eingabe zählt auch gecachte Tokens mit: Sie belegen weiterhin das Kontextfenster, genau wie es die Kontextanzeige von Claude Code zählt.",
2910
+ "fresh": "{tokens} frisch",
2911
+ "freshHint": "Eingabe, die von Grund auf verarbeitet wurde, ohne Anteil aus dem Cache",
2912
+ "cacheRead": "{tokens} aus dem Cache gelesen",
2913
+ "cacheReadHint": "Eingabe-Tokens, die aus dem Cache des Providers bedient wurden (etwa 0,1x der Preis frischer Eingabe-Tokens)",
2914
+ "cacheWrite": "{tokens} in den Cache geschrieben",
2915
+ "cacheWriteHint": "Eingabe-Tokens, die in den Cache des Providers geschrieben wurden (1,25x bis 2x der Preis frischer Eingabe-Tokens)"
2910
2916
  },
2911
2917
  "metricsBar": {
2912
2918
  "calls": "{count} Aufruf | {count} Aufrufe",
2913
- "promptCompletionTokens": "Frische (nicht gecachte) Prompt-/Completion-Tokens",
2914
- "cachedTokens": "({tokens} zwischengespeichert)",
2915
- "cachedTokensHint": "Prompt-Tokens, die aus dem Cache des Providers bedient wurden",
2919
+ "inputCompletionTokens": "Eingabe- / Ausgabe-Tokens insgesamt. Die Eingabe zählt auch gecachte Tokens mit: Sie belegen weiterhin das Kontextfenster, genau wie es die Kontextanzeige von Claude Code zählt.",
2920
+ "fresh": "{tokens} frisch",
2921
+ "freshHint": "Eingabe, die von Grund auf verarbeitet wurde, ohne Anteil aus dem Cache",
2922
+ "cacheRead": "{tokens} aus dem Cache gelesen",
2923
+ "cacheReadHint": "Eingabe-Tokens, die aus dem Cache des Providers bedient wurden (etwa 0,1x der Preis frischer Eingabe-Tokens)",
2924
+ "cacheWrite": "{tokens} in den Cache geschrieben",
2925
+ "cacheWriteHint": "Eingabe-Tokens, die in den Cache des Providers geschrieben wurden (1,25x bis 2x der Preis frischer Eingabe-Tokens)",
2916
2926
  "errors": "{count} Fehler | {count} Fehler",
2917
2927
  "warnings": "{count} Warnung | {count} Warnungen",
2918
2928
  "outputLimit": "Ausgabelimit",
@@ -2922,7 +2932,7 @@
2922
2932
  "modelExecution": "Modellausführung"
2923
2933
  },
2924
2934
  "call": {
2925
- "tokensTitle": "{prompt} Prompt- / {completion} Completion-Tokens",
2935
+ "tokensTitle": "{input} Eingabe-Tokens (davon {fresh} frisch) / {completion} Ausgabe-Tokens",
2926
2936
  "outputUsedVsLimit": "Ausgabe genutzt vs. Limit",
2927
2937
  "transportVsExecution": "Transport-Overhead / Modellausführung",
2928
2938
  "error": "Fehler",
@@ -2932,7 +2942,9 @@
2932
2942
  "streamed": "gestreamt",
2933
2943
  "buffered": "gepuffert",
2934
2944
  "maxTokens": "max_tokens {value}",
2935
- "promptCached": "{cached}/{prompt} Prompt zwischengespeichert",
2945
+ "fresh": "{tokens} frisch",
2946
+ "cacheRead": "{tokens} aus dem Cache gelesen",
2947
+ "cacheWrite": "{tokens} in den Cache geschrieben",
2936
2948
  "total": "gesamt {duration}",
2937
2949
  "prompt": "Prompt",
2938
2950
  "promptPrefixOmitted": "(nur neue Nachrichten: {count} frühere ausgelassen)",
@@ -1434,13 +1434,23 @@
1434
1434
  "transportOverhead": "Transport overhead",
1435
1435
  "modelExecution": "Model execution",
1436
1436
  "truncated": "{count} truncated | {count} truncated",
1437
- "cached": "{tokens} cached"
1437
+ "inputTokensHint": "Total input / completion tokens. Input counts cached tokens too: they still occupy the context window, exactly as Claude Code's own context gauge counts them.",
1438
+ "fresh": "{tokens} fresh",
1439
+ "freshHint": "Input processed from scratch, with nothing served from the cache",
1440
+ "cacheRead": "{tokens} cache read",
1441
+ "cacheReadHint": "Input tokens served from the provider's cache (about 0.1x the price of fresh input)",
1442
+ "cacheWrite": "{tokens} cache write",
1443
+ "cacheWriteHint": "Input tokens written into the provider's cache (1.25x to 2x the price of fresh input)"
1438
1444
  },
1439
1445
  "metricsBar": {
1440
1446
  "calls": "{count} call | {count} calls",
1441
- "promptCompletionTokens": "Fresh (uncached) prompt / completion tokens",
1442
- "cachedTokens": "({tokens} cached)",
1443
- "cachedTokensHint": "Prompt tokens served from the provider's cache",
1447
+ "inputCompletionTokens": "Total input / completion tokens. Input counts cached tokens too: they still occupy the context window, exactly as Claude Code's own context gauge counts them.",
1448
+ "fresh": "{tokens} fresh",
1449
+ "freshHint": "Input processed from scratch, with nothing served from the cache",
1450
+ "cacheRead": "{tokens} cache read",
1451
+ "cacheReadHint": "Input tokens served from the provider's cache (about 0.1x the price of fresh input)",
1452
+ "cacheWrite": "{tokens} cache write",
1453
+ "cacheWriteHint": "Input tokens written into the provider's cache (1.25x to 2x the price of fresh input)",
1444
1454
  "errors": "{count} error | {count} errors",
1445
1455
  "warnings": "{count} warning | {count} warnings",
1446
1456
  "outputLimit": "Output limit",
@@ -1450,7 +1460,7 @@
1450
1460
  "modelExecution": "Model execution"
1451
1461
  },
1452
1462
  "call": {
1453
- "tokensTitle": "{prompt} prompt / {completion} completion tokens",
1463
+ "tokensTitle": "{input} input tokens ({fresh} of them fresh) / {completion} completion tokens",
1454
1464
  "outputUsedVsLimit": "Output used vs limit",
1455
1465
  "transportVsExecution": "Transport overhead / model execution",
1456
1466
  "error": "error",
@@ -1460,7 +1470,9 @@
1460
1470
  "streamed": "streamed",
1461
1471
  "buffered": "buffered",
1462
1472
  "maxTokens": "max_tokens {value}",
1463
- "promptCached": "{cached}/{prompt} prompt cached",
1473
+ "fresh": "{tokens} fresh",
1474
+ "cacheRead": "{tokens} cache read",
1475
+ "cacheWrite": "{tokens} cache write",
1464
1476
  "total": "total {duration}",
1465
1477
  "prompt": "Prompt",
1466
1478
  "promptPrefixOmitted": "(new messages only: {count} earlier omitted)",
@@ -1368,13 +1368,23 @@
1368
1368
  "transportOverhead": "Sobrecarga de transporte",
1369
1369
  "modelExecution": "Ejecución del modelo",
1370
1370
  "truncated": "{count} truncada | {count} truncadas",
1371
- "cached": "{tokens} en caché"
1371
+ "inputTokensHint": "Tokens totales de entrada / salida. La entrada tambien cuenta los tokens en caché: siguen ocupando la ventana de contexto, igual que los cuenta el medidor de contexto de Claude Code.",
1372
+ "fresh": "{tokens} frescos",
1373
+ "freshHint": "Entrada procesada desde cero, sin nada servido desde la caché",
1374
+ "cacheRead": "{tokens} leídos de caché",
1375
+ "cacheReadHint": "Tokens de entrada servidos desde la caché del proveedor (unas 0,1 veces el precio de la entrada fresca)",
1376
+ "cacheWrite": "{tokens} escritos en caché",
1377
+ "cacheWriteHint": "Tokens de entrada escritos en la caché del proveedor (de 1,25 a 2 veces el precio de la entrada fresca)"
1372
1378
  },
1373
1379
  "metricsBar": {
1374
1380
  "calls": "{count} llamada | {count} llamadas",
1375
- "promptCompletionTokens": "Tokens frescos (sin caché) de prompt / completado",
1376
- "cachedTokens": "({tokens} en caché)",
1377
- "cachedTokensHint": "Tokens de prompt servidos desde la caché del proveedor",
1381
+ "inputCompletionTokens": "Tokens totales de entrada / salida. La entrada tambien cuenta los tokens en caché: siguen ocupando la ventana de contexto, igual que los cuenta el medidor de contexto de Claude Code.",
1382
+ "fresh": "{tokens} frescos",
1383
+ "freshHint": "Entrada procesada desde cero, sin nada servido desde la caché",
1384
+ "cacheRead": "{tokens} leídos de caché",
1385
+ "cacheReadHint": "Tokens de entrada servidos desde la caché del proveedor (unas 0,1 veces el precio de la entrada fresca)",
1386
+ "cacheWrite": "{tokens} escritos en caché",
1387
+ "cacheWriteHint": "Tokens de entrada escritos en la caché del proveedor (de 1,25 a 2 veces el precio de la entrada fresca)",
1378
1388
  "errors": "{count} error | {count} errores",
1379
1389
  "warnings": "{count} advertencia | {count} advertencias",
1380
1390
  "outputLimit": "Límite de salida",
@@ -1384,7 +1394,7 @@
1384
1394
  "modelExecution": "Ejecución del modelo"
1385
1395
  },
1386
1396
  "call": {
1387
- "tokensTitle": "{prompt} tokens de prompt / {completion} de completado",
1397
+ "tokensTitle": "{input} tokens de entrada ({fresh} de ellos frescos) / {completion} tokens de salida",
1388
1398
  "outputUsedVsLimit": "Salida usada frente al límite",
1389
1399
  "transportVsExecution": "Sobrecarga de transporte / ejecución del modelo",
1390
1400
  "error": "error",
@@ -1394,7 +1404,9 @@
1394
1404
  "streamed": "en streaming",
1395
1405
  "buffered": "en búfer",
1396
1406
  "maxTokens": "max_tokens {value}",
1397
- "promptCached": "{cached}/{prompt} de prompt en caché",
1407
+ "fresh": "{tokens} frescos",
1408
+ "cacheRead": "{tokens} leídos de caché",
1409
+ "cacheWrite": "{tokens} escritos en caché",
1398
1410
  "total": "total {duration}",
1399
1411
  "prompt": "Prompt",
1400
1412
  "promptPrefixOmitted": "(solo mensajes nuevos: {count} anteriores omitidos)",
@@ -1368,13 +1368,23 @@
1368
1368
  "transportOverhead": "Surcoût de transport",
1369
1369
  "modelExecution": "Exécution du modèle",
1370
1370
  "truncated": "{count} tronqué | {count} tronqués",
1371
- "cached": "{tokens} en cache"
1371
+ "inputTokensHint": "Total des tokens d'entrée / de sortie. L'entree compte aussi les tokens en cache : ils occupent toujours la fenêtre de contexte, exactement comme les compte la jauge de contexte de Claude Code.",
1372
+ "fresh": "{tokens} frais",
1373
+ "freshHint": "Entrée traitée de zero, sans rien servi depuis le cache",
1374
+ "cacheRead": "{tokens} lus depuis le cache",
1375
+ "cacheReadHint": "Tokens d'entrée servis depuis le cache du fournisseur (environ 0,1 fois le prix d'une entrée fraîche)",
1376
+ "cacheWrite": "{tokens} écrits dans le cache",
1377
+ "cacheWriteHint": "Tokens d'entrée écrits dans le cache du fournisseur (1,25 à 2 fois le prix d'une entrée fraîche)"
1372
1378
  },
1373
1379
  "metricsBar": {
1374
1380
  "calls": "{count} appel | {count} appels",
1375
- "promptCompletionTokens": "Tokens frais (non mis en cache) de prompt / complétion",
1376
- "cachedTokens": "({tokens} en cache)",
1377
- "cachedTokensHint": "Tokens de prompt servis depuis le cache du fournisseur",
1381
+ "inputCompletionTokens": "Total des tokens d'entrée / de sortie. L'entree compte aussi les tokens en cache : ils occupent toujours la fenêtre de contexte, exactement comme les compte la jauge de contexte de Claude Code.",
1382
+ "fresh": "{tokens} frais",
1383
+ "freshHint": "Entrée traitée de zero, sans rien servi depuis le cache",
1384
+ "cacheRead": "{tokens} lus depuis le cache",
1385
+ "cacheReadHint": "Tokens d'entrée servis depuis le cache du fournisseur (environ 0,1 fois le prix d'une entrée fraîche)",
1386
+ "cacheWrite": "{tokens} écrits dans le cache",
1387
+ "cacheWriteHint": "Tokens d'entrée écrits dans le cache du fournisseur (1,25 à 2 fois le prix d'une entrée fraîche)",
1378
1388
  "errors": "{count} erreur | {count} erreurs",
1379
1389
  "warnings": "{count} avertissement | {count} avertissements",
1380
1390
  "outputLimit": "Limite de sortie",
@@ -1384,7 +1394,7 @@
1384
1394
  "modelExecution": "Exécution du modèle"
1385
1395
  },
1386
1396
  "call": {
1387
- "tokensTitle": "{prompt} tokens de prompt / {completion} de complétion",
1397
+ "tokensTitle": "{input} tokens d'entrée (dont {fresh} frais) / {completion} tokens de sortie",
1388
1398
  "outputUsedVsLimit": "Sortie utilisée vs limite",
1389
1399
  "transportVsExecution": "Surcoût de transport / exécution du modèle",
1390
1400
  "error": "erreur",
@@ -1394,7 +1404,9 @@
1394
1404
  "streamed": "en streaming",
1395
1405
  "buffered": "en mémoire tampon",
1396
1406
  "maxTokens": "max_tokens {value}",
1397
- "promptCached": "{cached}/{prompt} prompt en cache",
1407
+ "fresh": "{tokens} frais",
1408
+ "cacheRead": "{tokens} lus depuis le cache",
1409
+ "cacheWrite": "{tokens} écrits dans le cache",
1398
1410
  "total": "total {duration}",
1399
1411
  "prompt": "Prompt",
1400
1412
  "promptPrefixOmitted": "(nouveaux messages uniquement : {count} antérieurs omis)",
@@ -1368,13 +1368,23 @@
1368
1368
  "transportOverhead": "תקורת תעבורה",
1369
1369
  "modelExecution": "הרצת מודל",
1370
1370
  "truncated": "{count} נקטע | {count} נקטעו",
1371
- "cached": "{tokens} במטמון"
1371
+ "inputTokensHint": "סך טוקני הקלט / הפלט. הקלט סופר גם טוקנים מהמטמון: הם עדיין תופסים את חלון ההקשר, בדיוק כפי שמד ההקשר של Claude Code סופר אותם.",
1372
+ "fresh": "{tokens} חדשים",
1373
+ "freshHint": "קלט שעובד מאפס, ללא שום חלק שהוגש מהמטמון",
1374
+ "cacheRead": "{tokens} נקראו מהמטמון",
1375
+ "cacheReadHint": "טוקני קלט שהוגשו ממטמון הספק (כ-0.1 ממחיר קלט חדש)",
1376
+ "cacheWrite": "{tokens} נכתבו למטמון",
1377
+ "cacheWriteHint": "טוקני קלט שנכתבו למטמון הספק (פי 1.25 עד 2 ממחיר קלט חדש)"
1372
1378
  },
1373
1379
  "metricsBar": {
1374
1380
  "calls": "{count} קריאה | {count} קריאות",
1375
- "promptCompletionTokens": "טוקני פרומפט / השלמה חדשים (ללא מטמון)",
1376
- "cachedTokens": "({tokens} במטמון)",
1377
- "cachedTokensHint": "טוקני פרומפט שהוגשו ממטמון הספק",
1381
+ "inputCompletionTokens": "סך טוקני הקלט / הפלט. הקלט סופר גם טוקנים מהמטמון: הם עדיין תופסים את חלון ההקשר, בדיוק כפי שמד ההקשר של Claude Code סופר אותם.",
1382
+ "fresh": "{tokens} חדשים",
1383
+ "freshHint": "קלט שעובד מאפס, ללא שום חלק שהוגש מהמטמון",
1384
+ "cacheRead": "{tokens} נקראו מהמטמון",
1385
+ "cacheReadHint": "טוקני קלט שהוגשו ממטמון הספק (כ-0.1 ממחיר קלט חדש)",
1386
+ "cacheWrite": "{tokens} נכתבו למטמון",
1387
+ "cacheWriteHint": "טוקני קלט שנכתבו למטמון הספק (פי 1.25 עד 2 ממחיר קלט חדש)",
1378
1388
  "errors": "{count} שגיאה | {count} שגיאות",
1379
1389
  "warnings": "{count} אזהרה | {count} אזהרות",
1380
1390
  "outputLimit": "מגבלת פלט",
@@ -1384,7 +1394,7 @@
1384
1394
  "modelExecution": "הרצת מודל"
1385
1395
  },
1386
1396
  "call": {
1387
- "tokensTitle": "{prompt} טוקני פרומפט / {completion} טוקני השלמה",
1397
+ "tokensTitle": "{input} טוקני קלט ({fresh} מהם חדשים) / {completion} טוקני פלט",
1388
1398
  "outputUsedVsLimit": "פלט בשימוש לעומת המגבלה",
1389
1399
  "transportVsExecution": "תקורת תעבורה / הרצת מודל",
1390
1400
  "error": "שגיאה",
@@ -1394,7 +1404,9 @@
1394
1404
  "streamed": "הוזרם",
1395
1405
  "buffered": "באגירה",
1396
1406
  "maxTokens": "max_tokens {value}",
1397
- "promptCached": "{cached}/{prompt} טוקני פרומפט במטמון",
1407
+ "fresh": "{tokens} חדשים",
1408
+ "cacheRead": "{tokens} נקראו מהמטמון",
1409
+ "cacheWrite": "{tokens} נכתבו למטמון",
1398
1410
  "total": "סך הכל {duration}",
1399
1411
  "prompt": "פרומפט",
1400
1412
  "promptPrefixOmitted": "(הודעות חדשות בלבד: {count} מוקדמות יותר הושמטו)",
@@ -2906,13 +2906,23 @@
2906
2906
  "transportOverhead": "Overhead di trasporto",
2907
2907
  "modelExecution": "Esecuzione del modello",
2908
2908
  "truncated": "{count} troncata | {count} troncate",
2909
- "cached": "{tokens} in cache"
2909
+ "inputTokensHint": "Token totali di input / output. L'input conta anche i token in cache: occupano comunque la finestra di contesto, esattamente come li conta l'indicatore di contesto di Claude Code.",
2910
+ "fresh": "{tokens} freschi",
2911
+ "freshHint": "Input elaborato da zero, senza nulla servito dalla cache",
2912
+ "cacheRead": "{tokens} letti dalla cache",
2913
+ "cacheReadHint": "Token di input serviti dalla cache del provider (circa 0,1 volte il prezzo dell'input fresco)",
2914
+ "cacheWrite": "{tokens} scritti nella cache",
2915
+ "cacheWriteHint": "Token di input scritti nella cache del provider (da 1,25 a 2 volte il prezzo dell'input fresco)"
2910
2916
  },
2911
2917
  "metricsBar": {
2912
2918
  "calls": "{count} chiamata | {count} chiamate",
2913
- "promptCompletionTokens": "Token freschi (non in cache) di prompt / completion",
2914
- "cachedTokens": "({tokens} in cache)",
2915
- "cachedTokensHint": "Token di prompt serviti dalla cache del provider",
2919
+ "inputCompletionTokens": "Token totali di input / output. L'input conta anche i token in cache: occupano comunque la finestra di contesto, esattamente come li conta l'indicatore di contesto di Claude Code.",
2920
+ "fresh": "{tokens} freschi",
2921
+ "freshHint": "Input elaborato da zero, senza nulla servito dalla cache",
2922
+ "cacheRead": "{tokens} letti dalla cache",
2923
+ "cacheReadHint": "Token di input serviti dalla cache del provider (circa 0,1 volte il prezzo dell'input fresco)",
2924
+ "cacheWrite": "{tokens} scritti nella cache",
2925
+ "cacheWriteHint": "Token di input scritti nella cache del provider (da 1,25 a 2 volte il prezzo dell'input fresco)",
2916
2926
  "errors": "{count} errore | {count} errori",
2917
2927
  "warnings": "{count} avviso | {count} avvisi",
2918
2928
  "outputLimit": "Limite di output",
@@ -2922,7 +2932,7 @@
2922
2932
  "modelExecution": "Esecuzione del modello"
2923
2933
  },
2924
2934
  "call": {
2925
- "tokensTitle": "{prompt} token di prompt / {completion} di completion",
2935
+ "tokensTitle": "{input} token di input (di cui {fresh} freschi) / {completion} token di output",
2926
2936
  "outputUsedVsLimit": "Output usato vs limite",
2927
2937
  "transportVsExecution": "Overhead di trasporto / esecuzione del modello",
2928
2938
  "error": "errore",
@@ -2932,7 +2942,9 @@
2932
2942
  "streamed": "in streaming",
2933
2943
  "buffered": "bufferizzato",
2934
2944
  "maxTokens": "max_tokens {value}",
2935
- "promptCached": "{cached}/{prompt} prompt in cache",
2945
+ "fresh": "{tokens} freschi",
2946
+ "cacheRead": "{tokens} letti dalla cache",
2947
+ "cacheWrite": "{tokens} scritti nella cache",
2936
2948
  "total": "totale {duration}",
2937
2949
  "prompt": "Prompt",
2938
2950
  "promptPrefixOmitted": "(solo nuovi messaggi: {count} precedenti omessi)",
@@ -1368,13 +1368,23 @@
1368
1368
  "transportOverhead": "転送オーバーヘッド",
1369
1369
  "modelExecution": "モデル実行",
1370
1370
  "truncated": "{count} 件が切り詰め | {count} 件が切り詰め",
1371
- "cached": "{tokens} キャッシュ済み"
1371
+ "inputTokensHint": "入力 / 出力トークンの合計。入力にはキャッシュされたトークンも含まれます。それらも依然としてコンテキストウィンドウを占有するため、Claude Code のコンテキスト表示と同じ数え方です。",
1372
+ "fresh": "{tokens} 新規",
1373
+ "freshHint": "キャッシュから提供された分を含まない、ゼロから処理された入力",
1374
+ "cacheRead": "{tokens} キャッシュ読み取り",
1375
+ "cacheReadHint": "プロバイダーのキャッシュから提供された入力トークン (新規入力の約 0.1 倍の価格)",
1376
+ "cacheWrite": "{tokens} キャッシュ書き込み",
1377
+ "cacheWriteHint": "プロバイダーのキャッシュに書き込まれた入力トークン (新規入力の 1.25 倍から 2 倍の価格)"
1372
1378
  },
1373
1379
  "metricsBar": {
1374
1380
  "calls": "{count} 件の呼び出し | {count} 件の呼び出し",
1375
- "promptCompletionTokens": "新規(キャッシュ外)のプロンプト / 完了トークン",
1376
- "cachedTokens": "({tokens} 件キャッシュ済み)",
1377
- "cachedTokensHint": "プロバイダーのキャッシュから提供されたプロンプトトークン",
1381
+ "inputCompletionTokens": "入力 / 出力トークンの合計。入力にはキャッシュされたトークンも含まれます。それらも依然としてコンテキストウィンドウを占有するため、Claude Code のコンテキスト表示と同じ数え方です。",
1382
+ "fresh": "{tokens} 新規",
1383
+ "freshHint": "キャッシュから提供された分を含まない、ゼロから処理された入力",
1384
+ "cacheRead": "{tokens} キャッシュ読み取り",
1385
+ "cacheReadHint": "プロバイダーのキャッシュから提供された入力トークン (新規入力の約 0.1 倍の価格)",
1386
+ "cacheWrite": "{tokens} キャッシュ書き込み",
1387
+ "cacheWriteHint": "プロバイダーのキャッシュに書き込まれた入力トークン (新規入力の 1.25 倍から 2 倍の価格)",
1378
1388
  "errors": "{count} 件のエラー | {count} 件のエラー",
1379
1389
  "warnings": "{count} 件の警告 | {count} 件の警告",
1380
1390
  "outputLimit": "出力上限",
@@ -1384,7 +1394,7 @@
1384
1394
  "modelExecution": "モデル実行"
1385
1395
  },
1386
1396
  "call": {
1387
- "tokensTitle": "{prompt} プロンプト / {completion} 完了トークン",
1397
+ "tokensTitle": "入力トークン {input} (うち新規 {fresh} 件) / 出力トークン {completion} ",
1388
1398
  "outputUsedVsLimit": "使用済み出力 vs 上限",
1389
1399
  "transportVsExecution": "転送オーバーヘッド / モデル実行",
1390
1400
  "error": "エラー",
@@ -1394,7 +1404,9 @@
1394
1404
  "streamed": "ストリーミング",
1395
1405
  "buffered": "バッファ済み",
1396
1406
  "maxTokens": "max_tokens {value}",
1397
- "promptCached": "{cached}/{prompt} プロンプトをキャッシュ",
1407
+ "fresh": "{tokens} 新規",
1408
+ "cacheRead": "{tokens} キャッシュ読み取り",
1409
+ "cacheWrite": "{tokens} キャッシュ書き込み",
1398
1410
  "total": "合計 {duration}",
1399
1411
  "prompt": "プロンプト",
1400
1412
  "promptPrefixOmitted": "(新規メッセージのみ: 以前の {count} 件を省略)",
@@ -1368,13 +1368,23 @@
1368
1368
  "transportOverhead": "Narzut transportu",
1369
1369
  "modelExecution": "Wykonanie modelu",
1370
1370
  "truncated": "{count} obcięte | {count} obcięte | {count} obciętych",
1371
- "cached": "{tokens} z pamięci podręcznej"
1371
+ "inputTokensHint": "Łączna liczba tokenów wejścia / wyjscia. Wejście liczy takze tokeny z pamięci podręcznej: nadal zajmują okno kontekstu, dokładnie tak, jak liczy je wskaźnik kontekstu w Claude Code.",
1372
+ "fresh": "{tokens} świeżych",
1373
+ "freshHint": "Wejście przetworzone od zera, bez żadnej części obsłużonej z pamięci podręcznej",
1374
+ "cacheRead": "{tokens} odczytane z pamięci podręcznej",
1375
+ "cacheReadHint": "Tokeny wejściowe obsłużone z pamięci podręcznej dostawcy (około 0,1 ceny świeżego wejścia)",
1376
+ "cacheWrite": "{tokens} zapisane do pamięci podręcznej",
1377
+ "cacheWriteHint": "Tokeny wejściowe zapisane do pamięci podręcznej dostawcy (od 1,25 do 2 razy cena świeżego wejścia)"
1372
1378
  },
1373
1379
  "metricsBar": {
1374
1380
  "calls": "{count} wywołanie | {count} wywołania | {count} wywołań",
1375
- "promptCompletionTokens": "Świeże (niebuforowane) tokeny promptu / dopełnienia",
1376
- "cachedTokens": "({tokens} z pamięci podręcznej)",
1377
- "cachedTokensHint": "Tokeny promptu obsłużone z pamięci podręcznej dostawcy",
1381
+ "inputCompletionTokens": "Łączna liczba tokenów wejścia / wyjscia. Wejście liczy takze tokeny z pamięci podręcznej: nadal zajmują okno kontekstu, dokładnie tak, jak liczy je wskaźnik kontekstu w Claude Code.",
1382
+ "fresh": "{tokens} świeżych",
1383
+ "freshHint": "Wejście przetworzone od zera, bez żadnej części obsłużonej z pamięci podręcznej",
1384
+ "cacheRead": "{tokens} odczytane z pamięci podręcznej",
1385
+ "cacheReadHint": "Tokeny wejściowe obsłużone z pamięci podręcznej dostawcy (około 0,1 ceny świeżego wejścia)",
1386
+ "cacheWrite": "{tokens} zapisane do pamięci podręcznej",
1387
+ "cacheWriteHint": "Tokeny wejściowe zapisane do pamięci podręcznej dostawcy (od 1,25 do 2 razy cena świeżego wejścia)",
1378
1388
  "errors": "{count} błąd | {count} błędy | {count} błędów",
1379
1389
  "warnings": "{count} ostrzeżenie | {count} ostrzeżenia | {count} ostrzeżeń",
1380
1390
  "outputLimit": "Limit wyjścia",
@@ -1384,7 +1394,7 @@
1384
1394
  "modelExecution": "Wykonanie modelu"
1385
1395
  },
1386
1396
  "call": {
1387
- "tokensTitle": "{prompt} tokenów promptu / {completion} tokenów dopełnienia",
1397
+ "tokensTitle": "{input} tokenów wejścia (w tym {fresh} świeżych) / {completion} tokenów wyjścia",
1388
1398
  "outputUsedVsLimit": "Użyte wyjście a limit",
1389
1399
  "transportVsExecution": "Narzut transportu / wykonanie modelu",
1390
1400
  "error": "błąd",
@@ -1394,7 +1404,9 @@
1394
1404
  "streamed": "strumieniowane",
1395
1405
  "buffered": "buforowane",
1396
1406
  "maxTokens": "max_tokens {value}",
1397
- "promptCached": "{cached}/{prompt} promptu w pamięci podręcznej",
1407
+ "fresh": "{tokens} świeżych",
1408
+ "cacheRead": "{tokens} odczytane z pamięci podręcznej",
1409
+ "cacheWrite": "{tokens} zapisane do pamięci podręcznej",
1398
1410
  "total": "łącznie {duration}",
1399
1411
  "prompt": "Prompt",
1400
1412
  "promptPrefixOmitted": "(tylko nowe wiadomości: {count} wcześniejszych pominięto)",
@@ -1368,13 +1368,23 @@
1368
1368
  "transportOverhead": "Taşıma yükü",
1369
1369
  "modelExecution": "Model yürütme",
1370
1370
  "truncated": "{count} kesildi | {count} kesildi",
1371
- "cached": "{tokens} önbellekte"
1371
+ "inputTokensHint": "Toplam giriş / çıkış token'i. Giriş, önbelleğe alınmış token'ları da sayar: bunlar hâlâ bağlam penceresini kaplar, tıpkı Claude Code'un kendi bağlam göstergesinin saydığı gibi.",
1372
+ "fresh": "{tokens} yeni",
1373
+ "freshHint": "Önbellekten hiçbir şey sunulmadan sıfırdan işlenen giriş",
1374
+ "cacheRead": "{tokens} önbellekten okundu",
1375
+ "cacheReadHint": "Sağlayıcının önbelleğinden sunulan giriş token'ları (yeni girişin yaklaşık 0,1 katı fiyat)",
1376
+ "cacheWrite": "{tokens} önbelleğe yazıldı",
1377
+ "cacheWriteHint": "Sağlayıcının önbelleğine yazılan giriş token'ları (yeni girişin 1,25 ila 2 katı fiyat)"
1372
1378
  },
1373
1379
  "metricsBar": {
1374
1380
  "calls": "{count} çağrı | {count} çağrı",
1375
- "promptCompletionTokens": "Yeni (önbelleğe alınmamış) istem / tamamlama token'ları",
1376
- "cachedTokens": "({tokens} önbelleğe alındı)",
1377
- "cachedTokensHint": "Sağlayıcının önbelleğinden sunulan istem token'ları",
1381
+ "inputCompletionTokens": "Toplam giriş / çıkış token'i. Giriş, önbelleğe alınmış token'ları da sayar: bunlar hâlâ bağlam penceresini kaplar, tıpkı Claude Code'un kendi bağlam göstergesinin saydığı gibi.",
1382
+ "fresh": "{tokens} yeni",
1383
+ "freshHint": "Önbellekten hiçbir şey sunulmadan sıfırdan işlenen giriş",
1384
+ "cacheRead": "{tokens} önbellekten okundu",
1385
+ "cacheReadHint": "Sağlayıcının önbelleğinden sunulan giriş token'ları (yeni girişin yaklaşık 0,1 katı fiyat)",
1386
+ "cacheWrite": "{tokens} önbelleğe yazıldı",
1387
+ "cacheWriteHint": "Sağlayıcının önbelleğine yazılan giriş token'ları (yeni girişin 1,25 ila 2 katı fiyat)",
1378
1388
  "errors": "{count} hata | {count} hata",
1379
1389
  "warnings": "{count} uyarı | {count} uyarı",
1380
1390
  "outputLimit": "Çıkış sınırı",
@@ -1384,7 +1394,7 @@
1384
1394
  "modelExecution": "Model yürütme"
1385
1395
  },
1386
1396
  "call": {
1387
- "tokensTitle": "{prompt} istem / {completion} tamamlama token'ı",
1397
+ "tokensTitle": "{input} giriş token'i ({fresh} tanesi yeni) / {completion} çıkış token'i",
1388
1398
  "outputUsedVsLimit": "Kullanılan çıkış ile sınır",
1389
1399
  "transportVsExecution": "Taşıma yükü / model yürütme",
1390
1400
  "error": "hata",
@@ -1394,7 +1404,9 @@
1394
1404
  "streamed": "akışlı",
1395
1405
  "buffered": "ara belleğe alınmış",
1396
1406
  "maxTokens": "max_tokens {value}",
1397
- "promptCached": "{cached}/{prompt} istem önbelleğe alındı",
1407
+ "fresh": "{tokens} yeni",
1408
+ "cacheRead": "{tokens} önbellekten okundu",
1409
+ "cacheWrite": "{tokens} önbelleğe yazıldı",
1398
1410
  "total": "toplam {duration}",
1399
1411
  "prompt": "İstem",
1400
1412
  "promptPrefixOmitted": "(yalnızca yeni mesajlar: {count} öncekisi atlandı)",
@@ -1368,13 +1368,23 @@
1368
1368
  "transportOverhead": "Накладні витрати транспорту",
1369
1369
  "modelExecution": "Виконання моделі",
1370
1370
  "truncated": "{count} обрізаний | {count} обрізані | {count} обрізаних",
1371
- "cached": "{tokens} з кешу"
1371
+ "inputTokensHint": "Загальна кількість токенів входу / виходу. Вхід враховує й токени з кешу: вони так само займають вікно контексту, точно як їх рахує індикатор контексту в Claude Code.",
1372
+ "fresh": "{tokens} нових",
1373
+ "freshHint": "Вхід, оброблений з нуля, без жодної частини з кешу",
1374
+ "cacheRead": "{tokens} прочитано з кешу",
1375
+ "cacheReadHint": "Вхідні токени, надані з кешу провайдера (приблизно 0,1 ціни нових вхідних токенів)",
1376
+ "cacheWrite": "{tokens} записано в кеш",
1377
+ "cacheWriteHint": "Вхідні токени, записані в кеш провайдера (від 1,25 до 2 разів ціна нових вхідних токенів)"
1372
1378
  },
1373
1379
  "metricsBar": {
1374
1380
  "calls": "{count} виклик | {count} виклики | {count} викликів",
1375
- "promptCompletionTokens": "Нові (некешовані) токени запиту / завершення",
1376
- "cachedTokens": "({tokens} з кешу)",
1377
- "cachedTokensHint": "Токени запиту, надані з кешу провайдера",
1381
+ "inputCompletionTokens": "Загальна кількість токенів входу / виходу. Вхід враховує й токени з кешу: вони так само займають вікно контексту, точно як їх рахує індикатор контексту в Claude Code.",
1382
+ "fresh": "{tokens} нових",
1383
+ "freshHint": "Вхід, оброблений з нуля, без жодної частини з кешу",
1384
+ "cacheRead": "{tokens} прочитано з кешу",
1385
+ "cacheReadHint": "Вхідні токени, надані з кешу провайдера (приблизно 0,1 ціни нових вхідних токенів)",
1386
+ "cacheWrite": "{tokens} записано в кеш",
1387
+ "cacheWriteHint": "Вхідні токени, записані в кеш провайдера (від 1,25 до 2 разів ціна нових вхідних токенів)",
1378
1388
  "errors": "{count} помилка | {count} помилки | {count} помилок",
1379
1389
  "warnings": "{count} попередження | {count} попередження | {count} попереджень",
1380
1390
  "outputLimit": "Ліміт виводу",
@@ -1384,7 +1394,7 @@
1384
1394
  "modelExecution": "Виконання моделі"
1385
1395
  },
1386
1396
  "call": {
1387
- "tokensTitle": "{prompt} токенів запиту / {completion} токенів завершення",
1397
+ "tokensTitle": "{input} токенів входу (з них {fresh} нових) / {completion} токенів виходу",
1388
1398
  "outputUsedVsLimit": "Використано виводу проти ліміту",
1389
1399
  "transportVsExecution": "Накладні витрати транспорту / виконання моделі",
1390
1400
  "error": "помилка",
@@ -1394,7 +1404,9 @@
1394
1404
  "streamed": "потоково",
1395
1405
  "buffered": "буферизовано",
1396
1406
  "maxTokens": "max_tokens {value}",
1397
- "promptCached": "{cached}/{prompt} запиту з кешу",
1407
+ "fresh": "{tokens} нових",
1408
+ "cacheRead": "{tokens} прочитано з кешу",
1409
+ "cacheWrite": "{tokens} записано в кеш",
1398
1410
  "total": "усього {duration}",
1399
1411
  "prompt": "Запит",
1400
1412
  "promptPrefixOmitted": "(лише нові повідомлення: {count} раніших пропущено)",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.168.0",
3
+ "version": "0.170.0",
4
4
  "description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -40,7 +40,7 @@
40
40
  "valibot": "^1.4.2",
41
41
  "vue": "3.5.40",
42
42
  "wretch": "^3.0.9",
43
- "@cat-factory/contracts": "0.181.0"
43
+ "@cat-factory/contracts": "0.182.0"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@toad-contracts/testing": "0.3.2",