@warlock.js/ai-openai 5.26.0 → 5.28.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 (38) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/cjs/index.cjs +958 -280
  3. package/cjs/index.cjs.map +1 -1
  4. package/esm/config.type.d.mts +22 -0
  5. package/esm/config.type.d.mts.map +1 -1
  6. package/esm/model.mjs +26 -119
  7. package/esm/model.mjs.map +1 -1
  8. package/esm/responses-model.mjs +329 -0
  9. package/esm/responses-model.mjs.map +1 -0
  10. package/esm/sdk.d.mts +7 -0
  11. package/esm/sdk.d.mts.map +1 -1
  12. package/esm/sdk.mjs +21 -1
  13. package/esm/sdk.mjs.map +1 -1
  14. package/esm/utils/build-usage.mjs +23 -0
  15. package/esm/utils/build-usage.mjs.map +1 -0
  16. package/esm/utils/index.mjs +9 -1
  17. package/esm/utils/map-responses-status.mjs +54 -0
  18. package/esm/utils/map-responses-status.mjs.map +1 -0
  19. package/esm/utils/prompt-cache-params.mjs +17 -0
  20. package/esm/utils/prompt-cache-params.mjs.map +1 -0
  21. package/esm/utils/reasoning-effort.mjs +35 -0
  22. package/esm/utils/reasoning-effort.mjs.map +1 -0
  23. package/esm/utils/reasoning-replay.mjs +85 -0
  24. package/esm/utils/reasoning-replay.mjs.map +1 -0
  25. package/esm/utils/structured-output.mjs +84 -0
  26. package/esm/utils/structured-output.mjs.map +1 -0
  27. package/esm/utils/to-openai-messages.mjs +9 -2
  28. package/esm/utils/to-openai-messages.mjs.map +1 -1
  29. package/esm/utils/to-openai-tools.mjs +3 -3
  30. package/esm/utils/to-openai-tools.mjs.map +1 -1
  31. package/esm/utils/to-responses-input.mjs +112 -0
  32. package/esm/utils/to-responses-input.mjs.map +1 -0
  33. package/esm/utils/to-responses-tools.mjs +51 -0
  34. package/esm/utils/to-responses-tools.mjs.map +1 -0
  35. package/llms-full.txt +20 -3
  36. package/llms.txt +1 -1
  37. package/package.json +3 -3
  38. package/skills/setup-openai/SKILL.md +20 -3
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
  /**
@@ -178,7 +609,7 @@ function toOpenAIContentPart(part) {
178
609
  };
179
610
  if (part.type === "image") return {
180
611
  type: "image_url",
181
- 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) }
182
613
  };
183
614
  if (part.type === "pdf") {
184
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.");
@@ -200,6 +631,13 @@ function toOpenAIContentPart(part) {
200
631
  };
201
632
  }
202
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
+ /**
203
641
  * Narrow a neutral audio media type to the two formats OpenAI's
204
642
  * `input_audio` accepts (`wav` / `mp3`). An unsupported type raises a
205
643
  * typed `InvalidRequestError` up front rather than a provider 400.
@@ -228,7 +666,7 @@ function toOpenAITools(tools) {
228
666
  function: {
229
667
  name: tool.name,
230
668
  description: tool.description,
231
- parameters: toParameters(tool.input)
669
+ parameters: toToolParameters(tool.input)
232
670
  }
233
671
  }));
234
672
  }
@@ -238,165 +676,171 @@ function toOpenAITools(tools) {
238
676
  * failed extraction) degrades to an empty-object schema so the tool
239
677
  * still registers and the model simply sees no parameters.
240
678
  */
