@warlock.js/ai-openai 5.24.0 → 5.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/esm/model.mjs CHANGED
@@ -11,6 +11,23 @@ import { log } from "@warlock.js/logger";
11
11
  //#region ../ai-openai/src/model.ts
12
12
  const LOG_MODULE = "ai.openai";
13
13
  /**
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
+ /**
14
31
  * Map an explicit `responseFormat` override to the default
15
32
  * `structuredOutput` capability. Loose wire modes (`"json_object"`,
16
33
  * `"text"`) don't enforce shape, so the agent needs to see the soft
@@ -72,7 +89,7 @@ var OpenAIModel = class {
72
89
  structuredOutput: config.structuredOutput ?? inferStructuredOutput(config.responseFormat),
73
90
  vision: config.vision ?? inferVisionCapability(config.name),
74
91
  reasoning: config.reasoning ?? inferReasoningCapability(config.name),
75
- promptCaching: true,
92
+ promptCaching: this.provider === "openai",
76
93
  pdf: config.pdf ?? false,
77
94
  audio: config.audio ?? false
78
95
  };
@@ -94,9 +111,9 @@ var OpenAIModel = class {
94
111
  try {
95
112
  response = await this.client.chat.completions.create({
96
113
  model: this.name,
97
- messages: toOpenAIMessages(messages),
98
- temperature: options?.temperature ?? this.config.temperature,
99
- max_tokens: options?.maxTokens ?? this.config.maxTokens,
114
+ messages: toOpenAIMessages(messages, this.promptCacheBreakpoints(options)),
115
+ ...this.buildSamplingParams(options),
116
+ ...this.buildPromptCacheParams(),
100
117
  tools: toOpenAITools(options?.tools),
101
118
  ...this.buildResponseFormat(options?.responseSchema),
102
119
  ...this.buildReasoningParams(options?.reasoning, Boolean(options?.tools?.length))
@@ -145,9 +162,9 @@ var OpenAIModel = class {
145
162
  try {
146
163
  stream = await this.client.chat.completions.create({
147
164
  model: this.name,
148
- messages: toOpenAIMessages(messages),
149
- temperature: options?.temperature ?? this.config.temperature,
150
- max_tokens: options?.maxTokens ?? this.config.maxTokens,
165
+ messages: toOpenAIMessages(messages, this.promptCacheBreakpoints(options)),
166
+ ...this.buildSamplingParams(options),
167
+ ...this.buildPromptCacheParams(),
151
168
  tools: toOpenAITools(options?.tools),
152
169
  stream: true,
153
170
  stream_options: { include_usage: true },
@@ -228,6 +245,18 @@ var OpenAIModel = class {
228
245
  usage
229
246
  };
230
247
  }
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
+ /** Map the neutral cache hint only for the direct OpenAI provider. */
257
+ promptCacheBreakpoints(options) {
258
+ return this.provider === "openai" ? options?.cacheControl?.breakpoints ?? 0 : 0;
259
+ }
231
260
  /**
232
261
  * Translate the neutral `responseSchema` option into OpenAI's
233
262
  * `response_format` parameter.
@@ -377,10 +406,27 @@ var OpenAIModel = class {
377
406
  * Returns an empty spread when nothing applies, so the caller can
378
407
  * unconditionally `...buildReasoningParams(...)` into the request.
379
408
  */
409
+ /**
410
+ * Output-length and sampling params, in current Chat Completions shapes only.
411
+ *
412
+ * - `max_completion_tokens`, never the legacy `max_tokens`: current
413
+ * models (the gpt-5 family and later) answer `400 unsupported_parameter`
414
+ * to `max_tokens`, and `max_completion_tokens` is accepted everywhere.
415
+ * - No `temperature` for a reasoning-capable model: those accept only the
416
+ * default value and reject any other with a 400.
417
+ */
418
+ buildSamplingParams(options) {
419
+ const maxTokens = options?.maxTokens ?? this.config.maxTokens;
420
+ const temperature = options?.temperature ?? this.config.temperature;
421
+ return {
422
+ max_completion_tokens: maxTokens,
423
+ ...!this.capabilities.reasoning && { temperature }
424
+ };
425
+ }
380
426
  buildReasoningParams(reasoning, hasTools) {
381
427
  if (!this.capabilities.reasoning) return {};
382
428
  if (!reasoning?.effort) return hasTools ? { reasoning_effort: "none" } : {};
383
- return { reasoning_effort: reasoning.effort };
429
+ return { reasoning_effort: EFFORT_TO_OPENAI_EFFORT[reasoning.effort] };
384
430
  }
385
431
  /**
386
432
  * 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 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\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 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 // OpenAI prompt caching is automatic on the Chat Completions API\n // (no caller-supplied breakpoints — the platform caches long\n // prompt prefixes server-side and reports the hit count via\n // `prompt_tokens_details.cached_tokens`). We therefore advertise\n // the read-side accounting capability as always available while\n // treating `ModelCallOptions.cacheControl` write breakpoints as a\n // no-op (see `buildReasoningParams` siblings — there is no cache\n // param to emit).\n promptCaching: true,\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),\n temperature: options?.temperature ?? this.config.temperature,\n max_tokens: options?.maxTokens ?? this.config.maxTokens,\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),\n temperature: options?.temperature ?? this.config.temperature,\n max_tokens: options?.maxTokens ?? this.config.maxTokens,\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 /**\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 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: 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 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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"}
@@ -25,8 +25,8 @@ import { InvalidRequestError } from "@warlock.js/ai";
25
25
  * ]},
26
26
  * ]);
27
27
  */
