@warlock.js/ai-openai 5.30.0 → 5.31.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 (44) hide show
  1. package/CHANGELOG.md +24 -1
  2. package/README.md +3 -3
  3. package/cjs/index.cjs +416 -48
  4. package/cjs/index.cjs.map +1 -1
  5. package/esm/config.type.d.mts +5 -3
  6. package/esm/config.type.d.mts.map +1 -1
  7. package/esm/embedder.d.mts.map +1 -1
  8. package/esm/embedder.mjs.map +1 -1
  9. package/esm/index.d.mts +3 -1
  10. package/esm/index.mjs +2 -1
  11. package/esm/known-pricing.d.mts +35 -0
  12. package/esm/known-pricing.d.mts.map +1 -0
  13. package/esm/known-pricing.mjs +117 -0
  14. package/esm/known-pricing.mjs.map +1 -0
  15. package/esm/model.mjs +75 -23
  16. package/esm/model.mjs.map +1 -1
  17. package/esm/provider-options.d.mts +8 -0
  18. package/esm/provider-options.d.mts.map +1 -0
  19. package/esm/provider-options.mjs +49 -0
  20. package/esm/provider-options.mjs.map +1 -0
  21. package/esm/responses-model.mjs +116 -17
  22. package/esm/responses-model.mjs.map +1 -1
  23. package/esm/sdk.d.mts +3 -2
  24. package/esm/sdk.d.mts.map +1 -1
  25. package/esm/sdk.mjs +5 -3
  26. package/esm/sdk.mjs.map +1 -1
  27. package/esm/transcription.mjs.map +1 -1
  28. package/esm/utils/build-usage.mjs +14 -2
  29. package/esm/utils/build-usage.mjs.map +1 -1
  30. package/esm/utils/index.mjs +1 -1
  31. package/esm/utils/map-responses-status.mjs.map +1 -1
  32. package/esm/utils/prompt-cache-params.mjs +25 -6
  33. package/esm/utils/prompt-cache-params.mjs.map +1 -1
  34. package/esm/utils/reasoning-replay.mjs +23 -1
  35. package/esm/utils/reasoning-replay.mjs.map +1 -1
  36. package/esm/utils/structured-output.mjs.map +1 -1
  37. package/esm/utils/to-openai-messages.mjs.map +1 -1
  38. package/esm/utils/to-openai-tools.mjs.map +1 -1
  39. package/esm/utils/wrap-openai-error.mjs +6 -1
  40. package/esm/utils/wrap-openai-error.mjs.map +1 -1
  41. package/llms-full.txt +128 -37
  42. package/llms.txt +1 -1
  43. package/package.json +3 -3
  44. package/skills/setup-openai/SKILL.md +128 -37
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: setup-openai
3
- description: 'Wire @warlock.js/ai-openai — new OpenAISDK({apiKey, baseURL?, provider?, pricing?}) for OpenAI / Azure / OpenRouter, .model({name, vision?, reasoning?, structuredOutput?, responseFormat?, pdf?, audio?}) for ModelContract, .embedder({name, dimensions?}) for embeddings, .image({name, pricing?}) for gpt-image-*/dall-e-* image generation (via ai.image). PDF input maps to OpenAI file parts (opt-in pdf:true), audio input to input_audio (opt-in audio:true). Triggers: `OpenAISDK`, `.model`, `.embedder`, `.image`, `.embed`, `.embedMany`, `baseURL`, `pricing`, `responseSchema`, `responseFormat`, `reasoning_effort`, `reasoningTokens`, `cachedTokens`, `gpt-image`, `dall-e`, `ai.image`, `pdf input`, `input_audio`, o-series / gpt-5 reasoning, `api: "responses"`, Responses API, reasoning with tools, prompt caching; "wire openai into a warlock agent", "configure gpt-4o", "use o3 / gpt-5 reasoning effort", "route through openrouter or azure openai", "openai embeddings with warlock", "generate images with gpt-image / dall-e", "send a pdf / audio to gpt-4o"; typical import `import { OpenAISDK } from "@warlock.js/ai-openai"`. Skip: the ai.image verb surface — the `generate-images` topic of the `warlock-js-ai` skill; agent wiring — the `run-ai-agent` topic of the `warlock-js-ai` skill; adapter comparison — the `pick-ai-provider` topic of the `warlock-js-ai` skill; competing adapters `@warlock.js/ai-anthropic`, `@warlock.js/ai-bedrock`, `@warlock.js/ai-google`, `@warlock.js/ai-ollama`; raw `openai` SDK, Vercel `@ai-sdk/openai`.'
3
+ description: 'Wire @warlock.js/ai-openai — new OpenAISDK({apiKey, baseURL?, provider?, pricing?}) for OpenAI / Azure / OpenRouter, .model({name, vision?, reasoning?, structuredOutput?, responseFormat?, pdf?, audio?}) for ModelContract, .embedder({name, dimensions?}) for embeddings, .image({name, pricing?}) for gpt-image-*/dall-e-* image generation (via ai.image). PDF input maps to OpenAI file parts (opt-in pdf:true), audio input to input_audio (opt-in audio:true). Triggers: `providerOptions`, `OpenAICallOptions`, `countTokens`, `OpenAISDK`, `.model`, `.embedder`, `.image`, `.embed`, `.embedMany`, `baseURL`, `pricing`, `responseSchema`, `responseFormat`, `reasoning_effort`, `reasoningTokens`, `cachedTokens`, `cacheWriteTokens`, `inputIncludesCache`, `OPENAI_MODEL_PRICING`, `openAIPricingFor`, `ProviderOverloadedError`, `gpt-image`, `dall-e`, `ai.image`, `pdf input`, `input_audio`, o-series / gpt-5 reasoning, `api: "responses"`, Responses API, `ai.providerTool`, hosted `web_search`, `streamChunks`, `reasoning_content`, reasoning with tools, prompt caching; "wire openai into a warlock agent", "configure gpt-4o", "use o3 / gpt-5 reasoning effort", "route through openrouter or azure openai", "openai embeddings with warlock", "generate images with gpt-image / dall-e", "send a pdf / audio to gpt-4o"; typical import `import { OpenAISDK } from "@warlock.js/ai-openai"`. Skip: the ai.image verb surface — the `generate-images` topic of the `warlock-js-ai` skill; agent wiring — the `run-ai-agent` topic of the `warlock-js-ai` skill; adapter comparison — the `pick-ai-provider` topic of the `warlock-js-ai` skill; competing adapters `@warlock.js/ai-anthropic`, `@warlock.js/ai-bedrock`, `@warlock.js/ai-google`, `@warlock.js/ai-ollama`; raw `openai` SDK, Vercel `@ai-sdk/openai`.'
4
4
  ---
