@effect-uai/google 0.9.0 → 0.11.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.
Files changed (46) hide show
  1. package/dist/Gemini.d.mts +4 -2
  2. package/dist/Gemini.d.mts.map +1 -1
  3. package/dist/Gemini.mjs +18 -6
  4. package/dist/Gemini.mjs.map +1 -1
  5. package/dist/GeminiEmbedding.mjs +1 -1
  6. package/dist/GeminiEmbedding.mjs.map +1 -1
  7. package/dist/GeminiSynthesizer.d.mts.map +1 -1
  8. package/dist/GeminiSynthesizer.mjs +2 -2
  9. package/dist/GeminiSynthesizer.mjs.map +1 -1
  10. package/dist/GeminiSynthesizer.test.mjs +1 -1
  11. package/dist/GeminiTools.d.mts +42 -0
  12. package/dist/GeminiTools.d.mts.map +1 -0
  13. package/dist/GeminiTools.mjs +64 -0
  14. package/dist/GeminiTools.mjs.map +1 -0
  15. package/dist/GoogleDeepResearch.d.mts +70 -0
  16. package/dist/GoogleDeepResearch.d.mts.map +1 -0
  17. package/dist/GoogleDeepResearch.mjs +269 -0
  18. package/dist/GoogleDeepResearch.mjs.map +1 -0
  19. package/dist/LyriaGenerator.mjs +1 -1
  20. package/dist/LyriaGenerator.mjs.map +1 -1
  21. package/dist/LyriaGenerator.test.mjs +1 -1
  22. package/dist/codec.d.mts +22 -5
  23. package/dist/codec.d.mts.map +1 -1
  24. package/dist/codec.mjs +52 -30
  25. package/dist/codec.mjs.map +1 -1
  26. package/dist/{dist-DMsg8YLb.mjs → dist-CWoAuwS9.mjs} +7 -7
  27. package/dist/{dist-DMsg8YLb.mjs.map → dist-CWoAuwS9.mjs.map} +1 -1
  28. package/dist/geminiSpeechCodec.test.mjs +1 -1
  29. package/dist/geminiTools.test.d.mts +1 -0
  30. package/dist/geminiTools.test.mjs +80 -0
  31. package/dist/geminiTools.test.mjs.map +1 -0
  32. package/dist/index.d.mts +4 -2
  33. package/dist/index.mjs +3 -1
  34. package/dist/models.d.mts +11 -1
  35. package/dist/models.d.mts.map +1 -1
  36. package/package.json +8 -4
  37. package/src/Gemini.ts +21 -3
  38. package/src/GeminiEmbedding.ts +1 -1
  39. package/src/GeminiSynthesizer.ts +4 -2
  40. package/src/GeminiTools.ts +95 -0
  41. package/src/GoogleDeepResearch.ts +446 -0
  42. package/src/LyriaGenerator.ts +1 -1
  43. package/src/codec.ts +81 -28
  44. package/src/geminiTools.test.ts +88 -0
  45. package/src/index.ts +2 -0
  46. package/src/models.ts +15 -0
@@ -1,4 +1,4 @@
1
- import { i as it, n as globalExpect, r as describe } from "./dist-DMsg8YLb.mjs";
1
+ import { i as it, n as globalExpect, r as describe } from "./dist-CWoAuwS9.mjs";
2
2
  import { audioSourceToInlineData, wrapPcmAsWav } from "./geminiSpeechCodec.mjs";
3
3
  import { Effect, Encoding } from "effect";
