@code-yeongyu/senpi-ai 2026.9.30 → 2026.10.1

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 (274) hide show
  1. package/README.md +189 -19
  2. package/dist/api/anthropic-messages.js +168 -32
  3. package/dist/api/azure-openai-responses.js +23 -10
  4. package/dist/api/bedrock-converse-stream.js +12 -4
  5. package/dist/api/cloudflare-workers-ai-system-one.d.ts +4 -0
  6. package/dist/api/cloudflare-workers-ai-system-one.js +43 -0
  7. package/dist/api/cloudflare-workers-ai-system-one.lazy.d.ts +3 -0
  8. package/dist/api/cloudflare-workers-ai-system-one.lazy.js +4 -0
  9. package/dist/api/cloudflare.d.ts +2 -0
  10. package/dist/api/cloudflare.js +2 -0
  11. package/dist/api/context-room.d.ts +2 -2
  12. package/dist/api/cursor-agent.js +19 -10
  13. package/dist/api/devin-agent/request.d.ts +9 -9
  14. package/dist/api/devin-agent/request.js +14 -10
  15. package/dist/api/google-generative-ai.js +20 -80
  16. package/dist/api/google-shared.d.ts +13 -4
  17. package/dist/api/google-shared.js +54 -4
  18. package/dist/api/google-vertex.js +19 -62
  19. package/dist/api/llama-cpp-classify.d.ts +33 -0
  20. package/dist/api/llama-cpp-classify.js +365 -0
  21. package/dist/api/llama-cpp-classify.lazy.d.ts +3 -0
  22. package/dist/api/llama-cpp-classify.lazy.js +4 -0
  23. package/dist/api/mistral-conversations.d.ts +1 -1
  24. package/dist/api/mistral-conversations.js +36 -29
  25. package/dist/api/openai-codex-responses.d.ts +1 -1
  26. package/dist/api/openai-codex-responses.js +72 -37
  27. package/dist/api/openai-completions.d.ts +3 -2
  28. package/dist/api/openai-completions.js +76 -50
  29. package/dist/api/openai-images-params.d.ts +2 -2
  30. package/dist/api/openai-images.d.ts +1 -1
  31. package/dist/api/openai-responses-shared.d.ts +31 -8
  32. package/dist/api/openai-responses-shared.js +115 -27
  33. package/dist/api/openai-responses.d.ts +1 -1
  34. package/dist/api/openai-responses.js +38 -33
  35. package/dist/api/openrouter-images.d.ts +2 -1
  36. package/dist/api/openrouter-images.js +1 -0
  37. package/dist/api/pi-messages.d.ts +3 -3
  38. package/dist/api/pi-messages.js +3 -2
  39. package/dist/api/simple-options.d.ts +2 -2
  40. package/dist/api/simple-options.js +1 -0
  41. package/dist/api/system-one-shared.d.ts +23 -0
  42. package/dist/api/system-one-shared.js +183 -0
  43. package/dist/api/transform-messages.js +5 -2
  44. package/dist/api/typesafe-system-one.d.ts +4 -0
  45. package/dist/api/typesafe-system-one.js +19 -0
  46. package/dist/api/typesafe-system-one.lazy.d.ts +3 -0
  47. package/dist/api/typesafe-system-one.lazy.js +4 -0
  48. package/dist/api-registry.d.ts +3 -3
  49. package/dist/auth/helpers.js +1 -1
  50. package/dist/auth/oauth/anthropic-callback-listener.js +1 -1
  51. package/dist/auth/oauth/callback-server.d.ts +55 -0
  52. package/dist/auth/oauth/callback-server.js +146 -0
  53. package/dist/auth/oauth/chatgpt-subscription.d.ts +1 -1
  54. package/dist/auth/oauth/chatgpt-subscription.js +20 -124
  55. package/dist/auth/oauth/devin-callback.js +1 -1
  56. package/dist/auth/oauth/load.d.ts +4 -0
  57. package/dist/auth/oauth/load.js +10 -0
  58. package/dist/auth/oauth/meta.d.ts +17 -0
  59. package/dist/auth/oauth/meta.js +190 -0
  60. package/dist/auth/oauth/openai-chatgpt.d.ts +9 -0
  61. package/dist/auth/oauth/openai-chatgpt.js +266 -0
  62. package/dist/auth/oauth/openrouter.d.ts +1 -1
  63. package/dist/auth/oauth/openrouter.js +19 -138
  64. package/dist/auth/oauth/radius.d.ts +1 -1
  65. package/dist/auth/oauth/radius.js +21 -89
  66. package/dist/auth/resolve.d.ts +3 -8
  67. package/dist/auth/resolve.js +3 -18
  68. package/dist/auth/types.d.ts +10 -1
  69. package/dist/bun-oauth.js +4 -0
  70. package/dist/cli.js +3 -1
  71. package/dist/compat.js +17 -14
  72. package/dist/env-api-keys.js +2 -0
  73. package/dist/image-models.d.ts +19 -8
  74. package/dist/image-models.js +14 -13
  75. package/dist/images-api-registry.d.ts +8 -8
  76. package/dist/images.d.ts +7 -2
  77. package/dist/images.js +5 -0
  78. package/dist/index.d.ts +2 -2
  79. package/dist/index.js +2 -2
  80. package/dist/model-catalog.d.ts +27 -9
  81. package/dist/model-catalog.js +24 -2
  82. package/dist/model.d.ts +13 -13
  83. package/dist/models-store.d.ts +3 -2
  84. package/dist/models.d.ts +106 -36
  85. package/dist/models.generated.d.ts +142 -42
  86. package/dist/models.generated.js +142 -42
  87. package/dist/models.js +174 -36
  88. package/dist/providers/alibaba-token-plan.models.d.ts +4 -2
  89. package/dist/providers/alibaba-token-plan.models.js +4 -2
  90. package/dist/providers/all.d.ts +72 -19
  91. package/dist/providers/all.js +28 -22
  92. package/dist/providers/amazon-bedrock.models.d.ts +4 -2
  93. package/dist/providers/amazon-bedrock.models.js +4 -2
  94. package/dist/providers/ant-ling.models.d.ts +4 -2
  95. package/dist/providers/ant-ling.models.js +4 -2
  96. package/dist/providers/anthropic.models.d.ts +4 -2
  97. package/dist/providers/anthropic.models.js +4 -2
  98. package/dist/providers/azure-openai-responses.models.d.ts +4 -2
  99. package/dist/providers/azure-openai-responses.models.js +4 -2
  100. package/dist/providers/bai.models.d.ts +4 -2
  101. package/dist/providers/bai.models.js +4 -2
  102. package/dist/providers/baseten.models.d.ts +4 -2
  103. package/dist/providers/baseten.models.js +4 -2
  104. package/dist/providers/cerebras.models.d.ts +4 -2
  105. package/dist/providers/cerebras.models.js +4 -2
  106. package/dist/providers/chatgpt-subscription.models.d.ts +4 -2
  107. package/dist/providers/chatgpt-subscription.models.js +4 -2
  108. package/dist/providers/cloudflare-ai-gateway.models.d.ts +4 -2
  109. package/dist/providers/cloudflare-ai-gateway.models.js +4 -2
  110. package/dist/providers/cloudflare-stream.d.ts +6 -2
  111. package/dist/providers/cloudflare-stream.js +6 -0
  112. package/dist/providers/cloudflare-workers-ai.js +10 -3
  113. package/dist/providers/cloudflare-workers-ai.models.d.ts +4 -2
  114. package/dist/providers/cloudflare-workers-ai.models.js +4 -2
  115. package/dist/providers/data/.manifest.json +1 -1
  116. package/dist/providers/data/alibaba-token-plan.json +1 -1
  117. package/dist/providers/data/amazon-bedrock.json +1 -1
  118. package/dist/providers/data/ant-ling.json +1 -1
  119. package/dist/providers/data/anthropic.json +1 -1
  120. package/dist/providers/data/azure-openai-responses.json +1 -1
  121. package/dist/providers/data/bai.json +1 -1
  122. package/dist/providers/data/baseten.json +1 -1
  123. package/dist/providers/data/cerebras.json +1 -1
  124. package/dist/providers/data/chatgpt-subscription.json +1 -1
  125. package/dist/providers/data/cloudflare-ai-gateway.json +1 -1
  126. package/dist/providers/data/cloudflare-workers-ai.json +1 -1
  127. package/dist/providers/data/deepseek.json +1 -1
  128. package/dist/providers/data/fireworks.json +1 -1
  129. package/dist/providers/data/github-copilot.json +1 -1
  130. package/dist/providers/data/google-vertex.json +1 -1
  131. package/dist/providers/data/google.json +1 -1
  132. package/dist/providers/data/groq.json +1 -1
  133. package/dist/providers/data/huggingface.json +1 -1
  134. package/dist/providers/data/meta.json +1 -0
  135. package/dist/providers/data/minimax-cn.json +1 -1
  136. package/dist/providers/data/minimax.json +1 -1
  137. package/dist/providers/data/mistral.json +1 -1
  138. package/dist/providers/data/moonshotai-cn.json +1 -1
  139. package/dist/providers/data/moonshotai.json +1 -1
  140. package/dist/providers/data/nvidia.json +1 -1
  141. package/dist/providers/data/openai.json +1 -1
  142. package/dist/providers/data/opencode-go.json +1 -1
  143. package/dist/providers/data/opencode.json +1 -1
  144. package/dist/providers/data/opengateway.json +1 -1
  145. package/dist/providers/data/openrouter.json +1 -1
  146. package/dist/providers/data/qwen-token-plan-cn.json +1 -1
  147. package/dist/providers/data/qwen-token-plan-individual.json +1 -1
  148. package/dist/providers/data/qwen-token-plan.json +1 -1
  149. package/dist/providers/data/radius.json +1 -0
  150. package/dist/providers/data/together.json +1 -1
  151. package/dist/providers/data/typesafe.json +1 -0
  152. package/dist/providers/data/venice.json +1 -1
  153. package/dist/providers/data/vercel-ai-gateway.json +1 -1
  154. package/dist/providers/data/xai.json +1 -1
  155. package/dist/providers/data/xiaomi-token-plan-ams.json +1 -1
  156. package/dist/providers/data/xiaomi-token-plan-cn.json +1 -1
  157. package/dist/providers/data/xiaomi-token-plan-sgp.json +1 -1
  158. package/dist/providers/data/xiaomi.json +1 -1
  159. package/dist/providers/data/zai-coding-cn.json +1 -1
  160. package/dist/providers/data/zai.json +1 -1
  161. package/dist/providers/deepseek.models.d.ts +4 -2
  162. package/dist/providers/deepseek.models.js +4 -2
  163. package/dist/providers/faux.d.ts +7 -2
  164. package/dist/providers/faux.js +31 -22
  165. package/dist/providers/fireworks.models.d.ts +4 -2
  166. package/dist/providers/fireworks.models.js +4 -2
  167. package/dist/providers/github-copilot.models.d.ts +4 -2
  168. package/dist/providers/github-copilot.models.js +4 -2
  169. package/dist/providers/google-vertex.models.d.ts +4 -2
  170. package/dist/providers/google-vertex.models.js +4 -2
  171. package/dist/providers/google.models.d.ts +4 -2
  172. package/dist/providers/google.models.js +4 -2
  173. package/dist/providers/groq.models.d.ts +4 -2
  174. package/dist/providers/groq.models.js +4 -2
  175. package/dist/providers/huggingface.models.d.ts +4 -2
  176. package/dist/providers/huggingface.models.js +4 -2
  177. package/dist/providers/images/register-builtins.d.ts +2 -2
  178. package/dist/providers/kimi-coding.models.d.ts +54 -7
  179. package/dist/providers/kimi-coding.models.js +34 -7
  180. package/dist/providers/meta.d.ts +3 -0
  181. package/dist/providers/meta.js +24 -0
  182. package/dist/providers/meta.models.d.ts +6 -0
  183. package/dist/providers/meta.models.js +8 -0
  184. package/dist/providers/minimax-cn.models.d.ts +4 -2
  185. package/dist/providers/minimax-cn.models.js +4 -2
  186. package/dist/providers/minimax.models.d.ts +4 -2
  187. package/dist/providers/minimax.models.js +4 -2
  188. package/dist/providers/mistral.models.d.ts +4 -2
  189. package/dist/providers/mistral.models.js +4 -2
  190. package/dist/providers/moonshotai-cn.models.d.ts +4 -2
  191. package/dist/providers/moonshotai-cn.models.js +4 -2
  192. package/dist/providers/moonshotai.models.d.ts +4 -2
  193. package/dist/providers/moonshotai.models.js +4 -2
  194. package/dist/providers/nvidia.models.d.ts +4 -2
  195. package/dist/providers/nvidia.models.js +4 -2
  196. package/dist/providers/openai.js +4 -2
  197. package/dist/providers/openai.models.d.ts +4 -2
  198. package/dist/providers/openai.models.js +4 -2
  199. package/dist/providers/opencode-go.models.d.ts +4 -2
  200. package/dist/providers/opencode-go.models.js +4 -2
  201. package/dist/providers/opencode.d.ts +3 -1
  202. package/dist/providers/opencode.js +5 -2
  203. package/dist/providers/opencode.models.d.ts +4 -2
  204. package/dist/providers/opencode.models.js +4 -2
  205. package/dist/providers/opengateway.models.d.ts +4 -2
  206. package/dist/providers/opengateway.models.js +4 -2
  207. package/dist/providers/openrouter.js +11 -2
  208. package/dist/providers/openrouter.models.d.ts +4 -2
  209. package/dist/providers/openrouter.models.js +4 -2
  210. package/dist/providers/qwen-token-plan-cn.models.d.ts +4 -2
  211. package/dist/providers/qwen-token-plan-cn.models.js +4 -2
  212. package/dist/providers/qwen-token-plan-individual.models.d.ts +4 -2
  213. package/dist/providers/qwen-token-plan-individual.models.js +4 -2
  214. package/dist/providers/qwen-token-plan.models.d.ts +4 -2
  215. package/dist/providers/qwen-token-plan.models.js +4 -2
  216. package/dist/providers/radius.js +19 -5
  217. package/dist/providers/radius.models.d.ts +6 -0
  218. package/dist/providers/radius.models.js +8 -0
  219. package/dist/providers/together.models.d.ts +4 -2
  220. package/dist/providers/together.models.js +4 -2
  221. package/dist/providers/typesafe.d.ts +3 -0
  222. package/dist/providers/typesafe.js +18 -0
  223. package/dist/providers/typesafe.models.d.ts +6 -0
  224. package/dist/providers/typesafe.models.js +8 -0
  225. package/dist/providers/venice.models.d.ts +4 -2
  226. package/dist/providers/venice.models.js +4 -2
  227. package/dist/providers/vercel-ai-gateway.js +5 -2
  228. package/dist/providers/vercel-ai-gateway.models.d.ts +4 -2
  229. package/dist/providers/vercel-ai-gateway.models.js +4 -2
  230. package/dist/providers/xai.models.d.ts +4 -2
  231. package/dist/providers/xai.models.js +4 -2
  232. package/dist/providers/xiaomi-token-plan-ams.models.d.ts +4 -2
  233. package/dist/providers/xiaomi-token-plan-ams.models.js +4 -2
  234. package/dist/providers/xiaomi-token-plan-cn.models.d.ts +4 -2
  235. package/dist/providers/xiaomi-token-plan-cn.models.js +4 -2
  236. package/dist/providers/xiaomi-token-plan-sgp.models.d.ts +4 -2
  237. package/dist/providers/xiaomi-token-plan-sgp.models.js +4 -2
  238. package/dist/providers/xiaomi.models.d.ts +4 -2
  239. package/dist/providers/xiaomi.models.js +4 -2
  240. package/dist/providers/zai-coding-cn.models.d.ts +4 -2
  241. package/dist/providers/zai-coding-cn.models.js +4 -2
  242. package/dist/providers/zai.models.d.ts +4 -2
  243. package/dist/providers/zai.models.js +4 -2
  244. package/dist/tool-call-middleware/context-transformer.d.ts +8 -5
  245. package/dist/tool-call-middleware/context-transformer.js +44 -15
  246. package/dist/types.d.ts +262 -28
  247. package/dist/utils/diagnostics.d.ts +3 -2
  248. package/dist/utils/estimate.d.ts +2 -2
  249. package/dist/utils/estimate.js +22 -30
  250. package/dist/utils/headers.d.ts +1 -1
  251. package/dist/utils/headers.js +10 -8
  252. package/dist/utils/model-operations.d.ts +11 -0
  253. package/dist/utils/model-operations.js +47 -0
  254. package/dist/utils/models-error.d.ts +8 -0
  255. package/dist/utils/models-error.js +18 -0
  256. package/dist/utils/overflow.d.ts +1 -0
  257. package/dist/utils/overflow.js +11 -5
  258. package/dist/utils/prompt-cache-ttl.js +10 -3
  259. package/dist/utils/retry.js +9 -0
  260. package/dist/utils/text.d.ts +9 -1
  261. package/dist/utils/text.js +26 -0
  262. package/dist/utils/transcript.d.ts +85 -0
  263. package/dist/utils/transcript.js +205 -0
  264. package/package.json +3 -4
  265. package/dist/image-models.generated.d.ts +0 -925
  266. package/dist/image-models.generated.js +0 -927
  267. package/dist/images-models.d.ts +0 -95
  268. package/dist/images-models.js +0 -141
  269. package/dist/providers/openai-images.d.ts +0 -3
  270. package/dist/providers/openai-images.js +0 -16
  271. package/dist/providers/openrouter-images.d.ts +0 -3
  272. package/dist/providers/openrouter-images.js +0 -22
  273. /package/dist/{auth/oauth → utils}/oauth-page.d.ts +0 -0
  274. /package/dist/{auth/oauth → utils}/oauth-page.js +0 -0
