@opencode-ai/ai 0.0.0-bootstrap.0 → 0.0.0-dev-17472

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 (195) hide show
  1. package/README.md +390 -1
  2. package/dist/cache-policy.d.ts +2 -0
  3. package/dist/cache-policy.js +121 -0
  4. package/dist/image-client.d.ts +19 -0
  5. package/dist/image-client.js +19 -0
  6. package/dist/image.d.ts +110 -0
  7. package/dist/image.js +102 -0
  8. package/dist/index.d.ts +18 -0
  9. package/dist/index.js +12 -0
  10. package/dist/llm.d.ts +230 -0
  11. package/dist/llm.js +77 -0
  12. package/dist/protocols/anthropic-messages.d.ts +633 -0
  13. package/dist/protocols/anthropic-messages.js +860 -0
  14. package/dist/protocols/bedrock-converse.d.ts +533 -0
  15. package/dist/protocols/bedrock-converse.js +584 -0
  16. package/dist/protocols/bedrock-event-stream.d.ts +9 -0
  17. package/dist/protocols/bedrock-event-stream.js +73 -0
  18. package/dist/protocols/gemini.d.ts +300 -0
  19. package/dist/protocols/gemini.js +512 -0
  20. package/dist/protocols/google-images.d.ts +30 -0
  21. package/dist/protocols/google-images.js +191 -0
  22. package/dist/protocols/index.d.ts +10 -0
  23. package/dist/protocols/index.js +10 -0
  24. package/dist/protocols/open-responses-channel.d.ts +26 -0
  25. package/dist/protocols/open-responses-channel.js +120 -0
  26. package/dist/protocols/open-responses.d.ts +951 -0
  27. package/dist/protocols/open-responses.js +873 -0
  28. package/dist/protocols/openai-chat.d.ts +795 -0
  29. package/dist/protocols/openai-chat.js +718 -0
  30. package/dist/protocols/openai-compatible-chat.d.ts +107 -0
  31. package/dist/protocols/openai-compatible-chat.js +20 -0
  32. package/dist/protocols/openai-compatible-responses.d.ts +96 -0
  33. package/dist/protocols/openai-compatible-responses.js +17 -0
  34. package/dist/protocols/openai-images.d.ts +32 -0
  35. package/dist/protocols/openai-images.js +188 -0
  36. package/dist/protocols/openai-responses-channel.d.ts +12 -0
  37. package/dist/protocols/openai-responses-channel.js +142 -0
  38. package/dist/protocols/openai-responses.d.ts +596 -0
  39. package/dist/protocols/openai-responses.js +208 -0
  40. package/dist/protocols/shared.d.ts +194 -0
  41. package/dist/protocols/shared.js +238 -0
  42. package/dist/protocols/utils/bedrock-auth.d.ts +21 -0
  43. package/dist/protocols/utils/bedrock-auth.js +43 -0
  44. package/dist/protocols/utils/bedrock-cache.d.ts +15 -0
  45. package/dist/protocols/utils/bedrock-cache.js +29 -0
  46. package/dist/protocols/utils/bedrock-media.d.ts +49 -0
  47. package/dist/protocols/utils/bedrock-media.js +70 -0
  48. package/dist/protocols/utils/cache.d.ts +6 -0
  49. package/dist/protocols/utils/cache.js +8 -0
  50. package/dist/protocols/utils/gemini-tool-schema.d.ts +2 -0
  51. package/dist/protocols/utils/gemini-tool-schema.js +103 -0
  52. package/dist/protocols/utils/image-input.d.ts +21 -0
  53. package/dist/protocols/utils/image-input.js +22 -0
  54. package/dist/protocols/utils/lifecycle.d.ts +20 -0
  55. package/dist/protocols/utils/lifecycle.js +68 -0
  56. package/dist/protocols/utils/open-responses-options.d.ts +21 -0
  57. package/dist/protocols/utils/open-responses-options.js +41 -0
  58. package/dist/protocols/utils/openai-image.d.ts +5 -0
  59. package/dist/protocols/utils/openai-image.js +18 -0
  60. package/dist/protocols/utils/openai-options.d.ts +14 -0
  61. package/dist/protocols/utils/openai-options.js +14 -0
  62. package/dist/protocols/utils/tool-schema.d.ts +8 -0
  63. package/dist/protocols/utils/tool-schema.js +81 -0
  64. package/dist/protocols/utils/tool-stream.d.ts +639 -0
  65. package/dist/protocols/utils/tool-stream.js +141 -0
  66. package/dist/protocols/xai-images.d.ts +26 -0
  67. package/dist/protocols/xai-images.js +111 -0
  68. package/dist/protocols/zai-images.d.ts +22 -0
  69. package/dist/protocols/zai-images.js +84 -0
  70. package/dist/protocols.d.ts +1 -0
  71. package/dist/protocols.js +1 -0
  72. package/dist/provider-error.d.ts +14 -0
  73. package/dist/provider-error.js +130 -0
  74. package/dist/provider-package.d.ts +15 -0
  75. package/dist/provider-package.js +1 -0
  76. package/dist/provider.d.ts +23 -0
  77. package/dist/provider.js +2 -0
  78. package/dist/providers/amazon-bedrock/mantle/chat.d.ts +2 -0
  79. package/dist/providers/amazon-bedrock/mantle/chat.js +1 -0
  80. package/dist/providers/amazon-bedrock/mantle/responses.d.ts +2 -0
  81. package/dist/providers/amazon-bedrock/mantle/responses.js +1 -0
  82. package/dist/providers/amazon-bedrock/mantle.d.ts +2 -0
  83. package/dist/providers/amazon-bedrock/mantle.js +1 -0
  84. package/dist/providers/amazon-bedrock-mantle.d.ts +247 -0
  85. package/dist/providers/amazon-bedrock-mantle.js +68 -0
  86. package/dist/providers/amazon-bedrock.d.ts +172 -0
  87. package/dist/providers/amazon-bedrock.js +41 -0
  88. package/dist/providers/anthropic-compatible.d.ts +218 -0
  89. package/dist/providers/anthropic-compatible.js +45 -0
  90. package/dist/providers/anthropic.d.ts +216 -0
  91. package/dist/providers/anthropic.js +40 -0
  92. package/dist/providers/azure/chat.d.ts +2 -0
  93. package/dist/providers/azure/chat.js +1 -0
  94. package/dist/providers/azure/responses.d.ts +2 -0
  95. package/dist/providers/azure/responses.js +1 -0
  96. package/dist/providers/azure.d.ts +248 -0
  97. package/dist/providers/azure.js +90 -0
  98. package/dist/providers/cloudflare.d.ts +338 -0
  99. package/dist/providers/cloudflare.js +87 -0
  100. package/dist/providers/google-vertex/chat.d.ts +2 -0
  101. package/dist/providers/google-vertex/chat.js +1 -0
  102. package/dist/providers/google-vertex/gemini.d.ts +2 -0
  103. package/dist/providers/google-vertex/gemini.js +1 -0
  104. package/dist/providers/google-vertex/messages.d.ts +2 -0
  105. package/dist/providers/google-vertex/messages.js +1 -0
  106. package/dist/providers/google-vertex/responses.d.ts +2 -0
  107. package/dist/providers/google-vertex/responses.js +1 -0
  108. package/dist/providers/google-vertex-chat.d.ts +131 -0
  109. package/dist/providers/google-vertex-chat.js +50 -0
  110. package/dist/providers/google-vertex-messages.d.ts +215 -0
  111. package/dist/providers/google-vertex-messages.js +75 -0
  112. package/dist/providers/google-vertex-responses.d.ts +122 -0
  113. package/dist/providers/google-vertex-responses.js +51 -0
  114. package/dist/providers/google-vertex-shared.d.ts +22 -0
  115. package/dist/providers/google-vertex-shared.js +61 -0
  116. package/dist/providers/google-vertex.d.ts +115 -0
  117. package/dist/providers/google-vertex.js +64 -0
  118. package/dist/providers/google.d.ts +112 -0
  119. package/dist/providers/google.js +43 -0
  120. package/dist/providers/index.d.ts +18 -0
  121. package/dist/providers/index.js +18 -0
  122. package/dist/providers/open-responses-options.d.ts +16 -0
  123. package/dist/providers/open-responses-options.js +1 -0
  124. package/dist/providers/openai/chat.d.ts +2 -0
  125. package/dist/providers/openai/chat.js +1 -0
  126. package/dist/providers/openai/responses.d.ts +2 -0
  127. package/dist/providers/openai/responses.js +1 -0
  128. package/dist/providers/openai-compatible/responses.d.ts +1 -0
  129. package/dist/providers/openai-compatible/responses.js +1 -0
  130. package/dist/providers/openai-compatible-profile.d.ts +43 -0
  131. package/dist/providers/openai-compatible-profile.js +12 -0
  132. package/dist/providers/openai-compatible-responses.d.ts +120 -0
  133. package/dist/providers/openai-compatible-responses.js +33 -0
  134. package/dist/providers/openai-compatible.d.ts +167 -0
  135. package/dist/providers/openai-compatible.js +55 -0
  136. package/dist/providers/openai-options.d.ts +21 -0
  137. package/dist/providers/openai-options.js +41 -0
  138. package/dist/providers/openai.d.ts +264 -0
  139. package/dist/providers/openai.js +83 -0
  140. package/dist/providers/openrouter.d.ts +537 -0
  141. package/dist/providers/openrouter.js +117 -0
  142. package/dist/providers/xai.d.ts +249 -0
  143. package/dist/providers/xai.js +79 -0
  144. package/dist/providers/zai.d.ts +24 -0
  145. package/dist/providers/zai.js +21 -0
  146. package/dist/providers.d.ts +1 -0
  147. package/dist/providers.js +1 -0
  148. package/dist/route/auth-options.d.ts +34 -0
  149. package/dist/route/auth-options.js +14 -0
  150. package/dist/route/auth.d.ts +51 -0
  151. package/dist/route/auth.js +89 -0
  152. package/dist/route/client.d.ts +348 -0
  153. package/dist/route/client.js +214 -0
  154. package/dist/route/endpoint.d.ts +28 -0
  155. package/dist/route/endpoint.js +21 -0
  156. package/dist/route/executor.d.ts +23 -0
  157. package/dist/route/executor.js +227 -0
  158. package/dist/route/framing.d.ts +23 -0
  159. package/dist/route/framing.js +4 -0
  160. package/dist/route/index.d.ts +16 -0
  161. package/dist/route/index.js +9 -0
  162. package/dist/route/protocol.d.ts +77 -0
  163. package/dist/route/protocol.js +17 -0
  164. package/dist/route/transport/http.d.ts +34 -0
  165. package/dist/route/transport/http.js +63 -0
  166. package/dist/route/transport/index.d.ts +39 -0
  167. package/dist/route/transport/index.js +4 -0
  168. package/dist/route/transport/websocket-channel.d.ts +56 -0
  169. package/dist/route/transport/websocket-channel.js +1 -0
  170. package/dist/route/transport/websocket.d.ts +55 -0
  171. package/dist/route/transport/websocket.js +326 -0
  172. package/dist/route.d.ts +1 -0
  173. package/dist/route.js +1 -0
  174. package/dist/schema/errors.d.ts +159 -0
  175. package/dist/schema/errors.js +155 -0
  176. package/dist/schema/events.d.ts +4587 -0
  177. package/dist/schema/events.js +492 -0
  178. package/dist/schema/ids.d.ts +30 -0
  179. package/dist/schema/ids.js +19 -0
  180. package/dist/schema/index.d.ts +5 -0
  181. package/dist/schema/index.js +5 -0
  182. package/dist/schema/messages.d.ts +402 -0
  183. package/dist/schema/messages.js +244 -0
  184. package/dist/schema/options.d.ts +159 -0
  185. package/dist/schema/options.js +202 -0
  186. package/dist/testing.d.ts +1493 -0
  187. package/dist/testing.js +88 -0
  188. package/dist/tool-runtime.d.ts +15 -0
  189. package/dist/tool-runtime.js +54 -0
  190. package/dist/tool.d.ts +135 -0
  191. package/dist/tool.js +66 -0
  192. package/dist/utils/record.d.ts +2 -0
  193. package/dist/utils/record.js +2 -0
  194. package/package.json +40 -9
  195. package/index.js +0 -1
