@opencode/ai 0.0.0-reserved → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +879 -2
- package/dist/cache-policy.d.ts +2 -0
- package/dist/cache-policy.js +129 -0
- package/dist/image-client.d.ts +19 -0
- package/dist/image-client.js +28 -0
- package/dist/image.d.ts +110 -0
- package/dist/image.js +103 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +12 -0
- package/dist/llm.d.ts +252 -0
- package/dist/llm.js +74 -0
- package/dist/protocols/alibaba-chat.d.ts +225 -0
- package/dist/protocols/alibaba-chat.js +75 -0
- package/dist/protocols/alibaba-messages.d.ts +350 -0
- package/dist/protocols/alibaba-messages.js +40 -0
- package/dist/protocols/alibaba-responses.d.ts +255 -0
- package/dist/protocols/alibaba-responses.js +80 -0
- package/dist/protocols/anthropic-messages.d.ts +980 -0
- package/dist/protocols/anthropic-messages.js +1460 -0
- package/dist/protocols/bedrock-converse.d.ts +499 -0
- package/dist/protocols/bedrock-converse.js +643 -0
- package/dist/protocols/bedrock-event-stream.d.ts +9 -0
- package/dist/protocols/bedrock-event-stream.js +97 -0
- package/dist/protocols/gemini.d.ts +285 -0
- package/dist/protocols/gemini.js +671 -0
- package/dist/protocols/google-images.d.ts +30 -0
- package/dist/protocols/google-images.js +181 -0
- package/dist/protocols/index.d.ts +12 -0
- package/dist/protocols/index.js +12 -0
- package/dist/protocols/meta-images.d.ts +26 -0
- package/dist/protocols/meta-images.js +89 -0
- package/dist/protocols/meta-messages.d.ts +356 -0
- package/dist/protocols/meta-messages.js +41 -0
- package/dist/protocols/meta-responses.d.ts +473 -0
- package/dist/protocols/meta-responses.js +173 -0
- package/dist/protocols/mistral-chat.d.ts +540 -0
- package/dist/protocols/mistral-chat.js +624 -0
- package/dist/protocols/open-responses-channel.d.ts +25 -0
- package/dist/protocols/open-responses-channel.js +141 -0
- package/dist/protocols/open-responses-continuation.d.ts +16 -0
- package/dist/protocols/open-responses-continuation.js +170 -0
- package/dist/protocols/open-responses.d.ts +1715 -0
- package/dist/protocols/open-responses.js +1228 -0
- package/dist/protocols/openai-chat.d.ts +860 -0
- package/dist/protocols/openai-chat.js +966 -0
- package/dist/protocols/openai-compatible-chat.d.ts +109 -0
- package/dist/protocols/openai-compatible-chat.js +19 -0
- package/dist/protocols/openai-compatible-responses.d.ts +172 -0
- package/dist/protocols/openai-compatible-responses.js +18 -0
- package/dist/protocols/openai-images.d.ts +32 -0
- package/dist/protocols/openai-images.js +183 -0
- package/dist/protocols/openai-responses.d.ts +1389 -0
- package/dist/protocols/openai-responses.js +260 -0
- package/dist/protocols/shared.d.ts +193 -0
- package/dist/protocols/shared.js +290 -0
- package/dist/protocols/utils/bedrock-auth.d.ts +60 -0
- package/dist/protocols/utils/bedrock-auth.js +85 -0
- package/dist/protocols/utils/bedrock-cache.d.ts +19 -0
- package/dist/protocols/utils/bedrock-cache.js +49 -0
- package/dist/protocols/utils/bedrock-media.d.ts +55 -0
- package/dist/protocols/utils/bedrock-media.js +73 -0
- package/dist/protocols/utils/cache.d.ts +6 -0
- package/dist/protocols/utils/cache.js +5 -0
- package/dist/protocols/utils/gemini-tool-schema.d.ts +2 -0
- package/dist/protocols/utils/gemini-tool-schema.js +103 -0
- package/dist/protocols/utils/image-input.d.ts +21 -0
- package/dist/protocols/utils/image-input.js +20 -0
- package/dist/protocols/utils/lifecycle.d.ts +24 -0
- package/dist/protocols/utils/lifecycle.js +72 -0
- package/dist/protocols/utils/meta-image.d.ts +2 -0
- package/dist/protocols/utils/meta-image.js +13 -0
- package/dist/protocols/utils/open-responses-options.d.ts +56 -0
- package/dist/protocols/utils/open-responses-options.js +61 -0
- package/dist/protocols/utils/openai-image.d.ts +5 -0
- package/dist/protocols/utils/openai-image.js +18 -0
- package/dist/protocols/utils/openai-options.d.ts +16 -0
- package/dist/protocols/utils/openai-options.js +14 -0
- package/dist/protocols/utils/partial-json-options.d.ts +62 -0
- package/dist/protocols/utils/partial-json-options.js +52 -0
- package/dist/protocols/utils/partial-json.d.ts +8 -0
- package/dist/protocols/utils/partial-json.js +279 -0
- package/dist/protocols/utils/responses-checkpoint.d.ts +4 -0
- package/dist/protocols/utils/responses-checkpoint.js +107 -0
- package/dist/protocols/utils/responses-compaction.d.ts +4 -0
- package/dist/protocols/utils/responses-compaction.js +129 -0
- package/dist/protocols/utils/responses-hosted-tools.d.ts +26 -0
- package/dist/protocols/utils/responses-hosted-tools.js +32 -0
- package/dist/protocols/utils/tool-schema.d.ts +8 -0
- package/dist/protocols/utils/tool-schema.js +54 -0
- package/dist/protocols/utils/tool-stream.d.ts +710 -0
- package/dist/protocols/utils/tool-stream.js +155 -0
- package/dist/protocols/xai-images.d.ts +26 -0
- package/dist/protocols/xai-images.js +106 -0
- package/dist/protocols/xai-responses.d.ts +250 -0
- package/dist/protocols/xai-responses.js +81 -0
- package/dist/protocols/zai-chat.d.ts +202 -0
- package/dist/protocols/zai-chat.js +49 -0
- package/dist/protocols/zai-images.d.ts +22 -0
- package/dist/protocols/zai-images.js +79 -0
- package/dist/protocols/zai-messages.d.ts +344 -0
- package/dist/protocols/zai-messages.js +27 -0
- package/dist/protocols.d.ts +1 -0
- package/dist/protocols.js +1 -0
- package/dist/provider-error.d.ts +15 -0
- package/dist/provider-error.js +125 -0
- package/dist/provider-package.d.ts +15 -0
- package/dist/provider-package.js +1 -0
- package/dist/provider.d.ts +23 -0
- package/dist/provider.js +2 -0
- package/dist/providers/alibaba/chat.d.ts +1 -0
- package/dist/providers/alibaba/chat.js +1 -0
- package/dist/providers/alibaba/messages.d.ts +3 -0
- package/dist/providers/alibaba/messages.js +1 -0
- package/dist/providers/alibaba/responses.d.ts +3 -0
- package/dist/providers/alibaba/responses.js +1 -0
- package/dist/providers/alibaba.d.ts +711 -0
- package/dist/providers/alibaba.js +94 -0
- package/dist/providers/amazon-bedrock/mantle/chat.d.ts +2 -0
- package/dist/providers/amazon-bedrock/mantle/chat.js +1 -0
- package/dist/providers/amazon-bedrock/mantle/responses.d.ts +2 -0
- package/dist/providers/amazon-bedrock/mantle/responses.js +1 -0
- package/dist/providers/amazon-bedrock/mantle.d.ts +2 -0
- package/dist/providers/amazon-bedrock/mantle.js +1 -0
- package/dist/providers/amazon-bedrock-mantle.d.ts +313 -0
- package/dist/providers/amazon-bedrock-mantle.js +79 -0
- package/dist/providers/amazon-bedrock.d.ts +173 -0
- package/dist/providers/amazon-bedrock.js +41 -0
- package/dist/providers/anthropic-compatible.d.ts +309 -0
- package/dist/providers/anthropic-compatible.js +50 -0
- package/dist/providers/anthropic.d.ts +307 -0
- package/dist/providers/anthropic.js +42 -0
- package/dist/providers/azure/chat.d.ts +2 -0
- package/dist/providers/azure/chat.js +1 -0
- package/dist/providers/azure/responses.d.ts +2 -0
- package/dist/providers/azure/responses.js +1 -0
- package/dist/providers/azure.d.ts +598 -0
- package/dist/providers/azure.js +115 -0
- package/dist/providers/baseten.d.ts +228 -0
- package/dist/providers/baseten.js +38 -0
- package/dist/providers/cerebras.d.ts +227 -0
- package/dist/providers/cerebras.js +40 -0
- package/dist/providers/cloudflare-ai-gateway.d.ts +237 -0
- package/dist/providers/cloudflare-ai-gateway.js +65 -0
- package/dist/providers/cloudflare-workers-ai.d.ts +231 -0
- package/dist/providers/cloudflare-workers-ai.js +51 -0
- package/dist/providers/deepinfra.d.ts +227 -0
- package/dist/providers/deepinfra.js +43 -0
- package/dist/providers/deepseek.d.ts +228 -0
- package/dist/providers/deepseek.js +41 -0
- package/dist/providers/fireworks.d.ts +228 -0
- package/dist/providers/fireworks.js +38 -0
- package/dist/providers/google-vertex/chat.d.ts +2 -0
- package/dist/providers/google-vertex/chat.js +1 -0
- package/dist/providers/google-vertex/gemini.d.ts +2 -0
- package/dist/providers/google-vertex/gemini.js +1 -0
- package/dist/providers/google-vertex/messages.d.ts +2 -0
- package/dist/providers/google-vertex/messages.js +1 -0
- package/dist/providers/google-vertex/responses.d.ts +2 -0
- package/dist/providers/google-vertex/responses.js +1 -0
- package/dist/providers/google-vertex-chat.d.ts +132 -0
- package/dist/providers/google-vertex-chat.js +55 -0
- package/dist/providers/google-vertex-messages.d.ts +306 -0
- package/dist/providers/google-vertex-messages.js +75 -0
- package/dist/providers/google-vertex-responses.d.ts +197 -0
- package/dist/providers/google-vertex-responses.js +56 -0
- package/dist/providers/google-vertex-shared.d.ts +22 -0
- package/dist/providers/google-vertex-shared.js +72 -0
- package/dist/providers/google-vertex.d.ts +119 -0
- package/dist/providers/google-vertex.js +98 -0
- package/dist/providers/google.d.ts +114 -0
- package/dist/providers/google.js +42 -0
- package/dist/providers/groq.d.ts +314 -0
- package/dist/providers/groq.js +81 -0
- package/dist/providers/index.d.ts +31 -0
- package/dist/providers/index.js +31 -0
- package/dist/providers/meta/chat.d.ts +2 -0
- package/dist/providers/meta/chat.js +1 -0
- package/dist/providers/meta/messages.d.ts +2 -0
- package/dist/providers/meta/messages.js +1 -0
- package/dist/providers/meta/responses.d.ts +2 -0
- package/dist/providers/meta/responses.js +1 -0
- package/dist/providers/meta.d.ts +636 -0
- package/dist/providers/meta.js +113 -0
- package/dist/providers/minimax/chat.d.ts +4 -0
- package/dist/providers/minimax/chat.js +8 -0
- package/dist/providers/minimax/messages.d.ts +1 -0
- package/dist/providers/minimax/messages.js +1 -0
- package/dist/providers/minimax/responses.d.ts +4 -0
- package/dist/providers/minimax/responses.js +8 -0
- package/dist/providers/minimax.d.ts +598 -0
- package/dist/providers/minimax.js +89 -0
- package/dist/providers/mistral.d.ts +207 -0
- package/dist/providers/mistral.js +28 -0
- package/dist/providers/moonshot/chat.d.ts +1 -0
- package/dist/providers/moonshot/chat.js +1 -0
- package/dist/providers/moonshot/messages.d.ts +4 -0
- package/dist/providers/moonshot/messages.js +8 -0
- package/dist/providers/moonshot/responses.d.ts +4 -0
- package/dist/providers/moonshot/responses.js +8 -0
- package/dist/providers/moonshot.d.ts +596 -0
- package/dist/providers/moonshot.js +90 -0
- package/dist/providers/open-responses-options.d.ts +6 -0
- package/dist/providers/open-responses-options.js +1 -0
- package/dist/providers/openai/chat.d.ts +2 -0
- package/dist/providers/openai/chat.js +1 -0
- package/dist/providers/openai/responses.d.ts +2 -0
- package/dist/providers/openai/responses.js +1 -0
- package/dist/providers/openai-compatible/responses.d.ts +1 -0
- package/dist/providers/openai-compatible/responses.js +1 -0
- package/dist/providers/openai-compatible-responses.d.ts +195 -0
- package/dist/providers/openai-compatible-responses.js +32 -0
- package/dist/providers/openai-compatible.d.ts +130 -0
- package/dist/providers/openai-compatible.js +33 -0
- package/dist/providers/openai-options.d.ts +26 -0
- package/dist/providers/openai-options.js +41 -0
- package/dist/providers/openai.d.ts +404 -0
- package/dist/providers/openai.js +82 -0
- package/dist/providers/openrouter.d.ts +563 -0
- package/dist/providers/openrouter.js +114 -0
- package/dist/providers/togetherai.d.ts +227 -0
- package/dist/providers/togetherai.js +40 -0
- package/dist/providers/xai.d.ts +163 -0
- package/dist/providers/xai.js +88 -0
- package/dist/providers/zai/chat.d.ts +1 -0
- package/dist/providers/zai/chat.js +1 -0
- package/dist/providers/zai-coding-plan/chat.d.ts +1 -0
- package/dist/providers/zai-coding-plan/chat.js +1 -0
- package/dist/providers/zai-coding-plan/messages.d.ts +4 -0
- package/dist/providers/zai-coding-plan/messages.js +8 -0
- package/dist/providers/zai-coding-plan/responses.d.ts +4 -0
- package/dist/providers/zai-coding-plan/responses.js +8 -0
- package/dist/providers/zai-coding-plan.d.ts +577 -0
- package/dist/providers/zai-coding-plan.js +63 -0
- package/dist/providers/zai.d.ts +149 -0
- package/dist/providers/zai.js +53 -0
- package/dist/providers.d.ts +1 -0
- package/dist/providers.js +1 -0
- package/dist/route/auth-options.d.ts +34 -0
- package/dist/route/auth-options.js +14 -0
- package/dist/route/auth.d.ts +51 -0
- package/dist/route/auth.js +87 -0
- package/dist/route/client.d.ts +430 -0
- package/dist/route/client.js +307 -0
- package/dist/route/endpoint.d.ts +28 -0
- package/dist/route/endpoint.js +21 -0
- package/dist/route/executor.d.ts +27 -0
- package/dist/route/executor.js +183 -0
- package/dist/route/framing.d.ts +29 -0
- package/dist/route/framing.js +14 -0
- package/dist/route/index.d.ts +16 -0
- package/dist/route/index.js +9 -0
- package/dist/route/protocol.d.ts +76 -0
- package/dist/route/protocol.js +16 -0
- package/dist/route/transport/http.d.ts +34 -0
- package/dist/route/transport/http.js +68 -0
- package/dist/route/transport/index.d.ts +41 -0
- package/dist/route/transport/index.js +4 -0
- package/dist/route/transport/websocket-channel.d.ts +57 -0
- package/dist/route/transport/websocket-channel.js +1 -0
- package/dist/route/transport/websocket.d.ts +56 -0
- package/dist/route/transport/websocket.js +370 -0
- package/dist/route.d.ts +1 -0
- package/dist/route.js +1 -0
- package/dist/schema/errors.d.ts +170 -0
- package/dist/schema/errors.js +131 -0
- package/dist/schema/events.d.ts +8123 -0
- package/dist/schema/events.js +550 -0
- package/dist/schema/ids.d.ts +17 -0
- package/dist/schema/ids.js +10 -0
- package/dist/schema/index.d.ts +5 -0
- package/dist/schema/index.js +5 -0
- package/dist/schema/messages.d.ts +646 -0
- package/dist/schema/messages.js +301 -0
- package/dist/schema/options.d.ts +159 -0
- package/dist/schema/options.js +189 -0
- package/dist/testing.d.ts +1693 -0
- package/dist/testing.js +129 -0
- package/dist/tool-history.d.ts +2 -0
- package/dist/tool-history.js +84 -0
- package/dist/tool-runtime.d.ts +15 -0
- package/dist/tool-runtime.js +62 -0
- package/dist/tool.d.ts +136 -0
- package/dist/tool.js +66 -0
- package/dist/utils/record.d.ts +2 -0
- package/dist/utils/record.js +2 -0
- package/dist/utils/sanitize.d.ts +1 -0
- package/dist/utils/sanitize.js +12 -0
- package/package.json +43 -10
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
import { Effect, Option, Schema } from "effect";
|
|
2
|
+
import { Tool } from "@opencode/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 { AIError, LLMEvent, Usage, } from "../schema/index.js";
|
|
9
|
+
import { classifyProviderFailure } from "../provider-error.js";
|
|
10
|
+
import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js";
|
|
11
|
+
import { GeminiToolSchema } from "./utils/gemini-tool-schema.js";
|
|
12
|
+
import { Lifecycle } from "./utils/lifecycle.js";
|
|
13
|
+
import { ToolSchemaProjection } from "./utils/tool-schema.js";
|
|
14
|
+
const ADAPTER = "gemini";
|
|
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
|
+
// Gemini 3 accepts media nested inside function responses; matched Gemini 2.5 variants reject it,
|
|
31
|
+
// so their tool-result attachments lower as a separate user turn instead.
|
|
32
|
+
const routesLegacyToolMedia = (modelID) => /gemini-2[.-]5(?:[.-]|$)/i.test(modelID);
|
|
33
|
+
// Blacklist: Gemini 1.x/2.x ignore or reject explicit function call ids.
|
|
34
|
+
// Every other model id (Gemini 3+, gemma, anything unrecognized) gets them.
|
|
35
|
+
const omitsFunctionCallIds = (modelID) => {
|
|
36
|
+
const match = /^gemini(?:-live)?-(\d+)/i.exec(modelID);
|
|
37
|
+
return match !== null && Number(match[1]) < 3;
|
|
38
|
+
};
|
|
39
|
+
// =============================================================================
|
|
40
|
+
// Request Body Schema
|
|
41
|
+
// =============================================================================
|
|
42
|
+
// Gemini is known to send explicit `null` for optional streaming fields
|
|
43
|
+
// (usage counts, flags, whole subtrees), so every response-side optional uses
|
|
44
|
+
// `optionalNull` instead of bare `Schema.optional`. The same part/content
|
|
45
|
+
// schemas lower the outbound request body; encoding drops `undefined` keys,
|
|
46
|
+
// so the shared schemas stay safe there.
|
|
47
|
+
const GeminiTextPart = Schema.Struct({
|
|
48
|
+
text: Schema.String,
|
|
49
|
+
thought: optionalNull(Schema.Boolean),
|
|
50
|
+
thoughtSignature: optionalNull(Schema.String),
|
|
51
|
+
});
|
|
52
|
+
const GeminiInlineDataPart = Schema.Struct({
|
|
53
|
+
inlineData: Schema.Struct({
|
|
54
|
+
mimeType: Schema.String,
|
|
55
|
+
data: Schema.String,
|
|
56
|
+
}),
|
|
57
|
+
});
|
|
58
|
+
const GeminiFunctionCallPart = Schema.Struct({
|
|
59
|
+
functionCall: Schema.Struct({
|
|
60
|
+
id: optionalNull(Schema.String),
|
|
61
|
+
name: Schema.String,
|
|
62
|
+
args: Schema.optional(Schema.Unknown),
|
|
63
|
+
}),
|
|
64
|
+
thoughtSignature: optionalNull(Schema.String),
|
|
65
|
+
});
|
|
66
|
+
const GeminiFunctionResponsePart = Schema.Struct({
|
|
67
|
+
functionResponse: Schema.Struct({
|
|
68
|
+
id: Schema.optional(Schema.String),
|
|
69
|
+
name: Schema.String,
|
|
70
|
+
response: Schema.Unknown,
|
|
71
|
+
parts: Schema.optional(Schema.Array(GeminiInlineDataPart)),
|
|
72
|
+
}),
|
|
73
|
+
});
|
|
74
|
+
const GeminiContentPart = Schema.Union([
|
|
75
|
+
GeminiTextPart,
|
|
76
|
+
GeminiInlineDataPart,
|
|
77
|
+
GeminiFunctionCallPart,
|
|
78
|
+
GeminiFunctionResponsePart,
|
|
79
|
+
]);
|
|
80
|
+
const decodeGeminiContentPart = Schema.decodeUnknownOption(GeminiContentPart);
|
|
81
|
+
const GeminiContent = Schema.Struct({
|
|
82
|
+
role: optionalNull(Schema.Literals(["user", "model"])),
|
|
83
|
+
parts: optionalNull(Schema.Array(GeminiContentPart)),
|
|
84
|
+
});
|
|
85
|
+
const GeminiResponseContent = Schema.Struct({
|
|
86
|
+
role: optionalNull(Schema.Literals(["user", "model"])),
|
|
87
|
+
parts: optionalNull(Schema.Array(Schema.Unknown)),
|
|
88
|
+
});
|
|
89
|
+
const GeminiSystemInstruction = Schema.Struct({
|
|
90
|
+
parts: Schema.Array(Schema.Struct({ text: Schema.String })),
|
|
91
|
+
});
|
|
92
|
+
const GeminiFunctionDeclaration = Schema.Struct({
|
|
93
|
+
name: Schema.String,
|
|
94
|
+
description: Schema.String,
|
|
95
|
+
parameters: Schema.optional(JsonObject),
|
|
96
|
+
});
|
|
97
|
+
const GeminiTool = Schema.Struct({
|
|
98
|
+
functionDeclarations: Schema.Array(GeminiFunctionDeclaration),
|
|
99
|
+
});
|
|
100
|
+
const GeminiToolConfig = Schema.Struct({
|
|
101
|
+
functionCallingConfig: Schema.Struct({
|
|
102
|
+
mode: Schema.Literals(["AUTO", "NONE", "ANY"]),
|
|
103
|
+
allowedFunctionNames: optionalArray(Schema.String),
|
|
104
|
+
}),
|
|
105
|
+
});
|
|
106
|
+
const GeminiThinkingConfig = Schema.Struct({
|
|
107
|
+
thinkingBudget: Schema.optional(Schema.Number),
|
|
108
|
+
includeThoughts: Schema.optional(Schema.Boolean),
|
|
109
|
+
thinkingLevel: Schema.optional(Schema.String),
|
|
110
|
+
});
|
|
111
|
+
const GeminiSafetySetting = Schema.Struct({
|
|
112
|
+
category: Schema.String,
|
|
113
|
+
threshold: Schema.String,
|
|
114
|
+
});
|
|
115
|
+
const GeminiGenerationConfig = Schema.Struct({
|
|
116
|
+
maxOutputTokens: Schema.optional(Schema.Number),
|
|
117
|
+
temperature: Schema.optional(Schema.Number),
|
|
118
|
+
topP: Schema.optional(Schema.Number),
|
|
119
|
+
topK: Schema.optional(Schema.Number),
|
|
120
|
+
frequencyPenalty: Schema.optional(Schema.Number),
|
|
121
|
+
presencePenalty: Schema.optional(Schema.Number),
|
|
122
|
+
seed: Schema.optional(Schema.Number),
|
|
123
|
+
stopSequences: optionalArray(Schema.String),
|
|
124
|
+
thinkingConfig: Schema.optional(GeminiThinkingConfig),
|
|
125
|
+
});
|
|
126
|
+
const GeminiBodyFields = {
|
|
127
|
+
cachedContent: Schema.optional(Schema.String),
|
|
128
|
+
contents: Schema.Array(GeminiContent),
|
|
129
|
+
labels: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
|
130
|
+
safetySettings: optionalArray(GeminiSafetySetting),
|
|
131
|
+
serviceTier: Schema.optional(Schema.String),
|
|
132
|
+
systemInstruction: Schema.optional(GeminiSystemInstruction),
|
|
133
|
+
tools: optionalArray(GeminiTool),
|
|
134
|
+
toolConfig: Schema.optional(GeminiToolConfig),
|
|
135
|
+
generationConfig: Schema.optional(GeminiGenerationConfig),
|
|
136
|
+
};
|
|
137
|
+
const GeminiBody = Schema.Struct(GeminiBodyFields);
|
|
138
|
+
const GeminiUsage = Schema.Struct({
|
|
139
|
+
cachedContentTokenCount: optionalNull(Schema.Number),
|
|
140
|
+
thoughtsTokenCount: optionalNull(Schema.Number),
|
|
141
|
+
promptTokenCount: optionalNull(Schema.Number),
|
|
142
|
+
candidatesTokenCount: optionalNull(Schema.Number),
|
|
143
|
+
totalTokenCount: optionalNull(Schema.Number),
|
|
144
|
+
});
|
|
145
|
+
const GeminiCandidate = Schema.Struct({
|
|
146
|
+
content: optionalNull(GeminiResponseContent),
|
|
147
|
+
finishReason: optionalNull(Schema.String),
|
|
148
|
+
});
|
|
149
|
+
const GeminiPromptFeedback = Schema.StructWithRest(Schema.Struct({
|
|
150
|
+
blockReason: optionalNull(Schema.String),
|
|
151
|
+
blockReasonMessage: optionalNull(Schema.String),
|
|
152
|
+
safetyRatings: optionalNull(Schema.Unknown),
|
|
153
|
+
}), [Schema.Record(Schema.String, Schema.Unknown)]);
|
|
154
|
+
const GeminiEvent = Schema.Struct({
|
|
155
|
+
error: Schema.optional(Schema.Unknown),
|
|
156
|
+
candidates: optionalNull(Schema.Array(GeminiCandidate)),
|
|
157
|
+
promptFeedback: optionalNull(GeminiPromptFeedback),
|
|
158
|
+
usageMetadata: optionalNull(GeminiUsage),
|
|
159
|
+
});
|
|
160
|
+
// =============================================================================
|
|
161
|
+
// Tool Schema Conversion
|
|
162
|
+
// =============================================================================
|
|
163
|
+
// Tool-schema conversion has two distinct concerns:
|
|
164
|
+
//
|
|
165
|
+
// 1. Sanitize — fix common authoring mistakes Gemini rejects: integer/number
|
|
166
|
+
// enums (must be strings), `required` entries that don't match a property,
|
|
167
|
+
// untyped arrays (`items` must be present), and `properties`/`required`
|
|
168
|
+
// keys on non-object scalars. Mirrors OpenCode's historical Gemini rules.
|
|
169
|
+
//
|
|
170
|
+
// 2. Project — lossy mapping from JSON Schema to Gemini's schema dialect:
|
|
171
|
+
// drop empty root parameter schemas while preserving nested empty objects,
|
|
172
|
+
// expand type arrays into `anyOf`, derive `nullable: true` from null members,
|
|
173
|
+
// coerce `const` to `[const]` enum, recurse properties/items, and propagate
|
|
174
|
+
// only an allowlisted set of keys (description, required, format, type,
|
|
175
|
+
// nullable, enum, properties, items, allOf, anyOf, oneOf, minLength).
|
|
176
|
+
// Anything outside the allowlist (e.g. `additionalProperties`, `$ref`) is
|
|
177
|
+
// silently dropped.
|
|
178
|
+
//
|
|
179
|
+
// Sanitize runs first, then project. The implementation lives in
|
|
180
|
+
// `utils/gemini-tool-schema` so this protocol keeps the same shape as the other
|
|
181
|
+
// provider protocols.
|
|
182
|
+
// =============================================================================
|
|
183
|
+
// Request Lowering
|
|
184
|
+
// =============================================================================
|
|
185
|
+
const lowerTool = (tool, inputSchema) => ({
|
|
186
|
+
name: tool.name,
|
|
187
|
+
description: tool.description,
|
|
188
|
+
parameters: GeminiToolSchema.convert(inputSchema),
|
|
189
|
+
});
|
|
190
|
+
const lowerToolConfig = (toolChoice) => ProviderShared.matchToolChoice("Gemini", toolChoice, {
|
|
191
|
+
auto: () => ({ functionCallingConfig: { mode: "AUTO" } }),
|
|
192
|
+
none: () => ({ functionCallingConfig: { mode: "NONE" } }),
|
|
193
|
+
required: () => ({ functionCallingConfig: { mode: "ANY" } }),
|
|
194
|
+
tool: (name) => ({ functionCallingConfig: { mode: "ANY", allowedFunctionNames: [name] } }),
|
|
195
|
+
});
|
|
196
|
+
const lowerUserPart = Effect.fn("Gemini.lowerUserPart")(function* (part) {
|
|
197
|
+
if (part.type === "text")
|
|
198
|
+
return { text: part.text };
|
|
199
|
+
const media = ProviderShared.normalizeMedia(part);
|
|
200
|
+
return { inlineData: { mimeType: media.mime, data: media.base64 } };
|
|
201
|
+
});
|
|
202
|
+
const providerMetadata = (key, metadata) => ({ [key]: metadata });
|
|
203
|
+
const thoughtSignature = (metadata, key) => {
|
|
204
|
+
const value = metadata?.[key];
|
|
205
|
+
return ProviderShared.isRecord(value) && typeof value.thoughtSignature === "string"
|
|
206
|
+
? value.thoughtSignature
|
|
207
|
+
: undefined;
|
|
208
|
+
};
|
|
209
|
+
const lowerToolCall = (part, omitIds, metadataKey) => ({
|
|
210
|
+
functionCall: { ...(omitIds ? {} : { id: part.id }), name: part.name, args: part.input },
|
|
211
|
+
thoughtSignature: thoughtSignature(part.providerMetadata, metadataKey),
|
|
212
|
+
});
|
|
213
|
+
const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request) {
|
|
214
|
+
const contents = [];
|
|
215
|
+
const metadataKey = request.model.route.providerMetadataKey ?? String(request.model.provider);
|
|
216
|
+
const omitCallIds = omitsFunctionCallIds(request.model.id);
|
|
217
|
+
const legacyToolMedia = routesLegacyToolMedia(request.model.id);
|
|
218
|
+
let pendingMedia;
|
|
219
|
+
const flushMedia = () => {
|
|
220
|
+
if (!pendingMedia)
|
|
221
|
+
return;
|
|
222
|
+
contents.push({ role: "user", parts: [{ text: "Attached media from tool result:" }, ...pendingMedia] });
|
|
223
|
+
pendingMedia = undefined;
|
|
224
|
+
};
|
|
225
|
+
for (const message of request.messages) {
|
|
226
|
+
if (message.role !== "tool")
|
|
227
|
+
flushMedia();
|
|
228
|
+
if (message.role === "system") {
|
|
229
|
+
const part = yield* ProviderShared.wrappedSystemUpdate("Gemini", message);
|
|
230
|
+
const previous = contents.at(-1);
|
|
231
|
+
// Gemini rejects a continuation whose function-response turn carries extra
|
|
232
|
+
// parts, so an update after a tool result starts its own user turn.
|
|
233
|
+
if (previous?.role === "user" && !(previous.parts ?? []).some((item) => "functionResponse" in item))
|
|
234
|
+
contents[contents.length - 1] = { role: "user", parts: [...(previous.parts ?? []), { text: part.text }] };
|
|
235
|
+
else
|
|
236
|
+
contents.push({ role: "user", parts: [{ text: part.text }] });
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (message.role === "user") {
|
|
240
|
+
const parts = [];
|
|
241
|
+
for (const part of message.content) {
|
|
242
|
+
if (!ProviderShared.supportsContent(part, ["text", "media"]))
|
|
243
|
+
return yield* ProviderShared.unsupportedContent("Gemini", "user", ["text", "media"]);
|
|
244
|
+
parts.push(yield* lowerUserPart(part));
|
|
245
|
+
}
|
|
246
|
+
contents.push({ role: "user", parts });
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (message.role === "assistant") {
|
|
250
|
+
const parts = [];
|
|
251
|
+
// Parallel Gemini 3 calls may carry one signature on the first call; unsigned sibling calls are valid.
|
|
252
|
+
let hasSignedToolCall = false;
|
|
253
|
+
for (const part of message.content) {
|
|
254
|
+
if (!ProviderShared.supportsContent(part, ["text", "reasoning", "tool-call"]))
|
|
255
|
+
return yield* ProviderShared.unsupportedContent("Gemini", "assistant", ["text", "reasoning", "tool-call"]);
|
|
256
|
+
if (part.type === "text") {
|
|
257
|
+
parts.push({ text: part.text, thoughtSignature: thoughtSignature(part.providerMetadata, metadataKey) });
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (part.type === "reasoning") {
|
|
261
|
+
parts.push({
|
|
262
|
+
text: part.text,
|
|
263
|
+
thought: true,
|
|
264
|
+
thoughtSignature: thoughtSignature(part.providerMetadata, metadataKey),
|
|
265
|
+
});
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (part.type === "tool-call") {
|
|
269
|
+
const lowered = lowerToolCall(part, omitCallIds, metadataKey);
|
|
270
|
+
const signature = lowered.thoughtSignature;
|
|
271
|
+
parts.push({
|
|
272
|
+
...lowered,
|
|
273
|
+
thoughtSignature: signature ??
|
|
274
|
+
(requiresThoughtSignatureFallback(request.model.id) && !hasSignedToolCall
|
|
275
|
+
? SKIP_THOUGHT_SIGNATURE_VALIDATOR
|
|
276
|
+
: undefined),
|
|
277
|
+
});
|
|
278
|
+
if (signature !== undefined)
|
|
279
|
+
hasSignedToolCall = true;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
contents.push({ role: "model", parts });
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const parts = [];
|
|
287
|
+
for (const part of message.content) {
|
|
288
|
+
if (!ProviderShared.supportsContent(part, ["tool-result"]))
|
|
289
|
+
return yield* ProviderShared.unsupportedContent("Gemini", "tool", ["tool-result"]);
|
|
290
|
+
if (part.result.type !== "content") {
|
|
291
|
+
parts.push({
|
|
292
|
+
functionResponse: {
|
|
293
|
+
...(omitCallIds ? {} : { id: part.id }),
|
|
294
|
+
name: part.name,
|
|
295
|
+
response: {
|
|
296
|
+
name: part.name,
|
|
297
|
+
content: ProviderShared.toolResultText(part),
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
const content = part.result.value;
|
|
304
|
+
const text = content.filter((item) => item.type === "text").map((item) => item.text);
|
|
305
|
+
const media = [];
|
|
306
|
+
for (const item of content) {
|
|
307
|
+
if (item.type === "text")
|
|
308
|
+
continue;
|
|
309
|
+
const value = ProviderShared.normalizeToolFile(item);
|
|
310
|
+
media.push({ inlineData: { mimeType: value.mime, data: value.base64 } });
|
|
311
|
+
}
|
|
312
|
+
if (legacyToolMedia && media.length > 0)
|
|
313
|
+
(pendingMedia ??= []).push(...media);
|
|
314
|
+
parts.push({
|
|
315
|
+
functionResponse: {
|
|
316
|
+
...(omitCallIds ? {} : { id: part.id }),
|
|
317
|
+
name: part.name,
|
|
318
|
+
response: {
|
|
319
|
+
name: part.name,
|
|
320
|
+
content: text.join("\n"),
|
|
321
|
+
},
|
|
322
|
+
parts: legacyToolMedia || media.length === 0 ? undefined : media,
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
// Gemini requires every response to a parallel call batch in one user turn,
|
|
327
|
+
// so consecutive tool results join the open function-response turn.
|
|
328
|
+
const previous = contents.at(-1);
|
|
329
|
+
if (previous?.role === "user" && (previous.parts ?? []).some((item) => "functionResponse" in item))
|
|
330
|
+
contents[contents.length - 1] = { role: "user", parts: [...(previous.parts ?? []), ...parts] };
|
|
331
|
+
else
|
|
332
|
+
contents.push({ role: "user", parts });
|
|
333
|
+
}
|
|
334
|
+
flushMedia();
|
|
335
|
+
return contents;
|
|
336
|
+
});
|
|
337
|
+
const resolveOptions = (request) => {
|
|
338
|
+
const input = request.providerOptions;
|
|
339
|
+
const value = input?.thinkingConfig;
|
|
340
|
+
const thinkingConfig = {
|
|
341
|
+
thinkingBudget: ProviderShared.isRecord(value) && typeof value.thinkingBudget === "number" ? value.thinkingBudget : undefined,
|
|
342
|
+
includeThoughts: ProviderShared.isRecord(value) && typeof value.includeThoughts === "boolean"
|
|
343
|
+
? value.includeThoughts
|
|
344
|
+
: ProviderShared.isRecord(value)
|
|
345
|
+
? true
|
|
346
|
+
: undefined,
|
|
347
|
+
thinkingLevel: ProviderShared.isRecord(value) && typeof value.thinkingLevel === "string" ? value.thinkingLevel : undefined,
|
|
348
|
+
};
|
|
349
|
+
return {
|
|
350
|
+
cachedContent: typeof input?.cachedContent === "string" ? input.cachedContent : undefined,
|
|
351
|
+
safetySettings: mapSafetySettings(input?.safetySettings),
|
|
352
|
+
serviceTier: typeof input?.serviceTier === "string" ? input.serviceTier : undefined,
|
|
353
|
+
thinkingConfig: Object.values(thinkingConfig).some((item) => item !== undefined) ? thinkingConfig : undefined,
|
|
354
|
+
};
|
|
355
|
+
};
|
|
356
|
+
function mapSafetySettings(value) {
|
|
357
|
+
if (!Array.isArray(value))
|
|
358
|
+
return undefined;
|
|
359
|
+
const settings = value.flatMap((item) => ProviderShared.isRecord(item) && typeof item.category === "string" && typeof item.threshold === "string"
|
|
360
|
+
? [{ category: item.category, threshold: item.threshold }]
|
|
361
|
+
: []);
|
|
362
|
+
return settings;
|
|
363
|
+
}
|
|
364
|
+
const fromRequest = Effect.fn("Gemini.fromRequest")(function* (request) {
|
|
365
|
+
const flattened = ProviderShared.flattenToolRequest(request);
|
|
366
|
+
const hasTools = flattened.tools.length > 0;
|
|
367
|
+
const generation = request.generation;
|
|
368
|
+
const options = resolveOptions(request);
|
|
369
|
+
const toolSchemaCompatibility = request.model.compatibility?.toolSchema;
|
|
370
|
+
const generationConfig = {
|
|
371
|
+
maxOutputTokens: generation?.maxTokens,
|
|
372
|
+
temperature: generation?.temperature,
|
|
373
|
+
topP: generation?.topP,
|
|
374
|
+
topK: generation?.topK,
|
|
375
|
+
frequencyPenalty: generation?.frequencyPenalty,
|
|
376
|
+
presencePenalty: generation?.presencePenalty,
|
|
377
|
+
seed: generation?.seed,
|
|
378
|
+
stopSequences: generation?.stop,
|
|
379
|
+
thinkingConfig: options.thinkingConfig,
|
|
380
|
+
};
|
|
381
|
+
return {
|
|
382
|
+
cachedContent: options.cachedContent,
|
|
383
|
+
contents: yield* lowerMessages(flattened.request),
|
|
384
|
+
safetySettings: options.safetySettings,
|
|
385
|
+
serviceTier: options.serviceTier,
|
|
386
|
+
systemInstruction: request.system.length === 0 ? undefined : { parts: [{ text: ProviderShared.joinText(request.system) }] },
|
|
387
|
+
tools: hasTools
|
|
388
|
+
? [
|
|
389
|
+
{
|
|
390
|
+
functionDeclarations: flattened.tools.map((tool) => lowerTool(tool, ToolSchemaProjection.modelCompatibility(tool.inputSchema, toolSchemaCompatibility))),
|
|
391
|
+
},
|
|
392
|
+
]
|
|
393
|
+
: undefined,
|
|
394
|
+
toolConfig: hasTools && request.toolChoice ? yield* lowerToolConfig(request.toolChoice) : undefined,
|
|
395
|
+
generationConfig: Object.values(generationConfig).some((value) => value !== undefined)
|
|
396
|
+
? generationConfig
|
|
397
|
+
: undefined,
|
|
398
|
+
};
|
|
399
|
+
});
|
|
400
|
+
// =============================================================================
|
|
401
|
+
// Stream Parsing
|
|
402
|
+
// =============================================================================
|
|
403
|
+
// Gemini reports `promptTokenCount` (inclusive total) with a
|
|
404
|
+
// `cachedContentTokenCount` subset. `candidatesTokenCount` is *exclusive*
|
|
405
|
+
// of `thoughtsTokenCount` — visible-only, not a total — so we sum the two
|
|
406
|
+
// to produce the inclusive `outputTokens` the rest of the contract expects.
|
|
407
|
+
const mapUsage = (usage, metadataKey) => {
|
|
408
|
+
if (!usage)
|
|
409
|
+
return undefined;
|
|
410
|
+
// Explicit provider nulls decode as `null`; normalize to `undefined` so the
|
|
411
|
+
// token arithmetic below treats them like absent counts.
|
|
412
|
+
const promptTokens = usage.promptTokenCount ?? undefined;
|
|
413
|
+
const cached = usage.cachedContentTokenCount ?? undefined;
|
|
414
|
+
const thoughts = usage.thoughtsTokenCount ?? undefined;
|
|
415
|
+
const visible = usage.candidatesTokenCount ?? undefined;
|
|
416
|
+
const nonCached = ProviderShared.subtractTokens(promptTokens, cached);
|
|
417
|
+
// `candidatesTokenCount` is visible-only; sum with thoughts to produce the
|
|
418
|
+
// inclusive `outputTokens` the contract expects. Only compute the total
|
|
419
|
+
// when the visible component is reported — otherwise we'd fabricate an
|
|
420
|
+
// inclusive number from a partial breakdown.
|
|
421
|
+
const outputTokens = visible !== undefined ? visible + (thoughts ?? 0) : undefined;
|
|
422
|
+
return new Usage({
|
|
423
|
+
inputTokens: promptTokens,
|
|
424
|
+
outputTokens,
|
|
425
|
+
nonCachedInputTokens: nonCached,
|
|
426
|
+
cacheReadInputTokens: cached,
|
|
427
|
+
reasoningTokens: thoughts,
|
|
428
|
+
totalTokens: ProviderShared.totalTokens(promptTokens, outputTokens, usage.totalTokenCount ?? undefined),
|
|
429
|
+
providerMetadata: providerMetadata(metadataKey, usage),
|
|
430
|
+
});
|
|
431
|
+
};
|
|
432
|
+
const mapFinishReason = (finishReason, hasToolCalls) => {
|
|
433
|
+
if (finishReason === undefined)
|
|
434
|
+
return hasToolCalls ? "tool-calls" : "unknown";
|
|
435
|
+
if (finishReason === "STOP")
|
|
436
|
+
return hasToolCalls ? "tool-calls" : "stop";
|
|
437
|
+
if (finishReason === "MAX_TOKENS")
|
|
438
|
+
return "length";
|
|
439
|
+
if (finishReason === "IMAGE_SAFETY" ||
|
|
440
|
+
finishReason === "RECITATION" ||
|
|
441
|
+
finishReason === "SAFETY" ||
|
|
442
|
+
finishReason === "BLOCKLIST" ||
|
|
443
|
+
finishReason === "PROHIBITED_CONTENT" ||
|
|
444
|
+
finishReason === "SPII" ||
|
|
445
|
+
finishReason === "MODEL_ARMOR" ||
|
|
446
|
+
finishReason === "IMAGE_PROHIBITED_CONTENT" ||
|
|
447
|
+
finishReason === "IMAGE_RECITATION" ||
|
|
448
|
+
finishReason === "LANGUAGE")
|
|
449
|
+
return "content-filter";
|
|
450
|
+
if (finishReason === "MALFORMED_FUNCTION_CALL" ||
|
|
451
|
+
finishReason === "UNEXPECTED_TOOL_CALL" ||
|
|
452
|
+
finishReason === "NO_IMAGE" ||
|
|
453
|
+
finishReason === "TOO_MANY_TOOL_CALLS" ||
|
|
454
|
+
finishReason === "MISSING_THOUGHT_SIGNATURE" ||
|
|
455
|
+
finishReason === "MALFORMED_RESPONSE")
|
|
456
|
+
return "error";
|
|
457
|
+
return "unknown";
|
|
458
|
+
};
|
|
459
|
+
const finish = (state) => {
|
|
460
|
+
// `?? undefined` normalizes an explicit `null` blockReason back to absent so
|
|
461
|
+
// the "nothing to finish" check below keeps its meaning.
|
|
462
|
+
const promptBlockReason = state.finishReason === undefined ? (state.promptFeedback?.blockReason ?? undefined) : undefined;
|
|
463
|
+
const finishReason = state.finishReason ?? promptBlockReason;
|
|
464
|
+
if (finishReason === undefined && state.usage === undefined)
|
|
465
|
+
return [];
|
|
466
|
+
const events = [];
|
|
467
|
+
let lifecycle = state.lifecycle;
|
|
468
|
+
if (state.reasoningId !== undefined)
|
|
469
|
+
lifecycle = Lifecycle.reasoningEnd(lifecycle, events, state.reasoningId, state.reasoningSignature === undefined
|
|
470
|
+
? undefined
|
|
471
|
+
: providerMetadata(state.providerMetadataKey, { thoughtSignature: state.reasoningSignature }));
|
|
472
|
+
if (state.textId !== undefined)
|
|
473
|
+
lifecycle = Lifecycle.textEnd(lifecycle, events, state.textId, state.textSignature === undefined
|
|
474
|
+
? undefined
|
|
475
|
+
: providerMetadata(state.providerMetadataKey, { thoughtSignature: state.textSignature }));
|
|
476
|
+
Lifecycle.finish(lifecycle, events, {
|
|
477
|
+
reason: {
|
|
478
|
+
normalized: promptBlockReason === undefined ? mapFinishReason(finishReason, state.hasToolCalls) : "content-filter",
|
|
479
|
+
raw: finishReason,
|
|
480
|
+
},
|
|
481
|
+
usage: state.usage,
|
|
482
|
+
providerMetadata: state.promptFeedback === undefined
|
|
483
|
+
? undefined
|
|
484
|
+
: providerMetadata(state.providerMetadataKey, { promptFeedback: state.promptFeedback }),
|
|
485
|
+
});
|
|
486
|
+
return events;
|
|
487
|
+
};
|
|
488
|
+
const step = (state, event) => {
|
|
489
|
+
if (ProviderShared.isRecord(event.error)) {
|
|
490
|
+
const body = ProviderShared.encodeJson(event);
|
|
491
|
+
return Effect.fail(new AIError({
|
|
492
|
+
reason: classifyProviderFailure({
|
|
493
|
+
message: typeof event.error.message === "string" && event.error.message.length > 0
|
|
494
|
+
? event.error.message
|
|
495
|
+
: typeof event.error.status === "string" && event.error.status.length > 0
|
|
496
|
+
? event.error.status
|
|
497
|
+
: "Gemini provider error",
|
|
498
|
+
status: typeof event.error.code === "number" ? event.error.code : undefined,
|
|
499
|
+
rawBody: body,
|
|
500
|
+
}),
|
|
501
|
+
}));
|
|
502
|
+
}
|
|
503
|
+
if ("error" in event)
|
|
504
|
+
return Effect.fail(ProviderShared.eventError(state.route, `Invalid ${state.route} stream event`, ProviderShared.encodeJson(event)));
|
|
505
|
+
const nextState = {
|
|
506
|
+
...state,
|
|
507
|
+
promptFeedback: event.promptFeedback ?? state.promptFeedback,
|
|
508
|
+
usage: event.usageMetadata
|
|
509
|
+
? (mapUsage(event.usageMetadata, state.providerMetadataKey) ?? state.usage)
|
|
510
|
+
: state.usage,
|
|
511
|
+
};
|
|
512
|
+
const candidate = event.candidates?.[0];
|
|
513
|
+
if (candidate?.finishReason && mapFinishReason(candidate.finishReason, state.hasToolCalls) === "error")
|
|
514
|
+
return Effect.fail(ProviderShared.eventError(state.route, `Gemini stopped with ${candidate.finishReason}`, ProviderShared.encodeJson(event)));
|
|
515
|
+
if (!candidate?.content)
|
|
516
|
+
return Effect.succeed([
|
|
517
|
+
{ ...nextState, finishReason: candidate?.finishReason ?? nextState.finishReason },
|
|
518
|
+
[],
|
|
519
|
+
]);
|
|
520
|
+
const events = [];
|
|
521
|
+
let hasToolCalls = nextState.hasToolCalls;
|
|
522
|
+
let lifecycle = nextState.lifecycle;
|
|
523
|
+
let reasoningSignature = nextState.reasoningSignature;
|
|
524
|
+
let textSignature = nextState.textSignature;
|
|
525
|
+
let reasoningId = nextState.reasoningId;
|
|
526
|
+
let textId = nextState.textId;
|
|
527
|
+
let nextReasoningId = nextState.nextReasoningId;
|
|
528
|
+
let nextTextId = nextState.nextTextId;
|
|
529
|
+
// Supplier ids must be tracked across chunks of the same response, not just within one event's parts.
|
|
530
|
+
const seenCallIds = new Set(nextState.seenCallIds);
|
|
531
|
+
for (const input of candidate.content.parts ?? []) {
|
|
532
|
+
if (ProviderShared.isRecord(input) &&
|
|
533
|
+
!("text" in input) &&
|
|
534
|
+
!("inlineData" in input) &&
|
|
535
|
+
!("functionCall" in input) &&
|
|
536
|
+
!("functionResponse" in input))
|
|
537
|
+
continue;
|
|
538
|
+
const decoded = decodeGeminiContentPart(input);
|
|
539
|
+
if (Option.isNone(decoded))
|
|
540
|
+
return Effect.fail(ProviderShared.eventError(ADAPTER, `Invalid ${state.route} stream event`, ProviderShared.encodeJson(event)));
|
|
541
|
+
const part = decoded.value;
|
|
542
|
+
const signature = "thoughtSignature" in part && part.thoughtSignature ? part.thoughtSignature : undefined;
|
|
543
|
+
// Gemini attaches replay signatures to thought parts, visible text, or function calls;
|
|
544
|
+
// each block kind must retain the signature attached to its own parts.
|
|
545
|
+
if (signature !== undefined && "thought" in part && part.thought)
|
|
546
|
+
reasoningSignature = signature;
|
|
547
|
+
else if (signature !== undefined && "text" in part)
|
|
548
|
+
textSignature = signature;
|
|
549
|
+
if ("text" in part && part.text.length > 0) {
|
|
550
|
+
if (part.thought) {
|
|
551
|
+
if (textId !== undefined) {
|
|
552
|
+
lifecycle = Lifecycle.textEnd(lifecycle, events, textId, textSignature
|
|
553
|
+
? providerMetadata(state.providerMetadataKey, { thoughtSignature: textSignature })
|
|
554
|
+
: undefined);
|
|
555
|
+
textId = undefined;
|
|
556
|
+
textSignature = undefined;
|
|
557
|
+
}
|
|
558
|
+
if (reasoningId === undefined) {
|
|
559
|
+
reasoningId = `reasoning-${nextReasoningId}`;
|
|
560
|
+
nextReasoningId += 1;
|
|
561
|
+
}
|
|
562
|
+
lifecycle = Lifecycle.reasoningDelta(lifecycle, events, reasoningId, part.text, signature ? providerMetadata(state.providerMetadataKey, { thoughtSignature: signature }) : undefined);
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
if (reasoningId !== undefined) {
|
|
566
|
+
lifecycle = Lifecycle.reasoningEnd(lifecycle, events, reasoningId, reasoningSignature
|
|
567
|
+
? providerMetadata(state.providerMetadataKey, { thoughtSignature: reasoningSignature })
|
|
568
|
+
: undefined);
|
|
569
|
+
reasoningId = undefined;
|
|
570
|
+
reasoningSignature = undefined;
|
|
571
|
+
}
|
|
572
|
+
if (textId === undefined) {
|
|
573
|
+
textId = `text-${nextTextId}`;
|
|
574
|
+
nextTextId += 1;
|
|
575
|
+
}
|
|
576
|
+
lifecycle = Lifecycle.textDelta(lifecycle, events, textId, part.text, textSignature ? providerMetadata(state.providerMetadataKey, { thoughtSignature: textSignature }) : undefined);
|
|
577
|
+
textSignature = undefined;
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
if ("functionCall" in part) {
|
|
581
|
+
const input = part.functionCall.args === undefined ? {} : part.functionCall.args;
|
|
582
|
+
// Gemini 2.0+ supplies a unique function call ID on the part; when omitted (e.g. Gemini 1.5),
|
|
583
|
+
// generate a globally unique ID rather than a per-request counter to prevent cross-request collisions in downstream registries.
|
|
584
|
+
// A repeated supplier id would replay as two identical calls, so only the first occurrence keeps it.
|
|
585
|
+
// A `null` supplier id normalizes to absent so the generated-id fallback applies.
|
|
586
|
+
const supplied = part.functionCall.id ?? undefined;
|
|
587
|
+
const duplicate = supplied !== undefined && seenCallIds.has(supplied);
|
|
588
|
+
if (supplied !== undefined)
|
|
589
|
+
seenCallIds.add(supplied);
|
|
590
|
+
const id = supplied !== undefined && !duplicate ? supplied : `tool_${crypto.randomUUID().replaceAll("-", "")}`;
|
|
591
|
+
if (reasoningId !== undefined) {
|
|
592
|
+
lifecycle = Lifecycle.reasoningEnd(lifecycle, events, reasoningId, reasoningSignature
|
|
593
|
+
? providerMetadata(state.providerMetadataKey, { thoughtSignature: reasoningSignature })
|
|
594
|
+
: undefined);
|
|
595
|
+
reasoningId = undefined;
|
|
596
|
+
reasoningSignature = undefined;
|
|
597
|
+
}
|
|
598
|
+
if (textId !== undefined) {
|
|
599
|
+
lifecycle = Lifecycle.textEnd(lifecycle, events, textId, textSignature ? providerMetadata(state.providerMetadataKey, { thoughtSignature: textSignature }) : undefined);
|
|
600
|
+
textId = undefined;
|
|
601
|
+
textSignature = undefined;
|
|
602
|
+
}
|
|
603
|
+
lifecycle = Lifecycle.stepStart(lifecycle, events);
|
|
604
|
+
events.push(LLMEvent.toolCall({
|
|
605
|
+
id,
|
|
606
|
+
name: part.functionCall.name,
|
|
607
|
+
input,
|
|
608
|
+
providerMetadata: part.thoughtSignature
|
|
609
|
+
? providerMetadata(state.providerMetadataKey, { thoughtSignature: part.thoughtSignature })
|
|
610
|
+
: undefined,
|
|
611
|
+
}));
|
|
612
|
+
hasToolCalls = true;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return Effect.succeed([
|
|
616
|
+
{
|
|
617
|
+
...nextState,
|
|
618
|
+
hasToolCalls,
|
|
619
|
+
lifecycle,
|
|
620
|
+
reasoningSignature,
|
|
621
|
+
textSignature,
|
|
622
|
+
reasoningId,
|
|
623
|
+
textId,
|
|
624
|
+
nextReasoningId,
|
|
625
|
+
nextTextId,
|
|
626
|
+
seenCallIds,
|
|
627
|
+
finishReason: candidate.finishReason ?? nextState.finishReason,
|
|
628
|
+
},
|
|
629
|
+
events,
|
|
630
|
+
]);
|
|
631
|
+
};
|
|
632
|
+
// =============================================================================
|
|
633
|
+
// Protocol And Gemini Route
|
|
634
|
+
// =============================================================================
|
|
635
|
+
/**
|
|
636
|
+
* The Gemini protocol — request body construction, body schema, and the
|
|
637
|
+
* streaming-event state machine shared by Google AI Studio and Vertex Gemini.
|
|
638
|
+
*/
|
|
639
|
+
export const protocol = Protocol.make({
|
|
640
|
+
id: ADAPTER,
|
|
641
|
+
body: {
|
|
642
|
+
schema: GeminiBody,
|
|
643
|
+
from: fromRequest,
|
|
644
|
+
},
|
|
645
|
+
stream: {
|
|
646
|
+
event: Protocol.jsonEvent(GeminiEvent),
|
|
647
|
+
initial: (request) => ({
|
|
648
|
+
route: `${request.model.provider}/${request.model.route.id}`,
|
|
649
|
+
providerMetadataKey: request.model.route.providerMetadataKey ?? String(request.model.provider),
|
|
650
|
+
hasToolCalls: false,
|
|
651
|
+
lifecycle: Lifecycle.initial(),
|
|
652
|
+
nextReasoningId: 0,
|
|
653
|
+
nextTextId: 0,
|
|
654
|
+
}),
|
|
655
|
+
step,
|
|
656
|
+
onHalt: (state) => Effect.succeed(finish(state)),
|
|
657
|
+
},
|
|
658
|
+
});
|
|
659
|
+
export const route = Route.make({
|
|
660
|
+
id: ADAPTER,
|
|
661
|
+
provider: "google",
|
|
662
|
+
providerMetadataKey: "google",
|
|
663
|
+
protocol,
|
|
664
|
+
// Gemini's path embeds the model id and pins SSE framing at the URL level.
|
|
665
|
+
endpoint: Endpoint.path(({ request }) => `/models/${request.model.id}:streamGenerateContent?alt=sse`, {
|
|
666
|
+
baseURL: DEFAULT_BASE_URL,
|
|
667
|
+
}),
|
|
668
|
+
auth: Auth.none,
|
|
669
|
+
framing: Framing.sse,
|
|
670
|
+
});
|
|
671
|
+
export * as Gemini from "./gemini.js";
|