@agentionai/agents 1.1.0 → 1.3.0

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.
@@ -12,10 +12,60 @@ export interface BaseAgentConfig extends CommonAgentConfig {
12
12
  vendor: AgentVendor;
13
13
  vendorConfig?: VendorSpecificConfig;
14
14
  }
15
+ /**
16
+ * Token counts and timing for one or more provider API calls.
17
+ *
18
+ * Counts are always present. Timing fields are optional because their
19
+ * availability depends on the provider and on whether the call was streamed:
20
+ *
21
+ * - `totalMs` is always measured (wall clock around the API call).
22
+ * - `timeToFirstTokenMs` / `generationMs` require either a streamed response
23
+ * (measured locally) or a provider that reports its own timings
24
+ * (Ollama, llama.cpp).
25
+ *
26
+ * When usage from several calls is folded together (a tool-use loop, for
27
+ * example) the counts and durations are summed and the rates recomputed from
28
+ * those totals.
29
+ */
15
30
  export type TokenUsage = {
16
31
  input_tokens: number;
17
32
  output_tokens: number;
18
33
  total_tokens: number;
34
+ /**
35
+ * Reasoning/thinking tokens, where the provider reports them separately
36
+ * (OpenAI, Gemini). These are a subset of `output_tokens`, not an addition
37
+ * to them. Undefined when the provider does not break them out — Anthropic,
38
+ * for instance, folds thinking tokens into `output_tokens`.
39
+ */
40
+ reasoning_tokens?: number;
41
+ /**
42
+ * Milliseconds from sending the request to the first token of the response —
43
+ * prompt upload plus prompt processing.
44
+ */
45
+ timeToFirstTokenMs?: number;
46
+ /** Milliseconds spent generating the response after the first token. */
47
+ generationMs?: number;
48
+ /** Total wall-clock milliseconds spent in provider API calls. */
49
+ totalMs?: number;
50
+ /**
51
+ * Prompt processing throughput: `input_tokens` over `timeToFirstTokenMs`.
52
+ * Undefined when the time to first token is unknown.
53
+ */
54
+ inputTokensPerSecond?: number;
55
+ /**
56
+ * Generation throughput: `output_tokens` over `generationMs`. Falls back to
57
+ * `totalMs` when the first-token time is unknown (an unstreamed call), in
58
+ * which case it is an end-to-end rate rather than a pure generation rate.
59
+ *
60
+ * Accurate wherever thinking is streamed, because the first thinking chunk
61
+ * starts the generation window — verified on Anthropic extended thinking and
62
+ * on DeepSeek via OpenRouter. It over-reports on OpenAI's Responses API,
63
+ * which does not stream raw reasoning: there the thinking finishes before the
64
+ * first visible token, so it lands inside `timeToFirstTokenMs` while its
65
+ * tokens still count toward `output_tokens`. For the visible-output rate on
66
+ * that path, divide `output_tokens - reasoning_tokens` by `generationMs`.
67
+ */
68
+ outputTokensPerSecond?: number;
19
69
  };
20
70
  /**
21
71
  * The base agent is what the other agents are inheriting from
@@ -33,6 +83,16 @@ export declare abstract class BaseAgent<TInput = unknown, TOutput = unknown> ext
33
83
  protected vendor: AgentVendor;
34
84
  /** The model identifier for this agent */
35
85
  protected model: string;
86
+ /**
87
+ * Token counts and timings for the most recent `execute()` call, summed
88
+ * across every provider API call it made (including tool-use follow-ups).
89
+ * Reset at the start of each execution.
90
+ */
91
+ lastTokenUsage?: TokenUsage;
92
+ /** Start of the API call currently in flight, set by `startTurnTimer()`. */
93
+ private turnStartedAt?;
94
+ /** First-token timestamp of the call in flight, set by `markFirstToken()`. */
95
+ private turnFirstTokenAt?;
36
96
  /**
37
97
  * An Agent is the primary LLM entity.
38
98
  *
@@ -78,5 +138,35 @@ export declare abstract class BaseAgent<TInput = unknown, TOutput = unknown> ext
78
138
  getTools(): Tool<unknown>[];
79
139
  clearHistory(): void;
80
140
  protected abstract parseUsage(input: unknown): TokenUsage;
141
+ /**
142
+ * Clear accumulated usage. Called at the start of every `execute()` /
143
+ * `executeStream()` so `lastTokenUsage` describes a single execution.
144
+ */
145
+ protected resetTokenUsage(): void;
146
+ /**
147
+ * Mark the moment a provider API call is sent. Call this immediately before
148
+ * every request so the usage folded in afterwards can be timed.
149
+ */
150
+ protected startTurnTimer(): void;
151
+ /**
152
+ * Mark the arrival of the first token of a streamed response. Subsequent
153
+ * calls within the same turn are ignored, so it is safe to call on every
154
+ * chunk.
155
+ */
156
+ protected markFirstToken(): void;
157
+ /**
158
+ * Fold one API call's usage into `lastTokenUsage`, filling in any timings
159
+ * the provider did not report from the local turn timer.
160
+ *
161
+ * @returns the usage for this single call, with timings and rates filled in —
162
+ * `lastTokenUsage` holds the running total across calls.
163
+ */
164
+ protected accumulateUsage(usage: TokenUsage): TokenUsage;
165
+ /**
166
+ * Add locally measured timings to a provider-parsed usage object. Timings the
167
+ * provider reported itself (Ollama, llama.cpp) are kept as-is — they exclude
168
+ * network overhead and are more accurate than anything measured here.
169
+ */
170
+ private applyTurnTiming;
81
171
  }
82
172
  //# sourceMappingURL=BaseAgent.d.ts.map