241
- function toParameters(input) {
242
- const schema = (0, _warlock_js_ai.extractJsonSchema)(input);
243
- if (schema && schema.type === "object") return schema;
244
- return {
245
- type: "object",
246
- properties: {}
247
- };
248
- }
249
-
250
- //#endregion
251
- //#region ../ai-openai/src/utils/wrap-openai-error.ts
252
- /**
253
- * Wrap any thrown value caught inside the OpenAI adapter into the
254
- * appropriate `@warlock.js/ai` `AIError` subclass.
255
- *
256
- * **Dispatch strategy.** Prefers `APIError.code` when present (stable
257
- * machine identifier across SDK versions), falls back to `status` when
258
- * `code` is missing (common with proxied deployments that strip the
259
- * field). Name-based detection (`APIConnectionTimeoutError`) catches
260
- * transport-layer errors that never produced an HTTP response.
261
- *
262
- * `AIError` instances are returned unchanged — callers can pass the
263
- * error through `try/catch/throw wrap(e)` pipelines without accidental
264
- * double-wrapping.
265
- *
266
- * @example
267
- * try {
268
- * return await this.client.chat.completions.create(...);
269
- * } catch (thrown) {
270
- * throw wrapOpenAIError(thrown);
271
- * }
272
- */
273
- function wrapOpenAIError(thrown) {
274
- if (thrown instanceof _warlock_js_ai.AIError) return thrown;
275
- const shape = toShape(thrown);
276
- const context = buildContext(thrown, shape);
277
- const message = shape.message ?? (thrown instanceof Error ? thrown.message : String(thrown));
278
- if (isTimeout(thrown, shape)) return new _warlock_js_ai.ProviderTimeoutError(message, {
279
- cause: thrown,
280
- context
281
- });
282
- if (shape.status === 401 || shape.code === "invalid_api_key") return new _warlock_js_ai.ProviderAuthError(message, {
283
- cause: thrown,
284
- context
285
- });
286
- if (shape.code === "insufficient_quota") return new _warlock_js_ai.QuotaExceededError(message, {
287
- cause: thrown,
288
- context
289
- });
290
- if (shape.status === 429 || shape.code === "rate_limit_exceeded") return new _warlock_js_ai.ProviderRateLimitError(message, {
291
- cause: thrown,
292
- context,
293
- retryAfter: parseRetryAfter(shape.headers)
294
- });
295
- if (shape.code === "context_length_exceeded") return new _warlock_js_ai.ContextLengthExceededError(message, {
296
- cause: thrown,
297
- context
298
- });
299
- if (shape.code === "content_filter") return new _warlock_js_ai.ContentFilterError(message, {
300
- cause: thrown,
301
- context,
302
- reason: message
303
- });
304
- if (typeof shape.status === "number" && shape.status >= 400 && shape.status < 500) return new _warlock_js_ai.InvalidRequestError(message, {
305
- cause: thrown,
306
- context
307
- });
308
- return new _warlock_js_ai.ProviderError(message, {
309
- cause: thrown,
310
- context
311
- });
312
- }
313
- /**
314
- * Read the raw error shape without depending on `instanceof APIError`
315
- * — some consumers wrap the SDK, and proxies sometimes strip the
316
- * prototype chain. Duck-typing on the visible fields is resilient to
317
- * both.
318
- */
319
- function toShape(thrown) {
320
- if (thrown instanceof openai.default.APIError) return {
321
- status: thrown.status,
322
- code: thrown.code,
323
- message: thrown.message,
324
- type: thrown.type,
325
- param: thrown.param,
326
- headers: thrown.headers,
327
- name: thrown.name
328
- };
329
- if (typeof thrown === "object" && thrown !== null) {
330
- const raw = thrown;
331
- return {
332
- status: typeof raw.status === "number" ? raw.status : void 0,
333
- code: typeof raw.code === "string" ? raw.code : void 0,
334
- message: typeof raw.message === "string" ? raw.message : void 0,
335
- type: typeof raw.type === "string" ? raw.type : void 0,
336
- param: typeof raw.param === "string" ? raw.param : void 0,
337
- headers: typeof raw.headers === "object" && raw.headers !== null ? raw.headers : void 0,
338
- name: typeof raw.name === "string" ? raw.name : void 0
339
- };
340
- }
341
- return {};
342
- }
343
- /**
344
- * Decide whether the thrown value represents a timeout. OpenAI's SDK
345
- * throws `APIConnectionTimeoutError` for transport-level timeouts, and
346
- * Node surfaces `ETIMEDOUT` / `ECONNABORTED` on the lower socket
347
- * layer. Either signal counts.
348
- */
349
- function isTimeout(thrown, shape) {
350
- if (thrown instanceof openai.default.APIConnectionTimeoutError) return true;
351
- if (shape.name === "APIConnectionTimeoutError") return true;
352
- if (shape.code === "ETIMEDOUT" || shape.code === "ECONNABORTED") return true;
353
- 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
+ };
354
686
  }
