@rayadesu/dsh-client-ui-billing 0.3.11 → 0.3.13

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/lib/client.js CHANGED
@@ -4108,6 +4108,30 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4108
4108
  "toppedUp": string()
4109
4109
  }))
4110
4110
  });
4111
+ const _rayadesu_dsh_llm_billing_billing_getDelegatedSpend_parameter_0$schema = intersection(string(), unknown());
4112
+ const _rayadesu_dsh_llm_billing_billing_getDelegatedSpend_parameter_1$schema = union([
4113
+ _undefined(),
4114
+ literal(false),
4115
+ literal(true)
4116
+ ]);
4117
+ const _rayadesu_dsh_llm_billing_billing_getDelegatedSpend_result$schema = object({
4118
+ "total": number(),
4119
+ "models": array(object({
4120
+ "model": string(),
4121
+ "displayName": string(),
4122
+ "cost": number(),
4123
+ "peakCost": number(),
4124
+ "offPeakCost": number(),
4125
+ "cacheHitInputTokens": number(),
4126
+ "cacheMissInputTokens": number(),
4127
+ "outputTokens": number(),
4128
+ "cacheHitInputCost": number(),
4129
+ "cacheMissInputCost": number(),
4130
+ "outputCost": number()
4131
+ })),
4132
+ "isSubagent": boolean(),
4133
+ "crossedDay": boolean()
4134
+ });
4111
4135
  const _rayadesu_dsh_llm_billing_billing_getSessionSpend_parameter_0$schema = intersection(string(), unknown());
4112
4136
  const _rayadesu_dsh_llm_billing_billing_getSessionSpend_result$schema = object({
4113
4137
  "total": number(),
@@ -4138,7 +4162,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4138
4162
  const _rayadesu_dsh_llm_billing_billing_getTodaySessionsSpend_result$schema = object({ "sessions": array(object({
4139
4163
  "sessionId": intersection(string(), unknown()),
4140
4164
  "title": union([literal(null), string()]),
4141
- "total": number()
4165
+ "total": number(),
4166
+ "ownTotal": number()
4142
4167
  })) });
4143
4168
  const _rayadesu_dsh_llm_billing_billing_getTodaySpend_parameter_0$schema = union([
4144
4169
  _undefined(),
@@ -4181,17 +4206,59 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4181
4206
  codec: {
4182
4207
  mode: "strict",
4183
4208
  typeSymbol: "@rayadesu/dsh-llm-billing#billing/getBalance:force",
4184
- schema: _rayadesu_dsh_llm_billing_billing_getBalance_parameter_0$schema
4209
+ schema: _rayadesu_dsh_llm_billing_billing_getBalance_parameter_0$schema,
4210
+ create: () => _rayadesu_dsh_llm_billing_billing_getBalance_parameter_0$schema
4185
4211
  }
4186
4212
  }],
4187
4213
  result: {
4188
4214
  mode: "strict",
4189
4215
  typeSymbol: "@rayadesu/dsh-llm-billing/types#DeepSeekBalance",
4190
- schema: _rayadesu_dsh_llm_billing_billing_getBalance_result$schema
4216
+ schema: _rayadesu_dsh_llm_billing_billing_getBalance_result$schema,
4217
+ create: () => _rayadesu_dsh_llm_billing_billing_getBalance_result$schema
4218
+ },
4219
+ sourceLocation: {
4220
+ "file": "packages/llm-billing/src/balance.ts",
4221
+ "line": 172,
4222
+ "column": 3
4223
+ }
4224
+ },
4225
+ {
4226
+ id: "@rayadesu/dsh-llm-billing#billing/getDelegatedSpend",
4227
+ service: "billing",
4228
+ namespace: "billing",
4229
+ method: "getDelegatedSpend",
4230
+ invocation: { kind: "direct" },
4231
+ parameters: [{
4232
+ name: "sessionId",
4233
+ wire: "sessionId",
4234
+ source: "json",
4235
+ codec: {
4236
+ mode: "strict",
4237
+ typeSymbol: "@rayadesu/dsh-llm-billing#billing/getDelegatedSpend:sessionId",
4238
+ schema: _rayadesu_dsh_llm_billing_billing_getDelegatedSpend_parameter_0$schema,
4239
+ create: () => _rayadesu_dsh_llm_billing_billing_getDelegatedSpend_parameter_0$schema
4240
+ }
4241
+ }, {
4242
+ name: "force",
4243
+ wire: "force",
4244
+ source: "json",
4245
+ acceptsUndefined: true,
4246
+ codec: {
4247
+ mode: "strict",
4248
+ typeSymbol: "@rayadesu/dsh-llm-billing#billing/getDelegatedSpend:force",
4249
+ schema: _rayadesu_dsh_llm_billing_billing_getDelegatedSpend_parameter_1$schema,
4250
+ create: () => _rayadesu_dsh_llm_billing_billing_getDelegatedSpend_parameter_1$schema
4251
+ }
4252
+ }],
4253
+ result: {
4254
+ mode: "strict",
4255
+ typeSymbol: "@rayadesu/dsh-llm-billing/types#DeepSeekDelegatedSpend",
4256
+ schema: _rayadesu_dsh_llm_billing_billing_getDelegatedSpend_result$schema,
4257
+ create: () => _rayadesu_dsh_llm_billing_billing_getDelegatedSpend_result$schema
4191
4258
  },
4192
4259
  sourceLocation: {
4193
4260
  "file": "packages/llm-billing/src/balance.ts",
4194
- "line": 165,
4261
+ "line": 226,
4195
4262
  "column": 3
4196
4263
  }
4197
4264
  },
@@ -4208,17 +4275,19 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4208
4275
  codec: {
4209
4276
  mode: "strict",
4210
4277
  typeSymbol: "@rayadesu/dsh-llm-billing#billing/getSessionSpend:sessionId",
4211
- schema: _rayadesu_dsh_llm_billing_billing_getSessionSpend_parameter_0$schema
4278
+ schema: _rayadesu_dsh_llm_billing_billing_getSessionSpend_parameter_0$schema,
4279
+ create: () => _rayadesu_dsh_llm_billing_billing_getSessionSpend_parameter_0$schema
4212
4280
  }
4213
4281
  }],
4214
4282
  result: {
4215
4283
  mode: "strict",
4216
4284
  typeSymbol: "@rayadesu/dsh-llm-billing/types#DeepSeekSessionSpend",
4217
- schema: _rayadesu_dsh_llm_billing_billing_getSessionSpend_result$schema
4285
+ schema: _rayadesu_dsh_llm_billing_billing_getSessionSpend_result$schema,
4286
+ create: () => _rayadesu_dsh_llm_billing_billing_getSessionSpend_result$schema
4218
4287
  },
4219
4288
  sourceLocation: {
4220
4289
  "file": "packages/llm-billing/src/balance.ts",
4221
- "line": 176,
4290
+ "line": 183,
4222
4291
  "column": 3
4223
4292
  }
4224
4293
  },
@@ -4235,17 +4304,19 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4235
4304
  codec: {
4236
4305
  mode: "strict",
4237
4306
  typeSymbol: "@rayadesu/dsh-llm-billing#billing/getSessionTurnSpends:sessionId",
4238
- schema: _rayadesu_dsh_llm_billing_billing_getSessionTurnSpends_parameter_0$schema
4307
+ schema: _rayadesu_dsh_llm_billing_billing_getSessionTurnSpends_parameter_0$schema,
4308
+ create: () => _rayadesu_dsh_llm_billing_billing_getSessionTurnSpends_parameter_0$schema
4239
4309
  }
4240
4310
  }],
4241
4311
  result: {
4242
4312
  mode: "strict",
4243
4313
  typeSymbol: "@rayadesu/dsh-llm-billing/types#DeepSeekSessionTurnSpends",
4244
- schema: _rayadesu_dsh_llm_billing_billing_getSessionTurnSpends_result$schema
4314
+ schema: _rayadesu_dsh_llm_billing_billing_getSessionTurnSpends_result$schema,
4315
+ create: () => _rayadesu_dsh_llm_billing_billing_getSessionTurnSpends_result$schema
4245
4316
  },
