@opencode/ai 0.0.0-reserved → 2.0.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 (288) hide show
  1. package/README.md +879 -2
  2. package/dist/cache-policy.d.ts +2 -0
  3. package/dist/cache-policy.js +129 -0
  4. package/dist/image-client.d.ts +19 -0
  5. package/dist/image-client.js +28 -0
  6. package/dist/image.d.ts +110 -0
  7. package/dist/image.js +103 -0
  8. package/dist/index.d.ts +18 -0
  9. package/dist/index.js +12 -0
  10. package/dist/llm.d.ts +252 -0
  11. package/dist/llm.js +74 -0
  12. package/dist/protocols/alibaba-chat.d.ts +225 -0
  13. package/dist/protocols/alibaba-chat.js +75 -0
  14. package/dist/protocols/alibaba-messages.d.ts +350 -0
  15. package/dist/protocols/alibaba-messages.js +40 -0
  16. package/dist/protocols/alibaba-responses.d.ts +255 -0
  17. package/dist/protocols/alibaba-responses.js +80 -0
  18. package/dist/protocols/anthropic-messages.d.ts +980 -0
  19. package/dist/protocols/anthropic-messages.js +1460 -0
  20. package/dist/protocols/bedrock-converse.d.ts +499 -0
  21. package/dist/protocols/bedrock-converse.js +643 -0
  22. package/dist/protocols/bedrock-event-stream.d.ts +9 -0
  23. package/dist/protocols/bedrock-event-stream.js +97 -0
  24. package/dist/protocols/gemini.d.ts +285 -0
  25. package/dist/protocols/gemini.js +671 -0
  26. package/dist/protocols/google-images.d.ts +30 -0
  27. package/dist/protocols/google-images.js +181 -0
  28. package/dist/protocols/index.d.ts +12 -0
  29. package/dist/protocols/index.js +12 -0
  30. package/dist/protocols/meta-images.d.ts +26 -0
  31. package/dist/protocols/meta-images.js +89 -0
  32. package/dist/protocols/meta-messages.d.ts +356 -0
  33. package/dist/protocols/meta-messages.js +41 -0
  34. package/dist/protocols/meta-responses.d.ts +473 -0
  35. package/dist/protocols/meta-responses.js +173 -0
  36. package/dist/protocols/mistral-chat.d.ts +540 -0
  37. package/dist/protocols/mistral-chat.js +624 -0
  38. package/dist/protocols/open-responses-channel.d.ts +25 -0
  39. package/dist/protocols/open-responses-channel.js +141 -0
  40. package/dist/protocols/open-responses-continuation.d.ts +16 -0
  41. package/dist/protocols/open-responses-continuation.js +170 -0
  42. package/dist/protocols/open-responses.d.ts +1715 -0
  43. package/dist/protocols/open-responses.js +1228 -0
  44. package/dist/protocols/openai-chat.d.ts +860 -0
  45. package/dist/protocols/openai-chat.js +966 -0
  46. package/dist/protocols/openai-compatible-chat.d.ts +109 -0
  47. package/dist/protocols/openai-compatible-chat.js +19 -0
  48. package/dist/protocols/openai-compatible-responses.d.ts +172 -0
  49. package/dist/protocols/openai-compatible-responses.js +18 -0
  50. package/dist/protocols/openai-images.d.ts +32 -0
  51. package/dist/protocols/openai-images.js +183 -0
  52. package/dist/protocols/openai-responses.d.ts +1389 -0
  53. package/dist/protocols/openai-responses.js +260 -0
  54. package/dist/protocols/shared.d.ts +193 -0
  55. package/dist/protocols/shared.js +290 -0
  56. package/dist/protocols/utils/bedrock-auth.d.ts +60 -0
  57. package/dist/protocols/utils/bedrock-auth.js +85 -0
  58. package/dist/protocols/utils/bedrock-cache.d.ts +19 -0
  59. package/dist/protocols/utils/bedrock-cache.js +49 -0
  60. package/dist/protocols/utils/bedrock-media.d.ts +55 -0
  61. package/dist/protocols/utils/bedrock-media.js +73 -0
  62. package/dist/protocols/utils/cache.d.ts +6 -0
  63. package/dist/protocols/utils/cache.js +5 -0
  64. package/dist/protocols/utils/gemini-tool-schema.d.ts +2 -0
  65. package/dist/protocols/utils/gemini-tool-schema.js +103 -0
  66. package/dist/protocols/utils/image-input.d.ts +21 -0
  67. package/dist/protocols/utils/image-input.js +20 -0
  68. package/dist/protocols/utils/lifecycle.d.ts +24 -0
  69. package/dist/protocols/utils/lifecycle.js +72 -0
  70. package/dist/protocols/utils/meta-image.d.ts +2 -0
  71. package/dist/protocols/utils/meta-image.js +13 -0
  72. package/dist/protocols/utils/open-responses-options.d.ts +56 -0
  73. package/dist/protocols/utils/open-responses-options.js +61 -0
  74. package/dist/protocols/utils/openai-image.d.ts +5 -0
  75. package/dist/protocols/utils/openai-image.js +18 -0
  76. package/dist/protocols/utils/openai-options.d.ts +16 -0
  77. package/dist/protocols/utils/openai-options.js +14 -0
  78. package/dist/protocols/utils/partial-json-options.d.ts +62 -0
  79. package/dist/protocols/utils/partial-json-options.js +52 -0
  80. package/dist/protocols/utils/partial-json.d.ts +8 -0
  81. package/dist/protocols/utils/partial-json.js +279 -0
  82. package/dist/protocols/utils/responses-checkpoint.d.ts +4 -0
  83. package/dist/protocols/utils/responses-checkpoint.js +107 -0
  84. package/dist/protocols/utils/responses-compaction.d.ts +4 -0
  85. package/dist/protocols/utils/responses-compaction.js +129 -0
  86. package/dist/protocols/utils/responses-hosted-tools.d.ts +26 -0
  87. package/dist/protocols/utils/responses-hosted-tools.js +32 -0
  88. package/dist/protocols/utils/tool-schema.d.ts +8 -0
  89. package/dist/protocols/utils/tool-schema.js +54 -0
  90. package/dist/protocols/utils/tool-stream.d.ts +710 -0
  91. package/dist/protocols/utils/tool-stream.js +155 -0
  92. package/dist/protocols/xai-images.d.ts +26 -0
  93. package/dist/protocols/xai-images.js +106 -0
  94. package/dist/protocols/xai-responses.d.ts +250 -0
  95. package/dist/protocols/xai-responses.js +81 -0
  96. package/dist/protocols/zai-chat.d.ts +202 -0
  97. package/dist/protocols/zai-chat.js +49 -0
  98. package/dist/protocols/zai-images.d.ts +22 -0
  99. package/dist/protocols/zai-images.js +79 -0
  100. package/dist/protocols/zai-messages.d.ts +344 -0
  101. package/dist/protocols/zai-messages.js +27 -0
  102. package/dist/protocols.d.ts +1 -0
  103. package/dist/protocols.js +1 -0
  104. package/dist/provider-error.d.ts +15 -0
  105. package/dist/provider-error.js +125 -0
  106. package/dist/provider-package.d.ts +15 -0
  107. package/dist/provider-package.js +1 -0
  108. package/dist/provider.d.ts +23 -0
  109. package/dist/provider.js +2 -0
  110. package/dist/providers/alibaba/chat.d.ts +1 -0
  111. package/dist/providers/alibaba/chat.js +1 -0
  112. package/dist/providers/alibaba/messages.d.ts +3 -0
  113. package/dist/providers/alibaba/messages.js +1 -0
  114. package/dist/providers/alibaba/responses.d.ts +3 -0
  115. package/dist/providers/alibaba/responses.js +1 -0
  116. package/dist/providers/alibaba.d.ts +711 -0
  117. package/dist/providers/alibaba.js +94 -0
  118. package/dist/providers/amazon-bedrock/mantle/chat.d.ts +2 -0
  119. package/dist/providers/amazon-bedrock/mantle/chat.js +1 -0
  120. package/dist/providers/amazon-bedrock/mantle/responses.d.ts +2 -0
  121. package/dist/providers/amazon-bedrock/mantle/responses.js +1 -0
  122. package/dist/providers/amazon-bedrock/mantle.d.ts +2 -0
  123. package/dist/providers/amazon-bedrock/mantle.js +1 -0
  124. package/dist/providers/amazon-bedrock-mantle.d.ts +313 -0
  125. package/dist/providers/amazon-bedrock-mantle.js +79 -0
  126. package/dist/providers/amazon-bedrock.d.ts +173 -0
  127. package/dist/providers/amazon-bedrock.js +41 -0
  128. package/dist/providers/anthropic-compatible.d.ts +309 -0
  129. package/dist/providers/anthropic-compatible.js +50 -0
  130. package/dist/providers/anthropic.d.ts +307 -0
  131. package/dist/providers/anthropic.js +42 -0
  132. package/dist/providers/azure/chat.d.ts +2 -0
  133. package/dist/providers/azure/chat.js +1 -0
  134. package/dist/providers/azure/responses.d.ts +2 -0
  135. package/dist/providers/azure/responses.js +1 -0
  136. package/dist/providers/azure.d.ts +598 -0
  137. package/dist/providers/azure.js +115 -0
  138. package/dist/providers/baseten.d.ts +228 -0
  139. package/dist/providers/baseten.js +38 -0
  140. package/dist/providers/cerebras.d.ts +227 -0
  141. package/dist/providers/cerebras.js +40 -0
  142. package/dist/providers/cloudflare-ai-gateway.d.ts +237 -0
  143. package/dist/providers/cloudflare-ai-gateway.js +65 -0
  144. package/dist/providers/cloudflare-workers-ai.d.ts +231 -0
  145. package/dist/providers/cloudflare-workers-ai.js +51 -0
  146. package/dist/providers/deepinfra.d.ts +227 -0
  147. package/dist/providers/deepinfra.js +43 -0
  148. package/dist/providers/deepseek.d.ts +228 -0
  149. package/dist/providers/deepseek.js +41 -0
  150. package/dist/providers/fireworks.d.ts +228 -0
  151. package/dist/providers/fireworks.js +38 -0
  152. package/dist/providers/google-vertex/chat.d.ts +2 -0
  153. package/dist/providers/google-vertex/chat.js +1 -0
  154. package/dist/providers/google-vertex/gemini.d.ts +2 -0
  155. package/dist/providers/google-vertex/gemini.js +1 -0
  156. package/dist/providers/google-vertex/messages.d.ts +2 -0
  157. package/dist/providers/google-vertex/messages.js +1 -0
  158. package/dist/providers/google-vertex/responses.d.ts +2 -0
  159. package/dist/providers/google-vertex/responses.js +1 -0
  160. package/dist/providers/google-vertex-chat.d.ts +132 -0
  161. package/dist/providers/google-vertex-chat.js +55 -0
  162. package/dist/providers/google-vertex-messages.d.ts +306 -0
  163. package/dist/providers/google-vertex-messages.js +75 -0
  164. package/dist/providers/google-vertex-responses.d.ts +197 -0
  165. package/dist/providers/google-vertex-responses.js +56 -0
  166. package/dist/providers/google-vertex-shared.d.ts +22 -0
  167. package/dist/providers/google-vertex-shared.js +72 -0
  168. package/dist/providers/google-vertex.d.ts +119 -0
  169. package/dist/providers/google-vertex.js +98 -0
  170. package/dist/providers/google.d.ts +114 -0
  171. package/dist/providers/google.js +42 -0
  172. package/dist/providers/groq.d.ts +314 -0
  173. package/dist/providers/groq.js +81 -0
  174. package/dist/providers/index.d.ts +31 -0
  175. package/dist/providers/index.js +31 -0
  176. package/dist/providers/meta/chat.d.ts +2 -0
  177. package/dist/providers/meta/chat.js +1 -0
  178. package/dist/providers/meta/messages.d.ts +2 -0
  179. package/dist/providers/meta/messages.js +1 -0
  180. package/dist/providers/meta/responses.d.ts +2 -0
  181. package/dist/providers/meta/responses.js +1 -0
  182. package/dist/providers/meta.d.ts +636 -0
  183. package/dist/providers/meta.js +113 -0
  184. package/dist/providers/minimax/chat.d.ts +4 -0
  185. package/dist/providers/minimax/chat.js +8 -0
  186. package/dist/providers/minimax/messages.d.ts +1 -0
  187. package/dist/providers/minimax/messages.js +1 -0
  188. package/dist/providers/minimax/responses.d.ts +4 -0
  189. package/dist/providers/minimax/responses.js +8 -0
  190. package/dist/providers/minimax.d.ts +598 -0
  191. package/dist/providers/minimax.js +89 -0
  192. package/dist/providers/mistral.d.ts +207 -0
  193. package/dist/providers/mistral.js +28 -0
  194. package/dist/providers/moonshot/chat.d.ts +1 -0
  195. package/dist/providers/moonshot/chat.js +1 -0
  196. package/dist/providers/moonshot/messages.d.ts +4 -0
  197. package/dist/providers/moonshot/messages.js +8 -0
  198. package/dist/providers/moonshot/responses.d.ts +4 -0
  199. package/dist/providers/moonshot/responses.js +8 -0
  200. package/dist/providers/moonshot.d.ts +596 -0
  201. package/dist/providers/moonshot.js +90 -0
  202. package/dist/providers/open-responses-options.d.ts +6 -0
  203. package/dist/providers/open-responses-options.js +1 -0
  204. package/dist/providers/openai/chat.d.ts +2 -0
  205. package/dist/providers/openai/chat.js +1 -0
  206. package/dist/providers/openai/responses.d.ts +2 -0
  207. package/dist/providers/openai/responses.js +1 -0
  208. package/dist/providers/openai-compatible/responses.d.ts +1 -0
  209. package/dist/providers/openai-compatible/responses.js +1 -0
  210. package/dist/providers/openai-compatible-responses.d.ts +195 -0
  211. package/dist/providers/openai-compatible-responses.js +32 -0
  212. package/dist/providers/openai-compatible.d.ts +130 -0
  213. package/dist/providers/openai-compatible.js +33 -0
  214. package/dist/providers/openai-options.d.ts +26 -0
  215. package/dist/providers/openai-options.js +41 -0
  216. package/dist/providers/openai.d.ts +404 -0
  217. package/dist/providers/openai.js +82 -0
  218. package/dist/providers/openrouter.d.ts +563 -0
  219. package/dist/providers/openrouter.js +114 -0
  220. package/dist/providers/togetherai.d.ts +227 -0
  221. package/dist/providers/togetherai.js +40 -0
  222. package/dist/providers/xai.d.ts +163 -0
  223. package/dist/providers/xai.js +88 -0
  224. package/dist/providers/zai/chat.d.ts +1 -0
  225. package/dist/providers/zai/chat.js +1 -0
  226. package/dist/providers/zai-coding-plan/chat.d.ts +1 -0
  227. package/dist/providers/zai-coding-plan/chat.js +1 -0
  228. package/dist/providers/zai-coding-plan/messages.d.ts +4 -0
  229. package/dist/providers/zai-coding-plan/messages.js +8 -0
  230. package/dist/providers/zai-coding-plan/responses.d.ts +4 -0
  231. package/dist/providers/zai-coding-plan/responses.js +8 -0
  232. package/dist/providers/zai-coding-plan.d.ts +577 -0
  233. package/dist/providers/zai-coding-plan.js +63 -0
  234. package/dist/providers/zai.d.ts +149 -0
  235. package/dist/providers/zai.js +53 -0
  236. package/dist/providers.d.ts +1 -0
  237. package/dist/providers.js +1 -0
  238. package/dist/route/auth-options.d.ts +34 -0
  239. package/dist/route/auth-options.js +14 -0
  240. package/dist/route/auth.d.ts +51 -0
  241. package/dist/route/auth.js +87 -0
  242. package/dist/route/client.d.ts +430 -0
  243. package/dist/route/client.js +307 -0
  244. package/dist/route/endpoint.d.ts +28 -0
  245. package/dist/route/endpoint.js +21 -0
  246. package/dist/route/executor.d.ts +27 -0
  247. package/dist/route/executor.js +183 -0
  248. package/dist/route/framing.d.ts +29 -0
  249. package/dist/route/framing.js +14 -0
  250. package/dist/route/index.d.ts +16 -0
  251. package/dist/route/index.js +9 -0
  252. package/dist/route/protocol.d.ts +76 -0
  253. package/dist/route/protocol.js +16 -0
  254. package/dist/route/transport/http.d.ts +34 -0
  255. package/dist/route/transport/http.js +68 -0
  256. package/dist/route/transport/index.d.ts +41 -0
  257. package/dist/route/transport/index.js +4 -0
  258. package/dist/route/transport/websocket-channel.d.ts +57 -0
  259. package/dist/route/transport/websocket-channel.js +1 -0
  260. package/dist/route/transport/websocket.d.ts +56 -0
  261. package/dist/route/transport/websocket.js +370 -0
  262. package/dist/route.d.ts +1 -0
  263. package/dist/route.js +1 -0
  264. package/dist/schema/errors.d.ts +170 -0
  265. package/dist/schema/errors.js +131 -0
  266. package/dist/schema/events.d.ts +8123 -0
  267. package/dist/schema/events.js +550 -0
  268. package/dist/schema/ids.d.ts +17 -0
  269. package/dist/schema/ids.js +10 -0
  270. package/dist/schema/index.d.ts +5 -0
  271. package/dist/schema/index.js +5 -0
  272. package/dist/schema/messages.d.ts +646 -0
  273. package/dist/schema/messages.js +301 -0
  274. package/dist/schema/options.d.ts +159 -0
  275. package/dist/schema/options.js +189 -0
  276. package/dist/testing.d.ts +1693 -0
  277. package/dist/testing.js +129 -0
  278. package/dist/tool-history.d.ts +2 -0
  279. package/dist/tool-history.js +84 -0
  280. package/dist/tool-runtime.d.ts +15 -0
  281. package/dist/tool-runtime.js +62 -0
  282. package/dist/tool.d.ts +136 -0
  283. package/dist/tool.js +66 -0
  284. package/dist/utils/record.d.ts +2 -0
  285. package/dist/utils/record.js +2 -0
  286. package/dist/utils/sanitize.d.ts +1 -0
  287. package/dist/utils/sanitize.js +12 -0
  288. package/package.json +43 -10
