@agentionai/agents 1.12.0 → 1.14.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.
@@ -2,14 +2,30 @@ import { BaseAgent, BaseAgentConfig, ModelInfo, TokenUsage } from "../BaseAgent"
2
2
  import { ExecuteOptions } from "../cancellation";
3
3
  import { History, MessageContent } from "../../history/History";
4
4
  import { type BuiltInTool } from "../../tools/BuiltInTool";
5
- import { Tool, Response, ResponseUsage } from "openai/resources/responses/responses";
5
+ import { Tool, Response, ResponseInputItem, ResponseUsage } from "openai/resources/responses/responses";
6
6
  import type { Model as OpenAIModelCard } from "openai/resources/models";
7
7
  import { OpenAIModel, ReasoningEffort, ReasoningEffortFor } from "../model-types";
8
8
  import { StreamChunk } from "../openai-compatible/OpenAICompatibleAgent";
9
- type AgentConfig<M extends OpenAIModel = OpenAIModel> = BaseAgentConfig & {
10
- apiKey: string;
9
+ export type AgentConfig<M extends OpenAIModel = OpenAIModel> = Omit<BaseAgentConfig, "apiKey"> & {
10
+ /**
11
+ * Platform API key, or an async function returning one.
12
+ *
13
+ * A function is re-invoked before every request, so a rotating or refreshed
14
+ * credential stays current across a long run — which is how `CodexAgent`
15
+ * keeps a ChatGPT OAuth token alive.
16
+ */
17
+ apiKey: string | (() => Promise<string>);
11
18
  model?: M;
12
19
  maxTokens?: number;
20
+ /**
21
+ * Override the API base URL. Defaults to the SDK's `api.openai.com/v1`.
22
+ */
23
+ baseURL?: string;
24
+ /**
25
+ * Replace the `fetch` used for every request — for interception, proxying, or
26
+ * normalising a non-OpenAI host's error bodies (see {@link wrapErrorBodyFetch}).
27
+ */
28
+ fetch?: typeof fetch;
13
29
  disableParallelToolUse?: boolean;
14
30
  /**
15
31
  * Ask for the least reasoning the configured model supports (e.g. `minimal` on
@@ -30,6 +46,26 @@ type AgentConfig<M extends OpenAIModel = OpenAIModel> = BaseAgentConfig & {
30
46
  * @see lib/tools/BuiltInTool.ts
31
47
  */
32
48
  builtInTools?: BuiltInTool[];
49
+ /**
50
+ * Cache-routing key sent as `prompt_cache_key`. Requests sharing a key are
51
+ * steered to the same cache, which raises the prompt-cache hit rate for a
52
+ * long conversation or a fleet of agents that share a system prompt and tool
53
+ * belt. Any stable string works — a conversation id is the usual choice.
54
+ *
55
+ * Left unset by default: caching still happens without it, this only
56
+ * improves the routing.
57
+ *
58
+ * @see https://platform.openai.com/docs/guides/prompt-caching
59
+ */
60
+ promptCacheKey?: string;
61
+ /**
62
+ * How long cached prefixes stay warm. `"24h"` opts into extended retention;
63
+ * the default (`undefined`, i.e. the API's `in-memory`) expires a prefix
64
+ * within minutes.
65
+ *
66
+ * Ignored by the ChatGPT/Codex backend, which manages its own cache.
67
+ */
68
+ promptCacheRetention?: "in-memory" | "24h";
33
69
  };
34
70
  /**
35
71
  * Lowest `reasoning.effort` the given model accepts, used to resolve
@@ -47,6 +83,52 @@ type AgentConfig<M extends OpenAIModel = OpenAIModel> = BaseAgentConfig & {
47
83
  * `reasoningEffort` explicitly to override.
48
84
  */
49
85
  export declare function lowestReasoningEffort(model: string | undefined): ReasoningEffort | undefined;
86
+ /**
87
+ * `usage.input_tokens_details` as the wire actually carries it.
88
+ *
89
+ * The SDK's `ResponseUsage.InputTokensDetails` declares `cached_tokens` alone,
90
+ * but the ChatGPT/Codex backend also reports `cache_write_tokens` (observed
91
+ * live on 2026-09-10). Declared here rather than cast at the use site, and
92
+ * every field optional because a non-OpenAI host behind this SDK may report
93
+ * neither.
94
+ */
95
+ export type OpenAIInputTokensDetails = {
96
+ /** Prompt tokens served from cache. */
97
+ cached_tokens?: number;
98
+ /** Prompt tokens written to cache. Codex backend only. */
99
+ cache_write_tokens?: number;
100
+ };
101
+ /**
102
+ * `fetch` wrapper that rewrites a non-OpenAI-shaped error body into the shape
103
+ * the SDK can read.
104
+ *
105
+ * `APIError.generate` takes the message from `body.error` and throws the rest
106
+ * away (`openai/core/error.js`), so a backend that reports failures as
107
+ * `{"detail": "..."}` — which the ChatGPT Codex endpoint does, for all four of
108
+ * its body validations plus auth failures — surfaces as the useless
109
+ * `400 status code (no body)`. Nesting the original body under `error` puts the
110
+ * real reason back in the thrown error.
111
+ *
112
+ * Only touches error responses; successful (streaming) responses pass straight
113
+ * through untouched.
114
+ */
115
+ export declare function wrapErrorBodyFetch(baseFetch?: typeof fetch): typeof fetch;
116
+ /**
117
+ * Pull a human-readable message out of an OpenAI-shaped error.
118
+ *
119
+ * `api.openai.com` answers with `{ error: { message, code } }`, but not every
120
+ * host behind this SDK does — the ChatGPT Codex backend reports its validation
121
+ * failures as `{ detail: "Instructions are required" }`. Reading
122
+ * `error.error.message` blindly turns those into a `TypeError` that hides the
123
+ * real cause, so every field is probed defensively and the SDK's own `message`
124
+ * is the last resort.
125
+ */
126
+ export declare function describeOpenAIError(error: unknown): {
127
+ message: string;
128
+ code?: string;
129
+ status?: number;
130
+ body?: unknown;
131
+ };
50
132
  /**
51
133
  * Agent for OpenAI models using the Responses API.
52
134
  *
@@ -62,7 +144,7 @@ export declare function lowestReasoningEffort(model: string | undefined): Reason
62
144
  * const response = await agent.execute("Hello!");
63
145
  * ```
64
146
  */
65
- export declare class OpenAiAgent<M extends OpenAIModel = OpenAIModel> extends BaseAgent {
147
+ export declare class OpenAiAgent<M extends OpenAIModel = OpenAIModel, TModelCard = OpenAIModelCard> extends BaseAgent {
66
148
  private client;
67
149
  /**
68
150
  * Resolved runtime config. Deliberately not narrowed by `M` — the constructor
@@ -75,6 +157,19 @@ export declare class OpenAiAgent<M extends OpenAIModel = OpenAIModel> extends Ba
75
157
  private vizEventId?;
76
158
  /** Count of tool calls in current execution */
77
159
  private currentToolCallCount;
160
+ /**
161
+ * Whether a non-streaming call must be issued as a stream and collapsed.
162
+ * `false` here; `CodexAgent` overrides it, since that backend refuses
163
+ * `stream: false` outright.
164
+ */
165
+ protected get forceStreaming(): boolean;
166
+ /**
167
+ * Last chance to reshape a request body before it goes out. Identity here —
168
+ * `CodexAgent` overrides it to satisfy that backend's extra validations.
169
+ */
170
+ protected transformRequestParams<T extends {
171
+ input: ResponseInputItem[];
172
+ }>(params: T): T;
78
173
  constructor(config: Omit<AgentConfig<M>, "vendor">, history?: History);
79
174
  /**
80
175
  * List the models available to this API key.
@@ -83,7 +178,9 @@ export declare class OpenAiAgent<M extends OpenAIModel = OpenAIModel> extends Ba
83
178
  * image models alike — so filter by `id` if you only want the ones this
84
179
  * agent can drive.
85
180
  */
86
- listModels(): Promise<ModelInfo<OpenAIModelCard>[]>;
181
+ listModels(): Promise<ModelInfo<TModelCard>[]>;
182
+ /** The configured key, resolving the function form if that is what was given. */
183
+ protected resolveApiKey(): Promise<string>;
87
184
  protected getToolDefinitions(): Tool[];
88
185
  /**
89
186
  * Combine locally-executed tool definitions with provider-defined
@@ -94,6 +191,28 @@ export declare class OpenAiAgent<M extends OpenAIModel = OpenAIModel> extends Ba
94
191
  * uses for its own `ToolUnion[]`.
95
192
  */
96
193
  protected getAllToolDefinitions(): Tool[];
194
+ /**
195
+ * Rebuild a terminal response's `output` from the items streamed alongside it.
196
+ *
197
+ * The Codex backend sends `response.completed` with `output: []` and no
198
+ * `output_text`, unlike the platform API which fills both in — the content
199
+ * only ever arrives as `response.output_item.done` events. Everything
200
+ * downstream (tool-call detection, the text written to history) reads
201
+ * `output`, so without this a Codex turn silently commits an empty assistant
202
+ * message and drops every tool call.
203
+ *
204
+ * A no-op wherever `output` is already populated, so the platform path is
205
+ * untouched.
206
+ */
207
+ private repairStreamedOutput;
208
+ /**
209
+ * Issue a non-streaming Responses API call.
210
+ *
211
+ * When {@link forceStreaming} is set the request is streamed and the terminal
212
+ * event's `response` handed back instead — giving callers the same `Response`
213
+ * either way, at the cost of buffering the turn.
214
+ */
215
+ private createResponse;
97
216
  /**
98
217
  * Build the `reasoning` field for a Responses API request, as an object to
99
218
  * spread into the request params.
@@ -111,6 +230,15 @@ export declare class OpenAiAgent<M extends OpenAIModel = OpenAIModel> extends Ba
111
230
  * emits `response.reasoning_summary_text.delta` events when it is set.
112
231
  */
113
232
  private buildReasoningParams;
233
+ /**
234
+ * Prompt-caching parameters, omitted entirely when unconfigured so a request
235
+ * stays byte-identical to what earlier versions sent.
236
+ *
237
+ * Caching itself is automatic and needs no opt-in — these only influence
238
+ * which cache a request is routed to and how long the prefix stays warm. What
239
+ * was actually reused comes back on `lastTokenUsage.cache_read_tokens`.
240
+ */
241
+ private buildCacheParams;
114
242
  protected process(_input: string): Promise<string>;
115
243
  execute(input: string | MessageContent[], options?: ExecuteOptions): Promise<string>;
116
244
  protected handleResponse(response: Response, options?: ExecuteOptions): Promise<string>;
@@ -131,5 +259,4 @@ export declare class OpenAiAgent<M extends OpenAIModel = OpenAIModel> extends Ba
131
259
  private streamTurn;
132
260
  protected parseUsage(input: ResponseUsage): TokenUsage;
133
261
  }
134
- export {};
135
262
  //# sourceMappingURL=OpenAiAgent.d.ts.map
@@ -5,6 +5,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.OpenAiAgent = void 0;
7
7
  exports.lowestReasoningEffort = lowestReasoningEffort;
8
+ exports.wrapErrorBodyFetch = wrapErrorBodyFetch;
9
+ exports.describeOpenAIError = describeOpenAIError;
8
10
  const openai_1 = __importDefault(require("openai"));
9
11
  const BaseAgent_1 = require("../BaseAgent");
10
12
  const AgentEvent_1 = require("../AgentEvent");
@@ -38,6 +40,81 @@ function lowestReasoningEffort(model) {
38
40
  const group = model_types_1.OPENAI_REASONING_SUPPORT.find((entry) => entry.models.includes(base));
39
41
  return group?.efforts[0];
40
42
  }
43
+ /**
44
+ * `fetch` wrapper that rewrites a non-OpenAI-shaped error body into the shape
45
+ * the SDK can read.
46
+ *
47
+ * `APIError.generate` takes the message from `body.error` and throws the rest
48
+ * away (`openai/core/error.js`), so a backend that reports failures as
49
+ * `{"detail": "..."}` — which the ChatGPT Codex endpoint does, for all four of
50
+ * its body validations plus auth failures — surfaces as the useless
51
+ * `400 status code (no body)`. Nesting the original body under `error` puts the
52
+ * real reason back in the thrown error.
53
+ *
54
+ * Only touches error responses; successful (streaming) responses pass straight
55
+ * through untouched.
56
+ */
57
+ function wrapErrorBodyFetch(baseFetch = fetch) {
58
+ return async (input, init) => {
59
+ const res = await baseFetch(input, init);
60
+ if (res.ok)
61
+ return res;
62
+ const text = await res.text().catch(() => "");
63
+ let body = text;
64
+ try {
65
+ const parsed = JSON.parse(text);
66
+ if (parsed && typeof parsed === "object" && !("error" in parsed)) {
67
+ body = JSON.stringify({
68
+ error: {
69
+ message: typeof parsed.detail === "string"
70
+ ? parsed.detail
71
+ : JSON.stringify(parsed),
72
+ ...parsed,
73
+ },
74
+ });
75
+ }
76
+ }
77
+ catch {
78
+ // Not JSON (an HTML error page, say) — hand the text back unchanged so
79
+ // the SDK reports it as the message.
80
+ }
81
+ // Reading the body consumed it, so the Response has to be rebuilt. Drop the
82
+ // length/encoding headers, which no longer describe the new payload.
83
+ const headers = new Headers(res.headers);
84
+ headers.delete("content-length");
85
+ headers.delete("content-encoding");
86
+ // `globalThis.Response`, not `Response`: this module imports the Responses
87
+ // API's `Response` *type*, which shadows the global class name here.
88
+ return new globalThis.Response(body, {
89
+ status: res.status,
90
+ statusText: res.statusText,
91
+ headers,
92
+ });
93
+ };
94
+ }
95
+ /**
96
+ * Pull a human-readable message out of an OpenAI-shaped error.
97
+ *
98
+ * `api.openai.com` answers with `{ error: { message, code } }`, but not every
99
+ * host behind this SDK does — the ChatGPT Codex backend reports its validation
100
+ * failures as `{ detail: "Instructions are required" }`. Reading
101
+ * `error.error.message` blindly turns those into a `TypeError` that hides the
102
+ * real cause, so every field is probed defensively and the SDK's own `message`
103
+ * is the last resort.
104
+ */
105
+ function describeOpenAIError(error) {
106
+ const err = error;
107
+ const body = err?.error;
108
+ const fromBody = typeof body === "string"
109
+ ? body
110
+ : (body?.message ?? body?.detail ?? undefined);
111
+ return {
112
+ message: fromBody ?? err?.detail ?? err?.message ?? "Unknown error",
113
+ code: typeof body === "object" ? body?.code : undefined,
114
+ status: err?.status,
115
+ body: body ?? err?.detail,
116
+ };
117
+ }
41
118
  /**
42
119
  * Agent for OpenAI models using the Responses API.
43
120
  *
@@ -54,17 +131,39 @@ function lowestReasoningEffort(model) {
54
131
  * ```
55
132
  */
56
133
  class OpenAiAgent extends BaseAgent_1.BaseAgent {
134
+ /**
135
+ * Whether a non-streaming call must be issued as a stream and collapsed.
136
+ * `false` here; `CodexAgent` overrides it, since that backend refuses
137
+ * `stream: false` outright.
138
+ */
139
+ get forceStreaming() {
140
+ return false;
141
+ }
142
+ /**
143
+ * Last chance to reshape a request body before it goes out. Identity here —
144
+ * `CodexAgent` overrides it to satisfy that backend's extra validations.
145
+ */
146
+ transformRequestParams(params) {
147
+ return params;
148
+ }
57
149
  constructor(config, history) {
150
+ // Cast: `BaseAgentConfig.apiKey` is `string`, while this agent also accepts
151
+ // a token-returning function. BaseAgent never reads the field — it only
152
+ // declares it — so widening the base config for one provider would be the
153
+ // more invasive fix.
58
154
  super({ ...config, vendor: "openai" }, history);
59
155
  /** Count of tool calls in current execution */
60
156
  this.currentToolCallCount = 0;
157
+ // Merge flat config (deprecated) with nested vendorConfig
158
+ // Flat config takes precedence for backward compatibility
159
+ const vendorConfig = config.vendorConfig?.openai || {};
160
+ const baseURL = config.baseURL ?? vendorConfig.baseURL;
61
161
  this.client = new openai_1.default({
62
162
  apiKey: config.apiKey,
163
+ baseURL,
63
164
  defaultHeaders: config.defaultHeaders,
165
+ fetch: config.fetch,
64
166
  });
65
- // Merge flat config (deprecated) with nested vendorConfig
66
- // Flat config takes precedence for backward compatibility
67
- const vendorConfig = config.vendorConfig?.openai || {};
68
167
  const disableParallelToolUse = config.disableParallelToolUse ??
69
168
  vendorConfig.disableParallelToolUse ??
70
169
  false;
@@ -72,6 +171,8 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
72
171
  const reasoningEffort = config.reasoningEffort ?? vendorConfig.reasoningEffort;
73
172
  const user = config.user ?? vendorConfig.user;
74
173
  const builtInTools = config.builtInTools ?? vendorConfig.builtInTools;
174
+ const promptCacheKey = config.promptCacheKey ?? vendorConfig.promptCacheKey;
175
+ const promptCacheRetention = config.promptCacheRetention ?? vendorConfig.promptCacheRetention;
75
176
  this.config = {
76
177
  model: config.model || "gpt-4.1-mini",
77
178
  // No default. `max_output_tokens` is optional on the Responses API, and
@@ -86,7 +187,10 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
86
187
  reasoningEffort,
87
188
  user,
88
189
  builtInTools,
190
+ promptCacheKey,
191
+ promptCacheRetention,
89
192
  apiKey: config.apiKey,
193
+ baseURL,
90
194
  temperature: config.temperature,
91
195
  topP: config.topP,
92
196
  seed: config.seed,
@@ -111,6 +215,8 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
111
215
  id: model.id,
112
216
  created: model.created ? new Date(model.created * 1000) : undefined,
113
217
  ownedBy: model.owned_by,
218
+ // Cast: this implementation always returns OpenAI's own cards; a
219
+ // subclass that reports a different shape overrides the whole method.
114
220
  raw: model,
115
221
  }));
116
222
  }
@@ -118,6 +224,11 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
118
224
  throw new AgentError_1.ExecutionError(`Failed to list OpenAI models: ${error instanceof Error ? error.message : "Unknown error"}`);
119
225
  }
