@plumbus/core 0.8.0-beta.0 → 0.8.0-beta.10

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 (275) hide show
  1. package/CHANGELOG.md +197 -0
  2. package/README.md +8 -1
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/ai/ai/cost-tracker.d.ts +22 -2
  5. package/dist/ai/ai/model-pricing.d.ts +8 -2
  6. package/dist/ai/ai/openai-strict-schema.d.ts +84 -0
  7. package/dist/ai/ai/provider.d.ts +90 -6
  8. package/dist/ai/ai-service.d.ts +53 -1
  9. package/dist/ai/ai-service.d.ts.map +1 -1
  10. package/dist/ai/ai-service.js +550 -119
  11. package/dist/ai/ai-service.js.map +1 -1
  12. package/dist/ai/cost-tracker.d.ts +22 -2
  13. package/dist/ai/cost-tracker.d.ts.map +1 -1
  14. package/dist/ai/cost-tracker.js +59 -20
  15. package/dist/ai/cost-tracker.js.map +1 -1
  16. package/dist/ai/explainability.d.ts +2 -1
  17. package/dist/ai/explainability.d.ts.map +1 -1
  18. package/dist/ai/explainability.js +1 -0
  19. package/dist/ai/explainability.js.map +1 -1
  20. package/dist/ai/index.d.ts +2 -2
  21. package/dist/ai/index.d.ts.map +1 -1
  22. package/dist/ai/index.js +1 -1
  23. package/dist/ai/index.js.map +1 -1
  24. package/dist/ai/model-pricing.d.ts +8 -2
  25. package/dist/ai/model-pricing.d.ts.map +1 -1
  26. package/dist/ai/model-pricing.js +113 -15
  27. package/dist/ai/model-pricing.js.map +1 -1
  28. package/dist/ai/openai-strict-schema.d.ts +85 -0
  29. package/dist/ai/openai-strict-schema.d.ts.map +1 -0
  30. package/dist/ai/openai-strict-schema.js +730 -0
  31. package/dist/ai/openai-strict-schema.js.map +1 -0
  32. package/dist/ai/prompt-overrides.d.ts +19 -0
  33. package/dist/ai/prompt-overrides.d.ts.map +1 -0
  34. package/dist/ai/prompt-overrides.js +60 -0
  35. package/dist/ai/prompt-overrides.js.map +1 -0
  36. package/dist/ai/provider-failure.d.ts +9 -0
  37. package/dist/ai/provider-failure.d.ts.map +1 -0
  38. package/dist/ai/provider-failure.js +112 -0
  39. package/dist/ai/provider-failure.js.map +1 -0
  40. package/dist/ai/provider.d.ts +90 -6
  41. package/dist/ai/provider.d.ts.map +1 -1
  42. package/dist/ai/provider.js +546 -97
  43. package/dist/ai/provider.js.map +1 -1
  44. package/dist/ai/rag/pipeline.d.ts +21 -1
  45. package/dist/ai/rag/pipeline.d.ts.map +1 -1
  46. package/dist/ai/rag/pipeline.js +17 -1
  47. package/dist/ai/rag/pipeline.js.map +1 -1
  48. package/dist/ai/tool-loop.d.ts +8 -1
  49. package/dist/ai/tool-loop.d.ts.map +1 -1
  50. package/dist/ai/tool-loop.js +7 -0
  51. package/dist/ai/tool-loop.js.map +1 -1
  52. package/dist/ai/types/config.d.ts +13 -0
  53. package/dist/ai/types/fields.d.ts +6 -0
  54. package/dist/ai/types/prompt.d.ts +9 -0
  55. package/dist/ai/usage-validation.d.ts.map +1 -1
  56. package/dist/ai/usage-validation.js +6 -1
  57. package/dist/ai/usage-validation.js.map +1 -1
  58. package/dist/ai/validation.d.ts +7 -3
  59. package/dist/ai/validation.d.ts.map +1 -1
  60. package/dist/ai/validation.js +38 -25
  61. package/dist/ai/validation.js.map +1 -1
  62. package/dist/ai/zod-to-provider-schema.d.ts.map +1 -1
  63. package/dist/ai/zod-to-provider-schema.js +246 -16
  64. package/dist/ai/zod-to-provider-schema.js.map +1 -1
  65. package/dist/api/route-generator.d.ts +2 -12
  66. package/dist/api/route-generator.d.ts.map +1 -1
  67. package/dist/api/route-generator.js +33 -41
  68. package/dist/api/route-generator.js.map +1 -1
  69. package/dist/approvals/index.d.ts +1 -1
  70. package/dist/approvals/index.d.ts.map +1 -1
  71. package/dist/approvals/index.js.map +1 -1
  72. package/dist/approvals/memory-store.d.ts.map +1 -1
  73. package/dist/approvals/memory-store.js +22 -0
  74. package/dist/approvals/memory-store.js.map +1 -1
  75. package/dist/approvals/schema.d.ts +34 -0
  76. package/dist/approvals/schema.d.ts.map +1 -1
  77. package/dist/approvals/schema.js +2 -0
  78. package/dist/approvals/schema.js.map +1 -1
  79. package/dist/approvals/service.d.ts.map +1 -1
  80. package/dist/approvals/service.js +36 -0
  81. package/dist/approvals/service.js.map +1 -1
  82. package/dist/approvals/sql-store.d.ts.map +1 -1
  83. package/dist/approvals/sql-store.js +42 -1
  84. package/dist/approvals/sql-store.js.map +1 -1
  85. package/dist/approvals/types.d.ts +11 -0
  86. package/dist/approvals/types.d.ts.map +1 -1
  87. package/dist/approvals/types.js +1 -0
  88. package/dist/approvals/types.js.map +1 -1
  89. package/dist/audit/service.d.ts.map +1 -1
  90. package/dist/audit/service.js +22 -2
  91. package/dist/audit/service.js.map +1 -1
  92. package/dist/cli/cli.d.ts.map +1 -1
  93. package/dist/cli/cli.js +2 -1
  94. package/dist/cli/cli.js.map +1 -1
  95. package/dist/cli/commands/browser-extension.d.ts +16 -0
  96. package/dist/cli/commands/browser-extension.d.ts.map +1 -1
  97. package/dist/cli/commands/browser-extension.js +16 -5
  98. package/dist/cli/commands/browser-extension.js.map +1 -1
  99. package/dist/cli/commands/dev.d.ts.map +1 -1
  100. package/dist/cli/commands/dev.js +7 -1
  101. package/dist/cli/commands/dev.js.map +1 -1
  102. package/dist/cli/commands/e2e.d.ts +12 -0
  103. package/dist/cli/commands/e2e.d.ts.map +1 -1
  104. package/dist/cli/commands/e2e.js +11 -0
  105. package/dist/cli/commands/e2e.js.map +1 -1
  106. package/dist/cli/commands/generate-openapi-from-zod.d.ts.map +1 -1
  107. package/dist/cli/commands/generate-openapi-from-zod.js +9 -1
  108. package/dist/cli/commands/generate-openapi-from-zod.js.map +1 -1
  109. package/dist/cli/commands/index.d.ts +1 -0
  110. package/dist/cli/commands/index.d.ts.map +1 -1
  111. package/dist/cli/commands/index.js +1 -0
  112. package/dist/cli/commands/index.js.map +1 -1
  113. package/dist/cli/commands/init.d.ts +1 -1
  114. package/dist/cli/commands/init.d.ts.map +1 -1
  115. package/dist/cli/commands/init.js +63 -3
  116. package/dist/cli/commands/init.js.map +1 -1
  117. package/dist/cli/commands/migrate.d.ts.map +1 -1
  118. package/dist/cli/commands/migrate.js +8 -14
  119. package/dist/cli/commands/migrate.js.map +1 -1
  120. package/dist/cli/commands/payments.d.ts +56 -0
  121. package/dist/cli/commands/payments.d.ts.map +1 -0
  122. package/dist/cli/commands/payments.js +184 -0
  123. package/dist/cli/commands/payments.js.map +1 -0
  124. package/dist/cli/commands/run.d.ts.map +1 -1
  125. package/dist/cli/commands/run.js +2 -7
  126. package/dist/cli/commands/run.js.map +1 -1
  127. package/dist/cli/commands/seed.d.ts.map +1 -1
  128. package/dist/cli/commands/seed.js +2 -7
  129. package/dist/cli/commands/seed.js.map +1 -1
  130. package/dist/cli/commands/start.d.ts.map +1 -1
  131. package/dist/cli/commands/start.js +9 -1
  132. package/dist/cli/commands/start.js.map +1 -1
  133. package/dist/cli/commands/worker.js +1 -1
  134. package/dist/cli/commands/worker.js.map +1 -1
  135. package/dist/cli/discover.d.ts +2 -0
  136. package/dist/cli/discover.d.ts.map +1 -1
  137. package/dist/cli/discover.js +39 -5
  138. package/dist/cli/discover.js.map +1 -1
  139. package/dist/config/loader.d.ts +4 -1
  140. package/dist/config/loader.d.ts.map +1 -1
  141. package/dist/config/loader.js +112 -24
  142. package/dist/config/loader.js.map +1 -1
  143. package/dist/data/connection.d.ts +23 -0
  144. package/dist/data/connection.d.ts.map +1 -1
  145. package/dist/data/connection.js +23 -6
  146. package/dist/data/connection.js.map +1 -1
  147. package/dist/data/schema-generator.d.ts.map +1 -1
  148. package/dist/data/schema-generator.js +2 -2
  149. package/dist/data/schema-generator.js.map +1 -1
  150. package/dist/define/definePrompt.d.ts.map +1 -1
  151. package/dist/define/definePrompt.js +6 -0
  152. package/dist/define/definePrompt.js.map +1 -1
  153. package/dist/durable/index.d.ts +2 -2
  154. package/dist/durable/index.d.ts.map +1 -1
  155. package/dist/durable/index.js +2 -2
  156. package/dist/durable/index.js.map +1 -1
  157. package/dist/durable/postgres-persist.d.ts +47 -2
  158. package/dist/durable/postgres-persist.d.ts.map +1 -1
  159. package/dist/durable/postgres-persist.js +152 -6
  160. package/dist/durable/postgres-persist.js.map +1 -1
  161. package/dist/durable/spine-claim.d.ts +22 -1
  162. package/dist/durable/spine-claim.d.ts.map +1 -1
  163. package/dist/durable/spine-claim.js +54 -3
  164. package/dist/durable/spine-claim.js.map +1 -1
  165. package/dist/errors/http.d.ts.map +1 -1
  166. package/dist/errors/http.js +9 -1
  167. package/dist/errors/http.js.map +1 -1
  168. package/dist/events/dispatcher.d.ts +20 -0
  169. package/dist/events/dispatcher.d.ts.map +1 -1
  170. package/dist/events/dispatcher.js +61 -26
  171. package/dist/events/dispatcher.js.map +1 -1
  172. package/dist/events/pipeline-audit.d.ts +14 -0
  173. package/dist/events/pipeline-audit.d.ts.map +1 -0
  174. package/dist/events/pipeline-audit.js +29 -0
  175. package/dist/events/pipeline-audit.js.map +1 -0
  176. package/dist/events/worker.d.ts +3 -0
  177. package/dist/events/worker.d.ts.map +1 -1
  178. package/dist/events/worker.js +14 -14
  179. package/dist/events/worker.js.map +1 -1
  180. package/dist/execution/capability-error-hook.d.ts +24 -0
  181. package/dist/execution/capability-error-hook.d.ts.map +1 -0
  182. package/dist/execution/capability-error-hook.js +17 -0
  183. package/dist/execution/capability-error-hook.js.map +1 -0
  184. package/dist/execution/capability-executor.d.ts +2 -2
  185. package/dist/execution/capability-executor.d.ts.map +1 -1
  186. package/dist/execution/capability-executor.js +45 -7
  187. package/dist/execution/capability-executor.js.map +1 -1
  188. package/dist/execution/context-factory.d.ts.map +1 -1
  189. package/dist/execution/context-factory.js +9 -1
  190. package/dist/execution/context-factory.js.map +1 -1
  191. package/dist/execution/deferred-flow-service.d.ts.map +1 -1
  192. package/dist/execution/deferred-flow-service.js +7 -0
  193. package/dist/execution/deferred-flow-service.js.map +1 -1
  194. package/dist/execution/index.d.ts +1 -0
  195. package/dist/execution/index.d.ts.map +1 -1
  196. package/dist/execution/index.js.map +1 -1
  197. package/dist/fields/index.d.ts +7 -0
  198. package/dist/fields/index.d.ts.map +1 -1
  199. package/dist/fields/index.js +10 -1
  200. package/dist/fields/index.js.map +1 -1
  201. package/dist/flows/dead-letter.d.ts +18 -1
  202. package/dist/flows/dead-letter.d.ts.map +1 -1
  203. package/dist/flows/dead-letter.js +17 -1
  204. package/dist/flows/dead-letter.js.map +1 -1
  205. package/dist/flows/engine.d.ts +16 -1
  206. package/dist/flows/engine.d.ts.map +1 -1
  207. package/dist/flows/engine.js +218 -29
  208. package/dist/flows/engine.js.map +1 -1
  209. package/dist/flows/flow-service.d.ts.map +1 -1
  210. package/dist/flows/flow-service.js +3 -0
  211. package/dist/flows/flow-service.js.map +1 -1
  212. package/dist/flows/step-executor.d.ts +8 -0
  213. package/dist/flows/step-executor.d.ts.map +1 -1
  214. package/dist/flows/step-executor.js.map +1 -1
  215. package/dist/index.d.ts +4 -4
  216. package/dist/index.d.ts.map +1 -1
  217. package/dist/index.js +1 -1
  218. package/dist/index.js.map +1 -1
  219. package/dist/runtime/bootstrap.d.ts +18 -1
  220. package/dist/runtime/bootstrap.d.ts.map +1 -1
  221. package/dist/runtime/bootstrap.js +36 -5
  222. package/dist/runtime/bootstrap.js.map +1 -1
  223. package/dist/runtime/load-extensions.d.ts +2 -1
  224. package/dist/runtime/load-extensions.d.ts.map +1 -1
  225. package/dist/runtime/load-extensions.js +49 -1
  226. package/dist/runtime/load-extensions.js.map +1 -1
  227. package/dist/runtime/register-consumers.d.ts +3 -0
  228. package/dist/runtime/register-consumers.d.ts.map +1 -1
  229. package/dist/runtime/register-consumers.js +27 -6
  230. package/dist/runtime/register-consumers.js.map +1 -1
  231. package/dist/runtime/start-worker-pool.d.ts.map +1 -1
  232. package/dist/runtime/start-worker-pool.js +36 -15
  233. package/dist/runtime/start-worker-pool.js.map +1 -1
  234. package/dist/server/bootstrap.d.ts +42 -16
  235. package/dist/server/bootstrap.d.ts.map +1 -1
  236. package/dist/server/bootstrap.js +91 -21
  237. package/dist/server/bootstrap.js.map +1 -1
  238. package/dist/testing/context.d.ts +2 -0
  239. package/dist/testing/context.d.ts.map +1 -1
  240. package/dist/testing/context.js +8 -3
  241. package/dist/testing/context.js.map +1 -1
  242. package/dist/testing/field-validation.d.ts.map +1 -1
  243. package/dist/testing/field-validation.js +3 -0
  244. package/dist/testing/field-validation.js.map +1 -1
  245. package/dist/types/config.d.ts +13 -0
  246. package/dist/types/config.d.ts.map +1 -1
  247. package/dist/types/context.d.ts +70 -4
  248. package/dist/types/context.d.ts.map +1 -1
  249. package/dist/types/fields.d.ts +6 -0
  250. package/dist/types/fields.d.ts.map +1 -1
  251. package/dist/types/index.d.ts +1 -1
  252. package/dist/types/index.d.ts.map +1 -1
  253. package/dist/types/index.js.map +1 -1
  254. package/dist/types/prompt.d.ts +9 -0
  255. package/dist/types/prompt.d.ts.map +1 -1
  256. package/dist/worker/bootstrap.d.ts +19 -0
  257. package/dist/worker/bootstrap.d.ts.map +1 -1
  258. package/dist/worker/bootstrap.js +156 -84
  259. package/dist/worker/bootstrap.js.map +1 -1
  260. package/instructions/README.md +4 -1
  261. package/instructions/ai-classification.md +132 -0
  262. package/instructions/ai.md +94 -15
  263. package/instructions/cli.md +19 -1
  264. package/instructions/entities.md +3 -1
  265. package/instructions/flows.md +6 -0
  266. package/instructions/framework.md +13 -3
  267. package/instructions/peer-dependencies.md +2 -1
  268. package/instructions/prompts.md +2 -1
  269. package/migrations/durable-tenant/0002_approval_cancellation.sql +3 -0
  270. package/migrations/durable-tenant/meta/_journal.json +7 -0
  271. package/package.json +5 -4
  272. package/dist/ai/__typecheck-fixtures__/legacy-cost-api.d.ts +0 -5
  273. package/dist/ai/__typecheck-fixtures__/legacy-cost-api.d.ts.map +0 -1
  274. package/dist/ai/__typecheck-fixtures__/legacy-cost-api.js +0 -15
  275. package/dist/ai/__typecheck-fixtures__/legacy-cost-api.js.map +0 -1
