@warlock.js/ai-openai 5.25.0 → 5.27.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/cjs/index.cjs +1043 -272
  3. package/cjs/index.cjs.map +1 -1
  4. package/esm/config.type.d.mts +36 -2
  5. package/esm/config.type.d.mts.map +1 -1
  6. package/esm/known-reasoning-models.mjs +4 -2
  7. package/esm/known-reasoning-models.mjs.map +1 -1
  8. package/esm/known-vision-models.mjs +3 -0
  9. package/esm/known-vision-models.mjs.map +1 -1
  10. package/esm/model.mjs +52 -99
  11. package/esm/model.mjs.map +1 -1
  12. package/esm/responses-model.mjs +329 -0
  13. package/esm/responses-model.mjs.map +1 -0
  14. package/esm/sdk.d.mts +7 -0
  15. package/esm/sdk.d.mts.map +1 -1
  16. package/esm/sdk.mjs +21 -1
  17. package/esm/sdk.mjs.map +1 -1
  18. package/esm/utils/build-usage.mjs +23 -0
  19. package/esm/utils/build-usage.mjs.map +1 -0
  20. package/esm/utils/index.mjs +9 -1
  21. package/esm/utils/map-responses-status.mjs +54 -0
  22. package/esm/utils/map-responses-status.mjs.map +1 -0
  23. package/esm/utils/prompt-cache-params.mjs +17 -0
  24. package/esm/utils/prompt-cache-params.mjs.map +1 -0
  25. package/esm/utils/reasoning-effort.mjs +35 -0
  26. package/esm/utils/reasoning-effort.mjs.map +1 -0
  27. package/esm/utils/reasoning-replay.mjs +85 -0
  28. package/esm/utils/reasoning-replay.mjs.map +1 -0
  29. package/esm/utils/structured-output.mjs +84 -0
  30. package/esm/utils/structured-output.mjs.map +1 -0
  31. package/esm/utils/to-openai-messages.mjs +49 -5
  32. package/esm/utils/to-openai-messages.mjs.map +1 -1
  33. package/esm/utils/to-openai-tools.mjs +3 -3
  34. package/esm/utils/to-openai-tools.mjs.map +1 -1
  35. package/esm/utils/to-responses-input.mjs +112 -0
  36. package/esm/utils/to-responses-input.mjs.map +1 -0
  37. package/esm/utils/to-responses-tools.mjs +51 -0
  38. package/esm/utils/to-responses-tools.mjs.map +1 -0
  39. package/esm/utils/wrap-openai-error.mjs +7 -2
  40. package/esm/utils/wrap-openai-error.mjs.map +1 -1
  41. package/llms-full.txt +43 -4
  42. package/llms.txt +1 -1
  43. package/package.json +4 -4
  44. package/skills/setup-openai/SKILL.md +43 -4
package/cjs/index.cjs CHANGED
@@ -31,6 +31,224 @@ openai = __toESM(openai, 1);
31
31
  let _warlock_js_ai = require("@warlock.js/ai");
32
32
  let _warlock_js_logger = require("@warlock.js/logger");
33
33
 
34
+ //#region ../ai-openai/src/utils/build-usage.ts
35
+ /**
36
+ * Build the neutral `Usage` from extracted counts. The optional detail
37
+ * counters are emitted only when the provider reports a positive value, so
38
+ * non-reasoning / uncached calls keep the lean `{ input, output, total }`
39
+ * shape. Shared by the Chat Completions and Responses adapters so both
40
+ * apply the same rule.
41
+ */
42
+ function buildUsage(counts) {
43
+ const { input, output, total, cachedTokens, reasoningTokens, cacheWriteTokens } = counts;
44
+ return {
45
+ input,
46
+ output,
47
+ total,
48
+ ...cachedTokens !== void 0 && cachedTokens > 0 ? { cachedTokens } : {},
49
+ ...reasoningTokens !== void 0 && reasoningTokens > 0 ? { reasoningTokens } : {},
50
+ ...cacheWriteTokens !== void 0 && cacheWriteTokens > 0 ? { cacheWriteTokens } : {}
51
+ };
52
+ }
53
+
54
+ //#endregion
55
+ //#region ../ai-openai/src/utils/wrap-openai-error.ts
56
+ /**
57
+ * Wrap any thrown value caught inside the OpenAI adapter into the
58
+ * appropriate `@warlock.js/ai` `AIError` subclass.
59
+ *
60
+ * **Dispatch strategy.** Prefers `APIError.code` when present (stable
61
+ * machine identifier across SDK versions), falls back to `status` when
62
+ * `code` is missing (common with proxied deployments that strip the
63
+ * field). Name-based detection (`APIConnectionTimeoutError`) catches
64
+ * transport-layer errors that never produced an HTTP response.
65
+ *
66
+ * `AIError` instances are returned unchanged — callers can pass the
67
+ * error through `try/catch/throw wrap(e)` pipelines without accidental
68
+ * double-wrapping.
69
+ *
70
+ * @example
71
+ * try {
72
+ * return await this.client.chat.completions.create(...);
73
+ * } catch (thrown) {
74
+ * throw wrapOpenAIError(thrown);
75
+ * }
76
+ */
77
+ function wrapOpenAIError(thrown) {
78
+ if (thrown instanceof _warlock_js_ai.AIError) return thrown;
79
+ const shape = toShape(thrown);
80
+ const context = buildContext(thrown, shape);
81
+ const message = shape.message ?? (thrown instanceof Error ? thrown.message : String(thrown));
82
+ if (isTimeout(thrown, shape)) return new _warlock_js_ai.ProviderTimeoutError(message, {
83
+ cause: thrown,
84
+ context
85
+ });
86
+ if (shape.status === 401 || shape.code === "invalid_api_key") return new _warlock_js_ai.ProviderAuthError(message, {
87
+ cause: thrown,
88
+ context
89
+ });
90
+ if (shape.code === "insufficient_quota") return new _warlock_js_ai.QuotaExceededError(message, {
91
+ cause: thrown,
92
+ context
93
+ });
94
+ if (shape.status === 429 || shape.code === "rate_limit_exceeded") return new _warlock_js_ai.ProviderRateLimitError(message, {
95
+ cause: thrown,
96
+ context,
97
+ retryAfter: parseRetryAfter(shape.headers)
98
+ });
99
+ if (shape.code === "context_length_exceeded") return new _warlock_js_ai.ContextLengthExceededError(message, {
100
+ cause: thrown,
101
+ context
102
+ });
103
+ if (shape.code === "content_filter") return new _warlock_js_ai.ContentFilterError(message, {
104
+ cause: thrown,
105
+ context,
106
+ reason: message
107
+ });
108
+ if (typeof shape.status === "number" && shape.status >= 400 && shape.status < 500) return new _warlock_js_ai.InvalidRequestError(message, {
109
+ cause: thrown,
110
+ context
111
+ });
112
+ return new _warlock_js_ai.ProviderError(message, {
113
+ cause: thrown,
114
+ context
115
+ });
116
+ }
117
+ /**
118
+ * Read the raw error shape without depending on `instanceof APIError`
119
+ * — some consumers wrap the SDK, and proxies sometimes strip the
120
+ * prototype chain. Duck-typing on the visible fields is resilient to
121
+ * both.
122
+ */
123
+ function toShape(thrown) {
124
+ if (thrown instanceof openai.default.APIError) return {
125
+ status: thrown.status,
126
+ code: thrown.code,
127
+ message: thrown.message,
128
+ type: thrown.type,
129
+ param: thrown.param,
130
+ headers: thrown.headers,
131
+ name: thrown.name
132
+ };
133
+ if (typeof thrown === "object" && thrown !== null) {
134
+ const raw = thrown;
135
+ return {
136
+ status: typeof raw.status === "number" ? raw.status : void 0,
137
+ code: typeof raw.code === "string" ? raw.code : void 0,
138
+ message: typeof raw.message === "string" ? raw.message : void 0,
139
+ type: typeof raw.type === "string" ? raw.type : void 0,
140
+ param: typeof raw.param === "string" ? raw.param : void 0,
141
+ headers: typeof raw.headers === "object" && raw.headers !== null ? raw.headers : void 0,
142
+ name: typeof raw.name === "string" ? raw.name : void 0
143
+ };
144
+ }
145
+ return {};
146
+ }
147
+ /**
148
+ * Decide whether the thrown value represents a timeout. OpenAI's SDK
149
+ * throws `APIConnectionTimeoutError` for transport-level timeouts, and
150
+ * Node surfaces `ETIMEDOUT` / `ECONNABORTED` on the lower socket
151
+ * layer. Either signal counts.
152
+ */
153
+ function isTimeout(thrown, shape) {
154
+ if (thrown instanceof openai.default.APIConnectionTimeoutError) return true;
155
+ if (shape.name === "APIConnectionTimeoutError") return true;
156
+ if (shape.code === "ETIMEDOUT" || shape.code === "ECONNABORTED") return true;
157
+ return false;
158
+ }
159
+ /**
160
+ * Attach the raw diagnostic fields to `error.context` so consumers
161
+ * have everything the provider surfaced without each subclass having
162
+ * to redeclare them. Never includes `cause` — that lives on
163
+ * `error.cause`.
164
+ */
165
+ function buildContext(thrown, shape) {
166
+ const context = {};
167
+ if (shape.status !== void 0) context.status = shape.status;
168
+ if (shape.code) context.code = shape.code;
169
+ if (shape.type) context.type = shape.type;
170
+ if (shape.param) context.param = shape.param;
171
+ const requestId = readRequestId(thrown);
172
+ if (requestId) context.requestId = requestId;
173
+ return context;
174
+ }
175
+ /**
176
+ * The request id OpenAI returned. Current SDKs expose it as
177
+ * `APIError.requestID`; older ones and hand-rolled errors use
178
+ * `request_id` / `requestId`. Extract defensively.
179
+ */
180
+ function readRequestId(thrown) {
181
+ if (typeof thrown !== "object" || thrown === null) return;
182
+ const raw = thrown;
183
+ if (typeof raw.request_id === "string") return raw.request_id;
184
+ if (typeof raw.requestId === "string") return raw.requestId;
185
+ if (typeof raw.requestID === "string") return raw.requestID;
186
+ }
187
+ /**
188
+ * Parse the `Retry-After` response header (seconds per HTTP spec)
189
+ * into milliseconds so consumers can feed it straight to `setTimeout`.
190
+ * Returns `undefined` when missing or unparseable.
191
+ */
192
+ function parseRetryAfter(headers) {
193
+ if (!headers) return;
194
+ const raw = headers["retry-after"] ?? headers["Retry-After"];
195
+ if (!raw) return;
196
+ const seconds = Number(raw);
197
+ if (!Number.isFinite(seconds) || seconds < 0) return;
198
+ return Math.round(seconds * 1e3);
199
+ }
200
+
201
+ //#endregion
202
+ //#region ../ai-openai/src/utils/map-responses-status.ts
203
+ /**
204
+ * Derive our `FinishReason` from a Responses terminal state. Responses has
205
+ * no `finish_reason`, so it comes from `status`, `incomplete_details` and the
206
+ * output items (design section 2, decision 5):
207
+ *
208
+ * - `completed` with a function call -> `tool_calls`; without -> `stop`
209
+ * (a refusal is ordinary text and also `stop`).
210
+ * - `incomplete` / `max_output_tokens` and `max_messages` -> `length`.
211
+ * - `incomplete` / `content_filter` and `steered` (or an unknown / missing
212
+ * reason) -> `error`. There is no neutral `content_filter` value.
213
+ * - `cancelled`, `queued`, `in_progress` at a terminal read -> `error`.
214
+ * - `failed` is not mapped here: callers throw it via {@link toResponseFailure}.
215
+ *
216
+ * A missing `status` is treated as `completed` (the SDK types it optional).
217
+ */
218
+ function mapResponsesFinishReason(state) {
219
+ const status = state.status ?? "completed";
220
+ if (status === "completed") return state.hasToolCalls ? "tool_calls" : "stop";
221
+ if (status === "incomplete") {
222
+ const reason = state.incomplete_details?.reason;
223
+ return reason === "max_output_tokens" || reason === "max_messages" ? "length" : "error";
224
+ }
225
+ return "error";
226
+ }
227
+ /**
228
+ * Turn a `failed` Responses payload into the same typed `AIError` the Chat
229
+ * path throws for a provider failure, by running it through
230
+ * `wrapOpenAIError` (so `rate_limit_exceeded` becomes a
231
+ * `ProviderRateLimitError`, everything else a `ProviderError`, with `code`
232
+ * and request id kept on `context`). The response id and model are added so
233
+ * the failure is traceable.
234
+ */
235
+ function toResponseFailure(response, model) {
236
+ const error = response.error;
237
+ const requestId = response._request_id;
238
+ const wrapped = wrapOpenAIError({
239
+ code: error?.code,
240
+ message: error?.message ?? "OpenAI response failed without an error object.",
241
+ ...requestId ? { request_id: requestId } : {}
242
+ });
243
+ if (wrapped.context) Object.assign(wrapped.context, {
244
+ provider: "openai",
245
+ model,
246
+ responseId: response.id
247
+ });
248
+ return wrapped;
249
+ }
250
+
251
+ //#endregion
34
252
  //#region ../ai-openai/src/utils/map-finish-reason.ts