4246
4317
  sourceLocation: {
4247
4318
  "file": "packages/llm-billing/src/balance.ts",
4248
- "line": 227,
4319
+ "line": 251,
4249
4320
  "column": 3
4250
4321
  }
4251
4322
  },
@@ -4263,17 +4334,19 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4263
4334
  codec: {
4264
4335
  mode: "strict",
4265
4336
  typeSymbol: "@rayadesu/dsh-llm-billing#billing/getTodaySessionsSpend:force",
4266
- schema: _rayadesu_dsh_llm_billing_billing_getTodaySessionsSpend_parameter_0$schema
4337
+ schema: _rayadesu_dsh_llm_billing_billing_getTodaySessionsSpend_parameter_0$schema,
4338
+ create: () => _rayadesu_dsh_llm_billing_billing_getTodaySessionsSpend_parameter_0$schema
4267
4339
  }
4268
4340
  }],
4269
4341
  result: {
4270
4342
  mode: "strict",
4271
4343
  typeSymbol: "@rayadesu/dsh-llm-billing/types#DeepSeekTodaySessionsSpend",
4272
- schema: _rayadesu_dsh_llm_billing_billing_getTodaySessionsSpend_result$schema
4344
+ schema: _rayadesu_dsh_llm_billing_billing_getTodaySessionsSpend_result$schema,
4345
+ create: () => _rayadesu_dsh_llm_billing_billing_getTodaySessionsSpend_result$schema
4273
4346
  },
4274
4347
  sourceLocation: {
4275
4348
  "file": "packages/llm-billing/src/balance.ts",
4276
- "line": 202,
4349
+ "line": 211,
4277
4350
  "column": 3
4278
4351
  }
4279
4352
  },
@@ -4291,17 +4364,19 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4291
4364
  codec: {
4292
4365
  mode: "strict",
4293
4366
  typeSymbol: "@rayadesu/dsh-llm-billing#billing/getTodaySpend:force",
4294
- schema: _rayadesu_dsh_llm_billing_billing_getTodaySpend_parameter_0$schema
4367
+ schema: _rayadesu_dsh_llm_billing_billing_getTodaySpend_parameter_0$schema,
4368
+ create: () => _rayadesu_dsh_llm_billing_billing_getTodaySpend_parameter_0$schema
4295
4369
  }
4296
4370
  }],
4297
4371
  result: {
4298
4372
  mode: "strict",
4299
4373
  typeSymbol: "@rayadesu/dsh-llm-billing/types#DeepSeekTodaySpend",
4300
- schema: _rayadesu_dsh_llm_billing_billing_getTodaySpend_result$schema
4374
+ schema: _rayadesu_dsh_llm_billing_billing_getTodaySpend_result$schema,
4375
+ create: () => _rayadesu_dsh_llm_billing_billing_getTodaySpend_result$schema
4301
4376
  },
4302
4377
  sourceLocation: {
4303
4378
  "file": "packages/llm-billing/src/balance.ts",
4304
- "line": 189,
4379
+ "line": 196,
4305
4380
  "column": 3
4306
4381
  }
4307
4382
  },
@@ -4318,7 +4393,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4318
4393
  codec: {
4319
4394
  mode: "strict",
4320
4395
  typeSymbol: "@rayadesu/dsh-llm-billing#billing/getTurnSpend:sessionId",
4321
- schema: _rayadesu_dsh_llm_billing_billing_getTurnSpend_parameter_0$schema
4396
+ schema: _rayadesu_dsh_llm_billing_billing_getTurnSpend_parameter_0$schema,
4397
+ create: () => _rayadesu_dsh_llm_billing_billing_getTurnSpend_parameter_0$schema
4322
4398
  }
4323
4399
  }, {
4324
4400
  name: "messageId",
@@ -4327,17 +4403,19 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4327
4403
  codec: {
4328
4404
  mode: "strict",
4329
4405
  typeSymbol: "@rayadesu/dsh-llm-billing#billing/getTurnSpend:messageId",
4330
- schema: _rayadesu_dsh_llm_billing_billing_getTurnSpend_parameter_1$schema
4406
+ schema: _rayadesu_dsh_llm_billing_billing_getTurnSpend_parameter_1$schema,
4407
+ create: () => _rayadesu_dsh_llm_billing_billing_getTurnSpend_parameter_1$schema
4331
4408
  }
4332
4409
  }],
4333
4410
  result: {
4334
4411
  mode: "strict",
4335
4412
  typeSymbol: "@rayadesu/dsh-llm-billing/types#DeepSeekTurnSpend",
4336
- schema: _rayadesu_dsh_llm_billing_billing_getTurnSpend_result$schema
4413
+ schema: _rayadesu_dsh_llm_billing_billing_getTurnSpend_result$schema,
4414
+ create: () => _rayadesu_dsh_llm_billing_billing_getTurnSpend_result$schema
4337
4415
  },
4338
4416
  sourceLocation: {
4339
4417
  "file": "packages/llm-billing/src/balance.ts",
4340
- "line": 215,
4418
+ "line": 239,
4341
4419
  "column": 3
4342
4420
  }
4343
4421
  }
@@ -4360,10 +4438,229 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4360
4438
  total: line.total
4361
4439
  };
4362
4440
  }
4363
- /** CNY amount, up to four decimals with trailing zeros trimmed. */
4441
+ /**
4442
+ * CNY amount, up to four decimals with trailing zeros trimmed. The balance line's
4443
+ * renderer; spend figures go through {@link formatSpendSignificant} instead.
4444
+ */
4364
4445
  function formatSpend(amount) {
4365
4446
  return `¥${amount.toFixed(4).replace(/\.?0+$/, "")}`;
4366
4447
  }
