@vibe-cafe/vibe-usage 0.10.17 → 0.10.18

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-cafe/vibe-usage",
3
- "version": "0.10.17",
3
+ "version": "0.10.18",
4
4
  "description": "Track your AI coding tool token usage and sync to vibecafe.ai",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -14,7 +14,7 @@ import { join } from 'node:path';
14
14
  // separate from ~/.vibe-usage/state.json, whose hashes are the authoritative
15
15
  // record of successful uploads and must remain backward-compatible.
16
16
  export const CODEX_CACHE_SCHEMA_VERSION = 1;
17
- export const CODEX_PARSER_ALGORITHM_VERSION = 1;
17
+ export const CODEX_PARSER_ALGORITHM_VERSION = 2;
18
18
 
19
19
  function hash(value, length = 24) {
20
20
  return createHash('sha256').update(value).digest('hex').slice(0, length);
@@ -26,6 +26,39 @@ import {
26
26
  saveCodexFileTail,
27
27
  } from './codex-cache.js';
28
28
 
29
+ const CODEX_API_BILLING_MARKER = '#billing=api';
30
+ const CODEX_SUBSCRIPTION_BILLING_MARKER = '#billing=subscription';
31
+ // Changing a model id changes its server-side bucket key. Keep pre-release
32
+ // history byte-for-byte stable so upgrading cannot re-upload the same tokens
33
+ // under decorated keys and double-count them.
34
+ const CODEX_BILLING_ATTRIBUTION_START_MS = Date.parse('2026-08-31T00:00:00.000Z');
35
+
36
+ function normalizeCodexServiceTier(value) {
37
+ if (typeof value !== 'string') return null;
38
+ const tier = value.trim().toLowerCase();
39
+ if (tier === 'fast' || tier === 'priority') return tier;
40
+ if (tier === 'flex' || tier === 'batch') return tier;
41
+ return null;
42
+ }
43
+
44
+ function hasSubscriptionPlan(planType) {
45
+ return typeof planType === 'string'
46
+ && planType.trim() !== ''
47
+ && planType.toLowerCase() !== 'unknown';
48
+ }
49
+
50
+ function decorateCodexModel(model, serviceTier, subscriptionBilling, timestampMs) {
51
+ let decorated = model || 'unknown';
52
+ if (decorated === 'unknown' || timestampMs < CODEX_BILLING_ATTRIBUTION_START_MS) {
53
+ return decorated;
54
+ }
55
+ if (serviceTier) decorated += `-${serviceTier}`;
56
+ decorated += subscriptionBilling
57
+ ? CODEX_SUBSCRIPTION_BILLING_MARKER
58
+ : CODEX_API_BILLING_MARKER;
59
+ return decorated;
60
+ }
61
+
29
62
  // Codex stores live sessions in $CODEX_HOME/sessions (default ~/.codex) and,
30
63
  // once a session is "completed", moves its rollout file verbatim into
31
64
  // $CODEX_HOME/archived_sessions. A session can be archived between two syncs,
@@ -600,6 +633,8 @@ async function parseSessionFile(filePath, snapshotSize, fm, boundary, {
600
633
  const sessionKey = fm.sessionId || filePath;
601
634
 
602
635
  let turnContextModel = previousTail?.turnContextModel || 'unknown';
636
+ let serviceTier = previousTail?.serviceTier || null;
637
+ let subscriptionBilling = previousTail?.subscriptionBilling || false;
603
638
  let prevTotal = previousTail?.prevTotal || null;
604
639
  let prevCumulativeTotal = previousTail?.prevCumulativeTotal ?? null;
605
640
  const start = previousTail?.parsedBytes || 0;
@@ -644,8 +679,11 @@ async function parseSessionFile(filePath, snapshotSize, fm, boundary, {
644
679
  }
645
680
  }
646
681
 
647
- if (obj.type === 'turn_context' && obj.payload?.model) {
648
- turnContextModel = obj.payload.model;
682
+ if (obj.type === 'turn_context') {
683
+ if (obj.payload?.model) turnContextModel = obj.payload.model;
684
+ if (Object.hasOwn(obj.payload || {}, 'service_tier')) {
685
+ serviceTier = normalizeCodexServiceTier(obj.payload.service_tier);
686
+ }
649
687
  continue;
650
688
  }
651
689
 
@@ -654,6 +692,15 @@ async function parseSessionFile(filePath, snapshotSize, fm, boundary, {
654
692
  const payload = obj.payload;
655
693
  if (!payload) continue;
656
694
 
695
+ if (payload.type === 'thread_settings_applied') {
696
+ const settings = payload.thread_settings;
697
+ if (settings?.model) turnContextModel = settings.model;
698
+ if (Object.hasOwn(settings || {}, 'service_tier')) {
699
+ serviceTier = normalizeCodexServiceTier(settings.service_tier);
700
+ }
701
+ continue;
702
+ }
703
+
657
704
  if (payload.type !== 'token_count') continue;
658
705
 
659
706
  // Raw ordinals advance before validating usage/timestamp so pass 1 and
@@ -661,6 +708,10 @@ async function parseSessionFile(filePath, snapshotSize, fm, boundary, {
661
708
  const isReplayedHistory = inReplayBlock;
662
709
  rawTokenSeen++;
663
710
 
711
+ if (hasSubscriptionPlan(payload.rate_limits?.plan_type)) {
712
+ subscriptionBilling = true;
713
+ }
714
+
664
715
  const info = payload.info;
665
716
  if (!info) continue;
666
717
 
@@ -709,7 +760,13 @@ async function parseSessionFile(filePath, snapshotSize, fm, boundary, {
709
760
  const timestamp = obj.timestamp ? new Date(obj.timestamp) : null;
710
761
  if (!timestamp || isNaN(timestamp.getTime())) continue;
711
762
 
712
- const model = info.model || payload.model || turnContextModel || 'unknown';
763
+ const rawModel = info.model || payload.model || turnContextModel || 'unknown';
764
+ const model = decorateCodexModel(
765
+ rawModel,
766
+ serviceTier,
767
+ subscriptionBilling,
768
+ timestamp.getTime()
769
+ );
713
770
 
714
771
  // OpenAI API: input_tokens INCLUDES cached, output_tokens INCLUDES reasoning.
715
772
  // Normalize to Anthropic-style semantics where each field is non-overlapping.
@@ -764,6 +821,8 @@ async function parseSessionFile(filePath, snapshotSize, fm, boundary, {
764
821
  rawTokenSeen,
765
822
  firstSessionMetaSeen,
766
823
  turnContextModel,
824
+ serviceTier,
825
+ subscriptionBilling,
767
826
  prevTotal,
768
827
  prevCumulativeTotal,
769
828
  buckets,