@warlock.js/ai-openai 5.30.0 → 5.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +42 -1
  2. package/README.md +3 -3
  3. package/cjs/index.cjs +715 -51
  4. package/cjs/index.cjs.map +1 -1
  5. package/esm/config.type.d.mts +13 -13
  6. package/esm/config.type.d.mts.map +1 -1
  7. package/esm/embedder.d.mts +1 -3
  8. package/esm/embedder.d.mts.map +1 -1
  9. package/esm/embedder.mjs.map +1 -1
  10. package/esm/image.d.mts +1 -3
  11. package/esm/image.d.mts.map +1 -1
  12. package/esm/index.d.mts +4 -1
  13. package/esm/index.mjs +3 -1
  14. package/esm/kimi-tools.d.mts +69 -0
  15. package/esm/kimi-tools.d.mts.map +1 -0
  16. package/esm/kimi-tools.mjs +256 -0
  17. package/esm/kimi-tools.mjs.map +1 -0
  18. package/esm/known-pricing.d.mts +39 -0
  19. package/esm/known-pricing.d.mts.map +1 -0
  20. package/esm/known-pricing.mjs +121 -0
  21. package/esm/known-pricing.mjs.map +1 -0
  22. package/esm/model.mjs +82 -23
  23. package/esm/model.mjs.map +1 -1
  24. package/esm/provider-options.d.mts +13 -0
  25. package/esm/provider-options.d.mts.map +1 -0
  26. package/esm/provider-options.mjs +56 -0
  27. package/esm/provider-options.mjs.map +1 -0
  28. package/esm/responses-model.mjs +125 -19
  29. package/esm/responses-model.mjs.map +1 -1
  30. package/esm/sdk.d.mts +4 -5
  31. package/esm/sdk.d.mts.map +1 -1
  32. package/esm/sdk.mjs +5 -3
  33. package/esm/sdk.mjs.map +1 -1
  34. package/esm/speech.d.mts +1 -3
  35. package/esm/speech.d.mts.map +1 -1
  36. package/esm/transcription.d.mts +1 -3
  37. package/esm/transcription.d.mts.map +1 -1
  38. package/esm/transcription.mjs.map +1 -1
  39. package/esm/utils/build-usage.mjs +14 -2
  40. package/esm/utils/build-usage.mjs.map +1 -1
  41. package/esm/utils/hosted-output.mjs +22 -0
  42. package/esm/utils/hosted-output.mjs.map +1 -0
  43. package/esm/utils/index.mjs +2 -2
  44. package/esm/utils/map-responses-status.mjs.map +1 -1
  45. package/esm/utils/prompt-cache-params.mjs +25 -6
  46. package/esm/utils/prompt-cache-params.mjs.map +1 -1
  47. package/esm/utils/reasoning-replay.mjs +23 -1
  48. package/esm/utils/reasoning-replay.mjs.map +1 -1
  49. package/esm/utils/structured-output.mjs +1 -1
  50. package/esm/utils/structured-output.mjs.map +1 -1
  51. package/esm/utils/to-openai-messages.mjs.map +1 -1
  52. package/esm/utils/to-openai-tools.mjs.map +1 -1
  53. package/esm/utils/wrap-openai-error.mjs +6 -1
  54. package/esm/utils/wrap-openai-error.mjs.map +1 -1
  55. package/llms-full.txt +235 -37
  56. package/llms.txt +1 -1
  57. package/package.json +3 -3
  58. package/skills/setup-openai/SKILL.md +235 -37
package/llms-full.txt CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  ---
10
10
  name: setup-openai
11
- 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`.'
11
+ 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`, `kimiSearchTool`, `kimiFetchTool`, `KIMI_SERVER_TOOL_PRICING`, `.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`.'
12
12
  ---
13
13
 
14
14
  # `@warlock.js/ai-openai`