4448
+ /**
4449
+ * CNY spend amount at a fixed number of SIGNIFICANT digits (default three),
4450
+ * trailing zeros trimmed, prefixed with `¥` — and never finer than the panel's
4451
+ * own four-decimal resolution.
4452
+ *
4453
+ * Every spend figure the plugin renders goes through this: the day's own amount
4454
+ * and its three bucket costs, this session's spend and the parenthesized today
4455
+ * share after it, the per-model rows and their bucket breakdowns, the ranking
4456
+ * amounts, and the badge's own spend line. Four decimals are noise at these
4457
+ * magnitudes — their last digits move with every request (`¥0.5068` → `¥0.507`)
4458
+ * — while the four-decimal bound keeps a microscopic amount from growing a long
4459
+ * `¥0.00002` string that would widen its column: an amount that rounds to zero
4460
+ * there reads `¥0`. Only the BALANCE line keeps {@link formatSpend}'s plain four
4461
+ * decimals, because remaining credit is a balance, not a spend measurement.
4462
+ * @param amount - the amount to render.
4463
+ * @param digits - significant digits to keep (default 3).
4464
+ * @returns the rendered amount, e.g. `¥9.58`, `¥0.507`, `¥1230`, `¥0`.
4465
+ */
4466
+ function formatSpendSignificant(amount, digits = 3) {
4467
+ if (!Number.isFinite(amount) || amount === 0) return "¥0";
4468
+ return `¥${Number(Number(amount.toPrecision(digits)).toFixed(4))}`;
4469
+ }
4470
+ /**
4471
+ * A token count in DSH's own compact notation: `517` / `12.2K` / `517K` / `1.2M`.
4472
+ *
4473
+ * Mirrors ui-chat's `formatTokens` (`packages/client/ui-chat/src/client/chat/
4474
+ * token-format.ts`) rule for rule, so one shell never renders the same count two
4475
+ * ways. That rule reads: below 1e3 the plain integer (no digit grouping); below
4476
+ * 1e6 the shared `number.thousand` unit (`{value}K`); otherwise the shared
4477
+ * `number.million` unit (`{value}M`). A scaled value keeps one decimal while it
4478
+ * is below 100 and rounds to a whole number from 100 up (`12.2K`, but `517K`),
4479
+ * and there is no third unit, so 1.2e9 renders as `1200M` — exactly as DSH
4480
+ * renders it. The unit letters are identical in both DSH dictionaries, so they
4481
+ * are literals here rather than a locale lookup into the shared vocabulary.
4482
+ * @param count - the token count to render.
4483
+ * @returns the compact count, without the label's own ` tok` suffix.
4484
+ */
4485
+ function formatTokens(count) {
4486
+ const scaled = (value) => value >= 100 ? String(Math.round(value)) : String(Math.round(value * 10) / 10);
4487
+ if (count < 1e3) return String(count);
4488
+ if (count < 1e6) return `${scaled(count / 1e3)}K`;
4489
+ return `${scaled(count / 1e6)}M`;
4490
+ }
4491
+ /** Round a cache-read ratio to exact percentage units, with positive ties rounded up. */
4492
+ function roundedPercentUnits(cacheReadTokens, denominator, decimalPlaces) {
4493
+ const scale = (decimalPlaces === 0 ? 1 : 10) * 100;
4494
+ const doubledScale = scale * 2;
4495
+ const denominatorQuotient = Math.floor(denominator / doubledScale);
4496
+ const denominatorRemainder = denominator % doubledScale;
4497
+ let lower = 0;
4498
+ let upper = scale;
4499
+ while (lower < upper) {
4500
+ const candidate = Math.floor((lower + upper + 1) / 2);
4501
+ const factor = candidate * 2 - 1;
4502
+ if (cacheReadTokens >= factor * denominatorQuotient + Math.ceil(factor * denominatorRemainder / doubledScale)) lower = candidate;
4503
+ else upper = candidate - 1;
4504
+ }
4505
+ return lower;
4506
+ }
4507
+ /** Percentage units back to text, dropping a redundant trailing `.0`. */
4508
+ function displayPercentUnits(units, decimalPlaces) {
4509
+ if (decimalPlaces === 0) return String(units);
4510
+ const whole = Math.floor(units / 10);
4511
+ const tenths = units % 10;
4512
+ return tenths === 0 ? String(whole) : `${whole}.${tenths}`;
4513
+ }
4514
+ /**
4515
+ * The cache-hit share of one prompt, in DSH's own percentage rule.
4516
+ *
4517
+ * Mirrors ui-chat's `formatCacheHitPercent`
4518
+ * (`packages/client/ui-chat/src/client/chat/token-format.ts`) rule for rule, so
4519
+ * one shell never prints the same ratio two ways: an integer percentage by
4520
+ * default, and — because a partial hit must never round up to a full one — just
4521
+ * enough extra decimals to stay below 100 when integer rounding would reach it
4522
+ * (`99`, `99.5`, `99.95`, …). The rounding runs in integer arithmetic with
4523
+ * positive ties going up, so it does not ride on binary-float precision.
4524
+ *
4525
+ * The ratio is over PROMPT-side tokens only (cache read over cache read plus
4526
+ * uncached input, the split DSH's own `billedInputTokens` makes); output tokens
4527
+ * never enter the denominator.
4528
+ * @param cacheReadTokens - exact prompt tokens served from cache.
4529
+ * @param promptTokens - exact aggregate prompt-side tokens.
4530
+ * @param decimalPlaces - ordinary-ratio precision; a partial hit that would
4531
+ * round to 100 automatically uses enough additional precision to stay honest.
4532
+ * @returns percentage text without its `%`, or null when there was no prompt input.
4533
+ */
4534
+ function formatCacheHitPercent(cacheReadTokens, promptTokens, decimalPlaces = 0) {
4535
+ if (promptTokens === 0) return null;
4536
+ const missedInputTokens = promptTokens - cacheReadTokens;
4537
+ if (missedInputTokens === 0) return "100";
4538
+ const roundedUnits = roundedPercentUnits(cacheReadTokens, promptTokens, decimalPlaces);
4539
+ if (roundedUnits < (decimalPlaces === 0 ? 100 : 1e3)) return displayPercentUnits(roundedUnits, decimalPlaces);
4540
+ let distinguishingPlaces = 1;
4541
+ let scaledDoubleGap = missedInputTokens * 200;
4542
+ const denominatorTens = Math.floor(promptTokens / 10);
4543
+ while (scaledDoubleGap <= denominatorTens) {
4544
+ scaledDoubleGap *= 10;
4545
+ distinguishingPlaces += 1;
4546
+ }
4547
+ const denominatorOnes = promptTokens % 10;
4548
+ let roundedLoss = 5;
4549
+ for (let loss = 1; loss < 5; loss += 1) {
4550
+ const factor = loss * 2 + 1;
4551
+ const threshold = factor * denominatorTens + Math.floor(factor * denominatorOnes / 10);
4552
+ if (scaledDoubleGap <= threshold) {
4553
+ roundedLoss = loss;
4554
+ break;
4555
+ }
4556
+ }
4557
+ return `99.${"9".repeat(distinguishingPlaces - 1)}${10 - roundedLoss}`;
4558
+ }
4559
+ /**
4560
+ * The total billed tokens of a priced row: the three buckets summed. Tokens are
4561
+ * already counted per bucket when they were priced, so a cache-hit input token
4562
+ * and an output token are both one token here — the number answers "how many
4563
+ * tokens did this cost", not "how many characters were sent".
4564
+ * @param row - one priced model row.
4565
+ * @returns the row's total token count.
4566
+ */
4567
+ function rowTokens(row) {
4568
+ return row.cacheHitInputTokens + row.cacheMissInputTokens + row.outputTokens;
4569
+ }
4570
+ //#endregion
4571
+ //#region lib/types/client/spendBuckets.js
4572
+ /**
4573
+ * The composer spend card's three bucket amounts, derived from the pushed
4574
+ * `billingTodaySpend` projection. The host fold already prices every sample
4575
+ * into the three disjoint billing buckets (cache-hit input, cache-miss input —
4576
+ * which includes cache writes, output including reasoning), so the card sums
4577
+ * the session's priced model rows instead of adding a Remote call or a second
4578
+ * fold. Keeping the derivation here (pure, no React) lets the sum and its
4579
+ * float reconciliation be tested on their own.
4580
+ * @module @rayadesu/dsh-client-ui-billing/spendBuckets
4581
+ */
4582
+ /** Sum one model row's three bucket costs. */
4583
+ function bucketsOf(row) {
4584
+ return [
4585
+ row.cacheMissInputCost,
4586
+ row.cacheHitInputCost,
4587
+ row.outputCost
4588
+ ];
4589
+ }
4590
+ /**
4591
+ * Fold one spend's model rows into the card's three bucket amounts.
4592
+ *
4593
+ * The rows carry float costs, so the three independently rendered values can
4594
+ * miss the displayed total by one unit in the last rendered decimal after each
4595
+ * is formatted to four decimals. The residual is absorbed by the largest
4596
+ * bucket — the only one where a 1e-4 nudge cannot be seen — so the card's rows
4597
+ * always add up to the total it shows in its header.
4598
+ * @param spend - the session bucket of the `billingTodaySpend` projection
4599
+ * (the session's own events, every Beijing day).
4600
+ * @returns the three bucket totals plus the billed total.
4601
+ */
4602
+ function spendBucketsOf(spend) {
4603
+ let uncachedInput = 0;
4604
+ let cacheRead = 0;
4605
+ let output = 0;
4606
+ for (const row of spend.models) {
4607
+ const [miss, hit, out] = bucketsOf(row);
4608
+ uncachedInput += miss;
4609
+ cacheRead += hit;
4610
+ output += out;
4611
+ }
4612
+ const total = spend.total;
4613
+ const residual = total - (uncachedInput + cacheRead + output);
4614
+ if (residual !== 0) {
4615
+ if (uncachedInput >= cacheRead && uncachedInput >= output) uncachedInput += residual;
4616
+ else if (cacheRead >= output) cacheRead += residual;
4617
+ else output += residual;
4618
+ }
4619
+ return {
4620
+ uncachedInput,
4621
+ cacheRead,
4622
+ output,
4623
+ total
4624
+ };
4625
+ }
4626
+ /**
4627
+ * Fold one spend's model rows into the three bucket TOKEN counts — the token
4628
+ * side of {@link spendBucketsOf}, so a surface that shows both (the panel's
4629
+ * today rows) reads one bucket's tokens and its cost from the same rows.
4630
+ *
4631
+ * No reconciliation is needed here: tokens are integers, so the three counts
4632
+ * always add up to the total the token line shows.
4633
+ * @param spend - the day's (or one session's) priced rows.
4634
+ * @returns the three bucket token counts.
4635
+ */
4636
+ function tokenBucketsOf(spend) {
4637
+ let uncachedInput = 0;
4638
+ let cacheRead = 0;
4639
+ let output = 0;
4640
+ for (const row of spend.models) {
4641
+ uncachedInput += row.cacheMissInputTokens;
4642
+ cacheRead += row.cacheHitInputTokens;
4643
+ output += row.outputTokens;
4644
+ }
4645
+ return {
4646
+ uncachedInput,
4647
+ cacheRead,
4648
+ output
4649
+ };
4650
+ }
4651
+ /**
4652
+ * The cache-hit share of one spend, in DSH's own percentage rule — the ratio of
4653
+ * the cache-read bucket to every PROMPT-side bucket (uncached input with cache
4654
+ * writes folded in, plus cache read), output excluded, exactly the split DSH's
4655
+ * `billedInputTokens` makes. Rendered by {@link formatCacheHitPercent}, i.e. an
4656
+ * integer percent that grows decimals only to keep a partial hit below 100.
4657
+ * @param spend - the day's priced rows.
4658
+ * @returns the percentage text without its `%`, or null when the spend billed no prompt input.
4659
+ */
4660
+ function cacheHitPercentOf(spend) {
4661
+ const tokens = tokenBucketsOf(spend);
4662
+ return formatCacheHitPercent(tokens.cacheRead, tokens.cacheRead + tokens.uncachedInput);
4663
+ }
4367
4664
  //#endregion