5
5
 
6
6
  # `@warlock.js/ai-openai`
@@ -31,23 +31,23 @@ const openrouter = new OpenAISDK({
31
31
  ## Producing a model
32
32
 
33
33
  ```ts
34
- openai.model({ name: "gpt-4o-mini" }) // common case
35
- openai.model({ name: "gpt-4o", temperature: 0.2 }) // sampling controls
36
- openai.model({ name: "fine-tuned-x", vision: true }) // explicit capability override
34
+ openai.model({ name: "gpt-4o-mini" }); // common case
35
+ openai.model({ name: "gpt-4o", temperature: 0.2 }); // sampling controls
36
+ openai.model({ name: "fine-tuned-x", vision: true }); // explicit capability override
37
37
  ```
38
38
 
39
39
  Returns a `ModelContract` you pass straight into `ai.agent({ model })`.
40
40
 
41
41
  ## Capabilities — what's auto-set
42
42
 
43
- | Flag | Default |
44
- | --- | --- |
45
- | `structuredOutput` | `true`, unless `responseFormat` is set to `"json_object"` or `"text"` (loose modes) — then `false`. |
46
- | `vision` | Inferred from model name. `true` for `gpt-4o*`, `gpt-4-turbo*`, `gpt-4.1*`, `o1*`, `o3*`, `chatgpt-4o*`; `false` otherwise. |
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` | `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
- | `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
- | `audio` | `false` by default (opt-in `.model({ audio: true })`). Only the `gpt-4o-audio-preview` family accepts `input_audio`. |
43
+ | Flag | Default |
44
+ | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
45
+ | `structuredOutput` | `true`, unless `responseFormat` is set to `"json_object"` or `"text"` (loose modes) — then `false`. |
46
+ | `vision` | Inferred from model name. `true` for `gpt-4o*`, `gpt-4-turbo*`, `gpt-4.1*`, `o1*`, `o3*`, `chatgpt-4o*`; `false` otherwise. |
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` | `true` for the direct OpenAI provider. It supports automatic cache hits plus explicit content-part write breakpoints; wrappers send only `prompt_cache_key`, and only where the endpoint documents it (xAI, Mistral). |
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
+ | `audio` | `false` by default (opt-in `.model({ audio: true })`). Only the `gpt-4o-audio-preview` family accepts `input_audio`. |
51
51
 
52
52
  **Override `vision`, `structuredOutput`, `reasoning`, `pdf`, or `audio` explicitly** via `.model({ name, vision?, structuredOutput?, reasoning?, pdf?, audio? })` — an explicit value always wins over inference.
53
53
 
@@ -58,8 +58,8 @@ When the agent passes `responseSchema` (a JSON Schema object), the adapter conve
58
58
  Some targets reject strict `json_schema` (older OpenAI models, OpenRouter routes, Ollama OpenAI-compat). Override the wire mode per model with `responseFormat`:
59
59
 
60
60
  ```ts
61
- openai.model({ name: "legacy-model", responseFormat: "json_object" }) // valid JSON, no strict shape
62
- openai.model({ name: "some-route", responseFormat: "text" }) // no response_format on the wire
61
+ openai.model({ name: "legacy-model", responseFormat: "json_object" }); // valid JSON, no strict shape
62
+ openai.model({ name: "some-route", responseFormat: "text" }); // no response_format on the wire
63
63
  ```
64
64
 
65
65
  `"json_object"` and `"text"` also flip `structuredOutput` to `false`, so the agent re-injects the schema as a soft prompt hint. Pin `structuredOutput` explicitly to override that.
@@ -82,7 +82,7 @@ PDF and audio reach the wire only when the model declares the matching capabilit
82
82
 
83
83
  ```ts
84
84
  const gpt = openai.image({ name: "gpt-image-1", pricing: { input: 5, output: 40 } }); // token-metered
85
- const dalle = openai.image({ name: "dall-e-3", pricing: { perImage: 0.04 } }); // per-image
85
+ const dalle = openai.image({ name: "dall-e-3", pricing: { perImage: 0.04 } }); // per-image
86
86
 
87
87
  const { data } = await ai.image({ model: gpt, prompt: "a red bicycle", size: "1024x1024" });
88
88
  ```
@@ -97,6 +97,20 @@ const { data } = await ai.image({ model: gpt, prompt: "a red bicycle", size: "10
97
97
 
98
98
  Tool-call argument fragments arrive split across deltas; the adapter accumulates them per `tool_calls[n].index` and parses the assembled JSON once on completion, so streamed tool calls round-trip identically to non-streaming `complete()`. Accumulators that never received a function name are skipped.
99
99
 
100
+ Opt-in chunks (`options.streamChunks`, set by `agent.stream()`): Chat yields `{ type: "tool-args", callId, name, delta }` per argument fragment and `{ type: "reasoning", delta }` for a compatible vendor's `delta.reasoning_content` (DeepSeek) / `delta.reasoning` (Groq); Responses yields `reasoning` for `response.reasoning_summary_text.delta` / `response.reasoning_text.delta` (summaries need `body: { reasoning: { summary: "auto" } }`) and `tool-args` for `response.function_call_arguments.delta`. No `usage` chunk (usage arrives at the end).
101
+
102
+ ## Provider-defined tools
103
+
104
+ With `api: "responses"`, `ai.providerTool({ provider: "openai", definition })` appends hosted tools verbatim (`{ type: "web_search" }`, `file_search`, `code_interpreter`, `image_generation`, `mcp`). Hosted-call items are not replayed (`store: false`), and a reasoning item that led into one is dropped from the replay. Client-run built-ins (`shell_call`, `apply_patch_call`, `computer_call`, `custom_tool_call`, `local_shell_call`, `mcp_approval_request`) throw `InvalidRequestError`. On Chat Completions a provider tool is sent only when `provider` equals the model's provider label (e.g. Groq `{ type: "browser_search" }` with `provider: "groq"`).
105
+
106
+ ```ts
107
+ const model = openai.model({ name: "gpt-5.6", api: "responses" });
108
+ ai.agent({
109
+ model,
110
+ tools: [ai.providerTool({ provider: "openai", definition: { type: "web_search" } }), lookupOrder],
111
+ });
112
+ ```
113
+
100
114
  ## Embeddings
101
115
 
102
116
  ```ts
@@ -117,8 +131,8 @@ openai.embedder({ name: "text-embedding-3-large", dimensions: 256 });
117
131
  Reasoning models accept a discrete effort knob. The agent passes it through `ModelCallOptions.reasoning`:
118
132
 
119
133
  ```ts
120
- const model = openai.model({ name: "o3-mini" }); // reasoning auto-true
121
- await model.complete(messages, { reasoning: { effort: "high" } }); // → reasoning_effort: "high"
134
+ const model = openai.model({ name: "o3-mini" }); // reasoning auto-true
135
+ await model.complete(messages, { reasoning: { effort: "high" } }); // → reasoning_effort: "high"
122
136
  ```
123
137
 
124
138
  - `reasoning.effort` (`"low" | "medium" | "high" | "none"`) maps verbatim to OpenAI's `reasoning_effort` request param.
@@ -138,8 +152,8 @@ reasoning_effort to 'none'.
138
152
  Since there is no working alternative to `"none"` in that state, the adapter defaults to it automatically: whenever a call is made to a reasoning-capable model WITH `tools` AND the caller supplied no `reasoning.effort` at all, `reasoning_effort: "none"` is emitted for you — no opt-in required.
139
153
 
140
154
  ```ts
141
- const model = openai.model({ name: "gpt-5-mini" }); // reasoning auto-true
142
- await model.complete(messages, { tools }); // → reasoning_effort: "none", automatically
155
+ const model = openai.model({ name: "gpt-5-mini" }); // reasoning auto-true
156
+ await model.complete(messages, { tools }); // → reasoning_effort: "none", automatically
143
157
  ```
144
158
 
145
159
  An explicit `reasoning.effort` always overrides the default, in either direction — pass it yourself if you want a different value (and accept that tools may then be rejected, per the error above, since Chat Completions still can't do reasoning + tools together):
@@ -180,15 +194,18 @@ await model.complete(messages, { tools, reasoning: { effort: "high" } }); // rea
180
194
 
181
195
  `usage` on the result carries the neutral channel breakdown, populated from OpenAI's `usage` block:
182
196
 
183
- | `Usage` field | OpenAI source | Notes |
184
- | --- | --- | --- |
185
- | `input` / `output` / `total` | `prompt_tokens` / `completion_tokens` / `total_tokens` | always present (zeroed if OpenAI omits the block). |
186
- | `cachedTokens` | `prompt_tokens_details.cached_tokens` | subset of `input` served from OpenAI's automatic prompt cache. Emitted only when > 0. |
187
- | `reasoningTokens` | `completion_tokens_details.reasoning_tokens` | hidden reasoning channel on o-series / gpt-5 (already counted within `output`). Emitted only when > 0. |
197
+ | `Usage` field | OpenAI source | Notes |
198
+ | ---------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ |
199
+ | `input` / `output` / `total` | `prompt_tokens` / `completion_tokens` / `total_tokens` | always present (zeroed if OpenAI omits the block). |
200
+ | `inputIncludesCache` | — (always `true`) | `prompt_tokens` already contains the cached and cache-written tokens; cost bills them out of it once. |
201
+ | `cachedTokens` | `prompt_tokens_details.cached_tokens` | subset of `input` served from OpenAI's automatic prompt cache. Emitted only when > 0. |
202
+ | `cacheWriteTokens` | `prompt_tokens_details.cache_write_tokens` | subset of `input` written to the cache, on the models that charge for writes. Emitted only when > 0. |
203
+ | `reasoningTokens` | `completion_tokens_details.reasoning_tokens` | hidden reasoning channel on o-series / gpt-5 (already counted within `output`). Emitted only when > 0. |
204
+ | `cost` | computed | per-channel USD when the model is priced (see [Pricing](#pricing--built-in-list-prices-and-registry)). |
188
205
 
189
- 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.
206
+ The detail counters are omitted (not set to `0`) when the provider reports zero, so non-reasoning / uncached calls keep the lean `{ input, output, total, inputIncludesCache }` shape. Reported identically on `complete()`, the streaming `done` event and the Responses API (`input_tokens_details.cached_tokens` / `cache_write_tokens`). The OpenAI-compatible wrappers (DeepSeek, xAI, Groq, Mistral) report the same fields and the same inclusive semantics.
190
207
 
191
- ### Explicit prompt caching (direct OpenAI only)
208
+ ### Explicit prompt caching
192
209
 
193
210
  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:
194
211
 
@@ -202,39 +219,113 @@ const model = openai.model({
202
219
 
203
220
  `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.
204
221
 
205
- 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.
222
+ Which fields reach the wire depends on the SDK provider label:
223
+
224
+ | Provider label | `prompt_cache_key` | `prompt_cache_retention` / explicit breakpoints | Why |
225
+ | ---------------------- | ------------------ | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
226
+ | `"openai"` | sent | sent | https://developers.openai.com/api/docs/guides/prompt-caching |
227
+ | `"xai"` | sent | — | xAI Chat Completions accepts `prompt_cache_key` (routed as `x-grok-conv-id`) — https://docs.x.ai/developers/advanced-api-usage/prompt-caching/maximizing-cache-hits |
228
+ | `"mistral"` | sent | — | https://docs.mistral.ai/studio/conversations/advanced/prompt-caching |
229
+ | `"groq"`, `"deepseek"` | — | — | both cache automatically and document no request hint |
230
+ | any custom label | — | — | an unknown endpoint may reject the fields |
231
+
232
+ (Checked 2026-10-17.) `XaiSDK` and `MistralSDK` keep their default labels unless you pass `provider`, so `xai.model({ name, promptCacheKey })` works out of the box. Read-side `usage.cachedTokens` accounting remains available whenever the upstream reports it.
206
233
 
207
234
  ## Token counting
208
235
 
209
236
  ```ts
210
- await openai.count("some text") // approximate (heuristic, not tiktoken)
237
+ await openai.count("some text"); // approximate (heuristic, not tiktoken)
211
238
  ```
212
239
 
213
240
  Good enough for budgeting; not for billing.
214
241
 
215
- ## Pricing — per-model registry
242
+ ## Pricing — built-in list prices and registry
216
243
 
217
- `pricing` is a **registry keyed by model name** — one entry per model, all rates in **USD per 1,000,000 tokens** (`ModelPricing`: `input`, `output`, optional `cachedInput` / `cachedOutput`).
244
+ All rates are **USD per 1,000,000 tokens** (`ModelPricing`: `input`, `output`, optional `cachedInput`, `cacheWrite`, `cacheWrite1h`, `reasoning`). Resolution at `model()` time: per-model `pricing` (`openai.model({ name, pricing })`) > SDK `pricing` registry > built-in `OPENAI_MODEL_PRICING` (only when the SDK provider label is `"openai"`, exact model id) > `undefined` (no cost computed).
218
245
 
219
246
  ```ts
247
+ import { ai, totalCost } from "@warlock.js/ai";
248
+ import { OPENAI_MODEL_PRICING, OpenAISDK, openAIPricingFor } from "@warlock.js/ai-openai";
249
+
220
250
  const openai = new OpenAISDK({
221
- apiKey,
251
+ apiKey: process.env.OPENAI_API_KEY ?? "",
222
252
  pricing: {
223
- // USD per 1M tokens.
224
- "gpt-4o-mini": { input: 0.15, output: 0.6, cachedInput: 0.075 },
225
- "gpt-4o": { input: 2.5, output: 10, cachedInput: 1.25 },
253
+ // Prompt-length-tiered models have no built-in rate — price them yourself.
254
+ "gpt-5.5": { input: 5, output: 30, cachedInput: 0.5 },
226
255
  },
227
256
  });
228
257
 
258
+ openai.model({ name: "gpt-4o-mini" }).pricing; // built-in: { input: 0.15, output: 0.6, cachedInput: 0.075 }
259
+ openAIPricingFor("gpt-4o-2024-05-13"); // undefined — dated snapshots can carry other prices
260
+ Object.keys(OPENAI_MODEL_PRICING); // the priced ids
261
+
229
262
  const { usage } = await ai.agent({ model: openai.model({ name: "gpt-4o-mini" }) }).execute("hi");
230
- usage.cost; // { input, output, cachedInput?, cachedOutput? } — per-channel USD breakdown of THIS run
263
+ console.log(usage.cost, totalCost(usage.cost)); // per-channel USD breakdown of THIS run, and its sum
264
+
265
+ // A direct call is costed too — no agent needed.
266
+ const side = await openai
267
+ .model({ name: "gpt-4.1-mini" })
268
+ .complete([{ role: "user", content: "Classify: …" }]);
269
+ console.log(totalCost(side.usage.cost));
231
270
  ```
232
271
 
233
- The registry is per-model; `usage.cost` is the per-channel breakdown the framework computes from `tokens × pricing[model]`. Resolution at `model()` time: per-model `pricing` (`openai.model({ name, pricing })`) > SDK registry > `undefined` (no cost computed). See the `pick-ai-provider` topic of the `warlock-js-ai` skill.
272
+ - **Source.** `OPENAI_MODEL_PRICING` comes from https://developers.openai.com/api/docs/pricing (the target of platform.openai.com/docs/pricing), Standard tier, fetched 2026-10-17. It covers the flat-priced models: `gpt-5.4-mini`, `gpt-5.4-nano`, `gpt-5.2`, `gpt-5.1`, `gpt-5`, `gpt-5-mini`, `gpt-5-nano`, `gpt-4.1` (+ `-mini` / `-nano`), `gpt-4o`, `gpt-4o-mini`, `o3`, `o4-mini`, `o3-mini`, `o1`. None of them charges for cache writes.
273
+ - **Deliberately unpriced:** models priced by prompt length (a higher rate above 272K input tokens — the gpt-6, gpt-5.6, gpt-5.5 and gpt-5.4 families and the `-pro` models), dated snapshot ids, promotional / Batch / Flex / Priority pricing, and the regional data-residency uplift. Wrapper SDKs (`provider` other than `"openai"`) never fall back to this table.
234
274
 
235
275
  ## Errors
236
276
 
237
- Raw OpenAI SDK errors are wrapped into the typed `@warlock.js/ai` `AIError` hierarchy via the adapter's error wrapper. Dispatch keys on `APIError.status + code` combined — see the `handle-ai-errors` topic of the `warlock-js-ai` skill.
277
+ Raw OpenAI SDK errors are wrapped into the typed `@warlock.js/ai` `AIError` hierarchy via the adapter's error wrapper. Dispatch keys on `APIError.status + code` combined — see the `handle-ai-errors` topic of the `warlock-js-ai` skill. HTTP 503 and code `server_is_overloaded` (https://developers.openai.com/api/docs/guides/error-codes) map to `ProviderOverloadedError` (`code: "PROVIDER_OVERLOADED"`, `retryable: true`, `retryAfter` from `retry-after`); the compatible wrappers use 503 for the same condition (DeepSeek documents it as "Server Overloaded"). Every wrapped error carries `status`, `retryable` and `billed` — `false` for 401 / 402 / 403, otherwise `"unknown"`, because OpenAI publishes no rule for failed requests.
278
+
279
+ ```ts
280
+ import { ai, ProviderOverloadedError } from "@warlock.js/ai";
281
+ import { OpenAISDK } from "@warlock.js/ai-openai";
282
+
283
+ const openai = new OpenAISDK({ apiKey: process.env.OPENAI_API_KEY ?? "" });
284
+ const result = await ai.agent({ model: openai.model({ name: "gpt-4o-mini" }) }).execute("hi");
285
+
286
+ if (result.error instanceof ProviderOverloadedError) {
287
+ console.log(result.error.status, result.error.retryAfter); // back off, or fall back to another model
288
+ }
289
+ ```
290
+
291
+ ## Provider body options and input-token counts
292
+
293
+ Native body fields merge after neutral options. When both values are plain
294
+ objects, their immediate keys merge and caller-provided keys win; untouched
295
+ neutral fields remain. Arrays, scalars and `null` replace the field. This merge
296
+ is one level deep: a nested object explicitly supplied by the caller replaces
297
+ that nested value. When both objects have string `type` discriminators and the
298
+ caller changes the type, the whole object is replaced. For example, overriding
299
+ `response_format` from `json_schema` to `json_object` removes the old
300
+ `json_schema` fields; switching `tool_choice` variants removes old variant fields
301
+ as well. The rule also applies to OpenAI-compatible adapters using this helper.
302
+
303
+ `providerOptions.openai.body` adds native request fields to Chat Completions or
304
+ Responses, merged after the neutral options. For example:
305
+
306
+ ```ts
307
+ const options = { providerOptions: { openai: { body: { service_tier: "default" } } } };
308
+ await model.complete(messages, options);
309
+ ```
310
+
311
+ The key stays `openai` for compatible gateways with a custom provider label.
312
+ Other provider keys are ignored. `OpenAICallOptions` is exported. The body cannot
313
+ replace `model`, `messages`, `input`, `instructions`, `tools`, `stream`,
314
+ `stream_options`, `previous_response_id`, `conversation`, `store`, `include`, `functions`, `function_call`, `prompt` or `background`;
315
+ those fields protect the adapter's conversation, tools and stateless replay.
316
+
317
+ Models using `api: "responses"` expose native `countTokens(messages, options?)`
318
+ through `responses.inputTokens.count`: the same input, system instructions,
319
+ tools, reasoning, text format, tool choice, parallel-tool setting and truncation
320
+ used for generation are counted. The endpoint supports only input-shaping fields;
321
+ output caps and generation-only options are omitted. The Chat Completions model
322
+ has no `countTokens` method. `sdk.count(text)` remains a separate offline estimate.
323
+
324
+ ```ts
325
+ if (model.countTokens) {
326
+ const inputTokens = await model.countTokens(messages, options);
327
+ }
328
+ ```
238
329
 
239
330
  ## When NOT to use this skill
240
331