11 Commits

Author SHA1 Message Date
797fad1b00 chore(release): 2.0.0
Major release: @google/generative-ai → @google/genai migration changes
the type signature of GeminiConfig.safetySettings, which is a breaking
change for consumers using that option.

Other changes accumulated since 1.3.3:
- @anthropic-ai/sdk 0.65 → 0.97
- openai 4 → 6
- @types/node 24 → 25
- TypeScript 6 support (peer widened to ^5 || ^6)
- Default Gemini model: gemini-1.5-flash → gemini-2.5-flash
2026-05-21 12:56:46 +02:00
fbe66623a1 Merge pull request 'chore(deps)!: migrate from @google/generative-ai to @google/genai' (#6) from chore/deps-google-genai-migration into main
Reviewed-on: #6
2026-05-21 10:56:04 +00:00
c658c67a0c 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.
2026-05-21 12:54:48 +02:00
442e535d17 Merge pull request 'chore(deps): support TypeScript 6 (widen peer to ^5 || ^6)' (#5) from chore/deps-typescript-6 into main
Reviewed-on: #5
2026-05-21 10:49:23 +00:00
bcdf04a77f chore(deps): support TypeScript 6 (widen peer to ^5 || ^6)
- Add typescript ^6.0.0 as devDependency for verified builds
- Widen peerDependency to ^5 || ^6 to keep TS 5 consumers supported
- tsconfig.build.json: set explicit rootDir (required by TS 6 when outDir is set)
- tsconfig.build.json: replace deprecated moduleResolution "node" with "bundler"
2026-05-21 12:49:05 +02:00
10dcb9da85 Merge pull request 'chore(deps): update openai to ^6.0.0' (#4) from chore/deps-openai-v6 into main
Reviewed-on: #4
2026-05-21 10:46:42 +00:00
a596d0adc3 chore(deps): update openai to ^6.0.0 2026-05-21 12:46:30 +02:00
7b3a8297b8 Merge pull request 'chore(deps): update @anthropic-ai/sdk to ^0.97.0' (#3) from chore/deps-anthropic-sdk-0.97 into main
Reviewed-on: #3
2026-05-21 10:45:53 +00:00
b58e1d833f chore(deps): update @anthropic-ai/sdk to ^0.97.0 2026-05-21 12:42:26 +02:00
5c95c972ce Merge pull request 'chore(deps): update @types/node to v25' (#2) from chore/deps-types-node-25 into main
Reviewed-on: #2
2026-05-21 10:41:46 +00:00
a476ec7b25 chore(deps): update @types/node to v25 2026-05-21 12:41:25 +02:00
4 changed files with 223 additions and 502 deletions

127
bun.lock
View File

@@ -4,116 +4,129 @@
"": {
"name": "simple-ai-provider",
"dependencies": {
"@anthropic-ai/sdk": "^0.65.0",
"@google/generative-ai": "^0.24.1",
"openai": "^4.104.0",
"@anthropic-ai/sdk": "^0.97.0",
"@google/genai": "^2.5.0",
"openai": "^6.0.0",
},
"devDependencies": {
"@types/bun": "^1.3.14",
"@types/node": "^24.0.0",
"@types/node": "^25.0.0",
"typescript": "^6.0.0",
},
"peerDependencies": {
"typescript": "^5",
"typescript": "^5 || ^6",
},
},
},
"packages": {
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.65.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-zIdPOcrCVEI8t3Di40nH4z9EoeyGZfXbYSvWdDLsB/KkaSYMnEgC7gmcgWu83g2NTn1ZTpbMvpdttWDGGIk6zw=="],
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.97.1", "", { "dependencies": { "json-schema-to-ts": "^3.1.1", "standardwebhooks": "^1.0.0" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-wOf7AUeJPitcVpvKO4UMu63mWH5SaVipkGd7OOQJt/G6VYGlV8D2Gp9dLxOrttDJh/9gqPqdaBwDGcBevumeAg=="],
"@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=="],
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
"@types/node": ["@types/node@24.12.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA=="],
"@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="],
"@types/node-fetch": ["@types/node-fetch@2.6.12", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.0" } }, "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA=="],
"@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="],
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
"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=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"fast-sha256": ["fast-sha256@1.3.0", "", {}, "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
"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=="],
"event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
"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=="],
"form-data": ["form-data@4.0.2", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.12" } }, "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w=="],
"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=="],
"form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="],
"google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="],
"formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="],
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="],
"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=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"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=="],
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"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=="],
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"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@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
"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@4.104.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" }, "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA=="],
"openai": ["openai@6.38.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-AoMplt2UalrpgUDMh3L09QWjNRlgJPipclQvA6sYAaeF6nHNBMgmikAZGmcYLn8on4d9sQY9Q8bOLfrBS7Lc8g=="],
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
"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=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
"web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="],
"web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
"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=="],
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
"bun-types/@types/node": ["@types/node@24.12.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA=="],
"@types/node-fetch/@types/node": ["@types/node@22.15.23", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-7Ec1zaFPF4RJ0eXu1YT/xgiebqwqoJz8rYPDi/O2BcZ++Wpt0Kq9cl0eg6NN6bYbPnR67ZLo7St5Q3UK0SnARw=="],
"openai/@types/node": ["@types/node@18.19.104", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-mqjoYx1RjmN61vjnHWfiWzAlwvBKutoUdm+kYLPnjI5DCh8ZqofUhaTbT3WLl7bt3itR8DuCf8ShnxI0JvIC3g=="],
"@types/node-fetch/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"openai/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"bun-types/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "simple-ai-provider",
"version": "1.3.3",
"version": "2.0.0",
"description": "A simple and extensible AI provider package for easy integration of multiple AI services",
"main": "dist/index.js",
"module": "dist/index.mjs",
@@ -52,16 +52,17 @@
"url": "https://gitea.jleibl.net/jleibl/simple-ai-provider.git"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.65.0",
"@google/generative-ai": "^0.24.1",
"openai": "^4.104.0"
"@anthropic-ai/sdk": "^0.97.0",
"@google/genai": "^2.5.0",
"openai": "^6.0.0"
},
"devDependencies": {
"@types/bun": "^1.3.14",
"@types/node": "^24.0.0"
"@types/node": "^25.0.0",
"typescript": "^6.0.0"
},
"peerDependencies": {
"typescript": "^5"
"typescript": "^5 || ^6"
},
"engines": {
"node": ">=18.0.0"

View File

@@ -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<void> {
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<CompletionResponse<string>> {
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<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);
}
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<void> {
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<CompletionChunk> {
let fullText = '';
const requestId = `gemini-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
private async *processStreamChunks(
stream: AsyncGenerator<GenerateContentResponse>
): AsyncIterable<CompletionChunk> {
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<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('');
private formatCompletionResponse(
response: GenerateContentResponse,
model: string
): CompletionResponse<string> {
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 {
);
}
}
}
}

View File

@@ -5,7 +5,8 @@
"emitDeclarationOnly": true,
"declaration": true,
"outDir": "./dist",
"moduleResolution": "node",
"rootDir": "./src",
"moduleResolution": "bundler",
"verbatimModuleSyntax": false
},
"include": [