@@ -120,6 +120,113 @@ class BaseAgent extends events_1.default {
120
120
  clearHistory() {
121
121
  this.history.clear();
122
122
  }
123
+ /**
124
+ * Clear accumulated usage. Called at the start of every `execute()` /
125
+ * `executeStream()` so `lastTokenUsage` describes a single execution.
126
+ */
127
+ resetTokenUsage() {
128
+ this.lastTokenUsage = undefined;
129
+ this.turnStartedAt = undefined;
130
+ this.turnFirstTokenAt = undefined;
131
+ }
132
+ /**
133
+ * Mark the moment a provider API call is sent. Call this immediately before
134
+ * every request so the usage folded in afterwards can be timed.
135
+ */
136
+ startTurnTimer() {
137
+ this.turnStartedAt = Date.now();
138
+ this.turnFirstTokenAt = undefined;
139
+ }
140
+ /**
141
+ * Mark the arrival of the first token of a streamed response. Subsequent
142
+ * calls within the same turn are ignored, so it is safe to call on every
143
+ * chunk.
144
+ */
145
+ markFirstToken() {
146
+ if (this.turnStartedAt !== undefined &&
147
+ this.turnFirstTokenAt === undefined) {
148
+ this.turnFirstTokenAt = Date.now();
149
+ }
150
+ }
151
+ /**
152
+ * Fold one API call's usage into `lastTokenUsage`, filling in any timings
153
+ * the provider did not report from the local turn timer.
154
+ *
155
+ * @returns the usage for this single call, with timings and rates filled in —
156
+ * `lastTokenUsage` holds the running total across calls.
157
+ */
158
+ accumulateUsage(usage) {
159
+ const timed = this.applyTurnTiming(usage);
160
+ const previous = this.lastTokenUsage;
161
+ const merged = previous
162
+ ? {
163
+ input_tokens: previous.input_tokens + timed.input_tokens,
164
+ output_tokens: previous.output_tokens + timed.output_tokens,
165
+ total_tokens: previous.total_tokens + timed.total_tokens,
166
+ reasoning_tokens: sumOptional(previous.reasoning_tokens, timed.reasoning_tokens),
167
+ timeToFirstTokenMs: sumOptional(previous.timeToFirstTokenMs, timed.timeToFirstTokenMs),
168
+ generationMs: sumOptional(previous.generationMs, timed.generationMs),
169
+ totalMs: sumOptional(previous.totalMs, timed.totalMs),
170
+ }
171
+ : { ...timed };
172
+ this.lastTokenUsage = withThroughput(merged);
173
+ this.turnStartedAt = undefined;
174
+ this.turnFirstTokenAt = undefined;
175
+ return withThroughput(timed);
176
+ }
177
+ /**
178
+ * Add locally measured timings to a provider-parsed usage object. Timings the
179
+ * provider reported itself (Ollama, llama.cpp) are kept as-is — they exclude
180
+ * network overhead and are more accurate than anything measured here.
181
+ */
182
+ applyTurnTiming(usage) {
183
+ if (this.turnStartedAt === undefined)
184
+ return usage;
185
+ const now = Date.now();
186
+ const timed = { ...usage };
187
+ timed.totalMs ?? (timed.totalMs = now - this.turnStartedAt);
188
+ if (this.turnFirstTokenAt !== undefined) {
189
+ timed.timeToFirstTokenMs ?? (timed.timeToFirstTokenMs = this.turnFirstTokenAt - this.turnStartedAt);
190
+ timed.generationMs ?? (timed.generationMs = now - this.turnFirstTokenAt);
191
+ }
192
+ return timed;
193
+ }
123
194
  }
124
195
  exports.BaseAgent = BaseAgent;
196
+ /**
197
+ * Add two values that may each be undefined, returning undefined only when
198
+ * neither side has a value (so "not reported" never reads as zero).
199
+ */
200
+ function sumOptional(a, b) {
201
+ if (a === undefined)
202
+ return b;
203
+ if (b === undefined)
204
+ return a;
205
+ return a + b;
206
+ }
207
+ /**
208
+ * Derive tokens-per-second rates from the counts and durations on a usage
209
+ * object. Recomputed from totals rather than averaged, so folding several calls
210
+ * together stays correct.
211
+ *
212
+ * Fields that stayed unknown are dropped rather than left as explicit
213
+ * `undefined`, keeping serialized usage free of empty keys.
214
+ */
215
+ function withThroughput(usage) {
216
+ const result = { ...usage };
217
+ if (result.timeToFirstTokenMs) {
218
+ result.inputTokensPerSecond =
219
+ result.input_tokens / (result.timeToFirstTokenMs / 1000);
220
+ }
221
+ const outputWindowMs = result.generationMs || result.totalMs;
222
+ if (outputWindowMs) {
223
+ result.outputTokensPerSecond =
224
+ result.output_tokens / (outputWindowMs / 1000);
225
+ }
226
+ for (const key of Object.keys(result)) {
227
+ if (result[key] === undefined)
228
+ delete result[key];
229
+ }
230
+ return result;
231
+ }
125
232
  //# sourceMappingURL=BaseAgent.js.map
