Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/three-islands-pump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@openai/agents-extensions': patch
'@openai/agents-openai': patch
'@openai/agents-core': patch
---

feat: #678 Add a list of per-request usage data to Usage
2 changes: 1 addition & 1 deletion packages/agents-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ export type {
StreamEventResponseStarted,
StreamEventGenericItem,
} from './types';
export { Usage } from './usage';
export { RequestUsage, Usage } from './usage';
export type { Session, SessionInputCallback } from './memory/session';
export { MemorySession } from './memory/memorySession';

Expand Down
40 changes: 35 additions & 5 deletions packages/agents-core/src/runState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,22 @@ const SerializedSpan: z.ZodType<SerializedSpanType> = serializedSpanBase.extend(
},
);

const requestUsageSchema = z.object({
inputTokens: z.number(),
outputTokens: z.number(),
totalTokens: z.number(),
inputTokensDetails: z.record(z.string(), z.number()).optional(),
outputTokensDetails: z.record(z.string(), z.number()).optional(),
});

const usageSchema = z.object({
requests: z.number(),
inputTokens: z.number(),
outputTokens: z.number(),
totalTokens: z.number(),
inputTokensDetails: z.array(z.record(z.string(), z.number())).optional(),
outputTokensDetails: z.array(z.record(z.string(), z.number())).optional(),
requestUsageEntries: z.array(requestUsageSchema).optional(),
});