120
226
  }
227
+ /** The configured key, resolving the function form if that is what was given. */
228
+ async resolveApiKey() {
229
+ const key = this.config.apiKey;
230
+ return typeof key === "function" ? await key() : (key ?? "");
231
+ }
121
232
  getToolDefinitions() {
122
233
  return Array.from(this.tools.values()).map((tool) => {
123
234
  const prompt = tool.getPrompt();
@@ -152,6 +263,66 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
152
263
  ...(this.config.builtInTools ?? []),
153
264
  ];
154
265
  }
266
+ /**
267
+ * Rebuild a terminal response's `output` from the items streamed alongside it.
268
+ *
269
+ * The Codex backend sends `response.completed` with `output: []` and no
270
+ * `output_text`, unlike the platform API which fills both in — the content
271
+ * only ever arrives as `response.output_item.done` events. Everything
272
+ * downstream (tool-call detection, the text written to history) reads
273
+ * `output`, so without this a Codex turn silently commits an empty assistant
274
+ * message and drops every tool call.
275
+ *
276
+ * A no-op wherever `output` is already populated, so the platform path is
277
+ * untouched.
278
+ */
279
+ repairStreamedOutput(response, streamedItems) {
280
+ if (response.output?.length || streamedItems.length === 0)
281
+ return response;
282
+ const output = streamedItems;
283
+ const outputText = output
284
+ .filter((item) => item.type === "message")
285
+ .flatMap((item) => ("content" in item ? (item.content ?? []) : []))
286
+ .filter((part) => part?.type === "output_text")
287
+ .map((part) => ("text" in part ? part.text : ""))
288
+ .join("");
289
+ return { ...response, output, output_text: outputText };
290
+ }
291
+ /**
292
+ * Issue a non-streaming Responses API call.
293
+ *
294
+ * When {@link forceStreaming} is set the request is streamed and the terminal
295
+ * event's `response` handed back instead — giving callers the same `Response`
296
+ * either way, at the cost of buffering the turn.
297
+ */
298
+ async createResponse(params, requestOptions) {
299
+ const body = this.transformRequestParams(params);
300
+ if (!this.forceStreaming) {
301
+ return this.client.responses.create({ ...body, stream: false }, requestOptions);
302
+ }
303
+ const stream = (await this.client.responses.create({ ...body, stream: true }, requestOptions));
304
+ let terminal;
305
+ const streamedItems = [];
306
+ for await (const event of stream) {
307
+ // Collected because the Codex backend leaves `output` empty on the
308
+ // terminal event — see repairStreamedOutput().
309
+ if (event.type === "response.output_item.done") {
310
+ streamedItems.push(event.item);
311
+ }
312
+ // `incomplete` and `failed` carry a Response too — handleResponse()
313
+ // already reads `status` off it, so let it report the reason rather than
314
+ // failing here with a vaguer message.
315
+ if (event.type === "response.completed" ||
316
+ event.type === "response.incomplete" ||
317
+ event.type === "response.failed") {
318
+ terminal = event.response;
319
+ }
320
+ }
321
+ if (!terminal) {
322
+ throw new AgentError_1.ExecutionError("OpenAI stream ended without a terminal response event");
323
+ }
324
+ return this.repairStreamedOutput(terminal, streamedItems);
325
+ }
155
326
  /**
156
327
  * Build the `reasoning` field for a Responses API request, as an object to
157
328
  * spread into the request params.
@@ -183,6 +354,24 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
183
354
  },
184
355
  };
185
356
  }
357
+ /**
358
+ * Prompt-caching parameters, omitted entirely when unconfigured so a request
359
+ * stays byte-identical to what earlier versions sent.
360
+ *
361
+ * Caching itself is automatic and needs no opt-in — these only influence
362
+ * which cache a request is routed to and how long the prefix stays warm. What
363
+ * was actually reused comes back on `lastTokenUsage.cache_read_tokens`.
364
+ */
365
+ buildCacheParams() {
366
+ return {
367
+ ...(this.config.promptCacheKey
368
+ ? { prompt_cache_key: this.config.promptCacheKey }
369
+ : {}),
370
+ ...(this.config.promptCacheRetention
371
+ ? { prompt_cache_retention: this.config.promptCacheRetention }
372
+ : {}),
373
+ };
374
+ }
186
375
  async process(_input) {
187
376
  return "";
188
377
  }
@@ -217,7 +406,7 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
217
406
  try {
218
407
  const inputMessages = transformers_1.openAiTransformer.toProvider(this.history.getEntries());
219
408
  this.startTurnTimer();
220
- const response = await this.client.responses.create({
409
+ const response = await this.createResponse({
221
410
  model: this.config.model,
222
411
  max_output_tokens: this.config.maxTokens,
223
412
  input: inputMessages,
@@ -228,6 +417,7 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
228
417
  // Note: Responses API doesn't support seed, presence_penalty, frequency_penalty, stop
229
418
  user: this.config.user,
230
419
  ...this.buildReasoningParams(),
420
+ ...this.buildCacheParams(),
231
421
  }, { signal: options?.signal });
232
422
  this.emit(AgentEvent_1.AgentEvent.AFTER_EXECUTE, response);
233
423
  return await this.handleResponse(response, options);
@@ -242,16 +432,16 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
242
432
  throw abortError;
243
433
  }
244
434
  if (error && typeof error === "object" && "error" in error) {
245
- const openAIError = error;
246
- const apiError = new AgentError_1.ApiError(`OpenAI API error: ${openAIError.error.message || "Unknown error"}`, openAIError.status, openAIError.error);
247
- if (openAIError.error.code === "insufficient_quota") {
435
+ const openAIError = describeOpenAIError(error);
436
+ const apiError = new AgentError_1.ApiError(`OpenAI API error: ${openAIError.message}`, openAIError.status, openAIError.body);
437
+ if (openAIError.code === "insufficient_quota") {
248
438
  apiError.message =
249
439
  "OpenAI API quota exceeded. Please check your billing details.";
250
440
  }
251
441
  this.emit(AgentEvent_1.AgentEvent.ERROR, apiError);
252
442
  // Report error to viz
253
443
  if (this.vizEventId) {
254
- VizReporter_1.vizReporter.agentError(this.vizEventId, "ApiError", apiError.message, openAIError.error.code === "rate_limit_exceeded");
444
+ VizReporter_1.vizReporter.agentError(this.vizEventId, "ApiError", apiError.message, openAIError.code === "rate_limit_exceeded");
255
445
  this.vizEventId = undefined;
256
446
  }
257
447
  throw apiError;
@@ -344,7 +534,7 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
344
534
  try {
345
535
  const inputMessages = transformers_1.openAiTransformer.toProvider(this.history.getEntries());
346
536
  this.startTurnTimer();
347
- const newResponse = await this.client.responses.create({
537
+ const newResponse = await this.createResponse({
348
538
  model: this.config.model,
349
539
  max_output_tokens: this.config.maxTokens,
350
540
  input: inputMessages,
@@ -355,14 +545,15 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
355
545
  // Note: Responses API doesn't support seed, presence_penalty, frequency_penalty, stop
356
546
  user: this.config.user,
357
547
  ...this.buildReasoningParams(),
548
+ ...this.buildCacheParams(),
358
549
  }, { signal: options?.signal });
359
550
  this.emit(AgentEvent_1.AgentEvent.AFTER_EXECUTE, newResponse);
360
551
  return this.handleResponse(newResponse, options);
361
552
  }
362
553
  catch (error) {
363
554
  if (error && typeof error === "object" && "error" in error) {
364
- const openAIError = error;
365
- const apiError = new AgentError_1.ApiError(`OpenAI API error during tool response: ${openAIError.error.message || "Unknown error"}`, openAIError.status, openAIError.error);
555
+ const openAIError = describeOpenAIError(error);
556
+ const apiError = new AgentError_1.ApiError(`OpenAI API error during tool response: ${openAIError.message}`, openAIError.status, openAIError.body);
366
557
  this.emit(AgentEvent_1.AgentEvent.ERROR, apiError);
367
558
  throw apiError;
368
559
  }
@@ -501,11 +692,11 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
501
692
  throw this.withPartialTurn(error);
502
693
  }
503
694
  if (error && typeof error === "object" && "error" in error) {
504
- const openAIError = error;
505
- const apiError = new AgentError_1.ApiError(`OpenAI API error: ${openAIError.error.message || "Unknown error"}`, openAIError.status, openAIError.error);
695
+ const openAIError = describeOpenAIError(error);
696
+ const apiError = new AgentError_1.ApiError(`OpenAI API error: ${openAIError.message}`, openAIError.status, openAIError.body);
506
697
  this.emit(AgentEvent_1.AgentEvent.ERROR, apiError);
507
698
  if (this.vizEventId) {
508
- VizReporter_1.vizReporter.agentError(this.vizEventId, "ApiError", apiError.message, openAIError.error.code === "rate_limit_exceeded");
699
+ VizReporter_1.vizReporter.agentError(this.vizEventId, "ApiError", apiError.message, openAIError.code === "rate_limit_exceeded");
509
700
  this.vizEventId = undefined;
510
701
  }
511
702
  throw this.withPartialTurn(apiError);
@@ -525,7 +716,7 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
525
716
  async *streamTurn(options) {
526
717
  const inputMessages = transformers_1.openAiTransformer.toProvider(this.history.getEntries());
527
718
  this.startTurnTimer();
528
- const stream = await this.client.responses.create({
719
+ const stream = await this.client.responses.create(this.transformRequestParams({
529
720
  model: this.config.model,
530
721
  max_output_tokens: this.config.maxTokens,
531
722
  input: inputMessages,
@@ -536,8 +727,10 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
536
727
  top_p: this.config.topP,
537
728
  user: this.config.user,
538
729
  ...this.buildReasoningParams("auto"),
539
- }, { signal: options?.signal });
730
+ ...this.buildCacheParams(),
731
+ }), { signal: options?.signal });
540
732
  let completedEvent = null;
733
+ const streamedItems = [];
541
734
  // The Responses API builds the committed turn out of `response.completed`,
542
735
  // which only arrives on success, so the deltas are mirrored here as well:
543
736
  // without them a stream that dies mid-flight leaves nothing behind at all,
@@ -580,6 +773,11 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
580
773
  if (acc)
581
774
  acc.arguments += event.delta;
582
775
  }
776
+ if (event.type === "response.output_item.done") {
777
+ // The Codex backend leaves `output` empty on the terminal event, so
778
+ // the finished items are kept here — see repairStreamedOutput().
779
+ streamedItems.push(event.item);
780
+ }
583
781
  if (event.type === "response.completed") {
584
782
  completedEvent = event;
585
783
  if (event.response.usage) {
@@ -597,7 +795,7 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
597
795
  if (!completedEvent) {
598
796
  throw new AgentError_1.ExecutionError("OpenAI stream ended without a completed event");
599
797
  }
600
- const response = completedEvent.response;
798
+ const response = this.repairStreamedOutput(completedEvent.response, streamedItems);
601
799
  const toolCalls = response.output.filter((o) => o.type === "function_call");
602
800
  if (toolCalls.length > 0) {
603
801
  // As in handleResponse(): bail out before the assistant turn is written,
@@ -659,12 +857,16 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
659
857
  }
660
858
  }
661
859
  parseUsage(input) {
860
+ const inputDetails = input.input_tokens_details;
662
861
  return {
663
862
  input_tokens: input.input_tokens,
664
863
  output_tokens: input.output_tokens,
665
864
  total_tokens: input.total_tokens,
666
865
  // Reasoning tokens are already counted inside `output_tokens`.
667
866
  reasoning_tokens: input.output_tokens_details?.reasoning_tokens,
867
+ // Cache counts are part of `input_tokens`, not extra on top of it.
868
+ cache_read_tokens: inputDetails?.cached_tokens,
869
+ cache_write_tokens: inputDetails?.cache_write_tokens,
668
870
  };
669
871
  }
670
872
  }