@@ -2,13 +2,74 @@
2
2
  // Abstract interface for AI provider adapters (OpenAI, Anthropic, etc.)
3
3
  import { createRequire } from 'node:module';
4
4
  import { allKnownModels, estimateModelCost as calculateModelCost, } from './model-pricing.js';
5
+ import { createOpenAIStrictOutputStreamUnwrapper, toOpenAIStrictOutputSchema, unwrapOpenAIStrictOutput, } from './openai-strict-schema.js';
5
6
  import { AIIncompleteOutputError, AIInvalidRequestError, AIRefusalError } from './refusal.js';
6
- /** Compose a request's optional user signal with a timeout, returning a single AbortSignal. */
7
- export function combineRequestSignals(userSignal, timeoutMs) {
8
- const timeoutSignal = AbortSignal.timeout(timeoutMs);
9
- if (!userSignal)
10
- return timeoutSignal;
11
- return AbortSignal.any([userSignal, timeoutSignal]);
7
+ /**
8
+ * Resolve a `cache` option into per-section flags.
9
+ * `true` → `{ system: true, tools: true, messages: false }`.
10
+ * `false` / `undefined` / no section enabled → `undefined` (no provider marks).
11
+ */
12
+ function resolvePromptCache(cache) {
13
+ if (cache === undefined || cache === false)
14
+ return undefined;
15
+ if (cache === true)
16
+ return { system: true, tools: true, messages: false };
17
+ const resolved = {
18
+ system: cache.system === true,
19
+ tools: cache.tools === true,
20
+ messages: cache.messages === true,
21
+ };
22
+ if (!resolved.system && !resolved.tools && !resolved.messages)
23
+ return undefined;
24
+ return resolved;
25
+ }
26
+ const ANTHROPIC_CACHE_CONTROL = Object.freeze({ type: 'ephemeral' });
27
+ /** Attach Anthropic `cache_control` to the last content block of a message. */
28
+ function withAnthropicMessageCacheControl(message) {
29
+ const content = message.content;
30
+ if (typeof content === 'string') {
31
+ return {
32
+ ...message,
33
+ content: [{ type: 'text', text: content, cache_control: { ...ANTHROPIC_CACHE_CONTROL } }],
34
+ };
35
+ }
36
+ if (!Array.isArray(content) || content.length === 0)
37
+ return message;
38
+ const blocks = content.map((block, index) => {
39
+ if (index !== content.length - 1 || block == null || typeof block !== 'object') {
40
+ return block;
41
+ }
42
+ return { ...block, cache_control: { ...ANTHROPIC_CACHE_CONTROL } };
43
+ });
44
+ return { ...message, content: blocks };
45
+ }
46
+ /** Apply Anthropic cache marks to a Messages API request body in place. */
47
+ function applyAnthropicPromptCache(body, cache) {
48
+ if (cache.system && typeof body.system === 'string' && body.system.length > 0) {
49
+ body.system = [
50
+ {
51
+ type: 'text',
52
+ text: body.system,
53
+ cache_control: { ...ANTHROPIC_CACHE_CONTROL },
54
+ },
55
+ ];
56
+ }
57
+ if (cache.tools && Array.isArray(body.tools) && body.tools.length > 0) {
58
+ const tools = body.tools;
59
+ const lastIndex = tools.length - 1;
60
+ const last = tools[lastIndex];
61
+ if (last) {
62
+ tools[lastIndex] = { ...last, cache_control: { ...ANTHROPIC_CACHE_CONTROL } };
63
+ }
64
+ }
65
+ if (cache.messages && Array.isArray(body.messages) && body.messages.length > 0) {
66
+ const messages = body.messages;
67
+ const lastIndex = messages.length - 1;
68
+ const last = messages[lastIndex];
69
+ if (last) {
70
+ messages[lastIndex] = withAnthropicMessageCacheControl(last);
71
+ }
72
+ }
12
73
  }
13
74
  /**
14
75
  * Shared logic used by adapter `listModels` implementations: join a list of
@@ -20,6 +81,11 @@ function knownReasoningEfforts(provider, model) {
20
81
  if (provider !== 'openai')
21
82
  return null;
22
83
  const id = model.replace(/-\d{4}-\d{2}-\d{2}$/, '');
84
+ // https://developers.openai.com/api/docs/guides/latest-model and each model's page.
85
+ if (['gpt-6-sol', 'gpt-6-luna'].includes(id))
86
+ return ['none', 'low', 'medium', 'high', 'xhigh', 'max'];
87
+ if (['gpt-6-astra', 'gpt-6.1-sol'].includes(id))
88
+ return ['low', 'medium', 'high', 'xhigh', 'max'];
23
89
  if (['gpt-5.6', 'gpt-5.6-sol', 'gpt-5.6-terra', 'gpt-5.6-luna'].includes(id))
24
90
  return ['none', 'low', 'medium', 'high', 'xhigh', 'max'];
25
91
  if (['gpt-5.2', 'gpt-5.4', 'gpt-5.5'].includes(id))
@@ -81,15 +147,28 @@ export class ProviderAPIError extends Error {
81
147
  statusCode;
82
148
  retryable;
83
149
  attempts;
150
+ /**
151
+ * Set when the request's own timeout ended it (`ProviderRequest.timeout`, a prompt's
152
+ * `model.timeoutMs`, or the adapter's `requestTimeout`): the limit that was reached, in
153
+ * milliseconds. Such a failure is not retryable — the provider may have billed the work, and
154
+ * asking again with the same limit usually ends the same way.
155
+ */
156
+ timeoutMs;
84
157
  constructor(args) {
85
- super(args.message);
158
+ super(args.message, args.cause === undefined ? undefined : { cause: args.cause });
86
159
  this.name = 'ProviderAPIError';
87
160
  this.providerName = args.providerName;
88
161
  this.statusCode = args.statusCode;
89
162
  this.retryable = args.retryable;
90
163
  this.attempts = args.attempts;
164
+ if (args.timeoutMs !== undefined)
165
+ this.timeoutMs = args.timeoutMs;
91
166
  }
92
167
  }
