@opencode-ai/ai 0.0.0-beta-17492

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 (194) hide show
  1. package/README.md +392 -0
  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 +47 -0
@@ -0,0 +1,512 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { Tool } from "@opencode-ai/schema/tool";
3
+ import { Route } from "../route/client.js";
4
+ import { Auth } from "../route/auth.js";
5
+ import { Endpoint } from "../route/endpoint.js";
6
+ import { Framing } from "../route/framing.js";
7
+ import { Protocol } from "../route/protocol.js";
8
+ import { LLMEvent, Usage, } from "../schema/index.js";
9
+ import { JsonObject, optionalArray, ProviderShared } from "./shared.js";
10
+ import { GeminiToolSchema } from "./utils/gemini-tool-schema.js";
11
+ import { Lifecycle } from "./utils/lifecycle.js";
12
+ import { ToolSchemaProjection } from "./utils/tool-schema.js";
13
+ const ADAPTER = "gemini";
14
+ const MEDIA_MIMES = new Set(ProviderShared.MEDIA_MIMES);
15
+ // Google documents this sentinel for replaying Gemini 3 function calls after their original signature was lost.
16
+ const SKIP_THOUGHT_SIGNATURE_VALIDATOR = "skip_thought_signature_validator";
17
+ export const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
18
+ // Gemini 3 rejects replayed function calls without a thought signature. Google's SDKs avoid that in normal chats by
19
+ // retaining complete model responses, but OpenCode reconstructs durable history and may encounter an unsigned call
20
+ // from an older or external session. Model IDs are open-ended, so unknown Gemini aliases inherit current behavior.
21
+ const requiresThoughtSignatureFallback = (modelID) => {
22
+ if (!/(^|\/)gemini-/i.test(modelID))
23
+ return false;
24
+ if (/(^|\/)gemini-(?:1|2)(?:[.-]|$)/i.test(modelID))
25
+ return false;
26
+ if (/(^|\/)gemini-pro(?:-vision)?$/i.test(modelID))
27
+ return false;
28
+ return !/(^|\/)gemini-robotics-er-1\.5(?:[.-]|$)/i.test(modelID);
29
+ };
30
+ // =============================================================================
31
+ // Request Body Schema
32
+ // =============================================================================
33
+ const GeminiTextPart = Schema.Struct({
34
+ text: Schema.String,
35
+ thought: Schema.optional(Schema.Boolean),
36
+ thoughtSignature: Schema.optional(Schema.String),
37
+ });
38
+ const GeminiInlineDataPart = Schema.Struct({
39
+ inlineData: Schema.Struct({
40
+ mimeType: Schema.String,
41
+ data: Schema.String,
42
+ }),
43
+ });
44
+ const GeminiFunctionCallPart = Schema.Struct({
45
+ functionCall: Schema.Struct({
46
+ id: Schema.optional(Schema.String),
47
+ name: Schema.String,
48
+ args: Schema.Unknown,
49
+ }),
50
+ thoughtSignature: Schema.optional(Schema.String),
51
+ });
52
+ const GeminiFunctionResponsePart = Schema.Struct({
53
+ functionResponse: Schema.Struct({
54
+ id: Schema.optional(Schema.String),
55
+ name: Schema.String,
56
+ response: Schema.Unknown,
57
+ parts: Schema.optional(Schema.Array(GeminiInlineDataPart)),
58
+ }),
59
+ });
60
+ const GeminiContentPart = Schema.Union([
61
+ GeminiTextPart,
62
+ GeminiInlineDataPart,
63
+ GeminiFunctionCallPart,
64
+ GeminiFunctionResponsePart,
65
+ ]);
66
+ const GeminiContent = Schema.Struct({
67
+ role: Schema.Literals(["user", "model"]),
68
+ parts: Schema.Array(GeminiContentPart),
69
+ });
70
+ const GeminiSystemInstruction = Schema.Struct({
71
+ parts: Schema.Array(Schema.Struct({ text: Schema.String })),
72
+ });
73
+ const GeminiFunctionDeclaration = Schema.Struct({
74
+ name: Schema.String,
75
+ description: Schema.String,
76
+ parameters: Schema.optional(JsonObject),
77
+ });
78
+ const GeminiTool = Schema.Struct({
79
+ functionDeclarations: Schema.Array(GeminiFunctionDeclaration),
80
+ });
81
+ const GeminiToolConfig = Schema.Struct({
82
+ functionCallingConfig: Schema.Struct({
83
+ mode: Schema.Literals(["AUTO", "NONE", "ANY"]),
84
+ allowedFunctionNames: optionalArray(Schema.String),
85
+ }),
86
+ });
87
+ const GeminiThinkingConfig = Schema.Struct({
88
+ thinkingBudget: Schema.optional(Schema.Number),
89
+ includeThoughts: Schema.optional(Schema.Boolean),
90
+ thinkingLevel: Schema.optional(Schema.String),
91
+ });
92
+ const GeminiSafetySetting = Schema.Struct({
93
+ category: Schema.String,
94
+ threshold: Schema.String,
95
+ });
96
+ const GeminiGenerationConfig = Schema.Struct({
97
+ maxOutputTokens: Schema.optional(Schema.Number),
98
+ temperature: Schema.optional(Schema.Number),
99
+ topP: Schema.optional(Schema.Number),
100
+ topK: Schema.optional(Schema.Number),
101
+ frequencyPenalty: Schema.optional(Schema.Number),
102
+ presencePenalty: Schema.optional(Schema.Number),
103
+ seed: Schema.optional(Schema.Number),
104
+ stopSequences: optionalArray(Schema.String),
105
+ thinkingConfig: Schema.optional(GeminiThinkingConfig),
106
+ });
107
+ const GeminiBodyFields = {
108
+ cachedContent: Schema.optional(Schema.String),
109
+ contents: Schema.Array(GeminiContent),
110
+ safetySettings: optionalArray(GeminiSafetySetting),
111
+ serviceTier: Schema.optional(Schema.String),
112
+ systemInstruction: Schema.optional(GeminiSystemInstruction),
113
+ tools: optionalArray(GeminiTool),
114
+ toolConfig: Schema.optional(GeminiToolConfig),
115
+ generationConfig: Schema.optional(GeminiGenerationConfig),
116
+ };
117
+ const GeminiBody = Schema.Struct(GeminiBodyFields);
118
+ const GeminiUsage = Schema.Struct({
119
+ cachedContentTokenCount: Schema.optional(Schema.Number),
120
+ thoughtsTokenCount: Schema.optional(Schema.Number),
121
+ promptTokenCount: Schema.optional(Schema.Number),
122
+ candidatesTokenCount: Schema.optional(Schema.Number),
123
+ totalTokenCount: Schema.optional(Schema.Number),
124
+ });
125
+ const GeminiCandidate = Schema.Struct({
126
+ content: Schema.optional(GeminiContent),
127
+ finishReason: Schema.optional(Schema.String),
128
+ });
129
+ const GeminiEvent = Schema.Struct({
130
+ candidates: optionalArray(GeminiCandidate),
131
+ usageMetadata: Schema.optional(GeminiUsage),
132
+ });
133
+ // =============================================================================
134
+ // Tool Schema Conversion
135
+ // =============================================================================
136
+ // Tool-schema conversion has two distinct concerns:
137
+ //
138
+ // 1. Sanitize — fix common authoring mistakes Gemini rejects: integer/number
139
+ // enums (must be strings), `required` entries that don't match a property,
140
+ // untyped arrays (`items` must be present), and `properties`/`required`
141
+ // keys on non-object scalars. Mirrors OpenCode's historical Gemini rules.
142
+ //
143
+ // 2. Project — lossy mapping from JSON Schema to Gemini's schema dialect:
144
+ // drop empty root parameter schemas while preserving nested empty objects,
145
+ // expand type arrays into `anyOf`, derive `nullable: true` from null members,
146
+ // coerce `const` to `[const]` enum, recurse properties/items, and propagate
147
+ // only an allowlisted set of keys (description, required, format, type,
148
+ // nullable, enum, properties, items, allOf, anyOf, oneOf, minLength).
149
+ // Anything outside the allowlist (e.g. `additionalProperties`, `$ref`) is
150
+ // silently dropped.
151
+ //
152
+ // Sanitize runs first, then project. The implementation lives in
153
+ // `utils/gemini-tool-schema` so this protocol keeps the same shape as the other
154
+ // provider protocols.
155
+ // =============================================================================
156
+ // Request Lowering
157
+ // =============================================================================
158
+ const lowerTool = (tool, inputSchema) => ({
159
+ name: tool.name,
160
+ description: tool.description,
161
+ parameters: GeminiToolSchema.convert(inputSchema),
162
+ });
163
+ const lowerToolConfig = (toolChoice) => ProviderShared.matchToolChoice("Gemini", toolChoice, {
164
+ auto: () => ({ functionCallingConfig: { mode: "AUTO" } }),
165
+ none: () => ({ functionCallingConfig: { mode: "NONE" } }),
166
+ required: () => ({ functionCallingConfig: { mode: "ANY" } }),
167
+ tool: (name) => ({ functionCallingConfig: { mode: "ANY", allowedFunctionNames: [name] } }),
168
+ });
169
+ const lowerUserPart = Effect.fn("Gemini.lowerUserPart")(function* (part) {
170
+ if (part.type === "text")
171
+ return { text: part.text };
172
+ const media = yield* ProviderShared.validateMedia("Gemini", part, MEDIA_MIMES);
173
+ return { inlineData: { mimeType: media.mime, data: media.base64 } };
174
+ });
175
+ const googleMetadata = (metadata) => ({ google: metadata });
176
+ const thoughtSignature = (providerMetadata) => {
177
+ const google = providerMetadata?.google;
178
+ return ProviderShared.isRecord(google) && typeof google.thoughtSignature === "string"
179
+ ? google.thoughtSignature
180
+ : undefined;
181
+ };
182
+ const functionCallId = (providerMetadata) => {
183
+ const google = providerMetadata?.google;
184
+ return ProviderShared.isRecord(google) && typeof google.functionCallId === "string"
185
+ ? google.functionCallId
186
+ : undefined;
187
+ };
188
+ const lowerToolCall = (part) => ({
189
+ functionCall: { id: functionCallId(part.providerMetadata), name: part.name, args: part.input },
190
+ thoughtSignature: thoughtSignature(part.providerMetadata),
191
+ });
192
+ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request) {
193
+ const contents = [];
194
+ for (const message of request.messages) {
195
+ if (message.role === "system") {
196
+ const part = yield* ProviderShared.wrappedSystemUpdate("Gemini", message);
197
+ const previous = contents.at(-1);
198
+ if (previous?.role === "user")
199
+ contents[contents.length - 1] = { role: "user", parts: [...previous.parts, { text: part.text }] };
200
+ else
201
+ contents.push({ role: "user", parts: [{ text: part.text }] });
202
+ continue;
203
+ }
204
+ if (message.role === "user") {
205
+ const parts = [];
206
+ for (const part of message.content) {
207
+ if (!ProviderShared.supportsContent(part, ["text", "media"]))
208
+ return yield* ProviderShared.unsupportedContent("Gemini", "user", ["text", "media"]);
209
+ parts.push(yield* lowerUserPart(part));
210
+ }
211
+ contents.push({ role: "user", parts });
212
+ continue;
213
+ }
214
+ if (message.role === "assistant") {
215
+ const parts = [];
216
+ // Parallel Gemini 3 calls may carry one signature on the first call; unsigned sibling calls are valid.
217
+ let hasSignedToolCall = false;
218
+ for (const part of message.content) {
219
+ if (!ProviderShared.supportsContent(part, ["text", "reasoning", "tool-call"]))
220
+ return yield* ProviderShared.unsupportedContent("Gemini", "assistant", ["text", "reasoning", "tool-call"]);
221
+ if (part.type === "text") {
222
+ parts.push({ text: part.text });
223
+ continue;
224
+ }
225
+ if (part.type === "reasoning") {
226
+ parts.push({ text: part.text, thought: true, thoughtSignature: thoughtSignature(part.providerMetadata) });
227
+ continue;
228
+ }
229
+ if (part.type === "tool-call") {
230
+ const lowered = lowerToolCall(part);
231
+ const signature = lowered.thoughtSignature;
232
+ parts.push({
233
+ ...lowered,
234
+ thoughtSignature: signature ??
235
+ (requiresThoughtSignatureFallback(request.model.id) && !hasSignedToolCall
236
+ ? SKIP_THOUGHT_SIGNATURE_VALIDATOR
237
+ : undefined),
238
+ });
239
+ if (signature !== undefined)
240
+ hasSignedToolCall = true;
241
+ continue;
242
+ }
243
+ }
244
+ contents.push({ role: "model", parts });
245
+ continue;
246
+ }
247
+ const parts = [];
248
+ for (const part of message.content) {
249
+ if (!ProviderShared.supportsContent(part, ["tool-result"]))
250
+ return yield* ProviderShared.unsupportedContent("Gemini", "tool", ["tool-result"]);
251
+ if (part.result.type !== "content") {
252
+ parts.push({
253
+ functionResponse: {
254
+ id: functionCallId(part.providerMetadata),
255
+ name: part.name,
256
+ response: {
257
+ name: part.name,
258
+ content: ProviderShared.toolResultText(part),
259
+ },
260
+ },
261
+ });
262
+ continue;
263
+ }
264
+ const content = part.result.value;
265
+ const text = content.filter((item) => item.type === "text").map((item) => item.text);
266
+ const media = [];
267
+ for (const item of content) {
268
+ if (item.type === "text")
269
+ continue;
270
+ const value = yield* ProviderShared.validateToolFile("Gemini", item, MEDIA_MIMES);
271
+ media.push({ inlineData: { mimeType: value.mime, data: value.base64 } });
272
+ }
273
+ parts.push({
274
+ functionResponse: {
275
+ id: functionCallId(part.providerMetadata),
276
+ name: part.name,
277
+ response: {
278
+ name: part.name,
279
+ content: text.join("\n"),
280
+ },
281
+ parts: media.length > 0 ? media : undefined,
282
+ },
283
+ });
284
+ }
285
+ contents.push({ role: "user", parts });
286
+ }
287
+ return contents;
288
+ });
289
+ const resolveOptions = (request) => {
290
+ const input = request.providerOptions?.gemini;
291
+ const value = input?.thinkingConfig;
292
+ const thinkingConfig = {
293
+ thinkingBudget: ProviderShared.isRecord(value) && typeof value.thinkingBudget === "number" ? value.thinkingBudget : undefined,
294
+ includeThoughts: ProviderShared.isRecord(value) && typeof value.includeThoughts === "boolean"
295
+ ? value.includeThoughts
296
+ : ProviderShared.isRecord(value)
297
+ ? true
298
+ : undefined,
299
+ thinkingLevel: ProviderShared.isRecord(value) && typeof value.thinkingLevel === "string" ? value.thinkingLevel : undefined,
300
+ };
301
+ return {
302
+ cachedContent: typeof input?.cachedContent === "string" ? input.cachedContent : undefined,
303
+ safetySettings: mapSafetySettings(input?.safetySettings),
304
+ serviceTier: typeof input?.serviceTier === "string" ? input.serviceTier : undefined,
305
+ thinkingConfig: Object.values(thinkingConfig).some((item) => item !== undefined) ? thinkingConfig : undefined,
306
+ };
307
+ };
308
+ function mapSafetySettings(value) {
309
+ if (!Array.isArray(value))
310
+ return undefined;
311
+ const settings = value.flatMap((item) => ProviderShared.isRecord(item) && typeof item.category === "string" && typeof item.threshold === "string"
312
+ ? [{ category: item.category, threshold: item.threshold }]
313
+ : []);
314
+ return settings;
315
+ }
316
+ const fromRequest = Effect.fn("Gemini.fromRequest")(function* (request) {
317
+ const hasTools = request.tools.length > 0;
318
+ const generation = request.generation;
319
+ const options = resolveOptions(request);
320
+ const toolSchemaCompatibility = request.model.compatibility?.toolSchema;
321
+ const generationConfig = {
322
+ maxOutputTokens: generation?.maxTokens,
323
+ temperature: generation?.temperature,
324
+ topP: generation?.topP,
325
+ topK: generation?.topK,
326
+ frequencyPenalty: generation?.frequencyPenalty,
327
+ presencePenalty: generation?.presencePenalty,
328
+ seed: generation?.seed,
329
+ stopSequences: generation?.stop,
330
+ thinkingConfig: options.thinkingConfig,
331
+ };
332
+ return {
333
+ cachedContent: options.cachedContent,
334
+ contents: yield* lowerMessages(request),
335
+ safetySettings: options.safetySettings,
336
+ serviceTier: options.serviceTier,
337
+ systemInstruction: request.system.length === 0 ? undefined : { parts: [{ text: ProviderShared.joinText(request.system) }] },
338
+ tools: hasTools
339
+ ? [
340
+ {
341
+ functionDeclarations: request.tools.map((tool) => lowerTool(tool, ToolSchemaProjection.modelCompatibility(tool.inputSchema, toolSchemaCompatibility))),
342
+ },
343
+ ]
344
+ : undefined,
345
+ toolConfig: hasTools && request.toolChoice ? yield* lowerToolConfig(request.toolChoice) : undefined,
346
+ generationConfig: Object.values(generationConfig).some((value) => value !== undefined)
347
+ ? generationConfig
348
+ : undefined,
349
+ };
350
+ });
351
+ // =============================================================================
352
+ // Stream Parsing
353
+ // =============================================================================
354
+ // Gemini reports `promptTokenCount` (inclusive total) with a
355
+ // `cachedContentTokenCount` subset. `candidatesTokenCount` is *exclusive*
356
+ // of `thoughtsTokenCount` — visible-only, not a total — so we sum the two
357
+ // to produce the inclusive `outputTokens` the rest of the contract expects.
358
+ const mapUsage = (usage) => {
359
+ if (!usage)
360
+ return undefined;
361
+ const cached = usage.cachedContentTokenCount;
362
+ const nonCached = ProviderShared.subtractTokens(usage.promptTokenCount, cached);
363
+ // `candidatesTokenCount` is visible-only; sum with thoughts to produce the
364
+ // inclusive `outputTokens` the contract expects. Only compute the total
365
+ // when the visible component is reported — otherwise we'd fabricate an
366
+ // inclusive number from a partial breakdown.
367
+ const outputTokens = usage.candidatesTokenCount !== undefined ? usage.candidatesTokenCount + (usage.thoughtsTokenCount ?? 0) : undefined;
368
+ return new Usage({
369
+ inputTokens: usage.promptTokenCount,
370
+ outputTokens,
371
+ nonCachedInputTokens: nonCached,
372
+ cacheReadInputTokens: cached,
373
+ reasoningTokens: usage.thoughtsTokenCount,
374
+ totalTokens: ProviderShared.totalTokens(usage.promptTokenCount, outputTokens, usage.totalTokenCount),
375
+ providerMetadata: { google: usage },
376
+ });
377
+ };
378
+ const mapFinishReason = (finishReason, hasToolCalls) => {
379
+ if (finishReason === undefined)
380
+ return hasToolCalls ? "tool-calls" : "unknown";
381
+ if (finishReason === "STOP")
382
+ return hasToolCalls ? "tool-calls" : "stop";
383
+ if (finishReason === "MAX_TOKENS")
384
+ return "length";
385
+ if (finishReason === "IMAGE_SAFETY" ||
386
+ finishReason === "RECITATION" ||
387
+ finishReason === "SAFETY" ||
388
+ finishReason === "BLOCKLIST" ||
389
+ finishReason === "PROHIBITED_CONTENT" ||
390
+ finishReason === "SPII" ||
391
+ finishReason === "MODEL_ARMOR" ||
392
+ finishReason === "IMAGE_PROHIBITED_CONTENT" ||
393
+ finishReason === "IMAGE_RECITATION" ||
394
+ finishReason === "LANGUAGE")
395
+ return "content-filter";
396
+ if (finishReason === "MALFORMED_FUNCTION_CALL" ||
397
+ finishReason === "UNEXPECTED_TOOL_CALL" ||
398
+ finishReason === "NO_IMAGE" ||
399
+ finishReason === "TOO_MANY_TOOL_CALLS" ||
400
+ finishReason === "MISSING_THOUGHT_SIGNATURE" ||
401
+ finishReason === "MALFORMED_RESPONSE")
402
+ return "error";
403
+ return "unknown";
404
+ };
405
+ const finish = (state) => state.finishReason || state.usage
406
+ ? (() => {
407
+ const events = [];
408
+ const lifecycle = state.reasoningSignature
409
+ ? Lifecycle.reasoningEnd(state.lifecycle, events, "reasoning-0", googleMetadata({ thoughtSignature: state.reasoningSignature }))
410
+ : state.lifecycle;
411
+ Lifecycle.finish(lifecycle, events, {
412
+ reason: {
413
+ normalized: mapFinishReason(state.finishReason, state.hasToolCalls),
414
+ raw: state.finishReason,
415
+ },
416
+ usage: state.usage,
417
+ });
418
+ return events;
419
+ })()
420
+ : [];
421
+ const step = (state, event) => {
422
+ const nextState = {
423
+ ...state,
424
+ usage: event.usageMetadata ? (mapUsage(event.usageMetadata) ?? state.usage) : state.usage,
425
+ };
426
+ const candidate = event.candidates?.[0];
427
+ if (!candidate?.content)
428
+ return Effect.succeed([
429
+ { ...nextState, finishReason: candidate?.finishReason ?? nextState.finishReason },
430
+ [],
431
+ ]);
432
+ const events = [];
433
+ let hasToolCalls = nextState.hasToolCalls;
434
+ let lifecycle = nextState.lifecycle;
435
+ let nextToolCallId = nextState.nextToolCallId;
436
+ let reasoningSignature = nextState.reasoningSignature;
437
+ for (const part of candidate.content.parts) {
438
+ if ("thoughtSignature" in part && part.thoughtSignature && "thought" in part && part.thought)
439
+ reasoningSignature = part.thoughtSignature;
440
+ if ("text" in part && part.text.length > 0) {
441
+ if (part.thought) {
442
+ lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", part.text, part.thoughtSignature ? googleMetadata({ thoughtSignature: part.thoughtSignature }) : undefined);
443
+ continue;
444
+ }
445
+ lifecycle = Lifecycle.reasoningEnd(lifecycle, events, "reasoning-0", reasoningSignature ? googleMetadata({ thoughtSignature: reasoningSignature }) : undefined);
446
+ lifecycle = Lifecycle.textDelta(lifecycle, events, "text-0", part.text);
447
+ continue;
448
+ }
449
+ if ("functionCall" in part) {
450
+ const input = part.functionCall.args;
451
+ const id = `tool_${nextToolCallId++}`;
452
+ const metadata = {
453
+ ...(part.functionCall.id === undefined ? {} : { functionCallId: part.functionCall.id }),
454
+ ...(part.thoughtSignature === undefined ? {} : { thoughtSignature: part.thoughtSignature }),
455
+ };
456
+ lifecycle = Lifecycle.reasoningEnd(lifecycle, events, "reasoning-0", reasoningSignature ? googleMetadata({ thoughtSignature: reasoningSignature }) : undefined);
457
+ lifecycle = Lifecycle.stepStart(lifecycle, events);
458
+ events.push(LLMEvent.toolCall({
459
+ id,
460
+ name: part.functionCall.name,
461
+ input,
462
+ providerMetadata: Object.keys(metadata).length > 0 ? googleMetadata(metadata) : undefined,
463
+ }));
464
+ hasToolCalls = true;
465
+ }
466
+ }
467
+ return Effect.succeed([
468
+ {
469
+ ...nextState,
470
+ hasToolCalls,
471
+ lifecycle,
472
+ nextToolCallId,
473
+ reasoningSignature,
474
+ finishReason: candidate.finishReason ?? nextState.finishReason,
475
+ },
476
+ events,
477
+ ]);
478
+ };
479
+ // =============================================================================
480
+ // Protocol And Gemini Route
481
+ // =============================================================================
482
+ /**
483
+ * The Gemini protocol — request body construction, body schema, and the
484
+ * streaming-event state machine. Used by Google AI Studio Gemini and (once
485
+ * registered) Vertex Gemini.
486
+ */
487
+ export const protocol = Protocol.make({
488
+ id: ADAPTER,
489
+ body: {
490
+ schema: GeminiBody,
491
+ from: fromRequest,
492
+ },
493
+ stream: {
494
+ event: Protocol.jsonEvent(GeminiEvent),
495
+ initial: () => ({ hasToolCalls: false, nextToolCallId: 0, lifecycle: Lifecycle.initial() }),
496
+ step,
497
+ onHalt: finish,
498
+ },
499
+ });
500
+ export const route = Route.make({
501
+ id: ADAPTER,
502
+ provider: "google",
503
+ providerMetadataKey: "google",
504
+ protocol,
505
+ // Gemini's path embeds the model id and pins SSE framing at the URL level.
506
+ endpoint: Endpoint.path(({ request }) => `/models/${request.model.id}:streamGenerateContent?alt=sse`, {
507
+ baseURL: DEFAULT_BASE_URL,
508
+ }),
509
+ auth: Auth.none,
510
+ framing: Framing.sse,
511
+ });
512
+ export * as Gemini from "./gemini.js";
@@ -0,0 +1,30 @@
1
+ import { ImageModel } from "../image.js";
2
+ import { type Definition as AuthDefinition } from "../route/auth.js";
3
+ import { type HttpOptions } from "../schema/index.js";
4
+ export declare const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
5
+ export type GoogleImageString<Known extends string> = Known | (string & {});
6
+ export type GoogleImageOptions = {
7
+ readonly aspectRatio?: GoogleImageString<"1:1" | "2:3" | "3:2" | "3:4" | "4:3" | "4:5" | "5:4" | "9:16" | "16:9" | "21:9">;
8
+ readonly imageSize?: GoogleImageString<"1K" | "2K" | "4K">;
9
+ readonly seed?: number;
10
+ readonly thinkingLevel?: GoogleImageString<"MINIMAL" | "LOW" | "MEDIUM" | "HIGH">;
11
+ readonly includeThoughts?: boolean;
12
+ } & Record<string, unknown>;
13
+ export type GoogleImageBody = Record<string, unknown> & {
14
+ readonly contents: ReadonlyArray<{
15
+ readonly role: "user";
16
+ readonly parts: ReadonlyArray<Record<string, unknown>>;
17
+ }>;
18
+ readonly generationConfig: Record<string, unknown>;
19
+ };
20
+ export interface ModelInput {
21
+ readonly id: string;
22
+ readonly auth: AuthDefinition;
23
+ readonly baseURL?: string;
24
+ readonly headers?: Record<string, string>;
25
+ readonly http?: HttpOptions;
26
+ }
27
+ export declare const model: (input: ModelInput) => ImageModel<GoogleImageOptions>;
28
+ export declare const GoogleImages: {
29
+ readonly model: (input: ModelInput) => ImageModel<GoogleImageOptions>;
30
+ };