4368
4665
  //#region lib/types/client/version.js
4369
4666
  /**
@@ -4380,10 +4677,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4380
4677
  * @module @rayadesu/dsh-client-ui-billing/version
4381
4678
  */
4382
4679
  /** The installed plugin version, e.g. `0.3.10`; `dev` when nothing stamped one. */
4383
- const PLUGIN_VERSION = "0.3.11";
4680
+ const PLUGIN_VERSION = "0.3.13";
4384
4681
  //#endregion
4385
4682
  //#region \0dsh-css:C:\Users\admin\Desktop\me\code\DeepSeek-Harness-chat-billing\packages\ui-billing\src\client\BalanceBadge.module.css.mjs
4386
- const css$1 = "._4_Wqcq_root{position:relative}._4_Wqcq_trigger{background:var(--dsw-alias-fill-l2);min-height:32px;color:var(--dsw-alias-label-secondary);cursor:pointer;border:0;border-radius:8px;align-items:center;gap:4px;padding:4px 8px;display:inline-flex}._4_Wqcq_trigger:hover,._4_Wqcq_trigger:focus-visible{color:var(--dsw-alias-label-primary)}._4_Wqcq_trigger svg{transition:transform .12s}._4_Wqcq_chevronOpen{transform:rotate(180deg)}._4_Wqcq_triggerLines{text-align:left;flex-direction:column;align-items:flex-start;gap:0;display:flex}._4_Wqcq_linePrimary{font-variant-numeric:tabular-nums;font-size:12px;line-height:16px}._4_Wqcq_lineSecondary{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;font-size:11px;line-height:15px}._4_Wqcq_unavailable{color:var(--dsw-alias-label-tertiary)}._4_Wqcq_inlineIcon{color:var(--dsw-alias-label-tertiary);flex:none}._4_Wqcq_spinning{color:var(--dsw-alias-label-tertiary);flex:none;animation:.8s linear infinite _4_Wqcq_spin}@keyframes _4_Wqcq_spin{to{transform:rotate(360deg)}}._4_Wqcq_panel{z-index:100;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-specific-menu);width:336px;max-width:min(360px,100vw - 32px);box-shadow:var(--dsw-shadow-lv3);border-radius:12px;flex-direction:column;gap:2px;padding:6px;display:flex;position:absolute;top:calc(100% + 6px);right:0}._4_Wqcq_amountRow{border-radius:8px;justify-content:space-between;align-items:center;gap:8px;padding:6px 8px;display:flex}._4_Wqcq_amountLabel{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;font-size:13px;line-height:18px}._4_Wqcq_amountRow>._4_Wqcq_amountLabel{font-size:15px;line-height:22px}._4_Wqcq_spendToday{color:var(--dsw-alias-label-tertiary);font-size:12px}._4_Wqcq_spendRow{border-top:1px solid var(--dsw-alias-border-l2);border-radius:0;justify-content:space-between;align-items:center;gap:8px;padding:6px 8px;display:flex}._4_Wqcq_costRow{align-items:center;padding:2px 8px 6px;display:flex}._4_Wqcq_costBreakdown{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;font-size:11px;line-height:16px}._4_Wqcq_amountActions{align-items:center;gap:2px;display:inline-flex}._4_Wqcq_infoButton,._4_Wqcq_refreshButton{width:24px;height:24px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:0;border-radius:6px;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}._4_Wqcq_infoButton:hover,._4_Wqcq_infoButton:focus-visible,._4_Wqcq_refreshButton:hover,._4_Wqcq_refreshButton:focus-visible{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-fill-l2)}._4_Wqcq_modelRow{border-radius:8px;justify-content:space-between;align-items:baseline;gap:8px;padding:6px 8px;display:flex}._4_Wqcq_modelName{color:var(--dsw-alias-label-secondary);flex:none;font-size:12px;line-height:18px}._4_Wqcq_tasks{text-align:right;min-width:0;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;overflow-wrap:anywhere;font-size:12px;line-height:18px}._4_Wqcq_ranking{border-top:1px solid var(--dsw-alias-border-l2);flex-direction:column;gap:0;padding-top:2px;display:flex}._4_Wqcq_rankingTitle{color:var(--dsw-alias-label-secondary);padding:6px 8px 2px;font-size:12px;line-height:18px}._4_Wqcq_rankingRow{border-radius:8px;align-items:baseline;gap:0;min-width:0;padding:3px 8px;display:flex}._4_Wqcq_rankingRow:hover{background:var(--dsw-alias-fill-l2)}._4_Wqcq_rankingIndex{width:16px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;text-align:right;flex:none;font-size:11px;line-height:16px}._4_Wqcq_rankingDot{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;white-space:pre;flex:none;font-size:11px;line-height:16px}._4_Wqcq_rankingName{min-width:0;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;text-overflow:ellipsis;white-space:nowrap;flex:1;font-size:11px;line-height:16px;overflow:hidden}._4_Wqcq_rankingAmount{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;flex:none;margin-left:8px;font-size:11px;line-height:16px}._4_Wqcq_rankingMore{color:var(--dsw-alias-label-tertiary);padding:2px 8px 4px;font-size:11px;line-height:16px}";
4683
+ const css$1 = "._4_Wqcq_root{position:relative}._4_Wqcq_trigger{background:var(--dsw-alias-fill-l2);min-height:32px;color:var(--dsw-alias-label-secondary);cursor:pointer;border:0;border-radius:8px;align-items:center;gap:4px;padding:4px 8px;display:inline-flex}._4_Wqcq_trigger:hover,._4_Wqcq_trigger:focus-visible{color:var(--dsw-alias-label-primary)}._4_Wqcq_trigger svg{transition:transform .12s}._4_Wqcq_chevronOpen{transform:rotate(180deg)}._4_Wqcq_triggerLines{text-align:left;flex-direction:column;align-items:flex-start;gap:0;display:flex}._4_Wqcq_linePrimary{font-variant-numeric:tabular-nums;font-size:12px;line-height:16px}._4_Wqcq_lineSecondary{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;font-size:11px;line-height:15px}._4_Wqcq_unavailable{color:var(--dsw-alias-label-tertiary)}._4_Wqcq_inlineIcon{color:var(--dsw-alias-label-tertiary);flex:none}._4_Wqcq_spinning{color:var(--dsw-alias-label-tertiary);flex:none;animation:.8s linear infinite _4_Wqcq_spin}@keyframes _4_Wqcq_spin{to{transform:rotate(360deg)}}._4_Wqcq_panel{--billing-type-1-size:11px;--billing-type-1-line:16px;--billing-type-1-tone:var(--dsw-alias-label-tertiary);--billing-type-2-size:12px;--billing-type-2-line:18px;--billing-type-2-tone:var(--dsw-alias-label-secondary);--billing-type-2-number-tone:var(--dsw-alias-label-tertiary);--billing-type-3-size:13px;--billing-type-3-line:18px;--billing-type-3-tone:var(--dsw-alias-label-primary);z-index:100;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-specific-menu);width:336px;max-width:min(360px,100vw - 32px);box-shadow:var(--dsw-shadow-lv3);border-radius:12px;flex-direction:column;gap:2px;padding:6px;display:flex;position:absolute;top:calc(100% + 6px);right:0}._4_Wqcq_amountRow{border-radius:8px;justify-content:space-between;align-items:center;gap:8px;padding:6px 8px;display:flex}._4_Wqcq_amountLabel{color:var(--billing-type-3-tone);font-size:var(--billing-type-3-size);line-height:var(--billing-type-3-line);font-variant-numeric:tabular-nums}._4_Wqcq_amountRow>._4_Wqcq_amountLabel{font-size:15px;line-height:22px}._4_Wqcq_sessionToday,._4_Wqcq_todayHit{color:var(--billing-type-1-tone);font-size:var(--billing-type-1-size);line-height:var(--billing-type-1-line);font-variant-numeric:tabular-nums;margin-left:5px}._4_Wqcq_spendRow{border-top:1px solid var(--dsw-alias-border-l2);border-radius:0;justify-content:space-between;align-items:center;gap:8px;padding:8px 8px 6px;display:flex}._4_Wqcq_costRow{align-items:center;padding:0 8px;display:flex}._4_Wqcq_costRow:last-child{padding-bottom:6px}._4_Wqcq_spendRow+._4_Wqcq_costRow{padding-top:2px}._4_Wqcq_dayBucketRow{align-items:center;padding:2px 8px;display:flex}._4_Wqcq_dayBucketRow+._4_Wqcq_dayBucketRow{padding-bottom:7px}._4_Wqcq_costBreakdown{color:var(--billing-type-1-tone);font-size:var(--billing-type-1-size);line-height:var(--billing-type-1-line);font-variant-numeric:tabular-nums}._4_Wqcq_amountActions{align-items:center;gap:2px;display:inline-flex}._4_Wqcq_infoButton,._4_Wqcq_refreshButton{width:24px;height:24px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:0;border-radius:6px;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}._4_Wqcq_infoButton:hover,._4_Wqcq_infoButton:focus-visible,._4_Wqcq_refreshButton:hover,._4_Wqcq_refreshButton:focus-visible{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-fill-l2)}._4_Wqcq_modelRow{border-radius:8px;justify-content:space-between;align-items:baseline;gap:8px;padding:4px 8px;display:flex}._4_Wqcq_spendRow+._4_Wqcq_modelRow{padding-top:2px}._4_Wqcq_modelName{color:var(--billing-type-2-tone);font-size:var(--billing-type-2-size);line-height:var(--billing-type-2-line);flex:none}._4_Wqcq_tasks{text-align:right;min-width:0;color:var(--billing-type-2-number-tone);font-size:var(--billing-type-2-size);line-height:var(--billing-type-2-line);font-variant-numeric:tabular-nums;overflow-wrap:anywhere}._4_Wqcq_ranking{border-top:1px solid var(--dsw-alias-border-l2);flex-direction:column;gap:0;margin-top:7px;padding-top:2px;display:flex}._4_Wqcq_rankingTitle{color:var(--billing-type-2-tone);font-size:var(--billing-type-2-size);line-height:var(--billing-type-2-line);padding:6px 8px 2px}._4_Wqcq_rankingRow{border-radius:8px;align-items:baseline;gap:0;min-width:0;padding:3px 8px;display:flex}._4_Wqcq_rankingRow:hover{background:var(--dsw-alias-fill-l2)}._4_Wqcq_rankingIndex{width:16px;color:var(--billing-type-1-tone);font-size:var(--billing-type-1-size);line-height:var(--billing-type-1-line);font-variant-numeric:tabular-nums;text-align:right;flex:none}._4_Wqcq_rankingDot{color:var(--billing-type-1-tone);font-size:var(--billing-type-1-size);line-height:var(--billing-type-1-line);font-variant-numeric:tabular-nums;white-space:pre;flex:none}._4_Wqcq_rankingName{min-width:0;color:var(--billing-type-1-tone);font-size:var(--billing-type-1-size);line-height:var(--billing-type-1-line);font-variant-numeric:tabular-nums;text-overflow:ellipsis;white-space:nowrap;flex:1;overflow:hidden}._4_Wqcq_rankingAmount{color:var(--billing-type-1-tone);font-size:var(--billing-type-1-size);line-height:var(--billing-type-1-line);font-variant-numeric:tabular-nums;flex:none;margin-left:8px}._4_Wqcq_rankingMore{color:var(--billing-type-1-tone);font-size:var(--billing-type-1-size);line-height:var(--billing-type-1-line);padding:2px 8px 4px}";
4387
4684
  const tagId$1 = "@rayadesu/dsh-client-ui-billing/BalanceBadge.module.css";