4
4
  //#region src/geminiSpeechCodec.test.ts
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,80 @@
1
+ import { codeExecutionTool, googleSearchTool, renderProviderTools } from "./GeminiTools.mjs";
2
+ import { WireChunk, buildRequestBody } from "./codec.mjs";
3
+ import { i as it, n as globalExpect, r as describe } from "./dist-CWoAuwS9.mjs";
4
+ import { Effect, Option, Result, Schema } from "effect";
5
+ import * as Tool from "@effect-uai/core/Tool";
6
+ import { descriptorsOf, providerToolsOf } from "@effect-uai/core/Tool";
7
+ //#region src/geminiTools.test.ts
8
+ const localTool = Tool.make({
9
+ name: "get_weather",
10
+ description: "Look up the weather.",
11
+ inputSchema: Tool.fromEffectSchema(Schema.Struct({ city: Schema.String })),
12
+ run: ({ city }) => Effect.succeed(`sunny in ${city}`)
13
+ });
14
+ const build = (toolkit) => {
15
+ const entries = Result.getOrThrow(renderProviderTools(providerToolsOf(toolkit)));
16
+ return buildRequestBody([], Option.none(), descriptorsOf(toolkit), entries);
17
+ };
18
+ describe("Gemini provider tools", () => {
19
+ it("renders google_search to the native googleSearch entry", () => {
20
+ globalExpect(build({ google_search: googleSearchTool }).tools).toEqual([{ googleSearch: {} }]);
21
+ });
22
+ it("renders a mixed toolkit to both a functionDeclarations entry and the native entry", () => {
23
+ globalExpect(build({
24
+ get_weather: localTool,
25
+ code_execution: codeExecutionTool
26
+ }).tools).toEqual([{ functionDeclarations: [globalExpect.objectContaining({
27
+ name: "get_weather",
28
+ description: "Look up the weather."
29
+ })] }, { codeExecution: {} }]);
30
+ });
31
+ it("omits toolConfig for a grounding-only request (no forced function-calling mode)", () => {
32
+ globalExpect(build({ google_search: googleSearchTool }).toolConfig).toBeUndefined();
33
+ });
34
+ it("sets toolConfig when a function declaration is present", () => {
35
+ globalExpect(build({
36
+ get_weather: localTool,
37
+ google_search: googleSearchTool
38
+ }).toolConfig).toEqual({ functionCallingConfig: { mode: "AUTO" } });
39
+ });
40
+ it("fails on a foreign provider tool", () => {
41
+ const result = renderProviderTools(providerToolsOf({ web_search: Tool.provider({
42
+ name: "web_search",
43
+ description: "Anthropic web search.",
44
+ inputSchema: Tool.noInput,
45
+ provider: "anthropic",
46
+ config: { type: "web_search_20250305" }
47
+ }) }));
48
+ globalExpect(Result.isFailure(result)).toBe(true);
49
+ });
50
+ it("fails on an unrecognized config kind", () => {
51
+ const result = renderProviderTools(providerToolsOf({ mystery: Tool.provider({
52
+ name: "mystery",
53
+ description: "Unknown Gemini tool.",
54
+ inputSchema: Tool.noInput,
55
+ provider: "gemini",
56
+ config: { kind: "telepathy" }
57
+ }) }));
58
+ globalExpect(Result.isFailure(result)).toBe(true);
59
+ });
60
+ it("decodes a grounded chunk without throwing (groundingMetadata is dropped)", () => {
61
+ globalExpect(Schema.decodeUnknownSync(WireChunk)({ candidates: [{
62
+ content: {
63
+ role: "model",
64
+ parts: [{ text: "Grounded answer." }]
65
+ },
66
+ groundingMetadata: {
67
+ webSearchQueries: ["effect ts"],
68
+ groundingChunks: [{ web: {
69
+ uri: "https://example.com",
70
+ title: "Example"
71
+ } }]
72
+ },
73
+ finishReason: "STOP"
74
+ }] }).candidates?.[0]?.content?.parts?.[0]).toEqual({ text: "Grounded answer." });
75
+ });
76
+ });
77
+ //#endregion
78
+ export {};
79
+
80
+ //# sourceMappingURL=geminiTools.test.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geminiTools.test.mjs","names":["expect"],"sources":["../src/geminiTools.test.ts"],"sourcesContent":["import { Effect, Option, Result, Schema } from \"effect\"\nimport * as Tool from \"@effect-uai/core/Tool\"\nimport { descriptorsOf, providerToolsOf } from \"@effect-uai/core/Tool\"\nimport { describe, expect, it } from \"vitest\"\nimport { WireChunk, buildRequestBody } from \"./codec.js\"\nimport { codeExecutionTool, googleSearchTool, renderProviderTools } from \"./GeminiTools.js\"\n\nconst localTool = Tool.make({\n name: \"get_weather\",\n description: \"Look up the weather.\",\n inputSchema: Tool.fromEffectSchema(Schema.Struct({ city: Schema.String })),\n run: ({ city }) => Effect.succeed(`sunny in ${city}`),\n})\n\nconst build = (toolkit: Record<string, Tool.AnyTool>) => {\n const entries = Result.getOrThrow(renderProviderTools(providerToolsOf(toolkit)))\n return buildRequestBody([], Option.none(), descriptorsOf(toolkit), entries)\n}\n\ndescribe(\"Gemini provider tools\", () => {\n it(\"renders google_search to the native googleSearch entry\", () => {\n const body = build({ google_search: googleSearchTool })\n expect(body.tools).toEqual([{ googleSearch: {} }])\n })\n\n it(\"renders a mixed toolkit to both a functionDeclarations entry and the native entry\", () => {\n const body = build({ get_weather: localTool, code_execution: codeExecutionTool })\n expect(body.tools).toEqual([\n {\n functionDeclarations: [\n expect.objectContaining({ name: \"get_weather\", description: \"Look up the weather.\" }),\n ],\n },\n { codeExecution: {} },\n ])\n })\n\n it(\"omits toolConfig for a grounding-only request (no forced function-calling mode)\", () => {\n const body = build({ google_search: googleSearchTool })\n expect(body.toolConfig).toBeUndefined()\n })\n\n it(\"sets toolConfig when a function declaration is present\", () => {\n const body = build({ get_weather: localTool, google_search: googleSearchTool })\n expect(body.toolConfig).toEqual({ functionCallingConfig: { mode: \"AUTO\" } })\n })\n\n it(\"fails on a foreign provider tool\", () => {\n const foreign = Tool.provider({\n name: \"web_search\",\n description: \"Anthropic web search.\",\n inputSchema: Tool.noInput,\n provider: \"anthropic\",\n config: { type: \"web_search_20250305\" },\n })\n const result = renderProviderTools(providerToolsOf({ web_search: foreign }))\n expect(Result.isFailure(result)).toBe(true)\n })\n\n it(\"fails on an unrecognized config kind\", () => {\n const bogus = Tool.provider({\n name: \"mystery\",\n description: \"Unknown Gemini tool.\",\n inputSchema: Tool.noInput,\n provider: \"gemini\",\n config: { kind: \"telepathy\" },\n })\n const result = renderProviderTools(providerToolsOf({ mystery: bogus }))\n expect(Result.isFailure(result)).toBe(true)\n })\n\n it(\"decodes a grounded chunk without throwing (groundingMetadata is dropped)\", () => {\n const grounded = {\n candidates: [\n {\n content: { role: \"model\", parts: [{ text: \"Grounded answer.\" }] },\n groundingMetadata: {\n webSearchQueries: [\"effect ts\"],\n groundingChunks: [{ web: { uri: \"https://example.com\", title: \"Example\" } }],\n },\n finishReason: \"STOP\",\n },\n ],\n }\n const chunk = Schema.decodeUnknownSync(WireChunk)(grounded)\n expect(chunk.candidates?.[0]?.content?.parts?.[0]).toEqual({ text: \"Grounded answer.\" })\n })\n})\n"],"mappings":";;;;;;;AAOA,MAAM,YAAY,KAAK,KAAK;CAC1B,MAAM;CACN,aAAa;CACb,aAAa,KAAK,iBAAiB,OAAO,OAAO,EAAE,MAAM,OAAO,QAAQ,CAAC,CAAC;CAC1E,MAAM,EAAE,WAAW,OAAO,QAAQ,YAAY,OAAO;CACtD,CAAC;AAEF,MAAM,SAAS,YAA0C;CACvD,MAAM,UAAU,OAAO,WAAW,oBAAoB,gBAAgB,QAAQ,CAAC,CAAC;AAChF,QAAO,iBAAiB,EAAE,EAAE,OAAO,MAAM,EAAE,cAAc,QAAQ,EAAE,QAAQ;;AAG7E,SAAS,+BAA+B;AACtC,IAAG,gEAAgE;AAEjE,eADa,MAAM,EAAE,eAAe,kBAAkB,CAC3C,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,cAAc,EAAE,EAAE,CAAC,CAAC;GAClD;AAEF,IAAG,2FAA2F;AAE5F,eADa,MAAM;GAAE,aAAa;GAAW,gBAAgB;GAAmB,CACrE,CAAC,MAAM,CAAC,QAAQ,CACzB,EACE,sBAAsB,CACpBA,aAAO,iBAAiB;GAAE,MAAM;GAAe,aAAa;GAAwB,CAAC,CACtF,EACF,EACD,EAAE,eAAe,EAAE,EAAE,CACtB,CAAC;GACF;AAEF,IAAG,yFAAyF;AAE1F,eADa,MAAM,EAAE,eAAe,kBAAkB,CAC3C,CAAC,WAAW,CAAC,eAAe;GACvC;AAEF,IAAG,gEAAgE;AAEjE,eADa,MAAM;GAAE,aAAa;GAAW,eAAe;GAAkB,CACnE,CAAC,WAAW,CAAC,QAAQ,EAAE,uBAAuB,EAAE,MAAM,QAAQ,EAAE,CAAC;GAC5E;AAEF,IAAG,0CAA0C;EAQ3C,MAAM,SAAS,oBAAoB,gBAAgB,EAAE,YAPrC,KAAK,SAAS;GAC5B,MAAM;GACN,aAAa;GACb,aAAa,KAAK;GAClB,UAAU;GACV,QAAQ,EAAE,MAAM,uBAAuB;GACxC,CACuE,EAAE,CAAC,CAAC;AAC5E,eAAO,OAAO,UAAU,OAAO,CAAC,CAAC,KAAK,KAAK;GAC3C;AAEF,IAAG,8CAA8C;EAQ/C,MAAM,SAAS,oBAAoB,gBAAgB,EAAE,SAPvC,KAAK,SAAS;GAC1B,MAAM;GACN,aAAa;GACb,aAAa,KAAK;GAClB,UAAU;GACV,QAAQ,EAAE,MAAM,aAAa;GAC9B,CACkE,EAAE,CAAC,CAAC;AACvE,eAAO,OAAO,UAAU,OAAO,CAAC,CAAC,KAAK,KAAK;GAC3C;AAEF,IAAG,kFAAkF;AAcnF,eADc,OAAO,kBAAkB,UAAU,CAAC,EAXhD,YAAY,CACV;GACE,SAAS;IAAE,MAAM;IAAS,OAAO,CAAC,EAAE,MAAM,oBAAoB,CAAC;IAAE;GACjE,mBAAmB;IACjB,kBAAkB,CAAC,YAAY;IAC/B,iBAAiB,CAAC,EAAE,KAAK;KAAE,KAAK;KAAuB,OAAO;KAAW,EAAE,CAAC;IAC7E;GACD,cAAc;GACf,CACF,EAEuD,CAC9C,CAAC,aAAa,IAAI,SAAS,QAAQ,GAAG,CAAC,QAAQ,EAAE,MAAM,oBAAoB,CAAC;GACxF;EACF"}
package/dist/index.d.mts CHANGED
@@ -1,7 +1,9 @@
1
1
  import { t as codec_d_exports } from "./codec.mjs";