package/README.md CHANGED
@@ -1,3 +1,392 @@
1
1
  # @opencode-ai/ai
2
2
 
3
- Bootstrap release. Use a current `next` or `latest` version for the real package.
3
+ Schema-first AI primitives for opencode. Provider quirks live in adapters, not in calling code.
4
+
5
+ ```ts
6
+ import { Effect, Layer } from "effect"
7
+ import { LLM, LLMClient } from "@opencode-ai/ai"
8
+ import { RequestExecutor } from "@opencode-ai/ai/route"
9
+ import { OpenAI } from "@opencode-ai/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
+ ## Image generation
33
+
34
+ Use `Image.generate` with an image model for direct asset generation:
35
+
36
+ ```ts
37
+ import { Image, ImageInput } from "@opencode-ai/ai"
38
+ import { OpenAI } from "@opencode-ai/ai/providers"
39
+
40
+ const program = Effect.gen(function* () {
41
+ const response = yield* Image.generate({
42
+ model: OpenAI.configure({ apiKey: process.env.OPENAI_API_KEY }).image("gpt-image-2"),
43
+ prompt: "A robot tending a rooftop garden",
44
+ options: {
45
+ n: 2,
46
+ size: "1024x1024",
47
+ quality: "high", // inferred from the OpenAI image model
48
+ outputFormat: "webp",
49
+ future_option: true, // unknown native options pass through unchanged
50
+ },
51
+ })
52
+
53
+ return response.images // GeneratedImage[] with owned bytes or a provider URL
54
+ })
55
+ ```
56
+
57
+ Pass ordered image inputs to the same method for editing, composition, or image-conditioned generation:
58
+
59
+ ```ts
60
+ const response =
61
+ yield *
62
+ Image.generate({
63
+ model,
64
+ prompt: "Combine these product photos into one studio scene",
65
+ images: [
66
+ ImageInput.bytes(firstBytes, "image/png"),
67
+ ImageInput.url("https://example.com/second.webp"),
68
+ ImageInput.file("file_123"),
69
+ ],
70
+ options,
71
+ http,
72
+ })
73
+ ```
74
+
75
+ `ImageInput.fileUri(uri, mediaType)` represents provider file URIs such as Gemini Files. Raw strings are not
76
+ accepted as image inputs, avoiding ambiguity between base64, URLs, and provider IDs. Empty or omitted `images`
77
+ uses text-to-image generation; a non-empty array selects the provider's edit behavior without enforcing provider
78
+ image-count limits locally. `images` is the only common image-editing field. OpenAI uses multipart for byte/data-URL
79
+ edits and its JSON reference body for URL or file-ID edits. Its provider-specific `options.mask` accepts an
80
+ `ImageInput` for inpainting:
81
+
82
+ ```ts
83
+ yield *
84
+ Image.generate({
85
+ model: OpenAI.configure({ apiKey }).image("gpt-image-2"),
86
+ prompt,
87
+ images: [ImageInput.bytes(sourceBytes, "image/png")],
88
+ options: { mask: ImageInput.bytes(maskBytes, "image/png") },
89
+ })
90
+ ```
91
+
92
+ The OpenAI adapter extracts this helper value into the edit request's native `mask` field rather than passing the
93
+ tagged `ImageInput` object through as an ordinary option. On multipart requests, `http.body` can override option
94
+ fields but not structural `model`, `prompt`, `image[]`, or `mask` fields, and the transport owns the multipart
95
+ `Content-Type` boundary. For JSON requests, `http.body` remains the final raw-native overlay. Gemini does not fetch
96
+ public HTTP URLs, and hosted Z.ai image generation does not accept image inputs. These cases fail with
97
+ `InvalidRequest` before network I/O.
98
+
99
+ Provider-native image options belong to each request. Raw `http.body` fields have final precedence over them:
100
+
101
+ ```ts
102
+ const model = OpenAI.configure({ apiKey }).image("gpt-image-2")
103
+
104
+ yield *
105
+ Image.generate({
106
+ model,
107
+ prompt,
108
+ options: { quality: "medium" },
109
+ http,
110
+ })
111
+ ```
112
+
113
+ xAI image models use the same request API with xAI-native controls:
114
+
115
+ ```ts
116
+ yield *
117
+ Image.generate({
118
+ model: XAI.configure({ apiKey }).image("any-model-id"),
119
+ prompt,
120
+ options: {
121
+ n: 2,
122
+ aspectRatio: "16:9",
123
+ resolution: "1k",
124
+ responseFormat: "b64_json",
125
+ future_option: true,
126
+ },
127
+ http,
128
+ })
129
+ ```
130
+
131
+ Google's current Gemini image models use the same direct API:
132
+
133
+ ```ts
134
+ import { Google } from "@opencode-ai/ai/providers"
135
+
136
+ const googleProgram = Effect.gen(function* () {
137
+ const response = yield* Image.generate({
138
+ model: Google.configure({ apiKey }).image("any-model-id"),
139
+ prompt: "A robot tending a rooftop garden",
140
+ options: {
141
+ aspectRatio: "16:9",
142
+ imageSize: "2K",
143
+ seed: 42,
144
+ thinkingLevel: "HIGH",
145
+ includeThoughts: true,
146
+ futureOption: true,
147
+ },
148
+ http,
149
+ })
150
+
151
+ return response.images
152
+ })
153
+ ```
154
+
155
+ Google image options are request-scoped and inferred from the selected model. Known fields autocomplete while
156
+ future string values and arbitrary native Gemini `generationConfig` fields remain available. Native fields override
157
+ their mapped aliases, and `http.body` is the final deep overlay. The selected model ID is sent to Gemini
158
+ `generateContent` without a local allowlist.
159
+
160
+ Z.ai image models infer open Z.ai-native options from the selected model:
161
+
162
+ ```ts
163
+ yield *
164
+ Image.generate({
165
+ model: ZAI.configure({ apiKey }).image("any-model-id"),
166
+ prompt,
167
+ options: {
168
+ quality: "hd",
169
+ userID: "user-123",
170
+ future_option: true,
171
+ },
172
+ http,
173
+ })
174
+ ```
175
+
176
+ Z.ai does not include trustworthy MIME metadata for output URLs, so generated images use
177
+ `application/octet-stream`. Output URLs expire after 30 days; download and persist them promptly if they must
178
+ remain available.
179
+
180
+ Conversational image generation remains part of the LLM interaction. OpenAI Responses exposes it through its hosted image tool:
181
+
182
+ ```ts
183
+ const program = Effect.gen(function* () {
184
+ const response = yield* LLM.generate(
185
+ LLM.request({
186
+ model: OpenAI.configure({ apiKey }).responses("gpt-5"),
187
+ prompt: "Design a solarpunk rooftop garden, then show me.",
188
+ tools: [OpenAI.imageGeneration({ quality: "high" })],
189
+ }),
190
+ )
191
+
192
+ return response.message
193
+ })
194
+ ```
195
+
196
+ 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.
197
+
198
+ ## Public API
199
+
200
+ - **`LLM.request({...})`** — build a provider-neutral `LLMRequest`. Accepts ergonomic inputs (`system: string`, `prompt: string`) that normalize into the canonical Schema classes.
201
+ - **`LLM.generate` / `LLM.stream`** — re-exported from `LLMClient` for one-import use.
202
+ - **`Message.user(...)` / `Message.assistant(...)` / `Message.tool(...)`** — message constructors from the canonical schema model.
203
+ - **`LanguageModel.make(...)` / `ToolCallPart.make(...)` / `ToolResultPart.make(...)` / `ToolDefinition.make(...)`** — model and tool-related constructors from the canonical schema model.
204
+ - **`LLMEvent.is.*`** — typed guards (`is.textDelta`, `is.toolCall`, `is.finish`, …) for filtering streams.
205
+ - **`Image.generate({...})`** — generate images through a provider-neutral image request and response model.
206
+ - **`ImageClient`** — Effect service and layer for image execution, parallel to `LLMClient`.
207
+
208
+ ## Testing
209
+
210
+ Use the deterministic test client from `@opencode-ai/ai/testing` to script provider-neutral responses and inspect
211
+ the requests sent by code under test:
212
+
213
+ ```ts
214
+ import { Effect } from "effect"
215
+ import { TestLLM } from "@opencode-ai/ai/testing"
216
+
217
+ const testLLM = TestLLM.layer({
218
+ fallback: TestLLM.text("Hello from the test model", "text-1"),
219
+ })
220
+
221
+ // TestLLM.clientLayer provides LLMClient.Service and consumes TestLLM.Service.
222
+ const programWithTestClient = Effect.gen(function* () {
223
+ const result = yield* program
224
+ const test = yield* TestLLM.Service
225
+ console.log(test.requests)
226
+ return result
227
+ }).pipe(Effect.provide(TestLLM.clientLayer), Effect.provide(testLLM))
228
+ ```
229
+
230
+ `TestLLM.push(...)` scripts one-shot responses, `TestLLM.always(...)` changes the fallback, and
231
+ `TestLLM.wait(...)` lets concurrent tests wait until a request has arrived. Every received canonical request is
232
+ available on the yielded `TestLLM.Service`.
233
+
234
+ ## Caching
235
+
236
+ 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).
237
+
238
+ ### Auto placement
239
+
240
+ `"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 tools, the base agent, project instructions, and the active conversation. The rolling final-message boundary is the load-bearing detail in tool loops: it advances on every request so the previous cache entry stays within Anthropic's 20-block lookback.
241
+
242
+ 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.
243
+
244
+ The math justifies the default: Anthropic's 5-minute cache write is 1.25× base, read is 0.1×, so a single reuse within 5 minutes already wins. One-shot completions below the per-model minimum-cacheable-token threshold silently no-op on the wire, so the worst case is harmless.
245
+
246
+ ### Opting out
247
+
248
+ ```ts
249
+ LLM.request({
250
+ model,
251
+ system,
252
+ prompt: "one-off question",
253
+ cache: "none",
254
+ })
255
+ ```
256
+
257
+ ### Granular policy
258
+
259
+ ```ts
260
+ cache: {
261
+ tools?: boolean,
262
+ system?: boolean,
263
+ messages?: "latest-user-message" | "latest-assistant" | { tail: number },
264
+ ttlSeconds?: number, // ≥ 3600 → 1h on Anthropic/Bedrock; else 5m
265
+ }
266
+ ```
267
+
268
+ ### Manual hints
269
+
270
+ 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.
271
+
272
+ ```ts
273
+ LLM.request({
274
+ model,
275
+ system: [
276
+ { type: "text", text: "stable system prompt", cache: { type: "ephemeral" } },
277
+ ],
278
+ ...
279
+ })
280
+ ```
281
+
282
+ ### Provider behavior table
283
+
284
+ | Protocol | `cache: "auto"` |
285
+ | ----------------------- | ------------------------------------------------------------------------- |
286
+ | Anthropic Messages | emits up to 4 `cache_control` markers (4-breakpoint cap enforced) |
287
+ | Bedrock Converse | emits up to 4 `cachePoint` blocks (4-breakpoint cap enforced) |
288
+ | OpenAI Chat / Responses | no-op (implicit caching above 1024 tokens) |
289
+ | Gemini | no-op (implicit caching on 2.5+; explicit `CachedContent` is out-of-band) |
290
+
291
+ Normalized cache usage is read back into `response.usage.cacheReadInputTokens` and `cacheWriteInputTokens` across every provider.
292
+
293
+ ## Providers
294
+
295
+ 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.
296
+
297
+ ```ts
298
+ import { OpenAI, CloudflareAIGateway } from "@opencode-ai/ai/providers"
299
+
300
+ const openai = OpenAI.configure({ apiKey: process.env.OPENAI_API_KEY }).responses("gpt-4o-mini")
301
+ const gateway = CloudflareAIGateway.configure({
302
+ accountId: process.env.CLOUDFLARE_ACCOUNT_ID,
303
+ gatewayApiKey: process.env.CLOUDFLARE_API_TOKEN,
304
+ }).model("workers-ai/@cf/meta/llama-3.1-8b-instruct")
305
+ ```
306
+
307
+ Included providers: OpenAI, Anthropic, Google (Gemini), Google Vertex Gemini and Anthropic, Amazon Bedrock, Azure OpenAI, Cloudflare AI Gateway, Cloudflare Workers AI, GitHub Copilot, OpenRouter, xAI, Z.ai, plus generic OpenAI-compatible Chat and Responses entrypoints and an Anthropic Messages-compatible entrypoint.
308
+
309
+ ### Package-like entrypoints
310
+
311
+ Native catalog integrations load provider behavior through package-like entrypoints. These are export paths from the same `@opencode-ai/ai` npm package, not independently published packages. Each entrypoint exports the same `model(modelID, settings)` contract, and `settings` contains serializable provider configuration plus common `headers`, `body`, and `limits` overlays.
312
+
313
+ ```ts
314
+ import { model } from "@opencode-ai/ai/providers/openai/responses"
315
+
316
+ const selected = model("gpt-5", {
317
+ apiKey: process.env.OPENAI_API_KEY,
318
+ headers: { "x-application": "opencode" },
319
+ limits: { context: 200_000, output: 64_000 },
320
+ })
321
+ ```
322
+
323
+ OpenAI Chat and OpenAI Responses are separate semantic entrypoints:
324
+
325
+ - `@opencode-ai/ai/providers/openai/chat`
326
+ - `@opencode-ai/ai/providers/openai/responses`
327
+ - `@opencode-ai/ai/providers/openai-compatible/responses`
328
+ - `@opencode-ai/ai/providers/anthropic-compatible`
329
+ - `@opencode-ai/ai/providers/google-vertex/gemini`
330
+ - `@opencode-ai/ai/providers/google-vertex/chat`
331
+ - `@opencode-ai/ai/providers/google-vertex/responses`
332
+ - `@opencode-ai/ai/providers/google-vertex/messages`
333
+
334
+ 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.
335
+
336
+ 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`.
337
+
338
+ 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.
339
+
340
+ ```ts
341
+ import { model } from "@opencode-ai/ai/providers/google-vertex/gemini"
342
+
343
+ model("gemini-3.5-flash", { project: "my-project", location: "global" })
344
+ ```
345
+
346
+ ```ts
347
+ import { model } from "@opencode-ai/ai/providers/google-vertex/chat"
348
+
349
+ model("deepseek-ai/deepseek-v3.2-maas", { project: "my-project", location: "global" })
350
+ ```
351
+
352
+ ```ts
353
+ import { model } from "@opencode-ai/ai/providers/google-vertex/responses"
354
+
355
+ model("xai/grok-4.20-reasoning", { project: "my-project", location: "global" })
356
+ ```
357
+
358
+ ```ts
359
+ import { model } from "@opencode-ai/ai/providers/google-vertex/messages"
360
+
361
+ model("claude-sonnet-4-6", { project: "my-project", location: "global" })
362
+ ```
363
+
364
+ Provider facades such as `OpenAI.configure(...).responses(...)` remain the direct application API. Package-like entrypoints are the self-similar loading contract used when a catalog selects behavior by export path.
365
+
366
+ Other provider exports listed above remain direct facades until they explicitly implement the package-like contract. Exporting a provider facade does not implicitly make it a catalog-loadable provider package.
367
+
368
+ ## Provider options & HTTP overlays
369
+
370
+ Request options in order of stability:
371
+
372
+ 1. **`generation`** — portable knobs (`maxTokens`, `temperature`, `topP`, `topK`, penalties, seed, stop).
373
+ 2. **`promptCacheKey`** — stable cache affinity lowered by every protocol that supports it.
374
+ 3. **`providerOptions: { <provider>: {...} }`** — typed-at-the-facade provider-specific knobs (OpenAI `store`, Anthropic `thinking`, Gemini `thinkingConfig`, OpenRouter routing).
375
+ 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.
376
+
377
+ Route/provider defaults are overridden by request-level values for each axis.
378
+
379
+ ## Routes
380
+
381
+ Adding a new model or deployment is usually 5-15 lines using `Route.make({ protocol, endpoint, auth, framing, ... })`. The route owns endpoint/auth/framing and the protocol owns body construction plus stream parsing. Transports are reusable IO templates that receive route endpoint/auth at compile time. Capability/catalog metadata lives outside this low-level package; unsupported request shapes fail during protocol lowering. See `AGENTS.md` for the architectural detail.
382
+
383
+ ## Effect
384
+
385
+ 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.
386
+
387
+ ## See also
388
+
389
+ - `AGENTS.md` — architecture, route construction, contributor guide
390
+ - `STATUS.md` — native provider parity status and AI SDK migration gaps
391
+ - `example/tutorial.ts` — runnable end-to-end walkthrough
392
+ - `test/provider/*.test.ts` — fixture-first protocol tests; `*.recorded.test.ts` files cover live cassettes
@@ -0,0 +1,2 @@
1
+ import { LLMRequest } from "./schema/messages.js";
2
+ export declare const applyCachePolicy: (request: LLMRequest) => LLMRequest;
@@ -0,0 +1,121 @@
1
+ // Apply an `LLMRequest.cache` policy by injecting `CacheHint`s onto the parts
2
+ // the policy designates. Runs once at compile time, before the per-protocol
3
+ // body builder, so the existing inline-hint lowering path handles the rest.
4
+ //
5
+ // The default `"auto"` shape places breakpoints at the last tool definition,
6
+ // the first and last distinct system parts, and the conversation tail. This
7
+ // exposes reusable tool, base-agent, project, and session prefixes while
8
+ // advancing the tail after each tool result keeps the previous cache entry
9
+ // within Anthropic's 20-block lookback during long agent turns.
10
+ //
11
+ // Manual `cache: CacheHint` placements on individual parts are preserved and
12
+ // count against the four-breakpoint budget; auto only fills remaining slots.
13
+ import { CacheHint } from "./schema/options.js";
14
+ import { LLMRequest, Message, ToolDefinition } from "./schema/messages.js";
15
+ const AUTO = {
16
+ tools: true,
17
+ system: true,
18
+ messages: { tail: 1 },
19
+ };
20
+ const NONE = {};
21
+ const BREAKPOINT_CAP = 4;
22
+ // Resolution rules:
23
+ // - undefined → "auto" — caching is on by default. The math favors it:
24
+ // Anthropic 5m-cache write is 1.25x base, read is 0.1x,
25
+ // so a single reuse within 5 minutes already wins.
26
+ // - "auto" → tools + first/last system + final message boundary.
27
+ // - "none" → no auto placement; manual `CacheHint`s still flow.
28
+ // - object form → exactly what the caller asked for.
29
+ const resolve = (policy) => {
30
+ if (policy === undefined || policy === "auto")
31
+ return AUTO;
32
+ if (policy === "none")
33
+ return NONE;
34
+ return policy;
35
+ };
36
+ // Protocols whose wire format ignores inline cache markers (OpenAI's implicit
37
+ // prefix caching, Gemini's implicit + out-of-band CachedContent). Skip the
38
+ // whole policy pass for these — emitting hints would be harmless but pointless.
39
+ const RESPECTS_INLINE_HINTS = new Set(["anthropic-messages", "bedrock-converse", "openrouter"]);
40
+ const makeHint = (ttlSeconds) => ttlSeconds !== undefined ? new CacheHint({ type: "ephemeral", ttlSeconds }) : new CacheHint({ type: "ephemeral" });
41
+ const markLastTool = (tools, hint, budget) => {
42
+ if (tools.length === 0)
43
+ return tools;
44
+ const last = tools.length - 1;
45
+ if (tools[last].cache || budget.remaining === 0)
46
+ return tools;
47
+ budget.remaining -= 1;
48
+ return tools.map((tool, i) => (i === last ? new ToolDefinition({ ...tool, cache: hint }) : tool));
49
+ };
50
+ const markSystemBoundaries = (system, hint, budget) => {
51
+ if (system.length === 0)
52
+ return system;
53
+ let changed = false;
54
+ const next = system.map((part, index) => {
55
+ if ((index !== 0 && index !== system.length - 1) || part.cache || budget.remaining === 0)
56
+ return part;
57
+ budget.remaining -= 1;
58
+ changed = true;
59
+ return { ...part, cache: hint };
60
+ });
61
+ return changed ? next : system;
62
+ };
63
+ const lastIndexOfRole = (messages, role) => messages.findLastIndex((m) => m.role === role);
64
+ // Mark the last text part of `messages[index]`. If no text part exists, mark
65
+ // the last content part regardless of type — that's the breakpoint position
66
+ // in tool-result-only messages too.
67
+ const markMessageAt = (messages, index, hint, budget) => {
68
+ if (index < 0 || index >= messages.length)
69
+ return messages;
70
+ const target = messages[index];
71
+ if (target.content.length === 0)
72
+ return messages;
73
+ const lastTextIndex = target.content.findLastIndex((part) => part.type === "text");
74
+ const markAt = lastTextIndex >= 0 ? lastTextIndex : target.content.length - 1;
75
+ const existing = target.content[markAt];
76
+ if (("cache" in existing && existing.cache) || budget.remaining === 0)
77
+ return messages;
78
+ budget.remaining -= 1;
79
+ const nextContent = target.content.map((part, i) => (i === markAt ? { ...part, cache: hint } : part));
80
+ const next = new Message({ ...target, content: nextContent });
81
+ // Single pass over `messages`, substituting the one updated entry. Long
82
+ // conversations call this on every request, so avoid `.map()` here — its
83
+ // closure dispatch and identity copies show up in profiling.
84
+ const result = messages.slice();
85
+ result[index] = next;
86
+ return result;
87
+ };
88
+ const markMessages = (messages, strategy, hint, budget) => {
89
+ if (messages.length === 0)
90
+ return messages;
91
+ if (strategy === "latest-user-message")
92
+ return markMessageAt(messages, lastIndexOfRole(messages, "user"), hint, budget);
93
+ if (strategy === "latest-assistant")
94
+ return markMessageAt(messages, lastIndexOfRole(messages, "assistant"), hint, budget);
95
+ const start = Math.max(0, messages.length - strategy.tail);
96
+ let next = messages;
97
+ for (let i = start; i < messages.length; i++)
98
+ next = markMessageAt(next, i, hint, budget);
99
+ return next;
100
+ };
101
+ const countHints = (request) => request.tools.reduce((count, tool) => count + (tool.cache === undefined ? 0 : 1), 0) +
102
+ request.system.reduce((count, part) => count + (part.cache === undefined ? 0 : 1), 0) +
103
+ request.messages.reduce((count, message) => count +
104
+ message.content.reduce((contentCount, part) => contentCount + ("cache" in part && part.cache !== undefined ? 1 : 0), 0), 0);
105
+ export const applyCachePolicy = (request) => {
106
+ if (!RESPECTS_INLINE_HINTS.has(request.model.route.id))
107
+ return request;
108
+ if (request.model.route.id === "openrouter" && (request.cache === undefined || request.cache === "auto"))
109
+ return request;
110
+ const policy = resolve(request.cache);
111
+ if (!policy.tools && !policy.system && !policy.messages)
112
+ return request;
113
+ const hint = makeHint(policy.ttlSeconds);
114
+ const budget = { remaining: Math.max(0, BREAKPOINT_CAP - countHints(request)) };
115
+ const tools = policy.tools ? markLastTool(request.tools, hint, budget) : request.tools;
116
+ const system = policy.system ? markSystemBoundaries(request.system, hint, budget) : request.system;
117
+ const messages = policy.messages ? markMessages(request.messages, policy.messages, hint, budget) : request.messages;
118
+ if (tools === request.tools && system === request.system && messages === request.messages)
119
+ return request;
120
+ return LLMRequest.update(request, { tools, system, messages });
121
+ };
@@ -0,0 +1,19 @@
1
+ import { Context, Effect, Layer } from "effect";
2
+ import { RequestExecutor } from "./route/executor.js";
3
+ import type { ImageOptions, ImageRequestFor, ImageResponse } from "./image.js";
4
+ import type { AIError } from "./schema/index.js";
5
+ export type Execute = RequestExecutor.Interface["execute"];
6
+ export interface Interface {
7
+ readonly generate: <Options extends ImageOptions>(request: ImageRequestFor<Options>) => Effect.Effect<ImageResponse, AIError>;
8
+ }
9
+ declare const Service_base: Context.ServiceClass<Service, "@opencode/ImageClient", Interface>;
10
+ export declare class Service extends Service_base {
11
+ }
12
+ export declare const generate: <Options extends ImageOptions>(request: ImageRequestFor<Options>) => Effect.Effect<ImageResponse, AIError, Service>;
13
+ export declare const layer: Layer.Layer<Service, never, RequestExecutor.Service>;
14
+ export declare const ImageClient: {
15
+ readonly Service: typeof Service;
16
+ readonly layer: Layer.Layer<Service, never, RequestExecutor.Service>;
17
+ readonly generate: <Options extends ImageOptions>(request: ImageRequestFor<Options>) => Effect.Effect<ImageResponse, AIError, Service>;
18
+ };
19
+ export {};
@@ -0,0 +1,19 @@
1
+ import { Context, Effect, Layer } from "effect";
2
+ import { RequestExecutor } from "./route/executor.js";
3
+ export class Service extends Context.Service()("@opencode/ImageClient") {
4
+ }
5
+ export const generate = (request) => Effect.gen(function* () {
6
+ const client = yield* Service;
7
+ return yield* client.generate(request);
8
+ });
9
+ export const layer = Layer.effect(Service, Effect.gen(function* () {
10
+ const executor = yield* RequestExecutor.Service;
11
+ return Service.of({
12
+ generate: (request) => request.model.route.generate(request, executor.execute),
13
+ });
14
+ }));
15
+ export const ImageClient = {
16
+ Service,
17
+ layer,
18
+ generate,
19
+ };
@@ -0,0 +1,110 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { HttpOptions, AIError, ModelID, ProviderID, Usage } from "./schema/index.js";
3
+ import { Service, type Execute as ImageExecute } from "./image-client.js";
4
+ export interface ImageRoute<Options extends ImageOptions = ImageOptions> {
5
+ readonly id: string;
6
+ readonly generate: (request: ImageRequestFor<Options>, execute: ImageExecute) => Effect.Effect<ImageResponse, AIError>;
7
+ }
8
+ export type ImageOptions = Record<string, unknown>;
9
+ export declare class ImageModel<Options extends ImageOptions = ImageOptions> {
10
+ protected readonly _Options: (options: Options) => Options;
11
+ readonly id: ModelID;
12
+ readonly provider: ProviderID;
13
+ readonly route: ImageRoute<Options>;
14
+ readonly http?: HttpOptions;
15
+ constructor(input: ImageModel.Input<Options>);
16
+ static make<Options extends ImageOptions = ImageOptions>(input: ImageModel.MakeInput<Options>): ImageModel<Options>;
17
+ }
18
+ export declare namespace ImageModel {
19
+ interface Input<Options extends ImageOptions = ImageOptions> {
20
+ readonly id: ModelID;
21
+ readonly provider: ProviderID;
22
+ readonly route: ImageRoute<Options>;
23
+ readonly http?: HttpOptions;
24
+ }
25
+ interface MakeInput<Options extends ImageOptions = ImageOptions> extends Omit<Input<Options>, "id" | "provider"> {
26
+ readonly id: string | ModelID;
27
+ readonly provider: string | ProviderID;
28
+ }
29
+ }
30
+ export declare const ImageModelSchema: Schema.declare<ImageModel<ImageOptions>, ImageModel<ImageOptions>>;
31
+ export declare const ImageInputSchema: Schema.toTaggedUnion<"type", readonly [Schema.Struct<{
32
+ readonly type: Schema.Literal<"bytes">;
33
+ readonly data: Schema.Uint8Array;
34
+ readonly mediaType: Schema.String;
35
+ }>, Schema.Struct<{
36
+ readonly type: Schema.Literal<"url">;
37
+ readonly url: Schema.String;
38
+ }>, Schema.Struct<{
39
+ readonly type: Schema.Literal<"file-id">;
40
+ readonly id: Schema.String;
41
+ }>, Schema.Struct<{
42
+ readonly type: Schema.Literal<"file-uri">;
43
+ readonly uri: Schema.String;
44
+ readonly mediaType: Schema.String;
45
+ }>]>;
46
+ export type ImageInput = Schema.Schema.Type<typeof ImageInputSchema>;
47
+ export declare const ImageInput: {
48
+ readonly bytes: (data: Uint8Array, mediaType: string) => ImageInput;
49
+ readonly url: (url: string) => ImageInput;
50
+ readonly file: (id: string) => ImageInput;
51
+ readonly fileUri: (uri: string, mediaType: string) => ImageInput;
52
+ };
53
+ declare const ImageRequest_base: Schema.Class<ImageRequest, Schema.Struct<{
54
+ readonly model: Schema.declare<ImageModel<ImageOptions>, ImageModel<ImageOptions>>;
55
+ readonly prompt: Schema.String;
56
+ readonly images: Schema.optional<Schema.$Array<Schema.toTaggedUnion<"type", readonly [Schema.Struct<{
57
+ readonly type: Schema.Literal<"bytes">;
58
+ readonly data: Schema.Uint8Array;
59
+ readonly mediaType: Schema.String;
60
+ }>, Schema.Struct<{
61
+ readonly type: Schema.Literal<"url">;
62
+ readonly url: Schema.String;
63
+ }>, Schema.Struct<{
64
+ readonly type: Schema.Literal<"file-id">;
65
+ readonly id: Schema.String;
66
+ }>, Schema.Struct<{
67
+ readonly type: Schema.Literal<"file-uri">;
68
+ readonly uri: Schema.String;
69
+ readonly mediaType: Schema.String;
70
+ }>]>>>;
71
+ readonly options: Schema.optional<Schema.$Record<Schema.String, Schema.Unknown>>;
72
+ readonly http: Schema.optional<typeof HttpOptions>;
73
+ }>, {}>;
74
+ export declare class ImageRequest extends ImageRequest_base {
75
+ protected readonly _ImageRequest: void;
76
+ }
77
+ export type ImageRequestFor<Options extends ImageOptions = ImageOptions> = Omit<ImageRequest, "model" | "options"> & {
78
+ readonly model: ImageModel<Options>;
79
+ readonly options?: Options;
80
+ };
81
+ export type ImageModelOptions<Model> = Model extends ImageModel<infer Options> ? Options : never;
82
+ export type ImageRequestInput<Model extends object = ImageModel> = Omit<ConstructorParameters<typeof ImageRequest>[0], "model" | "options" | "http"> & {
83
+ readonly model: Model;
84
+ readonly options?: NoInfer<ImageModelOptions<Model>>;
85
+ readonly http?: HttpOptions.Input;
86
+ } & (Model extends ImageModel<ImageModelOptions<Model>> ? unknown : never);
87
+ declare const GeneratedImage_base: Schema.Class<GeneratedImage, Schema.Struct<{
88
+ readonly mediaType: Schema.String;
89
+ readonly data: Schema.Union<readonly [Schema.String, Schema.Uint8Array]>;
90
+ readonly providerMetadata: Schema.optional<Schema.$Record<Schema.String, Schema.$Record<Schema.String, Schema.Unknown>>>;
91
+ }>, {}>;
92
+ export declare class GeneratedImage extends GeneratedImage_base {
93
+ }
94
+ declare const ImageResponse_base: Schema.Class<ImageResponse, Schema.Struct<{
95
+ readonly images: Schema.$Array<typeof GeneratedImage>;
96
+ readonly usage: Schema.optional<typeof Usage>;
97
+ readonly providerMetadata: Schema.optional<Schema.$Record<Schema.String, Schema.$Record<Schema.String, Schema.Unknown>>>;
98
+ }>, {}>;
99
+ export declare class ImageResponse extends ImageResponse_base {
100
+ get image(): GeneratedImage;
101
+ }
102
+ export declare function request<const Model extends object>(input: ImageRequestInput<Model>): ImageRequestFor<ImageModelOptions<Model>>;
103
+ export declare function request(input: ImageRequest): ImageRequest;
104
+ export declare function generate<const Model extends object>(input: ImageRequestInput<Model>): Effect.Effect<ImageResponse, AIError, Service>;
105
+ export declare function generate(input: ImageRequest): Effect.Effect<ImageResponse, AIError, Service>;
106
+ export declare const Image: {
107
+ readonly request: typeof request;
108
+ readonly generate: typeof generate;
109
+ };
110
+ export {};