@@ -39,23 +39,23 @@ const openrouter = new OpenAISDK({
39
39
  ## Producing a model
40
40
 
41
41
  ```ts
42
- openai.model({ name: "gpt-4o-mini" }) // common case
43
- openai.model({ name: "gpt-4o", temperature: 0.2 }) // sampling controls
44
- openai.model({ name: "fine-tuned-x", vision: true }) // explicit capability override
42
+ openai.model({ name: "gpt-4o-mini" }); // common case
43
+ openai.model({ name: "gpt-4o", temperature: 0.2 }); // sampling controls
44
+ openai.model({ name: "fine-tuned-x", vision: true }); // explicit capability override
45
45
  ```
46
46
 
47
47
  Returns a `ModelContract` you pass straight into `ai.agent({ model })`.
48
48
 
49
49
  ## Capabilities — what's auto-set
50
50
 
51
- | Flag | Default |
52
- | --- | --- |
53
- | `structuredOutput` | `true`, unless `responseFormat` is set to `"json_object"` or `"text"` (loose modes) — then `false`. |
54
- | `vision` | Inferred from model name. `true` for `gpt-4o*`, `gpt-4-turbo*`, `gpt-4.1*`, `o1*`, `o3*`, `chatgpt-4o*`; `false` otherwise. |
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` | `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
- | `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
- | `audio` | `false` by default (opt-in `.model({ audio: true })`). Only the `gpt-4o-audio-preview` family accepts `input_audio`. |
51
+ | Flag | Default |
52
+ | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
53
+ | `structuredOutput` | `true`, unless `responseFormat` is set to `"json_object"` or `"text"` (loose modes) — then `false`. |
54
+ | `vision` | Inferred from model name. `true` for `gpt-4o*`, `gpt-4-turbo*`, `gpt-4.1*`, `o1*`, `o3*`, `chatgpt-4o*`; `false` otherwise. |
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` | `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). |
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
+ | `audio` | `false` by default (opt-in `.model({ audio: true })`). Only the `gpt-4o-audio-preview` family accepts `input_audio`. |
59
59
 
60
60
  **Override `vision`, `structuredOutput`, `reasoning`, `pdf`, or `audio` explicitly** via `.model({ name, vision?, structuredOutput?, reasoning?, pdf?, audio? })` — an explicit value always wins over inference.
61
61
 
@@ -66,8 +66,8 @@ When the agent passes `responseSchema` (a JSON Schema object), the adapter conve
66
66
  Some targets reject strict `json_schema` (older OpenAI models, OpenRouter routes, Ollama OpenAI-compat). Override the wire mode per model with `responseFormat`:
67
67
 
68
68
  ```ts
69
- openai.model({ name: "legacy-model", responseFormat: "json_object" }) // valid JSON, no strict shape
70
- openai.model({ name: "some-route", responseFormat: "text" }) // no response_format on the wire
69
+ openai.model({ name: "legacy-model", responseFormat: "json_object" }); // valid JSON, no strict shape
70
+ openai.model({ name: "some-route", responseFormat: "text" }); // no response_format on the wire
71
71
  ```
72
72
 
73
73
  `"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.
@@ -90,7 +90,7 @@ PDF and audio reach the wire only when the model declares the matching capabilit
90
90
 
91
91
  ```ts
92
92
  const gpt = openai.image({ name: "gpt-image-1", pricing: { input: 5, output: 40 } }); // token-metered
93
- const dalle = openai.image({ name: "dall-e-3", pricing: { perImage: 0.04 } }); // per-image
93
+ const dalle = openai.image({ name: "dall-e-3", pricing: { perImage: 0.04 } }); // per-image
94
94
 
95
95
  const { data } = await ai.image({ model: gpt, prompt: "a red bicycle", size: "1024x1024" });
96
96
  ```
@@ -105,6 +105,20 @@ const { data } = await ai.image({ model: gpt, prompt: "a red bicycle", size: "10
105
105
 
106
106
  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.
107
107
 
108
+ 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).
109
+
110
+ ## Provider-defined tools
111
+
112
+ 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"`).
113
+
114
+ ```ts
115
+ const model = openai.model({ name: "gpt-5.6", api: "responses" });
116
+ ai.agent({
117
+ model,
118
+ tools: [ai.providerTool({ provider: "openai", definition: { type: "web_search" } }), lookupOrder],
119
+ });
120
+ ```
121
+
108
122
  ## Embeddings
109
123
 
110
124
  ```ts
@@ -125,8 +139,8 @@ openai.embedder({ name: "text-embedding-3-large", dimensions: 256 });
125
139
  Reasoning models accept a discrete effort knob. The agent passes it through `ModelCallOptions.reasoning`:
126
140
 
127
141
  ```ts
128
- const model = openai.model({ name: "o3-mini" }); // reasoning auto-true
129
- await model.complete(messages, { reasoning: { effort: "high" } }); // → reasoning_effort: "high"
142
+ const model = openai.model({ name: "o3-mini" }); // reasoning auto-true
143
+ await model.complete(messages, { reasoning: { effort: "high" } }); // → reasoning_effort: "high"
130
144
  ```
131
145
 
132
146
  - `reasoning.effort` (`"low" | "medium" | "high" | "none"`) maps verbatim to OpenAI's `reasoning_effort` request param.
@@ -146,8 +160,8 @@ reasoning_effort to 'none'.
146
160
  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.
147
161
 
148
162
  ```ts
149
- const model = openai.model({ name: "gpt-5-mini" }); // reasoning auto-true
150
- await model.complete(messages, { tools }); // → reasoning_effort: "none", automatically
163
+ const model = openai.model({ name: "gpt-5-mini" }); // reasoning auto-true
164
+ await model.complete(messages, { tools }); // → reasoning_effort: "none", automatically
151
165
  ```
152
166
 
153
167
  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):
@@ -188,15 +202,18 @@ await model.complete(messages, { tools, reasoning: { effort: "high" } }); // rea
188
202
 
189
203
  `usage` on the result carries the neutral channel breakdown, populated from OpenAI's `usage` block:
190
204
 
191
- | `Usage` field | OpenAI source | Notes |
192
- | --- | --- | --- |
193
- | `input` / `output` / `total` | `prompt_tokens` / `completion_tokens` / `total_tokens` | always present (zeroed if OpenAI omits the block). |
194
- | `cachedTokens` | `prompt_tokens_details.cached_tokens` | subset of `input` served from OpenAI's automatic prompt cache. Emitted only when > 0. |
195
- | `reasoningTokens` | `completion_tokens_details.reasoning_tokens` | hidden reasoning channel on o-series / gpt-5 (already counted within `output`). Emitted only when > 0. |
205
+ | `Usage` field | OpenAI source | Notes |
206
+ | ---------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ |
207
+ | `input` / `output` / `total` | `prompt_tokens` / `completion_tokens` / `total_tokens` | always present (zeroed if OpenAI omits the block). |
208
+ | `inputIncludesCache` | — (always `true`) | `prompt_tokens` already contains the cached and cache-written tokens; cost bills them out of it once. |
209
+ | `cachedTokens` | `prompt_tokens_details.cached_tokens` | subset of `input` served from OpenAI's automatic prompt cache. Emitted only when > 0. |
210
+ | `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. |
211
+ | `reasoningTokens` | `completion_tokens_details.reasoning_tokens` | hidden reasoning channel on o-series / gpt-5 (already counted within `output`). Emitted only when > 0. |
212
+ | `cost` | computed | per-channel USD when the model is priced (see [Pricing](#pricing--built-in-list-prices-and-registry)). |
196
213
 
197
- 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.
214
+ 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.
198
215
 
199
- ### Explicit prompt caching (direct OpenAI only)
216
+ ### Explicit prompt caching
200
217
 
201
218
  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:
202
219
 
@@ -210,39 +227,120 @@ const model = openai.model({
210
227
 
211
228
  `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.
212
229
 
213
- 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.
230
+ Which fields reach the wire depends on the SDK provider label:
231
+
232
+ | Provider label | `prompt_cache_key` | `prompt_cache_retention` / explicit breakpoints | Why |
233
+ | ---------------------- | ------------------ | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
234
+ | `"openai"` | sent | sent | https://developers.openai.com/api/docs/guides/prompt-caching |
235
+ | `"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 |
236
+ | `"mistral"` | sent | — | https://docs.mistral.ai/studio/conversations/advanced/prompt-caching |
237
+ | `"groq"`, `"deepseek"` | — | — | both cache automatically and document no request hint |
238
+ | any custom label | — | — | an unknown endpoint may reject the fields |
239
+
240
+ (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.
214
241
 
215
242
  ## Token counting
216
243
 
217
244
  ```ts
218
- await openai.count("some text") // approximate (heuristic, not tiktoken)
245
+ await openai.count("some text"); // approximate (heuristic, not tiktoken)
219
246
  ```
220
247
 
221
248
  Good enough for budgeting; not for billing.
222
249
 
223
- ## Pricing — per-model registry
250
+ ## Pricing — built-in list prices and registry
224
251
 
225
- `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`).
252
+ 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).
226
253
 
227
254
  ```ts
255
+ import { ai, totalCost } from "@warlock.js/ai";
256
+ import { OPENAI_MODEL_PRICING, OpenAISDK, openAIPricingFor } from "@warlock.js/ai-openai";
257
+
228
258
  const openai = new OpenAISDK({
229
- apiKey,
259
+ apiKey: process.env.OPENAI_API_KEY ?? "",
230
260
  pricing: {
231
- // USD per 1M tokens.
232
- "gpt-4o-mini": { input: 0.15, output: 0.6, cachedInput: 0.075 },
233
- "gpt-4o": { input: 2.5, output: 10, cachedInput: 1.25 },
261
+ // Prompt-length-tiered models have no built-in rate — price them yourself.
262
+ "gpt-5.5": { input: 5, output: 30, cachedInput: 0.5 },
234
263
  },
235
264
  });
236
265
 
266
+ openai.model({ name: "gpt-4o-mini" }).pricing; // built-in: { input: 0.15, output: 0.6, cachedInput: 0.075 }
267
+ openAIPricingFor("gpt-4o-2024-05-13"); // undefined — dated snapshots can carry other prices
268
+ Object.keys(OPENAI_MODEL_PRICING); // the priced ids
269
+
237
270
  const { usage } = await ai.agent({ model: openai.model({ name: "gpt-4o-mini" }) }).execute("hi");
238
- usage.cost; // { input, output, cachedInput?, cachedOutput? } — per-channel USD breakdown of THIS run
271
+ console.log(usage.cost, totalCost(usage.cost)); // per-channel USD breakdown of THIS run, and its sum
272
+
273
+ // A direct call is costed too — no agent needed.
274
+ const side = await openai
275
+ .model({ name: "gpt-4.1-mini" })
276
+ .complete([{ role: "user", content: "Classify: …" }]);
277
+ console.log(totalCost(side.usage.cost));
239
278
  ```
240
279
 
241
- 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.
280
+ - **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.
281
+ - **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.
242
282
 
243
283
  ## Errors
244
284
 
245
- 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.
285
+ 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.
286
+
287
+ ```ts
288
+ import { ai, ProviderOverloadedError } from "@warlock.js/ai";
289
+ import { OpenAISDK } from "@warlock.js/ai-openai";
290
+
291
+ const openai = new OpenAISDK({ apiKey: process.env.OPENAI_API_KEY ?? "" });
292
+ const result = await ai.agent({ model: openai.model({ name: "gpt-4o-mini" }) }).execute("hi");
293
+
294
+ if (result.error instanceof ProviderOverloadedError) {
295
+ console.log(result.error.status, result.error.retryAfter); // back off, or fall back to another model
296
+ }
297
+ ```
298
+
299
+ ## Provider body options and input-token counts
300
+
301
+ Native body fields merge after neutral options. When both values are plain
302
+ objects, their immediate keys merge and caller-provided keys win; untouched
303
+ neutral fields remain. Arrays, scalars and `null` replace the field. This merge
304
+ is one level deep: a nested object explicitly supplied by the caller replaces
305
+ that nested value. When both objects have string `type` discriminators and the
306
+ caller changes the type, the whole object is replaced. For example, overriding
307
+ `response_format` from `json_schema` to `json_object` removes the old
308
+ `json_schema` fields; switching `tool_choice` variants removes old variant fields
309
+ as well. The rule also applies to OpenAI-compatible adapters using this helper.
310
+
311
+ `providerOptions.openai.body` adds native request fields to Chat Completions or
312
+ Responses, merged after the neutral options. For example:
313
+
314
+ ```ts
315
+ const options = { providerOptions: { openai: { body: { service_tier: "default" } } } };
316
+ await model.complete(messages, options);
317
+ ```
318
+
319
+ The key stays `openai` for compatible gateways with a custom provider label.
320
+ Other provider keys are ignored. `OpenAICallOptions` is exported. The body cannot
321
+ replace `model`, `messages`, `input`, `instructions`, `tools`, `stream`,
322
+ `stream_options`, `previous_response_id`, `conversation`, `store`, `include`, `functions`, `function_call`, `prompt` or `background`;
323
+ those fields protect the adapter's conversation, tools and stateless replay.
324
+
325
+ Models using `api: "responses"` expose native `countTokens(messages, options?)`
326
+ through `responses.inputTokens.count`: the same input, system instructions,
327
+ tools, reasoning, text format, tool choice, parallel-tool setting and truncation
328
+ used for generation are counted. The endpoint supports only input-shaping fields;
329
+ output caps and generation-only options are omitted. The Chat Completions model
330
+ has no `countTokens` method. `sdk.count(text)` remains a separate offline estimate.
331
+
332
+ For budget preflight, `countInputTokens(model, messages, { tools })` from
333
+ `@warlock.js/ai` returns `{ tokens, source }`. It estimates only when native
334
+ counting is absent or explicitly throws `InvalidRequestError` with
335
+ `reason: "count-unsupported"`; this adapter preserves that signal. System and
336
+ tools are supported natively. Ordinary validation, auth, network and rate-limit
337
+ errors propagate. See the cost/budget guide for reserve mode.
338
+
339
+ ```ts
340
+ if (model.countTokens) {
341
+ const inputTokens = await model.countTokens(messages, options);
342
+ }
343
+ ```
246
344
 
247
345
  ## When NOT to use this skill
248
346
 
@@ -255,4 +353,104 @@ Raw OpenAI SDK errors are wrapped into the typed `@warlock.js/ai` `AIError` hier
255
353
  - The `embed-text` topic of the `warlock-js-ai` skill — embedder usage
256
354
  - The `pick-ai-provider` topic of the `warlock-js-ai` skill — adapter comparison
257
355
 
356
+ ## Hosted-search metering and metadata
357
+
358
+ On Responses, each structured `web_search_call` output item contributes one
359
+ `usage.serverToolUse.webSearchRequests`. Streaming item events and terminal
360
+ output are reconciled as snapshots, so the same call is counted once.
361
+ `response.providerMetadata.openai.output` (also on streaming `done`) retains
362
+ hosted output items with their action and sources. The adapter automatically
363
+ requests `web_search_call.action.sources` when web-search tools are supplied.
364
+ Agent reports preserve this metadata on each trip; it is not replayed or
365
+ passed to function dispatch.
366
+
367
+ Explicitly set `pricing.serverTools: OPENAI_SERVER_TOOL_PRICING` (exported by
368
+ this package) for standard `web_search`: $0.01 per tool call, plus search-content
369
+ tokens at model rates. Non-reasoning `web_search_preview` has a different fee
370
+ and needs an explicit override. These prices were verified on 2026-10-18 from
371
+ [OpenAI pricing](https://developers.openai.com/api/docs/pricing#built-in-tools);
372
+ see the [official search output schema](https://developers.openai.com/api/docs/guides/tools-web-search)
373
+ and [cost tracking](https://warlock.js.org/v/latest/ai/recipes/cost-tracking/#hosted-tool-request-fees).
374
+ Chat Completions does not map hosted-search billing counters.
375
+
376
+ Qwen's `enableSearch` flag and Kimi's standalone `kimiSearchTool()` /
377
+ `kimiFetchTool()` have separate controls; Kimi's legacy builtin retires on
378
+ 20 October 2026 and is unsupported; see [OpenAI-compatible native search](https://warlock.js.org/v/latest/ai/providers/openai-compatible/).
379
+
380
+ ```ts
381
+ import { OPENAI_SERVER_TOOL_PRICING } from "@warlock.js/ai-openai";
382
+ const model = openai.model({
383
+ name: "gpt-4.1",
384
+ api: "responses",
385
+ pricing: { ...{ input: 2, output: 8 }, serverTools: OPENAI_SERVER_TOOL_PRICING },
386
+ });
387
+ const response = await model.complete(messages, {
388
+ providerTools: [{ provider: "openai", definition: { type: "web_search" } }],
389
+ });
390
+ console.log(response.usage.serverToolUse?.webSearchRequests);
391
+ console.log(response.providerMetadata);
392
+ ```
393
+
394
+ ## Kimi first-party search and fetch
395
+
396
+ Use the same API key/base URL configuration for the model and client tools:
397
+
398
+ ```ts
399
+ import { ai, createToolContext } from "@warlock.js/ai";
400
+ import { OpenAISDK, kimiSearchTool, kimiFetchTool } from "@warlock.js/ai-openai";
401
+
402
+ const config = {
403
+ apiKey: process.env.MOONSHOT_API_KEY ?? "",
404
+ baseURL: "https://api.moonshot.ai/v1",
405
+ };
406
+ const sdk = new OpenAISDK({ ...config, provider: "moonshot" });
407
+ const basic = kimiSearchTool({ ...config, includeContent: false });
408
+ const pro = kimiSearchTool({
409
+ ...config,
410
+ mode: "pro",
411
+ sites: ["moonshot.ai"],
412
+ timeWindow: { start: "2026-01", end: "2026-10" },
413
+ limit: 5,
414
+ timeoutSeconds: 30,
415
+ });
416
+ const fetchPage = kimiFetchTool({ ...config, maxContentChars: 12_000 });
417
+ const assistant = ai.agent({ model: sdk.model({ name: "kimi-k3" }), tools: [pro, fetchPage] });
418
+ const result = await assistant.execute("Research prompt caching");
419
+ console.log(result.usage.serverToolUse, result.usage.cost?.serverTools);
420
+
421
+ const controller = new AbortController();
422
+ const found = await basic.invoke(
423
+ { query: "Kimi prompt caching" },
424
+ createToolContext({ signal: controller.signal }),
425
+ );
426
+ console.log(found.data?.results, found.usage);
427
+ ```
428
+
429
+ Pass these functions directly as ordinary `tools`, without `ai.providerTool`.
430
+ They call `/v1/tools/search`, `/v1/tools/search_pro` or `/v1/tools/fetch` with
431
+ Bearer auth. Search arguments are `{ query }`; fetch arguments are `{ url }`.
432
+ Search results retain sources and optional full text (Basic) or scored chunks
433
+ (Pro); fetch returns Markdown. Text is bounded by `maxContentChars` (12,000 by
434
+ default), with `truncated` reporting trimming. `limit` is 1–20; timeoutSeconds
435
+ is 1–60; Pro accepts up to five sites and date filters. See
436
+ https://platform.kimi.ai/docs/api/tools-search,
437
+ https://platform.kimi.ai/docs/api/tools-search-pro and
438
+ https://platform.kimi.ai/docs/api/tools-fetch.
439
+
440
+ Use `invoke()` for the usage/report envelope. Per response, billable Basic,
441
+ Pro and Fetch units appear as `kimiSearchRequests`, `kimiSearchProRequests`
442
+ and `kimiFetchRequests`, alongside common search/fetch counters. Agents merge
443
+ these into their totals. `KIMI_SERVER_TOOL_PRICING` supplies the default rates
444
+ in `ModelPricing.serverTools` format; only namespaced keys are priced to avoid
445
+ double charges. Override `pricing` or pass `{}` to leave calls unpriced.
446
+ Official prices verified 2026-10-18: $0.002 / $0.003 / $0.002 respectively,
447
+ excluding taxes, only for HTTP 200 with nonempty search results/nonblank
448
+ Markdown: https://platform.kimi.ai/docs/pricing/websearch.
449
+
450
+ The legacy `$web_search` builtin retires on 20 October 2026 and is rejected.
451
+ Abort signals cancel transport; retries are disabled. `invoke().error` is a
452
+ `ToolExecutionError` whose `cause` is the mapped provider error (or
453
+ `AgentCancelledError` for caller cancellation). `execute()` returns data only
454
+ and throws mapped errors. Never infer fees from function calls or prose.
455
+
258
456
 
package/llms.txt CHANGED
@@ -6,4 +6,4 @@
6
6
 
7
7
  ## Skills
8
8
 
9
- - [setup-openai](@warlock.js/ai-openai/setup-openai/SKILL.md): 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`.
9
+ - [setup-openai](@warlock.js/ai-openai/setup-openai/SKILL.md): 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`, `kimiSearchTool`, `kimiFetchTool`, `KIMI_SERVER_TOOL_PRICING`, `.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`.
package/package.json CHANGED
@@ -15,12 +15,12 @@
15
15
  },
16
16
  "dependencies": {
17
17
  "openai": "^7.23.0",
18
- "@warlock.js/logger": "5.30.0"
18
+ "@warlock.js/logger": "5.32.0"
19
19
  },
20
20
  "peerDependencies": {
21
- "@warlock.js/ai": "5.30.0"
21
+ "@warlock.js/ai": "5.32.0"
22
22
  },
23
- "version": "5.30.0",
23
+ "version": "5.32.0",
24
24
  "main": "./cjs/index.cjs",
25
25
  "module": "./esm/index.mjs",
26
26
  "types": "./esm/index.d.mts",