@warlock.js/ai-openai 5.31.0 → 5.33.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 (46) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/cjs/index.cjs +304 -8
  3. package/cjs/index.cjs.map +1 -1
  4. package/esm/config.type.d.mts +8 -10
  5. package/esm/config.type.d.mts.map +1 -1
  6. package/esm/embedder.d.mts +1 -3
  7. package/esm/embedder.d.mts.map +1 -1
  8. package/esm/image.d.mts +1 -3
  9. package/esm/image.d.mts.map +1 -1
  10. package/esm/index.d.mts +4 -3
  11. package/esm/index.mjs +3 -2
  12. package/esm/kimi-tools.d.mts +69 -0
  13. package/esm/kimi-tools.d.mts.map +1 -0
  14. package/esm/kimi-tools.mjs +256 -0
  15. package/esm/kimi-tools.mjs.map +1 -0
  16. package/esm/known-pricing.d.mts +8 -4
  17. package/esm/known-pricing.d.mts.map +1 -1
  18. package/esm/known-pricing.mjs +5 -1
  19. package/esm/known-pricing.mjs.map +1 -1
  20. package/esm/model.mjs +8 -1
  21. package/esm/model.mjs.map +1 -1
  22. package/esm/provider-options.d.mts +7 -2
  23. package/esm/provider-options.d.mts.map +1 -1
  24. package/esm/provider-options.mjs +9 -2
  25. package/esm/provider-options.mjs.map +1 -1
  26. package/esm/responses-model.mjs +11 -4
  27. package/esm/responses-model.mjs.map +1 -1
  28. package/esm/sdk.d.mts +1 -3
  29. package/esm/sdk.d.mts.map +1 -1
  30. package/esm/speech.d.mts +1 -3
  31. package/esm/speech.d.mts.map +1 -1
  32. package/esm/transcription.d.mts +1 -3
  33. package/esm/transcription.d.mts.map +1 -1
  34. package/esm/utils/hosted-output.mjs +22 -0
  35. package/esm/utils/hosted-output.mjs.map +1 -0
  36. package/esm/utils/index.mjs +1 -1
  37. package/esm/utils/prompt-cache-params.mjs +1 -1
  38. package/esm/utils/prompt-cache-params.mjs.map +1 -1
  39. package/esm/utils/reasoning-replay.mjs +1 -1
  40. package/esm/utils/reasoning-replay.mjs.map +1 -1
  41. package/esm/utils/structured-output.mjs +1 -1
  42. package/esm/utils/to-openai-messages.mjs.map +1 -1
  43. package/llms-full.txt +108 -1
  44. package/llms.txt +1 -1
  45. package/package.json +3 -3
  46. package/skills/setup-openai/SKILL.md +108 -1
