@earendil-works/pi-ai 1.4.0 → 1.5.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 +35 -35
  2. package/dist/api/anthropic-messages.d.ts.map +1 -1
  3. package/dist/api/anthropic-messages.js +59 -71
  4. package/dist/api/anthropic-messages.js.map +1 -1
  5. package/dist/api/azure-openai-responses.d.ts.map +1 -1
  6. package/dist/api/azure-openai-responses.js +0 -6
  7. package/dist/api/azure-openai-responses.js.map +1 -1
  8. package/dist/api/bedrock-converse-stream.d.ts.map +1 -1
  9. package/dist/api/bedrock-converse-stream.js +125 -103
  10. package/dist/api/bedrock-converse-stream.js.map +1 -1
  11. package/dist/api/constrained-sampling.d.ts +15 -1
  12. package/dist/api/constrained-sampling.d.ts.map +1 -1
  13. package/dist/api/constrained-sampling.js +51 -18
  14. package/dist/api/constrained-sampling.js.map +1 -1
  15. package/dist/api/google-generative-ai.d.ts.map +1 -1
  16. package/dist/api/google-generative-ai.js +0 -6
  17. package/dist/api/google-generative-ai.js.map +1 -1
  18. package/dist/api/google-shared.d.ts +2 -1
  19. package/dist/api/google-shared.d.ts.map +1 -1
  20. package/dist/api/google-shared.js +57 -2
  21. package/dist/api/google-shared.js.map +1 -1
  22. package/dist/api/google-vertex.d.ts.map +1 -1
  23. package/dist/api/google-vertex.js +0 -6
  24. package/dist/api/google-vertex.js.map +1 -1
  25. package/dist/api/mistral-conversations.d.ts.map +1 -1
  26. package/dist/api/mistral-conversations.js +17 -44
  27. package/dist/api/mistral-conversations.js.map +1 -1
  28. package/dist/api/openai-codex-responses.d.ts.map +1 -1
  29. package/dist/api/openai-codex-responses.js +0 -5
  30. package/dist/api/openai-codex-responses.js.map +1 -1
  31. package/dist/api/openai-completions.d.ts.map +1 -1
  32. package/dist/api/openai-completions.js +42 -54
  33. package/dist/api/openai-completions.js.map +1 -1
  34. package/dist/api/openai-responses-shared.d.ts.map +1 -1
  35. package/dist/api/openai-responses-shared.js +58 -57
  36. package/dist/api/openai-responses-shared.js.map +1 -1
  37. package/dist/api/openai-responses.d.ts.map +1 -1
  38. package/dist/api/openai-responses.js +0 -6
  39. package/dist/api/openai-responses.js.map +1 -1
  40. package/dist/api/openrouter-images.js +1 -0
  41. package/dist/api/openrouter-images.js.map +1 -1
  42. package/dist/api/pi-messages.d.ts +2 -0
  43. package/dist/api/pi-messages.d.ts.map +1 -1
  44. package/dist/api/pi-messages.js +24 -9
  45. package/dist/api/pi-messages.js.map +1 -1
  46. package/dist/auth/oauth/github-copilot.d.ts.map +1 -1
  47. package/dist/auth/oauth/github-copilot.js +4 -11
  48. package/dist/auth/oauth/github-copilot.js.map +1 -1
  49. package/dist/auth/oauth/kimi-coding.d.ts.map +1 -1
  50. package/dist/auth/oauth/kimi-coding.js +4 -22
  51. package/dist/auth/oauth/kimi-coding.js.map +1 -1
  52. package/dist/auth/oauth/meta.d.ts.map +1 -1
  53. package/dist/auth/oauth/meta.js +1 -14
  54. package/dist/auth/oauth/meta.js.map +1 -1
  55. package/dist/auth/oauth/radius.d.ts.map +1 -1
  56. package/dist/auth/oauth/radius.js +9 -3
  57. package/dist/auth/oauth/radius.js.map +1 -1
  58. package/dist/auth/oauth/trusted-url.d.ts +7 -0
  59. package/dist/auth/oauth/trusted-url.d.ts.map +1 -0
  60. package/dist/auth/oauth/trusted-url.js +12 -0
  61. package/dist/auth/oauth/trusted-url.js.map +1 -0
  62. package/dist/auth/types.d.ts +4 -0
  63. package/dist/auth/types.d.ts.map +1 -1
  64. package/dist/auth/types.js.map +1 -1
  65. package/dist/cli.js +6 -2
  66. package/dist/cli.js.map +1 -1
  67. package/dist/compat/extension-oauth-types.d.ts +4 -0
  68. package/dist/compat/extension-oauth-types.d.ts.map +1 -1
  69. package/dist/compat/extension-oauth-types.js.map +1 -1
  70. package/dist/index.d.ts +2 -3
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +2 -2
  73. package/dist/index.js.map +1 -1
  74. package/dist/models.d.ts.map +1 -1
  75. package/dist/models.generated.js +1 -1
  76. package/dist/models.generated.js.map +1 -1
  77. package/dist/models.js +1 -0
  78. package/dist/models.js.map +1 -1
  79. package/dist/providers/amazon-bedrock.models.js +1 -1
  80. package/dist/providers/amazon-bedrock.models.js.map +1 -1
  81. package/dist/providers/ant-ling.models.js +1 -1
  82. package/dist/providers/ant-ling.models.js.map +1 -1
  83. package/dist/providers/anthropic.models.js +1 -1
  84. package/dist/providers/anthropic.models.js.map +1 -1
  85. package/dist/providers/azure.models.js +1 -1
  86. package/dist/providers/azure.models.js.map +1 -1
  87. package/dist/providers/baseten.models.js +1 -1
  88. package/dist/providers/baseten.models.js.map +1 -1
  89. package/dist/providers/cerebras.models.js +1 -1
  90. package/dist/providers/cerebras.models.js.map +1 -1
  91. package/dist/providers/cloudflare-ai-gateway.models.js +1 -1
  92. package/dist/providers/cloudflare-ai-gateway.models.js.map +1 -1
  93. package/dist/providers/cloudflare-workers-ai.models.js +1 -1
  94. package/dist/providers/cloudflare-workers-ai.models.js.map +1 -1
  95. package/dist/providers/compat-schema.d.ts +428 -456
  96. package/dist/providers/compat-schema.d.ts.map +1 -1
  97. package/dist/providers/compat-schema.js +144 -111
  98. package/dist/providers/compat-schema.js.map +1 -1
  99. package/dist/providers/data/.manifest.json +1 -1
  100. package/dist/providers/data/amazon-bedrock.json +1 -1
  101. package/dist/providers/data/anthropic.json +1 -1
  102. package/dist/providers/data/azure.json +1 -1
  103. package/dist/providers/data/baseten.json +1 -1
  104. package/dist/providers/data/cloudflare-ai-gateway.json +1 -1
  105. package/dist/providers/data/cloudflare-workers-ai.json +1 -1
  106. package/dist/providers/data/fireworks.json +1 -1
  107. package/dist/providers/data/github-copilot.json +1 -1
  108. package/dist/providers/data/google-vertex.json +1 -1
  109. package/dist/providers/data/google.json +1 -1
  110. package/dist/providers/data/huggingface.json +1 -1
  111. package/dist/providers/data/kimi-coding.json +1 -1
  112. package/dist/providers/data/mistral.json +1 -1
  113. package/dist/providers/data/moonshotai-cn.json +1 -1
  114. package/dist/providers/data/moonshotai.json +1 -1
  115. package/dist/providers/data/nvidia.json +1 -1
  116. package/dist/providers/data/openai-codex.json +1 -1
  117. package/dist/providers/data/openai.json +1 -1
  118. package/dist/providers/data/opencode-go.json +1 -1
  119. package/dist/providers/data/opencode.json +1 -1
  120. package/dist/providers/data/openrouter.json +1 -1
  121. package/dist/providers/data/qwen-token-plan-cn.json +1 -1
  122. package/dist/providers/data/qwen-token-plan-individual.json +1 -1
  123. package/dist/providers/data/qwen-token-plan.json +1 -1
  124. package/dist/providers/data/radius.json +1 -1
  125. package/dist/providers/data/together.json +1 -1
  126. package/dist/providers/data/vercel-ai-gateway.json +1 -1
  127. package/dist/providers/data/xai.json +1 -1
  128. package/dist/providers/data/xiaomi-token-plan-ams.json +1 -1
  129. package/dist/providers/data/xiaomi-token-plan-cn.json +1 -1
  130. package/dist/providers/data/xiaomi-token-plan-sgp.json +1 -1
  131. package/dist/providers/data/xiaomi.json +1 -1
  132. package/dist/providers/data/zai-coding-cn.json +1 -1
  133. package/dist/providers/data/zai.json +1 -1
  134. package/dist/providers/deepseek.models.js +1 -1
  135. package/dist/providers/deepseek.models.js.map +1 -1
  136. package/dist/providers/faux.d.ts.map +1 -1
  137. package/dist/providers/faux.js +7 -2
  138. package/dist/providers/faux.js.map +1 -1
  139. package/dist/providers/fireworks.models.js +1 -1
  140. package/dist/providers/fireworks.models.js.map +1 -1
  141. package/dist/providers/github-copilot.models.js +1 -1
  142. package/dist/providers/github-copilot.models.js.map +1 -1
  143. package/dist/providers/google-vertex.models.js +1 -1
  144. package/dist/providers/google-vertex.models.js.map +1 -1
  145. package/dist/providers/google.models.js +1 -1
  146. package/dist/providers/google.models.js.map +1 -1
  147. package/dist/providers/groq.models.js +1 -1
  148. package/dist/providers/groq.models.js.map +1 -1
  149. package/dist/providers/huggingface.models.js +1 -1
  150. package/dist/providers/huggingface.models.js.map +1 -1
  151. package/dist/providers/kimi-coding.models.js +1 -1
  152. package/dist/providers/kimi-coding.models.js.map +1 -1
  153. package/dist/providers/meta.models.js +1 -1
  154. package/dist/providers/meta.models.js.map +1 -1
  155. package/dist/providers/minimax-cn.models.js +1 -1
  156. package/dist/providers/minimax-cn.models.js.map +1 -1
  157. package/dist/providers/minimax.models.js +1 -1
  158. package/dist/providers/minimax.models.js.map +1 -1
  159. package/dist/providers/mistral.models.js +1 -1
  160. package/dist/providers/mistral.models.js.map +1 -1
  161. package/dist/providers/model-schema.d.ts +65 -65
  162. package/dist/providers/model-schema.d.ts.map +1 -1
  163. package/dist/providers/model-schema.js +41 -30
  164. package/dist/providers/model-schema.js.map +1 -1
  165. package/dist/providers/moonshotai-cn.models.js +1 -1
  166. package/dist/providers/moonshotai-cn.models.js.map +1 -1
  167. package/dist/providers/moonshotai.models.js +1 -1
  168. package/dist/providers/moonshotai.models.js.map +1 -1
  169. package/dist/providers/nvidia.models.js +1 -1
  170. package/dist/providers/nvidia.models.js.map +1 -1
  171. package/dist/providers/openai-codex.models.js +1 -1
  172. package/dist/providers/openai-codex.models.js.map +1 -1
  173. package/dist/providers/openai.models.js +1 -1
  174. package/dist/providers/openai.models.js.map +1 -1
  175. package/dist/providers/opencode-go.models.js +1 -1
  176. package/dist/providers/opencode-go.models.js.map +1 -1
  177. package/dist/providers/opencode.js +1 -1
  178. package/dist/providers/opencode.js.map +1 -1
  179. package/dist/providers/opencode.models.js +1 -1
  180. package/dist/providers/opencode.models.js.map +1 -1
  181. package/dist/providers/openrouter.models.js +1 -1
  182. package/dist/providers/openrouter.models.js.map +1 -1
  183. package/dist/providers/qwen-token-plan-cn.models.js +1 -1
  184. package/dist/providers/qwen-token-plan-cn.models.js.map +1 -1
  185. package/dist/providers/qwen-token-plan-individual.models.js +1 -1
  186. package/dist/providers/qwen-token-plan-individual.models.js.map +1 -1
  187. package/dist/providers/qwen-token-plan.models.js +1 -1
  188. package/dist/providers/qwen-token-plan.models.js.map +1 -1
  189. package/dist/providers/radius-config.d.ts.map +1 -1
  190. package/dist/providers/radius-config.js +2 -1
  191. package/dist/providers/radius-config.js.map +1 -1
  192. package/dist/providers/radius.models.js +1 -1
  193. package/dist/providers/radius.models.js.map +1 -1
  194. package/dist/providers/schema-builders.d.ts +10 -0
  195. package/dist/providers/schema-builders.d.ts.map +1 -0
  196. package/dist/providers/schema-builders.js +18 -0
  197. package/dist/providers/schema-builders.js.map +1 -0
  198. package/dist/providers/together.models.js +1 -1
  199. package/dist/providers/together.models.js.map +1 -1
  200. package/dist/providers/typesafe.models.js +1 -1
  201. package/dist/providers/typesafe.models.js.map +1 -1
  202. package/dist/providers/vercel-ai-gateway.models.js +1 -1
  203. package/dist/providers/vercel-ai-gateway.models.js.map +1 -1
  204. package/dist/providers/xai.models.js +1 -1
  205. package/dist/providers/xai.models.js.map +1 -1
  206. package/dist/providers/xiaomi-token-plan-ams.models.js +1 -1
  207. package/dist/providers/xiaomi-token-plan-ams.models.js.map +1 -1
  208. package/dist/providers/xiaomi-token-plan-cn.models.js +1 -1
  209. package/dist/providers/xiaomi-token-plan-cn.models.js.map +1 -1
  210. package/dist/providers/xiaomi-token-plan-sgp.models.js +1 -1
  211. package/dist/providers/xiaomi-token-plan-sgp.models.js.map +1 -1
  212. package/dist/providers/xiaomi.models.js +1 -1
  213. package/dist/providers/xiaomi.models.js.map +1 -1
  214. package/dist/providers/zai-coding-cn.models.js +1 -1
  215. package/dist/providers/zai-coding-cn.models.js.map +1 -1
  216. package/dist/providers/zai.models.js +1 -1
  217. package/dist/providers/zai.models.js.map +1 -1
  218. package/dist/telemetry.d.ts.map +1 -1
  219. package/dist/telemetry.js +2 -1
  220. package/dist/telemetry.js.map +1 -1
  221. package/dist/types.d.ts +12 -2
  222. package/dist/types.d.ts.map +1 -1
  223. package/dist/types.js.map +1 -1
  224. package/dist/utils/error-body.d.ts.map +1 -1
  225. package/dist/utils/error-body.js +2 -1
  226. package/dist/utils/error-body.js.map +1 -1
  227. package/dist/utils/openrouter-cost.d.ts +24 -0
  228. package/dist/utils/openrouter-cost.d.ts.map +1 -0
  229. package/dist/utils/openrouter-cost.js +34 -0
  230. package/dist/utils/openrouter-cost.js.map +1 -0
  231. package/dist/utils/overflow.d.ts.map +1 -1
  232. package/dist/utils/overflow.js +2 -0
  233. package/dist/utils/overflow.js.map +1 -1
  234. package/dist/utils/retry.d.ts.map +1 -1
  235. package/dist/utils/retry.js +3 -0
  236. package/dist/utils/retry.js.map +1 -1
  237. package/dist/utils/streaming-json.d.ts +84 -0
  238. package/dist/utils/streaming-json.d.ts.map +1 -0
  239. package/dist/utils/streaming-json.js +513 -0
  240. package/dist/utils/streaming-json.js.map +1 -0
  241. package/dist/utils/{typebox-helpers.d.ts → string-enum.d.ts} +5 -5
  242. package/dist/utils/string-enum.d.ts.map +1 -0
  243. package/dist/utils/{typebox-helpers.js → string-enum.js} +5 -6
  244. package/dist/utils/string-enum.js.map +1 -0
  245. package/dist/utils/text.d.ts +7 -0
  246. package/dist/utils/text.d.ts.map +1 -1
  247. package/dist/utils/text.js +9 -0
  248. package/dist/utils/text.js.map +1 -1
  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 +27 -277
  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 +11 -10
  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