2
- import { GeminiTtsModel, GeminiVoiceName, GoogleEmbeddingModel, GoogleModel, LyriaModel } from "./models.mjs";
2
+ import { t as GeminiTools_d_exports } from "./GeminiTools.mjs";
3
+ import { GeminiTtsModel, GeminiVoiceName, GoogleDeepResearchModel, GoogleEmbeddingModel, GoogleModel, LyriaModel } from "./models.mjs";
3
4
  import { t as Gemini_d_exports } from "./Gemini.mjs";
4
5
  import { t as GeminiEmbedding_d_exports } from "./GeminiEmbedding.mjs";
5
6
  import { t as GeminiSynthesizer_d_exports } from "./GeminiSynthesizer.mjs";
7
+ import { t as GoogleDeepResearch_d_exports } from "./GoogleDeepResearch.mjs";
6
8
  import { t as LyriaGenerator_d_exports } from "./LyriaGenerator.mjs";
7
- export { Gemini_d_exports as Gemini, GeminiEmbedding_d_exports as GeminiEmbedding, GeminiSynthesizer_d_exports as GeminiSynthesizer, GeminiTtsModel, GeminiVoiceName, GoogleEmbeddingModel, GoogleModel, LyriaGenerator_d_exports as LyriaGenerator, LyriaModel, codec_d_exports as codec };
9
+ export { Gemini_d_exports as Gemini, GeminiEmbedding_d_exports as GeminiEmbedding, GeminiSynthesizer_d_exports as GeminiSynthesizer, GeminiTools_d_exports as GeminiTools, GeminiTtsModel, GeminiVoiceName, GoogleDeepResearch_d_exports as GoogleDeepResearch, GoogleDeepResearchModel, GoogleEmbeddingModel, GoogleModel, LyriaGenerator_d_exports as LyriaGenerator, LyriaModel, codec_d_exports as codec };
package/dist/index.mjs CHANGED
@@ -1,7 +1,9 @@
1
+ import { t as GeminiTools_exports } from "./GeminiTools.mjs";
1
2
  import { t as codec_exports } from "./codec.mjs";