28
- function toOpenAIMessages(messages) {
29
- return messages.map((m) => {
28
+ function toOpenAIMessages(messages, cacheBreakpoints = 0) {
29
+ return addPromptCacheBreakpoints(messages.map((m) => {
30
30
  if (m.role === "tool") return {
31
31
  role: "tool",
32
32
  content: stringifyContent(m.content),
@@ -52,7 +52,44 @@ function toOpenAIMessages(messages) {
52
52
  role: m.role,
53
53
  content: stringifyContent(m.content)
54
54
  };
55
- });
55
+ }), cacheBreakpoints);
56
+ }
57
+ /** Place explicit markers on the last requested user-content parts (maximum four). */
58
+ function addPromptCacheBreakpoints(messages, requested) {
59
+ let remaining = Math.min(4, Math.max(0, Math.floor(requested)));
60
+ if (remaining === 0) return messages;
61
+ const marked = [...messages];
62
+ for (let index = marked.length - 1; index >= 0 && remaining > 0; index--) {
63
+ const message = marked[index];
64
+ if (message === void 0 || message.role !== "user") continue;
65
+ if (typeof message.content === "string") {
66
+ marked[index] = {
67
+ ...message,
68
+ content: [{
69
+ type: "text",
70
+ text: message.content,
71
+ prompt_cache_breakpoint: { mode: "explicit" }
72
+ }]
73
+ };
74
+ remaining--;
75
+ continue;
76
+ }
77
+ const content = [...message.content];
78
+ for (let partIndex = content.length - 1; partIndex >= 0 && remaining > 0; partIndex--) {
79
+ const part = content[partIndex];
80
+ if (part === void 0) continue;
81
+ content[partIndex] = {
82
+ ...part,
83
+ prompt_cache_breakpoint: { mode: "explicit" }
84
+ };
85
+ remaining--;
86
+ }
87
+ marked[index] = {
88
+ ...message,
89
+ content
90
+ };
91
+ }
92
+ return marked;
56
93
  }
57
94
  /**
58
95
  * Multipart content is only meaningful on user messages — for any other
@@ -1 +1 @@
1
- {"version":3,"file":"to-openai-messages.mjs","names":[],"sources":["../../../../../../../ai-openai/src/utils/to-openai-messages.ts"],"sourcesContent":["import { InvalidRequestError, type ContentPart, type Message } from \"@warlock.js/ai\";\nimport type OpenAI from \"openai\";\n\n/**\n * Convert vendor-neutral Message[] to OpenAI's chat message shape.\n * Handles the `tool` role (requires `tool_call_id`) and assistant messages\n * that carry `toolCalls` from a prior model response.\n *\n * Multipart `content` (a `ContentPart[]`) is mapped into OpenAI's user-message\n * content-parts shape: text becomes `{ type: \"text\", text }`, images become\n * `{ type: \"image_url\", image_url: { url } }` — with base64 sources rendered\n * as `data:` URLs inline.\n *\n * @example\n * const openaiMessages = toOpenAIMessages([\n * { role: \"user\", content: \"Hi\" },\n * { role: \"tool\", toolCallId: \"call_1\", content: '{\"ok\":true}' },\n * ]);\n *\n * @example\n * toOpenAIMessages([\n * { role: \"user\", content: [\n * { type: \"text\", text: \"What is this?\" },\n * { type: \"image\", source: { url: \"https://example.com/cat.jpg\" } },\n * ]},\n * ]);\n */\nexport function toOpenAIMessages(\n messages: Message[],\n): OpenAI.Chat.Completions.ChatCompletionMessageParam[] {\n return messages.map((m) => {\n if (m.role === \"tool\") {\n return {\n role: \"tool\",\n content: stringifyContent(m.content),\n tool_call_id: m.toolCallId ?? \"\",\n };\n }\n if (m.role === \"assistant\" && m.toolCalls && m.toolCalls.length > 0) {\n return {\n role: \"assistant\",\n content: stringifyContent(m.content),\n tool_calls: m.toolCalls.map((tc) => ({\n id: tc.id,\n type: \"function\" as const,\n function: { name: tc.name, arguments: JSON.stringify(tc.input ?? {}) },\n })),\n };\n }\n\n if (m.role === \"user\" && Array.isArray(m.content)) {\n return {\n role: \"user\",\n content: m.content.map(toOpenAIContentPart),\n };\n }\n\n return { role: m.role, content: stringifyContent(m.content) } as\n | OpenAI.Chat.Completions.ChatCompletionUserMessageParam\n | OpenAI.Chat.Completions.ChatCompletionSystemMessageParam\n | OpenAI.Chat.Completions.ChatCompletionAssistantMessageParam;\n });\n}\n\n/**\n * Multipart content is only meaningful on user messages — for any other\n * role (system / assistant text / tool), collapse a `ContentPart[]` to\n * its concatenated text so OpenAI's wire format stays valid. Plain\n * strings pass through unchanged.\n */\nfunction stringifyContent(content: string | ContentPart[]): string {\n if (typeof content === \"string\") {\n return content;\n }\n\n return content\n .filter((part): part is { type: \"text\"; text: string } => part.type === \"text\")\n .map((part) => part.text)\n .join(\"\");\n}\n\n/**\n * Map a resolved `ContentPart` to an OpenAI chat content part — one\n * branch per modality, each to its real wire shape:\n *\n * - `text` → `{ type: \"text\" }`.\n * - `image` → `{ type: \"image_url\" }` (remote URL, or a `data:` URL for\n * inlined base64 bytes).\n * - `pdf` → `{ type: \"file\", file: { file_data } }` (OpenAI document\n * input; base64 only — there is no remote-URL file source).\n * - `audio` → `{ type: \"input_audio\", input_audio: { data, format } }`\n * (base64 only; `wav` / `mp3` are the only formats OpenAI accepts).\n *\n * PDF and audio reach this point ONLY when the model declared the\n * matching capability (`openai.model({ name, pdf: true })` /\n * `{ audio: true }`) — the agent's modality gate throws upfront\n * otherwise, so capability and behavior stay in lockstep. A remote-URL\n * pdf/audio source raises a typed `InvalidRequestError` here rather\n * than a downstream provider fault.\n */\nfunction toOpenAIContentPart(part: ContentPart): OpenAI.Chat.Completions.ChatCompletionContentPart {\n if (part.type === \"text\") {\n return { type: \"text\", text: part.text };\n }\n\n if (part.type === \"image\") {\n const url =\n \"url\" in part.source\n ? part.source.url\n : `data:${part.source.mediaType};base64,${part.source.base64}`;\n\n return { type: \"image_url\", image_url: { url } };\n }\n\n if (part.type === \"pdf\") {\n if (\"url\" in part.source) {\n throw new InvalidRequestError(\n \"OpenAI chat completions cannot fetch a remote-URL PDF; supply base64 document bytes instead.\",\n );\n }\n\n return {\n type: \"file\",\n file: {\n filename: \"document.pdf\",\n file_data: `data:${part.source.mediaType};base64,${part.source.base64}`,\n },\n };\n }\n\n // Audio — the remaining `ContentPart` variant.\n if (\"url\" in part.source) {\n throw new InvalidRequestError(\n \"OpenAI chat completions cannot fetch remote-URL audio; supply base64 audio bytes instead.\",\n );\n }\n\n return {\n type: \"input_audio\",\n input_audio: {\n data: part.source.base64,\n format: toOpenAIAudioFormat(part.source.mediaType),\n },\n };\n}\n\n/**\n * Narrow a neutral audio media type to the two formats OpenAI's\n * `input_audio` accepts (`wav` / `mp3`). An unsupported type raises a\n * typed `InvalidRequestError` up front rather than a provider 400.\n */\nfunction toOpenAIAudioFormat(mediaType: string): \"wav\" | \"mp3\" {\n if (mediaType === \"audio/wav\" || mediaType === \"audio/x-wav\" || mediaType === \"audio/wave\") {\n return \"wav\";\n }\n\n if (mediaType === \"audio/mp3\" || mediaType === \"audio/mpeg\" || mediaType === \"audio/mpga\") {\n return \"mp3\";\n }\n\n throw new InvalidRequestError(\n `OpenAI input_audio supports only \"wav\" and \"mp3\"; got \"${mediaType}\".`,\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,SAAgB,iBACd,UACsD;CACtD,OAAO,SAAS,KAAK,MAAM;EACzB,IAAI,EAAE,SAAS,QACb,OAAO;GACL,MAAM;GACN,SAAS,iBAAiB,EAAE,OAAO;GACnC,cAAc,EAAE,cAAc;EAChC;EAEF,IAAI,EAAE,SAAS,eAAe,EAAE,aAAa,EAAE,UAAU,SAAS,GAChE,OAAO;GACL,MAAM;GACN,SAAS,iBAAiB,EAAE,OAAO;GACnC,YAAY,EAAE,UAAU,KAAK,QAAQ;IACnC,IAAI,GAAG;IACP,MAAM;IACN,UAAU;KAAE,MAAM,GAAG;KAAM,WAAW,KAAK,UAAU,GAAG,SAAS,CAAC,CAAC;IAAE;GACvE,EAAE;EACJ;EAGF,IAAI,EAAE,SAAS,UAAU,MAAM,QAAQ,EAAE,OAAO,GAC9C,OAAO;GACL,MAAM;GACN,SAAS,EAAE,QAAQ,IAAI,mBAAmB;EAC5C;EAGF,OAAO;GAAE,MAAM,EAAE;GAAM,SAAS,iBAAiB,EAAE,OAAO;EAAE;CAI9D,CAAC;AACH;;;;;;;AAQA,SAAS,iBAAiB,SAAyC;CACjE,IAAI,OAAO,YAAY,UACrB,OAAO;CAGT,OAAO,QACJ,QAAQ,SAAiD,KAAK,SAAS,MAAM,CAAC,CAC9E,KAAK,SAAS,KAAK,IAAI,CAAC,CACxB,KAAK,EAAE;AACZ;;;;;;;;;;;;;;;;;;;;AAqBA,SAAS,oBAAoB,MAAsE;CACjG,IAAI,KAAK,SAAS,QAChB,OAAO;EAAE,MAAM;EAAQ,MAAM,KAAK;CAAK;CAGzC,IAAI,KAAK,SAAS,SAMhB,OAAO;EAAE,MAAM;EAAa,WAAW,EAAE,KAJvC,SAAS,KAAK,SACV,KAAK,OAAO,MACZ,QAAQ,KAAK,OAAO,UAAU,UAAU,KAAK,OAAO,SAEb;CAAE;CAGjD,IAAI,KAAK,SAAS,OAAO;EACvB,IAAI,SAAS,KAAK,QAChB,MAAM,IAAI,oBACR,8FACF;EAGF,OAAO;GACL,MAAM;GACN,MAAM;IACJ,UAAU;IACV,WAAW,QAAQ,KAAK,OAAO,UAAU,UAAU,KAAK,OAAO;GACjE;EACF;CACF;CAGA,IAAI,SAAS,KAAK,QAChB,MAAM,IAAI,oBACR,2FACF;CAGF,OAAO;EACL,MAAM;EACN,aAAa;GACX,MAAM,KAAK,OAAO;GAClB,QAAQ,oBAAoB,KAAK,OAAO,SAAS;EACnD;CACF;AACF;;;;;;AAOA,SAAS,oBAAoB,WAAkC;CAC7D,IAAI,cAAc,eAAe,cAAc,iBAAiB,cAAc,cAC5E,OAAO;CAGT,IAAI,cAAc,eAAe,cAAc,gBAAgB,cAAc,cAC3E,OAAO;CAGT,MAAM,IAAI,oBACR,0DAA0D,UAAU,GACtE;AACF"}
1
+ {"version":3,"file":"to-openai-messages.mjs","names":[],"sources":["../../../../../../../ai-openai/src/utils/to-openai-messages.ts"],"sourcesContent":["import { InvalidRequestError, type ContentPart, type Message } from \"@warlock.js/ai\";\nimport type OpenAI from \"openai\";\n\n/**\n * Convert vendor-neutral Message[] to OpenAI's chat message shape.\n * Handles the `tool` role (requires `tool_call_id`) and assistant messages\n * that carry `toolCalls` from a prior model response.\n *\n * Multipart `content` (a `ContentPart[]`) is mapped into OpenAI's user-message\n * content-parts shape: text becomes `{ type: \"text\", text }`, images become\n * `{ type: \"image_url\", image_url: { url } }` — with base64 sources rendered\n * as `data:` URLs inline.\n *\n * @example\n * const openaiMessages = toOpenAIMessages([\n * { role: \"user\", content: \"Hi\" },\n * { role: \"tool\", toolCallId: \"call_1\", content: '{\"ok\":true}' },\n * ]);\n *\n * @example\n * toOpenAIMessages([\n * { role: \"user\", content: [\n * { type: \"text\", text: \"What is this?\" },\n * { type: \"image\", source: { url: \"https://example.com/cat.jpg\" } },\n * ]},\n * ]);\n */\nexport function toOpenAIMessages(\n messages: Message[],\n cacheBreakpoints: number = 0,\n): OpenAI.Chat.Completions.ChatCompletionMessageParam[] {\n const mapped = messages.map((m) => {\n if (m.role === \"tool\") {\n return {\n role: \"tool\",\n content: stringifyContent(m.content),\n tool_call_id: m.toolCallId ?? \"\",\n } satisfies OpenAI.Chat.Completions.ChatCompletionToolMessageParam;\n }\n if (m.role === \"assistant\" && m.toolCalls && m.toolCalls.length > 0) {\n return {\n role: \"assistant\",\n content: stringifyContent(m.content),\n tool_calls: m.toolCalls.map((tc) => ({\n id: tc.id,\n type: \"function\" as const,\n function: { name: tc.name, arguments: JSON.stringify(tc.input ?? {}) },\n })),\n } satisfies OpenAI.Chat.Completions.ChatCompletionAssistantMessageParam;\n }\n\n if (m.role === \"user\" && Array.isArray(m.content)) {\n return {\n role: \"user\",\n content: m.content.map(toOpenAIContentPart),\n } satisfies OpenAI.Chat.Completions.ChatCompletionUserMessageParam;\n }\n\n return { role: m.role, content: stringifyContent(m.content) } as\n | OpenAI.Chat.Completions.ChatCompletionUserMessageParam\n | OpenAI.Chat.Completions.ChatCompletionSystemMessageParam\n | OpenAI.Chat.Completions.ChatCompletionAssistantMessageParam;\n });\n\n return addPromptCacheBreakpoints(mapped, cacheBreakpoints);\n}\n\n/** Place explicit markers on the last requested user-content parts (maximum four). */\nfunction addPromptCacheBreakpoints(\n messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[],\n requested: number,\n): OpenAI.Chat.Completions.ChatCompletionMessageParam[] {\n let remaining = Math.min(4, Math.max(0, Math.floor(requested)));\n if (remaining === 0) return messages;\n\n const marked = [...messages];\n for (let index = marked.length - 1; index >= 0 && remaining > 0; index--) {\n const message = marked[index];\n if (message === undefined || message.role !== \"user\") continue;\n\n if (typeof message.content === \"string\") {\n marked[index] = {\n ...message,\n content: [\n { type: \"text\", text: message.content, prompt_cache_breakpoint: { mode: \"explicit\" } },\n ],\n };\n remaining--;\n continue;\n }\n\n const content = [...message.content];\n for (let partIndex = content.length - 1; partIndex >= 0 && remaining > 0; partIndex--) {\n const part = content[partIndex];\n if (part === undefined) continue;\n content[partIndex] = { ...part, prompt_cache_breakpoint: { mode: \"explicit\" } };\n remaining--;\n }\n marked[index] = { ...message, content };\n }\n\n return marked;\n}\n\n/**\n * Multipart content is only meaningful on user messages — for any other\n * role (system / assistant text / tool), collapse a `ContentPart[]` to\n * its concatenated text so OpenAI's wire format stays valid. Plain\n * strings pass through unchanged.\n */\nfunction stringifyContent(content: string | ContentPart[]): string {\n if (typeof content === \"string\") {\n return content;\n }\n\n return content\n .filter((part): part is { type: \"text\"; text: string } => part.type === \"text\")\n .map((part) => part.text)\n .join(\"\");\n}\n\n/**\n * Map a resolved `ContentPart` to an OpenAI chat content part — one\n * branch per modality, each to its real wire shape:\n *\n * - `text` → `{ type: \"text\" }`.\n * - `image` → `{ type: \"image_url\" }` (remote URL, or a `data:` URL for\n * inlined base64 bytes).\n * - `pdf` → `{ type: \"file\", file: { file_data } }` (OpenAI document\n * input; base64 only — there is no remote-URL file source).\n * - `audio` → `{ type: \"input_audio\", input_audio: { data, format } }`\n * (base64 only; `wav` / `mp3` are the only formats OpenAI accepts).\n *\n * PDF and audio reach this point ONLY when the model declared the\n * matching capability (`openai.model({ name, pdf: true })` /\n * `{ audio: true }`) — the agent's modality gate throws upfront\n * otherwise, so capability and behavior stay in lockstep. A remote-URL\n * pdf/audio source raises a typed `InvalidRequestError` here rather\n * than a downstream provider fault.\n */\nfunction toOpenAIContentPart(part: ContentPart): OpenAI.Chat.Completions.ChatCompletionContentPart {\n if (part.type === \"text\") {\n return { type: \"text\", text: part.text };\n }\n\n if (part.type === \"image\") {\n const url =\n \"url\" in part.source\n ? part.source.url\n : `data:${part.source.mediaType};base64,${part.source.base64}`;\n\n return { type: \"image_url\", image_url: { url } };\n }\n\n if (part.type === \"pdf\") {\n if (\"url\" in part.source) {\n throw new InvalidRequestError(\n \"OpenAI chat completions cannot fetch a remote-URL PDF; supply base64 document bytes instead.\",\n );\n }\n\n return {\n type: \"file\",\n file: {\n filename: \"document.pdf\",\n file_data: `data:${part.source.mediaType};base64,${part.source.base64}`,\n },\n };\n }\n\n // Audio — the remaining `ContentPart` variant.\n if (\"url\" in part.source) {\n throw new InvalidRequestError(\n \"OpenAI chat completions cannot fetch remote-URL audio; supply base64 audio bytes instead.\",\n );\n }\n\n return {\n type: \"input_audio\",\n input_audio: {\n data: part.source.base64,\n format: toOpenAIAudioFormat(part.source.mediaType),\n },\n };\n}\n\n/**\n * Narrow a neutral audio media type to the two formats OpenAI's\n * `input_audio` accepts (`wav` / `mp3`). An unsupported type raises a\n * typed `InvalidRequestError` up front rather than a provider 400.\n */\nfunction toOpenAIAudioFormat(mediaType: string): \"wav\" | \"mp3\" {\n if (mediaType === \"audio/wav\" || mediaType === \"audio/x-wav\" || mediaType === \"audio/wave\") {\n return \"wav\";\n }\n\n if (mediaType === \"audio/mp3\" || mediaType === \"audio/mpeg\" || mediaType === \"audio/mpga\") {\n return \"mp3\";\n }\n\n throw new InvalidRequestError(\n `OpenAI input_audio supports only \"wav\" and \"mp3\"; got \"${mediaType}\".`,\n );\n}\n"],"mappings":"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"}
@@ -75,6 +75,7 @@ function toShape(thrown) {
75
75
  code: thrown.code,
76
76
  message: thrown.message,
77
77
  type: thrown.type,
78
+ param: thrown.param,
78
79
  headers: thrown.headers,
79
80
  name: thrown.name
80
81
  };
@@ -85,6 +86,7 @@ function toShape(thrown) {
85
86
  code: typeof raw.code === "string" ? raw.code : void 0,
86
87
  message: typeof raw.message === "string" ? raw.message : void 0,
87
88
  type: typeof raw.type === "string" ? raw.type : void 0,
89
+ param: typeof raw.param === "string" ? raw.param : void 0,
88
90
  headers: typeof raw.headers === "object" && raw.headers !== null ? raw.headers : void 0,
89
91
  name: typeof raw.name === "string" ? raw.name : void 0
90
92
  };
@@ -114,19 +116,22 @@ function buildContext(thrown, shape) {
114
116
  if (shape.status !== void 0) context.status = shape.status;
115
117
  if (shape.code) context.code = shape.code;
116
118
  if (shape.type) context.type = shape.type;
119
+ if (shape.param) context.param = shape.param;
117
120
  const requestId = readRequestId(thrown);
118
121
  if (requestId) context.requestId = requestId;
119
122
  return context;
120
123
  }
121
124
  /**
122
- * OpenAI puts the request id on `APIError.request_id`. Extract
123
- * defensively — both camel and snake keys exist across SDK versions.
125
+ * The request id OpenAI returned. Current SDKs expose it as
126
+ * `APIError.requestID`; older ones and hand-rolled errors use
127
+ * `request_id` / `requestId`. Extract defensively.
124
128
  */
125
129
  function readRequestId(thrown) {
126
130
  if (typeof thrown !== "object" || thrown === null) return;
127
131
  const raw = thrown;
128
132
  if (typeof raw.request_id === "string") return raw.request_id;
129
133
  if (typeof raw.requestId === "string") return raw.requestId;
134
+ if (typeof raw.requestID === "string") return raw.requestID;
130
135
  }
131
136
  /**
132
137
  * Parse the `Retry-After` response header (seconds per HTTP spec)
@@ -1 +1 @@
1
- {"version":3,"file":"wrap-openai-error.mjs","names":[],"sources":["../../../../../../../ai-openai/src/utils/wrap-openai-error.ts"],"sourcesContent":["import {\n AIError,\n ContentFilterError,\n ContextLengthExceededError,\n InvalidRequestError,\n ProviderAuthError,\n ProviderError,\n ProviderRateLimitError,\n ProviderTimeoutError,\n QuotaExceededError,\n} from \"@warlock.js/ai\";\nimport OpenAI from \"openai\";\n\n/**\n * Raw-error fields the wrapper reads off an OpenAI SDK error.\n *\n * `APIError` exposes `status`, `code`, `message`, `type`, `headers` —\n * we duck-type because wrapped retries, proxied errors, and custom\n * error subclasses sometimes lose the `instanceof` relationship.\n */\ntype OpenAIErrorShape = {\n status?: number;\n code?: string | null;\n message?: string;\n type?: string | null;\n headers?: Record<string, string> | undefined;\n name?: string;\n};\n\n/**\n * Wrap any thrown value caught inside the OpenAI adapter into the\n * appropriate `@warlock.js/ai` `AIError` subclass.\n *\n * **Dispatch strategy.** Prefers `APIError.code` when present (stable\n * machine identifier across SDK versions), falls back to `status` when\n * `code` is missing (common with proxied deployments that strip the\n * field). Name-based detection (`APIConnectionTimeoutError`) catches\n * transport-layer errors that never produced an HTTP response.\n *\n * `AIError` instances are returned unchanged — callers can pass the\n * error through `try/catch/throw wrap(e)` pipelines without accidental\n * double-wrapping.\n *\n * @example\n * try {\n * return await this.client.chat.completions.create(...);\n * } catch (thrown) {\n * throw wrapOpenAIError(thrown);\n * }\n */\nexport function wrapOpenAIError(thrown: unknown): AIError {\n if (thrown instanceof AIError) {\n return thrown;\n }\n\n const shape = toShape(thrown);\n const context = buildContext(thrown, shape);\n const message = shape.message ?? (thrown instanceof Error ? thrown.message : String(thrown));\n\n if (isTimeout(thrown, shape)) {\n return new ProviderTimeoutError(message, { cause: thrown, context });\n }\n\n if (shape.status === 401 || shape.code === \"invalid_api_key\") {\n return new ProviderAuthError(message, { cause: thrown, context });\n }\n\n if (shape.code === \"insufficient_quota\") {\n return new QuotaExceededError(message, { cause: thrown, context });\n }\n\n if (shape.status === 429 || shape.code === \"rate_limit_exceeded\") {\n return new ProviderRateLimitError(message, {\n cause: thrown,\n context,\n retryAfter: parseRetryAfter(shape.headers),\n });\n }\n\n if (shape.code === \"context_length_exceeded\") {\n return new ContextLengthExceededError(message, { cause: thrown, context });\n }\n\n if (shape.code === \"content_filter\") {\n return new ContentFilterError(message, {\n cause: thrown,\n context,\n reason: message,\n });\n }\n\n if (typeof shape.status === \"number\" && shape.status >= 400 && shape.status < 500) {\n return new InvalidRequestError(message, { cause: thrown, context });\n }\n\n return new ProviderError(message, { cause: thrown, context });\n}\n\n/**\n * Read the raw error shape without depending on `instanceof APIError`\n * — some consumers wrap the SDK, and proxies sometimes strip the\n * prototype chain. Duck-typing on the visible fields is resilient to\n * both.\n */\nfunction toShape(thrown: unknown): OpenAIErrorShape {\n if (thrown instanceof OpenAI.APIError) {\n return {\n status: thrown.status,\n code: thrown.code,\n message: thrown.message,\n type: thrown.type,\n headers: thrown.headers as Record<string, string> | undefined,\n name: thrown.name,\n };\n }\n\n if (typeof thrown === \"object\" && thrown !== null) {\n const raw = thrown as Record<string, unknown>;\n\n return {\n status: typeof raw.status === \"number\" ? raw.status : undefined,\n code: typeof raw.code === \"string\" ? raw.code : undefined,\n message: typeof raw.message === \"string\" ? raw.message : undefined,\n type: typeof raw.type === \"string\" ? raw.type : undefined,\n headers:\n typeof raw.headers === \"object\" && raw.headers !== null\n ? (raw.headers as Record<string, string>)\n : undefined,\n name: typeof raw.name === \"string\" ? raw.name : undefined,\n };\n }\n\n return {};\n}\n\n/**\n * Decide whether the thrown value represents a timeout. OpenAI's SDK\n * throws `APIConnectionTimeoutError` for transport-level timeouts, and\n * Node surfaces `ETIMEDOUT` / `ECONNABORTED` on the lower socket\n * layer. Either signal counts.\n */\nfunction isTimeout(thrown: unknown, shape: OpenAIErrorShape): boolean {\n if (thrown instanceof OpenAI.APIConnectionTimeoutError) {\n return true;\n }\n\n if (shape.name === \"APIConnectionTimeoutError\") {\n return true;\n }\n\n if (shape.code === \"ETIMEDOUT\" || shape.code === \"ECONNABORTED\") {\n return true;\n }\n\n return false;\n}\n\n/**\n * Attach the raw diagnostic fields to `error.context` so consumers\n * have everything the provider surfaced without each subclass having\n * to redeclare them. Never includes `cause` — that lives on\n * `error.cause`.\n */\nfunction buildContext(\n thrown: unknown,\n shape: OpenAIErrorShape,\n): Record<string, unknown> {\n const context: Record<string, unknown> = {};\n\n if (shape.status !== undefined) {\n context.status = shape.status;\n }\n\n if (shape.code) {\n context.code = shape.code;\n }\n\n if (shape.type) {\n context.type = shape.type;\n }\n\n const requestId = readRequestId(thrown);\n\n if (requestId) {\n context.requestId = requestId;\n }\n\n return context;\n}\n\n/**\n * OpenAI puts the request id on `APIError.request_id`. Extract\n * defensively — both camel and snake keys exist across SDK versions.\n */\nfunction readRequestId(thrown: unknown): string | undefined {\n if (typeof thrown !== \"object\" || thrown === null) {\n return undefined;\n }\n\n const raw = thrown as Record<string, unknown>;\n\n if (typeof raw.request_id === \"string\") {\n return raw.request_id;\n }\n\n if (typeof raw.requestId === \"string\") {\n return raw.requestId;\n }\n\n return undefined;\n}\n\n/**\n * Parse the `Retry-After` response header (seconds per HTTP spec)\n * into milliseconds so consumers can feed it straight to `setTimeout`.\n * Returns `undefined` when missing or unparseable.\n */\nfunction parseRetryAfter(headers: Record<string, string> | undefined): number | undefined {\n if (!headers) {\n return undefined;\n }\n\n const raw = headers[\"retry-after\"] ?? headers[\"Retry-After\"];\n\n if (!raw) {\n return undefined;\n }\n\n const seconds = Number(raw);\n\n if (!Number.isFinite(seconds) || seconds < 0) {\n return undefined;\n }\n\n return Math.round(seconds * 1000);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"wrap-openai-error.mjs","names":[],"sources":["../../../../../../../ai-openai/src/utils/wrap-openai-error.ts"],"sourcesContent":["import {\n AIError,\n ContentFilterError,\n ContextLengthExceededError,\n InvalidRequestError,\n ProviderAuthError,\n ProviderError,\n ProviderRateLimitError,\n ProviderTimeoutError,\n QuotaExceededError,\n} from \"@warlock.js/ai\";\nimport OpenAI from \"openai\";\n\n/**\n * Raw-error fields the wrapper reads off an OpenAI SDK error.\n *\n * `APIError` exposes `status`, `code`, `message`, `type`, `headers` —\n * we duck-type because wrapped retries, proxied errors, and custom\n * error subclasses sometimes lose the `instanceof` relationship.\n */\ntype OpenAIErrorShape = {\n status?: number;\n code?: string | null;\n message?: string;\n type?: string | null;\n /** The request parameter the provider rejected, e.g. `max_tokens`. */\n param?: string | null;\n headers?: Record<string, string> | undefined;\n name?: string;\n};\n\n/**\n * Wrap any thrown value caught inside the OpenAI adapter into the\n * appropriate `@warlock.js/ai` `AIError` subclass.\n *\n * **Dispatch strategy.** Prefers `APIError.code` when present (stable\n * machine identifier across SDK versions), falls back to `status` when\n * `code` is missing (common with proxied deployments that strip the\n * field). Name-based detection (`APIConnectionTimeoutError`) catches\n * transport-layer errors that never produced an HTTP response.\n *\n * `AIError` instances are returned unchanged — callers can pass the\n * error through `try/catch/throw wrap(e)` pipelines without accidental\n * double-wrapping.\n *\n * @example\n * try {\n * return await this.client.chat.completions.create(...);\n * } catch (thrown) {\n * throw wrapOpenAIError(thrown);\n * }\n */\nexport function wrapOpenAIError(thrown: unknown): AIError {\n if (thrown instanceof AIError) {\n return thrown;\n }\n\n const shape = toShape(thrown);\n const context = buildContext(thrown, shape);\n const message = shape.message ?? (thrown instanceof Error ? thrown.message : String(thrown));\n\n if (isTimeout(thrown, shape)) {\n return new ProviderTimeoutError(message, { cause: thrown, context });\n }\n\n if (shape.status === 401 || shape.code === \"invalid_api_key\") {\n return new ProviderAuthError(message, { cause: thrown, context });\n }\n\n if (shape.code === \"insufficient_quota\") {\n return new QuotaExceededError(message, { cause: thrown, context });\n }\n\n if (shape.status === 429 || shape.code === \"rate_limit_exceeded\") {\n return new ProviderRateLimitError(message, {\n cause: thrown,\n context,\n retryAfter: parseRetryAfter(shape.headers),\n });\n }\n\n if (shape.code === \"context_length_exceeded\") {\n return new ContextLengthExceededError(message, { cause: thrown, context });\n }\n\n if (shape.code === \"content_filter\") {\n return new ContentFilterError(message, {\n cause: thrown,\n context,\n reason: message,\n });\n }\n\n if (typeof shape.status === \"number\" && shape.status >= 400 && shape.status < 500) {\n return new InvalidRequestError(message, { cause: thrown, context });\n }\n\n return new ProviderError(message, { cause: thrown, context });\n}\n\n/**\n * Read the raw error shape without depending on `instanceof APIError`\n * — some consumers wrap the SDK, and proxies sometimes strip the\n * prototype chain. Duck-typing on the visible fields is resilient to\n * both.\n */\nfunction toShape(thrown: unknown): OpenAIErrorShape {\n if (thrown instanceof OpenAI.APIError) {\n return {\n status: thrown.status,\n code: thrown.code,\n message: thrown.message,\n type: thrown.type,\n param: thrown.param,\n headers: thrown.headers as Record<string, string> | undefined,\n name: thrown.name,\n };\n }\n\n if (typeof thrown === \"object\" && thrown !== null) {\n const raw = thrown as Record<string, unknown>;\n\n return {\n status: typeof raw.status === \"number\" ? raw.status : undefined,\n code: typeof raw.code === \"string\" ? raw.code : undefined,\n message: typeof raw.message === \"string\" ? raw.message : undefined,\n type: typeof raw.type === \"string\" ? raw.type : undefined,\n param: typeof raw.param === \"string\" ? raw.param : undefined,\n headers:\n typeof raw.headers === \"object\" && raw.headers !== null\n ? (raw.headers as Record<string, string>)\n : undefined,\n name: typeof raw.name === \"string\" ? raw.name : undefined,\n };\n }\n\n return {};\n}\n\n/**\n * Decide whether the thrown value represents a timeout. OpenAI's SDK\n * throws `APIConnectionTimeoutError` for transport-level timeouts, and\n * Node surfaces `ETIMEDOUT` / `ECONNABORTED` on the lower socket\n * layer. Either signal counts.\n */\nfunction isTimeout(thrown: unknown, shape: OpenAIErrorShape): boolean {\n if (thrown instanceof OpenAI.APIConnectionTimeoutError) {\n return true;\n }\n\n if (shape.name === \"APIConnectionTimeoutError\") {\n return true;\n }\n\n if (shape.code === \"ETIMEDOUT\" || shape.code === \"ECONNABORTED\") {\n return true;\n }\n\n return false;\n}\n\n/**\n * Attach the raw diagnostic fields to `error.context` so consumers\n * have everything the provider surfaced without each subclass having\n * to redeclare them. Never includes `cause` — that lives on\n * `error.cause`.\n */\nfunction buildContext(\n thrown: unknown,\n shape: OpenAIErrorShape,\n): Record<string, unknown> {\n const context: Record<string, unknown> = {};\n\n if (shape.status !== undefined) {\n context.status = shape.status;\n }\n\n if (shape.code) {\n context.code = shape.code;\n }\n\n if (shape.type) {\n context.type = shape.type;\n }\n\n if (shape.param) {\n context.param = shape.param;\n }\n\n const requestId = readRequestId(thrown);\n\n if (requestId) {\n context.requestId = requestId;\n }\n\n return context;\n}\n\n/**\n * The request id OpenAI returned. Current SDKs expose it as\n * `APIError.requestID`; older ones and hand-rolled errors use\n * `request_id` / `requestId`. Extract defensively.\n */\nfunction readRequestId(thrown: unknown): string | undefined {\n if (typeof thrown !== \"object\" || thrown === null) {\n return undefined;\n }\n\n const raw = thrown as Record<string, unknown>;\n\n if (typeof raw.request_id === \"string\") {\n return raw.request_id;\n }\n\n if (typeof raw.requestId === \"string\") {\n return raw.requestId;\n }\n\n if (typeof raw.requestID === \"string\") {\n return raw.requestID;\n }\n\n return undefined;\n}\n\n/**\n * Parse the `Retry-After` response header (seconds per HTTP spec)\n * into milliseconds so consumers can feed it straight to `setTimeout`.\n * Returns `undefined` when missing or unparseable.\n */\nfunction parseRetryAfter(headers: Record<string, string> | undefined): number | undefined {\n if (!headers) {\n return undefined;\n }\n\n const raw = headers[\"retry-after\"] ?? headers[\"Retry-After\"];\n\n if (!raw) {\n return undefined;\n }\n\n const seconds = Number(raw);\n\n if (!Number.isFinite(seconds) || seconds < 0) {\n return undefined;\n }\n\n return Math.round(seconds * 1000);\n}\n"],"mappings":"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"}
package/llms-full.txt CHANGED
@@ -53,7 +53,7 @@ Returns a `ModelContract` you pass straight into `ai.agent({ model })`.
53
53
  | `structuredOutput` | `true`, unless `responseFormat` is set to `"json_object"` or `"text"` (loose modes) — then `false`. |
54
54
  | `vision` | Inferred from model name. `true` for `gpt-4o*`, `gpt-4-turbo*`, `gpt-4.1*`, `o1*`, `o3*`, `chatgpt-4o*`; `false` otherwise. |
55
55
  | `reasoning` | Inferred from model name. `true` for the o-series (`o1*`, `o3*`, `o4*`) and the `gpt-5*` family; `false` otherwise. Drives whether `reasoning_effort` is forwarded. |
56
- | `promptCaching` | Always `true`. OpenAI caches long prompt prefixes automatically and reports hits via `usage.cachedTokens` — there are no caller-supplied write breakpoints. |
56
+ | `promptCaching` | `true` for the direct OpenAI provider. It supports automatic cache hits plus explicit content-part write breakpoints; OpenAI-compatible wrapper providers leave the OpenAI-only fields off. |
57
57
  | `pdf` | `false` by default (opt-in `.model({ pdf: true })`). OpenAI accepts PDF `file` parts only on specific models (the `gpt-4o` family) — honest off until you set it. |
58
58
  | `audio` | `false` by default (opt-in `.model({ audio: true })`). Only the `gpt-4o-audio-preview` family accepts `input_audio`. |
59
59
 
@@ -157,6 +157,14 @@ await model.complete(messages, { tools, reasoning: { effort: "high" } }); // you
157
157
  await model.complete(messages, { tools, reasoning: { effort: "none" } }); // same effect as the default, explicit
158
158
  ```
159
159
 
160
+ The gpt-6 models (`gpt-6-astra` / `gpt-6-sol` / `gpt-6-luna`) are hard-400 cases, verified live on `gpt-6-sol`:
161
+
162
+ ```
163
+ 400 Function tools with reasoning_effort are not supported for gpt-6-sol in /v1/chat/completions. To use function tools, use /v1/responses or set reasoning_effort to 'none'.
164
+ ```
165
+
166
+ Tools work with reasoning off (the adapter's default when tools are present and no reasoning is set). Responses API support, planned for 5.27, lifts this.
167
+
160
168
  Calling **without** `tools` is unaffected — `reasoning_effort` is still omitted by default in that case (provider default reasoning, nothing to unblock). Trade-off of the `"none"` default: you lose reasoning on that call. For tool-heavy agent work (function calls + good replies rather than deep analysis) this is the right call — it's the difference between empty replies and working ones. When you need reasoning **and** tools together, use the Responses API (planned).
161
169
 
162
170
  ## Token usage — what's reported
@@ -171,7 +179,21 @@ Calling **without** `tools` is unaffected — `reasoning_effort` is still omitte
171
179
 
172
180
  Both `cachedTokens` and `reasoningTokens` are omitted (not set to `0`) when the provider reports zero, so non-reasoning / uncached calls keep the lean `{ input, output, total }` shape. Reported identically on `complete()` and the streaming `done` event.
173
181
 
174
- `cacheControl` write breakpoints (`ModelCallOptions.cacheControl`) are a **no-op** — OpenAI has no caller-driven cache marker on the Chat Completions wire. The read-side `cachedTokens` accounting above works without any caller action.
182
+ ### Explicit prompt caching (direct OpenAI only)
183
+
184
+ Chat Completions exposes OpenAI-specific cache controls on current SDK types. Configure a stable cache bucket and (when needed) its maximum retention policy per model:
185
+
186
+ ```ts
187
+ const model = openai.model({
188
+ name: "gpt-5.6",
189
+ promptCacheKey: "tenant:acme",
190
+ promptCacheRetention: "24h", // or "in_memory"
191
+ });
192
+ ```
193
+
194
+ `promptCacheKey` improves matching for related requests. `promptCacheRetention` is OpenAI's maximum policy (`"in_memory" | "24h"`). `cacheControl.breakpoints` maps to `prompt_cache_breakpoint: { mode: "explicit" }` on the final user content parts (up to OpenAI's four-request limit); use it when you need to pin a reusable prefix rather than rely only on OpenAI's implicit breakpoint.
195
+
196
+ These fields are sent only when the SDK provider label is exactly `"openai"`. `DeepSeekSDK`, `GroqSDK`, `XaiSDK`, and `MistralSDK` label their wrapped endpoint differently, so their requests omit every OpenAI-only cache field even though they reuse this transport. Read-side `usage.cachedTokens` accounting remains available whenever the upstream reports it.
175
197
 
176
198
  ## Token counting
177
199
 
package/package.json CHANGED
@@ -13,13 +13,13 @@
13
13
  "url": "https://github.com/warlockjs/ai-openai"
14
14
  },
15
15
  "dependencies": {
16
- "openai": "^7.4.0",
17
- "@warlock.js/logger": "5.24.0"
16
+ "openai": "^7.23.0",
17
+ "@warlock.js/logger": "5.26.0"
18
18
  },
19
19
  "peerDependencies": {
20
- "@warlock.js/ai": "5.24.0"
20
+ "@warlock.js/ai": "5.26.0"
21
21
  },
22
- "version": "5.24.0",
22
+ "version": "5.26.0",
23
23
  "main": "./cjs/index.cjs",
24
24
  "module": "./esm/index.mjs",
25
25
  "types": "./esm/index.d.mts",
@@ -45,7 +45,7 @@ Returns a `ModelContract` you pass straight into `ai.agent({ model })`.
45
45
  | `structuredOutput` | `true`, unless `responseFormat` is set to `"json_object"` or `"text"` (loose modes) — then `false`. |
46
46
  | `vision` | Inferred from model name. `true` for `gpt-4o*`, `gpt-4-turbo*`, `gpt-4.1*`, `o1*`, `o3*`, `chatgpt-4o*`; `false` otherwise. |
47
47
  | `reasoning` | Inferred from model name. `true` for the o-series (`o1*`, `o3*`, `o4*`) and the `gpt-5*` family; `false` otherwise. Drives whether `reasoning_effort` is forwarded. |
48
- | `promptCaching` | Always `true`. OpenAI caches long prompt prefixes automatically and reports hits via `usage.cachedTokens` — there are no caller-supplied write breakpoints. |
48
+ | `promptCaching` | `true` for the direct OpenAI provider. It supports automatic cache hits plus explicit content-part write breakpoints; OpenAI-compatible wrapper providers leave the OpenAI-only fields off. |
49
49
  | `pdf` | `false` by default (opt-in `.model({ pdf: true })`). OpenAI accepts PDF `file` parts only on specific models (the `gpt-4o` family) — honest off until you set it. |
50
50
  | `audio` | `false` by default (opt-in `.model({ audio: true })`). Only the `gpt-4o-audio-preview` family accepts `input_audio`. |
51
51
 
@@ -149,6 +149,14 @@ await model.complete(messages, { tools, reasoning: { effort: "high" } }); // you
149
149
  await model.complete(messages, { tools, reasoning: { effort: "none" } }); // same effect as the default, explicit
150
150
  ```
151
151
 
152
+ The gpt-6 models (`gpt-6-astra` / `gpt-6-sol` / `gpt-6-luna`) are hard-400 cases, verified live on `gpt-6-sol`:
153
+
154
+ ```
155
+ 400 Function tools with reasoning_effort are not supported for gpt-6-sol in /v1/chat/completions. To use function tools, use /v1/responses or set reasoning_effort to 'none'.
156
+ ```
157
+
158
+ Tools work with reasoning off (the adapter's default when tools are present and no reasoning is set). Responses API support, planned for 5.27, lifts this.
159
+
152
160
  Calling **without** `tools` is unaffected — `reasoning_effort` is still omitted by default in that case (provider default reasoning, nothing to unblock). Trade-off of the `"none"` default: you lose reasoning on that call. For tool-heavy agent work (function calls + good replies rather than deep analysis) this is the right call — it's the difference between empty replies and working ones. When you need reasoning **and** tools together, use the Responses API (planned).
153
161
 
154
162
  ## Token usage — what's reported
@@ -163,7 +171,21 @@ Calling **without** `tools` is unaffected — `reasoning_effort` is still omitte
163
171
 
164
172
  Both `cachedTokens` and `reasoningTokens` are omitted (not set to `0`) when the provider reports zero, so non-reasoning / uncached calls keep the lean `{ input, output, total }` shape. Reported identically on `complete()` and the streaming `done` event.
165
173
 
166
- `cacheControl` write breakpoints (`ModelCallOptions.cacheControl`) are a **no-op** — OpenAI has no caller-driven cache marker on the Chat Completions wire. The read-side `cachedTokens` accounting above works without any caller action.
174
+ ### Explicit prompt caching (direct OpenAI only)
175
+
176
+ Chat Completions exposes OpenAI-specific cache controls on current SDK types. Configure a stable cache bucket and (when needed) its maximum retention policy per model:
177
+
178
+ ```ts
179
+ const model = openai.model({
180
+ name: "gpt-5.6",
181
+ promptCacheKey: "tenant:acme",
182
+ promptCacheRetention: "24h", // or "in_memory"
183
+ });
184
+ ```
185
+
186
+ `promptCacheKey` improves matching for related requests. `promptCacheRetention` is OpenAI's maximum policy (`"in_memory" | "24h"`). `cacheControl.breakpoints` maps to `prompt_cache_breakpoint: { mode: "explicit" }` on the final user content parts (up to OpenAI's four-request limit); use it when you need to pin a reusable prefix rather than rely only on OpenAI's implicit breakpoint.
187
+
188
+ These fields are sent only when the SDK provider label is exactly `"openai"`. `DeepSeekSDK`, `GroqSDK`, `XaiSDK`, and `MistralSDK` label their wrapped endpoint differently, so their requests omit every OpenAI-only cache field even though they reuse this transport. Read-side `usage.cachedTokens` accounting remains available whenever the upstream reports it.
167
189
 
168
190
  ## Token counting
169
191