@agentionai/agents 1.0.2 → 1.2.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.
@@ -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
  }
@@ -1,18 +1,43 @@
1
1
  import { BaseAgent, BaseAgentConfig, TokenUsage } from "../BaseAgent";
2
2
  import { History, MessageContent } from "../../history/History";
3
3
  import { Tool, Response, ResponseUsage } from "openai/resources/responses/responses";
4
- import { OpenAIModel } from "../model-types";
4
+ import { OpenAIModel, ReasoningEffort, ReasoningEffortFor } from "../model-types";
5
5
  import { StreamChunk } from "../openai-compatible/OpenAICompatibleAgent";
6
- type AgentConfig = BaseAgentConfig & {
6
+ type AgentConfig<M extends OpenAIModel = OpenAIModel> = BaseAgentConfig & {
7
7
  apiKey: string;
8
- model?: OpenAIModel;
8
+ model?: M;
9
9
  maxTokens?: number;
10
10
  disableParallelToolUse?: boolean;
11
- /** Disable extended thinking/reasoning for models that support it (like gpt-5-nano) */
11
+ /**
12
+ * Ask for the least reasoning the configured model supports (e.g. `minimal` on
13
+ * `gpt-5-nano`, `none` on `gpt-5.6`). Takes precedence over `reasoningEffort`.
14
+ * No effect on models without reasoning support.
15
+ */
12
16
  disableReasoning?: boolean;
13
- reasoningEffort?: "low" | "medium" | "high";
17
+ /**
18
+ * How hard the model should think. Narrowed to the values the configured
19
+ * `model` actually accepts — `reasoningEffort: "none"` is a type error on
20
+ * `gpt-5-nano`, which takes `minimal` instead.
21
+ */
22
+ reasoningEffort?: ReasoningEffortFor<M>;
14
23
  user?: string;
15
24
  };
25
+ /**
26
+ * Lowest `reasoning.effort` the given model accepts, used to resolve
27
+ * `disableReasoning`. Returns `undefined` when the model has no reasoning to turn
28
+ * off, in which case the caller omits `reasoning` entirely rather than risk a 400
29
+ * — non-reasoning models such as `gpt-4.1-mini` reject the parameter outright.
30
+ *
31
+ * There is no single "off" value, and `effort: null` is not one either: it means
32
+ * *unset*, so the model falls back to its own default (`medium` on every family
33
+ * released before `gpt-5.1`).
34
+ *
35
+ * Reads {@link OPENAI_REASONING_SUPPORT}, the same table {@link ReasoningEffortFor}
36
+ * is derived from, so the compile-time and runtime views cannot disagree. Models
37
+ * missing from it — including newer families — return `undefined`; set
38
+ * `reasoningEffort` explicitly to override.
39
+ */
40
+ export declare function lowestReasoningEffort(model: string | undefined): ReasoningEffort | undefined;
16
41
  /**
17
42
  * Agent for OpenAI models using the Responses API.
18
43
  *
@@ -28,17 +53,38 @@ type AgentConfig = BaseAgentConfig & {
28
53
  * const response = await agent.execute("Hello!");
29
54
  * ```
30
55
  */
31
- export declare class OpenAiAgent extends BaseAgent {
56
+ export declare class OpenAiAgent<M extends OpenAIModel = OpenAIModel> extends BaseAgent {
32
57
  private client;
58
+ /**
59
+ * Resolved runtime config. Deliberately not narrowed by `M` — the constructor
60
+ * fills in defaults and merges `vendorConfig`, whose values are not
61
+ * model-scoped. Narrowing happens on the constructor's parameter, where the
62
+ * caller's model is known.
63
+ */
33
64
  protected config: Partial<AgentConfig>;
34
- /** Token usage from the last execution (for metrics tracking) */
35
- lastTokenUsage?: TokenUsage;
36
65
  /** Current visualization event ID for tracking */
37
66
  private vizEventId?;
38
67
  /** Count of tool calls in current execution */
39
68
  private currentToolCallCount;
40
- constructor(config: Omit<AgentConfig, "vendor">, history?: History);
69
+ constructor(config: Omit<AgentConfig<M>, "vendor">, history?: History);
41
70
  protected getToolDefinitions(): Tool[];
71
+ /**
72
+ * Build the `reasoning` field for a Responses API request, as an object to
73
+ * spread into the request params.
74
+ *
75
+ * `disableReasoning` takes precedence over `reasoningEffort` and resolves to the
76
+ * lowest effort the configured model accepts (see {@link lowestReasoningEffort}).
77
+ * The field is omitted entirely when neither option applies — `reasoning: {}` is
78
+ * not the same as omitting it, and non-reasoning models reject the parameter.
79
+ *
80
+ * All three request sites go through here: they were copies of the same
81
+ * expression, and one drifted into overwriting the disable case with an
82
+ * unconditional `reasoning` key.
83
+ *
84
+ * @param summary Pass `"auto"` for streaming requests — the Responses API only
85
+ * emits `response.reasoning_summary_text.delta` events when it is set.
86
+ */
87
+ private buildReasoningParams;
42
88
  protected process(_input: string): Promise<string>;
43
89
  execute(input: string | MessageContent[]): Promise<string>;
44
90
  protected handleResponse(response: Response): Promise<string>;
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.OpenAiAgent = void 0;
7
+ exports.lowestReasoningEffort = lowestReasoningEffort;
7
8
  const openai_1 = __importDefault(require("openai"));
8
9
  const BaseAgent_1 = require("../BaseAgent");
9
10
  const AgentEvent_1 = require("../AgentEvent");
@@ -11,6 +12,30 @@ const AgentError_1 = require("../errors/AgentError");
11
12
  const transformers_1 = require("../../history/transformers");
12
13
  const VizReporter_1 = require("../../viz/VizReporter");
13
14
  const VizConfig_1 = require("../../viz/VizConfig");
15
+ const model_types_1 = require("../model-types");
16
+ /**
17
+ * Lowest `reasoning.effort` the given model accepts, used to resolve
18
+ * `disableReasoning`. Returns `undefined` when the model has no reasoning to turn
19
+ * off, in which case the caller omits `reasoning` entirely rather than risk a 400
20
+ * — non-reasoning models such as `gpt-4.1-mini` reject the parameter outright.
21
+ *
22
+ * There is no single "off" value, and `effort: null` is not one either: it means
23
+ * *unset*, so the model falls back to its own default (`medium` on every family
24
+ * released before `gpt-5.1`).
25
+ *
26
+ * Reads {@link OPENAI_REASONING_SUPPORT}, the same table {@link ReasoningEffortFor}
27
+ * is derived from, so the compile-time and runtime views cannot disagree. Models
28
+ * missing from it — including newer families — return `undefined`; set
29
+ * `reasoningEffort` explicitly to override.
30
+ */
31
+ function lowestReasoningEffort(model) {
32
+ if (!model)
33
+ return undefined;
34
+ // Snapshot ids (`gpt-5-nano-2025-08-07`) share their alias's support set.
35
+ const base = model.replace(/-20\d{2}-\d{2}-\d{2}$/, "");
36
+ const group = model_types_1.OPENAI_REASONING_SUPPORT.find((entry) => entry.models.includes(base));
37
+ return group?.efforts[0];
38
+ }
14
39
  /**
15
40
  * Agent for OpenAI models using the Responses API.
16
41
  *
@@ -78,13 +103,44 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
78
103
  };
79
104
  });
80
105
  }
106
+ /**
107
+ * Build the `reasoning` field for a Responses API request, as an object to
108
+ * spread into the request params.
109
+ *
110
+ * `disableReasoning` takes precedence over `reasoningEffort` and resolves to the
111
+ * lowest effort the configured model accepts (see {@link lowestReasoningEffort}).
112
+ * The field is omitted entirely when neither option applies — `reasoning: {}` is
113
+ * not the same as omitting it, and non-reasoning models reject the parameter.
114
+ *
115
+ * All three request sites go through here: they were copies of the same
116
+ * expression, and one drifted into overwriting the disable case with an
117
+ * unconditional `reasoning` key.
118
+ *
119
+ * @param summary Pass `"auto"` for streaming requests — the Responses API only
120
+ * emits `response.reasoning_summary_text.delta` events when it is set.
121
+ */
122
+ buildReasoningParams(summary) {
123
+ const effort = this.config.disableReasoning
124
+ ? lowestReasoningEffort(this.config.model)
125
+ : this.config.reasoningEffort;
126
+ if (!effort)
127
+ return {};
128
+ return {
129
+ reasoning: {
130
+ // The Responses API accepts "max" (verified on gpt-5.6), but the installed
131
+ // SDK's ReasoningEffort union predates it — cast at this one boundary.
132
+ effort: effort,
133
+ ...(summary ? { summary } : {}),
134
+ },
135
+ };
136
+ }
81
137
  async process(_input) {
82
138
  return "";
83
139
  }
84
140
  async execute(input) {
85
141
  this.emit(AgentEvent_1.AgentEvent.BEFORE_EXECUTE, input);
86
142
  // Reset token usage for this execution
87
- this.lastTokenUsage = undefined;
143
+ this.resetTokenUsage();
88
144
  this.currentToolCallCount = 0;
89
145
  const inputPreview = typeof input === "string" ? input : JSON.stringify(input);
90
146
  // Start visualization reporting
@@ -110,6 +166,7 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
110
166
  this.history.beginExecution();
111
167
  try {
112
168
  const inputMessages = transformers_1.openAiTransformer.toProvider(this.history.getEntries());
169
+ this.startTurnTimer();
113
170
  const response = await this.client.responses.create({
114
171
  model: this.config.model,
115
172
  max_output_tokens: this.config.maxTokens,
@@ -120,8 +177,7 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
120
177
  top_p: this.config.topP,
121
178
  // Note: Responses API doesn't support seed, presence_penalty, frequency_penalty, stop
122
179
  user: this.config.user,
123
- ...(this.config.disableReasoning && { reasoning: { effort: null } }),
124
- reasoning: { effort: this.config.reasoningEffort },
180
+ ...this.buildReasoningParams(),
125
181
  });
126
182
  this.emit(AgentEvent_1.AgentEvent.AFTER_EXECUTE, response);
127
183
  return await this.handleResponse(response);
@@ -165,15 +221,7 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
165
221
  }
166
222
  // Track token usage if available
167
223
  if (response.usage) {
168
- const usage = this.parseUsage(response.usage);
169
- if (this.lastTokenUsage) {
170
- this.lastTokenUsage.input_tokens += usage.input_tokens;
171
- this.lastTokenUsage.output_tokens += usage.output_tokens;
172
- this.lastTokenUsage.total_tokens += usage.total_tokens;
173
- }
174
- else {
175
- this.lastTokenUsage = { ...usage };
176
- }
224
+ this.accumulateUsage(this.parseUsage(response.usage));
177
225
  }
178
226
  const toolCalls = response.output.filter((output) => output.type === "function_call");
179
227
  // Find the message output (skip reasoning outputs)
@@ -232,6 +280,7 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
232
280
  // Continue conversation
233
281
  try {
234
282
  const inputMessages = transformers_1.openAiTransformer.toProvider(this.history.getEntries());
283
+ this.startTurnTimer();
235
284
  const newResponse = await this.client.responses.create({
236
285
  model: this.config.model,
237
286
  max_output_tokens: this.config.maxTokens,
@@ -242,13 +291,7 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
242
291
  top_p: this.config.topP,
243
292
  // Note: Responses API doesn't support seed, presence_penalty, frequency_penalty, stop
244
293
  user: this.config.user,
245
- ...(this.config.disableReasoning && {
246
- reasoning: { effort: null },
247
- }),
248
- ...(this.config.reasoningEffort &&
249
- !this.config.disableReasoning && {
250
- reasoning: { effort: this.config.reasoningEffort },
251
- }),
294
+ ...this.buildReasoningParams(),
252
295
  });
253
296
  this.emit(AgentEvent_1.AgentEvent.AFTER_EXECUTE, newResponse);
254
297
  return this.handleResponse(newResponse);
@@ -355,7 +398,7 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
355
398
  */
356
399
  async *executeStream(input) {
357
400
  this.emit(AgentEvent_1.AgentEvent.BEFORE_EXECUTE, input);
358
- this.lastTokenUsage = undefined;
401
+ this.resetTokenUsage();
359
402
  this.currentToolCallCount = 0;
360
403
  const inputPreview = typeof input === "string" ? input : JSON.stringify(input);
361
404
  if (VizConfig_1.vizConfig.isEnabled()) {
@@ -409,6 +452,7 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
409
452
  }
410
453
  async *streamTurn() {
411
454
  const inputMessages = transformers_1.openAiTransformer.toProvider(this.history.getEntries());
455
+ this.startTurnTimer();
412
456
  const stream = await this.client.responses.create({
413
457
  model: this.config.model,
414
458
  max_output_tokens: this.config.maxTokens,
@@ -419,35 +463,24 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
419
463
  temperature: this.config.temperature,
420
464
  top_p: this.config.topP,
421
465
  user: this.config.user,
422
- ...(this.config.disableReasoning && { reasoning: { effort: null } }),
423
- ...(this.config.reasoningEffort && !this.config.disableReasoning && {
424
- // `summary: "auto"` is required for the Responses API to stream
425
- // `response.reasoning_summary_text.delta` events.
426
- reasoning: { effort: this.config.reasoningEffort, summary: "auto" },
427
- }),
466
+ ...this.buildReasoningParams("auto"),
428
467
  });
429
468
  let completedEvent = null;
430
469
  for await (const event of stream) {
431
470
  if (event.type === "response.output_text.delta") {
471
+ this.markFirstToken();
432
472
  this.emit(AgentEvent_1.AgentEvent.CHUNK, event.delta);
433
473
  yield { type: "text", content: event.delta };
434
474
  }
435
475
  if (event.type === "response.reasoning_summary_text.delta") {
476
+ this.markFirstToken();
436
477
  this.emit(AgentEvent_1.AgentEvent.REASONING_CHUNK, event.delta);
437
478
  yield { type: "reasoning", content: event.delta };
438
479
  }
439
480
  if (event.type === "response.completed") {
440
481
  completedEvent = event;
441
482
  if (event.response.usage) {
442
- const usage = this.parseUsage(event.response.usage);
443
- if (this.lastTokenUsage) {
444
- this.lastTokenUsage.input_tokens += usage.input_tokens;
445
- this.lastTokenUsage.output_tokens += usage.output_tokens;
446
- this.lastTokenUsage.total_tokens += usage.total_tokens;
447
- }
448
- else {
449
- this.lastTokenUsage = { ...usage };
450
- }
483
+ this.accumulateUsage(this.parseUsage(event.response.usage));
451
484
  }
452
485
  }
453
486
  if (event.type === "response.incomplete") {
@@ -496,6 +529,8 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
496
529
  input_tokens: input.input_tokens,
497
530
  output_tokens: input.output_tokens,
498
531
  total_tokens: input.total_tokens,
532
+ // Reasoning tokens are already counted inside `output_tokens`.
533
+ reasoning_tokens: input.output_tokens_details?.reasoning_tokens,
499
534
  };
500
535
  }
501
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,
@@ -323,6 +317,7 @@ class OpenAICompatibleAgent extends BaseAgent_1.BaseAgent {
323
317
  ...this.buildExtraRequestParams(),
324
318
  });
325
319
  let textContent = "";
320
+ let reasoningContent = "";
326
321
  const toolCallAcc = new Map();
327
322
  let finishReason = null;
328
323
  for await (const chunk of stream) {
@@ -336,6 +331,7 @@ class OpenAICompatibleAgent extends BaseAgent_1.BaseAgent {
336
331
  finishReason = choice.finish_reason ?? finishReason;
337
332
  const delta = choice.delta;
338
333
  if (delta.content) {
334
+ this.markFirstToken();
339
335
  textContent += delta.content;
340
336
  this.emit(AgentEvent_1.AgentEvent.CHUNK, delta.content);
341
337
  yield { type: "text", content: delta.content };
@@ -347,6 +343,11 @@ class OpenAICompatibleAgent extends BaseAgent_1.BaseAgent {
347
343
  const deltaExtras = delta;
348
344
  const reasoningDelta = (deltaExtras.reasoning ?? deltaExtras.reasoning_content);
349
345
  if (reasoningDelta) {
346
+ this.markFirstToken();
347
+ // Accumulated as well as yielded: DeepSeek's thinking mode requires the
348
+ // assistant turn's reasoning to be replayed on the next request, so it
349
+ // has to reach history rather than only the caller.
350
+ reasoningContent += reasoningDelta;
350
351
  this.emit(AgentEvent_1.AgentEvent.REASONING_CHUNK, reasoningDelta);
351
352
  yield { type: "reasoning", content: reasoningDelta };
352
353
  }
@@ -389,6 +390,7 @@ class OpenAICompatibleAgent extends BaseAgent_1.BaseAgent {
389
390
  role: "assistant",
390
391
  content: textContent || null,
391
392
  tool_calls: toolCalls,
393
+ reasoning_content: reasoningContent || null,
392
394
  });
393
395
  this.addToHistory(assistantEntry);
394
396
  const toolResults = await this.handleToolCalls(toolCalls);
@@ -401,6 +403,7 @@ class OpenAICompatibleAgent extends BaseAgent_1.BaseAgent {
401
403
  const assistantEntry = transformers_1.chatCompletionsTransformer.fromProviderMessage({
402
404
  role: "assistant",
403
405
  content: textContent || null,
406
+ reasoning_content: reasoningContent || null,
404
407
  });
405
408
  this.addToHistory(assistantEntry);
406
409
  this.emit(AgentEvent_1.AgentEvent.DONE, { content: textContent }, this.lastTokenUsage);
@@ -417,19 +420,12 @@ class OpenAICompatibleAgent extends BaseAgent_1.BaseAgent {
417
420
  accumulateStreamUsage(usage) {
418
421
  if (!usage)
419
422
  return;
420
- const u = {
423
+ this.accumulateUsage({
421
424
  input_tokens: usage.prompt_tokens ?? 0,
422
425
  output_tokens: usage.completion_tokens ?? 0,
423
426
  total_tokens: usage.total_tokens ?? 0,
424
- };
425
- if (this.lastTokenUsage) {
426
- this.lastTokenUsage.input_tokens += u.input_tokens;
427
- this.lastTokenUsage.output_tokens += u.output_tokens;
428
- this.lastTokenUsage.total_tokens += u.total_tokens;
429
- }
430
- else {
431
- this.lastTokenUsage = u;
432
- }
427
+ reasoning_tokens: extractReasoningTokens(usage),
428
+ });
433
429
  }
434
430
  parseUsage(response) {
435
431
  const usage = response.usage;
@@ -437,8 +433,38 @@ class OpenAICompatibleAgent extends BaseAgent_1.BaseAgent {
437
433
  input_tokens: usage?.prompt_tokens ?? 0,
438
434
  output_tokens: usage?.completion_tokens ?? 0,
439
435
  total_tokens: usage?.total_tokens ?? 0,
436
+ reasoning_tokens: extractReasoningTokens(usage),
437
+ ...extractServerTimings(response),
440
438
  };
441
439
  }
442
440
  }
443
441
  exports.OpenAICompatibleAgent = OpenAICompatibleAgent;
442
+ /**
443
+ * Reasoning token count from `completion_tokens_details`, which reasoning
444
+ * models (and OpenAI-compatible proxies fronting them) fill in.
445
+ */
446
+ function extractReasoningTokens(usage) {
447
+ const details = usage
448
+ ?.completion_tokens_details;
449
+ return details?.reasoning_tokens;
450
+ }
451
+ /**
452
+ * Timings reported by llama.cpp's `llama-server`, which adds a non-standard
453
+ * `timings` object to its chat completion responses. Absent on every other
454
+ * OpenAI-compatible server, in which case the durations are measured locally.
455
+ */
456
+ function extractServerTimings(response) {
457
+ const timings = response?.timings;
458
+ if (!timings)
459
+ return {};
460
+ const { prompt_ms, predicted_ms } = timings;
461
+ const totalMs = prompt_ms === undefined && predicted_ms === undefined
462
+ ? undefined
463
+ : (prompt_ms ?? 0) + (predicted_ms ?? 0);
464
+ return {
465
+ timeToFirstTokenMs: prompt_ms,
466
+ generationMs: predicted_ms,
467
+ totalMs,
468
+ };
469
+ }
444
470
  //# sourceMappingURL=OpenAICompatibleAgent.js.map
@@ -182,6 +182,12 @@ type ChatCompletionMessage = {
182
182
  role: "assistant";
183
183
  content: string | null;
184
184
  tool_calls?: ChatCompletionToolCallParam[];
185
+ /**
186
+ * Reasoning replayed from a previous turn. Required by DeepSeek's thinking
187
+ * mode; accepted by OpenRouter as an alias for `reasoning`. Omitted
188
+ * entirely when the turn carried no reasoning.
189
+ */
190
+ reasoning_content?: string;
185
191
  } | {
186
192
  role: "tool";
187
193
  tool_call_id: string;
@@ -197,6 +203,10 @@ type ChatCompletionResponseMessage = {
197
203
  arguments: string;
198
204
  };
199
205
  }>;
206
+ /** Reasoning tokens as sent by OpenRouter. Not part of the OpenAI schema. */
207
+ reasoning?: string | null;
208
+ /** Reasoning tokens as sent by DeepSeek and llama.cpp. */
209
+ reasoning_content?: string | null;
200
210
  };
201
211
  export {};
202
212
  //# sourceMappingURL=transformers.d.ts.map
@@ -614,6 +614,7 @@ exports.chatCompletionsTransformer = {
614
614
  const textBlocks = entry.content.filter(types_1.isTextContent);
615
615
  const toolUseBlocks = entry.content.filter(types_1.isToolUseContent);
616
616
  const toolResultBlocks = entry.content.filter(types_1.isToolResultContent);
617
+ const thinkingBlocks = entry.content.filter(types_1.isThinkingContent);
617
618
  const imageUrlBlocks = entry.content.filter(types_1.isImageUrlContent);
618
619
  const imageBase64Blocks = entry.content.filter(types_1.isImageBase64Content);
619
620
  const hasImages = imageUrlBlocks.length > 0 || imageBase64Blocks.length > 0;
@@ -626,6 +627,20 @@ exports.chatCompletionsTransformer = {
626
627
  role: "assistant",
627
628
  content: textBlocks.map((c) => c.text).join("\n") || null,
628
629
  };
630
+ // DeepSeek's thinking mode rejects a conversation whose assistant turns
631
+ // dropped their reasoning ("The reasoning_content in the thinking mode
632
+ // must be passed back to the API"), so it has to survive the round trip.
633
+ // `reasoning_content` is DeepSeek's field name and an accepted alias for
634
+ // `reasoning` on OpenRouter. Only set it when there is something to send:
635
+ // servers that reject unknown fields must not start seeing it, and
636
+ // redacted-only blocks (Anthropic) carry no text to replay.
637
+ const reasoning = thinkingBlocks
638
+ .map((block) => block.thinking)
639
+ .filter((thought) => thought.length > 0)
640
+ .join("\n");
641
+ if (reasoning) {
642
+ msg.reasoning_content = reasoning;
643
+ }
629
644
  if (toolUseBlocks.length > 0) {
630
645
  msg.tool_calls = toolUseBlocks.map((block) => ({
631
646
  id: block.id,
@@ -684,6 +699,15 @@ exports.chatCompletionsTransformer = {
684
699
  */
685
700
  fromProviderMessage(message) {
686
701
  const content = [];
702
+ // Reasoning first, matching the order the model produced it in. Servers
703
+ // disagree on the field name — OpenRouter sends `reasoning`, DeepSeek and
704
+ // llama.cpp send `reasoning_content` — so accept either, preferring
705
+ // `reasoning` as the streaming path does. Stored as the neutral thinking
706
+ // block the history layer already round-trips for Anthropic.
707
+ const reasoning = message.reasoning ?? message.reasoning_content;
708
+ if (typeof reasoning === "string" && reasoning) {
709
+ content.push((0, types_1.thinking)(reasoning));
710
+ }
687
711
  if (typeof message.content === "string" && message.content) {
688
712
  content.push((0, types_1.text)(message.content));
689
713
  }