4388
4685
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
4389
4686
  const tag = document.createElement("style");
@@ -4399,6 +4696,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4399
4696
  "chevronOpen": "_4_Wqcq_chevronOpen",
4400
4697
  "costBreakdown": "_4_Wqcq_costBreakdown",
4401
4698
  "costRow": "_4_Wqcq_costRow",
4699
+ "dayBucketRow": "_4_Wqcq_dayBucketRow",
4402
4700
  "infoButton": "_4_Wqcq_infoButton",
4403
4701
  "inlineIcon": "_4_Wqcq_inlineIcon",
4404
4702
  "linePrimary": "_4_Wqcq_linePrimary",
@@ -4416,18 +4714,56 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4416
4714
  "rankingTitle": "_4_Wqcq_rankingTitle",
4417
4715
  "refreshButton": "_4_Wqcq_refreshButton",
4418
4716
  "root": "_4_Wqcq_root",
4717
+ "sessionToday": "_4_Wqcq_sessionToday",
4419
4718
  "spendRow": "_4_Wqcq_spendRow",
4420
- "spendToday": "_4_Wqcq_spendToday",
4421
4719
  "spin": "_4_Wqcq_spin",
4422
4720
  "spinning": "_4_Wqcq_spinning",
4423
4721
  "tasks": "_4_Wqcq_tasks",
4722
+ "todayHit": "_4_Wqcq_todayHit",
4424
4723
  "trigger": "_4_Wqcq_trigger",
4425
4724
  "triggerLines": "_4_Wqcq_triggerLines",
4426
4725
  "unavailable": "_4_Wqcq_unavailable"
4427
4726
  };
4727
+ /**
4728
+ * Today's two bucket detail lines, rendered under the 今日 Token / 今日花费 row:
4729
+ * the three buckets' token counts, then their costs — DSH's own bucket wording
4730
+ * and row order in both lines, so they read like the per-model breakdown line
4731
+ * further down and keep its own row rhythm. Each line stands on its own (no
4732
+ * column alignment between them): the middots keep the natural " · " spacing of
4733
+ * every other breakdown line in the panel.
4734
+ *
4735
+ * Tokens and costs come from the SAME day row set (`todaySpend.models`) the row
4736
+ * above is built from: the three token counts add up to the token figure, and
4737
+ * the three costs (with `spendBucketsOf`'s residual absorbed into the largest
4738
+ * bucket) to the cost figure. Costs render at three significant digits, like the
4739
+ * day figure above them, and never finer than four decimals — their last digits
4740
+ * move with every request and carry no meaning.
4741
+ */
4742
+ function todayBucketLines(t, spend) {
4743
+ const tokens = tokenBucketsOf(spend);
4744
+ const costs = spendBucketsOf(spend);
4745
+ return {
4746
+ tokenLine: [
4747
+ `${t("label.bucket.input")} ${t("unit.tokens", { count: formatTokens(tokens.uncachedInput) })}`,
4748
+ `${t("label.bucket.cacheRead")} ${t("unit.tokens", { count: formatTokens(tokens.cacheRead) })}`,
4749
+ `${t("label.bucket.output")} ${t("unit.tokens", { count: formatTokens(tokens.output) })}`
4750
+ ].join(" · "),
4751
+ costLine: [
4752
+ t("label.cost.input", { amount: formatSpendSignificant(costs.uncachedInput) }),
4753
+ t("label.cost.cacheRead", { amount: formatSpendSignificant(costs.cacheRead) }),
4754
+ t("label.cost.output", { amount: formatSpendSignificant(costs.output) })
4755
+ ].join(" · ")
4756
+ };
4757
+ }
4428
4758
  /** The detail box opened from the badge trigger. */
