Skip to content

Commit c05c9a1

Browse files
authored
fix(core): preserve harness provider token usage (#16072)
1 parent 1331cf2 commit c05c9a1

5 files changed

Lines changed: 246 additions & 25 deletions

File tree

.changeset/great-papers-shake.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@mastra/core': patch
3+
---
4+
5+
Fixed Harness token usage so provider-reported totals, reasoning tokens, and cache token fields are preserved. Fixes https://github.com/mastra-ai/mastra/issues/16055

packages/core/src/harness/display-state.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,40 @@ describe('usage_update', () => {
671671
expect(usage.totalTokens).toBe(150);
672672
});
673673

674+
it('preserves richer token usage fields from internal token counters', () => {
675+
(harness as any).tokenUsage = {
676+
promptTokens: 100,
677+
completionTokens: 50,
678+
totalTokens: 220,
679+
reasoningTokens: 70,
680+
cachedInputTokens: 25,
681+
cacheCreationInputTokens: 5,
682+
raw: { provider: 'test-provider' },
683+
};
684+
emit(harness, {
685+
type: 'usage_update',
686+
usage: {
687+
promptTokens: 100,
688+
completionTokens: 50,
689+
totalTokens: 220,
690+
reasoningTokens: 70,
691+
cachedInputTokens: 25,
692+
cacheCreationInputTokens: 5,
693+
raw: { provider: 'test-provider' },
694+
},
695+
});
696+
697+
expect(harness.getDisplayState().tokenUsage).toEqual({
698+
promptTokens: 100,
699+
completionTokens: 50,
700+
totalTokens: 220,
701+
reasoningTokens: 70,
702+
cachedInputTokens: 25,
703+
cacheCreationInputTokens: 5,
704+
raw: { provider: 'test-provider' },
705+
});
706+
});
707+
674708
it('accumulates usage across multiple updates', () => {
675709
(harness as any).tokenUsage = { promptTokens: 100, completionTokens: 50, totalTokens: 150 };
676710
emit(harness, { type: 'usage_update', usage: { promptTokens: 100, completionTokens: 50, totalTokens: 150 } });

packages/core/src/harness/harness.ts

Lines changed: 70 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,38 @@ import type {
4040
ModelAuthStatus,
4141
PermissionPolicy,
4242
PermissionRules,
43+
TokenUsage,
4344
ToolCategory,
4445
} from './types';
4546

47+
function createEmptyTokenUsage(): TokenUsage {
48+
return { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
49+
}
50+
51+
function getUsageNumber(usage: Record<string, unknown>, key: string): number | undefined {
52+
const value = usage[key];
53+
if (typeof value === 'number' && Number.isFinite(value)) {
54+
return value;
55+
}
56+
if (typeof value === 'string' && value.trim() !== '') {
57+
const numericValue = Number(value);
58+
if (Number.isFinite(numericValue)) {
59+
return numericValue;
60+
}
61+
}
62+
return undefined;
63+
}
64+
65+
function addOptionalUsageField(
66+
usage: TokenUsage,
67+
key: keyof Pick<TokenUsage, 'reasoningTokens' | 'cachedInputTokens' | 'cacheCreationInputTokens'>,
68+
value: number | undefined,
69+
): void {
70+
if (value !== undefined) {
71+
usage[key] = (usage[key] ?? 0) + value;
72+
}
73+
}
74+
4675
/**
4776
* The Harness orchestrates multiple agent modes, shared state, memory, and storage.
4877
* It's the core abstraction that a TUI (or other UI) controls.
@@ -108,11 +137,7 @@ export class Harness<TState = {}> {
108137
| ((ctx: { requestContext: RequestContext }) => Promise<MastraBrowser | undefined> | MastraBrowser | undefined)
109138
| undefined = undefined;
110139
private heartbeatTimers = new Map<string, { timer: NodeJS.Timeout; shutdown?: () => void | Promise<void> }>();
111-
private tokenUsage: { promptTokens: number; completionTokens: number; totalTokens: number } = {
112-
promptTokens: 0,
113-
completionTokens: 0,
114-
totalTokens: 0,
115-
};
140+
private tokenUsage: TokenUsage = createEmptyTokenUsage();
116141
private sessionGrantedCategories = new Set<string>();
117142
private sessionGrantedTools = new Set<string>();
118143
private displayState: HarnessDisplayState = defaultDisplayState();
@@ -731,7 +756,7 @@ export class Harness<TState = {}> {
731756
void this.setState({ currentModelId: modelId } as unknown as Partial<TState>);
732757
}
733758

734-
this.tokenUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
759+
this.tokenUsage = createEmptyTokenUsage();
735760
this.emit({ type: 'thread_created', thread });
736761

737762
return thread;
@@ -770,7 +795,7 @@ export class Harness<TState = {}> {
770795
// Lock release failed; proceed with state cleanup regardless
771796
}
772797
this.currentThreadId = null;
773-
this.tokenUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
798+
this.tokenUsage = createEmptyTokenUsage();
774799
}
775800

776801
this.emit({ type: 'thread_deleted', threadId });
@@ -844,7 +869,7 @@ export class Harness<TState = {}> {
844869

845870
this.currentThreadId = clonedThread.id;
846871
await this.loadThreadMetadata();
847-
this.tokenUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
872+
this.tokenUsage = createEmptyTokenUsage();
848873
this.emit({ type: 'thread_created', thread: clonedThread });
849874

850875
return clonedThread;
@@ -957,7 +982,7 @@ export class Harness<TState = {}> {
957982

958983
private async loadThreadMetadata(): Promise<void> {
959984
if (!this.currentThreadId || !this.config.storage) {
960-
this.tokenUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
985+
this.tokenUsage = createEmptyTokenUsage();
961986
return;
962987
}
963988

@@ -969,12 +994,14 @@ export class Harness<TState = {}> {
969994
const savedUsage = thread?.metadata?.tokenUsage as typeof this.tokenUsage | undefined;
970995
if (savedUsage) {
971996
this.tokenUsage = {
997+
...createEmptyTokenUsage(),
998+
...savedUsage,
972999
promptTokens: savedUsage.promptTokens ?? 0,
9731000
completionTokens: savedUsage.completionTokens ?? 0,
9741001
totalTokens: savedUsage.totalTokens ?? 0,
9751002
};
9761003
} else {
977-
this.tokenUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
1004+
this.tokenUsage = createEmptyTokenUsage();
9781005
}
9791006

9801007
const meta = thread?.metadata as Record<string, unknown> | undefined;
@@ -1040,7 +1067,7 @@ export class Harness<TState = {}> {
10401067
}
10411068
}
10421069
} catch {
1043-
this.tokenUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
1070+
this.tokenUsage = createEmptyTokenUsage();
10441071
}
10451072
}
10461073

@@ -1966,16 +1993,40 @@ export class Harness<TState = {}> {
19661993
case 'step-finish': {
19671994
const usage = chunk.payload?.output?.usage;
19681995
if (usage) {
1969-
const promptTokens = usage.promptTokens ?? usage.inputTokens ?? 0;
1970-
const completionTokens = usage.completionTokens ?? usage.outputTokens ?? 0;
1971-
const totalTokens = promptTokens + completionTokens;
1996+
const usageRecord = usage as Record<string, unknown>;
1997+
const promptTokens =
1998+
getUsageNumber(usageRecord, 'promptTokens') ?? getUsageNumber(usageRecord, 'inputTokens') ?? 0;
1999+
const completionTokens =
2000+
getUsageNumber(usageRecord, 'completionTokens') ?? getUsageNumber(usageRecord, 'outputTokens') ?? 0;
2001+
const totalTokens = getUsageNumber(usageRecord, 'totalTokens') ?? promptTokens + completionTokens;
2002+
const stepUsage: TokenUsage = {
2003+
promptTokens,
2004+
completionTokens,
2005+
totalTokens,
2006+
};
2007+
addOptionalUsageField(stepUsage, 'reasoningTokens', getUsageNumber(usageRecord, 'reasoningTokens'));
2008+
addOptionalUsageField(stepUsage, 'cachedInputTokens', getUsageNumber(usageRecord, 'cachedInputTokens'));
2009+
addOptionalUsageField(
2010+
stepUsage,
2011+
'cacheCreationInputTokens',
2012+
getUsageNumber(usageRecord, 'cacheCreationInputTokens'),
2013+
);
2014+
if (usageRecord.raw !== undefined) {
2015+
stepUsage.raw = usageRecord.raw;
2016+
}
19722017

19732018
this.tokenUsage.promptTokens += promptTokens;
19742019
this.tokenUsage.completionTokens += completionTokens;
19752020
this.tokenUsage.totalTokens += totalTokens;
2021+
addOptionalUsageField(this.tokenUsage, 'reasoningTokens', stepUsage.reasoningTokens);
2022+
addOptionalUsageField(this.tokenUsage, 'cachedInputTokens', stepUsage.cachedInputTokens);
2023+
addOptionalUsageField(this.tokenUsage, 'cacheCreationInputTokens', stepUsage.cacheCreationInputTokens);
2024+
if (stepUsage.raw !== undefined) {
2025+
this.tokenUsage.raw = stepUsage.raw;
2026+
}
19762027

19772028
this.persistTokenUsage().catch(() => {});
1978-
this.emit({ type: 'usage_update', usage: { promptTokens, completionTokens, totalTokens } });
2029+
this.emit({ type: 'usage_update', usage: stepUsage });
19792030
}
19802031
break;
19812032
}
@@ -2959,11 +3010,7 @@ export class Harness<TState = {}> {
29593010

29603011
// ── Token usage ────────────────────────────────────────────────────
29613012
case 'usage_update':
2962-
ds.tokenUsage = {
2963-
promptTokens: this.tokenUsage.promptTokens,
2964-
completionTokens: this.tokenUsage.completionTokens,
2965-
totalTokens: this.tokenUsage.totalTokens,
2966-
};
3013+
ds.tokenUsage = { ...this.tokenUsage };
29673014
break;
29683015

29693016
// ── Tasks ──────────────────────────────────────────────────────────
@@ -2980,13 +3027,13 @@ export class Harness<TState = {}> {
29803027

29813028
case 'thread_created':
29823029
this.resetThreadDisplayState();
2983-
ds.tokenUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
3030+
ds.tokenUsage = createEmptyTokenUsage();
29843031
break;
29853032

29863033
case 'thread_deleted':
29873034
if (!this.currentThreadId) {
29883035
this.resetThreadDisplayState();
2989-
ds.tokenUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
3036+
ds.tokenUsage = createEmptyTokenUsage();
29903037
}
29913038
break;
29923039

@@ -3164,7 +3211,7 @@ export class Harness<TState = {}> {
31643211
// Token Usage
31653212
// ===========================================================================
31663213

3167-
getTokenUsage(): { promptTokens: number; completionTokens: number; totalTokens: number } {
3214+
getTokenUsage(): TokenUsage {
31683215
return { ...this.tokenUsage };
31693216
}
31703217

0 commit comments

Comments
 (0)