diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..fefb52c --- /dev/null +++ b/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "svelte-llm": { + "type": "http", + "url": "https://svelte-llm.stanislav.garden/mcp/mcp" + } + } +} \ No newline at end of file diff --git a/bun.lock b/bun.lock index d3e6705..124bee5 100644 --- a/bun.lock +++ b/bun.lock @@ -13,7 +13,7 @@ "@fontsource-variable/nunito-sans": "^5.2.6", "@fontsource-variable/open-sans": "^5.2.6", "@fontsource/instrument-serif": "^5.2.6", - "@keplersystems/kepler-ai-sdk": "^1.0.5", + "@keplersystems/kepler-ai-sdk": "file:./tmp/kepler-ai-sdk", "better-auth": "^1.2.9", "convex-helpers": "^0.1.94", "hastscript": "^9.0.1", @@ -21,10 +21,13 @@ "zod": "^3.25.64", }, "devDependencies": { + "@anthropic-ai/sdk": "^0.29.0", "@better-auth-kit/convex": "^1.2.2", "@eslint/compat": "^1.2.5", "@eslint/js": "^9.18.0", + "@google/generative-ai": "^0.21.0", "@iconify/json": "^2.2.349", + "@mistralai/mistralai": "^1.1.0", "@playwright/test": "^1.49.1", "@shikijs/langs": "^3.6.0", "@shikijs/markdown-it": "^3.6.0", @@ -38,6 +41,7 @@ "@vercel/functions": "^2.2.0", "bits-ui": "^2.8.5", "clsx": "^2.1.1", + "cohere-ai": "^7.14.0", "concurrently": "^9.1.2", "convex": "^1.24.8", "convex-svelte": "^0.0.11", @@ -79,7 +83,7 @@ "@antfu/utils": ["@antfu/utils@8.1.1", "", {}, "sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ=="], - "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.55.1", "", { "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-gjOMS4chmm8BxClKmCjNHmvf1FrO1Cn++CSX6K3YCZjz5JG4I9ZttQ/xEH4FBsz6HQyZvnUpiKlOAkmxaGmEaQ=="], + "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.29.2", "", { "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" } }, "sha512-5dwiOPO/AZvhY4bJIG9vjFKU9Kza3hA6VEsbIQg6L9vny2RQIpCFhV50nB9IrG2edZaHZb4HuQ9Wmsn5zgWyZg=="], "@asamuzakjp/css-color": ["@asamuzakjp/css-color@3.2.0", "", { "dependencies": { "@csstools/css-calc": "^2.1.3", "@csstools/css-color-parser": "^3.0.9", "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "lru-cache": "^10.4.3" } }, "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw=="], @@ -259,6 +263,8 @@ "@google/genai": ["@google/genai@1.16.0", "", { "dependencies": { "google-auth-library": "^9.14.2", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.11.4" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-hdTYu39QgDFxv+FB6BK2zi4UIJGWhx2iPc0pHQ0C5Q/RCi+m+4gsryIzTGO+riqWcUA8/WGYp6hpqckdOBNysw=="], + "@google/generative-ai": ["@google/generative-ai@0.21.0", "", {}, "sha512-7XhUbtnlkSEZK15kN3t+tzIMxsbKm/dSkKBFalj+20NvPKe1kBY7mR2P7vuijEn+f06z5+A8bVGKO0v39cr6Wg=="], + "@hexagon/base64": ["@hexagon/base64@1.1.28", "", {}, "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw=="], "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], @@ -277,6 +283,8 @@ "@internationalized/date": ["@internationalized/date@3.9.0", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-yaN3brAnHRD+4KyyOsJyk49XUvj2wtbNACSqg0bz3u8t2VuzhC8Q5dfRnrSxjnnbDb+ienBnkn1TzQfE154vyg=="], + "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], "@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], @@ -291,7 +299,7 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.30", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q=="], - "@keplersystems/kepler-ai-sdk": ["@keplersystems/kepler-ai-sdk@1.0.5", "", { "dependencies": { "@anthropic-ai/sdk": "^0.55.0", "@google/genai": "^1.7.0", "@mistralai/mistralai": "^1.7.2", "cohere-ai": "^7.17.1", "exa-js": "^1.8.20", "openai": "^5.7.0" }, "peerDependencies": { "typescript": "^5" } }, "sha512-aynAClqQtS1Xkt53lZ4TdOS1YbtkfaYIrWxSmNXEV5DahLzHRi8YJ3zooAq2cmHUwjaEXmpaB2AMFdwAxWWTkQ=="], + "@keplersystems/kepler-ai-sdk": ["@keplersystems/kepler-ai-sdk@file:tmp/kepler-ai-sdk", { "dependencies": { "@anthropic-ai/sdk": "^0.55.0", "@google/genai": "^1.7.0", "@mistralai/mistralai": "^1.7.2", "cohere-ai": "^7.17.1", "exa-js": "^1.8.20", "openai": "^5.7.0" }, "devDependencies": { "@types/bun": "latest", "rimraf": "^5.0.5" }, "peerDependencies": { "typescript": "^5" } }], "@levischuck/tiny-cbor": ["@levischuck/tiny-cbor@0.2.11", "", {}, "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow=="], @@ -317,6 +325,8 @@ "@peculiar/asn1-x509": ["@peculiar/asn1-x509@2.4.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.4.0", "asn1js": "^3.0.6", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-F7mIZY2Eao2TaoVqigGMLv+NDdpwuBKU1fucHPONfzaBS4JXXCNCmfO0Z3dsy7JzKGqtDcYC1mr9JjaZQZNiuw=="], + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], + "@playwright/test": ["@playwright/test@1.55.0", "", { "dependencies": { "playwright": "1.55.0" }, "bin": { "playwright": "cli.js" } }, "sha512-04IXzPwHrW69XusN/SIdDdKZBzMfOT9UNT/YiJit/xpy2VuAoB8NHc8Aplb96zsWDddLnbkPL3TsmrS04ZU2xQ=="], "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], @@ -515,6 +525,8 @@ "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], + "@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="], + "@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="], "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], @@ -537,6 +549,12 @@ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + "@types/node": ["@types/node@18.19.123", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-K7DIaHnh0mzVxreCR9qwgNxp3MH9dltPNIEddW9MYUlcKAzm+3grKNSTe2vCJHI1FaLpvpL5JGJrz1UZDKYvDg=="], + + "@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="], + + "@types/react": ["@types/react@19.1.12", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w=="], + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], @@ -591,6 +609,8 @@ "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=="], + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -633,6 +653,8 @@ "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], + "bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="], + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], "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=="], @@ -697,6 +719,8 @@ "cssstyle": ["cssstyle@4.6.0", "", { "dependencies": { "@asamuzakjp/css-color": "^3.2.0", "rrweb-cssom": "^0.8.0" } }, "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg=="], + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + "data-urls": ["data-urls@5.0.0", "", { "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0" } }, "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg=="], "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], @@ -731,6 +755,8 @@ "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=="], + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -819,11 +845,13 @@ "focus-trap": ["focus-trap@7.6.5", "", { "dependencies": { "tabbable": "^6.2.0" } }, "sha512-7Ke1jyybbbPZyZXFxEftUtxFGLMpE2n6A+z//m4CRDlj0hW+o3iYSmh8nFlYMurOiJVDmJRilUQtJr08KfIxlg=="], + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + "form-data": ["form-data@4.0.4", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow=="], - "form-data-encoder": ["form-data-encoder@4.1.0", "", {}, "sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw=="], + "form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="], - "formdata-node": ["formdata-node@6.0.3", "", {}, "sha512-8e1++BCiTzUno9v5IZ2J6bv4RU+3UKDmqWUQD0MIMVCd9AdhWkO1gw57oo1mNEX1dMq2EGI+FbWz4B92pscSQg=="], + "formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], @@ -839,6 +867,8 @@ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + "glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], "globals": ["globals@16.3.0", "", {}, "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ=="], @@ -879,6 +909,8 @@ "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], + "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], @@ -911,6 +943,8 @@ "isomorphic-dompurify": ["isomorphic-dompurify@2.26.0", "", { "dependencies": { "dompurify": "^3.2.6", "jsdom": "^26.1.0" } }, "sha512-nZmoK4wKdzPs5USq4JHBiimjdKSVAOm2T1KyDoadtMPNXYHxiENd19ou4iU/V4juFM6LVgYQnpxCYmxqNP4Obw=="], + "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + "jest-axe": ["jest-axe@9.0.0", "", { "dependencies": { "axe-core": "4.9.1", "chalk": "4.1.2", "jest-matcher-utils": "29.2.2", "lodash.merge": "4.6.2" } }, "sha512-Xt7O0+wIpW31lv0SO1wQZUTyJE7DEmnDEZeTt9/S9L5WUywxrv8BrgvTuQEqujtfaQOcJ70p4wg7UUgK1E2F5g=="], "jest-diff": ["jest-diff@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw=="], @@ -1059,6 +1093,8 @@ "neverthrow": ["neverthrow@8.2.0", "", { "optionalDependencies": { "@rollup/rollup-linux-x64-gnu": "^4.24.0" } }, "sha512-kOCT/1MCPAxY5iUV3wytNFUMUolzuwd/VF/1KCx7kf6CutrOsTie+84zTGTpgQycjvfLdBBdvBvFLqFD2c0wkQ=="], + "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=="], "nwsapi": ["nwsapi@2.2.21", "", {}, "sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA=="], @@ -1077,6 +1113,8 @@ "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + "package-manager-detector": ["package-manager-detector@1.3.0", "", {}, "sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ=="], "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], @@ -1087,6 +1125,8 @@ "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], @@ -1159,6 +1199,8 @@ "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + "rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="], + "rollup": ["rollup@4.49.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.49.0", "@rollup/rollup-android-arm64": "4.49.0", "@rollup/rollup-darwin-arm64": "4.49.0", "@rollup/rollup-darwin-x64": "4.49.0", "@rollup/rollup-freebsd-arm64": "4.49.0", "@rollup/rollup-freebsd-x64": "4.49.0", "@rollup/rollup-linux-arm-gnueabihf": "4.49.0", "@rollup/rollup-linux-arm-musleabihf": "4.49.0", "@rollup/rollup-linux-arm64-gnu": "4.49.0", "@rollup/rollup-linux-arm64-musl": "4.49.0", "@rollup/rollup-linux-loongarch64-gnu": "4.49.0", "@rollup/rollup-linux-ppc64-gnu": "4.49.0", "@rollup/rollup-linux-riscv64-gnu": "4.49.0", "@rollup/rollup-linux-riscv64-musl": "4.49.0", "@rollup/rollup-linux-s390x-gnu": "4.49.0", "@rollup/rollup-linux-x64-gnu": "4.49.0", "@rollup/rollup-linux-x64-musl": "4.49.0", "@rollup/rollup-win32-arm64-msvc": "4.49.0", "@rollup/rollup-win32-ia32-msvc": "4.49.0", "@rollup/rollup-win32-x64-msvc": "4.49.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3IVq0cGJ6H7fKXXEdVt+RcYvRCt8beYY9K1760wGQwSAHZcS9eot1zDG5axUbcp/kWRi5zKIIDX8MoKv/TzvZA=="], "rou3": ["rou3@0.5.1", "", {}, "sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ=="], @@ -1203,6 +1245,8 @@ "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "sirv": ["sirv@3.0.1", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], @@ -1215,12 +1259,16 @@ "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="], "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], @@ -1303,6 +1351,8 @@ "uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="], + "undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "unist-util-is": ["unist-util-is@6.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw=="], "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], @@ -1339,6 +1389,8 @@ "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], + "web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="], + "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], @@ -1357,6 +1409,8 @@ "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], @@ -1371,7 +1425,7 @@ "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], - "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + "yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], @@ -1397,6 +1451,14 @@ "@iconify/utils/globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="], + "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + + "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + + "@keplersystems/kepler-ai-sdk/@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.55.1", "", { "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-gjOMS4chmm8BxClKmCjNHmvf1FrO1Cn++CSX6K3YCZjz5JG4I9ZttQ/xEH4FBsz6HQyZvnUpiKlOAkmxaGmEaQ=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.5.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="], @@ -1413,6 +1475,8 @@ "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], + "@types/node-fetch/@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], @@ -1423,14 +1487,20 @@ "bits-ui/runed": ["runed@0.29.2", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-0cq6cA6sYGZwl/FvVqjx9YN+1xEBu9sDDyuWdDW1yWX7JF2wmvmVKfH+hVCZs+csW+P3ARH92MjI3H9QTagOQA=="], + "bun-types/@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], + "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "convict/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], + "cohere-ai/form-data-encoder": ["form-data-encoder@4.1.0", "", {}, "sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw=="], + + "cohere-ai/formdata-node": ["formdata-node@6.0.3", "", {}, "sha512-8e1++BCiTzUno9v5IZ2J6bv4RU+3UKDmqWUQD0MIMVCd9AdhWkO1gw57oo1mNEX1dMq2EGI+FbWz4B92pscSQg=="], "exa-js/dotenv": ["dotenv@16.4.7", "", {}, "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ=="], "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "jest-diff/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], "jest-matcher-utils/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], @@ -1467,12 +1537,26 @@ "vitest/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.0", "", {}, "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg=="], + + "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + + "@types/node-fetch/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "bun-types/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + + "glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "jest-diff/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], "jest-diff/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], diff --git a/package.json b/package.json index 4816f95..0bd4ca0 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "@fontsource-variable/nunito-sans": "^5.2.6", "@fontsource-variable/open-sans": "^5.2.6", "@fontsource/instrument-serif": "^5.2.6", - "@keplersystems/kepler-ai-sdk": "^1.0.5", + "@keplersystems/kepler-ai-sdk": "file:./tmp/kepler-ai-sdk", "better-auth": "^1.2.9", "convex-helpers": "^0.1.94", "hastscript": "^9.0.1", diff --git a/src/lib/backend/convex/user_enabled_models.ts b/src/lib/backend/convex/user_enabled_models.ts index 173f026..900d71f 100644 --- a/src/lib/backend/convex/user_enabled_models.ts +++ b/src/lib/backend/convex/user_enabled_models.ts @@ -101,16 +101,20 @@ export const set = mutation({ ) .first(); - if (args.enabled && existing) return; // nothing to do here - - if (existing) { - await ctx.db.delete(existing._id); + if (args.enabled) { + // Enable model: insert if not exists + if (!existing) { + await ctx.db.insert('user_enabled_models', { + ...object.pick(args, ['provider', 'model_id']), + user_id: session.userId, + pinned: false, + }); + } } else { - await ctx.db.insert('user_enabled_models', { - ...object.pick(args, ['provider', 'model_id']), - user_id: session.userId, - pinned: false, - }); + // Disable model: delete if exists + if (existing) { + await ctx.db.delete(existing._id); + } } }, }); diff --git a/src/lib/components/model-picker/model-picker.svelte b/src/lib/components/model-picker/model-picker.svelte index 40c18cb..405a97e 100644 --- a/src/lib/components/model-picker/model-picker.svelte +++ b/src/lib/components/model-picker/model-picker.svelte @@ -8,9 +8,9 @@ import { models as modelsState } from '$lib/state/models.svelte'; import { session } from '$lib/state/session.svelte'; import { settings } from '$lib/state/settings.svelte'; - import { Provider } from '$lib/types'; + import { Provider, PROVIDER_META } from '$lib/types'; import { fuzzysearch } from '$lib/utils/fuzzy-search'; - import { supportsImages, supportsReasoning } from '$lib/utils/model-capabilities'; + import { supportsImages, supportsReasoning, supportsStreaming, supportsToolCalls } from '$lib/utils/model-capabilities'; import { capitalize } from '$lib/utils/strings'; import { cn } from '$lib/utils/utils'; import { type Component } from 'svelte'; @@ -57,7 +57,14 @@ session_token: session.current?.session.token ?? '', }); - const enabledArr = $derived(Object.values(enabledModelsQuery.data ?? {})); + // Get enabled models from our models state with ModelInfo data + const enabledArr = $derived.by(() => { + const enabledModelIds = Object.keys(enabledModelsQuery.data ?? {}); + const enabledModels = modelsState.all().filter(model => + enabledModelIds.some(id => id.includes(model.id)) + ); + return enabledModels; + }); modelsState.init(); @@ -133,37 +140,38 @@ fuzzysearch({ haystack: enabledArr, needle: search, - property: 'model_id', + property: 'id', }) ); - // Group models by company + // Group models by provider const groupedModels = $derived.by(() => { - const groups: Record = {}; + const groups: Record = {} as Record; filteredModels.forEach((model) => { - const company = getCompanyFromModelId(model.model_id); - if (!groups[company]) { - groups[company] = []; + const provider = model.provider as Provider; + if (!groups[provider]) { + groups[provider] = []; } - groups[company].push(model); + groups[provider].push(model); }); - // Sort companies with known icons first - const result = Object.entries(groups).sort(([a], [b]) => { - const aHasIcon = companyIcons[a] ? 0 : 1; - const bHasIcon = companyIcons[b] ? 0 : 1; - return aHasIcon - bHasIcon || a.localeCompare(b); - }); + // Sort by provider order and name + const result = Object.entries(groups) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([provider, models]) => [ + provider, + models.sort((a, b) => a.name.localeCompare(b.name)) + ] as [Provider, typeof models]); return result; }); - const currentModel = $derived(enabledArr.find((m) => m.model_id === settings.modelId)); + const currentModel = $derived(enabledArr.find((m) => m.id === settings.modelId)); $effect(() => { - if (!enabledArr.find((m) => m.model_id === settings.modelId) && enabledArr.length > 0) { - settings.modelId = enabledArr[0]!.model_id; + if (!enabledArr.find((m) => m.id === settings.modelId) && enabledArr.length > 0) { + settings.modelId = enabledArr[0]!.id; } }); @@ -179,8 +187,10 @@ { from: 'o3', to: 'o3' }, ]; - function formatModelName(modelId: string) { - const cleanId = modelId.replace(/^[^/]+\//, ''); + function formatModelName(model: { id: string; name: string }) { + // Use the name field if available, fallback to processing ID + const displayName = model.name || model.id; + const cleanId = displayName.replace(/^[^/]+\//, ''); const parts = cleanId.split(/[-_,:]/); const formattedParts = parts.map((part) => { @@ -228,17 +238,20 @@ const activeModelInfo = $derived.by(() => { if (activeModel === '') return null; - const model = enabledArr.find((m) => m.model_id === activeModel); + const model = enabledArr.find((m) => m.id === activeModel); if (!model) return null; return { ...model, - formatted: formatModelName(activeModel), + formatted: formatModelName(model), }; }); - const pinnedModels = $derived(enabledArr.filter((m) => isPinned(m))); + // For now, we'll need to maintain pinned models using the old enabled models structure + // until we migrate the pinning system to work with the new ModelInfo structure + const enabledModelsData = $derived(Object.values(enabledModelsQuery.data ?? {})); + const pinnedModels = $derived(enabledModelsData.filter((m) => isPinned(m)));
- {#if currentModel && getModelIcon(currentModel.model_id)} - {@const IconComponent = getModelIcon(currentModel.model_id)} + {#if currentModel && getModelIcon(currentModel.id)} + {@const IconComponent = getModelIcon(currentModel.id)} {/if} - {currentModel ? formatModelName(currentModel.model_id).full : 'Select model'} + {currentModel ? formatModelName(currentModel).full : 'Select model'}
@@ -324,12 +337,9 @@ > {#if view === 'favorites' && pinnedModels.length > 0} {#each pinnedModels as model (model._id)} - {@const formatted = formatModelName(model.model_id)} - {@const openRouterModel = modelsState - .from(Provider.OpenRouter) - .find((m) => m.id === model.model_id)} - {@const disabled = - onlyImageModels && openRouterModel && !supportsImages(openRouterModel)} + {@const modelInfo = enabledArr.find((m) => m.id === model.model_id)} + {@const formatted = modelInfo ? formatModelName(modelInfo) : { full: model.model_id, primary: model.model_id, secondary: '' }} + {@const disabled = onlyImageModels && modelInfo && !supportsImages(modelInfo)}
- {#if openRouterModel && supportsImages(openRouterModel)} + {#if modelInfo && supportsImages(modelInfo)} {#snippet trigger(tooltip)}
{/snippet} - Supports image analysis + Supports vision/image analysis
{/if} - {#if openRouterModel && supportsReasoning(openRouterModel)} + {#if modelInfo && supportsReasoning(modelInfo)} {#snippet trigger(tooltip)}
{/if} + + {#if modelInfo && supportsStreaming(modelInfo)} + + {#snippet trigger(tooltip)} +
+ +
+ {/snippet} + Supports streaming responses +
+ {/if} + + {#if modelInfo && supportsToolCalls(modelInfo)} + + {#snippet trigger(tooltip)} +
+ +
+ {/snippet} + Supports tool/function calling +
+ {/if}
{/each} @@ -394,22 +432,25 @@ {#each pinnedModels as model (model._id)} - {@render modelCard(model)} + {@const modelInfo = enabledArr.find((m) => m.id === model.model_id)} + {#if modelInfo} + {@render modelCard(modelInfo)} + {/if} {/each} {/if} - {#each groupedModels as [company, models] (company)} - {@const filteredModels = models.filter((m) => !isPinned(m))} - {#if filteredModels.length > 0} + {#each groupedModels as [provider, models] (provider)} + {@const providerMeta = PROVIDER_META[provider]} + {#if models.length > 0} - {company} + {providerMeta.title} - {#each filteredModels as model (model._id)} + {#each models as model (model.id)} {@render modelCard(model)} {/each} @@ -457,14 +498,12 @@ {#snippet modelCard(model: (typeof enabledArr)[number])} - {@const formatted = formatModelName(model.model_id)} - {@const openRouterModel = modelsState - .from(Provider.OpenRouter) - .find((m) => m.id === model.model_id)} - {@const disabled = onlyImageModels && openRouterModel && !supportsImages(openRouterModel)} + {@const formatted = formatModelName(model)} + {@const disabled = onlyImageModels && !supportsImages(model)} + {@const enabledModelData = enabledModelsData.find(m => m.model_id === model.id)} modelSelected(model.model_id)} + onSelect={() => modelSelected(model.id)} >
- {#if getModelIcon(model.model_id)} - {@const ModelIcon = getModelIcon(model.model_id)} + {#if getModelIcon(model.id)} + {@const ModelIcon = getModelIcon(model.id)} {/if} @@ -494,7 +533,7 @@
- {#if openRouterModel && supportsImages(openRouterModel)} + {#if supportsImages(model)} {#snippet trigger(tooltip)}
{/snippet} - Supports image analysis + Supports vision/image analysis
{/if} - {#if openRouterModel && supportsReasoning(openRouterModel)} + {#if supportsReasoning(model)} {#snippet trigger(tooltip)}
{/if} + + {#if supportsStreaming(model)} + + {#snippet trigger(tooltip)} +
+ +
+ {/snippet} + Supports streaming responses +
+ {/if} + + {#if supportsToolCalls(model)} + + {#snippet trigger(tooltip)} +
+ +
+ {/snippet} + Supports tool/function calling +
+ {/if}
-
- -
+ +
+ {/if}
{/snippet} diff --git a/src/lib/services/model-loader.server.ts b/src/lib/services/model-loader.server.ts new file mode 100644 index 0000000..8b73dc2 --- /dev/null +++ b/src/lib/services/model-loader.server.ts @@ -0,0 +1,160 @@ +import { ConvexHttpClient } from 'convex/browser'; +import { PUBLIC_CONVEX_URL } from '$env/static/public'; +import { api } from '$lib/backend/convex/_generated/api'; +import { Provider } from '$lib/types'; +import { createModelManager } from './model-manager'; +import type { UserApiKeys } from './model-manager'; +import type { ModelInfo } from '@keplersystems/kepler-ai-sdk'; +import { ResultAsync } from 'neverthrow'; + +export interface MultiProviderModels { + [Provider.OpenAI]?: ModelInfo[]; + [Provider.Anthropic]?: ModelInfo[]; + [Provider.Gemini]?: ModelInfo[]; + [Provider.Mistral]?: ModelInfo[]; + [Provider.Cohere]?: ModelInfo[]; + [Provider.OpenRouter]?: ModelInfo[]; +} + +const client = new ConvexHttpClient(PUBLIC_CONVEX_URL); + +// Cache models for 10 minutes to avoid excessive API calls +const MODEL_CACHE_TTL = 10 * 60 * 1000; +const modelCache = new Map(); + +/** + * Load models for a specific user based on their API keys + */ +export async function loadUserModels(sessionToken: string): Promise { + const cacheKey = sessionToken; + const cached = modelCache.get(cacheKey); + + // Return cached models if still valid + if (cached && Date.now() - cached.timestamp < MODEL_CACHE_TTL) { + console.log('Returning cached models:', Object.keys(cached.models)); + return cached.models; + } + + try { + // Get user's API keys + const userApiKeys = await getUserApiKeys(sessionToken); + console.log('getUserApiKeys result:', userApiKeys); + if (!userApiKeys) { + console.log('No user API keys found'); + return {}; + } + + // Initialize ModelManager with user's API keys + const modelManager = createModelManager(); + modelManager.initializeProviders(userApiKeys); + console.log('Enabled providers:', modelManager.getEnabledProviders()); + + // Load models from all enabled providers + const models: MultiProviderModels = {}; + const enabledProviders = modelManager.getEnabledProviders(); + console.log('Loading models from providers:', enabledProviders); + + const results = await Promise.allSettled( + enabledProviders.map(async (provider) => { + try { + console.log(`Loading models from ${provider}...`); + const providerModels = await modelManager.getModelsByProvider(provider); + console.log(`${provider} returned ${providerModels.length} models`); + if (providerModels.length > 0) { + models[provider] = providerModels; + } + return { provider, count: providerModels.length }; + } catch (error) { + console.warn(`Failed to load models from ${provider}:`, error); + return { provider, error: error.message }; + } + }) + ); + + console.log('Model loading results:', results); + console.log('Final models object:', Object.keys(models)); + + // Cache the results + modelCache.set(cacheKey, { + models, + timestamp: Date.now(), + }); + + return models; + } catch (error) { + console.error('Failed to load user models:', error); + return {}; + } +} + +/** + * Load models without authentication (fallback to empty state) + */ +export function loadGuestModels(): MultiProviderModels { + return {}; +} + +/** + * Clear model cache for a specific user or all users + */ +export function clearModelCache(sessionToken?: string): void { + if (sessionToken) { + modelCache.delete(sessionToken); + } else { + modelCache.clear(); + } +} + +/** + * Get user's API keys from Convex + */ +async function getUserApiKeys(sessionToken: string): Promise { + const keysResult = await ResultAsync.fromPromise( + client.query(api.user_keys.all, { + session_token: sessionToken, + }), + (e) => `Failed to get user API keys: ${e}` + ); + + if (keysResult.isErr()) { + console.error('Failed to get user API keys:', keysResult.error); + return null; + } + + const keys = keysResult.value; + return { + openai: keys.openai, + anthropic: keys.anthropic, + google: keys.gemini, + mistral: keys.mistral, + cohere: keys.cohere, + openrouter: keys.openrouter, + }; +} + +/** + * Get models for a specific provider (useful for partial loading) + */ +export async function loadProviderModels( + sessionToken: string, + provider: Provider +): Promise { + try { + const userApiKeys = await getUserApiKeys(sessionToken); + if (!userApiKeys) { + return []; + } + + const modelManager = createModelManager(); + modelManager.initializeProviders(userApiKeys); + + if (!modelManager.hasProviderEnabled(provider)) { + return []; + } + + return await modelManager.getModelsByProvider(provider); + } catch (error) { + console.error(`Failed to load models from ${provider}:`, error); + return []; + } +} diff --git a/src/lib/services/model-manager.ts b/src/lib/services/model-manager.ts index 34d7c72..219d217 100644 --- a/src/lib/services/model-manager.ts +++ b/src/lib/services/model-manager.ts @@ -19,7 +19,7 @@ export interface ProviderConfig { export interface UserApiKeys { openai?: string; anthropic?: string; - gemini?: string; + google?: string; mistral?: string; cohere?: string; openrouter?: string; @@ -52,9 +52,9 @@ export class ChatModelManager { this.enabledProviders.set('anthropic', provider); } - if (userApiKeys.gemini) { + if (userApiKeys.google) { const provider = new GeminiProvider({ - apiKey: userApiKeys.gemini, + apiKey: userApiKeys.google, }); this.modelManager.addProvider(provider); this.enabledProviders.set('gemini', provider); @@ -97,9 +97,19 @@ export class ChatModelManager { return await this.modelManager.listModels(); } + async getModelsByProvider(provider: Provider): Promise { + if (!this.hasProviderEnabled(provider)) { + return []; + } + const allModels = await this.listAvailableModels(); + return allModels.filter((model) => model.provider === provider); + } + async getModelsByCapability(capability: string): Promise { const allModels = await this.listAvailableModels(); - return allModels.filter(model => model.capabilities[capability as keyof typeof model.capabilities]); + return allModels.filter( + (model) => model.capabilities[capability as keyof typeof model.capabilities] + ); } hasProviderEnabled(provider: Provider): boolean { @@ -111,10 +121,10 @@ export class ChatModelManager { } isModelAvailable(modelId: string): Promise { - return this.getModel(modelId).then(model => model !== null); + return this.getModel(modelId).then((model) => model !== null); } } export const createModelManager = (): ChatModelManager => { return new ChatModelManager(); -}; \ No newline at end of file +}; diff --git a/src/lib/state/models.svelte.ts b/src/lib/state/models.svelte.ts index f301928..37163a7 100644 --- a/src/lib/state/models.svelte.ts +++ b/src/lib/state/models.svelte.ts @@ -1,13 +1,17 @@ import { page } from '$app/state'; import { api } from '$lib/backend/convex/_generated/api'; import { getModelKey } from '$lib/backend/convex/user_enabled_models'; -import type { ProviderModelMap } from '$lib/backend/models/all'; import { useCachedQuery } from '$lib/cache/cached-query.svelte'; import { createInit } from '$lib/spells/create-init.svelte'; import { Provider } from '$lib/types'; +import type { ModelInfo } from '@keplersystems/kepler-ai-sdk'; import { watch } from 'runed'; import { session } from './session.svelte'; +export interface ModelWithEnabledStatus extends ModelInfo { + enabled: boolean; +} + export class Models { enabled = $state({} as Record); @@ -23,14 +27,104 @@ export class Models { ); }); - from

(provider: Provider) { - return page.data.models[provider].map((m: { id: string }) => { - return { - ...m, - enabled: this.enabled[getModelKey({ provider, model_id: m.id })] !== undefined, - }; - }) as Array; + /** + * Get models from a specific provider with enabled status + */ + from(provider: Provider): ModelWithEnabledStatus[] { + const providerModels = page.data.models[provider] || []; + return providerModels.map((model: ModelInfo) => ({ + ...model, + enabled: this.enabled[getModelKey({ provider, model_id: model.id })] !== undefined, + })); + } + + /** + * Get all models from all providers with enabled status + */ + all(): ModelWithEnabledStatus[] { + const allModels: ModelWithEnabledStatus[] = []; + const availableProviders = Object.keys(page.data.models || {}) as Provider[]; + + for (const provider of availableProviders) { + const providerModels = this.from(provider); + allModels.push(...providerModels); + } + + return allModels; + } + + /** + * Get models that support specific capabilities + */ + withCapability(capability: keyof ModelInfo['capabilities']): ModelWithEnabledStatus[] { + return this.all().filter(model => model.capabilities[capability]); + } + + /** + * Get enabled models from all providers + */ + enabledModels(): ModelWithEnabledStatus[] { + return this.all().filter(model => model.enabled); + } + + /** + * Get enabled models from a specific provider + */ + enabledFrom(provider: Provider): ModelWithEnabledStatus[] { + return this.from(provider).filter(model => model.enabled); + } + + /** + * Get available providers (providers that have models loaded) + */ + availableProviders(): Provider[] { + const models = page.data.models || {}; + return Object.keys(models).filter(provider => + models[provider as Provider] && models[provider as Provider].length > 0 + ) as Provider[]; + } + + /** + * Check if a provider has models loaded + */ + hasProvider(provider: Provider): boolean { + const models = page.data.models[provider]; + return Array.isArray(models) && models.length > 0; + } + + /** + * Search models across all providers + */ + search(query: string): ModelWithEnabledStatus[] { + const lowerQuery = query.toLowerCase(); + return this.all().filter(model => + model.name.toLowerCase().includes(lowerQuery) || + model.id.toLowerCase().includes(lowerQuery) || + model.description?.toLowerCase().includes(lowerQuery) + ); + } + + /** + * Get models sorted by preference (enabled first, then by provider order) + */ + sorted(): ModelWithEnabledStatus[] { + return this.all().sort((a, b) => { + // Enabled models first + if (a.enabled && !b.enabled) return -1; + if (!a.enabled && b.enabled) return 1; + + // Then by provider order + const providerOrder = Object.values(Provider); + const aProviderIndex = providerOrder.indexOf(a.provider as Provider); + const bProviderIndex = providerOrder.indexOf(b.provider as Provider); + if (aProviderIndex !== bProviderIndex) { + return aProviderIndex - bProviderIndex; + } + + // Finally by name + return a.name.localeCompare(b.name); + }); } } -export const models = new Models(); +export const models = new Models(); \ No newline at end of file diff --git a/src/lib/types.ts b/src/lib/types.ts index 3fdae2f..76d7f08 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -69,7 +69,7 @@ export const PROVIDER_META: Record = { }, [Provider.Gemini]: { title: 'Google Gemini', - link: 'https://cloud.google.com/vertex-ai', + link: 'https://ai.google.dev/docs', description: 'Gemini models from Google', apiKeyName: 'Google AI API Key', placeholder: 'AIza...', diff --git a/src/lib/utils/model-capabilities.ts b/src/lib/utils/model-capabilities.ts index 9f39a8b..55280f5 100644 --- a/src/lib/utils/model-capabilities.ts +++ b/src/lib/utils/model-capabilities.ts @@ -1,13 +1,29 @@ -import type { OpenRouterModel } from '$lib/backend/models/open-router'; +import type { ModelInfo } from '@keplersystems/kepler-ai-sdk'; -export function supportsImages(model: OpenRouterModel): boolean { - return model.architecture.input_modalities.includes('image'); +export function supportsImages(model: ModelInfo): boolean { + return model.capabilities.vision; } -export function supportsReasoning(model: OpenRouterModel): boolean { - return model.supported_parameters.includes('reasoning'); +export function supportsReasoning(model: ModelInfo): boolean { + return model.capabilities.reasoning; } -export function getImageSupportedModels(models: OpenRouterModel[]): OpenRouterModel[] { +export function supportsStreaming(model: ModelInfo): boolean { + return model.capabilities.streaming; +} + +export function supportsToolCalls(model: ModelInfo): boolean { + return model.capabilities.toolCalls; +} + +export function getImageSupportedModels(models: ModelInfo[]): ModelInfo[] { return models.filter(supportsImages); } + +export function getReasoningModels(models: ModelInfo[]): ModelInfo[] { + return models.filter(supportsReasoning); +} + +export function getStreamingModels(models: ModelInfo[]): ModelInfo[] { + return models.filter(supportsStreaming); +} diff --git a/src/lib/utils/providers.ts b/src/lib/utils/providers.ts index edfe692..59dcddd 100644 --- a/src/lib/utils/providers.ts +++ b/src/lib/utils/providers.ts @@ -13,25 +13,29 @@ export type ProviderApiKeyData = { export const ProviderUtils = { /** - * Validate an API key for a specific provider + * Validate an API key for a specific provider via server endpoint */ validateApiKey: async (provider: Provider, key: string): Promise> => { - switch (provider) { - case Provider.OpenRouter: - return await validateOpenRouterKey(key); - case Provider.OpenAI: - return await validateOpenAIKey(key); - case Provider.Anthropic: - return await validateAnthropicKey(key); - case Provider.Gemini: - return await validateGeminiKey(key); - case Provider.Mistral: - return await validateMistralKey(key); - case Provider.Cohere: - return await validateCohereKey(key); - default: - return Result.err(`Validation not implemented for provider: ${provider}`); - } + return await ResultAsync.fromPromise( + (async () => { + const response = await fetch('/api/validate-key', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ provider, key }), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || `HTTP ${response.status}`); + } + + const result = await response.json(); + return result.data; + })(), + (e) => `Failed to validate API key: ${e}` + ); }, /** @@ -54,159 +58,4 @@ export const ProviderUtils = { getSupportedProviders: () => { return Object.values(Provider); }, -}; - -// Provider-specific validation functions -async function validateOpenRouterKey(key: string): Promise> { - return await ResultAsync.fromPromise( - (async () => { - const res = await fetch('https://openrouter.ai/api/v1/auth/key', { - headers: { - Authorization: `Bearer ${key}`, - 'Content-Type': 'application/json', - }, - }); - - if (!res.ok) { - throw new Error(`HTTP ${res.status}: ${res.statusText}`); - } - - const { data } = await res.json(); - - if (!data) { - throw new Error('No key information returned'); - } - - return { - label: data.label || 'OpenRouter API Key', - usage: data.usage, - is_free_tier: data.is_free_tier, - is_provisioning_key: data.is_provisioning_key, - limit: data.limit, - limit_remaining: data.limit_remaining, - valid: true, - }; - })(), - (e) => `Failed to validate OpenRouter API key: ${e}` - ); -} - -async function validateOpenAIKey(key: string): Promise> { - return await ResultAsync.fromPromise( - (async () => { - const res = await fetch('https://api.openai.com/v1/models', { - headers: { - Authorization: `Bearer ${key}`, - 'Content-Type': 'application/json', - }, - }); - - if (!res.ok) { - throw new Error(`HTTP ${res.status}: ${res.statusText}`); - } - - return { - label: 'OpenAI API Key', - valid: true, - }; - })(), - (e) => `Failed to validate OpenAI API key: ${e}` - ); -} - -async function validateAnthropicKey(key: string): Promise> { - return await ResultAsync.fromPromise( - (async () => { - // Anthropic doesn't have a simple key validation endpoint - // We'll try a minimal request to test the key - const res = await fetch('https://api.anthropic.com/v1/messages', { - method: 'POST', - headers: { - 'x-api-key': key, - 'anthropic-version': '2023-06-01', - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - model: 'claude-3-haiku-20240307', - max_tokens: 1, - messages: [{ role: 'user', content: 'test' }], - }), - }); - - // Even a 400 error means the key is valid (just bad request format) - if (res.status === 401 || res.status === 403) { - throw new Error('Invalid API key'); - } - - return { - label: 'Anthropic API Key', - valid: true, - }; - })(), - (e) => `Failed to validate Anthropic API key: ${e}` - ); -} - -async function validateGeminiKey(key: string): Promise> { - return await ResultAsync.fromPromise( - (async () => { - const res = await fetch(`https://generativelanguage.googleapis.com/v1/models?key=${key}`); - - if (!res.ok) { - throw new Error(`HTTP ${res.status}: ${res.statusText}`); - } - - return { - label: 'Google Gemini API Key', - valid: true, - }; - })(), - (e) => `Failed to validate Gemini API key: ${e}` - ); -} - -async function validateMistralKey(key: string): Promise> { - return await ResultAsync.fromPromise( - (async () => { - const res = await fetch('https://api.mistral.ai/v1/models', { - headers: { - Authorization: `Bearer ${key}`, - 'Content-Type': 'application/json', - }, - }); - - if (!res.ok) { - throw new Error(`HTTP ${res.status}: ${res.statusText}`); - } - - return { - label: 'Mistral API Key', - valid: true, - }; - })(), - (e) => `Failed to validate Mistral API key: ${e}` - ); -} - -async function validateCohereKey(key: string): Promise> { - return await ResultAsync.fromPromise( - (async () => { - const res = await fetch('https://api.cohere.ai/v1/models', { - headers: { - Authorization: `Bearer ${key}`, - 'Content-Type': 'application/json', - }, - }); - - if (!res.ok) { - throw new Error(`HTTP ${res.status}: ${res.statusText}`); - } - - return { - label: 'Cohere API Key', - valid: true, - }; - })(), - (e) => `Failed to validate Cohere API key: ${e}` - ); -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts index 2370456..0d6abfa 100644 --- a/src/routes/+layout.server.ts +++ b/src/routes/+layout.server.ts @@ -1,17 +1,19 @@ -import { getOpenRouterModels, type OpenRouterModel } from '$lib/backend/models/open-router'; -import { Provider } from '$lib/types'; +import { loadUserModels, loadGuestModels } from '$lib/services/model-loader.server'; import type { LayoutServerLoad } from './$types'; export const load: LayoutServerLoad = async ({ locals }) => { - const [session, openRouterModels] = await Promise.all([locals.auth(), getOpenRouterModels()]); + const session = await locals.auth(); + + // Load models based on user's API keys + const models = session?.session?.token + ? await loadUserModels(session.session.token) + : loadGuestModels(); return { session, - models: { - [Provider.OpenRouter]: openRouterModels.unwrapOr([] as OpenRouterModel[]), - }, + models, }; }; -// Makes caching easier, and tbf, we don't need SSR anyways here +// Enable SSR for better performance export const ssr = true; diff --git a/src/routes/account/api-keys/provider-card.svelte b/src/routes/account/api-keys/provider-card.svelte index c348009..54c2619 100644 --- a/src/routes/account/api-keys/provider-card.svelte +++ b/src/routes/account/api-keys/provider-card.svelte @@ -96,7 +96,7 @@ {#if apiKeyInfoResource.loading}

{:else if apiKeyInfoResource.current} - {#if apiKeyInfoResource.current.usage !== undefined && apiKeyInfoResource.current.limit_remaining !== undefined} + {#if apiKeyInfoResource.current.usage !== undefined && apiKeyInfoResource.current.limit_remaining !== undefined && apiKeyInfoResource.current.usage !== null && apiKeyInfoResource.current.limit_remaining !== null} ${apiKeyInfoResource.current.usage.toFixed(3)} used ・ ${apiKeyInfoResource.current.limit_remaining.toFixed( 3 diff --git a/src/routes/account/models/+page.svelte b/src/routes/account/models/+page.svelte index 23dfd52..26fcfe8 100644 --- a/src/routes/account/models/+page.svelte +++ b/src/routes/account/models/+page.svelte @@ -5,36 +5,37 @@ import { Search } from '$lib/components/ui/search'; import { models } from '$lib/state/models.svelte'; import { session } from '$lib/state/session.svelte'; - import { Provider } from '$lib/types.js'; + import { page } from '$app/state'; + import { Provider, PROVIDER_META } from '$lib/types.js'; import { fuzzysearch } from '$lib/utils/fuzzy-search'; import { cn } from '$lib/utils/utils'; import { Toggle } from 'melt/builders'; import PlusIcon from '~icons/lucide/plus'; import XIcon from '~icons/lucide/x'; import ModelCard from './model-card.svelte'; - import { supportsImages, supportsReasoning } from '$lib/utils/model-capabilities'; + import type { ModelInfo } from '@keplersystems/kepler-ai-sdk'; - const openRouterKeyQuery = useCachedQuery(api.user_keys.get, { - provider: Provider.OpenRouter, + // Get all user's API keys to determine which providers are available + const userKeysQuery = useCachedQuery(api.user_keys.all, { session_token: session.current?.session.token ?? '', }); - const hasOpenRouterKey = $derived( - openRouterKeyQuery.data !== undefined && openRouterKeyQuery.data !== '' - ); + + // Show providers that have API keys, regardless of whether models are loaded yet + const availableProviders = $derived.by(() => { + if (!userKeysQuery.data) { + return []; + } + + return Object.entries(userKeysQuery.data) + .filter(([_, key]) => key) // Only providers with API keys + .map(([provider, _]) => provider as Provider); + }); let search = $state(''); + let selectedProvider = $state('all'); - const openRouterToggle = new Toggle({ - value: true, - // TODO: enable this if and when when we use multiple providers - disabled: true, - }); - - const freeModelsToggle = new Toggle({ - value: false, - }); - + // Filter toggles const reasoningModelsToggle = new Toggle({ value: false, }); @@ -43,43 +44,47 @@ value: false, }); - let initiallyEnabled = $state([]); - $effect(() => { - if (Object.keys(models.enabled).length && initiallyEnabled.length === 0) { - initiallyEnabled = models - .from(Provider.OpenRouter) - .filter((m) => m.enabled) - .map((m) => m.id); - } + const streamingModelsToggle = new Toggle({ + value: false, }); - const openRouterModels = $derived( - fuzzysearch({ - haystack: models.from(Provider.OpenRouter).filter((m) => { - if (freeModelsToggle.value) { - if (m.pricing.prompt !== '0') return false; - } + // Get models based on current filters + const filteredModels = $derived.by(() => { + let modelList = selectedProvider === 'all' + ? models.all() + : models.from(selectedProvider); - if (reasoningModelsToggle.value) { - if (!supportsReasoning(m)) return false; - } + // Apply capability filters + if (reasoningModelsToggle.value) { + modelList = modelList.filter(m => m.capabilities.reasoning); + } - if (imageModelsToggle.value) { - if (!supportsImages(m)) return false; - } + if (imageModelsToggle.value) { + modelList = modelList.filter(m => m.capabilities.vision); + } - return true; - }), - needle: search, - property: 'name', - }).sort((a, b) => { - const aEnabled = initiallyEnabled.includes(a.id); - const bEnabled = initiallyEnabled.includes(b.id); - if (aEnabled && !bEnabled) return -1; - if (!aEnabled && bEnabled) return 1; - return 0; - }) - ); + if (streamingModelsToggle.value) { + modelList = modelList.filter(m => m.capabilities.streaming); + } + + // Apply text search + if (search) { + modelList = fuzzysearch({ + haystack: modelList, + needle: search, + property: 'name', + }); + } + + // Sort: enabled first, then by name + return modelList.sort((a, b) => { + if (a.enabled && !b.enabled) return -1; + if (!a.enabled && b.enabled) return 1; + return a.name.localeCompare(b.name); + }); + }); + + const hasAnyApiKeys = $derived(availableProviders.length > 0); @@ -91,76 +96,125 @@ Choose which models appear in your model selector. This won't affect existing conversations. -
- -
- - - - +{#if !hasAnyApiKeys} +
+

No API Keys Configured

+

+ You need to add API keys for at least one provider to see and manage models. + Go to API Keys Settings +

-
+{:else} +
+ + -{#if openRouterModels.length > 0} -
-
-

OpenRouter

-

Easy access to over 400 models.

-
-
-
+
+ +
+ + + {#each availableProviders as provider} + {@const providerMeta = PROVIDER_META[provider]} + {@const providerModels = models.from(provider)} + {@const hasModels = models.hasProvider(provider)} + {#if providerMeta} + + {/if} + {/each} +
+ + +
+ + + + + + +
+ + + {#if filteredModels.length === 0} +
+ {#if selectedProvider !== 'all' && !models.hasProvider(selectedProvider)} +

Loading models...

+

+ Models are being loaded from {PROVIDER_META[selectedProvider]?.title || selectedProvider}. Please refresh the page in a moment. +

+ {:else} +

No models found

+

+ {#if search} + Try adjusting your search or filters. + {:else} + No models match your current filters. + {/if} +

+ {/if} +
+ {:else} +
+ {#each filteredModels as model (model.id)} {/each}
- {#if !hasOpenRouterKey} -
- -
- {/if} -
+ {/if}
-{/if} +{/if} \ No newline at end of file diff --git a/src/routes/account/models/model-card.svelte b/src/routes/account/models/model-card.svelte index 2ad59ef..f1337d0 100644 --- a/src/routes/account/models/model-card.svelte +++ b/src/routes/account/models/model-card.svelte @@ -1,5 +1,6 @@
-
- {model.name} - +
+
+ {model.name} + + {providerMeta.title} + +
+ {model.id}
- enabled, toggleEnabled} {disabled} /> +
- - {showMore ? fullDescription : (shortDescription ?? fullDescription)} - - {#if shortDescription !== null} - + + {#if model.description} + + {showMore ? fullDescription : (shortDescription ?? fullDescription)} + + {#if shortDescription !== null} + + {/if} {/if} - -
- {#if model && provider === 'openrouter' && supportsImages(model)} - - {#snippet trigger(tooltip)} -
- -
- {/snippet} - Supports image analysis -
- {/if} - {#if model && provider === 'openrouter' && supportsReasoning(model)} - - {#snippet trigger(tooltip)} -
- -
- {/snippet} - Supports reasoning -
- {/if} + +
+ +
+ {#if model.capabilities.vision} + + {#snippet trigger(tooltip)} +
+ +
+ {/snippet} + Supports vision/image analysis +
+ {/if} + + {#if model.capabilities.reasoning} + + {#snippet trigger(tooltip)} +
+ +
+ {/snippet} + Supports reasoning +
+ {/if} + + {#if model.capabilities.streaming} + + {#snippet trigger(tooltip)} +
+ +
+ {/snippet} + Supports streaming responses +
+ {/if} + + {#if model.capabilities.toolCalls} + + {#snippet trigger(tooltip)} +
+ +
+ {/snippet} + Supports tool/function calling +
+ {/if} +
+ + +
+ {#if pricingInfo} + {pricingInfo} + {/if} + {contextInfo} +
- + \ No newline at end of file diff --git a/src/routes/api/enhance-prompt/+server.ts b/src/routes/api/enhance-prompt/+server.ts index 8e55212..b57a4ba 100644 --- a/src/routes/api/enhance-prompt/+server.ts +++ b/src/routes/api/enhance-prompt/+server.ts @@ -44,7 +44,7 @@ async function getUserApiKeys(sessionToken: string): Promise return { openai: keys.openai, anthropic: keys.anthropic, - gemini: keys.gemini, + gemini: keys.google, mistral: keys.mistral, cohere: keys.cohere, openrouter: keys.openrouter, diff --git a/src/routes/api/generate-message/+server.ts b/src/routes/api/generate-message/+server.ts index 3d3737e..90cb4ff 100644 --- a/src/routes/api/generate-message/+server.ts +++ b/src/routes/api/generate-message/+server.ts @@ -81,7 +81,7 @@ async function getUserApiKeys(sessionToken: string): Promise model.id.includes('kimi-k2')) || availableModels.find((model) => model.id.includes('gemini-2.5-flash-lite')) || + availableModels.find((model) => model.id.includes('kimi-k2')) || availableModels.find((model) => model.id.includes('gpt-5-mini')) || availableModels[0]; @@ -396,21 +396,19 @@ ${attachedRules.map((r) => `- ${r.name}: ${r.rule}`).join('\n')}`, return; } - // Generate completion with streaming - const streamResult = await ResultAsync.fromPromise( - provider.generateCompletion({ + // Generate streaming completion + let stream: AsyncIterable; + try { + stream = provider.streamCompletion({ model: finalModelId, messages: messagesToSend, temperature: 0.7, - stream: true, ...(reasoningEffort && { reasoning_effort: reasoningEffort }), - }), - (e) => `API call failed: ${e}` - ); - - if (streamResult.isErr()) { + }); + log('Background: Stream created successfully', startTime); + } catch (error) { handleGenerationError({ - error: `Failed to create stream: ${streamResult.error}`, + error: `Failed to create stream: API call failed: ${error}`, conversationId, messageId: mid, sessionToken, @@ -419,9 +417,6 @@ ${attachedRules.map((r) => `- ${r.name}: ${r.rule}`).join('\n')}`, return; } - const stream = streamResult.value; - log('Background: Stream created successfully', startTime); - let content = ''; let reasoning = ''; let chunkCount = 0; @@ -430,77 +425,48 @@ ${attachedRules.map((r) => `- ${r.name}: ${r.rule}`).join('\n')}`, try { // Handle streaming response - if (stream && typeof stream[Symbol.asyncIterator] === 'function') { - for await (const chunk of stream) { - if (abortSignal?.aborted) { - log('AI response generation aborted during streaming', startTime); - break; - } + for await (const chunk of stream) { + if (abortSignal?.aborted) { + log('AI response generation aborted during streaming', startTime); + break; + } - chunkCount++; + chunkCount++; - // Extract content from chunk based on the stream format - if (chunk && typeof chunk === 'object') { - const chunkContent = chunk.content || chunk.text || ''; - const chunkReasoning = chunk.reasoning || ''; - const chunkAnnotations = chunk.annotations || []; + // Extract content from chunk based on the kepler-ai-sdk format + if (chunk && typeof chunk === 'object') { + const chunkContent = chunk.delta || chunk.content || chunk.text || ''; + const chunkReasoning = chunk.reasoning || ''; + const chunkAnnotations = chunk.annotations || []; - reasoning += chunkReasoning; - content += chunkContent; - annotations.push(...chunkAnnotations); + reasoning += chunkReasoning; + content += chunkContent; + annotations.push(...chunkAnnotations); - if (!content && !reasoning) continue; + if (!chunkContent && !chunkReasoning) continue; - generationId = chunk.id || generationId; + generationId = chunk.id || generationId; - const updateResult = await ResultAsync.fromPromise( - client.mutation(api.messages.updateContent, { - message_id: mid, - content, - reasoning: reasoning.length > 0 ? reasoning : undefined, - session_token: sessionToken, - generation_id: generationId, - annotations, - reasoning_effort: reasoningEffort, - }), - (e) => `Failed to update message content: ${e}` + const updateResult = await ResultAsync.fromPromise( + client.mutation(api.messages.updateContent, { + message_id: mid, + content, + reasoning: reasoning.length > 0 ? reasoning : undefined, + session_token: sessionToken, + generation_id: generationId, + annotations, + reasoning_effort: reasoningEffort, + }), + (e) => `Failed to update message content: ${e}` + ); + + if (updateResult.isErr()) { + log( + `Background message update failed on chunk ${chunkCount}: ${updateResult.error}`, + startTime ); - - if (updateResult.isErr()) { - log( - `Background message update failed on chunk ${chunkCount}: ${updateResult.error}`, - startTime - ); - } } } - } else { - // Handle non-streaming response - const response = stream as any; - content = response.content || response.text || ''; - reasoning = response.reasoning || ''; - generationId = response.id; - - if (response.annotations) { - annotations.push(...response.annotations); - } - - const updateResult = await ResultAsync.fromPromise( - client.mutation(api.messages.updateContent, { - message_id: mid, - content, - reasoning: reasoning.length > 0 ? reasoning : undefined, - session_token: sessionToken, - generation_id: generationId, - annotations, - reasoning_effort: reasoningEffort, - }), - (e) => `Failed to update message content: ${e}` - ); - - if (updateResult.isErr()) { - log(`Background message update failed: ${updateResult.error}`, startTime); - } } log( diff --git a/src/routes/api/validate-key/+server.ts b/src/routes/api/validate-key/+server.ts new file mode 100644 index 0000000..32df948 --- /dev/null +++ b/src/routes/api/validate-key/+server.ts @@ -0,0 +1,218 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { Provider } from '$lib/types'; +import { Result, ResultAsync } from 'neverthrow'; + +export type ProviderApiKeyData = { + label: string; + usage?: number; + is_free_tier?: boolean; + is_provisioning_key?: boolean; + limit?: number; + limit_remaining?: number; + valid: boolean; +}; + +export const POST: RequestHandler = async ({ request, locals }) => { + const session = await locals.auth(); + if (!session) { + return json({ error: 'Unauthorized' }, { status: 401 }); + } + + try { + const { provider, key } = await request.json(); + + if (!provider || !key) { + return json({ error: 'Missing provider or key' }, { status: 400 }); + } + + if (!Object.values(Provider).includes(provider)) { + return json({ error: 'Invalid provider' }, { status: 400 }); + } + + const result = await validateApiKey(provider, key); + + if (result.isErr()) { + return json({ error: result.error }, { status: 400 }); + } + + return json({ data: result.value }); + } catch (error) { + console.error('API key validation error:', error); + return json({ error: 'Internal server error' }, { status: 500 }); + } +}; + +async function validateApiKey(provider: Provider, key: string): Promise> { + switch (provider) { + case Provider.OpenRouter: + return await validateOpenRouterKey(key); + case Provider.OpenAI: + return await validateOpenAIKey(key); + case Provider.Anthropic: + return await validateAnthropicKey(key); + case Provider.Gemini: + return await validateGeminiKey(key); + case Provider.Mistral: + return await validateMistralKey(key); + case Provider.Cohere: + return await validateCohereKey(key); + default: + return Result.err(`Validation not implemented for provider: ${provider}`); + } +} + +// Provider-specific validation functions +async function validateOpenRouterKey(key: string): Promise> { + return await ResultAsync.fromPromise( + (async () => { + const res = await fetch('https://openrouter.ai/api/v1/auth/key', { + headers: { + Authorization: `Bearer ${key}`, + 'Content-Type': 'application/json', + }, + }); + + if (!res.ok) { + throw new Error(`HTTP ${res.status}: ${res.statusText}`); + } + + const { data } = await res.json(); + + if (!data) { + throw new Error('No key information returned'); + } + + return { + label: data.label || 'OpenRouter API Key', + usage: data.usage, + is_free_tier: data.is_free_tier, + is_provisioning_key: data.is_provisioning_key, + limit: data.limit, + limit_remaining: data.limit_remaining, + valid: true, + }; + })(), + (e) => `Failed to validate OpenRouter API key: ${e}` + ); +} + +async function validateOpenAIKey(key: string): Promise> { + return await ResultAsync.fromPromise( + (async () => { + const res = await fetch('https://api.openai.com/v1/models', { + headers: { + Authorization: `Bearer ${key}`, + 'Content-Type': 'application/json', + }, + }); + + if (!res.ok) { + throw new Error(`HTTP ${res.status}: ${res.statusText}`); + } + + return { + label: 'OpenAI API Key', + valid: true, + }; + })(), + (e) => `Failed to validate OpenAI API key: ${e}` + ); +} + +async function validateAnthropicKey(key: string): Promise> { + return await ResultAsync.fromPromise( + (async () => { + // Anthropic doesn't have a simple key validation endpoint + // We'll try a minimal request to test the key + const res = await fetch('https://api.anthropic.com/v1/messages', { + method: 'POST', + headers: { + 'x-api-key': key, + 'anthropic-version': '2023-06-01', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + model: 'claude-3-haiku-20240307', + max_tokens: 1, + messages: [{ role: 'user', content: 'test' }], + }), + }); + + // Even a 400 error means the key is valid (just bad request format) + if (res.status === 401 || res.status === 403) { + throw new Error('Invalid API key'); + } + + return { + label: 'Anthropic API Key', + valid: true, + }; + })(), + (e) => `Failed to validate Anthropic API key: ${e}` + ); +} + +async function validateGeminiKey(key: string): Promise> { + return await ResultAsync.fromPromise( + (async () => { + const res = await fetch(`https://generativelanguage.googleapis.com/v1/models?key=${key}`); + + if (!res.ok) { + throw new Error(`HTTP ${res.status}: ${res.statusText}`); + } + + return { + label: 'Google Gemini API Key', + valid: true, + }; + })(), + (e) => `Failed to validate Gemini API key: ${e}` + ); +} + +async function validateMistralKey(key: string): Promise> { + return await ResultAsync.fromPromise( + (async () => { + const res = await fetch('https://api.mistral.ai/v1/models', { + headers: { + Authorization: `Bearer ${key}`, + 'Content-Type': 'application/json', + }, + }); + + if (!res.ok) { + throw new Error(`HTTP ${res.status}: ${res.statusText}`); + } + + return { + label: 'Mistral API Key', + valid: true, + }; + })(), + (e) => `Failed to validate Mistral API key: ${e}` + ); +} + +async function validateCohereKey(key: string): Promise> { + return await ResultAsync.fromPromise( + (async () => { + const res = await fetch('https://api.cohere.ai/v1/models', { + headers: { + Authorization: `Bearer ${key}`, + 'Content-Type': 'application/json', + }, + }); + + if (!res.ok) { + throw new Error(`HTTP ${res.status}: ${res.statusText}`); + } + + return { + label: 'Cohere API Key', + valid: true, + }; + })(), + (e) => `Failed to validate Cohere API key: ${e}` + ); +} \ No newline at end of file diff --git a/src/routes/chat/+layout.svelte b/src/routes/chat/+layout.svelte index 94a7a23..be00c11 100644 --- a/src/routes/chat/+layout.svelte +++ b/src/routes/chat/+layout.svelte @@ -213,15 +213,15 @@ const currentModelSupportsImages = $derived.by(() => { if (!settings.modelId) return false; - const openRouterModels = models.from(Provider.OpenRouter); - const currentModel = openRouterModels.find((m) => m.id === settings.modelId); + const allModels = models.all(); + const currentModel = allModels.find((m) => m.id === settings.modelId); return currentModel ? supportsImages(currentModel) : false; }); const currentModelSupportsReasoning = $derived.by(() => { if (!settings.modelId) return false; - const openRouterModels = models.from(Provider.OpenRouter); - const currentModel = openRouterModels.find((m) => m.id === settings.modelId); + const allModels = models.all(); + const currentModel = allModels.find((m) => m.id === settings.modelId); if (!currentModel) return false; return supportsReasoning(currentModel); }); diff --git a/src/routes/chat/+page.svelte b/src/routes/chat/+page.svelte index e643da6..19b589c 100644 --- a/src/routes/chat/+page.svelte +++ b/src/routes/chat/+page.svelte @@ -63,8 +63,7 @@ let selectedCategory = $state(null); - const openRouterKeyQuery = useCachedQuery(api.user_keys.get, { - provider: Provider.OpenRouter, + const userKeysQuery = useCachedQuery(api.user_keys.all, { session_token: session.current?.session.token ?? '', }); @@ -76,7 +75,7 @@
- {#if prompt.current.length === 0 && openRouterKeyQuery.data} + {#if prompt.current.length === 0 && userKeysQuery.data && Object.values(userKeysQuery.data).some(key => key !== null)}

Hey there

- {:else if !openRouterKeyQuery.data && !openRouterKeyQuery.isLoading} + {:else if userKeysQuery.data && !Object.values(userKeysQuery.data).some(key => key !== null) && !userKeysQuery.isLoading}

Hey there,