@@ -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
 
@@ -233,8 +234,12 @@ for (const block of response.content) {
233
234
  }
234
235
  ```
235
236
 
237
+ `usage.cost.totalFromProvider` is `true` when `usage.cost.total` is the billed cost reported by the provider and `false` when it is estimated from the model's catalog rates. A missing value, as in sessions recorded before this field existed, also means `false`.
238
+
236
239
  Snippets in the rest of this README assume a `models` collection set up like this (with the relevant providers registered).
237
240
 
241
+ `usage.cost.total` is the best available total cost. For OpenRouter chat responses, it uses the cost reported by OpenRouter, including both the OpenRouter charge and upstream inference cost for BYOK requests. When provider accounting is unavailable or invalid, it falls back to the model's catalog rates. The `input`, `output`, `cacheRead`, and `cacheWrite` cost components remain catalog estimates and may not sum to a provider-reported total.
242
+
238
243
  ## Providers and Models
239
244
 
240
245
  A **provider** is the runtime unit: it owns its model catalog, its auth (API key resolution, OAuth flows), and its stream behavior. A `Models` collection holds providers and routes every request to the provider that owns the model.
@@ -534,34 +539,35 @@ Amazon Bedrock resolves ambient AWS credentials (`AWS_PROFILE`, access key pairs
534
539
 
535
540
  ## Tools
536
541
 
537
- 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.
542
+ 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.
538
543
 
539
544
  ### Defining Tools
540
545
 
541
546
  ```typescript
