@fractaal/pi-ai 1.4.1 → 1.6.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 (261) hide show
  1. package/README.md +50 -34
  2. package/dist/api/anthropic-messages.d.ts.map +1 -1
  3. package/dist/api/anthropic-messages.js +224 -27
  4. package/dist/api/anthropic-messages.js.map +1 -1
  5. package/dist/api/azure-openai-responses.js +1 -1
  6. package/dist/api/azure-openai-responses.js.map +1 -1
  7. package/dist/api/bedrock-converse-stream.d.ts.map +1 -1
  8. package/dist/api/bedrock-converse-stream.js +54 -25
  9. package/dist/api/bedrock-converse-stream.js.map +1 -1
  10. package/dist/api/bedrock-converse-stream.lazy.d.ts +7 -0
  11. package/dist/api/bedrock-converse-stream.lazy.d.ts.map +1 -1
  12. package/dist/api/bedrock-converse-stream.lazy.js +18 -5
  13. package/dist/api/bedrock-converse-stream.lazy.js.map +1 -1
  14. package/dist/api/constrained-sampling.d.ts +4 -0
  15. package/dist/api/constrained-sampling.d.ts.map +1 -1
  16. package/dist/api/constrained-sampling.js +18 -0
  17. package/dist/api/constrained-sampling.js.map +1 -1
  18. package/dist/api/lazy.d.ts +1 -1
  19. package/dist/api/lazy.d.ts.map +1 -1
  20. package/dist/api/lazy.js +9 -8
  21. package/dist/api/lazy.js.map +1 -1
  22. package/dist/api/openai-codex-responses.d.ts.map +1 -1
  23. package/dist/api/openai-codex-responses.js +7 -3
  24. package/dist/api/openai-codex-responses.js.map +1 -1
  25. package/dist/api/openai-completions.d.ts +2 -1
  26. package/dist/api/openai-completions.d.ts.map +1 -1
  27. package/dist/api/openai-completions.js +10 -3
  28. package/dist/api/openai-completions.js.map +1 -1
  29. package/dist/api/openai-responses.d.ts.map +1 -1
  30. package/dist/api/openai-responses.js +4 -2
  31. package/dist/api/openai-responses.js.map +1 -1
  32. package/dist/api/pi-messages.d.ts.map +1 -1
  33. package/dist/api/pi-messages.js +9 -0
  34. package/dist/api/pi-messages.js.map +1 -1
  35. package/dist/api/simple-options.d.ts +1 -0
  36. package/dist/api/simple-options.d.ts.map +1 -1
  37. package/dist/api/simple-options.js +16 -2
  38. package/dist/api/simple-options.js.map +1 -1
  39. package/dist/auth/oauth/github-copilot.d.ts.map +1 -1
  40. package/dist/auth/oauth/github-copilot.js +4 -11
  41. package/dist/auth/oauth/github-copilot.js.map +1 -1
  42. package/dist/auth/oauth/kimi-coding.d.ts.map +1 -1
  43. package/dist/auth/oauth/kimi-coding.js +4 -22
  44. package/dist/auth/oauth/kimi-coding.js.map +1 -1
  45. package/dist/auth/oauth/load.d.ts.map +1 -1
  46. package/dist/auth/oauth/load.js +5 -2
  47. package/dist/auth/oauth/load.js.map +1 -1
  48. package/dist/auth/oauth/meta.d.ts.map +1 -1
  49. package/dist/auth/oauth/meta.js +1 -14
  50. package/dist/auth/oauth/meta.js.map +1 -1
  51. package/dist/auth/oauth/radius.d.ts.map +1 -1
  52. package/dist/auth/oauth/radius.js +9 -3
  53. package/dist/auth/oauth/radius.js.map +1 -1
  54. package/dist/auth/oauth/trusted-url.d.ts +7 -0
  55. package/dist/auth/oauth/trusted-url.d.ts.map +1 -0
  56. package/dist/auth/oauth/trusted-url.js +12 -0
  57. package/dist/auth/oauth/trusted-url.js.map +1 -0
  58. package/dist/cli.js +6 -2
  59. package/dist/cli.js.map +1 -1
  60. package/dist/env-api-keys.d.ts.map +1 -1
  61. package/dist/env-api-keys.js +1 -0
  62. package/dist/env-api-keys.js.map +1 -1
  63. package/dist/index.d.ts +1 -3
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +1 -2
  66. package/dist/index.js.map +1 -1
  67. package/dist/lazy-bedrock-provider.d.ts +10 -0
  68. package/dist/lazy-bedrock-provider.d.ts.map +1 -0
  69. package/dist/lazy-bedrock-provider.js +13 -0
  70. package/dist/lazy-bedrock-provider.js.map +1 -0
  71. package/dist/lazy-oauth.d.ts +10 -0
  72. package/dist/lazy-oauth.d.ts.map +1 -0
  73. package/dist/lazy-oauth.js +23 -0
  74. package/dist/lazy-oauth.js.map +1 -0
  75. package/dist/models.d.ts.map +1 -1
  76. package/dist/models.generated.d.ts +4 -0
  77. package/dist/models.generated.d.ts.map +1 -1
  78. package/dist/models.generated.js +5 -1
  79. package/dist/models.generated.js.map +1 -1
  80. package/dist/models.js +8 -8
  81. package/dist/models.js.map +1 -1
  82. package/dist/providers/all.d.ts.map +1 -1
  83. package/dist/providers/all.js +2 -0
  84. package/dist/providers/all.js.map +1 -1
  85. package/dist/providers/amazon-bedrock.models.js +1 -1
  86. package/dist/providers/amazon-bedrock.models.js.map +1 -1
  87. package/dist/providers/ant-ling.models.js +1 -1
  88. package/dist/providers/ant-ling.models.js.map +1 -1
  89. package/dist/providers/anthropic.models.js +1 -1
  90. package/dist/providers/anthropic.models.js.map +1 -1
  91. package/dist/providers/azure.models.js +1 -1
  92. package/dist/providers/azure.models.js.map +1 -1
  93. package/dist/providers/baseten.models.js +1 -1
  94. package/dist/providers/baseten.models.js.map +1 -1
  95. package/dist/providers/cerebras.models.js +1 -1
  96. package/dist/providers/cerebras.models.js.map +1 -1
  97. package/dist/providers/cloudflare-ai-gateway.models.js +1 -1
  98. package/dist/providers/cloudflare-ai-gateway.models.js.map +1 -1
  99. package/dist/providers/cloudflare-workers-ai.models.js +1 -1
  100. package/dist/providers/cloudflare-workers-ai.models.js.map +1 -1
  101. package/dist/providers/compat-schema.d.ts +441 -456
  102. package/dist/providers/compat-schema.d.ts.map +1 -1
  103. package/dist/providers/compat-schema.js +163 -112
  104. package/dist/providers/compat-schema.js.map +1 -1
  105. package/dist/providers/data/.manifest.json +1 -1
  106. package/dist/providers/data/amazon-bedrock.json +1 -1
  107. package/dist/providers/data/anthropic.json +1 -1
  108. package/dist/providers/data/azure.json +1 -1
  109. package/dist/providers/data/baseten.json +1 -1
  110. package/dist/providers/data/cloudflare-ai-gateway.json +1 -1
  111. package/dist/providers/data/cloudflare-workers-ai.json +1 -1
  112. package/dist/providers/data/fireworks.json +1 -1
  113. package/dist/providers/data/github-copilot.json +1 -1
  114. package/dist/providers/data/google-vertex.json +1 -1
  115. package/dist/providers/data/google.json +1 -1
  116. package/dist/providers/data/huggingface.json +1 -1
  117. package/dist/providers/data/kimi-coding.json +1 -1
  118. package/dist/providers/data/mistral.json +1 -1
  119. package/dist/providers/data/nvidia.json +1 -1
  120. package/dist/providers/data/openai-codex.json +1 -1
  121. package/dist/providers/data/openai.json +1 -1
  122. package/dist/providers/data/opencode-go.json +1 -1
  123. package/dist/providers/data/opencode.json +1 -1
  124. package/dist/providers/data/openrouter.json +1 -1
  125. package/dist/providers/data/qwen-token-plan-cn.json +1 -1
  126. package/dist/providers/data/qwen-token-plan-individual-cn.json +1 -0
  127. package/dist/providers/data/qwen-token-plan-individual.json +1 -1
  128. package/dist/providers/data/qwen-token-plan.json +1 -1
  129. package/dist/providers/data/radius.json +1 -1
  130. package/dist/providers/data/together.json +1 -1
  131. package/dist/providers/data/vercel-ai-gateway.json +1 -1
  132. package/dist/providers/data/xai.json +1 -1
  133. package/dist/providers/data/xiaomi-token-plan-ams.json +1 -1
  134. package/dist/providers/data/xiaomi-token-plan-cn.json +1 -1
  135. package/dist/providers/data/xiaomi-token-plan-sgp.json +1 -1
  136. package/dist/providers/data/xiaomi.json +1 -1
  137. package/dist/providers/data/zai-coding-cn.json +1 -1
  138. package/dist/providers/data/zai.json +1 -1
  139. package/dist/providers/deepseek.models.js +1 -1
  140. package/dist/providers/deepseek.models.js.map +1 -1
  141. package/dist/providers/fireworks.models.js +1 -1
  142. package/dist/providers/fireworks.models.js.map +1 -1
  143. package/dist/providers/github-copilot.d.ts +3 -1
  144. package/dist/providers/github-copilot.d.ts.map +1 -1
  145. package/dist/providers/github-copilot.js +13 -2
  146. package/dist/providers/github-copilot.js.map +1 -1
  147. package/dist/providers/github-copilot.models.js +1 -1
  148. package/dist/providers/github-copilot.models.js.map +1 -1
  149. package/dist/providers/google-vertex.models.js +1 -1
  150. package/dist/providers/google-vertex.models.js.map +1 -1
  151. package/dist/providers/google.models.js +1 -1
  152. package/dist/providers/google.models.js.map +1 -1
  153. package/dist/providers/groq.models.js +1 -1
  154. package/dist/providers/groq.models.js.map +1 -1
  155. package/dist/providers/huggingface.models.js +1 -1
  156. package/dist/providers/huggingface.models.js.map +1 -1
  157. package/dist/providers/kimi-coding.models.js +1 -1
  158. package/dist/providers/kimi-coding.models.js.map +1 -1
  159. package/dist/providers/meta.models.js +1 -1
  160. package/dist/providers/meta.models.js.map +1 -1
  161. package/dist/providers/minimax-cn.models.js +1 -1
  162. package/dist/providers/minimax-cn.models.js.map +1 -1
  163. package/dist/providers/minimax.models.js +1 -1
  164. package/dist/providers/minimax.models.js.map +1 -1
  165. package/dist/providers/mistral.models.js +1 -1
  166. package/dist/providers/mistral.models.js.map +1 -1
  167. package/dist/providers/model-schema.d.ts +65 -65
  168. package/dist/providers/model-schema.d.ts.map +1 -1
  169. package/dist/providers/model-schema.js +41 -30
  170. package/dist/providers/model-schema.js.map +1 -1
  171. package/dist/providers/moonshotai-cn.models.js +1 -1
  172. package/dist/providers/moonshotai-cn.models.js.map +1 -1
  173. package/dist/providers/moonshotai.models.js +1 -1
  174. package/dist/providers/moonshotai.models.js.map +1 -1
  175. package/dist/providers/nvidia.models.js +1 -1
  176. package/dist/providers/nvidia.models.js.map +1 -1
  177. package/dist/providers/openai-codex-catalog.d.ts.map +1 -1
  178. package/dist/providers/openai-codex-catalog.js +44 -44
  179. package/dist/providers/openai-codex-catalog.js.map +1 -1
  180. package/dist/providers/openai-codex.models.js +1 -1
  181. package/dist/providers/openai-codex.models.js.map +1 -1
  182. package/dist/providers/openai.models.js +1 -1
  183. package/dist/providers/openai.models.js.map +1 -1
  184. package/dist/providers/opencode-go.models.js +1 -1
  185. package/dist/providers/opencode-go.models.js.map +1 -1
  186. package/dist/providers/opencode.js +1 -1
  187. package/dist/providers/opencode.js.map +1 -1
  188. package/dist/providers/opencode.models.js +1 -1
  189. package/dist/providers/opencode.models.js.map +1 -1
  190. package/dist/providers/openrouter.models.js +1 -1
  191. package/dist/providers/openrouter.models.js.map +1 -1
  192. package/dist/providers/qwen-token-plan-cn.models.js +1 -1
  193. package/dist/providers/qwen-token-plan-cn.models.js.map +1 -1
  194. package/dist/providers/qwen-token-plan-individual-cn.d.ts +3 -0
  195. package/dist/providers/qwen-token-plan-individual-cn.d.ts.map +1 -0
  196. package/dist/providers/qwen-token-plan-individual-cn.js +15 -0
  197. package/dist/providers/qwen-token-plan-individual-cn.js.map +1 -0
  198. package/dist/providers/qwen-token-plan-individual-cn.models.d.ts +6 -0
  199. package/dist/providers/qwen-token-plan-individual-cn.models.d.ts.map +1 -0
  200. package/dist/providers/qwen-token-plan-individual-cn.models.js +8 -0
  201. package/dist/providers/qwen-token-plan-individual-cn.models.js.map +1 -0
  202. package/dist/providers/qwen-token-plan-individual.models.js +1 -1
  203. package/dist/providers/qwen-token-plan-individual.models.js.map +1 -1
  204. package/dist/providers/qwen-token-plan.models.js +1 -1
  205. package/dist/providers/qwen-token-plan.models.js.map +1 -1
  206. package/dist/providers/radius.models.js +1 -1
  207. package/dist/providers/radius.models.js.map +1 -1
  208. package/dist/providers/schema-builders.d.ts +10 -0
  209. package/dist/providers/schema-builders.d.ts.map +1 -0
  210. package/dist/providers/schema-builders.js +18 -0
  211. package/dist/providers/schema-builders.js.map +1 -0
  212. package/dist/providers/together.models.js +1 -1
  213. package/dist/providers/together.models.js.map +1 -1
  214. package/dist/providers/typesafe.models.js +1 -1
  215. package/dist/providers/typesafe.models.js.map +1 -1
  216. package/dist/providers/vercel-ai-gateway.models.js +1 -1
  217. package/dist/providers/vercel-ai-gateway.models.js.map +1 -1
  218. package/dist/providers/xai.models.js +1 -1
  219. package/dist/providers/xai.models.js.map +1 -1
  220. package/dist/providers/xiaomi-token-plan-ams.models.js +1 -1
  221. package/dist/providers/xiaomi-token-plan-ams.models.js.map +1 -1
  222. package/dist/providers/xiaomi-token-plan-cn.models.js +1 -1
  223. package/dist/providers/xiaomi-token-plan-cn.models.js.map +1 -1
  224. package/dist/providers/xiaomi-token-plan-sgp.models.js +1 -1
  225. package/dist/providers/xiaomi-token-plan-sgp.models.js.map +1 -1
  226. package/dist/providers/xiaomi.models.js +1 -1
  227. package/dist/providers/xiaomi.models.js.map +1 -1
  228. package/dist/providers/zai-coding-cn.models.js +1 -1
  229. package/dist/providers/zai-coding-cn.models.js.map +1 -1
  230. package/dist/providers/zai.models.js +1 -1
  231. package/dist/providers/zai.models.js.map +1 -1
  232. package/dist/types.d.ts +13 -3
  233. package/dist/types.d.ts.map +1 -1
  234. package/dist/types.js.map +1 -1
  235. package/dist/utils/error-body.d.ts +8 -0
  236. package/dist/utils/error-body.d.ts.map +1 -1
  237. package/dist/utils/error-body.js +28 -1
  238. package/dist/utils/error-body.js.map +1 -1
  239. package/dist/utils/overflow.d.ts.map +1 -1
  240. package/dist/utils/overflow.js +2 -0
  241. package/dist/utils/overflow.js.map +1 -1
  242. package/dist/utils/retry.d.ts.map +1 -1
  243. package/dist/utils/retry.js +3 -0
  244. package/dist/utils/retry.js.map +1 -1
  245. package/dist/utils/{typebox-helpers.d.ts → string-enum.d.ts} +5 -5
  246. package/dist/utils/string-enum.d.ts.map +1 -0
  247. package/dist/utils/{typebox-helpers.js → string-enum.js} +5 -6
  248. package/dist/utils/string-enum.js.map +1 -0
  249. package/dist/utils/transcript.d.ts +1 -1
  250. package/dist/utils/transcript.js +1 -1
  251. package/dist/utils/transcript.js.map +1 -1
  252. package/dist/utils/validation.d.ts +5 -3
  253. package/dist/utils/validation.d.ts.map +1 -1
  254. package/dist/utils/validation.js +25 -446
  255. package/dist/utils/validation.js.map +1 -1
  256. package/dist/utils/version.d.ts +1 -1
  257. package/dist/utils/version.js +1 -1
  258. package/dist/utils/version.js.map +1 -1
  259. package/package.json +18 -9
  260. package/dist/utils/typebox-helpers.d.ts.map +0 -1
  261. package/dist/utils/typebox-helpers.js.map +0 -1