2
3
  import { t as Gemini_exports } from "./Gemini.mjs";
3
4
  import { t as GeminiEmbedding_exports } from "./GeminiEmbedding.mjs";
4
5
  import { t as GeminiSynthesizer_exports } from "./GeminiSynthesizer.mjs";
6
+ import { t as GoogleDeepResearch_exports } from "./GoogleDeepResearch.mjs";
5
7
  import { t as LyriaGenerator_exports } from "./LyriaGenerator.mjs";
6
8
  import "./models.mjs";
7
- export { Gemini_exports as Gemini, GeminiEmbedding_exports as GeminiEmbedding, GeminiSynthesizer_exports as GeminiSynthesizer, LyriaGenerator_exports as LyriaGenerator, codec_exports as codec };
9
+ export { Gemini_exports as Gemini, GeminiEmbedding_exports as GeminiEmbedding, GeminiSynthesizer_exports as GeminiSynthesizer, GeminiTools_exports as GeminiTools, GoogleDeepResearch_exports as GoogleDeepResearch, LyriaGenerator_exports as LyriaGenerator, codec_exports as codec };
package/dist/models.d.mts CHANGED
@@ -7,6 +7,16 @@
7
7
  * Reference: https://ai.google.dev/gemini-api/docs/models
8
8
  */