4429
- function BalancePanel({ amount, sessionId, spend, todaySpend, sessionsSpend, refreshing, onRefresh, t }) {
4430
- const sessionToday = sessionsSpend === null ? void 0 : sessionsSpend.sessions.find((row) => row.sessionId === sessionId)?.total ?? 0;
4759
+ function BalancePanel({ amount, sessionId, spend, todaySpend, sessionsSpend, isSubagent, crossedDay, refreshing, onRefresh, t }) {
4760
+ const row = sessionsSpend === null ? void 0 : sessionsSpend.sessions.find((entry) => entry.sessionId === sessionId);
4761
+ const sessionToday = spend !== null && !isSubagent && crossedDay && sessionsSpend !== null ? row?.total ?? 0 : null;
4762
+ const sessionAmount = spend === null ? "—" : spend.models.length === 0 ? t("stat.none") : formatSpendSignificant(spend.total);
4763
+ const todayTokens = todaySpend === null ? "—" : todaySpend.models.length === 0 ? t("stat.none") : t("unit.tokens", { count: formatTokens(todaySpend.models.reduce((sum, row) => sum + rowTokens(row), 0)) });
4764
+ const todayAmount = todaySpend === null ? "—" : todaySpend.models.length === 0 ? t("stat.none") : formatSpendSignificant(todaySpend.total);
4765
+ const todayHit = todaySpend === null || todaySpend.models.length === 0 ? null : cacheHitPercentOf(todaySpend);
4766
+ const modelCount = spend === null ? 0 : spend.models.length;
4431
4767
  return (0, react_jsx_runtime.jsxs)("div", {
4432
4768
  className: BalanceBadge_module_css_default.panel,
4433
4769
  role: "dialog",
@@ -4465,34 +4801,57 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4465
4801
  className: BalanceBadge_module_css_default.spendRow,
4466
4802
  children: [(0, react_jsx_runtime.jsxs)("span", {
4467
4803
  className: BalanceBadge_module_css_default.amountLabel,
4468
- children: [t("label.sessionSpend", { amount: spend === null ? "—" : spend.models.length === 0 ? t("stat.none") : formatSpend(spend.total) }), (0, react_jsx_runtime.jsx)("span", {
4469
- className: BalanceBadge_module_css_default.spendToday,
4470
- children: t("label.sessionSpend.today", { amount: sessionToday === void 0 ? "—" : formatSpend(sessionToday) })
4471
- })]
4804
+ children: [t("label.todayTokens", { count: todayTokens }), todayHit !== null ? (0, react_jsx_runtime.jsx)("span", {
4805
+ className: BalanceBadge_module_css_default.todayHit,
4806
+ children: t("label.todayTokens.hit", { percent: todayHit })
4807
+ }) : null]
4472
4808
  }), (0, react_jsx_runtime.jsx)("span", {
4473
4809
  className: BalanceBadge_module_css_default.amountLabel,
4474
- children: t("label.todaySpend", { amount: todaySpend === null ? "—" : todaySpend.models.length === 0 ? t("stat.none") : formatSpend(todaySpend.total) })
4810
+ children: t("label.todaySpend", { amount: todayAmount })
4475
4811
  })]
4476
4812
  }),
4477
- spend?.models.map((model) => (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [(0, react_jsx_runtime.jsxs)("div", {
4813
+ todaySpend !== null && todaySpend.models.length > 0 && (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)("div", {
4814
+ className: BalanceBadge_module_css_default.dayBucketRow,
4815
+ children: (0, react_jsx_runtime.jsx)("span", {
4816
+ className: BalanceBadge_module_css_default.costBreakdown,
4817
+ children: todayBucketLines(t, todaySpend).tokenLine
4818
+ })
4819
+ }), (0, react_jsx_runtime.jsx)("div", {
4820
+ className: BalanceBadge_module_css_default.dayBucketRow,
4821
+ children: (0, react_jsx_runtime.jsx)("span", {
4822
+ className: BalanceBadge_module_css_default.costBreakdown,
4823
+ children: todayBucketLines(t, todaySpend).costLine
4824
+ })
4825
+ })] }),
4826
+ (0, react_jsx_runtime.jsx)("div", {
4827
+ className: BalanceBadge_module_css_default.spendRow,
4828
+ children: (0, react_jsx_runtime.jsxs)("span", {
4829
+ className: BalanceBadge_module_css_default.amountLabel,
4830
+ children: [t("label.sessionSpend", { amount: sessionAmount }), sessionToday !== null ? (0, react_jsx_runtime.jsx)("span", {
4831
+ className: BalanceBadge_module_css_default.sessionToday,
4832
+ children: t("label.sessionSpend.today", { amount: formatSpendSignificant(sessionToday) })
4833
+ }) : null]
4834
+ })
4835
+ }),
4836
+ spend?.models.map((model) => (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [modelCount > 1 && (0, react_jsx_runtime.jsxs)("div", {
4478
4837
  className: BalanceBadge_module_css_default.modelRow,
4479
4838
  children: [(0, react_jsx_runtime.jsx)("span", {
4480
4839
  className: BalanceBadge_module_css_default.modelName,
4481
4840
  children: model.displayName
4482
4841
  }), (0, react_jsx_runtime.jsx)("span", {
4483
4842
  className: BalanceBadge_module_css_default.tasks,
4484
- children: formatSpend(model.cost)
4843
+ children: formatSpendSignificant(model.cost)
4485
4844
  })]
4486
4845
  }), (0, react_jsx_runtime.jsx)("div", {
4487
4846
  className: BalanceBadge_module_css_default.costRow,
4488
4847
  children: (0, react_jsx_runtime.jsxs)("span", {
4489
4848
  className: BalanceBadge_module_css_default.costBreakdown,
4490
4849
  children: [
4491
- t("label.cost.hit", { amount: formatSpend(model.cacheHitInputCost) }),
4850
+ t("label.cost.input", { amount: formatSpendSignificant(model.cacheMissInputCost) }),
4492
4851
  " · ",
4493
- t("label.cost.input", { amount: formatSpend(model.cacheMissInputCost) }),
4852
+ t("label.cost.cacheRead", { amount: formatSpendSignificant(model.cacheHitInputCost) }),
4494
4853
  " · ",
4495
- t("label.cost.output", { amount: formatSpend(model.outputCost) })
4854
+ t("label.cost.output", { amount: formatSpendSignificant(model.outputCost) })
4496
4855
  ]
4497
4856
  })
4498
4857
  })] }, model.model)),
@@ -4522,7 +4881,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4522
4881
  }),
4523
4882
  (0, react_jsx_runtime.jsx)("span", {
4524
4883
  className: BalanceBadge_module_css_default.rankingAmount,
4525
- children: formatSpend(row.total)
4884
+ children: formatSpendSignificant(row.total)
4526
4885
  })
4527
4886
  ]
4528
4887
  }, row.sessionId)),
@@ -4539,7 +4898,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4539
4898
  //#region lib/types/client/BalanceTrigger.js
4540
4899
  /**
4541
4900
  * Session-header billing trigger: the balance plus the conversation-spend
4542
- * line. Pure view over the data hook's values — no state, no effects.
4901
+ * line. Pure view over the data hook's values — no state, no effects. The
4902
+ * balance line uses the trigger's own `trigger.balance` (the panel headline
4903
+ * without its "API" prefix, since the chip is narrow); the spend line arrives
4904
+ * precomputed from the parent carrying the PANEL's `label.sessionSpend`.
4543
4905
  */
4544
4906
  /** The badge button: balance line, spend line, and the open-state chevron. */
4545
4907
  function BalanceTrigger({ amount, spendLine, open, onToggle, t }) {
@@ -4562,9 +4924,57 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4562
4924
  });
4563
4925
  }
4564
4926
  //#endregion