package/README.md CHANGED
@@ -64,7 +64,7 @@ Unified LLM API with provider collections, automatic auth resolution, token and
64
64
  - **OpenAI** (including the Decisions classifier API)
65
65
  - **Ant Ling**
66
66
  - **Azure OpenAI (Responses)**
67
- - **OpenAI Codex (legacy)** (ChatGPT Plus/Pro subscription, requires OAuth, see below)
67
+ - **OpenAI Codex** (ChatGPT Plus/Pro subscription, requires OAuth, see below)
68
68
  - **Radius** (API key or OAuth, with a dynamically refreshed gateway catalog)
69
69
  - **TypeSafe** (System One classifier API)
70
70
  - **DeepSeek**
@@ -103,15 +103,16 @@ Unified LLM API with provider collections, automatic auth resolution, token and
103
103
  npm install @earendil-works/pi-ai
104
104
  ```
105
105
 
106
- TypeBox exports are re-exported from `@earendil-works/pi-ai`: `Type`, `Static`, and `TSchema`.
106
+ Tool parameters are JSON Schema. The examples build them with `@earendil-works/pi-schema`, which also types the arguments (`npm install @earendil-works/pi-schema`); TypeBox schemas and plain JSON Schema objects work as well.
107
107
 
108
108
  ## Quick Start
109
109
 
110
110
  You build a `Models` collection of providers and stream through it. The quickest start registers every built-in provider; apps that care about bundle size register individual providers instead (see [Provider Factories](#provider-factories) and [Bundling and Tree Shaking](#bundling-and-tree-shaking)).
111
111
 
112
112
  ```typescript