package/README.md CHANGED
@@ -1,5 +1,882 @@
1
1
  # @opencode/ai
2
2
 
3
- Bootstrap placeholder for the official [OpenCode](https://opencode.ai) release pipeline.
3
+ Schema-first language model and image-generation APIs built with Effect.
4
4
 
5
- This version contains no implementation or executable. Do not use it as a functional release.
5
+ ```ts
6
+ import { Effect, Layer } from "effect"
7
+ import { LLM, LLMClient } from "@opencode/ai"
8
+ import { RequestExecutor } from "@opencode/ai/route"
9
+ import { OpenAI } from "@opencode/ai/providers"
10
+
11
+ const model = OpenAI.configure({ apiKey: process.env.OPENAI_API_KEY }).responses("gpt-4o-mini")
12
+
13
+ const request = LLM.request({
14
+ model,
15
+ system: "You are concise.",
16
+ prompt: "Say hello in one short sentence.",
17
+ generation: { maxTokens: 40 },
18
+ })
19
+
20
+ const program = Effect.gen(function* () {
21
+ const response = yield* LLMClient.generate(request)
22
+ console.log(response.text)
23
+ })
24
+
25
+ const llmLayer = LLMClient.layer.pipe(Layer.provide(RequestExecutor.fetchLayer))
26
+
27
+ await Effect.runPromise(program.pipe(Effect.provide(llmLayer)))
28
+ ```
29
+
30
+ Run `LLMClient.stream(request)` instead of `generate` when you want incremental `LLMEvent`s. The event stream is provider-neutral — same shape across OpenAI Chat, OpenAI Responses, Anthropic Messages, Gemini, Bedrock Converse, and any OpenAI-compatible deployment.
31
+
32
+ ## Alibaba Cloud Model Studio
33
+
34
+ `Alibaba` provides standard Model Studio inference. Configure a region explicitly, then select
35
+ Chat Completions (`.model` or `.chat`), Anthropic-compatible Messages (`.messages`), or OpenAI-compatible
36
+ Responses (`.responses`). These routes use HTTP/SSE.
37
+
38
+ ```ts
39
+ import { LLM } from "@opencode/ai"
40
+ import { Alibaba } from "@opencode/ai/providers"
41
+
42
+ const alibaba = Alibaba.configure({
43
+ region: "ap-southeast-1", // Singapore
44
+ apiKey: process.env.DASHSCOPE_API_KEY,
45
+ // workspaceID: "llm-your-workspace", // use a workspace-dedicated endpoint
46
+ })
47
+
48
+ const request = LLM.request({
49
+ model: alibaba.model("qwen3.8-max"),
50
+ prompt: "Explain this design.",
51
+ providerOptions: { reasoningEffort: "medium" },
52
+ })
53
+ ```
54
+
55
+ ### Regions and credentials
56
+
57
+ | Region | `region` | Shared host when `workspaceID` is omitted |
58
+ | ------------------- | ---------------- | ----------------------------------------- |
59
+ | Singapore | `ap-southeast-1` | `dashscope-intl.aliyuncs.com` |
60
+ | China (Beijing) | `cn-beijing` | `dashscope.aliyuncs.com` |
61
+ | China (Hong Kong) | `cn-hongkong` | `cn-hongkong.dashscope.aliyuncs.com` |
62
+ | US (Virginia) | `us-east-1` | `dashscope-us.aliyuncs.com` |
63
+ | Germany (Frankfurt) | `eu-central-1` | Supply `workspaceID` or `baseURL` |
64
+ | Japan (Tokyo) | `ap-northeast-1` | Supply `workspaceID` or `baseURL` |
65
+
66
+ With `workspaceID`, the host is `{workspaceID}.{region}.maas.aliyuncs.com`. A complete `baseURL`
67
+ overrides regional setup, including the API prefix: `/compatible-mode/v1` for Chat/Responses,
68
+ or `/apps/anthropic/v1` for Messages. The selector appends its operation path.
69
+
70
+ Keys and model availability are region-specific. Auth resolves from explicit `auth` or `apiKey`,
71
+ then `DASHSCOPE_API_KEY`, then `ALIBABA_API_KEY`.
72
+
73
+ The access region and inference scope differ: Virginia's `-us` model IDs request US-only inference;
74
+ some regions select scope through their workspace. Model IDs pass through unchanged.
75
+ Alibaba's [regional guide](https://www.alibabacloud.com/help/en/model-studio/regions) and
76
+ [base URL table](https://www.alibabacloud.com/help/en/model-studio/base-url) disagree about Virginia's
77
+ shared host; the entry above follows the base URL table. Dedicated hosts can be copied from the console.
78
+
79
+ ### Native options
80
+
81
+ - **Chat:** `reasoningEffort` → `reasoning_effort`, `enableThinking` → `enable_thinking`,
82
+ `thinkingBudget` → `thinking_budget`, and `preserveThinking` → `preserve_thinking`.
83
+ Replay complete `response.message` values to retain `reasoning_content` separately from answer text.
84
+ Qwen 3.8 defaults to preserving thinking; older models have different defaults.
85
+ Additional options include `toolStream`, `parallelToolCalls`, `repetitionPenalty`, `responseFormat`,
86
+ `enableSearch`, and native `searchOptions`. `generation.topK` lowers to `top_k`.
87
+ `clearThinking` is a hosted GLM control, and `thinking.type` is available for hosted MiniMax models.
88
+ - **Messages:** `effort` → `output_config.effort`. `thinking.type` accepts enabled/disabled with an
89
+ optional `budgetTokens` (or native `budget_tokens`). `outputConfig.format` accepts a JSON schema.
90
+ Model Studio's empty thinking signatures are accepted; supplied signatures are replayed unchanged.
91
+ - **Responses:** `reasoningEffort` → `reasoning.effort`, plus `enableThinking`, `store`,
92
+ `previousResponseId`, and `conversation`. Omitted `store` retains the API's default (`true`);
93
+ set it to `false` for client-managed history. `previousResponseId` requires a stored response.
94
+ Hosted tools are `Alibaba.webSearch()`, `Alibaba.webExtractor()`, and `Alibaba.codeInterpreter()`.
95
+ Web extraction is used together with web search. Hosted calls/results carry `providerExecuted: true`.
96
+
97
+ Omitted options preserve provider defaults. Effort values pass through unchanged and accept future
98
+ strings. Qwen 3.8 Chat rejects requests combining a thinking budget with effort.
99
+
100
+ Package entrypoints are `@opencode/ai/providers/alibaba`, `alibaba/chat`, `alibaba/messages`,
101
+ and `alibaba/responses`. Live recordings cover all three APIs in Singapore; regional URL construction
102
+ is unit-tested for all six regions.
103
+
104
+ ## Z.AI
105
+
106
+ `ZAI` uses the standard API. Chat Completions is the default language-model API;
107
+ the existing `.image(...)` selector provides image generation.
108
+
109
+ ```ts
110
+ import { LLM } from "@opencode/ai"
111
+ import { ZAI, ZAICodingPlan } from "@opencode/ai/providers"
112
+
113
+ const zai = ZAI.configure({ apiKey: process.env.ZAI_API_KEY })
114
+ const request = LLM.request({
115
+ model: zai.model("glm-5.3"), // also zai.chat("glm-5.3")
116
+ prompt: "Explain this design.",
117
+ providerOptions: {
118
+ reasoningEffort: "high",
119
+ thinking: { type: "enabled", clear_thinking: false },
120
+ },
121
+ })
122
+
123
+ const coding = ZAICodingPlan.configure({ apiKey: process.env.ZAI_API_KEY })
124
+ const messages = LLM.request({
125
+ model: coding.messages("glm-5.3"),
126
+ prompt: "Explain this design.",
127
+ providerOptions: { effort: "high" },
128
+ })
129
+ ```
130
+
131
+ The products have distinct provider identities and endpoints:
132
+
133
+ | Provider | Selector | Default base URL |
134
+ | ----------------------------------- | --------------------------- | ------------------------------------- |
135
+ | `ZAI` (`zai`) | `.model`, `.chat`, `.image` | `https://api.z.ai/api/paas/v4` |
136
+ | `ZAICodingPlan` (`zai-coding-plan`) | `.model`, `.chat` | `https://api.z.ai/api/coding/paas/v4` |
137
+ | `ZAICodingPlan` | `.messages` | `https://api.z.ai/api/anthropic/v1` |
138
+ | `ZAICodingPlan` | `.responses` | `https://api.z.ai/api/v1` |
139
+
140
+ Both read `ZAI_API_KEY` when `apiKey` is omitted and support an explicit `auth` override.
141
+ Coding Plan requires an active subscription. `baseURL` overrides the selected API's
142
+ complete base, including its version prefix. Language-model routes use HTTP/SSE.
143
+
144
+ Options retain the selected API's native semantics:
145
+
146
+ - Chat `reasoningEffort` lowers to `reasoning_effort`; Responses lowers it to `reasoning.effort`.
147
+ Messages `effort` lowers to `output_config.effort`. Omission preserves provider defaults.
148
+ - Chat `thinking` passes `type` and `clear_thinking` through unchanged. Set
149
+ `clear_thinking: false` and replay complete `response.message` values to preserve reasoning
150
+ across user messages and tool loops. The standard API defaults to clearing historical thinking;
151
+ Coding Plan documents preservation by default.
152
+ - Messages accepts `thinking: { type: "enabled" | "adaptive" | "disabled" }` without requiring
153
+ an Anthropic token budget. Coding Plan documents a disabled toggle as low-effort thinking
154
+ for GLM-5.3, with explicit effort taking precedence.
155
+ - Chat also offers `toolStream`, `doSample`, `responseFormat`, `requestID`, and `userID`.
156
+ Tool-argument streaming is enabled when tools are present on GLM-4.6/4.7/5.x;
157
+ `toolStream: false` explicitly disables it. Older model families omit the opt-in.
158
+ - Effort and thinking values remain forward-compatible strings. Their meaning is model-specific:
159
+ GLM-5.3 accepts `low`, `high`, and `max` effort and rejects disabled thinking with HTTP 400;
160
+ the direct GLM-5.2 recordings returned reasoning even with `none` and `minimal` effort,
161
+ whereas explicit `thinking.type: "disabled"` disabled it on GLM-5.2 and GLM-4.7.
162
+
163
+ Standard API recordings cover GLM-5.3 efforts and a full preserved-reasoning tool loop with
164
+ a subsequent user follow-up, GLM-5.2 efforts, older-model thinking toggles, GLM-4.5 tool calls,
165
+ GLM-5.3-Flash image input, and JSON output. Coding Plan has unit coverage for routing,
166
+ request options, and reasoning replay; successful live recordings are pending.
167
+
168
+ Package entrypoints are `@opencode/ai/providers/zai`, `zai/chat`, `zai-coding-plan`,
169
+ `zai-coding-plan/chat`, `zai-coding-plan/messages`, and `zai-coding-plan/responses`.
170
+
171
+ ## Moonshot
172
+
173
+ Moonshot defaults to Chat Completions, with Messages and Responses selectors for Kimi K3:
174
+
175
+ ```ts
176
+ import { LLM } from "@opencode/ai"
177
+ import { Moonshot } from "@opencode/ai/providers"
178
+
179
+ const moonshot = Moonshot.configure({ apiKey: process.env.MOONSHOT_API_KEY })
180
+
181
+ const request = LLM.request({
182
+ model: moonshot.model("kimi-k3"), // also moonshot.chat("kimi-k3")
183
+ prompt: "Explain the tradeoffs in this design.",
184
+ providerOptions: { reasoningEffort: "high" },
185
+ })
186
+
187
+ const messages = LLM.request({
188
+ model: moonshot.messages("kimi-k3"),
189
+ prompt: "Explain the tradeoffs in this design.",
190
+ providerOptions: { effort: "high" },
191
+ })
192
+
193
+ const responses = LLM.request({
194
+ model: moonshot.responses("kimi-k3"),
195
+ prompt: "Explain the tradeoffs in this design.",
196
+ providerOptions: { reasoningEffort: "high" },
197
+ })
198
+ ```
199
+
200
+ When `apiKey` is omitted, authentication reads `MOONSHOT_API_KEY`, then `MOONSHOTAI_API_KEY`.
201
+ Chat and Responses use `https://api.moonshot.ai/v1`; Messages uses
202
+ `https://api.moonshot.ai/anthropic/v1`. `baseURL` overrides the selected API's complete base,
203
+ including the version prefix, for regional endpoints or gateways. Each endpoint requires its own valid credentials.
204
+ All three routes use HTTP/SSE.
205
+
206
+ Reasoning options stay native to the selected API and model:
207
+
208
+ | Model/API | Provider options |
209
+ | --------------------------- | --------------------------------------------------------------------------------------- |
210
+ | K3 Chat / Responses | `reasoningEffort: "low" \| "high" \| "max"`; default is `max` |
211
+ | K3 Messages | `effort: "low" \| "high" \| "max"`; default is `max` |
212
+ | K2.6 Chat | `thinking: { type: "enabled" \| "disabled", keep?: "all" \| null }`; default is enabled |
213
+ | K2.7 Code / high-speed Chat | Omit `thinking` to use always-on, preserved reasoning |
214
+
215
+ Omitting options preserves the model's defaults. K3 uses effort rather than the K2.x `thinking`
216
+ parameter. Known effort values have autocomplete while future strings remain accepted.
217
+ For K2.6, `thinking.keep: "all"` enables preservation of reasoning across user messages.
218
+ K3 and both K2.7 Code variants always preserve reasoning. Continue with the returned
219
+ `response.message` and matching tool results so reasoning content and any Messages signatures are retained.
220
+ Leave sampling options such as `temperature` unset to use these models' fixed defaults.
221
+
222
+ The recorded suite covers all three K3 APIs, default and explicit efforts, K2.6 thinking modes,
223
+ both K2.7 Code variants, generated tool loops with a subsequent user follow-up, required/disabled
224
+ tool choice, image-byte input, and native structured output through `http.body` overlays.
225
+ K3 Chat and Messages accept required and disabled tool choice. Responses supports automatic tool
226
+ choice only; explicit `required` and `none` produce a provider `InvalidRequest` error, also covered by recordings.
227
+ The provider targets the Moonshot Open Platform; Kimi Code is a separate product and endpoint.
228
+
229
+ Package entrypoints are `@opencode/ai/providers/moonshot`, `moonshot/chat`, `moonshot/messages`,
230
+ and `moonshot/responses`; each exports `model(modelID, settings)`.
231
+
232
+ ## MiniMax
233
+
234
+ MiniMax defaults to its Messages API and reads `MINIMAX_API_KEY` when `apiKey` is omitted:
235
+
236
+ ```ts
237
+ import { Effect, Layer } from "effect"
238
+ import { LLM, LLMClient } from "@opencode/ai"
239
+ import { MiniMax } from "@opencode/ai/providers"
240
+ import { RequestExecutor } from "@opencode/ai/route"
241
+
242
+ const minimax = MiniMax.configure({ apiKey: process.env.MINIMAX_API_KEY })
243
+ const request = LLM.request({
244
+ model: minimax.model("MiniMax-M3"), // also minimax.messages("MiniMax-M3")
245
+ prompt: "What is 173 multiplied by 219?",
246
+ providerOptions: { thinking: { type: "adaptive" } },
247
+ generation: { maxTokens: 1536 },
248
+ })
249
+
250
+ const layer = LLMClient.layer.pipe(Layer.provide(RequestExecutor.fetchLayer))
251
+ const response = await Effect.runPromise(LLMClient.generate(request).pipe(Effect.provide(layer)))
252
+ console.log(response.text)
253
+ ```
254
+
255
+ Select `minimax.chat("MiniMax-M3")` or `minimax.responses("MiniMax-M3")` for MiniMax's native Chat Completions
256
+ and Responses APIs. The matching package entrypoints are `@opencode/ai/providers/minimax/messages`,
257
+ `@opencode/ai/providers/minimax/chat`, and `@opencode/ai/providers/minimax/responses`.
258
+
259
+ - **Messages:** M3 thinking defaults off. Set `thinking: { type: "adaptive" }` to enable it or
260
+ `thinking: { type: "disabled" }` to disable it.
261
+ - **Chat:** M3 thinking defaults on and uses the same `thinking` control. The provider enables `reasoning_split`
262
+ by default so reasoning is separate from answer text; `reasoningSplit: false` selects native `<think>`-tagged text.
263
+ - **Responses:** M3 reasoning defaults off. `reasoningEffort: "none"` disables it; `"minimal"`, `"low"`,
264
+ `"medium"`, and `"high"` enable reasoning without changing its depth.
265
+
266
+ M2.x models always think, even when a disabling option is supplied. For tool continuations, retain the complete
267
+ `response.message` in history before adding `Message.tool(...)` results; this preserves reasoning and any signatures.
268
+
269
+ The default API bases are `https://api.minimax.io/anthropic/v1` for Messages and `https://api.minimax.io/v1` for
270
+ Chat and Responses. `configure({ baseURL })` replaces the selected API's base, including its version prefix.
271
+
272
+ ## Meta
273
+
274
+ Use Meta's direct [Model API](https://dev.meta.ai/docs/overview) with `META_API_KEY`:
275
+
276
+ ```ts
277
+ import { Meta } from "@opencode/ai/providers"
278
+
279
+ const meta = Meta.configure() // or Meta.configure({ apiKey })
280
+ const request = LLM.request({
281
+ model: meta.responses("muse-spark-1.3"), // meta.model(...) also selects Responses
282
+ prompt: "What is 173 multiplied by 219? Reply with the integer.",
283
+ providerOptions: { reasoningEffort: "low" },
284
+ generation: { maxTokens: 1024 },
285
+ })
286
+ ```
287
+
288
+ `meta.chat("muse-spark-1.3")` selects Chat Completions; `meta.messages("muse-spark-1.3")` selects
289
+ the Anthropic-compatible Messages API. All use `https://api.meta.ai/v1`. The package entrypoints
290
+ `@opencode/ai/providers/meta/responses`, `meta/chat`, and `meta/messages` expose `model(modelID, settings)`.
291
+
292
+ [Muse Spark](https://dev.meta.ai/docs/models) supports `minimal`, `low`, `medium`, `high`, and
293
+ `xhigh` reasoning effort; standard-tier 1.3 also supports `max`. Omitting effort uses the model's
294
+ default. Muse Spark always reasons and rejects `none`. The output-token budget includes private reasoning.
295
+
296
+ Responses defaults to `store: false` and `include: ["reasoning.encrypted_content"]`. Preserve
297
+ `response.message` along with matching `Message.tool(...)` results in subsequent requests to replay
298
+ reasoning through tool loops. Optional `reasoningSummary: "auto"` requests a readable summary.
299
+ For server-managed history, override `store: true, include: []` and send the response ID through
300
+ `http: { body: { previous_response_id: responseID } }` with only the new input.
301
+ Chat Completions redacts private reasoning and cannot carry it between calls.
302
+ Responses and Chat support only `toolChoice: "auto"` (the default). Messages also accepts `"none"`;
303
+ its documented forced `"any"` choice currently returns HTTP 400. Messages defaults to adaptive thinking
304
+ with `display: "omitted"`, preserving encrypted `redacted_thinking` in `response.message`. Use
305
+ `providerOptions: { effort: "low" }` for depth or `thinking: { type: "enabled", budgetTokens: 1024 }`
306
+ for budget compatibility (with `generation.maxTokens > 1024`).
307
+
308
+ Add `tools: [Meta.webSearch()]` to a Spark Responses or Messages request for hosted web search.
309
+ Responses exposes hosted results and URL citations in text-part `providerMetadata.meta.annotations`.
310
+ To include search result lists, set `include: ["reasoning.encrypted_content", "web_search_call.results"]`.
311
+ Messages exposes hosted search calls; the recorded Messages API stream does not supply structured
312
+ citations or separate result blocks. Retain `response.message` for either API's continuation.
313
+
314
+ Use `Image.generate` for one-off generation or editing:
315
+
316
+ ```ts
317
+ import { Image, ImageInput } from "@opencode/ai"
318
+
319
+ const generation = Image.generate({
320
+ model: meta.image("muse-image-1.0"),
321
+ prompt: "A flat black square on a white background.",
322
+ options: { n: 1, reasoningStrength: "low" },
323
+ })
324
+
325
+ const edit = Image.generate({
326
+ model: meta.image("muse-image-1.0"),
327
+ prompt: "Make the square purple.",
328
+ images: [ImageInput.bytes(imageBytes, "image/webp")],
329
+ options: { outputFormat: "png", reasoningStrength: "low" },
330
+ })
331
+ ```
332
+
333
+ The default image format is WEBP; `outputFormat` also accepts PNG/JPEG and `responseFormat: "url"`
334
+ returns a signed URL. `size` is an aspect-ratio hint. For conversational images, select
335
+ `meta.responses("muse-image-1.0")` with `tools: [Meta.imageGeneration({ reasoningStrength: "low" })]`.
336
+ Generated images are provider-executed tool results with file content. Retain `response.message` to
337
+ replay the signed image handle on the next request. Muse Image accepts only the `image_generation` tool.
338
+
339
+ Meta Responses is explicitly HTTP/SSE-only and does not use WebSockets, even when a caller supplies
340
+ `StreamOptions.webSocket`. The public `/v1/responses` endpoint rejects WebSocket upgrades with HTTP 405 (`Allow: POST`).
341
+
342
+ ## Image generation
343
+
344
+ Use `Image.generate` with an image model for direct asset generation:
345
+
346
+ ```ts
347
+ import { Image, ImageInput } from "@opencode/ai"
348
+ import { OpenAI } from "@opencode/ai/providers"
349
+
350
+ const program = Effect.gen(function* () {
351
+ const response = yield* Image.generate({
352
+ model: OpenAI.configure({ apiKey: process.env.OPENAI_API_KEY }).image("gpt-image-2"),
353
+ prompt: "A robot tending a rooftop garden",
354
+ options: {
355
+ n: 2,
356
+ size: "1024x1024",
357
+ quality: "high", // inferred from the OpenAI image model
358
+ outputFormat: "webp",
359
+ future_option: true, // unknown native options pass through unchanged
360
+ },
361
+ })
362
+
363
+ return response.images // GeneratedImage[] with owned bytes or a provider URL
364
+ })
365
+ ```
366
+
367
+ Pass ordered image inputs to the same method for editing, composition, or image-conditioned generation:
368
+
369
+ ```ts
370
+ const response =
371
+ yield *
372
+ Image.generate({
373
+ model,
374
+ prompt: "Combine these product photos into one studio scene",
375
+ images: [
376
+ ImageInput.bytes(firstBytes, "image/png"),
377
+ ImageInput.url("https://example.com/second.webp"),
378
+ ImageInput.file("file_123"),
379
+ ],
380
+ options,
381
+ http,
382
+ })
383
+ ```
384
+
385
+ `ImageInput.fileUri(uri, mediaType)` represents provider file URIs such as Gemini Files. Raw strings are not
386
+ accepted as image inputs, avoiding ambiguity between base64, URLs, and provider IDs. Empty or omitted `images`
387
+ uses text-to-image generation; a non-empty array selects the provider's edit behavior without enforcing provider
388
+ image-count limits locally. `images` is the only common image-editing field. OpenAI uses multipart for byte/data-URL
389
+ edits and its JSON reference body for URL or file-ID edits. Its provider-specific `options.mask` accepts an
390
+ `ImageInput` for inpainting:
391
+
392
+ ```ts
393
+ yield *
394
+ Image.generate({
395
+ model: OpenAI.configure({ apiKey }).image("gpt-image-2"),
396
+ prompt,
397
+ images: [ImageInput.bytes(sourceBytes, "image/png")],
398
+ options: { mask: ImageInput.bytes(maskBytes, "image/png") },
399
+ })
400
+ ```
401
+
402
+ The OpenAI adapter extracts this helper value into the edit request's native `mask` field rather than passing the
403
+ tagged `ImageInput` object through as an ordinary option. On multipart requests, `http.body` can override option
404
+ fields but not structural `model`, `prompt`, `image[]`, or `mask` fields, and the transport owns the multipart
405
+ `Content-Type` boundary. For JSON requests, `http.body` remains the final raw-native overlay. Gemini does not fetch
406
+ public HTTP URLs, and hosted Z.ai image generation does not accept image inputs. These cases fail with
407
+ `InvalidRequest` before network I/O.
408
+
409
+ Provider-native image options belong to each request. Raw `http.body` fields have final precedence over them:
410
+
411
+ ```ts
412
+ const model = OpenAI.configure({ apiKey }).image("gpt-image-2")
413
+
414
+ yield *
415
+ Image.generate({
416
+ model,
417
+ prompt,
418
+ options: { quality: "medium" },
419
+ http,
420
+ })
421
+ ```
422
+
423
+ xAI image models use the same request API with xAI-native controls:
424
+
425
+ ```ts
426
+ yield *
427
+ Image.generate({
428
+ model: XAI.configure({ apiKey }).image("any-model-id"),
429
+ prompt,
430
+ options: {
431
+ n: 2,
432
+ aspectRatio: "16:9",
433
+ resolution: "1k",
434
+ responseFormat: "b64_json",
435
+ future_option: true,
436
+ },
437
+ http,
438
+ })
439
+ ```
440
+
441
+ Google's current Gemini image models use the same direct API:
442
+
443
+ ```ts
444
+ import { Google } from "@opencode/ai/providers"
445
+
446
+ const googleProgram = Effect.gen(function* () {
447
+ const response = yield* Image.generate({
448
+ model: Google.configure({ apiKey }).image("any-model-id"),
449
+ prompt: "A robot tending a rooftop garden",
450
+ options: {
451
+ aspectRatio: "16:9",
452
+ imageSize: "2K",
453
+ seed: 42,
454
+ thinkingLevel: "HIGH",
455
+ includeThoughts: true,
456
+ futureOption: true,
457
+ },
458
+ http,
459
+ })
460
+
461
+ return response.images
462
+ })
463
+ ```
464
+
465
+ Google image options are request-scoped and inferred from the selected model. Known fields autocomplete while
466
+ future string values and arbitrary native Gemini `generationConfig` fields remain available. Native fields override
467
+ their mapped aliases, and `http.body` is the final deep overlay. The selected model ID is sent to Gemini
468
+ `generateContent` without a local allowlist.
469
+
470
+ Z.ai image models infer open Z.ai-native options from the selected model:
471
+
472
+ ```ts
473
+ yield *
474
+ Image.generate({
475
+ model: ZAI.configure({ apiKey }).image("any-model-id"),
476
+ prompt,
477
+ options: {
478
+ quality: "hd",
479
+ userID: "user-123",
480
+ future_option: true,
481
+ },
482
+ http,
483
+ })
484
+ ```
485
+
486
+ Z.ai does not include trustworthy MIME metadata for output URLs, so generated images use
487
+ `application/octet-stream`. Output URLs expire after 30 days; download and persist them promptly if they must
488
+ remain available.
489
+
490
+ Conversational image generation remains part of the LLM interaction. OpenAI Responses exposes it through its hosted image tool:
491
+
492
+ ```ts
493
+ const program = Effect.gen(function* () {
494
+ const response = yield* LLM.generate(
495
+ LLM.request({
496
+ model: OpenAI.configure({ apiKey }).responses("gpt-5"),
497
+ prompt: "Design a solarpunk rooftop garden, then show me.",
498
+ tools: [OpenAI.imageGeneration({ quality: "high" })],
499
+ }),
500
+ )
501
+
502
+ return response.message
503
+ })
504
+ ```
505
+
506
+ The hosted result is represented as a provider-executed tool call and tool result. Its image is a `file` content item with a data URI, so retaining `response.message` preserves the generated image for continuation.
507
+
508
+ ## Public API
509
+
510
+ - **`LLM.request({...})`** — build a provider-neutral `LLMRequest`. Accepts ergonomic inputs (`system: string`, `prompt: string`) that normalize into the canonical Schema classes.
511
+ - **`LLM.generate` / `LLM.stream`** — re-exported from `LLMClient` for one-import use.
512
+ - **`Message.user(...)` / `Message.assistant(...)` / `Message.tool(...)`** — message constructors from the canonical schema model.
513
+ - **`LanguageModel.make(...)` / `ToolCallPart.make(...)` / `ToolResultPart.make(...)` / `ToolDefinition.make(...)`** — model and tool-related constructors from the canonical schema model.
514
+ - **`LLMEvent.is.*`** — typed guards (`is.textDelta`, `is.toolCall`, `is.finish`, …) for filtering streams.
515
+ - **`Image.generate({...})`** — generate images through a provider-neutral image request and response model.
516
+ - **`ImageClient`** — Effect service and layer for image execution, parallel to `LLMClient`.
517
+
518
+ ## Testing
519
+
520
+ Use the deterministic test client from `@opencode/ai/testing` to script provider-neutral responses and inspect
521
+ the requests sent by code under test:
522
+
523
+ ```ts
524
+ import { Effect } from "effect"
525
+ import { TestLLM } from "@opencode/ai/testing"
526
+
527
+ const programWithTestClient = Effect.gen(function* () {
528
+ const test = yield* TestLLM.Test
529
+ yield* test.push(TestLLM.text("Hello from the test model", "text-1"))
530
+ const result = yield* program
531
+ console.log(yield* test.requests())
532
+ return result
533
+ }).pipe(Effect.provide(TestLLM.testLayer()))
534
+ ```
535
+
536
+ `testLayer()` provides the same object under `LLMClient.Service` and `TestLLM.Test`. Production consumes the
537
+ normal client; tests use the additional controls. Each layer build has fresh state.
538
+
539
+ - `test.push(...)` queues one-shot responses in execution order. Each argument is one response.
540
+ - `test.always(response)` installs a repeatable fallback. The layer's `fallback` option sets its initial value.
541
+ - `test.serve(request => response)` installs a request-dependent fallback. `always` and `serve` replace each
542
+ other without changing queued replies; queued replies take precedence.
543
+ - `test.requests()` returns an array snapshot. `transformRequest` changes only the recorded observation;
544
+ `serve` receives the original canonical request.
545
+ - `test.wait(count)` waits for request arrivals, not output or completion, and supports concurrent waiters.
546
+ - `test.gate()` returns a scoped gate with countable `started` notifications and a `release` Effect. Release
547
+ unblocks all requests captured by that gate; closing its scope also releases it. Effect-aware test runners
548
+ already provide Scope.
549
+
550
+ Constructing `stream()` or `generate()` does not record a request, invoke a responder, or consume a script.
551
+ Each execution does. An exhausted queue without a fallback defects immediately rather than waiting for a
552
+ future reply.
553
+
554
+ Generation responses remain canonical event arrays or arbitrary `Stream<LLMEvent, AIError>` values. The client consumes
555
+ supplied streams directly, preserving failure identity, finalizers, incomplete output, and post-finish tails;
556
+ it does not repair or truncate them.
557
+
558
+ For explicit compaction, script a `CompactionResponse` through `push`, `always`, or `serve`. Its `replacement` contains the next context window, including retained user messages. The client returns that result and usage directly, with the same lazy request recording and gates. Generation and compaction reject fixtures for the wrong operation instead of converting between response shapes.
559
+
560
+ For `compact(request, { mechanism: "trigger" })`, script a `CompactionCheckpointResponse` instead. It carries `checkpoint`, `responseID`, and optional `usage`. Endpoint and trigger calls reject each other's fixtures; both share the same queue, gates, lazy recording, and fallback controls.
561
+
562
+ The published legacy `Service`, `layer`, `clientLayer`, and module-level controls remain available as adapters
563
+ over the same implementation, including the legacy live `requests` array. New tests should use `Test` and
564
+ `testLayer`.
565
+
566
+ ## Provider compaction
567
+
568
+ Compaction is opt-in. The package supports automatic compaction in OpenAI/Azure Responses and Anthropic Messages (including Claude on Vertex), and explicit compaction calls in OpenAI/Azure/xAI Responses. Model and deployment support still depends on the provider.
569
+
570
+ This is different from prompt caching, server-side history storage, or truncation. Compaction returns provider-owned context that must be replayed to continue the conversation.
571
+
572
+ ### Explicit compaction
573
+
574
+ `LLMClient.compact(request)` (equivalently, `{ mechanism: "endpoint" }`) is the caller-controlled operation for OpenAI, Azure, and xAI Responses. It performs exactly one HTTP call to `/responses/compact`, using the selected route's endpoint, credentials, query, and HTTP middleware. It returns a `CompactionResponse` with `replacement: Message[]` and optional `usage`, not a normal generation response. This mechanism does not accept a WebSocket executor.
575
+
576
+ Prefer this operation, where supported, when the application owns compaction policy and durable context updates.
577
+
578
+ ```ts
579
+ const result = yield * LLMClient.compact(request)
580
+ const next = LLMRequest.update(request, {
581
+ messages: result.replacement,
582
+ })
583
+ const response = yield * LLMClient.generate(next)
584
+ ```
585
+
586
+ `replacement` replaces the complete input window. Do not append it to the original transcript or extract only the encrypted item: the provider may retain additional messages in its output. Retained user and assistant messages remain ordinary messages with typed text, media, or reasoning parts, in their original order. Provider-specific message IDs, status, and phase use `providerMetadata`, not a raw output array hidden in an assistant message. Unsupported returned item types fail explicitly.
587
+
588
+ The selected model carries explicit-compaction capability through request construction and updates. Calls using unsupported routes fail type checking. When the model is selected dynamically, narrow the request with `LLMClient.canCompact(request)` before calling `LLMClient.compact`; a model or route switch does not inherit the old capability. Runtime validation still rejects unsupported calls from untyped consumers. Capability describes the route's API, not whether every model or custom deployment supports the operation.
589
+
590
+ Generation-only body overlays such as `stream` and `store` are not sent to the compact endpoint. Supported compact controls such as service tier and prompt-cache settings preserve request defaults and HTTP-overlay precedence. Retained image and file detail settings survive serialization and replay.
591
+
592
+ The input must still fit the model's context window. Explicit compaction is not an overflow-recovery operation. Anthropic does not expose this operation in this package; its in-band compaction remains available below. Compatible routes do not inherit an explicit compact endpoint simply because they use a Responses protocol.
593
+
594
+ ### Streamed checkpoint compaction
595
+
596
+ OpenAI Responses also exposes a separate, explicitly selected mechanism:
597
+
598
+ ```ts
599
+ const result =
600
+ yield *
601
+ LLMClient.compact(request, {
602
+ mechanism: "trigger",
603
+ webSocket, // Optional: without it, the request uses HTTP/SSE.
604
+ })
605
+
606
+ result.checkpoint // Successful encrypted CompactionPart.
607
+ result.responseID
608
+ result.usage
609
+ ```
610
+
611
+ This appends a native `compaction_trigger` control item to the full input and sends a normal Responses request, with tools and instructions retained, `stream: true`, `store: false`, and parallel tool calls enabled. It removes normal-answer text/output-format controls, forced tool choices, output-token/tool-call limits, and automatic `context_management`. Body overlays cannot replace `input` or supply `previous_response_id`/`conversation`; the complete canonical history is required for safe stateless replay. Request metadata, auth, headers, query parameters, service tier, and supported prompt-cache settings are preserved.
612
+
613
+ Only a successful `response.completed` with a response ID and exactly one logical encrypted checkpoint succeeds. Repeated item events are correlated by ID/output slot, including ID-less checkpoints. Other output is ignored, not returned as assistant text or dispatched as tools. Failed, incomplete, malformed, and interrupted responses return errors rather than partial checkpoints.
614
+
615
+ The result is **not a replacement window**. The caller selects retained history, combines it with `result.checkpoint`, and durably installs it before continuing. The operation does not choose a retention budget, prune messages, or modify the original request.
616
+
617
+ The supplied WebSocket executor can reuse a compatible append baseline for the compaction request. On completion the protocol supplies no continuation checkpoint, clearing the old baseline so the next generation sends the newly installed window in full. Validation occurs before transport completion is acknowledged. There is no operation-level retry or fallback to `/responses/compact`; existing safe transport fallback may use SSE, with full history and no connection-local response ID.
618
+
619
+ Trigger support is separate from endpoint support. Only the OpenAI Responses route advertises it; Azure, xAI, Chat, and compatible Responses routes do not inherit it. Untyped calls still fail before sending: missing route capabilities return `UnsupportedOperation`, while unknown mechanism names and invalid inputs return `InvalidRequest`. Dynamic callers must narrow for the selected mechanism:
620
+
621
+ ```ts
622
+ if (LLMClient.canCompact(request, { mechanism: "trigger" })) {
623
+ const result = yield * LLMClient.compact(request, { mechanism: "trigger" })
624
+ }
625
+ ```
626
+
627
+ This capability describes protocol implementation, **not universal availability on OpenAI API deployments**. The host application owns subscription/deployment eligibility, OAuth, endpoint selection, and deployment-specific headers. Local protocol/socket tests do not establish live provider support.
628
+
629
+ ### Advanced: in-band compaction
630
+
631
+ `providerOptions.contextManagement` lets the provider decide when to compact during an ordinary `generate` or `stream` call. This is an advanced option for callers that own persistence and recovery: persist the complete assistant message, including its checkpoint, before continuing. Enabling the option does not provide durable checkpoint storage, interruption recovery, or model-switch policy. Keep the prior context until a successful checkpoint has been persisted.
632
+
633
+ Inside an `Effect.gen`, enable OpenAI compaction with typed provider options:
634
+
635
+ ```ts
636
+ import { LLM, LLMClient, LLMRequest, Message } from "@opencode/ai"
637
+ import { OpenAI } from "@opencode/ai/providers"
638
+
639
+ const request = LLM.request({
640
+ model: OpenAI.configure({ apiKey }).responses("gpt-5.3-codex"),
641
+ messages,
642
+ providerOptions: {
643
+ contextManagement: [{ type: "compaction", compactThreshold: 200_000 }],
644
+ },
645
+ })
646
+ const response = yield * LLMClient.generate(request)
647
+ const next = LLMRequest.update(request, {
648
+ messages: [...request.messages, response.message, Message.user("Continue")],
649
+ })
650
+ ```
651
+
652
+ `store: false` remains the default. Keep the entire `response.message`, not just `response.text`. Compaction events become ordered `CompactionPart`s alongside text and reasoning. The conversation contains everything needed to continue; there is no separate replay object or hidden provider transcript.
653
+
654
+ A compaction part has `provider` and exactly one representation: `encrypted` for Responses, or `text` for Anthropic. Responses also preserves the optional checkpoint `id`. These fields survive message serialization without becoming visible assistant text. Sending a checkpoint to another provider or an incompatible API fails rather than silently losing context.
655
+
656
+ ```ts
657
+ import { CompactionPart, ProviderID } from "@opencode/ai"
658
+
659
+ CompactionPart.make({ provider: ProviderID.make("openai"), id: "cmp_123", encrypted: "..." })
660
+ CompactionPart.make({ provider: ProviderID.make("anthropic"), text: "Summary of the conversation..." })
661
+ ```
662
+
663
+ For Anthropic, use:
664
+
665
+ ```ts
666
+ providerOptions: {
667
+ contextManagement: {
668
+ edits: [{
669
+ type: "compact_20260112",
670
+ trigger: { type: "input_tokens", value: 150_000 },
671
+ pauseAfterCompaction: true,
672
+ instructions: "Summarize the task and decisions. Do not call tools while summarizing.",
673
+ }],
674
+ },
675
+ }
676
+ ```
677
+
678
+ - The trigger is optional (provider default: 150,000 tokens), with a minimum of 50,000.
679
+ - Custom instructions replace Anthropic's default summarization instructions.
680
+ - The route adds `compact-2026-01-12` to existing beta headers, including when replaying a checkpoint without enabling new compactions.
681
+ - A pause is exposed as `response.finishReason.raw === "compaction"`. It occurs only if the threshold triggers compaction: `pauseAfterCompaction` does not mean "compact now". The caller explicitly issues the next request; the package never automatically resumes.
682
+ - Anthropic can return a compaction block with `content: null` when summarization fails. This becomes a compaction part with `text: null`, which is **not** a successful replacement for prior history. The package never prunes history automatically.
683
+ - `Usage` totals include all reported Anthropic `usage.iterations`, including compaction. `contextTokens` separately reports the final message iteration's inclusive input size, when available. A compaction-only pause does not report a post-compaction context size. Raw iteration usage remains in `providerMetadata`.
684
+
685
+ ### Recording tests
686
+
687
+ Tests cover serialized round trips, real local HTTP plus a tool loop, WebSocket recovery, provider errors, malformed blocks, and usage accounting. Live provider tests are gated by `RECORD=true` and the relevant API keys:
688
+
689
+ ```sh
690
+ # Run from packages/ai. Only records the selected new cassette group.
691
+ RECORD=true RECORDED_PREFIX=openai-compaction bun test test/provider/compaction.recorded.test.ts
692
+ RECORD=true RECORDED_PREFIX=xai-compaction bun test test/provider/compaction.recorded.test.ts
693
+ RECORD=true RECORDED_PREFIX=anthropic-compaction bun test test/provider/compaction.recorded.test.ts
694
+ ```
695
+
696
+ Provider references: [OpenAI](https://developers.openai.com/api/docs/guides/compaction), [Azure](https://learn.microsoft.com/en-us/azure/foundry/openai/how-to/responses#server-side-compaction), [Anthropic](https://platform.claude.com/docs/en/build-with-claude/compaction), [xAI](https://docs.x.ai/developers/advanced-api-usage/context-compaction).
697
+
698
+ ## Caching
699
+
700
+ Prompt caching is **on by default**. Every `LLMRequest` resolves to `cache: "auto"` unless the caller opts out with `cache: "none"`. Each protocol translates `CacheHint`s to its wire format (`cache_control` on Anthropic, `cachePoint` on Bedrock; OpenAI and Gemini do implicit caching server-side and don't need inline markers — auto is a no-op there).
701
+
702
+ ### Auto placement
703
+
704
+ `"auto"` places up to four breakpoints — the last tool definition, the first system part, the last system part when distinct, and the final message boundary. These expose successively larger reusable prefixes for tool definitions, system instructions, and the active conversation. The rolling final-message boundary advances on every request so recent conversation prefixes remain reusable during tool loops.
705
+
706
+ Tools precede every system and conversation block in the provider prefix, so tool definitions must remain byte-stable and deterministically ordered for downstream breakpoints to remain reusable.
707
+
708
+ Requests below a provider's minimum cacheable size simply do not produce a reusable cache entry.
709
+
710
+ ### Opting out
711
+
712
+ ```ts
713
+ LLM.request({
714
+ model,
715
+ system,
716
+ prompt: "one-off question",
717
+ cache: "none",
718
+ })
719
+ ```
720
+
721
+ ### Granular policy
722
+
723
+ ```ts
724
+ cache: {
725
+ tools?: boolean,
726
+ system?: boolean,
727
+ messages?: "latest-user-message" | "latest-assistant" | { tail: number },
728
+ ttlSeconds?: number, // ≥ 3600 → 1h on Anthropic/Bedrock; else 5m
729
+ }
730
+ ```
731
+
732
+ ### Manual hints
733
+
734
+ Inline `CacheHint` on any text / system / tool / tool-result part overrides automatic placement. The auto policy preserves manual hints, counts them against Anthropic and Bedrock's four-breakpoint limit, and only fills the remaining slots.
735
+
736
+ ```ts
737
+ LLM.request({
738
+ model,
739
+ system: [
740
+ { type: "text", text: "stable system prompt", cache: { type: "ephemeral" } },
741
+ ],
742
+ ...
743
+ })
744
+ ```
745
+
746
+ ### Provider behavior table
747
+
748
+ | Protocol | `cache: "auto"` |
749
+ | ----------------------- | ------------------------------------------------------------------------- |
750
+ | Anthropic Messages | emits up to 4 `cache_control` markers (4-breakpoint cap enforced) |
751
+ | Bedrock Converse | emits up to 4 `cachePoint` blocks (4-breakpoint cap enforced) |
752
+ | OpenRouter | emits up to 4 `cache_control` markers |
753
+ | OpenAI Chat / Responses | no-op (implicit caching above 1024 tokens) |
754
+ | Gemini | no-op (implicit caching on 2.5+; explicit `CachedContent` is out-of-band) |
755
+
756
+ Normalized cache usage is read back into `response.usage.cacheReadInputTokens` and `cacheWriteInputTokens` across every provider.
757
+
758
+ ## Providers
759
+
760
+ Provider facades configure endpoint/auth/deployment details first, then expose model selectors that take only a model or deployment id. The selected model carries the executable route value used at runtime.
761
+
762
+ ```ts
763
+ import { OpenAI, CloudflareAIGateway } from "@opencode/ai/providers"
764
+
765
+ const openai = OpenAI.configure({ apiKey: process.env.OPENAI_API_KEY }).responses("gpt-4o-mini")
766
+ const gateway = CloudflareAIGateway.configure({
767
+ accountId: process.env.CLOUDFLARE_ACCOUNT_ID,
768
+ gatewayApiKey: process.env.CLOUDFLARE_API_TOKEN,
769
+ }).model("workers-ai/@cf/meta/llama-3.1-8b-instruct")
770
+ ```
771
+
772
+ Included LLM providers: OpenAI, Anthropic, Google (Gemini), Google Vertex, Amazon Bedrock, Azure OpenAI, Baseten, Cerebras, Cloudflare AI Gateway, Cloudflare Workers AI, DeepInfra, DeepSeek, Fireworks, Groq, Mistral, OpenRouter, TogetherAI, and xAI. Z.ai currently exposes image generation. Generic Chat Completions, Responses, and Anthropic Messages-compatible entrypoints support custom endpoints.
773
+
774
+ Each named provider owns its module, endpoint, authentication, and route setup. Providers with the same wire format compose the shared protocol directly:
775
+
776
+ ```ts
777
+ import { DeepSeek, Fireworks } from "@opencode/ai/providers"
778
+
779
+ const deepseek = DeepSeek.configure({ apiKey }).model("deepseek-chat")
780
+ const fireworks = Fireworks.configure({ apiKey }).model("accounts/fireworks/models/my-model")
781
+ ```
782
+
783
+ The former `OpenAICompatible.baseten`, `.cerebras`, `.deepinfra`, `.deepseek`, `.fireworks`, `.groq`, and `.togetherai` presets are replaced by the top-level `Baseten`, `Cerebras`, `DeepInfra`, `DeepSeek`, `Fireworks`, `Groq`, and `TogetherAI` exports. Use `CloudflareAIGateway` and `CloudflareWorkersAI` directly; each has its own module. `OpenAICompatible` configures generic endpoints with an explicit `baseURL`.
784
+
785
+ ### Provider entrypoints
786
+
787
+ Provider modules are available through dedicated exports from `@opencode/ai`. Each LLM entrypoint exports `model(modelID, settings)`, where `settings` contains provider configuration plus common `headers` and `body` overlays.
788
+
789
+ ```ts
790
+ import { model } from "@opencode/ai/providers/openai/responses"
791
+
792
+ const selected = model("gpt-5", {
793
+ apiKey: process.env.OPENAI_API_KEY,
794
+ headers: { "x-application": "example" },
795
+ })
796
+ ```
797
+
798
+ APIs have separate entrypoints:
799
+
800
+ - `@opencode/ai/providers/openai/chat`
801
+ - `@opencode/ai/providers/openai/responses`
802
+ - `@opencode/ai/providers/openai-compatible/responses`
803
+ - `@opencode/ai/providers/anthropic-compatible`
804
+ - `@opencode/ai/providers/google-vertex/gemini`
805
+ - `@opencode/ai/providers/google-vertex/chat`
806
+ - `@opencode/ai/providers/google-vertex/responses`
807
+ - `@opencode/ai/providers/google-vertex/messages`
808
+
809
+ OpenAI Responses has one semantic route and uses HTTP by default. Advanced callers may supply a per-call WebSocket channel executor through `StreamOptions`; transport policy does not change provider settings, model identity, or route identity. The provider-neutral Open Responses implementation owns the reusable WebSocket request and event contract, while each provider opts in with its own handshake and connection policy. Azure follows the same Chat/Responses split at `providers/azure/chat` and `providers/azure/responses`. Generic OpenAI-compatible Chat remains at `providers/openai-compatible`; the Responses adapter at `providers/openai-compatible/responses` uses the provider-neutral Open Responses protocol. OpenAI Responses extends that baseline with OpenAI tools, event variants, metadata, and defaults. Generic Anthropic Messages-compatible providers use `providers/anthropic-compatible`, which the named Anthropic provider composes. Google Gemini and Amazon Bedrock expose their single native API through their existing provider paths.
810
+
811
+ Vertex Gemini, Vertex Chat, Vertex Responses, and Vertex Messages are separate API entrypoints. All accept `project`, `location`, and an optional `accessToken`; when no explicit token or auth override is supplied they lazily use Google Application Default Credentials. Vertex Gemini instead selects express mode when `apiKey` or `GOOGLE_VERTEX_API_KEY` is present. Vertex Chat targets MaaS models through the OpenAI-compatible Chat Completions endpoint, while Vertex Responses targets Grok models and defaults `store` to `false` as required by Vertex. `providers/google-vertex` remains the default alias for `providers/google-vertex/gemini`.
812
+
813
+ Tuned Vertex Gemini deployments use model ids shaped like `endpoints/1234567890` and require OAuth or ADC; Vertex express-mode API keys support publisher models only.
814
+
815
+ ```ts
816
+ import { model } from "@opencode/ai/providers/google-vertex/gemini"
817
+
818
+ model("gemini-3.5-flash", { project: "my-project", location: "global" })
819
+ ```
820
+
821
+ ```ts
822
+ import { model } from "@opencode/ai/providers/google-vertex/chat"
823
+
824
+ model("deepseek-ai/deepseek-v3.2-maas", { project: "my-project", location: "global" })
825
+ ```
826
+
827
+ ```ts
828
+ import { model } from "@opencode/ai/providers/google-vertex/responses"
829
+
830
+ model("xai/grok-4.20-reasoning", { project: "my-project", location: "global" })
831
+ ```
832
+
833
+ ```ts
834
+ import { model } from "@opencode/ai/providers/google-vertex/messages"
835
+
836
+ model("claude-sonnet-4-6", { project: "my-project", location: "global" })
837
+ ```
838
+
839
+ Additional provider entrypoints include:
840
+
841
+ - `@opencode/ai/providers/baseten`
842
+ - `@opencode/ai/providers/deepseek`
843
+ - `@opencode/ai/providers/fireworks`
844
+ - `@opencode/ai/providers/cloudflare-ai-gateway`
845
+ - `@opencode/ai/providers/cloudflare-workers-ai`
846
+
847
+ ## Provider options & HTTP overlays
848
+
849
+ Request options in order of stability:
850
+
851
+ 1. **`generation`** — portable knobs (`maxTokens`, `temperature`, `topP`, `topK`, penalties, seed, stop).
852
+ 2. **`promptCacheKey`** — stable cache affinity lowered by every protocol that supports it.
853
+ 3. **`providerOptions: { ... }`** — flat options inferred from the selected model (OpenAI `store`, Anthropic `thinking`, Gemini `thinkingConfig`, OpenRouter routing).
854
+ 4. **`http: { body, headers, query }`** — last-resort serializable overlays merged into the final HTTP request. Reach for this only when a stable typed path doesn't yet exist.
855
+
856
+ Route/provider defaults are overridden by request-level values for each axis.
857
+
858
+ The selected model supplies the provider-specific option type, so per-request overrides stay flat while the canonical runtime request remains provider-neutral:
859
+
860
+ ```ts
861
+ LLM.request({
862
+ model,
863
+ prompt,
864
+ providerOptions: {
865
+ reasoningEffort: "low",
866
+ },
867
+ })
868
+ ```
869
+
870
+ ## Routes
871
+
872
+ Compose a route with `Route.make({ protocol, endpoint, auth, framing, ... })`. The route owns endpoint/auth/framing and the protocol owns body construction plus stream parsing. Transports receive the route's endpoint and auth when preparing requests. Unsupported request shapes fail during protocol lowering.
873
+
874
+ ## Effect
875
+
876
+ This package is built on Effect. Public methods return `Effect` or `Stream`; provide `LLMClient.layer` for LLM dispatch and `ImageClient.layer` for image dispatch, then import the provider/protocol modules for the routes you use. The example at `example/tutorial.ts` is a runnable walkthrough.
877
+
878
+ ## See also
879
+
880
+ - `AGENTS.md` — architecture, route construction, contributor guide
881
+ - `example/tutorial.ts` — runnable end-to-end walkthrough
882
+ - `test/provider/*.test.ts` — fixture-first protocol tests; `*.recorded.test.ts` files cover live cassettes