35
253
  const finishReasonMap = {
36
254
  stop: "stop",
@@ -50,6 +268,219 @@ function mapFinishReason(raw) {
50
268
  return finishReasonMap[raw ?? ""] ?? "error";
51
269
  }
52
270
 
271
+ //#endregion
272
+ //#region ../ai-openai/src/utils/prompt-cache-params.ts
273
+ /**
274
+ * Build the OpenAI-only prompt-cache request fields from the model config.
275
+ * Returns nothing for any provider label other than `"openai"`: wrapper
276
+ * endpoints use the same transport but may reject these OpenAI-only fields.
277
+ */
278
+ function buildPromptCacheParams(provider, config) {
279
+ if (provider !== "openai") return {};
280
+ return {
281
+ ...config.promptCacheKey !== void 0 ? { prompt_cache_key: config.promptCacheKey } : {},
282
+ ...config.promptCacheRetention !== void 0 ? { prompt_cache_retention: config.promptCacheRetention } : {}
283
+ };
284
+ }
285
+
286
+ //#endregion
287
+ //#region ../ai-openai/src/utils/reasoning-effort.ts
288
+ /**
289
+ * The installed OpenAI SDK 7.23.0 declares `reasoning_effort` as
290
+ * `none | minimal | low | medium | high | xhigh | max` in
291
+ * `openai/src/resources/shared.ts:367`. Its type notes that model support
292
+ * varies but does not provide a model-family-specific accepted set, so map by
293
+ * that union alone. This exhaustive record makes a future core level a
294
+ * typecheck failure until its OpenAI clamp is consciously selected.
295
+ *
296
+ * Both the Chat Completions adapter (`reasoning_effort`) and the Responses
297
+ * adapter (`reasoning.effort`) read this one table, so the two can never
298
+ * disagree on how a neutral level reaches the wire.
299
+ */
300
+ const EFFORT_TO_OPENAI_EFFORT = {
301
+ none: "none",
302
+ minimal: "minimal",
303
+ low: "low",
304
+ medium: "medium",
305
+ high: "high",
306
+ xhigh: "xhigh",
307
+ max: "max"
308
+ };
309
+ /**
310
+ * Map a neutral `ReasoningEffort` to the value OpenAI accepts.
311
+ *
312
+ * @example
313
+ * toOpenAIReasoningEffort("high"); // "high"
314
+ */
315
+ function toOpenAIReasoningEffort(effort) {
316
+ return EFFORT_TO_OPENAI_EFFORT[effort];
317
+ }
318
+
319
+ //#endregion
320
+ //#region ../ai-openai/src/utils/structured-output.ts
321
+ /**
322
+ * Map an explicit `responseFormat` override to the default
323
+ * `structuredOutput` capability. Loose wire modes (`"json_object"`,
324
+ * `"text"`) don't enforce shape, so the agent needs to see the soft
325
+ * schema hint in the system prompt — that only happens when the
326
+ * capability is `false`. Default (no override) stays `true` to
327
+ * preserve the prior assumption that OpenAI models support strict
328
+ * structured output.
329
+ */
330
+ function inferStructuredOutput(responseFormat) {
331
+ if (responseFormat === "json_object" || responseFormat === "text") return false;
332
+ return true;
333
+ }
334
+ /**
335
+ * Decide the structured-output mode for a call.
336
+ *
337
+ * When `override` (the `responseFormat` config) is set, it wins: `"text"`
338
+ * emits nothing, `"json_object"` always picks the loose mode, and
339
+ * `"json_schema"` picks strict mode (with the same strict-compatibility
340
+ * safety check - a malformed schema still degrades to `json_object` rather
341
+ * than 400). The override exists because some targets (older OpenAI models,
342
+ * OpenRouter routes, Ollama OpenAI-compat) reject strict `json_schema`
343
+ * outright.
344
+ *
345
+ * When the override is omitted, strict `json_schema` mode (token-level
346
+ * enforcement) is used only when the schema is a proper root-object JSON
347
+ * Schema whose every object lists all of its properties in `required`. For
348
+ * anything else it falls back to loose `json_object` mode. Client-side
349
+ * Standard Schema `validate()` still enforces the full shape.
350
+ */
351
+ function planStructuredOutput(responseSchema, override) {
352
+ if (!responseSchema || override === "text") return { mode: "none" };
353
+ if (override === "json_object") return { mode: "json_object" };
354
+ if (isStrictCompatible(responseSchema)) return {
355
+ mode: "json_schema",
356
+ schema: responseSchema
357
+ };
358
+ return { mode: "json_object" };
359
+ }
360
+ /**
361
+ * OpenAI strict `json_schema` mode requires the root to be a JSON Schema
362
+ * object type (`{ type: "object", properties: ... }`). Anything else
363
+ * (top-level arrays, primitives, unknown shapes) is rejected with a 400
364
+ * before a token is sampled. Checked structurally here so the first call
365
+ * doesn't crash on a malformed extraction.
366
+ */
367
+ function isStrictCompatible(schema) {
368
+ return schema.type === "object" && typeof schema.properties === "object" && schema.properties !== null && isStrictSafeNode(schema);
369
+ }
370
+ /**
371
+ * Recursively check the one strict-mode rule schemas most often trip on:
372
+ * every object must list ALL of its `properties` in `required` (OpenAI
373
+ * strict has no notion of optional - optional fields must be expressed as
374
+ * nullable, e.g. `type: ["string", "null"]`, and still appear in
375
+ * `required`). A schema that violates this anywhere in the tree is NOT sent
376
+ * in strict mode, so a hand-built or optional-bearing schema can't 400 the
377
+ * call ("'required' ... must include every key in properties").
378
+ */
379
+ function isStrictSafeNode(node) {
380
+ if (!node || typeof node !== "object") return true;
381
+ const record = node;
382
+ if (record.type === "object" && record.properties && typeof record.properties === "object") {
383
+ const properties = record.properties;
384
+ const keys = Object.keys(properties);
385
+ const required = Array.isArray(record.required) ? record.required : [];
386
+ if (keys.some((key) => !required.includes(key))) return false;
387
+ for (const key of keys) if (!isStrictSafeNode(properties[key])) return false;
388
+ }
389
+ if (record.items !== void 0 && !isStrictSafeNode(record.items)) return false;
390
+ for (const branch of [
391
+ "anyOf",
392
+ "allOf",
393
+ "oneOf"
394
+ ]) {
395
+ const value = record[branch];
396
+ if (Array.isArray(value) && value.some((sub) => !isStrictSafeNode(sub))) return false;
397
+ }
398
+ return true;
399
+ }
400
+
401
+ //#endregion
402
+ //#region ../ai-openai/src/utils/reasoning-replay.ts
403
+ /**
404
+ * Key under `ModelToolCallRequest.providerMetadata` owned by the Responses
405
+ * adapter. This name and the shape below are PERSISTED in users' conversation
406
+ * memory, so they are part of the public contract: do not rename.
407
+ */
408
+ const RESPONSES_METADATA_KEY = "openaiResponses";
409
+ /**
410
+ * Keep only the reasoning items that can actually be replayed. With
411
+ * `store: false` the server keeps nothing, so an item without
412
+ * `encrypted_content` has nothing to restore and replaying its bare `id`
413
+ * would be rejected as an unknown item.
414
+ */
415
+ function pickReplayableReasoning(items) {
416
+ return items.filter((item) => typeof item.encrypted_content === "string" && item.encrypted_content !== "");
417
+ }
418
+ /**
419
+ * Attach the turn's reasoning items to the FIRST tool call only (parallel
420
+ * calls do not each repeat the blobs). Returns the calls unchanged when
421
+ * there is nothing to replay or no tool call to ride on (a text-only turn is
422
+ * deliberately not replayed, decision 2). Does not mutate its input.
423
+ */
424
+ function attachReasoningReplay(toolCalls, model, reasoningItems) {
425
+ const replayable = pickReplayableReasoning(reasoningItems);
426
+ const [first, ...rest] = toolCalls;
427
+ if (first === void 0 || replayable.length === 0) return [...toolCalls];
428
+ const metadata = {
429
+ model,
430
+ reasoningItems: replayable
431
+ };
432
+ return [{
433
+ ...first,
434
+ providerMetadata: {
435
+ ...first.providerMetadata,
436
+ [RESPONSES_METADATA_KEY]: metadata
437
+ }
438
+ }, ...rest];
439
+ }
440
+ /**
441
+ * Read the stored reasoning items off an assistant turn's tool calls, for
442
+ * replay on `model`. Returns `[]` when the key is absent or malformed, or
443
+ * when the stored `model` differs from `model` (encrypted blobs are not
444
+ * portable across models, design section 4). De-duplicates by item `id`.
445
+ */
446
+ function readReasoningReplay(toolCalls, model) {
447
+ const seen = /* @__PURE__ */ new Set();
448
+ const items = [];
449
+ for (const call of toolCalls) {
450
+ const stored = call.providerMetadata?.[RESPONSES_METADATA_KEY];
451
+ if (!isReplayMetadata(stored) || stored.model !== model) continue;
452
+ for (const item of stored.reasoningItems) {
453
+ if (item?.type !== "reasoning" || seen.has(item.id)) continue;
454
+ seen.add(item.id);
455
+ items.push(item);
456
+ }
457
+ }
458
+ return items;
459
+ }
460
+ /**
461
+ * Shape a stored reasoning item for the next request. Sends the four fields
462
+ * a stateless replay needs (`type`, `id`, `summary`, `encrypted_content`) and
463
+ * drops the response-only `status` and the optional raw `content`.
464
+ *
465
+ * UNVERIFIED: the SDK types accept the full item as input (`ResponseInputItem`
466
+ * includes `ResponseReasoningItem`) and do not say which fields the API
467
+ * refuses on replay with `store: false`. This is the conservative guess; a
468
+ * live probe settles it.
469
+ */
470
+ function toReplayReasoningItem(item) {
471
+ return {
472
+ type: "reasoning",
473
+ id: item.id,
474
+ summary: Array.isArray(item.summary) ? item.summary : [],
475
+ encrypted_content: item.encrypted_content
476
+ };
477
+ }
478
+ function isReplayMetadata(value) {
479
+ if (typeof value !== "object" || value === null) return false;
480
+ const candidate = value;
481
+ return typeof candidate.model === "string" && Array.isArray(candidate.reasoningItems);
482
+ }
483
+
53
484
  //#endregion
54
485
  //#region ../ai-openai/src/utils/to-openai-messages.ts
55
486
  /**
@@ -76,8 +507,8 @@ function mapFinishReason(raw) {
76
507
  * ]},
77
508
  * ]);
78
509
  */
79
- function toOpenAIMessages(messages) {
80
- return messages.map((m) => {
510
+ function toOpenAIMessages(messages, cacheBreakpoints = 0) {
511
+ return addPromptCacheBreakpoints(messages.map((m) => {
81
512
  if (m.role === "tool") return {
82
513
  role: "tool",
83
514
  content: stringifyContent(m.content),
@@ -103,7 +534,44 @@ function toOpenAIMessages(messages) {
103
534
  role: m.role,
104
535
  content: stringifyContent(m.content)
105
536
  };
106
- });
537
+ }), cacheBreakpoints);
538
+ }
539
+ /** Place explicit markers on the last requested user-content parts (maximum four). */
540
+ function addPromptCacheBreakpoints(messages, requested) {
541
+ let remaining = Math.min(4, Math.max(0, Math.floor(requested)));
542
+ if (remaining === 0) return messages;
543
+ const marked = [...messages];
544
+ for (let index = marked.length - 1; index >= 0 && remaining > 0; index--) {
545
+ const message = marked[index];
546
+ if (message === void 0 || message.role !== "user") continue;
547
+ if (typeof message.content === "string") {
548
+ marked[index] = {
549
+ ...message,
550
+ content: [{
551
+ type: "text",
552
+ text: message.content,
553
+ prompt_cache_breakpoint: { mode: "explicit" }
554
+ }]
555
+ };
556
+ remaining--;
557
+ continue;
558
+ }
559
+ const content = [...message.content];
560
+ for (let partIndex = content.length - 1; partIndex >= 0 && remaining > 0; partIndex--) {
561
+ const part = content[partIndex];
562
+ if (part === void 0) continue;
563
+ content[partIndex] = {
564
+ ...part,
565
+ prompt_cache_breakpoint: { mode: "explicit" }
566
+ };
567
+ remaining--;
568
+ }
569
+ marked[index] = {
570
+ ...message,
571
+ content
572
+ };
573
+ }
574
+ return marked;
107
575
  }
108
576
  /**
109
577
  * Multipart content is only meaningful on user messages — for any other
@@ -141,7 +609,7 @@ function toOpenAIContentPart(part) {
141
609
  };
142
610
  if (part.type === "image") return {
143
611
  type: "image_url",
144
- image_url: { url: "url" in part.source ? part.source.url : `data:${part.source.mediaType};base64,${part.source.base64}` }
612
+ image_url: { url: toImageUrl(part.source) }
145
613
  };
146
614
  if (part.type === "pdf") {
147
615
  if ("url" in part.source) throw new _warlock_js_ai.InvalidRequestError("OpenAI chat completions cannot fetch a remote-URL PDF; supply base64 document bytes instead.");
@@ -163,6 +631,13 @@ function toOpenAIContentPart(part) {
163
631
  };
164
632
  }
165
633
  /**
634
+ * Render an image source as a URL: a remote URL passes through, inlined
635
+ * base64 bytes become a `data:` URL. Shared with the Responses adapter.
636
+ */
637
+ function toImageUrl(source) {
638
+ return "url" in source ? source.url : `data:${source.mediaType};base64,${source.base64}`;
639
+ }
640
+ /**
166
641
  * Narrow a neutral audio media type to the two formats OpenAI's
167
642
  * `input_audio` accepts (`wav` / `mp3`). An unsupported type raises a
168
643
  * typed `InvalidRequestError` up front rather than a provider 400.
@@ -185,176 +660,187 @@ function toOpenAIAudioFormat(mediaType) {
185
660
  * await client.chat.completions.create({ model, messages, tools });
186
661
  */
187
662
  function toOpenAITools(tools) {
188
- if (!tools || tools.length === 0) return;
189
- return tools.map((tool) => ({
190
- type: "function",
191
- function: {
192
- name: tool.name,
193
- description: tool.description,
194
- parameters: toParameters(tool.input)
195
- }
196
- }));
197
- }
198
- /**
199
- * Resolve a tool's input schema to a JSON-Schema object. OpenAI's
200
- * function `parameters` expects an object root; anything else (or a
201
- * failed extraction) degrades to an empty-object schema so the tool
202
- * still registers and the model simply sees no parameters.
203
- */
204
- function toParameters(input) {
205
- const schema = (0, _warlock_js_ai.extractJsonSchema)(input);
206
- if (schema && schema.type === "object") return schema;
207
- return {
208
- type: "object",
209
- properties: {}
210
- };
211
- }
212
-
213
- //#endregion
214
- //#region ../ai-openai/src/utils/wrap-openai-error.ts
215
- /**
216
- * Wrap any thrown value caught inside the OpenAI adapter into the
217
- * appropriate `@warlock.js/ai` `AIError` subclass.
218
- *
219
- * **Dispatch strategy.** Prefers `APIError.code` when present (stable
220
- * machine identifier across SDK versions), falls back to `status` when
221
- * `code` is missing (common with proxied deployments that strip the
222
- * field). Name-based detection (`APIConnectionTimeoutError`) catches
223
- * transport-layer errors that never produced an HTTP response.
224
- *
225
- * `AIError` instances are returned unchanged — callers can pass the
226
- * error through `try/catch/throw wrap(e)` pipelines without accidental
227
- * double-wrapping.
228
- *
229
- * @example
230
- * try {
231
- * return await this.client.chat.completions.create(...);
232
- * } catch (thrown) {
233
- * throw wrapOpenAIError(thrown);
234
- * }
235
- */
236
- function wrapOpenAIError(thrown) {
237
- if (thrown instanceof _warlock_js_ai.AIError) return thrown;
238
- const shape = toShape(thrown);
239
- const context = buildContext(thrown, shape);
240
- const message = shape.message ?? (thrown instanceof Error ? thrown.message : String(thrown));
241
- if (isTimeout(thrown, shape)) return new _warlock_js_ai.ProviderTimeoutError(message, {
242
- cause: thrown,
243
- context
244
- });
245
- if (shape.status === 401 || shape.code === "invalid_api_key") return new _warlock_js_ai.ProviderAuthError(message, {
246
- cause: thrown,
247
- context
248
- });
249
- if (shape.code === "insufficient_quota") return new _warlock_js_ai.QuotaExceededError(message, {
250
- cause: thrown,
251
- context
252
- });
253
- if (shape.status === 429 || shape.code === "rate_limit_exceeded") return new _warlock_js_ai.ProviderRateLimitError(message, {
254
- cause: thrown,
255
- context,
256
- retryAfter: parseRetryAfter(shape.headers)
257
- });
258
- if (shape.code === "context_length_exceeded") return new _warlock_js_ai.ContextLengthExceededError(message, {
259
- cause: thrown,
260
- context
261
- });
262
- if (shape.code === "content_filter") return new _warlock_js_ai.ContentFilterError(message, {
263
- cause: thrown,
264
- context,
265
- reason: message
266
- });
267
- if (typeof shape.status === "number" && shape.status >= 400 && shape.status < 500) return new _warlock_js_ai.InvalidRequestError(message, {
268
- cause: thrown,
269
- context
270
- });
271
- return new _warlock_js_ai.ProviderError(message, {
272
- cause: thrown,
273
- context
274
- });
275
- }
276
- /**
277
- * Read the raw error shape without depending on `instanceof APIError`
278
- * — some consumers wrap the SDK, and proxies sometimes strip the
279
- * prototype chain. Duck-typing on the visible fields is resilient to
280
- * both.
281
- */
282
- function toShape(thrown) {
283
- if (thrown instanceof openai.default.APIError) return {
284
- status: thrown.status,
285
- code: thrown.code,
286
- message: thrown.message,
287
- type: thrown.type,
288
- headers: thrown.headers,
289
- name: thrown.name
290
- };
291
- if (typeof thrown === "object" && thrown !== null) {
292
- const raw = thrown;
293
- return {
294
- status: typeof raw.status === "number" ? raw.status : void 0,
295
- code: typeof raw.code === "string" ? raw.code : void 0,
296
- message: typeof raw.message === "string" ? raw.message : void 0,
297
- type: typeof raw.type === "string" ? raw.type : void 0,
298
- headers: typeof raw.headers === "object" && raw.headers !== null ? raw.headers : void 0,
299
- name: typeof raw.name === "string" ? raw.name : void 0
300
- };
301
- }
302
- return {};
663
+ if (!tools || tools.length === 0) return;
664
+ return tools.map((tool) => ({
665
+ type: "function",
666
+ function: {
667
+ name: tool.name,
668
+ description: tool.description,
669
+ parameters: toToolParameters(tool.input)
670
+ }
671
+ }));
303
672
  }
304
673
  /**
305
- * Decide whether the thrown value represents a timeout. OpenAI's SDK
306
- * throws `APIConnectionTimeoutError` for transport-level timeouts, and
307
- * Node surfaces `ETIMEDOUT` / `ECONNABORTED` on the lower socket
308
- * layer. Either signal counts.
674
+ * Resolve a tool's input schema to a JSON-Schema object. OpenAI's
675
+ * function `parameters` expects an object root; anything else (or a
676
+ * failed extraction) degrades to an empty-object schema so the tool
677
+ * still registers and the model simply sees no parameters.
309
678
  */
310
- function isTimeout(thrown, shape) {
311
- if (thrown instanceof openai.default.APIConnectionTimeoutError) return true;
312
- if (shape.name === "APIConnectionTimeoutError") return true;
313
- if (shape.code === "ETIMEDOUT" || shape.code === "ECONNABORTED") return true;
314
- return false;
679
+ function toToolParameters(input) {
680
+ const schema = (0, _warlock_js_ai.extractJsonSchema)(input);
681
+ if (schema && schema.type === "object") return schema;
682
+ return {
683
+ type: "object",
684
+ properties: {}
685
+ };
315
686
  }
687
+
688
+ //#endregion
689
+ //#region ../ai-openai/src/utils/to-responses-input.ts
316
690
  /**
317
- * Attach the raw diagnostic fields to `error.context` so consumers
318
- * have everything the provider surfaced without each subclass having
319
- * to redeclare them. Never includes `cause` — that lives on
320
- * `error.cause`.
691
+ * Convert vendor-neutral `Message[]` into the Responses request pieces.
692
+ *
693
+ * - The FIRST system message becomes `instructions` (decision 9: it avoids
694
+ * the `system` vs `developer` role question on reasoning models). Any
695
+ * later system message stays inline as a `system` role item.
696
+ * - `user` / `assistant` text becomes an `EasyInputMessage` (`{ role,
697
+ * content }`). Multipart user content becomes `input_text` / `input_image`
698
+ * parts; images always carry `detail: "auto"` because `ResponseInputImage.detail`
699
+ * is required by the SDK types.
700
+ * - An assistant message that requested tools becomes its text (when
701
+ * non-empty) followed by one top-level `function_call` item per call; our
702
+ * `ModelToolCallRequest.id` is the wire `call_id`. No item `id` is sent:
703
+ * with `store: false` nothing was persisted for it to refer to.
704
+ * - When `model` is given, reasoning items stored on the turn's tool calls
705
+ * (`providerMetadata.openaiResponses`, see `reasoning-replay.ts`) are
706
+ * emitted BEFORE the turn's text and `function_call` items (the order the
707
+ * API produced them), only if they were produced by that same model.
708
+ * Without `model` nothing is replayed.
709
+ * - A `tool` message becomes a top-level `function_call_output` item.
710
+ *
711
+ * PDF and audio parts are rejected with a typed `InvalidRequestError`: the
712
+ * Responses adapter declares those capabilities off, so they only arrive
713
+ * here if a caller bypasses the agent's capability gate.
714
+ *
715
+ * @example
716
+ * toResponsesInput([
717
+ * { role: "system", content: "Be brief." },
718
+ * { role: "user", content: "Hi" },
719
+ * ]);
720
+ * // { instructions: "Be brief.", input: [{ role: "user", content: "Hi" }] }
321
721
  */
322
- function buildContext(thrown, shape) {
323
- const context = {};
324
- if (shape.status !== void 0) context.status = shape.status;
325
- if (shape.code) context.code = shape.code;
326
- if (shape.type) context.type = shape.type;
327
- const requestId = readRequestId(thrown);
328
- if (requestId) context.requestId = requestId;
329
- return context;
722
+ function toResponsesInput(messages, model) {
723
+ const input = [];
724
+ let instructions;
725
+ let instructionsTaken = false;
726
+ for (const message of messages) {
727
+ if (message.role === "system") {
728
+ const text = stringifyContent(message.content);
729
+ if (!instructionsTaken) {
730
+ instructionsTaken = true;
731
+ instructions = text === "" ? void 0 : text;
732
+ continue;
733
+ }
734
+ input.push({
735
+ role: "system",
736
+ content: text
737
+ });
738
+ continue;
739
+ }
740
+ if (message.role === "tool") {
741
+ input.push({
742
+ type: "function_call_output",
743
+ call_id: message.toolCallId ?? "",
744
+ output: stringifyContent(message.content)
745
+ });
746
+ continue;
747
+ }
748
+ if (message.role === "assistant" && message.toolCalls && message.toolCalls.length > 0) {
749
+ const text = stringifyContent(message.content);
750
+ if (model !== void 0) for (const item of readReasoningReplay(message.toolCalls, model)) input.push(toReplayReasoningItem(item));
751
+ if (text !== "") input.push({
752
+ role: "assistant",
753
+ content: text
754
+ });
755
+ for (const call of message.toolCalls) input.push({
756
+ type: "function_call",
757
+ call_id: call.id,
758
+ name: call.name,
759
+ arguments: JSON.stringify(call.input ?? {})
760
+ });
761
+ continue;
762
+ }
763
+ if (message.role === "user" && Array.isArray(message.content)) {
764
+ input.push({
765
+ role: "user",
766
+ content: message.content.map(toResponsesContentPart)
767
+ });
768
+ continue;
769
+ }
770
+ input.push({
771
+ role: message.role,
772
+ content: stringifyContent(message.content)
773
+ });
774
+ }
775
+ return {
776
+ ...instructions !== void 0 ? { instructions } : {},
777
+ input
778
+ };
779
+ }
780
+ /** Map a resolved `ContentPart` to a Responses input content part (text and images only). */
781
+ function toResponsesContentPart(part) {
782
+ if (part.type === "text") return {
783
+ type: "input_text",
784
+ text: part.text
785
+ };
786
+ if (part.type === "image") return {
787
+ type: "input_image",
788
+ image_url: toImageUrl(part.source),
789
+ detail: "auto"
790
+ };
791
+ throw new _warlock_js_ai.InvalidRequestError(`OpenAI api: "responses" does not accept ${part.type} input in this adapter; use api: "chat" for ${part.type} attachments.`);
330
792
  }
793
+
794
+ //#endregion
795
+ //#region ../ai-openai/src/utils/to-responses-tools.ts
331
796
  /**
332
- * OpenAI puts the request id on `APIError.request_id`. Extract
333
- * defensively — both camel and snake keys exist across SDK versions.
797
+ * Convert vendor-neutral `ToolConfig[]` to Responses function tools.
798
+ *
799
+ * Responses tools are FLAT (`name` / `description` / `parameters` sit beside
800
+ * `type`, not under a nested `function` object) and `strict` is required by
801
+ * the SDK type (`FunctionTool.strict: boolean | null`). The Chat path never
802
+ * sets `strict` (non-strict is the OpenAI default there), so every tool is
803
+ * sent with `strict: false` to keep the same behavior.
804
+ *
805
+ * @example
806
+ * toResponsesTools([weatherTool]);
807
+ * // [{ type: "function", name: "getWeather", description: "...", parameters: {...}, strict: false }]
334
808
  */
335
- function readRequestId(thrown) {
336
- if (typeof thrown !== "object" || thrown === null) return;
337
- const raw = thrown;
338
- if (typeof raw.request_id === "string") return raw.request_id;
339
- if (typeof raw.requestId === "string") return raw.requestId;
809
+ function toResponsesTools(tools) {
810
+ if (!tools || tools.length === 0) return;
811
+ return tools.map((tool) => ({
812
+ type: "function",
813
+ name: tool.name,
814
+ description: tool.description,
815
+ parameters: toToolParameters(tool.input),
816
+ strict: false
817
+ }));
340
818
  }
341
819
  /**
342
- * Parse the `Retry-After` response header (seconds per HTTP spec)
343
- * into milliseconds so consumers can feed it straight to `setTimeout`.
344
- * Returns `undefined` when missing or unparseable.
820
+ * Read an optional `toolChoice` call option into the Responses `tool_choice`.
821
+ *
822
+ * `ModelCallOptions` does not declare `toolChoice` today (it only arrives
823
+ * through the open index signature), so the value is validated here rather
824
+ * than trusted. Accepted: `"none" | "auto" | "required"`, or a forced
825
+ * function as `{ name }` / `{ type: "function", name }` (the Chat-style
826
+ * nested `{ type: "function", function: { name } }` is also accepted). The
827
+ * Responses form is flat: `{ type: "function", name }`. Anything else yields
828
+ * `undefined`, leaving the provider default.
345
829
  */
346
- function parseRetryAfter(headers) {
347
- if (!headers) return;
348
- const raw = headers["retry-after"] ?? headers["Retry-After"];
349
- if (!raw) return;
350
- const seconds = Number(raw);
351
- if (!Number.isFinite(seconds) || seconds < 0) return;
352
- return Math.round(seconds * 1e3);
830
+ function toResponsesToolChoice(toolChoice) {
831
+ if (toolChoice === "none" || toolChoice === "auto" || toolChoice === "required") return toolChoice;
832
+ if (typeof toolChoice !== "object" || toolChoice === null) return;
833
+ const record = toolChoice;
834
+ const name = typeof record.name === "string" ? record.name : record.function?.name;
835
+ if (typeof name === "string" && name !== "") return {
836
+ type: "function",
837
+ name
838
+ };
353
839
  }
354
840
 
355
841
  //#endregion
356
842
  //#region ../ai-openai/src/embedder.ts
357
- const LOG_MODULE$4 = "ai.openai";
843
+ const LOG_MODULE$5 = "ai.openai";
358
844
  /**
359
845
  * OpenAI-backed implementation of `EmbedderContract`.
360
846
  *
@@ -419,7 +905,7 @@ var OpenAIEmbedder = class {
419
905
  * plus a camelCase usage object for the caller to shape.
420
906
  */
421
907
  async request(input) {
422
- this.logger.debug(LOG_MODULE$4, "embedder.request", "embeddings.create", {
908
+ this.logger.debug(LOG_MODULE$5, "embedder.request", "embeddings.create", {
423
909
  model: this.name,
424
910
  batch: Array.isArray(input),
425
911
  count: Array.isArray(input) ? input.length : 1
@@ -433,13 +919,13 @@ var OpenAIEmbedder = class {
433
919
  });
434
920
  } catch (thrown) {
435
921
  const wrapped = wrapOpenAIError(thrown);
436
- this.logger.error(LOG_MODULE$4, "embedder.error", wrapped.message, {
922
+ this.logger.error(LOG_MODULE$5, "embedder.error", wrapped.message, {
437
923
  code: wrapped.code,
438
924
  context: wrapped.context
439
925
  });
440
926
  throw wrapped;
441
927
  }
442
- this.logger.debug(LOG_MODULE$4, "embedder.response", "embeddings.create returned", {
928
+ this.logger.debug(LOG_MODULE$5, "embedder.response", "embeddings.create returned", {
443
929
  dimensions: response.data[0]?.embedding.length,
444
930
  usage: {
445
931
  promptTokens: response.usage.prompt_tokens,
@@ -463,7 +949,7 @@ var OpenAIEmbedder = class {
463
949
 
464
950
  //#endregion
465
951
  //#region ../ai-openai/src/image.ts
466
- const LOG_MODULE$3 = "ai.openai";
952
+ const LOG_MODULE$4 = "ai.openai";
467
953
  /** Map a neutral output container to its IANA media type. */
468
954
  function mediaTypeFor(format) {
469
955
  switch (format) {
@@ -522,7 +1008,7 @@ var OpenAIImageModel = class {
522
1008
  if (!isGptImage && responseFormat) body.response_format = responseFormat;
523
1009
  if (isGptImage && options?.format !== void 0) body.output_format = options.format;
524
1010
  if (options?.background !== void 0) body.background = options.background;
525
- this.logger.debug(LOG_MODULE$3, "image.request", "images.generate", {
1011
+ this.logger.debug(LOG_MODULE$4, "image.request", "images.generate", {
526
1012
  model: this.name,
527
1013
  count: options?.count ?? 1
528
1014
  });
@@ -531,7 +1017,7 @@ var OpenAIImageModel = class {
531
1017
  response = await this.client.images.generate(body, options?.signal ? { signal: options.signal } : void 0);
532
1018
  } catch (thrown) {
533
1019
  const wrapped = wrapOpenAIError(thrown);
534
- this.logger.error(LOG_MODULE$3, "image.error", wrapped.message, {
1020
+ this.logger.error(LOG_MODULE$4, "image.error", wrapped.message, {
535
1021
  code: wrapped.code,
536
1022
  context: wrapped.context
537
1023
  });
@@ -547,7 +1033,7 @@ var OpenAIImageModel = class {
547
1033
  output: 0,
548
1034
  total: 0
549
1035
  };
550
- this.logger.debug(LOG_MODULE$3, "image.response", "images.generate succeeded", {
1036
+ this.logger.debug(LOG_MODULE$4, "image.response", "images.generate succeeded", {
551
1037
  images: images.length,
552
1038
  usage
553
1039
  });
@@ -599,11 +1085,12 @@ const REASONING_CAPABLE_PREFIXES = [
599
1085
  "o1",
600
1086
  "o3",
601
1087
  "o4",
602
- "gpt-5"
1088
+ "gpt-5",
1089
+ "gpt-6"
603
1090
  ];
604
1091
  /**
605
1092
  * Infer whether a given OpenAI model name is a reasoning model (o-series
606
- * and the gpt-5 family) based on the known-prefix list. Unknown models
1093
+ * and the gpt-5 / gpt-6 families) based on the known-prefix list. Unknown models
607
1094
  * default to `false` so the adapter never forwards an unsupported
608
1095
  * `reasoning_effort` param to a non-reasoning model (which would 400).
609
1096
  *
@@ -611,6 +1098,7 @@ const REASONING_CAPABLE_PREFIXES = [
611
1098
  * inferReasoningCapability("o3-mini"); // → true
612
1099
  * inferReasoningCapability("o4-mini"); // → true
613
1100
  * inferReasoningCapability("gpt-5-pro"); // → true
1101
+ * inferReasoningCapability("gpt-6-astra"); // → true
614
1102
  * inferReasoningCapability("gpt-4o"); // → false
615
1103
  * inferReasoningCapability("custom-llm"); // → false
616
1104
  */
@@ -638,8 +1126,11 @@ const VISION_CAPABLE_PREFIXES = [
638
1126
  "gpt-4o",
639
1127
  "gpt-4-turbo",
640
1128
  "gpt-4.1",
1129
+ "gpt-5",
1130
+ "gpt-6",
641
1131
  "o1",
642
1132
  "o3",
1133
+ "o4",
643
1134
  "chatgpt-4o"
644
1135
  ];
645
1136
  /**
@@ -661,20 +1152,7 @@ function inferVisionCapability(modelName) {
661
1152
 
662
1153
  //#endregion
663
1154
  //#region ../ai-openai/src/model.ts
664
- const LOG_MODULE$2 = "ai.openai";
665
- /**
666
- * Map an explicit `responseFormat` override to the default
667
- * `structuredOutput` capability. Loose wire modes (`"json_object"`,
668
- * `"text"`) don't enforce shape, so the agent needs to see the soft
669
- * schema hint in the system prompt — that only happens when the
670
- * capability is `false`. Default (no override) stays `true` to
671
- * preserve the prior assumption that OpenAI models support strict
672
- * structured output.
673
- */
674
- function inferStructuredOutput(responseFormat) {
675
- if (responseFormat === "json_object" || responseFormat === "text") return false;
676
- return true;
677
- }
1155
+ const LOG_MODULE$3 = "ai.openai";
678
1156
  /**
679
1157
  * OpenAI-backed implementation of `ModelContract`.
680
1158
  *
@@ -715,6 +1193,11 @@ function inferStructuredOutput(responseFormat) {
715
1193
  var OpenAIModel = class {
716
1194
  constructor(client, config, provider = "openai") {
717
1195
  this.logger = _warlock_js_logger.log;
1196
+ if (config.api === "responses") throw new _warlock_js_ai.InvalidRequestError("OpenAIModel speaks Chat Completions only and cannot serve api: \"responses\". Use OpenAISDK.model({ api: \"responses\" }) on the direct OpenAI provider.", { context: {
1197
+ provider,
1198
+ model: config.name,
1199
+ api: config.api
1200
+ } });
718
1201
  this.client = client;
719
1202
  this.config = config;
720
1203
  this.name = config.name;
@@ -724,7 +1207,7 @@ var OpenAIModel = class {
724
1207
  structuredOutput: config.structuredOutput ?? inferStructuredOutput(config.responseFormat),
725
1208
  vision: config.vision ?? inferVisionCapability(config.name),
726
1209
  reasoning: config.reasoning ?? inferReasoningCapability(config.name),
727
- promptCaching: true,
1210
+ promptCaching: this.provider === "openai",
728
1211
  pdf: config.pdf ?? false,
729
1212
  audio: config.audio ?? false
730
1213
  };
@@ -736,7 +1219,7 @@ var OpenAIModel = class {
736
1219
  * instance's `ModelConfig` defaults for this call only.
737
1220
  */
738
1221
  async complete(messages, options) {
739
- this.logger.debug(LOG_MODULE$2, "request", "Starting call to chat.completions", {
1222
+ this.logger.debug(LOG_MODULE$3, "request", "Starting call to chat.completions", {
740
1223
  model: this.name,
741
1224
  messageCount: messages.length,
742
1225
  streaming: false,
@@ -746,16 +1229,16 @@ var OpenAIModel = class {
746
1229
  try {
747
1230
  response = await this.client.chat.completions.create({
748
1231
  model: this.name,
749
- messages: toOpenAIMessages(messages),
750
- temperature: options?.temperature ?? this.config.temperature,
751
- max_tokens: options?.maxTokens ?? this.config.maxTokens,
1232
+ messages: toOpenAIMessages(messages, this.promptCacheBreakpoints(options)),
1233
+ ...this.buildSamplingParams(options),
1234
+ ...buildPromptCacheParams(this.provider, this.config),
752
1235
  tools: toOpenAITools(options?.tools),
753
1236
  ...this.buildResponseFormat(options?.responseSchema),
754
1237
  ...this.buildReasoningParams(options?.reasoning, Boolean(options?.tools?.length))
755
1238
  }, options?.signal ? { signal: options.signal } : void 0);
756
1239
  } catch (thrown) {
757
1240
  const wrapped = wrapOpenAIError(thrown);
758
- this.logger.error(LOG_MODULE$2, "error", wrapped.message, {
1241
+ this.logger.error(LOG_MODULE$3, "error", wrapped.message, {
759
1242
  code: wrapped.code,
760
1243
  context: wrapped.context
761
1244
  });
@@ -769,7 +1252,7 @@ var OpenAIModel = class {
769
1252
  } });
770
1253
  const finishReason = mapFinishReason(choice.finish_reason);
771
1254
  const usage = this.extractUsage(response.usage);
772
- this.logger.debug(LOG_MODULE$2, "response", "call to chat.completions succeeded", {
1255
+ this.logger.debug(LOG_MODULE$3, "response", "call to chat.completions succeeded", {
773
1256
  finishReason,
774
1257
  usage
775
1258
  });
@@ -787,7 +1270,7 @@ var OpenAIModel = class {
787
1270
  * Callers consume it with `for await`.
788
1271
  */
789
1272
  async *stream(messages, options) {
790
- this.logger.debug(LOG_MODULE$2, "request", "Starting streaming call to chat.completions", {
1273
+ this.logger.debug(LOG_MODULE$3, "request", "Starting streaming call to chat.completions", {
791
1274
  model: this.name,
792
1275
  messageCount: messages.length,
793
1276
  streaming: true,
@@ -797,9 +1280,9 @@ var OpenAIModel = class {
797
1280
  try {
798
1281
  stream = await this.client.chat.completions.create({
799
1282
  model: this.name,
800
- messages: toOpenAIMessages(messages),
801
- temperature: options?.temperature ?? this.config.temperature,
802
- max_tokens: options?.maxTokens ?? this.config.maxTokens,
1283
+ messages: toOpenAIMessages(messages, this.promptCacheBreakpoints(options)),
1284
+ ...this.buildSamplingParams(options),
1285
+ ...buildPromptCacheParams(this.provider, this.config),
803
1286
  tools: toOpenAITools(options?.tools),
804
1287
  stream: true,
805
1288
  stream_options: { include_usage: true },
@@ -808,7 +1291,7 @@ var OpenAIModel = class {
808
1291
  }, options?.signal ? { signal: options.signal } : void 0);
809
1292
  } catch (thrown) {
810
1293
  const wrapped = wrapOpenAIError(thrown);
811
- this.logger.error(LOG_MODULE$2, "error", wrapped.message, {
1294
+ this.logger.error(LOG_MODULE$3, "error", wrapped.message, {
812
1295
  code: wrapped.code,
813
1296
  context: wrapped.context
814
1297
  });
@@ -863,14 +1346,14 @@ var OpenAIModel = class {
863
1346
  }
864
1347
  } catch (thrown) {
865
1348
  const wrapped = wrapOpenAIError(thrown);
866
- this.logger.error(LOG_MODULE$2, "error", wrapped.message, {
1349
+ this.logger.error(LOG_MODULE$3, "error", wrapped.message, {
867
1350
  code: wrapped.code,
868
1351
  context: wrapped.context
869
1352
  });
870
1353
  throw wrapped;
871
1354
  }
872
1355
  const finishReason = mapFinishReason(rawFinishReason);
873
- this.logger.debug(LOG_MODULE$2, "response", "Streaming call to chat.completions succeeded", {
1356
+ this.logger.debug(LOG_MODULE$3, "response", "Streaming call to chat.completions succeeded", {
874
1357
  finishReason,
875
1358
  usage
876
1359
  });
@@ -880,89 +1363,31 @@ var OpenAIModel = class {
880
1363
  usage
881
1364
  };
882
1365
  }
1366
+ /** Map the neutral cache hint only for the direct OpenAI provider. */
1367
+ promptCacheBreakpoints(options) {
1368
+ return this.provider === "openai" ? options?.cacheControl?.breakpoints ?? 0 : 0;
1369
+ }
883
1370
  /**
884
1371
  * Translate the neutral `responseSchema` option into OpenAI's
885
- * `response_format` parameter.
886
- *
887
- * When `config.responseFormat` is set, it wins: `"text"` emits no
888
- * `response_format` at all, `"json_object"` always picks the loose
889
- * mode, and `"json_schema"` picks strict mode (with the same
890
- * `isStrictCompatible` safety check — a malformed schema still
891
- * degrades to `json_object` rather than 400). The override exists
892
- * because some targets (older OpenAI models, OpenRouter routes,
893
- * Ollama OpenAI-compat) reject strict `json_schema` outright.
894
- *
895
- * When the override is omitted, uses strict `json_schema` mode
896
- * (token-level enforcement) only when the schema is a proper
897
- * root-object JSON Schema (`{ type: "object", properties: ... }`).
898
- * For anything else — malformed extractor output, non-object
899
- * schemas, or future shapes we haven't tested — falls back to loose
900
- * `json_object` mode, which guarantees *some* valid JSON without
901
- * enforcing shape. The agent's soft instruction already embeds the
902
- * schema text in the system prompt when the model declares no
903
- * native structured-output capability, so shape validation still
904
- * runs client-side via the Standard Schema `validate()` call.
1372
+ * `response_format` parameter. The mode decision (override handling,
1373
+ * strict-compatibility downgrade to `json_object`) lives in
1374
+ * `planStructuredOutput`, shared with the Responses adapter.
905
1375
  *
906
1376
  * Returns an empty spread when no schema was supplied, so the caller
907
1377
  * can unconditionally `...buildResponseFormat(...)` into the request.
908
1378
  */
909
1379
  buildResponseFormat(responseSchema) {
910
- if (!responseSchema) return {};
911
- const override = this.config.responseFormat;
912
- if (override === "text") return {};
913
- if (override === "json_object") return { response_format: { type: "json_object" } };
914
- if (this.isStrictCompatible(responseSchema)) return { response_format: {
1380
+ const plan = planStructuredOutput(responseSchema, this.config.responseFormat);
1381
+ if (plan.mode === "json_schema") return { response_format: {
915
1382
  type: "json_schema",
916
1383
  json_schema: {
917
1384
  name: "response",
918
- schema: responseSchema,
1385
+ schema: plan.schema,
919
1386
  strict: true
920
1387
  }
921
1388
  } };
922
- return { response_format: { type: "json_object" } };
923
- }
924
- /**
925
- * OpenAI strict `json_schema` mode requires the root to be a JSON
926
- * Schema object type (`{ type: "object", properties: ... }`). Anything
927
- * else (top-level arrays, primitives, unknown shapes) is rejected with
928
- * a 400 before a token is sampled. We check structurally here so the
929
- * first call doesn't crash on a malformed extraction — loose
930
- * `json_object` mode is a safe degradation.
931
- */
932
- isStrictCompatible(schema) {
933
- return schema.type === "object" && typeof schema.properties === "object" && schema.properties !== null && this.isStrictSafeNode(schema);
934
- }
935
- /**
936
- * Recursively check the one strict-mode rule schemas most often trip on:
937
- * every object must list ALL of its `properties` in `required` (OpenAI
938
- * strict has no notion of optional — optional fields must be expressed
939
- * as nullable, e.g. `type: ["string", "null"]`, and still appear in
940
- * `required`). A schema that violates this anywhere in the tree is NOT
941
- * sent in strict `json_schema` mode — it degrades to loose
942
- * `json_object` so a hand-built or optional-bearing schema can't 400
943
- * the call ("'required' ... must include every key in properties").
944
- * Client-side `validate()` still enforces the full shape.
945
- */
946
- isStrictSafeNode(node) {
947
- if (!node || typeof node !== "object") return true;
948
- const record = node;
949
- if (record.type === "object" && record.properties && typeof record.properties === "object") {
950
- const properties = record.properties;
951
- const keys = Object.keys(properties);
952
- const required = Array.isArray(record.required) ? record.required : [];
953
- if (keys.some((key) => !required.includes(key))) return false;
954
- for (const key of keys) if (!this.isStrictSafeNode(properties[key])) return false;
955
- }
956
- if (record.items !== void 0 && !this.isStrictSafeNode(record.items)) return false;
957
- for (const branch of [
958
- "anyOf",
959
- "allOf",
960
- "oneOf"
961
- ]) {
962
- const value = record[branch];
963
- if (Array.isArray(value) && value.some((sub) => !this.isStrictSafeNode(sub))) return false;
964
- }
965
- return true;
1389
+ if (plan.mode === "json_object") return { response_format: { type: "json_object" } };
1390
+ return {};
966
1391
  }
967
1392
  /**
968
1393
  * Normalize OpenAI's `usage` block (which may be absent on some responses
@@ -984,15 +1409,13 @@ var OpenAIModel = class {
984
1409
  output: 0,
985
1410
  total: 0
986
1411
  };
987
- const cachedTokens = raw.prompt_tokens_details?.cached_tokens;
988
- const reasoningTokens = raw.completion_tokens_details?.reasoning_tokens;
989
- return {
1412
+ return buildUsage({
990
1413
  input: raw.prompt_tokens,
991
1414
  output: raw.completion_tokens,
992
1415
  total: raw.total_tokens,
993
- ...cachedTokens !== void 0 && cachedTokens > 0 ? { cachedTokens } : {},
994
- ...reasoningTokens !== void 0 && reasoningTokens > 0 ? { reasoningTokens } : {}
995
- };
1416
+ cachedTokens: raw.prompt_tokens_details?.cached_tokens,
1417
+ reasoningTokens: raw.completion_tokens_details?.reasoning_tokens
1418
+ });
996
1419
  }
997
1420
  /**
998
1421
  * Translate the neutral `ModelCallOptions.reasoning` hint into OpenAI's
@@ -1029,10 +1452,27 @@ var OpenAIModel = class {
1029
1452
  * Returns an empty spread when nothing applies, so the caller can
1030
1453
  * unconditionally `...buildReasoningParams(...)` into the request.
1031
1454
  */
1455
+ /**
1456
+ * Output-length and sampling params, in current Chat Completions shapes only.
1457
+ *
1458
+ * - `max_completion_tokens`, never the legacy `max_tokens`: current
1459
+ * models (the gpt-5 family and later) answer `400 unsupported_parameter`
1460
+ * to `max_tokens`, and `max_completion_tokens` is accepted everywhere.
1461
+ * - No `temperature` for a reasoning-capable model: those accept only the
1462
+ * default value and reject any other with a 400.
1463
+ */
1464
+ buildSamplingParams(options) {
1465
+ const maxTokens = options?.maxTokens ?? this.config.maxTokens;
1466
+ const temperature = options?.temperature ?? this.config.temperature;
1467
+ return {
1468
+ max_completion_tokens: maxTokens,
1469
+ ...!this.capabilities.reasoning && { temperature }
1470
+ };
1471
+ }
1032
1472
  buildReasoningParams(reasoning, hasTools) {
1033
1473
  if (!this.capabilities.reasoning) return {};
1034
1474
  if (!reasoning?.effort) return hasTools ? { reasoning_effort: "none" } : {};
1035
- return { reasoning_effort: reasoning.effort };
1475
+ return { reasoning_effort: toOpenAIReasoningEffort(reasoning.effort) };
1036
1476
  }
1037
1477
  /**
1038
1478
  * Reshape OpenAI's `tool_calls` array into the neutral
@@ -1052,6 +1492,318 @@ var OpenAIModel = class {
1052
1492
  }
1053
1493
  };
1054
1494
 
1495
+ //#endregion
1496
+ //#region ../ai-openai/src/responses-model.ts
1497
+ const LOG_MODULE$2 = "ai.openai";
1498
+ /**
1499
+ * OpenAI Responses API implementation of `ModelContract` (`api: "responses"`).
1500
+ *
1501
+ * **Role.** A sibling of `OpenAIModel` that speaks `POST /v1/responses`
1502
+ * instead of Chat Completions. Its point is letting a tool-using agent run a
1503
+ * reasoning model with reasoning ON: the Chat path has to force
1504
+ * `reasoning_effort: "none"` whenever tools are attached, Responses does not.
1505
+ *
1506
+ * **Why a separate class.** Only `OpenAISDK.model()` constructs it, and only
1507
+ * for the direct `"openai"` provider. The wrapper packages (deepseek, groq,
1508
+ * xai, mistral) build `OpenAIModel` and cannot reach this class, so an
1509
+ * endpoint that does not implement `/v1/responses` is never sent there. The
1510
+ * constructor re-checks the provider label as a second lock.
1511
+ *
1512
+ * **Stateless.** Every request sends `store: false`; nothing is kept on the
1513
+ * server and no `previous_response_id` is used. Full history is re-sent each
1514
+ * call, exactly like the Chat path.
1515
+ *
1516
+ * **Reasoning replay.** Every request asks for
1517
+ * `include: ["reasoning.encrypted_content"]`. The `reasoning` items of a
1518
+ * tool-calling turn are stored on the FIRST tool call as
1519
+ * `providerMetadata.openaiResponses = { model, reasoningItems }` and sent back
1520
+ * before that turn's `function_call` items on the next request (only when the
1521
+ * stored model equals the current one). Text-only turns are not replayed.
1522
+ *
1523
+ * **Streaming.** `stream()` uses `responses.create({ stream: true })` and yields
1524
+ * the same chunk sequence as the Chat path: text `delta`s, then the
1525
+ * `tool-call`s, then one `done`.
1526
+ */
1527
+ var OpenAIResponsesModel = class {
1528
+ constructor(client, config, provider = "openai") {
1529
+ this.logger = _warlock_js_logger.log;
1530
+ if (provider !== "openai") throw new _warlock_js_ai.InvalidRequestError(`api: "responses" is supported only on the direct "openai" provider; got provider "${provider}".`, { context: {
1531
+ provider,
1532
+ model: config.name,
1533
+ api: "responses"
1534
+ } });
1535
+ this.client = client;
1536
+ this.config = config;
1537
+ this.name = config.name;
1538
+ this.provider = provider;
1539
+ this.pricing = config.pricing;
1540
+ this.capabilities = {
1541
+ structuredOutput: config.structuredOutput ?? inferStructuredOutput(config.responseFormat),
1542
+ vision: config.vision ?? inferVisionCapability(config.name),
1543
+ reasoning: config.reasoning ?? inferReasoningCapability(config.name),
1544
+ promptCaching: true,
1545
+ pdf: false,
1546
+ audio: false
1547
+ };
1548
+ }
1549
+ /**
1550
+ * Single-shot completion against `client.responses.create`. Sends the full
1551
+ * history as Responses `input` items, walks `response.output`, and reshapes
1552
+ * it into a vendor-neutral `ModelResponse`. Per-call `options` override the
1553
+ * instance defaults for this call only.
1554
+ */
1555
+ async complete(messages, options) {
1556
+ this.logger.debug(LOG_MODULE$2, "request", "Starting call to responses.create", {
1557
+ model: this.name,
1558
+ messageCount: messages.length,
1559
+ streaming: false,
1560
+ toolCount: options?.tools?.length ?? 0
1561
+ });
1562
+ let response;
1563
+ try {
1564
+ response = await this.client.responses.create(this.buildRequest(messages, options), options?.signal ? { signal: options.signal } : void 0);
1565
+ } catch (thrown) {
1566
+ throw this.logAndWrap(thrown);
1567
+ }
1568
+ if (response.status === "failed") throw this.logAndWrap(toResponseFailure(response, this.name));
1569
+ const output = Array.isArray(response.output) ? response.output : [];
1570
+ const { content, toolCalls } = this.readOutput(output);
1571
+ const finishReason = mapResponsesFinishReason({
1572
+ status: response.status,
1573
+ incomplete_details: response.incomplete_details,
1574
+ hasToolCalls: toolCalls !== void 0
1575
+ });
1576
+ const usage = this.extractUsage(response.usage);
1577
+ this.logger.debug(LOG_MODULE$2, "response", "call to responses.create succeeded", {
1578
+ finishReason,
1579
+ usage
1580
+ });
1581
+ return {
1582
+ content,
1583
+ finishReason,
1584
+ usage,
1585
+ ...toolCalls ? { toolCalls } : {}
1586
+ };
1587
+ }
1588
+ /**
1589
+ * Incremental streaming completion via `responses.create({ stream: true })`
1590
+ * (the SDK's `Stream<ResponseStreamEvent>`, `responses.d.ts:55`). Yields the
1591
+ * same neutral `ModelStreamChunk` sequence as the Chat path:
1592
+ *
1593
+ * - `response.output_text.delta` / `response.refusal.delta` -> `delta`.
1594
+ * - `response.output_item.done` with a completed `function_call` item ->
1595
+ * buffered; `reasoning` items are captured for replay. The `done` copy is
1596
+ * used because the `added` copy may be incomplete (`responses.d.ts:5711`).
1597
+ * - After the loop: the buffered `tool-call`s (the first carries the
1598
+ * replay metadata), then a single `done` with the finish reason and usage
1599
+ * from `response.completed` / `response.incomplete`.
1600
+ * - `response.failed` and `error` events throw a typed `AIError`; a stream
1601
+ * that ends with no terminal event throws `ProviderError`.
1602
+ * - Abort goes through the request `signal`; the SDK rejects the read and
1603
+ * the error is wrapped like any other.
1604
+ */
1605
+ async *stream(messages, options) {
1606
+ this.logger.debug(LOG_MODULE$2, "request", "Starting streaming call to responses.create", {
1607
+ model: this.name,
1608
+ messageCount: messages.length,
1609
+ streaming: true,
1610
+ toolCount: options?.tools?.length ?? 0
1611
+ });
1612
+ let events;
1613
+ try {
1614
+ events = await this.client.responses.create({
1615
+ ...this.buildRequest(messages, options),
1616
+ stream: true
1617
+ }, options?.signal ? { signal: options.signal } : void 0);
1618
+ } catch (thrown) {
1619
+ throw this.logAndWrap(thrown);
1620
+ }
1621
+ const toolCalls = [];
1622
+ const reasoningItems = [];
1623
+ let terminal;
1624
+ try {
1625
+ for await (const event of events) {
1626
+ if (event.type === "response.output_text.delta" || event.type === "response.refusal.delta") {
1627
+ if (event.delta) yield {
1628
+ type: "delta",
1629
+ content: event.delta
1630
+ };
1631
+ continue;
1632
+ }
1633
+ if (event.type === "response.output_item.done") {
1634
+ const { item } = event;
1635
+ if (item.type === "reasoning") reasoningItems.push(item);
1636
+ else if (item.type === "function_call" && item.status !== "incomplete") toolCalls.push(this.toToolCall(item));
1637
+ continue;
1638
+ }
1639
+ if (event.type === "response.completed" || event.type === "response.incomplete") {
1640
+ terminal = event.response;
1641
+ continue;
1642
+ }
1643
+ if (event.type === "response.failed") throw toResponseFailure(event.response, this.name);
1644
+ if (event.type === "error") throw wrapOpenAIError({
1645
+ code: event.code,
1646
+ message: event.message,
1647
+ param: event.param
1648
+ });
1649
+ }
1650
+ if (terminal === void 0) throw new _warlock_js_ai.ProviderError("OpenAI Responses stream ended before response.completed.", { context: {
1651
+ provider: this.provider,
1652
+ model: this.name
1653
+ } });
1654
+ } catch (thrown) {
1655
+ throw this.logAndWrap(thrown);
1656
+ }
1657
+ for (const call of attachReasoningReplay(toolCalls, this.name, reasoningItems)) yield {
1658
+ type: "tool-call",
1659
+ id: call.id,
1660
+ name: call.name,
1661
+ input: call.input,
1662
+ ...call.providerMetadata ? { providerMetadata: call.providerMetadata } : {}
1663
+ };
1664
+ const finishReason = mapResponsesFinishReason({
1665
+ status: terminal.status,
1666
+ incomplete_details: terminal.incomplete_details,
1667
+ hasToolCalls: toolCalls.length > 0
1668
+ });
1669
+ const usage = this.extractUsage(terminal.usage);
1670
+ this.logger.debug(LOG_MODULE$2, "response", "Streaming call to responses.create succeeded", {
1671
+ finishReason,
1672
+ usage
1673
+ });
1674
+ yield {
1675
+ type: "done",
1676
+ finishReason,
1677
+ usage
1678
+ };
1679
+ }
1680
+ /**
1681
+ * Assemble the `responses.create` body.
1682
+ *
1683
+ * - System prompt -> `instructions`; history -> `input` items.
1684
+ * - `store: false` always (the API default is `true` when omitted), plus
1685
+ * `include: ["reasoning.encrypted_content"]` for stateless reasoning replay.
1686
+ * - `max_output_tokens` caps reasoning and visible output together.
1687
+ * - `temperature` only for non-reasoning models (same rule as Chat).
1688
+ * - `tools` flat with `strict: false`; `tool_choice` only with tools.
1689
+ * - `reasoning.effort` only when the caller gave one, through the same
1690
+ * clamp table as Chat. With no explicit effort the provider default
1691
+ * applies EVEN with tools attached - that is the point of this API.
1692
+ */
1693
+ buildRequest(messages, options) {
1694
+ const { instructions, input } = toResponsesInput(messages, this.name);
1695
+ const tools = toResponsesTools(options?.tools);
1696
+ const toolChoice = tools ? toResponsesToolChoice(options?.toolChoice) : void 0;
1697
+ const maxTokens = options?.maxTokens ?? this.config.maxTokens;
1698
+ const temperature = options?.temperature ?? this.config.temperature;
1699
+ return {
1700
+ model: this.name,
1701
+ ...instructions !== void 0 ? { instructions } : {},
1702
+ input,
1703
+ store: false,
1704
+ include: ["reasoning.encrypted_content"],
1705
+ ...maxTokens !== void 0 ? { max_output_tokens: maxTokens } : {},
1706
+ ...!this.capabilities.reasoning && temperature !== void 0 ? { temperature } : {},
1707
+ ...tools ? { tools } : {},
1708
+ ...toolChoice !== void 0 ? { tool_choice: toolChoice } : {},
1709
+ ...this.buildTextFormat(options?.responseSchema),
1710
+ ...this.buildReasoningParams(options?.reasoning),
1711
+ ...buildPromptCacheParams(this.provider, this.config)
1712
+ };
1713
+ }
1714
+ /**
1715
+ * Structured output on Responses lives in `text.format`, and `name` /
1716
+ * `schema` / `strict` sit directly on the format object (Chat nests them
1717
+ * under `json_schema`). The strict-or-downgrade decision is the shared
1718
+ * `planStructuredOutput`.
1719
+ */
1720
+ buildTextFormat(responseSchema) {
1721
+ const plan = planStructuredOutput(responseSchema, this.config.responseFormat);
1722
+ if (plan.mode === "json_schema") return { text: { format: {
1723
+ type: "json_schema",
1724
+ name: "response",
1725
+ schema: plan.schema,
1726
+ strict: true
1727
+ } } };
1728
+ if (plan.mode === "json_object") return { text: { format: { type: "json_object" } } };
1729
+ return {};
1730
+ }
1731
+ /** `reasoning.effort` for reasoning-capable models, only when the caller set one. */
1732
+ buildReasoningParams(reasoning) {
1733
+ if (!this.capabilities.reasoning || !reasoning?.effort) return {};
1734
+ return { reasoning: { effort: toOpenAIReasoningEffort(reasoning.effort) } };
1735
+ }
1736
+ /**
1737
+ * Walk `response.output`: `message` items contribute their `output_text`
1738
+ * text and any `refusal` text (surfaced as ordinary content, decision 5);
1739
+ * each completed `function_call` item becomes one tool call with
1740
+ * `id = call_id`. `reasoning` items carry no visible content and are
1741
+ * not content. A `function_call` the API marked `incomplete` (truncated
1742
+ * arguments) is skipped rather than dispatched with empty input.
1743
+ * `reasoning` items are captured and attached to the first tool call for
1744
+ * replay (see `attachReasoningReplay`).
1745
+ */
1746
+ readOutput(output) {
1747
+ let content = "";
1748
+ const toolCalls = [];
1749
+ const reasoningItems = [];
1750
+ for (const item of output) {
1751
+ if (item.type === "reasoning") {
1752
+ reasoningItems.push(item);
1753
+ continue;
1754
+ }
1755
+ if (item.type === "message") {
1756
+ for (const part of item.content) content += part.type === "refusal" ? part.refusal : part.text;
1757
+ continue;
1758
+ }
1759
+ if (item.type === "function_call" && item.status !== "incomplete") toolCalls.push(this.toToolCall(item));
1760
+ }
1761
+ return {
1762
+ content,
1763
+ toolCalls: toolCalls.length > 0 ? attachReasoningReplay(toolCalls, this.name, reasoningItems) : void 0
1764
+ };
1765
+ }
1766
+ /** One completed `function_call` item as a neutral tool call (`call_id` -> `id`). */
1767
+ toToolCall(item) {
1768
+ return {
1769
+ id: item.call_id,
1770
+ name: item.name,
1771
+ input: (0, _warlock_js_ai.safeJsonParse)(item.arguments, {})
1772
+ };
1773
+ }
1774
+ /**
1775
+ * Normalize `usage` (absent on some partials) into the neutral `Usage`.
1776
+ * `input_tokens_details.cached_tokens` -> `cachedTokens`,
1777
+ * `cache_write_tokens` -> `cacheWriteTokens`,
1778
+ * `output_tokens_details.reasoning_tokens` -> `reasoningTokens`. The
1779
+ * detail blocks are typed non-optional but read defensively.
1780
+ */
1781
+ extractUsage(raw) {
1782
+ if (!raw) return {
1783
+ input: 0,
1784
+ output: 0,
1785
+ total: 0
1786
+ };
1787
+ return buildUsage({
1788
+ input: raw.input_tokens,
1789
+ output: raw.output_tokens,
1790
+ total: raw.total_tokens,
1791
+ cachedTokens: raw.input_tokens_details?.cached_tokens,
1792
+ cacheWriteTokens: raw.input_tokens_details?.cache_write_tokens,
1793
+ reasoningTokens: raw.output_tokens_details?.reasoning_tokens
1794
+ });
1795
+ }
1796
+ /** Wrap (idempotently), log and return a failure for the caller to throw. */
1797
+ logAndWrap(thrown) {
1798
+ const wrapped = wrapOpenAIError(thrown);
1799
+ this.logger.error(LOG_MODULE$2, "error", wrapped.message, {
1800
+ code: wrapped.code,
1801
+ context: wrapped.context
1802
+ });
1803
+ return wrapped;
1804
+ }
1805
+ };
1806
+
1055
1807
  //#endregion
1056
1808
  //#region ../ai-openai/src/speech.ts
1057
1809
  const LOG_MODULE$1 = "ai.openai";
@@ -1264,13 +2016,32 @@ var OpenAISDK = class {
1264
2016
  * Pricing resolution: per-model `config.pricing` wins; otherwise the
1265
2017
  * SDK-level registry entry keyed by `config.name`; otherwise
1266
2018
  * `undefined` (no cost computed).
2019
+ *
2020
+ * `config.api` selects the wire API. Omitted or `"chat"` builds the Chat
2021
+ * Completions `OpenAIModel`. `"responses"` builds `OpenAIResponsesModel`
2022
+ * and is accepted ONLY when this SDK's provider label is `"openai"`: any
2023
+ * other label (the wrapper packages deepseek / groq / xai / mistral, or a
2024
+ * custom gateway label) throws, so an endpoint not known to implement
2025
+ * `/v1/responses` is never sent there.
1267
2026
  */
1268
2027
  model(config) {
2028
+ const api = config.api;
2029
+ if (api !== void 0 && api !== "chat" && api !== "responses") throw new _warlock_js_ai.InvalidRequestError(`Unknown OpenAI api "${String(api)}"; expected "chat" or "responses".`, { context: {
2030
+ provider: this.provider,
2031
+ model: config.name,
2032
+ api
2033
+ } });
2034
+ if (api === "responses" && this.provider !== "openai") throw new _warlock_js_ai.InvalidRequestError(`api: "responses" is supported only on the direct "openai" provider; this SDK is labeled "${this.provider}". Use the default api: "chat".`, { context: {
2035
+ provider: this.provider,
2036
+ model: config.name,
2037
+ api
2038
+ } });
1269
2039
  const resolvedPricing = config.pricing ?? this.pricing?.[config.name];
1270
2040
  const resolvedConfig = resolvedPricing === config.pricing ? config : {
1271
2041
  ...config,
1272
2042
  pricing: resolvedPricing
1273
2043
  };
2044
+ if (api === "responses") return new OpenAIResponsesModel(this.client, resolvedConfig, this.provider);
1274
2045
  return new OpenAIModel(this.client, resolvedConfig, this.provider);
1275
2046
  }
1276
2047
  /**