113
- import { Type, type Context, type Tool } from '@earendil-works/pi-ai';
113
+ import { type Context, type Tool } from '@earendil-works/pi-ai';
114
114
  import { builtinModels } from '@earendil-works/pi-ai/providers/all';
115
+ import { obj, opt, str } from '@earendil-works/pi-schema';
115
116
 
116
117
  // A Models collection with every built-in provider registered
117
118
  const models = builtinModels();
@@ -119,12 +120,12 @@ const models = builtinModels();
119
120
  // Sync lookup against the collection
120
121
  const model = models.getModel('openai', 'gpt-4o-mini')!;
121
122
 
122
- // Define tools with TypeBox schemas for type safety and validation
123
+ // Define tools with JSON Schema; pi-schema's builders also type the arguments
123
124
  const tools: Tool[] = [{
124
125
  name: 'get_time',
125
126
  description: 'Get the current time',
126
- parameters: Type.Object({
127
- timezone: Type.Optional(Type.String({ description: 'Optional timezone (e.g., America/New_York)' }))
127
+ parameters: obj({
128
+ timezone: opt(str({ description: 'Optional timezone (e.g., America/New_York)' }))
128
129
  })
129
130
  }];
130
131
 
@@ -523,6 +524,7 @@ Built-in providers resolve these env vars (Node.js; in browsers pass `apiKey` ex
523
524
  | Qwen Token Plan (existing catalog) | `QWEN_TOKEN_PLAN_API_KEY` |
524
525
  | Qwen Token Plan (Individual) | `QWEN_TOKEN_PLAN_API_KEY` |
525
526
  | Qwen Token Plan (China) | `QWEN_TOKEN_PLAN_CN_API_KEY` |
527
+ | Qwen Token Plan (Individual, China) | `QWEN_TOKEN_PLAN_CN_API_KEY` |
526
528
  | Xiaomi MiMo (API billing) | `XIAOMI_API_KEY` |
527
529
  | Xiaomi MiMo Token Plan (China) | `XIAOMI_TOKEN_PLAN_CN_API_KEY` |
528
530
  | Xiaomi MiMo Token Plan (Amsterdam) | `XIAOMI_TOKEN_PLAN_AMS_API_KEY` |
@@ -532,40 +534,43 @@ Built-in providers resolve these env vars (Node.js; in browsers pass `apiKey` ex
532
534
  `qwen-token-plan-individual` and `qwen-token-plan` share the international endpoint and
533
535
  `QWEN_TOKEN_PLAN_API_KEY`. The Individual provider exposes only the models documented for Individual
534
536
  subscriptions, while the existing provider retains its broader catalog for backward compatibility.
537
+ `qwen-token-plan-individual-cn` applies the same Individual catalog to the China endpoint and
538
+ `QWEN_TOKEN_PLAN_CN_API_KEY`.
535
539
  Stored credentials remain provider-scoped, so save the key under the provider ID you register.
536
540
 
537
541
  Amazon Bedrock resolves ambient AWS credentials (`AWS_PROFILE`, access key pairs, `AWS_BEARER_TOKEN_BEDROCK`, ECS task roles, web identity tokens); its provider-owned login flow supports bearer tokens, AWS profiles, and the existing credential chain. Vertex AI resolves either an explicit key or gcloud Application Default Credentials plus project/location, with a provider-owned login flow for API keys, ADC, and service-account files.
538
542
 
539
543
  ## Tools
540
544
 
541
- Tools enable LLMs to interact with external systems. This library uses TypeBox schemas for type-safe tool definitions with automatic validation using TypeBox's built-in validator and value conversion utilities. TypeBox schemas can be serialized and deserialized as plain JSON, making them ideal for distributed systems.
545
+ Tools enable LLMs to interact with external systems. A tool's `parameters` is a JSON Schema object: built with `@earendil-works/pi-schema`'s builders (which also type the arguments through its `Static`), with TypeBox, or written by hand. `validateToolCall()` and `validateToolArguments()` validate with pi-schema, coercing loosely typed arguments first (numeric strings, `"true"`, a single value for an array, `null` for an omitted optional property). Schemas are plain JSON, so they serialize with the rest of a context.
542
546
 
543
547
  ### Defining Tools
544
548
 
545
549
  ```typescript
546
- import { Type, type Tool, StringEnum } from '@earendil-works/pi-ai';
550
+ import { type Tool, StringEnum } from '@earendil-works/pi-ai';
551
+ import { arr, obj, str } from '@earendil-works/pi-schema';
547
552
 
548
- // Define tool parameters with TypeBox
553
+ // Define tool parameters with pi-schema's builders
549
554
  const weatherTool: Tool = {
550
555
  name: 'get_weather',
551
556
  description: 'Get current weather for a location',
552
- parameters: Type.Object({
553
- location: Type.String({ description: 'City name or coordinates' }),
557
+ parameters: obj({
558
+ location: str({ description: 'City name or coordinates' }),
554
559
  units: StringEnum(['celsius', 'fahrenheit'], { default: 'celsius' })
555
560
  })
556
561
  };
557
562
 
558
- // Note: For Google API compatibility, use StringEnum helper instead of Type.Enum
559
- // Type.Enum generates anyOf/const patterns that Google doesn't support
563
+ // Note: StringEnum gives { type: "string", enum: [...] }, which every provider accepts,
564
+ // including Google's, which rejects anyOf/const patterns
560
565
 
561
566
  const bookMeetingTool: Tool = {
562
567
  name: 'book_meeting',
563
568
  description: 'Schedule a meeting',
564
- parameters: Type.Object({
565
- title: Type.String({ minLength: 1 }),
566
- startTime: Type.String({ format: 'date-time' }),
567
- endTime: Type.String({ format: 'date-time' }),
568
- attendees: Type.Array(Type.String({ format: 'email' }), { minItems: 1 })
569
+ parameters: obj({
570
+ title: str({ minLength: 1 }),
571
+ startTime: str({ format: 'date-time' }),
572
+ endTime: str({ format: 'date-time' }),
573
+ attendees: arr(str({ format: 'email' }), { minItems: 1 })
569
574
  })
570
575
  };
571
576
  ```
@@ -578,10 +583,7 @@ Tools can opt in to provider-side constrained sampling. For JSON-schema tools, `
578
583
  const strictTool: Tool = {
579
584
  name: 'edit_file',
580
585
  description: 'Edit a file',
581
- parameters: Type.Object({
582
- path: Type.String(),
583
- content: Type.String()
584
- }, { additionalProperties: false }),
586
+ parameters: obj({ path: str(), content: str() }), // obj() is closed: additionalProperties: false
585
587
  constrainedSampling: { type: 'json_schema', strict: 'prefer' }
586
588
  };
587
589
  ```
@@ -592,9 +594,7 @@ Strict JSON-schema constrained sampling is supported for OpenAI, Anthropic, supp
592
594
  const patchTool: Tool = {
593
595
  name: 'apply_patch',
594
596
  description: 'Apply a patch',
595
- parameters: Type.Object({
596
- input: Type.String()
597
- }, { additionalProperties: false }),
597
+ parameters: obj({ input: str() }),
598
598
  constrainedSampling: {
599
599
  type: 'grammar',
600
600
  variants: {
@@ -698,7 +698,7 @@ for await (const event of s) {
698
698
 
699
699
  ### Validating Tool Arguments
700
700
 
701
- When implementing your own tool execution loop, use `validateToolCall` to validate arguments before passing them to your tools:
701
+ When implementing your own tool execution loop, use `validateToolCall` to validate arguments before passing them to your tools. It coerces loosely typed arguments with pi-schema (`compile(schema, { coerce: true }).parse()`), returns the coerced arguments, and throws an error listing pi-schema's errors (`path: message`) followed by the arguments it received:
702
702
 
703
703
  ```typescript
704
704
  import { validateToolCall, type Tool } from '@earendil-works/pi-ai';
@@ -894,7 +894,7 @@ console.log(model.output); // ['image'] or ['image', 'text']
894
894
 
895
895
  ## Classification
896
896
 
897
- Classifier models consume structured JSON state, and optionally images, and answer one or more typed questions. They do not use chat or image-generation APIs. TypeSafe's Jev model, Cloudflare's Clef models, and OpenAI's GPT-6 Luna are available from these built-in providers:
897
+ Classifier models consume structured JSON state, and optionally images, and answer one or more typed questions. They do not use chat or image-generation APIs. TypeSafe's Jev model, Cloudflare's Clef models, OpenAI's GPT-6 Luna, and Microsoft-Decision-1 are available from these built-in providers:
898
898
 
899
899
  | Provider | Model IDs | Auth |
900
900
  | --- | --- | --- |
@@ -904,6 +904,7 @@ Classifier models consume structured JSON state, and optionally images, and answ
904
904
  | `vercel-ai-gateway` | `typesafe-ai/jev` | `AI_GATEWAY_API_KEY` |
905
905
  | `opencode` | `jev-1.13`, `jev-1.13-free` | `OPENCODE_API_KEY` |
906
906
  | `openai` | `gpt-6-luna` | `OPENAI_API_KEY` |
907
+ | `github-copilot` | `microsoft-decision-1` | `COPILOT_GITHUB_TOKEN` or GitHub Copilot OAuth |
907
908
 
908
909
  ```typescript
909
910
  import { builtinModels } from '@earendil-works/pi-ai/providers/all';
@@ -1440,6 +1441,8 @@ Built-in API implementations live under `./api/<api-id>`:
1440
1441
 
1441
1442
  Importing an implementation module loads its SDK. The `./api/<id>.lazy` wrappers (used by the provider factories) defer that load to the first request when the runtime or bundler supports dynamic import chunking. Legacy raw API subpaths from older releases (`./anthropic`, `./google`, `./mistral`, `./openai-completions`, ...) were removed; use `@earendil-works/pi-ai/api/<api-id>`.
1442
1443
 
1444
+ **Anthropic message threads:** with `sessionId` set and `cacheRetention` not `"none"`, Anthropic requests continue from the previous reply on the server (`message-threads-2026-08-12` beta) and send only the new messages. When anything earlier in the request changed, or the server can't continue, the full request is sent. Token counts and prompt caching are unchanged. On by default only for the `anthropic` provider on `api.anthropic.com`; set `compat.supportsMessageThreads` to change that, for example for gateways that forward the `thread` field. Set `disableContinuation: true` on requests that repeat an earlier one, such as cache warming. Call `cleanupSessionResources(sessionId)` when a session ends.
1445
+
1443
1446
  ### OpenAI Compatibility Settings
1444
1447
 
1445
1448
  The `openai-completions` API is implemented by many providers with minor differences. By default, the library auto-detects compatibility settings based on `baseUrl` for a small set of known OpenAI-compatible providers (Cerebras, xAI, Chutes, DeepSeek, NVIDIA NIM, Together AI, zAi, OpenCode, Cloudflare Workers AI, etc.). For custom proxies or unknown endpoints, you can override these settings via the `compat` field. For `openai-responses` models, the compat field supports Responses-specific flags.
@@ -1459,6 +1462,7 @@ interface OpenAICompletionsCompat {
1459
1462
  maxTokensField?: 'max_completion_tokens' | 'max_tokens'; // Which field name to use (default: max_completion_tokens)
1460
1463
  requiresToolResultName?: boolean; // Whether tool results require the `name` field (default: false)
1461
1464
  requiresAssistantAfterToolResult?: boolean; // Whether tool results must be followed by an assistant message (default: false)
1465
+ requiresNonNullAssistantContent?: boolean; // Whether replayed assistant messages with no text must send "" instead of null content (default: false)
1462
1466
  requiresThinkingAsText?: boolean; // Whether thinking blocks must be converted to text (default: false)
1463
1467
  requiresReasoningContentOnAssistantMessages?: boolean; // Whether all replayed assistant messages must include empty reasoning_content when reasoning is enabled (default: auto-detected for DeepSeek)
1464
1468
  thinkingFormat?: 'openai' | 'openrouter' | 'deepseek' | 'together' | 'baseten' | 'zai' | 'qwen' | 'chat-template' | 'qwen-chat-template' | 'string-thinking' | 'ant-ling'; // Format for reasoning param: 'openai' uses reasoning_effort, 'openrouter' uses reasoning: { effort }, 'deepseek' uses thinking: { type } plus reasoning_effort when supported, 'together' uses reasoning: { enabled } plus reasoning_effort when supported, 'baseten' uses configurable chat_template_args plus reasoning_effort when supported, 'zai' uses thinking: { type }, 'qwen' uses enable_thinking, 'chat-template' uses configurable chat_template_kwargs, 'qwen-chat-template' uses chat_template_kwargs.enable_thinking and preserve_thinking, 'string-thinking' uses top-level thinking, 'ant-ling' uses reasoning: { effort } only for mapped efforts (default: openai)
@@ -1726,8 +1730,8 @@ models.setProvider(openaiProvider());
1726
1730
 
1727
1731
  Rules:
1728
1732
 
1729
- - `@earendil-works/pi-ai/models` exports the model runtime (`createModels`, `createProvider`, model helpers, and their types) without TypeBox, built-in catalogs, or SDK implementations. Other types can still use `import type` from the root.
1730
- - `@earendil-works/pi-ai` is the core entrypoint and does not import built-in catalogs, real provider factories, or SDK implementations, but it eagerly imports TypeBox and schema validation. Unbundled Node scripts do not tree-shake its unused exports; prefer `./models`, `./providers/faux`, and specific `./utils/*` subpaths when those are all you need.
1733
+ - `@earendil-works/pi-ai/models` exports the model runtime (`createModels`, `createProvider`, model helpers, and their types) without schema validation, built-in catalogs, or SDK implementations. Other types can still use `import type` from the root.
1734
+ - `@earendil-works/pi-ai` is the core entrypoint and does not import built-in catalogs, real provider factories, or SDK implementations, but it eagerly imports schema validation (pi-schema). Unbundled Node scripts do not tree-shake its unused exports; prefer `./models`, `./providers/faux`, and specific `./utils/*` subpaths when those are all you need.
1731
1735
  - `@earendil-works/pi-ai/providers/<provider>` imports that provider's catalog and lazy API wrapper only.
1732
1736
  - `@earendil-works/pi-ai/providers/all` imports every built-in provider factory and all catalogs. Use it only when you want the full built-in set.
1733
1737
  - With code splitting, provider SDKs stay in lazy chunks and load on first request.
@@ -1767,6 +1771,18 @@ setBedrockProviderModule(bedrockProviderModule);
1767
1771
 
1768
1772
  That explicit override bundles the AWS SDK. Without it, Bedrock's opaque runtime import expects the package's Bedrock implementation file to be available at runtime.
1769
1773
 
1774
+ Node bundles that split at dynamic imports (esbuild `splitting: true`) can keep Bedrock and the OAuth login flows inside the bundle but lazy: register loaders with literal dynamic imports, which the bundler turns into chunks that load on first use:
1775
+
1776
+ ```typescript
1777
+ import { registerLazyBedrockProvider } from '@earendil-works/pi-ai/lazy-bedrock-provider';
1778
+ import { registerLazyOAuthFlows } from '@earendil-works/pi-ai/lazy-oauth';
1779
+
1780
+ registerLazyBedrockProvider();
1781
+ registerLazyOAuthFlows();
1782
+ ```
1783
+
1784
+ Both modules are Node-only; never import them into a browser bundle.
1785
+
1770
1786
  ### Provider-Scoped Environment Overrides
1771
1787
 
1772
1788
  Pass `env` in stream options to scope provider configuration to a request. Values in `env` are used before process environment variables for provider auth and configuration such as Cloudflare account IDs, Azure OpenAI settings, Vertex project/location, Bedrock settings, `PI_CACHE_RETENTION`, and `HTTP_PROXY`/`HTTPS_PROXY`.
@@ -1792,7 +1808,7 @@ Several providers support OAuth authentication instead of static API keys:
1792
1808
 
1793
1809
  - **Anthropic** (Claude Pro/Max subscription)
1794
1810
  - **OpenAI** (Sign in with ChatGPT: uses the ChatGPT subscription with the OpenAI API)
1795
- - **OpenAI Codex (legacy)** (ChatGPT Plus/Pro subscription, access to GPT-5.x Codex models)
1811
+ - **OpenAI Codex** (ChatGPT Plus/Pro subscription, access to GPT-5.x Codex models)
1796
1812
  - **GitHub Copilot** (Copilot subscription)
1797
1813
  - **OpenRouter** (OAuth PKCE that mints a user-controlled API key)
1798
1814
 
@@ -1872,7 +1888,7 @@ Built-in login and refresh flows are private provider implementations. Use provi
1872
1888
 
1873
1889
  Provider notes:
1874
1890
 
1875
- **OpenAI Codex (legacy)**: Superseded by Sign in with ChatGPT on the OpenAI provider. Requires a ChatGPT Plus or Pro subscription. Provides access to GPT-5.x Codex models with extended context windows and reasoning capabilities. The library automatically handles session-based prompt caching when `sessionId` is provided in stream options unless `cacheRetention` is `"none"`. You can set `transport` in stream options to `"sse"`, `"websocket"`, or `"auto"` for Codex Responses transport selection. When using WebSocket with a `sessionId` and cache retention enabled, connections are reused per session and expire after 5 minutes of inactivity. Call `cleanupSessionResources(sessionId)` when finished so the pooled connection does not keep the process alive.
1891
+ **OpenAI Codex**: Superseded by Sign in with ChatGPT on the OpenAI provider. Requires a ChatGPT Plus or Pro subscription. Provides access to GPT-5.x Codex models with extended context windows and reasoning capabilities. The library automatically handles session-based prompt caching when `sessionId` is provided in stream options unless `cacheRetention` is `"none"`. You can set `transport` in stream options to `"sse"`, `"websocket"`, or `"auto"` for Codex Responses transport selection. When using WebSocket with a `sessionId` and cache retention enabled, connections are reused per session and expire after 5 minutes of inactivity. Call `cleanupSessionResources(sessionId)` when finished so the pooled connection does not keep the process alive.
1876
1892
 
1877
1893
  **Azure OpenAI (Responses)**: Uses the Responses API only. Set `AZURE_OPENAI_API_KEY` and either `AZURE_OPENAI_BASE_URL` or `AZURE_OPENAI_RESOURCE_NAME`. `AZURE_OPENAI_BASE_URL` supports both `https://<resource>.openai.azure.com` and `https://<resource>.cognitiveservices.azure.com`; root endpoints are normalized to `.../openai/v1` automatically. Use `AZURE_OPENAI_API_VERSION` (defaults to `v1`) to override the API version if needed. Deployment names are treated as model IDs by default, override with `azureDeploymentName` or `AZURE_OPENAI_DEPLOYMENT_NAME_MAP` using comma-separated `model-id=deployment` pairs (for example `gpt-4o-mini=my-deployment,gpt-4o=prod`). Legacy deployment-based URLs are intentionally unsupported.
1878
1894
 
@@ -1928,12 +1944,12 @@ Create a new API implementation file (for example `bedrock-converse-stream.ts`)
1928
1944
 
1929
1945
  Add a lazy wrapper `src/api/<api-id>.lazy.ts` (`<name>Api()` via `lazyApi()`) so providers can reference the implementation without importing its SDK. Add any root-level `export type` re-exports in `src/index.ts` that should remain available from `@earendil-works/pi-ai`.
1930
1946
 
1931
- #### 3. Model Generation (`scripts/generate-models.ts`)
1947
+ #### 3. Model Catalog (`catalog/providers/<id>.ts`)
1932
1948
 
1933
- - Add logic to fetch and parse models from the provider's source (e.g., models.dev API)
1949
+ - Add a provider definition and register it in `catalog/providers/index.ts`; see `catalog/README.md` for the build order and helpers
1934
1950
  - Map chat/tool-capable provider data to `Model`, image-generation data to `ImageModel`, and models.dev `type: "decision"` entries to `ClassifierModel`; hydration groups the ignored `src/providers/data/<id>.json` values by API while stable `src/providers/<id>.models.ts` wrappers derive exact model/API types directly from those JSON keys
1935
1951
  - Keep model ids unique within each provider and model type; emit separate entries when an upstream model supports multiple operations
1936
- - Handle provider-specific quirks (pricing format, capability flags, model ID transformations)
1952
+ - Handle provider-specific quirks (pricing format, capability flags, model ID transformations) in the provider definition
1937
1953
 
1938
1954
  #### 4. Provider Factory (`src/providers/<id>.ts`)
1939
1955
 
@@ -1 +1 @@
1
- {"version":3,"file":"anthropic-messages.d.ts","sourceRoot":"","sources":["../../src/api/anthropic-messages.ts"],"names":[],"mappings":"AAAA,OAAO,SAAiC,MAAM,mBAAmB,CAAC;AAoBlE,OAAO,KAAK,EASX,mBAAmB,EAEnB,cAAc,EACd,aAAa,EAIb,MAAM,aAAa,CAAC;AA6JrB,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,CAAC;AAE1E,MAAM,MAAM,wBAAwB,GAAG,YAAY,GAAG,SAAS,CAAC;AA2ChE,MAAM,WAAW,gBAAiB,SAAQ,aAAa;IACtD;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB;;;;;;;;;;;OAWG;IACH,eAAe,CAAC,EAAE,wBAAwB,CAAC;IAC3C;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACtE;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;CACnB;AAoRD,eAAO,MAAM,MAAM,EAAE,cAAc,CAAC,oBAAoB,EAAE,gBAAgB,CAkWzE,CAAC;AA2BF,eAAO,MAAM,YAAY,EAAE,cAAc,CAAC,oBAAoB,EAAE,mBAAmB,CAgDlF,CAAC"}
1
+ {"version":3,"file":"anthropic-messages.d.ts","sourceRoot":"","sources":["../../src/api/anthropic-messages.ts"],"names":[],"mappings":"AAAA,OAAO,SAAiC,MAAM,mBAAmB,CAAC;AAqBlE,OAAO,KAAK,EASX,mBAAmB,EAEnB,cAAc,EACd,aAAa,EAIb,MAAM,aAAa,CAAC;AAiKrB,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,CAAC;AAE1E,MAAM,MAAM,wBAAwB,GAAG,YAAY,GAAG,SAAS,CAAC;AA6QhE,MAAM,WAAW,gBAAiB,SAAQ,aAAa;IACtD;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB;;;;;;;;;;;OAWG;IACH,eAAe,CAAC,EAAE,wBAAwB,CAAC;IAC3C;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACtE;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;CACnB;AAoRD,eAAO,MAAM,MAAM,EAAE,cAAc,CAAC,oBAAoB,EAAE,gBAAgB,CAgXzE,CAAC;AA2BF,eAAO,MAAM,YAAY,EAAE,cAAc,CAAC,oBAAoB,EAAE,mBAAmB,CAgDlF,CAAC"}
@@ -1,8 +1,11 @@
1
1
  import Anthropic, {} from "@anthropic-ai/sdk";
2
2
  import { ANTHROPIC_FEDERATION_RULE_ID_ENV, ANTHROPIC_IDENTITY_TOKEN_FILE_ENV, ANTHROPIC_ORGANIZATION_ID_ENV, ANTHROPIC_SERVICE_ACCOUNT_ID_ENV, ANTHROPIC_WORKSPACE_ID_ENV, } from "../env-api-keys.js";
3
3
  import { calculateCost } from "../models.js";
4
+ import { registerSessionResourceCleanup } from "../session-resources.js";
4
5
  import { appendAssistantMessageDiagnostic } from "../utils/diagnostics.js";
6
+ import { withCauseDetail } from "../utils/error-body.js";
5
7
  import { AssistantMessageEventStream } from "../utils/event-stream.js";
8
+ import { shortHash } from "../utils/hash.js";
6
9
  import { headersToRecord } from "../utils/headers.js";
7
10
  import { getIdentityHeaders } from "../utils/identity.js";
8
11
  import { parseJsonWithRepair } from "../utils/json-parse.js";
@@ -13,7 +16,7 @@ import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
13
16
  import { ToolCallArgumentsStream } from "../utils/streaming-json.js";
14
17
  import { getSystemMessageText, renderSystemMessageUpdate } from "../utils/text.js";
15
18
  import { getCurrentTools, getInitialSystemMessage, resolveTranscript, } from "../utils/transcript.js";
16
- import { getJsonSchemaToolParameters, resolveJsonSchemaStrictSampling, } from "./constrained-sampling.js";
19
+ import { getJsonSchemaToolParameters, isStrictToolLimitErrorMessage, prefersJsonSchemaStrictSampling, resolveJsonSchemaStrictSampling, } from "./constrained-sampling.js";
17
20
  import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js";
18
21
  import { adjustMaxTokensForThinking, buildBaseOptions, clampMaxTokensToContext } from "./simple-options.js";
19
22
  import { splitErrorToolResultImages, transformMessages } from "./transform-messages.js";
@@ -122,6 +125,7 @@ const SERVER_SIDE_FALLBACK_BETA = "server-side-fallback-2026-07-01";
122
125
  const MID_CONVERSATION_OUTPUT_CONFIG_BETA = "mid-conversation-output-config-2026-07-01";
123
126
  const THINKING_BINDING_CONTROLS_BETA = "thinking-binding-controls-2026-08-01";
124
127
  const INLINE_TOOLS_BETA = "inline-tools-2026-09-15";
128
+ const MESSAGE_THREADS_BETA = "message-threads-2026-08-12";
125
129
  /**
126
130
  * Stable deferred tool declared whenever native tool changes are in use. Anthropic adds
127
131
  * hidden prompt scaffolding for mid-conversation tool changes; declaring this placeholder
@@ -151,8 +155,178 @@ function getAnthropicCompat(model) {
151
155
  supportsStrictTools: model.compat?.supportsStrictTools ?? false,
152
156
  supportsMidConvoSystemMessages: model.compat?.supportsMidConvoSystemMessages ?? false,
153
157
  supportsMidConvoToolChanges: model.compat?.supportsMidConvoToolChanges ?? false,
158
+ // Gateways that drop unknown request fields would silently turn a `continue` into a request
159
+ // without the earlier conversation, so the default is limited to Anthropic's own endpoint.
160
+ // Widen or remove this restriction once proxies generally forward the `thread` field.
161
+ supportsMessageThreads: model.compat?.supportsMessageThreads ??
162
+ (model.provider === "anthropic" && model.baseUrl.includes("api.anthropic.com")),
154
163
  };
155
164
  }
165
+ /** Per session: where the next request can continue, or "off" after the endpoint rejected threads. */
166
+ const threadState = new Map();
167
+ const MAX_THREAD_STATE_ENTRIES = 10_000;
168
+ /** Rejections of a `continue` that a new thread fixes: expired, raced, or a fixed setting changed. */
169
+ const NEW_THREAD_ERRORS = new Set([
170
+ "thread_not_found",
171
+ "thread_already_continued",
172
+ "thread_continue_in_flight",
173
+ "thread_fingerprint_mismatch",
174
+ ]);
175
+ /**
176
+ * Sets a session's thread state, keeping at most `MAX_THREAD_STATE_ENTRIES` sessions.
177
+ *
178
+ * State is normally freed by `cleanupSessionResources(sessionId)` when a session ends, as pi's
179
+ * coding agent does. The cap bounds memory for library users that never call it, for example a
180
+ * server passing a new `sessionId` per conversation. Dropping the least recently updated session
181
+ * is cheap: its thread has most likely expired on the server already (after a few minutes idle),
182
+ * and otherwise its next request just starts a new thread.
183
+ */
184
+ function setThreadState(sessionId, value) {
185
+ // Re-insert so Map order stays oldest-first for the cap.
186
+ threadState.delete(sessionId);
187
+ threadState.set(sessionId, value);
188
+ if (threadState.size > MAX_THREAD_STATE_ENTRIES) {
189
+ const oldest = threadState.keys().next().value;
190
+ if (oldest !== undefined)
191
+ threadState.delete(oldest);
192
+ }
193
+ }
194
+ registerSessionResourceCleanup((sessionId) => {
195
+ if (sessionId)
196
+ threadState.delete(sessionId);
197
+ else
198
+ threadState.clear();
199
+ });
200
+ /**
201
+ * Hash of everything a `continue` builds on: the settings that must stay the same within a thread
202
+ * and the messages the server already holds.
203
+ *
204
+ * - Settings, as sent: the endpoint (a thread exists on one endpoint only), and the fixed request
205
+ * fields. A `continue` omits `system` and `tools` and the server keeps the thread's own, so only
206
+ * this check can notice a change to them, including their cache markers. The server rejects
207
+ * changes to the other fields with `thread_fingerprint_mismatch`; checking them here saves that
208
+ * rejected request. A fixed field missing here costs only that rejection, which also starts a
209
+ * new thread.
210
+ * - Messages, normalized so messages that mean the same to the API hash the same.
211
+ */
212
+ function hashThreadPrefix(model, params, messages) {
213
+ const { system, tools, thinking, output_config, tool_choice, betas, fallbacks } = params;
214
+ const settings = JSON.stringify([
215
+ model.provider,
216
+ model.baseUrl,
217
+ params.model,
218
+ system,
219
+ tools,
220
+ thinking,
221
+ output_config,
222
+ tool_choice,
223
+ betas,
224
+ fallbacks,
225
+ ]);
226
+ const conversation = JSON.stringify(messages, (key, value) => {
227
+ // Cache markers don't change what a message means, but they move: pi puts one on the newest
228
+ // message, so the message that had it in the last request has none in this one. Extensions
229
+ // may add markers anywhere too.
230
+ if (key === "cache_control")
231
+ return undefined;
232
+ // To attach that marker, the newest user message's string content is sent as one text block;
233
+ // the next request sends the same message as a string: "hi" vs [{ type: "text", text: "hi" }].
234
+ if (key === "content" && typeof value === "string")
235
+ return [{ type: "text", text: value }];
236
+ return value;
237
+ });
238
+ return shortHash(settings + conversation);
239
+ }
240
+ function createThreadRequest(sessionId, params) {
241
+ return { sessionId, body: { ...params, thread: { type: "create" } } };
242
+ }
243
+ function prepareThreadRequest(model, sessionId, params) {
244
+ const state = threadState.get(sessionId);
245
+ if (state === "off")
246
+ return undefined;
247
+ if (state !== undefined &&
248
+ params.messages.length > state.prefixCount &&
249
+ hashThreadPrefix(model, params, params.messages.slice(0, state.prefixCount)) === state.prefixHash) {
250
+ // Each reply can be continued once; deleting also keeps a concurrent request from trying.
251
+ threadState.delete(sessionId);
252
+ const { system: _system, tools: _tools, ...rest } = params;
253
+ return {
254
+ body: {
255
+ ...rest,
256
+ messages: params.messages.slice(state.prefixCount),
257
+ thread: { type: "continue", previous_message_id: state.replyId },
258
+ },
259
+ sessionId,
260
+ };
261
+ }
262
+ return createThreadRequest(sessionId, params);
263
+ }
264
+ /** The `error_code` Anthropic puts in `error.details` of thread rejections. */
265
+ function getThreadErrorCode(error) {
266
+ const body = error.error;
267
+ const code = body?.error?.details?.error_code;
268
+ return typeof code === "string" ? code : undefined;
269
+ }
270
+ /**
271
+ * Sends a thread request. A rejection about the thread (never a started stream) is resent once in
272
+ * full: as a new thread when the old one can't be continued, or without `thread` when the endpoint
273
+ * can't use threads, which also turns threads off for the session. Other errors are passed on.
274
+ * Returns the response and the thread request that produced it, if any.
275
+ */
276
+ async function sendThreadRequest(thread, params, send, output, signal) {
277
+ try {
278
+ return { response: await send(thread.body), thread };
279
+ }
280
+ catch (error) {
281
+ if (signal?.aborted || !(error instanceof Anthropic.APIError))
282
+ throw error;
283
+ const errorCode = getThreadErrorCode(error);
284
+ const startNewThread = errorCode !== undefined && NEW_THREAD_ERRORS.has(errorCode);
285
+ // The endpoint can't use threads: it can't thread this request, or it doesn't know the
286
+ // `thread` field and rejects it without a code ("thread: Extra inputs are not permitted"),
287
+ // e.g. with an `anthropic-beta` header that lacks the threads beta.
288
+ const threadsUnsupported = errorCode === "thread_unsupported_request" ||
289
+ (errorCode === undefined && error.status === 400 && error.message.includes("thread"));
290
+ // Any other error (prompt too long, unknown model, strict tool limits, ...) is not about the
291
+ // thread, and resending would only fail again.
292
+ if (!startNewThread && !threadsUnsupported)
293
+ throw error;
294
+ // Recorded before the resend, so a failed resend keeps it.
295
+ appendAssistantMessageDiagnostic(output, {
296
+ type: "anthropic_thread_fallback",
297
+ timestamp: Date.now(),
298
+ error: { message: error.message },
299
+ details: { reason: errorCode ?? String(error.status) },
300
+ });
301
+ if (startNewThread) {
302
+ const created = createThreadRequest(thread.sessionId, params);
303
+ return { response: await send(created.body), thread: created };
304
+ }
305
+ // With the threads beta, a request without `thread` is a normal request.
306
+ const response = await send(params);
307
+ setThreadState(thread.sessionId, "off");
308
+ return { response, thread: undefined };
309
+ }
310
+ }
311
+ /**
312
+ * Records where the session's next request can continue: after `output`, the clean reply to the
313
+ * thread request `thread`, whose full request was `params`.
314
+ */
315
+ function recordThreadReply(model, thread, params, output, isOAuth, allowEmptySignature) {
316
+ // `responseModel` is set only when a fallback model answered; the server does not store those
317
+ // replies, so they cannot be continued.
318
+ if (output.responseId === undefined || output.responseModel !== undefined)
319
+ return;
320
+ // Convert the reply like the next request will, keeping only the assistant message
321
+ // (transformMessages adds placeholder results for trailing tool calls).
322
+ const reply = convertMessages(transformMessages([output], model, normalizeToolCallId), isOAuth, undefined, allowEmptySignature).messages.filter((message) => message.role === "assistant");
323
+ const prefix = [...params.messages, ...reply];
324
+ setThreadState(thread.sessionId, {
325
+ prefixCount: prefix.length,
326
+ prefixHash: hashThreadPrefix(model, params, prefix),
327
+ replyId: output.responseId,
328
+ });
329
+ }
156
330
  function mergeHeaders(...headerSources) {
157
331
  const merged = {};
158
332
  for (const headers of headerSources) {
@@ -377,7 +551,8 @@ async function* iterateAnthropicEvents(response, signal) {
377
551
  }
378
552
  export const stream = (model, context, options) => {
379
553
  const stream = new AssistantMessageEventStream();
380
- const normalizedContext = resolveTranscript(context, getAnthropicCompat(model).supportsMidConvoSystemMessages);
554
+ const compat = getAnthropicCompat(model);
555
+ const normalizedContext = resolveTranscript(context, compat.supportsMidConvoSystemMessages);
381
556
  const currentTools = getCurrentTools(normalizedContext.messages);
382
557
  (async () => {
383
558
  const output = {
@@ -402,6 +577,12 @@ export const stream = (model, context, options) => {
402
577
  let isOAuth;
403
578
  let usageModel = model;
404
579
  let inputTransformations;
580
+ const cacheRetention = resolveCacheRetention(options?.cacheRetention, options?.env);
581
+ const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId;
582
+ // Session whose requests may use threads; undefined when threads don't apply. Sessions where
583
+ // the endpoint rejected threads ("off") still send the threads beta, which the resend that
584
+ // turned them off was accepted with; `prepareThreadRequest()` just sends no `thread`.
585
+ const threadSessionId = compat.supportsMessageThreads ? cacheSessionId : undefined;
405
586
  if (options?.client) {
406
587
  client = options.client;
407
588
  isOAuth = false;
@@ -419,8 +600,6 @@ export const stream = (model, context, options) => {
419
600
  hasImages,
420
601
  });
421
602
  }
422
- const cacheRetention = resolveCacheRetention(options?.cacheRetention, options?.env);
423
- const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId;
424
603
  const created = createClient(model, apiKey, options?.headers, options?.fetch, copilotDynamicHeaders, cacheSessionId, federation, options?.identity);
425
604
  client = created.client;
426
605
  isOAuth = created.isOAuthToken;
@@ -430,24 +609,38 @@ export const stream = (model, context, options) => {
430
609
  ...(options?.timeoutMs !== undefined ? { timeout: options.timeoutMs } : {}),
431
610
  maxRetries: 0,
432
611
  };
612
+ const send = (body) => retryProviderRequest(() => client.beta.messages.create(body, requestOptions).asResponse(), {
613
+ maxRetries: options?.maxRetries,
614
+ maxRetryDelayMs: options?.maxRetryDelayMs,
615
+ signal: options?.signal,
616
+ });
433
617
  let disablePreferredStrictTools = false;
434
618
  let response;
619
+ let params;
620
+ // The thread request that produced the response; undefined when no `thread` was sent.
621
+ let thread;
435
622
  for (;;) {
436
623
  options?.signal?.throwIfAborted();
437
- const built = buildParams(model, normalizedContext, isOAuth, options, disablePreferredStrictTools);
624
+ const built = buildParams(model, normalizedContext, isOAuth, options, {
625
+ disablePreferredStrictTools,
626
+ messageThreads: threadSessionId !== undefined,
627
+ });
438
628
  if (built.providerThinkingLevel)
439
629
  output.providerThinkingLevel = built.providerThinkingLevel;
440
- let params = built.params;
630
+ params = built.params;
441
631
  const nextParams = await options?.onPayload?.(params, model);
442
632
  if (nextParams !== undefined) {
443
633
  params = { ...nextParams, stream: true };
444
634
  }
635
+ const prepared = threadSessionId !== undefined && options?.disableContinuation !== true
636
+ ? prepareThreadRequest(model, threadSessionId, params)
637
+ : undefined;
445
638
  try {
446
- response = await retryProviderRequest(() => client.beta.messages.create(params, requestOptions).asResponse(), {
447
- maxRetries: options?.maxRetries,
448
- maxRetryDelayMs: options?.maxRetryDelayMs,
449
- signal: options?.signal,
450
- });
639
+ const sent = prepared
640
+ ? await sendThreadRequest(prepared, params, send, output, options?.signal)
641
+ : { response: await send(params), thread: undefined };
642
+ response = sent.response;
643
+ thread = sent.thread;
451
644
  break;
452
645
  }
453
646
  catch (error) {
@@ -458,11 +651,7 @@ export const stream = (model, context, options) => {
458
651
  !(error instanceof Anthropic.APIError) ||
459
652
  error.status !== 400 ||
460
653
  error.type !== "invalid_request_error" ||
461
- ![
462
- "The compiled grammar is too large",
463
- "Too many strict tools (",
464
- "Schemas contains too many parameters with union types (",
465
- ].some((message) => error.message.includes(message))) {
654
+ !isStrictToolLimitErrorMessage(error.message)) {
466
655
  throw error;
467
656
  }
468
657
  disablePreferredStrictTools = true;
@@ -687,6 +876,8 @@ export const stream = (model, context, options) => {
687
876
  if (output.stopReason === "aborted" || output.stopReason === "error") {
688
877
  throw new Error(output.errorMessage || "An unknown error occurred");
689
878
  }
879
+ if (thread)
880
+ recordThreadReply(model, thread, params, output, isOAuth, compat.allowEmptySignature);
690
881
  if (inputTransformations && inputTransformations.length > 0) {
691
882
  appendAssistantMessageDiagnostic(output, {
692
883
  type: "anthropic_input_transformations",
@@ -705,7 +896,7 @@ export const stream = (model, context, options) => {
705
896
  }
706
897
  catch (error) {
707
898
  output.stopReason = options?.signal?.aborted ? "aborted" : "error";
708
- output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
899
+ output.errorMessage = error instanceof Error ? withCauseDetail(error) : JSON.stringify(error);
709
900
  stream.push({ type: "error", reason: output.stopReason, error: output });
710
901
  stream.end();
711
902
  }
@@ -840,7 +1031,7 @@ function createClient(model, apiKey, optionsHeaders, fetch, dynamicHeaders, sess
840
1031
  });
841
1032
  return { client, isOAuthToken: false };
842
1033
  }
843
- function getBetaFeatures(model, context, isOAuthToken, nativeToolChanges, options) {
1034
+ function getBetaFeatures(model, context, options, { isOAuthToken, nativeToolChanges, messageThreads }) {
844
1035
  let configuredFeatures;
845
1036
  for (const headers of [model.headers, options?.headers]) {
846
1037
  for (const [name, value] of Object.entries(headers ?? {})) {
@@ -876,9 +1067,11 @@ function getBetaFeatures(model, context, isOAuthToken, nativeToolChanges, option
876
1067
  }
877
1068
  if (nativeToolChanges)
878
1069
  features.push(INLINE_TOOLS_BETA);
1070
+ if (messageThreads)
1071
+ features.push(MESSAGE_THREADS_BETA);
879
1072
  return [...new Set(features)];
880
1073
  }
881
- function buildParams(model, context, isOAuthToken, options, disablePreferredStrictTools = false) {
1074
+ function buildParams(model, context, isOAuthToken, options, flags) {
882
1075
  const { cacheControl } = getCacheControl(model, options?.cacheRetention, options?.env);
883
1076
  const compat = getAnthropicCompat(model);
884
1077
  const initialSystemMessage = getInitialSystemMessage(context.messages);
@@ -891,11 +1084,8 @@ function buildParams(model, context, isOAuthToken, options, disablePreferredStri
891
1084
  // active tool to anchor the placeholder. Otherwise the current tool list is sent.
892
1085
  let hasPreferredStrictTools = false;
893
1086
  const convertToolDefinitions = (tools, toolCacheControl) => {
894
- const definitions = convertTools(tools, isOAuthToken, compat.supportsEagerToolInputStreaming, compat.supportsStrictTools, toolCacheControl, disablePreferredStrictTools);
895
- if (definitions.some((definition, index) => {
896
- const config = tools[index].constrainedSampling;
897
- return definition.strict === true && config && config.type === "json_schema" && config.strict === "prefer";
898
- })) {
1087
+ const definitions = convertTools(tools, isOAuthToken, compat.supportsEagerToolInputStreaming, compat.supportsStrictTools, toolCacheControl, flags.disablePreferredStrictTools);
1088
+ if (definitions.some((definition, index) => definition.strict === true && prefersJsonSchemaStrictSampling(tools[index]))) {
899
1089
  hasPreferredStrictTools = true;
900
1090
  }
901
1091
  return definitions;
@@ -907,7 +1097,11 @@ function buildParams(model, context, isOAuthToken, options, disablePreferredStri
907
1097
  const effortMessages = model.compat?.supportsMidConvoEffort === true && !thinkingDisabled;
908
1098
  const converted = convertMessages(conversationMessages, isOAuthToken, cacheControl, compat.allowEmptySignature, effortMessages ? model.provider : undefined, nativeToolChanges ? convertToolDefinitions : undefined);
909
1099
  const activeEffort = options?.effort ?? "high";
910
- const betaFeatures = getBetaFeatures(model, context, isOAuthToken, nativeToolChanges, options);
1100
+ const betaFeatures = getBetaFeatures(model, context, options, {
1101
+ isOAuthToken,
1102
+ nativeToolChanges,
1103
+ messageThreads: flags.messageThreads,
1104
+ });
911
1105
  const params = {
912
1106
  model: model.id,
913
1107
  messages: effortMessages ? insertThinkingLevelMessages(converted, activeEffort) : converted.messages,
@@ -1295,16 +1489,19 @@ function convertTools(tools, isOAuthToken, supportsEagerToolInputStreaming, supp
1295
1489
  if (!tools)
1296
1490
  return [];
1297
1491
  return tools.map((tool, index) => {
1298
- const config = tool.constrainedSampling;
1299
- const strict = disablePreferredStrictTools && config && config.type === "json_schema" && config.strict === "prefer"
1492
+ const strict = disablePreferredStrictTools && prefersJsonSchemaStrictSampling(tool)
1300
1493
  ? undefined
1301
1494
  : resolveJsonSchemaStrictSampling(tool, supportsStrictTools, isAnthropicStrictUnsupportedKeyword);
1302
1495
  const parameters = getJsonSchemaToolParameters(tool, strict);
1303
1496
  const schema = parameters;
1497
+ // Keep shared definitions so property $refs still resolve. Other root keywords stay out:
1498
+ // Anthropic rejects root anyOf/oneOf/allOf with a 400.
1304
1499
  const legacyInputSchema = {
1305
1500
  type: "object",
1306
1501
  properties: schema.properties ?? {},
1307
1502
  required: schema.required ?? [],
1503
+ ...(schema.$defs === undefined ? {} : { $defs: schema.$defs }),
1504
+ ...(schema.definitions === undefined ? {} : { definitions: schema.definitions }),
1308
1505
  };
1309
1506
  const inputSchema = strict === true
1310
1507
  ? {