Compare commits
12 Commits
7b3a8297b8
...
refactor/o
| Author | SHA1 | Date | |
|---|---|---|---|
|
3d985a95a8
|
|||
|
eacd76d259
|
|||
|
b36d57711b
|
|||
|
8e73296ac2
|
|||
|
bb37e61eaf
|
|||
|
797fad1b00
|
|||
|
fbe66623a1
|
|||
|
c658c67a0c
|
|||
|
442e535d17
|
|||
|
bcdf04a77f
|
|||
|
10dcb9da85
|
|||
|
a596d0adc3
|
115
bun.lock
115
bun.lock
@@ -5,15 +5,16 @@
|
||||
"name": "simple-ai-provider",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.97.0",
|
||||
"@google/generative-ai": "^0.24.1",
|
||||
"openai": "^4.104.0",
|
||||
"@google/genai": "^2.5.0",
|
||||
"openai": "^6.0.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.14",
|
||||
"@types/node": "^25.0.0",
|
||||
"typescript": "^6.0.0",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
"typescript": "^5 || ^6",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -22,7 +23,27 @@
|
||||
|
||||
"@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
|
||||
|
||||
"@google/generative-ai": ["@google/generative-ai@0.24.1", "", {}, "sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q=="],
|
||||
"@google/genai": ["@google/genai@2.5.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-qDi3LLh9I3llJK0f9uV8kZ8EdT9oHPxGJJ9yOJ/i5YXYrVwRCs8jHo9x4e99uOeKYDvD3TZwT70p/H/LS3BixQ=="],
|
||||
|
||||
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
|
||||
|
||||
"@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
|
||||
|
||||
"@protobufjs/codegen": ["@protobufjs/codegen@2.0.5", "", {}, "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g=="],
|
||||
|
||||
"@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="],
|
||||
|
||||
"@protobufjs/fetch": ["@protobufjs/fetch@1.1.1", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1" } }, "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw=="],
|
||||
|
||||
"@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="],
|
||||
|
||||
"@protobufjs/inquire": ["@protobufjs/inquire@1.1.2", "", {}, "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw=="],
|
||||
|
||||
"@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="],
|
||||
|
||||
"@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="],
|
||||
|
||||
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.1", "", {}, "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg=="],
|
||||
|
||||
"@stablelib/base64": ["@stablelib/base64@1.0.1", "", {}, "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="],
|
||||
|
||||
@@ -30,100 +51,82 @@
|
||||
|
||||
"@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=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
|
||||
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
||||
|
||||
"fast-sha256": ["fast-sha256@1.3.0", "", {}, "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ=="],
|
||||
|
||||
"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=="],
|
||||
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
|
||||
|
||||
"form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="],
|
||||
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
|
||||
|
||||
"formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="],
|
||||
"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=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
"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=="],
|
||||
|
||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||
"google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||
|
||||
"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.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=="],
|
||||
|
||||
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
||||
|
||||
"@types/node-fetch/@types/node": ["@types/node@22.15.23", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-7Ec1zaFPF4RJ0eXu1YT/xgiebqwqoJz8rYPDi/O2BcZ++Wpt0Kq9cl0eg6NN6bYbPnR67ZLo7St5Q3UK0SnARw=="],
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"bun-types/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
"openai/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
||||
}
|
||||
}
|
||||
|
||||
11
package.json
11
package.json
@@ -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",
|
||||
@@ -53,15 +53,16 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.97.0",
|
||||
"@google/generative-ai": "^0.24.1",
|
||||
"openai": "^4.104.0"
|
||||
"@google/genai": "^2.5.0",
|
||||
"openai": "^6.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.14",
|
||||
"@types/node": "^25.0.0"
|
||||
"@types/node": "^25.0.0",
|
||||
"typescript": "^6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
"typescript": "^5 || ^6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
|
||||
35
src/constants.ts
Normal file
35
src/constants.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Shared defaults used across all providers.
|
||||
*/
|
||||
|
||||
export const DEFAULT_TIMEOUT_MS = 30_000;
|
||||
export const DEFAULT_MAX_RETRIES = 3;
|
||||
export const DEFAULT_MAX_TOKENS = 1000;
|
||||
export const DEFAULT_TEMPERATURE = 0.7;
|
||||
|
||||
/**
|
||||
* Minimal prompt used to validate API credentials on initialization.
|
||||
*/
|
||||
export const VALIDATION_PROMPT = 'Hi';
|
||||
|
||||
export const DEFAULT_MODELS = {
|
||||
claude: 'claude-3-5-sonnet-20241022',
|
||||
openai: 'gpt-4o',
|
||||
gemini: 'gemini-2.5-flash',
|
||||
openwebui: 'llama3.1:latest'
|
||||
} as const;
|
||||
|
||||
export const DEFAULT_OPENWEBUI_BASE_URL = 'http://localhost:3000';
|
||||
|
||||
export const DEFAULT_ANTHROPIC_VERSION = '2023-06-01';
|
||||
|
||||
/**
|
||||
* Bounds for configuration value validation.
|
||||
*/
|
||||
export const CONFIG_BOUNDS = {
|
||||
timeoutMs: { min: 1000, max: 300_000 },
|
||||
maxRetries: { min: 0, max: 10 },
|
||||
temperature: { min: 0, max: 1 },
|
||||
topP: { min: 0, max: 1, exclusiveMin: true },
|
||||
maxTokens: { min: 1 }
|
||||
} as const;
|
||||
@@ -58,4 +58,4 @@ export const SUPPORTED_PROVIDERS = ['claude', 'openai', 'gemini', 'openwebui'] a
|
||||
/**
|
||||
* Package version
|
||||
*/
|
||||
export const VERSION = '1.3.1';
|
||||
export const VERSION = '2.0.0';
|
||||
|
||||
@@ -23,6 +23,11 @@ import type {
|
||||
ResponseType
|
||||
} from '../types/index.js';
|
||||
import { AIProviderError, AIErrorType, generateResponseTypePrompt, parseAndValidateResponseType } from '../types/index.js';
|
||||
import {
|
||||
CONFIG_BOUNDS,
|
||||
DEFAULT_MAX_RETRIES,
|
||||
DEFAULT_TIMEOUT_MS
|
||||
} from '../constants.js';
|
||||
|
||||
// ============================================================================
|
||||
// ABSTRACT BASE PROVIDER CLASS
|
||||
@@ -283,10 +288,104 @@ export abstract class BaseAIProvider {
|
||||
*/
|
||||
protected abstract doStream<T = any>(params: CompletionParams<T>): AsyncIterable<CompletionChunk>;
|
||||
|
||||
// ========================================================================
|
||||
// PROTECTED HOOKS (subclass overrides; safe defaults provided)
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Regex patterns that valid model names should match.
|
||||
* Empty array (the default) skips validation entirely.
|
||||
*/
|
||||
protected getModelNamePatterns(): RegExp[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a minimal request to verify the API key and connectivity.
|
||||
* The default does nothing; SDK-based providers override this and the
|
||||
* base `validateConnection` handles error mapping.
|
||||
*/
|
||||
protected async sendValidationProbe(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a provider-specific error to a normalized AIProviderError, or
|
||||
* return null to let the base error mapping handle it generically.
|
||||
* Override to recognize provider-specific message patterns (e.g. "API
|
||||
* key", "quota", "model not found").
|
||||
*/
|
||||
protected mapProviderError(_error: any): AIProviderError | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-HTTP-status message overrides used by `normalizeError` when no
|
||||
* provider-specific mapping applies. Lets providers add brand-flavored
|
||||
* hints (console URLs, etc.) without re-implementing the status switch.
|
||||
*/
|
||||
protected providerErrorMessages(): Partial<Record<number, string>> {
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Human-readable provider name used in warning messages.
|
||||
* Default derives from getInfo(); override if construction order makes
|
||||
* getInfo() unsafe to call here.
|
||||
*/
|
||||
protected get providerName(): string {
|
||||
try {
|
||||
return this.getInfo().name;
|
||||
} catch {
|
||||
return 'Provider';
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// PROTECTED UTILITY METHODS
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Validates a model name against the patterns returned by
|
||||
* `getModelNamePatterns()`. Throws for invalid input, warns for
|
||||
* non-matching names (since model lists change frequently).
|
||||
*/
|
||||
protected validateModelName(modelName: string): void {
|
||||
if (!modelName || typeof modelName !== 'string') {
|
||||
throw new AIProviderError(
|
||||
'Model name must be a non-empty string',
|
||||
AIErrorType.INVALID_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
const patterns = this.getModelNamePatterns();
|
||||
if (patterns.length === 0) return;
|
||||
|
||||
const matches = patterns.some(pattern => pattern.test(modelName));
|
||||
if (!matches) {
|
||||
console.warn(
|
||||
`Model name '${modelName}' doesn't match expected ${this.providerName} naming patterns. This may cause API errors.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps `sendValidationProbe()` with consistent error handling: throws
|
||||
* AIProviderError for provider-recognized failures, warns and continues
|
||||
* for transient ones.
|
||||
*/
|
||||
protected async validateConnection(): Promise<void> {
|
||||
try {
|
||||
await this.sendValidationProbe();
|
||||
} catch (error: any) {
|
||||
const mapped = this.mapProviderError(error);
|
||||
if (mapped) {
|
||||
throw mapped;
|
||||
}
|
||||
console.warn(`${this.providerName} connection validation warning:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates and normalizes provider configuration.
|
||||
*
|
||||
@@ -314,60 +413,78 @@ export abstract class BaseAIProvider {
|
||||
);
|
||||
}
|
||||
|
||||
// Apply defaults and return normalized config
|
||||
this.validateNumberInRange('timeout', config.timeout, {
|
||||
...CONFIG_BOUNDS.timeoutMs,
|
||||
label: `${CONFIG_BOUNDS.timeoutMs.min}ms and ${CONFIG_BOUNDS.timeoutMs.max}ms`
|
||||
});
|
||||
this.validateNumberInRange('maxRetries', config.maxRetries, {
|
||||
...CONFIG_BOUNDS.maxRetries,
|
||||
integer: true
|
||||
});
|
||||
|
||||
return {
|
||||
...config,
|
||||
timeout: this.validateTimeout(config.timeout),
|
||||
maxRetries: this.validateMaxRetries(config.maxRetries)
|
||||
timeout: config.timeout ?? DEFAULT_TIMEOUT_MS,
|
||||
maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates timeout configuration value.
|
||||
*
|
||||
* @private
|
||||
* @param timeout - Timeout value to validate
|
||||
* @returns Validated timeout value with default if needed
|
||||
* Validates an optional numeric parameter against a range.
|
||||
* Skips validation when value is undefined so callers can apply defaults afterward.
|
||||
*/
|
||||
private validateTimeout(timeout?: number): number {
|
||||
const defaultTimeout = 30000; // 30 seconds
|
||||
|
||||
if (timeout === undefined) {
|
||||
return defaultTimeout;
|
||||
private validateNumberInRange(
|
||||
name: string,
|
||||
value: number | undefined,
|
||||
bounds: {
|
||||
min?: number;
|
||||
max?: number;
|
||||
exclusiveMin?: boolean;
|
||||
integer?: boolean;
|
||||
label?: string;
|
||||
}
|
||||
): void {
|
||||
if (value === undefined) return;
|
||||
|
||||
if (typeof timeout !== 'number' || timeout < 1000 || timeout > 300000) {
|
||||
const { min, max, exclusiveMin, integer, label } = bounds;
|
||||
const range = label ?? this.describeRange(min, max, exclusiveMin);
|
||||
|
||||
if (typeof value !== 'number' || Number.isNaN(value)) {
|
||||
throw new AIProviderError(
|
||||
'Timeout must be a number between 1000ms (1s) and 300000ms (5min)',
|
||||
`${name} must be a number between ${range}`,
|
||||
AIErrorType.INVALID_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
return timeout;
|
||||
if (integer && !Number.isInteger(value)) {
|
||||
throw new AIProviderError(
|
||||
`${name} must be an integer`,
|
||||
AIErrorType.INVALID_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
if (min !== undefined && (exclusiveMin ? value <= min : value < min)) {
|
||||
throw new AIProviderError(
|
||||
`${name} must be ${exclusiveMin ? 'greater than' : 'at least'} ${min}`,
|
||||
AIErrorType.INVALID_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
if (max !== undefined && value > max) {
|
||||
throw new AIProviderError(
|
||||
`${name} must be at most ${max}`,
|
||||
AIErrorType.INVALID_REQUEST
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates max retries configuration value.
|
||||
*
|
||||
* @private
|
||||
* @param maxRetries - Max retries value to validate
|
||||
* @returns Validated max retries value with default if needed
|
||||
*/
|
||||
private validateMaxRetries(maxRetries?: number): number {
|
||||
const defaultMaxRetries = 3;
|
||||
|
||||
if (maxRetries === undefined) {
|
||||
return defaultMaxRetries;
|
||||
private describeRange(min?: number, max?: number, exclusiveMin?: boolean): string {
|
||||
if (min !== undefined && max !== undefined) {
|
||||
return `${exclusiveMin ? '>' : '>='}${min} and <=${max}`;
|
||||
}
|
||||
|
||||
if (typeof maxRetries !== 'number' || maxRetries < 0 || maxRetries > 10) {
|
||||
throw new AIProviderError(
|
||||
'Max retries must be a number between 0 and 10',
|
||||
AIErrorType.INVALID_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
return maxRetries;
|
||||
if (min !== undefined) return `${exclusiveMin ? '>' : '>='}${min}`;
|
||||
if (max !== undefined) return `<=${max}`;
|
||||
return 'a valid number';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -403,13 +520,11 @@ export abstract class BaseAIProvider {
|
||||
);
|
||||
}
|
||||
|
||||
// Validate messages array
|
||||
this.validateMessages(params.messages);
|
||||
|
||||
// Validate optional parameters
|
||||
this.validateTemperature(params.temperature);
|
||||
this.validateTopP(params.topP);
|
||||
this.validateMaxTokens(params.maxTokens);
|
||||
this.validateNumberInRange('temperature', params.temperature, CONFIG_BOUNDS.temperature);
|
||||
this.validateNumberInRange('topP', params.topP, CONFIG_BOUNDS.topP);
|
||||
this.validateNumberInRange('maxTokens', params.maxTokens, { ...CONFIG_BOUNDS.maxTokens, integer: true });
|
||||
this.validateStopSequences(params.stopSequences);
|
||||
}
|
||||
|
||||
@@ -456,67 +571,6 @@ export abstract class BaseAIProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates temperature parameter.
|
||||
*
|
||||
* @private
|
||||
* @param temperature - Temperature value to validate
|
||||
* @throws {AIProviderError} If temperature is invalid
|
||||
*/
|
||||
private validateTemperature(temperature?: number): void {
|
||||
if (temperature !== undefined) {
|
||||
if (typeof temperature !== 'number' || temperature < 0 || temperature > 1) {
|
||||
throw new AIProviderError(
|
||||
'Temperature must be a number between 0.0 and 1.0',
|
||||
AIErrorType.INVALID_REQUEST
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates top-p parameter.
|
||||
*
|
||||
* @private
|
||||
* @param topP - Top-p value to validate
|
||||
* @throws {AIProviderError} If top-p is invalid
|
||||
*/
|
||||
private validateTopP(topP?: number): void {
|
||||
if (topP !== undefined) {
|
||||
if (typeof topP !== 'number' || topP <= 0 || topP > 1) {
|
||||
throw new AIProviderError(
|
||||
'Top-p must be a number between 0.0 (exclusive) and 1.0 (inclusive)',
|
||||
AIErrorType.INVALID_REQUEST
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates max tokens parameter.
|
||||
*
|
||||
* @private
|
||||
* @param maxTokens - Max tokens value to validate
|
||||
* @throws {AIProviderError} If max tokens is invalid
|
||||
*/
|
||||
private validateMaxTokens(maxTokens?: number): void {
|
||||
if (maxTokens !== undefined) {
|
||||
if (typeof maxTokens !== 'number' || !Number.isInteger(maxTokens) || maxTokens < 1) {
|
||||
throw new AIProviderError(
|
||||
'Max tokens must be a positive integer',
|
||||
AIErrorType.INVALID_REQUEST
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates stop sequences parameter.
|
||||
*
|
||||
* @private
|
||||
* @param stopSequences - Stop sequences to validate
|
||||
* @throws {AIProviderError} If stop sequences are invalid
|
||||
*/
|
||||
private validateStopSequences(stopSequences?: string[]): void {
|
||||
if (stopSequences !== undefined) {
|
||||
if (!Array.isArray(stopSequences)) {
|
||||
@@ -587,65 +641,52 @@ export abstract class BaseAIProvider {
|
||||
* @returns Normalized AIProviderError with appropriate type and context
|
||||
*/
|
||||
protected normalizeError(error: Error): AIProviderError {
|
||||
// If already normalized, return as-is
|
||||
if (error instanceof AIProviderError) {
|
||||
return error;
|
||||
}
|
||||
|
||||
// Extract status code if available
|
||||
const status = (error as any).status || (error as any).statusCode;
|
||||
const message = error.message || 'Unknown error occurred';
|
||||
|
||||
// Map HTTP status codes to error types
|
||||
if (status) {
|
||||
switch (status) {
|
||||
case 400:
|
||||
return new AIProviderError(
|
||||
`Bad request: ${message}`,
|
||||
AIErrorType.INVALID_REQUEST,
|
||||
status,
|
||||
error
|
||||
);
|
||||
|
||||
case 401:
|
||||
case 403:
|
||||
return new AIProviderError(
|
||||
'Authentication failed. Please verify your API key is correct and has the necessary permissions.',
|
||||
AIErrorType.AUTHENTICATION,
|
||||
status,
|
||||
error
|
||||
);
|
||||
|
||||
case 404:
|
||||
return new AIProviderError(
|
||||
'The specified model or endpoint was not found. Please check the model name and availability.',
|
||||
AIErrorType.MODEL_NOT_FOUND,
|
||||
status,
|
||||
error
|
||||
);
|
||||
|
||||
case 429:
|
||||
return new AIProviderError(
|
||||
'Rate limit exceeded. Please reduce your request frequency and try again later.',
|
||||
AIErrorType.RATE_LIMIT,
|
||||
status,
|
||||
error
|
||||
);
|
||||
|
||||
case 500:
|
||||
case 502:
|
||||
case 503:
|
||||
case 504:
|
||||
return new AIProviderError(
|
||||
'Service temporarily unavailable. Please try again in a few moments.',
|
||||
AIErrorType.NETWORK,
|
||||
status,
|
||||
error
|
||||
);
|
||||
}
|
||||
const providerError = this.mapProviderError(error);
|
||||
if (providerError) {
|
||||
return providerError;
|
||||
}
|
||||
|
||||
const status = (error as any).status || (error as any).statusCode;
|
||||
const message = error.message || 'Unknown error occurred';
|
||||
const overrides = this.providerErrorMessages();
|
||||
|
||||
const statusTypeMap: Record<number, AIErrorType> = {
|
||||
400: AIErrorType.INVALID_REQUEST,
|
||||
401: AIErrorType.AUTHENTICATION,
|
||||
403: AIErrorType.AUTHENTICATION,
|
||||
404: AIErrorType.MODEL_NOT_FOUND,
|
||||
429: AIErrorType.RATE_LIMIT,
|
||||
500: AIErrorType.NETWORK,
|
||||
502: AIErrorType.NETWORK,
|
||||
503: AIErrorType.NETWORK,
|
||||
504: AIErrorType.NETWORK
|
||||
};
|
||||
|
||||
const defaultMessages: Record<number, string> = {
|
||||
400: `Bad request: ${message}`,
|
||||
401: 'Authentication failed. Please verify your API key is correct and has the necessary permissions.',
|
||||
403: 'Authentication failed. Please verify your API key is correct and has the necessary permissions.',
|
||||
404: 'The specified model or endpoint was not found. Please check the model name and availability.',
|
||||
429: 'Rate limit exceeded. Please reduce your request frequency and try again later.',
|
||||
500: 'Service temporarily unavailable. Please try again in a few moments.',
|
||||
502: 'Service temporarily unavailable. Please try again in a few moments.',
|
||||
503: 'Service temporarily unavailable. Please try again in a few moments.',
|
||||
504: 'Service temporarily unavailable. Please try again in a few moments.'
|
||||
};
|
||||
|
||||
if (status && statusTypeMap[status]) {
|
||||
return new AIProviderError(
|
||||
overrides[status] ?? defaultMessages[status]!,
|
||||
statusTypeMap[status]!,
|
||||
status,
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
// Map common error patterns
|
||||
if (message.includes('timeout') || message.includes('ETIMEDOUT')) {
|
||||
return new AIProviderError(
|
||||
'Request timed out. The operation took longer than expected.',
|
||||
@@ -664,9 +705,8 @@ export abstract class BaseAIProvider {
|
||||
);
|
||||
}
|
||||
|
||||
// Default to unknown error type
|
||||
return new AIProviderError(
|
||||
`Provider error: ${message}`,
|
||||
`${this.providerName} error: ${message}`,
|
||||
AIErrorType.UNKNOWN,
|
||||
status,
|
||||
error
|
||||
|
||||
@@ -34,6 +34,13 @@ import type {
|
||||
} from '../types/index.js';
|
||||
import { BaseAIProvider } from './base.js';
|
||||
import { AIProviderError, AIErrorType } from '../types/index.js';
|
||||
import {
|
||||
DEFAULT_ANTHROPIC_VERSION,
|
||||
DEFAULT_MAX_TOKENS,
|
||||
DEFAULT_MODELS,
|
||||
DEFAULT_TEMPERATURE,
|
||||
VALIDATION_PROMPT
|
||||
} from '../constants.js';
|
||||
|
||||
// ============================================================================
|
||||
// TYPES AND INTERFACES
|
||||
@@ -153,9 +160,8 @@ export class ClaudeProvider extends BaseAIProvider {
|
||||
constructor(config: ClaudeConfig) {
|
||||
super(config);
|
||||
|
||||
// Set Claude-specific defaults
|
||||
this.defaultModel = config.defaultModel || 'claude-3-5-sonnet-20241022';
|
||||
this.version = config.version || '2023-06-01';
|
||||
this.defaultModel = config.defaultModel || DEFAULT_MODELS.claude;
|
||||
this.version = config.version || DEFAULT_ANTHROPIC_VERSION;
|
||||
|
||||
// Validate model name format
|
||||
this.validateModelName(this.defaultModel);
|
||||
@@ -178,7 +184,6 @@ export class ClaudeProvider extends BaseAIProvider {
|
||||
*/
|
||||
protected async doInitialize(): Promise<void> {
|
||||
try {
|
||||
// Create Anthropic client with optimized configuration
|
||||
this.client = new Anthropic({
|
||||
apiKey: this.config.apiKey,
|
||||
baseURL: this.config.baseUrl,
|
||||
@@ -186,15 +191,12 @@ export class ClaudeProvider extends BaseAIProvider {
|
||||
maxRetries: this.config.maxRetries,
|
||||
defaultHeaders: {
|
||||
'anthropic-version': this.version,
|
||||
'anthropic-beta': 'max-tokens-3-5-sonnet-2024-07-15', // Enable extended context
|
||||
'anthropic-beta': 'max-tokens-3-5-sonnet-2024-07-15',
|
||||
}
|
||||
});
|
||||
|
||||
// Validate connection and permissions
|
||||
await this.validateConnection();
|
||||
|
||||
} catch (error) {
|
||||
// Clean up on failure
|
||||
this.client = null;
|
||||
|
||||
throw new AIProviderError(
|
||||
@@ -225,22 +227,10 @@ export class ClaudeProvider extends BaseAIProvider {
|
||||
throw new AIProviderError('Claude client not initialized', AIErrorType.INVALID_REQUEST);
|
||||
}
|
||||
|
||||
try {
|
||||
// Process messages for Claude's format requirements
|
||||
const { system, messages } = this.processMessages(params.messages);
|
||||
|
||||
// Build optimized request parameters
|
||||
const requestParams = this.buildRequestParams(params, system, messages, false);
|
||||
|
||||
// Make API request
|
||||
const response = await this.client.messages.create(requestParams);
|
||||
|
||||
// Format and return response
|
||||
return this.formatCompletionResponse(response);
|
||||
|
||||
} catch (error) {
|
||||
throw this.handleAnthropicError(error as Error);
|
||||
}
|
||||
const { system, messages } = this.processMessages(params.messages);
|
||||
const requestParams = this.buildRequestParams(params, system, messages, false);
|
||||
const response = await this.client.messages.create(requestParams);
|
||||
return this.formatCompletionResponse(response);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -262,22 +252,10 @@ export class ClaudeProvider extends BaseAIProvider {
|
||||
throw new AIProviderError('Claude client not initialized', AIErrorType.INVALID_REQUEST);
|
||||
}
|
||||
|
||||
try {
|
||||
// Process messages for Claude's format requirements
|
||||
const { system, messages } = this.processMessages(params.messages);
|
||||
|
||||
// Build streaming request parameters
|
||||
const requestParams = this.buildRequestParams(params, system, messages, true);
|
||||
|
||||
// Create streaming request
|
||||
const stream = await this.client.messages.create(requestParams);
|
||||
|
||||
// Process stream chunks
|
||||
yield* this.processStreamChunks(stream);
|
||||
|
||||
} catch (error) {
|
||||
throw this.handleAnthropicError(error as Error);
|
||||
}
|
||||
const { system, messages } = this.processMessages(params.messages);
|
||||
const requestParams = this.buildRequestParams(params, system, messages, true);
|
||||
const stream = await this.client.messages.create(requestParams);
|
||||
yield* this.processStreamChunks(stream);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
@@ -317,76 +295,62 @@ export class ClaudeProvider 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> {
|
||||
protected override getModelNamePatterns(): RegExp[] {
|
||||
return [
|
||||
/^claude-3(?:-5)?-(?:opus|sonnet|haiku)-\d{8}$/,
|
||||
/^claude-instant-[0-9.]+$/,
|
||||
/^claude-[0-9.]+$/
|
||||
];
|
||||
}
|
||||
|
||||
protected override async sendValidationProbe(): Promise<void> {
|
||||
if (!this.client) {
|
||||
throw new Error('Client not initialized');
|
||||
}
|
||||
|
||||
try {
|
||||
// Make minimal request to test connection and permissions
|
||||
await this.client.messages.create({
|
||||
model: this.defaultModel,
|
||||
max_tokens: 1,
|
||||
messages: [{ role: 'user', content: 'Hi' }]
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
// Handle specific validation errors
|
||||
if (error.status === 401 || error.status === 403) {
|
||||
throw new AIProviderError(
|
||||
'Invalid Anthropic API key. Please verify your API key from https://console.anthropic.com/',
|
||||
AIErrorType.AUTHENTICATION,
|
||||
error.status
|
||||
);
|
||||
}
|
||||
|
||||
if (error.status === 404 && error.message?.includes('model')) {
|
||||
throw new AIProviderError(
|
||||
`Model '${this.defaultModel}' is not available. Please check the model name or your API access.`,
|
||||
AIErrorType.MODEL_NOT_FOUND,
|
||||
error.status
|
||||
);
|
||||
}
|
||||
|
||||
// For other errors during validation, log but don't fail initialization
|
||||
// They might be temporary network issues
|
||||
console.warn('Claude connection validation warning:', error.message);
|
||||
}
|
||||
await this.client.messages.create({
|
||||
model: this.defaultModel,
|
||||
max_tokens: 1,
|
||||
messages: [{ role: 'user', content: VALIDATION_PROMPT }]
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates model name format for Claude 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(
|
||||
'Model name must be a non-empty string',
|
||||
AIErrorType.INVALID_REQUEST
|
||||
);
|
||||
protected override providerErrorMessages(): Partial<Record<number, string>> {
|
||||
return {
|
||||
401: 'Authentication failed. Please check your Anthropic API key from https://console.anthropic.com/',
|
||||
403: 'Access forbidden. Your API key may not have permission for this model or feature.',
|
||||
404: 'Model not found. The specified Claude model may not be available to your account.',
|
||||
429: 'Rate limit exceeded. Claude APIs have usage limits. Please wait before retrying.',
|
||||
500: 'Anthropic service temporarily unavailable. Please try again in a few moments.',
|
||||
502: 'Anthropic service temporarily unavailable. Please try again in a few moments.',
|
||||
503: 'Anthropic service temporarily unavailable. Please try again in a few moments.'
|
||||
};
|
||||
}
|
||||
|
||||
protected override mapProviderError(error: any): AIProviderError | null {
|
||||
if (!error) return null;
|
||||
const message: string = error.message || '';
|
||||
const status = error.status || error.statusCode;
|
||||
|
||||
if (status === 400) {
|
||||
if (message.includes('max_tokens')) {
|
||||
return new AIProviderError(
|
||||
'Max tokens value is invalid. Must be between 1 and model limit.',
|
||||
AIErrorType.INVALID_REQUEST,
|
||||
status,
|
||||
error
|
||||
);
|
||||
}
|
||||
if (message.includes('model')) {
|
||||
return new AIProviderError(
|
||||
'Invalid model specified. Please check model availability.',
|
||||
AIErrorType.MODEL_NOT_FOUND,
|
||||
status,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Claude model names follow specific patterns
|
||||
const validPatterns = [
|
||||
/^claude-3(?:-5)?-(?:opus|sonnet|haiku)-\d{8}$/, // e.g., claude-3-5-sonnet-20241022
|
||||
/^claude-instant-[0-9.]+$/, // e.g., claude-instant-1.2
|
||||
/^claude-[0-9.]+$/ // e.g., claude-2.1
|
||||
];
|
||||
|
||||
const isValid = validPatterns.some(pattern => pattern.test(modelName));
|
||||
|
||||
if (!isValid) {
|
||||
console.warn(`Model name '${modelName}' doesn't match expected Claude naming patterns. This may cause API errors.`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -449,8 +413,8 @@ export class ClaudeProvider extends BaseAIProvider {
|
||||
) {
|
||||
return {
|
||||
model: params.model || this.defaultModel,
|
||||
max_tokens: params.maxTokens || 1000,
|
||||
temperature: params.temperature ?? 0.7,
|
||||
max_tokens: params.maxTokens || DEFAULT_MAX_TOKENS,
|
||||
temperature: params.temperature ?? DEFAULT_TEMPERATURE,
|
||||
top_p: params.topP,
|
||||
stop_sequences: params.stopSequences,
|
||||
system: system || undefined,
|
||||
@@ -564,118 +528,4 @@ export class ClaudeProvider extends BaseAIProvider {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles and transforms Anthropic-specific errors.
|
||||
*
|
||||
* This method maps Anthropic's error responses to our standardized
|
||||
* error format, providing helpful context and suggestions.
|
||||
*
|
||||
* @private
|
||||
* @param error - Original error from Anthropic API
|
||||
* @returns Normalized AIProviderError
|
||||
*/
|
||||
private handleAnthropicError(error: any): AIProviderError {
|
||||
if (error instanceof AIProviderError) {
|
||||
return error;
|
||||
}
|
||||
|
||||
const message = error.message || 'Unknown Anthropic API error';
|
||||
const status = error.status || error.statusCode;
|
||||
|
||||
// Map Anthropic-specific error codes
|
||||
switch (status) {
|
||||
case 400:
|
||||
if (message.includes('max_tokens')) {
|
||||
return new AIProviderError(
|
||||
'Max tokens value is invalid. Must be between 1 and model limit.',
|
||||
AIErrorType.INVALID_REQUEST,
|
||||
status,
|
||||
error
|
||||
);
|
||||
}
|
||||
if (message.includes('model')) {
|
||||
return new AIProviderError(
|
||||
'Invalid model specified. Please check model availability.',
|
||||
AIErrorType.MODEL_NOT_FOUND,
|
||||
status,
|
||||
error
|
||||
);
|
||||
}
|
||||
return new AIProviderError(
|
||||
`Invalid request: ${message}`,
|
||||
AIErrorType.INVALID_REQUEST,
|
||||
status,
|
||||
error
|
||||
);
|
||||
|
||||
case 401:
|
||||
return new AIProviderError(
|
||||
'Authentication failed. Please check your Anthropic API key from https://console.anthropic.com/',
|
||||
AIErrorType.AUTHENTICATION,
|
||||
status,
|
||||
error
|
||||
);
|
||||
|
||||
case 403:
|
||||
return new AIProviderError(
|
||||
'Access forbidden. Your API key may not have permission for this model or feature.',
|
||||
AIErrorType.AUTHENTICATION,
|
||||
status,
|
||||
error
|
||||
);
|
||||
|
||||
case 404:
|
||||
return new AIProviderError(
|
||||
'Model not found. The specified Claude model may not be available to your account.',
|
||||
AIErrorType.MODEL_NOT_FOUND,
|
||||
status,
|
||||
error
|
||||
);
|
||||
|
||||
case 429:
|
||||
return new AIProviderError(
|
||||
'Rate limit exceeded. Claude APIs have usage limits. Please wait before retrying.',
|
||||
AIErrorType.RATE_LIMIT,
|
||||
status,
|
||||
error
|
||||
);
|
||||
|
||||
case 500:
|
||||
case 502:
|
||||
case 503:
|
||||
return new AIProviderError(
|
||||
'Anthropic service temporarily unavailable. Please try again in a few moments.',
|
||||
AIErrorType.NETWORK,
|
||||
status,
|
||||
error
|
||||
);
|
||||
|
||||
default:
|
||||
// Handle timeout and network errors
|
||||
if (message.includes('timeout') || error.code === 'ETIMEDOUT') {
|
||||
return new AIProviderError(
|
||||
'Request timed out. Claude may be experiencing high load.',
|
||||
AIErrorType.TIMEOUT,
|
||||
status,
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
if (message.includes('network') || error.code === 'ECONNREFUSED') {
|
||||
return new AIProviderError(
|
||||
'Network error connecting to Anthropic servers.',
|
||||
AIErrorType.NETWORK,
|
||||
status,
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
return new AIProviderError(
|
||||
`Claude API error: ${message}`,
|
||||
AIErrorType.UNKNOWN,
|
||||
status,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
@@ -37,6 +31,12 @@ import type {
|
||||
} from '../types/index.js';
|
||||
import { BaseAIProvider } from './base.js';
|
||||
import { AIProviderError, AIErrorType } from '../types/index.js';
|
||||
import {
|
||||
DEFAULT_MAX_TOKENS,
|
||||
DEFAULT_MODELS,
|
||||
DEFAULT_TEMPERATURE,
|
||||
VALIDATION_PROMPT
|
||||
} from '../constants.js';
|
||||
|
||||
// ============================================================================
|
||||
// TYPES AND INTERFACES
|
||||
@@ -44,84 +44,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)
|
||||
* - '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-1.5-flash'
|
||||
* @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 +87,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
|
||||
* 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
|
||||
* });
|
||||
* ```
|
||||
* 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[];
|
||||
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) {
|
||||
super(config);
|
||||
|
||||
// Set Gemini-specific defaults
|
||||
this.defaultModel = config.defaultModel || 'gemini-1.5-flash';
|
||||
this.defaultModel = config.defaultModel || DEFAULT_MODELS.gemini;
|
||||
this.safetySettings = config.safetySettings;
|
||||
this.generationConfig = config.generationConfig;
|
||||
this.defaultGenerationConfig = config.generationConfig;
|
||||
|
||||
// Validate model name format
|
||||
this.validateModelName(this.defaultModel);
|
||||
}
|
||||
|
||||
@@ -213,37 +112,13 @@ 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);
|
||||
this.client = new GoogleGenAI({ apiKey: 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();
|
||||
|
||||
} catch (error) {
|
||||
// Clean up on failure
|
||||
this.client = null;
|
||||
this.model = null;
|
||||
|
||||
throw new AIProviderError(
|
||||
`Failed to initialize Gemini provider: ${(error as Error).message}`,
|
||||
@@ -254,151 +129,66 @@ 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);
|
||||
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) {
|
||||
// Multi-turn conversation - use chat session
|
||||
const chat = model.startChat({
|
||||
history: contents.slice(0, -1),
|
||||
systemInstruction,
|
||||
generationConfig: this.buildGenerationConfig(params)
|
||||
});
|
||||
const response = await this.client.models.generateContent({
|
||||
model,
|
||||
contents,
|
||||
config: this.buildConfig(params, systemInstruction)
|
||||
});
|
||||
|
||||
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) {
|
||||
throw this.handleGeminiError(error as Error);
|
||||
}
|
||||
return this.formatCompletionResponse(response, model);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
const { systemInstruction, contents } = this.convertMessages(params.messages);
|
||||
const model = params.model || this.defaultModel;
|
||||
|
||||
// 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 stream = await this.client.models.generateContentStream({
|
||||
model,
|
||||
contents,
|
||||
config: this.buildConfig(params, systemInstruction)
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
} catch (error) {
|
||||
throw this.handleGeminiError(error as Error);
|
||||
}
|
||||
yield* this.processStreamChunks(stream);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// 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,128 +197,90 @@ 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');
|
||||
}
|
||||
|
||||
try {
|
||||
// Make minimal request to test connection and permissions
|
||||
await this.model.generateContent({
|
||||
contents: [{ role: 'user', parts: [{ text: 'Hi' }] }],
|
||||
generationConfig: { 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',
|
||||
AIErrorType.AUTHENTICATION,
|
||||
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.',
|
||||
AIErrorType.AUTHENTICATION,
|
||||
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.`,
|
||||
AIErrorType.MODEL_NOT_FOUND,
|
||||
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(
|
||||
'Model name must be a non-empty string',
|
||||
AIErrorType.INVALID_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
// 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
|
||||
protected override getModelNamePatterns(): RegExp[] {
|
||||
return [
|
||||
/^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 {
|
||||
protected override async sendValidationProbe(): Promise<void> {
|
||||
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)
|
||||
await this.client.models.generateContent({
|
||||
model: this.defaultModel,
|
||||
contents: [{ role: 'user', parts: [{ text: VALIDATION_PROMPT }] }],
|
||||
config: { maxOutputTokens: 1 }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
protected override providerErrorMessages(): Partial<Record<number, string>> {
|
||||
return {
|
||||
401: 'Authentication failed with Gemini API. Please check your API key and permissions.',
|
||||
403: 'Authentication failed with Gemini API. Please check your API key and permissions.',
|
||||
404: 'Gemini API endpoint not found. Please check the model name and API version.',
|
||||
429: 'Rate limit exceeded. Please reduce request frequency.',
|
||||
500: 'Gemini service temporarily unavailable. Please try again in a few moments.',
|
||||
502: 'Gemini service temporarily unavailable. Please try again in a few moments.',
|
||||
503: 'Gemini service temporarily unavailable. Please try again in a few moments.'
|
||||
};
|
||||
}
|
||||
|
||||
protected override mapProviderError(error: any): AIProviderError | null {
|
||||
if (!error) return null;
|
||||
const message: string = error.message || '';
|
||||
const status = error.status || error.statusCode;
|
||||
|
||||
if (message.includes('API key')) {
|
||||
return new AIProviderError(
|
||||
'Invalid Google API key. Please verify your API key from https://aistudio.google.com/app/apikey',
|
||||
AIErrorType.AUTHENTICATION, status, error
|
||||
);
|
||||
}
|
||||
|
||||
if (message.includes('quota') || message.includes('limit exceeded')) {
|
||||
return new AIProviderError(
|
||||
'API quota exceeded. Please check your Google Cloud quotas and billing.',
|
||||
AIErrorType.RATE_LIMIT, status, error
|
||||
);
|
||||
}
|
||||
|
||||
if (message.includes('model not found') || message.includes('invalid model')) {
|
||||
return new AIProviderError(
|
||||
'Model not found. The specified Gemini model may not exist or be available to your API key.',
|
||||
AIErrorType.MODEL_NOT_FOUND, status, error
|
||||
);
|
||||
}
|
||||
|
||||
if (message.includes('safety') || message.includes('blocked')) {
|
||||
return new AIProviderError(
|
||||
'Content blocked by safety filters. Please modify your input or adjust safety settings.',
|
||||
AIErrorType.INVALID_REQUEST, status, error
|
||||
);
|
||||
}
|
||||
|
||||
if (message.includes('length') || message.includes('token')) {
|
||||
return new AIProviderError(
|
||||
'Input too long or exceeds token limits. Please reduce input size or max tokens.',
|
||||
AIErrorType.INVALID_REQUEST, status, error
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
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}\n\n${message.content}`
|
||||
: message.content;
|
||||
@@ -543,59 +295,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 ?? DEFAULT_TEMPERATURE,
|
||||
maxOutputTokens: params.maxTokens ?? defaults?.maxOutputTokens ?? DEFAULT_MAX_TOKENS
|
||||
};
|
||||
|
||||
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;
|
||||
for await (const chunk of stream) {
|
||||
if (chunk.usageMetadata) {
|
||||
lastUsage = chunk.usageMetadata;
|
||||
}
|
||||
|
||||
yield {
|
||||
content: chunkText,
|
||||
isComplete: false,
|
||||
id: requestId
|
||||
};
|
||||
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 +362,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 +375,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,147 +387,11 @@ 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;
|
||||
}
|
||||
|
||||
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',
|
||||
AIErrorType.AUTHENTICATION,
|
||||
status,
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
if (message.includes('quota') || message.includes('limit exceeded')) {
|
||||
return new AIProviderError(
|
||||
'API quota exceeded. Please check your Google Cloud quotas and billing.',
|
||||
AIErrorType.RATE_LIMIT,
|
||||
status,
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
if (message.includes('model not found') || message.includes('invalid model')) {
|
||||
return new AIProviderError(
|
||||
'Model not found. The specified Gemini model may not exist or be available to your API key.',
|
||||
AIErrorType.MODEL_NOT_FOUND,
|
||||
status,
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
if (message.includes('safety') || message.includes('blocked')) {
|
||||
return new AIProviderError(
|
||||
'Content blocked by safety filters. Please modify your input or adjust safety settings.',
|
||||
AIErrorType.INVALID_REQUEST,
|
||||
status,
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
if (message.includes('length') || message.includes('token')) {
|
||||
return new AIProviderError(
|
||||
'Input too long or exceeds token limits. Please reduce input size or max tokens.',
|
||||
AIErrorType.INVALID_REQUEST,
|
||||
status,
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
if (message.includes('timeout')) {
|
||||
return new AIProviderError(
|
||||
'Request timed out. Gemini may be experiencing high load.',
|
||||
AIErrorType.TIMEOUT,
|
||||
status,
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
if (message.includes('network') || message.includes('connection')) {
|
||||
return new AIProviderError(
|
||||
'Network error connecting to Gemini servers.',
|
||||
AIErrorType.NETWORK,
|
||||
status,
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
// Handle HTTP status codes
|
||||
switch (status) {
|
||||
case 400:
|
||||
return new AIProviderError(
|
||||
`Invalid request to Gemini API: ${message}`,
|
||||
AIErrorType.INVALID_REQUEST,
|
||||
status,
|
||||
error
|
||||
);
|
||||
|
||||
case 401:
|
||||
case 403:
|
||||
return new AIProviderError(
|
||||
'Authentication failed with Gemini API. Please check your API key and permissions.',
|
||||
AIErrorType.AUTHENTICATION,
|
||||
status,
|
||||
error
|
||||
);
|
||||
|
||||
case 404:
|
||||
return new AIProviderError(
|
||||
'Gemini API endpoint not found. Please check the model name and API version.',
|
||||
AIErrorType.MODEL_NOT_FOUND,
|
||||
status,
|
||||
error
|
||||
);
|
||||
|
||||
case 429:
|
||||
return new AIProviderError(
|
||||
'Rate limit exceeded. Please reduce request frequency.',
|
||||
AIErrorType.RATE_LIMIT,
|
||||
status,
|
||||
error
|
||||
);
|
||||
|
||||
case 500:
|
||||
case 502:
|
||||
case 503:
|
||||
return new AIProviderError(
|
||||
'Gemini service temporarily unavailable. Please try again in a few moments.',
|
||||
AIErrorType.NETWORK,
|
||||
status,
|
||||
error
|
||||
);
|
||||
|
||||
default:
|
||||
return new AIProviderError(
|
||||
`Gemini API error: ${message}`,
|
||||
AIErrorType.UNKNOWN,
|
||||
status,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,12 @@ import type {
|
||||
} from '../types/index.js';
|
||||
import { BaseAIProvider } from './base.js';
|
||||
import { AIProviderError, AIErrorType } from '../types/index.js';
|
||||
import {
|
||||
DEFAULT_MAX_TOKENS,
|
||||
DEFAULT_MODELS,
|
||||
DEFAULT_TEMPERATURE,
|
||||
VALIDATION_PROMPT
|
||||
} from '../constants.js';
|
||||
|
||||
// ============================================================================
|
||||
// TYPES AND INTERFACES
|
||||
@@ -173,7 +179,7 @@ export class OpenAIProvider extends BaseAIProvider {
|
||||
super(config);
|
||||
|
||||
// Set OpenAI-specific defaults
|
||||
this.defaultModel = config.defaultModel || 'gpt-4o';
|
||||
this.defaultModel = config.defaultModel || DEFAULT_MODELS.openai;
|
||||
this.organization = config.organization;
|
||||
this.project = config.project;
|
||||
|
||||
@@ -249,19 +255,9 @@ export class OpenAIProvider extends BaseAIProvider {
|
||||
throw new AIProviderError('OpenAI client not initialized', AIErrorType.INVALID_REQUEST);
|
||||
}
|
||||
|
||||
try {
|
||||
// Build optimized request parameters
|
||||
const requestParams = this.buildRequestParams(params, false);
|
||||
|
||||
// Make API request - explicitly type as ChatCompletion for non-streaming
|
||||
const response = await this.client.chat.completions.create(requestParams) as OpenAI.Chat.Completions.ChatCompletion;
|
||||
|
||||
// Format and return response
|
||||
return this.formatCompletionResponse(response);
|
||||
|
||||
} catch (error) {
|
||||
throw this.handleOpenAIError(error as Error);
|
||||
}
|
||||
const requestParams = this.buildRequestParams(params, false);
|
||||
const response = await this.client.chat.completions.create(requestParams) as OpenAI.Chat.Completions.ChatCompletion;
|
||||
return this.formatCompletionResponse(response);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -283,19 +279,9 @@ export class OpenAIProvider extends BaseAIProvider {
|
||||
throw new AIProviderError('OpenAI client not initialized', AIErrorType.INVALID_REQUEST);
|
||||
}
|
||||
|
||||
try {
|
||||
// Build streaming request parameters
|
||||
const requestParams = this.buildRequestParams(params, true);
|
||||
|
||||
// Create streaming request
|
||||
const stream = this.client.chat.completions.create(requestParams);
|
||||
|
||||
// Process stream chunks
|
||||
yield* this.processStreamChunks(stream);
|
||||
|
||||
} catch (error) {
|
||||
throw this.handleOpenAIError(error as Error);
|
||||
}
|
||||
const requestParams = this.buildRequestParams(params, true);
|
||||
const stream = this.client.chat.completions.create(requestParams);
|
||||
yield* this.processStreamChunks(stream);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
@@ -340,87 +326,82 @@ export class OpenAIProvider 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> {
|
||||
protected override getModelNamePatterns(): RegExp[] {
|
||||
return [
|
||||
/^gpt-4o(?:-mini)?(?:-\d{4}-\d{2}-\d{2})?$/,
|
||||
/^gpt-4(?:-turbo)?(?:-\d{4}-\d{2}-\d{2})?$/,
|
||||
/^gpt-3\.5-turbo(?:-\d{4})?$/,
|
||||
/^gpt-4-\d{4}-preview$/,
|
||||
/^text-davinci-\d{3}$/,
|
||||
/^ft:.+$/
|
||||
];
|
||||
}
|
||||
|
||||
protected override async sendValidationProbe(): Promise<void> {
|
||||
if (!this.client) {
|
||||
throw new Error('Client not initialized');
|
||||
}
|
||||
|
||||
try {
|
||||
// Make minimal request to test connection and permissions
|
||||
await this.client.chat.completions.create({
|
||||
model: this.defaultModel,
|
||||
messages: [{ role: 'user', content: 'Hi' }],
|
||||
max_tokens: 1
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
// Handle specific validation errors
|
||||
if (error.status === 401) {
|
||||
throw new AIProviderError(
|
||||
'Invalid OpenAI API key. Please verify your API key from https://platform.openai.com/api-keys',
|
||||
AIErrorType.AUTHENTICATION,
|
||||
error.status
|
||||
);
|
||||
}
|
||||
|
||||
if (error.status === 403) {
|
||||
throw new AIProviderError(
|
||||
'Access forbidden. Your API key may not have permission for this model or your account may have insufficient credits.',
|
||||
AIErrorType.AUTHENTICATION,
|
||||
error.status
|
||||
);
|
||||
}
|
||||
|
||||
if (error.status === 404 && error.message?.includes('model')) {
|
||||
throw new AIProviderError(
|
||||
`Model '${this.defaultModel}' is not available. Please check the model name or your API access level.`,
|
||||
AIErrorType.MODEL_NOT_FOUND,
|
||||
error.status
|
||||
);
|
||||
}
|
||||
|
||||
// For other errors during validation, log but don't fail initialization
|
||||
// They might be temporary network issues
|
||||
console.warn('OpenAI connection validation warning:', error.message);
|
||||
}
|
||||
await this.client.chat.completions.create({
|
||||
model: this.defaultModel,
|
||||
messages: [{ role: 'user', content: VALIDATION_PROMPT }],
|
||||
max_tokens: 1
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates model name format for OpenAI 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(
|
||||
'Model name must be a non-empty string',
|
||||
AIErrorType.INVALID_REQUEST
|
||||
protected override providerErrorMessages(): Partial<Record<number, string>> {
|
||||
return {
|
||||
401: 'Authentication failed. Please verify your OpenAI API key from https://platform.openai.com/api-keys',
|
||||
403: 'Access forbidden. Your API key may not have permission for this model or feature.',
|
||||
404: 'Model not found. The specified OpenAI model may not exist or be available to your account.',
|
||||
429: 'Too many requests. Please slow down and try again.',
|
||||
500: 'OpenAI service temporarily unavailable. Please try again in a few moments.',
|
||||
502: 'OpenAI service temporarily unavailable. Please try again in a few moments.',
|
||||
503: 'OpenAI service temporarily unavailable. Please try again in a few moments.',
|
||||
504: 'OpenAI service temporarily unavailable. Please try again in a few moments.'
|
||||
};
|
||||
}
|
||||
|
||||
protected override mapProviderError(error: any): AIProviderError | null {
|
||||
if (!error) return null;
|
||||
const message: string = error.message || '';
|
||||
const status = error.status || error.statusCode;
|
||||
|
||||
if (status === 400) {
|
||||
if (message.includes('max_tokens')) {
|
||||
return new AIProviderError(
|
||||
'Max tokens value exceeds model limit. Please reduce max_tokens or use a model with higher limits.',
|
||||
AIErrorType.INVALID_REQUEST, status, error
|
||||
);
|
||||
}
|
||||
if (message.includes('model')) {
|
||||
return new AIProviderError(
|
||||
'Invalid model specified. Please check model name and availability.',
|
||||
AIErrorType.MODEL_NOT_FOUND, status, error
|
||||
);
|
||||
}
|
||||
if (message.includes('messages')) {
|
||||
return new AIProviderError(
|
||||
'Invalid message format. Please check message structure and content.',
|
||||
AIErrorType.INVALID_REQUEST, status, error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (status === 403 && (message.includes('billing') || message.includes('quota'))) {
|
||||
return new AIProviderError(
|
||||
'Insufficient quota or billing issue. Please check your OpenAI account billing and usage limits.',
|
||||
AIErrorType.AUTHENTICATION, status, error
|
||||
);
|
||||
}
|
||||
|
||||
// OpenAI model names follow specific patterns
|
||||
const validPatterns = [
|
||||
/^gpt-4o(?:-mini)?(?:-\d{4}-\d{2}-\d{2})?$/, // e.g., gpt-4o, gpt-4o-mini, gpt-4o-2024-11-20
|
||||
/^gpt-4(?:-turbo)?(?:-\d{4}-\d{2}-\d{2})?$/, // e.g., gpt-4, gpt-4-turbo, gpt-4-turbo-2024-04-09
|
||||
/^gpt-3\.5-turbo(?:-\d{4})?$/, // e.g., gpt-3.5-turbo, gpt-3.5-turbo-0125
|
||||
/^gpt-4-\d{4}-preview$/, // e.g., gpt-4-0125-preview
|
||||
/^text-davinci-\d{3}$/, // Legacy models
|
||||
/^ft:.+$/ // Fine-tuned models
|
||||
];
|
||||
|
||||
const isValid = validPatterns.some(pattern => pattern.test(modelName));
|
||||
|
||||
if (!isValid) {
|
||||
console.warn(`Model name '${modelName}' doesn't match expected OpenAI naming patterns. This may cause API errors.`);
|
||||
if (status === 429 && message.includes('quota')) {
|
||||
return new AIProviderError(
|
||||
'Usage quota exceeded. Please check your OpenAI usage limits and billing.',
|
||||
AIErrorType.RATE_LIMIT, status, error
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -435,8 +416,8 @@ export class OpenAIProvider extends BaseAIProvider {
|
||||
return {
|
||||
model: params.model || this.defaultModel,
|
||||
messages: this.convertMessages(params.messages),
|
||||
max_tokens: params.maxTokens || 1000,
|
||||
temperature: params.temperature ?? 0.7,
|
||||
max_tokens: params.maxTokens || DEFAULT_MAX_TOKENS,
|
||||
temperature: params.temperature ?? DEFAULT_TEMPERATURE,
|
||||
top_p: params.topP,
|
||||
stop: params.stopSequences,
|
||||
stream,
|
||||
@@ -563,151 +544,4 @@ export class OpenAIProvider extends BaseAIProvider {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles and transforms OpenAI-specific errors.
|
||||
*
|
||||
* This method maps OpenAI's error responses to our standardized
|
||||
* error format, providing helpful context and actionable suggestions.
|
||||
*
|
||||
* @private
|
||||
* @param error - Original error from OpenAI API
|
||||
* @returns Normalized AIProviderError
|
||||
*/
|
||||
private handleOpenAIError(error: any): AIProviderError {
|
||||
if (error instanceof AIProviderError) {
|
||||
return error;
|
||||
}
|
||||
|
||||
const message = error.message || 'Unknown OpenAI API error';
|
||||
const status = error.status || error.statusCode;
|
||||
|
||||
// Map OpenAI-specific error codes and types
|
||||
switch (status) {
|
||||
case 400:
|
||||
if (message.includes('max_tokens')) {
|
||||
return new AIProviderError(
|
||||
'Max tokens value exceeds model limit. Please reduce max_tokens or use a model with higher limits.',
|
||||
AIErrorType.INVALID_REQUEST,
|
||||
status,
|
||||
error
|
||||
);
|
||||
}
|
||||
if (message.includes('model')) {
|
||||
return new AIProviderError(
|
||||
'Invalid model specified. Please check model name and availability.',
|
||||
AIErrorType.MODEL_NOT_FOUND,
|
||||
status,
|
||||
error
|
||||
);
|
||||
}
|
||||
if (message.includes('messages')) {
|
||||
return new AIProviderError(
|
||||
'Invalid message format. Please check message structure and content.',
|
||||
AIErrorType.INVALID_REQUEST,
|
||||
status,
|
||||
error
|
||||
);
|
||||
}
|
||||
return new AIProviderError(
|
||||
`Invalid request: ${message}`,
|
||||
AIErrorType.INVALID_REQUEST,
|
||||
status,
|
||||
error
|
||||
);
|
||||
|
||||
case 401:
|
||||
return new AIProviderError(
|
||||
'Authentication failed. Please verify your OpenAI API key from https://platform.openai.com/api-keys',
|
||||
AIErrorType.AUTHENTICATION,
|
||||
status,
|
||||
error
|
||||
);
|
||||
|
||||
case 403:
|
||||
if (message.includes('billing') || message.includes('quota')) {
|
||||
return new AIProviderError(
|
||||
'Insufficient quota or billing issue. Please check your OpenAI account billing and usage limits.',
|
||||
AIErrorType.AUTHENTICATION,
|
||||
status,
|
||||
error
|
||||
);
|
||||
}
|
||||
return new AIProviderError(
|
||||
'Access forbidden. Your API key may not have permission for this model or feature.',
|
||||
AIErrorType.AUTHENTICATION,
|
||||
status,
|
||||
error
|
||||
);
|
||||
|
||||
case 404:
|
||||
return new AIProviderError(
|
||||
'Model not found. The specified OpenAI model may not exist or be available to your account.',
|
||||
AIErrorType.MODEL_NOT_FOUND,
|
||||
status,
|
||||
error
|
||||
);
|
||||
|
||||
case 429:
|
||||
if (message.includes('rate')) {
|
||||
return new AIProviderError(
|
||||
'Rate limit exceeded. Please reduce request frequency and implement exponential backoff.',
|
||||
AIErrorType.RATE_LIMIT,
|
||||
status,
|
||||
error
|
||||
);
|
||||
}
|
||||
if (message.includes('quota')) {
|
||||
return new AIProviderError(
|
||||
'Usage quota exceeded. Please check your OpenAI usage limits and billing.',
|
||||
AIErrorType.RATE_LIMIT,
|
||||
status,
|
||||
error
|
||||
);
|
||||
}
|
||||
return new AIProviderError(
|
||||
'Too many requests. Please slow down and try again.',
|
||||
AIErrorType.RATE_LIMIT,
|
||||
status,
|
||||
error
|
||||
);
|
||||
|
||||
case 500:
|
||||
case 502:
|
||||
case 503:
|
||||
case 504:
|
||||
return new AIProviderError(
|
||||
'OpenAI service temporarily unavailable. Please try again in a few moments.',
|
||||
AIErrorType.NETWORK,
|
||||
status,
|
||||
error
|
||||
);
|
||||
|
||||
default:
|
||||
// Handle timeout and network errors
|
||||
if (message.includes('timeout') || error.code === 'ETIMEDOUT') {
|
||||
return new AIProviderError(
|
||||
'Request timed out. OpenAI may be experiencing high load.',
|
||||
AIErrorType.TIMEOUT,
|
||||
status,
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
if (message.includes('network') || error.code === 'ECONNREFUSED') {
|
||||
return new AIProviderError(
|
||||
'Network error connecting to OpenAI servers.',
|
||||
AIErrorType.NETWORK,
|
||||
status,
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
return new AIProviderError(
|
||||
`OpenAI API error: ${message}`,
|
||||
AIErrorType.UNKNOWN,
|
||||
status,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
59
src/providers/openwebui-http.ts
Normal file
59
src/providers/openwebui-http.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { AIProviderError, AIErrorType } from '../types/index.js';
|
||||
import { DEFAULT_TIMEOUT_MS } from '../constants.js';
|
||||
|
||||
export interface OpenWebUIHttpOptions {
|
||||
baseUrl: string;
|
||||
apiKey?: string;
|
||||
timeout?: number;
|
||||
dangerouslyAllowInsecureConnections?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Thin HTTP client shared by OpenWebUI strategies.
|
||||
* Handles auth header, request body serialization, and timeout-to-AIProviderError translation.
|
||||
*/
|
||||
export class OpenWebUIHttpClient {
|
||||
readonly baseUrl: string;
|
||||
private readonly apiKey: string | undefined;
|
||||
private readonly timeout: number;
|
||||
private readonly dangerouslyAllowInsecureConnections: boolean;
|
||||
|
||||
constructor(options: OpenWebUIHttpOptions) {
|
||||
this.baseUrl = options.baseUrl;
|
||||
this.apiKey = options.apiKey;
|
||||
this.timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
|
||||
this.dangerouslyAllowInsecureConnections = options.dangerouslyAllowInsecureConnections ?? true;
|
||||
}
|
||||
|
||||
async request(path: string, method: string, body?: unknown): Promise<Response> {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'simple-ai-provider/2.0.0'
|
||||
};
|
||||
|
||||
if (this.apiKey) {
|
||||
headers['Authorization'] = `Bearer ${this.apiKey}`;
|
||||
}
|
||||
|
||||
const requestOptions: RequestInit = {
|
||||
method,
|
||||
headers,
|
||||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||||
signal: AbortSignal.timeout(this.timeout)
|
||||
};
|
||||
|
||||
try {
|
||||
return await fetch(`${this.baseUrl}${path}`, requestOptions);
|
||||
} catch (error: any) {
|
||||
if (error.name === 'AbortError') {
|
||||
throw new AIProviderError(
|
||||
'Request timed out',
|
||||
AIErrorType.TIMEOUT,
|
||||
undefined,
|
||||
error
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
306
src/providers/openwebui-strategies.ts
Normal file
306
src/providers/openwebui-strategies.ts
Normal file
@@ -0,0 +1,306 @@
|
||||
import type {
|
||||
AIMessage,
|
||||
CompletionChunk,
|
||||
CompletionParams,
|
||||
CompletionResponse
|
||||
} from '../types/index.js';
|
||||
import { AIProviderError, AIErrorType } from '../types/index.js';
|
||||
import { DEFAULT_MAX_TOKENS, DEFAULT_TEMPERATURE } from '../constants.js';
|
||||
import type { OpenWebUIHttpClient } from './openwebui-http.js';
|
||||
import type {
|
||||
OllamaGenerateResponse,
|
||||
OpenWebUIChatResponse,
|
||||
OpenWebUIModelsResponse,
|
||||
OpenWebUIStreamChunk
|
||||
} from './openwebui-types.js';
|
||||
|
||||
/**
|
||||
* Strategy interface for OpenWebUI's two backend modes.
|
||||
* Selected at construction based on `useOllamaProxy`.
|
||||
*/
|
||||
export interface OpenWebUIStrategy {
|
||||
validateConnection(): Promise<void>;
|
||||
complete(params: CompletionParams, defaultModel: string): Promise<CompletionResponse<string>>;
|
||||
stream(params: CompletionParams, defaultModel: string): AsyncIterable<CompletionChunk>;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Chat strategy — OpenAI-compatible /api/chat/completions endpoint
|
||||
// ============================================================================
|
||||
|
||||
export class OpenWebUIChatStrategy implements OpenWebUIStrategy {
|
||||
constructor(private readonly http: OpenWebUIHttpClient) {}
|
||||
|
||||
async validateConnection(): Promise<void> {
|
||||
const response = await this.http.request('/api/models', 'GET');
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
const data = await response.json() as OpenWebUIModelsResponse;
|
||||
if (!data.data || !Array.isArray(data.data)) {
|
||||
throw new Error('Invalid models response format');
|
||||
}
|
||||
}
|
||||
|
||||
async complete(params: CompletionParams, defaultModel: string): Promise<CompletionResponse<string>> {
|
||||
const response = await this.http.request('/api/chat/completions', 'POST', {
|
||||
model: params.model || defaultModel,
|
||||
messages: convertMessages(params.messages),
|
||||
max_tokens: params.maxTokens || DEFAULT_MAX_TOKENS,
|
||||
temperature: params.temperature ?? DEFAULT_TEMPERATURE,
|
||||
top_p: params.topP,
|
||||
stop: params.stopSequences,
|
||||
stream: false
|
||||
});
|
||||
|
||||
const data = await response.json() as OpenWebUIChatResponse;
|
||||
return formatChatResponse(data);
|
||||
}
|
||||
|
||||
async *stream(params: CompletionParams, defaultModel: string): AsyncIterable<CompletionChunk> {
|
||||
const response = await this.http.request('/api/chat/completions', 'POST', {
|
||||
model: params.model || defaultModel,
|
||||
messages: convertMessages(params.messages),
|
||||
max_tokens: params.maxTokens || DEFAULT_MAX_TOKENS,
|
||||
temperature: params.temperature ?? DEFAULT_TEMPERATURE,
|
||||
top_p: params.topP,
|
||||
stop: params.stopSequences,
|
||||
stream: true
|
||||
});
|
||||
|
||||
if (!response.body) {
|
||||
throw new Error('No response body for streaming');
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
let messageId = '';
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || '';
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed.startsWith('data: ')) continue;
|
||||
|
||||
const data = trimmed.slice(6);
|
||||
if (data === '[DONE]') return;
|
||||
|
||||
let chunk: OpenWebUIStreamChunk;
|
||||
try {
|
||||
chunk = JSON.parse(data) as OpenWebUIStreamChunk;
|
||||
} catch (parseError) {
|
||||
console.warn('Failed to parse streaming chunk:', parseError);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (chunk.id && !messageId) {
|
||||
messageId = chunk.id;
|
||||
}
|
||||
|
||||
const delta = chunk.choices[0]?.delta;
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
content: delta.content,
|
||||
isComplete: false,
|
||||
id: messageId || chunk.id
|
||||
};
|
||||
}
|
||||
|
||||
if (chunk.choices[0]?.finish_reason) {
|
||||
yield {
|
||||
content: '',
|
||||
isComplete: true,
|
||||
id: messageId || chunk.id,
|
||||
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
|
||||
};
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Ollama strategy — direct /ollama/api/generate endpoint
|
||||
// ============================================================================
|
||||
|
||||
export class OpenWebUIOllamaStrategy implements OpenWebUIStrategy {
|
||||
constructor(private readonly http: OpenWebUIHttpClient) {}
|
||||
|
||||
async validateConnection(): Promise<void> {
|
||||
const response = await this.http.request('/ollama/api/tags', 'GET');
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
}
|
||||
|
||||
async complete(params: CompletionParams, defaultModel: string): Promise<CompletionResponse<string>> {
|
||||
const response = await this.http.request('/ollama/api/generate', 'POST', {
|
||||
model: params.model || defaultModel,
|
||||
prompt: convertMessagesToPrompt(params.messages),
|
||||
stream: false,
|
||||
options: {
|
||||
temperature: params.temperature ?? DEFAULT_TEMPERATURE,
|
||||
top_p: params.topP,
|
||||
num_predict: params.maxTokens || DEFAULT_MAX_TOKENS,
|
||||
stop: params.stopSequences
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json() as OllamaGenerateResponse;
|
||||
return formatOllamaResponse(data);
|
||||
}
|
||||
|
||||
async *stream(params: CompletionParams, defaultModel: string): AsyncIterable<CompletionChunk> {
|
||||
const response = await this.http.request('/ollama/api/generate', 'POST', {
|
||||
model: params.model || defaultModel,
|
||||
prompt: convertMessagesToPrompt(params.messages),
|
||||
stream: true,
|
||||
options: {
|
||||
temperature: params.temperature ?? DEFAULT_TEMPERATURE,
|
||||
top_p: params.topP,
|
||||
num_predict: params.maxTokens || DEFAULT_MAX_TOKENS,
|
||||
stop: params.stopSequences
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.body) {
|
||||
throw new Error('No response body for streaming');
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
const messageId = `ollama-${Date.now()}`;
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || '';
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
|
||||
let chunk: OllamaGenerateResponse;
|
||||
try {
|
||||
chunk = JSON.parse(trimmed) as OllamaGenerateResponse;
|
||||
} catch (parseError) {
|
||||
console.warn('Failed to parse Ollama streaming chunk:', parseError);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (chunk.response) {
|
||||
yield {
|
||||
content: chunk.response,
|
||||
isComplete: false,
|
||||
id: messageId
|
||||
};
|
||||
}
|
||||
|
||||
if (chunk.done) {
|
||||
yield {
|
||||
content: '',
|
||||
isComplete: true,
|
||||
id: messageId,
|
||||
usage: {
|
||||
promptTokens: chunk.prompt_eval_count || 0,
|
||||
completionTokens: chunk.eval_count || 0,
|
||||
totalTokens: (chunk.prompt_eval_count || 0) + (chunk.eval_count || 0)
|
||||
}
|
||||
};
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Shared message helpers
|
||||
// ============================================================================
|
||||
|
||||
function convertMessages(messages: AIMessage[]): Array<{ role: string; content: string }> {
|
||||
return messages.map(message => ({
|
||||
role: message.role,
|
||||
content: message.content
|
||||
}));
|
||||
}
|
||||
|
||||
function convertMessagesToPrompt(messages: AIMessage[]): string {
|
||||
let prompt = '';
|
||||
for (const message of messages) {
|
||||
switch (message.role) {
|
||||
case 'system':
|
||||
prompt += `System: ${message.content}\n\n`;
|
||||
break;
|
||||
case 'user':
|
||||
prompt += `Human: ${message.content}\n\n`;
|
||||
break;
|
||||
case 'assistant':
|
||||
prompt += `Assistant: ${message.content}\n\n`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return prompt + 'Assistant: ';
|
||||
}
|
||||
|
||||
function formatChatResponse(response: OpenWebUIChatResponse): CompletionResponse<string> {
|
||||
const choice = response.choices[0];
|
||||
if (!choice || !choice.message.content) {
|
||||
throw new AIProviderError('No content found in OpenWebUI response', AIErrorType.UNKNOWN);
|
||||
}
|
||||
|
||||
return {
|
||||
content: choice.message.content,
|
||||
model: response.model,
|
||||
usage: {
|
||||
promptTokens: response.usage?.prompt_tokens || 0,
|
||||
completionTokens: response.usage?.completion_tokens || 0,
|
||||
totalTokens: response.usage?.total_tokens || 0
|
||||
},
|
||||
id: response.id,
|
||||
metadata: {
|
||||
finishReason: choice.finish_reason,
|
||||
created: response.created
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function formatOllamaResponse(response: OllamaGenerateResponse): CompletionResponse<string> {
|
||||
return {
|
||||
content: response.response,
|
||||
model: response.model,
|
||||
usage: {
|
||||
promptTokens: response.prompt_eval_count || 0,
|
||||
completionTokens: response.eval_count || 0,
|
||||
totalTokens: (response.prompt_eval_count || 0) + (response.eval_count || 0)
|
||||
},
|
||||
id: `ollama-${Date.now()}`,
|
||||
metadata: {
|
||||
created: new Date(response.created_at).getTime(),
|
||||
totalDuration: response.total_duration,
|
||||
loadDuration: response.load_duration,
|
||||
promptEvalDuration: response.prompt_eval_duration,
|
||||
evalDuration: response.eval_duration
|
||||
}
|
||||
};
|
||||
}
|
||||
55
src/providers/openwebui-types.ts
Normal file
55
src/providers/openwebui-types.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Wire-format response types for OpenWebUI's two backends.
|
||||
*/
|
||||
|
||||
export interface OpenWebUIChatResponse {
|
||||
id: string;
|
||||
object: string;
|
||||
created: number;
|
||||
model: string;
|
||||
choices: Array<{
|
||||
index: number;
|
||||
message: { role: string; content: string };
|
||||
finish_reason: string | null;
|
||||
}>;
|
||||
usage?: {
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
total_tokens: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface OpenWebUIStreamChunk {
|
||||
id: string;
|
||||
object: string;
|
||||
created: number;
|
||||
model: string;
|
||||
choices: Array<{
|
||||
index: number;
|
||||
delta: { role?: string; content?: string };
|
||||
finish_reason: string | null;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface OllamaGenerateResponse {
|
||||
model: string;
|
||||
created_at: string;
|
||||
response: string;
|
||||
done: boolean;
|
||||
context?: number[];
|
||||
total_duration?: number;
|
||||
load_duration?: number;
|
||||
prompt_eval_count?: number;
|
||||
prompt_eval_duration?: number;
|
||||
eval_count?: number;
|
||||
eval_duration?: number;
|
||||
}
|
||||
|
||||
export interface OpenWebUIModelsResponse {
|
||||
data: Array<{
|
||||
id: string;
|
||||
object: string;
|
||||
created: number;
|
||||
owned_by: string;
|
||||
}>;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,23 +1,22 @@
|
||||
/**
|
||||
* Factory utilities for creating AI providers
|
||||
* Provides convenient methods for instantiating and configuring providers
|
||||
* Factory utilities for creating AI providers.
|
||||
*/
|
||||
|
||||
import type { AIProviderConfig } from '../types/index.js';
|
||||
import { ClaudeProvider, type ClaudeConfig } from '../providers/claude.js';
|
||||
import { OpenAIProvider, type OpenAIConfig } from '../providers/openai.js';
|
||||
import { GeminiProvider, type GeminiConfig } from '../providers/gemini.js';
|
||||
import { OpenWebUIProvider, type OpenWebUIConfig } from '../providers/openwebui.js';
|
||||
import { BaseAIProvider } from '../providers/base.js';
|
||||
|
||||
/**
|
||||
* Supported AI provider types
|
||||
*/
|
||||
export type ProviderType = 'claude' | 'openai' | 'gemini' | 'openwebui';
|
||||
export const PROVIDER_REGISTRY = {
|
||||
claude: ClaudeProvider,
|
||||
openai: OpenAIProvider,
|
||||
gemini: GeminiProvider,
|
||||
openwebui: OpenWebUIProvider
|
||||
} as const;
|
||||
|
||||
export type ProviderType = keyof typeof PROVIDER_REGISTRY;
|
||||
|
||||
/**
|
||||
* Configuration map for different provider types
|
||||
*/
|
||||
export interface ProviderConfigMap {
|
||||
claude: ClaudeConfig;
|
||||
openai: OpenAIConfig;
|
||||
@@ -25,144 +24,38 @@ export interface ProviderConfigMap {
|
||||
openwebui: OpenWebUIConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function to create AI providers
|
||||
* @param type - The type of provider to create
|
||||
* @param config - Configuration for the provider
|
||||
* @returns Configured AI provider instance
|
||||
*/
|
||||
export function createProvider<T extends ProviderType>(
|
||||
type: T,
|
||||
config: ProviderConfigMap[T]
|
||||
): BaseAIProvider {
|
||||
switch (type) {
|
||||
case 'claude':
|
||||
return new ClaudeProvider(config as ClaudeConfig);
|
||||
case 'openai':
|
||||
return new OpenAIProvider(config as OpenAIConfig);
|
||||
case 'gemini':
|
||||
return new GeminiProvider(config as GeminiConfig);
|
||||
case 'openwebui':
|
||||
return new OpenWebUIProvider(config as OpenWebUIConfig);
|
||||
default:
|
||||
throw new Error(`Unsupported provider type: ${type}`);
|
||||
const ProviderClass = PROVIDER_REGISTRY[type];
|
||||
if (!ProviderClass) {
|
||||
throw new Error(`Unsupported provider type: ${type}`);
|
||||
}
|
||||
return new ProviderClass(config as any);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Claude provider with simplified configuration
|
||||
* @param apiKey - Anthropic API key
|
||||
* @param options - Optional additional configuration
|
||||
* @returns Configured Claude provider instance
|
||||
*/
|
||||
export function createClaudeProvider(
|
||||
apiKey: string,
|
||||
options: Partial<Omit<ClaudeConfig, 'apiKey'>> = {}
|
||||
): ClaudeProvider {
|
||||
return new ClaudeProvider({
|
||||
apiKey,
|
||||
...options
|
||||
});
|
||||
return new ClaudeProvider({ apiKey, ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an OpenAI provider with simplified configuration
|
||||
* @param apiKey - OpenAI API key
|
||||
* @param options - Optional additional configuration
|
||||
* @returns Configured OpenAI provider instance
|
||||
*/
|
||||
export function createOpenAIProvider(
|
||||
apiKey: string,
|
||||
options: Partial<Omit<OpenAIConfig, 'apiKey'>> = {}
|
||||
): OpenAIProvider {
|
||||
return new OpenAIProvider({
|
||||
apiKey,
|
||||
...options
|
||||
});
|
||||
return new OpenAIProvider({ apiKey, ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Gemini provider with simplified configuration
|
||||
* @param apiKey - Google AI API key
|
||||
* @param options - Optional additional configuration
|
||||
* @returns Configured Gemini provider instance
|
||||
*/
|
||||
export function createGeminiProvider(
|
||||
apiKey: string,
|
||||
options: Partial<Omit<GeminiConfig, 'apiKey'>> = {}
|
||||
): GeminiProvider {
|
||||
return new GeminiProvider({
|
||||
apiKey,
|
||||
...options
|
||||
});
|
||||
return new GeminiProvider({ apiKey, ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an OpenWebUI provider instance
|
||||
*/
|
||||
export function createOpenWebUIProvider(config: OpenWebUIConfig): OpenWebUIProvider {
|
||||
return new OpenWebUIProvider(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider registry for dynamic provider creation
|
||||
*/
|
||||
export class ProviderRegistry {
|
||||
private static providers = new Map<string, new (config: AIProviderConfig) => BaseAIProvider>();
|
||||
|
||||
/**
|
||||
* Register a new provider type
|
||||
* @param name - Name of the provider
|
||||
* @param providerClass - Provider class constructor
|
||||
*/
|
||||
static register(name: string, providerClass: new (config: AIProviderConfig) => BaseAIProvider): void {
|
||||
this.providers.set(name.toLowerCase(), providerClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a provider by name
|
||||
* @param name - Name of the provider
|
||||
* @param config - Configuration for the provider
|
||||
* @returns Provider instance
|
||||
*/
|
||||
static create(name: string, config: AIProviderConfig): BaseAIProvider {
|
||||
const ProviderClass = this.providers.get(name.toLowerCase());
|
||||
if (!ProviderClass) {
|
||||
throw new Error(`Provider '${name}' is not registered`);
|
||||
}
|
||||
return new ProviderClass(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of registered provider names
|
||||
* @returns Array of registered provider names
|
||||
*/
|
||||
static getRegisteredProviders(): string[] {
|
||||
return Array.from(this.providers.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a provider is registered
|
||||
* @param name - Name of the provider
|
||||
* @returns True if provider is registered
|
||||
*/
|
||||
static isRegistered(name: string): boolean {
|
||||
return this.providers.has(name.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-register built-in providers
|
||||
ProviderRegistry.register('claude', ClaudeProvider);
|
||||
ProviderRegistry.register('openai', OpenAIProvider);
|
||||
ProviderRegistry.register('gemini', GeminiProvider);
|
||||
ProviderRegistry.register('openwebui', OpenWebUIProvider);
|
||||
|
||||
/**
|
||||
* Registry of all available providers
|
||||
*/
|
||||
export const PROVIDER_REGISTRY = {
|
||||
claude: ClaudeProvider,
|
||||
openai: OpenAIProvider,
|
||||
gemini: GeminiProvider,
|
||||
openwebui: OpenWebUIProvider
|
||||
} as const;
|
||||
@@ -5,7 +5,8 @@
|
||||
"emitDeclarationOnly": true,
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"moduleResolution": "node",
|
||||
"rootDir": "./src",
|
||||
"moduleResolution": "bundler",
|
||||
"verbatimModuleSyntax": false
|
||||
},
|
||||
"include": [
|
||||
|
||||
Reference in New Issue
Block a user