@@ -49,8 +49,6 @@ type AgentConfig = BaseAgentConfig & {
49
49
  export declare class ClaudeAgent extends BaseAgent {
50
50
  private client;
51
51
  protected config: Partial<AgentConfig>;
52
- /** Token usage from the last execution (for metrics tracking) */
53
- lastTokenUsage?: TokenUsage;
54
52
  /** Current visualization event ID for tracking */
55
53
  private vizEventId?;
56
54
  /** Count of tool calls in current execution */
@@ -43,7 +43,7 @@ class ClaudeAgent extends BaseAgent_1.BaseAgent {
43
43
  ? { authToken: config.apiKey }
44
44
  : { apiKey: config.apiKey });
45
45
  this.config = {
46
- model: config.model || "claude-3-5-haiku-latest",
46
+ model: config.model || "claude-haiku-4-5",
47
47
  maxTokens: config.maxTokens || 1024,
48
48
  disableParallelToolUse,
49
49
  metadata,
@@ -109,7 +109,7 @@ class ClaudeAgent extends BaseAgent_1.BaseAgent {
109
109
  async execute(input) {
110
110
  this.emit(AgentEvent_1.AgentEvent.BEFORE_EXECUTE, input);
111
111
  // Reset token usage for this execution
112
- this.lastTokenUsage = undefined;
112
+ this.resetTokenUsage();
113
113
  this.currentToolCallCount = 0;
114
114
  // Normalise input to a display string for viz reporting
115
115
  const inputPreview = typeof input === "string" ? input : JSON.stringify(input);
@@ -135,6 +135,7 @@ class ClaudeAgent extends BaseAgent_1.BaseAgent {
135
135
  // mid-loop. endExecution() in the finally block enforces limits once.
136
136
  this.history.beginExecution();
137
137
  try {
138
+ this.startTurnTimer();
138
139
  const response = (await this.client.messages.create(this.buildMessageParams()));
139
140
  this.emit(AgentEvent_1.AgentEvent.AFTER_EXECUTE, response);
140
141
  return await this.handleResponse(response);
@@ -167,16 +168,8 @@ class ClaudeAgent extends BaseAgent_1.BaseAgent {
167
168
  }
168
169
  }
169
170
  async handleResponse(response) {
170
- const usage = this.parseUsage(response.usage);
171
171
  // Store token usage for metrics tracking
172
- if (this.lastTokenUsage) {
173
- this.lastTokenUsage.input_tokens += usage.input_tokens;
174
- this.lastTokenUsage.output_tokens += usage.output_tokens;
175
- this.lastTokenUsage.total_tokens += usage.total_tokens;
176
- }
177
- else {
178
- this.lastTokenUsage = { ...usage };
179
- }
172
+ const usage = this.accumulateUsage(this.parseUsage(response.usage));
180
173
  if (response.stop_reason === "max_tokens") {
181
174
  const error = new AgentError_1.MaxTokensExceededError("Response exceeded maximum token limit", this.config.maxTokens || 1024);
182
175
  this.emit(AgentEvent_1.AgentEvent.MAX_TOKENS_EXCEEDED, error);
@@ -232,6 +225,7 @@ class ClaudeAgent extends BaseAgent_1.BaseAgent {
232
225
  this.addMessageToHistory("user", toolResults);
233
226
  // Continue conversation with tool results
234
227
  try {
228
+ this.startTurnTimer();
235
229
  const newResponse = (await this.client.messages.create(this.buildMessageParams()));
236
230
  this.emit(AgentEvent_1.AgentEvent.AFTER_EXECUTE, newResponse);
237
231
  return this.handleResponse(newResponse);
@@ -329,7 +323,7 @@ class ClaudeAgent extends BaseAgent_1.BaseAgent {
329
323
  */
330
324
  async *executeStream(input) {
331
325
  this.emit(AgentEvent_1.AgentEvent.BEFORE_EXECUTE, input);
332
- this.lastTokenUsage = undefined;
326
+ this.resetTokenUsage();
333
327
  this.currentToolCallCount = 0;
334
328
  const inputPreview = typeof input === "string" ? input : JSON.stringify(input);
335
329
  if (VizConfig_1.vizConfig.isEnabled()) {
@@ -379,6 +373,7 @@ class ClaudeAgent extends BaseAgent_1.BaseAgent {
379
373
  }
380
374
  }
381
375
  async *streamTurn() {
376
+ this.startTurnTimer();
382
377
  const stream = await this.client.messages.create({
383
378
  ...this.buildMessageParams(),
384
379
  stream: true,
@@ -416,6 +411,9 @@ class ClaudeAgent extends BaseAgent_1.BaseAgent {
416
411
  }
417
412
  }
418
413
  if (event.type === "content_block_delta") {
414
+ // First generated content of the turn — thinking counts, since it is
415
+ // generation time either way.
416
+ this.markFirstToken();
419
417
  const e = event;
420
418
  const delta = e.delta;
421
419
  const acc = blocks.get(e.index);
@@ -442,19 +440,11 @@ class ClaudeAgent extends BaseAgent_1.BaseAgent {
442
440
  }
443
441
  }
444
442
  }
445
- const usage = {
443
+ this.accumulateUsage({
446
444
  input_tokens: inputTokens,
447
445
  output_tokens: outputTokens,
448
446
  total_tokens: inputTokens + outputTokens,
449
- };
450
- if (this.lastTokenUsage) {
451
- this.lastTokenUsage.input_tokens += usage.input_tokens;
452
- this.lastTokenUsage.output_tokens += usage.output_tokens;
453
- this.lastTokenUsage.total_tokens += usage.total_tokens;
454
- }
455
- else {
456
- this.lastTokenUsage = { ...usage };
457
- }
447
+ });
458
448
  if (stopReason === "max_tokens") {
459
449
  const error = new AgentError_1.MaxTokensExceededError("Response exceeded maximum token limit", this.config.maxTokens || 1024);
460
450
  this.emit(AgentEvent_1.AgentEvent.MAX_TOKENS_EXCEEDED, error);
@@ -29,8 +29,6 @@ export declare class GeminiAgent extends BaseAgent {
29
29
  private client;
30
30
  private generativeModel;
31
31
  protected config: Partial<AgentConfig>;
32
- /** Token usage from the last execution (for metrics tracking) */
33
- lastTokenUsage?: TokenUsage;
34
32
  /** Current visualization event ID for tracking */
35
33
  private vizEventId?;
36
34
  /** Count of tool calls in current execution */
@@ -57,6 +55,7 @@ export declare class GeminiAgent extends BaseAgent {
57
55
  promptTokenCount?: number;
58
56
  candidatesTokenCount?: number;
59
57
  totalTokenCount?: number;
58
+ thoughtsTokenCount?: number;
60
59
  }): TokenUsage;
61
60
  }
62
61
  export {};
@@ -35,7 +35,7 @@ class GeminiAgent extends BaseAgent_1.BaseAgent {
35
35
  const responseMimeType = config.responseMimeType ?? vendorConfig.responseMimeType;
36
36
  const responseSchema = config.responseSchema ?? vendorConfig.responseSchema;
37
37
  this.config = {
38
- model: config.model || "gemini-2.0-flash",
38
+ model: config.model || "gemini-flash-latest",
39
39
  maxTokens: config.maxTokens || 1024,
40
40
  apiKey: config.apiKey,
41
41
  temperature: config.temperature,
@@ -164,7 +164,7 @@ class GeminiAgent extends BaseAgent_1.BaseAgent {
164
164
  async execute(input) {
165
165
  this.emit(AgentEvent_1.AgentEvent.BEFORE_EXECUTE, input);
166
166
  // Reset token usage for this execution
167
- this.lastTokenUsage = undefined;
167
+ this.resetTokenUsage();
168
168
  this.currentToolCallCount = 0;
169
169
  const inputPreview = typeof input === "string" ? input : JSON.stringify(input);
170
170
  // Start visualization reporting
@@ -192,6 +192,7 @@ class GeminiAgent extends BaseAgent_1.BaseAgent {
192
192
  const contents = transformers_1.geminiTransformer.toProvider(this.history.getEntries());
193
193
  const systemMessage = this.history.getSystemMessage();
194
194
  const tools = this.getToolDefinitionsForGemini();
195
+ this.startTurnTimer();
195
196
  const response = await this.generativeModel.generateContent({
196
197
  contents,
197
198
  systemInstruction: systemMessage,
@@ -241,15 +242,7 @@ class GeminiAgent extends BaseAgent_1.BaseAgent {
241
242
  const result = response.response;
242
243
  // Parse and track usage
243
244
  if (result.usageMetadata) {
244
- const usage = this.parseUsage(result.usageMetadata);
245
- if (this.lastTokenUsage) {
246
- this.lastTokenUsage.input_tokens += usage.input_tokens;
247
- this.lastTokenUsage.output_tokens += usage.output_tokens;
248
- this.lastTokenUsage.total_tokens += usage.total_tokens;
249
- }
250
- else {
251
- this.lastTokenUsage = { ...usage };
252
- }
245
+ this.accumulateUsage(this.parseUsage(result.usageMetadata));
253
246
  }
254
247
  // Check for finish reason
255
248
  const candidate = result.candidates?.[0];
@@ -309,6 +302,7 @@ class GeminiAgent extends BaseAgent_1.BaseAgent {
309
302
  const newContents = transformers_1.geminiTransformer.toProvider(this.history.getEntries());
310
303
  const systemMessage = this.history.getSystemMessage();
311
304
  const tools = this.getToolDefinitionsForGemini();
305
+ this.startTurnTimer();
312
306
  const newResponse = await this.generativeModel.generateContent({
313
307
  contents: newContents,
314
308
  systemInstruction: systemMessage,
@@ -400,10 +394,17 @@ class GeminiAgent extends BaseAgent_1.BaseAgent {
400
394
  return results;
401
395
  }
402
396
  parseUsage(input) {
397
+ // Gemini excludes thought tokens from `candidatesTokenCount` but includes
398
+ // them in `totalTokenCount`. Fold them into `output_tokens` so
399
+ // input + output === total holds, matching how other providers report.
400
+ // `thoughtsTokenCount` is sent by thinking models but is not declared on
401
+ // the legacy SDK's `UsageMetadata`, hence the widened parameter type.
402
+ const thoughts = input.thoughtsTokenCount;
403
403
  return {
404
404
  input_tokens: input.promptTokenCount || 0,
405
- output_tokens: input.candidatesTokenCount || 0,
405
+ output_tokens: (input.candidatesTokenCount || 0) + (thoughts || 0),
406
406
  total_tokens: input.totalTokenCount || 0,
407
+ reasoning_tokens: thoughts,
407
408
  };
408
409
  }
409
410
  }
@@ -29,8 +29,6 @@ type AgentConfig = BaseAgentConfig & {
29
29
  export declare class MistralAgent extends BaseAgent {
30
30
  private client;
31
31
  protected config: Partial<AgentConfig>;
32
- /** Token usage from the last execution (for metrics tracking) */
33
- lastTokenUsage?: TokenUsage;
34
32
  /** Current visualization event ID for tracking */
35
33
  private vizEventId?;
36
34
  /** Count of tool calls in current execution */
@@ -73,7 +73,7 @@ class MistralAgent extends BaseAgent_1.BaseAgent {
73
73
  async execute(input) {
74
74
  this.emit(AgentEvent_1.AgentEvent.BEFORE_EXECUTE, input);
75
75
  // Reset token usage for this execution
76
- this.lastTokenUsage = undefined;
76
+ this.resetTokenUsage();
77
77
  this.currentToolCallCount = 0;
78
78
  const inputPreview = typeof input === "string" ? input : JSON.stringify(input);
79
79
  // Start visualization reporting
@@ -99,6 +99,7 @@ class MistralAgent extends BaseAgent_1.BaseAgent {
99
99
  this.history.beginExecution();
100
100
  try {
101
101
  const messages = transformers_1.mistralTransformer.toProvider(this.history.getEntries());
102
+ this.startTurnTimer();
102
103
  const response = await this.client.chat.complete({
103
104
  model: this.config.model,
104
105
  messages: messages,
@@ -147,16 +148,8 @@ class MistralAgent extends BaseAgent_1.BaseAgent {
147
148
  throw error;
148
149
  }
149
150
  const choice = response.choices[0];
150
- const usage = this.parseUsage(response.usage);
151
151
  // Track token usage
152
- if (this.lastTokenUsage) {
153
- this.lastTokenUsage.input_tokens += usage.input_tokens;
154
- this.lastTokenUsage.output_tokens += usage.output_tokens;
155
- this.lastTokenUsage.total_tokens += usage.total_tokens;
156
- }
157
- else {
158
- this.lastTokenUsage = { ...usage };
159
- }
152
+ const usage = this.accumulateUsage(this.parseUsage(response.usage));
160
153
  if (choice.finishReason === "length") {
161
154
  const error = new AgentError_1.MaxTokensExceededError("Response exceeded maximum token limit", this.config.maxTokens || 1024);
162
155
  this.emit(AgentEvent_1.AgentEvent.MAX_TOKENS_EXCEEDED, error);
@@ -218,6 +211,7 @@ class MistralAgent extends BaseAgent_1.BaseAgent {
218
211
  // Continue conversation
219
212
  try {
220
213
  const messages = transformers_1.mistralTransformer.toProvider(this.history.getEntries());
214
+ this.startTurnTimer();
221
215
  const newResponse = await this.client.chat.complete({
222
216
  model: this.config.model,
223
217
  messages: messages,
@@ -8,19 +8,19 @@
8
8
  * You can also provide any custom string for newer models not yet listed.
9
9
  * @see https://docs.anthropic.com/en/docs/about-claude/models
10
10
  */
11
- export type ClaudeModel = "claude-opus-4-7" | "claude-opus-4-6" | "claude-sonnet-4-6" | "claude-opus-4-5" | "claude-sonnet-4-5" | "claude-haiku-4-5" | "claude-haiku-4-5-20251001" | (string & Record<never, never>);
11
+ export type ClaudeModel = "claude-opus-5" | "claude-sonnet-5" | "claude-fable-5" | "claude-opus-4-8" | "claude-opus-4-7" | "claude-opus-4-6" | "claude-sonnet-4-6" | "claude-opus-4-5" | "claude-opus-4-5-20251101" | "claude-sonnet-4-5" | "claude-sonnet-4-5-20250929" | "claude-haiku-4-5" | "claude-haiku-4-5-20251001" | (string & Record<never, never>);
12
12
  /**
13
13
  * Supported Google Gemini models.
14
14
  * You can also provide any custom string for newer models not yet listed.
15
15
  * @see https://ai.google.dev/gemini-api/docs/models/gemini
16
16
  */
17
- export type GeminiModel = "gemini-flash-latest" | "gemini-flash-lite-latest" | "gemini-3.0-pro" | "gemini-3.0-flash" | "gemini-2.5-flash-lite" | "gemini-2.0-flash-exp" | "gemini-2.0-flash" | "gemini-2.0-flash-lite" | (string & {});
17
+ export type GeminiModel = "gemini-pro-latest" | "gemini-flash-latest" | "gemini-flash-lite-latest" | "gemini-3.6-flash" | "gemini-3.5-flash" | "gemini-3.5-flash-lite" | "gemini-3.1-pro-preview" | "gemini-3.1-flash-lite" | "gemini-3.1-flash-lite-preview" | "gemini-3-pro-preview" | "gemini-3-flash-preview" | "gemini-2.5-pro" | "gemini-2.5-flash" | "gemini-2.5-flash-lite" | "gemini-2.0-flash" | "gemini-2.0-flash-001" | "gemini-2.0-flash-lite" | "gemini-2.0-flash-lite-001" | (string & {});
18
18
  /**
19
19
  * Supported Mistral models.
20
20
  * You can also provide any custom string for newer models not yet listed.
21
21
  * @see https://docs.mistral.ai/getting-started/models/
22
22
  */
23
- export type MistralModel = "mistral-large-latest" | "mistral-small-latest" | "ministral-8b-latest" | "ministral-8b-2410" | "ministral-3b-latest" | "ministral-3b-2410" | "codestral-latest" | "codestral-2405" | "mistral-embed" | "mistral-moderation-latest" | "mistral-moderation-2411" | (string & {});
23
+ export type MistralModel = "mistral-large-latest" | "mistral-large-2512" | "mistral-medium-latest" | "mistral-medium-3.5" | "mistral-small-latest" | "mistral-small-2603" | "magistral-small-latest" | "ministral-14b-latest" | "ministral-8b-latest" | "ministral-8b-2512" | "ministral-3b-latest" | "ministral-3b-2512" | "codestral-latest" | "codestral-2508" | "devstral-latest" | "devstral-medium-latest" | "mistral-embed" | "codestral-embed" | "mistral-moderation-latest" | "mistral-moderation-2603" | "mistral-ocr-latest" | (string & {});
24
24
  /**
25
25
  * Popular Ollama models (locally hosted).
26
26
  * You can also provide any custom string for models you have pulled.
@@ -46,8 +46,6 @@ type OllamaToolDefinition = {
46
46
  */
47
47
  export declare class OllamaAgent extends BaseAgent {
48
48
  protected config: Partial<AgentConfig>;
49
- /** Token usage from the last execution (for metrics tracking) */
50
- lastTokenUsage?: TokenUsage;
51
49
  /** Current visualization event ID */
52
50
  private vizEventId?;
53
51
  /** Count of tool calls in current execution */
@@ -135,7 +135,7 @@ class OllamaAgent extends BaseAgent_1.BaseAgent {
135
135
  }
136
136
  async execute(input) {
137
137
  this.emit(AgentEvent_1.AgentEvent.BEFORE_EXECUTE, input);
138
- this.lastTokenUsage = undefined;
138
+ this.resetTokenUsage();
139
139
  this.currentToolCallCount = 0;
140
140
  const inputPreview = typeof input === "string" ? input : JSON.stringify(input);
141
141
  if (VizConfig_1.vizConfig.isEnabled()) {
@@ -203,6 +203,7 @@ class OllamaAgent extends BaseAgent_1.BaseAgent {
203
203
  const messages = transformers_1.ollamaTransformer.toProvider(this.history.getEntries());
204
204
  const tools = this.tools.size > 0 ? this.getToolDefinitions() : undefined;
205
205
  const options = this.buildOptions();
206
+ this.startTurnTimer();
206
207
  return client.chat({
207
208
  model: this.config.model,
208
209
  messages,
@@ -214,15 +215,7 @@ class OllamaAgent extends BaseAgent_1.BaseAgent {
214
215
  }
215
216
  async handleResponse(response) {
216
217
  const ollamaResponse = response;
217
- const usage = this.parseUsage(ollamaResponse);
218
- if (this.lastTokenUsage) {
219
- this.lastTokenUsage.input_tokens += usage.input_tokens;
220
- this.lastTokenUsage.output_tokens += usage.output_tokens;
221
- this.lastTokenUsage.total_tokens += usage.total_tokens;
222
- }
223
- else {
224
- this.lastTokenUsage = { ...usage };
225
- }
218
+ const usage = this.accumulateUsage(this.parseUsage(ollamaResponse));
226
219
  if (ollamaResponse.done_reason === "length") {
227
220
  const error = new AgentError_1.MaxTokensExceededError("Response exceeded maximum token limit", this.config.maxTokens || 2048);
228
221
  this.emit(AgentEvent_1.AgentEvent.MAX_TOKENS_EXCEEDED, error);
@@ -306,10 +299,19 @@ class OllamaAgent extends BaseAgent_1.BaseAgent {
306
299
  }
307
300
  parseUsage(input) {
308
301
  const response = input;
302
+ // Ollama reports its own timings in nanoseconds. Time to first token is
303
+ // model load plus prompt evaluation — everything before generation starts.
304
+ const toMs = (ns) => ns === undefined ? undefined : ns / 1000000;
305
+ const timeToFirstTokenMs = response.prompt_eval_duration === undefined
306
+ ? undefined
307
+ : toMs((response.load_duration ?? 0) + response.prompt_eval_duration);
309
308
  return {
310
309
  input_tokens: response.prompt_eval_count ?? 0,
311
310
  output_tokens: response.eval_count ?? 0,
312
311
  total_tokens: (response.prompt_eval_count ?? 0) + (response.eval_count ?? 0),
312
+ timeToFirstTokenMs,
313
+ generationMs: toMs(response.eval_duration),
314
+ totalMs: toMs(response.total_duration),
313
315
  };
314
316
  }
315
317
  }
@@ -62,8 +62,6 @@ export declare class OpenAiAgent<M extends OpenAIModel = OpenAIModel> extends Ba
62
62
  * caller's model is known.
63
63
  */
64
64
  protected config: Partial<AgentConfig>;
65
- /** Token usage from the last execution (for metrics tracking) */
66
- lastTokenUsage?: TokenUsage;
67
65
  /** Current visualization event ID for tracking */
68
66
  private vizEventId?;
69
67
  /** Count of tool calls in current execution */
@@ -140,7 +140,7 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
140
140
  async execute(input) {
141
141
  this.emit(AgentEvent_1.AgentEvent.BEFORE_EXECUTE, input);
142
142
  // Reset token usage for this execution
143
- this.lastTokenUsage = undefined;
143
+ this.resetTokenUsage();
144
144
  this.currentToolCallCount = 0;
145
145
  const inputPreview = typeof input === "string" ? input : JSON.stringify(input);
146
146
  // Start visualization reporting
@@ -166,6 +166,7 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
166
166
  this.history.beginExecution();
167
167
  try {
168
168
  const inputMessages = transformers_1.openAiTransformer.toProvider(this.history.getEntries());
169
+ this.startTurnTimer();
169
170
  const response = await this.client.responses.create({
170
171
  model: this.config.model,
171
172
  max_output_tokens: this.config.maxTokens,
@@ -220,15 +221,7 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
220
221
  }
221
222
  // Track token usage if available
222
223
  if (response.usage) {
223
- const usage = this.parseUsage(response.usage);
224
- if (this.lastTokenUsage) {
225
- this.lastTokenUsage.input_tokens += usage.input_tokens;
226
- this.lastTokenUsage.output_tokens += usage.output_tokens;
227
- this.lastTokenUsage.total_tokens += usage.total_tokens;
228
- }
229
- else {
230
- this.lastTokenUsage = { ...usage };
231
- }
224
+ this.accumulateUsage(this.parseUsage(response.usage));
232
225
  }
233
226
  const toolCalls = response.output.filter((output) => output.type === "function_call");
234
227
  // Find the message output (skip reasoning outputs)
@@ -287,6 +280,7 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
287
280
  // Continue conversation
288
281
  try {
289
282
  const inputMessages = transformers_1.openAiTransformer.toProvider(this.history.getEntries());
283
+ this.startTurnTimer();
290
284
  const newResponse = await this.client.responses.create({
291
285
  model: this.config.model,
292
286
  max_output_tokens: this.config.maxTokens,
@@ -404,7 +398,7 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
404
398
  */
405
399
  async *executeStream(input) {
406
400
  this.emit(AgentEvent_1.AgentEvent.BEFORE_EXECUTE, input);
407
- this.lastTokenUsage = undefined;
401
+ this.resetTokenUsage();
408
402
  this.currentToolCallCount = 0;
409
403
  const inputPreview = typeof input === "string" ? input : JSON.stringify(input);
410
404
  if (VizConfig_1.vizConfig.isEnabled()) {
@@ -458,6 +452,7 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
458
452
  }
459
453
  async *streamTurn() {
460
454
  const inputMessages = transformers_1.openAiTransformer.toProvider(this.history.getEntries());
455
+ this.startTurnTimer();
461
456
  const stream = await this.client.responses.create({
462
457
  model: this.config.model,
463
458
  max_output_tokens: this.config.maxTokens,
@@ -473,25 +468,19 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
473
468
  let completedEvent = null;
474
469
  for await (const event of stream) {
475
470
  if (event.type === "response.output_text.delta") {
471
+ this.markFirstToken();
476
472
  this.emit(AgentEvent_1.AgentEvent.CHUNK, event.delta);
477
473
  yield { type: "text", content: event.delta };
478
474
  }
479
475
  if (event.type === "response.reasoning_summary_text.delta") {
476
+ this.markFirstToken();
480
477
  this.emit(AgentEvent_1.AgentEvent.REASONING_CHUNK, event.delta);
481
478
  yield { type: "reasoning", content: event.delta };
482
479
  }
483
480
  if (event.type === "response.completed") {
484
481
  completedEvent = event;
485
482
  if (event.response.usage) {
486
- const usage = this.parseUsage(event.response.usage);
487
- if (this.lastTokenUsage) {
488
- this.lastTokenUsage.input_tokens += usage.input_tokens;
489
- this.lastTokenUsage.output_tokens += usage.output_tokens;
490
- this.lastTokenUsage.total_tokens += usage.total_tokens;
491
- }
492
- else {
493
- this.lastTokenUsage = { ...usage };
494
- }
483
+ this.accumulateUsage(this.parseUsage(event.response.usage));
495
484
  }
496
485
  }
497
486
  if (event.type === "response.incomplete") {
@@ -540,6 +529,8 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
540
529
  input_tokens: input.input_tokens,
541
530
  output_tokens: input.output_tokens,
542
531
  total_tokens: input.total_tokens,
532
+ // Reasoning tokens are already counted inside `output_tokens`.
533
+ reasoning_tokens: input.output_tokens_details?.reasoning_tokens,
543
534
  };
544
535
  }
545
536
  }
@@ -32,7 +32,6 @@ export type OpenAICompatibleConfig = BaseAgentConfig & {
32
32
  export declare abstract class OpenAICompatibleAgent extends BaseAgent {
33
33
  protected client: OpenAI;
34
34
  protected config: Partial<OpenAICompatibleConfig>;
35
- lastTokenUsage?: TokenUsage;
36
35
  private vizEventId?;
37
36
  private currentToolCallCount;
38
37
  constructor(config: OpenAICompatibleConfig & {
@@ -77,7 +77,7 @@ class OpenAICompatibleAgent extends BaseAgent_1.BaseAgent {
77
77
  }
78
78
  async execute(input) {
79
79
  this.emit(AgentEvent_1.AgentEvent.BEFORE_EXECUTE, input);
80
- this.lastTokenUsage = undefined;
80
+ this.resetTokenUsage();
81
81
  this.currentToolCallCount = 0;
82
82
  const inputPreview = typeof input === "string" ? input : JSON.stringify(input);
83
83
  if (VizConfig_1.vizConfig.isEnabled()) {
@@ -133,6 +133,7 @@ class OpenAICompatibleAgent extends BaseAgent_1.BaseAgent {
133
133
  async callProvider() {
134
134
  const messages = transformers_1.chatCompletionsTransformer.toProvider(this.history.getEntries());
135
135
  const tools = this.tools.size > 0 ? this.getToolDefinitions() : undefined;
136
+ this.startTurnTimer();
136
137
  return this.client.chat.completions.create({
137
138
  model: this.config.model,
138
139
  messages,
@@ -149,15 +150,7 @@ class OpenAICompatibleAgent extends BaseAgent_1.BaseAgent {
149
150
  });
150
151
  }
151
152
  async handleResponse(response) {
152
- const usage = this.parseUsage(response);
153
- if (this.lastTokenUsage) {
154
- this.lastTokenUsage.input_tokens += usage.input_tokens;
155
- this.lastTokenUsage.output_tokens += usage.output_tokens;
156
- this.lastTokenUsage.total_tokens += usage.total_tokens;
157
- }
158
- else {
159
- this.lastTokenUsage = { ...usage };
160
- }
153
+ const usage = this.accumulateUsage(this.parseUsage(response));
161
154
  const choice = response.choices[0];
162
155
  const message = choice.message;
163
156
  if (choice.finish_reason === "length") {
@@ -253,7 +246,7 @@ class OpenAICompatibleAgent extends BaseAgent_1.BaseAgent {
253
246
  */
254
247
  async *executeStream(input) {
255
248
  this.emit(AgentEvent_1.AgentEvent.BEFORE_EXECUTE, input);
256
- this.lastTokenUsage = undefined;
249
+ this.resetTokenUsage();
257
250
  this.currentToolCallCount = 0;
258
251
  const inputPreview = typeof input === "string" ? input : JSON.stringify(input);
259
252
  if (VizConfig_1.vizConfig.isEnabled()) {
@@ -307,6 +300,7 @@ class OpenAICompatibleAgent extends BaseAgent_1.BaseAgent {
307
300
  async *streamTurn() {
308
301
  const messages = transformers_1.chatCompletionsTransformer.toProvider(this.history.getEntries());
309
302
  const tools = this.tools.size > 0 ? this.getToolDefinitions() : undefined;
303
+ this.startTurnTimer();
310
304
  const stream = await this.client.chat.completions.create({
311
305
  model: this.config.model,
312
306
  messages,
@@ -326,17 +320,22 @@ class OpenAICompatibleAgent extends BaseAgent_1.BaseAgent {
326
320
  let reasoningContent = "";
327
321
  const toolCallAcc = new Map();
328
322
  let finishReason = null;
323
+ let streamUsage;
329
324
  for await (const chunk of stream) {
330
- // Final chunk carrying usage (choices is empty)
331
- if (chunk.choices.length === 0) {
332
- if (chunk.usage)
333
- this.accumulateStreamUsage(chunk.usage);
325
+ // Usage can ride on any chunk: OpenAI sends it on a final choice-less
326
+ // chunk, OpenRouter attaches it to the last content chunk (the one
327
+ // carrying finish_reason). Keep the most recent and fold it in once the
328
+ // stream ends — it is a running total for the turn, not a delta, so
329
+ // taking the last one covers both layouts without double-counting.
330
+ if (chunk.usage)
331
+ streamUsage = chunk.usage;
332
+ if (chunk.choices.length === 0)
334
333
  continue;
335
- }
336
334
  const choice = chunk.choices[0];
337
335
  finishReason = choice.finish_reason ?? finishReason;
338
336
  const delta = choice.delta;
339
337
  if (delta.content) {
338
+ this.markFirstToken();
340
339
  textContent += delta.content;
341
340
  this.emit(AgentEvent_1.AgentEvent.CHUNK, delta.content);
342
341
  yield { type: "text", content: delta.content };
@@ -348,6 +347,7 @@ class OpenAICompatibleAgent extends BaseAgent_1.BaseAgent {
348
347
  const deltaExtras = delta;
349
348
  const reasoningDelta = (deltaExtras.reasoning ?? deltaExtras.reasoning_content);
350
349
  if (reasoningDelta) {
350
+ this.markFirstToken();
351
351
  // Accumulated as well as yielded: DeepSeek's thinking mode requires the
352
352
  // assistant turn's reasoning to be replayed on the next request, so it
353
353
  // has to reach history rather than only the caller.
@@ -370,6 +370,10 @@ class OpenAICompatibleAgent extends BaseAgent_1.BaseAgent {
370
370
  }
371
371
  }
372
372
  }
373
+ // Before any early return below, so a turn that hits the token limit or
374
+ // continues into a tool call still reports what it spent.
375
+ if (streamUsage)
376
+ this.accumulateStreamUsage(streamUsage);
373
377
  if (finishReason === "length") {
374
378
  const error = new AgentError_1.MaxTokensExceededError("Response exceeded maximum token limit", this.config.maxTokens || 1024);
375
379
  this.emit(AgentEvent_1.AgentEvent.MAX_TOKENS_EXCEEDED, error);
@@ -424,19 +428,12 @@ class OpenAICompatibleAgent extends BaseAgent_1.BaseAgent {
424
428
  accumulateStreamUsage(usage) {
425
429
  if (!usage)
426
430
  return;
427
- const u = {
431
+ this.accumulateUsage({
428
432
  input_tokens: usage.prompt_tokens ?? 0,
429
433
  output_tokens: usage.completion_tokens ?? 0,
430
434
  total_tokens: usage.total_tokens ?? 0,
431
- };
432
- if (this.lastTokenUsage) {
433
- this.lastTokenUsage.input_tokens += u.input_tokens;
434
- this.lastTokenUsage.output_tokens += u.output_tokens;
435
- this.lastTokenUsage.total_tokens += u.total_tokens;
436
- }
437
- else {
438
- this.lastTokenUsage = u;
439
- }
435
+ reasoning_tokens: extractReasoningTokens(usage),
436
+ });
440
437
  }
441
438
  parseUsage(response) {
442
439
  const usage = response.usage;
@@ -444,8 +441,38 @@ class OpenAICompatibleAgent extends BaseAgent_1.BaseAgent {
444
441
  input_tokens: usage?.prompt_tokens ?? 0,
445
442
  output_tokens: usage?.completion_tokens ?? 0,
446
443
  total_tokens: usage?.total_tokens ?? 0,
444
+ reasoning_tokens: extractReasoningTokens(usage),
445
+ ...extractServerTimings(response),
447
446
  };
448
447
  }
449
448
  }
450
449
  exports.OpenAICompatibleAgent = OpenAICompatibleAgent;
450
+ /**
451
+ * Reasoning token count from `completion_tokens_details`, which reasoning
452
+ * models (and OpenAI-compatible proxies fronting them) fill in.
453
+ */
454
+ function extractReasoningTokens(usage) {
455
+ const details = usage
456
+ ?.completion_tokens_details;
457
+ return details?.reasoning_tokens;
458
+ }
459
+ /**
460
+ * Timings reported by llama.cpp's `llama-server`, which adds a non-standard
461
+ * `timings` object to its chat completion responses. Absent on every other
462
+ * OpenAI-compatible server, in which case the durations are measured locally.
463
+ */
464
+ function extractServerTimings(response) {
465
+ const timings = response?.timings;
466
+ if (!timings)
467
+ return {};
468
+ const { prompt_ms, predicted_ms } = timings;
469
+ const totalMs = prompt_ms === undefined && predicted_ms === undefined
470
+ ? undefined
471
+ : (prompt_ms ?? 0) + (predicted_ms ?? 0);
472
+ return {
473
+ timeToFirstTokenMs: prompt_ms,
474
+ generationMs: predicted_ms,
475
+ totalMs,
476
+ };
477
+ }
451
478
  //# sourceMappingURL=OpenAICompatibleAgent.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@agentionai/agents",
3
3
  "author": "Laurent Zuijdwijk",
4
- "version": "1.1.0",
4
+ "version": "1.3.0",
5
5
  "description": "Agent Library",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",