9
9
  type GoogleModel = "gemini-3.1-pro-preview" | "gemini-3-flash-preview" | "gemini-3.1-flash-lite-preview" | "gemini-3.1-flash-live-preview" | "gemini-3.1-flash-tts-preview" | "gemini-2.5-pro" | "gemini-2.5-flash" | "gemini-2.5-flash-lite" | (string & {});
10
+ /**
11
+ * Deep-research agent identifiers for the Interactions API (`POST
12
+ * /v1beta/interactions`). `-preview` is the fast tier, `-max-` the
13
+ * comprehensive one. Preview surface: verify against live docs before relying on
14
+ * exact ids. The `(string & {})` tail accepts any string so a renamed agent
15
+ * works without an SDK update.
16
+ *
17
+ * Reference: https://ai.google.dev/gemini-api/docs/interactions/deep-research
18
+ */
19
+ type GoogleDeepResearchModel = "deep-research-preview-04-2026" | "deep-research-max-preview-04-2026" | (string & {});
10
20
  /**
11
21
  * Known Gemini embedding model identifiers.
12
22
  *
@@ -48,5 +58,5 @@ type GeminiVoiceName = "Zephyr" | "Puck" | "Charon" | "Kore" | "Fenrir" | "Leda"
48
58
  type LyriaModel = "lyria-3-clip-preview" | "lyria-3-pro-preview" | (string & {});
49
59
  type GoogleEmbeddingModel = "gemini-embedding-2" | "gemini-embedding-001" | (string & {});
50
60
  //#endregion
51
- export { GeminiTtsModel, GeminiVoiceName, GoogleEmbeddingModel, GoogleModel, LyriaModel };
61
+ export { GeminiTtsModel, GeminiVoiceName, GoogleDeepResearchModel, GoogleEmbeddingModel, GoogleModel, LyriaModel };
52
62
  //# sourceMappingURL=models.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"models.d.mts","names":[],"sources":["../src/models.ts"],"mappings":";;AAOA;;;;;AAgCA;KAhCY,WAAA;;;;AA8CZ;;;;;AAyCA;;;;;AAMA;;;;;;;KA7DY,cAAA;;;;;;;;KAcA,eAAA;;;;;;;;;;KAyCA,UAAA;AAAA,KAMA,oBAAA"}
1
+ {"version":3,"file":"models.d.mts","names":[],"sources":["../src/models.ts"],"mappings":";;AAOA;;;;;AAqBA;KArBY,WAAA;;;;AA+CZ;;;;;AAcA;KAxCY,uBAAA;;;;AAiFZ;;;;;AAMA;;;;;;;;;;;;KA7DY,cAAA;;;;;;;;KAcA,eAAA;;;;;;;;;;KAyCA,UAAA;AAAA,KAMA,oBAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effect-uai/google",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Google Gemini provider for @effect-uai/core.",
5
5
  "keywords": [
6
6
  "ai",
@@ -46,6 +46,10 @@
46
46
  "types": "./dist/GeminiSynthesizer.d.mts",
47
47
  "import": "./dist/GeminiSynthesizer.mjs"
48
48
  },
49
+ "./GoogleDeepResearch": {
50
+ "types": "./dist/GoogleDeepResearch.d.mts",
51
+ "import": "./dist/GoogleDeepResearch.mjs"
52
+ },
49
53
  "./LyriaGenerator": {
50
54
  "types": "./dist/LyriaGenerator.d.mts",
51
55
  "import": "./dist/LyriaGenerator.mjs"
@@ -55,13 +59,13 @@
55
59
  "access": "public"
56
60
  },
57
61
  "devDependencies": {
58
- "effect": "4.0.0-beta.57",
62
+ "effect": "4.0.0-beta.94",
59
63
  "typescript": "^6.0.3",
60
- "@effect-uai/core": "0.9.0"
64
+ "@effect-uai/core": "0.11.0"
61
65
  },
62
66
  "peerDependencies": {
63
67
  "@effect-uai/core": ">=0.2.0 <1",
64
- "effect": "4.0.0-beta.57"
68
+ "effect": ">=4.0.0-beta.94 <5.0.0"
65
69
  },
66
70
  "scripts": {
67
71
  "build": "tsdown",
package/src/Gemini.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Context, Effect, Layer, Option, Redacted, Schema, Stream } from "effect"
1
+ import { Context, Effect, Layer, Option, Redacted, Result, Schema, Stream } from "effect"
2
2
  import { HttpClient, HttpClientRequest } from "effect/unstable/http"
3
3
  import * as AiError from "@effect-uai/core/AiError"
4
4
  import {
@@ -9,8 +9,11 @@ import {
9
9
  } from "@effect-uai/core/LanguageModel"
10
10
  import { JsonParseError } from "@effect-uai/core/JSONL"
11
11
  import * as SSE from "@effect-uai/core/SSE"
12
- import { descriptorsOf } from "@effect-uai/core/Tool"
12
+ import { descriptorsOf, providerToolsOf } from "@effect-uai/core/Tool"
13
13
  import { type Turn, TurnEvent } from "@effect-uai/core/Turn"
14
+ import { renderProviderTools } from "./GeminiTools.js"
15
+
16
+ export { codeExecutionTool, googleSearchTool, urlContextTool } from "./GeminiTools.js"
14
17
  import {
15
18
  type ChunkPart,
16
19
  type GenerationConfig,
@@ -174,10 +177,25 @@ const buildNativeStream = (cfg: Config) => {
174
177
  }
175
178
  const url = `${baseUrl}/models/${request.model}:streamGenerateContent?alt=sse`
176
179
  const generationConfig = buildGenerationConfig(request)
180
+ const providerToolEntries = yield* Result.match(
181
+ renderProviderTools(providerToolsOf(request.tools)),
182
+ {
183
+ onFailure: (e) =>
184
+ Effect.fail(
185
+ new AiError.Unsupported({
186
+ provider: "gemini",
187
+ capability: e.capability,
188
+ reason: e.reason,
189
+ }),
190
+ ),
191
+ onSuccess: (entries) => Effect.succeed(entries),
192
+ },
193
+ )
177
194
  const body = buildRequestBody(
178
195
  request.history,
179
196
  generationConfig,
180
197
  descriptorsOf(request.tools),
198
+ providerToolEntries,
181
199
  request.toolChoice,
182
200
  )
183
201
  const httpRequest = HttpClientRequest.post(url).pipe(
@@ -287,7 +305,7 @@ export const toCanonical = <E, R>(
287
305
  * Build a `GeminiService` value. For Layer-based setup, prefer `layer`.
288
306
  */