package/README.md CHANGED
@@ -29,6 +29,7 @@ Unified LLM API with provider collections, automatic auth resolution, token and
29
29
  - [Compact Assistant Message Frames](#compact-assistant-message-frames)
30
30
  - [Image Input](#image-input)
31
31
  - [Image Generation](#image-generation)
32
+ - [Classification](#classification)
32
33
  - [Thinking/Reasoning](#thinkingreasoning)
33
34
  - [Unified Interface](#unified-interface-streamsimplecompletesimple)
34
35
  - [Provider-Specific Options](#provider-specific-options-streamcomplete)
@@ -38,12 +39,14 @@ Unified LLM API with provider collections, automatic auth resolution, token and
38
39
  - [Aborting Requests](#aborting-requests)
39
40
  - [Continuing After Abort](#continuing-after-abort)
40
41
  - [Debugging Provider Payloads](#debugging-provider-payloads)
42
+ - [Observing Provider Stream Events](#observing-provider-stream-events)
41
43
  - [Custom Providers](#custom-providers)
42
44
  - [createProvider()](#createprovider)
43
45
  - [Calling API Implementations Directly](#calling-api-implementations-directly)
44
46
  - [OpenAI Compatibility Settings](#openai-compatibility-settings)
45
47
  - [Faux Provider for Tests](#faux-provider-for-tests)
46
48
  - [Cross-Provider Handoffs](#cross-provider-handoffs)
49
+ - [System Messages](#system-messages)
47
50
  - [Context Serialization](#context-serialization)
48
51
  - [Browser Usage](#browser-usage)
49
52
  - [Bundling and Tree Shaking](#bundling-and-tree-shaking)
@@ -73,6 +76,8 @@ Unified LLM API with provider collections, automatic auth resolution, token and
73
76
  - **Cloudflare AI Gateway**
74
77
  - **Cloudflare Workers AI**
75
78
  - **xAI**
79
+ - **Meta** (Model API key or Muse subscription OAuth, uses the OpenAI Responses-compatible API)
80
+ - **TypeSafe** (classifier models through the System One API)
76
81
  - **OpenRouter**
77
82
  - **Vercel AI Gateway**
78
83
  - **OpenGateway** (OpenAI-compatible multi-provider gateway, `owner/model` ids)
@@ -761,20 +766,19 @@ for (const block of response.content) {
761
766
 
762
767
  ## Image Generation
763
768
 
764
- Image generation uses a separate API surface from text/chat generation, mirroring the chat-side design: an `ImagesModels` collection holds `ImagesProvider`s, reads are sync, and auth resolves through the owning provider. Image generation is a one-shot API: `generateImages()` waits for the provider response and returns the final `AssistantImages` result — do not use the chat/stream APIs for it.
769
+ Image models live in the same `Models` collection and on the same `Provider` as chat models, so one credential per provider covers both. They are typed `ImageModel` with `type: "image"` and are used through `generateImages()`, a one-shot API that waits for the provider response and returns the final `AssistantImages` result. Do not use the chat/stream APIs for them; `stream()` rejects image models.
765
770
 
766
771
  ### Basic Image Generation
767
772
 
768
773
  ```typescript
769
- import { builtinImagesModels } from '@earendil-works/pi-ai/providers/all';
774
+ import { builtinModels } from '@earendil-works/pi-ai/providers/all';
770
775
 
771
- // Every built-in image-generation provider; accepts the same options as createModels()
772
- const imagesModels = builtinImagesModels();
776
+ const models = builtinModels();
773
777
 
774
- const model = imagesModels.getModel('openrouter', 'google/gemini-2.5-flash-image')!;
778
+ const model = models.getModelOfType('image', 'openrouter', 'google/gemini-2.5-flash-image')!;
775
779
 
776
780
  // Auth resolves through the provider (OPENROUTER_API_KEY here); explicit apiKey wins
777
- const result = await imagesModels.generateImages(model, {
781
+ const result = await models.generateImages(model, {
778
782
  input: [{ type: 'text', text: 'Generate a red circle on a plain white background.' }]
779
783
  });
780
784
 
@@ -788,7 +792,33 @@ for (const block of result.output) {
788
792
  }
789
793
  ```
790
794
 
791
- Like the chat side, you can build the collection from parts: `createImagesModels({ credentials?, authContext? })`, the `openrouterImagesProvider()` factory from `@earendil-works/pi-ai/providers/openrouter-images`, and `createImagesProvider({ id, auth, models, refreshModels?, api })` for custom image providers (with `imagesModels.refresh(provider?)` for dynamic lists). Failures never reject — they return an `AssistantImages` with `stopReason: "error"`. The collection's provider-scoped `getAuth(providerId)` works exactly like the chat-side one.
795
+ `generateImages()` accepts only `ImageModel` values. If an upstream model supports both chat and image generation, the catalog contains separate entries with the same provider and ID: `getModel()` returns its chat operation and `getModelOfType('image', ...)` returns its image operation. Failures never reject; they return an `AssistantImages` with `stopReason: "error"`, including unknown providers, unconfigured auth, and providers without an image implementation.
796
+
797
+ A provider declares image support with the `images` option of [`createProvider()`](#createprovider): a map from `model.api` to an implementation with `generateImages()`. Image models go into the same `models` list as chat models. `api` becomes optional when `images` is present, so an image-only provider is just a provider without chat models:
798
+
799
+ ```typescript
800
+ import { createProvider, envApiKeyAuth } from '@earendil-works/pi-ai';
801
+
802
+ const pixels = createProvider({
803
+ id: 'pixels',
804
+ auth: { apiKey: envApiKeyAuth('Pixels API key', ['PIXELS_API_KEY']) },
805
+ models: [{
806
+ type: 'image',
807
+ id: 'flux-pro',
808
+ name: 'FLUX Pro',
809
+ api: 'pixels-images',
810
+ provider: 'pixels',
811
+ baseUrl: 'https://api.pixels.test/v1',
812
+ input: ['text'],
813
+ output: ['image'],
814
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
815
+ }],
816
+ images: {
817
+ 'pixels-images': { generateImages: async (model, context, options) => { /* ... */ } },
818
+ },
819
+ });
820
+ models.setProvider(pixels);
821
+ ```
792
822
 
793
823
  The old global API (`getImageModel()` / `getImageModels()` / `getImageProviders()` / `generateImages()`) remains available on the [compat entrypoint](#migrating-from-the-old-global-api):
794
824
 
@@ -809,7 +839,7 @@ Some models also support image input:
809
839
  import { readFileSync } from 'fs';
810
840
 
811
841
  const imageBuffer = readFileSync('input.png');
812
- const result = await imagesModels.generateImages(model, {
842
+ const result = await models.generateImages(model, {
813
843
  input: [
814
844
  { type: 'text', text: 'Create a variation of this image with a blue background.' },
815
845
  { type: 'image', data: imageBuffer.toString('base64'), mimeType: 'image/png' }
@@ -820,22 +850,111 @@ const result = await imagesModels.generateImages(model, {
820
850
  Check capabilities on the model metadata:
821
851
 
822
852
  ```typescript
823
- console.log(model.input); // ['text', 'image']
824
- console.log(model.output); // ['image'] or ['image', 'text']
853
+ console.log(model.input); // ['text'] or ['text', 'image']
854
+ console.log(model.output); // ['image'] or ['image', 'text']
825
855
  ```
826
856
 
827
857
  ### Notes and Limitations
828
858
 
829
- - Image models live in `ImagesModels` collections, chat models in `Models` collections; the two are separate surfaces.
830
- - Use `generateImages()`, not the chat/stream APIs.
859
+ - Image models and chat models share `Models` and `Provider`; list them with `getModelsOfType('image')` and run them with `generateImages()`, never the chat/stream APIs.
831
860
  - Image-generation models do not participate in tool calling.
832
861
  - Outputs are returned in `AssistantImages.output` and can include both base64-encoded `ImageContent` blocks and `TextContent` blocks.
833
862
  - Some models return only images, others return images plus text. Check `model.output`.
834
863
  - Some models accept image input, others are text-to-image only. Check `model.input`.
835
864
  - Like the streaming APIs, image generation supports options such as `apiKey`, `signal`, `headers`, `onPayload`, and `onResponse`, and results may include `stopReason`, `responseId`, and `usage`.
836
865
  - If you want a model to analyze images in a conversation or call tools, use the regular chat APIs with a model that supports image input.
837
- - At the moment, image generation is available through only one provider, OpenRouter.
866
+ - Built-in image generation is available through OpenRouter and OpenAI (`openai-images`).
867
+
868
+ ## Classification
869
+
870
+ Classifier models consume structured JSON state and answer one or more typed questions. They do not use chat or image-generation APIs. TypeSafe's Jev model is available from these built-in providers:
871
+
872
+ | Provider | Model IDs | Auth |
873
+ | --- | --- | --- |
874
+ | `typesafe` | `jev-latest` | `TYPESAFE_API_KEY` |
875
+ | `openrouter` | `typesafe/jev-1.13`, `~typesafe/jev-latest` | `OPENROUTER_API_KEY` or OpenRouter OAuth |
876
+ | `cloudflare-workers-ai` | `typesafe/jev` | `CLOUDFLARE_API_KEY` and `CLOUDFLARE_ACCOUNT_ID` |
877
+ | `vercel-ai-gateway` | `typesafe-ai/jev` | `AI_GATEWAY_API_KEY` |
878
+ | `opencode` | `jev-1.13`, `jev-1.13-free` | `OPENCODE_API_KEY` |
879
+
880
+ ```typescript
881
+ import { builtinModels } from '@earendil-works/pi-ai/providers/all';
882
+
883
+ const models = builtinModels();
884
+ const model = models.getModelOfType('classifier', 'typesafe', 'jev-latest')!;
885
+ const result = await models.classify(model, {
886
+ state: { message: 'The change works perfectly, thanks.' },
887
+ questions: {
888
+ category: {
889
+ type: 'choice',
890
+ instructions: 'Classify the message.',
891
+ criteria: {
892
+ approval: 'The user approves of the result',
893
+ correction: 'The user requests a correction'
894
+ }
895
+ },
896
+ satisfaction: {
897
+ type: 'score',
898
+ instructions: 'Score user satisfaction.',
899
+ criteria: ['dissatisfied', 'neutral', 'satisfied']
900
+ },
901
+ approved: {
902
+ type: 'bool',
903
+ instructions: 'Does the user approve?',
904
+ criteria: { true: 'Approval', false: 'No approval' }
905
+ }
906
+ }
907
+ });
908
+
909
+ console.log(result.answers);
910
+ ```
911
+
912
+ The public contract uses `bool` questions and `{ type: "bool", probability }` answers. The TypeSafe adapter translates those to and from its `noul` wire representation. Like image generation, `classify()` resolves to a result with `stopReason: "error"` instead of rejecting for provider, authentication, or response errors.
913
+
914
+ When the service reports token counts, `result.usage` carries them with their cost at the model's catalog price, the same `Usage` shape as chat messages. All System One services report token counts; a request that was answered with malformed answers keeps its usage. Local classifiers such as `llama-cpp-classify` report no usage.
915
+
916
+ `ClassifierOptions.temperature` divides the answer logits by the given value before they are normalized; values above 1 soften the distribution. APIs that cannot apply it, such as System One, ignore it.
917
+
918
+ ### Chat models on llama.cpp
919
+
920
+ The `llama-cpp-classify` API turns a chat model served by llama.cpp's `llama-server` into a classifier. Each question becomes one chat prompt: the state, every question of the request, the state again, and the question with its answers under single-token labels (letters for a choice, `Yes`/`No` for a bool, digits for a score). The prompt up to the final question is shared by all questions of a request, so the server's prompt cache evaluates the state once per request. The server returns the log-probabilities of the next token, and the answer is the softmax over the label tokens. Choices support up to 62 options and scores up to 10 levels. The model's `baseUrl` is the server URL; a trailing `/v1` is ignored. In router mode, the model ID selects the model.
921
+
922
+ ```typescript
923
+ import { createProvider } from '@earendil-works/pi-ai';
924
+ import { llamaCppClassifyApi } from '@earendil-works/pi-ai/api/llama-cpp-classify.lazy';
838
925
 
926
+ const provider = createProvider({
927
+ id: 'local-llama',
928
+ auth: { apiKey: { name: 'llama.cpp', resolve: async () => ({ auth: {} }) } },
929
+ models: [{
930
+ type: 'classifier',
931
+ id: 'qwen3-4b',
932
+ name: 'Qwen3 4B',
933
+ api: 'llama-cpp-classify',
934
+ provider: 'local-llama',
935
+ baseUrl: 'http://127.0.0.1:8080',
936
+ input: ['text'],
937
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
938
+ contextWindow: 32768
939
+ }],
940
+ classifiers: { 'llama-cpp-classify': llamaCppClassifyApi() }
941
+ });
942
+ ```
943
+
944
+ Raw label probabilities are usually overconfident; pass `temperature` above 1 to soften them.
945
+
946
+ Custom providers register classifier models and implementations by API ID:
947
+
948
+ ```typescript
949
+ createProvider({
950
+ id: 'classifier-service',
951
+ auth,
952
+ models: [model],
953
+ classifiers: {
954
+ 'classifier-api': { classify: async (model, context, options) => result }
955
+ }
956
+ });
957
+ ```
839
958
  ## Thinking/Reasoning
840
959
 
841
960
  Many models support thinking/reasoning capabilities where they can show their internal thought process. You can check if a model supports reasoning via the `reasoning` property. If you pass reasoning options to a non-reasoning model, they are silently ignored.
@@ -1044,11 +1163,29 @@ const response = await models.complete(model, context, {
1044
1163
 
1045
1164
  The callback is supported by `stream`, `complete`, `streamSimple`, and `completeSimple`.
1046
1165
 
1166
+ ### Observing Provider Stream Events
1167
+
1168
+ Use `onProviderStreamEvent` to inspect provider-specific fields that are not included in `AssistantMessage`. The callback receives the parsed event available to the adapter before it is normalized. Treat the event as read-only because mutations can affect normalization. This is not guaranteed to be the original HTTP bytes or SSE frame.
1169
+
1170
+ ```typescript
1171
+ const openRouterModel = models.getModel('openrouter', 'openrouter/auto')!;
1172
+ const response = await models.complete(openRouterModel, context, {
1173
+ headers: { "X-OpenRouter-Metadata": "enabled" },
1174
+ onProviderStreamEvent: (data) => {
1175
+ const chunk = data as Record<string, unknown>;
1176
+ if (chunk.openrouter_metadata) {
1177
+ console.log(chunk.openrouter_metadata);
1178
+ }
1179
+ },
1180
+ });
1181
+ ```
1182
+
1183
+ Callbacks are awaited in stream order, so slow callbacks delay stream consumption and thrown errors fail the request. SDK-backed adapters can expose only fields retained by their SDK.
1047
1184
  ## Custom Providers
1048
1185
 
1049
1186
  ### createProvider()
1050
1187
 
1051
- `createProvider()` builds a provider from parts: identity, auth, a model list, and an API implementation. Use it for local inference servers, proxies, or any OpenAI/Anthropic-compatible endpoint:
1188
+ `createProvider()` builds a provider from parts: identity, auth, a model list, and an API implementation (`api` for chat models, `images` for image generation, `classifiers` for classification; at least one is required). Use it for local inference servers, proxies, or any OpenAI/Anthropic-compatible endpoint:
1052
1189
 
1053
1190
  ```typescript
1054
1191
  import { createModels, createProvider, envApiKeyAuth, type Model } from '@earendil-works/pi-ai';
@@ -1188,12 +1325,13 @@ const ollamaReasoningModel: Model<'openai-completions'> = {
1188
1325
 
1189
1326
  ### Calling API Implementations Directly
1190
1327
 
1191
- The API implementations are importable on their own. Each module exports exactly `stream` and `streamSimple` with that API's full option typing. Direct calls bypass provider auth — pass `apiKey` explicitly:
1328
+ The API implementations are importable on their own. Each module exports exactly `stream` and `streamSimple` with that API's full option typing. Direct calls bypass provider auth and context normalization, so pass `apiKey` explicitly and wrap the context with `normalizeContext()`:
1192
1329
 
1193
1330
  ```typescript
1331
+ import { normalizeContext } from '@earendil-works/pi-ai';
1194
1332
  import { stream } from '@earendil-works/pi-ai/api/anthropic-messages';
1195
1333
 
1196
- const s = stream(claudeModel, context, {
1334
+ const s = stream(claudeModel, normalizeContext(context), {
1197
1335
  apiKey: process.env.ANTHROPIC_API_KEY,
1198
1336
  thinkingEnabled: true,
1199
1337
  thinkingBudgetTokens: 2048,
@@ -1392,6 +1530,38 @@ const geminiResponse = await models.complete(gemini, context);
1392
1530
 
1393
1531
  All providers can handle messages from other providers — text, tool calls and results (including images), thinking blocks (transformed to tagged text), and aborted messages with partial content. This enables flexible workflows: start with a fast model, switch to a more capable one for complex reasoning, or maintain continuity across provider outages.
1394
1532
 
1533
+ ## System Messages
1534
+
1535
+ `Context.systemPrompt` and `Context.tools` are shorthand for a leading system message. The public entry points (`Models.stream()`, `streamSimple()`, `complete()`, `completeSimple()`) accept a `Context` and call `normalizeContext()` once; everything below them, including `Provider.stream()`, `ProviderStreams`, and the API implementation modules, receives the resulting `TranscriptContext`, which only has `messages`. The transcript can also carry system messages later in the conversation to change the prompt or the tool set without rewriting the history:
1536
+
1537
+ ```typescript
1538
+ interface SystemMessage {
1539
+ role: "system";
1540
+ content: string | TextContent[]; // leading: base prompt; later: added instructions
1541
+ sections?: Record<string, string | null>; // named prompt sections; later messages patch by name, null removes
1542
+ toolsAdded?: Tool[]; // tools that become available here
1543
+ toolsRemoved?: ToolReference[]; // tools that stop being available here
1544
+ timestamp: number;
1545
+ }
1546
+ ```
1547
+
1548
+ Sections are opaque text rendered verbatim after `content`, joined by blank lines. Keep each one self-delimiting (a tag, a heading) so the model can relate an update to the original. Replaying every system message in order yields the current prompt and tools; the replay helpers take the message list:
1549
+
1550
+ ```typescript
1551
+ import { getCurrentSystemPrompt, getCurrentTools } from "@earendil-works/pi-ai";
1552
+
1553
+ const messages: Message[] = [
1554
+ { role: "system", content: "You are helpful.", sections: { rules: "<rules>Be brief.</rules>" }, toolsAdded: [readTool], timestamp: 1 },
1555
+ { role: "user", content: "hi", timestamp: 2 },
1556
+ { role: "system", content: "", sections: { rules: "<rules>Be thorough.</rules>" }, toolsRemoved: [{ name: "read" }], timestamp: 3 },
1557
+ ];
1558
+ getCurrentSystemPrompt(messages); // "You are helpful.\n\n<rules>Be thorough.</rules>"
1559
+ getCurrentTools(messages); // []
1560
+ ```
1561
+
1562
+ A custom `Provider` or `ProviderStreams` implementation reads the prompt and tools the same way from `context.messages`; `context.systemPrompt` and `context.tools` do not exist at that layer.
1563
+
1564
+ Models that accept system messages mid-conversation (`supportsMidConvoSystemMessages` in the model's compat settings, set by the generated catalog for verified models) receive each later system message in place, so the cached prefix stays intact; section changes are framed by name for the model. Every other model receives `collapseSystemMessages(transcript)`: the replayed prompt and current tools as the leading system message, with later system messages dropped. Anthropic models that also set `supportsMidConvoToolChanges` send tool changes as native `tool_addition`/`tool_removal` blocks: the initial tools stay active at the top level, every later declaration is sent with `defer_loading` (plus a stable deferred placeholder from the first request, which keeps Anthropic's deferred-tool scaffolding in the cached prefix), and removed tools stay declared, so tool changes do not invalidate the prompt cache. That needs at least one initial tool and no same-name redefinition; otherwise the current tool list is sent at the top level with the system text only. OpenAI Responses models with `supportsAdditionalTools` or `supportsToolSearch` anchor additive tool changes at their message; everything else sends the current tool list at the top level.
1395
1565
  ## Context Serialization
1396
1566
 
1397
1567
  The `Context` object can be easily serialized and deserialized using standard JSON methods, making it simple to persist conversations, implement chat history, or transfer contexts between services:
@@ -1660,17 +1830,17 @@ Adding a new LLM provider requires changes across multiple files. The layered la
1660
1830
  Create a new API implementation file (for example `bedrock-converse-stream.ts`) that exports exactly `stream` and `streamSimple`, plus:
1661
1831
 
1662
1832
  - An options interface extending `StreamOptions` (for example `BedrockOptions`)
1663
- - Message conversion functions to transform `Context` to provider format
1833
+ - Message conversion functions to transform the `TranscriptContext` messages to provider format; read the prompt and tools from the transcript with `getInitialSystemMessage()`, `getCurrentTools()`, and `resolveTranscript()`
1664
1834
  - Tool conversion if the provider supports tools
1665
1835
  - Response parsing to emit standardized events (`text`, `tool_call`, `thinking`, `usage`, `stop`)
1666
1836
 
1667
1837
  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`.
1668
1838
 
1669
- #### 3. Model Generation (`scripts/generate-models.ts`, `scripts/generate-image-models.ts`)
1839
+ #### 3. Model Generation (`scripts/generate-models.ts`)
1670
1840
 
1671
1841
  - Add logic to fetch and parse models from the provider's source (e.g., models.dev API)
1672
1842
  - Map chat/tool-capable provider model data to the standardized `Model` interface via `scripts/generate-models.ts`; regeneration emits structural `src/providers/<id>.models.ts` shards, committed generated values in `src/providers/data/`, and the aggregator; stable `src/providers/<id>.models.ts` wrappers derive exact model/API types directly from those JSON keys
1673
- - Map image-generation provider model data to the standardized `ImagesModel` interface via `scripts/generate-image-models.ts`
1843
+ - Map image-generation provider data to `ImageModel` (`type: "image"`) and classifier data to `ClassifierModel` in the same generator
1674
1844
  - Handle provider-specific quirks (pricing format, capability flags, model ID transformations)
1675
1845
 
1676
1846
  #### 4. Provider Factory (`src/providers/<id>.ts`)