@warlock.js/ai-openai 5.26.0 → 5.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/cjs/index.cjs +958 -280
  3. package/cjs/index.cjs.map +1 -1
  4. package/esm/config.type.d.mts +22 -0
  5. package/esm/config.type.d.mts.map +1 -1
  6. package/esm/model.mjs +26 -119
  7. package/esm/model.mjs.map +1 -1
  8. package/esm/responses-model.mjs +329 -0
  9. package/esm/responses-model.mjs.map +1 -0
  10. package/esm/sdk.d.mts +7 -0
  11. package/esm/sdk.d.mts.map +1 -1
  12. package/esm/sdk.mjs +21 -1
  13. package/esm/sdk.mjs.map +1 -1
  14. package/esm/utils/build-usage.mjs +23 -0
  15. package/esm/utils/build-usage.mjs.map +1 -0
  16. package/esm/utils/index.mjs +9 -1
  17. package/esm/utils/map-responses-status.mjs +54 -0
  18. package/esm/utils/map-responses-status.mjs.map +1 -0
  19. package/esm/utils/prompt-cache-params.mjs +17 -0
  20. package/esm/utils/prompt-cache-params.mjs.map +1 -0
  21. package/esm/utils/reasoning-effort.mjs +35 -0
  22. package/esm/utils/reasoning-effort.mjs.map +1 -0
  23. package/esm/utils/reasoning-replay.mjs +85 -0
  24. package/esm/utils/reasoning-replay.mjs.map +1 -0
  25. package/esm/utils/structured-output.mjs +84 -0
  26. package/esm/utils/structured-output.mjs.map +1 -0
  27. package/esm/utils/to-openai-messages.mjs +9 -2
  28. package/esm/utils/to-openai-messages.mjs.map +1 -1
  29. package/esm/utils/to-openai-tools.mjs +3 -3
  30. package/esm/utils/to-openai-tools.mjs.map +1 -1
  31. package/esm/utils/to-responses-input.mjs +112 -0
  32. package/esm/utils/to-responses-input.mjs.map +1 -0
  33. package/esm/utils/to-responses-tools.mjs +51 -0
  34. package/esm/utils/to-responses-tools.mjs.map +1 -0
  35. package/llms-full.txt +20 -3
  36. package/llms.txt +1 -1
  37. package/package.json +3 -3
  38. package/skills/setup-openai/SKILL.md +20 -3