@@ -1 +1 @@
1
- {"version":3,"file":"prompt-cache-params.mjs","names":[],"sources":["../../../../../../../../packages/ai-openai/src/utils/prompt-cache-params.ts"],"sourcesContent":["import type { OpenAIModelConfig } from \"../config.type\";\n\n/** The prompt-cache request fields common to Chat Completions and Responses. */\nexport type PromptCacheParams = {\n prompt_cache_key?: string;\n prompt_cache_retention?: NonNullable<OpenAIModelConfig[\"promptCacheRetention\"]>;\n};\n\n/**\n * Provider labels whose OpenAI-compatible endpoint documents a\n * `prompt_cache_key` request field:\n *\n * - `openai` — https://developers.openai.com/api/docs/guides/prompt-caching\n * - `xai` — Chat Completions accepts `prompt_cache_key` (\"plumbed to\n * x-grok-conv-id\"), https://docs.x.ai/openapi.json and\n * https://docs.x.ai/developers/advanced-api-usage/prompt-caching/maximizing-cache-hits\n * - `mistral` — https://docs.mistral.ai/studio/conversations/advanced/prompt-caching\n *\n * Groq and DeepSeek cache automatically and document no request hint, so\n * they receive nothing (checked 2026-10-17).\n */\nconst PROMPT_CACHE_KEY_PROVIDERS: ReadonlySet<string> = new Set([\"openai\", \"xai\", \"mistral\"]);\n\n/**\n * Build the prompt-cache request fields from the model config.\n * `prompt_cache_key` is sent for the providers that document it\n * ({@link PROMPT_CACHE_KEY_PROVIDERS}); `prompt_cache_retention` is\n * OpenAI-only. Any other provider label — including a custom label on a\n * wrapper SDK — gets neither, since an unknown endpoint may reject them.\n */\nexport function buildPromptCacheParams(\n provider: string,\n config: Pick<OpenAIModelConfig, \"promptCacheKey\" | \"promptCacheRetention\">,\n): PromptCacheParams {\n const sendsKey = PROMPT_CACHE_KEY_PROVIDERS.has(provider);\n\n return {\n ...(sendsKey && config.promptCacheKey !== undefined\n ? { prompt_cache_key: config.promptCacheKey }\n : {}),\n ...(provider === \"openai\" && config.promptCacheRetention !== undefined\n ? { prompt_cache_retention: config.promptCacheRetention }\n : {}),\n };\n}\n"],"mappings":";;;;;;;;;;;;;;AAqBA,MAAM,6BAAkD,IAAI,IAAI;CAAC;CAAU;CAAO;AAAS,CAAC;;;;;;;;AAS5F,SAAgB,uBACd,UACA,QACmB;CAGnB,OAAO;EACL,GAHe,2BAA2B,IAAI,QAGnC,KAAK,OAAO,mBAAmB,SACtC,EAAE,kBAAkB,OAAO,eAAe,IAC1C,CAAC;EACL,GAAI,aAAa,YAAY,OAAO,yBAAyB,SACzD,EAAE,wBAAwB,OAAO,qBAAqB,IACtD,CAAC;CACP;AACF"}
1
+ {"version":3,"file":"prompt-cache-params.mjs","names":[],"sources":["../../../../../../../../packages/ai-openai/src/utils/prompt-cache-params.ts"],"sourcesContent":["import type { OpenAIModelConfig } from \"../config.type\";\n\n/** The prompt-cache request fields common to Chat Completions and Responses. */\nexport type PromptCacheParams = {\n prompt_cache_key?: string;\n prompt_cache_retention?: NonNullable<OpenAIModelConfig[\"promptCacheRetention\"]>;\n};\n\n/**\n * Provider labels whose OpenAI-compatible endpoint documents a\n * `prompt_cache_key` request field:\n *\n * - `openai` — https://developers.openai.com/api/docs/guides/prompt-caching\n * - `xai` — Chat Completions accepts `prompt_cache_key` (\"plumbed to\n * x-grok-conv-id\"), https://docs.x.ai/openapi.json and\n * https://docs.x.ai/developers/advanced-api-usage/prompt-caching/maximizing-cache-hits\n * - `mistral` — https://docs.mistral.ai/studio/conversations/advanced/prompt-caching\n *\n * Groq and DeepSeek cache automatically and document no request hint, so\n * they receive nothing (checked 2026-10-17).\n */\nconst PROMPT_CACHE_KEY_PROVIDERS: ReadonlySet<string> = new Set([\"openai\", \"xai\", \"mistral\"]);\n\n/**\n * Build the prompt-cache request fields from the model config.\n * `prompt_cache_key` is sent for the providers that document it\n * ({@link PROMPT_CACHE_KEY_PROVIDERS}); `prompt_cache_retention` is\n * OpenAI-only. Any other provider label — including a custom label on a\n * wrapper SDK — gets neither, since an unknown endpoint may reject them.\n */\nexport function buildPromptCacheParams(\n provider: string,\n config: Pick<OpenAIModelConfig, \"promptCacheKey\" | \"promptCacheRetention\">,\n): PromptCacheParams {\n const sendsKey = PROMPT_CACHE_KEY_PROVIDERS.has(provider);\n\n return {\n ...(sendsKey && config.promptCacheKey !== undefined\n ? { prompt_cache_key: config.promptCacheKey }\n : {}),\n ...(provider === \"openai\" && config.promptCacheRetention !== undefined\n ? { prompt_cache_retention: config.promptCacheRetention }\n : {}),\n };\n}\n"],"mappings":";;;;;;;;;;;;;;AAqBA,MAAM,6CAAkD,IAAI,IAAI;CAAC;CAAU;CAAO;AAAS,CAAC;;;;;;;;AAS5F,SAAgB,uBACd,UACA,QACmB;CAGnB,OAAO;EACL,GAHe,2BAA2B,IAAI,QAGnC,KAAK,OAAO,mBAAmB,SACtC,EAAE,kBAAkB,OAAO,eAAe,IAC1C,CAAC;EACL,GAAI,aAAa,YAAY,OAAO,yBAAyB,SACzD,EAAE,wBAAwB,OAAO,qBAAqB,IACtD,CAAC;CACP;AACF"}
@@ -15,7 +15,7 @@ function pickReplayableReasoning(items) {
15
15
  return items.filter((item) => typeof item.encrypted_content === "string" && item.encrypted_content !== "");
16
16
  }
17
17
  /** Output item types that may follow a replayed reasoning item. */
