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.
This commit is contained in:
2026-05-21 12:54:48 +02:00
parent 442e535d17
commit c658c67a0c
3 changed files with 225 additions and 441 deletions

View File

@@ -5,7 +5,7 @@
"name": "simple-ai-provider", "name": "simple-ai-provider",
"dependencies": { "dependencies": {
"@anthropic-ai/sdk": "^0.97.0", "@anthropic-ai/sdk": "^0.97.0",
"@google/generative-ai": "^0.24.1", "@google/genai": "^2.5.0",
"openai": "^6.0.0", "openai": "^6.0.0",
}, },
"devDependencies": { "devDependencies": {
@@ -23,7 +23,27 @@
"@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], "@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=="], "@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/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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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": ["@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=="], "bun-types/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],

View File

@@ -53,7 +53,7 @@
}, },
"dependencies": { "dependencies": {
"@anthropic-ai/sdk": "^0.97.0", "@anthropic-ai/sdk": "^0.97.0",
"@google/generative-ai": "^0.24.1", "@google/genai": "^2.5.0",
"openai": "^6.0.0" "openai": "^6.0.0"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -1,32 +1,26 @@
/** /**
* Google Gemini Provider Implementation * Google Gemini Provider Implementation
* *
* This module provides integration with Google's Gemini models through the official * Integrates with Google's Gemini models through the official @google/genai SDK
* Generative AI SDK. Gemini offers cutting-edge AI capabilities including multimodal * (the successor to the deprecated @google/generative-ai package).
* understanding, advanced reasoning, and efficient text generation across various tasks. *
*
* Key Features: * Key Features:
* - Support for all Gemini model variants (Gemini 1.5 Pro, Flash, Pro Vision) * - Support for Gemini 1.5 / 2.0 / 2.5 model families
* - Advanced streaming support with real-time token delivery * - Streaming responses with real-time chunk delivery
* - Native multimodal capabilities (text, images, video) * - Multimodal capabilities and configurable safety filtering
* - Sophisticated safety settings and content filtering * - System instruction handling separate from conversation flow
* - 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
*
* @author Jan-Marlon Leibl * @author Jan-Marlon Leibl
* @version 1.0.0
* @see https://ai.google.dev/docs * @see https://ai.google.dev/docs
*/ */
import { GoogleGenerativeAI, GenerativeModel } from '@google/generative-ai'; import { GoogleGenAI } from '@google/genai';
import type { Content, Part, GenerationConfig, SafetySetting } from '@google/generative-ai'; import type {
Content,
GenerateContentConfig,
GenerateContentResponse,
SafetySetting
} from '@google/genai';
import type { import type {
AIProviderConfig, AIProviderConfig,
CompletionParams, CompletionParams,
@@ -44,84 +38,39 @@ import { AIProviderError, AIErrorType } from '../types/index.js';
/** /**
* Configuration interface for Gemini provider with Google-specific options. * 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 { export interface GeminiConfig extends AIProviderConfig {
/** /**
* Default Gemini model to use for requests. * Default Gemini model to use for requests.
* *
* Recommended models: * Recommended models:
* - 'gemini-1.5-pro': Flagship model, best overall performance and multimodal * - 'gemini-2.5-flash': Latest fast model, great default
* - 'gemini-1.5-flash': Faster and cheaper, good for simple tasks * - 'gemini-2.5-pro': Flagship reasoning model
* - 'gemini-1.0-pro': Previous generation, cost-effective * - 'gemini-2.0-flash': Previous generation fast model
* - 'gemini-pro-vision': Specialized for vision tasks (legacy) * - 'gemini-1.5-pro': Long-context flagship from the 1.5 generation
* *
* @default 'gemini-1.5-flash' * @default 'gemini-2.5-flash'
*/ */
defaultModel?: string; defaultModel?: string;
/** /** Safety settings for content filtering and harm prevention. */
* 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
*/
safetySettings?: SafetySetting[]; safetySettings?: SafetySetting[];
/** /** Default generation configuration applied to every request. */
* 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
*/
generationConfig?: { generationConfig?: {
/** Controls randomness in generation (0.0 to 1.0) */
temperature?: number; temperature?: number;
/** Controls nucleus sampling for diversity (0.0 to 1.0) */
topP?: number; topP?: number;
/** Controls top-k sampling for diversity (positive integer) */
topK?: number; topK?: number;
/** Maximum number of tokens to generate */
maxOutputTokens?: number; maxOutputTokens?: number;
/** Sequences that will stop generation when encountered */
stopSequences?: string[]; stopSequences?: string[];
}; };
} }
/** /**
* Processed messages structure for Gemini's conversation format. * Processed messages structure for Gemini's conversation format.
* Separates system instructions from conversational content.
*/ */
interface ProcessedMessages { interface ProcessedMessages {
/** System instruction for model behavior (if any) */ /** System instruction string (if any) */
systemInstruction?: string; systemInstruction?: string;
/** Conversation content in Gemini's format */ /** Conversation content in Gemini's format */
contents: Content[]; contents: Content[];
@@ -132,80 +81,24 @@ interface ProcessedMessages {
// ============================================================================ // ============================================================================
/** /**
* Google Gemini provider implementation. * Google Gemini provider implementation backed by @google/genai.
* *
* This class handles all interactions with Google's Gemini models through their * The new SDK is stateless w.r.t. model selection — each `generateContent` call
* official Generative AI SDK. It provides optimized handling of Gemini's unique * specifies the model directly, so this class only holds a single client.
* 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
* });
* ```
*/ */
export class GeminiProvider extends BaseAIProvider { export class GeminiProvider extends BaseAIProvider {
// ======================================================================== private client: GoogleGenAI | null = null;
// 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 readonly defaultModel: string; private readonly defaultModel: string;
/** Safety settings for content filtering */
private readonly safetySettings?: SafetySetting[]; private readonly safetySettings?: SafetySetting[];
private readonly defaultGenerationConfig?: GeminiConfig['generationConfig'];
/** Generation configuration defaults */
private readonly generationConfig?: any;
// ========================================================================
// CONSTRUCTOR
// ========================================================================
/**
* Creates a new Gemini provider instance.
*
* @param config - Gemini-specific configuration options
* @throws {AIProviderError} If configuration validation fails
*/
constructor(config: GeminiConfig) { constructor(config: GeminiConfig) {
super(config); super(config);
// Set Gemini-specific defaults this.defaultModel = config.defaultModel || 'gemini-2.5-flash';
this.defaultModel = config.defaultModel || 'gemini-1.5-flash';
this.safetySettings = config.safetySettings; this.safetySettings = config.safetySettings;
this.generationConfig = config.generationConfig; this.defaultGenerationConfig = config.generationConfig;
// Validate model name format
this.validateModelName(this.defaultModel); this.validateModelName(this.defaultModel);
} }
@@ -213,38 +106,14 @@ export class GeminiProvider extends BaseAIProvider {
// PROTECTED TEMPLATE METHOD IMPLEMENTATIONS // 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<void> { protected async doInitialize(): Promise<void> {
try { try {
// Create Google Generative AI client this.client = new GoogleGenAI({ apiKey: this.config.apiKey });
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
});
// Validate connection and permissions
await this.validateConnection(); await this.validateConnection();
} catch (error) { } catch (error) {
// Clean up on failure
this.client = null; this.client = null;
this.model = null;
throw new AIProviderError( throw new AIProviderError(
`Failed to initialize Gemini provider: ${(error as Error).message}`, `Failed to initialize Gemini provider: ${(error as Error).message}`,
AIErrorType.AUTHENTICATION, 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<CompletionResponse<string>> { protected async doComplete(params: CompletionParams): Promise<CompletionResponse<string>> {
if (!this.client || !this.model) { if (!this.client) {
throw new AIProviderError('Gemini client not initialized', AIErrorType.INVALID_REQUEST); throw new AIProviderError('Gemini client not initialized', AIErrorType.INVALID_REQUEST);
} }
try { try {
// Get the model for this request (might be different from default)
const model = this.getModelForRequest(params);
const { systemInstruction, contents } = this.convertMessages(params.messages); const { systemInstruction, contents } = this.convertMessages(params.messages);
const model = params.model || this.defaultModel;
// Choose appropriate generation method based on conversation length
if (contents.length > 1) { const response = await this.client.models.generateContent({
// Multi-turn conversation - use chat session model,
const chat = model.startChat({ contents,
history: contents.slice(0, -1), config: this.buildConfig(params, systemInstruction)
systemInstruction, });
generationConfig: this.buildGenerationConfig(params)
}); return this.formatCompletionResponse(response, model);
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);
}
} catch (error) { } catch (error) {
throw this.handleGeminiError(error as 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<T = any>(params: CompletionParams<T>): AsyncIterable<CompletionChunk> { protected async *doStream<T = any>(params: CompletionParams<T>): AsyncIterable<CompletionChunk> {
if (!this.client || !this.model) { if (!this.client) {
throw new AIProviderError('Gemini client not initialized', AIErrorType.INVALID_REQUEST); throw new AIProviderError('Gemini client not initialized', AIErrorType.INVALID_REQUEST);
} }
try { try {
// Get the model for this request
const model = this.getModelForRequest(params);
const { systemInstruction, contents } = this.convertMessages(params.messages); const { systemInstruction, contents } = this.convertMessages(params.messages);
const model = params.model || this.defaultModel;
// Set up streaming based on conversation type
let stream; const stream = await this.client.models.generateContentStream({
if (contents.length > 1) { model,
// Multi-turn conversation contents,
const chat = model.startChat({ config: this.buildConfig(params, systemInstruction)
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)
});
}
// Process stream chunks
yield* this.processStreamChunks(stream); yield* this.processStreamChunks(stream);
} catch (error) { } catch (error) {
throw this.handleGeminiError(error as Error); throw this.handleGeminiError(error as Error);
} }
@@ -370,35 +169,28 @@ export class GeminiProvider extends BaseAIProvider {
// PUBLIC INTERFACE METHODS // PUBLIC INTERFACE METHODS
// ======================================================================== // ========================================================================
/**
* Returns comprehensive information about the Gemini provider.
*
* @returns Provider information including models, capabilities, and limits
*/
public getInfo(): ProviderInfo { public getInfo(): ProviderInfo {
return { return {
name: 'Gemini', name: 'Gemini',
version: '1.0.0', version: '2.0.0',
models: [ models: [
'gemini-1.5-pro', // Latest flagship model 'gemini-2.5-pro',
'gemini-1.5-flash', // Fast and efficient variant 'gemini-2.5-flash',
'gemini-1.0-pro', // Previous generation 'gemini-2.0-flash',
'gemini-pro-vision', // Vision-specialized (legacy) 'gemini-1.5-pro',
'gemini-1.5-pro-vision', // Latest vision model 'gemini-1.5-flash'
'gemini-1.0-pro-latest', // Latest 1.0 variant
'gemini-1.5-pro-latest' // Latest 1.5 variant
], ],
maxContextLength: 1048576, // ~1M tokens for Gemini 1.5 maxContextLength: 1048576,
supportsStreaming: true, supportsStreaming: true,
capabilities: { capabilities: {
vision: true, // Advanced multimodal capabilities vision: true,
functionCalling: true, // Tool use and function calling functionCalling: true,
jsonMode: true, // Structured JSON output jsonMode: true,
systemMessages: true, // System instructions support systemMessages: true,
reasoning: true, // Strong reasoning capabilities reasoning: true,
codeGeneration: true, // Excellent at programming tasks codeGeneration: true,
multimodal: true, // Text, image, video understanding multimodal: true,
safetyFiltering: true // Built-in content safety safetyFiltering: true
} }
}; };
} }
@@ -407,26 +199,18 @@ export class GeminiProvider extends BaseAIProvider {
// PRIVATE UTILITY METHODS // PRIVATE UTILITY METHODS
// ======================================================================== // ========================================================================
/**
* Validates the connection by making a minimal test request.
*
* @private
* @throws {AIProviderError} If connection validation fails
*/
private async validateConnection(): Promise<void> { private async validateConnection(): Promise<void> {
if (!this.model) { if (!this.client) {
throw new Error('Model not initialized'); throw new Error('Client not initialized');
} }
try { try {
// Make minimal request to test connection and permissions await this.client.models.generateContent({
await this.model.generateContent({ model: this.defaultModel,
contents: [{ role: 'user', parts: [{ text: 'Hi' }] }], contents: [{ role: 'user', parts: [{ text: 'Hi' }] }],
generationConfig: { maxOutputTokens: 1 } config: { maxOutputTokens: 1 }
}); });
} catch (error: any) { } catch (error: any) {
// Handle specific validation errors
if (error.message?.includes('API key')) { if (error.message?.includes('API key')) {
throw new AIProviderError( throw new AIProviderError(
'Invalid Google API key. Please verify your API key from https://aistudio.google.com/app/apikey', '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 error.status
); );
} }
if (error.message?.includes('quota') || error.message?.includes('billing')) { if (error.message?.includes('quota') || error.message?.includes('billing')) {
throw new AIProviderError( throw new AIProviderError(
'API quota exceeded or billing issue. Please check your Google Cloud billing and API quotas.', '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 error.status
); );
} }
if (error.message?.includes('model')) { if (error.message?.includes('model')) {
throw new AIProviderError( throw new AIProviderError(
`Model '${this.defaultModel}' is not available. Please check the model name or your API access level.`, `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 error.status
); );
} }
// For other errors during validation, log but don't fail initialization
console.warn('Gemini connection validation warning:', error.message); 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 { private validateModelName(modelName: string): void {
if (!modelName || typeof modelName !== 'string') { if (!modelName || typeof modelName !== 'string') {
throw new AIProviderError( throw new AIProviderError(
@@ -471,65 +247,30 @@ export class GeminiProvider extends BaseAIProvider {
); );
} }
// Gemini model names follow specific patterns
const validPatterns = [ const validPatterns = [
/^gemini-1\.5-pro(?:-latest|-vision)?$/, // e.g., gemini-1.5-pro, gemini-1.5-pro-latest /^gemini-2\.5-(?:pro|flash)(?:-lite)?(?:-\d{4}-\d{2}-\d{2})?$/,
/^gemini-1\.5-flash(?:-latest)?$/, // e.g., gemini-1.5-flash, gemini-1.5-flash-latest /^gemini-2\.0-(?:pro|flash)(?:-lite)?(?:-\d{4}-\d{2}-\d{2})?$/,
/^gemini-1\.0-pro(?:-latest|-vision)?$/, // e.g., gemini-1.0-pro, gemini-pro-vision /^gemini-1\.5-pro(?:-latest|-vision)?$/,
/^gemini-pro(?:-vision)?$/, // Legacy names: gemini-pro, gemini-pro-vision /^gemini-1\.5-flash(?:-latest|-8b)?$/,
/^models\/gemini-.+$/ // Full model path format /^gemini-1\.0-pro(?:-latest|-vision)?$/,
/^gemini-pro(?:-vision)?$/,
/^models\/gemini-.+$/
]; ];
const isValid = validPatterns.some(pattern => pattern.test(modelName)); const isValid = validPatterns.some(pattern => pattern.test(modelName));
if (!isValid) { if (!isValid) {
console.warn(`Model name '${modelName}' doesn't match expected Gemini naming patterns. This may cause API errors.`); 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 { private convertMessages(messages: AIMessage[]): ProcessedMessages {
let systemInstruction: string | undefined; let systemInstruction: string | undefined;
const contents: Content[] = []; const contents: Content[] = [];
for (const message of messages) { for (const message of messages) {
if (message.role === 'system') { if (message.role === 'system') {
// Combine multiple system messages systemInstruction = systemInstruction
systemInstruction = systemInstruction
? `${systemInstruction}\n\n${message.content}` ? `${systemInstruction}\n\n${message.content}`
: message.content; : message.content;
} else { } else {
@@ -543,59 +284,61 @@ export class GeminiProvider extends BaseAIProvider {
return { systemInstruction, contents }; return { systemInstruction, contents };
} }
/** private buildConfig(
* Builds generation configuration from completion parameters. params: CompletionParams,
* systemInstruction?: string
* @private ): GenerateContentConfig {
* @param params - Completion parameters const defaults = this.defaultGenerationConfig;
* @returns Gemini generation configuration
*/ const config: GenerateContentConfig = {
private buildGenerationConfig(params: CompletionParams): GenerationConfig { temperature: params.temperature ?? defaults?.temperature ?? 0.7,
return { maxOutputTokens: params.maxTokens ?? defaults?.maxOutputTokens ?? 1000
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
}; };
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;
} }
/** private async *processStreamChunks(
* Processes streaming response chunks from Gemini API. stream: AsyncGenerator<GenerateContentResponse>
* ): AsyncIterable<CompletionChunk> {
* @private const requestId = `gemini-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
* @param stream - Gemini streaming response let lastUsage: GenerateContentResponse['usageMetadata'] | undefined;
* @returns AsyncIterable of formatted completion chunks
*/
private async *processStreamChunks(stream: any): AsyncIterable<CompletionChunk> {
let fullText = '';
const requestId = `gemini-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
try { try {
// Process streaming chunks for await (const chunk of stream) {
for await (const chunk of stream.stream) { if (chunk.usageMetadata) {
const chunkText = chunk.text(); lastUsage = chunk.usageMetadata;
fullText += chunkText; }
yield { const text = chunk.text;
content: chunkText, if (text) {
isComplete: false, yield {
id: requestId content: text,
}; isComplete: false,
id: requestId
};
}
} }
// Final chunk with usage information
const finalResponse = await stream.response;
const usageMetadata = finalResponse.usageMetadata;
yield { yield {
content: '', content: '',
isComplete: true, isComplete: true,
id: requestId, id: requestId,
usage: { usage: {
promptTokens: usageMetadata?.promptTokenCount || 0, promptTokens: lastUsage?.promptTokenCount || 0,
completionTokens: usageMetadata?.candidatesTokenCount || 0, completionTokens: lastUsage?.candidatesTokenCount || 0,
totalTokens: usageMetadata?.totalTokenCount || 0 totalTokens: lastUsage?.totalTokenCount || 0
} }
}; };
} catch (error) { } catch (error) {
@@ -608,38 +351,11 @@ export class GeminiProvider extends BaseAIProvider {
} }
} }
/** private formatCompletionResponse(
* Formats Gemini's response to our standard interface. response: GenerateContentResponse,
* model: string
* @private ): CompletionResponse<string> {
* @param response - Raw Gemini API response const text = response.text;
* @param model - Model used for generation
* @returns Formatted completion response
* @throws {AIProviderError} If response format is unexpected
*/
private formatCompletionResponse(response: any, model: string): CompletionResponse<string> {
// 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('');
if (!text) { if (!text) {
throw new AIProviderError( throw new AIProviderError(
@@ -648,9 +364,11 @@ export class GeminiProvider extends BaseAIProvider {
); );
} }
const candidate = response.candidates?.[0];
return { return {
content: text, content: text,
model: model, model,
usage: { usage: {
promptTokens: response.usageMetadata?.promptTokenCount || 0, promptTokens: response.usageMetadata?.promptTokenCount || 0,
completionTokens: response.usageMetadata?.candidatesTokenCount || 0, completionTokens: response.usageMetadata?.candidatesTokenCount || 0,
@@ -658,23 +376,13 @@ export class GeminiProvider extends BaseAIProvider {
}, },
id: `gemini-${Date.now()}`, id: `gemini-${Date.now()}`,
metadata: { metadata: {
finishReason: candidate.finishReason, finishReason: candidate?.finishReason,
safetyRatings: candidate.safetyRatings, safetyRatings: candidate?.safetyRatings,
citationMetadata: candidate.citationMetadata 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 { private handleGeminiError(error: any): AIProviderError {
if (error instanceof AIProviderError) { if (error instanceof AIProviderError) {
return error; return error;
@@ -683,7 +391,6 @@ export class GeminiProvider extends BaseAIProvider {
const message = error.message || 'Unknown Gemini API error'; const message = error.message || 'Unknown Gemini API error';
const status = error.status || error.statusCode; const status = error.status || error.statusCode;
// Map Gemini-specific error patterns
if (message.includes('API key')) { if (message.includes('API key')) {
return new AIProviderError( return new AIProviderError(
'Invalid Google API key. Please verify your API key from https://aistudio.google.com/app/apikey', '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) { switch (status) {
case 400: case 400:
return new AIProviderError( return new AIProviderError(
@@ -801,4 +507,4 @@ export class GeminiProvider extends BaseAIProvider {
); );
} }
} }
} }