const modelResponseSchema = z.object({
Expand Down Expand Up @@ -287,6 +298,13 @@ export class RunState<TContext, TAgent extends Agent<any, any>> {
* Run context tracking approvals, usage, and other metadata.
*/
public _context: RunContext<TContext>;

/**
* The usage aggregated for this run. This includes per-request breakdowns when available.
*/
get usage(): Usage {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

currently there is no public property/method to access the usage data of an agent run. so, i've added this method this time

return this._context.usage;
}
/**
* Tracks what tools each agent has used.
*/
Expand Down Expand Up @@ -440,6 +458,22 @@ export class RunState<TContext, TAgent extends Agent<any, any>> {
inputTokens: response.usage.inputTokens,
outputTokens: response.usage.outputTokens,
totalTokens: response.usage.totalTokens,
inputTokensDetails: response.usage.inputTokensDetails,
outputTokensDetails: response.usage.outputTokensDetails,
...(response.usage.requestUsageEntries &&
response.usage.requestUsageEntries.length > 0
? {
requestUsageEntries: response.usage.requestUsageEntries.map(
(entry) => ({
inputTokens: entry.inputTokens,
outputTokens: entry.outputTokens,
totalTokens: entry.totalTokens,
inputTokensDetails: entry.inputTokensDetails,
outputTokensDetails: entry.outputTokensDetails,
}),
),
}
: {}),
},
output: response.output as any,
responseId: response.responseId,
Expand Down Expand Up @@ -683,11 +717,7 @@ export function deserializeSpan(
export function deserializeModelResponse(
serializedModelResponse: z.infer<typeof modelResponseSchema>,
): ModelResponse {
const usage = new Usage();
usage.requests = serializedModelResponse.usage.requests;
usage.inputTokens = serializedModelResponse.usage.inputTokens;
usage.outputTokens = serializedModelResponse.usage.outputTokens;
usage.totalTokens = serializedModelResponse.usage.totalTokens;
const usage = new Usage(serializedModelResponse.usage);

return {
usage,
Expand Down
25 changes: 23 additions & 2 deletions packages/agents-core/src/types/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -743,15 +743,36 @@ export type ModelItem = z.infer<typeof ModelItem>;
// Meta data types
// ----------------------------

export const UsageData = z.object({
requests: z.number().optional(),
export const RequestUsageData = z.object({
inputTokens: z.number(),
outputTokens: z.number(),
totalTokens: z.number(),
inputTokensDetails: z.record(z.string(), z.number()).optional(),
outputTokensDetails: z.record(z.string(), z.number()).optional(),
});

export type RequestUsageData = z.infer<typeof RequestUsageData>;

export const UsageData = z.object({
requests: z.number().optional(),
inputTokens: z.number(),
outputTokens: z.number(),
totalTokens: z.number(),
inputTokensDetails: z
.union([
z.record(z.string(), z.number()),
z.array(z.record(z.string(), z.number())),
])
.optional(),
outputTokensDetails: z
.union([
z.record(z.string(), z.number()),
z.array(z.record(z.string(), z.number())),
])
.optional(),
requestUsageEntries: z.array(RequestUsageData).optional(),
});

export type UsageData = z.infer<typeof UsageData>;

// ----------------------------
Expand Down
155 changes: 139 additions & 16 deletions packages/agents-core/src/usage.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,80 @@
import { UsageData } from './types/protocol';
import { RequestUsageData, UsageData } from './types/protocol';

type UsageInput = Partial<
UsageData & {
type RequestUsageInput = Partial<
RequestUsageData & {
input_tokens: number;
output_tokens: number;
total_tokens: number;
input_tokens_details: object;
output_tokens_details: object;
}
> & { requests?: number };
>;

type UsageInput = Partial<
UsageData & {
input_tokens: number;
output_tokens: number;
total_tokens: number;
input_tokens_details:
| Record<string, number>
| Array<Record<string, number>>
| object;
output_tokens_details:
| Record<string, number>
| Array<Record<string, number>>
| object;
request_usage_entries: RequestUsageInput[];
}
> & { requests?: number; requestUsageEntries?: RequestUsageInput[] };

/**
* Usage details for a single API request.
*/
export class RequestUsage {
/**
* The number of input tokens used for this request.
*/
public inputTokens: number;

/**
* The number of output tokens used for this request.
*/
public outputTokens: number;

/**
* The total number of tokens sent and received for this request.
*/
public totalTokens: number;

/**
* Details about the input tokens used for this request.
*/
public inputTokensDetails: Record<string, number>;

/**
* Details about the output tokens used for this request.
*/
public outputTokensDetails: Record<string, number>;

constructor(input?: RequestUsageInput) {
this.inputTokens = input?.inputTokens ?? input?.input_tokens ?? 0;
this.outputTokens = input?.outputTokens ?? input?.output_tokens ?? 0;
this.totalTokens =
input?.totalTokens ??
input?.total_tokens ??
this.inputTokens + this.outputTokens;
const inputTokensDetails =
input?.inputTokensDetails ?? input?.input_tokens_details;
this.inputTokensDetails = inputTokensDetails
? (inputTokensDetails as Record<string, number>)
: {};
const outputTokensDetails =
input?.outputTokensDetails ?? input?.output_tokens_details;
this.outputTokensDetails = outputTokensDetails
? (outputTokensDetails as Record<string, number>)
: {};
}
}

/**
* Tracks token usage and request counts for an agent run.
Expand Down Expand Up @@ -44,6 +110,11 @@ export class Usage {
*/
public outputTokensDetails: Array<Record<string, number>> = [];

/**
* List of per-request usage entries for detailed cost calculations.
*/
public requestUsageEntries: RequestUsage[] | undefined;

constructor(input?: UsageInput) {
if (typeof input === 'undefined') {
this.requests = 0;
Expand All @@ -52,29 +123,58 @@ export class Usage {
this.totalTokens = 0;
this.inputTokensDetails = [];
this.outputTokensDetails = [];
this.requestUsageEntries = undefined;
} else {
this.requests = input?.requests ?? 1;
this.inputTokens = input?.inputTokens ?? input?.input_tokens ?? 0;
this.outputTokens = input?.outputTokens ?? input?.output_tokens ?? 0;
this.totalTokens = input?.totalTokens ?? input?.total_tokens ?? 0;
this.totalTokens =
input?.totalTokens ??
input?.total_tokens ??
this.inputTokens + this.outputTokens;
const inputTokensDetails =
input?.inputTokensDetails ?? input?.input_tokens_details;
this.inputTokensDetails = inputTokensDetails
? [inputTokensDetails as Record<string, number>]
: [];
if (Array.isArray(inputTokensDetails)) {
this.inputTokensDetails = inputTokensDetails as Array<
Record<string, number>
>;
} else {
this.inputTokensDetails = inputTokensDetails
? [inputTokensDetails as Record<string, number>]
: [];
}
const outputTokensDetails =
input?.outputTokensDetails ?? input?.output_tokens_details;
this.outputTokensDetails = outputTokensDetails
? [outputTokensDetails as Record<string, number>]
: [];
if (Array.isArray(outputTokensDetails)) {
this.outputTokensDetails = outputTokensDetails as Array<
Record<string, number>
>;
} else {
this.outputTokensDetails = outputTokensDetails
? [outputTokensDetails as Record<string, number>]
: [];
}

const requestUsageEntries =
input?.requestUsageEntries ?? input?.request_usage_entries;
const normalizedRequestUsageEntries = Array.isArray(requestUsageEntries)
? requestUsageEntries.map((entry) =>
entry instanceof RequestUsage ? entry : new RequestUsage(entry),
)
: undefined;
this.requestUsageEntries =
normalizedRequestUsageEntries &&
normalizedRequestUsageEntries.length > 0
? normalizedRequestUsageEntries
: undefined;
}
}

add(newUsage: Usage) {
this.requests += newUsage.requests;
this.inputTokens += newUsage.inputTokens;
this.outputTokens += newUsage.outputTokens;
this.totalTokens += newUsage.totalTokens;
this.requests += newUsage.requests ?? 0;
this.inputTokens += newUsage.inputTokens ?? 0;
this.outputTokens += newUsage.outputTokens ?? 0;
this.totalTokens += newUsage.totalTokens ?? 0;
if (newUsage.inputTokensDetails) {
// The type does not allow undefined, but it could happen runtime
this.inputTokensDetails.push(...newUsage.inputTokensDetails);
Expand All @@ -83,7 +183,30 @@ export class Usage {
// The type does not allow undefined, but it could happen runtime
this.outputTokensDetails.push(...newUsage.outputTokensDetails);
}

if (
Array.isArray(newUsage.requestUsageEntries) &&
newUsage.requestUsageEntries.length > 0
) {
this.requestUsageEntries ??= [];
this.requestUsageEntries.push(
...newUsage.requestUsageEntries.map((entry) =>
entry instanceof RequestUsage ? entry : new RequestUsage(entry),
),
);
} else if (newUsage.requests === 1 && newUsage.totalTokens > 0) {
this.requestUsageEntries ??= [];
this.requestUsageEntries.push(
new RequestUsage({
inputTokens: newUsage.inputTokens,
outputTokens: newUsage.outputTokens,
totalTokens: newUsage.totalTokens,
inputTokensDetails: newUsage.inputTokensDetails?.[0],
outputTokensDetails: newUsage.outputTokensDetails?.[0],
}),
);
}
}
}

export { UsageData };
export { RequestUsageData, UsageData };
34 changes: 34 additions & 0 deletions packages/agents-core/test/run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,40 @@ describe('Runner.run', () => {
expectTypeOf(result.finalOutput).toEqualTypeOf<string | undefined>();
});

it('exposes aggregated usage on run results', async () => {
const model = new FakeModel([
{
output: [fakeModelMessage('hi there')],
usage: new Usage({
requests: 1,
inputTokens: 2,
outputTokens: 3,
totalTokens: 5,
}),
responseId: 'usage-res',
},
]);
const agent = new Agent({
name: 'UsageAgent',
model,
});

const result = await run(agent, 'ping');

expect(result.state.usage.inputTokens).toBe(2);
expect(result.state.usage.outputTokens).toBe(3);
expect(result.state.usage.totalTokens).toBe(5);
expect(result.state.usage.requestUsageEntries).toEqual([
{
inputTokens: 2,
outputTokens: 3,
totalTokens: 5,
inputTokensDetails: {},
outputTokensDetails: {},
},
]);
});

it('sholuld handle structured output', async () => {
const fakeModel = new FakeModel([
{
Expand Down
Loading