@@ -64,6 +64,18 @@ type OpenAISDKConfig = Omit<ClientOptions, "provider"> & {
64
64
  * openai.model({ name: "some-legacy-model", responseFormat: "json_object" });
65
65
  */
66
66
  type OpenAIResponseFormat = "json_schema" | "json_object" | "text";
67
+ /**
68
+ * Which OpenAI endpoint a model speaks.
69
+ *
70
+ * - `"chat"` (default) - Chat Completions. Reasoning models reject function
71
+ * tools here unless reasoning is off, so the adapter sends
72
+ * `reasoning_effort: "none"` when tools are attached.
73
+ * - `"responses"` - the Responses API, stateless (`store: false`). Reasoning
74
+ * stays at the provider default even with tools attached. Direct OpenAI
75
+ * only: `OpenAISDK.model()` throws for any other provider label, so the
76
+ * wrapper packages (deepseek, groq, xai, mistral) can never reach it.
77
+ */
78
+ type OpenAIApi = "chat" | "responses";
67
79
  /**
68
80
  * Per-model configuration for `OpenAISDK.model()`. Extends the neutral
69
81
  * `ModelConfig` with OpenAI-specific capability overrides.
@@ -71,8 +83,18 @@ type OpenAIResponseFormat = "json_schema" | "json_object" | "text";
71
83
  * @example
72
84
  * openai.model({ name: "gpt-4o-mini" }); // vision auto-true
73
85
  * openai.model({ name: "fine-tuned-x", vision: true }); // dev override
86
+ *
87
+ * @example
88
+ * // Reasoning model + tools on the Responses API (direct OpenAI only):
89
+ * openai.model({ name: "gpt-5.6", api: "responses" });
74
90
  */
75
91
  type OpenAIModelConfig = ModelConfig & {
92
+ /**
93
+ * Wire API to use. Default `"chat"` (Chat Completions). `"responses"`
94
+ * selects the stateless Responses adapter and is accepted only on the
95
+ * direct `"openai"` provider label; see {@link OpenAIApi}.
96
+ */
97
+ api?: OpenAIApi;
76
98
  /**
77
99
  * Override the auto-inferred vision capability. When omitted, the
78
100
  * adapter checks the model name against a known-prefix list (see
@@ -1 +1 @@
1
- {"version":3,"file":"config.type.d.mts","names":[],"sources":["../../../../../../ai-openai/src/config.type.ts"],"mappings":";;;;;AAyCA;;;;;;;;;;;;;;;;AAeuC;AAiBvC;;;;AAAgC;AAUhC;;;;;;;KA1CY,eAAA,GAAkB,IAAA,CAAK,aAAA;EAgGH;;;;;;;;EAvF9B,QAAA;EAuFuB;;;;;EAjFvB,OAAA,GAAU,MAAA,SAAe,YAAA;AAAA;;AAqGpB;AAaP;;;;AAAiD;AAcjD;;;;AAAgD;AAahD;;KA5HY,oBAAA;;AA4HsC;AAalD;;;;AAAgE;;KA/HpD,iBAAA,GAAoB,WAAA;;;;;;;;EAQ9B,MAAA;;;;;;;;;;;;;;EAcA,cAAA,GAAiB,oBAAA;;;;;;;;;EASjB,gBAAA;;;;;;;;;;;EAWA,SAAA;;;;;;EAMA,cAAA;;;;;;EAMA,oBAAA,GAAuB,OAAA,CACrB,MAAA,CAAO,IAAA,CAAK,WAAA,CAAY,0BAAA;;;;;;;;;EAW1B,GAAA;;;;;;;;EAQA,KAAA;AAAA;;;;;;;;;;;KAaU,oBAAA,GAAuB,cAAc;;;;;;;;;;;;;KAcrC,iBAAA,GAAoB,gBAAgB;;;;;;;;;;;;KAapC,kBAAA,GAAqB,iBAAiB;;;;;;;;;;;;KAatC,yBAAA,GAA4B,wBAAwB"}
1
+ {"version":3,"file":"config.type.d.mts","names":[],"sources":["../../../../../../ai-openai/src/config.type.ts"],"mappings":";;;;;AAyCA;;;;;;;;;;;;;;;;AAeuC;AAiBvC;;;;AAAgC;AAahC;;;;AAAqB;AAcrB;;KA3DY,eAAA,GAAkB,IAAA,CAAK,aAAA;EA2DH;;;;;;;;EAlD9B,QAAA;EAwDM;;;;;EAlDN,OAAA,GAAU,MAAA,SAAe,YAAA;AAAA;;;;;;;;;;AA4HpB;AAaP;;;;KAxHY,oBAAA;AAsIZ;;;;AAAgD;AAahD;;;;AAAkD;AAalD;AA1BA,KAzHY,SAAA;;;AAmJoD;;;;;;;;;;KArIpD,iBAAA,GAAoB,WAAA;;;;;;EAM9B,GAAA,GAAM,SAAA;;;;;;;;EAQN,MAAA;;;;;;;;;;;;;;EAcA,cAAA,GAAiB,oBAAA;;;;;;;;;EASjB,gBAAA;;;;;;;;;;;EAWA,SAAA;;;;;;EAMA,cAAA;;;;;;EAMA,oBAAA,GAAuB,OAAA,CACrB,MAAA,CAAO,IAAA,CAAK,WAAA,CAAY,0BAAA;;;;;;;;;EAW1B,GAAA;;;;;;;;EAQA,KAAA;AAAA;;;;;;;;;;;KAaU,oBAAA,GAAuB,cAAc;;;;;;;;;;;;;KAcrC,iBAAA,GAAoB,gBAAgB;;;;;;;;;;;;KAapC,kBAAA,GAAqB,iBAAiB;;;;;;;;;;;;KAatC,yBAAA,GAA4B,wBAAwB"}
package/esm/model.mjs CHANGED
@@ -1,46 +1,20 @@
1
+ import { buildUsage } from "./utils/build-usage.mjs";
2
+ import { wrapOpenAIError } from "./utils/wrap-openai-error.mjs";
1
3
  import { mapFinishReason } from "./utils/map-finish-reason.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";
2
7
  import { toOpenAIMessages } from "./utils/to-openai-messages.mjs";
3
8
  import { toOpenAITools } from "./utils/to-openai-tools.mjs";
4
- import { wrapOpenAIError } from "./utils/wrap-openai-error.mjs";
5
9
  import "./utils/index.mjs";
6
10
  import { inferReasoningCapability } from "./known-reasoning-models.mjs";
7
11
  import { inferVisionCapability } from "./known-vision-models.mjs";
8
- import { ProviderError, safeJsonParse } from "@warlock.js/ai";
12
+ import { InvalidRequestError, ProviderError, safeJsonParse } from "@warlock.js/ai";
9
13
  import { log } from "@warlock.js/logger";
10
14
 
11
15
  //#region ../ai-openai/src/model.ts
12
16
  const LOG_MODULE = "ai.openai";
13
17
  /**
14
- * The installed OpenAI SDK 7.23.0 declares `reasoning_effort` as
15
- * `none | minimal | low | medium | high | xhigh | max` in
16
- * `openai/src/resources/shared.ts:367`. Its type notes that model support
17
- * varies but does not provide a model-family-specific accepted set, so map by
18
- * that union alone. This exhaustive record makes a future core level a
19
- * typecheck failure until its OpenAI clamp is consciously selected.
20
- */
21
- const EFFORT_TO_OPENAI_EFFORT = {
22
- none: "none",
23
- minimal: "minimal",
24
- low: "low",
25
- medium: "medium",
26
- high: "high",
27
- xhigh: "xhigh",
28
- max: "max"
29
- };
30
- /**
31
- * Map an explicit `responseFormat` override to the default
32
- * `structuredOutput` capability. Loose wire modes (`"json_object"`,
33
- * `"text"`) don't enforce shape, so the agent needs to see the soft
34
- * schema hint in the system prompt — that only happens when the
35
- * capability is `false`. Default (no override) stays `true` to
36
- * preserve the prior assumption that OpenAI models support strict
37
- * structured output.
38
- */
39
- function inferStructuredOutput(responseFormat) {
40
- if (responseFormat === "json_object" || responseFormat === "text") return false;
41
- return true;
42
- }
43
- /**
44
18
  * OpenAI-backed implementation of `ModelContract`.
45
19
  *
46
20
  * **Role.** The provider-facing bridge between the vendor-neutral
@@ -80,6 +54,11 @@ function inferStructuredOutput(responseFormat) {
80
54
  var OpenAIModel = class {
81
55
  constructor(client, config, provider = "openai") {
82
56
  this.logger = log;
57
+ if (config.api === "responses") throw new InvalidRequestError("OpenAIModel speaks Chat Completions only and cannot serve api: \"responses\". Use OpenAISDK.model({ api: \"responses\" }) on the direct OpenAI provider.", { context: {
58
+ provider,
59
+ model: config.name,
60
+ api: config.api
61
+ } });
83
62
  this.client = client;
84
63
  this.config = config;
85
64
  this.name = config.name;
@@ -113,7 +92,7 @@ var OpenAIModel = class {
113
92
  model: this.name,
114
93
  messages: toOpenAIMessages(messages, this.promptCacheBreakpoints(options)),
115
94
  ...this.buildSamplingParams(options),
116
- ...this.buildPromptCacheParams(),
95
+ ...buildPromptCacheParams(this.provider, this.config),
117
96
  tools: toOpenAITools(options?.tools),
118
97
  ...this.buildResponseFormat(options?.responseSchema),
119
98
  ...this.buildReasoningParams(options?.reasoning, Boolean(options?.tools?.length))
@@ -164,7 +143,7 @@ var OpenAIModel = class {
164
143
  model: this.name,
165
144
  messages: toOpenAIMessages(messages, this.promptCacheBreakpoints(options)),
166
145
  ...this.buildSamplingParams(options),
167
- ...this.buildPromptCacheParams(),
146
+ ...buildPromptCacheParams(this.provider, this.config),
168
147
  tools: toOpenAITools(options?.tools),
169
148
  stream: true,
170
149
  stream_options: { include_usage: true },
@@ -245,101 +224,31 @@ var OpenAIModel = class {
245
224
  usage
246
225
  };
247
226
  }
248
- /** OpenAI-only request fields; wrappers use the same transport but not this wire extension. */
249
- buildPromptCacheParams() {
250
- if (this.provider !== "openai") return {};
251
- return {
252
- ...this.config.promptCacheKey !== void 0 ? { prompt_cache_key: this.config.promptCacheKey } : {},
253
- ...this.config.promptCacheRetention !== void 0 ? { prompt_cache_retention: this.config.promptCacheRetention } : {}
254
- };
255
- }
256
227
  /** Map the neutral cache hint only for the direct OpenAI provider. */
257
228
  promptCacheBreakpoints(options) {
258
229
  return this.provider === "openai" ? options?.cacheControl?.breakpoints ?? 0 : 0;
259
230
  }
260
231
  /**
261
232
  * Translate the neutral `responseSchema` option into OpenAI's
262
- * `response_format` parameter.
263
- *
264
- * When `config.responseFormat` is set, it wins: `"text"` emits no
265
- * `response_format` at all, `"json_object"` always picks the loose
266
- * mode, and `"json_schema"` picks strict mode (with the same
267
- * `isStrictCompatible` safety check — a malformed schema still
268
- * degrades to `json_object` rather than 400). The override exists
269
- * because some targets (older OpenAI models, OpenRouter routes,
270
- * Ollama OpenAI-compat) reject strict `json_schema` outright.
271
- *
272
- * When the override is omitted, uses strict `json_schema` mode
273
- * (token-level enforcement) only when the schema is a proper
274
- * root-object JSON Schema (`{ type: "object", properties: ... }`).
275
- * For anything else — malformed extractor output, non-object
276
- * schemas, or future shapes we haven't tested — falls back to loose
277
- * `json_object` mode, which guarantees *some* valid JSON without
278
- * enforcing shape. The agent's soft instruction already embeds the
279
- * schema text in the system prompt when the model declares no
280
- * native structured-output capability, so shape validation still
281
- * runs client-side via the Standard Schema `validate()` call.
233
+ * `response_format` parameter. The mode decision (override handling,
234
+ * strict-compatibility downgrade to `json_object`) lives in
235
+ * `planStructuredOutput`, shared with the Responses adapter.
282
236
  *
283
237
  * Returns an empty spread when no schema was supplied, so the caller
284
238
  * can unconditionally `...buildResponseFormat(...)` into the request.
285
239
  */
286
240
  buildResponseFormat(responseSchema) {
287
- if (!responseSchema) return {};
288
- const override = this.config.responseFormat;
289
- if (override === "text") return {};
290
- if (override === "json_object") return { response_format: { type: "json_object" } };
291
- if (this.isStrictCompatible(responseSchema)) return { response_format: {
241
+ const plan = planStructuredOutput(responseSchema, this.config.responseFormat);
242
+ if (plan.mode === "json_schema") return { response_format: {
292
243
  type: "json_schema",
293
244
  json_schema: {
294
245
  name: "response",
295
- schema: responseSchema,
246
+ schema: plan.schema,
296
247
  strict: true
297
248
  }
298
249
  } };
299
- return { response_format: { type: "json_object" } };
300
- }
301
- /**
302
- * OpenAI strict `json_schema` mode requires the root to be a JSON
303
- * Schema object type (`{ type: "object", properties: ... }`). Anything
304
- * else (top-level arrays, primitives, unknown shapes) is rejected with
305
- * a 400 before a token is sampled. We check structurally here so the
306
- * first call doesn't crash on a malformed extraction — loose
307
- * `json_object` mode is a safe degradation.
308
- */
309
- isStrictCompatible(schema) {
310
- return schema.type === "object" && typeof schema.properties === "object" && schema.properties !== null && this.isStrictSafeNode(schema);
311
- }
312
- /**
313
- * Recursively check the one strict-mode rule schemas most often trip on:
314
- * every object must list ALL of its `properties` in `required` (OpenAI
315
- * strict has no notion of optional — optional fields must be expressed
316
- * as nullable, e.g. `type: ["string", "null"]`, and still appear in
317
- * `required`). A schema that violates this anywhere in the tree is NOT
318
- * sent in strict `json_schema` mode — it degrades to loose
319
- * `json_object` so a hand-built or optional-bearing schema can't 400
320
- * the call ("'required' ... must include every key in properties").
321
- * Client-side `validate()` still enforces the full shape.
322
- */
323
- isStrictSafeNode(node) {
324
- if (!node || typeof node !== "object") return true;
325
- const record = node;
326
- if (record.type === "object" && record.properties && typeof record.properties === "object") {
327
- const properties = record.properties;
328
- const keys = Object.keys(properties);
329
- const required = Array.isArray(record.required) ? record.required : [];
330
- if (keys.some((key) => !required.includes(key))) return false;
331
- for (const key of keys) if (!this.isStrictSafeNode(properties[key])) return false;
332
- }
333
- if (record.items !== void 0 && !this.isStrictSafeNode(record.items)) return false;
334
- for (const branch of [
335
- "anyOf",
336
- "allOf",
337
- "oneOf"
338
- ]) {
339
- const value = record[branch];
340
- if (Array.isArray(value) && value.some((sub) => !this.isStrictSafeNode(sub))) return false;
341
- }
342
- return true;
250
+ if (plan.mode === "json_object") return { response_format: { type: "json_object" } };
251
+ return {};
343
252
  }
344
253
  /**
345
254
  * Normalize OpenAI's `usage` block (which may be absent on some responses
@@ -361,15 +270,13 @@ var OpenAIModel = class {
361
270
  output: 0,
362
271
  total: 0
363
272
  };
364
- const cachedTokens = raw.prompt_tokens_details?.cached_tokens;
365
- const reasoningTokens = raw.completion_tokens_details?.reasoning_tokens;
366
- return {
273
+ return buildUsage({
367
274
  input: raw.prompt_tokens,
368
275
  output: raw.completion_tokens,
369
276
  total: raw.total_tokens,
370
- ...cachedTokens !== void 0 && cachedTokens > 0 ? { cachedTokens } : {},
371
- ...reasoningTokens !== void 0 && reasoningTokens > 0 ? { reasoningTokens } : {}
372
- };
277
+ cachedTokens: raw.prompt_tokens_details?.cached_tokens,
278
+ reasoningTokens: raw.completion_tokens_details?.reasoning_tokens
279
+ });
373
280
  }
374
281
  /**
375
282
  * Translate the neutral `ModelCallOptions.reasoning` hint into OpenAI's
@@ -426,7 +333,7 @@ var OpenAIModel = class {
426
333
  buildReasoningParams(reasoning, hasTools) {
427
334
  if (!this.capabilities.reasoning) return {};
428
335
  if (!reasoning?.effort) return hasTools ? { reasoning_effort: "none" } : {};
429
- return { reasoning_effort: EFFORT_TO_OPENAI_EFFORT[reasoning.effort] };
336
+ return { reasoning_effort: toOpenAIReasoningEffort(reasoning.effort) };
430
337
  }
431
338
  /**
432
339
  * Reshape OpenAI's `tool_calls` array into the neutral
package/esm/model.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"model.mjs","names":[],"sources":["../../../../../../ai-openai/src/model.ts"],"sourcesContent":["import {\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 ReasoningEffort,\n type Usage,\n} from \"@warlock.js/ai\";\nimport { log, type Logger } from \"@warlock.js/logger\";\nimport type OpenAI from \"openai\";\nimport type { OpenAIModelConfig, OpenAIResponseFormat } from \"./config.type\";\nimport { inferReasoningCapability } from \"./known-reasoning-models\";\nimport { inferVisionCapability } from \"./known-vision-models\";\nimport { mapFinishReason, toOpenAIMessages, toOpenAITools, wrapOpenAIError } from \"./utils\";\n\nconst LOG_MODULE = \"ai.openai\";\n\ntype OpenAIReasoningEffort = NonNullable<\n OpenAI.Chat.Completions.ChatCompletionCreateParams[\"reasoning_effort\"]\n>;\n\n/**\n * The installed OpenAI SDK 7.23.0 declares `reasoning_effort` as\n * `none | minimal | low | medium | high | xhigh | max` in\n * `openai/src/resources/shared.ts:367`. Its type notes that model support\n * varies but does not provide a model-family-specific accepted set, so map by\n * that union alone. This exhaustive record makes a future core level a\n * typecheck failure until its OpenAI clamp is consciously selected.\n */\nconst EFFORT_TO_OPENAI_EFFORT: Record<ReasoningEffort, OpenAIReasoningEffort> = {\n none: \"none\",\n minimal: \"minimal\",\n low: \"low\",\n medium: \"medium\",\n high: \"high\",\n xhigh: \"xhigh\",\n max: \"max\",\n};\n\n/**\n * Map an explicit `responseFormat` override to the default\n * `structuredOutput` capability. Loose wire modes (`\"json_object\"`,\n * `\"text\"`) don't enforce shape, so the agent needs to see the soft\n * schema hint in the system prompt — that only happens when the\n * capability is `false`. Default (no override) stays `true` to\n * preserve the prior assumption that OpenAI models support strict\n * structured output.\n */\nfunction inferStructuredOutput(responseFormat: OpenAIResponseFormat | undefined): boolean {\n if (responseFormat === \"json_object\" || responseFormat === \"text\") {\n return false;\n }\n\n return true;\n}\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 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 ...this.buildPromptCacheParams(),\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 ...this.buildPromptCacheParams(),\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 /** OpenAI-only request fields; wrappers use the same transport but not this wire extension. */\n private buildPromptCacheParams(): Pick<\n OpenAI.Chat.Completions.ChatCompletionCreateParams,\n \"prompt_cache_key\" | \"prompt_cache_retention\"\n > {\n if (this.provider !== \"openai\") {\n return {};\n }\n\n return {\n ...(this.config.promptCacheKey !== undefined\n ? { prompt_cache_key: this.config.promptCacheKey }\n : {}),\n ...(this.config.promptCacheRetention !== undefined\n ? { prompt_cache_retention: this.config.promptCacheRetention }\n : {}),\n };\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.\n *\n * When `config.responseFormat` is set, it wins: `\"text\"` emits no\n * `response_format` at all, `\"json_object\"` always picks the loose\n * mode, and `\"json_schema\"` picks strict mode (with the same\n * `isStrictCompatible` safety check — a malformed schema still\n * degrades to `json_object` rather than 400). The override exists\n * because some targets (older OpenAI models, OpenRouter routes,\n * Ollama OpenAI-compat) reject strict `json_schema` outright.\n *\n * When the override is omitted, uses strict `json_schema` mode\n * (token-level enforcement) only when the schema is a proper\n * root-object JSON Schema (`{ type: \"object\", properties: ... }`).\n * For anything else — malformed extractor output, non-object\n * schemas, or future shapes we haven't tested — falls back to loose\n * `json_object` mode, which guarantees *some* valid JSON without\n * enforcing shape. The agent's soft instruction already embeds the\n * schema text in the system prompt when the model declares no\n * native structured-output capability, so shape validation still\n * runs client-side via the Standard Schema `validate()` call.\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 if (!responseSchema) {\n return {};\n }\n\n const override = this.config.responseFormat;\n\n if (override === \"text\") {\n return {};\n }\n\n if (override === \"json_object\") {\n return { response_format: { type: \"json_object\" } };\n }\n\n // Either auto-select (no override) or explicit `\"json_schema\"`.\n // The strict-compat check still applies in the explicit case —\n // a malformed / non-object schema would 400 before sampling, so\n // we degrade to `json_object` rather than crash.\n if (this.isStrictCompatible(responseSchema)) {\n return {\n response_format: {\n type: \"json_schema\",\n json_schema: {\n name: \"response\",\n schema: responseSchema,\n strict: true,\n },\n },\n };\n }\n\n return { response_format: { type: \"json_object\" } };\n }\n\n /**\n * OpenAI strict `json_schema` mode requires the root to be a JSON\n * Schema object type (`{ type: \"object\", properties: ... }`). Anything\n * else (top-level arrays, primitives, unknown shapes) is rejected with\n * a 400 before a token is sampled. We check structurally here so the\n * first call doesn't crash on a malformed extraction — loose\n * `json_object` mode is a safe degradation.\n */\n private isStrictCompatible(schema: Record<string, unknown>): boolean {\n return (\n schema.type === \"object\" &&\n typeof schema.properties === \"object\" &&\n schema.properties !== null &&\n this.isStrictSafeNode(schema)\n );\n }\n\n /**\n * Recursively check the one strict-mode rule schemas most often trip on:\n * every object must list ALL of its `properties` in `required` (OpenAI\n * strict has no notion of optional — optional fields must be expressed\n * as nullable, e.g. `type: [\"string\", \"null\"]`, and still appear in\n * `required`). A schema that violates this anywhere in the tree is NOT\n * sent in strict `json_schema` mode — it degrades to loose\n * `json_object` so a hand-built or optional-bearing schema can't 400\n * the call (\"'required' ... must include every key in properties\").\n * Client-side `validate()` still enforces the full shape.\n */\n private isStrictSafeNode(node: unknown): boolean {\n if (!node || typeof node !== \"object\") {\n return true;\n }\n\n const record = node as Record<string, unknown>;\n\n if (record.type === \"object\" && record.properties && typeof record.properties === \"object\") {\n const properties = record.properties as Record<string, unknown>;\n const keys = Object.keys(properties);\n const required = Array.isArray(record.required) ? (record.required as unknown[]) : [];\n\n if (keys.some((key) => !required.includes(key))) {\n return false;\n }\n\n for (const key of keys) {\n if (!this.isStrictSafeNode(properties[key])) {\n return false;\n }\n }\n }\n\n if (record.items !== undefined && !this.isStrictSafeNode(record.items)) {\n return false;\n }\n\n for (const branch of [\"anyOf\", \"allOf\", \"oneOf\"] as const) {\n const value = record[branch];\n if (Array.isArray(value) && value.some((sub) => !this.isStrictSafeNode(sub))) {\n return false;\n }\n }\n\n return true;\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 const cachedTokens = raw.prompt_tokens_details?.cached_tokens;\n const reasoningTokens = raw.completion_tokens_details?.reasoning_tokens;\n\n return {\n input: raw.prompt_tokens,\n output: raw.completion_tokens,\n total: raw.total_tokens,\n ...(cachedTokens !== undefined && cachedTokens > 0 ? { cachedTokens } : {}),\n ...(reasoningTokens !== undefined && reasoningTokens > 0 ? { reasoningTokens } : {}),\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: EFFORT_TO_OPENAI_EFFORT[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":"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"}
1
+ {"version":3,"file":"model.mjs","names":[],"sources":["../../../../../../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":"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"}