4927
+ //#region lib/types/client/spends.js
4928
+ /**
4929
+ * Additive merge of a conversation's two spend halves: the session's OWN billed
4930
+ * spend (live, from the pushed `billingTodaySpend` projection or the
4931
+ * `billing/getSessionSpend` fallback) and the delegated-subagent subtotal
4932
+ * (`billing/getDelegatedSpend`). The panel shows one amount for the whole
4933
+ * conversation, so the two must be summed before rendering — and the per-model
4934
+ * rows must be summed the same way, or the breakdown would no longer add up to
4935
+ * the amount above it.
4936
+ *
4937
+ * Pure addition only: no pricing, no timezone, no fork-boundary knowledge lives
4938
+ * here (the host owns all of that), so this cannot drift from the host's own
4939
+ * sums.
4940
+ */
4941
+ /** Sum two model rows of the same wire model (display name taken from the first). */
4942
+ function addRows(left, right) {
4943
+ return {
4944
+ ...left,
4945
+ cost: left.cost + right.cost,
4946
+ peakCost: left.peakCost + right.peakCost,
4947
+ offPeakCost: left.offPeakCost + right.offPeakCost,
4948
+ cacheHitInputTokens: left.cacheHitInputTokens + right.cacheHitInputTokens,
4949
+ cacheMissInputTokens: left.cacheMissInputTokens + right.cacheMissInputTokens,
4950
+ outputTokens: left.outputTokens + right.outputTokens,
4951
+ cacheHitInputCost: left.cacheHitInputCost + right.cacheHitInputCost,
4952
+ cacheMissInputCost: left.cacheMissInputCost + right.cacheMissInputCost,
4953
+ outputCost: left.outputCost + right.outputCost
4954
+ };
4955
+ }
4956
+ /**
4957
+ * Sum one conversation's own spend and its delegated-subagent subtotal.
4958
+ * @param own - the session's own billed spend.
4959
+ * @param delegated - the delegated subtotal from the last `getDelegatedSpend` read.
4960
+ * @returns the merged spend (own model order first, then newly seen models).
4961
+ */
4962
+ function sumSpends(own, delegated) {
4963
+ const rows = /* @__PURE__ */ new Map();
4964
+ for (const row of own.models) rows.set(row.model, row);
4965
+ for (const row of delegated.models) {
4966
+ const existing = rows.get(row.model);
4967
+ rows.set(row.model, existing === void 0 ? row : addRows(existing, row));
4968
+ }
4969
+ return {
4970
+ total: own.total + delegated.total,
4971
+ models: [...rows.values()]
4972
+ };
4973
+ }
4974
+ //#endregion
4565
4975
  //#region lib/types/client/useBillingData.js
4566
4976
  /**
4567
- * Billing badge data hook: owns the balance/session/today states, the
4977
+ * Billing badge data hook: owns the balance/session/today/delegated states, the
4568
4978
  * mount+refresh fetch, the turn-settled recompute, and the click-outside
4569
4979
  * close. The trigger and the panel are pure views over the returned values,
4570
4980
  * so the concurrency/race handling lives in exactly one file.
@@ -4587,18 +4997,21 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4587
4997
  }
4588
4998
  /**
4589
4999
  * Start the badge's data lifecycle for one session. The spend follows the
4590
- * conversation: the host-pushed `billingTodaySpend` projection drives the
4591
- * session line live (zero Remote calls), with `getSessionSpend` as the
4592
- * bootstrap/fallback when the projection key is absent; today's spend is
4593
- * recomputed through `getTodaySpend` when a turn settles; the balance stays a
4594
- * manual-refresh snapshot and is never refetched on its own.
5000
+ * conversation: the host-pushed `billingTodaySpend` projection drives this
5001
+ * session's own part live (zero Remote calls), with `getSessionSpend` as the
5002
+ * bootstrap/fallback when the projection key is absent; the delegated-subagent
5003
+ * subtotal comes from `getDelegatedSpend` on mount, on refresh, and when a turn
5004
+ * settles; today's spend is recomputed through `getTodaySpend` on the same
5005
+ * events; the balance stays a manual-refresh snapshot and is never refetched on
5006
+ * its own.
4595
5007
  * @param props - the badge's injected face and session runtime share.
4596
5008
  */
4597
- function useBillingData({ getBalance, getCachedBalance, getSessionSpend, getTodaySpend, getTodaySessionsSpend, sessionId, useSession, useProjection }) {
5009
+ function useBillingData({ getBalance, getCachedBalance, getSessionSpend, getTodaySpend, getTodaySessionsSpend, getDelegatedSpend, sessionId, useSession, useProjection }) {
4598
5010
  const [balance, setBalance] = (0, react.useState)(getCachedBalance);
4599
5011
  const [spend, setSpend] = (0, react.useState)(null);
4600
5012
  const [todaySpend, setTodaySpend] = (0, react.useState)(null);
4601
5013
  const [sessionsSpend, setSessionsSpend] = (0, react.useState)(null);
5014
+ const [delegated, setDelegated] = (0, react.useState)(null);
4602
5015
  const [error, setError] = (0, react.useState)(null);
4603
5016
  const [refreshing, setRefreshing] = (0, react.useState)(false);
4604
5017
  const [open, setOpen] = (0, react.useState)(false);
@@ -4624,11 +5037,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4624
5037
  });
4625
5038
  const sessionSpendRequest = fetchLine(isCurrent, () => getSessionSpend(sessionId), setSpend);
4626
5039
  const todaySpendRequest = fetchLine(isCurrent, () => getTodaySpend(request > 0), setTodaySpend);
5040
+ const delegatedRequest = fetchLine(isCurrent, () => getDelegatedSpend(sessionId, request > 0), setDelegated);
4627
5041
  const sessionsSpendRequest = openRef.current ? fetchLine(isCurrent, () => getTodaySessionsSpend(request > 0), setSessionsSpend) : Promise.resolve();
4628
5042
  Promise.allSettled([
4629
5043
  balanceRequest,
4630
5044
  sessionSpendRequest,
4631
5045
  todaySpendRequest,
5046
+ delegatedRequest,
4632
5047
  sessionsSpendRequest
4633
5048
  ]).then(() => {
4634
5049
  if (current) setRefreshing(false);
@@ -4642,6 +5057,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4642
5057
  getSessionSpend,
4643
5058
  getTodaySpend,
4644
5059
  getTodaySessionsSpend,
5060
+ getDelegatedSpend,
4645
5061
  sessionId,
4646
5062
  request
4647
5063
  ]);
@@ -4661,6 +5077,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4661
5077
  const isCurrent = () => current;
4662
5078
  const timer = setTimeout(() => {
4663
5079
  fetchLine(isCurrent, () => getSessionSpend(sessionId), setSpend);
5080
+ fetchLine(isCurrent, () => getDelegatedSpend(sessionId), setDelegated);
4664
5081
  fetchLine(isCurrent, () => getTodaySpend(), setTodaySpend);
4665
5082
  if (openRef.current) fetchLine(isCurrent, () => getTodaySessionsSpend(), setSessionsSpend);
4666
5083
  }, TURN_SETTLE_DEBOUNCE_MS);
@@ -4670,6 +5087,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4670
5087
  };
4671
5088
  }, [
4672
5089
  getSessionSpend,
5090
+ getDelegatedSpend,
4673
5091
  getTodaySpend,
4674
5092
  getTodaySessionsSpend,
4675
5093
  sessionId,
@@ -4685,11 +5103,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4685
5103
  document.removeEventListener("pointerdown", closeOutside);
4686
5104
  };
4687
5105
  }, [open]);
