@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.
- package/CHANGELOG.md +14 -0
- package/cjs/index.cjs +958 -280
- package/cjs/index.cjs.map +1 -1
- package/esm/config.type.d.mts +22 -0
- package/esm/config.type.d.mts.map +1 -1
- package/esm/model.mjs +26 -119
- package/esm/model.mjs.map +1 -1
- package/esm/responses-model.mjs +329 -0
- package/esm/responses-model.mjs.map +1 -0
- package/esm/sdk.d.mts +7 -0
- package/esm/sdk.d.mts.map +1 -1
- package/esm/sdk.mjs +21 -1
- package/esm/sdk.mjs.map +1 -1
- package/esm/utils/build-usage.mjs +23 -0
- package/esm/utils/build-usage.mjs.map +1 -0
- package/esm/utils/index.mjs +9 -1
- package/esm/utils/map-responses-status.mjs +54 -0
- package/esm/utils/map-responses-status.mjs.map +1 -0
- package/esm/utils/prompt-cache-params.mjs +17 -0
- package/esm/utils/prompt-cache-params.mjs.map +1 -0
- package/esm/utils/reasoning-effort.mjs +35 -0
- package/esm/utils/reasoning-effort.mjs.map +1 -0
- package/esm/utils/reasoning-replay.mjs +85 -0
- package/esm/utils/reasoning-replay.mjs.map +1 -0
- package/esm/utils/structured-output.mjs +84 -0
- package/esm/utils/structured-output.mjs.map +1 -0
- package/esm/utils/to-openai-messages.mjs +9 -2
- package/esm/utils/to-openai-messages.mjs.map +1 -1
- package/esm/utils/to-openai-tools.mjs +3 -3
- package/esm/utils/to-openai-tools.mjs.map +1 -1
- package/esm/utils/to-responses-input.mjs +112 -0
- package/esm/utils/to-responses-input.mjs.map +1 -0
- package/esm/utils/to-responses-tools.mjs +51 -0
- package/esm/utils/to-responses-tools.mjs.map +1 -0
- package/llms-full.txt +20 -3
- package/llms.txt +1 -1
- package/package.json +3 -3
- 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:
|
|
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:
|
|
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
|
|
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
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
* `
|
|
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
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
*
|
|
373
|
-
*
|
|
374
|
-
* `
|
|
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
|
|
377
|
-
if (
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
*
|
|
385
|
-
*
|
|
386
|
-
*
|
|
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
|
|
389
|
-
if (
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
const
|
|
393
|
-
if (
|
|
394
|
-
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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.
|
|
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$
|
|
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$
|
|
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$
|
|
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.
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
-
*
|
|
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
|
-
|
|
987
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
1070
|
-
|
|
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:
|
|
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
|
/**
|