542
- import { Type, type Tool, StringEnum } from '@earendil-works/pi-ai';
547
+ import { type Tool, StringEnum } from '@earendil-works/pi-ai';
548
+ import { arr, obj, str } from '@earendil-works/pi-schema';
543
549
 
544
- // Define tool parameters with TypeBox
550
+ // Define tool parameters with pi-schema's builders
545
551
  const weatherTool: Tool = {
546
552
  name: 'get_weather',
547
553
  description: 'Get current weather for a location',
548
- parameters: Type.Object({
549
- location: Type.String({ description: 'City name or coordinates' }),
554
+ parameters: obj({
555
+ location: str({ description: 'City name or coordinates' }),
550
556
  units: StringEnum(['celsius', 'fahrenheit'], { default: 'celsius' })
551
557
  })
552
558
  };
553
559
 
554
- // Note: For Google API compatibility, use StringEnum helper instead of Type.Enum
555
- // Type.Enum generates anyOf/const patterns that Google doesn't support
560
+ // Note: StringEnum gives { type: "string", enum: [...] }, which every provider accepts,
561
+ // including Google's, which rejects anyOf/const patterns
556
562
 
557
563
  const bookMeetingTool: Tool = {
558
564
  name: 'book_meeting',
559
565
  description: 'Schedule a meeting',
560
- parameters: Type.Object({
561
- title: Type.String({ minLength: 1 }),
562
- startTime: Type.String({ format: 'date-time' }),
563
- endTime: Type.String({ format: 'date-time' }),
564
- attendees: Type.Array(Type.String({ format: 'email' }), { minItems: 1 })
566
+ parameters: obj({
567
+ title: str({ minLength: 1 }),
568
+ startTime: str({ format: 'date-time' }),
569
+ endTime: str({ format: 'date-time' }),
570
+ attendees: arr(str({ format: 'email' }), { minItems: 1 })
565
571
  })
566
572
  };
567
573
  ```
@@ -574,10 +580,7 @@ Tools can opt in to provider-side constrained sampling. For JSON-schema tools, `
574
580
  const strictTool: Tool = {
575
581
  name: 'edit_file',
576
582
  description: 'Edit a file',
577
- parameters: Type.Object({
578
- path: Type.String(),
579
- content: Type.String()
580
- }, { additionalProperties: false }),
583
+ parameters: obj({ path: str(), content: str() }), // obj() is closed: additionalProperties: false
581
584
  constrainedSampling: { type: 'json_schema', strict: 'prefer' }
582
585
  };
583
586
  ```
@@ -588,9 +591,7 @@ Strict JSON-schema constrained sampling is supported for OpenAI, Anthropic, supp
588
591
  const patchTool: Tool = {
589
592
  name: 'apply_patch',
590
593
  description: 'Apply a patch',
591
- parameters: Type.Object({
592
- input: Type.String()
593
- }, { additionalProperties: false }),
594
+ parameters: obj({ input: str() }),
594
595
  constrainedSampling: {
595
596
  type: 'grammar',
596
597
  variants: {
@@ -694,7 +695,7 @@ for await (const event of s) {
694
695
 
695
696
  ### Validating Tool Arguments
696
697
 
697
- When implementing your own tool execution loop, use `validateToolCall` to validate arguments before passing them to your tools:
698
+ 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:
698
699
 
699
700
  ```typescript
700
701
  import { validateToolCall, type Tool } from '@earendil-works/pi-ai';
@@ -1722,8 +1723,8 @@ models.setProvider(openaiProvider());
1722
1723
 
1723
1724
  Rules:
1724
1725
 
1725
- - `@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.
1726
- - `@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.
1726
+ - `@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.
1727
+ - `@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.
1727
1728
  - `@earendil-works/pi-ai/providers/<provider>` imports that provider's catalog and lazy API wrapper only.
1728
1729
  - `@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.
1729
1730
  - With code splitting, provider SDKs stay in lazy chunks and load on first request.
@@ -1924,19 +1925,19 @@ Create a new API implementation file (for example `bedrock-converse-stream.ts`)
1924
1925
 
1925
1926
  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`.
1926
1927
 
1927
- #### 3. Model Generation (`scripts/generate-models.ts`)
1928
+ #### 3. Model Catalog (`catalog/providers/<id>.ts`)
1928
1929
 
1929
- - Add logic to fetch and parse models from the provider's source (e.g., models.dev API)
1930
+ - Add a provider definition and register it in `catalog/providers/index.ts`; see `catalog/README.md` for the build order and helpers
1930
1931
  - 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
1931
1932
  - Keep model ids unique within each provider and model type; emit separate entries when an upstream model supports multiple operations
1932
- - Handle provider-specific quirks (pricing format, capability flags, model ID transformations)
1933
+ - Handle provider-specific quirks (pricing format, capability flags, model ID transformations) in the provider definition
1933
1934
 
1934
1935
  #### 4. Provider Factory (`src/providers/<id>.ts`)
1935
1936
 
1936
1937
  - `createProvider()` wiring catalog + auth + the lazy API wrapper
1937
1938
  - Auth: `envApiKeyAuth` for standard key providers, a custom `ApiKeyAuth` for ambient auth (AWS profiles, ADC), `lazyOAuth` where an OAuth flow exists
1938
1939
  - Register the factory in `src/providers/all.ts`
1939
- - If it is a new API: register it in the builtin list in `src/compat.ts` and add the package subpath export in `package.json`
1940
+ - If it is a new API: register it in the builtin list in `src/compat.ts`; the `./api/*` subpath export in `package.json` already covers the new module
1940
1941
 
1941
1942
  #### 5. Tests (`test/`)
1942
1943
 
@@ -1947,7 +1948,6 @@ Create or update test files to cover the new provider:
1947
1948
  - `abort.test.ts` - Request cancellation
1948
1949
  - `empty.test.ts` - Empty message handling
1949
1950
  - `context-overflow.test.ts` - Context limit errors
1950
- - `image-limits.test.ts` - Image support (if applicable)
1951
1951
  - `unicode-surrogate.test.ts` - Unicode handling
1952
1952
  - `tool-call-without-result.test.ts` - Orphaned tool calls
1953
1953
  - `image-tool-result.test.ts` - Images in tool results
@@ -1963,15 +1963,15 @@ For providers with non-standard auth (AWS, Google Vertex), create a utility like
1963
1963
 
1964
1964
  Update `src/core/model-resolver.ts`:
1965
1965
 
1966
- - Add a default model ID for the provider in `DEFAULT_MODELS`
1966
+ - Add a default model ID for the provider in `defaultModelPerProvider`
1967
1967
 
1968
1968
  Update `src/cli/args.ts`:
1969
1969
 
1970
1970
  - Add environment variable documentation in the help text
1971
1971
 
1972
- Update `README.md`:
1972
+ Update `docs/providers.md`:
1973
1973
 
1974
- - Add the provider to the providers section with setup instructions
1974
+ - Add setup instructions, the environment variable, and the `auth.json` key
1975
1975
 
1976
1976
  #### 7. Documentation
1977
1977
 
@@ -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,EAMb,MAAM,aAAa,CAAC;AAuJrB,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,CA2WzE,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;AAoBlE,OAAO,KAAK,EASX,mBAAmB,EAEnB,cAAc,EACd,aAAa,EAIb,MAAM,aAAa,CAAC;AA+JrB,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,CA8VzE,CAAC;AA2BF,eAAO,MAAM,YAAY,EAAE,cAAc,CAAC,oBAAoB,EAAE,mBAAmB,CAgDlF,CAAC"}
@@ -5,13 +5,15 @@ import { appendAssistantMessageDiagnostic } from "../utils/diagnostics.js";
5
5
  import { AssistantMessageEventStream } from "../utils/event-stream.js";
6
6
  import { headersToRecord } from "../utils/headers.js";
7
7
  import { getIdentityHeaders } from "../utils/identity.js";
8
- import { parseJsonWithRepair, parseStreamingJson, parseToolCallArguments } from "../utils/json-parse.js";
8
+ import { parseJsonWithRepair } from "../utils/json-parse.js";
9
+ import { applyOpenRouterReportedCost, isOpenRouterModel, } from "../utils/openrouter-cost.js";
9
10
  import { getProviderEnvValue } from "../utils/provider-env.js";
10
11
  import { retryProviderRequest } from "../utils/provider-retry.js";
11
12
  import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
13
+ import { ToolCallArgumentsStream } from "../utils/streaming-json.js";
12
14
  import { getSystemMessageText, renderSystemMessageUpdate } from "../utils/text.js";
13
15
  import { getCurrentTools, getInitialSystemMessage, resolveTranscript, } from "../utils/transcript.js";
14
- import { getJsonSchemaToolParameters, resolveJsonSchemaStrictSampling, } from "./constrained-sampling.js";
16
+ import { getJsonSchemaToolParameters, isStrictToolLimitErrorMessage, prefersJsonSchemaStrictSampling, resolveJsonSchemaStrictSampling, } from "./constrained-sampling.js";
15
17
  import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js";
16
18
  import { adjustMaxTokensForThinking, buildBaseOptions, clampMaxTokensToContext } from "./simple-options.js";
17
19
  import { splitErrorToolResultImages, transformMessages } from "./transform-messages.js";
@@ -456,11 +458,7 @@ export const stream = (model, context, options) => {
456
458
  !(error instanceof Anthropic.APIError) ||
457
459
  error.status !== 400 ||
458
460
  error.type !== "invalid_request_error" ||
459
- ![
460
- "The compiled grammar is too large",
461
- "Too many strict tools (",
462
- "Schemas contains too many parameters with union types (",
463
- ].some((message) => error.message.includes(message))) {
461
+ !isStrictToolLimitErrorMessage(error.message)) {
464
462
  throw error;
465
463
  }
466
464
  disablePreferredStrictTools = true;
@@ -474,7 +472,12 @@ export const stream = (model, context, options) => {
474
472
  }
475
473
  await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model);
476
474
  stream.push({ type: "start", partial: output });
477
- const blocks = output.content;
475
+ // Stream state per Anthropic block index, kept off the content blocks so it never reaches partials.
476
+ const openBlocks = new Map();
477
+ const openBlock = (anthropicIndex) => {
478
+ const open = openBlocks.get(anthropicIndex);
479
+ return open === undefined ? undefined : { ...open, block: output.content[open.contentIndex] };
480
+ };
478
481
  for await (const event of iterateAnthropicEvents(response, options?.signal)) {
479
482
  await options?.onProviderStreamEvent?.(event, model);
480
483
  if (event.type === "message_start") {
@@ -509,105 +512,92 @@ export const stream = (model, context, options) => {
509
512
  continue;
510
513
  }
511
514
  if (event.content_block.type === "text") {
512
- const block = {
513
- type: "text",
514
- text: event.content_block.text ?? "",
515
- index: event.index,
516
- };
517
- output.content.push(block);
515
+ output.content.push({ type: "text", text: event.content_block.text ?? "" });
516
+ openBlocks.set(event.index, { contentIndex: output.content.length - 1 });
518
517
  stream.push({ type: "text_start", contentIndex: output.content.length - 1, partial: output });
519
518
  }
520
519
  else if (event.content_block.type === "thinking") {
521
- const block = {
520
+ output.content.push({
522
521
  type: "thinking",
523
522
  thinking: event.content_block.thinking ?? "",
524
523
  thinkingSignature: event.content_block.signature ?? "",
525
- index: event.index,
526
- };
527
- output.content.push(block);
524
+ });
525
+ openBlocks.set(event.index, { contentIndex: output.content.length - 1 });
528
526
  stream.push({ type: "thinking_start", contentIndex: output.content.length - 1, partial: output });
529
527
  }
530
528
  else if (event.content_block.type === "redacted_thinking") {
531
- const block = {
529
+ output.content.push({
532
530
  type: "thinking",
533
531
  thinking: "[Reasoning redacted]",
534
532
  thinkingSignature: event.content_block.data,
535
533
  redacted: true,
536
- index: event.index,
537
- };
538
- output.content.push(block);
534
+ });
535
+ openBlocks.set(event.index, { contentIndex: output.content.length - 1 });
539
536
  stream.push({ type: "thinking_start", contentIndex: output.content.length - 1, partial: output });
540
537
  }
541
538
  else if (event.content_block.type === "tool_use") {
542
- const block = {
539
+ output.content.push({
543
540
  type: "toolCall",
544
541
  id: event.content_block.id,
545
542
  name: isOAuth
546
543
  ? fromClaudeCodeName(event.content_block.name, currentTools)
547
544
  : event.content_block.name,
548
545
  arguments: event.content_block.input ?? {},
549
- partialJson: "",
550
- index: event.index,
551
- };
552
- output.content.push(block);
546
+ });
547
+ openBlocks.set(event.index, {
548
+ contentIndex: output.content.length - 1,
549
+ arguments: new ToolCallArgumentsStream(),
550
+ });
553
551
  stream.push({ type: "toolcall_start", contentIndex: output.content.length - 1, partial: output });
554
552
  }
555
553
  }
556
554
  else if (event.type === "content_block_delta") {
555
+ const open = openBlock(event.index);
557
556
  if (event.delta.type === "text_delta") {
558
- const index = blocks.findIndex((b) => b.index === event.index);
559
- const block = blocks[index];
560
- if (block && block.type === "text") {
561
- block.text += event.delta.text;
557
+ if (open?.block.type === "text") {
558
+ open.block.text += event.delta.text;
562
559
  stream.push({
563
560
  type: "text_delta",
564
- contentIndex: index,
561
+ contentIndex: open.contentIndex,
565
562
  delta: event.delta.text,
566
563
  partial: output,
567
564
  });
568
565
  }
569
566
  }
570
567
  else if (event.delta.type === "thinking_delta") {
571
- const index = blocks.findIndex((b) => b.index === event.index);
572
- const block = blocks[index];
573
- if (block && block.type === "thinking") {
574
- block.thinking += event.delta.thinking;
568
+ if (open?.block.type === "thinking") {
569
+ open.block.thinking += event.delta.thinking;
575
570
  stream.push({
576
571
  type: "thinking_delta",
577
- contentIndex: index,
572
+ contentIndex: open.contentIndex,
578
573
  delta: event.delta.thinking,
579
574
  partial: output,
580
575
  });
581
576
  }
582
577
  }
583
578
  else if (event.delta.type === "input_json_delta") {
584
- const index = blocks.findIndex((b) => b.index === event.index);
585
- const block = blocks[index];
586
- if (block && block.type === "toolCall") {
587
- block.partialJson += event.delta.partial_json;
588
- block.arguments = parseStreamingJson(block.partialJson);
579
+ if (open?.block.type === "toolCall" && open.arguments !== undefined) {
580
+ open.block.arguments = open.arguments.append(event.delta.partial_json);
589
581
  stream.push({
590
582
  type: "toolcall_delta",
591
- contentIndex: index,
583
+ contentIndex: open.contentIndex,
592
584
  delta: event.delta.partial_json,
593
585
  partial: output,
594
586
  });
595
587
  }
596
588
  }
597
589
  else if (event.delta.type === "signature_delta") {
598
- const index = blocks.findIndex((b) => b.index === event.index);
599
- const block = blocks[index];
600
- if (block && block.type === "thinking") {
601
- block.thinkingSignature = block.thinkingSignature || "";
602
- block.thinkingSignature += event.delta.signature;
590
+ if (open?.block.type === "thinking") {
591
+ open.block.thinkingSignature = open.block.thinkingSignature || "";
592
+ open.block.thinkingSignature += event.delta.signature;
603
593
  }
604
594
  }
605
595
  }
606
596
  else if (event.type === "content_block_stop") {
607
- const index = blocks.findIndex((b) => b.index === event.index);
608
- const block = blocks[index];
609
- if (block) {
610
- delete block.index;
597
+ const open = openBlock(event.index);
598
+ openBlocks.delete(event.index);
599
+ if (open) {
600
+ const { block, contentIndex: index } = open;
611
601
  if (block.type === "text") {
612
602
  stream.push({
613
603
  type: "text_end",
@@ -624,11 +614,8 @@ export const stream = (model, context, options) => {
624
614
  partial: output,
625
615
  });
626
616
  }
627
- else if (block.type === "toolCall") {
628
- Object.assign(block, parseToolCallArguments(block.partialJson));
629
- // Finalize in-place and strip the scratch buffer so replay only
630
- // carries parsed arguments.
631
- delete block.partialJson;
617
+ else if (block.type === "toolCall" && open.arguments !== undefined) {
618
+ Object.assign(block, open.arguments.finish());
632
619
  stream.push({
633
620
  type: "toolcall_end",
634
621
  contentIndex: index,
@@ -680,6 +667,11 @@ export const stream = (model, context, options) => {
680
667
  output.usage.totalTokens =
681
668
  output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;
682
669
  calculateCost(usageModel, output.usage);
670
+ if (isOpenRouterModel(model) && event.usage) {
671
+ // OpenRouter adds billing fields to streamed message_delta usage at runtime,
672
+ // although its OpenAPI schema and the Anthropic SDK do not type those fields.
673
+ applyOpenRouterReportedCost(output.usage, event.usage);
674
+ }
683
675
  }
684
676
  }
685
677
  if (options?.signal?.aborted) {
@@ -708,11 +700,6 @@ export const stream = (model, context, options) => {
708
700
  stream.end();
709
701
  }
710
702
  catch (error) {
711
- for (const block of output.content) {
712
- delete block.index;
713
- // partialJson is only a streaming scratch buffer; never persist it.
714
- delete block.partialJson;
715
- }
716
703
  output.stopReason = options?.signal?.aborted ? "aborted" : "error";
717
704
  output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
718
705
  stream.push({ type: "error", reason: output.stopReason, error: output });
@@ -901,10 +888,7 @@ function buildParams(model, context, isOAuthToken, options, disablePreferredStri
901
888
  let hasPreferredStrictTools = false;
902
889
  const convertToolDefinitions = (tools, toolCacheControl) => {
903
890
  const definitions = convertTools(tools, isOAuthToken, compat.supportsEagerToolInputStreaming, compat.supportsStrictTools, toolCacheControl, disablePreferredStrictTools);
904
- if (definitions.some((definition, index) => {
905
- const config = tools[index].constrainedSampling;
906
- return definition.strict === true && config && config.type === "json_schema" && config.strict === "prefer";
907
- })) {
891
+ if (definitions.some((definition, index) => definition.strict === true && prefersJsonSchemaStrictSampling(tools[index]))) {
908
892
  hasPreferredStrictTools = true;
909
893
  }
910
894
  return definitions;
@@ -985,7 +969,10 @@ function buildParams(model, context, isOAuthToken, options, disablePreferredStri
985
969
  display: options?.thinkingDisplay ?? "summarized",
986
970
  block_binding: { prefix_mismatch_behavior: "drop_block" },
987
971
  };
988
- params.output_config = { effort: "high" };
972
+ // Effort markers set the level for every reply. The top-level effort is part of the cached
973
+ // prompt, so it stays fixed. It is "low" because organizations can cap effort and reject
974
+ // requests containing any level above the cap.
975
+ params.output_config = { effort: "low" };
989
976
  }
990
977
  else if (model.reasoning && options?.thinkingEnabled) {
991
978
  // Default to "summarized" so Opus 4.7 and Mythos Preview behave like
@@ -1045,7 +1032,9 @@ convertToolDefinitions) {
1045
1032
  const params = [];
1046
1033
  const assistantLevels = new Map();
1047
1034
  // Later system messages are held back and emitted directly before the next assistant
1048
- // message (or at the end of the transcript). Anthropic requires `tool_result` blocks to
1035
+ // message that is sent (or at the end of the transcript). Assistant messages that convert to
1036
+ // no blocks are dropped, so holding past them keeps an update from landing before a user
1037
+ // message (#11091). Anthropic requires `tool_result` blocks to
1049
1038
  // immediately follow their `tool_use`, so a system message between them is rejected; this
1050
1039
  // also mirrors where the managed-effort system messages are inserted. As a result an
1051
1040
  // update placed before a user message in the transcript lands after it on the wire.
@@ -1118,7 +1107,6 @@ convertToolDefinitions) {
1118
1107
  }
1119
1108
  }
1120
1109
  else if (msg.role === "assistant") {
1121
- flushPendingSystemMessages();
1122
1110
  const blocks = [];
1123
1111
  for (const block of msg.content) {
1124
1112
  if (block.type === "text") {
@@ -1176,6 +1164,7 @@ convertToolDefinitions) {
1176
1164
  }
1177
1165
  if (blocks.length === 0)
1178
1166
  continue;
1167
+ flushPendingSystemMessages();
1179
1168
  const messageIndex = params.length;
1180
1169
  params.push({
1181
1170
  role: "assistant",
@@ -1299,8 +1288,7 @@ function convertTools(tools, isOAuthToken, supportsEagerToolInputStreaming, supp
1299
1288
  if (!tools)
1300
1289
  return [];
1301
1290
  return tools.map((tool, index) => {
1302
- const config = tool.constrainedSampling;
1303
- const strict = disablePreferredStrictTools && config && config.type === "json_schema" && config.strict === "prefer"
1291
+ const strict = disablePreferredStrictTools && prefersJsonSchemaStrictSampling(tool)
1304
1292
  ? undefined
1305
1293
  : resolveJsonSchemaStrictSampling(tool, supportsStrictTools, isAnthropicStrictUnsupportedKeyword);
1306
1294
  const parameters = getJsonSchemaToolParameters(tool, strict);