18
- const REPLAY_SAFE_FOLLOWERS = new Set(["function_call", "message"]);
18
+ const REPLAY_SAFE_FOLLOWERS = /* @__PURE__ */ new Set(["function_call", "message"]);
19
19
  /**
20
20
  * The reasoning items of a turn that are safe to replay, in order.
21
21
  *
@@ -1 +1 @@
1
- {"version":3,"file":"reasoning-replay.mjs","names":[],"sources":["../../../../../../../../packages/ai-openai/src/utils/reasoning-replay.ts"],"sourcesContent":["import type { ModelToolCallRequest } from \"@warlock.js/ai\";\nimport type OpenAI from \"openai\";\n\n/** A `reasoning` output item as the Responses API returns it. */\nexport type ResponsesReasoningItem = OpenAI.Responses.ResponseReasoningItem;\n\n/**\n * Key under `ModelToolCallRequest.providerMetadata` owned by the Responses\n * adapter. This name and the shape below are PERSISTED in users' conversation\n * memory, so they are part of the public contract: do not rename.\n */\nexport const RESPONSES_METADATA_KEY = \"openaiResponses\";\n\n/**\n * What the Responses adapter stores on the FIRST tool call of an assistant\n * turn so the next request can replay the model's reasoning (design\n * section 4, decisions 2 and 3).\n */\nexport type ResponsesReplayMetadata = {\n /** Model the items were produced by; replay is skipped when it differs. */\n model: string;\n /** Reasoning items of the turn, in the order the API emitted them. */\n reasoningItems: ResponsesReasoningItem[];\n};\n\n/**\n * Keep only the reasoning items that can actually be replayed. With\n * `store: false` the server keeps nothing, so an item without\n * `encrypted_content` has nothing to restore and replaying its bare `id`\n * would be rejected as an unknown item.\n */\nexport function pickReplayableReasoning(\n items: readonly ResponsesReasoningItem[],\n): ResponsesReasoningItem[] {\n return items.filter(\n (item) => typeof item.encrypted_content === \"string\" && item.encrypted_content !== \"\",\n );\n}\n\n/** Output item types that may follow a replayed reasoning item. */\nconst REPLAY_SAFE_FOLLOWERS: ReadonlySet<string> = new Set([\"function_call\", \"message\"]);\n\n/**\n * The reasoning items of a turn that are safe to replay, in order.\n *\n * The API pairs a reasoning item with the output item that followed it and\n * rejects a replayed reasoning item whose follower is missing. Only\n * `function_call` items are replayed, so a reasoning item that led into a\n * hosted (server-run) tool call — `web_search_call`, `file_search_call`,\n * `code_interpreter_call`, … from a provider-defined tool — is dropped\n * rather than sent without its follower. Encrypted reasoning only improves\n * continuity; dropping it never breaks the request.\n */\nexport function pickTurnReasoning(\n output: ReadonlyArray<{ type: string }>,\n): ResponsesReasoningItem[] {\n const picked: ResponsesReasoningItem[] = [];\n\n output.forEach((item, index) => {\n if (item.type !== \"reasoning\") {\n return;\n }\n\n const follower = output.slice(index + 1).find((next) => next.type !== \"reasoning\");\n\n if (follower === undefined || REPLAY_SAFE_FOLLOWERS.has(follower.type)) {\n picked.push(item as ResponsesReasoningItem);\n }\n });\n\n return picked;\n}\n\n/**\n * Attach the turn's reasoning items to the FIRST tool call only (parallel\n * calls do not each repeat the blobs). Returns the calls unchanged when\n * there is nothing to replay or no tool call to ride on (a text-only turn is\n * deliberately not replayed, decision 2). Does not mutate its input.\n */\nexport function attachReasoningReplay(\n toolCalls: readonly ModelToolCallRequest[],\n model: string,\n reasoningItems: readonly ResponsesReasoningItem[],\n): ModelToolCallRequest[] {\n const replayable = pickReplayableReasoning(reasoningItems);\n const [first, ...rest] = toolCalls;\n\n if (first === undefined || replayable.length === 0) {\n return [...toolCalls];\n }\n\n const metadata: ResponsesReplayMetadata = { model, reasoningItems: replayable };\n\n return [\n {\n ...first,\n providerMetadata: { ...first.providerMetadata, [RESPONSES_METADATA_KEY]: metadata },\n },\n ...rest,\n ];\n}\n\n/**\n * Read the stored reasoning items off an assistant turn's tool calls, for\n * replay on `model`. Returns `[]` when the key is absent or malformed, or\n * when the stored `model` differs from `model` (encrypted blobs are not\n * portable across models, design section 4). De-duplicates by item `id`.\n */\nexport function readReasoningReplay(\n toolCalls: readonly ModelToolCallRequest[],\n model: string,\n): ResponsesReasoningItem[] {\n const seen = new Set<string>();\n const items: ResponsesReasoningItem[] = [];\n\n for (const call of toolCalls) {\n const stored = call.providerMetadata?.[RESPONSES_METADATA_KEY];\n\n if (!isReplayMetadata(stored) || stored.model !== model) {\n continue;\n }\n\n for (const item of stored.reasoningItems) {\n if (item?.type !== \"reasoning\" || seen.has(item.id)) {\n continue;\n }\n\n seen.add(item.id);\n items.push(item);\n }\n }\n\n return items;\n}\n\n/**\n * Shape a stored reasoning item for the next request. Sends the four fields\n * a stateless replay needs (`type`, `id`, `summary`, `encrypted_content`) and\n * drops the response-only `status` and the optional raw `content`.\n *\n * UNVERIFIED: the SDK types accept the full item as input (`ResponseInputItem`\n * includes `ResponseReasoningItem`) and do not say which fields the API\n * refuses on replay with `store: false`. This is the conservative guess; a\n * live probe settles it.\n */\nexport function toReplayReasoningItem(item: ResponsesReasoningItem): ResponsesReasoningItem {\n return {\n type: \"reasoning\",\n id: item.id,\n summary: Array.isArray(item.summary) ? item.summary : [],\n encrypted_content: item.encrypted_content,\n };\n}\n\nfunction isReplayMetadata(value: unknown): value is ResponsesReplayMetadata {\n if (typeof value !== \"object\" || value === null) {\n return false;\n }\n\n const candidate = value as Partial<ResponsesReplayMetadata>;\n\n return typeof candidate.model === \"string\" && Array.isArray(candidate.reasoningItems);\n}\n"],"mappings":";;;;;;AAWA,MAAa,yBAAyB;;;;;;;AAoBtC,SAAgB,wBACd,OAC0B;CAC1B,OAAO,MAAM,QACV,SAAS,OAAO,KAAK,sBAAsB,YAAY,KAAK,sBAAsB,EACrF;AACF;;AAGA,MAAM,wBAA6C,IAAI,IAAI,CAAC,iBAAiB,SAAS,CAAC;;;;;;;;;;;;AAavF,SAAgB,kBACd,QAC0B;CAC1B,MAAM,SAAmC,CAAC;CAE1C,OAAO,SAAS,MAAM,UAAU;EAC9B,IAAI,KAAK,SAAS,aAChB;EAGF,MAAM,WAAW,OAAO,MAAM,QAAQ,CAAC,CAAC,CAAC,MAAM,SAAS,KAAK,SAAS,WAAW;EAEjF,IAAI,aAAa,UAAa,sBAAsB,IAAI,SAAS,IAAI,GACnE,OAAO,KAAK,IAA8B;CAE9C,CAAC;CAED,OAAO;AACT;;;;;;;AAQA,SAAgB,sBACd,WACA,OACA,gBACwB;CACxB,MAAM,aAAa,wBAAwB,cAAc;CACzD,MAAM,CAAC,OAAO,GAAG,QAAQ;CAEzB,IAAI,UAAU,UAAa,WAAW,WAAW,GAC/C,OAAO,CAAC,GAAG,SAAS;CAGtB,MAAM,WAAoC;EAAE;EAAO,gBAAgB;CAAW;CAE9E,OAAO,CACL;EACE,GAAG;EACH,kBAAkB;GAAE,GAAG,MAAM;IAAmB,yBAAyB;EAAS;CACpF,GACA,GAAG,IACL;AACF;;;;;;;AAQA,SAAgB,oBACd,WACA,OAC0B;CAC1B,MAAM,uBAAO,IAAI,IAAY;CAC7B,MAAM,QAAkC,CAAC;CAEzC,KAAK,MAAM,QAAQ,WAAW;EAC5B,MAAM,SAAS,KAAK,mBAAmB;EAEvC,IAAI,CAAC,iBAAiB,MAAM,KAAK,OAAO,UAAU,OAChD;EAGF,KAAK,MAAM,QAAQ,OAAO,gBAAgB;GACxC,IAAI,MAAM,SAAS,eAAe,KAAK,IAAI,KAAK,EAAE,GAChD;GAGF,KAAK,IAAI,KAAK,EAAE;GAChB,MAAM,KAAK,IAAI;EACjB;CACF;CAEA,OAAO;AACT;;;;;;;;;;;AAYA,SAAgB,sBAAsB,MAAsD;CAC1F,OAAO;EACL,MAAM;EACN,IAAI,KAAK;EACT,SAAS,MAAM,QAAQ,KAAK,OAAO,IAAI,KAAK,UAAU,CAAC;EACvD,mBAAmB,KAAK;CAC1B;AACF;AAEA,SAAS,iBAAiB,OAAkD;CAC1E,IAAI,OAAO,UAAU,YAAY,UAAU,MACzC,OAAO;CAGT,MAAM,YAAY;CAElB,OAAO,OAAO,UAAU,UAAU,YAAY,MAAM,QAAQ,UAAU,cAAc;AACtF"}
1
+ {"version":3,"file":"reasoning-replay.mjs","names":[],"sources":["../../../../../../../../packages/ai-openai/src/utils/reasoning-replay.ts"],"sourcesContent":["import type { ModelToolCallRequest } from \"@warlock.js/ai\";\nimport type OpenAI from \"openai\";\n\n/** A `reasoning` output item as the Responses API returns it. */\nexport type ResponsesReasoningItem = OpenAI.Responses.ResponseReasoningItem;\n\n/**\n * Key under `ModelToolCallRequest.providerMetadata` owned by the Responses\n * adapter. This name and the shape below are PERSISTED in users' conversation\n * memory, so they are part of the public contract: do not rename.\n */\nexport const RESPONSES_METADATA_KEY = \"openaiResponses\";\n\n/**\n * What the Responses adapter stores on the FIRST tool call of an assistant\n * turn so the next request can replay the model's reasoning (design\n * section 4, decisions 2 and 3).\n */\nexport type ResponsesReplayMetadata = {\n /** Model the items were produced by; replay is skipped when it differs. */\n model: string;\n /** Reasoning items of the turn, in the order the API emitted them. */\n reasoningItems: ResponsesReasoningItem[];\n};\n\n/**\n * Keep only the reasoning items that can actually be replayed. With\n * `store: false` the server keeps nothing, so an item without\n * `encrypted_content` has nothing to restore and replaying its bare `id`\n * would be rejected as an unknown item.\n */\nexport function pickReplayableReasoning(\n items: readonly ResponsesReasoningItem[],\n): ResponsesReasoningItem[] {\n return items.filter(\n (item) => typeof item.encrypted_content === \"string\" && item.encrypted_content !== \"\",\n );\n}\n\n/** Output item types that may follow a replayed reasoning item. */\nconst REPLAY_SAFE_FOLLOWERS: ReadonlySet<string> = new Set([\"function_call\", \"message\"]);\n\n/**\n * The reasoning items of a turn that are safe to replay, in order.\n *\n * The API pairs a reasoning item with the output item that followed it and\n * rejects a replayed reasoning item whose follower is missing. Only\n * `function_call` items are replayed, so a reasoning item that led into a\n * hosted (server-run) tool call — `web_search_call`, `file_search_call`,\n * `code_interpreter_call`, … from a provider-defined tool — is dropped\n * rather than sent without its follower. Encrypted reasoning only improves\n * continuity; dropping it never breaks the request.\n */\nexport function pickTurnReasoning(\n output: ReadonlyArray<{ type: string }>,\n): ResponsesReasoningItem[] {\n const picked: ResponsesReasoningItem[] = [];\n\n output.forEach((item, index) => {\n if (item.type !== \"reasoning\") {\n return;\n }\n\n const follower = output.slice(index + 1).find((next) => next.type !== \"reasoning\");\n\n if (follower === undefined || REPLAY_SAFE_FOLLOWERS.has(follower.type)) {\n picked.push(item as ResponsesReasoningItem);\n }\n });\n\n return picked;\n}\n\n/**\n * Attach the turn's reasoning items to the FIRST tool call only (parallel\n * calls do not each repeat the blobs). Returns the calls unchanged when\n * there is nothing to replay or no tool call to ride on (a text-only turn is\n * deliberately not replayed, decision 2). Does not mutate its input.\n */\nexport function attachReasoningReplay(\n toolCalls: readonly ModelToolCallRequest[],\n model: string,\n reasoningItems: readonly ResponsesReasoningItem[],\n): ModelToolCallRequest[] {\n const replayable = pickReplayableReasoning(reasoningItems);\n const [first, ...rest] = toolCalls;\n\n if (first === undefined || replayable.length === 0) {\n return [...toolCalls];\n }\n\n const metadata: ResponsesReplayMetadata = { model, reasoningItems: replayable };\n\n return [\n {\n ...first,\n providerMetadata: { ...first.providerMetadata, [RESPONSES_METADATA_KEY]: metadata },\n },\n ...rest,\n ];\n}\n\n/**\n * Read the stored reasoning items off an assistant turn's tool calls, for\n * replay on `model`. Returns `[]` when the key is absent or malformed, or\n * when the stored `model` differs from `model` (encrypted blobs are not\n * portable across models, design section 4). De-duplicates by item `id`.\n */\nexport function readReasoningReplay(\n toolCalls: readonly ModelToolCallRequest[],\n model: string,\n): ResponsesReasoningItem[] {\n const seen = new Set<string>();\n const items: ResponsesReasoningItem[] = [];\n\n for (const call of toolCalls) {\n const stored = call.providerMetadata?.[RESPONSES_METADATA_KEY];\n\n if (!isReplayMetadata(stored) || stored.model !== model) {\n continue;\n }\n\n for (const item of stored.reasoningItems) {\n if (item?.type !== \"reasoning\" || seen.has(item.id)) {\n continue;\n }\n\n seen.add(item.id);\n items.push(item);\n }\n }\n\n return items;\n}\n\n/**\n * Shape a stored reasoning item for the next request. Sends the four fields\n * a stateless replay needs (`type`, `id`, `summary`, `encrypted_content`) and\n * drops the response-only `status` and the optional raw `content`.\n *\n * UNVERIFIED: the SDK types accept the full item as input (`ResponseInputItem`\n * includes `ResponseReasoningItem`) and do not say which fields the API\n * refuses on replay with `store: false`. This is the conservative guess; a\n * live probe settles it.\n */\nexport function toReplayReasoningItem(item: ResponsesReasoningItem): ResponsesReasoningItem {\n return {\n type: \"reasoning\",\n id: item.id,\n summary: Array.isArray(item.summary) ? item.summary : [],\n encrypted_content: item.encrypted_content,\n };\n}\n\nfunction isReplayMetadata(value: unknown): value is ResponsesReplayMetadata {\n if (typeof value !== \"object\" || value === null) {\n return false;\n }\n\n const candidate = value as Partial<ResponsesReplayMetadata>;\n\n return typeof candidate.model === \"string\" && Array.isArray(candidate.reasoningItems);\n}\n"],"mappings":";;;;;;AAWA,MAAa,yBAAyB;;;;;;;AAoBtC,SAAgB,wBACd,OAC0B;CAC1B,OAAO,MAAM,QACV,SAAS,OAAO,KAAK,sBAAsB,YAAY,KAAK,sBAAsB,EACrF;AACF;;AAGA,MAAM,wCAA6C,IAAI,IAAI,CAAC,iBAAiB,SAAS,CAAC;;;;;;;;;;;;AAavF,SAAgB,kBACd,QAC0B;CAC1B,MAAM,SAAmC,CAAC;CAE1C,OAAO,SAAS,MAAM,UAAU;EAC9B,IAAI,KAAK,SAAS,aAChB;EAGF,MAAM,WAAW,OAAO,MAAM,QAAQ,CAAC,CAAC,CAAC,MAAM,SAAS,KAAK,SAAS,WAAW;EAEjF,IAAI,aAAa,UAAa,sBAAsB,IAAI,SAAS,IAAI,GACnE,OAAO,KAAK,IAA8B;CAE9C,CAAC;CAED,OAAO;AACT;;;;;;;AAQA,SAAgB,sBACd,WACA,OACA,gBACwB;CACxB,MAAM,aAAa,wBAAwB,cAAc;CACzD,MAAM,CAAC,OAAO,GAAG,QAAQ;CAEzB,IAAI,UAAU,UAAa,WAAW,WAAW,GAC/C,OAAO,CAAC,GAAG,SAAS;CAGtB,MAAM,WAAoC;EAAE;EAAO,gBAAgB;CAAW;CAE9E,OAAO,CACL;EACE,GAAG;EACH,kBAAkB;GAAE,GAAG,MAAM;IAAmB,yBAAyB;EAAS;CACpF,GACA,GAAG,IACL;AACF;;;;;;;AAQA,SAAgB,oBACd,WACA,OAC0B;CAC1B,MAAM,uBAAO,IAAI,IAAY;CAC7B,MAAM,QAAkC,CAAC;CAEzC,KAAK,MAAM,QAAQ,WAAW;EAC5B,MAAM,SAAS,KAAK,mBAAmB;EAEvC,IAAI,CAAC,iBAAiB,MAAM,KAAK,OAAO,UAAU,OAChD;EAGF,KAAK,MAAM,QAAQ,OAAO,gBAAgB;GACxC,IAAI,MAAM,SAAS,eAAe,KAAK,IAAI,KAAK,EAAE,GAChD;GAGF,KAAK,IAAI,KAAK,EAAE;GAChB,MAAM,KAAK,IAAI;EACjB;CACF;CAEA,OAAO;AACT;;;;;;;;;;;AAYA,SAAgB,sBAAsB,MAAsD;CAC1F,OAAO;EACL,MAAM;EACN,IAAI,KAAK;EACT,SAAS,MAAM,QAAQ,KAAK,OAAO,IAAI,KAAK,UAAU,CAAC;EACvD,mBAAmB,KAAK;CAC1B;AACF;AAEA,SAAS,iBAAiB,OAAkD;CAC1E,IAAI,OAAO,UAAU,YAAY,UAAU,MACzC,OAAO;CAGT,MAAM,YAAY;CAElB,OAAO,OAAO,UAAU,UAAU,YAAY,MAAM,QAAQ,UAAU,cAAc;AACtF"}
@@ -80,5 +80,5 @@ function isStrictSafeNode(node) {
80
80
  }
