@warlock.js/ai-openai 5.30.0 → 5.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +42 -1
  2. package/README.md +3 -3
  3. package/cjs/index.cjs +715 -51
  4. package/cjs/index.cjs.map +1 -1
  5. package/esm/config.type.d.mts +13 -13
  6. package/esm/config.type.d.mts.map +1 -1
  7. package/esm/embedder.d.mts +1 -3
  8. package/esm/embedder.d.mts.map +1 -1
  9. package/esm/embedder.mjs.map +1 -1
  10. package/esm/image.d.mts +1 -3
  11. package/esm/image.d.mts.map +1 -1
  12. package/esm/index.d.mts +4 -1
  13. package/esm/index.mjs +3 -1
  14. package/esm/kimi-tools.d.mts +69 -0
  15. package/esm/kimi-tools.d.mts.map +1 -0
  16. package/esm/kimi-tools.mjs +256 -0
  17. package/esm/kimi-tools.mjs.map +1 -0
  18. package/esm/known-pricing.d.mts +39 -0
  19. package/esm/known-pricing.d.mts.map +1 -0
  20. package/esm/known-pricing.mjs +121 -0
  21. package/esm/known-pricing.mjs.map +1 -0
  22. package/esm/model.mjs +82 -23
  23. package/esm/model.mjs.map +1 -1
  24. package/esm/provider-options.d.mts +13 -0
  25. package/esm/provider-options.d.mts.map +1 -0
  26. package/esm/provider-options.mjs +56 -0
  27. package/esm/provider-options.mjs.map +1 -0
  28. package/esm/responses-model.mjs +125 -19
  29. package/esm/responses-model.mjs.map +1 -1
  30. package/esm/sdk.d.mts +4 -5
  31. package/esm/sdk.d.mts.map +1 -1
  32. package/esm/sdk.mjs +5 -3
  33. package/esm/sdk.mjs.map +1 -1
  34. package/esm/speech.d.mts +1 -3
  35. package/esm/speech.d.mts.map +1 -1
  36. package/esm/transcription.d.mts +1 -3
  37. package/esm/transcription.d.mts.map +1 -1
  38. package/esm/transcription.mjs.map +1 -1
  39. package/esm/utils/build-usage.mjs +14 -2
  40. package/esm/utils/build-usage.mjs.map +1 -1
  41. package/esm/utils/hosted-output.mjs +22 -0
  42. package/esm/utils/hosted-output.mjs.map +1 -0
  43. package/esm/utils/index.mjs +2 -2
  44. package/esm/utils/map-responses-status.mjs.map +1 -1
  45. package/esm/utils/prompt-cache-params.mjs +25 -6
  46. package/esm/utils/prompt-cache-params.mjs.map +1 -1
  47. package/esm/utils/reasoning-replay.mjs +23 -1
  48. package/esm/utils/reasoning-replay.mjs.map +1 -1
  49. package/esm/utils/structured-output.mjs +1 -1
  50. package/esm/utils/structured-output.mjs.map +1 -1
  51. package/esm/utils/to-openai-messages.mjs.map +1 -1
  52. package/esm/utils/to-openai-tools.mjs.map +1 -1
  53. package/esm/utils/wrap-openai-error.mjs +6 -1
  54. package/esm/utils/wrap-openai-error.mjs.map +1 -1
  55. package/llms-full.txt +235 -37
  56. package/llms.txt +1 -1
  57. package/package.json +3 -3
  58. package/skills/setup-openai/SKILL.md +235 -37
@@ -0,0 +1,121 @@
1
+ //#region ../../packages/ai-openai/src/known-pricing.ts
2
+ /**
3
+ * Built-in OpenAI API list prices, USD per 1,000,000 tokens, keyed by
4
+ * exact model id. `cachedInput` is the cached-input rate. None of these
5
+ * models charges for cache writes.
6
+ *
7
+ * Source: https://developers.openai.com/api/docs/pricing (the target of
8
+ * platform.openai.com/docs/pricing), Standard tier, fetched 2026-10-17.
9
+ *
10
+ * Applied only when the SDK's provider label is `"openai"`, and only on
11
+ * an exact id match — dated snapshots can carry other prices. Left out on
12
+ * purpose, so pass `pricing` yourself for them:
13
+ *
14
+ * - Models priced by prompt length (a higher rate above 272K input
15
+ * tokens): the gpt-6 / gpt-5.6 / gpt-5.5 / gpt-5.4 families and the
16
+ * `-pro` models. A flat `ModelPricing` cannot express the tiers.
17
+ * - Promotional, Batch, Flex and Priority pricing, and the regional
18
+ * (data-residency) uplift.
19
+ */
20
+ const OPENAI_MODEL_PRICING = {
21
+ "gpt-5.4-mini": {
22
+ input: .75,
23
+ output: 4.5,
24
+ cachedInput: .075
25
+ },
26
+ "gpt-5.4-nano": {
27
+ input: .2,
28
+ output: 1.25,
29
+ cachedInput: .02
30
+ },
31
+ "gpt-5.2": {
32
+ input: 1.75,
33
+ output: 14,
34
+ cachedInput: .175
35
+ },
36
+ "gpt-5.1": {
37
+ input: 1.25,
38
+ output: 10,
39
+ cachedInput: .125
40
+ },
41
+ "gpt-5": {
42
+ input: 1.25,
43
+ output: 10,
44
+ cachedInput: .125
45
+ },
46
+ "gpt-5-mini": {
47
+ input: .25,
48
+ output: 2,
49
+ cachedInput: .025
50
+ },
51
+ "gpt-5-nano": {
52
+ input: .05,
53
+ output: .4,
54
+ cachedInput: .005
55
+ },
56
+ "gpt-4.1": {
57
+ input: 2,
58
+ output: 8,
59
+ cachedInput: .5
60
+ },
61
+ "gpt-4.1-mini": {
62
+ input: .4,
63
+ output: 1.6,
64
+ cachedInput: .1
65
+ },
66
+ "gpt-4.1-nano": {
67
+ input: .1,
68
+ output: .4,
69
+ cachedInput: .025
70
+ },
71
+ "gpt-4o": {
72
+ input: 2.5,
73
+ output: 10,
74
+ cachedInput: 1.25
75
+ },
76
+ "gpt-4o-mini": {
77
+ input: .15,
78
+ output: .6,
79
+ cachedInput: .075
80
+ },
81
+ o3: {
82
+ input: 2,
83
+ output: 8,
84
+ cachedInput: .5
85
+ },
86
+ "o4-mini": {
87
+ input: 1.1,
88
+ output: 4.4,
89
+ cachedInput: .275
90
+ },
91
+ "o3-mini": {
92
+ input: 1.1,
93
+ output: 4.4,
94
+ cachedInput: .55
95
+ },
96
+ o1: {
97
+ input: 15,
98
+ output: 60,
99
+ cachedInput: 7.5
100
+ }
101
+ };
102
+ /**
103
+ * Look up the built-in list price for an OpenAI model id (exact match).
104
+ * Returns `undefined` for unknown ids, so cost stays unknown rather than
105
+ * wrong.
106
+ *
107
+ * @example
108
+ * openAIPricingFor("gpt-4o-mini"); // { input: 0.15, output: 0.6, cachedInput: 0.075 }
109
+ * openAIPricingFor("gpt-5.5"); // undefined — priced by prompt length
110
+ */
111
+ function openAIPricingFor(modelName) {
112
+ return OPENAI_MODEL_PRICING[modelName];
113
+ }
114
+ /** USD per request for web_search (NOT non-reasoning web_search_preview).
115
+ * Search content tokens are additional; preview variants can have other rates.
116
+ * https://developers.openai.com/api/docs/pricing#built-in-tools — verified 2026-10-18. */
117
+ const OPENAI_SERVER_TOOL_PRICING = { webSearch: .01 };
118
+
119
+ //#endregion
120
+ export { OPENAI_MODEL_PRICING, OPENAI_SERVER_TOOL_PRICING, openAIPricingFor };
121
+ //# sourceMappingURL=known-pricing.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"known-pricing.mjs","names":[],"sources":["../../../../../../../packages/ai-openai/src/known-pricing.ts"],"sourcesContent":["import type { ModelPricing } from \"@warlock.js/ai\";\n\n/**\n * Built-in OpenAI API list prices, USD per 1,000,000 tokens, keyed by\n * exact model id. `cachedInput` is the cached-input rate. None of these\n * models charges for cache writes.\n *\n * Source: https://developers.openai.com/api/docs/pricing (the target of\n * platform.openai.com/docs/pricing), Standard tier, fetched 2026-10-17.\n *\n * Applied only when the SDK's provider label is `\"openai\"`, and only on\n * an exact id match — dated snapshots can carry other prices. Left out on\n * purpose, so pass `pricing` yourself for them:\n *\n * - Models priced by prompt length (a higher rate above 272K input\n * tokens): the gpt-6 / gpt-5.6 / gpt-5.5 / gpt-5.4 families and the\n * `-pro` models. A flat `ModelPricing` cannot express the tiers.\n * - Promotional, Batch, Flex and Priority pricing, and the regional\n * (data-residency) uplift.\n */\nexport const OPENAI_MODEL_PRICING: Readonly<Record<string, ModelPricing>> = {\n \"gpt-5.4-mini\": { input: 0.75, output: 4.5, cachedInput: 0.075 },\n \"gpt-5.4-nano\": { input: 0.2, output: 1.25, cachedInput: 0.02 },\n \"gpt-5.2\": { input: 1.75, output: 14, cachedInput: 0.175 },\n \"gpt-5.1\": { input: 1.25, output: 10, cachedInput: 0.125 },\n \"gpt-5\": { input: 1.25, output: 10, cachedInput: 0.125 },\n \"gpt-5-mini\": { input: 0.25, output: 2, cachedInput: 0.025 },\n \"gpt-5-nano\": { input: 0.05, output: 0.4, cachedInput: 0.005 },\n \"gpt-4.1\": { input: 2, output: 8, cachedInput: 0.5 },\n \"gpt-4.1-mini\": { input: 0.4, output: 1.6, cachedInput: 0.1 },\n \"gpt-4.1-nano\": { input: 0.1, output: 0.4, cachedInput: 0.025 },\n \"gpt-4o\": { input: 2.5, output: 10, cachedInput: 1.25 },\n \"gpt-4o-mini\": { input: 0.15, output: 0.6, cachedInput: 0.075 },\n o3: { input: 2, output: 8, cachedInput: 0.5 },\n \"o4-mini\": { input: 1.1, output: 4.4, cachedInput: 0.275 },\n \"o3-mini\": { input: 1.1, output: 4.4, cachedInput: 0.55 },\n o1: { input: 15, output: 60, cachedInput: 7.5 },\n};\n\n/**\n * Look up the built-in list price for an OpenAI model id (exact match).\n * Returns `undefined` for unknown ids, so cost stays unknown rather than\n * wrong.\n *\n * @example\n * openAIPricingFor(\"gpt-4o-mini\"); // { input: 0.15, output: 0.6, cachedInput: 0.075 }\n * openAIPricingFor(\"gpt-5.5\"); // undefined — priced by prompt length\n */\nexport function openAIPricingFor(modelName: string): ModelPricing | undefined {\n return OPENAI_MODEL_PRICING[modelName];\n}\n\n/** USD per request for web_search (NOT non-reasoning web_search_preview).\n * Search content tokens are additional; preview variants can have other rates.\n * https://developers.openai.com/api/docs/pricing#built-in-tools — verified 2026-10-18. */\nexport const OPENAI_SERVER_TOOL_PRICING = { webSearch: 0.01 };\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAoBA,MAAa,uBAA+D;CAC1E,gBAAgB;EAAE,OAAO;EAAM,QAAQ;EAAK,aAAa;CAAM;CAC/D,gBAAgB;EAAE,OAAO;EAAK,QAAQ;EAAM,aAAa;CAAK;CAC9D,WAAW;EAAE,OAAO;EAAM,QAAQ;EAAI,aAAa;CAAM;CACzD,WAAW;EAAE,OAAO;EAAM,QAAQ;EAAI,aAAa;CAAM;CACzD,SAAS;EAAE,OAAO;EAAM,QAAQ;EAAI,aAAa;CAAM;CACvD,cAAc;EAAE,OAAO;EAAM,QAAQ;EAAG,aAAa;CAAM;CAC3D,cAAc;EAAE,OAAO;EAAM,QAAQ;EAAK,aAAa;CAAM;CAC7D,WAAW;EAAE,OAAO;EAAG,QAAQ;EAAG,aAAa;CAAI;CACnD,gBAAgB;EAAE,OAAO;EAAK,QAAQ;EAAK,aAAa;CAAI;CAC5D,gBAAgB;EAAE,OAAO;EAAK,QAAQ;EAAK,aAAa;CAAM;CAC9D,UAAU;EAAE,OAAO;EAAK,QAAQ;EAAI,aAAa;CAAK;CACtD,eAAe;EAAE,OAAO;EAAM,QAAQ;EAAK,aAAa;CAAM;CAC9D,IAAI;EAAE,OAAO;EAAG,QAAQ;EAAG,aAAa;CAAI;CAC5C,WAAW;EAAE,OAAO;EAAK,QAAQ;EAAK,aAAa;CAAM;CACzD,WAAW;EAAE,OAAO;EAAK,QAAQ;EAAK,aAAa;CAAK;CACxD,IAAI;EAAE,OAAO;EAAI,QAAQ;EAAI,aAAa;CAAI;AAChD;;;;;;;;;;AAWA,SAAgB,iBAAiB,WAA6C;CAC5E,OAAO,qBAAqB;AAC9B;;;;AAKA,MAAa,6BAA6B,EAAE,WAAW,IAAK"}
package/esm/model.mjs CHANGED
@@ -7,9 +7,10 @@ import { inferStructuredOutput, planStructuredOutput } from "./utils/structured-
7
7
  import { toOpenAIMessages } from "./utils/to-openai-messages.mjs";