687
+
688
+ //#endregion
689
+ //#region ../ai-openai/src/utils/to-responses-input.ts
355
690
  /**
356
- * Attach the raw diagnostic fields to `error.context` so consumers
357
- * have everything the provider surfaced without each subclass having
358
- * to redeclare them. Never includes `cause` — that lives on
359
- * `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" }] }
360
721
  */
361
- function buildContext(thrown, shape) {
362
- const context = {};
363
- if (shape.status !== void 0) context.status = shape.status;
364
- if (shape.code) context.code = shape.code;
365
- if (shape.type) context.type = shape.type;
366
- if (shape.param) context.param = shape.param;
367
- const requestId = readRequestId(thrown);
368
- if (requestId) context.requestId = requestId;
369
- 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.`);
370
792
  }
793
+
794
+ //#endregion
795
+ //#region ../ai-openai/src/utils/to-responses-tools.ts
371
796
  /**
372
- * The request id OpenAI returned. Current SDKs expose it as
373
- * `APIError.requestID`; older ones and hand-rolled errors use
374
- * `request_id` / `requestId`. Extract defensively.
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 }]
375
808
  */
376
- function readRequestId(thrown) {
377
- if (typeof thrown !== "object" || thrown === null) return;
378
- const raw = thrown;
379
- if (typeof raw.request_id === "string") return raw.request_id;
380
- if (typeof raw.requestId === "string") return raw.requestId;
381
- 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
+ }));
382
818
  }
383
819
  /**
384
- * Parse the `Retry-After` response header (seconds per HTTP spec)
385
- * into milliseconds so consumers can feed it straight to `setTimeout`.
386
- * 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.
387
829
  */
388
- function parseRetryAfter(headers) {
389
- if (!headers) return;
390
- const raw = headers["retry-after"] ?? headers["Retry-After"];
391
- if (!raw) return;
392
- const seconds = Number(raw);
393
- if (!Number.isFinite(seconds) || seconds < 0) return;
394
- 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
+ };
395
839
  }
396
840
 
397
841
  //#endregion
398
842
  //#region ../ai-openai/src/embedder.ts
399
- const LOG_MODULE$4 = "ai.openai";
843
+ const LOG_MODULE$5 = "ai.openai";
400
844
  /**
401
845
  * OpenAI-backed implementation of `EmbedderContract`.
402
846
  *
@@ -461,7 +905,7 @@ var OpenAIEmbedder = class {
461
905
  * plus a camelCase usage object for the caller to shape.
462
906
  */
463
907
  async request(input) {
464
- this.logger.debug(LOG_MODULE$4, "embedder.request", "embeddings.create", {
908
+ this.logger.debug(LOG_MODULE$5, "embedder.request", "embeddings.create", {
465
909
  model: this.name,
466
910
  batch: Array.isArray(input),
467
911
  count: Array.isArray(input) ? input.length : 1
@@ -475,13 +919,13 @@ var OpenAIEmbedder = class {
475
919
  });
476
920
  } catch (thrown) {
477
921
  const wrapped = wrapOpenAIError(thrown);
478
- this.logger.error(LOG_MODULE$4, "embedder.error", wrapped.message, {
922
+ this.logger.error(LOG_MODULE$5, "embedder.error", wrapped.message, {
479
923
  code: wrapped.code,
480
924
  context: wrapped.context
481
925
  });
482
926
  throw wrapped;
483
927
  }
484
- this.logger.debug(LOG_MODULE$4, "embedder.response", "embeddings.create returned", {
928
+ this.logger.debug(LOG_MODULE$5, "embedder.response", "embeddings.create returned", {
485
929
  dimensions: response.data[0]?.embedding.length,
486
930
  usage: {
487
931
  promptTokens: response.usage.prompt_tokens,
@@ -505,7 +949,7 @@ var OpenAIEmbedder = class {
505
949
 
506
950
  //#endregion
507
951
  //#region ../ai-openai/src/image.ts
508
- const LOG_MODULE$3 = "ai.openai";
952
+ const LOG_MODULE$4 = "ai.openai";
509
953
  /** Map a neutral output container to its IANA media type. */
510
954
  function mediaTypeFor(format) {
511
955
  switch (format) {
@@ -564,7 +1008,7 @@ var OpenAIImageModel = class {
564
1008
  if (!isGptImage && responseFormat) body.response_format = responseFormat;
565
1009
  if (isGptImage && options?.format !== void 0) body.output_format = options.format;
566
1010
  if (options?.background !== void 0) body.background = options.background;
567
- this.logger.debug(LOG_MODULE$3, "image.request", "images.generate", {
1011
+ this.logger.debug(LOG_MODULE$4, "image.request", "images.generate", {
568
1012
  model: this.name,
569
1013
  count: options?.count ?? 1
570
1014
  });
@@ -573,7 +1017,7 @@ var OpenAIImageModel = class {
573
1017
  response = await this.client.images.generate(body, options?.signal ? { signal: options.signal } : void 0);
574
1018
  } catch (thrown) {
575
1019
  const wrapped = wrapOpenAIError(thrown);
576
- this.logger.error(LOG_MODULE$3, "image.error", wrapped.message, {
1020
+ this.logger.error(LOG_MODULE$4, "image.error", wrapped.message, {
577
1021
  code: wrapped.code,
578
1022
  context: wrapped.context
579
1023
  });
@@ -589,7 +1033,7 @@ var OpenAIImageModel = class {
589
1033
  output: 0,
590
1034
  total: 0
591
1035
  };
592
- this.logger.debug(LOG_MODULE$3, "image.response", "images.generate succeeded", {
1036
+ this.logger.debug(LOG_MODULE$4, "image.response", "images.generate succeeded", {
593
1037
  images: images.length,
594
1038
  usage
595
1039
  });
@@ -708,37 +1152,7 @@ function inferVisionCapability(modelName) {
708
1152
 
709
1153
  //#endregion
710
1154
  //#region ../ai-openai/src/model.ts
711
- const LOG_MODULE$2 = "ai.openai";
712
- /**
713
- * The installed OpenAI SDK 7.23.0 declares `reasoning_effort` as
714
- * `none | minimal | low | medium | high | xhigh | max` in
715
- * `openai/src/resources/shared.ts:367`. Its type notes that model support
716
- * varies but does not provide a model-family-specific accepted set, so map by
717
- * that union alone. This exhaustive record makes a future core level a
718
- * typecheck failure until its OpenAI clamp is consciously selected.
719
- */
720
- const EFFORT_TO_OPENAI_EFFORT = {
721
- none: "none",
722
- minimal: "minimal",
723
- low: "low",
724
- medium: "medium",
725
- high: "high",
726
- xhigh: "xhigh",
727
- max: "max"
728
- };
729
- /**
730
- * Map an explicit `responseFormat` override to the default
731
- * `structuredOutput` capability. Loose wire modes (`"json_object"`,
732
- * `"text"`) don't enforce shape, so the agent needs to see the soft
733
- * schema hint in the system prompt — that only happens when the
734
- * capability is `false`. Default (no override) stays `true` to
735
- * preserve the prior assumption that OpenAI models support strict
736
- * structured output.
737
- */
738
- function inferStructuredOutput(responseFormat) {
739
- if (responseFormat === "json_object" || responseFormat === "text") return false;
740
- return true;
741
- }
1155
+ const LOG_MODULE$3 = "ai.openai";
742
1156
  /**
743
1157
  * OpenAI-backed implementation of `ModelContract`.
744
1158
  *
@@ -779,6 +1193,11 @@ function inferStructuredOutput(responseFormat) {
779
1193
  var OpenAIModel = class {
780
1194
  constructor(client, config, provider = "openai") {
781
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
+ } });
782
1201
  this.client = client;
783
1202
  this.config = config;
784
1203
  this.name = config.name;
@@ -800,7 +1219,7 @@ var OpenAIModel = class {
800
1219
  * instance's `ModelConfig` defaults for this call only.
801
1220
  */
802
1221
  async complete(messages, options) {
803
- 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", {
804
1223
  model: this.name,
805
1224
  messageCount: messages.length,
806
1225
  streaming: false,
@@ -812,14 +1231,14 @@ var OpenAIModel = class {
812
1231
  model: this.name,
813
1232
  messages: toOpenAIMessages(messages, this.promptCacheBreakpoints(options)),
814
1233
  ...this.buildSamplingParams(options),
815
- ...this.buildPromptCacheParams(),
1234
+ ...buildPromptCacheParams(this.provider, this.config),
816
1235
  tools: toOpenAITools(options?.tools),
817
1236
  ...this.buildResponseFormat(options?.responseSchema),
818
1237
  ...this.buildReasoningParams(options?.reasoning, Boolean(options?.tools?.length))
819
1238
  }, options?.signal ? { signal: options.signal } : void 0);
820
1239
  } catch (thrown) {
821
1240
  const wrapped = wrapOpenAIError(thrown);
822
- this.logger.error(LOG_MODULE$2, "error", wrapped.message, {
1241
+ this.logger.error(LOG_MODULE$3, "error", wrapped.message, {
823
1242
  code: wrapped.code,
824
1243
  context: wrapped.context
825
1244
  });
@@ -833,7 +1252,7 @@ var OpenAIModel = class {
833
1252
  } });
834
1253
  const finishReason = mapFinishReason(choice.finish_reason);
835
1254
  const usage = this.extractUsage(response.usage);
836
- 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", {
837
1256
  finishReason,
838
1257
  usage
839
1258
  });
@@ -851,7 +1270,7 @@ var OpenAIModel = class {
851
1270
  * Callers consume it with `for await`.
852
1271
  */
853
1272
  async *stream(messages, options) {
854
- 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", {
855
1274
  model: this.name,
856
1275
  messageCount: messages.length,
857
1276
  streaming: true,
@@ -863,7 +1282,7 @@ var OpenAIModel = class {
863
1282
  model: this.name,
864
1283
  messages: toOpenAIMessages(messages, this.promptCacheBreakpoints(options)),
865
1284
  ...this.buildSamplingParams(options),
866
- ...this.buildPromptCacheParams(),
1285
+ ...buildPromptCacheParams(this.provider, this.config),
867
1286
  tools: toOpenAITools(options?.tools),
868
1287
  stream: true,
869
1288
  stream_options: { include_usage: true },
@@ -872,7 +1291,7 @@ var OpenAIModel = class {
872
1291
  }, options?.signal ? { signal: options.signal } : void 0);
873
1292
  } catch (thrown) {
874
1293
  const wrapped = wrapOpenAIError(thrown);
875
- this.logger.error(LOG_MODULE$2, "error", wrapped.message, {
1294
+ this.logger.error(LOG_MODULE$3, "error", wrapped.message, {
876
1295
  code: wrapped.code,
877
1296
  context: wrapped.context
878
1297
  });
@@ -927,14 +1346,14 @@ var OpenAIModel = class {
927
1346
  }
928
1347
  } catch (thrown) {
929
1348
  const wrapped = wrapOpenAIError(thrown);
930
- this.logger.error(LOG_MODULE$2, "error", wrapped.message, {
1349
+ this.logger.error(LOG_MODULE$3, "error", wrapped.message, {
931
1350
  code: wrapped.code,
932
1351
  context: wrapped.context
933
1352
  });
934
1353
  throw wrapped;
935
1354
  }
936
1355
  const finishReason = mapFinishReason(rawFinishReason);
937
- 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", {
938
1357
  finishReason,
939
1358
  usage
940
1359
  });
@@ -944,101 +1363,31 @@ var OpenAIModel = class {
944
1363
  usage
945
1364
  };
946
1365
  }
947
- /** OpenAI-only request fields; wrappers use the same transport but not this wire extension. */
948
- buildPromptCacheParams() {
949
- if (this.provider !== "openai") return {};
950
- return {
951
- ...this.config.promptCacheKey !== void 0 ? { prompt_cache_key: this.config.promptCacheKey } : {},
952
- ...this.config.promptCacheRetention !== void 0 ? { prompt_cache_retention: this.config.promptCacheRetention } : {}
953
- };
954
- }
955
1366
  /** Map the neutral cache hint only for the direct OpenAI provider. */
956
1367
  promptCacheBreakpoints(options) {
957
1368
  return this.provider === "openai" ? options?.cacheControl?.breakpoints ?? 0 : 0;
958
1369
  }
959
1370
  /**
960
1371
  * Translate the neutral `responseSchema` option into OpenAI's
961
- * `response_format` parameter.
962
- *
963
- * When `config.responseFormat` is set, it wins: `"text"` emits no
964
- * `response_format` at all, `"json_object"` always picks the loose
965
- * mode, and `"json_schema"` picks strict mode (with the same
966
- * `isStrictCompatible` safety check — a malformed schema still
967
- * degrades to `json_object` rather than 400). The override exists
968
- * because some targets (older OpenAI models, OpenRouter routes,
969
- * Ollama OpenAI-compat) reject strict `json_schema` outright.
970
- *
971
- * When the override is omitted, uses strict `json_schema` mode
972
- * (token-level enforcement) only when the schema is a proper
973
- * root-object JSON Schema (`{ type: "object", properties: ... }`).
974
- * For anything else — malformed extractor output, non-object
975
- * schemas, or future shapes we haven't tested — falls back to loose
976
- * `json_object` mode, which guarantees *some* valid JSON without
977
- * enforcing shape. The agent's soft instruction already embeds the
978
- * schema text in the system prompt when the model declares no
979
- * native structured-output capability, so shape validation still
980
- * 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.
981
1375
  *
982
1376
  * Returns an empty spread when no schema was supplied, so the caller
983
1377
  * can unconditionally `...buildResponseFormat(...)` into the request.
984
1378
  */
985
1379
  buildResponseFormat(responseSchema) {
986
- if (!responseSchema) return {};
987
- const override = this.config.responseFormat;
988
- if (override === "text") return {};
989
- if (override === "json_object") return { response_format: { type: "json_object" } };
990
- if (this.isStrictCompatible(responseSchema)) return { response_format: {
1380
+ const plan = planStructuredOutput(responseSchema, this.config.responseFormat);
1381
+ if (plan.mode === "json_schema") return { response_format: {
991
1382
  type: "json_schema",
992
1383
  json_schema: {
993
1384
  name: "response",
994
- schema: responseSchema,
1385
+ schema: plan.schema,
995
1386
  strict: true
996
1387
  }
997
1388
  } };
998
- return { response_format: { type: "json_object" } };
999
- }
1000
- /**
1001
- * OpenAI strict `json_schema` mode requires the root to be a JSON
1002
- * Schema object type (`{ type: "object", properties: ... }`). Anything
1003
- * else (top-level arrays, primitives, unknown shapes) is rejected with
1004
- * a 400 before a token is sampled. We check structurally here so the
1005
- * first call doesn't crash on a malformed extraction — loose
1006
- * `json_object` mode is a safe degradation.
1007
- */
1008
- isStrictCompatible(schema) {
1009
- return schema.type === "object" && typeof schema.properties === "object" && schema.properties !== null && this.isStrictSafeNode(schema);
1010
- }
1011
- /**
1012
- * Recursively check the one strict-mode rule schemas most often trip on:
1013
- * every object must list ALL of its `properties` in `required` (OpenAI
1014
- * strict has no notion of optional — optional fields must be expressed
1015
- * as nullable, e.g. `type: ["string", "null"]`, and still appear in
1016
- * `required`). A schema that violates this anywhere in the tree is NOT
1017
- * sent in strict `json_schema` mode — it degrades to loose
1018
- * `json_object` so a hand-built or optional-bearing schema can't 400
1019
- * the call ("'required' ... must include every key in properties").
1020
- * Client-side `validate()` still enforces the full shape.
1021
- */
1022
- isStrictSafeNode(node) {
1023
- if (!node || typeof node !== "object") return true;
1024
- const record = node;
1025
- if (record.type === "object" && record.properties && typeof record.properties === "object") {
1026
- const properties = record.properties;
1027
- const keys = Object.keys(properties);
1028
- const required = Array.isArray(record.required) ? record.required : [];
1029
- if (keys.some((key) => !required.includes(key))) return false;
1030
- for (const key of keys) if (!this.isStrictSafeNode(properties[key])) return false;
1031
- }
1032
- if (record.items !== void 0 && !this.isStrictSafeNode(record.items)) return false;
1033
- for (const branch of [
1034
- "anyOf",
1035
- "allOf",
1036
- "oneOf"
1037
- ]) {
1038
- const value = record[branch];
1039
- if (Array.isArray(value) && value.some((sub) => !this.isStrictSafeNode(sub))) return false;
1040
- }
1041
- return true;
1389
+ if (plan.mode === "json_object") return { response_format: { type: "json_object" } };
1390
+ return {};
1042
1391
  }
1043
1392
  /**
1044
1393
  * Normalize OpenAI's `usage` block (which may be absent on some responses
@@ -1060,15 +1409,13 @@ var OpenAIModel = class {
1060
1409
  output: 0,
1061
1410
  total: 0
1062
1411
  };
1063
- const cachedTokens = raw.prompt_tokens_details?.cached_tokens;
1064
- const reasoningTokens = raw.completion_tokens_details?.reasoning_tokens;
1065
- return {
1412
+ return buildUsage({
1066
1413
  input: raw.prompt_tokens,
1067
1414
  output: raw.completion_tokens,
1068
1415
  total: raw.total_tokens,
1069
- ...cachedTokens !== void 0 && cachedTokens > 0 ? { cachedTokens } : {},
1070
- ...reasoningTokens !== void 0 && reasoningTokens > 0 ? { reasoningTokens } : {}
1071
- };
1416
+ cachedTokens: raw.prompt_tokens_details?.cached_tokens,
1417
+ reasoningTokens: raw.completion_tokens_details?.reasoning_tokens
1418
+ });
1072
1419
  }
1073
1420
  /**
1074
1421
  * Translate the neutral `ModelCallOptions.reasoning` hint into OpenAI's
@@ -1125,7 +1472,7 @@ var OpenAIModel = class {
1125
1472
  buildReasoningParams(reasoning, hasTools) {
1126
1473
  if (!this.capabilities.reasoning) return {};
1127
1474
  if (!reasoning?.effort) return hasTools ? { reasoning_effort: "none" } : {};
1128
- return { reasoning_effort: EFFORT_TO_OPENAI_EFFORT[reasoning.effort] };
1475
+ return { reasoning_effort: toOpenAIReasoningEffort(reasoning.effort) };
1129
1476
  }
1130
1477
  /**
1131
1478
  * Reshape OpenAI's `tool_calls` array into the neutral
@@ -1145,6 +1492,318 @@ var OpenAIModel = class {
1145
1492
  }
1146
1493
  };
1147
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
+
1148
1807
  //#endregion
1149
1808
  //#region ../ai-openai/src/speech.ts
1150
1809
  const LOG_MODULE$1 = "ai.openai";
@@ -1357,13 +2016,32 @@ var OpenAISDK = class {
1357
2016
  * Pricing resolution: per-model `config.pricing` wins; otherwise the
1358
2017
  * SDK-level registry entry keyed by `config.name`; otherwise
1359
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.
1360
2026
  */
1361
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
+ } });
1362
2039
  const resolvedPricing = config.pricing ?? this.pricing?.[config.name];
1363
2040
  const resolvedConfig = resolvedPricing === config.pricing ? config : {
1364
2041
  ...config,
1365
2042
  pricing: resolvedPricing
1366
2043
  };
2044
+ if (api === "responses") return new OpenAIResponsesModel(this.client, resolvedConfig, this.provider);
1367
2045
  return new OpenAIModel(this.client, resolvedConfig, this.provider);
1368
2046
  }
1369
2047
  /**