@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
@@ -1,3 +1,5 @@
1
+ import { computeCost } from "@warlock.js/ai";
2
+
1
3
  //#region ../../packages/ai-openai/src/utils/build-usage.ts
2
4
  /**
3
5
  * Build the neutral `Usage` from extracted counts. The optional detail
@@ -5,17 +7,27 @@
5
7
  * non-reasoning / uncached calls keep the lean `{ input, output, total }`
6
8
  * shape. Shared by the Chat Completions and Responses adapters so both
7
9
  * apply the same rule.
10
+ *
11
+ * OpenAI-style usage counts cache reads and writes inside the prompt
12
+ * total (`input_tokens` includes `cached_tokens` and `cache_write_tokens`,
13
+ * and the compatible vendors — DeepSeek, xAI, Groq, Mistral — document
14
+ * the same), so the result is marked `inputIncludesCache: true`. When the
15
+ * model is priced, `cost` is attached.
8
16
  */
9
- function buildUsage(counts) {
17
+ function buildUsage(counts, pricing) {
10
18
  const { input, output, total, cachedTokens, reasoningTokens, cacheWriteTokens } = counts;
11
- return {
19
+ const usage = {
12
20
  input,
13
21
  output,
14
22
  total,
23
+ inputIncludesCache: true,
15
24
  ...cachedTokens !== void 0 && cachedTokens > 0 ? { cachedTokens } : {},
16
25
  ...reasoningTokens !== void 0 && reasoningTokens > 0 ? { reasoningTokens } : {},
17
26
  ...cacheWriteTokens !== void 0 && cacheWriteTokens > 0 ? { cacheWriteTokens } : {}
18
27
  };
28
+ const cost = computeCost(usage, pricing);
29
+ if (cost) usage.cost = cost;
30
+ return usage;
19
31
  }
20
32
 
21
33
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"build-usage.mjs","names":[],"sources":["../../../../../../../../packages/ai-openai/src/utils/build-usage.ts"],"sourcesContent":["import type { Usage } from \"@warlock.js/ai\";\n\n/** The already-extracted token counts of one provider usage block. */\nexport type UsageCounts = {\n input: number;\n output: number;\n total: number;\n /** Subset of `input` served from the prompt cache. */\n cachedTokens?: number;\n /** Hidden reasoning channel, already counted within `output`. */\n reasoningTokens?: number;\n /** Subset of `input` written to the prompt cache. */\n cacheWriteTokens?: number;\n};\n\n/**\n * Build the neutral `Usage` from extracted counts. The optional detail\n * counters are emitted only when the provider reports a positive value, so\n * non-reasoning / uncached calls keep the lean `{ input, output, total }`\n * shape. Shared by the Chat Completions and Responses adapters so both\n * apply the same rule.\n */\nexport function buildUsage(counts: UsageCounts): Usage {\n const { input, output, total, cachedTokens, reasoningTokens, cacheWriteTokens } = counts;\n\n return {\n input,\n output,\n total,\n ...(cachedTokens !== undefined && cachedTokens > 0 ? { cachedTokens } : {}),\n ...(reasoningTokens !== undefined && reasoningTokens > 0 ? { reasoningTokens } : {}),\n ...(cacheWriteTokens !== undefined && cacheWriteTokens > 0 ? { cacheWriteTokens } : {}),\n };\n}\n"],"mappings":";;;;;;;;AAsBA,SAAgB,WAAW,QAA4B;CACrD,MAAM,EAAE,OAAO,QAAQ,OAAO,cAAc,iBAAiB,qBAAqB;CAElF,OAAO;EACL;EACA;EACA;EACA,GAAI,iBAAiB,UAAa,eAAe,IAAI,EAAE,aAAa,IAAI,CAAC;EACzE,GAAI,oBAAoB,UAAa,kBAAkB,IAAI,EAAE,gBAAgB,IAAI,CAAC;EAClF,GAAI,qBAAqB,UAAa,mBAAmB,IAAI,EAAE,iBAAiB,IAAI,CAAC;CACvF;AACF"}
1
+ {"version":3,"file":"build-usage.mjs","names":[],"sources":["../../../../../../../../packages/ai-openai/src/utils/build-usage.ts"],"sourcesContent":["import { computeCost, type ModelPricing, type Usage } from \"@warlock.js/ai\";\n\n/** The already-extracted token counts of one provider usage block. */\nexport type UsageCounts = {\n input: number;\n output: number;\n total: number;\n /** Subset of `input` served from the prompt cache. */\n cachedTokens?: number;\n /** Hidden reasoning channel, already counted within `output`. */\n reasoningTokens?: number;\n /** Subset of `input` written to the prompt cache. */\n cacheWriteTokens?: number;\n};\n\n/**\n * Build the neutral `Usage` from extracted counts. The optional detail\n * counters are emitted only when the provider reports a positive value, so\n * non-reasoning / uncached calls keep the lean `{ input, output, total }`\n * shape. Shared by the Chat Completions and Responses adapters so both\n * apply the same rule.\n *\n * OpenAI-style usage counts cache reads and writes inside the prompt\n * total (`input_tokens` includes `cached_tokens` and `cache_write_tokens`,\n * and the compatible vendors — DeepSeek, xAI, Groq, Mistral — document\n * the same), so the result is marked `inputIncludesCache: true`. When the\n * model is priced, `cost` is attached.\n */\nexport function buildUsage(counts: UsageCounts, pricing?: ModelPricing): Usage {\n const { input, output, total, cachedTokens, reasoningTokens, cacheWriteTokens } = counts;\n\n const usage: Usage = {\n input,\n output,\n total,\n inputIncludesCache: true,\n ...(cachedTokens !== undefined && cachedTokens > 0 ? { cachedTokens } : {}),\n ...(reasoningTokens !== undefined && reasoningTokens > 0 ? { reasoningTokens } : {}),\n ...(cacheWriteTokens !== undefined && cacheWriteTokens > 0 ? { cacheWriteTokens } : {}),\n };\n\n const cost = computeCost(usage, pricing);\n\n if (cost) {\n usage.cost = cost;\n }\n\n return usage;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AA4BA,SAAgB,WAAW,QAAqB,SAA+B;CAC7E,MAAM,EAAE,OAAO,QAAQ,OAAO,cAAc,iBAAiB,qBAAqB;CAElF,MAAM,QAAe;EACnB;EACA;EACA;EACA,oBAAoB;EACpB,GAAI,iBAAiB,UAAa,eAAe,IAAI,EAAE,aAAa,IAAI,CAAC;EACzE,GAAI,oBAAoB,UAAa,kBAAkB,IAAI,EAAE,gBAAgB,IAAI,CAAC;EAClF,GAAI,qBAAqB,UAAa,mBAAmB,IAAI,EAAE,iBAAiB,IAAI,CAAC;CACvF;CAEA,MAAM,OAAO,YAAY,OAAO,OAAO;CAEvC,IAAI,MACF,MAAM,OAAO;CAGf,OAAO;AACT"}
@@ -0,0 +1,22 @@
1
+ import { attachCost } from "@warlock.js/ai";
2
+
3
+ //#region ../../packages/ai-openai/src/utils/hosted-output.ts
4
+ /** Installed openai 7.23.0 ResponseFunctionWebSearch (action includes search/open_page/find).
5
+ * https://developers.openai.com/api/docs/guides/tools-web-search
6
+ * Count output items, never URLs, sources, answer text, or function calls.
7
+ * Fees and search content tokens are separate; preview pricing varies:
8
+ * https://developers.openai.com/api/docs/pricing#built-in-tools */
9
+ function hostedOutput(usage, output, pricing) {
10
+ const hosted = output.filter((item) => item.type !== "message" && item.type !== "reasoning" && item.type !== "function_call");
11
+ const searches = hosted.filter((item) => item.type === "web_search_call");
12
+ if (searches.length) usage.serverToolUse = {
13
+ ...usage.serverToolUse,
14
+ webSearchRequests: searches.length
15
+ };
16
+ attachCost(usage, pricing);
17
+ return hosted.length ? { openai: { output: hosted } } : void 0;
18
+ }
19
+
20
+ //#endregion
21
+ export { hostedOutput };
22
+ //# sourceMappingURL=hosted-output.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hosted-output.mjs","names":[],"sources":["../../../../../../../../packages/ai-openai/src/utils/hosted-output.ts"],"sourcesContent":["import { attachCost, type ModelPricing, type Usage } from \"@warlock.js/ai\";\nimport type OpenAI from \"openai\";\n\n/** Installed openai 7.23.0 ResponseFunctionWebSearch (action includes search/open_page/find).\n * https://developers.openai.com/api/docs/guides/tools-web-search\n * Count output items, never URLs, sources, answer text, or function calls.\n * Fees and search content tokens are separate; preview pricing varies:\n * https://developers.openai.com/api/docs/pricing#built-in-tools */\nexport function hostedOutput(\n usage: Usage,\n output: OpenAI.Responses.ResponseOutputItem[],\n pricing: ModelPricing | undefined,\n): Record<string, unknown> | undefined {\n const hosted = output.filter(\n (item) => item.type !== \"message\" && item.type !== \"reasoning\" && item.type !== \"function_call\",\n );\n const searches = hosted.filter((item) => item.type === \"web_search_call\");\n if (searches.length)\n usage.serverToolUse = { ...usage.serverToolUse, webSearchRequests: searches.length };\n attachCost(usage, pricing);\n return hosted.length ? { openai: { output: hosted } } : undefined;\n}\n"],"mappings":";;;;;;;;AAQA,SAAgB,aACd,OACA,QACA,SACqC;CACrC,MAAM,SAAS,OAAO,QACnB,SAAS,KAAK,SAAS,aAAa,KAAK,SAAS,eAAe,KAAK,SAAS,eAClF;CACA,MAAM,WAAW,OAAO,QAAQ,SAAS,KAAK,SAAS,iBAAiB;CACxE,IAAI,SAAS,QACX,MAAM,gBAAgB;EAAE,GAAG,MAAM;EAAe,mBAAmB,SAAS;CAAO;CACrF,WAAW,OAAO,OAAO;CACzB,OAAO,OAAO,SAAS,EAAE,QAAQ,EAAE,QAAQ,OAAO,EAAE,IAAI;AAC1D"}
@@ -5,10 +5,10 @@ import { mapFinishReason } from "./map-finish-reason.mjs";
5
5
  import { buildPromptCacheParams } from "./prompt-cache-params.mjs";
6
6
  import { toOpenAIReasoningEffort } from "./reasoning-effort.mjs";
7
7
  import { inferStructuredOutput, planStructuredOutput } from "./structured-output.mjs";
8
- import { RESPONSES_METADATA_KEY, attachReasoningReplay, pickReplayableReasoning, readReasoningReplay, toReplayReasoningItem } from "./reasoning-replay.mjs";
8
+ import { RESPONSES_METADATA_KEY, attachReasoningReplay, pickReplayableReasoning, pickTurnReasoning, readReasoningReplay, toReplayReasoningItem } from "./reasoning-replay.mjs";
9
9
  import { toOpenAIMessages } from "./to-openai-messages.mjs";
10
10
  import { toOpenAITools } from "./to-openai-tools.mjs";
11
11
  import { toResponsesInput } from "./to-responses-input.mjs";
12
12
  import { toResponsesToolChoice, toResponsesTools } from "./to-responses-tools.mjs";
13
13
 
14
- export { };
14
+ export { attachReasoningReplay, buildPromptCacheParams, buildUsage, inferStructuredOutput, mapFinishReason, mapResponsesFinishReason, pickTurnReasoning, planStructuredOutput, toOpenAIMessages, toOpenAIReasoningEffort, toOpenAITools, toResponseFailure, toResponsesInput, toResponsesToolChoice, toResponsesTools, wrapOpenAIError };
@@ -1 +1 @@
1
- {"version":3,"file":"map-responses-status.mjs","names":[],"sources":["../../../../../../../../packages/ai-openai/src/utils/map-responses-status.ts"],"sourcesContent":["import type { AIError, FinishReason } from \"@warlock.js/ai\";\nimport type OpenAI from \"openai\";\nimport { wrapOpenAIError } from \"./wrap-openai-error\";\n\n/** The slice of a Responses `Response` the status mapper reads. */\nexport type ResponsesTerminalState = Pick<\n OpenAI.Responses.Response,\n \"status\" | \"incomplete_details\"\n> & {\n /** True when `output` holds at least one `function_call` item the model completed. */\n hasToolCalls: boolean;\n};\n\n/**\n * Derive our `FinishReason` from a Responses terminal state. Responses has\n * no `finish_reason`, so it comes from `status`, `incomplete_details` and the\n * output items (design section 2, decision 5):\n *\n * - `completed` with a function call -> `tool_calls`; without -> `stop`\n * (a refusal is ordinary text and also `stop`).\n * - `incomplete` / `max_output_tokens` and `max_messages` -> `length`.\n * - `incomplete` / `content_filter` and `steered` (or an unknown / missing\n * reason) -> `error`. There is no neutral `content_filter` value.\n * - `cancelled`, `queued`, `in_progress` at a terminal read -> `error`.\n * - `failed` is not mapped here: callers throw it via {@link toResponseFailure}.\n *\n * A missing `status` is treated as `completed` (the SDK types it optional).\n */\nexport function mapResponsesFinishReason(state: ResponsesTerminalState): FinishReason {\n const status = state.status ?? \"completed\";\n\n if (status === \"completed\") {\n return state.hasToolCalls ? \"tool_calls\" : \"stop\";\n }\n\n if (status === \"incomplete\") {\n const reason = state.incomplete_details?.reason;\n\n return reason === \"max_output_tokens\" || reason === \"max_messages\" ? \"length\" : \"error\";\n }\n\n return \"error\";\n}\n\n/**\n * Turn a `failed` Responses payload into the same typed `AIError` the Chat\n * path throws for a provider failure, by running it through\n * `wrapOpenAIError` (so `rate_limit_exceeded` becomes a\n * `ProviderRateLimitError`, everything else a `ProviderError`, with `code`\n * and request id kept on `context`). The response id and model are added so\n * the failure is traceable.\n */\nexport function toResponseFailure(\n response: OpenAI.Responses.Response,\n model: string,\n): AIError {\n const error = response.error;\n const requestId = (response as { _request_id?: string | null })._request_id;\n\n const wrapped = wrapOpenAIError({\n code: error?.code,\n message: error?.message ?? \"OpenAI response failed without an error object.\",\n ...(requestId ? { request_id: requestId } : {}),\n });\n\n if (wrapped.context) {\n Object.assign(wrapped.context, { provider: \"openai\", model, responseId: response.id });\n }\n\n return wrapped;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AA4BA,SAAgB,yBAAyB,OAA6C;CACpF,MAAM,SAAS,MAAM,UAAU;CAE/B,IAAI,WAAW,aACb,OAAO,MAAM,eAAe,eAAe;CAG7C,IAAI,WAAW,cAAc;EAC3B,MAAM,SAAS,MAAM,oBAAoB;EAEzC,OAAO,WAAW,uBAAuB,WAAW,iBAAiB,WAAW;CAClF;CAEA,OAAO;AACT;;;;;;;;;AAUA,SAAgB,kBACd,UACA,OACS;CACT,MAAM,QAAQ,SAAS;CACvB,MAAM,YAAa,SAA6C;CAEhE,MAAM,UAAU,gBAAgB;EAC9B,MAAM,OAAO;EACb,SAAS,OAAO,WAAW;EAC3B,GAAI,YAAY,EAAE,YAAY,UAAU,IAAI,CAAC;CAC/C,CAAC;CAED,IAAI,QAAQ,SACV,OAAO,OAAO,QAAQ,SAAS;EAAE,UAAU;EAAU;EAAO,YAAY,SAAS;CAAG,CAAC;CAGvF,OAAO;AACT"}
1
+ {"version":3,"file":"map-responses-status.mjs","names":[],"sources":["../../../../../../../../packages/ai-openai/src/utils/map-responses-status.ts"],"sourcesContent":["import type { AIError, FinishReason } from \"@warlock.js/ai\";\nimport type OpenAI from \"openai\";\nimport { wrapOpenAIError } from \"./wrap-openai-error\";\n\n/** The slice of a Responses `Response` the status mapper reads. */\nexport type ResponsesTerminalState = Pick<\n OpenAI.Responses.Response,\n \"status\" | \"incomplete_details\"\n> & {\n /** True when `output` holds at least one `function_call` item the model completed. */\n hasToolCalls: boolean;\n};\n\n/**\n * Derive our `FinishReason` from a Responses terminal state. Responses has\n * no `finish_reason`, so it comes from `status`, `incomplete_details` and the\n * output items (design section 2, decision 5):\n *\n * - `completed` with a function call -> `tool_calls`; without -> `stop`\n * (a refusal is ordinary text and also `stop`).\n * - `incomplete` / `max_output_tokens` and `max_messages` -> `length`.\n * - `incomplete` / `content_filter` and `steered` (or an unknown / missing\n * reason) -> `error`. There is no neutral `content_filter` value.\n * - `cancelled`, `queued`, `in_progress` at a terminal read -> `error`.\n * - `failed` is not mapped here: callers throw it via {@link toResponseFailure}.\n *\n * A missing `status` is treated as `completed` (the SDK types it optional).\n */\nexport function mapResponsesFinishReason(state: ResponsesTerminalState): FinishReason {\n const status = state.status ?? \"completed\";\n\n if (status === \"completed\") {\n return state.hasToolCalls ? \"tool_calls\" : \"stop\";\n }\n\n if (status === \"incomplete\") {\n const reason = state.incomplete_details?.reason;\n\n return reason === \"max_output_tokens\" || reason === \"max_messages\" ? \"length\" : \"error\";\n }\n\n return \"error\";\n}\n\n/**\n * Turn a `failed` Responses payload into the same typed `AIError` the Chat\n * path throws for a provider failure, by running it through\n * `wrapOpenAIError` (so `rate_limit_exceeded` becomes a\n * `ProviderRateLimitError`, everything else a `ProviderError`, with `code`\n * and request id kept on `context`). The response id and model are added so\n * the failure is traceable.\n */\nexport function toResponseFailure(response: OpenAI.Responses.Response, model: string): AIError {\n const error = response.error;\n const requestId = (response as { _request_id?: string | null })._request_id;\n\n const wrapped = wrapOpenAIError({\n code: error?.code,\n message: error?.message ?? \"OpenAI response failed without an error object.\",\n ...(requestId ? { request_id: requestId } : {}),\n });\n\n if (wrapped.context) {\n Object.assign(wrapped.context, { provider: \"openai\", model, responseId: response.id });\n }\n\n return wrapped;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AA4BA,SAAgB,yBAAyB,OAA6C;CACpF,MAAM,SAAS,MAAM,UAAU;CAE/B,IAAI,WAAW,aACb,OAAO,MAAM,eAAe,eAAe;CAG7C,IAAI,WAAW,cAAc;EAC3B,MAAM,SAAS,MAAM,oBAAoB;EAEzC,OAAO,WAAW,uBAAuB,WAAW,iBAAiB,WAAW;CAClF;CAEA,OAAO;AACT;;;;;;;;;AAUA,SAAgB,kBAAkB,UAAqC,OAAwB;CAC7F,MAAM,QAAQ,SAAS;CACvB,MAAM,YAAa,SAA6C;CAEhE,MAAM,UAAU,gBAAgB;EAC9B,MAAM,OAAO;EACb,SAAS,OAAO,WAAW;EAC3B,GAAI,YAAY,EAAE,YAAY,UAAU,IAAI,CAAC;CAC/C,CAAC;CAED,IAAI,QAAQ,SACV,OAAO,OAAO,QAAQ,SAAS;EAAE,UAAU;EAAU;EAAO,YAAY,SAAS;CAAG,CAAC;CAGvF,OAAO;AACT"}
@@ -1,14 +1,33 @@
1
1
  //#region ../../packages/ai-openai/src/utils/prompt-cache-params.ts
2
2
  /**
3
- * Build the OpenAI-only prompt-cache request fields from the model config.
4
- * Returns nothing for any provider label other than `"openai"`: wrapper
5
- * endpoints use the same transport but may reject these OpenAI-only fields.
3
+ * Provider labels whose OpenAI-compatible endpoint documents a
4
+ * `prompt_cache_key` request field:
5
+ *
6
+ * - `openai` — https://developers.openai.com/api/docs/guides/prompt-caching
7
+ * - `xai` — Chat Completions accepts `prompt_cache_key` ("plumbed to
8
+ * x-grok-conv-id"), https://docs.x.ai/openapi.json and
9
+ * https://docs.x.ai/developers/advanced-api-usage/prompt-caching/maximizing-cache-hits
10
+ * - `mistral` — https://docs.mistral.ai/studio/conversations/advanced/prompt-caching
11
+ *
12
+ * Groq and DeepSeek cache automatically and document no request hint, so
13
+ * they receive nothing (checked 2026-10-17).
14
+ */
15
+ const PROMPT_CACHE_KEY_PROVIDERS = /* @__PURE__ */ new Set([
16
+ "openai",
17
+ "xai",
18
+ "mistral"
19
+ ]);
20
+ /**
21
+ * Build the prompt-cache request fields from the model config.
22
+ * `prompt_cache_key` is sent for the providers that document it
23
+ * ({@link PROMPT_CACHE_KEY_PROVIDERS}); `prompt_cache_retention` is
24
+ * OpenAI-only. Any other provider label — including a custom label on a
25
+ * wrapper SDK — gets neither, since an unknown endpoint may reject them.
6
26
  */
7
27
  function buildPromptCacheParams(provider, config) {
8
- if (provider !== "openai") return {};
9
28
  return {
10
- ...config.promptCacheKey !== void 0 ? { prompt_cache_key: config.promptCacheKey } : {},
11
- ...config.promptCacheRetention !== void 0 ? { prompt_cache_retention: config.promptCacheRetention } : {}
29
+ ...PROMPT_CACHE_KEY_PROVIDERS.has(provider) && config.promptCacheKey !== void 0 ? { prompt_cache_key: config.promptCacheKey } : {},
30
+ ...provider === "openai" && config.promptCacheRetention !== void 0 ? { prompt_cache_retention: config.promptCacheRetention } : {}
12
31
  };
13
32
  }
14
33
 
@@ -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 OpenAI-only 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 * Build the OpenAI-only prompt-cache request fields from the model config.\n * Returns nothing for any provider label other than `\"openai\"`: wrapper\n * endpoints use the same transport but may reject these OpenAI-only fields.\n */\nexport function buildPromptCacheParams(\n provider: string,\n config: Pick<OpenAIModelConfig, \"promptCacheKey\" | \"promptCacheRetention\">,\n): PromptCacheParams {\n if (provider !== \"openai\") {\n return {};\n }\n\n return {\n ...(config.promptCacheKey !== undefined ? { prompt_cache_key: config.promptCacheKey } : {}),\n ...(config.promptCacheRetention !== undefined\n ? { prompt_cache_retention: config.promptCacheRetention }\n : {}),\n };\n}\n"],"mappings":";;;;;;AAaA,SAAgB,uBACd,UACA,QACmB;CACnB,IAAI,aAAa,UACf,OAAO,CAAC;CAGV,OAAO;EACL,GAAI,OAAO,mBAAmB,SAAY,EAAE,kBAAkB,OAAO,eAAe,IAAI,CAAC;EACzF,GAAI,OAAO,yBAAyB,SAChC,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"}
@@ -14,6 +14,28 @@ const RESPONSES_METADATA_KEY = "openaiResponses";
14
14
  function pickReplayableReasoning(items) {
15
15
  return items.filter((item) => typeof item.encrypted_content === "string" && item.encrypted_content !== "");
16
16
  }
17
+ /** Output item types that may follow a replayed reasoning item. */
18
+ const REPLAY_SAFE_FOLLOWERS = /* @__PURE__ */ new Set(["function_call", "message"]);
19
+ /**
20
+ * The reasoning items of a turn that are safe to replay, in order.
21
+ *
22
+ * The API pairs a reasoning item with the output item that followed it and
23
+ * rejects a replayed reasoning item whose follower is missing. Only
24
+ * `function_call` items are replayed, so a reasoning item that led into a
25
+ * hosted (server-run) tool call — `web_search_call`, `file_search_call`,
26
+ * `code_interpreter_call`, … from a provider-defined tool — is dropped
27
+ * rather than sent without its follower. Encrypted reasoning only improves
28
+ * continuity; dropping it never breaks the request.
29
+ */
30
+ function pickTurnReasoning(output) {
31
+ const picked = [];
32
+ output.forEach((item, index) => {
33
+ if (item.type !== "reasoning") return;
34
+ const follower = output.slice(index + 1).find((next) => next.type !== "reasoning");
35
+ if (follower === void 0 || REPLAY_SAFE_FOLLOWERS.has(follower.type)) picked.push(item);
36
+ });
37
+ return picked;
38
+ }
17
39
  /**
18
40
  * Attach the turn's reasoning items to the FIRST tool call only (parallel
19
41
  * calls do not each repeat the blobs). Returns the calls unchanged when
@@ -81,5 +103,5 @@ function isReplayMetadata(value) {
81
103
  }
82
104
 
83
105
  //#endregion
84
- export { RESPONSES_METADATA_KEY, attachReasoningReplay, pickReplayableReasoning, readReasoningReplay, toReplayReasoningItem };
106
+ export { RESPONSES_METADATA_KEY, attachReasoningReplay, pickReplayableReasoning, pickTurnReasoning, readReasoningReplay, toReplayReasoningItem };
85
107
  //# sourceMappingURL=reasoning-replay.mjs.map
@@ -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/**\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 { ...first, providerMetadata: { ...first.providerMetadata, [RESPONSES_METADATA_KEY]: metadata } },\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;;;;;;;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;EAAE,GAAG;EAAO,kBAAkB;GAAE,GAAG,MAAM;IAAmB,yBAAyB;EAAS;CAAE,GAChG,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":"structured-output.mjs","names":[],"sources":["../../../../../../../../packages/ai-openai/src/utils/structured-output.ts"],"sourcesContent":["import type { OpenAIResponseFormat } from \"../config.type\";\n\n/**\n * How a `responseSchema` should reach the wire, decided once for both the\n * Chat Completions adapter (`response_format`) and the Responses adapter\n * (`text.format`). Only the wire location differs between the two.\n */\nexport type StructuredOutputPlan =\n | { mode: \"none\" }\n | { mode: \"json_object\" }\n | { mode: \"json_schema\"; schema: Record<string, unknown> };\n\n/**\n * Map an explicit `responseFormat` override to the default\n * `structuredOutput` capability. Loose wire modes (`\"json_object\"`,\n * `\"text\"`) don't enforce shape, so the agent needs to see the soft\n * schema hint in the system prompt — that only happens when the\n * capability is `false`. Default (no override) stays `true` to\n * preserve the prior assumption that OpenAI models support strict\n * structured output.\n */\nexport function inferStructuredOutput(\n responseFormat: OpenAIResponseFormat | undefined,\n): boolean {\n if (responseFormat === \"json_object\" || responseFormat === \"text\") {\n return false;\n }\n\n return true;\n}\n\n/**\n * Decide the structured-output mode for a call.\n *\n * When `override` (the `responseFormat` config) is set, it wins: `\"text\"`\n * emits nothing, `\"json_object\"` always picks the loose mode, and\n * `\"json_schema\"` picks strict mode (with the same strict-compatibility\n * safety check - a malformed schema still degrades to `json_object` rather\n * than 400). The override exists because some targets (older OpenAI models,\n * OpenRouter routes, Ollama OpenAI-compat) reject strict `json_schema`\n * outright.\n *\n * When the override is omitted, strict `json_schema` mode (token-level\n * enforcement) is used only when the schema is a proper root-object JSON\n * Schema whose every object lists all of its properties in `required`. For\n * anything else it falls back to loose `json_object` mode. Client-side\n * Standard Schema `validate()` still enforces the full shape.\n */\nexport function planStructuredOutput(\n responseSchema: Record<string, unknown> | undefined,\n override: OpenAIResponseFormat | undefined,\n): StructuredOutputPlan {\n if (!responseSchema || override === \"text\") {\n return { mode: \"none\" };\n }\n\n if (override === \"json_object\") {\n return { mode: \"json_object\" };\n }\n\n // Either auto-select (no override) or explicit `\"json_schema\"`. The\n // strict-compat check still applies in the explicit case - a malformed or\n // non-object schema would 400 before sampling, so degrade to `json_object`.\n if (isStrictCompatible(responseSchema)) {\n return { mode: \"json_schema\", schema: responseSchema };\n }\n\n return { mode: \"json_object\" };\n}\n\n/**\n * OpenAI strict `json_schema` mode requires the root to be a JSON Schema\n * object type (`{ type: \"object\", properties: ... }`). Anything else\n * (top-level arrays, primitives, unknown shapes) is rejected with a 400\n * before a token is sampled. Checked structurally here so the first call\n * doesn't crash on a malformed extraction.\n */\nexport function isStrictCompatible(schema: Record<string, unknown>): boolean {\n return (\n schema.type === \"object\" &&\n typeof schema.properties === \"object\" &&\n schema.properties !== null &&\n isStrictSafeNode(schema)\n );\n}\n\n/**\n * Recursively check the one strict-mode rule schemas most often trip on:\n * every object must list ALL of its `properties` in `required` (OpenAI\n * strict has no notion of optional - optional fields must be expressed as\n * nullable, e.g. `type: [\"string\", \"null\"]`, and still appear in\n * `required`). A schema that violates this anywhere in the tree is NOT sent\n * in strict mode, so a hand-built or optional-bearing schema can't 400 the\n * call (\"'required' ... must include every key in properties\").\n */\nfunction isStrictSafeNode(node: unknown): boolean {\n if (!node || typeof node !== \"object\") {\n return true;\n }\n\n const record = node as Record<string, unknown>;\n\n if (record.type === \"object\" && record.properties && typeof record.properties === \"object\") {\n const properties = record.properties as Record<string, unknown>;\n const keys = Object.keys(properties);\n const required = Array.isArray(record.required) ? (record.required as unknown[]) : [];\n\n if (keys.some((key) => !required.includes(key))) {\n return false;\n }\n\n for (const key of keys) {\n if (!isStrictSafeNode(properties[key])) {\n return false;\n }\n }\n }\n\n if (record.items !== undefined && !isStrictSafeNode(record.items)) {\n return false;\n }\n\n for (const branch of [\"anyOf\", \"allOf\", \"oneOf\"] as const) {\n const value = record[branch];\n\n if (Array.isArray(value) && value.some((sub) => !isStrictSafeNode(sub))) {\n return false;\n }\n }\n\n return true;\n}\n"],"mappings":";;;;;;;;;;AAqBA,SAAgB,sBACd,gBACS;CACT,IAAI,mBAAmB,iBAAiB,mBAAmB,QACzD,OAAO;CAGT,OAAO;AACT;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,qBACd,gBACA,UACsB;CACtB,IAAI,CAAC,kBAAkB,aAAa,QAClC,OAAO,EAAE,MAAM,OAAO;CAGxB,IAAI,aAAa,eACf,OAAO,EAAE,MAAM,cAAc;CAM/B,IAAI,mBAAmB,cAAc,GACnC,OAAO;EAAE,MAAM;EAAe,QAAQ;CAAe;CAGvD,OAAO,EAAE,MAAM,cAAc;AAC/B;;;;;;;;AASA,SAAgB,mBAAmB,QAA0C;CAC3E,OACE,OAAO,SAAS,YAChB,OAAO,OAAO,eAAe,YAC7B,OAAO,eAAe,QACtB,iBAAiB,MAAM;AAE3B;;;;;;;;;;AAWA,SAAS,iBAAiB,MAAwB;CAChD,IAAI,CAAC,QAAQ,OAAO,SAAS,UAC3B,OAAO;CAGT,MAAM,SAAS;CAEf,IAAI,OAAO,SAAS,YAAY,OAAO,cAAc,OAAO,OAAO,eAAe,UAAU;EAC1F,MAAM,aAAa,OAAO;EAC1B,MAAM,OAAO,OAAO,KAAK,UAAU;EACnC,MAAM,WAAW,MAAM,QAAQ,OAAO,QAAQ,IAAK,OAAO,WAAyB,CAAC;EAEpF,IAAI,KAAK,MAAM,QAAQ,CAAC,SAAS,SAAS,GAAG,CAAC,GAC5C,OAAO;EAGT,KAAK,MAAM,OAAO,MAChB,IAAI,CAAC,iBAAiB,WAAW,IAAI,GACnC,OAAO;CAGb;CAEA,IAAI,OAAO,UAAU,UAAa,CAAC,iBAAiB,OAAO,KAAK,GAC9D,OAAO;CAGT,KAAK,MAAM,UAAU;EAAC;EAAS;EAAS;CAAO,GAAY;EACzD,MAAM,QAAQ,OAAO;EAErB,IAAI,MAAM,QAAQ,KAAK,KAAK,MAAM,MAAM,QAAQ,CAAC,iBAAiB,GAAG,CAAC,GACpE,OAAO;CAEX;CAEA,OAAO;AACT"}
1
+ {"version":3,"file":"structured-output.mjs","names":[],"sources":["../../../../../../../../packages/ai-openai/src/utils/structured-output.ts"],"sourcesContent":["import type { OpenAIResponseFormat } from \"../config.type\";\n\n/**\n * How a `responseSchema` should reach the wire, decided once for both the\n * Chat Completions adapter (`response_format`) and the Responses adapter\n * (`text.format`). Only the wire location differs between the two.\n */\nexport type StructuredOutputPlan =\n | { mode: \"none\" }\n | { mode: \"json_object\" }\n | { mode: \"json_schema\"; schema: Record<string, unknown> };\n\n/**\n * Map an explicit `responseFormat` override to the default\n * `structuredOutput` capability. Loose wire modes (`\"json_object\"`,\n * `\"text\"`) don't enforce shape, so the agent needs to see the soft\n * schema hint in the system prompt — that only happens when the\n * capability is `false`. Default (no override) stays `true` to\n * preserve the prior assumption that OpenAI models support strict\n * structured output.\n */\nexport function inferStructuredOutput(responseFormat: OpenAIResponseFormat | undefined): boolean {\n if (responseFormat === \"json_object\" || responseFormat === \"text\") {\n return false;\n }\n\n return true;\n}\n\n/**\n * Decide the structured-output mode for a call.\n *\n * When `override` (the `responseFormat` config) is set, it wins: `\"text\"`\n * emits nothing, `\"json_object\"` always picks the loose mode, and\n * `\"json_schema\"` picks strict mode (with the same strict-compatibility\n * safety check - a malformed schema still degrades to `json_object` rather\n * than 400). The override exists because some targets (older OpenAI models,\n * OpenRouter routes, Ollama OpenAI-compat) reject strict `json_schema`\n * outright.\n *\n * When the override is omitted, strict `json_schema` mode (token-level\n * enforcement) is used only when the schema is a proper root-object JSON\n * Schema whose every object lists all of its properties in `required`. For\n * anything else it falls back to loose `json_object` mode. Client-side\n * Standard Schema `validate()` still enforces the full shape.\n */\nexport function planStructuredOutput(\n responseSchema: Record<string, unknown> | undefined,\n override: OpenAIResponseFormat | undefined,\n): StructuredOutputPlan {\n if (!responseSchema || override === \"text\") {\n return { mode: \"none\" };\n }\n\n if (override === \"json_object\") {\n return { mode: \"json_object\" };\n }\n\n // Either auto-select (no override) or explicit `\"json_schema\"`. The\n // strict-compat check still applies in the explicit case - a malformed or\n // non-object schema would 400 before sampling, so degrade to `json_object`.\n if (isStrictCompatible(responseSchema)) {\n return { mode: \"json_schema\", schema: responseSchema };\n }\n\n return { mode: \"json_object\" };\n}\n\n/**\n * OpenAI strict `json_schema` mode requires the root to be a JSON Schema\n * object type (`{ type: \"object\", properties: ... }`). Anything else\n * (top-level arrays, primitives, unknown shapes) is rejected with a 400\n * before a token is sampled. Checked structurally here so the first call\n * doesn't crash on a malformed extraction.\n */\nexport function isStrictCompatible(schema: Record<string, unknown>): boolean {\n return (\n schema.type === \"object\" &&\n typeof schema.properties === \"object\" &&\n schema.properties !== null &&\n isStrictSafeNode(schema)\n );\n}\n\n/**\n * Recursively check the one strict-mode rule schemas most often trip on:\n * every object must list ALL of its `properties` in `required` (OpenAI\n * strict has no notion of optional - optional fields must be expressed as\n * nullable, e.g. `type: [\"string\", \"null\"]`, and still appear in\n * `required`). A schema that violates this anywhere in the tree is NOT sent\n * in strict mode, so a hand-built or optional-bearing schema can't 400 the\n * call (\"'required' ... must include every key in properties\").\n */\nfunction isStrictSafeNode(node: unknown): boolean {\n if (!node || typeof node !== \"object\") {\n return true;\n }\n\n const record = node as Record<string, unknown>;\n\n if (record.type === \"object\" && record.properties && typeof record.properties === \"object\") {\n const properties = record.properties as Record<string, unknown>;\n const keys = Object.keys(properties);\n const required = Array.isArray(record.required) ? (record.required as unknown[]) : [];\n\n if (keys.some((key) => !required.includes(key))) {\n return false;\n }\n\n for (const key of keys) {\n if (!isStrictSafeNode(properties[key])) {\n return false;\n }\n }\n }\n\n if (record.items !== undefined && !isStrictSafeNode(record.items)) {\n return false;\n }\n\n for (const branch of [\"anyOf\", \"allOf\", \"oneOf\"] as const) {\n const value = record[branch];\n\n if (Array.isArray(value) && value.some((sub) => !isStrictSafeNode(sub))) {\n return false;\n }\n }\n\n return true;\n}\n"],"mappings":";;;;;;;;;;AAqBA,SAAgB,sBAAsB,gBAA2D;CAC/F,IAAI,mBAAmB,iBAAiB,mBAAmB,QACzD,OAAO;CAGT,OAAO;AACT;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,qBACd,gBACA,UACsB;CACtB,IAAI,CAAC,kBAAkB,aAAa,QAClC,OAAO,EAAE,MAAM,OAAO;CAGxB,IAAI,aAAa,eACf,OAAO,EAAE,MAAM,cAAc;CAM/B,IAAI,mBAAmB,cAAc,GACnC,OAAO;EAAE,MAAM;EAAe,QAAQ;CAAe;CAGvD,OAAO,EAAE,MAAM,cAAc;AAC/B;;;;;;;;AASA,SAAgB,mBAAmB,QAA0C;CAC3E,OACE,OAAO,SAAS,YAChB,OAAO,OAAO,eAAe,YAC7B,OAAO,eAAe,QACtB,iBAAiB,MAAM;AAE3B;;;;;;;;;;AAWA,SAAS,iBAAiB,MAAwB;CAChD,IAAI,CAAC,QAAQ,OAAO,SAAS,UAC3B,OAAO;CAGT,MAAM,SAAS;CAEf,IAAI,OAAO,SAAS,YAAY,OAAO,cAAc,OAAO,OAAO,eAAe,UAAU;EAC1F,MAAM,aAAa,OAAO;EAC1B,MAAM,OAAO,OAAO,KAAK,UAAU;EACnC,MAAM,WAAW,MAAM,QAAQ,OAAO,QAAQ,IAAK,OAAO,WAAyB,CAAC;EAEpF,IAAI,KAAK,MAAM,QAAQ,CAAC,SAAS,SAAS,GAAG,CAAC,GAC5C,OAAO;EAGT,KAAK,MAAM,OAAO,MAChB,IAAI,CAAC,iBAAiB,WAAW,IAAI,GACnC,OAAO;CAGb;CAEA,IAAI,OAAO,UAAU,UAAa,CAAC,iBAAiB,OAAO,KAAK,GAC9D,OAAO;CAGT,KAAK,MAAM,UAAU;EAAC;EAAS;EAAS;CAAO,GAAY;EACzD,MAAM,QAAQ,OAAO;EAErB,IAAI,MAAM,QAAQ,KAAK,KAAK,MAAM,MAAM,QAAQ,CAAC,iBAAiB,GAAG,CAAC,GACpE,OAAO;CAEX;CAEA,OAAO;AACT"}
@@ -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(\n source: Extract<ContentPart, { type: \"image\" }>[\"source\"],\n): 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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"}
@@ -1 +1 @@
1
- {"version":3,"file":"to-openai-tools.mjs","names":[],"sources":["../../../../../../../../packages/ai-openai/src/utils/to-openai-tools.ts"],"sourcesContent":["import { extractJsonSchema, type ToolConfig } from \"@warlock.js/ai\";\nimport type OpenAI from \"openai\";\n\n/**\n * Convert vendor-neutral ToolConfig[] to OpenAI's tools array.\n * Uses the shared `extractJsonSchema` helper; falls back to an empty-object\n * schema when extraction fails so the tool still registers with the provider.\n *\n * @example\n * const tools = toOpenAITools([weatherTool, calculatorTool]);\n * await client.chat.completions.create({ model, messages, tools });\n */\nexport function toOpenAITools(\n tools: ToolConfig<unknown, unknown>[] | undefined,\n): OpenAI.Chat.Completions.ChatCompletionTool[] | undefined {\n if (!tools || tools.length === 0) {\n return undefined;\n }\n\n return tools.map((tool) => ({\n type: \"function\",\n function: {\n name: tool.name,\n description: tool.description,\n parameters: toToolParameters(tool.input),\n },\n }));\n}\n\n/**\n * Resolve a tool's input schema to a JSON-Schema object. OpenAI's\n * function `parameters` expects an object root; anything else (or a\n * failed extraction) degrades to an empty-object schema so the tool\n * still registers and the model simply sees no parameters.\n */\nexport function toToolParameters(input: ToolConfig<unknown, unknown>[\"input\"]): Record<string, unknown> {\n const schema = extractJsonSchema(input);\n\n if (schema && schema.type === \"object\") {\n return schema;\n }\n\n return { type: \"object\", properties: {} };\n}\n"],"mappings":";;;;;;;;;;;;AAYA,SAAgB,cACd,OAC0D;CAC1D,IAAI,CAAC,SAAS,MAAM,WAAW,GAC7B;CAGF,OAAO,MAAM,KAAK,UAAU;EAC1B,MAAM;EACN,UAAU;GACR,MAAM,KAAK;GACX,aAAa,KAAK;GAClB,YAAY,iBAAiB,KAAK,KAAK;EACzC;CACF,EAAE;AACJ;;;;;;;AAQA,SAAgB,iBAAiB,OAAuE;CACtG,MAAM,SAAS,kBAAkB,KAAK;CAEtC,IAAI,UAAU,OAAO,SAAS,UAC5B,OAAO;CAGT,OAAO;EAAE,MAAM;EAAU,YAAY,CAAC;CAAE;AAC1C"}
1
+ {"version":3,"file":"to-openai-tools.mjs","names":[],"sources":["../../../../../../../../packages/ai-openai/src/utils/to-openai-tools.ts"],"sourcesContent":["import { extractJsonSchema, type ToolConfig } from \"@warlock.js/ai\";\nimport type OpenAI from \"openai\";\n\n/**\n * Convert vendor-neutral ToolConfig[] to OpenAI's tools array.\n * Uses the shared `extractJsonSchema` helper; falls back to an empty-object\n * schema when extraction fails so the tool still registers with the provider.\n *\n * @example\n * const tools = toOpenAITools([weatherTool, calculatorTool]);\n * await client.chat.completions.create({ model, messages, tools });\n */\nexport function toOpenAITools(\n tools: ToolConfig<unknown, unknown>[] | undefined,\n): OpenAI.Chat.Completions.ChatCompletionTool[] | undefined {\n if (!tools || tools.length === 0) {\n return undefined;\n }\n\n return tools.map((tool) => ({\n type: \"function\",\n function: {\n name: tool.name,\n description: tool.description,\n parameters: toToolParameters(tool.input),\n },\n }));\n}\n\n/**\n * Resolve a tool's input schema to a JSON-Schema object. OpenAI's\n * function `parameters` expects an object root; anything else (or a\n * failed extraction) degrades to an empty-object schema so the tool\n * still registers and the model simply sees no parameters.\n */\nexport function toToolParameters(\n input: ToolConfig<unknown, unknown>[\"input\"],\n): Record<string, unknown> {\n const schema = extractJsonSchema(input);\n\n if (schema && schema.type === \"object\") {\n return schema;\n }\n\n return { type: \"object\", properties: {} };\n}\n"],"mappings":";;;;;;;;;;;;AAYA,SAAgB,cACd,OAC0D;CAC1D,IAAI,CAAC,SAAS,MAAM,WAAW,GAC7B;CAGF,OAAO,MAAM,KAAK,UAAU;EAC1B,MAAM;EACN,UAAU;GACR,MAAM,KAAK;GACX,aAAa,KAAK;GAClB,YAAY,iBAAiB,KAAK,KAAK;EACzC;CACF,EAAE;AACJ;;;;;;;AAQA,SAAgB,iBACd,OACyB;CACzB,MAAM,SAAS,kBAAkB,KAAK;CAEtC,IAAI,UAAU,OAAO,SAAS,UAC5B,OAAO;CAGT,OAAO;EAAE,MAAM;EAAU,YAAY,CAAC;CAAE;AAC1C"}
@@ -1,5 +1,5 @@
1
1
  import OpenAI from "openai";
2
- import { AIError, ContentFilterError, ContextLengthExceededError, InvalidRequestError, ProviderAuthError, ProviderError, ProviderRateLimitError, ProviderTimeoutError, QuotaExceededError } from "@warlock.js/ai";
2
+ import { AIError, ContentFilterError, ContextLengthExceededError, InvalidRequestError, ProviderAuthError, ProviderError, ProviderOverloadedError, ProviderRateLimitError, ProviderTimeoutError, QuotaExceededError } from "@warlock.js/ai";
3
3
 
4
4
  //#region ../../packages/ai-openai/src/utils/wrap-openai-error.ts
5
5
  /**
@@ -58,6 +58,11 @@ function wrapOpenAIError(thrown) {
58
58
  cause: thrown,
59
59
  context
60
60
  });
61
+ if (shape.code === "server_is_overloaded" || shape.status === 503) return new ProviderOverloadedError(message, {
62
+ cause: thrown,
63
+ context,
64
+ retryAfter: parseRetryAfter(shape.headers)
65
+ });
61
66
  return new ProviderError(message, {
62
67
  cause: thrown,
63
68
  context
@@ -1 +1 @@
1
- {"version":3,"file":"wrap-openai-error.mjs","names":[],"sources":["../../../../../../../../packages/ai-openai/src/utils/wrap-openai-error.ts"],"sourcesContent":["import {\n AIError,\n ContentFilterError,\n ContextLengthExceededError,\n InvalidRequestError,\n ProviderAuthError,\n ProviderError,\n ProviderRateLimitError,\n ProviderTimeoutError,\n QuotaExceededError,\n} from \"@warlock.js/ai\";\nimport OpenAI from \"openai\";\n\n/**\n * Raw-error fields the wrapper reads off an OpenAI SDK error.\n *\n * `APIError` exposes `status`, `code`, `message`, `type`, `headers` —\n * we duck-type because wrapped retries, proxied errors, and custom\n * error subclasses sometimes lose the `instanceof` relationship.\n */\ntype OpenAIErrorShape = {\n status?: number;\n code?: string | null;\n message?: string;\n type?: string | null;\n /** The request parameter the provider rejected, e.g. `max_tokens`. */\n param?: string | null;\n headers?: Record<string, string> | undefined;\n name?: string;\n};\n\n/**\n * Wrap any thrown value caught inside the OpenAI adapter into the\n * appropriate `@warlock.js/ai` `AIError` subclass.\n *\n * **Dispatch strategy.** Prefers `APIError.code` when present (stable\n * machine identifier across SDK versions), falls back to `status` when\n * `code` is missing (common with proxied deployments that strip the\n * field). Name-based detection (`APIConnectionTimeoutError`) catches\n * transport-layer errors that never produced an HTTP response.\n *\n * `AIError` instances are returned unchanged — callers can pass the\n * error through `try/catch/throw wrap(e)` pipelines without accidental\n * double-wrapping.\n *\n * @example\n * try {\n * return await this.client.chat.completions.create(...);\n * } catch (thrown) {\n * throw wrapOpenAIError(thrown);\n * }\n */\nexport function wrapOpenAIError(thrown: unknown): AIError {\n if (thrown instanceof AIError) {\n return thrown;\n }\n\n const shape = toShape(thrown);\n const context = buildContext(thrown, shape);\n const message = shape.message ?? (thrown instanceof Error ? thrown.message : String(thrown));\n\n if (isTimeout(thrown, shape)) {\n return new ProviderTimeoutError(message, { cause: thrown, context });\n }\n\n if (shape.status === 401 || shape.code === \"invalid_api_key\") {\n return new ProviderAuthError(message, { cause: thrown, context });\n }\n\n if (shape.code === \"insufficient_quota\") {\n return new QuotaExceededError(message, { cause: thrown, context });\n }\n\n if (shape.status === 429 || shape.code === \"rate_limit_exceeded\") {\n return new ProviderRateLimitError(message, {\n cause: thrown,\n context,\n retryAfter: parseRetryAfter(shape.headers),\n });\n }\n\n if (shape.code === \"context_length_exceeded\") {\n return new ContextLengthExceededError(message, { cause: thrown, context });\n }\n\n if (shape.code === \"content_filter\") {\n return new ContentFilterError(message, {\n cause: thrown,\n context,\n reason: message,\n });\n }\n\n if (typeof shape.status === \"number\" && shape.status >= 400 && shape.status < 500) {\n return new InvalidRequestError(message, { cause: thrown, context });\n }\n\n return new ProviderError(message, { cause: thrown, context });\n}\n\n/**\n * Read the raw error shape without depending on `instanceof APIError`\n * — some consumers wrap the SDK, and proxies sometimes strip the\n * prototype chain. Duck-typing on the visible fields is resilient to\n * both.\n */\nfunction toShape(thrown: unknown): OpenAIErrorShape {\n if (thrown instanceof OpenAI.APIError) {\n return {\n status: thrown.status,\n code: thrown.code,\n message: thrown.message,\n type: thrown.type,\n param: thrown.param,\n headers: thrown.headers as Record<string, string> | undefined,\n name: thrown.name,\n };\n }\n\n if (typeof thrown === \"object\" && thrown !== null) {\n const raw = thrown as Record<string, unknown>;\n\n return {\n status: typeof raw.status === \"number\" ? raw.status : undefined,\n code: typeof raw.code === \"string\" ? raw.code : undefined,\n message: typeof raw.message === \"string\" ? raw.message : undefined,\n type: typeof raw.type === \"string\" ? raw.type : undefined,\n param: typeof raw.param === \"string\" ? raw.param : undefined,\n headers:\n typeof raw.headers === \"object\" && raw.headers !== null\n ? (raw.headers as Record<string, string>)\n : undefined,\n name: typeof raw.name === \"string\" ? raw.name : undefined,\n };\n }\n\n return {};\n}\n\n/**\n * Decide whether the thrown value represents a timeout. OpenAI's SDK\n * throws `APIConnectionTimeoutError` for transport-level timeouts, and\n * Node surfaces `ETIMEDOUT` / `ECONNABORTED` on the lower socket\n * layer. Either signal counts.\n */\nfunction isTimeout(thrown: unknown, shape: OpenAIErrorShape): boolean {\n if (thrown instanceof OpenAI.APIConnectionTimeoutError) {\n return true;\n }\n\n if (shape.name === \"APIConnectionTimeoutError\") {\n return true;\n }\n\n if (shape.code === \"ETIMEDOUT\" || shape.code === \"ECONNABORTED\") {\n return true;\n }\n\n return false;\n}\n\n/**\n * Attach the raw diagnostic fields to `error.context` so consumers\n * have everything the provider surfaced without each subclass having\n * to redeclare them. Never includes `cause` — that lives on\n * `error.cause`.\n */\nfunction buildContext(\n thrown: unknown,\n shape: OpenAIErrorShape,\n): Record<string, unknown> {\n const context: Record<string, unknown> = {};\n\n if (shape.status !== undefined) {\n context.status = shape.status;\n }\n\n if (shape.code) {\n context.code = shape.code;\n }\n\n if (shape.type) {\n context.type = shape.type;\n }\n\n if (shape.param) {\n context.param = shape.param;\n }\n\n const requestId = readRequestId(thrown);\n\n if (requestId) {\n context.requestId = requestId;\n }\n\n return context;\n}\n\n/**\n * The request id OpenAI returned. Current SDKs expose it as\n * `APIError.requestID`; older ones and hand-rolled errors use\n * `request_id` / `requestId`. Extract defensively.\n */\nfunction readRequestId(thrown: unknown): string | undefined {\n if (typeof thrown !== \"object\" || thrown === null) {\n return undefined;\n }\n\n const raw = thrown as Record<string, unknown>;\n\n if (typeof raw.request_id === \"string\") {\n return raw.request_id;\n }\n\n if (typeof raw.requestId === \"string\") {\n return raw.requestId;\n }\n\n if (typeof raw.requestID === \"string\") {\n return raw.requestID;\n }\n\n return undefined;\n}\n\n/**\n * Parse the `Retry-After` response header (seconds per HTTP spec)\n * into milliseconds so consumers can feed it straight to `setTimeout`.\n * Returns `undefined` when missing or unparseable.\n */\nfunction parseRetryAfter(headers: Record<string, string> | undefined): number | undefined {\n if (!headers) {\n return undefined;\n }\n\n const raw = headers[\"retry-after\"] ?? headers[\"Retry-After\"];\n\n if (!raw) {\n return undefined;\n }\n\n const seconds = Number(raw);\n\n if (!Number.isFinite(seconds) || seconds < 0) {\n return undefined;\n }\n\n return Math.round(seconds * 1000);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"wrap-openai-error.mjs","names":[],"sources":["../../../../../../../../packages/ai-openai/src/utils/wrap-openai-error.ts"],"sourcesContent":["import {\n AIError,\n ContentFilterError,\n ContextLengthExceededError,\n InvalidRequestError,\n ProviderAuthError,\n ProviderError,\n ProviderOverloadedError,\n ProviderRateLimitError,\n ProviderTimeoutError,\n QuotaExceededError,\n} from \"@warlock.js/ai\";\nimport OpenAI from \"openai\";\n\n/**\n * Raw-error fields the wrapper reads off an OpenAI SDK error.\n *\n * `APIError` exposes `status`, `code`, `message`, `type`, `headers` —\n * we duck-type because wrapped retries, proxied errors, and custom\n * error subclasses sometimes lose the `instanceof` relationship.\n */\ntype OpenAIErrorShape = {\n status?: number;\n code?: string | null;\n message?: string;\n type?: string | null;\n /** The request parameter the provider rejected, e.g. `max_tokens`. */\n param?: string | null;\n headers?: Record<string, string> | undefined;\n name?: string;\n};\n\n/**\n * Wrap any thrown value caught inside the OpenAI adapter into the\n * appropriate `@warlock.js/ai` `AIError` subclass.\n *\n * **Dispatch strategy.** Prefers `APIError.code` when present (stable\n * machine identifier across SDK versions), falls back to `status` when\n * `code` is missing (common with proxied deployments that strip the\n * field). Name-based detection (`APIConnectionTimeoutError`) catches\n * transport-layer errors that never produced an HTTP response.\n *\n * `AIError` instances are returned unchanged — callers can pass the\n * error through `try/catch/throw wrap(e)` pipelines without accidental\n * double-wrapping.\n *\n * @example\n * try {\n * return await this.client.chat.completions.create(...);\n * } catch (thrown) {\n * throw wrapOpenAIError(thrown);\n * }\n */\nexport function wrapOpenAIError(thrown: unknown): AIError {\n if (thrown instanceof AIError) {\n return thrown;\n }\n\n const shape = toShape(thrown);\n const context = buildContext(thrown, shape);\n const message = shape.message ?? (thrown instanceof Error ? thrown.message : String(thrown));\n\n if (isTimeout(thrown, shape)) {\n return new ProviderTimeoutError(message, { cause: thrown, context });\n }\n\n if (shape.status === 401 || shape.code === \"invalid_api_key\") {\n return new ProviderAuthError(message, { cause: thrown, context });\n }\n\n if (shape.code === \"insufficient_quota\") {\n return new QuotaExceededError(message, { cause: thrown, context });\n }\n\n if (shape.status === 429 || shape.code === \"rate_limit_exceeded\") {\n return new ProviderRateLimitError(message, {\n cause: thrown,\n context,\n retryAfter: parseRetryAfter(shape.headers),\n });\n }\n\n if (shape.code === \"context_length_exceeded\") {\n return new ContextLengthExceededError(message, { cause: thrown, context });\n }\n\n if (shape.code === \"content_filter\") {\n return new ContentFilterError(message, {\n cause: thrown,\n context,\n reason: message,\n });\n }\n\n if (typeof shape.status === \"number\" && shape.status >= 400 && shape.status < 500) {\n return new InvalidRequestError(message, { cause: thrown, context });\n }\n\n // OpenAI: 503 `service_unavailable_error` / `server_is_overloaded`\n // (https://developers.openai.com/api/docs/guides/error-codes). The\n // OpenAI-compatible wrappers use 503 for the same condition (DeepSeek\n // documents 503 \"Server Overloaded\").\n if (shape.code === \"server_is_overloaded\" || shape.status === 503) {\n return new ProviderOverloadedError(message, {\n cause: thrown,\n context,\n retryAfter: parseRetryAfter(shape.headers),\n });\n }\n\n return new ProviderError(message, { cause: thrown, context });\n}\n\n/**\n * Read the raw error shape without depending on `instanceof APIError`\n * — some consumers wrap the SDK, and proxies sometimes strip the\n * prototype chain. Duck-typing on the visible fields is resilient to\n * both.\n */\nfunction toShape(thrown: unknown): OpenAIErrorShape {\n if (thrown instanceof OpenAI.APIError) {\n return {\n status: thrown.status,\n code: thrown.code,\n message: thrown.message,\n type: thrown.type,\n param: thrown.param,\n headers: thrown.headers as Record<string, string> | undefined,\n name: thrown.name,\n };\n }\n\n if (typeof thrown === \"object\" && thrown !== null) {\n const raw = thrown as Record<string, unknown>;\n\n return {\n status: typeof raw.status === \"number\" ? raw.status : undefined,\n code: typeof raw.code === \"string\" ? raw.code : undefined,\n message: typeof raw.message === \"string\" ? raw.message : undefined,\n type: typeof raw.type === \"string\" ? raw.type : undefined,\n param: typeof raw.param === \"string\" ? raw.param : undefined,\n headers:\n typeof raw.headers === \"object\" && raw.headers !== null\n ? (raw.headers as Record<string, string>)\n : undefined,\n name: typeof raw.name === \"string\" ? raw.name : undefined,\n };\n }\n\n return {};\n}\n\n/**\n * Decide whether the thrown value represents a timeout. OpenAI's SDK\n * throws `APIConnectionTimeoutError` for transport-level timeouts, and\n * Node surfaces `ETIMEDOUT` / `ECONNABORTED` on the lower socket\n * layer. Either signal counts.\n */\nfunction isTimeout(thrown: unknown, shape: OpenAIErrorShape): boolean {\n if (thrown instanceof OpenAI.APIConnectionTimeoutError) {\n return true;\n }\n\n if (shape.name === \"APIConnectionTimeoutError\") {\n return true;\n }\n\n if (shape.code === \"ETIMEDOUT\" || shape.code === \"ECONNABORTED\") {\n return true;\n }\n\n return false;\n}\n\n/**\n * Attach the raw diagnostic fields to `error.context` so consumers\n * have everything the provider surfaced without each subclass having\n * to redeclare them. Never includes `cause` — that lives on\n * `error.cause`.\n */\nfunction buildContext(thrown: unknown, shape: OpenAIErrorShape): Record<string, unknown> {\n const context: Record<string, unknown> = {};\n\n if (shape.status !== undefined) {\n context.status = shape.status;\n }\n\n if (shape.code) {\n context.code = shape.code;\n }\n\n if (shape.type) {\n context.type = shape.type;\n }\n\n if (shape.param) {\n context.param = shape.param;\n }\n\n const requestId = readRequestId(thrown);\n\n if (requestId) {\n context.requestId = requestId;\n }\n\n return context;\n}\n\n/**\n * The request id OpenAI returned. Current SDKs expose it as\n * `APIError.requestID`; older ones and hand-rolled errors use\n * `request_id` / `requestId`. Extract defensively.\n */\nfunction readRequestId(thrown: unknown): string | undefined {\n if (typeof thrown !== \"object\" || thrown === null) {\n return undefined;\n }\n\n const raw = thrown as Record<string, unknown>;\n\n if (typeof raw.request_id === \"string\") {\n return raw.request_id;\n }\n\n if (typeof raw.requestId === \"string\") {\n return raw.requestId;\n }\n\n if (typeof raw.requestID === \"string\") {\n return raw.requestID;\n }\n\n return undefined;\n}\n\n/**\n * Parse the `Retry-After` response header (seconds per HTTP spec)\n * into milliseconds so consumers can feed it straight to `setTimeout`.\n * Returns `undefined` when missing or unparseable.\n */\nfunction parseRetryAfter(headers: Record<string, string> | undefined): number | undefined {\n if (!headers) {\n return undefined;\n }\n\n const raw = headers[\"retry-after\"] ?? headers[\"Retry-After\"];\n\n if (!raw) {\n return undefined;\n }\n\n const seconds = Number(raw);\n\n if (!Number.isFinite(seconds) || seconds < 0) {\n return undefined;\n }\n\n return Math.round(seconds * 1000);\n}\n"],"mappings":"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"}