feat(usage): add TokenTracker export models and test fixture
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
namespace ClaudeDo.Worker.Usage.TokenTracker;
|
||||
|
||||
/// <summary>One row of <c>tokentracker sessions --format json</c>. Field names mirror the
|
||||
/// export's <c>snake_case</c> keys: <c>cached_input_tokens</c> is the cache *read* count,
|
||||
/// <c>cache_creation_input_tokens</c> the write count.
|
||||
/// ⚠️ <paramref name="SessionHash"/> is <b>not</b> unique across rows — TokenTracker splits one
|
||||
/// Claude session into several rows (measured: 886 rows over 547 hashes), e.g. on a model switch.
|
||||
/// The segments are disjoint, so summing rows that share a hash is correct; counting them is not
|
||||
/// the same as counting sessions.</summary>
|
||||
public sealed record TokenTrackerSession(
|
||||
string SessionHash,
|
||||
string Source,
|
||||
string Model,
|
||||
DateTimeOffset StartedAt,
|
||||
long InputTokens,
|
||||
long OutputTokens,
|
||||
long CacheReadTokens,
|
||||
long CacheCreationTokens,
|
||||
double CostUsd,
|
||||
int Turns,
|
||||
int RetryTurns,
|
||||
bool Productive,
|
||||
bool OneShot);
|
||||
|
||||
/// <summary>A parsed export plus when we fetched it. <paramref name="FormatVersion"/> is the
|
||||
/// <c>version</c> field of the session rows — the only compatibility signal the export gives us.</summary>
|
||||
public sealed record TokenTrackerExport(
|
||||
int FormatVersion,
|
||||
IReadOnlyList<TokenTrackerSession> Sessions,
|
||||
DateTime FetchedAtUtc);
|
||||
|
||||
public sealed record TokenTrackerProbe(
|
||||
bool Installed,
|
||||
string? Version,
|
||||
bool NodeOk,
|
||||
string? NodeVersion,
|
||||
string? Error)
|
||||
{
|
||||
public static TokenTrackerProbe Unknown { get; } = new(false, null, false, null, null);
|
||||
}
|
||||
|
||||
/// <summary>Outcome of one CLI invocation. <paramref name="Ok"/> false always carries an
|
||||
/// <paramref name="Error"/> — callers turn that into state, never into an exception.</summary>
|
||||
public sealed record TokenTrackerRunResult(bool Ok, string StdOut, string? Error);
|
||||
|
||||
/// <summary>Aggregated per date/model/scope. <c>Scope</c> is <c>"claudedo"</c> or <c>"other"</c>,
|
||||
/// decided by whether the session hash belongs to one of our runs. <c>Sessions</c> replaces the
|
||||
/// old assistant-message count — it counts export <i>rows</i> (session segments), because the
|
||||
/// export has neither message granularity nor one row per session.</summary>
|
||||
public sealed record TokenTrackerModelRow(
|
||||
DateOnly Date,
|
||||
string Model,
|
||||
string Scope,
|
||||
long InputTokens,
|
||||
long OutputTokens,
|
||||
long CacheReadTokens,
|
||||
long CacheCreationTokens,
|
||||
int Sessions,
|
||||
double CostUsd);
|
||||
|
||||
/// <summary>What the export adds on top of what <c>task_runs</c> already knows about a task.</summary>
|
||||
public sealed record TokenTrackerTaskExtras(
|
||||
double CostUsd,
|
||||
int Retries,
|
||||
bool Productive,
|
||||
bool OneShot);
|
||||
@@ -0,0 +1,138 @@
|
||||
namespace ClaudeDo.Worker.Tests.Usage.TokenTracker;
|
||||
|
||||
public static class TokenTrackerFixtures
|
||||
{
|
||||
/// <summary>Session id whose hash is the fixture's first ClaudeDo session.</summary>
|
||||
public const string ClaudeDoSessionIdA = "77470328-94f7-49d3-b378-a562d1501b5f";
|
||||
|
||||
/// <summary>Session id whose hash is the fixture's second ClaudeDo session.</summary>
|
||||
public const string ClaudeDoSessionIdB = "20ebc1bc-3e6a-4c09-a4b3-19472dffa841";
|
||||
|
||||
/// <summary>Shape copied from a real `tokentracker sessions --format json` run
|
||||
/// (v0.88.4, 2026-08-24), trimmed to four rows and stripped of everything we don't read.</summary>
|
||||
public const string SessionsJson = """
|
||||
{
|
||||
"available": true,
|
||||
"sessions": [
|
||||
{
|
||||
"version": 11,
|
||||
"session_hash": "b24babcbf2b615730459773f",
|
||||
"source": "claude",
|
||||
"project_key": "ClaudeDo",
|
||||
"model": "claude-opus-5",
|
||||
"started_at": "2026-08-20T10:00:00.000Z",
|
||||
"ended_at": "2026-08-20T10:10:00.000Z",
|
||||
"turns": 4,
|
||||
"edit_turns": 2,
|
||||
"retry_turns": 1,
|
||||
"tokens": {
|
||||
"input_tokens": 100,
|
||||
"cached_input_tokens": 900,
|
||||
"cache_creation_input_tokens": 50,
|
||||
"output_tokens": 400,
|
||||
"reasoning_output_tokens": 0,
|
||||
"total_tokens": 1450
|
||||
},
|
||||
"total_tokens": 1450,
|
||||
"cost_usd": 2.5,
|
||||
"productive": true,
|
||||
"first_pass": true,
|
||||
"one_shot": true
|
||||
},
|
||||
{
|
||||
"version": 11,
|
||||
"session_hash": "3e2a08fe8f2b941817fb554f",
|
||||
"source": "claude",
|
||||
"project_key": "02f6746f-d67b-4217-a581-f2f911f6ff0d",
|
||||
"model": "claude-opus-5",
|
||||
"started_at": "2026-08-20T12:00:00.000Z",
|
||||
"ended_at": "2026-08-20T12:30:00.000Z",
|
||||
"turns": 6,
|
||||
"edit_turns": 0,
|
||||
"retry_turns": 2,
|
||||
"tokens": {
|
||||
"input_tokens": 10,
|
||||
"cached_input_tokens": 90,
|
||||
"cache_creation_input_tokens": 5,
|
||||
"output_tokens": 40,
|
||||
"reasoning_output_tokens": 0,
|
||||
"total_tokens": 145
|
||||
},
|
||||
"total_tokens": 145,
|
||||
"cost_usd": 1.0,
|
||||
"productive": false,
|
||||
"first_pass": false,
|
||||
"one_shot": false
|
||||
},
|
||||
{
|
||||
"version": 11,
|
||||
"session_hash": "ffffffffffffffffffffffff",
|
||||
"source": "claude",
|
||||
"project_key": "SomeOtherRepo",
|
||||
"model": "claude-sonnet-5",
|
||||
"started_at": "2026-08-20T14:00:00.000Z",
|
||||
"ended_at": "2026-08-20T14:05:00.000Z",
|
||||
"turns": 2,
|
||||
"edit_turns": 1,
|
||||
"retry_turns": 0,
|
||||
"tokens": {
|
||||
"input_tokens": 7,
|
||||
"cached_input_tokens": 3,
|
||||
"cache_creation_input_tokens": 1,
|
||||
"output_tokens": 9,
|
||||
"reasoning_output_tokens": 0,
|
||||
"total_tokens": 20
|
||||
},
|
||||
"total_tokens": 20,
|
||||
"cost_usd": 0.25,
|
||||
"productive": true,
|
||||
"first_pass": true,
|
||||
"one_shot": true
|
||||
},
|
||||
{
|
||||
"version": 11,
|
||||
"session_hash": "aaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"source": "claude",
|
||||
"project_key": "ClaudeDo",
|
||||
"model": "claude-opus-5",
|
||||
"started_at": "2026-08-25T10:00:00.000Z",
|
||||
"ended_at": "2026-08-25T10:20:00.000Z",
|
||||
"turns": 3,
|
||||
"edit_turns": 1,
|
||||
"retry_turns": 0,
|
||||
"tokens": {
|
||||
"input_tokens": 1000,
|
||||
"cached_input_tokens": 2000,
|
||||
"cache_creation_input_tokens": 300,
|
||||
"output_tokens": 4000,
|
||||
"reasoning_output_tokens": 0,
|
||||
"total_tokens": 7300
|
||||
},
|
||||
"total_tokens": 7300,
|
||||
"cost_usd": 99.0,
|
||||
"productive": true,
|
||||
"first_pass": true,
|
||||
"one_shot": true
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
/// <summary>Same as <see cref="SessionsJson"/> but with a future format version.</summary>
|
||||
public const string UnsupportedVersionJson = """
|
||||
{
|
||||
"available": true,
|
||||
"sessions": [
|
||||
{
|
||||
"version": 99,
|
||||
"session_hash": "b24babcbf2b615730459773f",
|
||||
"source": "claude",
|
||||
"model": "claude-opus-5",
|
||||
"started_at": "2026-08-20T10:00:00.000Z",
|
||||
"tokens": { "input_tokens": 1, "output_tokens": 2 },
|
||||
"cost_usd": 0.1
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
}
|
||||
Reference in New Issue
Block a user