From c658c67a0c43fc49599f1d820e9cbd32db1d3368 Mon Sep 17 00:00:00 2001 From: Jan-Marlon Leibl Date: Thu, 21 May 2026 12:54:48 +0200 Subject: [PATCH] chore(deps)!: migrate from @google/generative-ai to @google/genai The @google/generative-ai SDK has been deprecated by Google. This switches to the successor @google/genai package and rewrites the Gemini provider against the new stateless models/chats API. BREAKING CHANGE: GeminiConfig.safetySettings now uses the SafetySetting type from @google/genai. Consumers passing this field must update their import from '@google/generative-ai' to '@google/genai'. The shape of the type is similar but not identical. Notable simplifications enabled by the new SDK: - No per-model client caching: generateContent specifies model per-call - Single code path for both single- and multi-turn (full contents array is passed to generateContent directly; no more startChat branching) - Stream chunks expose .text as a property (was .text() method) - Stream iteration is direct on the response (no .stream sub-property) Default model bumped from gemini-1.5-flash to gemini-2.5-flash. --- bun.lock | 82 +++++- package.json | 2 +- src/providers/gemini.ts | 582 ++++++++++------------------------------ 3 files changed, 225 insertions(+), 441 deletions(-) diff --git a/bun.lock b/bun.lock index 4651b39..37329a1 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "name": "simple-ai-provider", "dependencies": { "@anthropic-ai/sdk": "^0.97.0", - "@google/generative-ai": "^0.24.1", + "@google/genai": "^2.5.0", "openai": "^6.0.0", }, "devDependencies": { @@ -23,7 +23,27 @@ "@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], - "@google/generative-ai": ["@google/generative-ai@0.24.1", "", {}, "sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q=="], + "@google/genai": ["@google/genai@2.5.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-qDi3LLh9I3llJK0f9uV8kZ8EdT9oHPxGJJ9yOJ/i5YXYrVwRCs8jHo9x4e99uOeKYDvD3TZwT70p/H/LS3BixQ=="], + + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], + + "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], + + "@protobufjs/codegen": ["@protobufjs/codegen@2.0.5", "", {}, "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g=="], + + "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="], + + "@protobufjs/fetch": ["@protobufjs/fetch@1.1.1", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1" } }, "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw=="], + + "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], + + "@protobufjs/inquire": ["@protobufjs/inquire@1.1.2", "", {}, "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw=="], + + "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], + + "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], + + "@protobufjs/utf8": ["@protobufjs/utf8@1.1.1", "", {}, "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg=="], "@stablelib/base64": ["@stablelib/base64@1.0.1", "", {}, "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="], @@ -31,14 +51,68 @@ "@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="], + "@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="], + + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], + + "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], + + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + "fast-sha256": ["fast-sha256@1.3.0", "", {}, "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ=="], + "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + + "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], + + "gaxios": ["gaxios@7.1.4", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA=="], + + "gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="], + + "google-auth-library": ["google-auth-library@10.6.2", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.1.4", "gcp-metadata": "8.1.2", "google-logging-utils": "1.1.3", "jws": "^4.0.0" } }, "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw=="], + + "google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], + + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + + "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], + "json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="], + "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], + + "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], + + "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], + + "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + "openai": ["openai@6.38.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-AoMplt2UalrpgUDMh3L09QWjNRlgJPipclQvA6sYAaeF6nHNBMgmikAZGmcYLn8on4d9sQY9Q8bOLfrBS7Lc8g=="], + "p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="], + + "protobufjs": ["protobufjs@7.6.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-LtESOsMPTZgyYtwxhvdgdjGL0HmXEaRA/hVD6sol4zA60hVXXXP/SGmxnqDbgGE8gy7pYex7cym+5vYPcmaXBQ=="], + + "retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "standardwebhooks": ["standardwebhooks@1.0.0", "", { "dependencies": { "@stablelib/base64": "^1.0.0", "fast-sha256": "^1.3.0" } }, "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg=="], "ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="], @@ -47,6 +121,10 @@ "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], + "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + + "ws": ["ws@8.20.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w=="], + "bun-types/@types/node": ["@types/node@24.12.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA=="], "bun-types/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], diff --git a/package.json b/package.json index 6fa6c1d..f8cddd5 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ }, "dependencies": { "@anthropic-ai/sdk": "^0.97.0", - "@google/generative-ai": "^0.24.1", + "@google/genai": "^2.5.0", "openai": "^6.0.0" }, "devDependencies": { diff --git a/src/providers/gemini.ts b/src/providers/gemini.ts index 8e3b6dd..7ddee45 100644 --- a/src/providers/gemini.ts +++ b/src/providers/gemini.ts @@ -1,32 +1,26 @@ /** * Google Gemini Provider Implementation - * - * This module provides integration with Google's Gemini models through the official - * Generative AI SDK. Gemini offers cutting-edge AI capabilities including multimodal - * understanding, advanced reasoning, and efficient text generation across various tasks. - * + * + * Integrates with Google's Gemini models through the official @google/genai SDK + * (the successor to the deprecated @google/generative-ai package). + * * Key Features: - * - Support for all Gemini model variants (Gemini 1.5 Pro, Flash, Pro Vision) - * - Advanced streaming support with real-time token delivery - * - Native multimodal capabilities (text, images, video) - * - Sophisticated safety settings and content filtering - * - Flexible generation configuration with fine-grained control - * - * Gemini-Specific Considerations: - * - Uses "candidates" for response variants and "usageMetadata" for token counts - * - Supports system instructions as separate parameter (not in conversation) - * - Has sophisticated safety filtering with customizable thresholds - * - Provides extensive generation configuration options - * - Supports both single-turn and multi-turn conversations - * - Offers advanced reasoning and coding capabilities - * + * - Support for Gemini 1.5 / 2.0 / 2.5 model families + * - Streaming responses with real-time chunk delivery + * - Multimodal capabilities and configurable safety filtering + * - System instruction handling separate from conversation flow + * * @author Jan-Marlon Leibl - * @version 1.0.0 * @see https://ai.google.dev/docs */ -import { GoogleGenerativeAI, GenerativeModel } from '@google/generative-ai'; -import type { Content, Part, GenerationConfig, SafetySetting } from '@google/generative-ai'; +import { GoogleGenAI } from '@google/genai'; +import type { + Content, + GenerateContentConfig, + GenerateContentResponse, + SafetySetting +} from '@google/genai'; import type { AIProviderConfig, CompletionParams, @@ -44,84 +38,39 @@ import { AIProviderError, AIErrorType } from '../types/index.js'; /** * Configuration interface for Gemini provider with Google-specific options. - * - * @example - * ```typescript - * const config: GeminiConfig = { - * apiKey: process.env.GOOGLE_API_KEY!, - * defaultModel: 'gemini-1.5-pro', - * safetySettings: [ - * { - * category: HarmCategory.HARM_CATEGORY_HARASSMENT, - * threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE, - * } - * ], - * generationConfig: { - * temperature: 0.7, - * topP: 0.8, - * topK: 40, - * maxOutputTokens: 2048 - * } - * }; - * ``` */ export interface GeminiConfig extends AIProviderConfig { - /** + /** * Default Gemini model to use for requests. - * + * * Recommended models: - * - 'gemini-1.5-pro': Flagship model, best overall performance and multimodal - * - 'gemini-1.5-flash': Faster and cheaper, good for simple tasks - * - 'gemini-1.0-pro': Previous generation, cost-effective - * - 'gemini-pro-vision': Specialized for vision tasks (legacy) - * - * @default 'gemini-1.5-flash' + * - 'gemini-2.5-flash': Latest fast model, great default + * - 'gemini-2.5-pro': Flagship reasoning model + * - 'gemini-2.0-flash': Previous generation fast model + * - 'gemini-1.5-pro': Long-context flagship from the 1.5 generation + * + * @default 'gemini-2.5-flash' */ defaultModel?: string; - /** - * Safety settings for content filtering and harm prevention. - * - * Gemini includes built-in safety filtering across multiple categories: - * - Harassment and bullying - * - Hate speech and discrimination - * - Sexually explicit content - * - Dangerous or harmful activities - * - * Each category can be configured with different blocking thresholds. - * - * @see https://ai.google.dev/docs/safety_setting - */ + /** Safety settings for content filtering and harm prevention. */ safetySettings?: SafetySetting[]; - /** - * Generation configuration for controlling output characteristics. - * - * This allows fine-tuned control over the generation process including - * creativity, diversity, length, and stopping conditions. - * - * @see https://ai.google.dev/docs/concepts#generation_configuration - */ + /** Default generation configuration applied to every request. */ generationConfig?: { - /** Controls randomness in generation (0.0 to 1.0) */ temperature?: number; - /** Controls nucleus sampling for diversity (0.0 to 1.0) */ topP?: number; - /** Controls top-k sampling for diversity (positive integer) */ topK?: number; - /** Maximum number of tokens to generate */ maxOutputTokens?: number; - /** Sequences that will stop generation when encountered */ stopSequences?: string[]; }; } /** * Processed messages structure for Gemini's conversation format. - * Separates system instructions from conversational content. */ interface ProcessedMessages { - /** System instruction for model behavior (if any) */ + /** System instruction string (if any) */ systemInstruction?: string; /** Conversation content in Gemini's format */ contents: Content[]; @@ -132,80 +81,24 @@ interface ProcessedMessages { // ============================================================================ /** - * Google Gemini provider implementation. - * - * This class handles all interactions with Google's Gemini models through their - * official Generative AI SDK. It provides optimized handling of Gemini's unique - * features including multimodal inputs, safety filtering, and advanced generation control. - * - * Usage Pattern: - * 1. Create instance with Google API key and configuration - * 2. Call initialize() to set up client and validate credentials - * 3. Use complete() or stream() for text generation - * 4. Handle any AIProviderError exceptions appropriately - * - * @example - * ```typescript - * const gemini = new GeminiProvider({ - * apiKey: process.env.GOOGLE_API_KEY!, - * defaultModel: 'gemini-1.5-pro', - * generationConfig: { - * temperature: 0.7, - * maxOutputTokens: 2048 - * } - * }); - * - * await gemini.initialize(); - * - * const response = await gemini.complete({ - * messages: [ - * { role: 'system', content: 'You are a helpful research assistant.' }, - * { role: 'user', content: 'Explain quantum computing.' } - * ], - * maxTokens: 1000, - * temperature: 0.8 - * }); - * ``` + * Google Gemini provider implementation backed by @google/genai. + * + * The new SDK is stateless w.r.t. model selection — each `generateContent` call + * specifies the model directly, so this class only holds a single client. */ export class GeminiProvider extends BaseAIProvider { - // ======================================================================== - // INSTANCE PROPERTIES - // ======================================================================== - - /** Google Generative AI client instance (initialized during doInitialize) */ - private client: GoogleGenerativeAI | null = null; - - /** Default model instance for requests */ - private model: GenerativeModel | null = null; - - /** Default model identifier for requests */ + private client: GoogleGenAI | null = null; private readonly defaultModel: string; - - /** Safety settings for content filtering */ private readonly safetySettings?: SafetySetting[]; - - /** Generation configuration defaults */ - private readonly generationConfig?: any; + private readonly defaultGenerationConfig?: GeminiConfig['generationConfig']; - // ======================================================================== - // CONSTRUCTOR - // ======================================================================== - - /** - * Creates a new Gemini provider instance. - * - * @param config - Gemini-specific configuration options - * @throws {AIProviderError} If configuration validation fails - */ constructor(config: GeminiConfig) { super(config); - - // Set Gemini-specific defaults - this.defaultModel = config.defaultModel || 'gemini-1.5-flash'; + + this.defaultModel = config.defaultModel || 'gemini-2.5-flash'; this.safetySettings = config.safetySettings; - this.generationConfig = config.generationConfig; - - // Validate model name format + this.defaultGenerationConfig = config.generationConfig; + this.validateModelName(this.defaultModel); } @@ -213,38 +106,14 @@ export class GeminiProvider extends BaseAIProvider { // PROTECTED TEMPLATE METHOD IMPLEMENTATIONS // ======================================================================== - /** - * Initializes the Gemini provider by setting up the client and model. - * - * This method: - * 1. Creates the Google Generative AI client with API key - * 2. Sets up the default model with safety and generation settings - * 3. Tests the connection with a minimal API call - * 4. Validates API key permissions and model access - * - * @protected - * @throws {Error} If client creation or connection validation fails - */ protected async doInitialize(): Promise { try { - // Create Google Generative AI client - this.client = new GoogleGenerativeAI(this.config.apiKey); - - // Set up default model with configuration - this.model = this.client.getGenerativeModel({ - model: this.defaultModel, - safetySettings: this.safetySettings, - generationConfig: this.generationConfig - }); + this.client = new GoogleGenAI({ apiKey: this.config.apiKey }); - // Validate connection and permissions await this.validateConnection(); - } catch (error) { - // Clean up on failure this.client = null; - this.model = null; - + throw new AIProviderError( `Failed to initialize Gemini provider: ${(error as Error).message}`, AIErrorType.AUTHENTICATION, @@ -254,113 +123,43 @@ export class GeminiProvider extends BaseAIProvider { } } - /** - * Generates a text completion using Gemini's generation API. - * - * This method: - * 1. Converts messages to Gemini's format with system instructions - * 2. Chooses between single-turn and multi-turn conversation modes - * 3. Makes API call with comprehensive error handling - * 4. Formats response to standard interface - * - * @protected - * @param params - Validated completion parameters - * @returns Promise resolving to formatted completion response - * @throws {Error} If API request fails - */ protected async doComplete(params: CompletionParams): Promise> { - if (!this.client || !this.model) { + if (!this.client) { throw new AIProviderError('Gemini client not initialized', AIErrorType.INVALID_REQUEST); } try { - // Get the model for this request (might be different from default) - const model = this.getModelForRequest(params); const { systemInstruction, contents } = this.convertMessages(params.messages); - - // Choose appropriate generation method based on conversation length - if (contents.length > 1) { - // Multi-turn conversation - use chat session - const chat = model.startChat({ - history: contents.slice(0, -1), - systemInstruction, - generationConfig: this.buildGenerationConfig(params) - }); - - const lastMessage = contents[contents.length - 1]; - if (!lastMessage) { - throw new AIProviderError('No valid messages provided', AIErrorType.INVALID_REQUEST); - } - - const result = await chat.sendMessage(lastMessage.parts); - return this.formatCompletionResponse(result.response, params.model || this.defaultModel); - - } else { - // Single message - use generateContent - const result = await model.generateContent({ - contents, - systemInstruction, - generationConfig: this.buildGenerationConfig(params) - }); - - return this.formatCompletionResponse(result.response, params.model || this.defaultModel); - } + const model = params.model || this.defaultModel; + + const response = await this.client.models.generateContent({ + model, + contents, + config: this.buildConfig(params, systemInstruction) + }); + + return this.formatCompletionResponse(response, model); } catch (error) { throw this.handleGeminiError(error as Error); } } - /** - * Generates a streaming text completion using Gemini's streaming API. - * - * This method: - * 1. Sets up appropriate streaming mode based on conversation type - * 2. Handles real-time stream chunks from Gemini - * 3. Tracks token usage throughout the stream - * 4. Yields formatted chunks with proper completion tracking - * - * @protected - * @param params - Validated completion parameters - * @returns AsyncIterable yielding completion chunks - * @throws {Error} If streaming request fails - */ protected async *doStream(params: CompletionParams): AsyncIterable { - if (!this.client || !this.model) { + if (!this.client) { throw new AIProviderError('Gemini client not initialized', AIErrorType.INVALID_REQUEST); } try { - // Get the model for this request - const model = this.getModelForRequest(params); const { systemInstruction, contents } = this.convertMessages(params.messages); - - // Set up streaming based on conversation type - let stream; - if (contents.length > 1) { - // Multi-turn conversation - const chat = model.startChat({ - history: contents.slice(0, -1), - systemInstruction, - generationConfig: this.buildGenerationConfig(params) - }); - - const lastMessage = contents[contents.length - 1]; - if (!lastMessage) { - throw new AIProviderError('No valid messages provided', AIErrorType.INVALID_REQUEST); - } - stream = await chat.sendMessageStream(lastMessage.parts); - } else { - // Single message - stream = await model.generateContentStream({ - contents, - systemInstruction, - generationConfig: this.buildGenerationConfig(params) - }); - } + const model = params.model || this.defaultModel; + + const stream = await this.client.models.generateContentStream({ + model, + contents, + config: this.buildConfig(params, systemInstruction) + }); - // Process stream chunks yield* this.processStreamChunks(stream); - } catch (error) { throw this.handleGeminiError(error as Error); } @@ -370,35 +169,28 @@ export class GeminiProvider extends BaseAIProvider { // PUBLIC INTERFACE METHODS // ======================================================================== - /** - * Returns comprehensive information about the Gemini provider. - * - * @returns Provider information including models, capabilities, and limits - */ public getInfo(): ProviderInfo { return { name: 'Gemini', - version: '1.0.0', + version: '2.0.0', models: [ - 'gemini-1.5-pro', // Latest flagship model - 'gemini-1.5-flash', // Fast and efficient variant - 'gemini-1.0-pro', // Previous generation - 'gemini-pro-vision', // Vision-specialized (legacy) - 'gemini-1.5-pro-vision', // Latest vision model - 'gemini-1.0-pro-latest', // Latest 1.0 variant - 'gemini-1.5-pro-latest' // Latest 1.5 variant + 'gemini-2.5-pro', + 'gemini-2.5-flash', + 'gemini-2.0-flash', + 'gemini-1.5-pro', + 'gemini-1.5-flash' ], - maxContextLength: 1048576, // ~1M tokens for Gemini 1.5 + maxContextLength: 1048576, supportsStreaming: true, capabilities: { - vision: true, // Advanced multimodal capabilities - functionCalling: true, // Tool use and function calling - jsonMode: true, // Structured JSON output - systemMessages: true, // System instructions support - reasoning: true, // Strong reasoning capabilities - codeGeneration: true, // Excellent at programming tasks - multimodal: true, // Text, image, video understanding - safetyFiltering: true // Built-in content safety + vision: true, + functionCalling: true, + jsonMode: true, + systemMessages: true, + reasoning: true, + codeGeneration: true, + multimodal: true, + safetyFiltering: true } }; } @@ -407,26 +199,18 @@ export class GeminiProvider extends BaseAIProvider { // PRIVATE UTILITY METHODS // ======================================================================== - /** - * Validates the connection by making a minimal test request. - * - * @private - * @throws {AIProviderError} If connection validation fails - */ private async validateConnection(): Promise { - if (!this.model) { - throw new Error('Model not initialized'); + if (!this.client) { + throw new Error('Client not initialized'); } try { - // Make minimal request to test connection and permissions - await this.model.generateContent({ + await this.client.models.generateContent({ + model: this.defaultModel, contents: [{ role: 'user', parts: [{ text: 'Hi' }] }], - generationConfig: { maxOutputTokens: 1 } + config: { maxOutputTokens: 1 } }); - } catch (error: any) { - // Handle specific validation errors if (error.message?.includes('API key')) { throw new AIProviderError( 'Invalid Google API key. Please verify your API key from https://aistudio.google.com/app/apikey', @@ -434,7 +218,7 @@ export class GeminiProvider extends BaseAIProvider { error.status ); } - + if (error.message?.includes('quota') || error.message?.includes('billing')) { throw new AIProviderError( 'API quota exceeded or billing issue. Please check your Google Cloud billing and API quotas.', @@ -442,7 +226,7 @@ export class GeminiProvider extends BaseAIProvider { error.status ); } - + if (error.message?.includes('model')) { throw new AIProviderError( `Model '${this.defaultModel}' is not available. Please check the model name or your API access level.`, @@ -450,19 +234,11 @@ export class GeminiProvider extends BaseAIProvider { error.status ); } - - // For other errors during validation, log but don't fail initialization + console.warn('Gemini connection validation warning:', error.message); } } - /** - * Validates model name format for Gemini models. - * - * @private - * @param modelName - Model name to validate - * @throws {AIProviderError} If model name format is invalid - */ private validateModelName(modelName: string): void { if (!modelName || typeof modelName !== 'string') { throw new AIProviderError( @@ -471,65 +247,30 @@ export class GeminiProvider extends BaseAIProvider { ); } - // Gemini model names follow specific patterns const validPatterns = [ - /^gemini-1\.5-pro(?:-latest|-vision)?$/, // e.g., gemini-1.5-pro, gemini-1.5-pro-latest - /^gemini-1\.5-flash(?:-latest)?$/, // e.g., gemini-1.5-flash, gemini-1.5-flash-latest - /^gemini-1\.0-pro(?:-latest|-vision)?$/, // e.g., gemini-1.0-pro, gemini-pro-vision - /^gemini-pro(?:-vision)?$/, // Legacy names: gemini-pro, gemini-pro-vision - /^models\/gemini-.+$/ // Full model path format + /^gemini-2\.5-(?:pro|flash)(?:-lite)?(?:-\d{4}-\d{2}-\d{2})?$/, + /^gemini-2\.0-(?:pro|flash)(?:-lite)?(?:-\d{4}-\d{2}-\d{2})?$/, + /^gemini-1\.5-pro(?:-latest|-vision)?$/, + /^gemini-1\.5-flash(?:-latest|-8b)?$/, + /^gemini-1\.0-pro(?:-latest|-vision)?$/, + /^gemini-pro(?:-vision)?$/, + /^models\/gemini-.+$/ ]; const isValid = validPatterns.some(pattern => pattern.test(modelName)); - + if (!isValid) { console.warn(`Model name '${modelName}' doesn't match expected Gemini naming patterns. This may cause API errors.`); } } - /** - * Gets the appropriate model instance for a request. - * - * @private - * @param params - Completion parameters - * @returns GenerativeModel instance - */ - private getModelForRequest(params: CompletionParams): GenerativeModel { - if (!this.client) { - throw new Error('Client not initialized'); - } - - // Use default model if no specific model requested - if (!params.model || params.model === this.defaultModel) { - return this.model!; - } - - // Create new model instance for different model - return this.client.getGenerativeModel({ - model: params.model, - safetySettings: this.safetySettings, - generationConfig: this.buildGenerationConfig(params) - }); - } - - /** - * Converts generic messages to Gemini's conversation format. - * - * Gemini handles system messages as separate system instructions - * rather than part of the conversation flow. - * - * @private - * @param messages - Input messages array - * @returns Processed messages with system instruction separated - */ private convertMessages(messages: AIMessage[]): ProcessedMessages { let systemInstruction: string | undefined; const contents: Content[] = []; for (const message of messages) { if (message.role === 'system') { - // Combine multiple system messages - systemInstruction = systemInstruction + systemInstruction = systemInstruction ? `${systemInstruction}\n\n${message.content}` : message.content; } else { @@ -543,59 +284,61 @@ export class GeminiProvider extends BaseAIProvider { return { systemInstruction, contents }; } - /** - * Builds generation configuration from completion parameters. - * - * @private - * @param params - Completion parameters - * @returns Gemini generation configuration - */ - private buildGenerationConfig(params: CompletionParams): GenerationConfig { - return { - temperature: params.temperature ?? this.generationConfig?.temperature ?? 0.7, - topP: params.topP ?? this.generationConfig?.topP, - topK: this.generationConfig?.topK, - maxOutputTokens: params.maxTokens ?? this.generationConfig?.maxOutputTokens ?? 1000, - stopSequences: params.stopSequences ?? this.generationConfig?.stopSequences + private buildConfig( + params: CompletionParams, + systemInstruction?: string + ): GenerateContentConfig { + const defaults = this.defaultGenerationConfig; + + const config: GenerateContentConfig = { + temperature: params.temperature ?? defaults?.temperature ?? 0.7, + maxOutputTokens: params.maxTokens ?? defaults?.maxOutputTokens ?? 1000 }; + + const topP = params.topP ?? defaults?.topP; + if (topP !== undefined) config.topP = topP; + + if (defaults?.topK !== undefined) config.topK = defaults.topK; + + const stopSequences = params.stopSequences ?? defaults?.stopSequences; + if (stopSequences) config.stopSequences = stopSequences; + + if (systemInstruction) config.systemInstruction = systemInstruction; + if (this.safetySettings) config.safetySettings = this.safetySettings; + + return config; } - /** - * Processes streaming response chunks from Gemini API. - * - * @private - * @param stream - Gemini streaming response - * @returns AsyncIterable of formatted completion chunks - */ - private async *processStreamChunks(stream: any): AsyncIterable { - let fullText = ''; - const requestId = `gemini-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + private async *processStreamChunks( + stream: AsyncGenerator + ): AsyncIterable { + const requestId = `gemini-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`; + let lastUsage: GenerateContentResponse['usageMetadata'] | undefined; try { - // Process streaming chunks - for await (const chunk of stream.stream) { - const chunkText = chunk.text(); - fullText += chunkText; - - yield { - content: chunkText, - isComplete: false, - id: requestId - }; + for await (const chunk of stream) { + if (chunk.usageMetadata) { + lastUsage = chunk.usageMetadata; + } + + const text = chunk.text; + if (text) { + yield { + content: text, + isComplete: false, + id: requestId + }; + } } - // Final chunk with usage information - const finalResponse = await stream.response; - const usageMetadata = finalResponse.usageMetadata; - yield { content: '', isComplete: true, id: requestId, usage: { - promptTokens: usageMetadata?.promptTokenCount || 0, - completionTokens: usageMetadata?.candidatesTokenCount || 0, - totalTokens: usageMetadata?.totalTokenCount || 0 + promptTokens: lastUsage?.promptTokenCount || 0, + completionTokens: lastUsage?.candidatesTokenCount || 0, + totalTokens: lastUsage?.totalTokenCount || 0 } }; } catch (error) { @@ -608,38 +351,11 @@ export class GeminiProvider extends BaseAIProvider { } } - /** - * Formats Gemini's response to our standard interface. - * - * @private - * @param response - Raw Gemini API response - * @param model - Model used for generation - * @returns Formatted completion response - * @throws {AIProviderError} If response format is unexpected - */ - private formatCompletionResponse(response: any, model: string): CompletionResponse { - // Handle multiple text parts in the response - const candidate = response.candidates?.[0]; - if (!candidate) { - throw new AIProviderError( - 'No candidates found in Gemini response', - AIErrorType.UNKNOWN - ); - } - - const content = candidate.content; - if (!content || !content.parts) { - throw new AIProviderError( - 'No content found in Gemini response', - AIErrorType.UNKNOWN - ); - } - - // Combine all text parts - const text = content.parts - .filter((part: any) => part.text) - .map((part: any) => part.text) - .join(''); + private formatCompletionResponse( + response: GenerateContentResponse, + model: string + ): CompletionResponse { + const text = response.text; if (!text) { throw new AIProviderError( @@ -648,9 +364,11 @@ export class GeminiProvider extends BaseAIProvider { ); } + const candidate = response.candidates?.[0]; + return { content: text, - model: model, + model, usage: { promptTokens: response.usageMetadata?.promptTokenCount || 0, completionTokens: response.usageMetadata?.candidatesTokenCount || 0, @@ -658,23 +376,13 @@ export class GeminiProvider extends BaseAIProvider { }, id: `gemini-${Date.now()}`, metadata: { - finishReason: candidate.finishReason, - safetyRatings: candidate.safetyRatings, - citationMetadata: candidate.citationMetadata + finishReason: candidate?.finishReason, + safetyRatings: candidate?.safetyRatings, + citationMetadata: candidate?.citationMetadata } }; } - /** - * Handles and transforms Gemini-specific errors. - * - * This method maps Gemini's error responses to our standardized - * error format, providing helpful context and actionable suggestions. - * - * @private - * @param error - Original error from Gemini API - * @returns Normalized AIProviderError - */ private handleGeminiError(error: any): AIProviderError { if (error instanceof AIProviderError) { return error; @@ -683,7 +391,6 @@ export class GeminiProvider extends BaseAIProvider { const message = error.message || 'Unknown Gemini API error'; const status = error.status || error.statusCode; - // Map Gemini-specific error patterns if (message.includes('API key')) { return new AIProviderError( 'Invalid Google API key. Please verify your API key from https://aistudio.google.com/app/apikey', @@ -747,7 +454,6 @@ export class GeminiProvider extends BaseAIProvider { ); } - // Handle HTTP status codes switch (status) { case 400: return new AIProviderError( @@ -801,4 +507,4 @@ export class GeminiProvider extends BaseAIProvider { ); } } -} \ No newline at end of file +}