81
81
 
82
82
  //#endregion
83
- export { inferStructuredOutput, planStructuredOutput };
83
+ export { inferStructuredOutput, isStrictCompatible, planStructuredOutput };
84
84
  //# sourceMappingURL=structured-output.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"to-openai-messages.mjs","names":[],"sources":["../../../../../../../../packages/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 */\nexport function 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 return { type: \"image_url\", image_url: { url: toImageUrl(part.source) } };\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 * Render an image source as a URL: a remote URL passes through, inlined\n * base64 bytes become a `data:` URL. Shared with the Responses adapter.\n */\nexport function toImageUrl(source: Extract<ContentPart, { type: \"image\" }>[\"source\"]): string {\n return \"url\" in source ? source.url : `data:${source.mediaType};base64,${source.base64}`;\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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"}
1
+ {"version":3,"file":"to-openai-messages.mjs","names":[],"sources":["../../../../../../../../packages/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: {\n name: tc.name,\n arguments: JSON.stringify(tc.input ?? {}),\n },\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 */\nexport function 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 return { type: \"image_url\", image_url: { url: toImageUrl(part.source) } };\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 * Render an image source as a URL: a remote URL passes through, inlined\n * base64 bytes become a `data:` URL. Shared with the Responses adapter.\n */\nexport function toImageUrl(source: Extract<ContentPart, { type: \"image\" }>[\"source\"]): string {\n return \"url\" in source ? source.url : `data:${source.mediaType};base64,${source.base64}`;\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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"}
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: `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`.'
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`
@@ -329,6 +329,13 @@ used for generation are counted. The endpoint supports only input-shaping fields
329
329
  output caps and generation-only options are omitted. The Chat Completions model
330
330
  has no `countTokens` method. `sdk.count(text)` remains a separate offline estimate.
331
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
+
332
339
  ```ts
333
340
  if (model.countTokens) {
334
341
  const inputTokens = await model.countTokens(messages, options);
@@ -346,4 +353,104 @@ if (model.countTokens) {
346
353
  - The `embed-text` topic of the `warlock-js-ai` skill — embedder usage
347
354
  - The `pick-ai-provider` topic of the `warlock-js-ai` skill — adapter comparison
348
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
+
349
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: `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`.
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.31.0"
18
+ "@warlock.js/logger": "5.33.0"
19
19
  },
20
20
  "peerDependencies": {
21
- "@warlock.js/ai": "5.31.0"
21
+ "@warlock.js/ai": "5.33.0"
22
22
  },
23
- "version": "5.31.0",
23
+ "version": "5.33.0",
24
24
  "main": "./cjs/index.cjs",
25
25
  "module": "./esm/index.mjs",
26
26
  "types": "./esm/index.d.mts",
@@ -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: `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`.'
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`, `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`.'
4
4
  ---
5
5
 
6
6
  # `@warlock.js/ai-openai`
@@ -321,6 +321,13 @@ used for generation are counted. The endpoint supports only input-shaping fields
321
321
  output caps and generation-only options are omitted. The Chat Completions model
322
322
  has no `countTokens` method. `sdk.count(text)` remains a separate offline estimate.
323
323
 
324
+ For budget preflight, `countInputTokens(model, messages, { tools })` from
325
+ `@warlock.js/ai` returns `{ tokens, source }`. It estimates only when native
326
+ counting is absent or explicitly throws `InvalidRequestError` with
327
+ `reason: "count-unsupported"`; this adapter preserves that signal. System and
328
+ tools are supported natively. Ordinary validation, auth, network and rate-limit
329
+ errors propagate. See the cost/budget guide for reserve mode.
330
+
324
331
  ```ts
325
332
  if (model.countTokens) {
326
333
  const inputTokens = await model.countTokens(messages, options);
@@ -337,3 +344,103 @@ if (model.countTokens) {
337
344
  - The `run-ai-agent` topic of the `warlock-js-ai` skill — passing the model into `ai.agent({...})`
338
345
  - The `embed-text` topic of the `warlock-js-ai` skill — embedder usage
339
346
  - The `pick-ai-provider` topic of the `warlock-js-ai` skill — adapter comparison
347
+
348
+ ## Hosted-search metering and metadata
349
+
350
+ On Responses, each structured `web_search_call` output item contributes one
351
+ `usage.serverToolUse.webSearchRequests`. Streaming item events and terminal
352
+ output are reconciled as snapshots, so the same call is counted once.
353
+ `response.providerMetadata.openai.output` (also on streaming `done`) retains
354
+ hosted output items with their action and sources. The adapter automatically
355
+ requests `web_search_call.action.sources` when web-search tools are supplied.
356
+ Agent reports preserve this metadata on each trip; it is not replayed or
357
+ passed to function dispatch.
358
+
359
+ Explicitly set `pricing.serverTools: OPENAI_SERVER_TOOL_PRICING` (exported by
360
+ this package) for standard `web_search`: $0.01 per tool call, plus search-content
361
+ tokens at model rates. Non-reasoning `web_search_preview` has a different fee
362
+ and needs an explicit override. These prices were verified on 2026-10-18 from
363
+ [OpenAI pricing](https://developers.openai.com/api/docs/pricing#built-in-tools);
364
+ see the [official search output schema](https://developers.openai.com/api/docs/guides/tools-web-search)
365
+ and [cost tracking](https://warlock.js.org/v/latest/ai/recipes/cost-tracking/#hosted-tool-request-fees).
366
+ Chat Completions does not map hosted-search billing counters.
367
+
368
+ Qwen's `enableSearch` flag and Kimi's standalone `kimiSearchTool()` /
369
+ `kimiFetchTool()` have separate controls; Kimi's legacy builtin retires on
370
+ 20 October 2026 and is unsupported; see [OpenAI-compatible native search](https://warlock.js.org/v/latest/ai/providers/openai-compatible/).
371
+
372
+ ```ts
373
+ import { OPENAI_SERVER_TOOL_PRICING } from "@warlock.js/ai-openai";
374
+ const model = openai.model({
375
+ name: "gpt-4.1",
376
+ api: "responses",
377
+ pricing: { ...{ input: 2, output: 8 }, serverTools: OPENAI_SERVER_TOOL_PRICING },
378
+ });
379
+ const response = await model.complete(messages, {
380
+ providerTools: [{ provider: "openai", definition: { type: "web_search" } }],
381
+ });
382
+ console.log(response.usage.serverToolUse?.webSearchRequests);
383
+ console.log(response.providerMetadata);
384
+ ```
385
+
386
+ ## Kimi first-party search and fetch
387
+
388
+ Use the same API key/base URL configuration for the model and client tools:
389
+
390
+ ```ts
391
+ import { ai, createToolContext } from "@warlock.js/ai";
392
+ import { OpenAISDK, kimiSearchTool, kimiFetchTool } from "@warlock.js/ai-openai";
393
+
394
+ const config = {
395
+ apiKey: process.env.MOONSHOT_API_KEY ?? "",
396
+ baseURL: "https://api.moonshot.ai/v1",
397
+ };
398
+ const sdk = new OpenAISDK({ ...config, provider: "moonshot" });
399
+ const basic = kimiSearchTool({ ...config, includeContent: false });
400
+ const pro = kimiSearchTool({
401
+ ...config,
402
+ mode: "pro",
403
+ sites: ["moonshot.ai"],
404
+ timeWindow: { start: "2026-01", end: "2026-10" },
405
+ limit: 5,
406
+ timeoutSeconds: 30,
407
+ });
408
+ const fetchPage = kimiFetchTool({ ...config, maxContentChars: 12_000 });
409
+ const assistant = ai.agent({ model: sdk.model({ name: "kimi-k3" }), tools: [pro, fetchPage] });
410
+ const result = await assistant.execute("Research prompt caching");
411
+ console.log(result.usage.serverToolUse, result.usage.cost?.serverTools);
412
+
413
+ const controller = new AbortController();
414
+ const found = await basic.invoke(
415
+ { query: "Kimi prompt caching" },
416
+ createToolContext({ signal: controller.signal }),
417
+ );
418
+ console.log(found.data?.results, found.usage);
419
+ ```
420
+
421
+ Pass these functions directly as ordinary `tools`, without `ai.providerTool`.
422
+ They call `/v1/tools/search`, `/v1/tools/search_pro` or `/v1/tools/fetch` with
423
+ Bearer auth. Search arguments are `{ query }`; fetch arguments are `{ url }`.
424
+ Search results retain sources and optional full text (Basic) or scored chunks
425
+ (Pro); fetch returns Markdown. Text is bounded by `maxContentChars` (12,000 by
426
+ default), with `truncated` reporting trimming. `limit` is 1–20; timeoutSeconds
427
+ is 1–60; Pro accepts up to five sites and date filters. See
428
+ https://platform.kimi.ai/docs/api/tools-search,
429
+ https://platform.kimi.ai/docs/api/tools-search-pro and
430
+ https://platform.kimi.ai/docs/api/tools-fetch.
431
+
432
+ Use `invoke()` for the usage/report envelope. Per response, billable Basic,
433
+ Pro and Fetch units appear as `kimiSearchRequests`, `kimiSearchProRequests`
434
+ and `kimiFetchRequests`, alongside common search/fetch counters. Agents merge
435
+ these into their totals. `KIMI_SERVER_TOOL_PRICING` supplies the default rates
436
+ in `ModelPricing.serverTools` format; only namespaced keys are priced to avoid
437
+ double charges. Override `pricing` or pass `{}` to leave calls unpriced.
438
+ Official prices verified 2026-10-18: $0.002 / $0.003 / $0.002 respectively,
439
+ excluding taxes, only for HTTP 200 with nonempty search results/nonblank
440
+ Markdown: https://platform.kimi.ai/docs/pricing/websearch.
441
+
442
+ The legacy `$web_search` builtin retires on 20 October 2026 and is rejected.
443
+ Abort signals cancel transport; retries are disabled. `invoke().error` is a
444
+ `ToolExecutionError` whose `cause` is the mapped provider error (or
445
+ `AgentCancelledError` for caller cancellation). `execute()` returns data only
446
+ and throws mapped errors. Never infer fees from function calls or prose.