168
+ /**
169
+ * True for a provider HTTP failure from any adapter: a `ProviderAPIError` instance, or an error
170
+ * of the same shape built by an adapter package with its own copy of core (`@plumbus/ai-bedrock`).
171
+ */
93
172
  export function isProviderAPIError(value) {
94
173
  if (value instanceof ProviderAPIError)
95
174
  return true;
@@ -102,6 +181,14 @@ export function isProviderAPIError(value) {
102
181
  typeof candidate.retryable === 'boolean' &&
103
182
  typeof candidate.attempts === 'number');
104
183
  }
184
+ /**
185
+ * True when the request's own timeout ended it (`timeoutMs` on the call, a prompt override, the
186
+ * prompt's `model.timeoutMs`, or the adapter's `requestTimeout`): a `ProviderAPIError` carrying
187
+ * the limit reached as `timeoutMs`. Not retryable; raise the timeout rather than asking again.
188
+ */
189
+ export function isProviderTimeoutError(value) {
190
+ return isProviderAPIError(value) && typeof value.timeoutMs === 'number';
191
+ }
105
192
  const retryableStatusCodes = new Set([408, 429, 500, 502, 503, 504]);
106
193
  const maxProviderAttempts = 3;
107
194
  const baseRetryDelayMs = 750;
@@ -128,6 +215,13 @@ function computeRetryDelayMs(attempt, retryAfterHeader) {
128
215
  }
129
216
  return Math.min(baseRetryDelayMs * 2 ** (attempt - 1), maxRetryDelayMs);
130
217
  }
218
+ // Every OpenAI structured-output path is strict, and strict mode refuses optional properties:
219
+ // each one goes out as required-but-nullable. The AI service drops the nulls again before the
220
+ // prompt's Zod schema validates the answer (`restoreOmittedOptionalProperties`). String formats
221
+ // strict mode does not support (`uri`) are left out of the sent schema, and `allOf` is merged
222
+ // into one schema; Zod still checks the original. A root that is not an object (an array, a
223
+ // union) goes inside a `{ "value": ... }` envelope that `unwrapOpenAIStructuredContent` (or, on a
224
+ // stream, `unwrapOpenAIStructuredStream`) takes off the answer again.
131
225
  function buildOpenAIResponseFormat(request) {
132
226
  if (request.responseFormat !== 'json')
133
227
  return undefined;
@@ -138,27 +232,63 @@ function buildOpenAIResponseFormat(request) {
138
232
  json_schema: {
139
233
  name: 'response',
140
234
  strict: true,
141
- schema: request.responseSchema,
235
+ schema: toOpenAIStrictOutputSchema(request.responseSchema).schema,
142
236
  },
143
237
  };
144
238
  }
239
+ /** Whether the request's strict structured output went out inside the root envelope. */
240
+ function hasOpenAIStructuredEnvelope(request) {
241
+ return (request.responseFormat === 'json' &&
242
+ !!request.responseSchema &&
243
+ toOpenAIStrictOutputSchema(request.responseSchema).enveloped);
244
+ }
245
+ /** The answer as the caller's schema describes it: out of the root envelope if it was sent in one. */
246
+ function unwrapOpenAIStructuredContent(request, content) {
247
+ return hasOpenAIStructuredEnvelope(request) ? unwrapOpenAIStrictOutput(content) : content;
248
+ }
249
+ /** A Chat Completions stream with the root envelope taken out of its text deltas. */
250
+ async function* unwrapOpenAIStructuredStream(events) {
251
+ const unwrapper = createOpenAIStrictOutputStreamUnwrapper();
252
+ let ended = false;
253
+ for await (const event of events) {
254
+ if (event.type === 'content_delta' && typeof event.delta === 'string') {
255
+ const delta = unwrapper.push(event.delta);
256
+ if (delta)
257
+ yield { ...event, delta };
258
+ continue;
259
+ }
260
+ if (event.type === 'done' && !ended) {
261
+ ended = true;
262
+ const rest = unwrapper.end();
263
+ if (rest)
264
+ yield { type: 'content_delta', delta: rest };
265
+ }
266
+ yield event;
267
+ }
268
+ if (!ended) {
269
+ const rest = unwrapper.end();
270
+ if (rest)
271
+ yield { type: 'content_delta', delta: rest };
272
+ }
273
+ }
145
274
  const OPENAI_STRUCTURED_OUTPUT_TOOL_NAME = 'return_structured_response';