289
307
  export const make = (cfg: Config): Effect.Effect<GeminiService, never, HttpClient.HttpClient> =>
290
- Effect.map(HttpClient.HttpClient.asEffect(), (client) => {
308
+ Effect.map(HttpClient.HttpClient, (client) => {
291
309
  const streamNative: GeminiService["streamNative"] = (request) =>
292
310
  buildNativeStream(cfg)(request).pipe(Stream.provideService(HttpClient.HttpClient, client))
293
311
  const streamTurn: GeminiService["streamTurn"] = (request) => toCanonical(streamNative(request))
@@ -328,7 +328,7 @@ const embedManyImpl =
328
328
  export const make = (
329
329
  cfg: Config,
330
330
  ): Effect.Effect<GeminiEmbeddingService, never, HttpClient.HttpClient> =>
331
- Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({
331
+ Effect.map(HttpClient.HttpClient, (client) => ({
332
332
  embed: (request) =>
333
333
  embedImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),
334
334
  embedMany: (request) =>
@@ -150,7 +150,9 @@ const synthesizeImpl = (cfg: Config) => (request: GeminiSynthesizeRequest) =>
150
150
  reason: "Gemini `:generateContent` TTS has no language parameter.",
151
151
  })
152
152
  const client = yield* HttpClient.HttpClient
153
- const [format, wrap] = yield* realizeOutput(request.outputFormat?.container ?? "raw")
153
+ const [format, wrap] = yield* Effect.fromResult(
154
+ realizeOutput(request.outputFormat?.container ?? "raw"),
155
+ )
154
156
  const httpRequest = HttpClientRequest.post(
155
157
  `${baseUrl(cfg)}/models/${request.model}:generateContent`,
156
158
  ).pipe(
@@ -221,7 +223,7 @@ const unsupportedStreamDialogue: SpeechSynthesizerService["streamSynthesizeDialo
221
223
 
222
224
  export const make = (cfg: Config) =>
223
225
  Effect.map(
224
- HttpClient.HttpClient.asEffect(),
226
+ HttpClient.HttpClient,
225
227
  (client): GeminiSynthesizerService => ({
226
228
  synthesize: (r) =>
227
229
  synthesizeImpl(cfg)(r).pipe(Effect.provideService(HttpClient.HttpClient, client)),
@@ -0,0 +1,95 @@
1
+ import { Match, Result, Schema } from "effect"
2
+ import * as Tool from "@effect-uai/core/Tool"
3
+ import type { RequestTool } from "./codec.js"
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Gemini provider-hosted tools. Each is a `Tool.provider` whose `config`
7
+ // carries the discriminant the renderer maps to a native `tools[]` entry
8
+ // (`googleSearch`, `urlContext`, `codeExecution`). The `inputSchema` is never
9
+ // sent — provider tools are partitioned out of the function declarations — so
10
+ // `Tool.noInput` suffices.
11
+ // Reference: https://ai.google.dev/api/caching#Tool
12
+ // ---------------------------------------------------------------------------
13
+
14
+ /** Discriminated config for a Gemini hosted tool. */
15
+ export const GeminiToolConfig = Schema.Union([
16
+ Schema.Struct({ kind: Schema.Literal("google_search") }),
17
+ Schema.Struct({ kind: Schema.Literal("url_context") }),
18
+ Schema.Struct({ kind: Schema.Literal("code_execution") }),
19
+ ])
20
+ export type GeminiToolConfig = typeof GeminiToolConfig.Type
21
+
22
+ /** Google Search grounding, executed by Gemini. */
23
+ export const googleSearchTool = Tool.provider({
24
+ name: "google_search",
25
+ description: "Ground the answer with Google Search, executed by Gemini.",
26
+ inputSchema: Tool.noInput,
27
+ provider: "gemini",
28
+ config: { kind: "google_search" } as const,
29
+ })
30
+
31
+ /** URL context: Gemini fetches and reads URLs referenced in the prompt. */
32
+ export const urlContextTool = Tool.provider({
33
+ name: "url_context",
34
+ description: "Let Gemini fetch and read URLs referenced in the prompt.",
35
+ inputSchema: Tool.noInput,
36
+ provider: "gemini",
37
+ config: { kind: "url_context" } as const,
38
+ })
39
+
40
+ /** Code execution: Gemini runs generated Python in a sandbox. */
41
+ export const codeExecutionTool = Tool.provider({
42
+ name: "code_execution",
43
+ description: "Let Gemini write and run Python in a sandbox.",
44
+ inputSchema: Tool.noInput,
45
+ provider: "gemini",
46
+ config: { kind: "code_execution" } as const,
47
+ })
48
+
49
+ /**
50
+ * A provider tool this adapter cannot render — a foreign `provider` or an
51
+ * unrecognized `config`. Carried as data so `Gemini.ts` can lift it into a
52
+ * typed `AiError.Unsupported`, mirroring how `hasUrlImageSource` surfaces a
53
+ * request-shaping failure.
54
+ */
55
+ export type ToolRenderError = {
56
+ readonly capability: string
57
+ readonly reason: string
58
+ }
59
+
60
+ const decodeConfig = Schema.decodeUnknownResult(GeminiToolConfig)
61
+
62
+ const configToWire = Match.type<GeminiToolConfig>().pipe(
63
+ Match.discriminatorsExhaustive("kind")({
64
+ google_search: (): RequestTool => ({ googleSearch: {} }),
65
+ url_context: (): RequestTool => ({ urlContext: {} }),
66
+ code_execution: (): RequestTool => ({ codeExecution: {} }),
67
+ }),
68
+ )
69
+
70
+ const renderOne = (
71
+ tool: Tool.ProviderTool<string, any, string, any>,
72
+ ): Result.Result<RequestTool, ToolRenderError> =>
73
+ tool.provider !== "gemini"
74
+ ? Result.fail({
75
+ capability: `providerTool:${tool.provider}`,
76
+ reason: `tool "${tool.name}" is hosted by "${tool.provider}", not gemini`,
77
+ })
78
+ : decodeConfig(tool.config).pipe(
79
+ Result.map(configToWire),
80
+ Result.mapError(
81
+ (): ToolRenderError => ({
82
+ capability: "providerTool",
83
+ reason: `unrecognized Gemini provider tool config on "${tool.name}"`,
84
+ }),
85
+ ),
86
+ )
87
+
88
+ /**
89
+ * Render a toolkit's Gemini provider tools to native `tools[]` entries. Fails
90
+ * with a `ToolRenderError` on the first foreign or unrecognized tool rather
91
+ * than dropping it.
92
+ */
93
+ export const renderProviderTools = (
94
+ tools: ReadonlyArray<Tool.ProviderTool<string, any, string, any>>,
95
+ ): Result.Result<ReadonlyArray<RequestTool>, ToolRenderError> => Result.all(tools.map(renderOne))