5106
+ const own = projected === void 0 ? spend : projected.session;
4688
5107
  return {
4689
5108
  balance,
4690
- spend: projected === void 0 ? spend : projected.session,
5109
+ spend: own === null ? null : delegated === null ? own : sumSpends(own, delegated),
4691
5110
  todaySpend,
4692
5111
  sessionsSpend,
5112
+ isSubagent: delegated?.isSubagent ?? false,
5113
+ crossedDay: delegated?.crossedDay ?? false,
4693
5114
  error,
4694
5115
  refreshing,
4695
5116
  open,
@@ -4712,13 +5133,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4712
5133
  * @param props - Remote face, locale, and the standard session-header runtime share.
4713
5134
  * @returns the badge, or null until the first balance fetch settles.
4714
5135
  */
4715
- function BalanceBadge({ getBalance, getCachedBalance, getSessionSpend, getTodaySpend, getTodaySessionsSpend, sessionId, useSession, useProjection, t }) {
4716
- const { balance, spend, todaySpend, sessionsSpend, error, refreshing, open, rootRef, refresh, toggleOpen } = useBillingData({
5136
+ function BalanceBadge({ getBalance, getCachedBalance, getSessionSpend, getTodaySpend, getTodaySessionsSpend, getDelegatedSpend, sessionId, useSession, useProjection, t }) {
5137
+ const { balance, spend, todaySpend, sessionsSpend, isSubagent, crossedDay, error, refreshing, open, rootRef, refresh, toggleOpen } = useBillingData({
4717
5138
  getBalance,
4718
5139
  getCachedBalance,
4719
5140
  getSessionSpend,
4720
5141
  getTodaySpend,
4721
5142
  getTodaySessionsSpend,
5143
+ getDelegatedSpend,
4722
5144
  sessionId,
4723
5145
  useSession,
4724
5146
  useProjection
@@ -4742,7 +5164,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4742
5164
  }
4743
5165
  const line = primaryLine(balance);
4744
5166
  const amount = line === void 0 ? "—" : `${line.symbol}${line.total}`;
4745
- const spendLine = spend !== null && spend.models.length > 0 ? t("trigger.conversationSpend", { amount: formatSpend(spend.total) }) : void 0;
5167
+ const spendLine = spend !== null && spend.models.length > 0 ? t("label.sessionSpend", { amount: formatSpendSignificant(spend.total) }) : void 0;
4746
5168
  return (0, react_jsx_runtime.jsxs)("div", {
4747
5169
  ref: rootRef,
4748
5170
  className: BalanceBadge_module_css_default.root,
@@ -4758,6 +5180,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4758
5180
  spend,
4759
5181
  todaySpend,
4760
5182
  sessionsSpend,
5183
+ isSubagent,
5184
+ crossedDay,
4761
5185
  refreshing,
4762
5186
  onRefresh: refresh,
4763
5187
  t
@@ -4879,21 +5303,28 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4879
5303
  const NS = "billing";
4880
5304
  /** Simplified Chinese dictionary (the key-set source of truth). */
4881
5305
  const zh = {
4882
- "trigger.balance": "剩余额度:{amount}",
4883
- "trigger.conversationSpend": "本轮对话花费:{amount}",
5306
+ "trigger.balance": "剩余金额:{amount}",
4884
5307
  "label.amount": "API 剩余金额:{amount}",
4885
5308
  "label.sessionSpend": "本会话花费:{amount}",
4886
- "label.sessionSpend.today": "{amount}",
4887
- "label.todaySpend": "今日:{amount}",
4888
- "label.cost.hit": "缓存命中 {amount}",
4889
- "label.cost.input": "未命中输入 {amount}",
5309
+ "label.sessionSpend.today": " {amount}",
5310
+ "label.todaySpend": "今日花费:{amount}",
5311
+ "label.todayTokens": "今日 Token:{count}",
5312
+ "label.todayTokens.hit": " {percent}%",
5313
+ "unit.tokens": "{count} tok",
5314
+ "label.cost.input": "未缓存输入 {amount}",
5315
+ "label.cost.cacheRead": "缓存读取 {amount}",
4890
5316
  "label.cost.output": "输出 {amount}",
5317
+ "card.title": "花费金额",
5318
+ "card.aria": "本轮对话花费:{amount}",
5319
+ "label.bucket.input": "未缓存输入",
5320
+ "label.bucket.cacheRead": "缓存读取",
5321
+ "label.bucket.output": "输出",
4891
5322
  "stat.none": "暂无消耗记录",
4892
5323
  "stat.untitled": "未命名",
4893
5324
  "state.unavailable": "额度不可用",
4894
5325
  "action.refresh": "刷新",
4895
5326
  "info.aria": "花费说明",
4896
- "info.hint": "估算:仅 DeepSeek 与 MiMo 模型,按每条消息自身时刻的峰谷官方单价计价(高峰:工作日 9:00–12:00、14:00–18:00)。\nv{version}",
5327
+ "info.hint": "估算:仅 DeepSeek 与 MiMo 模型,按每条消息自身时刻的峰谷官方单价计价(高峰:工作日 9:00–12:00、14:00–18:00)。\n金额含本会话委派的子代理会话。\n紧跟的数字为本会话今日花费。\nv{version}",
4897
5328
  "badge.aria": "DeepSeek 额度:{amount}",
4898
5329
  "panel.aria": "DeepSeek 额度详情",
4899
5330
  "label.sessionRanking": "今日会话花费",
@@ -4902,20 +5333,27 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4902
5333
  /** English dictionary, key-identical to the Chinese source of truth. */
4903
5334
  const en = {
4904
5335
  "trigger.balance": "Balance: {amount}",
4905
- "trigger.conversationSpend": "This conversation: {amount}",
4906
5336
  "label.amount": "API balance: {amount}",
4907
5337
  "label.sessionSpend": "This session: {amount}",
4908
- "label.sessionSpend.today": " ({amount})",
4909
- "label.todaySpend": "Today: {amount}",
4910
- "label.cost.hit": "Cache hit {amount}",
4911
- "label.cost.input": "Missed input {amount}",
5338
+ "label.sessionSpend.today": " {amount}",
5339
+ "label.todaySpend": "Today spend: {amount}",
5340
+ "label.todayTokens": "Today tokens: {count}",
5341
+ "label.todayTokens.hit": " {percent}%",
5342
+ "unit.tokens": "{count} tok",
5343
+ "label.cost.input": "Uncached input {amount}",
5344
+ "label.cost.cacheRead": "Cached input {amount}",
4912
5345
  "label.cost.output": "Output {amount}",
5346
+ "card.title": "Spend",
5347
+ "card.aria": "This conversation’s spend: {amount}",
5348
+ "label.bucket.input": "Uncached input",
5349
+ "label.bucket.cacheRead": "Cached input",
5350
+ "label.bucket.output": "Output",
4913
5351
  "stat.none": "No usage recorded",
4914
5352
  "stat.untitled": "Untitled",
4915
5353
  "state.unavailable": "Balance unavailable",
4916
5354
  "action.refresh": "Refresh",
4917
5355
  "info.aria": "About this spend",
4918
- "info.hint": "Estimate: DeepSeek and MiMo models only, each message priced at the official peak/off-peak rate of its own time (peak: weekdays 09:00–12:00, 14:00–18:00).\nv{version}",
5356
+ "info.hint": "Estimate: DeepSeek and MiMo models only, each message priced at the official peak/off-peak rate of its own time (peak: weekdays 09:00–12:00, 14:00–18:00).\nThe amounts include the subagent sessions this session delegated.\nThe figure after it is this session's spend today.\nv{version}",
4919
5357
  "badge.aria": "DeepSeek balance {amount}",
4920
5358
  "panel.aria": "DeepSeek balance details",
4921
5359
  "label.sessionRanking": "Today session spend",
@@ -4958,7 +5396,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4958
5396
  getCachedBalance: () => lastBalance,
4959
5397
  getSessionSpend: async (sessionId) => unwrap("billing.getSessionSpend", await billing.getSessionSpend(sessionId)),
4960
5398
  getTodaySpend: async (force) => unwrap("billing.getTodaySpend", await billing.getTodaySpend(force)),
4961
- getTodaySessionsSpend: async (force) => unwrap("billing.getTodaySessionsSpend", await billing.getTodaySessionsSpend(force))
5399
+ getTodaySessionsSpend: async (force) => unwrap("billing.getTodaySessionsSpend", await billing.getTodaySessionsSpend(force)),
5400
+ getDelegatedSpend: async (sessionId, force) => unwrap("billing.getDelegatedSpend", await billing.getDelegatedSpend(sessionId, force))
4962
5401
  };
4963
5402
  ctx.slots.inject("conversation.session.header.utilities", () => ctx.slots.register({
4964
5403
  name: "conversation.session.header.utilities",