146
275
  function shouldUseOpenAIStructuredTool(args) {
147
276
  return (args.request.structuredOutputTransport === 'tool' &&
148
277
  args.request.responseFormat === 'json' &&
149
278
  !!args.request.responseSchema);
150
279
  }
151
- function buildOpenAIStructuredOutputTool(schema) {
280
+ /** The structured-output function; Chat Completions nests it under `function`, Responses does not. */
281
+ function openAIStructuredOutputFunction(schema) {
152
282
  return {
153
- type: 'function',
154
- function: {
155
- name: OPENAI_STRUCTURED_OUTPUT_TOOL_NAME,
156
- description: 'Return the complete structured response for this request.',
157
- strict: true,
158
- parameters: schema,
159
- },
283
+ name: OPENAI_STRUCTURED_OUTPUT_TOOL_NAME,
284
+ description: 'Return the complete structured response for this request.',
285
+ strict: true,
286
+ parameters: toOpenAIStrictOutputSchema(schema).schema,
160
287
  };
161
288
  }
289
+ function buildOpenAIStructuredOutputTool(schema) {
290
+ return { type: 'function', function: openAIStructuredOutputFunction(schema) };
291
+ }
162
292
  function extractOpenAIMessageContent(message) {
163
293
  const structuredToolCall = message.tool_calls?.find((toolCall) => toolCall.function?.name === OPENAI_STRUCTURED_OUTPUT_TOOL_NAME);
164
294
  const toolArguments = structuredToolCall?.function?.arguments;
@@ -183,11 +313,16 @@ function buildEmptyOpenAIStructuredContentError(args) {
183
313
  }
184
314
  function openAIUsageToTokenUsage(usage) {
185
315
  const reasoning = usage.completion_tokens_details?.reasoning_tokens;
316
+ const cacheWrites = usage.prompt_tokens_details?.cache_write_tokens;
186
317
  return {
187
318
  inputTokens: usage.prompt_tokens,
188
319
  outputTokens: usage.completion_tokens,
189
320
  totalTokens: usage.total_tokens,
190
321
  cachedInputTokens: usage.prompt_tokens_details?.cached_tokens ?? 0,
322
+ // Prompt tokens written to the cache, counted inside `prompt_tokens` like cache reads. GPT-5.6
323
+ // and later bill them at 1.25× input (`estimateModelCost`); unread they were priced as
324
+ // ordinary input.
325
+ ...(typeof cacheWrites === 'number' ? { cacheWriteTokens: cacheWrites } : {}),
191
326
  // Reasoning models count their thinking inside `completion_tokens`. Without this split a
192
327
  // caller that hit `max_completion_tokens` cannot tell a long answer from a long deliberation,
193
328
  // and can only guess at which knob to turn.
@@ -196,19 +331,26 @@ function openAIUsageToTokenUsage(usage) {
196
331
  }
197
332
  function shouldUseOpenAIMaxCompletionTokens(model) {
198
333
  const normalizedModel = model.trim().toLowerCase();
199
- return /^(?:o\d|o\d-|o\d\.)/.test(normalizedModel) || normalizedModel.startsWith('gpt-5');
334
+ return (/^(?:o\d|o\d-|o\d\.)/.test(normalizedModel) ||
335
+ normalizedModel.startsWith('gpt-5') ||
336
+ normalizedModel.startsWith('gpt-6'));
200
337
  }
201
338
  /**
202
- * `gpt-5.5+` only accept the API default temperature (1). Sending any other
203
- * value (including Plumbus's 0.7 default) returns HTTP 400 `unsupported_value`.
339
+ * `gpt-5.5+` only accept the API default temperature (1). GPT-6 accepts
340
+ * temperature only when reasoning is explicitly disabled. Sending any other
341
+ * value with active/default GPT-6 reasoning returns HTTP 400.
204
342
  *
205
343
  * Do not broaden this to all `gpt-5*` models — earlier lines such as
206
344
  * `gpt-5.4` / `gpt-5.4-mini` still support custom temperature. Token-limit
207
345
  * mapping (`max_completion_tokens`) remains a separate, wider check.
208
346
  */
209
- function shouldOmitOpenAITemperature(model) {
347
+ function shouldOmitOpenAITemperature(model, request) {
210
348
  const normalizedModel = model.trim().toLowerCase();
211
- return /^gpt-5\.(?:[5-9]|\d{2,})(?:$|[-.])/.test(normalizedModel);
349
+ if (/^gpt-5\.(?:[5-9]|\d{2,})(?:$|[-.])/.test(normalizedModel))
350
+ return true;
351
+ if (!isGpt6Family(model))
352
+ return false;
353
+ return normalizeProviderRequestReasoning(request)?.mode !== 'disabled';
212
354
  }
213
355
  function applyOpenAITokenLimit(body, model, maxTokens) {
214
356
  if (!maxTokens)
@@ -219,10 +361,10 @@ function applyOpenAITokenLimit(body, model, maxTokens) {
219
361
  }
220
362
  body.max_tokens = maxTokens;
221
363
  }
222
- function applyOpenAITemperature(body, model, temperature) {
223
- if (shouldOmitOpenAITemperature(model))
364
+ function applyOpenAITemperature(body, model, request) {
365
+ if (shouldOmitOpenAITemperature(model, request))
224
366
  return;
225
- body.temperature = temperature ?? 0.7;
367
+ body.temperature = request.temperature ?? 0.7;
226
368
  }
227
369
  function normalizeProviderRequestReasoning(request) {
228
370
  return request.reasoning;
@@ -233,8 +375,28 @@ function unsupportedReasoning(provider, reasoning) {
233
375
  message: `AI provider "${provider}" cannot represent reasoning mode "${reasoning.mode}"`,
234
376
  });
235
377
  }
378
+ /**
379
+ * GPT-6 models accept no `minimal` effort, and GPT-6 Astra and GPT-6.1 Sol also
380
+ * reject `none`, so disabled reasoning cannot be represented for them.
381
+ */
382
+ function assertOpenAIReasoningSupported(model, reasoning) {
383
+ if (!isGpt6Family(model))
384
+ return;
385
+ if (reasoning.mode === 'effort' && reasoning.effort === 'minimal') {
386
+ throw new AIInvalidRequestError({
387
+ reason: 'reasoning_effort_unsupported',
388
+ message: `OpenAI model "${model}" does not support reasoning effort "minimal"; use "low"`,
389
+ });
390
+ }
391
+ if (reasoning.mode === 'disabled' && isGpt6WithoutNoneEffort(model)) {
392
+ throw new AIInvalidRequestError({
393
+ reason: 'reasoning_disabled_unsupported',
394
+ message: `OpenAI model "${model}" cannot disable reasoning; its lowest effort is "low"`,
395
+ });
396
+ }
397
+ }
236
398
  /** Translate provider-neutral reasoning onto OpenAI's top-level `reasoning_effort`. */
237
- function applyOpenAIReasoning(body, request) {
399
+ function applyOpenAIReasoning(body, request, model) {
238
400
  const reasoning = normalizeProviderRequestReasoning(request);
239
401
  if (!reasoning) {
240
402
  if (request.reasoningEffort)
@@ -243,11 +405,70 @@ function applyOpenAIReasoning(body, request) {
243
405
  }
244
406
  if (reasoning.mode === 'budget')
245
407
  unsupportedReasoning('openai', reasoning);
408
+ assertOpenAIReasoningSupported(model, reasoning);
246
409
  body.reasoning_effort = reasoning.mode === 'disabled' ? 'none' : reasoning.effort;
247
410
  }
411
+ /**
412
+ * Claude models that return HTTP 400 for any non-default `temperature`, `top_p` or
413
+ * `top_k` on every request, thinking on or off. `disabled` is the `thinking` type sent
414
+ * for `{ mode: 'disabled' }` (Sonnet 5.5 takes `between_tools` instead); a model
415
+ * without it cannot turn thinking off. Opus 5, Sonnet 5.5 and Haiku 5.5 accept that
416
+ * type only at effort `high` or below; a disabled request sends no effort, and their
417
+ * defaults are `high` (`medium` on Haiku 5.5). Of these models only Mythos Preview
418
+ * accepts `thinking: { type: 'enabled', budget_tokens }`.
419
+ * Sources: https://platform.claude.com/docs/en/build-with-claude/thinking#sampling-parameters
420
+ * and https://platform.claude.com/docs/en/build-with-claude/thinking-troubleshooting
421
+ */
422
+ const FIXED_SAMPLING_CLAUDE_MODELS = {
423
+ 'fable-5-1': {},
424
+ 'mythos-5-1': {},
425
+ 'fable-5': {},
426
+ 'mythos-5': {},
427
+ 'mythos-preview': { budget: true },
428
+ 'opus-5-5': {},
429
+ 'opus-5': { disabled: 'disabled' },
430
+ 'opus-4-8': { disabled: 'disabled' },
431
+ 'opus-4-7': { disabled: 'disabled' },
432
+ 'sonnet-5-5': { disabled: 'between_tools' },
433
+ 'sonnet-5': { disabled: 'disabled' },
434
+ 'haiku-5-5': { disabled: 'disabled' },
435
+ };
436
+ /** Looks up a fixed-sampling Claude model, including dated and platform-prefixed ids. */
437
+ function fixedSamplingClaudeModel(model) {
438
+ if (typeof model !== 'string')
439
+ return undefined;
440
+ const normalizedModel = model.trim().toLowerCase();
441
+ const id = /(?:^|[./])claude-([a-z]+-(?:\d+(?:-\d{1,2})?|preview))(?=$|[-@:])/.exec(normalizedModel)?.[1];
442
+ return id && Object.hasOwn(FIXED_SAMPLING_CLAUDE_MODELS, id)
443
+ ? FIXED_SAMPLING_CLAUDE_MODELS[id]
444
+ : undefined;
445
+ }
248
446
  /** Translate provider-neutral reasoning onto Anthropic Messages API fields. */
249
447
  function applyAnthropicReasoning(body, request) {
250
448
  const reasoning = normalizeProviderRequestReasoning(request);
449
+ const fixedSampling = fixedSamplingClaudeModel(body.model);
450
+ // These models accept only the API default, so any temperature (Plumbus's 0.7
451
+ // default or a prompt's own) is left out, as for OpenAI gpt-5.5+.
452
+ const temperature = fixedSampling ? undefined : request.temperature;
453
+ if (fixedSampling) {
454
+ delete body.temperature;
455
+ if (reasoning?.mode === 'budget' && !fixedSampling.budget) {
456
+ throw new AIInvalidRequestError({
457
+ reason: 'reasoning_budget_unsupported',
458
+ message: `Anthropic model "${body.model}" does not accept a thinking budget; use an effort`,
459
+ });
460
+ }
461
+ if (reasoning?.mode === 'disabled') {
462
+ if (!fixedSampling.disabled) {
463
+ throw new AIInvalidRequestError({
464
+ reason: 'reasoning_disabled_unsupported',
465
+ message: `Anthropic model "${body.model}" cannot turn thinking off; use effort "low"`,
466
+ });
467
+ }
468
+ body.thinking = { type: fixedSampling.disabled };
469
+ return;
470
+ }
471
+ }
251
472
  if (!reasoning)
252
473
  return;
253
474
  if (reasoning.mode === 'disabled') {
@@ -267,13 +488,13 @@ function applyAnthropicReasoning(body, request) {
267
488
  message: 'Anthropic reasoning budget must be smaller than maxTokens',
268
489
  });
269
490
  }
270
- if (request.temperature != null && request.temperature !== 1) {
491
+ if (temperature != null && temperature !== 1) {
271
492
  throw new AIInvalidRequestError({
272
493
  reason: 'reasoning_temperature_conflict',
273
494
  message: 'Anthropic thinking requires temperature 1 or an omitted temperature',
274
495
  });
275
496
  }
276
- if (request.temperature == null)
497
+ if (temperature == null)
277
498
  delete body.temperature;
278
499
  body.thinking = {
279
500
  type: 'enabled',
@@ -285,13 +506,13 @@ function applyAnthropicReasoning(body, request) {
285
506
  if (reasoning.effort === 'minimal') {
286
507
  unsupportedReasoning('anthropic', reasoning);
287
508
  }
288
- if (request.temperature != null && request.temperature !== 1) {
509
+ if (temperature != null && temperature !== 1) {
289
510
  throw new AIInvalidRequestError({
290
511
  reason: 'reasoning_temperature_conflict',
291
512
  message: 'Anthropic adaptive thinking requires temperature 1 or an omitted temperature',
292
513
  });
293
514
  }
294
- if (request.temperature == null)
515
+ if (temperature == null)
295
516
  delete body.temperature;
296
517
  body.thinking = { type: 'adaptive', display: 'omitted' };
297
518
  const outputConfig = body.output_config && typeof body.output_config === 'object'
@@ -321,30 +542,157 @@ function buildProviderErrorMessage(label, statusCode, body, attempts) {
321
542
  const detail = body || 'Request failed';
322
543
  return `${label} error (${statusCode})${suffix}: ${detail}`;
323
544
  }
545
+ // ── Request deadlines ──
546
+ /** The longest delay Node's timers keep: `AbortSignal.timeout` fires at once above it. */
547
+ const MAX_REQUEST_TIMEOUT_MS = 2_147_483_647;
548
+ /**
549
+ * Where Node's fetch (undici, bundled or installed) keeps the process's dispatcher: undici 7.27+
550
+ * under `.2` (and the same agent under `.1`), earlier versions under `.1` only.
551
+ */
552
+ const UNDICI_GLOBAL_DISPATCHERS = [
553
+ Symbol.for('undici.globalDispatcher.2'),
554
+ Symbol.for('undici.globalDispatcher.1'),
555
+ ];
556
+ function globalUndiciDispatcher() {
557
+ for (const key of UNDICI_GLOBAL_DISPATCHERS) {
558
+ const dispatcher = globalThis[key];
559
+ if (typeof dispatcher?.dispatch === 'function') {
560
+ return dispatcher;
561
+ }
562
+ }
563
+ return undefined;
564
+ }
565
+ /**
566
+ * How far above the request's timeout undici's own limits are set. Undici runs them on coarse
567
+ * timers (a 499 ms tick) that can fire up to half a second before their delay, so limits equal
568
+ * to the timeout beat the request's own timer on a reused connection and the call failed as a
569
+ * bare `fetch failed` instead of the timeout. One second keeps undici's limits strictly behind.
570
+ */
571
+ const TRANSPORT_LIMIT_MARGIN_MS = 1_000;
572
+ /**
573
+ * Node's fetch gives up by itself after 300 s without response headers, or without the next
574
+ * body chunk (undici's `headersTimeout` / `bodyTimeout`). A non-streamed answer that took longer
575
+ * to generate failed with `fetch failed` (`UND_ERR_HEADERS_TIMEOUT`) whatever the request's
576
+ * timeout allowed. This dispatcher passes each request to the process's dispatcher — a proxy or
577
+ * agent the application installed included — with both limits set just above the request's
578
+ * timeout, so that timeout alone decides. Fetch implementations other than undici ignore
579
+ * `dispatcher`.
580
+ */
581
+ function deadlineDispatcher(timeoutMs) {
582
+ const transportLimitMs = Math.min(timeoutMs + TRANSPORT_LIMIT_MARGIN_MS, MAX_REQUEST_TIMEOUT_MS);
583
+ return {
584
+ get isMockActive() {
585
+ return globalUndiciDispatcher()?.isMockActive;
586
+ },
587
+ dispatch(options, handler) {
588
+ const dispatcher = globalUndiciDispatcher();
589
+ if (!dispatcher)
590
+ throw new Error('Node fetch has no global dispatcher');
591
+ return dispatcher.dispatch({ ...options, headersTimeout: transportLimitMs, bodyTimeout: transportLimitMs }, handler);
592
+ },
593
+ };
594
+ }
595
+ /** `error` and the errors it wraps (`cause`), outermost first. */
596
+ function errorChain(error) {
597
+ const chain = [];
598
+ let current = error;
599
+ for (let depth = 0; depth < 5 && typeof current === 'object' && current !== null; depth += 1) {
600
+ chain.push(current);
601
+ current = current.cause;
602
+ }
603
+ return chain;
604
+ }
605
+ /** Throws `AIInvalidRequestError` (before any I/O) unless `timeoutMs` is a usable request timeout. */
606
+ export function assertRequestTimeout(timeoutMs) {
607
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_REQUEST_TIMEOUT_MS) {
608
+ throw new AIInvalidRequestError({
609
+ reason: 'timeout_invalid',
610
+ message: `AI request timeout must be a whole number of milliseconds from 1 to ${MAX_REQUEST_TIMEOUT_MS}; got ${String(timeoutMs)}`,
611
+ });
612
+ }
613
+ }
614
+ function createRequestDeadline(args) {
615
+ const timeoutMs = args.timeout ?? args.defaultTimeout;
616
+ assertRequestTimeout(timeoutMs);
617
+ let attempts = 0;
618
+ let timer;
619
+ return {
620
+ providerName: args.providerName,
621
+ errorLabel: args.errorLabel,
622
+ timeoutMs,
623
+ attempt() {
624
+ attempts += 1;
625
+ timer = AbortSignal.timeout(timeoutMs);
626
+ return {
627
+ signal: args.signal ? AbortSignal.any([args.signal, timer]) : timer,
628
+ dispatcher: deadlineDispatcher(timeoutMs),
629
+ };
630
+ },
631
+ classify(error) {
632
+ if (!timer?.aborted || isProviderAPIError(error))
633
+ return error;
634
+ const chain = errorChain(error);
635
+ const aborted = chain.some((link) => {
636
+ const name = link.name;
637
+ return link === timer?.reason || name === 'TimeoutError' || name === 'AbortError';
638
+ });
639
+ if (!aborted)
640
+ return error;
641
+ // The caller's own cancellation is rethrown as raised, unless the timeout fired first.
642
+ if (args.signal?.aborted && !chain.includes(timer.reason))
643
+ return error;
644
+ return new ProviderAPIError({
645
+ providerName: args.providerName,
646
+ message: `${args.errorLabel} request timed out after ${timeoutMs} ms`,
647
+ retryable: false,
648
+ attempts,
649
+ timeoutMs,
650
+ cause: error,
651
+ });
652
+ },
653
+ };
654
+ }
655
+ /** Reads a JSON answer; a body the request's timeout cut off fails as that timeout. */
656
+ async function readResponseJson(response, deadline) {
657
+ try {
658
+ return await response.json();
659
+ }
660
+ catch (error) {
661
+ throw deadline.classify(error);
662
+ }
663
+ }
324
664
  async function executeRequestWithRetry(args) {
665
+ const { deadline } = args;
325
666
  for (let attempt = 1; attempt <= maxProviderAttempts; attempt++) {
326
- const response = await args.send();
327
- if (response.ok) {
328
- return response;
667
+ let response;
668
+ let body;
669
+ try {
670
+ response = await args.send(deadline.attempt());
671
+ if (response.ok) {
672
+ return response;
673
+ }
674
+ body = await response.text();
675
+ }
676
+ catch (error) {
677
+ throw deadline.classify(error);
329
678
  }
330
- const body = await response.text();
331
679
  const retryable = isRetryableStatusCode(response.status);
332
680
  if (!retryable || attempt === maxProviderAttempts) {
333
681
  throw new ProviderAPIError({
334
- providerName: args.providerName,
682
+ providerName: deadline.providerName,
335
683
  statusCode: response.status,
336
684
  retryable,
337
685
  attempts: attempt,
338
- message: buildProviderErrorMessage(args.errorLabel, response.status, body, attempt),
686
+ message: buildProviderErrorMessage(deadline.errorLabel, response.status, body, attempt),
339
687
  });
340
688
  }
341
689
  await waitForRetry(computeRetryDelayMs(attempt, response.headers.get('retry-after')));
342
690
  }
343
691
  throw new ProviderAPIError({
344
- providerName: args.providerName,
692
+ providerName: deadline.providerName,
345
693
  retryable: false,
346
694
  attempts: maxProviderAttempts,
347
- message: `${args.errorLabel} request failed`,
695
+ message: `${deadline.errorLabel} request failed`,
348
696
  });
349
697
  }
350
698
  // ── Provider tool-calling helpers ──
@@ -480,23 +828,39 @@ function hasOpenAIResponsesContinuation(request) {
480
828
  function isGpt56Family(model) {
481
829
  return /^gpt-5\.6(?:$|[-.])/.test(model.trim().toLowerCase());
482
830
  }
831
+ function isGpt6Family(model) {
832
+ return /^gpt-6(?:$|[-.])/.test(model.trim().toLowerCase());
833
+ }
834
+ /** GPT-6 Astra and GPT-6.1 Sol: lowest reasoning effort is `low`, not `none`. */
835
+ function isGpt6WithoutNoneEffort(model) {
836
+ return /^gpt-6(?:-astra|\.1-sol)(?:$|-)/.test(model.trim().toLowerCase());
837
+ }
483
838
  /**
484
839
  * Chat Completions cannot combine function tools with active reasoning on
485
- * GPT-5.6. Keep every compatible request on the existing transport, but use
486
- * Responses for explicit reasoning effort, GPT-5.6's default reasoning, and
487
- * continuation rounds that already carry Responses output state.
840
+ * GPT-5.6 or GPT-6 ("Function tools with reasoning_effort are not supported for
841
+ * gpt-6-luna in /v1/chat/completions. To use function tools, use /v1/responses or
842
+ * set reasoning_effort to 'none'."). Keep every compatible request on the existing
843
+ * transport. Caller tools use Responses for explicit reasoning effort, these families'
844
+ * default reasoning, and continuation rounds that already carry Responses output
845
+ * state. The structured-output tool of `structuredOutputTransport: 'tool'` is one
846
+ * forced call with no continuation, so it moves only where Chat Completions refuses
847
+ * it: these families with reasoning not disabled. On other models (o-series, earlier
848
+ * gpt-5 lines, an OpenAI-compatible `baseUrl` that may have no `/responses`) it stays
849
+ * on Chat Completions, with its reasoning effort, as before.
488
850
  */
489
851
  function shouldUseOpenAIResponses(request, model) {
490
852
  if (hasOpenAIResponsesContinuation(request))
491
853
  return true;
492
- if (!request.tools || request.tools.length === 0)
493
- return false;
494
854
  const reasoning = normalizeProviderRequestReasoning(request);
495
855
  if (reasoning?.mode === 'disabled')
496
856
  return false;
497
- if (reasoning?.mode === 'effort' || request.reasoningEffort !== undefined)
498
- return true;
499
- return reasoning === undefined && isGpt56Family(model);
857
+ const chatRefusesToolsUnderReasoning = isGpt56Family(model) || isGpt6Family(model);
858
+ if (request.tools && request.tools.length > 0) {
859
+ if (reasoning?.mode === 'effort' || request.reasoningEffort !== undefined)
860
+ return true;
861
+ return reasoning === undefined && chatRefusesToolsUnderReasoning;
862
+ }
863
+ return shouldUseOpenAIStructuredTool({ request }) && chatRefusesToolsUnderReasoning;
500
864
  }
501
865
  function toOpenAIResponsesInput(request) {
502
866
  if (!request.messages || request.messages.length === 0) {
@@ -544,7 +908,7 @@ function buildOpenAIResponsesToolChoice(toolChoice) {
544
908
  return toolChoice;
545
909
  return { type: 'function', name: toolChoice.function.name };
546
910
  }
547
- function applyOpenAIResponsesReasoning(body, request) {
911
+ function applyOpenAIResponsesReasoning(body, request, model) {
548
912
  const reasoning = normalizeProviderRequestReasoning(request);
549
913
  if (!reasoning) {
550
914
  if (request.reasoningEffort)
@@ -553,16 +917,24 @@ function applyOpenAIResponsesReasoning(body, request) {
553
917
  }
554
918
  if (reasoning.mode === 'budget')
555
919
  unsupportedReasoning('openai', reasoning);
920
+ assertOpenAIReasoningSupported(model, reasoning);
556
921
  body.reasoning = { effort: reasoning.mode === 'disabled' ? 'none' : reasoning.effort };
557
922
  }
558
923
  function openAIResponsesUsageToTokenUsage(usage) {
559
924
  const inputTokens = usage?.input_tokens ?? 0;
560
925
  const outputTokens = usage?.output_tokens ?? 0;
926
+ const reasoning = usage?.output_tokens_details?.reasoning_tokens;
927
+ const cacheWrites = usage?.input_tokens_details?.cache_write_tokens;
561
928
  return {
562
929
  inputTokens,
563
930
  outputTokens,
564
931
  totalTokens: usage?.total_tokens ?? inputTokens + outputTokens,
565
932
  cachedInputTokens: usage?.input_tokens_details?.cached_tokens ?? 0,
933
+ // As on Chat Completions: the share of `input_tokens` written to the cache.
934
+ ...(typeof cacheWrites === 'number' ? { cacheWriteTokens: cacheWrites } : {}),
935
+ // As on Chat Completions (`openAIUsageToTokenUsage`): the reasoning share of `output_tokens`,
936
+ // which already counts it and is what is priced.
937
+ ...(typeof reasoning === 'number' ? { reasoningTokens: reasoning } : {}),
566
938
  };
567
939
  }
568
940
  function extractOpenAIResponsesContent(output) {
@@ -597,6 +969,11 @@ async function completeOpenAIResponses(args) {
597
969
  assertNoStructuredOutputToolConflict(request);
598
970
  validateCallerTools(request.tools);
599
971
  }
972
+ // `structuredOutputTransport: 'tool'` under active reasoning: the same forced strict function
973
+ // as on Chat Completions, in the Responses tool shape (its fields sit on the tool itself).
974
+ const structuredToolSchema = shouldUseOpenAIStructuredTool({ request })
975
+ ? request.responseSchema
976
+ : undefined;
600
977
  const body = {
601
978
  model,
602
979
  input: toOpenAIResponsesInput(request),
@@ -605,16 +982,38 @@ async function completeOpenAIResponses(args) {
605
982
  };
606
983
  if (request.system)
607
984
  body.instructions = request.system;
608
- applyOpenAITemperature(body, model, request.temperature);
609
- applyOpenAIResponsesReasoning(body, request);
985
+ applyOpenAITemperature(body, model, request);
986
+ applyOpenAIResponsesReasoning(body, request, model);
610
987
  if (request.maxTokens)
611
988
  body.max_output_tokens = request.maxTokens;
989
+ if (structuredToolSchema) {
990
+ body.tools = [{ type: 'function', ...openAIStructuredOutputFunction(structuredToolSchema) }];
991
+ body.tool_choice = { type: 'function', name: OPENAI_STRUCTURED_OUTPUT_TOOL_NAME };
992
+ }
993
+ else if (request.responseFormat === 'json') {
994
+ body.text = {
995
+ format: request.responseSchema
996
+ ? {
997
+ type: 'json_schema',
998
+ name: 'response',
999
+ strict: true,
1000
+ schema: toOpenAIStrictOutputSchema(request.responseSchema).schema,
1001
+ }
1002
+ : { type: 'json_object' },
1003
+ };
1004
+ }
612
1005
  if (request.tools && request.tools.length > 0) {
1006
+ // `strict: false` keeps caller tools as they are on Chat Completions. With `strict` omitted,
1007
+ // Responses "will attempt to normalize your schema into strict mode when possible", and does
1008
+ // so by listing every property as required: an optional argument the user never gave
1009
+ // (`unit` of `get_weather`) came back filled in (`"unit": "celsius"`), and the app's input
1010
+ // check cannot tell it from a real one.
613
1011
  body.tools = request.tools.map((tool) => ({
614
1012
  type: 'function',
615
1013
  name: tool.name,
616
1014
  description: tool.description,
617
1015
  parameters: tool.parameters,
1016
+ strict: false,
618
1017
  }));
619
1018
  const toolChoice = buildOpenAIResponsesToolChoice(request.toolChoice);
620
1019
  if (toolChoice !== undefined)
@@ -622,24 +1021,36 @@ async function completeOpenAIResponses(args) {
622
1021
  if (request.toolExecution?.parallelToolCalls === false)
623
1022
  body.parallel_tool_calls = false;
624
1023
  }
625
- const response = await executeRequestWithRetry({
1024
+ const deadline = createRequestDeadline({
626
1025
  providerName: 'openai',
627
1026
  errorLabel: 'OpenAI Responses API',
628
- send: () => fetch(`${args.baseUrl}/responses`, {
1027
+ signal: request.signal,
1028
+ timeout: request.timeout,
1029
+ defaultTimeout: args.defaultTimeout,
1030
+ });
1031
+ const response = await executeRequestWithRetry({
1032
+ deadline,
1033
+ send: (transport) => fetch(`${args.baseUrl}/responses`, {
629
1034
  method: 'POST',
630
1035
  headers: {
1036
+ ...request.transportHeaders,
631
1037
  'Content-Type': 'application/json',
632
1038
  Authorization: `Bearer ${args.apiKey}`,
633
1039
  },
634
1040
  body: JSON.stringify(body),
635
- signal: combineRequestSignals(request.signal, request.timeout ?? args.defaultTimeout),
1041
+ ...transport,
636
1042
  }),
637
1043
  });
638
- const data = (await response.json());
1044
+ const data = (await readResponseJson(response, deadline));
639
1045
  const usage = openAIResponsesUsageToTokenUsage(data.usage);
640
1046
  const output = data.output ?? [];
641
1047
  const extracted = extractOpenAIResponsesContent(output);
642
- const content = extracted.content || data.output_text || '';
1048
+ const structuredArguments = structuredToolSchema
1049
+ ? output.find((item) => item.type === 'function_call' && item.name === OPENAI_STRUCTURED_OUTPUT_TOOL_NAME)?.arguments
1050
+ : undefined;
1051
+ const content = unwrapOpenAIStructuredContent(request, typeof structuredArguments === 'string' && structuredArguments.length > 0
1052
+ ? structuredArguments
1053
+ : extracted.content || data.output_text || '');
643
1054
  const responseModel = data.model ?? model;
644
1055
  if (data.status === 'failed' || data.error) {
645
1056
  throw new ProviderAPIError({
@@ -657,7 +1068,8 @@ async function completeOpenAIResponses(args) {
657
1068
  usage,
658
1069
  });
659
1070
  }
660
- const toolCalls = parseOpenAIResponsesToolCalls(output);
1071
+ // The structured-output call is the answer, not a tool call for the caller to run.
1072
+ const toolCalls = structuredToolSchema ? [] : parseOpenAIResponsesToolCalls(output);
661
1073
  if (toolCalls.length > 0) {
662
1074
  return {
663
1075
  content,
@@ -779,12 +1191,15 @@ export function createOpenAIAdapter(config) {
779
1191
  model,
780
1192
  messages,
781
1193
  };
782
- applyOpenAITemperature(body, model, request.temperature);
783
- applyOpenAIReasoning(body, request);
1194
+ applyOpenAITemperature(body, model, request);
1195
+ applyOpenAIReasoning(body, request, model);
784
1196
  applyOpenAITokenLimit(body, model, request.maxTokens);
785
1197
  if (request.tools && request.tools.length > 0) {
786
1198
  assertNoStructuredOutputToolConflict(request);
787
1199
  validateCallerTools(request.tools);
1200
+ const responseFormat = buildOpenAIResponseFormat(request);
1201
+ if (responseFormat)
1202
+ body.response_format = responseFormat;
788
1203
  body.tools = request.tools.map((t) => ({
789
1204
  type: 'function',
790
1205
  function: { name: t.name, description: t.description, parameters: t.parameters },
@@ -817,25 +1232,32 @@ export function createOpenAIAdapter(config) {
817
1232
  if (typeof request.seed === 'number') {
818
1233
  body.seed = request.seed;
819
1234
  }
820
- const resp = await executeRequestWithRetry({
1235
+ const deadline = createRequestDeadline({
821
1236
  providerName: 'openai',
822
1237
  errorLabel: 'OpenAI API',
823
- send: () => fetch(`${baseUrl}/chat/completions`, {
1238
+ signal: request.signal,
1239
+ timeout: request.timeout,
1240
+ defaultTimeout,
1241
+ });
1242
+ const resp = await executeRequestWithRetry({
1243
+ deadline,
1244
+ send: (transport) => fetch(`${baseUrl}/chat/completions`, {
824
1245
  method: 'POST',
825
1246
  headers: {
1247
+ ...request.transportHeaders,
826
1248
  'Content-Type': 'application/json',
827
1249
  Authorization: `Bearer ${config.apiKey}`,
828
1250
  },
829
1251
  body: JSON.stringify(body),
830
- signal: combineRequestSignals(request.signal, request.timeout ?? defaultTimeout),
1252
+ ...transport,
831
1253
  }),
832
1254
  });
833
- const data = (await resp.json());
1255
+ const data = (await readResponseJson(resp, deadline));
834
1256
  const choice = data.choices[0];
835
1257
  if (!choice)
836
1258
  throw new Error('OpenAI returned no choices');
837
1259
  const usage = openAIUsageToTokenUsage(data.usage);
838
- const content = extractOpenAIMessageContent(choice.message);
1260
+ const content = unwrapOpenAIStructuredContent(request, extractOpenAIMessageContent(choice.message));
839
1261
  if (choice.message.refusal) {
840
1262
  throw new AIRefusalError({
841
1263
  provider: 'openai',
@@ -899,8 +1321,8 @@ export function createOpenAIAdapter(config) {
899
1321
  stream: true,
900
1322
  stream_options: { include_usage: true },
901
1323
  };
902
- applyOpenAITemperature(body, model, request.temperature);
903
- applyOpenAIReasoning(body, request);
1324
+ applyOpenAITemperature(body, model, request);
1325
+ applyOpenAIReasoning(body, request, model);
904
1326
  applyOpenAITokenLimit(body, model, request.maxTokens);
905
1327
  const responseFormat = buildOpenAIResponseFormat(request);
906
1328
  if (responseFormat) {
@@ -909,19 +1331,26 @@ export function createOpenAIAdapter(config) {
909
1331
  if (typeof request.seed === 'number') {
910
1332
  body.seed = request.seed;
911
1333
  }
1334
+ const deadline = createRequestDeadline({
1335
+ providerName: 'openai',
1336
+ errorLabel: 'OpenAI API',
1337
+ signal: request.signal,
1338
+ timeout: request.timeout,
1339
+ defaultTimeout,
1340
+ });
912
1341
  let resp;
913
1342
  try {
914
1343
  resp = await executeRequestWithRetry({
915
- providerName: 'openai',
916
- errorLabel: 'OpenAI API',
917
- send: () => fetch(`${baseUrl}/chat/completions`, {
1344
+ deadline,
1345
+ send: (transport) => fetch(`${baseUrl}/chat/completions`, {
918
1346
  method: 'POST',
919
1347
  headers: {
1348
+ ...request.transportHeaders,
920
1349
  'Content-Type': 'application/json',
921
1350
  Authorization: `Bearer ${config.apiKey}`,
922
1351
  },
923
1352
  body: JSON.stringify(body),
924
- signal: combineRequestSignals(request.signal, request.timeout ?? defaultTimeout),
1353
+ ...transport,
925
1354
  }),
926
1355
  });
927
1356
  }
@@ -929,30 +1358,38 @@ export function createOpenAIAdapter(config) {
929
1358
  yield {
930
1359
  type: 'error',
931
1360
  error: error instanceof Error ? error.message : String(error),
1361
+ cause: error,
932
1362
  };
933
1363
  return;
934
1364
  }
935
- yield* parseSSEStream(resp, parseOpenAISSEChunk);
1365
+ const events = parseSSEStream(resp, parseOpenAISSEChunk, deadline);
1366
+ yield* hasOpenAIStructuredEnvelope(request) ? unwrapOpenAIStructuredStream(events) : events;
936
1367
  },
937
1368
  async embed(request) {
938
1369
  const body = {
939
1370
  model: request.model ?? defaultEmbeddingModel,
940
1371
  input: request.texts,
941
1372
  };
942
- const resp = await executeRequestWithRetry({
1373
+ const deadline = createRequestDeadline({
943
1374
  providerName: 'openai',
944
1375
  errorLabel: 'OpenAI Embedding API',
945
- send: () => fetch(`${baseUrl}/embeddings`, {
1376
+ signal: request.signal,
1377
+ timeout: request.timeout,
1378
+ defaultTimeout,
1379
+ });
1380
+ const resp = await executeRequestWithRetry({
1381
+ deadline,
1382
+ send: (transport) => fetch(`${baseUrl}/embeddings`, {
946
1383
  method: 'POST',
947
1384
  headers: {
948
1385
  'Content-Type': 'application/json',
949
1386
  Authorization: `Bearer ${config.apiKey}`,
950
1387
  },
951
1388
  body: JSON.stringify(body),
952
- signal: AbortSignal.timeout(defaultTimeout),
1389
+ ...transport,
953
1390
  }),
954
1391
  });
955
- const data = (await resp.json());
1392
+ const data = (await readResponseJson(resp, deadline));
956
1393
  const totalTokens = data.usage.total_tokens;
957
1394
  return {
958
1395
  embeddings: data.data.map((d) => d.embedding),
@@ -1046,21 +1483,31 @@ export function createAnthropicAdapter(config) {
1046
1483
  if (callerToolChoice !== undefined)
1047
1484
  body.tool_choice = callerToolChoice;
1048
1485
  }
1049
- const resp = await executeRequestWithRetry({
1486
+ const promptCache = resolvePromptCache(request.cache);
1487
+ if (promptCache)
1488
+ applyAnthropicPromptCache(body, promptCache);
1489
+ const deadline = createRequestDeadline({
1050
1490
  providerName: 'anthropic',
1051
1491
  errorLabel: 'Anthropic API',
1052
- send: () => fetch(`${baseUrl}/messages`, {
1492
+ signal: request.signal,
1493
+ timeout: request.timeout,
1494
+ defaultTimeout,
1495
+ });
1496
+ const resp = await executeRequestWithRetry({
1497
+ deadline,
1498
+ send: (transport) => fetch(`${baseUrl}/messages`, {
1053
1499
  method: 'POST',
1054
1500
  headers: {
1501
+ ...request.transportHeaders,
1055
1502
  'Content-Type': 'application/json',
1056
1503
  'x-api-key': config.apiKey,
1057
1504
  'anthropic-version': '2023-06-01',
1058
1505
  },
1059
1506
  body: JSON.stringify(body),
1060
- signal: combineRequestSignals(request.signal, request.timeout ?? defaultTimeout),
1507
+ ...transport,
1061
1508
  }),
1062
1509
  });
1063
- const data = (await resp.json());
1510
+ const data = (await readResponseJson(resp, deadline));
1064
1511
  const usage = anthropicUsageToTokenUsage(data.usage);
1065
1512
  const text = data.content
1066
1513
  .filter((c) => c.type === 'text')
@@ -1108,11 +1555,7 @@ export function createAnthropicAdapter(config) {
1108
1555
  };
1109
1556
  },
1110
1557
  async *stream(request) {
1111
- // Multi-turn path: when `messages` is supplied, send them verbatim.
1112
- // Single-turn path: synthesize a single user message from `prompt`.
1113
- const messages = request.messages && request.messages.length > 0
1114
- ? request.messages.map((m) => ({ role: m.role, content: m.content }))
1115
- : [{ role: 'user', content: request.prompt }];
1558
+ const messages = toAnthropicMessages(request);
1116
1559
  const body = {
1117
1560
  model: request.model ?? defaultModel,
1118
1561
  messages,
@@ -1131,20 +1574,30 @@ export function createAnthropicAdapter(config) {
1131
1574
  };
1132
1575
  }
1133
1576
  applyAnthropicReasoning(body, request);
1577
+ const promptCache = resolvePromptCache(request.cache);
1578
+ if (promptCache)
1579
+ applyAnthropicPromptCache(body, promptCache);
1580
+ const deadline = createRequestDeadline({
1581
+ providerName: 'anthropic',
1582
+ errorLabel: 'Anthropic API',
1583
+ signal: request.signal,
1584
+ timeout: request.timeout,
1585
+ defaultTimeout,
1586
+ });
1134
1587
  let resp;
1135
1588
  try {
1136
1589
  resp = await executeRequestWithRetry({
1137
- providerName: 'anthropic',
1138
- errorLabel: 'Anthropic API',
1139
- send: () => fetch(`${baseUrl}/messages`, {
1590
+ deadline,
1591
+ send: (transport) => fetch(`${baseUrl}/messages`, {
1140
1592
  method: 'POST',
1141
1593
  headers: {
1594
+ ...request.transportHeaders,
1142
1595
  'Content-Type': 'application/json',
1143
1596
  'x-api-key': config.apiKey,
1144
1597
  'anthropic-version': '2023-06-01',
1145
1598
  },
1146
1599
  body: JSON.stringify(body),
1147
- signal: combineRequestSignals(request.signal, request.timeout ?? defaultTimeout),
1600
+ ...transport,
1148
1601
  }),
1149
1602
  });
1150
1603
  }
@@ -1152,11 +1605,12 @@ export function createAnthropicAdapter(config) {
1152
1605
  yield {
1153
1606
  type: 'error',
1154
1607
  error: error instanceof Error ? error.message : String(error),
1608
+ cause: error,
1155
1609
  };
1156
1610
  return;
1157
1611
  }
1158
1612
  let initialUsage;
1159
- for await (const event of parseSSEStream(resp, parseAnthropicSSEChunk)) {
1613
+ for await (const event of parseSSEStream(resp, parseAnthropicSSEChunk, deadline)) {
1160
1614
  if (event.type === 'usage' && event.usage)
1161
1615
  initialUsage = event.usage;
1162
1616
  if (event.type === 'done' && event.usage && initialUsage) {
@@ -1230,7 +1684,7 @@ export function createAnthropicAdapter(config) {
1230
1684
  },
1231
1685
  };
1232
1686
  }
1233
- async function* parseSSEStream(resp, parseChunk) {
1687
+ async function* parseSSEStream(resp, parseChunk, deadline) {
1234
1688
  if (!resp.body) {
1235
1689
  yield { type: 'error', error: 'No response body for streaming' };
1236
1690
  return;
@@ -1241,7 +1695,10 @@ async function* parseSSEStream(resp, parseChunk) {
1241
1695
  let currentEvent = '';
1242
1696
  try {
1243
1697
  while (true) {
1244
- const { done, value } = await reader.read();
1698
+ // The request's timeout covers the whole stream: a cut-off read fails as that timeout.
1699
+ const { done, value } = await reader.read().catch((error) => {
1700
+ throw deadline.classify(error);
1701
+ });
1245
1702
  if (done)
1246
1703
  break;
1247
1704
  buffer += decoder.decode(value, { stream: true });
@@ -1282,15 +1739,7 @@ function parseOpenAISSEChunk(_eventType, data) {
1282
1739
  // Usage-only chunk (sent when stream_options.include_usage is true)
1283
1740
  if (parsed.usage &&
1284
1741
  (!parsed.choices || parsed.choices.length === 0 || !parsed.choices[0]?.delta?.content)) {
1285
- return {
1286
- type: 'usage',
1287
- usage: {
1288
- inputTokens: parsed.usage.prompt_tokens,
1289
- outputTokens: parsed.usage.completion_tokens,
1290
- totalTokens: parsed.usage.total_tokens,
1291
- cachedInputTokens: parsed.usage.prompt_tokens_details?.cached_tokens ?? 0,
1292
- },
1293
- };
1742
+ return { type: 'usage', usage: openAIUsageToTokenUsage(parsed.usage) };
1294
1743
  }
1295
1744
  const choice = parsed.choices?.[0];
1296
1745
  if (!choice)