8
8
  import { toOpenAITools } from "./utils/to-openai-tools.mjs";
9
9
  import "./utils/index.mjs";
10
+ import { compatibleSearchParams, mergeProviderBody } from "./provider-options.mjs";
10
11
  import { inferReasoningCapability } from "./known-reasoning-models.mjs";
11
12
  import { inferVisionCapability } from "./known-vision-models.mjs";
12
- import { InvalidRequestError, ProviderError, safeJsonParse } from "@warlock.js/ai";
13
+ import { InvalidRequestError, ProviderError, resolveStreamChunks, safeJsonParse, selectProviderTools } from "@warlock.js/ai";
13
14
  import { log } from "@warlock.js/logger";
14
15
 
15
16
  //#region ../../packages/ai-openai/src/model.ts
@@ -88,15 +89,16 @@ var OpenAIModel = class {
88
89
  });
89
90
  let response;
90
91
  try {
91
- response = await this.client.chat.completions.create({
92
+ response = await this.client.chat.completions.create(mergeProviderBody({
92
93
  model: this.name,
94
+ ...compatibleSearchParams(this.provider, options),
93
95
  messages: toOpenAIMessages(messages, this.promptCacheBreakpoints(options)),
94
96
  ...this.buildSamplingParams(options),
95
97
  ...buildPromptCacheParams(this.provider, this.config),
96
- tools: toOpenAITools(options?.tools),
98
+ ...this.buildTools(options),
97
99
  ...this.buildResponseFormat(options?.responseSchema),
98
100
  ...this.buildReasoningParams(options?.reasoning, Boolean(options?.tools?.length))
99
- }, options?.signal ? { signal: options.signal } : void 0);
101
+ }, options), options?.signal ? { signal: options.signal } : void 0);
100
102
  } catch (thrown) {
101
103
  const wrapped = wrapOpenAIError(thrown);
102
104
  this.logger.error(LOG_MODULE, "error", wrapped.message, {
@@ -129,6 +131,11 @@ var OpenAIModel = class {
129
131
  * `delta` for text tokens, `tool-call` when the model requests a tool,
130
132
  * and a terminal `done` carrying the final finish reason + usage totals.
131
133
  * Callers consume it with `for await`.
134
+ *
135
+ * With `options.streamChunks` it also yields `tool-args` for each
136
+ * `tool_calls[].function.arguments` fragment and `reasoning` for a
137
+ * vendor's `reasoning_content` / `reasoning` delta field. Usage arrives
138
+ * only on the final chunk here, so no `usage` chunk is emitted.
132
139
  */
133
140
  async *stream(messages, options) {
134
141
  this.logger.debug(LOG_MODULE, "request", "Starting streaming call to chat.completions", {
@@ -139,17 +146,18 @@ var OpenAIModel = class {
139
146
  });
140
147
  let stream;
141
148
  try {
142
- stream = await this.client.chat.completions.create({
149
+ stream = await this.client.chat.completions.create(mergeProviderBody({
143
150
  model: this.name,
151
+ ...compatibleSearchParams(this.provider, options),
144
152
  messages: toOpenAIMessages(messages, this.promptCacheBreakpoints(options)),
145
153
  ...this.buildSamplingParams(options),
146
154
  ...buildPromptCacheParams(this.provider, this.config),
147
- tools: toOpenAITools(options?.tools),
155
+ ...this.buildTools(options),
148
156
  stream: true,
149
157
  stream_options: { include_usage: true },
150
158
  ...this.buildResponseFormat(options?.responseSchema),
151
159
  ...this.buildReasoningParams(options?.reasoning, Boolean(options?.tools?.length))
152
- }, options?.signal ? { signal: options.signal } : void 0);
160
+ }, options), options?.signal ? { signal: options.signal } : void 0);
153
161
  } catch (thrown) {
154
162
  const wrapped = wrapOpenAIError(thrown);
155
163
  this.logger.error(LOG_MODULE, "error", wrapped.message, {
@@ -159,16 +167,24 @@ var OpenAIModel = class {
159
167
  throw wrapped;
160
168
  }
161
169
  let rawFinishReason = "stop";
162
- const usage = {
170
+ let usage = {
163
171
  input: 0,
164
172
  output: 0,
165
173
  total: 0
166
174
  };
175
+ const want = resolveStreamChunks(options);
167
176
  const toolCallAccum = /* @__PURE__ */ new Map();
168
177
  try {
169
178
  for await (const chunk of stream) {
170
179
  const delta = chunk.choices[0]?.delta;
171
180
  const finish = chunk.choices[0]?.finish_reason;
181
+ if (want.reasoning && delta) {
182
+ const reasoning = readReasoningDelta(delta);
183
+ if (reasoning) yield {
184
+ type: "reasoning",
185
+ delta: reasoning
186
+ };
187
+ }
172
188
  if (delta?.content) yield {
173
189
  type: "delta",
174
190
  content: delta.content
@@ -178,23 +194,25 @@ var OpenAIModel = class {
178
194
  if (!toolCallAccum.has(idx)) toolCallAccum.set(idx, {
179
195
  id: "",
180
196
  name: "",
181
- arguments: ""
197
+ arguments: "",
198
+ streamed: 0
182
199
  });
183
200
  const acc = toolCallAccum.get(idx);
184
201
  if (toolCall.id) acc.id = toolCall.id;
185
202
  if (toolCall.function?.name) acc.name = toolCall.function.name;
186
203
  if (toolCall.function?.arguments) acc.arguments += toolCall.function.arguments;
204
+ if (want.toolArgs && acc.name && acc.arguments.length > acc.streamed) {
205
+ yield {
206
+ type: "tool-args",
207
+ callId: acc.id,
208
+ name: acc.name,
209
+ delta: acc.arguments.slice(acc.streamed)
210
+ };
211
+ acc.streamed = acc.arguments.length;
212
+ }
187
213
  }
188
214
  if (finish) rawFinishReason = finish;
189
- if (chunk.usage) {
190
- usage.input = chunk.usage.prompt_tokens ?? 0;
191
- usage.output = chunk.usage.completion_tokens ?? 0;
192
- usage.total = chunk.usage.total_tokens ?? 0;
193
- const cached = chunk.usage.prompt_tokens_details?.cached_tokens;
194
- if (cached !== void 0 && cached > 0) usage.cachedTokens = cached;
195
- const reasoning = chunk.usage.completion_tokens_details?.reasoning_tokens;
196
- if (reasoning !== void 0 && reasoning > 0) usage.reasoningTokens = reasoning;
197
- }
215
+ if (chunk.usage) usage = this.extractUsage(chunk.usage);
198
216
  }
199
217
  for (const acc of toolCallAccum.values()) {
200
218
  if (!acc.name) continue;
@@ -224,6 +242,30 @@ var OpenAIModel = class {
224
242
  usage
225
243
  };
226
244
  }
245
+ /**
246
+ * Spread-friendly `tools` fragment: the function tools, then the
247
+ * provider-defined entries whose `provider` equals this model's
248
+ * provider label (`"openai"`, or the vendor label an OpenAI-compatible
249
+ * wrapper sets, e.g. `"groq"`), sent verbatim. Chat Completions only
250
+ * accepts `function` and `custom` tool types from OpenAI itself; its
251
+ * hosted tools (`web_search`, `file_search`, …) need `api: "responses"`
252
+ * and OpenAI rejects them here with a 400.
253
+ */
254
+ buildTools(options) {
255
+ const { tools, definitions } = selectProviderTools(options, {
256
+ accepts: [this.provider],
257
+ module: LOG_MODULE,
258
+ model: this.name,
259
+ logger: this.logger
260
+ });
261
+ if ((this.provider === "moonshot" || this.provider === "kimi") && definitions.some((definition) => {
262
+ const fn = definition.function;
263
+ return definition.type === "builtin_function" && typeof fn === "object" && fn !== null && "name" in fn && fn.name === "$web_search";
264
+ })) throw new InvalidRequestError("Kimi's builtin $web_search retires on 20 October 2026. Use kimiSearchTool() / kimiFetchTool() from @warlock.js/ai-openai.");
265
+ if (this.provider === "xai" && definitions.some((definition) => definition.type === "web_search" || definition.type === "x_search")) throw new InvalidRequestError("xAI native web_search / x_search requires POST /v1/responses. XaiSDK currently supports Chat Completions only; use xAI's Responses API with the vendor SDK for native search.");
266
+ const mapped = [...toOpenAITools(tools) ?? [], ...definitions];
267
+ return mapped.length > 0 ? { tools: mapped } : {};
268
+ }
227
269
  /** Map the neutral cache hint only for the direct OpenAI provider. */
228
270
  promptCacheBreakpoints(options) {
229
271
  return this.provider === "openai" ? options?.cacheControl?.breakpoints ?? 0 : 0;
@@ -262,7 +304,9 @@ var OpenAIModel = class {
262
304
  * (the hidden reasoning channel on o-series / gpt-5 models, already
263
305
  * counted within `output`). Both are emitted only when the provider
264
306
  * reports a positive value, so non-reasoning / uncached calls keep the
265
- * lean `{ input, output, total }` shape.
307
+ * lean `{ input, output, total }` shape. `cacheWriteTokens` mirrors
308
+ * `prompt_tokens_details.cache_write_tokens` (billed writes on the
309
+ * models that charge for them). `cost` is attached when priced.
266
310
  */
267
311
  extractUsage(raw) {
268
312
  if (!raw) return {
@@ -271,12 +315,13 @@ var OpenAIModel = class {
271
315
  total: 0
272
316
  };
273
317
  return buildUsage({
274
- input: raw.prompt_tokens,
275
- output: raw.completion_tokens,
276
- total: raw.total_tokens,
318
+ input: raw.prompt_tokens ?? 0,
319
+ output: raw.completion_tokens ?? 0,
320
+ total: raw.total_tokens ?? 0,
277
321
  cachedTokens: raw.prompt_tokens_details?.cached_tokens,
322
+ cacheWriteTokens: raw.prompt_tokens_details?.cache_write_tokens,
278
323
  reasoningTokens: raw.completion_tokens_details?.reasoning_tokens
279
- });
324
+ }, this.pricing);
280
325
  }
281
326
  /**
282
327
  * Translate the neutral `ModelCallOptions.reasoning` hint into OpenAI's
@@ -352,6 +397,20 @@ var OpenAIModel = class {
352
397
  }));
353
398
  }
354
399
  };
400
+ /**
401
+ * Reasoning text an OpenAI-compatible vendor puts on a Chat Completions
402
+ * stream delta. OpenAI itself streams none on this API. DeepSeek documents
403
+ * `delta.reasoning_content` (API reference, chat completion chunk); Groq
404
+ * documents the parsed reasoning under a `reasoning` field. Neither is in
405
+ * the OpenAI SDK's delta type, so the delta is read as a plain record.
406
+ */
407
+ function readReasoningDelta(delta) {
408
+ const record = delta;
409
+ for (const key of ["reasoning_content", "reasoning"]) {
410
+ const value = record[key];
411
+ if (typeof value === "string" && value !== "") return value;
412
+ }
413
+ }
355
414
 
356
415
  //#endregion
357
416
  export { OpenAIModel };
package/esm/model.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"model.mjs","names":[],"sources":["../../../../../../../packages/ai-openai/src/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 buildPromptCacheParams,\n buildUsage,\n inferStructuredOutput,\n mapFinishReason,\n planStructuredOutput,\n toOpenAIMessages,\n toOpenAIReasoningEffort,\n toOpenAITools,\n wrapOpenAIError,\n} from \"./utils\";\n\nconst LOG_MODULE = \"ai.openai\";\n\n/**\n * OpenAI-backed implementation of `ModelContract`.\n *\n * **Role.** The provider-facing bridge between the vendor-neutral\n * `@warlock.js/ai` agent runtime and the official `openai` SDK. Agents,\n * workflows, and supervisors never talk to OpenAI directly — they hold a\n * `ModelContract`, and this class is what makes that contract concrete for\n * any OpenAI-compatible endpoint (OpenAI, Azure OpenAI, OpenRouter, local\n * gateways that speak the Chat Completions protocol).\n *\n * **Responsibility.**\n * - Owns: a long-lived `OpenAI` client + frozen `ModelConfig` (name,\n * temperature, maxTokens) used as defaults for every call.\n * - Owns: translating vendor-neutral `Message[]` and\n * `ToolContract[]` into OpenAI wire shapes on the way out, and\n * translating OpenAI's response (content, finish reason, tool calls,\n * usage) back into the neutral shapes on the way in.\n * - Does NOT own: dispatching tools, deciding whether to loop, tracking\n * conversation history, or retrying on failure — those are agent\n * concerns. The model is a stateless (per-call) protocol adapter.\n *\n * Because it holds a live client and shared defaults, it is modeled as a\n * class (see §4.2 of code-style.md — \"long-lived state across calls\").\n *\n * @example\n * import OpenAI from \"openai\";\n * const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });\n * const model = new OpenAIModel(client, { name: \"gpt-4o\", temperature: 0.3 });\n *\n * const myAgent = agent({\n * model,\n * systemPrompt: \"You are a helpful assistant.\",\n * tools: [searchTool],\n * });\n *\n * const result = await myAgent.execute(\"Summarize today's news.\");\n */\nexport class OpenAIModel 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 // `api: \"responses\"` belongs to `OpenAIResponsesModel`, which only the\n // `OpenAISDK.model()` factory constructs. This class speaks Chat\n // Completions only; accepting the flag here would silently ignore it.\n if (config.api === \"responses\") {\n throw new InvalidRequestError(\n 'OpenAIModel speaks Chat Completions only and cannot serve api: \"responses\". Use OpenAISDK.model({ api: \"responses\" }) on the direct OpenAI provider.',\n { context: { provider, model: config.name, api: config.api } },\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 // o-series + gpt-5 / gpt-6 models surface a reasoning channel and accept\n // the `reasoning_effort` param. Explicit config wins over the\n // name-prefix inference.\n reasoning: config.reasoning ?? inferReasoningCapability(config.name),\n // Direct OpenAI requests support automatic cache hits and explicit\n // content-part breakpoints. Wrapper endpoints have distinct provider\n // labels and may reject these OpenAI-only request fields.\n promptCaching: this.provider === \"openai\",\n // PDF + audio INPUT are off by default — OpenAI accepts `file`\n // (PDF) and `input_audio` parts only on specific models, so the\n // flags are conservative/honest and opt-in via config rather than\n // name-inferred. When set, the agent admits the attachments and\n // `toOpenAIMessages` maps them to the real wire parts.\n pdf: config.pdf ?? false,\n audio: config.audio ?? false,\n };\n }\n\n /**\n * Single-shot completion. Sends the full message list to the Chat\n * Completions endpoint, waits for the terminal response, and reshapes it\n * into a vendor-neutral `ModelResponse`. Per-call `options` override the\n * instance's `ModelConfig` defaults for this call only.\n */\n public async complete(messages: Message[], options?: ModelCallOptions): Promise<ModelResponse> {\n // Per-call request/response logs are hot-path in production agents\n // — keep them at `debug` so `info` stays reserved for lifecycle\n // events (agent starting/completed, etc.). Operators who need to\n // audit every LLM call can raise log-level at runtime.\n this.logger.debug(LOG_MODULE, \"request\", \"Starting call to chat.completions\", {\n model: this.name,\n messageCount: messages.length,\n streaming: false,\n toolCount: options?.tools?.length ?? 0,\n });\n\n let response: OpenAI.Chat.Completions.ChatCompletion;\n\n try {\n response = await this.client.chat.completions.create(\n {\n model: this.name,\n messages: toOpenAIMessages(messages, this.promptCacheBreakpoints(options)),\n ...this.buildSamplingParams(options),\n ...buildPromptCacheParams(this.provider, this.config),\n tools: toOpenAITools(options?.tools),\n ...this.buildResponseFormat(options?.responseSchema),\n ...this.buildReasoningParams(options?.reasoning, Boolean(options?.tools?.length)),\n },\n options?.signal ? { signal: options.signal } : undefined,\n );\n } catch (thrown) {\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 throw wrapped;\n }\n\n const [choice] = response.choices;\n\n if (choice === undefined) {\n // A 200 carrying an empty `choices` array. Every line below reads off\n // `choice`, so without this the caller got a bare `TypeError: Cannot\n // read properties of undefined` — no provider, no model, no hint that\n // the response itself was the problem rather than our handling of it.\n //\n // `ProviderError` because that is what this is: the provider answered\n // successfully and returned nothing usable. Callers already branch on\n // `instanceof ProviderError` for any provider-side failure.\n throw new ProviderError(\"OpenAI returned a response with no choices.\", {\n context: { provider: \"openai\", model: this.name, choices: response.choices.length },\n });\n }\n\n const finishReason = mapFinishReason(choice.finish_reason);\n const usage = this.extractUsage(response.usage);\n\n this.logger.debug(LOG_MODULE, \"response\", \"call to chat.completions succeeded\", {\n finishReason,\n usage,\n });\n\n return {\n content: choice.message.content ?? \"\",\n finishReason,\n usage,\n toolCalls: this.extractToolCalls(choice.message.tool_calls),\n };\n }\n\n /**\n * Incremental streaming completion. Yields neutral `ModelStreamChunk`s —\n * `delta` for text tokens, `tool-call` when the model requests a tool,\n * and a terminal `done` carrying the final finish reason + usage totals.\n * Callers consume it with `for await`.\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 chat.completions\", {\n model: this.name,\n messageCount: messages.length,\n streaming: true,\n toolCount: options?.tools?.length ?? 0,\n });\n\n let stream: Awaited<ReturnType<typeof this.client.chat.completions.create>>;\n\n try {\n stream = await this.client.chat.completions.create(\n {\n model: this.name,\n messages: toOpenAIMessages(messages, this.promptCacheBreakpoints(options)),\n ...this.buildSamplingParams(options),\n ...buildPromptCacheParams(this.provider, this.config),\n tools: toOpenAITools(options?.tools),\n stream: true,\n stream_options: { include_usage: true },\n ...this.buildResponseFormat(options?.responseSchema),\n ...this.buildReasoningParams(options?.reasoning, Boolean(options?.tools?.length)),\n },\n options?.signal ? { signal: options.signal } : undefined,\n );\n } catch (thrown) {\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 throw wrapped;\n }\n\n let rawFinishReason: string = \"stop\";\n const usage: Usage = { input: 0, output: 0, total: 0 };\n const toolCallAccum = new Map<number, { id: string; name: string; arguments: string }>();\n\n try {\n for await (const chunk of stream as AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>) {\n const delta = chunk.choices[0]?.delta;\n const finish = chunk.choices[0]?.finish_reason;\n\n if (delta?.content) {\n yield { type: \"delta\", content: delta.content };\n }\n\n if (delta?.tool_calls) {\n for (const toolCall of delta.tool_calls) {\n const idx = toolCall.index ?? 0;\n if (!toolCallAccum.has(idx)) {\n toolCallAccum.set(idx, { id: \"\", name: \"\", arguments: \"\" });\n }\n const acc = toolCallAccum.get(idx)!;\n if (toolCall.id) acc.id = toolCall.id;\n if (toolCall.function?.name) acc.name = toolCall.function.name;\n if (toolCall.function?.arguments) acc.arguments += toolCall.function.arguments;\n }\n }\n\n if (finish) {\n rawFinishReason = finish;\n }\n\n if (chunk.usage) {\n usage.input = chunk.usage.prompt_tokens ?? 0;\n usage.output = chunk.usage.completion_tokens ?? 0;\n usage.total = chunk.usage.total_tokens ?? 0;\n const cached = chunk.usage.prompt_tokens_details?.cached_tokens;\n if (cached !== undefined && cached > 0) {\n usage.cachedTokens = cached;\n }\n const reasoning = chunk.usage.completion_tokens_details?.reasoning_tokens;\n if (reasoning !== undefined && reasoning > 0) {\n usage.reasoningTokens = reasoning;\n }\n }\n }\n\n for (const acc of toolCallAccum.values()) {\n // Skip accumulators that never received a function name — those\n // are partial fragments the model started but never identified\n // (e.g. arguments-only deltas with no originating `id`/`name`).\n // Yielding them produces nameless tool-calls the agent runtime\n // can't dispatch and would mis-attribute as a registered tool.\n if (!acc.name) continue;\n\n yield {\n type: \"tool-call\",\n id: acc.id,\n name: acc.name,\n input: safeJsonParse<Record<string, unknown>>(acc.arguments, {}),\n };\n }\n } catch (thrown) {\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 throw wrapped;\n }\n\n const finishReason = mapFinishReason(rawFinishReason);\n\n this.logger.debug(LOG_MODULE, \"response\", \"Streaming call to chat.completions succeeded\", {\n finishReason,\n usage,\n });\n\n yield { type: \"done\", finishReason, usage };\n }\n\n /** Map the neutral cache hint only for the direct OpenAI provider. */\n private promptCacheBreakpoints(options: ModelCallOptions | undefined): number {\n return this.provider === \"openai\" ? (options?.cacheControl?.breakpoints ?? 0) : 0;\n }\n\n /**\n * Translate the neutral `responseSchema` option into OpenAI's\n * `response_format` parameter. The mode decision (override handling,\n * strict-compatibility downgrade to `json_object`) lives in\n * `planStructuredOutput`, shared with the Responses adapter.\n *\n * Returns an empty spread when no schema was supplied, so the caller\n * can unconditionally `...buildResponseFormat(...)` into the request.\n */\n private buildResponseFormat(responseSchema: Record<string, unknown> | undefined): {\n response_format?: OpenAI.Chat.Completions.ChatCompletionCreateParams[\"response_format\"];\n } {\n const plan = planStructuredOutput(responseSchema, this.config.responseFormat);\n\n if (plan.mode === \"json_schema\") {\n return {\n response_format: {\n type: \"json_schema\",\n json_schema: { name: \"response\", schema: plan.schema, strict: true },\n },\n };\n }\n\n if (plan.mode === \"json_object\") {\n return { response_format: { type: \"json_object\" } };\n }\n\n return {};\n }\n\n /**\n * Normalize OpenAI's `usage` block (which may be absent on some responses\n * or partials) into the neutral `Usage` shape. Missing usage collapses to\n * zeros rather than propagating `undefined`, so downstream aggregation\n * math stays safe.\n *\n * `cachedTokens` mirrors `prompt_tokens_details.cached_tokens` (the\n * subset of the prompt served from OpenAI's automatic prompt cache);\n * `reasoningTokens` mirrors `completion_tokens_details.reasoning_tokens`\n * (the hidden reasoning channel on o-series / gpt-5 models, already\n * counted within `output`). Both are emitted only when the provider\n * reports a positive value, so non-reasoning / uncached calls keep the\n * lean `{ input, output, total }` shape.\n */\n private extractUsage(raw: OpenAI.Completions.CompletionUsage | undefined): Usage {\n if (!raw) {\n return { input: 0, output: 0, total: 0 };\n }\n\n return buildUsage({\n input: raw.prompt_tokens,\n output: raw.completion_tokens,\n total: raw.total_tokens,\n cachedTokens: raw.prompt_tokens_details?.cached_tokens,\n reasoningTokens: raw.completion_tokens_details?.reasoning_tokens,\n });\n }\n\n /**\n * Translate the neutral `ModelCallOptions.reasoning` hint into OpenAI's\n * `reasoning_effort` request param. Only `effort` maps — OpenAI's Chat\n * Completions API exposes a discrete effort knob, not a token budget,\n * so `reasoning.maxTokens` (the Anthropic extended-thinking cap) has no\n * wire equivalent here and is silently ignored.\n *\n * The neutral `ReasoningEffort` (`\"low\" | \"medium\" | \"high\" | \"none\"`)\n * is a subset of OpenAI's accepted values, so it forwards verbatim —\n * `\"none\"` included. `\"none\"` is load-bearing: gpt-5 / o-series models\n * **reject function tools** on Chat Completions while reasoning is\n * active, and the endpoint accepts tools only when `reasoning_effort`\n * is `\"none\"` (the alternative is the Responses API). This is why the\n * param is EMITTED for `\"none\"` rather than omitted — omitting it\n * leaves the model reasoning server-side by default, so tools would\n * still be rejected.\n *\n * No-ops when the model is not reasoning-capable\n * (`capabilities.reasoning` is false — e.g. `gpt-4o`, which 400s on any\n * `reasoning_effort`).\n *\n * When the caller gave no explicit `effort`: defaults to `\"none\"` IF\n * `hasTools` is true, else omits the param (provider default — the\n * pre-existing behavior for a tool-less call). The default exists\n * because, on a reasoning-capable model, there is no working alternative\n * to `\"none\"` when tools are attached — omitting `reasoning_effort`\n * leaves reasoning on server-side, which Chat Completions rejects tools\n * for (empty replies on some model generations, a hard 400 — \"Function\n * tools with reasoning_effort are not supported ... in\n * /v1/chat/completions\" — on newer ones). An explicit `effort` from the\n * caller always wins over this default, in either direction.\n *\n * Returns an empty spread when nothing applies, so the caller can\n * unconditionally `...buildReasoningParams(...)` into the request.\n */\n /**\n * Output-length and sampling params, in current Chat Completions shapes only.\n *\n * - `max_completion_tokens`, never the legacy `max_tokens`: current\n * models (the gpt-5 family and later) answer `400 unsupported_parameter`\n * to `max_tokens`, and `max_completion_tokens` is accepted everywhere.\n * - No `temperature` for a reasoning-capable model: those accept only the\n * default value and reject any other with a 400.\n */\n private buildSamplingParams(options: ModelCallOptions | undefined): {\n max_completion_tokens?: number;\n temperature?: number;\n } {\n const maxTokens = options?.maxTokens ?? this.config.maxTokens;\n const temperature = options?.temperature ?? this.config.temperature;\n\n return {\n max_completion_tokens: maxTokens,\n ...(!this.capabilities.reasoning && { temperature }),\n };\n }\n\n private buildReasoningParams(\n reasoning: ModelCallOptions[\"reasoning\"],\n hasTools: boolean,\n ): {\n reasoning_effort?: OpenAI.Chat.Completions.ChatCompletionCreateParams[\"reasoning_effort\"];\n } {\n if (!this.capabilities.reasoning) {\n return {};\n }\n\n if (!reasoning?.effort) {\n return hasTools ? { reasoning_effort: \"none\" } : {};\n }\n\n return { reasoning_effort: toOpenAIReasoningEffort(reasoning.effort) };\n }\n\n /**\n * Reshape OpenAI's `tool_calls` array into the neutral\n * `ModelToolCallRequest[]`. The raw `arguments` field is a JSON string\n * per OpenAI's protocol — we parse it defensively via `safeJsonParse` so\n * malformed or empty arguments yield an empty object instead of crashing\n * the trip. Returns `undefined` when no tools were requested so callers\n * can branch on presence.\n */\n private extractToolCalls(\n rawToolCalls: OpenAI.Chat.Completions.ChatCompletionMessageToolCall[] | undefined,\n ): ModelToolCallRequest[] | undefined {\n if (!rawToolCalls || rawToolCalls.length === 0) {\n return undefined;\n }\n\n return rawToolCalls.map((toolCall) => ({\n id: toolCall.id,\n name: (toolCall as any).function.name,\n input: safeJsonParse<Record<string, unknown>>((toolCall as any).function.arguments, {}),\n }));\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;AA+BA,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCnB,IAAa,cAAb,MAAkD;CAUhD,AAAO,YAAY,QAAgB,QAA2B,WAAmB,UAAU;gBAFzD;EAMhC,IAAI,OAAO,QAAQ,aACjB,MAAM,IAAI,oBACR,4JACA,EAAE,SAAS;GAAE;GAAU,OAAO,OAAO;GAAM,KAAK,OAAO;EAAI,EAAE,CAC/D;EAGF,KAAK,SAAS;EACd,KAAK,SAAS;EACd,KAAK,OAAO,OAAO;EACnB,KAAK,WAAW;EAChB,KAAK,UAAU,OAAO;EACtB,KAAK,eAAe;GAClB,kBAAkB,OAAO,oBAAoB,sBAAsB,OAAO,cAAc;GACxF,QAAQ,OAAO,UAAU,sBAAsB,OAAO,IAAI;GAI1D,WAAW,OAAO,aAAa,yBAAyB,OAAO,IAAI;GAInE,eAAe,KAAK,aAAa;GAMjC,KAAK,OAAO,OAAO;GACnB,OAAO,OAAO,SAAS;EACzB;CACF;;;;;;;CAQA,MAAa,SAAS,UAAqB,SAAoD;EAK7F,KAAK,OAAO,MAAM,YAAY,WAAW,qCAAqC;GAC5E,OAAO,KAAK;GACZ,cAAc,SAAS;GACvB,WAAW;GACX,WAAW,SAAS,OAAO,UAAU;EACvC,CAAC;EAED,IAAI;EAEJ,IAAI;GACF,WAAW,MAAM,KAAK,OAAO,KAAK,YAAY,OAC5C;IACE,OAAO,KAAK;IACZ,UAAU,iBAAiB,UAAU,KAAK,uBAAuB,OAAO,CAAC;IACzE,GAAG,KAAK,oBAAoB,OAAO;IACnC,GAAG,uBAAuB,KAAK,UAAU,KAAK,MAAM;IACpD,OAAO,cAAc,SAAS,KAAK;IACnC,GAAG,KAAK,oBAAoB,SAAS,cAAc;IACnD,GAAG,KAAK,qBAAqB,SAAS,WAAW,QAAQ,SAAS,OAAO,MAAM,CAAC;GAClF,GACA,SAAS,SAAS,EAAE,QAAQ,QAAQ,OAAO,IAAI,MACjD;EACF,SAAS,QAAQ;GACf,MAAM,UAAU,gBAAgB,MAAM;GAEtC,KAAK,OAAO,MAAM,YAAY,SAAS,QAAQ,SAAS;IACtD,MAAM,QAAQ;IACd,SAAS,QAAQ;GACnB,CAAC;GAED,MAAM;EACR;EAEA,MAAM,CAAC,UAAU,SAAS;EAE1B,IAAI,WAAW,QASb,MAAM,IAAI,cAAc,+CAA+C,EACrE,SAAS;GAAE,UAAU;GAAU,OAAO,KAAK;GAAM,SAAS,SAAS,QAAQ;EAAO,EACpF,CAAC;EAGH,MAAM,eAAe,gBAAgB,OAAO,aAAa;EACzD,MAAM,QAAQ,KAAK,aAAa,SAAS,KAAK;EAE9C,KAAK,OAAO,MAAM,YAAY,YAAY,sCAAsC;GAC9E;GACA;EACF,CAAC;EAED,OAAO;GACL,SAAS,OAAO,QAAQ,WAAW;GACnC;GACA;GACA,WAAW,KAAK,iBAAiB,OAAO,QAAQ,UAAU;EAC5D;CACF;;;;;;;CAQA,OAAc,OACZ,UACA,SACiC;EACjC,KAAK,OAAO,MAAM,YAAY,WAAW,+CAA+C;GACtF,OAAO,KAAK;GACZ,cAAc,SAAS;GACvB,WAAW;GACX,WAAW,SAAS,OAAO,UAAU;EACvC,CAAC;EAED,IAAI;EAEJ,IAAI;GACF,SAAS,MAAM,KAAK,OAAO,KAAK,YAAY,OAC1C;IACE,OAAO,KAAK;IACZ,UAAU,iBAAiB,UAAU,KAAK,uBAAuB,OAAO,CAAC;IACzE,GAAG,KAAK,oBAAoB,OAAO;IACnC,GAAG,uBAAuB,KAAK,UAAU,KAAK,MAAM;IACpD,OAAO,cAAc,SAAS,KAAK;IACnC,QAAQ;IACR,gBAAgB,EAAE,eAAe,KAAK;IACtC,GAAG,KAAK,oBAAoB,SAAS,cAAc;IACnD,GAAG,KAAK,qBAAqB,SAAS,WAAW,QAAQ,SAAS,OAAO,MAAM,CAAC;GAClF,GACA,SAAS,SAAS,EAAE,QAAQ,QAAQ,OAAO,IAAI,MACjD;EACF,SAAS,QAAQ;GACf,MAAM,UAAU,gBAAgB,MAAM;GAEtC,KAAK,OAAO,MAAM,YAAY,SAAS,QAAQ,SAAS;IACtD,MAAM,QAAQ;IACd,SAAS,QAAQ;GACnB,CAAC;GAED,MAAM;EACR;EAEA,IAAI,kBAA0B;EAC9B,MAAM,QAAe;GAAE,OAAO;GAAG,QAAQ;GAAG,OAAO;EAAE;EACrD,MAAM,gCAAgB,IAAI,IAA6D;EAEvF,IAAI;GACF,WAAW,MAAM,SAAS,QAAsE;IAC9F,MAAM,QAAQ,MAAM,QAAQ,EAAE,EAAE;IAChC,MAAM,SAAS,MAAM,QAAQ,EAAE,EAAE;IAEjC,IAAI,OAAO,SACT,MAAM;KAAE,MAAM;KAAS,SAAS,MAAM;IAAQ;IAGhD,IAAI,OAAO,YACT,KAAK,MAAM,YAAY,MAAM,YAAY;KACvC,MAAM,MAAM,SAAS,SAAS;KAC9B,IAAI,CAAC,cAAc,IAAI,GAAG,GACxB,cAAc,IAAI,KAAK;MAAE,IAAI;MAAI,MAAM;MAAI,WAAW;KAAG,CAAC;KAE5D,MAAM,MAAM,cAAc,IAAI,GAAG;KACjC,IAAI,SAAS,IAAI,IAAI,KAAK,SAAS;KACnC,IAAI,SAAS,UAAU,MAAM,IAAI,OAAO,SAAS,SAAS;KAC1D,IAAI,SAAS,UAAU,WAAW,IAAI,aAAa,SAAS,SAAS;IACvE;IAGF,IAAI,QACF,kBAAkB;IAGpB,IAAI,MAAM,OAAO;KACf,MAAM,QAAQ,MAAM,MAAM,iBAAiB;KAC3C,MAAM,SAAS,MAAM,MAAM,qBAAqB;KAChD,MAAM,QAAQ,MAAM,MAAM,gBAAgB;KAC1C,MAAM,SAAS,MAAM,MAAM,uBAAuB;KAClD,IAAI,WAAW,UAAa,SAAS,GACnC,MAAM,eAAe;KAEvB,MAAM,YAAY,MAAM,MAAM,2BAA2B;KACzD,IAAI,cAAc,UAAa,YAAY,GACzC,MAAM,kBAAkB;IAE5B;GACF;GAEA,KAAK,MAAM,OAAO,cAAc,OAAO,GAAG;IAMxC,IAAI,CAAC,IAAI,MAAM;IAEf,MAAM;KACJ,MAAM;KACN,IAAI,IAAI;KACR,MAAM,IAAI;KACV,OAAO,cAAuC,IAAI,WAAW,CAAC,CAAC;IACjE;GACF;EACF,SAAS,QAAQ;GACf,MAAM,UAAU,gBAAgB,MAAM;GAEtC,KAAK,OAAO,MAAM,YAAY,SAAS,QAAQ,SAAS;IACtD,MAAM,QAAQ;IACd,SAAS,QAAQ;GACnB,CAAC;GAED,MAAM;EACR;EAEA,MAAM,eAAe,gBAAgB,eAAe;EAEpD,KAAK,OAAO,MAAM,YAAY,YAAY,gDAAgD;GACxF;GACA;EACF,CAAC;EAED,MAAM;GAAE,MAAM;GAAQ;GAAc;EAAM;CAC5C;;CAGA,AAAQ,uBAAuB,SAA+C;EAC5E,OAAO,KAAK,aAAa,WAAY,SAAS,cAAc,eAAe,IAAK;CAClF;;;;;;;;;;CAWA,AAAQ,oBAAoB,gBAE1B;EACA,MAAM,OAAO,qBAAqB,gBAAgB,KAAK,OAAO,cAAc;EAE5E,IAAI,KAAK,SAAS,eAChB,OAAO,EACL,iBAAiB;GACf,MAAM;GACN,aAAa;IAAE,MAAM;IAAY,QAAQ,KAAK;IAAQ,QAAQ;GAAK;EACrE,EACF;EAGF,IAAI,KAAK,SAAS,eAChB,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,EAAE;EAGpD,OAAO,CAAC;CACV;;;;;;;;;;;;;;;CAgBA,AAAQ,aAAa,KAA4D;EAC/E,IAAI,CAAC,KACH,OAAO;GAAE,OAAO;GAAG,QAAQ;GAAG,OAAO;EAAE;EAGzC,OAAO,WAAW;GAChB,OAAO,IAAI;GACX,QAAQ,IAAI;GACZ,OAAO,IAAI;GACX,cAAc,IAAI,uBAAuB;GACzC,iBAAiB,IAAI,2BAA2B;EAClD,CAAC;CACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8CA,AAAQ,oBAAoB,SAG1B;EACA,MAAM,YAAY,SAAS,aAAa,KAAK,OAAO;EACpD,MAAM,cAAc,SAAS,eAAe,KAAK,OAAO;EAExD,OAAO;GACL,uBAAuB;GACvB,GAAI,CAAC,KAAK,aAAa,aAAa,EAAE,YAAY;EACpD;CACF;CAEA,AAAQ,qBACN,WACA,UAGA;EACA,IAAI,CAAC,KAAK,aAAa,WACrB,OAAO,CAAC;EAGV,IAAI,CAAC,WAAW,QACd,OAAO,WAAW,EAAE,kBAAkB,OAAO,IAAI,CAAC;EAGpD,OAAO,EAAE,kBAAkB,wBAAwB,UAAU,MAAM,EAAE;CACvE;;;;;;;;;CAUA,AAAQ,iBACN,cACoC;EACpC,IAAI,CAAC,gBAAgB,aAAa,WAAW,GAC3C;EAGF,OAAO,aAAa,KAAK,cAAc;GACrC,IAAI,SAAS;GACb,MAAO,SAAiB,SAAS;GACjC,OAAO,cAAwC,SAAiB,SAAS,WAAW,CAAC,CAAC;EACxF,EAAE;CACJ;AACF"}
1
+ {"version":3,"file":"model.mjs","names":[],"sources":["../../../../../../../packages/ai-openai/src/model.ts"],"sourcesContent":["import {\n InvalidRequestError,\n ProviderError,\n resolveStreamChunks,\n safeJsonParse,\n selectProviderTools,\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 { compatibleSearchParams, mergeProviderBody } from \"./provider-options\";\nimport type { OpenAIModelConfig } from \"./config.type\";\nimport { inferReasoningCapability } from \"./known-reasoning-models\";\nimport { inferVisionCapability } from \"./known-vision-models\";\nimport {\n buildPromptCacheParams,\n buildUsage,\n inferStructuredOutput,\n mapFinishReason,\n planStructuredOutput,\n toOpenAIMessages,\n toOpenAIReasoningEffort,\n toOpenAITools,\n wrapOpenAIError,\n} from \"./utils\";\n\nconst LOG_MODULE = \"ai.openai\";\n\n/**\n * OpenAI-backed implementation of `ModelContract`.\n *\n * **Role.** The provider-facing bridge between the vendor-neutral\n * `@warlock.js/ai` agent runtime and the official `openai` SDK. Agents,\n * workflows, and supervisors never talk to OpenAI directly — they hold a\n * `ModelContract`, and this class is what makes that contract concrete for\n * any OpenAI-compatible endpoint (OpenAI, Azure OpenAI, OpenRouter, local\n * gateways that speak the Chat Completions protocol).\n *\n * **Responsibility.**\n * - Owns: a long-lived `OpenAI` client + frozen `ModelConfig` (name,\n * temperature, maxTokens) used as defaults for every call.\n * - Owns: translating vendor-neutral `Message[]` and\n * `ToolContract[]` into OpenAI wire shapes on the way out, and\n * translating OpenAI's response (content, finish reason, tool calls,\n * usage) back into the neutral shapes on the way in.\n * - Does NOT own: dispatching tools, deciding whether to loop, tracking\n * conversation history, or retrying on failure — those are agent\n * concerns. The model is a stateless (per-call) protocol adapter.\n *\n * Because it holds a live client and shared defaults, it is modeled as a\n * class (see §4.2 of code-style.md — \"long-lived state across calls\").\n *\n * @example\n * import OpenAI from \"openai\";\n * const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });\n * const model = new OpenAIModel(client, { name: \"gpt-4o\", temperature: 0.3 });\n *\n * const myAgent = agent({\n * model,\n * systemPrompt: \"You are a helpful assistant.\",\n * tools: [searchTool],\n * });\n *\n * const result = await myAgent.execute(\"Summarize today's news.\");\n */\nexport class OpenAIModel 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 // `api: \"responses\"` belongs to `OpenAIResponsesModel`, which only the\n // `OpenAISDK.model()` factory constructs. This class speaks Chat\n // Completions only; accepting the flag here would silently ignore it.\n if (config.api === \"responses\") {\n throw new InvalidRequestError(\n 'OpenAIModel speaks Chat Completions only and cannot serve api: \"responses\". Use OpenAISDK.model({ api: \"responses\" }) on the direct OpenAI provider.',\n { context: { provider, model: config.name, api: config.api } },\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 // o-series + gpt-5 / gpt-6 models surface a reasoning channel and accept\n // the `reasoning_effort` param. Explicit config wins over the\n // name-prefix inference.\n reasoning: config.reasoning ?? inferReasoningCapability(config.name),\n // Direct OpenAI requests support automatic cache hits and explicit\n // content-part breakpoints. Wrapper endpoints have distinct provider\n // labels and may reject these OpenAI-only request fields.\n promptCaching: this.provider === \"openai\",\n // PDF + audio INPUT are off by default — OpenAI accepts `file`\n // (PDF) and `input_audio` parts only on specific models, so the\n // flags are conservative/honest and opt-in via config rather than\n // name-inferred. When set, the agent admits the attachments and\n // `toOpenAIMessages` maps them to the real wire parts.\n pdf: config.pdf ?? false,\n audio: config.audio ?? false,\n };\n }\n\n /**\n * Single-shot completion. Sends the full message list to the Chat\n * Completions endpoint, waits for the terminal response, and reshapes it\n * into a vendor-neutral `ModelResponse`. Per-call `options` override the\n * instance's `ModelConfig` defaults for this call only.\n */\n public async complete(messages: Message[], options?: ModelCallOptions): Promise<ModelResponse> {\n // Per-call request/response logs are hot-path in production agents\n // — keep them at `debug` so `info` stays reserved for lifecycle\n // events (agent starting/completed, etc.). Operators who need to\n // audit every LLM call can raise log-level at runtime.\n this.logger.debug(LOG_MODULE, \"request\", \"Starting call to chat.completions\", {\n model: this.name,\n messageCount: messages.length,\n streaming: false,\n toolCount: options?.tools?.length ?? 0,\n });\n\n let response: OpenAI.Chat.Completions.ChatCompletion;\n\n try {\n response = await this.client.chat.completions.create(\n mergeProviderBody<OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming>(\n {\n model: this.name,\n ...compatibleSearchParams(this.provider, options),\n messages: toOpenAIMessages(messages, this.promptCacheBreakpoints(options)),\n ...this.buildSamplingParams(options),\n ...buildPromptCacheParams(this.provider, this.config),\n ...this.buildTools(options),\n ...this.buildResponseFormat(options?.responseSchema),\n ...this.buildReasoningParams(options?.reasoning, Boolean(options?.tools?.length)),\n },\n options,\n ),\n options?.signal ? { signal: options.signal } : undefined,\n );\n } catch (thrown) {\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 throw wrapped;\n }\n\n const [choice] = response.choices;\n\n if (choice === undefined) {\n // A 200 carrying an empty `choices` array. Every line below reads off\n // `choice`, so without this the caller got a bare `TypeError: Cannot\n // read properties of undefined` — no provider, no model, no hint that\n // the response itself was the problem rather than our handling of it.\n //\n // `ProviderError` because that is what this is: the provider answered\n // successfully and returned nothing usable. Callers already branch on\n // `instanceof ProviderError` for any provider-side failure.\n throw new ProviderError(\"OpenAI returned a response with no choices.\", {\n context: { provider: \"openai\", model: this.name, choices: response.choices.length },\n });\n }\n\n const finishReason = mapFinishReason(choice.finish_reason);\n const usage = this.extractUsage(response.usage);\n\n this.logger.debug(LOG_MODULE, \"response\", \"call to chat.completions succeeded\", {\n finishReason,\n usage,\n });\n\n return {\n content: choice.message.content ?? \"\",\n finishReason,\n usage,\n toolCalls: this.extractToolCalls(choice.message.tool_calls),\n };\n }\n\n /**\n * Incremental streaming completion. Yields neutral `ModelStreamChunk`s —\n * `delta` for text tokens, `tool-call` when the model requests a tool,\n * and a terminal `done` carrying the final finish reason + usage totals.\n * Callers consume it with `for await`.\n *\n * With `options.streamChunks` it also yields `tool-args` for each\n * `tool_calls[].function.arguments` fragment and `reasoning` for a\n * vendor's `reasoning_content` / `reasoning` delta field. Usage arrives\n * only on the final chunk here, so no `usage` chunk is emitted.\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 chat.completions\", {\n model: this.name,\n messageCount: messages.length,\n streaming: true,\n toolCount: options?.tools?.length ?? 0,\n });\n\n let stream: Awaited<ReturnType<typeof this.client.chat.completions.create>>;\n\n try {\n stream = await this.client.chat.completions.create(\n mergeProviderBody<OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming>(\n {\n model: this.name,\n ...compatibleSearchParams(this.provider, options),\n messages: toOpenAIMessages(messages, this.promptCacheBreakpoints(options)),\n ...this.buildSamplingParams(options),\n ...buildPromptCacheParams(this.provider, this.config),\n ...this.buildTools(options),\n stream: true,\n stream_options: { include_usage: true },\n ...this.buildResponseFormat(options?.responseSchema),\n ...this.buildReasoningParams(options?.reasoning, Boolean(options?.tools?.length)),\n },\n options,\n ),\n options?.signal ? { signal: options.signal } : undefined,\n );\n } catch (thrown) {\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 throw wrapped;\n }\n\n let rawFinishReason: string = \"stop\";\n let usage: Usage = { input: 0, output: 0, total: 0 };\n const want = resolveStreamChunks(options);\n const toolCallAccum = new Map<\n number,\n { id: string; name: string; arguments: string; streamed: number }\n >();\n\n try {\n for await (const chunk of stream as AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>) {\n const delta = chunk.choices[0]?.delta;\n const finish = chunk.choices[0]?.finish_reason;\n\n if (want.reasoning && delta) {\n const reasoning = readReasoningDelta(delta);\n\n if (reasoning) {\n yield { type: \"reasoning\", delta: reasoning };\n }\n }\n\n if (delta?.content) {\n yield { type: \"delta\", content: delta.content };\n }\n\n if (delta?.tool_calls) {\n for (const toolCall of delta.tool_calls) {\n const idx = toolCall.index ?? 0;\n if (!toolCallAccum.has(idx)) {\n toolCallAccum.set(idx, { id: \"\", name: \"\", arguments: \"\", streamed: 0 });\n }\n const acc = toolCallAccum.get(idx)!;\n if (toolCall.id) acc.id = toolCall.id;\n if (toolCall.function?.name) acc.name = toolCall.function.name;\n if (toolCall.function?.arguments) acc.arguments += toolCall.function.arguments;\n\n // The first fragment of a call carries its id and name; later\n // ones only `index` + argument text. Anything that arrived\n // before the name is flushed once the call is identified.\n if (want.toolArgs && acc.name && acc.arguments.length > acc.streamed) {\n yield {\n type: \"tool-args\",\n callId: acc.id,\n name: acc.name,\n delta: acc.arguments.slice(acc.streamed),\n };\n acc.streamed = acc.arguments.length;\n }\n }\n }\n\n if (finish) {\n rawFinishReason = finish;\n }\n\n if (chunk.usage) {\n usage = this.extractUsage(chunk.usage);\n }\n }\n\n for (const acc of toolCallAccum.values()) {\n // Skip accumulators that never received a function name — those\n // are partial fragments the model started but never identified\n // (e.g. arguments-only deltas with no originating `id`/`name`).\n // Yielding them produces nameless tool-calls the agent runtime\n // can't dispatch and would mis-attribute as a registered tool.\n if (!acc.name) continue;\n\n yield {\n type: \"tool-call\",\n id: acc.id,\n name: acc.name,\n input: safeJsonParse<Record<string, unknown>>(acc.arguments, {}),\n };\n }\n } catch (thrown) {\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 throw wrapped;\n }\n\n const finishReason = mapFinishReason(rawFinishReason);\n\n this.logger.debug(LOG_MODULE, \"response\", \"Streaming call to chat.completions succeeded\", {\n finishReason,\n usage,\n });\n\n yield {\n type: \"done\",\n finishReason,\n usage,\n };\n }\n\n /**\n * Spread-friendly `tools` fragment: the function tools, then the\n * provider-defined entries whose `provider` equals this model's\n * provider label (`\"openai\"`, or the vendor label an OpenAI-compatible\n * wrapper sets, e.g. `\"groq\"`), sent verbatim. Chat Completions only\n * accepts `function` and `custom` tool types from OpenAI itself; its\n * hosted tools (`web_search`, `file_search`, …) need `api: \"responses\"`\n * and OpenAI rejects them here with a 400.\n */\n private buildTools(options: ModelCallOptions | undefined): {\n tools?: OpenAI.Chat.Completions.ChatCompletionTool[];\n } {\n const { tools, definitions } = selectProviderTools(options, {\n accepts: [this.provider],\n module: LOG_MODULE,\n model: this.name,\n logger: this.logger,\n });\n // https://platform.kimi.ai/docs/guide/use-web-search — retires 2026-10-20.\n if (\n (this.provider === \"moonshot\" || this.provider === \"kimi\") &&\n definitions.some((definition) => {\n const fn = definition.function;\n return (\n definition.type === \"builtin_function\" &&\n typeof fn === \"object\" &&\n fn !== null &&\n \"name\" in fn &&\n fn.name === \"$web_search\"\n );\n })\n ) {\n throw new InvalidRequestError(\n \"Kimi's builtin $web_search retires on 20 October 2026. Use kimiSearchTool() / kimiFetchTool() from @warlock.js/ai-openai.\",\n );\n }\n // Native search is a Responses-only xAI feature. Fail before transport.\n // https://docs.x.ai/developers/tools/web-search\n if (\n this.provider === \"xai\" &&\n definitions.some(\n (definition) => definition.type === \"web_search\" || definition.type === \"x_search\",\n )\n ) {\n throw new InvalidRequestError(\n \"xAI native web_search / x_search requires POST /v1/responses. XaiSDK currently supports Chat Completions only; use xAI's Responses API with the vendor SDK for native search.\",\n );\n }\n const mapped = [\n ...(toOpenAITools(tools) ?? []),\n ...(definitions as unknown as OpenAI.Chat.Completions.ChatCompletionTool[]),\n ];\n\n return mapped.length > 0 ? { tools: mapped } : {};\n }\n\n /** Map the neutral cache hint only for the direct OpenAI provider. */\n private promptCacheBreakpoints(options: ModelCallOptions | undefined): number {\n return this.provider === \"openai\" ? (options?.cacheControl?.breakpoints ?? 0) : 0;\n }\n\n /**\n * Translate the neutral `responseSchema` option into OpenAI's\n * `response_format` parameter. The mode decision (override handling,\n * strict-compatibility downgrade to `json_object`) lives in\n * `planStructuredOutput`, shared with the Responses adapter.\n *\n * Returns an empty spread when no schema was supplied, so the caller\n * can unconditionally `...buildResponseFormat(...)` into the request.\n */\n private buildResponseFormat(responseSchema: Record<string, unknown> | undefined): {\n response_format?: OpenAI.Chat.Completions.ChatCompletionCreateParams[\"response_format\"];\n } {\n const plan = planStructuredOutput(responseSchema, this.config.responseFormat);\n\n if (plan.mode === \"json_schema\") {\n return {\n response_format: {\n type: \"json_schema\",\n json_schema: { name: \"response\", schema: plan.schema, strict: true },\n },\n };\n }\n\n if (plan.mode === \"json_object\") {\n return { response_format: { type: \"json_object\" } };\n }\n\n return {};\n }\n\n /**\n * Normalize OpenAI's `usage` block (which may be absent on some responses\n * or partials) into the neutral `Usage` shape. Missing usage collapses to\n * zeros rather than propagating `undefined`, so downstream aggregation\n * math stays safe.\n *\n * `cachedTokens` mirrors `prompt_tokens_details.cached_tokens` (the\n * subset of the prompt served from OpenAI's automatic prompt cache);\n * `reasoningTokens` mirrors `completion_tokens_details.reasoning_tokens`\n * (the hidden reasoning channel on o-series / gpt-5 models, already\n * counted within `output`). Both are emitted only when the provider\n * reports a positive value, so non-reasoning / uncached calls keep the\n * lean `{ input, output, total }` shape. `cacheWriteTokens` mirrors\n * `prompt_tokens_details.cache_write_tokens` (billed writes on the\n * models that charge for them). `cost` is attached when priced.\n */\n private extractUsage(raw: OpenAI.Completions.CompletionUsage | undefined): Usage {\n if (!raw) {\n return { input: 0, output: 0, total: 0 };\n }\n\n return buildUsage(\n {\n input: raw.prompt_tokens ?? 0,\n output: raw.completion_tokens ?? 0,\n total: raw.total_tokens ?? 0,\n cachedTokens: raw.prompt_tokens_details?.cached_tokens,\n cacheWriteTokens: raw.prompt_tokens_details?.cache_write_tokens,\n reasoningTokens: raw.completion_tokens_details?.reasoning_tokens,\n },\n this.pricing,\n );\n }\n\n /**\n * Translate the neutral `ModelCallOptions.reasoning` hint into OpenAI's\n * `reasoning_effort` request param. Only `effort` maps — OpenAI's Chat\n * Completions API exposes a discrete effort knob, not a token budget,\n * so `reasoning.maxTokens` (the Anthropic extended-thinking cap) has no\n * wire equivalent here and is silently ignored.\n *\n * The neutral `ReasoningEffort` (`\"low\" | \"medium\" | \"high\" | \"none\"`)\n * is a subset of OpenAI's accepted values, so it forwards verbatim —\n * `\"none\"` included. `\"none\"` is load-bearing: gpt-5 / o-series models\n * **reject function tools** on Chat Completions while reasoning is\n * active, and the endpoint accepts tools only when `reasoning_effort`\n * is `\"none\"` (the alternative is the Responses API). This is why the\n * param is EMITTED for `\"none\"` rather than omitted — omitting it\n * leaves the model reasoning server-side by default, so tools would\n * still be rejected.\n *\n * No-ops when the model is not reasoning-capable\n * (`capabilities.reasoning` is false — e.g. `gpt-4o`, which 400s on any\n * `reasoning_effort`).\n *\n * When the caller gave no explicit `effort`: defaults to `\"none\"` IF\n * `hasTools` is true, else omits the param (provider default — the\n * pre-existing behavior for a tool-less call). The default exists\n * because, on a reasoning-capable model, there is no working alternative\n * to `\"none\"` when tools are attached — omitting `reasoning_effort`\n * leaves reasoning on server-side, which Chat Completions rejects tools\n * for (empty replies on some model generations, a hard 400 — \"Function\n * tools with reasoning_effort are not supported ... in\n * /v1/chat/completions\" — on newer ones). An explicit `effort` from the\n * caller always wins over this default, in either direction.\n *\n * Returns an empty spread when nothing applies, so the caller can\n * unconditionally `...buildReasoningParams(...)` into the request.\n */\n /**\n * Output-length and sampling params, in current Chat Completions shapes only.\n *\n * - `max_completion_tokens`, never the legacy `max_tokens`: current\n * models (the gpt-5 family and later) answer `400 unsupported_parameter`\n * to `max_tokens`, and `max_completion_tokens` is accepted everywhere.\n * - No `temperature` for a reasoning-capable model: those accept only the\n * default value and reject any other with a 400.\n */\n private buildSamplingParams(options: ModelCallOptions | undefined): {\n max_completion_tokens?: number;\n temperature?: number;\n } {\n const maxTokens = options?.maxTokens ?? this.config.maxTokens;\n const temperature = options?.temperature ?? this.config.temperature;\n\n return {\n max_completion_tokens: maxTokens,\n ...(!this.capabilities.reasoning && { temperature }),\n };\n }\n\n private buildReasoningParams(\n reasoning: ModelCallOptions[\"reasoning\"],\n hasTools: boolean,\n ): {\n reasoning_effort?: OpenAI.Chat.Completions.ChatCompletionCreateParams[\"reasoning_effort\"];\n } {\n if (!this.capabilities.reasoning) {\n return {};\n }\n\n if (!reasoning?.effort) {\n return hasTools ? { reasoning_effort: \"none\" } : {};\n }\n\n return { reasoning_effort: toOpenAIReasoningEffort(reasoning.effort) };\n }\n\n /**\n * Reshape OpenAI's `tool_calls` array into the neutral\n * `ModelToolCallRequest[]`. The raw `arguments` field is a JSON string\n * per OpenAI's protocol — we parse it defensively via `safeJsonParse` so\n * malformed or empty arguments yield an empty object instead of crashing\n * the trip. Returns `undefined` when no tools were requested so callers\n * can branch on presence.\n */\n private extractToolCalls(\n rawToolCalls: OpenAI.Chat.Completions.ChatCompletionMessageToolCall[] | undefined,\n ): ModelToolCallRequest[] | undefined {\n if (!rawToolCalls || rawToolCalls.length === 0) {\n return undefined;\n }\n\n return rawToolCalls.map((toolCall) => ({\n id: toolCall.id,\n name: (toolCall as any).function.name,\n input: safeJsonParse<Record<string, unknown>>((toolCall as any).function.arguments, {}),\n }));\n }\n}\n\n/**\n * Reasoning text an OpenAI-compatible vendor puts on a Chat Completions\n * stream delta. OpenAI itself streams none on this API. DeepSeek documents\n * `delta.reasoning_content` (API reference, chat completion chunk); Groq\n * documents the parsed reasoning under a `reasoning` field. Neither is in\n * the OpenAI SDK's delta type, so the delta is read as a plain record.\n */\nfunction readReasoningDelta(delta: object): string | undefined {\n const record = delta as Record<string, unknown>;\n\n for (const key of [\"reasoning_content\", \"reasoning\"]) {\n const value = record[key];\n\n if (typeof value === \"string\" && value !== \"\") {\n return value;\n }\n }\n\n return undefined;\n}\n"],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import "@warlock.js/ai";
2
+ //#region ../../packages/ai-openai/src/provider-options.d.ts
3
+ /** Adapter-only fields merged after the neutral request parameters. */
4
+ export type OpenAICallOptions = {
5
+ body?: Record<string, unknown>;
6
+ };
7
+ /** DashScope OpenAI-compatible request flag (not a tool).
8
+ * https://www.alibabacloud.com/help/en/model-studio/web-search */
9
+ export type QwenCallOptions = {
10
+ enableSearch?: boolean;
11
+ };
12
+ //#endregion
13
+ //# sourceMappingURL=provider-options.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-options.d.mts","names":[],"sources":["../../../../../../../packages/ai-openai/src/provider-options.ts"],"mappings":";;;YAGY;EAAsB,OAAO;;;;YA6D7B;EAAoB"}
@@ -0,0 +1,56 @@
1
+ import { InvalidRequestError } from "@warlock.js/ai";
2
+
3
+ //#region ../../packages/ai-openai/src/provider-options.ts
4
+ const RESERVED_FIELDS = /* @__PURE__ */ new Set([
5
+ "model",
6
+ "messages",
7
+ "input",
8
+ "instructions",
9
+ "tools",
10
+ "stream",
11
+ "stream_options",
12
+ "previous_response_id",
13
+ "conversation",
14
+ "store",
15
+ "include",
16
+ "functions",
17
+ "function_call",
18
+ "prompt",
19
+ "background"
20
+ ]);
21
+ function mergeProviderBody(built, options) {
22
+ const body = (options?.providerOptions?.openai)?.body ?? {};
23
+ const reserved = Object.keys(body).filter((key) => RESERVED_FIELDS.has(key));
24
+ if (reserved.length > 0) throw new InvalidRequestError(`providerOptions.openai.body cannot set ${reserved.join(", ")}: use the call's messages, tools and model options.`, { context: { fields: reserved } });
25
+ const defaults = built;
26
+ const overrides = Object.fromEntries(Object.entries(body).map(([key, value]) => {
27
+ const previous = defaults[key];
28
+ if (!isPlainObject(previous) || !isPlainObject(value)) return [key, value];
29
+ if (typeof previous.type === "string" && typeof value.type === "string" && previous.type !== value.type) return [key, value];
30
+ return [key, {
31
+ ...previous,
32
+ ...value
33
+ }];
34
+ }));
35
+ return {
36
+ ...built,
37
+ ...overrides
38
+ };
39
+ }
40
+ /** Merge only plain objects; arrays, null and provider scalar values replace the field. */
41
+ function isPlainObject(value) {
42
+ if (value === null || typeof value !== "object") return false;
43
+ const prototype = Object.getPrototypeOf(value);
44
+ return prototype === Object.prototype || prototype === null;
45
+ }
46
+ function compatibleSearchParams(provider, options) {
47
+ if (provider !== "qwen" && provider !== "dashscope") return {};
48
+ const value = options?.providerOptions?.[provider]?.enableSearch;
49
+ if (value === void 0) return {};
50
+ if (typeof value !== "boolean") throw new InvalidRequestError(`providerOptions.${provider}.enableSearch must be a boolean`);
51
+ return { enable_search: value };
52
+ }
53
+
54
+ //#endregion
55
+ export { compatibleSearchParams, mergeProviderBody };
56
+ //# sourceMappingURL=provider-options.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-options.mjs","names":[],"sources":["../../../../../../../packages/ai-openai/src/provider-options.ts"],"sourcesContent":["import { InvalidRequestError, type ModelCallOptions } from \"@warlock.js/ai\";\n\n/** Adapter-only fields merged after the neutral request parameters. */\nexport type OpenAICallOptions = { body?: Record<string, unknown> };\n\nconst RESERVED_FIELDS = new Set([\n \"model\",\n \"messages\",\n \"input\",\n \"instructions\",\n \"tools\",\n \"stream\",\n \"stream_options\",\n \"previous_response_id\",\n \"conversation\",\n \"store\",\n \"include\",\n \"functions\",\n \"function_call\",\n \"prompt\",\n \"background\",\n]);\n\nexport function mergeProviderBody<T extends object>(\n built: T,\n options: ModelCallOptions | undefined,\n): T {\n const provider = options?.providerOptions?.openai as OpenAICallOptions | undefined;\n const body = provider?.body ?? {};\n const reserved = Object.keys(body).filter((key) => RESERVED_FIELDS.has(key));\n if (reserved.length > 0) {\n throw new InvalidRequestError(\n `providerOptions.openai.body cannot set ${reserved.join(\", \")}: use the call's messages, tools and model options.`,\n { context: { fields: reserved } },\n );\n }\n const defaults = built as Record<string, unknown>;\n const overrides = Object.fromEntries(\n Object.entries(body).map(([key, value]) => {\n const previous = defaults[key];\n if (!isPlainObject(previous) || !isPlainObject(value)) return [key, value];\n // A different discriminator selects a whole SDK variant. Keeping fields\n // from the previous variant can make an otherwise valid override invalid.\n if (\n typeof previous.type === \"string\" &&\n typeof value.type === \"string\" &&\n previous.type !== value.type\n )\n return [key, value];\n return [key, { ...previous, ...value }];\n }),\n );\n return { ...built, ...overrides };\n}\n\n/** Merge only plain objects; arrays, null and provider scalar values replace the field. */\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n if (value === null || typeof value !== \"object\") return false;\n const prototype = Object.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n\n/** DashScope OpenAI-compatible request flag (not a tool).\n * https://www.alibabacloud.com/help/en/model-studio/web-search */\nexport type QwenCallOptions = { enableSearch?: boolean };\n\nexport function compatibleSearchParams(\n provider: string,\n options: ModelCallOptions | undefined,\n): { enable_search?: boolean } {\n if (provider !== \"qwen\" && provider !== \"dashscope\") return {};\n const value = options?.providerOptions?.[provider]?.enableSearch;\n if (value === undefined) return {};\n if (typeof value !== \"boolean\")\n throw new InvalidRequestError(`providerOptions.${provider}.enableSearch must be a boolean`);\n return { enable_search: value };\n}\n"],"mappings":";;;AAKA,MAAM,kCAAkB,IAAI,IAAI;CAC9B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AAED,SAAgB,kBACd,OACA,SACG;CAEH,MAAM,QADW,SAAS,iBAAiB,OACtB,EAAE,QAAQ,CAAC;CAChC,MAAM,WAAW,OAAO,KAAK,IAAI,CAAC,CAAC,QAAQ,QAAQ,gBAAgB,IAAI,GAAG,CAAC;CAC3E,IAAI,SAAS,SAAS,GACpB,MAAM,IAAI,oBACR,0CAA0C,SAAS,KAAK,IAAI,EAAE,sDAC9D,EAAE,SAAS,EAAE,QAAQ,SAAS,EAAE,CAClC;CAEF,MAAM,WAAW;CACjB,MAAM,YAAY,OAAO,YACvB,OAAO,QAAQ,IAAI,CAAC,CAAC,KAAK,CAAC,KAAK,WAAW;EACzC,MAAM,WAAW,SAAS;EAC1B,IAAI,CAAC,cAAc,QAAQ,KAAK,CAAC,cAAc,KAAK,GAAG,OAAO,CAAC,KAAK,KAAK;EAGzE,IACE,OAAO,SAAS,SAAS,YACzB,OAAO,MAAM,SAAS,YACtB,SAAS,SAAS,MAAM,MAExB,OAAO,CAAC,KAAK,KAAK;EACpB,OAAO,CAAC,KAAK;GAAE,GAAG;GAAU,GAAG;EAAM,CAAC;CACxC,CAAC,CACH;CACA,OAAO;EAAE,GAAG;EAAO,GAAG;CAAU;AAClC;;AAGA,SAAS,cAAc,OAAkD;CACvE,IAAI,UAAU,QAAQ,OAAO,UAAU,UAAU,OAAO;CACxD,MAAM,YAAY,OAAO,eAAe,KAAK;CAC7C,OAAO,cAAc,OAAO,aAAa,cAAc;AACzD;AAMA,SAAgB,uBACd,UACA,SAC6B;CAC7B,IAAI,aAAa,UAAU,aAAa,aAAa,OAAO,CAAC;CAC7D,MAAM,QAAQ,SAAS,kBAAkB,SAAS,EAAE;CACpD,IAAI,UAAU,QAAW,OAAO,CAAC;CACjC,IAAI,OAAO,UAAU,WACnB,MAAM,IAAI,oBAAoB,mBAAmB,SAAS,gCAAgC;CAC5F,OAAO,EAAE,eAAe,MAAM;AAChC"}