@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
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { buildUsage } from "./utils/build-usage.mjs";
|
|
2
|
+
import { wrapOpenAIError } from "./utils/wrap-openai-error.mjs";
|
|
3
|
+
import { mapResponsesFinishReason, toResponseFailure } from "./utils/map-responses-status.mjs";
|
|
4
|
+
import { buildPromptCacheParams } from "./utils/prompt-cache-params.mjs";
|
|
5
|
+
import { toOpenAIReasoningEffort } from "./utils/reasoning-effort.mjs";
|
|
6
|
+
import { inferStructuredOutput, planStructuredOutput } from "./utils/structured-output.mjs";
|
|
7
|
+
import { attachReasoningReplay } from "./utils/reasoning-replay.mjs";
|
|
8
|
+
import { toResponsesInput } from "./utils/to-responses-input.mjs";
|
|
9
|
+
import { toResponsesToolChoice, toResponsesTools } from "./utils/to-responses-tools.mjs";
|
|
10
|
+
import "./utils/index.mjs";
|
|
11
|
+
import { inferReasoningCapability } from "./known-reasoning-models.mjs";
|
|
12
|
+
import { inferVisionCapability } from "./known-vision-models.mjs";
|
|
13
|
+
import { InvalidRequestError, ProviderError, safeJsonParse } from "@warlock.js/ai";
|
|
14
|
+
import { log } from "@warlock.js/logger";
|
|
15
|
+
|
|
16
|
+
//#region ../ai-openai/src/responses-model.ts
|
|
17
|
+
const LOG_MODULE = "ai.openai";
|
|
18
|
+
/**
|
|
19
|
+
* OpenAI Responses API implementation of `ModelContract` (`api: "responses"`).
|
|
20
|
+
*
|
|
21
|
+
* **Role.** A sibling of `OpenAIModel` that speaks `POST /v1/responses`
|
|
22
|
+
* instead of Chat Completions. Its point is letting a tool-using agent run a
|
|
23
|
+
* reasoning model with reasoning ON: the Chat path has to force
|
|
24
|
+
* `reasoning_effort: "none"` whenever tools are attached, Responses does not.
|
|
25
|
+
*
|
|
26
|
+
* **Why a separate class.** Only `OpenAISDK.model()` constructs it, and only
|
|
27
|
+
* for the direct `"openai"` provider. The wrapper packages (deepseek, groq,
|
|
28
|
+
* xai, mistral) build `OpenAIModel` and cannot reach this class, so an
|
|
29
|
+
* endpoint that does not implement `/v1/responses` is never sent there. The
|
|
30
|
+
* constructor re-checks the provider label as a second lock.
|
|
31
|
+
*
|
|
32
|
+
* **Stateless.** Every request sends `store: false`; nothing is kept on the
|
|
33
|
+
* server and no `previous_response_id` is used. Full history is re-sent each
|
|
34
|
+
* call, exactly like the Chat path.
|
|
35
|
+
*
|
|
36
|
+
* **Reasoning replay.** Every request asks for
|
|
37
|
+
* `include: ["reasoning.encrypted_content"]`. The `reasoning` items of a
|
|
38
|
+
* tool-calling turn are stored on the FIRST tool call as
|
|
39
|
+
* `providerMetadata.openaiResponses = { model, reasoningItems }` and sent back
|
|
40
|
+
* before that turn's `function_call` items on the next request (only when the
|
|
41
|
+
* stored model equals the current one). Text-only turns are not replayed.
|
|
42
|
+
*
|
|
43
|
+
* **Streaming.** `stream()` uses `responses.create({ stream: true })` and yields
|
|
44
|
+
* the same chunk sequence as the Chat path: text `delta`s, then the
|
|
45
|
+
* `tool-call`s, then one `done`.
|
|
46
|
+
*/
|
|
47
|
+
var OpenAIResponsesModel = class {
|
|
48
|
+
constructor(client, config, provider = "openai") {
|
|
49
|
+
this.logger = log;
|
|
50
|
+
if (provider !== "openai") throw new InvalidRequestError(`api: "responses" is supported only on the direct "openai" provider; got provider "${provider}".`, { context: {
|
|
51
|
+
provider,
|
|
52
|
+
model: config.name,
|
|
53
|
+
api: "responses"
|
|
54
|
+
} });
|
|
55
|
+
this.client = client;
|
|
56
|
+
this.config = config;
|
|
57
|
+
this.name = config.name;
|
|
58
|
+
this.provider = provider;
|
|
59
|
+
this.pricing = config.pricing;
|
|
60
|
+
this.capabilities = {
|
|
61
|
+
structuredOutput: config.structuredOutput ?? inferStructuredOutput(config.responseFormat),
|
|
62
|
+
vision: config.vision ?? inferVisionCapability(config.name),
|
|
63
|
+
reasoning: config.reasoning ?? inferReasoningCapability(config.name),
|
|
64
|
+
promptCaching: true,
|
|
65
|
+
pdf: false,
|
|
66
|
+
audio: false
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Single-shot completion against `client.responses.create`. Sends the full
|
|
71
|
+
* history as Responses `input` items, walks `response.output`, and reshapes
|
|
72
|
+
* it into a vendor-neutral `ModelResponse`. Per-call `options` override the
|
|
73
|
+
* instance defaults for this call only.
|
|
74
|
+
*/
|
|
75
|
+
async complete(messages, options) {
|
|
76
|
+
this.logger.debug(LOG_MODULE, "request", "Starting call to responses.create", {
|
|
77
|
+
model: this.name,
|
|
78
|
+
messageCount: messages.length,
|
|
79
|
+
streaming: false,
|
|
80
|
+
toolCount: options?.tools?.length ?? 0
|
|
81
|
+
});
|
|
82
|
+
let response;
|
|
83
|
+
try {
|
|
84
|
+
response = await this.client.responses.create(this.buildRequest(messages, options), options?.signal ? { signal: options.signal } : void 0);
|
|
85
|
+
} catch (thrown) {
|
|
86
|
+
throw this.logAndWrap(thrown);
|
|
87
|
+
}
|
|
88
|
+
if (response.status === "failed") throw this.logAndWrap(toResponseFailure(response, this.name));
|
|
89
|
+
const output = Array.isArray(response.output) ? response.output : [];
|
|
90
|
+
const { content, toolCalls } = this.readOutput(output);
|
|
91
|
+
const finishReason = mapResponsesFinishReason({
|
|
92
|
+
status: response.status,
|
|
93
|
+
incomplete_details: response.incomplete_details,
|
|
94
|
+
hasToolCalls: toolCalls !== void 0
|
|
95
|
+
});
|
|
96
|
+
const usage = this.extractUsage(response.usage);
|
|
97
|
+
this.logger.debug(LOG_MODULE, "response", "call to responses.create succeeded", {
|
|
98
|
+
finishReason,
|
|
99
|
+
usage
|
|
100
|
+
});
|
|
101
|
+
return {
|
|
102
|
+
content,
|
|
103
|
+
finishReason,
|
|
104
|
+
usage,
|
|
105
|
+
...toolCalls ? { toolCalls } : {}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Incremental streaming completion via `responses.create({ stream: true })`
|
|
110
|
+
* (the SDK's `Stream<ResponseStreamEvent>`, `responses.d.ts:55`). Yields the
|
|
111
|
+
* same neutral `ModelStreamChunk` sequence as the Chat path:
|
|
112
|
+
*
|
|
113
|
+
* - `response.output_text.delta` / `response.refusal.delta` -> `delta`.
|
|
114
|
+
* - `response.output_item.done` with a completed `function_call` item ->
|
|
115
|
+
* buffered; `reasoning` items are captured for replay. The `done` copy is
|
|
116
|
+
* used because the `added` copy may be incomplete (`responses.d.ts:5711`).
|
|
117
|
+
* - After the loop: the buffered `tool-call`s (the first carries the
|
|
118
|
+
* replay metadata), then a single `done` with the finish reason and usage
|
|
119
|
+
* from `response.completed` / `response.incomplete`.
|
|
120
|
+
* - `response.failed` and `error` events throw a typed `AIError`; a stream
|
|
121
|
+
* that ends with no terminal event throws `ProviderError`.
|
|
122
|
+
* - Abort goes through the request `signal`; the SDK rejects the read and
|
|
123
|
+
* the error is wrapped like any other.
|
|
124
|
+
*/
|
|
125
|
+
async *stream(messages, options) {
|
|
126
|
+
this.logger.debug(LOG_MODULE, "request", "Starting streaming call to responses.create", {
|
|
127
|
+
model: this.name,
|
|
128
|
+
messageCount: messages.length,
|
|
129
|
+
streaming: true,
|
|
130
|
+
toolCount: options?.tools?.length ?? 0
|
|
131
|
+
});
|
|
132
|
+
let events;
|
|
133
|
+
try {
|
|
134
|
+
events = await this.client.responses.create({
|
|
135
|
+
...this.buildRequest(messages, options),
|
|
136
|
+
stream: true
|
|
137
|
+
}, options?.signal ? { signal: options.signal } : void 0);
|
|
138
|
+
} catch (thrown) {
|
|
139
|
+
throw this.logAndWrap(thrown);
|
|
140
|
+
}
|
|
141
|
+
const toolCalls = [];
|
|
142
|
+
const reasoningItems = [];
|
|
143
|
+
let terminal;
|
|
144
|
+
try {
|
|
145
|
+
for await (const event of events) {
|
|
146
|
+
if (event.type === "response.output_text.delta" || event.type === "response.refusal.delta") {
|
|
147
|
+
if (event.delta) yield {
|
|
148
|
+
type: "delta",
|
|
149
|
+
content: event.delta
|
|
150
|
+
};
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (event.type === "response.output_item.done") {
|
|
154
|
+
const { item } = event;
|
|
155
|
+
if (item.type === "reasoning") reasoningItems.push(item);
|
|
156
|
+
else if (item.type === "function_call" && item.status !== "incomplete") toolCalls.push(this.toToolCall(item));
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (event.type === "response.completed" || event.type === "response.incomplete") {
|
|
160
|
+
terminal = event.response;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (event.type === "response.failed") throw toResponseFailure(event.response, this.name);
|
|
164
|
+
if (event.type === "error") throw wrapOpenAIError({
|
|
165
|
+
code: event.code,
|
|
166
|
+
message: event.message,
|
|
167
|
+
param: event.param
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
if (terminal === void 0) throw new ProviderError("OpenAI Responses stream ended before response.completed.", { context: {
|
|
171
|
+
provider: this.provider,
|
|
172
|
+
model: this.name
|
|
173
|
+
} });
|
|
174
|
+
} catch (thrown) {
|
|
175
|
+
throw this.logAndWrap(thrown);
|
|
176
|
+
}
|
|
177
|
+
for (const call of attachReasoningReplay(toolCalls, this.name, reasoningItems)) yield {
|
|
178
|
+
type: "tool-call",
|
|
179
|
+
id: call.id,
|
|
180
|
+
name: call.name,
|
|
181
|
+
input: call.input,
|
|
182
|
+
...call.providerMetadata ? { providerMetadata: call.providerMetadata } : {}
|
|
183
|
+
};
|
|
184
|
+
const finishReason = mapResponsesFinishReason({
|
|
185
|
+
status: terminal.status,
|
|
186
|
+
incomplete_details: terminal.incomplete_details,
|
|
187
|
+
hasToolCalls: toolCalls.length > 0
|
|
188
|
+
});
|
|
189
|
+
const usage = this.extractUsage(terminal.usage);
|
|
190
|
+
this.logger.debug(LOG_MODULE, "response", "Streaming call to responses.create succeeded", {
|
|
191
|
+
finishReason,
|
|
192
|
+
usage
|
|
193
|
+
});
|
|
194
|
+
yield {
|
|
195
|
+
type: "done",
|
|
196
|
+
finishReason,
|
|
197
|
+
usage
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Assemble the `responses.create` body.
|
|
202
|
+
*
|
|
203
|
+
* - System prompt -> `instructions`; history -> `input` items.
|
|
204
|
+
* - `store: false` always (the API default is `true` when omitted), plus
|
|
205
|
+
* `include: ["reasoning.encrypted_content"]` for stateless reasoning replay.
|
|
206
|
+
* - `max_output_tokens` caps reasoning and visible output together.
|
|
207
|
+
* - `temperature` only for non-reasoning models (same rule as Chat).
|
|
208
|
+
* - `tools` flat with `strict: false`; `tool_choice` only with tools.
|
|
209
|
+
* - `reasoning.effort` only when the caller gave one, through the same
|
|
210
|
+
* clamp table as Chat. With no explicit effort the provider default
|
|
211
|
+
* applies EVEN with tools attached - that is the point of this API.
|
|
212
|
+
*/
|
|
213
|
+
buildRequest(messages, options) {
|
|
214
|
+
const { instructions, input } = toResponsesInput(messages, this.name);
|
|
215
|
+
const tools = toResponsesTools(options?.tools);
|
|
216
|
+
const toolChoice = tools ? toResponsesToolChoice(options?.toolChoice) : void 0;
|
|
217
|
+
const maxTokens = options?.maxTokens ?? this.config.maxTokens;
|
|
218
|
+
const temperature = options?.temperature ?? this.config.temperature;
|
|
219
|
+
return {
|
|
220
|
+
model: this.name,
|
|
221
|
+
...instructions !== void 0 ? { instructions } : {},
|
|
222
|
+
input,
|
|
223
|
+
store: false,
|
|
224
|
+
include: ["reasoning.encrypted_content"],
|
|
225
|
+
...maxTokens !== void 0 ? { max_output_tokens: maxTokens } : {},
|
|
226
|
+
...!this.capabilities.reasoning && temperature !== void 0 ? { temperature } : {},
|
|
227
|
+
...tools ? { tools } : {},
|
|
228
|
+
...toolChoice !== void 0 ? { tool_choice: toolChoice } : {},
|
|
229
|
+
...this.buildTextFormat(options?.responseSchema),
|
|
230
|
+
...this.buildReasoningParams(options?.reasoning),
|
|
231
|
+
...buildPromptCacheParams(this.provider, this.config)
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Structured output on Responses lives in `text.format`, and `name` /
|
|
236
|
+
* `schema` / `strict` sit directly on the format object (Chat nests them
|
|
237
|
+
* under `json_schema`). The strict-or-downgrade decision is the shared
|
|
238
|
+
* `planStructuredOutput`.
|
|
239
|
+
*/
|
|
240
|
+
buildTextFormat(responseSchema) {
|
|
241
|
+
const plan = planStructuredOutput(responseSchema, this.config.responseFormat);
|
|
242
|
+
if (plan.mode === "json_schema") return { text: { format: {
|
|
243
|
+
type: "json_schema",
|
|
244
|
+
name: "response",
|
|
245
|
+
schema: plan.schema,
|
|
246
|
+
strict: true
|
|
247
|
+
} } };
|
|
248
|
+
if (plan.mode === "json_object") return { text: { format: { type: "json_object" } } };
|
|
249
|
+
return {};
|
|
250
|
+
}
|
|
251
|
+
/** `reasoning.effort` for reasoning-capable models, only when the caller set one. */
|
|
252
|
+
buildReasoningParams(reasoning) {
|
|
253
|
+
if (!this.capabilities.reasoning || !reasoning?.effort) return {};
|
|
254
|
+
return { reasoning: { effort: toOpenAIReasoningEffort(reasoning.effort) } };
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Walk `response.output`: `message` items contribute their `output_text`
|
|
258
|
+
* text and any `refusal` text (surfaced as ordinary content, decision 5);
|
|
259
|
+
* each completed `function_call` item becomes one tool call with
|
|
260
|
+
* `id = call_id`. `reasoning` items carry no visible content and are
|
|
261
|
+
* not content. A `function_call` the API marked `incomplete` (truncated
|
|
262
|
+
* arguments) is skipped rather than dispatched with empty input.
|
|
263
|
+
* `reasoning` items are captured and attached to the first tool call for
|
|
264
|
+
* replay (see `attachReasoningReplay`).
|
|
265
|
+
*/
|
|
266
|
+
readOutput(output) {
|
|
267
|
+
let content = "";
|
|
268
|
+
const toolCalls = [];
|
|
269
|
+
const reasoningItems = [];
|
|
270
|
+
for (const item of output) {
|
|
271
|
+
if (item.type === "reasoning") {
|
|
272
|
+
reasoningItems.push(item);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (item.type === "message") {
|
|
276
|
+
for (const part of item.content) content += part.type === "refusal" ? part.refusal : part.text;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (item.type === "function_call" && item.status !== "incomplete") toolCalls.push(this.toToolCall(item));
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
content,
|
|
283
|
+
toolCalls: toolCalls.length > 0 ? attachReasoningReplay(toolCalls, this.name, reasoningItems) : void 0
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
/** One completed `function_call` item as a neutral tool call (`call_id` -> `id`). */
|
|
287
|
+
toToolCall(item) {
|
|
288
|
+
return {
|
|
289
|
+
id: item.call_id,
|
|
290
|
+
name: item.name,
|
|
291
|
+
input: safeJsonParse(item.arguments, {})
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Normalize `usage` (absent on some partials) into the neutral `Usage`.
|
|
296
|
+
* `input_tokens_details.cached_tokens` -> `cachedTokens`,
|
|
297
|
+
* `cache_write_tokens` -> `cacheWriteTokens`,
|
|
298
|
+
* `output_tokens_details.reasoning_tokens` -> `reasoningTokens`. The
|
|
299
|
+
* detail blocks are typed non-optional but read defensively.
|
|
300
|
+
*/
|
|
301
|
+
extractUsage(raw) {
|
|
302
|
+
if (!raw) return {
|
|
303
|
+
input: 0,
|
|
304
|
+
output: 0,
|
|
305
|
+
total: 0
|
|
306
|
+
};
|
|
307
|
+
return buildUsage({
|
|
308
|
+
input: raw.input_tokens,
|
|
309
|
+
output: raw.output_tokens,
|
|
310
|
+
total: raw.total_tokens,
|
|
311
|
+
cachedTokens: raw.input_tokens_details?.cached_tokens,
|
|
312
|
+
cacheWriteTokens: raw.input_tokens_details?.cache_write_tokens,
|
|
313
|
+
reasoningTokens: raw.output_tokens_details?.reasoning_tokens
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
/** Wrap (idempotently), log and return a failure for the caller to throw. */
|
|
317
|
+
logAndWrap(thrown) {
|
|
318
|
+
const wrapped = wrapOpenAIError(thrown);
|
|
319
|
+
this.logger.error(LOG_MODULE, "error", wrapped.message, {
|
|
320
|
+
code: wrapped.code,
|
|
321
|
+
context: wrapped.context
|
|
322
|
+
});
|
|
323
|
+
return wrapped;
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
//#endregion
|
|
328
|
+
export { OpenAIResponsesModel };
|
|
329
|
+
//# sourceMappingURL=responses-model.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"responses-model.mjs","names":[],"sources":["../../../../../../ai-openai/src/responses-model.ts"],"sourcesContent":["import {\n InvalidRequestError,\n ProviderError,\n safeJsonParse,\n type Message,\n type ModelCallOptions,\n type ModelCapabilities,\n type ModelContract,\n type ModelPricing,\n type ModelResponse,\n type ModelStreamChunk,\n type ModelToolCallRequest,\n type Usage,\n} from \"@warlock.js/ai\";\nimport { log, type Logger } from \"@warlock.js/logger\";\nimport type OpenAI from \"openai\";\nimport type { OpenAIModelConfig } from \"./config.type\";\nimport { inferReasoningCapability } from \"./known-reasoning-models\";\nimport { inferVisionCapability } from \"./known-vision-models\";\nimport {\n attachReasoningReplay,\n buildPromptCacheParams,\n buildUsage,\n inferStructuredOutput,\n mapResponsesFinishReason,\n planStructuredOutput,\n toOpenAIReasoningEffort,\n toResponseFailure,\n toResponsesInput,\n toResponsesToolChoice,\n toResponsesTools,\n wrapOpenAIError,\n} from \"./utils\";\n\nconst LOG_MODULE = \"ai.openai\";\n\n/**\n * OpenAI Responses API implementation of `ModelContract` (`api: \"responses\"`).\n *\n * **Role.** A sibling of `OpenAIModel` that speaks `POST /v1/responses`\n * instead of Chat Completions. Its point is letting a tool-using agent run a\n * reasoning model with reasoning ON: the Chat path has to force\n * `reasoning_effort: \"none\"` whenever tools are attached, Responses does not.\n *\n * **Why a separate class.** Only `OpenAISDK.model()` constructs it, and only\n * for the direct `\"openai\"` provider. The wrapper packages (deepseek, groq,\n * xai, mistral) build `OpenAIModel` and cannot reach this class, so an\n * endpoint that does not implement `/v1/responses` is never sent there. The\n * constructor re-checks the provider label as a second lock.\n *\n * **Stateless.** Every request sends `store: false`; nothing is kept on the\n * server and no `previous_response_id` is used. Full history is re-sent each\n * call, exactly like the Chat path.\n *\n * **Reasoning replay.** Every request asks for\n * `include: [\"reasoning.encrypted_content\"]`. The `reasoning` items of a\n * tool-calling turn are stored on the FIRST tool call as\n * `providerMetadata.openaiResponses = { model, reasoningItems }` and sent back\n * before that turn's `function_call` items on the next request (only when the\n * stored model equals the current one). Text-only turns are not replayed.\n *\n * **Streaming.** `stream()` uses `responses.create({ stream: true })` and yields\n * the same chunk sequence as the Chat path: text `delta`s, then the\n * `tool-call`s, then one `done`.\n */\nexport class OpenAIResponsesModel implements ModelContract {\n public readonly name: string;\n public readonly provider: string;\n public readonly capabilities: ModelCapabilities;\n public readonly pricing?: ModelPricing;\n\n private readonly client: OpenAI;\n private readonly config: OpenAIModelConfig;\n private readonly logger: Logger = log;\n\n public constructor(client: OpenAI, config: OpenAIModelConfig, provider: string = \"openai\") {\n if (provider !== \"openai\") {\n throw new InvalidRequestError(\n `api: \"responses\" is supported only on the direct \"openai\" provider; got provider \"${provider}\".`,\n { context: { provider, model: config.name, api: \"responses\" } },\n );\n }\n\n this.client = client;\n this.config = config;\n this.name = config.name;\n this.provider = provider;\n this.pricing = config.pricing;\n this.capabilities = {\n structuredOutput: config.structuredOutput ?? inferStructuredOutput(config.responseFormat),\n vision: config.vision ?? inferVisionCapability(config.name),\n reasoning: config.reasoning ?? inferReasoningCapability(config.name),\n // Read-side cache accounting (`cachedTokens`, `cacheWriteTokens`) works\n // and `prompt_cache_key` is forwarded. Explicit write breakpoints are not\n // mapped onto Responses yet, so a `cacheControl.breakpoints` hint is\n // ignored rather than rejected.\n promptCaching: true,\n // PDF and audio input are off under Responses in this adapter: the\n // `input_file` / `input_audio` field-level mapping is unverified. The\n // `pdf` / `audio` config flags are deliberately NOT honoured here.\n pdf: false,\n audio: false,\n };\n }\n\n /**\n * Single-shot completion against `client.responses.create`. Sends the full\n * history as Responses `input` items, walks `response.output`, and reshapes\n * it into a vendor-neutral `ModelResponse`. Per-call `options` override the\n * instance defaults for this call only.\n */\n public async complete(messages: Message[], options?: ModelCallOptions): Promise<ModelResponse> {\n this.logger.debug(LOG_MODULE, \"request\", \"Starting call to responses.create\", {\n model: this.name,\n messageCount: messages.length,\n streaming: false,\n toolCount: options?.tools?.length ?? 0,\n });\n\n let response: OpenAI.Responses.Response;\n\n try {\n response = await this.client.responses.create(\n this.buildRequest(messages, options),\n options?.signal ? { signal: options.signal } : undefined,\n );\n } catch (thrown) {\n throw this.logAndWrap(thrown);\n }\n\n if (response.status === \"failed\") {\n throw this.logAndWrap(toResponseFailure(response, this.name));\n }\n\n const output = Array.isArray(response.output) ? response.output : [];\n const { content, toolCalls } = this.readOutput(output);\n const finishReason = mapResponsesFinishReason({\n status: response.status,\n incomplete_details: response.incomplete_details,\n hasToolCalls: toolCalls !== undefined,\n });\n const usage = this.extractUsage(response.usage);\n\n this.logger.debug(LOG_MODULE, \"response\", \"call to responses.create succeeded\", {\n finishReason,\n usage,\n });\n\n return { content, finishReason, usage, ...(toolCalls ? { toolCalls } : {}) };\n }\n\n /**\n * Incremental streaming completion via `responses.create({ stream: true })`\n * (the SDK's `Stream<ResponseStreamEvent>`, `responses.d.ts:55`). Yields the\n * same neutral `ModelStreamChunk` sequence as the Chat path:\n *\n * - `response.output_text.delta` / `response.refusal.delta` -> `delta`.\n * - `response.output_item.done` with a completed `function_call` item ->\n * buffered; `reasoning` items are captured for replay. The `done` copy is\n * used because the `added` copy may be incomplete (`responses.d.ts:5711`).\n * - After the loop: the buffered `tool-call`s (the first carries the\n * replay metadata), then a single `done` with the finish reason and usage\n * from `response.completed` / `response.incomplete`.\n * - `response.failed` and `error` events throw a typed `AIError`; a stream\n * that ends with no terminal event throws `ProviderError`.\n * - Abort goes through the request `signal`; the SDK rejects the read and\n * the error is wrapped like any other.\n */\n public async *stream(\n messages: Message[],\n options?: ModelCallOptions,\n ): AsyncIterable<ModelStreamChunk> {\n this.logger.debug(LOG_MODULE, \"request\", \"Starting streaming call to responses.create\", {\n model: this.name,\n messageCount: messages.length,\n streaming: true,\n toolCount: options?.tools?.length ?? 0,\n });\n\n let events: AsyncIterable<OpenAI.Responses.ResponseStreamEvent>;\n\n try {\n events = await this.client.responses.create(\n { ...this.buildRequest(messages, options), stream: true },\n options?.signal ? { signal: options.signal } : undefined,\n );\n } catch (thrown) {\n throw this.logAndWrap(thrown);\n }\n\n const toolCalls: ModelToolCallRequest[] = [];\n const reasoningItems: OpenAI.Responses.ResponseReasoningItem[] = [];\n let terminal: OpenAI.Responses.Response | undefined;\n\n try {\n for await (const event of events) {\n if (event.type === \"response.output_text.delta\" || event.type === \"response.refusal.delta\") {\n if (event.delta) {\n yield { type: \"delta\", content: event.delta };\n }\n\n continue;\n }\n\n if (event.type === \"response.output_item.done\") {\n const { item } = event;\n\n if (item.type === \"reasoning\") {\n reasoningItems.push(item);\n } else if (item.type === \"function_call\" && item.status !== \"incomplete\") {\n toolCalls.push(this.toToolCall(item));\n }\n\n continue;\n }\n\n if (event.type === \"response.completed\" || event.type === \"response.incomplete\") {\n terminal = event.response;\n continue;\n }\n\n if (event.type === \"response.failed\") {\n throw toResponseFailure(event.response, this.name);\n }\n\n if (event.type === \"error\") {\n throw wrapOpenAIError({ code: event.code, message: event.message, param: event.param });\n }\n }\n\n if (terminal === undefined) {\n throw new ProviderError(\"OpenAI Responses stream ended before response.completed.\", {\n context: { provider: this.provider, model: this.name },\n });\n }\n } catch (thrown) {\n throw this.logAndWrap(thrown);\n }\n\n for (const call of attachReasoningReplay(toolCalls, this.name, reasoningItems)) {\n yield {\n type: \"tool-call\",\n id: call.id,\n name: call.name,\n input: call.input,\n ...(call.providerMetadata ? { providerMetadata: call.providerMetadata } : {}),\n };\n }\n\n const finishReason = mapResponsesFinishReason({\n status: terminal.status,\n incomplete_details: terminal.incomplete_details,\n hasToolCalls: toolCalls.length > 0,\n });\n const usage = this.extractUsage(terminal.usage);\n\n this.logger.debug(LOG_MODULE, \"response\", \"Streaming call to responses.create succeeded\", {\n finishReason,\n usage,\n });\n\n yield { type: \"done\", finishReason, usage };\n }\n\n /**\n * Assemble the `responses.create` body.\n *\n * - System prompt -> `instructions`; history -> `input` items.\n * - `store: false` always (the API default is `true` when omitted), plus\n * `include: [\"reasoning.encrypted_content\"]` for stateless reasoning replay.\n * - `max_output_tokens` caps reasoning and visible output together.\n * - `temperature` only for non-reasoning models (same rule as Chat).\n * - `tools` flat with `strict: false`; `tool_choice` only with tools.\n * - `reasoning.effort` only when the caller gave one, through the same\n * clamp table as Chat. With no explicit effort the provider default\n * applies EVEN with tools attached - that is the point of this API.\n */\n private buildRequest(\n messages: Message[],\n options: ModelCallOptions | undefined,\n ): OpenAI.Responses.ResponseCreateParamsNonStreaming {\n const { instructions, input } = toResponsesInput(messages, this.name);\n const tools = toResponsesTools(options?.tools);\n const toolChoice = tools ? toResponsesToolChoice(options?.toolChoice) : undefined;\n const maxTokens = options?.maxTokens ?? this.config.maxTokens;\n const temperature = options?.temperature ?? this.config.temperature;\n\n return {\n model: this.name,\n ...(instructions !== undefined ? { instructions } : {}),\n input,\n store: false,\n // Stateless replay: ask for the encrypted reasoning blobs so they can be\n // sent back on the next tool turn (decision 3).\n include: [\"reasoning.encrypted_content\"],\n ...(maxTokens !== undefined ? { max_output_tokens: maxTokens } : {}),\n ...(!this.capabilities.reasoning && temperature !== undefined ? { temperature } : {}),\n ...(tools ? { tools } : {}),\n ...(toolChoice !== undefined ? { tool_choice: toolChoice } : {}),\n ...this.buildTextFormat(options?.responseSchema),\n ...this.buildReasoningParams(options?.reasoning),\n ...buildPromptCacheParams(this.provider, this.config),\n };\n }\n\n /**\n * Structured output on Responses lives in `text.format`, and `name` /\n * `schema` / `strict` sit directly on the format object (Chat nests them\n * under `json_schema`). The strict-or-downgrade decision is the shared\n * `planStructuredOutput`.\n */\n private buildTextFormat(responseSchema: Record<string, unknown> | undefined): {\n text?: OpenAI.Responses.ResponseTextConfig;\n } {\n const plan = planStructuredOutput(responseSchema, this.config.responseFormat);\n\n if (plan.mode === \"json_schema\") {\n return {\n text: { format: { type: \"json_schema\", name: \"response\", schema: plan.schema, strict: true } },\n };\n }\n\n if (plan.mode === \"json_object\") {\n return { text: { format: { type: \"json_object\" } } };\n }\n\n return {};\n }\n\n /** `reasoning.effort` for reasoning-capable models, only when the caller set one. */\n private buildReasoningParams(reasoning: ModelCallOptions[\"reasoning\"]): {\n reasoning?: { effort: ReturnType<typeof toOpenAIReasoningEffort> };\n } {\n if (!this.capabilities.reasoning || !reasoning?.effort) {\n return {};\n }\n\n return { reasoning: { effort: toOpenAIReasoningEffort(reasoning.effort) } };\n }\n\n /**\n * Walk `response.output`: `message` items contribute their `output_text`\n * text and any `refusal` text (surfaced as ordinary content, decision 5);\n * each completed `function_call` item becomes one tool call with\n * `id = call_id`. `reasoning` items carry no visible content and are\n * not content. A `function_call` the API marked `incomplete` (truncated\n * arguments) is skipped rather than dispatched with empty input.\n * `reasoning` items are captured and attached to the first tool call for\n * replay (see `attachReasoningReplay`).\n */\n private readOutput(output: OpenAI.Responses.ResponseOutputItem[]): {\n content: string;\n toolCalls: ModelToolCallRequest[] | undefined;\n } {\n let content = \"\";\n const toolCalls: ModelToolCallRequest[] = [];\n const reasoningItems: OpenAI.Responses.ResponseReasoningItem[] = [];\n\n for (const item of output) {\n if (item.type === \"reasoning\") {\n reasoningItems.push(item);\n continue;\n }\n\n if (item.type === \"message\") {\n for (const part of item.content) {\n content += part.type === \"refusal\" ? part.refusal : part.text;\n }\n\n continue;\n }\n\n if (item.type === \"function_call\" && item.status !== \"incomplete\") {\n toolCalls.push(this.toToolCall(item));\n }\n }\n\n return {\n content,\n toolCalls:\n toolCalls.length > 0\n ? attachReasoningReplay(toolCalls, this.name, reasoningItems)\n : undefined,\n };\n }\n\n /** One completed `function_call` item as a neutral tool call (`call_id` -> `id`). */\n private toToolCall(item: OpenAI.Responses.ResponseFunctionToolCall): ModelToolCallRequest {\n return {\n id: item.call_id,\n name: item.name,\n input: safeJsonParse<Record<string, unknown>>(item.arguments, {}),\n };\n }\n\n /**\n * Normalize `usage` (absent on some partials) into the neutral `Usage`.\n * `input_tokens_details.cached_tokens` -> `cachedTokens`,\n * `cache_write_tokens` -> `cacheWriteTokens`,\n * `output_tokens_details.reasoning_tokens` -> `reasoningTokens`. The\n * detail blocks are typed non-optional but read defensively.\n */\n private extractUsage(raw: OpenAI.Responses.ResponseUsage | undefined): Usage {\n if (!raw) {\n return { input: 0, output: 0, total: 0 };\n }\n\n return buildUsage({\n input: raw.input_tokens,\n output: raw.output_tokens,\n total: raw.total_tokens,\n cachedTokens: raw.input_tokens_details?.cached_tokens,\n cacheWriteTokens: raw.input_tokens_details?.cache_write_tokens,\n reasoningTokens: raw.output_tokens_details?.reasoning_tokens,\n });\n }\n\n /** Wrap (idempotently), log and return a failure for the caller to throw. */\n private logAndWrap(thrown: unknown): ReturnType<typeof wrapOpenAIError> {\n const wrapped = wrapOpenAIError(thrown);\n\n this.logger.error(LOG_MODULE, \"error\", wrapped.message, {\n code: wrapped.code,\n context: wrapped.context,\n });\n\n return wrapped;\n }\n}\n"],"mappings":"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"}
|
package/esm/sdk.d.mts
CHANGED
|
@@ -52,6 +52,13 @@ declare class OpenAISDK implements SDKAdapterContract {
|
|
|
52
52
|
* Pricing resolution: per-model `config.pricing` wins; otherwise the
|
|
53
53
|
* SDK-level registry entry keyed by `config.name`; otherwise
|
|
54
54
|
* `undefined` (no cost computed).
|
|
55
|
+
*
|
|
56
|
+
* `config.api` selects the wire API. Omitted or `"chat"` builds the Chat
|
|
57
|
+
* Completions `OpenAIModel`. `"responses"` builds `OpenAIResponsesModel`
|
|
58
|
+
* and is accepted ONLY when this SDK's provider label is `"openai"`: any
|
|
59
|
+
* other label (the wrapper packages deepseek / groq / xai / mistral, or a
|
|
60
|
+
* custom gateway label) throws, so an endpoint not known to implement
|
|
61
|
+
* `/v1/responses` is never sent there.
|
|
55
62
|
*/
|
|
56
63
|
model(config: OpenAIModelConfig): ModelContract;
|
|
57
64
|
/**
|
package/esm/sdk.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sdk.d.mts","names":[],"sources":["../../../../../../ai-openai/src/sdk.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"sdk.d.mts","names":[],"sources":["../../../../../../ai-openai/src/sdk.ts"],"mappings":";;;;;;AA4DA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAAa,SAAA,YAAqB,kBAAA;EAAA,iBACf,MAAA;EAAA,iBACA,QAAA;EAAA,iBACA,OAAA;cAEE,MAAA,EAAQ,eAAA;EAqGpB;;;;;;;;;;;;;AAkCyE;;;;;;EAxGzE,KAAA,CAAM,MAAA,EAAQ,iBAAA,GAAoB,aAAA;;;;;;;;EAmC5B,KAAA,CAAM,IAAA,UAAc,MAAA,YAAkB,OAAA;;;;;;;;;;;EAc5C,QAAA,CAAS,MAAA,EAAQ,oBAAA,GAAuB,gBAAA;;;;;;;;;;;;;;;;;;EAqBxC,KAAA,CAAM,MAAA,EAAQ,iBAAA,GAAoB,kBAAA;;;;;;;;;;;;EAmBlC,MAAA,CAAO,MAAA,EAAQ,kBAAA,GAAqB,mBAAA;;;;;;;;;;;;EAepC,UAAA,CAAW,MAAA,EAAQ,yBAAA,GAA4B,0BAAA;AAAA"}
|
package/esm/sdk.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { OpenAIEmbedder } from "./embedder.mjs";
|
|
2
2
|
import { OpenAIImageModel } from "./image.mjs";
|
|
3
3
|
import { OpenAIModel } from "./model.mjs";
|
|
4
|
+
import { OpenAIResponsesModel } from "./responses-model.mjs";
|
|
4
5
|
import { OpenAISpeechModel } from "./speech.mjs";
|
|
5
6
|
import { OpenAITranscriptionModel } from "./transcription.mjs";
|
|
6
7
|
import OpenAI from "openai";
|
|
7
|
-
import { approximateTokenCount } from "@warlock.js/ai";
|
|
8
|
+
import { InvalidRequestError, approximateTokenCount } from "@warlock.js/ai";
|
|
8
9
|
|
|
9
10
|
//#region ../ai-openai/src/sdk.ts
|
|
10
11
|
/**
|
|
@@ -59,13 +60,32 @@ var OpenAISDK = class {
|
|
|
59
60
|
* Pricing resolution: per-model `config.pricing` wins; otherwise the
|
|
60
61
|
* SDK-level registry entry keyed by `config.name`; otherwise
|
|
61
62
|
* `undefined` (no cost computed).
|
|
63
|
+
*
|
|
64
|
+
* `config.api` selects the wire API. Omitted or `"chat"` builds the Chat
|
|
65
|
+
* Completions `OpenAIModel`. `"responses"` builds `OpenAIResponsesModel`
|
|
66
|
+
* and is accepted ONLY when this SDK's provider label is `"openai"`: any
|
|
67
|
+
* other label (the wrapper packages deepseek / groq / xai / mistral, or a
|
|
68
|
+
* custom gateway label) throws, so an endpoint not known to implement
|
|
69
|
+
* `/v1/responses` is never sent there.
|
|
62
70
|
*/
|
|
63
71
|
model(config) {
|
|
72
|
+
const api = config.api;
|
|
73
|
+
if (api !== void 0 && api !== "chat" && api !== "responses") throw new InvalidRequestError(`Unknown OpenAI api "${String(api)}"; expected "chat" or "responses".`, { context: {
|
|
74
|
+
provider: this.provider,
|
|
75
|
+
model: config.name,
|
|
76
|
+
api
|
|
77
|
+
} });
|
|
78
|
+
if (api === "responses" && this.provider !== "openai") throw new InvalidRequestError(`api: "responses" is supported only on the direct "openai" provider; this SDK is labeled "${this.provider}". Use the default api: "chat".`, { context: {
|
|
79
|
+
provider: this.provider,
|
|
80
|
+
model: config.name,
|
|
81
|
+
api
|
|
82
|
+
} });
|
|
64
83
|
const resolvedPricing = config.pricing ?? this.pricing?.[config.name];
|
|
65
84
|
const resolvedConfig = resolvedPricing === config.pricing ? config : {
|
|
66
85
|
...config,
|
|
67
86
|
pricing: resolvedPricing
|
|
68
87
|
};
|
|
88
|
+
if (api === "responses") return new OpenAIResponsesModel(this.client, resolvedConfig, this.provider);
|
|
69
89
|
return new OpenAIModel(this.client, resolvedConfig, this.provider);
|
|
70
90
|
}
|
|
71
91
|
/**
|
package/esm/sdk.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sdk.mjs","names":[],"sources":["../../../../../../ai-openai/src/sdk.ts"],"sourcesContent":["import OpenAI from \"openai\";\nimport type {\n EmbedderContract,\n ImageModelContract,\n ModelContract,\n ModelPricing,\n SDKAdapterContract,\n SpeechModelContract,\n TranscriptionModelContract,\n} from \"@warlock.js/ai\";\nimport { approximateTokenCount } from \"@warlock.js/ai\";\nimport type {\n OpenAIEmbedderConfig,\n OpenAIImageConfig,\n OpenAIModelConfig,\n OpenAISDKConfig,\n OpenAISpeechConfig,\n OpenAITranscriptionConfig,\n} from \"./config.type\";\nimport { OpenAIEmbedder } from \"./embedder\";\nimport { OpenAIImageModel } from \"./image\";\nimport { OpenAIModel } from \"./model\";\nimport { OpenAISpeechModel } from \"./speech\";\nimport { OpenAITranscriptionModel } from \"./transcription\";\n\n/**\n * OpenAI-backed implementation of `SDKAdapterContract`.\n *\n * **Role.** The package entry point for any OpenAI-compatible provider\n * (OpenAI, Azure OpenAI, OpenRouter, local gateways speaking the Chat\n * Completions protocol). A single `OpenAISDK` instance holds one live\n * `OpenAI` client, shared by every `ModelContract` it produces via\n * `model()`. Users construct one SDK per provider/account and reuse it\n * across all agents, workflows, and supervisors that target that\n * provider.\n *\n * **Responsibility.**\n * - Owns: a long-lived `OpenAI` client (authentication, base URL) and\n * its lifetime scope. Factory for `OpenAIModel` instances — each\n * model call gets a reference to the same client.\n * - Does NOT own: anything per-call (tool execution, message history,\n * streaming loop) — those live in `OpenAIModel` and the agent runtime.\n *\n * Modeled as a class (see §4.2 of code-style.md — \"long-lived state\n * across many calls\"): the `OpenAI` client is heavy to construct and\n * designed to be reused; keeping it on `this` makes that reuse\n * explicit and aligns with the PascalCase naming convention readers\n * expect from a constructor.\n *\n * @example\n * const openai = new OpenAISDK({ apiKey: process.env.OPENAI_API_KEY! });\n * const model = openai.model({ name: \"gpt-4o\", temperature: 0.7 });\n * const tokens = await openai.count(\"Hello world\");\n *\n * @example\n * // Compose into an `ai.openai` namespace for ergonomic agent wiring\n * const ai = { agent, tool, systemPrompt, persona, instruction, openai: new OpenAISDK({ apiKey }) };\n * const myAgent = ai.agent({ model: ai.openai.model({ name: \"gpt-4o-mini\" }) });\n */\nexport class OpenAISDK implements SDKAdapterContract {\n private readonly client: OpenAI;\n private readonly provider: string;\n private readonly pricing?: Record<string, ModelPricing>;\n\n public constructor(config: OpenAISDKConfig) {\n // Peel off the framework-only keys and forward every other upstream\n // `ClientOptions` (timeout, maxRetries, defaultHeaders, fetch,\n // organization, project, …) verbatim — they type-check, so dropping them\n // is a silent footgun. Mirrors the Bedrock/Google/Ollama adapters.\n const { provider, pricing, ...clientOptions } = config;\n\n this.client = new OpenAI(clientOptions);\n this.provider = provider ?? \"openai\";\n this.pricing = pricing;\n }\n\n /**\n * Build an `OpenAIModel` bound to this SDK's client. Each call returns\n * a fresh model instance, but all instances share the underlying\n * `OpenAI` client — connection pools, rate limits, and authentication\n * state stay unified across every model produced here. The SDK's\n * `provider` label is forwarded so every model self-identifies as\n * coming from the same upstream.\n *\n * Pricing resolution: per-model `config.pricing` wins; otherwise the\n * SDK-level registry entry keyed by `config.name`; otherwise\n * `undefined` (no cost computed).\n */\n public model(config: OpenAIModelConfig): ModelContract {\n const resolvedPricing = config.pricing ?? this.pricing?.[config.name];\n const resolvedConfig: OpenAIModelConfig =\n resolvedPricing === config.pricing ? config : { ...config, pricing: resolvedPricing };\n\n return new OpenAIModel(this.client, resolvedConfig, this.provider);\n }\n\n /**\n * Rough token-count estimate for a given text. Uses a\n * character-heuristic (`approximateTokenCount`) from the core package\n * — good enough for budgeting and quota guards, not for billing.\n * Accepts an optional model id for future per-model tokenizer\n * dispatch; currently ignored.\n */\n public async count(text: string, _model?: string): Promise<number> {\n return approximateTokenCount(text);\n }\n\n /**\n * Build an `OpenAIEmbedder` bound to this SDK's client. Each call\n * returns a fresh embedder instance sharing the same underlying\n * `OpenAI` client — connection pools and authentication stay unified\n * across every embedder produced here.\n *\n * @example\n * const embedder = openai.embedder({ name: \"text-embedding-3-small\" });\n * const { vector } = await embedder.embed(\"Hello world\");\n */\n public embedder(config: OpenAIEmbedderConfig): EmbedderContract {\n return new OpenAIEmbedder(this.client, config);\n }\n\n /**\n * Build an `OpenAIImageModel` bound to this SDK's client for use with\n * `ai.image({ model, prompt })`. `config.name` is passed through to\n * `client.images.generate` as given — no id is rejected locally, so\n * an unsupported model fails at OpenAI, not here. Metering follows\n * the family: `gpt-image-*` is token-metered, `dall-e-*` per-image.\n *\n * Pricing resolution mirrors `model()`: per-model `config.pricing`\n * wins, otherwise the SDK-level registry entry keyed by `config.name`,\n * otherwise `undefined` (no cost computed). A token-priced\n * `gpt-image-1` entry can live in the same SDK registry as the chat\n * models.\n *\n * @example\n * const model = openai.image({ name: \"gpt-image-1\" });\n * const { data } = await ai.image({ model, prompt: \"a red bicycle\" });\n */\n public image(config: OpenAIImageConfig): ImageModelContract {\n const resolvedPricing = config.pricing ?? this.pricing?.[config.name];\n const resolvedConfig: OpenAIImageConfig =\n resolvedPricing === config.pricing ? config : { ...config, pricing: resolvedPricing };\n\n return new OpenAIImageModel(this.client, resolvedConfig, this.provider);\n }\n\n /**\n * Build an `OpenAISpeechModel` (text-to-speech) bound to this SDK's\n * client, for use with `ai.speech({ model, text })`. `config.name` is\n * passed through to `audio.speech.create` as given — no id is\n * rejected locally, so an unsupported model fails at OpenAI, not\n * here.\n *\n * @example\n * const tts = openai.speech({ name: \"tts-1\", voice: \"alloy\" });\n * const { data } = await ai.speech({ model: tts, text: \"Hello\" });\n */\n public speech(config: OpenAISpeechConfig): SpeechModelContract {\n return new OpenAISpeechModel(this.client, config, this.provider);\n }\n\n /**\n * Build an `OpenAITranscriptionModel` (speech-to-text) bound to this\n * SDK's client, for use with `ai.transcribe({ model, audio })`.\n * `config.name` is passed through to `audio.transcriptions.create`\n * as given — no id is rejected locally, so an unsupported model\n * fails at OpenAI, not here.\n *\n * @example\n * const stt = openai.transcribe({ name: \"whisper-1\" });\n * const { data } = await ai.transcribe({ model: stt, audio });\n */\n public transcribe(config: OpenAITranscriptionConfig): TranscriptionModelContract {\n return new OpenAITranscriptionModel(this.client, config, this.provider);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2DA,IAAa,YAAb,MAAqD;CAKnD,AAAO,YAAY,QAAyB;EAK1C,MAAM,EAAE,UAAU,SAAS,GAAG,kBAAkB;EAEhD,KAAK,SAAS,IAAI,OAAO,aAAa;EACtC,KAAK,WAAW,YAAY;EAC5B,KAAK,UAAU;CACjB;;;;;;;;;;;;;CAcA,AAAO,MAAM,QAA0C;EACrD,MAAM,kBAAkB,OAAO,WAAW,KAAK,UAAU,OAAO;EAChE,MAAM,iBACJ,oBAAoB,OAAO,UAAU,SAAS;GAAE,GAAG;GAAQ,SAAS;EAAgB;EAEtF,OAAO,IAAI,YAAY,KAAK,QAAQ,gBAAgB,KAAK,QAAQ;CACnE;;;;;;;;CASA,MAAa,MAAM,MAAc,QAAkC;EACjE,OAAO,sBAAsB,IAAI;CACnC;;;;;;;;;;;CAYA,AAAO,SAAS,QAAgD;EAC9D,OAAO,IAAI,eAAe,KAAK,QAAQ,MAAM;CAC/C;;;;;;;;;;;;;;;;;;CAmBA,AAAO,MAAM,QAA+C;EAC1D,MAAM,kBAAkB,OAAO,WAAW,KAAK,UAAU,OAAO;EAChE,MAAM,iBACJ,oBAAoB,OAAO,UAAU,SAAS;GAAE,GAAG;GAAQ,SAAS;EAAgB;EAEtF,OAAO,IAAI,iBAAiB,KAAK,QAAQ,gBAAgB,KAAK,QAAQ;CACxE;;;;;;;;;;;;CAaA,AAAO,OAAO,QAAiD;EAC7D,OAAO,IAAI,kBAAkB,KAAK,QAAQ,QAAQ,KAAK,QAAQ;CACjE;;;;;;;;;;;;CAaA,AAAO,WAAW,QAA+D;EAC/E,OAAO,IAAI,yBAAyB,KAAK,QAAQ,QAAQ,KAAK,QAAQ;CACxE;AACF"}
|
|
1
|
+
{"version":3,"file":"sdk.mjs","names":[],"sources":["../../../../../../ai-openai/src/sdk.ts"],"sourcesContent":["import OpenAI from \"openai\";\nimport type {\n EmbedderContract,\n ImageModelContract,\n ModelContract,\n ModelPricing,\n SDKAdapterContract,\n SpeechModelContract,\n TranscriptionModelContract,\n} from \"@warlock.js/ai\";\nimport { InvalidRequestError, approximateTokenCount } from \"@warlock.js/ai\";\nimport type {\n OpenAIEmbedderConfig,\n OpenAIImageConfig,\n OpenAIModelConfig,\n OpenAISDKConfig,\n OpenAISpeechConfig,\n OpenAITranscriptionConfig,\n} from \"./config.type\";\nimport { OpenAIEmbedder } from \"./embedder\";\nimport { OpenAIImageModel } from \"./image\";\nimport { OpenAIModel } from \"./model\";\nimport { OpenAIResponsesModel } from \"./responses-model\";\nimport { OpenAISpeechModel } from \"./speech\";\nimport { OpenAITranscriptionModel } from \"./transcription\";\n\n/**\n * OpenAI-backed implementation of `SDKAdapterContract`.\n *\n * **Role.** The package entry point for any OpenAI-compatible provider\n * (OpenAI, Azure OpenAI, OpenRouter, local gateways speaking the Chat\n * Completions protocol). A single `OpenAISDK` instance holds one live\n * `OpenAI` client, shared by every `ModelContract` it produces via\n * `model()`. Users construct one SDK per provider/account and reuse it\n * across all agents, workflows, and supervisors that target that\n * provider.\n *\n * **Responsibility.**\n * - Owns: a long-lived `OpenAI` client (authentication, base URL) and\n * its lifetime scope. Factory for `OpenAIModel` instances — each\n * model call gets a reference to the same client.\n * - Does NOT own: anything per-call (tool execution, message history,\n * streaming loop) — those live in `OpenAIModel` and the agent runtime.\n *\n * Modeled as a class (see §4.2 of code-style.md — \"long-lived state\n * across many calls\"): the `OpenAI` client is heavy to construct and\n * designed to be reused; keeping it on `this` makes that reuse\n * explicit and aligns with the PascalCase naming convention readers\n * expect from a constructor.\n *\n * @example\n * const openai = new OpenAISDK({ apiKey: process.env.OPENAI_API_KEY! });\n * const model = openai.model({ name: \"gpt-4o\", temperature: 0.7 });\n * const tokens = await openai.count(\"Hello world\");\n *\n * @example\n * // Compose into an `ai.openai` namespace for ergonomic agent wiring\n * const ai = { agent, tool, systemPrompt, persona, instruction, openai: new OpenAISDK({ apiKey }) };\n * const myAgent = ai.agent({ model: ai.openai.model({ name: \"gpt-4o-mini\" }) });\n */\nexport class OpenAISDK implements SDKAdapterContract {\n private readonly client: OpenAI;\n private readonly provider: string;\n private readonly pricing?: Record<string, ModelPricing>;\n\n public constructor(config: OpenAISDKConfig) {\n // Peel off the framework-only keys and forward every other upstream\n // `ClientOptions` (timeout, maxRetries, defaultHeaders, fetch,\n // organization, project, …) verbatim — they type-check, so dropping them\n // is a silent footgun. Mirrors the Bedrock/Google/Ollama adapters.\n const { provider, pricing, ...clientOptions } = config;\n\n this.client = new OpenAI(clientOptions);\n this.provider = provider ?? \"openai\";\n this.pricing = pricing;\n }\n\n /**\n * Build an `OpenAIModel` bound to this SDK's client. Each call returns\n * a fresh model instance, but all instances share the underlying\n * `OpenAI` client — connection pools, rate limits, and authentication\n * state stay unified across every model produced here. The SDK's\n * `provider` label is forwarded so every model self-identifies as\n * coming from the same upstream.\n *\n * Pricing resolution: per-model `config.pricing` wins; otherwise the\n * SDK-level registry entry keyed by `config.name`; otherwise\n * `undefined` (no cost computed).\n *\n * `config.api` selects the wire API. Omitted or `\"chat\"` builds the Chat\n * Completions `OpenAIModel`. `\"responses\"` builds `OpenAIResponsesModel`\n * and is accepted ONLY when this SDK's provider label is `\"openai\"`: any\n * other label (the wrapper packages deepseek / groq / xai / mistral, or a\n * custom gateway label) throws, so an endpoint not known to implement\n * `/v1/responses` is never sent there.\n */\n public model(config: OpenAIModelConfig): ModelContract {\n const api: unknown = config.api;\n\n if (api !== undefined && api !== \"chat\" && api !== \"responses\") {\n throw new InvalidRequestError(\n `Unknown OpenAI api \"${String(api)}\"; expected \"chat\" or \"responses\".`,\n { context: { provider: this.provider, model: config.name, api } },\n );\n }\n\n if (api === \"responses\" && this.provider !== \"openai\") {\n throw new InvalidRequestError(\n `api: \"responses\" is supported only on the direct \"openai\" provider; this SDK is labeled \"${this.provider}\". Use the default api: \"chat\".`,\n { context: { provider: this.provider, model: config.name, api } },\n );\n }\n\n const resolvedPricing = config.pricing ?? this.pricing?.[config.name];\n const resolvedConfig: OpenAIModelConfig =\n resolvedPricing === config.pricing ? config : { ...config, pricing: resolvedPricing };\n\n if (api === \"responses\") {\n return new OpenAIResponsesModel(this.client, resolvedConfig, this.provider);\n }\n\n return new OpenAIModel(this.client, resolvedConfig, this.provider);\n }\n\n /**\n * Rough token-count estimate for a given text. Uses a\n * character-heuristic (`approximateTokenCount`) from the core package\n * — good enough for budgeting and quota guards, not for billing.\n * Accepts an optional model id for future per-model tokenizer\n * dispatch; currently ignored.\n */\n public async count(text: string, _model?: string): Promise<number> {\n return approximateTokenCount(text);\n }\n\n /**\n * Build an `OpenAIEmbedder` bound to this SDK's client. Each call\n * returns a fresh embedder instance sharing the same underlying\n * `OpenAI` client — connection pools and authentication stay unified\n * across every embedder produced here.\n *\n * @example\n * const embedder = openai.embedder({ name: \"text-embedding-3-small\" });\n * const { vector } = await embedder.embed(\"Hello world\");\n */\n public embedder(config: OpenAIEmbedderConfig): EmbedderContract {\n return new OpenAIEmbedder(this.client, config);\n }\n\n /**\n * Build an `OpenAIImageModel` bound to this SDK's client for use with\n * `ai.image({ model, prompt })`. `config.name` is passed through to\n * `client.images.generate` as given — no id is rejected locally, so\n * an unsupported model fails at OpenAI, not here. Metering follows\n * the family: `gpt-image-*` is token-metered, `dall-e-*` per-image.\n *\n * Pricing resolution mirrors `model()`: per-model `config.pricing`\n * wins, otherwise the SDK-level registry entry keyed by `config.name`,\n * otherwise `undefined` (no cost computed). A token-priced\n * `gpt-image-1` entry can live in the same SDK registry as the chat\n * models.\n *\n * @example\n * const model = openai.image({ name: \"gpt-image-1\" });\n * const { data } = await ai.image({ model, prompt: \"a red bicycle\" });\n */\n public image(config: OpenAIImageConfig): ImageModelContract {\n const resolvedPricing = config.pricing ?? this.pricing?.[config.name];\n const resolvedConfig: OpenAIImageConfig =\n resolvedPricing === config.pricing ? config : { ...config, pricing: resolvedPricing };\n\n return new OpenAIImageModel(this.client, resolvedConfig, this.provider);\n }\n\n /**\n * Build an `OpenAISpeechModel` (text-to-speech) bound to this SDK's\n * client, for use with `ai.speech({ model, text })`. `config.name` is\n * passed through to `audio.speech.create` as given — no id is\n * rejected locally, so an unsupported model fails at OpenAI, not\n * here.\n *\n * @example\n * const tts = openai.speech({ name: \"tts-1\", voice: \"alloy\" });\n * const { data } = await ai.speech({ model: tts, text: \"Hello\" });\n */\n public speech(config: OpenAISpeechConfig): SpeechModelContract {\n return new OpenAISpeechModel(this.client, config, this.provider);\n }\n\n /**\n * Build an `OpenAITranscriptionModel` (speech-to-text) bound to this\n * SDK's client, for use with `ai.transcribe({ model, audio })`.\n * `config.name` is passed through to `audio.transcriptions.create`\n * as given — no id is rejected locally, so an unsupported model\n * fails at OpenAI, not here.\n *\n * @example\n * const stt = openai.transcribe({ name: \"whisper-1\" });\n * const { data } = await ai.transcribe({ model: stt, audio });\n */\n public transcribe(config: OpenAITranscriptionConfig): TranscriptionModelContract {\n return new OpenAITranscriptionModel(this.client, config, this.provider);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4DA,IAAa,YAAb,MAAqD;CAKnD,AAAO,YAAY,QAAyB;EAK1C,MAAM,EAAE,UAAU,SAAS,GAAG,kBAAkB;EAEhD,KAAK,SAAS,IAAI,OAAO,aAAa;EACtC,KAAK,WAAW,YAAY;EAC5B,KAAK,UAAU;CACjB;;;;;;;;;;;;;;;;;;;;CAqBA,AAAO,MAAM,QAA0C;EACrD,MAAM,MAAe,OAAO;EAE5B,IAAI,QAAQ,UAAa,QAAQ,UAAU,QAAQ,aACjD,MAAM,IAAI,oBACR,uBAAuB,OAAO,GAAG,EAAE,qCACnC,EAAE,SAAS;GAAE,UAAU,KAAK;GAAU,OAAO,OAAO;GAAM;EAAI,EAAE,CAClE;EAGF,IAAI,QAAQ,eAAe,KAAK,aAAa,UAC3C,MAAM,IAAI,oBACR,4FAA4F,KAAK,SAAS,kCAC1G,EAAE,SAAS;GAAE,UAAU,KAAK;GAAU,OAAO,OAAO;GAAM;EAAI,EAAE,CAClE;EAGF,MAAM,kBAAkB,OAAO,WAAW,KAAK,UAAU,OAAO;EAChE,MAAM,iBACJ,oBAAoB,OAAO,UAAU,SAAS;GAAE,GAAG;GAAQ,SAAS;EAAgB;EAEtF,IAAI,QAAQ,aACV,OAAO,IAAI,qBAAqB,KAAK,QAAQ,gBAAgB,KAAK,QAAQ;EAG5E,OAAO,IAAI,YAAY,KAAK,QAAQ,gBAAgB,KAAK,QAAQ;CACnE;;;;;;;;CASA,MAAa,MAAM,MAAc,QAAkC;EACjE,OAAO,sBAAsB,IAAI;CACnC;;;;;;;;;;;CAYA,AAAO,SAAS,QAAgD;EAC9D,OAAO,IAAI,eAAe,KAAK,QAAQ,MAAM;CAC/C;;;;;;;;;;;;;;;;;;CAmBA,AAAO,MAAM,QAA+C;EAC1D,MAAM,kBAAkB,OAAO,WAAW,KAAK,UAAU,OAAO;EAChE,MAAM,iBACJ,oBAAoB,OAAO,UAAU,SAAS;GAAE,GAAG;GAAQ,SAAS;EAAgB;EAEtF,OAAO,IAAI,iBAAiB,KAAK,QAAQ,gBAAgB,KAAK,QAAQ;CACxE;;;;;;;;;;;;CAaA,AAAO,OAAO,QAAiD;EAC7D,OAAO,IAAI,kBAAkB,KAAK,QAAQ,QAAQ,KAAK,QAAQ;CACjE;;;;;;;;;;;;CAaA,AAAO,WAAW,QAA+D;EAC/E,OAAO,IAAI,yBAAyB,KAAK,QAAQ,QAAQ,KAAK,QAAQ;CACxE;AACF"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region ../ai-openai/src/utils/build-usage.ts
|
|
2
|
+
/**
|
|
3
|
+
* Build the neutral `Usage` from extracted counts. The optional detail
|
|
4
|
+
* counters are emitted only when the provider reports a positive value, so
|
|
5
|
+
* non-reasoning / uncached calls keep the lean `{ input, output, total }`
|
|
6
|
+
* shape. Shared by the Chat Completions and Responses adapters so both
|
|
7
|
+
* apply the same rule.
|
|
8
|
+
*/
|
|
9
|
+
function buildUsage(counts) {
|
|
10
|
+
const { input, output, total, cachedTokens, reasoningTokens, cacheWriteTokens } = counts;
|
|
11
|
+
return {
|
|
12
|
+
input,
|
|
13
|
+
output,
|
|
14
|
+
total,
|
|
15
|
+
...cachedTokens !== void 0 && cachedTokens > 0 ? { cachedTokens } : {},
|
|
16
|
+
...reasoningTokens !== void 0 && reasoningTokens > 0 ? { reasoningTokens } : {},
|
|
17
|
+
...cacheWriteTokens !== void 0 && cacheWriteTokens > 0 ? { cacheWriteTokens } : {}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
//#endregion
|
|
22
|
+
export { buildUsage };
|
|
23
|
+
//# sourceMappingURL=build-usage.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-usage.mjs","names":[],"sources":["../../../../../../../ai-openai/src/utils/build-usage.ts"],"sourcesContent":["import type { Usage } from \"@warlock.js/ai\";\n\n/** The already-extracted token counts of one provider usage block. */\nexport type UsageCounts = {\n input: number;\n output: number;\n total: number;\n /** Subset of `input` served from the prompt cache. */\n cachedTokens?: number;\n /** Hidden reasoning channel, already counted within `output`. */\n reasoningTokens?: number;\n /** Subset of `input` written to the prompt cache. */\n cacheWriteTokens?: number;\n};\n\n/**\n * Build the neutral `Usage` from extracted counts. The optional detail\n * counters are emitted only when the provider reports a positive value, so\n * non-reasoning / uncached calls keep the lean `{ input, output, total }`\n * shape. Shared by the Chat Completions and Responses adapters so both\n * apply the same rule.\n */\nexport function buildUsage(counts: UsageCounts): Usage {\n const { input, output, total, cachedTokens, reasoningTokens, cacheWriteTokens } = counts;\n\n return {\n input,\n output,\n total,\n ...(cachedTokens !== undefined && cachedTokens > 0 ? { cachedTokens } : {}),\n ...(reasoningTokens !== undefined && reasoningTokens > 0 ? { reasoningTokens } : {}),\n ...(cacheWriteTokens !== undefined && cacheWriteTokens > 0 ? { cacheWriteTokens } : {}),\n };\n}\n"],"mappings":";;;;;;;;AAsBA,SAAgB,WAAW,QAA4B;CACrD,MAAM,EAAE,OAAO,QAAQ,OAAO,cAAc,iBAAiB,qBAAqB;CAElF,OAAO;EACL;EACA;EACA;EACA,GAAI,iBAAiB,UAAa,eAAe,IAAI,EAAE,aAAa,IAAI,CAAC;EACzE,GAAI,oBAAoB,UAAa,kBAAkB,IAAI,EAAE,gBAAgB,IAAI,CAAC;EAClF,GAAI,qBAAqB,UAAa,mBAAmB,IAAI,EAAE,iBAAiB,IAAI,CAAC;CACvF;AACF"}
|
package/esm/utils/index.mjs
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
+
import { buildUsage } from "./build-usage.mjs";
|
|
2
|
+
import { wrapOpenAIError } from "./wrap-openai-error.mjs";
|
|
3
|
+
import { mapResponsesFinishReason, toResponseFailure } from "./map-responses-status.mjs";
|
|
1
4
|
import { mapFinishReason } from "./map-finish-reason.mjs";
|
|
5
|
+
import { buildPromptCacheParams } from "./prompt-cache-params.mjs";
|
|
6
|
+
import { toOpenAIReasoningEffort } from "./reasoning-effort.mjs";
|
|
7
|
+
import { inferStructuredOutput, planStructuredOutput } from "./structured-output.mjs";
|
|
8
|
+
import { RESPONSES_METADATA_KEY, attachReasoningReplay, pickReplayableReasoning, readReasoningReplay, toReplayReasoningItem } from "./reasoning-replay.mjs";
|
|
2
9
|
import { toOpenAIMessages } from "./to-openai-messages.mjs";
|
|
3
10
|
import { toOpenAITools } from "./to-openai-tools.mjs";
|
|
4
|
-
import {
|
|
11
|
+
import { toResponsesInput } from "./to-responses-input.mjs";
|
|
12
|
+
import { toResponsesToolChoice, toResponsesTools } from "./to-responses-tools.mjs";
|
|
5
13
|
|
|
6
14
|
export { };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { wrapOpenAIError } from "./wrap-openai-error.mjs";
|
|
2
|
+
|
|
3
|
+
//#region ../ai-openai/src/utils/map-responses-status.ts
|
|
4
|
+
/**
|
|
5
|
+
* Derive our `FinishReason` from a Responses terminal state. Responses has
|
|
6
|
+
* no `finish_reason`, so it comes from `status`, `incomplete_details` and the
|
|
7
|
+
* output items (design section 2, decision 5):
|
|
8
|
+
*
|
|
9
|
+
* - `completed` with a function call -> `tool_calls`; without -> `stop`
|
|
10
|
+
* (a refusal is ordinary text and also `stop`).
|
|
11
|
+
* - `incomplete` / `max_output_tokens` and `max_messages` -> `length`.
|
|
12
|
+
* - `incomplete` / `content_filter` and `steered` (or an unknown / missing
|
|
13
|
+
* reason) -> `error`. There is no neutral `content_filter` value.
|
|
14
|
+
* - `cancelled`, `queued`, `in_progress` at a terminal read -> `error`.
|
|
15
|
+
* - `failed` is not mapped here: callers throw it via {@link toResponseFailure}.
|
|
16
|
+
*
|
|
17
|
+
* A missing `status` is treated as `completed` (the SDK types it optional).
|
|
18
|
+
*/
|
|
19
|
+
function mapResponsesFinishReason(state) {
|
|
20
|
+
const status = state.status ?? "completed";
|
|
21
|
+
if (status === "completed") return state.hasToolCalls ? "tool_calls" : "stop";
|
|
22
|
+
if (status === "incomplete") {
|
|
23
|
+
const reason = state.incomplete_details?.reason;
|
|
24
|
+
return reason === "max_output_tokens" || reason === "max_messages" ? "length" : "error";
|
|
25
|
+
}
|
|
26
|
+
return "error";
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Turn a `failed` Responses payload into the same typed `AIError` the Chat
|
|
30
|
+
* path throws for a provider failure, by running it through
|
|
31
|
+
* `wrapOpenAIError` (so `rate_limit_exceeded` becomes a
|
|
32
|
+
* `ProviderRateLimitError`, everything else a `ProviderError`, with `code`
|
|
33
|
+
* and request id kept on `context`). The response id and model are added so
|
|
34
|
+
* the failure is traceable.
|
|
35
|
+
*/
|
|
36
|
+
function toResponseFailure(response, model) {
|
|
37
|
+
const error = response.error;
|
|
38
|
+
const requestId = response._request_id;
|
|
39
|
+
const wrapped = wrapOpenAIError({
|
|
40
|
+
code: error?.code,
|
|
41
|
+
message: error?.message ?? "OpenAI response failed without an error object.",
|
|
42
|
+
...requestId ? { request_id: requestId } : {}
|
|
43
|
+
});
|
|
44
|
+
if (wrapped.context) Object.assign(wrapped.context, {
|
|
45
|
+
provider: "openai",
|
|
46
|
+
model,
|
|
47
|
+
responseId: response.id
|
|
48
|
+
});
|
|
49
|
+
return wrapped;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
//#endregion
|
|
53
|
+
export { mapResponsesFinishReason, toResponseFailure };
|
|
54
|
+
//# sourceMappingURL=map-responses-status.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"map-responses-status.mjs","names":[],"sources":["../../../../../../../ai-openai/src/utils/map-responses-status.ts"],"sourcesContent":["import type { AIError, FinishReason } from \"@warlock.js/ai\";\nimport type OpenAI from \"openai\";\nimport { wrapOpenAIError } from \"./wrap-openai-error\";\n\n/** The slice of a Responses `Response` the status mapper reads. */\nexport type ResponsesTerminalState = Pick<\n OpenAI.Responses.Response,\n \"status\" | \"incomplete_details\"\n> & {\n /** True when `output` holds at least one `function_call` item the model completed. */\n hasToolCalls: boolean;\n};\n\n/**\n * Derive our `FinishReason` from a Responses terminal state. Responses has\n * no `finish_reason`, so it comes from `status`, `incomplete_details` and the\n * output items (design section 2, decision 5):\n *\n * - `completed` with a function call -> `tool_calls`; without -> `stop`\n * (a refusal is ordinary text and also `stop`).\n * - `incomplete` / `max_output_tokens` and `max_messages` -> `length`.\n * - `incomplete` / `content_filter` and `steered` (or an unknown / missing\n * reason) -> `error`. There is no neutral `content_filter` value.\n * - `cancelled`, `queued`, `in_progress` at a terminal read -> `error`.\n * - `failed` is not mapped here: callers throw it via {@link toResponseFailure}.\n *\n * A missing `status` is treated as `completed` (the SDK types it optional).\n */\nexport function mapResponsesFinishReason(state: ResponsesTerminalState): FinishReason {\n const status = state.status ?? \"completed\";\n\n if (status === \"completed\") {\n return state.hasToolCalls ? \"tool_calls\" : \"stop\";\n }\n\n if (status === \"incomplete\") {\n const reason = state.incomplete_details?.reason;\n\n return reason === \"max_output_tokens\" || reason === \"max_messages\" ? \"length\" : \"error\";\n }\n\n return \"error\";\n}\n\n/**\n * Turn a `failed` Responses payload into the same typed `AIError` the Chat\n * path throws for a provider failure, by running it through\n * `wrapOpenAIError` (so `rate_limit_exceeded` becomes a\n * `ProviderRateLimitError`, everything else a `ProviderError`, with `code`\n * and request id kept on `context`). The response id and model are added so\n * the failure is traceable.\n */\nexport function toResponseFailure(\n response: OpenAI.Responses.Response,\n model: string,\n): AIError {\n const error = response.error;\n const requestId = (response as { _request_id?: string | null })._request_id;\n\n const wrapped = wrapOpenAIError({\n code: error?.code,\n message: error?.message ?? \"OpenAI response failed without an error object.\",\n ...(requestId ? { request_id: requestId } : {}),\n });\n\n if (wrapped.context) {\n Object.assign(wrapped.context, { provider: \"openai\", model, responseId: response.id });\n }\n\n return wrapped;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AA4BA,SAAgB,yBAAyB,OAA6C;CACpF,MAAM,SAAS,MAAM,UAAU;CAE/B,IAAI,WAAW,aACb,OAAO,MAAM,eAAe,eAAe;CAG7C,IAAI,WAAW,cAAc;EAC3B,MAAM,SAAS,MAAM,oBAAoB;EAEzC,OAAO,WAAW,uBAAuB,WAAW,iBAAiB,WAAW;CAClF;CAEA,OAAO;AACT;;;;;;;;;AAUA,SAAgB,kBACd,UACA,OACS;CACT,MAAM,QAAQ,SAAS;CACvB,MAAM,YAAa,SAA6C;CAEhE,MAAM,UAAU,gBAAgB;EAC9B,MAAM,OAAO;EACb,SAAS,OAAO,WAAW;EAC3B,GAAI,YAAY,EAAE,YAAY,UAAU,IAAI,CAAC;CAC/C,CAAC;CAED,IAAI,QAAQ,SACV,OAAO,OAAO,QAAQ,SAAS;EAAE,UAAU;EAAU;EAAO,YAAY,SAAS;CAAG,CAAC;CAGvF,OAAO;AACT"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region ../ai-openai/src/utils/prompt-cache-params.ts
|
|
2
|
+
/**
|
|
3
|
+
* Build the OpenAI-only prompt-cache request fields from the model config.
|
|
4
|
+
* Returns nothing for any provider label other than `"openai"`: wrapper
|
|
5
|
+
* endpoints use the same transport but may reject these OpenAI-only fields.
|
|
6
|
+
*/
|
|
7
|
+
function buildPromptCacheParams(provider, config) {
|
|
8
|
+
if (provider !== "openai") return {};
|
|
9
|
+
return {
|
|
10
|
+
...config.promptCacheKey !== void 0 ? { prompt_cache_key: config.promptCacheKey } : {},
|
|
11
|
+
...config.promptCacheRetention !== void 0 ? { prompt_cache_retention: config.promptCacheRetention } : {}
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
//#endregion
|
|
16
|
+
export { buildPromptCacheParams };
|
|
17
|
+
//# sourceMappingURL=prompt-cache-params.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompt-cache-params.mjs","names":[],"sources":["../../../../../../../ai-openai/src/utils/prompt-cache-params.ts"],"sourcesContent":["import type { OpenAIModelConfig } from \"../config.type\";\n\n/** The OpenAI-only prompt-cache request fields common to Chat Completions and Responses. */\nexport type PromptCacheParams = {\n prompt_cache_key?: string;\n prompt_cache_retention?: NonNullable<OpenAIModelConfig[\"promptCacheRetention\"]>;\n};\n\n/**\n * Build the OpenAI-only prompt-cache request fields from the model config.\n * Returns nothing for any provider label other than `\"openai\"`: wrapper\n * endpoints use the same transport but may reject these OpenAI-only fields.\n */\nexport function buildPromptCacheParams(\n provider: string,\n config: Pick<OpenAIModelConfig, \"promptCacheKey\" | \"promptCacheRetention\">,\n): PromptCacheParams {\n if (provider !== \"openai\") {\n return {};\n }\n\n return {\n ...(config.promptCacheKey !== undefined ? { prompt_cache_key: config.promptCacheKey } : {}),\n ...(config.promptCacheRetention !== undefined\n ? { prompt_cache_retention: config.promptCacheRetention }\n : {}),\n };\n}\n"],"mappings":";;;;;;AAaA,SAAgB,uBACd,UACA,QACmB;CACnB,IAAI,aAAa,UACf,OAAO,CAAC;CAGV,OAAO;EACL,GAAI,OAAO,mBAAmB,SAAY,EAAE,kBAAkB,OAAO,eAAe,IAAI,CAAC;EACzF,GAAI,OAAO,yBAAyB,SAChC,EAAE,wBAAwB,OAAO,qBAAqB,IACtD,CAAC;CACP;AACF"}
|