diff --git a/bun.lock b/bun.lock index 6c75e29b1c2..d2e1fd70236 100644 --- a/bun.lock +++ b/bun.lock @@ -185,13 +185,26 @@ "name": "kilo-code", "version": "1.0.19", "dependencies": { + "@anthropic-ai/sdk": "^0.39.0", "@kilocode/kilo-i18n": "workspace:*", "@kilocode/kilo-ui": "workspace:*", "@kilocode/sdk": "workspace:*", + "diff": "^7.0.0", + "dotenv": "^16.4.7", "eventsource": "^2.0.2", + "fastest-levenshtein": "^1.0.16", + "ignore": "^7.0.3", + "js-tiktoken": "^1.0.18", + "lru-cache": "^11.0.2", + "openai": "^4.85.4", + "quick-lru": "^7.0.0", "solid-js": "^1.9.11", + "uri-js": "^4.4.1", + "web-tree-sitter": "^0.24.7", + "zod": "^3.24.2", }, "devDependencies": { + "@types/diff": "^6.0.0", "@types/mocha": "^10.0.10", "@types/node": "22.x", "@types/qrcode": "^1.5.6", @@ -520,7 +533,7 @@ "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], - "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.71.2", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-TGNDEUuEstk/DKu0/TflXAEt+p+p/WhTlFzEnoosvbaDU2LTjm42igSdlL0VijrKpWejtOKxX0b8A7uc+XiSAQ=="], + "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.39.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" } }, "sha512-eMyDIPRZbt1CCLErRCi3exlAvNkBtRe+kW5vvJyef93PmNr/clstYgHhtvmkxN82nlKgzyGPCyGxrm0JQ1ZIdg=="], "@anycable/core": ["@anycable/core@0.9.2", "", { "dependencies": { "nanoevents": "^7.0.1" } }, "sha512-x5ZXDcW/N4cxWl93CnbHs/u7qq4793jS2kNPWm+duPrXlrva+ml2ZGT7X9tuOBKzyIHf60zWCdIK7TUgMPAwXA=="], @@ -1390,6 +1403,8 @@ "@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], + "@types/diff": ["@types/diff@6.0.0", "", {}, "sha512-dhVCYGv3ZSbzmQaBSagrv1WJ6rXCdkyTcDyoNu1MD8JohI7pR7k8wdZEm+mvdxRKXyHVwckFzWU1vJc+Z29MlA=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], @@ -1494,6 +1509,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=="], + "ai": ["ai@5.0.124", "", { "dependencies": { "@ai-sdk/gateway": "2.0.30", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Li6Jw9F9qsvFJXZPBfxj38ddP2iURCnMs96f9Q3OeQzrDVcl1hvtwSEAuxA/qmfh6SDV2ERqFUOFzigvr0697g=="], "ai-gateway-provider": ["ai-gateway-provider@2.3.1", "", { "dependencies": { "@ai-sdk/provider": "^2.0.0", "@ai-sdk/provider-utils": "^3.0.19", "ai": "^5.0.116" }, "optionalDependencies": { "@ai-sdk/amazon-bedrock": "^3.0.71", "@ai-sdk/anthropic": "^2.0.56", "@ai-sdk/azure": "^2.0.90", "@ai-sdk/cerebras": "^1.0.33", "@ai-sdk/cohere": "^2.0.21", "@ai-sdk/deepgram": "^1.0.21", "@ai-sdk/deepseek": "^1.0.32", "@ai-sdk/elevenlabs": "^1.0.21", "@ai-sdk/fireworks": "^1.0.30", "@ai-sdk/google": "^2.0.51", "@ai-sdk/google-vertex": "3.0.90", "@ai-sdk/groq": "^2.0.33", "@ai-sdk/mistral": "^2.0.26", "@ai-sdk/openai": "^2.0.88", "@ai-sdk/perplexity": "^2.0.22", "@ai-sdk/xai": "^2.0.42", "@openrouter/ai-sdk-provider": "^1.5.3" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^1.0.29" } }, "sha512-PqI6TVNEDNwr7kOhy7XUGnA8XJB1SpeA9aLqGjr0CyWkKgH+y+ofPm8MZGZ74DOwVejDF+POZq0Qs9jKEKUeYg=="], @@ -1764,7 +1781,7 @@ "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], - "dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="], + "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], "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=="], @@ -1878,6 +1895,8 @@ "fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], + "fastest-levenshtein": ["fastest-levenshtein@1.0.16", "", {}, "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg=="], + "fastify": ["fastify@5.7.4", "", { "dependencies": { "@fastify/ajv-compiler": "^4.0.5", "@fastify/error": "^4.0.0", "@fastify/fast-json-stringify-compiler": "^5.0.0", "@fastify/proxy-addr": "^5.0.0", "abstract-logging": "^2.0.1", "avvio": "^9.0.0", "fast-json-stringify": "^6.0.0", "find-my-way": "^9.0.0", "light-my-request": "^6.0.0", "pino": "^10.1.0", "process-warning": "^5.0.0", "rfdc": "^1.3.1", "secure-json-parse": "^4.0.0", "semver": "^7.6.0", "toad-cache": "^3.7.0" } }, "sha512-e6l5NsRdaEP8rdD8VR0ErJASeyaRbzXYpmkrpr2SuvuMq6Si3lvsaVy5C+7gLanEkvjpMDzBXWE5HPeb/hgTxA=="], "fastify-plugin": ["fastify-plugin@5.1.0", "", {}, "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw=="], @@ -1916,6 +1935,10 @@ "form-data": ["form-data@4.0.5", "", { "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-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], + "form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="], + + "formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="], + "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], @@ -2026,6 +2049,8 @@ "human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="], + "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], + "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], @@ -2156,6 +2181,8 @@ "jpeg-js": ["jpeg-js@0.4.4", "", {}, "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg=="], + "js-tiktoken": ["js-tiktoken@1.0.21", "", { "dependencies": { "base64-js": "^1.5.1" } }, "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], @@ -2242,7 +2269,7 @@ "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], - "lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + "lru-cache": ["lru-cache@11.2.5", "", {}, "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw=="], "lru_map": ["lru_map@0.4.1", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="], @@ -2380,7 +2407,7 @@ "open": ["open@10.1.2", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "is-wsl": "^3.1.0" } }, "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw=="], - "openai": ["openai@6.18.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-odLRYyz9rlzz6g8gKn61RM2oP5UUm428sE2zOxZqS9MzVfD5/XW8UoEjpnRkzTuScXP7ZbP/m7fC+bl8jCOZZw=="], + "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=="], "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], @@ -2494,7 +2521,7 @@ "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], - "punycode": ["punycode@1.3.2", "", {}, "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw=="], + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="], @@ -2506,6 +2533,8 @@ "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], + "quick-lru": ["quick-lru@7.3.0", "", {}, "sha512-k9lSsjl36EJdK7I06v7APZCbyGT2vMTsYSRX1Q2nbYmnkBqgUhRkAuzH08Ciotteu/PLJmIF2+tti7o3C/ts2g=="], + "randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="], "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], @@ -2890,7 +2919,7 @@ "vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="], - "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + "web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="], "web-tree-sitter": ["web-tree-sitter@0.25.10", "", { "peerDependencies": { "@types/emscripten": "^1.40.0" }, "optionalPeers": ["@types/emscripten"] }, "sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA=="], @@ -3008,6 +3037,10 @@ "@eslint/eslintrc/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "@gitlab/gitlab-ai-provider/@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.71.2", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-TGNDEUuEstk/DKu0/TflXAEt+p+p/WhTlFzEnoosvbaDU2LTjm42igSdlL0VijrKpWejtOKxX0b8A7uc+XiSAQ=="], + + "@gitlab/gitlab-ai-provider/openai": ["openai@6.18.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-odLRYyz9rlzz6g8gKn61RM2oP5UUm428sE2zOxZqS9MzVfD5/XW8UoEjpnRkzTuScXP7ZbP/m7fC+bl8jCOZZw=="], + "@gitlab/gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "@hey-api/json-schema-ref-parser/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], @@ -3178,6 +3211,8 @@ "c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], + "c12/dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="], + "c8/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=="], "c8/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], @@ -3194,6 +3229,8 @@ "eslint/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "fetch-blob/web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "gaxios/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=="], @@ -3208,12 +3245,18 @@ "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], + "kilo-code/diff": ["diff@7.0.0", "", {}, "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw=="], + "kilo-code/prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], "kilo-code/solid-js": ["solid-js@1.9.11", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-WEJtcc5mkh/BnHA6Yrg4whlF8g6QwpmXXRg4P2ztPmcKeHHlH4+djYecBLhSpecZY2RRECXYUwIc/C2r3yzQ4Q=="], "kilo-code/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "kilo-code/web-tree-sitter": ["web-tree-sitter@0.24.7", "", {}, "sha512-CdC/TqVFbXqR+C51v38hv6wOPatKEUGxa39scAeFSm98wIhZxAYonhRQPSMmfZ2w7JDI0zQDdzdmgtNk06/krQ=="], + + "kilo-code/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], "light-my-request/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], @@ -3268,6 +3311,8 @@ "openid-client/jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="], + "openid-client/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + "ora/cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], "ora/log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="], @@ -3276,8 +3321,6 @@ "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], - "path-scurry/lru-cache": ["lru-cache@11.2.5", "", {}, "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw=="], - "pixelmatch/pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="], "pkg-up/find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="], @@ -3324,7 +3367,7 @@ "tree-sitter-bash/node-addon-api": ["node-addon-api@8.5.0", "", {}, "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A=="], - "uri-js/punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "url/punycode": ["punycode@1.3.2", "", {}, "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw=="], "vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], diff --git a/packages/kilo-gateway/src/server/routes.ts b/packages/kilo-gateway/src/server/routes.ts index d8811e851e3..0d5b7d371d4 100644 --- a/packages/kilo-gateway/src/server/routes.ts +++ b/packages/kilo-gateway/src/server/routes.ts @@ -8,6 +8,7 @@ import { fetchProfile, fetchBalance } from "../api/profile.js" import { fetchKilocodeNotifications, KilocodeNotificationSchema } from "../api/notifications.js" +import { KILO_API_BASE } from "../api/constants.js" // Type definitions for OpenCode dependencies (injected at runtime) type Hono = any @@ -162,6 +163,85 @@ export function createKiloRoutes(deps: KiloRoutesDeps) { return c.json(true) }, ) + .post( + "/fim", + describeRoute({ + summary: "FIM completion", + description: "Proxy a Fill-in-the-Middle completion request to the Kilo Gateway", + operationId: "kilo.fim", + responses: { + 200: { + description: "Streaming FIM completion response", + content: { + "text/event-stream": { + schema: resolver(z.any()), + }, + }, + }, + ...errors(400, 401), + }, + }), + validator( + "json", + z.object({ + prefix: z.string(), + suffix: z.string(), + model: z.string().optional(), + maxTokens: z.number().optional(), + temperature: z.number().optional(), + }), + ), + async (c: any) => { + const auth = await Auth.get("kilo") + + if (!auth) { + return c.json({ error: "Not authenticated with Kilo Gateway" }, 401) + } + + const token = auth.type === "api" ? auth.key : auth.type === "oauth" ? auth.access : undefined + if (!token) { + return c.json({ error: "No valid token found" }, 401) + } + + const { prefix, suffix, model, maxTokens, temperature } = c.req.valid("json") + const fimModel = model ?? "mistralai/codestral-2501" + const fimMaxTokens = maxTokens ?? 256 + const fimTemperature = temperature ?? 0.2 + + const baseApiUrl = KILO_API_BASE + "/api/" + const endpoint = new URL("fim/completions", baseApiUrl) + + const response = await fetch(endpoint, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + model: fimModel, + prompt: prefix, + suffix, + max_tokens: fimMaxTokens, + temperature: fimTemperature, + stream: true, + }), + }) + + if (!response.ok) { + const errorText = await response.text() + return c.json({ error: `FIM request failed: ${response.status} ${errorText}` }, response.status as any) + } + + // Stream the response through + return new Response(response.body, { + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }, + }) + }, + ) .get( "/notifications", describeRoute({ diff --git a/packages/kilo-vscode/package.json b/packages/kilo-vscode/package.json index 46a04840704..95e6ed686cc 100644 --- a/packages/kilo-vscode/package.json +++ b/packages/kilo-vscode/package.json @@ -68,6 +68,11 @@ "light": "assets/icons/kilo-light.svg", "dark": "assets/icons/kilo-dark.svg" } + }, + { + "command": "kilo-code.new.autocomplete.generateSuggestions", + "title": "Generate Suggested Edits", + "category": "Kilo Code" } ], "menus": { @@ -167,6 +172,21 @@ "default": "kilo/auto", "description": "Default model ID for new sessions" }, + "kilo-code.new.autocomplete.enableAutoTrigger": { + "type": "boolean", + "default": true, + "description": "Enable automatic inline completion suggestions" + }, + "kilo-code.new.autocomplete.enableSmartInlineTaskKeybinding": { + "type": "boolean", + "default": false, + "description": "Enable smart inline task keybinding" + }, + "kilo-code.new.autocomplete.enableChatAutocomplete": { + "type": "boolean", + "default": false, + "description": "Enable chat textarea autocomplete" + }, "kilo-code.new.browserAutomation.enabled": { "type": "boolean", "default": false, @@ -245,6 +265,7 @@ "test": "vscode-test" }, "devDependencies": { + "@types/diff": "^6.0.0", "@types/mocha": "^10.0.10", "@types/node": "22.x", "@types/qrcode": "^1.5.6", @@ -262,10 +283,22 @@ "typescript-eslint": "^8.54.0" }, "dependencies": { + "@anthropic-ai/sdk": "^0.39.0", "@kilocode/kilo-i18n": "workspace:*", "@kilocode/kilo-ui": "workspace:*", "@kilocode/sdk": "workspace:*", + "diff": "^7.0.0", + "dotenv": "^16.4.7", "eventsource": "^2.0.2", - "solid-js": "^1.9.11" + "fastest-levenshtein": "^1.0.16", + "ignore": "^7.0.3", + "js-tiktoken": "^1.0.18", + "lru-cache": "^11.0.2", + "quick-lru": "^7.0.0", + "openai": "^4.85.4", + "solid-js": "^1.9.11", + "uri-js": "^4.4.1", + "web-tree-sitter": "^0.24.7", + "zod": "^3.24.2" } } diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 71fb997b592..eec796bd4e6 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -1,5 +1,7 @@ import * as vscode from "vscode" import { type HttpClient, type SessionInfo, type SSEEvent, type KiloConnectionService } from "./services/cli-backend" +import { handleChatCompletionRequest } from "./services/autocomplete/chat-autocomplete/handleChatCompletionRequest" +import { handleChatCompletionAccepted } from "./services/autocomplete/chat-autocomplete/handleChatCompletionAccepted" export class KiloProvider implements vscode.WebviewViewProvider { public static readonly viewType = "kilo-code.new.sidebarView" @@ -222,6 +224,33 @@ export class KiloProvider implements vscode.WebviewViewProvider { .getConfiguration("kilo-code.new") .update("language", message.locale || undefined, vscode.ConfigurationTarget.Global) break + case "requestAutocompleteSettings": + this.sendAutocompleteSettings() + break + case "updateAutocompleteSetting": { + const allowedKeys = new Set([ + "enableAutoTrigger", + "enableSmartInlineTaskKeybinding", + "enableChatAutocomplete", + ]) + if (allowedKeys.has(message.key)) { + await vscode.workspace + .getConfiguration("kilo-code.new.autocomplete") + .update(message.key, message.value, vscode.ConfigurationTarget.Global) + this.sendAutocompleteSettings() + } + break + } + case "requestChatCompletion": + void handleChatCompletionRequest( + { type: "requestChatCompletion", text: message.text, requestId: message.requestId }, + { postMessage: (msg) => this.postMessage(msg) }, + this.connectionService, + ) + break + case "chatCompletionAccepted": + handleChatCompletionAccepted({ type: "chatCompletionAccepted", suggestionLength: message.suggestionLength }) + break case "deleteSession": await this.handleDeleteSession(message.sessionID) break @@ -1112,6 +1141,21 @@ export class KiloProvider implements vscode.WebviewViewProvider { } } + /** + * Read autocomplete settings from VS Code configuration and push to the webview. + */ + private sendAutocompleteSettings(): void { + const config = vscode.workspace.getConfiguration("kilo-code.new.autocomplete") + this.postMessage({ + type: "autocompleteSettingsLoaded", + settings: { + enableAutoTrigger: config.get("enableAutoTrigger", true), + enableSmartInlineTaskKeybinding: config.get("enableSmartInlineTaskKeybinding", false), + enableChatAutocomplete: config.get("enableChatAutocomplete", false), + }, + }) + } + /** * Post a message to the webview. * Public so toolbar button commands can send messages. diff --git a/packages/kilo-vscode/src/extension.ts b/packages/kilo-vscode/src/extension.ts index a2e23a764de..ad4e4ac4af8 100644 --- a/packages/kilo-vscode/src/extension.ts +++ b/packages/kilo-vscode/src/extension.ts @@ -3,6 +3,7 @@ import { KiloProvider } from "./KiloProvider" import { AgentManagerProvider } from "./AgentManagerProvider" import { EXTENSION_DISPLAY_NAME } from "./constants" import { KiloConnectionService } from "./services/cli-backend" +import { registerAutocompleteProvider } from "./services/autocomplete" import { BrowserAutomationService } from "./services/browser-automation" export function activate(context: vscode.ExtensionContext) { @@ -57,6 +58,9 @@ export function activate(context: vscode.ExtensionContext) { }), ) + // Register autocomplete provider + registerAutocompleteProvider(context, connectionService) + // Dispose services when extension deactivates (kills the server) context.subscriptions.push({ dispose: () => { diff --git a/packages/kilo-vscode/src/services/autocomplete/AutocompleteCodeActionProvider.ts b/packages/kilo-vscode/src/services/autocomplete/AutocompleteCodeActionProvider.ts index c7ff68762d2..fde8fedea09 100644 --- a/packages/kilo-vscode/src/services/autocomplete/AutocompleteCodeActionProvider.ts +++ b/packages/kilo-vscode/src/services/autocomplete/AutocompleteCodeActionProvider.ts @@ -1,5 +1,5 @@ import * as vscode from "vscode" -import { t } from "../../i18n" +import { t } from "./shims/i18n" export class AutocompleteCodeActionProvider implements vscode.CodeActionProvider { public readonly providedCodeActionKinds = { @@ -17,7 +17,7 @@ export class AutocompleteCodeActionProvider implements vscode.CodeActionProvider this.providedCodeActionKinds["quickfix"], ) action.command = { - command: "kilo-code.autocomplete.generateSuggestions", + command: "kilo-code.new.autocomplete.generateSuggestions", title: "", arguments: [document.uri, range], } diff --git a/packages/kilo-vscode/src/services/autocomplete/AutocompleteJetbrainsBridge.ts b/packages/kilo-vscode/src/services/autocomplete/AutocompleteJetbrainsBridge.ts deleted file mode 100644 index 0414f66de2c..00000000000 --- a/packages/kilo-vscode/src/services/autocomplete/AutocompleteJetbrainsBridge.ts +++ /dev/null @@ -1,291 +0,0 @@ -// kilocode_change - new file -import * as vscode from "vscode" -import { z } from "zod" -import { AutocompleteServiceManager } from "./AutocompleteServiceManager" -import { ClineProvider } from "../../core/webview/ClineProvider" -import { getKiloCodeWrapperProperties } from "../../core/kilocode/wrapper" -import { languageForFilepath } from "./continuedev/core/autocomplete/constants/AutocompleteLanguageInfo" -import { AutocompleteContextProvider } from "./types" -import { FimPromptBuilder } from "./classic-auto-complete/FillInTheMiddle" -import { HoleFiller } from "./classic-auto-complete/HoleFiller" -import { MockTextDocument } from "../mocking/MockTextDocument" - -const GET_INLINE_COMPLETIONS_COMMAND = "kilo-code.jetbrains.getInlineCompletions" - -// Zod schemas for validation -const PositionSchema = z.object({ - line: z.number().int().nonnegative(), - character: z.number().int().nonnegative(), -}) - -const InlineCompletionArgsSchema = z.tuple([ - z.union([z.string(), z.any()]).transform((val) => String(val)), // documentUri - coerce to string - z.union([PositionSchema, z.any()]), // position (can be object or any) - z.union([z.string(), z.any()]).transform((val) => String(val)), // fileContent - coerce to string - z.union([z.string(), z.any()]).transform((val) => String(val)), // languageId - coerce to string - z.union([z.string(), z.any()]).transform((val) => String(val)), // requestId - coerce to string -]) - -type InlineCompletionArgs = z.infer - -interface DocumentParams { - uri: string - position: { line: number; character: number } - content: string - languageId: string - requestId: string -} - -interface NormalizedContent { - normalizedContent: string - lines: string[] -} - -interface CompletionResult { - requestId: string - items: Array<{ - insertText: string - range: { - start: { line: number; character: number } - end: { line: number; character: number } - } | null - }> - error: string | null -} - -export class AutocompleteJetbrainsBridge { - private autocomplete: AutocompleteServiceManager - - constructor(autocomplete: AutocompleteServiceManager) { - this.autocomplete = autocomplete - } - - private determineLanguage(langId: string, uri: string): string { - // If we have a valid language ID that's not generic, use it - if (langId && langId !== "text" && langId !== "textmate") { - return langId - } - - // Use the languageForFilepath function to get language info from file extension - const languageInfo = languageForFilepath(uri) - const languageName = languageInfo.name.toLowerCase() - - // Map language names to VSCode language IDs - const languageIdMap: { [key: string]: string } = { - typescript: "typescript", - javascript: "javascript", - python: "python", - java: "java", - "c++": "cpp", - "c#": "csharp", - c: "c", - scala: "scala", - go: "go", - rust: "rust", - haskell: "haskell", - php: "php", - ruby: "ruby", - "ruby on rails": "ruby", - swift: "swift", - kotlin: "kotlin", - clojure: "clojure", - julia: "julia", - "f#": "fsharp", - r: "r", - dart: "dart", - solidity: "solidity", - yaml: "yaml", - json: "json", - markdown: "markdown", - lua: "lua", - } - - return languageIdMap[languageName] || languageName - } - - /** - * Parse and validate the RPC arguments using Zod schemas - */ - private parseAndValidateArgs(...args: any[]): DocumentParams { - // RPC passes all arguments as a single array in args[0] - const argsArray = Array.isArray(args[0]) ? args[0] : args - - // Parse with Zod schema - const parsed = InlineCompletionArgsSchema.parse(argsArray) - const [documentUri, position, fileContent, languageId, requestId] = parsed - - // Safely extract and normalize parameters - const uri = typeof documentUri === "string" ? documentUri : String(documentUri) - const pos = - typeof position === "object" && position !== null && "line" in position && "character" in position - ? { line: position.line, character: position.character } - : { line: 0, character: 0 } - const content = typeof fileContent === "string" ? fileContent : String(fileContent) - const langId = typeof languageId === "string" ? languageId : String(languageId || "") - const reqId = typeof requestId === "string" ? requestId : String(requestId || "") - - return { - uri, - position: pos, - content, - languageId: langId, - requestId: reqId, - } - } - - /** - * Normalize content line endings to LF for consistent processing - * JetBrains may send content with different line endings - */ - private normalizeContent(content: string): NormalizedContent { - const normalizedContent = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n") - const lines = normalizedContent.split("\n") - - return { - normalizedContent, - lines, - } - } - - /** - * Create a mock VSCode TextDocument from the provided parameters - */ - private createMockDocument(uri: string, normalizedContent: string, language: string): vscode.TextDocument { - const mockDocument = new MockTextDocument(vscode.Uri.parse(uri), normalizedContent) - mockDocument.languageId = language - mockDocument.fileName = uri - return mockDocument - } - - /** - * Create a mock context provider that prevents workspace file access. - * This is used for JetBrains bridge to ensure only the provided document content is used. - */ - private createMockContextProvider(normalizedContent: string): AutocompleteContextProvider { - // Access the model through the inline completion provider which has access to it - const provider = this.autocomplete.inlineCompletionProvider as any - const model = provider.model - - return { - ide: { - readFile: async () => normalizedContent, - getWorkspaceDirs: async () => [], - getClipboardContent: async () => ({ text: "", copiedAt: new Date().toISOString() }), - }, - contextService: { - initializeForFile: async () => {}, - getRootPathSnippets: async () => [], - getSnippetsFromImportDefinitions: async () => [], - getStaticContextSnippets: async () => [], - }, - model, - ignoreController: undefined, - } as unknown as AutocompleteContextProvider - } - - /** - * Serialize completion results to a format suitable for RPC response - */ - private serializeCompletionResult( - completions: vscode.InlineCompletionItem[] | vscode.InlineCompletionList | undefined, - requestId: string, - ): CompletionResult { - const items = Array.isArray(completions) ? completions : completions?.items || [] - - return { - requestId, - items: items.map((item) => ({ - insertText: typeof item.insertText === "string" ? item.insertText : item.insertText.value, - range: item.range - ? { - start: { - line: item.range.start.line, - character: item.range.start.character, - }, - end: { line: item.range.end.line, character: item.range.end.character }, - } - : null, - })), - error: null, - } - } - - public async getInlineCompletions(...args: any[]): Promise { - try { - // Parse and validate arguments - const params = this.parseAndValidateArgs(...args) - - // Normalize content - const { normalizedContent, lines } = this.normalizeContent(params.content) - - // Determine language from languageId or file extension - const language = this.determineLanguage(params.languageId, params.uri) - - // Create mock document - const mockDocument = this.createMockDocument(params.uri, normalizedContent, language) - - // Create VSCode position and context - const vscodePosition = new vscode.Position(params.position.line, params.position.character) - const context: vscode.InlineCompletionContext = { - triggerKind: vscode.InlineCompletionTriggerKind.Invoke, - selectedCompletionInfo: undefined, - } - const tokenSource = new vscode.CancellationTokenSource() - - // Create mock context provider to prevent workspace file access - const mockContextProvider = this.createMockContextProvider(normalizedContent) - - // Save original builders - const originalFimBuilder = this.autocomplete.inlineCompletionProvider.fimPromptBuilder - const originalHoleFiller = this.autocomplete.inlineCompletionProvider.holeFiller - - try { - // Temporarily replace builders with ones using mock context - this.autocomplete.inlineCompletionProvider.fimPromptBuilder = new FimPromptBuilder(mockContextProvider) - this.autocomplete.inlineCompletionProvider.holeFiller = new HoleFiller(mockContextProvider) - - // Get completions from the provider (will use mock builders internally) - const completions = await this.autocomplete.inlineCompletionProvider.provideInlineCompletionItems( - mockDocument, - vscodePosition, - context, - tokenSource.token, - ) - - // Serialize and return the result - return this.serializeCompletionResult(completions, params.requestId) - } finally { - // Always restore original builders - this.autocomplete.inlineCompletionProvider.fimPromptBuilder = originalFimBuilder - this.autocomplete.inlineCompletionProvider.holeFiller = originalHoleFiller - tokenSource.dispose() - } - } catch (error) { - return { - requestId: "", - items: [], - error: error instanceof Error ? error.message : String(error), - } - } - } -} - -export const registerAutocompleteJetbrainsBridge = ( - context: vscode.ExtensionContext, - _cline: ClineProvider, - autocomplete: AutocompleteServiceManager, -) => { - // Check if we are running inside JetBrains IDE - const { kiloCodeWrapped, kiloCodeWrapperJetbrains } = getKiloCodeWrapperProperties() - if (!kiloCodeWrapped || !kiloCodeWrapperJetbrains) { - return - } - - // Initialize the JetBrains Bridge - const bridge = new AutocompleteJetbrainsBridge(autocomplete) - - // Register JetBrains inline completion command - context.subscriptions.push( - vscode.commands.registerCommand(GET_INLINE_COMPLETIONS_COMMAND, bridge.getInlineCompletions.bind(bridge)), - ) -} diff --git a/packages/kilo-vscode/src/services/autocomplete/AutocompleteModel.ts b/packages/kilo-vscode/src/services/autocomplete/AutocompleteModel.ts index 17543b7f008..6ef562c9785 100644 --- a/packages/kilo-vscode/src/services/autocomplete/AutocompleteModel.ts +++ b/packages/kilo-vscode/src/services/autocomplete/AutocompleteModel.ts @@ -1,219 +1,128 @@ -// kilocode_change new file -import { modelIdKeysByProvider, ProviderName } from "@roo-code/types" -import { ApiHandler, buildApiHandler, FimHandler } from "../../api" -import { ProviderSettingsManager } from "../../core/config/ProviderSettingsManager" -import { OpenRouterHandler } from "../../api/providers" -import { CompletionUsage } from "../../api/providers/openrouter" -import { ApiStreamChunk } from "../../api/transform/stream" -import { AUTOCOMPLETE_PROVIDER_MODELS, checkKilocodeBalance } from "./utils/kilocode-utils" -import { KilocodeOpenrouterHandler } from "../../api/providers/kilocode-openrouter" -import { PROVIDERS } from "../../../webview-ui/src/components/settings/constants" +// kilocode_change - rewritten to use CLI backend FIM endpoint import { ResponseMetaData } from "./types" +import type { KiloConnectionService } from "../cli-backend" -function getFimHandler(handler: ApiHandler): FimHandler | undefined { - if (typeof handler.fimSupport === "function") { - return handler.fimSupport() - } - return undefined -} +const DEFAULT_MODEL = "mistralai/codestral-2508" +const PROVIDER_DISPLAY_NAME = "Kilo Gateway" -// Convert PROVIDERS array to a lookup map for display names -const PROVIDER_DISPLAY_NAMES = Object.fromEntries(PROVIDERS.map(({ value, label }) => [value, label])) as Record< - ProviderName, - string -> +/** Chunk from an LLM streaming response */ +export type ApiStreamChunk = + | { type: "text"; text: string } + | { + type: "usage" + totalCost?: number + inputTokens?: number + outputTokens?: number + cacheReadTokens?: number + cacheWriteTokens?: number + } export class AutocompleteModel { - private apiHandler: ApiHandler | null = null + private connectionService: KiloConnectionService | null = null public profileName: string | null = null public profileType: string | null = null - private currentProvider: ProviderName | null = null public loaded = false public hasKilocodeProfileWithNoBalance = false - constructor(apiHandler: ApiHandler | null = null) { - if (apiHandler) { - this.apiHandler = apiHandler + constructor(connectionService?: KiloConnectionService) { + if (connectionService) { + this.connectionService = connectionService this.loaded = true } } - private cleanup(): void { - this.apiHandler = null - this.profileName = null - this.profileType = null - this.currentProvider = null - this.loaded = false - this.hasKilocodeProfileWithNoBalance = false - } - public async reload(providerSettingsManager: ProviderSettingsManager): Promise { - const profiles = await providerSettingsManager.listConfig() - - this.cleanup() - - const selectedProfile = profiles.find((x) => x.profileType === "autocomplete") - if (selectedProfile) { - const profile = await providerSettingsManager.getProfile({ id: selectedProfile.id }) - if (profile.apiProvider) { - await useProfile(this, profile, profile.apiProvider) - return true - } - } - - for (const [provider, model] of AUTOCOMPLETE_PROVIDER_MODELS) { - const selectedProfile = profiles.find((x) => x?.apiProvider === provider && !(x.profileType === "autocomplete")) - if (!selectedProfile) continue - const profile = await providerSettingsManager.getProfile({ id: selectedProfile.id }) - - if (provider === "kilocode") { - // For all other providers, assume they are usable - if (!profile.kilocodeToken) continue - const hasBalance = await checkKilocodeBalance(profile.kilocodeToken, profile.kilocodeOrganizationId) - if (!hasBalance) { - // Track that we found a kilocode profile but it has no balance - this.hasKilocodeProfileWithNoBalance = true - continue - } - } - await useProfile(this, { ...profile, [modelIdKeysByProvider[provider]]: model }, provider) - return true - } - - this.loaded = true // we loaded, and found nothing, but we do not wish to reload - return false - - type ProfileWithIdAndName = Awaited> - async function useProfile(self: AutocompleteModel, profile: ProfileWithIdAndName, provider: ProviderName) { - self.profileName = profile.name || null - self.profileType = profile.profileType || null - self.currentProvider = provider - self.apiHandler = buildApiHandler(profile) - if (self.apiHandler instanceof OpenRouterHandler) await self.apiHandler.fetchModel() - self.loaded = true - } - } - - public supportsFim(): boolean { - if (!this.apiHandler) { - return false - } - - return getFimHandler(this.apiHandler) !== undefined + /** + * Set the connection service (can be called after construction when service becomes available) + */ + public setConnectionService(service: KiloConnectionService): void { + this.connectionService = service } /** - * Generate FIM completion using the FIM API endpoint. + * Load model configuration. + * Returns true if the connection service is available. + */ + public async reload(): Promise { + this.loaded = true + + if (this.connectionService) { + const state = this.connectionService.getConnectionState() + return state === "connected" + } + + return false + } + + public supportsFim(): boolean { + return true + } + + /** + * Generate a FIM (Fill-in-the-Middle) completion via the CLI backend. + * The CLI backend handles auth using the stored kilo OAuth token. */ public async generateFimResponse( prefix: string, suffix: string, onChunk: (text: string) => void, - taskId?: string, + _taskId?: string, ): Promise { - if (!this.apiHandler) { - console.error("API handler is not initialized") - throw new Error("API handler is not initialized. Please check your configuration.") + if (!this.connectionService) { + throw new Error("Connection service is not available") } - const fimHandler = getFimHandler(this.apiHandler) - if (!fimHandler) { - throw new Error("Current provider/model does not support FIM completions") + const state = this.connectionService.getConnectionState() + if (state !== "connected") { + throw new Error(`CLI backend is not connected (state: ${state})`) } - console.log("USED MODEL (FIM)", fimHandler.getModel()) + const client = this.connectionService.getHttpClient() - let usage: CompletionUsage | undefined - - for await (const chunk of fimHandler.streamFim(prefix, suffix, taskId, (u: CompletionUsage) => { - usage = u - })) { - onChunk(chunk) - } - - // Calculate cost using the FimHandler's getTotalCost method - const cost = usage ? fimHandler.getTotalCost(usage) : 0 - const inputTokens = usage?.prompt_tokens ?? 0 - const outputTokens = usage?.completion_tokens ?? 0 - const cacheReadTokens = usage?.prompt_tokens_details?.cached_tokens ?? 0 + const result = await client.fimCompletion(prefix, suffix, onChunk, { + model: DEFAULT_MODEL, + maxTokens: 256, + temperature: 0.2, + }) return { - cost, - inputTokens, - outputTokens, - cacheWriteTokens: 0, // FIM doesn't support cache writes - cacheReadTokens, + cost: result.cost, + inputTokens: result.inputTokens, + outputTokens: result.outputTokens, + cacheWriteTokens: 0, + cacheReadTokens: 0, } } /** - * Generate response with streaming callback support + * Generate response via chat completions (holefiller fallback). + * Not used when FIM is supported, but kept for compatibility. */ public async generateResponse( systemPrompt: string, userPrompt: string, onChunk: (chunk: ApiStreamChunk) => void, ): Promise { - if (!this.apiHandler) { - console.error("API handler is not initialized") - throw new Error("API handler is not initialized. Please check your configuration.") - } - - console.log("USED MODEL", this.apiHandler.getModel()) - - const stream = this.apiHandler.createMessage(systemPrompt, [ - { role: "user", content: [{ type: "text", text: userPrompt }] }, - ]) - - let cost = 0 - let inputTokens = 0 - let outputTokens = 0 - let cacheReadTokens = 0 - let cacheWriteTokens = 0 - - try { - for await (const chunk of stream) { - // Call the callback with each chunk - onChunk(chunk) - - // Track usage information - if (chunk.type === "usage") { - cost = chunk.totalCost ?? 0 - cacheReadTokens = chunk.cacheReadTokens ?? 0 - cacheWriteTokens = chunk.cacheWriteTokens ?? 0 - inputTokens = chunk.inputTokens ?? 0 - outputTokens = chunk.outputTokens ?? 0 - } - } - } catch (error) { - console.error("Error streaming completion:", error) - throw error - } - - return { - cost, - inputTokens, - outputTokens, - cacheWriteTokens, - cacheReadTokens, - } + // FIM is the primary strategy; this method is a fallback. + // For now, throw — callers should use generateFimResponse via supportsFim(). + throw new Error("Chat-based completions are not supported via CLI backend. Use FIM (supportsFim() returns true).") } - public getModelName(): string | undefined { - if (!this.apiHandler) return undefined - - return this.apiHandler.getModel().id ?? undefined + public getModelName(): string { + return DEFAULT_MODEL } - public getProviderDisplayName(): string | undefined { - if (!this.currentProvider) return undefined - return PROVIDER_DISPLAY_NAMES[this.currentProvider] - } - - public getRolloutHash_IfLoggedInToKilo(): number | undefined { - return this.apiHandler instanceof KilocodeOpenrouterHandler ? this.apiHandler.getRolloutHash() : undefined + public getProviderDisplayName(): string { + return PROVIDER_DISPLAY_NAME } + /** + * Check if the model has valid credentials. + * With CLI backend, credentials are managed by the backend — we just need a connection. + */ public hasValidCredentials(): boolean { - return this.apiHandler !== null && this.loaded + if (!this.connectionService) { + return false + } + return this.connectionService.getConnectionState() === "connected" } } diff --git a/packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts b/packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts index ec7d7f05b0b..64797049cb1 100644 --- a/packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts +++ b/packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts @@ -1,22 +1,49 @@ import crypto from "crypto" import * as vscode from "vscode" -import { t } from "../../i18n" +import { t } from "./shims/i18n" +import { TelemetryStub } from "./shims/TelemetryStub" import { AutocompleteModel } from "./AutocompleteModel" import { AutocompleteStatusBar } from "./AutocompleteStatusBar" import { AutocompleteCodeActionProvider } from "./AutocompleteCodeActionProvider" import { AutocompleteInlineCompletionProvider } from "./classic-auto-complete/AutocompleteInlineCompletionProvider" -import { AutocompleteServiceSettings, TelemetryEventName } from "@roo-code/types" -import { ContextProxy } from "../../core/config/ContextProxy" -import { TelemetryService } from "@roo-code/telemetry" -import { ClineProvider } from "../../core/webview/ClineProvider" -import { AutocompleteTelemetry } from "./classic-auto-complete/AutocompleteTelemetry" +import { AutocompleteTelemetry, TelemetryEventName } from "./classic-auto-complete/AutocompleteTelemetry" +import type { KiloConnectionService } from "../cli-backend" + +const CONFIG_SECTION = "kilo-code.new.autocomplete" + +export interface AutocompleteServiceSettings { + enableAutoTrigger?: boolean + enableSmartInlineTaskKeybinding?: boolean + enableChatAutocomplete?: boolean + provider?: string + model?: string + snoozeUntil?: number + hasKilocodeProfileWithNoBalance?: boolean +} + +function readSettings(): AutocompleteServiceSettings { + const config = vscode.workspace.getConfiguration(CONFIG_SECTION) + return { + enableAutoTrigger: config.get("enableAutoTrigger") ?? true, + enableSmartInlineTaskKeybinding: config.get("enableSmartInlineTaskKeybinding") ?? true, + enableChatAutocomplete: config.get("enableChatAutocomplete") ?? true, + snoozeUntil: config.get("snoozeUntil"), + } +} + +async function writeSettings(patch: Partial): Promise { + const config = vscode.workspace.getConfiguration(CONFIG_SECTION) + for (const [key, value] of Object.entries(patch)) { + await config.update(key, value, vscode.ConfigurationTarget.Global) + } +} export class AutocompleteServiceManager { private static _instance: AutocompleteServiceManager | null = null private readonly model: AutocompleteModel - private readonly cline: ClineProvider private readonly context: vscode.ExtensionContext + private readonly telemetry = new TelemetryStub() private settings: AutocompleteServiceSettings | null = null private taskId: string | null = null @@ -34,7 +61,7 @@ export class AutocompleteServiceManager { public readonly inlineCompletionProvider: AutocompleteInlineCompletionProvider private inlineCompletionProviderDisposable: vscode.Disposable | null = null - constructor(context: vscode.ExtensionContext, cline: ClineProvider) { + constructor(context: vscode.ExtensionContext, connectionService: KiloConnectionService) { if (AutocompleteServiceManager._instance) { throw new Error( "AutocompleteServiceManager is a singleton. Use AutocompleteServiceManager.getInstance() instead.", @@ -42,11 +69,12 @@ export class AutocompleteServiceManager { } this.context = context - this.cline = cline AutocompleteServiceManager._instance = this // Register Internal Components - this.model = new AutocompleteModel() + this.model = new AutocompleteModel(connectionService) + + const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? "" // Register the providers this.codeActionProvider = new AutocompleteCodeActionProvider() @@ -55,7 +83,7 @@ export class AutocompleteServiceManager { this.model, this.updateCostTracking.bind(this), () => this.settings, - this.cline, + workspacePath, new AutocompleteTelemetry(), ) @@ -70,34 +98,13 @@ export class AutocompleteServiceManager { } public async load() { - await this.cline.providerSettingsManager.initialize() // avoid race condition with settings migrations - await this.model.reload(this.cline.providerSettingsManager) - - this.settings = ContextProxy.instance.getGlobalState("ghostServiceSettings") ?? { - enableSmartInlineTaskKeybinding: true, - } - // Auto-enable autocomplete by default - if (this.settings.enableAutoTrigger == undefined) { - this.settings.enableAutoTrigger = true - } - - // Auto-enable chat autocomplete by default - if (this.settings.enableChatAutocomplete == undefined) { - this.settings.enableChatAutocomplete = true - } + await this.model.reload() + this.settings = readSettings() await this.updateGlobalContext() this.updateStatusBar() await this.updateInlineCompletionProviderRegistration() this.setupSnoozeTimerIfNeeded() - const settingsWithModelInfo = { - ...this.settings, - provider: this.getCurrentProviderName(), - model: this.getCurrentModelName(), - hasKilocodeProfileWithNoBalance: this.model.hasKilocodeProfileWithNoBalance, - } - await ContextProxy.instance.setValues({ ghostServiceSettings: settingsWithModelInfo }) - await this.cline.postStateToWebview() } private async updateInlineCompletionProviderRegistration() { @@ -109,7 +116,9 @@ export class AutocompleteServiceManager { this.inlineCompletionProviderDisposable = null } - if (!shouldBeRegistered) return + if (!shouldBeRegistered) { + return + } // Register classic provider this.inlineCompletionProviderDisposable = vscode.languages.registerInlineCompletionItemProvider( @@ -120,16 +129,12 @@ export class AutocompleteServiceManager { } public async disable() { - const settings = ContextProxy.instance.getGlobalState("ghostServiceSettings") ?? {} - await ContextProxy.instance.setValues({ - ghostServiceSettings: { - ...settings, - enableAutoTrigger: false, - enableSmartInlineTaskKeybinding: false, - }, + await writeSettings({ + enableAutoTrigger: false, + enableSmartInlineTaskKeybinding: false, }) - TelemetryService.instance.captureEvent(TelemetryEventName.GHOST_SERVICE_DISABLED) + this.telemetry.captureEvent(TelemetryEventName.GHOST_SERVICE_DISABLED) await this.load() } @@ -139,7 +144,9 @@ export class AutocompleteServiceManager { */ public isSnoozed(): boolean { const snoozeUntil = this.settings?.snoozeUntil - if (!snoozeUntil) return false + if (!snoozeUntil) { + return false + } return Date.now() < snoozeUntil } @@ -148,7 +155,9 @@ export class AutocompleteServiceManager { */ public getSnoozeRemainingSeconds(): number { const snoozeUntil = this.settings?.snoozeUntil - if (!snoozeUntil) return 0 + if (!snoozeUntil) { + return 0 + } const remaining = Math.max(0, Math.ceil((snoozeUntil - Date.now()) / 1000)) return remaining } @@ -163,13 +172,7 @@ export class AutocompleteServiceManager { } const snoozeUntil = Date.now() + seconds * 1000 - const settings = ContextProxy.instance.getGlobalState("ghostServiceSettings") ?? {} - await ContextProxy.instance.setValues({ - ghostServiceSettings: { - ...settings, - snoozeUntil, - }, - }) + await writeSettings({ snoozeUntil }) this.snoozeTimer = setTimeout(() => { void this.unsnooze() @@ -187,22 +190,13 @@ export class AutocompleteServiceManager { this.snoozeTimer = null } - const settings = ContextProxy.instance.getGlobalState("ghostServiceSettings") ?? {} - await ContextProxy.instance.setValues({ - ghostServiceSettings: { - ...settings, - snoozeUntil: undefined, - }, - }) + await writeSettings({ snoozeUntil: undefined }) await this.load() } /** * Set up a timer to auto-unsnooze if we're currently in a snoozed state. - * This handles the case where the extension restarts while snoozed - - * the persisted snoozeUntil timestamp keeps autocomplete disabled, - * and this timer ensures we unsnooze at the correct time. */ private setupSnoozeTimerIfNeeded(): void { if (this.snoozeTimer) { @@ -225,7 +219,9 @@ export class AutocompleteServiceManager { */ private getSnoozeRemainingMs(): number { const snoozeUntil = this.settings?.snoozeUntil - if (!snoozeUntil) return 0 + if (!snoozeUntil) { + return 0 + } return Math.max(0, snoozeUntil - Date.now()) } @@ -236,7 +232,7 @@ export class AutocompleteServiceManager { } this.taskId = crypto.randomUUID() - TelemetryService.instance.captureEvent(TelemetryEventName.INLINE_ASSIST_AUTO_TASK, { + this.telemetry.captureEvent(TelemetryEventName.INLINE_ASSIST_AUTO_TASK, { taskId: this.taskId, }) @@ -255,32 +251,27 @@ export class AutocompleteServiceManager { } const tokenSource = new vscode.CancellationTokenSource() - try { - const completions = await this.inlineCompletionProvider.provideInlineCompletionItems_Internal( - document, - position, - context, - tokenSource.token, - ) + const completions = await this.inlineCompletionProvider.provideInlineCompletionItems_Internal( + document, + position, + context, + tokenSource.token, + ) + tokenSource.dispose() - // If we got completions, directly insert the first one - if (completions && (Array.isArray(completions) ? completions.length > 0 : completions.items.length > 0)) { - const items = Array.isArray(completions) ? completions : completions.items - const firstCompletion = items[0] + // If we got completions, directly insert the first one + if (completions && (Array.isArray(completions) ? completions.length > 0 : completions.items.length > 0)) { + const items = Array.isArray(completions) ? completions : completions.items + const firstCompletion = items[0] - if (firstCompletion && firstCompletion.insertText) { - const insertText = - typeof firstCompletion.insertText === "string" - ? firstCompletion.insertText - : firstCompletion.insertText.value + if (firstCompletion?.insertText) { + const insertText = + typeof firstCompletion.insertText === "string" ? firstCompletion.insertText : firstCompletion.insertText.value - await editor.edit((editBuilder) => { - editBuilder.insert(position, insertText) - }) - } + await editor.edit((editBuilder) => { + editBuilder.insert(position, insertText) + }) } - } finally { - tokenSource.dispose() } } @@ -318,12 +309,10 @@ export class AutocompleteServiceManager { } private hasNoUsableProvider(): boolean { - // We have no usable provider if the model is loaded but has no valid credentials - // and it's not because of a kilocode profile with no balance (that's a different error) return this.model.loaded && !this.model.hasValidCredentials() && !this.model.hasKilocodeProfileWithNoBalance } - private updateCostTracking(cost: number, inputTokens: number, outputTokens: number): void { + private updateCostTracking(cost: number, _inputTokens: number, _outputTokens: number): void { this.completionCount++ this.sessionCost += cost this.updateStatusBar() @@ -355,9 +344,9 @@ export class AutocompleteServiceManager { const response = await vscode.window.showErrorMessage(message, disableCopilot, disableInlineAssist) if (response === disableCopilot) { - await vscode.commands.executeCommand("github.copilot.completions.disable") + await vscode.commands.executeCommand("github.copilot.completions.disable") } else if (response === disableInlineAssist) { - await vscode.commands.executeCommand("kilo-code.autocomplete.disable") + await vscode.commands.executeCommand("kilo-code.new.autocomplete.disable") } } diff --git a/packages/kilo-vscode/src/services/autocomplete/AutocompleteStatusBar.ts b/packages/kilo-vscode/src/services/autocomplete/AutocompleteStatusBar.ts index 8575ca6dde7..b371f0b0c23 100644 --- a/packages/kilo-vscode/src/services/autocomplete/AutocompleteStatusBar.ts +++ b/packages/kilo-vscode/src/services/autocomplete/AutocompleteStatusBar.ts @@ -1,22 +1,8 @@ import * as vscode from "vscode" -import { AUTOCOMPLETE_PROVIDER_MODELS, ProviderName } from "@roo-code/types" -import { t } from "../../i18n" -import { PROVIDERS } from "../../../webview-ui/src/components/settings/constants" +import { t } from "./shims/i18n" import type { AutocompleteStatusBarStateProps } from "./types" -// Convert PROVIDERS array to a lookup map for display names -const PROVIDER_DISPLAY_NAMES = Object.fromEntries(PROVIDERS.map(({ value, label }) => [value, label])) as Record< - ProviderName, - string -> - -/** - * Get the display names of all supported autocomplete providers - */ -function getSupportedProviderDisplayNames(): string[] { - const providerKeys = Array.from(AUTOCOMPLETE_PROVIDER_MODELS.keys()) - return providerKeys.map((key) => PROVIDER_DISPLAY_NAMES[key as ProviderName] || key) -} +const SUPPORTED_PROVIDER_DISPLAY_NAME = "Kilo Gateway" export class AutocompleteStatusBar { statusBar: vscode.StatusBarItem @@ -55,8 +41,12 @@ export class AutocompleteStatusBar { private humanFormatSessionCost(): string { const cost = this.props.totalSessionCost - if (cost === 0) return t("kilocode:autocomplete.statusBar.cost.zero") - if (cost > 0 && cost < 0.01) return t("kilocode:autocomplete.statusBar.cost.lessThanCent") // Less than one cent + if (cost === 0) { + return t("kilocode:autocomplete.statusBar.cost.zero") + } + if (cost > 0 && cost < 0.01) { + return t("kilocode:autocomplete.statusBar.cost.lessThanCent") + } return `$${cost.toFixed(2)}` } @@ -64,7 +54,9 @@ export class AutocompleteStatusBar { this.props = { ...this.props, ...params } this.updateVisible() - if (this.props.enabled) this.render() + if (this.props.enabled) { + this.render() + } } private formatTime(timestamp: number): string { @@ -116,10 +108,8 @@ export class AutocompleteStatusBar { private renderNoUsableProviderError() { this.statusBar.text = t("kilocode:autocomplete.statusBar.warning") - const providers = getSupportedProviderDisplayNames() - const providerList = providers.join(", ") this.statusBar.tooltip = this.createMarkdownTooltip( - t("kilocode:autocomplete.statusBar.tooltip.noUsableProvider", { providers: providerList }), + t("kilocode:autocomplete.statusBar.tooltip.noUsableProvider", { providers: SUPPORTED_PROVIDER_DISPLAY_NAME }), ) } } diff --git a/packages/kilo-vscode/src/services/autocomplete/__tests__/AutocompleteJetbrainsBridge.spec.ts b/packages/kilo-vscode/src/services/autocomplete/__tests__/AutocompleteJetbrainsBridge.spec.ts deleted file mode 100644 index 6c7e72d53e6..00000000000 --- a/packages/kilo-vscode/src/services/autocomplete/__tests__/AutocompleteJetbrainsBridge.spec.ts +++ /dev/null @@ -1,359 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from "vitest" -import * as vscode from "vscode" -import { AutocompleteJetbrainsBridge } from "../AutocompleteJetbrainsBridge" -import { AutocompleteServiceManager } from "../AutocompleteServiceManager" - -// Mock vscode module -vi.mock("vscode", () => ({ - Uri: { - parse: vi.fn((uri: string) => ({ toString: () => uri, fsPath: uri })), - }, - Position: class Position { - constructor( - public line: number, - public character: number, - ) {} - }, - Range: class Range { - constructor( - public start: any, - public end: any, - ) {} - }, - EndOfLine: { - LF: 1, - CRLF: 2, - }, - InlineCompletionTriggerKind: { - Invoke: 0, - Automatic: 1, - }, - CancellationTokenSource: class CancellationTokenSource { - token = { isCancellationRequested: false } - dispose = vi.fn() - }, -})) - -describe("AutocompleteJetbrainsBridge", () => { - let bridge: AutocompleteJetbrainsBridge - let mockAutocomplete: any - - beforeEach(() => { - mockAutocomplete = { - inlineCompletionProvider: { - provideInlineCompletionItems: vi.fn().mockResolvedValue([ - { - insertText: "console.log('test')", - range: { - start: { line: 0, character: 0 }, - end: { line: 0, character: 10 }, - }, - }, - ]), - fimPromptBuilder: {}, - holeFiller: {}, - }, - } as any - - bridge = new AutocompleteJetbrainsBridge(mockAutocomplete) - }) - - describe("parseAndValidateArgs", () => { - it("should parse arguments when passed as array", async () => { - const args = [["file:///test.ts", { line: 5, character: 10 }, "const x = 1", "typescript", "req-123"]] - - const result = await bridge.getInlineCompletions(...args) - - expect(result.requestId).toBe("req-123") - expect(result.error).toBeNull() - }) - - it("should parse arguments when passed separately", async () => { - const args = ["file:///test.ts", { line: 5, character: 10 }, "const x = 1", "typescript", "req-456"] - - const result = await bridge.getInlineCompletions(...args) - - expect(result.requestId).toBe("req-456") - expect(result.error).toBeNull() - }) - - it("should handle invalid position gracefully", async () => { - const args = [["file:///test.ts", null, "const x = 1", "typescript", "req-789"]] - - const result = await bridge.getInlineCompletions(...args) - - // Should default to position 0,0 - expect(mockAutocomplete.inlineCompletionProvider.provideInlineCompletionItems).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ line: 0, character: 0 }), - expect.anything(), - expect.anything(), - ) - }) - - it("should convert non-string values to strings", async () => { - const args = [[123 as any, { line: 0, character: 0 }, 456 as any, 789 as any, 999 as any]] - - const result = await bridge.getInlineCompletions(...args) - - expect(result.error).toBeNull() - }) - }) - - describe("normalizeContent", () => { - it("should normalize CRLF line endings to LF", async () => { - const content = "line1\r\nline2\r\nline3" - const args = [["file:///test.ts", { line: 0, character: 0 }, content, "typescript", "req-1"]] - - await bridge.getInlineCompletions(...args) - - const mockDocument = mockAutocomplete.inlineCompletionProvider.provideInlineCompletionItems.mock.calls[0][0] - expect(mockDocument.getText()).toBe("line1\nline2\nline3") - }) - - it("should normalize CR line endings to LF", async () => { - const content = "line1\rline2\rline3" - const args = [["file:///test.ts", { line: 0, character: 0 }, content, "typescript", "req-2"]] - - await bridge.getInlineCompletions(...args) - - const mockDocument = mockAutocomplete.inlineCompletionProvider.provideInlineCompletionItems.mock.calls[0][0] - expect(mockDocument.getText()).toBe("line1\nline2\nline3") - }) - - it("should handle mixed line endings", async () => { - const content = "line1\r\nline2\rline3\nline4" - const args = [["file:///test.ts", { line: 0, character: 0 }, content, "typescript", "req-3"]] - - await bridge.getInlineCompletions(...args) - - const mockDocument = mockAutocomplete.inlineCompletionProvider.provideInlineCompletionItems.mock.calls[0][0] - expect(mockDocument.getText()).toBe("line1\nline2\nline3\nline4") - }) - }) - - describe("createMockDocument", () => { - it("should create a valid TextDocument mock", async () => { - const content = "line1\nline2\nline3" - const args = [["file:///test.ts", { line: 1, character: 5 }, content, "typescript", "req-4"]] - - await bridge.getInlineCompletions(...args) - - const mockDocument = mockAutocomplete.inlineCompletionProvider.provideInlineCompletionItems.mock.calls[0][0] - - expect(mockDocument.languageId).toBe("typescript") - expect(mockDocument.lineCount).toBe(3) - expect(mockDocument.getText()).toBe(content) - }) - - it("should implement getText with range correctly", async () => { - const content = "line1\nline2\nline3" - const args = [["file:///test.ts", { line: 0, character: 0 }, content, "typescript", "req-5"]] - - await bridge.getInlineCompletions(...args) - - const mockDocument = mockAutocomplete.inlineCompletionProvider.provideInlineCompletionItems.mock.calls[0][0] - - // Test single line range - const range1 = new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 5)) - expect(mockDocument.getText(range1)).toBe("line1") - - // Test multi-line range - const range2 = new vscode.Range(new vscode.Position(0, 0), new vscode.Position(1, 5)) - expect(mockDocument.getText(range2)).toBe("line1\nline2") - }) - - it("should implement lineAt correctly", async () => { - const content = " const x = 1\nlet y = 2\n" - const args = [["file:///test.ts", { line: 0, character: 0 }, content, "typescript", "req-6"]] - - await bridge.getInlineCompletions(...args) - - const mockDocument = mockAutocomplete.inlineCompletionProvider.provideInlineCompletionItems.mock.calls[0][0] - - const line = mockDocument.lineAt(0) - expect(line.text).toBe(" const x = 1") - expect(line.firstNonWhitespaceCharacterIndex).toBe(2) - expect(line.isEmptyOrWhitespace).toBe(false) - }) - - it("should implement offsetAt correctly", async () => { - const content = "abc\ndef\nghi" - const args = [["file:///test.ts", { line: 0, character: 0 }, content, "typescript", "req-7"]] - - await bridge.getInlineCompletions(...args) - - const mockDocument = mockAutocomplete.inlineCompletionProvider.provideInlineCompletionItems.mock.calls[0][0] - - // Position at start of line 0 - expect(mockDocument.offsetAt(new vscode.Position(0, 0))).toBe(0) - // Position at end of line 0 - expect(mockDocument.offsetAt(new vscode.Position(0, 3))).toBe(3) - // Position at start of line 1 - expect(mockDocument.offsetAt(new vscode.Position(1, 0))).toBe(4) - }) - - it("should implement positionAt correctly", async () => { - const content = "abc\ndef\nghi" - const args = [["file:///test.ts", { line: 0, character: 0 }, content, "typescript", "req-8"]] - - await bridge.getInlineCompletions(...args) - - const mockDocument = mockAutocomplete.inlineCompletionProvider.provideInlineCompletionItems.mock.calls[0][0] - - // Offset 0 should be position 0,0 - const pos1 = mockDocument.positionAt(0) - expect(pos1.line).toBe(0) - expect(pos1.character).toBe(0) - - // Offset 4 should be position 1,0 (start of second line) - const pos2 = mockDocument.positionAt(4) - expect(pos2.line).toBe(1) - expect(pos2.character).toBe(0) - }) - }) - - describe("serializeCompletionResult", () => { - it("should serialize completion items correctly", async () => { - const args = [["file:///test.ts", { line: 0, character: 0 }, "const x = 1", "typescript", "req-9"]] - - const result = await bridge.getInlineCompletions(...args) - - expect(result.requestId).toBe("req-9") - expect(result.items).toHaveLength(1) - expect(result.items[0].insertText).toBe("console.log('test')") - expect(result.items[0].range).toEqual({ - start: { line: 0, character: 0 }, - end: { line: 0, character: 10 }, - }) - expect(result.error).toBeNull() - }) - - it("should handle completions with string insertText", async () => { - mockAutocomplete.inlineCompletionProvider.provideInlineCompletionItems.mockResolvedValue([ - { - insertText: "simple string", - range: null, - }, - ]) - - const args = [["file:///test.ts", { line: 0, character: 0 }, "const x = 1", "typescript", "req-10"]] - - const result = await bridge.getInlineCompletions(...args) - - expect(result.items[0].insertText).toBe("simple string") - expect(result.items[0].range).toBeNull() - }) - - it("should handle completions with SnippetString insertText", async () => { - mockAutocomplete.inlineCompletionProvider.provideInlineCompletionItems.mockResolvedValue([ - { - insertText: { value: "snippet value" }, - range: null, - }, - ]) - - const args = [["file:///test.ts", { line: 0, character: 0 }, "const x = 1", "typescript", "req-11"]] - - const result = await bridge.getInlineCompletions(...args) - - expect(result.items[0].insertText).toBe("snippet value") - }) - - it("should handle InlineCompletionList format", async () => { - mockAutocomplete.inlineCompletionProvider.provideInlineCompletionItems.mockResolvedValue({ - items: [ - { - insertText: "from list", - range: null, - }, - ], - }) - - const args = [["file:///test.ts", { line: 0, character: 0 }, "const x = 1", "typescript", "req-12"]] - - const result = await bridge.getInlineCompletions(...args) - - expect(result.items).toHaveLength(1) - expect(result.items[0].insertText).toBe("from list") - }) - - it("should handle empty completions", async () => { - mockAutocomplete.inlineCompletionProvider.provideInlineCompletionItems.mockResolvedValue([]) - - const args = [["file:///test.ts", { line: 0, character: 0 }, "const x = 1", "typescript", "req-13"]] - - const result = await bridge.getInlineCompletions(...args) - - expect(result.items).toHaveLength(0) - expect(result.error).toBeNull() - }) - - it("should handle undefined completions", async () => { - mockAutocomplete.inlineCompletionProvider.provideInlineCompletionItems.mockResolvedValue(undefined) - - const args = [["file:///test.ts", { line: 0, character: 0 }, "const x = 1", "typescript", "req-14"]] - - const result = await bridge.getInlineCompletions(...args) - - expect(result.items).toHaveLength(0) - expect(result.error).toBeNull() - }) - }) - - describe("error handling", () => { - it("should return error result when validation fails", async () => { - const args = [[]] // Invalid args - - const result = await bridge.getInlineCompletions(...args) - - expect(result.requestId).toBe("") - expect(result.items).toHaveLength(0) - expect(result.error).toBeTruthy() - }) - - it("should return error result when provider throws", async () => { - mockAutocomplete.inlineCompletionProvider.provideInlineCompletionItems.mockRejectedValue( - new Error("Provider error"), - ) - - const args = [["file:///test.ts", { line: 0, character: 0 }, "const x = 1", "typescript", "req-15"]] - - const result = await bridge.getInlineCompletions(...args) - - expect(result.requestId).toBe("") - expect(result.items).toHaveLength(0) - expect(result.error).toBe("Provider error") - }) - - it("should handle non-Error exceptions", async () => { - mockAutocomplete.inlineCompletionProvider.provideInlineCompletionItems.mockRejectedValue("String error") - - const args = [["file:///test.ts", { line: 0, character: 0 }, "const x = 1", "typescript", "req-16"]] - - const result = await bridge.getInlineCompletions(...args) - - expect(result.error).toBe("String error") - }) - }) - - describe("language determination", () => { - it("should use provided languageId when valid", async () => { - const args = [["file:///test.ts", { line: 0, character: 0 }, "const x = 1", "typescript", "req-17"]] - - await bridge.getInlineCompletions(...args) - - const mockDocument = mockAutocomplete.inlineCompletionProvider.provideInlineCompletionItems.mock.calls[0][0] - expect(mockDocument.languageId).toBe("typescript") - }) - - it("should determine language from file extension when languageId is generic", async () => { - const args = [["file:///test.py", { line: 0, character: 0 }, "x = 1", "text", "req-18"]] - - await bridge.getInlineCompletions(...args) - - const mockDocument = mockAutocomplete.inlineCompletionProvider.provideInlineCompletionItems.mock.calls[0][0] - expect(mockDocument.languageId).toBe("python") - }) - }) -}) diff --git a/packages/kilo-vscode/src/services/autocomplete/chat-autocomplete/ChatTextAreaAutocomplete.ts b/packages/kilo-vscode/src/services/autocomplete/chat-autocomplete/ChatTextAreaAutocomplete.ts index afb06214b3d..7f86c55efe4 100644 --- a/packages/kilo-vscode/src/services/autocomplete/chat-autocomplete/ChatTextAreaAutocomplete.ts +++ b/packages/kilo-vscode/src/services/autocomplete/chat-autocomplete/ChatTextAreaAutocomplete.ts @@ -1,24 +1,21 @@ -import * as vscode from "vscode" import { AutocompleteModel } from "../AutocompleteModel" -import { ProviderSettingsManager } from "../../../core/config/ProviderSettingsManager" import { AutocompleteContext, VisibleCodeContext } from "../types" import { removePrefixOverlap } from "../continuedev/core/autocomplete/postprocessing/removePrefixOverlap.js" import { AutocompleteTelemetry } from "../classic-auto-complete/AutocompleteTelemetry" import { postprocessAutocompleteSuggestion } from "../classic-auto-complete/uselessSuggestionFilter" +import type { KiloConnectionService } from "../../cli-backend" export class ChatTextAreaAutocomplete { private model: AutocompleteModel - private providerSettingsManager: ProviderSettingsManager private telemetry: AutocompleteTelemetry - constructor(providerSettingsManager: ProviderSettingsManager) { - this.model = new AutocompleteModel() - this.providerSettingsManager = providerSettingsManager + constructor(connectionService: KiloConnectionService) { + this.model = new AutocompleteModel(connectionService) this.telemetry = new AutocompleteTelemetry("chat-textarea") } async initialize(): Promise { - return this.model.reload(this.providerSettingsManager) + return this.model.reload() } async getCompletion(userText: string, visibleCodeContext?: VisibleCodeContext): Promise<{ suggestion: string }> { diff --git a/packages/kilo-vscode/src/services/autocomplete/chat-autocomplete/handleChatCompletionAccepted.ts b/packages/kilo-vscode/src/services/autocomplete/chat-autocomplete/handleChatCompletionAccepted.ts index c5b24ee5d07..9bb9a153bd1 100644 --- a/packages/kilo-vscode/src/services/autocomplete/chat-autocomplete/handleChatCompletionAccepted.ts +++ b/packages/kilo-vscode/src/services/autocomplete/chat-autocomplete/handleChatCompletionAccepted.ts @@ -1,6 +1,10 @@ -import { WebviewMessage } from "../../../shared/WebviewMessage" import { AutocompleteTelemetry } from "../classic-auto-complete/AutocompleteTelemetry" +export interface ChatCompletionAcceptedMessage { + type: "chatCompletionAccepted" + suggestionLength?: number +} + // Singleton telemetry instance for chat-textarea autocomplete // This ensures we use the same instance across requests and acceptance events let telemetryInstance: AutocompleteTelemetry | null = null @@ -19,7 +23,7 @@ export function getChatAutocompleteTelemetry(): AutocompleteTelemetry { * Handles a chat completion accepted event from the webview. * Captures telemetry when the user accepts a suggestion via Tab or ArrowRight. */ -export function handleChatCompletionAccepted(message: WebviewMessage & { type: "chatCompletionAccepted" }): void { +export function handleChatCompletionAccepted(message: ChatCompletionAcceptedMessage): void { const telemetry = getChatAutocompleteTelemetry() telemetry.captureAcceptSuggestion(message.suggestionLength) } diff --git a/packages/kilo-vscode/src/services/autocomplete/chat-autocomplete/handleChatCompletionRequest.ts b/packages/kilo-vscode/src/services/autocomplete/chat-autocomplete/handleChatCompletionRequest.ts index dec6ae3826d..92877150587 100644 --- a/packages/kilo-vscode/src/services/autocomplete/chat-autocomplete/handleChatCompletionRequest.ts +++ b/packages/kilo-vscode/src/services/autocomplete/chat-autocomplete/handleChatCompletionRequest.ts @@ -1,37 +1,42 @@ -import { ClineProvider } from "../../../core/webview/ClineProvider" -import { WebviewMessage } from "../../../shared/WebviewMessage" +import * as vscode from "vscode" import { VisibleCodeTracker } from "../context/VisibleCodeTracker" +import { FileIgnoreController } from "../shims/FileIgnoreController" import { ChatTextAreaAutocomplete } from "./ChatTextAreaAutocomplete" +import type { KiloConnectionService } from "../../cli-backend" + +export interface ChatCompletionRequestMessage { + type: "requestChatCompletion" + text?: string + requestId?: string +} + +export interface ChatCompletionResponseSender { + postMessage(message: { type: "chatCompletionResult"; text: string; requestId: string }): void +} /** * Handles a chat completion request from the webview. - * Captures visible code context and generates a FIM-based autocomplete suggestion. + * Captures visible code context and generates an autocomplete suggestion. */ export async function handleChatCompletionRequest( - message: WebviewMessage & { type: "requestChatCompletion" }, - provider: ClineProvider, - getCurrentCwd: () => string, + message: ChatCompletionRequestMessage, + responseSender: ChatCompletionResponseSender, + connectionService: KiloConnectionService, ): Promise { - try { - const userText = message.text || "" - const requestId = message.requestId || "" + const userText = message.text || "" + const requestId = message.requestId || "" - // Pass RooIgnoreController to respect .kilocodeignore patterns - const currentTask = provider.getCurrentTask() - const tracker = new VisibleCodeTracker(getCurrentCwd(), currentTask?.rooIgnoreController ?? null) + const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? "" + const ignoreController = new FileIgnoreController() + await ignoreController.initialize() - const visibleContext = await tracker.captureVisibleCode() + const tracker = new VisibleCodeTracker(workspacePath, ignoreController) + const visibleContext = await tracker.captureVisibleCode() - const autocomplete = new ChatTextAreaAutocomplete(provider.providerSettingsManager) - const { suggestion } = await autocomplete.getCompletion(userText, visibleContext) + const autocomplete = new ChatTextAreaAutocomplete(connectionService) + const { suggestion } = await autocomplete.getCompletion(userText, visibleContext) - await provider.postMessageToWebview({ type: "chatCompletionResult", text: suggestion, requestId }) - } catch (error) { - provider.log(`Error getting chat completion: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`) - await provider.postMessageToWebview({ - type: "chatCompletionResult", - text: "", - requestId: message.requestId || "", - }) - } + responseSender.postMessage({ type: "chatCompletionResult", text: suggestion, requestId }) + + ignoreController.dispose() } diff --git a/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/AutocompleteInlineCompletionProvider.ts b/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/AutocompleteInlineCompletionProvider.ts index e24244e7091..e5c70619801 100644 --- a/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/AutocompleteInlineCompletionProvider.ts +++ b/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/AutocompleteInlineCompletionProvider.ts @@ -20,11 +20,10 @@ import { ContextRetrievalService } from "../continuedev/core/autocomplete/contex import { VsCodeIde } from "../continuedev/core/vscode-test-harness/src/VSCodeIde" import { RecentlyVisitedRangesService } from "../continuedev/core/vscode-test-harness/src/autocomplete/RecentlyVisitedRangesService" import { RecentlyEditedTracker } from "../continuedev/core/vscode-test-harness/src/autocomplete/recentlyEdited" -import type { AutocompleteServiceSettings } from "@roo-code/types" +import type { AutocompleteServiceSettings } from "../AutocompleteServiceManager" import { postprocessAutocompleteSuggestion } from "./uselessSuggestionFilter" import { shouldSkipAutocomplete } from "./contextualSkip" -import { RooIgnoreController } from "../../../core/ignore/RooIgnoreController" -import { ClineProvider } from "../../../core/webview/ClineProvider" +import { FileIgnoreController } from "../shims/FileIgnoreController" import { AutocompleteTelemetry } from "./AutocompleteTelemetry" const MAX_SUGGESTIONS_HISTORY = 20 @@ -251,8 +250,8 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple public suggestionsHistory: FillInAtCursorSuggestion[] = [] /** Tracks all pending/in-flight requests */ private pendingRequests: PendingRequest[] = [] - public holeFiller: HoleFiller // publicly exposed for Jetbrains autocomplete code - public fimPromptBuilder: FimPromptBuilder // publicly exposed for Jetbrains autocomplete code + private holeFiller: HoleFiller + private fimPromptBuilder: FimPromptBuilder private model: AutocompleteModel private costTrackingCallback: CostTrackingCallback private getSettings: () => AutocompleteServiceSettings | null @@ -260,7 +259,7 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple private recentlyEditedTracker: RecentlyEditedTracker private debounceTimer: NodeJS.Timeout | null = null private isFirstCall: boolean = true - private ignoreController?: Promise + private ignoreController?: Promise private acceptedCommand: vscode.Disposable | null = null private debounceDelayMs: number = INITIAL_DEBOUNCE_DELAY_MS private latencyHistory: number[] = [] @@ -273,7 +272,7 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple model: AutocompleteModel, costTrackingCallback: CostTrackingCallback, getSettings: () => AutocompleteServiceSettings | null, - cline: ClineProvider, + workspacePath: string, telemetry: AutocompleteTelemetry | null = null, ) { this.telemetry = telemetry @@ -283,7 +282,7 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple // Create ignore controller internally this.ignoreController = (async () => { - const ignoreController = new RooIgnoreController(cline.cwd) + const ignoreController = new FileIgnoreController() await ignoreController.initialize() return ignoreController })() @@ -495,8 +494,7 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple if (!isAccessible) { return [] } - } catch (error) { - console.error("[AutocompleteInlineCompletionProvider] Error checking file access:", error) + } catch { // On error, assume file is ignored return [] } @@ -545,10 +543,9 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple } return stringToInlineCompletions(cachedResult?.text ?? "", position) - } catch (error) { + } catch { // only big catch at the top of the call-chain, if anything goes wrong at a lower level // do not catch, just let the error cascade - console.error("[AutocompleteInlineCompletionProvider] Error providing inline completion:", error) return [] } } @@ -706,7 +703,6 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple }, telemetryContext, ) - console.error("Error getting inline completion from LLM:", error) } } } diff --git a/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/AutocompleteTelemetry.ts b/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/AutocompleteTelemetry.ts index 1885b2be9b6..3c168e60ff3 100644 --- a/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/AutocompleteTelemetry.ts +++ b/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/AutocompleteTelemetry.ts @@ -1,7 +1,19 @@ -import { TelemetryService } from "@roo-code/telemetry" -import { TelemetryEventName } from "@roo-code/types" +import { TelemetryStub, type ITelemetryClient } from "../shims/TelemetryStub" import type { AutocompleteContext, CacheMatchType, FillInAtCursorSuggestion } from "../types" +export const TelemetryEventName = { + AUTOCOMPLETE_SUGGESTION_REQUESTED: "Autocomplete Suggestion Requested", + AUTOCOMPLETE_LLM_REQUEST_COMPLETED: "Autocomplete LLM Request Completed", + AUTOCOMPLETE_LLM_REQUEST_FAILED: "Autocomplete LLM Request Failed", + AUTOCOMPLETE_LLM_SUGGESTION_RETURNED: "Autocomplete LLM Suggestion Returned", + AUTOCOMPLETE_SUGGESTION_CACHE_HIT: "Autocomplete Suggestion Cache Hit", + AUTOCOMPLETE_ACCEPT_SUGGESTION: "Autocomplete Accept Suggestion", + AUTOCOMPLETE_SUGGESTION_FILTERED: "Autocomplete Suggestion Filtered", + AUTOCOMPLETE_UNIQUE_SUGGESTION_SHOWN: "Autocomplete Unique Suggestion Shown", + INLINE_ASSIST_AUTO_TASK: "Inline Assist Auto Task", + GHOST_SERVICE_DISABLED: "Ghost Service Disabled", +} as const + export type { AutocompleteContext, CacheMatchType, FillInAtCursorSuggestion } /** @@ -83,14 +95,17 @@ export class AutocompleteTelemetry { this.autocompleteType = autocompleteType } - private captureEvent(event: TelemetryEventName, properties?: Record): void { - if (TelemetryService.hasInstance()) { - const propsWithType = { - ...properties, - autocompleteType: this.autocompleteType, - } - TelemetryService.instance.captureEvent(event, propsWithType) + private telemetryClient: ITelemetryClient = new TelemetryStub() + + private captureEvent( + event: (typeof TelemetryEventName)[keyof typeof TelemetryEventName], + properties?: Record, + ): void { + const propsWithType = { + ...properties, + autocompleteType: this.autocompleteType, } + this.telemetryClient.captureEvent(event, propsWithType) } /** diff --git a/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/HoleFiller.ts b/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/HoleFiller.ts index 13752bac111..1e67e83e960 100644 --- a/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/HoleFiller.ts +++ b/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/HoleFiller.ts @@ -7,8 +7,7 @@ import { } from "../types" import { getProcessedSnippets } from "./getProcessedSnippets" import { formatSnippets } from "../continuedev/core/autocomplete/templating/formatting" -import { AutocompleteModel } from "../AutocompleteModel" -import { ApiStreamChunk } from "../../../api/transform/stream" +import { AutocompleteModel, ApiStreamChunk } from "../AutocompleteModel" export type { HoleFillerAutocompletePrompt, FillInAtCursorSuggestion, ChatCompletionResult } @@ -189,29 +188,23 @@ Return the COMPLETION tags` ): Promise { const { systemPrompt, userPrompt } = prompt let response = "" + let chunkCount = 0 const onChunk = (chunk: ApiStreamChunk) => { if (chunk.type === "text") { response += chunk.text + chunkCount++ } } - console.log("[HoleFiller] userPrompt:", userPrompt) - const usageInfo = await model.generateResponse(systemPrompt, userPrompt, onChunk) - console.log("response", response) - // Extract just the text from the response - prefix/suffix are handled by the caller const completionMatch = response.match(/([\s\S]*?)<\/COMPLETION>/i) const suggestionText = completionMatch ? (completionMatch[1] || "").replace(/<\/?COMPLETION>/gi, "") : "" const fillInAtCursorSuggestion = processSuggestion(suggestionText) - if (fillInAtCursorSuggestion.text) { - console.info("Final suggestion:", fillInAtCursorSuggestion) - } - return { suggestion: fillInAtCursorSuggestion, cost: usageInfo.cost, diff --git a/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/getProcessedSnippets.ts b/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/getProcessedSnippets.ts index cb5290018f8..b82d06d4abd 100644 --- a/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/getProcessedSnippets.ts +++ b/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/getProcessedSnippets.ts @@ -8,7 +8,7 @@ import { getDefinitionsFromLsp } from "../continuedev/core/vscode-test-harness/s import { DEFAULT_AUTOCOMPLETE_OPTS } from "../continuedev/core/util/parameters" import { getSnippets } from "../continuedev/core/autocomplete/templating/filtering" import { AutocompleteModel } from "../AutocompleteModel" -import { RooIgnoreController } from "../../../core/ignore/RooIgnoreController" +import { FileIgnoreController } from "../shims/FileIgnoreController" import { AutocompleteSnippet, AutocompleteSnippetType } from "../continuedev/core/autocomplete/types" function uriToFsPath(filepath: string): string { @@ -24,7 +24,7 @@ function hasFilepath(snippet: AutocompleteSnippet): snippet is AutocompleteSnipp async function filterSnippetsByAccess( snippets: AutocompleteSnippet[], - ignoreController?: Promise, + ignoreController?: Promise, ): Promise { if (!ignoreController) { return snippets @@ -70,7 +70,7 @@ export async function getProcessedSnippets( contextService: ContextRetrievalService, model: AutocompleteModel, ide: VsCodeIde, - ignoreController?: Promise, + ignoreController?: Promise, ): Promise<{ filepathUri: string helper: any diff --git a/packages/kilo-vscode/src/services/autocomplete/context/VisibleCodeTracker.ts b/packages/kilo-vscode/src/services/autocomplete/context/VisibleCodeTracker.ts index 016413ef689..7003b04ed9f 100644 --- a/packages/kilo-vscode/src/services/autocomplete/context/VisibleCodeTracker.ts +++ b/packages/kilo-vscode/src/services/autocomplete/context/VisibleCodeTracker.ts @@ -10,9 +10,12 @@ import * as vscode from "vscode" -import { toRelativePath } from "../../../utils/path" import { isSecurityConcern } from "../continuedev/core/indexing/ignore" -import type { RooIgnoreController } from "../../../core/ignore/RooIgnoreController" +import type { FileIgnoreController } from "../shims/FileIgnoreController" + +function toRelativePath(absolutePath: string, workspacePath: string): string { + return vscode.workspace.asRelativePath(absolutePath, false) || absolutePath.replace(workspacePath + "/", "") +} import { VisibleCodeContext, VisibleEditorInfo, VisibleRange, DiffInfo } from "../types" @@ -24,7 +27,7 @@ export class VisibleCodeTracker { constructor( private workspacePath: string, - private rooIgnoreController: RooIgnoreController | null = null, + private ignoreController: FileIgnoreController | null = null, ) {} /** @@ -56,7 +59,7 @@ export class VisibleCodeTracker { console.log(`[VisibleCodeTracker] Filtered (security): ${relativePath}`) continue } - if (this.rooIgnoreController && !this.rooIgnoreController.validateAccess(relativePath)) { + if (this.ignoreController && !this.ignoreController.validateAccess(relativePath)) { console.log(`[VisibleCodeTracker] Filtered (.kilocodeignore): ${relativePath}`) continue } diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/CompletionProvider.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/CompletionProvider.ts index 3476ab6c208..8871fc5e0fb 100644 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/CompletionProvider.ts +++ b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/CompletionProvider.ts @@ -1,6 +1,5 @@ import { MinimalConfigProvider } from "./MinimalConfig.js" import { IDE, ILLM } from "../index.js" -import { OpenAI } from "../llm/llms/OpenAI.js" import { DEFAULT_AUTOCOMPLETE_OPTS } from "../util/parameters.js" import { shouldCompleteMultiline } from "./classification/shouldCompleteMultiline.js" import { ContextRetrievalService } from "./context/ContextRetrievalService.js" @@ -69,10 +68,6 @@ export class CompletionProvider { llm.completionOptions.temperature = 0.01 } - if (llm instanceof OpenAI) { - llm.useLegacyCompletionsEndpoint = true - } - return llm } diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/context/root-path-context/RootPathContextService.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/context/root-path-context/RootPathContextService.ts index 9fb1444ec07..e4ae749c279 100644 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/context/root-path-context/RootPathContextService.ts +++ b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/context/root-path-context/RootPathContextService.ts @@ -1,7 +1,10 @@ import { createHash } from "crypto" import { LRUCache } from "lru-cache" -import { Node as SyntaxNode, Query, Point } from "web-tree-sitter" +import type Parser from "web-tree-sitter" +type SyntaxNode = Parser.SyntaxNode +type Query = Parser.Query +type Point = Parser.Point import { IDE } from "../../.." import { getFullLanguageName, getQueryForFile, IGNORE_PATH_PATTERNS, LanguageName } from "../../../util/treeSitter" diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/context/static-context/StaticContextService.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/context/static-context/StaticContextService.ts index 40e5f02f76c..3a3609ccaf2 100644 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/context/static-context/StaticContextService.ts +++ b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/context/static-context/StaticContextService.ts @@ -1,7 +1,8 @@ import * as fs from "fs/promises" import path from "path" import { pathToFileURL } from "url" -import { Node as SyntaxNode } from "web-tree-sitter" +import type Parser from "web-tree-sitter" +type SyntaxNode = Parser.SyntaxNode import { IDE, Position } from "../../.." import { localPathOrUriToPath } from "../../../util/pathToUri" import { getFullLanguageName, getQueryForFile } from "../../../util/treeSitter" diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/context/static-context/tree-sitter-utils.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/context/static-context/tree-sitter-utils.ts index 41699a9efb6..857d8707d30 100644 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/context/static-context/tree-sitter-utils.ts +++ b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/context/static-context/tree-sitter-utils.ts @@ -1,5 +1,8 @@ import * as fs from "fs/promises" -import { Node as SyntaxNode, QueryMatch, Tree } from "web-tree-sitter" +import type Parser from "web-tree-sitter" +type SyntaxNode = Parser.SyntaxNode +type QueryMatch = Parser.QueryMatch +type Tree = Parser.Tree import { getFullLanguageName, getQueryForFile } from "../../../util/treeSitter" import { getAst } from "../../util/ast" diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/context/static-context/types.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/context/static-context/types.ts index 7db06d1df8c..15284f8b0e0 100644 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/context/static-context/types.ts +++ b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/context/static-context/types.ts @@ -1,4 +1,5 @@ -import { Tree } from "web-tree-sitter" +import type Parser from "web-tree-sitter" +type Tree = Parser.Tree import { Range } from "../../.." export interface TypeSpanAndSourceFile { diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/util/ast.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/util/ast.ts index 466670ea5c5..b58f9961124 100644 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/util/ast.ts +++ b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/util/ast.ts @@ -1,4 +1,6 @@ -import { Node as SyntaxNode, Tree } from "web-tree-sitter" +import type Parser from "web-tree-sitter" +type SyntaxNode = Parser.SyntaxNode +type Tree = Parser.Tree import { getParserForFile } from "../../util/treeSitter" diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/util/openedFilesLruCache.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/util/openedFilesLruCache.ts index fde95409599..b30190a7a73 100644 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/util/openedFilesLruCache.ts +++ b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/util/openedFilesLruCache.ts @@ -1,3 +1,4 @@ +// @ts-expect-error quick-lru is ESM-only; esbuild handles the import at build time import QuickLRU from "quick-lru" // maximum number of open files that can be cached diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/index.d.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/index.d.ts index 9b950bc3220..014efae8599 100644 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/index.d.ts +++ b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/index.d.ts @@ -4,6 +4,16 @@ import Parser from "web-tree-sitter" export type ModelRole = "chat" | "edit" | "autocomplete" | "apply" | "repoMapFileSelection" export type PromptTemplates = Record +// Stub types referenced but not defined in upstream continuedev +export type MockMessage = ChatMessage +export interface IFimProvider { + streamFim(prefix: string, suffix: string, signal: AbortSignal): AsyncGenerator +} +export interface ParameterInformation { + label: string | [number, number] + documentation?: string +} + declare global { interface Window { ide?: "vscode" @@ -311,13 +321,13 @@ export interface CustomLLMWithOptionals { prompt: string, signal: AbortSignal, options: CompletionOptions, - fetch: (input: RequestInfo | URL, init?: RequestInit) => Promise, + fetch: (input: string | URL, init?: RequestInit) => Promise, ) => AsyncGenerator streamChat?: ( messages: ChatMessage[], signal: AbortSignal, options: CompletionOptions, - fetch: (input: RequestInfo | URL, init?: RequestInit) => Promise, + fetch: (input: string | URL, init?: RequestInit) => Promise, ) => AsyncGenerator } diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/countTokens.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/countTokens.ts index f37012aaa3a..6cf1f8805f8 100644 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/countTokens.ts +++ b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/countTokens.ts @@ -1,3 +1,4 @@ +// @ts-expect-error js-tiktoken is ESM-only; esbuild handles the import at build time import { Tiktoken, encodingForModel as _encodingForModel } from "js-tiktoken" import { ChatMessage, CompiledMessagesResult, MessageContent } from "../index.js" import { addSpaceToAnyEmptyMessages, chatMessageIsEmpty } from "./messages.js" diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/index.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/index.ts index ce0350c2666..8888ab01604 100644 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/index.ts +++ b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/index.ts @@ -1,5 +1,4 @@ import { findLlmInfo } from "./model-info" -import { BaseLlmApi, constructLlmApi } from "./openai-adapters" import { ChatCompletionCreateParams } from "openai/resources/index" import { @@ -93,7 +92,7 @@ export abstract class BaseLLM implements ILLM { private _llmOptions: LLMOptions - protected openaiAdapter?: BaseLlmApi + protected openaiAdapter?: any constructor(_options: LLMOptions) { this._llmOptions = _options @@ -137,26 +136,13 @@ export abstract class BaseLLM implements ILLM { } this.capabilities = options.capabilities - this.openaiAdapter = this.createOpenAiAdapter() - this.autocompleteOptions = options.autocompleteOptions - - // openaiAdapter is initialized above } get contextLength() { return this._contextLength ?? DEFAULT_CONTEXT_LENGTH } - protected createOpenAiAdapter() { - return constructLlmApi({ - provider: this.providerName as any, - apiKey: this.apiKey ?? "", - apiBase: this.apiBase, - env: this._llmOptions.env, - }) - } - private _templatePromptLikeMessages(prompt: string): string { if (!this.templateMessages) { return prompt @@ -629,7 +615,9 @@ export abstract class BaseLLM implements ILLM { // Standard OpenAI format if (results.data && Array.isArray(results.data)) { - return results.data.sort((a, b) => a.index - b.index).map((result) => result.relevance_score) + return results.data + .sort((a: { index: number }, b: { index: number }) => a.index - b.index) + .map((result: { relevance_score: number }) => result.relevance_score) } throw new Error( diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/llms/KiloCode.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/llms/KiloCode.ts deleted file mode 100644 index b9ed5fe622a..00000000000 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/llms/KiloCode.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { ChatMessage, CompletionOptions, LLMOptions } from "../../index.js" -import { X_KILOCODE_VERSION } from "../../../../../../shared/kilocode/headers" -import { Package } from "../../../../../../shared/package" -import OpenRouter from "./OpenRouter" -import { IFimProvider } from "../../../../../../api/providers/kilocode/IFimProvider" -import { getKiloUrlFromToken } from "@roo-code/types" - -/** - * Extended CompletionOptions to include KiloCode-specific per-request metadata - */ -export interface KiloCodeCompletionOptions extends CompletionOptions { - kilocodeTaskId?: string - kilocodeProjectId?: string -} - -/** - * KiloCode LLM provider that extends OpenRouter with KiloCode-specific features: - * - Custom base URL using getKiloUrlFromToken() - * - KiloCode-specific headers (organizationId, taskId, projectId, version, tester) - * - Support for both static (organizationId) and per-request (taskId, projectId) metadata - * - * This provider maintains API parity with the kilocode-openrouter API provider - * while working within the continuedev LLM architecture. - */ -class KiloCode extends OpenRouter { - static override providerName = "kilocode" - - // Instance variables to store per-request metadata - private currentTaskId?: string - private currentProjectId?: string - public fimProvider?: IFimProvider - - constructor(options: LLMOptions) { - // Extract KiloCode-specific config from env - const kilocodeToken = options.apiKey ?? "" - - // Extract fimProvider before passing to parent - const { fimProvider, ...parentOptions } = options - - // Transform apiBase to use KiloCode backend - const transformedOptions = { - ...parentOptions, - apiBase: getKiloUrlFromToken("https://api.kilo.ai/api/openrouter/v1/", kilocodeToken), - } - - super(transformedOptions) - - // Use provided handler or create a new one if not provided - this.fimProvider = fimProvider - } - - /** - * Override _streamChat to extract per-request metadata from options - * This allows dynamic taskId and projectId per request - */ - protected override async *_streamChat( - messages: ChatMessage[], - signal: AbortSignal, - options: CompletionOptions, - ): AsyncGenerator { - // Extract KiloCode metadata from options if available - const kilocodeOptions = options as KiloCodeCompletionOptions - this.currentTaskId = kilocodeOptions.kilocodeTaskId - this.currentProjectId = kilocodeOptions.kilocodeProjectId - - try { - // Call parent implementation - yield* super._streamChat(messages, signal, options) - } finally { - // Clear per-request metadata after stream completes - this.currentTaskId = undefined - this.currentProjectId = undefined - } - } - - /** - * Override _streamComplete to support per-request metadata - */ - protected override async *_streamComplete( - prompt: string, - signal: AbortSignal, - options: CompletionOptions, - ): AsyncGenerator { - // Extract metadata (same pattern as _streamChat) - const kilocodeOptions = options as KiloCodeCompletionOptions - this.currentTaskId = kilocodeOptions.kilocodeTaskId - this.currentProjectId = kilocodeOptions.kilocodeProjectId - - try { - yield* super._streamComplete(prompt, signal, options) - } finally { - // Clear metadata - this.currentTaskId = undefined - this.currentProjectId = undefined - } - } - - /** - * Override _streamFim to delegate to IFimProvider - * This reuses the FIM implementation from the API handler - */ - protected override async *_streamFim( - prefix: string, - suffix: string, - signal: AbortSignal, - options: CompletionOptions, - ): AsyncGenerator { - if (!this.fimProvider) { - throw new Error("FIM provider not initialized") - } - - // Extract metadata (same pattern as _streamChat) - const kilocodeOptions = options as KiloCodeCompletionOptions - this.currentTaskId = kilocodeOptions.kilocodeTaskId - this.currentProjectId = kilocodeOptions.kilocodeProjectId - - try { - // Delegate to FIM provider's streamFim method - yield* this.fimProvider.streamFim(prefix, suffix, this.currentTaskId) - } finally { - // Clear metadata - this.currentTaskId = undefined - this.currentProjectId = undefined - } - } - - /** - * Override _getHeaders to inject KiloCode-specific headers - * Delegates to FIM provider's customRequestOptions() for consistency - */ - protected override _getHeaders() { - const baseHeaders = super._getHeaders() - - // Always add version header - const kilocodeHeaders: Record = { - [X_KILOCODE_VERSION]: Package.version, - } - - // Delegate to FIM provider's customRequestOptions for other KiloCode headers - // Only call if we have a taskId (required by the metadata interface) - if (this.fimProvider && this.currentTaskId) { - const customOptions = this.fimProvider.customRequestOptions({ - taskId: this.currentTaskId, - projectId: this.currentProjectId, - mode: "code", // Default mode for LLM operations - }) - - if (customOptions?.headers) { - Object.assign(kilocodeHeaders, customOptions.headers) - } - } - - return { - ...baseHeaders, - ...kilocodeHeaders, - } - } - - override supportsFim(): boolean { - return this.fimProvider?.supportsFim() || false - } -} - -export default KiloCode diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/llms/Mistral.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/llms/Mistral.ts deleted file mode 100644 index 179a2d64fbc..00000000000 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/llms/Mistral.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { ChatMessage, LLMOptions } from "../../index.js" - -import { OpenAI } from "./OpenAI" - -type MistralApiKeyType = "mistral" | "codestral" - -class Mistral extends OpenAI { - static override providerName = "mistral" - static defaultOptions: Partial = { - apiBase: "https://api.mistral.ai/v1/", - model: "codestral-latest", - } - - private async autodetectApiKeyType(): Promise { - const mistralResp = await fetch("https://api.mistral.ai/v1/models", { - method: "GET", - headers: this._getHeaders(), - }) - if (mistralResp.status === 401) { - return "codestral" - } - return "mistral" - } - - constructor(options: LLMOptions) { - super(options) - if (options.model.includes("codestral") && !options.model.includes("mamba")) { - this.apiBase = options.apiBase ?? "https://codestral.mistral.ai/v1/" - } - - if (!this.apiBase?.endsWith("/")) { - this.apiBase += "/" - } - - // Unless the user explicitly specifies, we will autodetect the API key type and adjust the API base accordingly - if (!options.apiBase) { - this.autodetectApiKeyType() - .then((keyType) => { - switch (keyType) { - case "codestral": - this.apiBase = "https://codestral.mistral.ai/v1/" - break - case "mistral": - this.apiBase = "https://api.mistral.ai/v1/" - break - } - - this.openaiAdapter = this.createOpenAiAdapter() - }) - .catch((err: any) => {}) - } - } - - private static modelConversion: { [key: string]: string } = { - "mistral-7b": "open-mistral-7b", - "mistral-8x7b": "open-mixtral-8x7b", - } - protected override _convertModelName(model: string): string { - return Mistral.modelConversion[model] ?? model - } - - override _convertArgs(options: any, messages: ChatMessage[]) { - const finalOptions = super._convertArgs(options, messages) - - const lastMessage = finalOptions.messages[finalOptions.messages.length - 1] - if (lastMessage?.role === "assistant") { - ;(lastMessage as any).prefix = true - } - - return finalOptions - } - - override supportsFim(): boolean { - return true - } -} - -export default Mistral diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/llms/OpenAI.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/llms/OpenAI.ts deleted file mode 100644 index b321c7fb191..00000000000 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/llms/OpenAI.ts +++ /dev/null @@ -1,273 +0,0 @@ -import { ChatCompletionCreateParams, ChatCompletionMessageParam } from "openai/resources/index" - -import { streamSse } from "../../fetch/stream.js" -import { ChatMessage, CompletionOptions, LLMOptions } from "../../index.js" -import { renderChatMessage } from "../../util/messageContent.js" -import { BaseLLM } from "../index.js" -import { fromChatCompletionChunk, LlmApiRequestType, toChatBody } from "../openaiTypeConverters.js" - -const NON_CHAT_MODELS = [ - "text-davinci-002", - "text-davinci-003", - "code-davinci-002", - "text-ada-001", - "text-babbage-001", - "text-curie-001", - "davinci", - "curie", - "babbage", - "ada", -] - -function isChatOnlyModel(model: string): boolean { - // gpt and o-series models - return model.startsWith("gpt") || model.startsWith("o") -} - -const formatMessageForO1OrGpt5 = (messages: ChatCompletionMessageParam[]) => { - return messages?.map((message: any) => { - if (message?.role === "system") { - return { - ...message, - role: "developer", - } - } - - return message - }) -} - -export class OpenAI extends BaseLLM { - public useLegacyCompletionsEndpoint: boolean | undefined = undefined - - constructor(options: LLMOptions) { - super({ - apiBase: "https://api.openai.com/v1/", - ...options, - }) - this.useLegacyCompletionsEndpoint = options.useLegacyCompletionsEndpoint - // Azure apiVersion removed from narrowed LLMOptions; not used - } - - static override providerName = "openai" - - protected override useOpenAIAdapterFor: (LlmApiRequestType | "*")[] = ["chat", "rerank", "streamChat", "streamFim"] - - protected _convertModelName(model: string): string { - return model - } - - public isOSeriesOrGpt5Model(model?: string): boolean { - return !!model && (!!model.match(/^o[0-9]+/) || model.includes("gpt-5")) - } - - protected extraBodyProperties(): Record { - return {} - } - - protected getMaxStopWords(): number { - const url = new URL(this.apiBase!) - - if (this.maxStopWords !== undefined) { - return this.maxStopWords - } else if (url.host === "api.deepseek.com") { - return 16 - } else if (url.port === "1337" || url.host === "api.openai.com" || url.host === "api.groq.com") { - return 4 - } else { - return Infinity - } - } - - protected _convertArgs(options: CompletionOptions, messages: ChatMessage[]): ChatCompletionCreateParams { - const finalOptions = toChatBody(messages, options) - - finalOptions.stop = options.stop?.slice(0, this.getMaxStopWords()) - - // OpenAI o1-preview and o1-mini or o3-mini: - if (this.isOSeriesOrGpt5Model(options.model)) { - // a) use max_completion_tokens instead of max_tokens - finalOptions.max_completion_tokens = options.maxTokens - finalOptions.max_tokens = undefined - - // b) don't support system message - finalOptions.messages = formatMessageForO1OrGpt5(finalOptions.messages) - } - - if (options.model === "o1") { - finalOptions.stream = false - } - - return finalOptions - } - - protected _getHeaders() { - return { - "Content-Type": "application/json", - ...(this.apiKey && { Authorization: `Bearer ${this.apiKey}` }), - "api-key": this.apiKey ?? "", // For Azure - } - } - - protected override async _complete(prompt: string, signal: AbortSignal, options: CompletionOptions): Promise { - let completion = "" - for await (const chunk of this._streamChat([{ role: "user", content: prompt }], signal, options)) { - completion += chunk.content - } - - return completion - } - - protected _getEndpoint(endpoint: "chat/completions" | "completions" | "models") { - if (!this.apiBase) { - throw new Error("No API base URL provided. Please set the 'apiBase' option in config.json") - } - return new URL(endpoint, this.apiBase) - } - - protected override async *_streamComplete( - prompt: string, - signal: AbortSignal, - options: CompletionOptions, - ): AsyncGenerator { - for await (const chunk of this._streamChat([{ role: "user", content: prompt }], signal, options)) { - yield renderChatMessage(chunk) - } - } - - protected override modifyChatBody(body: ChatCompletionCreateParams): ChatCompletionCreateParams { - body.stop = body.stop?.slice(0, this.getMaxStopWords()) - - // OpenAI o1-preview and o1-mini or o3-mini: - if (this.isOSeriesOrGpt5Model(body.model)) { - // a) use max_completion_tokens instead of max_tokens - body.max_completion_tokens = body.max_tokens - body.max_tokens = undefined - - // b) don't support system message - body.messages = formatMessageForO1OrGpt5(body.messages) - } - - if (body.model === "o1") { - // o1 doesn't support streaming - body.stream = false - } - - return body - } - - protected async *_legacystreamComplete( - prompt: string, - signal: AbortSignal, - options: CompletionOptions, - ): AsyncGenerator { - const args: any = this._convertArgs(options, []) - args.prompt = prompt - args.messages = undefined - - const response = await fetch(this._getEndpoint("completions"), { - method: "POST", - headers: this._getHeaders(), - body: JSON.stringify({ - ...args, - stream: true, - ...this.extraBodyProperties(), - }), - signal, - }) - - for await (const value of streamSse(response)) { - if (value.choices?.[0]?.text && value.finish_reason !== "eos") { - yield value.choices[0].text - } - } - } - - protected override async *_streamChat( - messages: ChatMessage[], - signal: AbortSignal, - options: CompletionOptions, - ): AsyncGenerator { - if ( - !isChatOnlyModel(options.model) && - this.supportsCompletions() && - (NON_CHAT_MODELS.includes(options.model) || this.useLegacyCompletionsEndpoint || options.raw) - ) { - for await (const content of this._legacystreamComplete( - renderChatMessage(messages[messages.length - 1]), - signal, - options, - )) { - yield { - role: "assistant", - content, - } - } - return - } - - const body = this._convertArgs(options, messages) - - const response = await fetch(this._getEndpoint("chat/completions"), { - method: "POST", - headers: this._getHeaders(), - body: JSON.stringify({ - ...body, - ...this.extraBodyProperties(), - }), - signal, - }) - - // Handle non-streaming response - if (body.stream === false) { - if (response.status === 499) { - return // Aborted by user - } - const data = await response.json() - yield data.choices[0].message - return - } - - for await (const value of streamSse(response)) { - const chunk = fromChatCompletionChunk(value) - if (chunk) { - yield chunk - } - } - } - - protected override async *_streamFim( - prefix: string, - suffix: string, - signal: AbortSignal, - options: CompletionOptions, - ): AsyncGenerator { - const endpoint = new URL("fim/completions", this.apiBase) - const resp = await fetch(endpoint, { - method: "POST", - body: JSON.stringify({ - model: options.model, - prompt: prefix, - suffix, - max_tokens: options.maxTokens, - temperature: options.temperature, - top_p: options.topP, - frequency_penalty: options.frequencyPenalty, - presence_penalty: options.presencePenalty, - stop: options.stop, - stream: true, - ...this.extraBodyProperties(), - }), - headers: { - "Content-Type": "application/json", - Accept: "application/json", - "x-api-key": this.apiKey ?? "", - Authorization: `Bearer ${this.apiKey}`, - }, - signal, - }) - for await (const chunk of streamSse(resp)) { - yield chunk.choices[0].delta.content - } - } -} diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/llms/OpenRouter.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/llms/OpenRouter.ts deleted file mode 100644 index 39afce51165..00000000000 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/llms/OpenRouter.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { ChatCompletionCreateParams } from "openai/resources/index" -import { LLMOptions } from "../../index.js" -import { osModelsEditPrompt } from "../templates/edit.js" -import { OpenAI } from "./OpenAI" - -class OpenRouter extends OpenAI { - static override providerName = "openrouter" - protected supportsReasoningField = true - protected supportsReasoningDetailsField = true - static defaultOptions: Partial = { - apiBase: "https://openrouter.ai/api/v1/", - model: "gpt-4o-mini", - promptTemplates: { - edit: osModelsEditPrompt, - }, - useLegacyCompletionsEndpoint: false, - } - - protected override extraBodyProperties(): Record { - if (this.completionOptions.reasoning === false) { - return { - reasoning: { - effort: "minimal", - }, - } - } - return {} - } - - /** - * Detect if the model is an Anthropic/Claude model - */ - private isAnthropicModel(model?: string): boolean { - if (!model) return false - const modelLower = model.toLowerCase() - return modelLower.includes("claude") - } - - /** - * Add cache_control to message content for Anthropic models - */ - private addCacheControlToContent(content: any, addCaching: boolean): any { - if (!addCaching) return content - - if (typeof content === "string") { - return [ - { - type: "text", - text: content, - cache_control: { type: "ephemeral" }, - }, - ] - } - - if (Array.isArray(content)) { - // For array content, add cache_control to the last text item - return content.map((part, idx) => { - if (part.type === "text" && idx === content.length - 1) { - return { - ...part, - cache_control: { type: "ephemeral" }, - } - } - return part - }) - } - - return content - } - - /** - * Override modifyChatBody to add Anthropic caching when appropriate - */ - protected override modifyChatBody(body: ChatCompletionCreateParams): ChatCompletionCreateParams { - // First apply parent modifications - body = super.modifyChatBody(body) - - // Check if we should apply Anthropic caching - if (!this.isAnthropicModel(body.model) || (!this.cacheBehavior && !this.completionOptions.promptCaching)) { - return body - } - - const shouldCacheConversation = this.cacheBehavior?.cacheConversation || this.completionOptions.promptCaching - const shouldCacheSystemMessage = this.cacheBehavior?.cacheSystemMessage || this.completionOptions.promptCaching - - if (!shouldCacheConversation && !shouldCacheSystemMessage) { - return body - } - - // Follow the same logic as Anthropic.ts: filter out system messages first - const filteredMessages = body.messages.filter((m: any) => m.role !== "system" && !!m.content) - - // Find the last two user message indices from the filtered array - const lastTwoUserMsgIndices = filteredMessages - .map((msg: any, index: number) => (msg.role === "user" ? index : -1)) - .filter((index: number) => index !== -1) - .slice(-2) - - // Create a mapping from filtered indices to original indices - let filteredIndex = 0 - const filteredToOriginalIndexMap: number[] = [] - body.messages.forEach((msg: any, originalIndex: number) => { - if (msg.role !== "system" && !!msg.content) { - filteredToOriginalIndexMap[filteredIndex] = originalIndex - filteredIndex++ - } - }) - - // Modify messages to add cache_control - body.messages = body.messages.map((message: any, idx) => { - // Handle system message caching - if (message.role === "system" && shouldCacheSystemMessage) { - return { - ...message, - content: this.addCacheControlToContent(message.content, true), - } - } - - // Handle conversation caching for last two user messages - // Check if this message's index (in filtered array) is one of the last two user messages - const filteredIdx = filteredToOriginalIndexMap.indexOf(idx) - if ( - message.role === "user" && - shouldCacheConversation && - filteredIdx !== -1 && - lastTwoUserMsgIndices.includes(filteredIdx) - ) { - return { - ...message, - content: this.addCacheControlToContent(message.content, true), - } - } - - return message - }) - - return body - } -} - -export default OpenRouter diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/Anthropic.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/Anthropic.ts deleted file mode 100644 index 719685b02a6..00000000000 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/Anthropic.ts +++ /dev/null @@ -1,347 +0,0 @@ -import { - ContentBlockParam, - MessageCreateParams, - MessageParam, - RawContentBlockDeltaEvent, - RawMessageDeltaEvent, - RawMessageStartEvent, - RawMessageStreamEvent, - Tool, -} from "@anthropic-ai/sdk/resources" -import { streamSse } from "../../../fetch/stream.js" -import { OpenAI } from "openai/index" -import { - ChatCompletion, - ChatCompletionChunk, - ChatCompletionContentPartRefusal, - ChatCompletionContentPartText, - ChatCompletionCreateParamsNonStreaming, - ChatCompletionCreateParamsStreaming, - Completion, - CompletionCreateParamsNonStreaming, - CompletionCreateParamsStreaming, - CompletionUsage, -} from "openai/resources/index" -import { ChatCompletionCreateParams } from "openai/resources/index.js" -import { AnthropicConfig } from "../types.js" -import { chatChunk, usageChatChunk } from "../util.js" -import { EMPTY_CHAT_COMPLETION } from "../util/emptyChatCompletion.js" -import { CACHING_STRATEGIES, CachingStrategyName } from "./AnthropicCachingStrategies.js" -import { - getAnthropicHeaders, - getAnthropicMediaTypeFromDataUrl, - openAiToolChoiceToAnthropicToolChoice, - openaiToolToAnthropicTool, -} from "./AnthropicUtils.js" -import { BaseLlmApi, CreateRerankResponse, FimCreateParamsStreaming, RerankCreateParams } from "./base.js" - -export class AnthropicApi implements BaseLlmApi { - apiBase: string = "https://api.anthropic.com/v1/" - - constructor( - protected config: AnthropicConfig & { - cachingStrategy?: CachingStrategyName - }, - ) { - this.apiBase = config.apiBase ?? this.apiBase - if (!this.apiBase.endsWith("/")) { - this.apiBase += "/" - } - } - - private _convertBody(oaiBody: ChatCompletionCreateParams) { - // Step 1: Convert to clean Anthropic body (no caching) - const cleanBody = this._convertToCleanAnthropicBody(oaiBody) - - // Step 2: Apply caching strategy - const cachingStrategy = CACHING_STRATEGIES[this.config.cachingStrategy ?? "systemAndTools"] - return cachingStrategy(cleanBody) - } - - private maxTokensForModel(model: string): number { - if (model.includes("haiku")) { - return 8192 - } - return 32_000 - } - - public _convertToCleanAnthropicBody(oaiBody: ChatCompletionCreateParams): MessageCreateParams { - let stop = undefined - if (oaiBody.stop && Array.isArray(oaiBody.stop)) { - stop = oaiBody.stop.filter((x) => x.trim() !== "") - } else if (typeof oaiBody.stop === "string" && oaiBody.stop.trim() !== "") { - stop = [oaiBody.stop] - } - - const systemMessage = oaiBody.messages.find((msg) => msg.role === "system")?.content - - // TODO support custom tools - const functionTools = oaiBody.tools?.filter((t) => t.type === "function") - let tools: Tool[] | undefined = undefined - - if (oaiBody.tool_choice !== "none" && functionTools && functionTools.length > 0) { - if (typeof oaiBody.tool_choice !== "string" && oaiBody.tool_choice?.type === "allowed_tools") { - const allowedToolNames = new Set(oaiBody.tool_choice?.allowed_tools.tools.map((tool) => tool["name"]) ?? []) - const allowedTools = functionTools.filter((t) => allowedToolNames.has(t.function.name)) - tools = allowedTools.map(openaiToolToAnthropicTool) - } else { - tools = functionTools.map(openaiToolToAnthropicTool) - } - } - - const anthropicBody: MessageCreateParams = { - messages: this._convertMessages(oaiBody.messages.filter((msg) => msg.role !== "system")), - system: - typeof systemMessage === "string" - ? [ - { - type: "text", - text: systemMessage, - }, - ] - : systemMessage, - top_p: oaiBody.top_p ?? undefined, - temperature: oaiBody.temperature ?? undefined, - max_tokens: oaiBody.max_tokens ?? this.maxTokensForModel(oaiBody.model), // max_tokens is required - model: oaiBody.model, - stop_sequences: stop, - stream: oaiBody.stream ?? undefined, - tools, - tool_choice: openAiToolChoiceToAnthropicToolChoice(oaiBody.tool_choice), - } - - return anthropicBody - } - - // 1. ignores empty content - // 2. converts string content to text parts - // 3. converts text and refusal parts to text blocks - // 4. converts image parts to image blocks - private convertMessageContentToBlocks( - content: - | string - | OpenAI.Chat.Completions.ChatCompletionContentPart[] - | (ChatCompletionContentPartText | ChatCompletionContentPartRefusal)[], - ): ContentBlockParam[] { - const blocks: ContentBlockParam[] = [] - if (typeof content === "string") { - if (content) { - blocks.push({ - type: "text", - text: content, - }) - } - } else { - const supportedParts = content.filter((p) => p.type === "text" || p.type === "image_url" || p.type === "refusal") - for (const part of supportedParts) { - if (part.type === "image_url") { - const dataUrl = part.image_url.url - if (dataUrl?.startsWith("data:")) { - blocks.push({ - type: "image", - source: { - type: "base64", - media_type: getAnthropicMediaTypeFromDataUrl(dataUrl), - data: dataUrl.split(",")[1], - }, - }) - } - } else { - const text = part.type === "text" ? part.text : part.refusal - if (text) { - blocks.push({ - type: "text", - text, - }) - } - } - } - } - return blocks - } - - private getContentBlocksFromChatMessage( - message: OpenAI.Chat.Completions.ChatCompletionMessageParam, - ): ContentBlockParam[] { - switch (message.role) { - // One tool message = one tool_result block - case "user": - return this.convertMessageContentToBlocks(message.content) - case "assistant": { - const blocks: ContentBlockParam[] = message.content ? this.convertMessageContentToBlocks(message.content) : [] - return blocks - } - // system, etc. - default: - return [] - } - } - - private _convertMessages(msgs: OpenAI.Chat.Completions.ChatCompletionMessageParam[]): MessageParam[] { - const nonSystemMessages = msgs.filter((m) => m.role !== "system") - - const convertedMessages: MessageParam[] = [] - let currentRole: "user" | "assistant" | undefined = undefined - let currentParts: ContentBlockParam[] = [] - - const flushCurrentMessage = () => { - if (currentRole && currentParts.length > 0) { - convertedMessages.push({ - role: currentRole, - content: currentParts, - }) - currentParts = [] - } - } - - for (const message of nonSystemMessages) { - const newRole = message.role === "user" || message.role === "tool" ? "user" : "assistant" - if (currentRole !== newRole) { - flushCurrentMessage() - currentRole = newRole - } - currentParts.push(...this.getContentBlocksFromChatMessage(message)) - } - flushCurrentMessage() - - return convertedMessages - } - - async chatCompletionNonStream( - body: ChatCompletionCreateParamsNonStreaming, - signal: AbortSignal, - ): Promise { - const response = await fetch(new URL("messages", this.apiBase), { - method: "POST", - headers: this.getHeaders(), - body: JSON.stringify(this._convertBody(body)), - signal, - }) - - if (response.status === 499) { - return EMPTY_CHAT_COMPLETION - } - - const completion = await response.json() - - const usage: Record | undefined = completion.usage - return { - id: completion.id, - object: "chat.completion", - model: body.model, - created: Date.now(), - usage: { - total_tokens: (usage?.input_tokens ?? 0) + (usage?.output_tokens ?? 0), - completion_tokens: usage?.output_tokens ?? 0, - prompt_tokens: usage?.input_tokens ?? 0, - prompt_tokens_details: { - cached_tokens: usage?.cache_read_input_tokens ?? 0, - }, - }, - choices: [ - { - logprobs: null, - finish_reason: "stop", - message: { - role: "assistant", - content: completion.content[0].text, - refusal: null, - }, - index: 0, - }, - ], - } - } - - // This is split off so e.g. VertexAI can use it - async *handleStreamResponse(response: any, model: string) { - const usage: CompletionUsage = { - completion_tokens: 0, - prompt_tokens: 0, - total_tokens: 0, - } - for await (const event of streamSse(response)) { - // https://docs.anthropic.com/en/api/messages-streaming#event-types - const rawEvent = event as RawMessageStreamEvent - switch (rawEvent.type) { - case "message_start": { - const startEvent = rawEvent as RawMessageStartEvent - usage.prompt_tokens = startEvent.message.usage?.input_tokens ?? 0 - usage.prompt_tokens_details = { - cached_tokens: startEvent.message.usage?.cache_read_input_tokens ?? 0, - } - break - } - case "message_delta": { - const deltaEvent = rawEvent as RawMessageDeltaEvent - usage.completion_tokens = deltaEvent.usage?.output_tokens ?? 0 - break - } - case "content_block_delta": { - // https://docs.anthropic.com/en/api/messages-streaming#delta-types - const blockDeltaEvent = rawEvent as RawContentBlockDeltaEvent - switch (blockDeltaEvent.delta.type) { - case "text_delta": - yield chatChunk({ - content: blockDeltaEvent.delta.text, - model, - }) - break - case "input_json_delta": - // Skip tool use delta - break - } - break - } - default: - break - } - } - - yield usageChatChunk({ - model, - usage: { - ...usage, - total_tokens: usage.completion_tokens + usage.prompt_tokens, - }, - }) - } - - async *chatCompletionStream( - body: ChatCompletionCreateParamsStreaming, - signal: AbortSignal, - ): AsyncGenerator { - const response = await fetch(new URL("messages", this.apiBase), { - method: "POST", - headers: this.getHeaders(), - body: JSON.stringify(this._convertBody(body)), - signal, - }) - yield* this.handleStreamResponse(response, body.model) - } - - private getHeaders(): Record { - const enableCaching = this.config?.cachingStrategy !== "none" - return getAnthropicHeaders(this.config.apiKey, enableCaching) - } - - async completionNonStream(_body: CompletionCreateParamsNonStreaming, _signal: AbortSignal): Promise { - throw new Error("Method not implemented.") - } - // eslint-disable-next-line require-yield - async *completionStream(_body: CompletionCreateParamsStreaming, _signal: AbortSignal): AsyncGenerator { - throw new Error("Method not implemented.") - } - // eslint-disable-next-line require-yield - async *fimStream(_body: FimCreateParamsStreaming, _signal: AbortSignal): AsyncGenerator { - throw new Error("Method not implemented.") - } - - async rerank(_body: RerankCreateParams): Promise { - throw new Error("Method not implemented.") - } - - list(): Promise { - throw new Error("Method not implemented.") - } -} diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/AnthropicCachingStrategies.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/AnthropicCachingStrategies.ts deleted file mode 100644 index 10135aaed95..00000000000 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/AnthropicCachingStrategies.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { MessageCreateParams } from "@anthropic-ai/sdk/resources" - -const MAX_CACHING_MESSAGES = 4 - -// Caching strategy type - transforms a clean Anthropic body by adding cache_control -type CachingStrategy = (anthropicBody: MessageCreateParams) => MessageCreateParams - -// Utility function to estimate token count -const estimateTokenCount = (text: string): number => Math.ceil(text.length / 4) - -// Strategy 1: No Caching -const noCachingStrategy: CachingStrategy = (body) => body - -// Strategy 2: System Messages Only -const systemOnlyStrategy: CachingStrategy = (body) => { - let availableCacheMessages = MAX_CACHING_MESSAGES - if (body.system && Array.isArray(body.system)) { - return { - ...body, - system: body.system.map((item) => { - if (availableCacheMessages > 0) { - availableCacheMessages -= 1 - return { - ...item, - cache_control: { type: "ephemeral" }, - } - } - return item - }), - } - } - return body -} - -// Strategy 3: System and Tools (High Impact) -const systemAndToolsStrategy: CachingStrategy = (body) => { - const result = { ...body } - let availableCacheMessages = MAX_CACHING_MESSAGES - - // Cache system messages - if (result.system && Array.isArray(result.system)) { - result.system = result.system.map((item) => { - if (availableCacheMessages > 0) { - availableCacheMessages -= 1 - return { - ...item, - cache_control: { type: "ephemeral" }, - } - } - return item - }) - } - - // Cache tool definitions - if (result.tools && Array.isArray(result.tools) && result.tools.length > 0) { - result.tools = result.tools.map((tool, index: number) => { - if (index === result.tools!.length - 1 && availableCacheMessages > 0) { - availableCacheMessages -= 1 - return { - ...tool, - cache_control: { type: "ephemeral" }, - } - } - return tool - }) - } - - return result -} - -// Strategy 4: Optimized (Intelligent Caching) -const optimizedStrategy: CachingStrategy = (body) => { - const result = { ...body } - let availableCacheMessages = MAX_CACHING_MESSAGES - - // Always cache system messages - if (result.system && Array.isArray(result.system)) { - result.system = result.system.map((item) => { - if (availableCacheMessages > 0) { - availableCacheMessages -= 1 - return { - ...item, - cache_control: { type: "ephemeral" }, - } - } - return item - }) - } - - // Cache tool definitions - if (result.tools && Array.isArray(result.tools) && result.tools.length > 0) { - result.tools = result.tools.map((tool, index: number) => { - if (index === result.tools!.length - 1 && availableCacheMessages > 0) { - availableCacheMessages -= 1 - return { - ...tool, - cache_control: { type: "ephemeral" }, - } - } - return tool - }) - } - - // Cache large messages (>500 tokens) - if (result.messages && Array.isArray(result.messages)) { - result.messages = result.messages.map((message) => { - if (message.content && typeof message.content === "string") { - const tokens = estimateTokenCount(message.content) - if (tokens > 500 && availableCacheMessages > 0) { - availableCacheMessages -= 1 - return { - ...message, - content: [ - { - type: "text", - text: message.content, - cache_control: { type: "ephemeral" as const }, - }, - ], - } - } - } else if (message.content && Array.isArray(message.content)) { - // Only add one cache control per message with array content - let addedCacheControl = false - const updatedContent = message.content.map((item) => { - if (item.type === "text" && item.text) { - const tokens = estimateTokenCount(item.text) - if (tokens > 500 && availableCacheMessages > 0 && !addedCacheControl) { - availableCacheMessages -= 1 - addedCacheControl = true - return { - ...item, - cache_control: { type: "ephemeral" as const }, - } - } - } - return item - }) - - return { - ...message, - content: updatedContent, - } - } - return message - }) - } - - return result -} - -// Available caching strategies -export const CACHING_STRATEGIES = { - none: noCachingStrategy, - systemOnly: systemOnlyStrategy, - systemAndTools: systemAndToolsStrategy, - optimized: optimizedStrategy, -} as const - -export type CachingStrategyName = keyof typeof CACHING_STRATEGIES diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/AnthropicUtils.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/AnthropicUtils.ts deleted file mode 100644 index e888c6b6752..00000000000 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/AnthropicUtils.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { Base64ImageSource, MessageParam, Tool, ToolChoice } from "@anthropic-ai/sdk/resources" -import { ChatCompletionTool, ChatCompletionToolChoiceOption } from "openai/resources" - -export function getAnthropicHeaders(apiKey: string, enableCaching: boolean): Record { - const headers: Record = { - "Content-Type": "application/json", - Accept: "application/json", - "anthropic-version": "2023-06-01", - "x-api-key": apiKey, - } - - if (enableCaching) { - headers["anthropic-beta"] = "prompt-caching-2024-07-31" - } - return headers -} - -export function addCacheControlToLastTwoUserMessages(messages: MessageParam[]) { - let userMessages = 0 - for (let i = messages.length - 1; i >= 0; i--) { - const msg = messages[i] - if (msg.role === "user") { - userMessages++ - if (typeof msg.content === "string") { - continue - } - for (let j = msg.content.length - 1; j >= 0; j--) { - const part = msg.content[j] - if (part.type === "text") { - part.cache_control = { type: "ephemeral" } - break - } - } - if (userMessages >= 2) { - break - } - } - } -} - -export function openAiToolChoiceToAnthropicToolChoice( - toolChoice: ChatCompletionToolChoiceOption | undefined, -): ToolChoice | undefined { - if (!toolChoice) { - return undefined - } - switch (toolChoice) { - case "none": - // "none" is handled by excluding tools - return undefined - case "required": - return { - type: "any", - } - case "auto": - return { - type: "auto", - } - default: - switch (toolChoice.type) { - case "allowed_tools": - // Filtering of tools is handled elsewhere - return { - type: toolChoice.allowed_tools.mode === "auto" ? "auto" : "any", - } - case "custom": - return undefined // TODO not supported yet - case "function": - return { - type: "tool", - name: toolChoice.function.name, - } - } - } -} - -export function openaiToolToAnthropicTool(tool: ChatCompletionTool): Tool { - if (tool.type === "function" && "function" in tool) { - return { - name: tool.function.name, - description: tool.function.description, - input_schema: tool.function.parameters as Tool.InputSchema, // TODO unsafe cast, may be differences between openai tool schema and anthropic tool schema, - } - } else { - throw new Error(`Unsupported tool type in Anthropic: ${tool.type}`) - } -} - -// Extract media type from data URL (ex. "data:image/png;base64,..." -> "image/png") -export function getAnthropicMediaTypeFromDataUrl(dataUrl: string): Base64ImageSource["media_type"] { - const match = dataUrl.match(/^data:([^;]+);base64,/) - if (match) { - switch (match[1]) { - case "image/png": - case "image/gif": - case "image/webp": - return match[1] - } - } - return "image/jpeg" -} diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/Azure.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/Azure.ts deleted file mode 100644 index 08914559fe8..00000000000 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/Azure.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { OpenAI } from "openai/index" -import { - ChatCompletionChunk, - ChatCompletionCreateParams, - ChatCompletionCreateParamsStreaming, -} from "openai/resources/index" -import { z } from "zod" -import { AzureConfigSchema } from "../types.js" -import { OpenAIApi } from "./OpenAI.js" - -export class AzureApi extends OpenAIApi { - constructor(azureConfig: z.infer) { - super({ - ...azureConfig, - provider: "openai", - }) - - const { baseURL, defaultQuery } = this._getAzureBaseURL(azureConfig) - - this.openai = new OpenAI({ - apiKey: azureConfig.apiKey, - baseURL, - defaultQuery, - }) - } - - /** - * Default is `azure-openai`, but previously was `azure` - * @param apiType - * @returns - */ - private _isAzureOpenAI(apiType?: string): boolean { - return apiType === "azure-openai" || apiType === "azure" - } - - private _getAzureBaseURL(config: z.infer): { - baseURL: string - defaultQuery: Record - } { - const url = new URL(this.apiBase) - - // Copy search params to separate object for OpenAI - const queryParams: Record = {} - for (const [key, value] of url.searchParams.entries()) { - queryParams[key] = value - } - - url.pathname = url.pathname.replace(/\/$/, "") // Remove trailing slash if present - url.search = "" // Clear original search params - - // Default is `azure-openai` in docs, but previously was `azure` - if (this._isAzureOpenAI(config.env?.apiType)) { - if (!config.env?.deployment) { - throw new Error("`env.deployment` is a required configuration property for Azure OpenAI") - } - - if (!config.env?.apiVersion) { - throw new Error("`env.apiVersion` is a required configuration property for Azure OpenAI") - } - - const basePathname = `openai/deployments/${config.env.deployment}` - - url.pathname = url.pathname === "/" ? basePathname : `${url.pathname}/${basePathname}` - - queryParams["api-version"] = config.env.apiVersion - } - - return { - baseURL: url.toString(), - defaultQuery: queryParams, - } - } - - /** - * Filters out empty text content parts from messages. - * - * Azure models may not support empty content parts, which can cause issues. - * This function removes any text content parts that are empty or contain only whitespace. - */ - private _filterEmptyContentParts(body: T): T { - const result = { ...body } - - result.messages = result.messages.map((message: any) => { - if (Array.isArray(message.content)) { - const filteredContent = message.content.filter((part: any) => { - return !(part.type === "text" && (!part.text || part.text.trim() === "")) - }) - return { - ...message, - content: filteredContent.length > 0 ? filteredContent : message.content, - } - } - return message - }) as any - - return result - } - - override modifyChatBody(body: T): T { - let modifiedBody = super.modifyChatBody(body) - modifiedBody = this._filterEmptyContentParts(modifiedBody) - return modifiedBody - } - - override async *chatCompletionStream( - body: ChatCompletionCreateParamsStreaming, - signal: AbortSignal, - ): AsyncGenerator { - const response = await this.openai.chat.completions.create(this.modifyChatBody(body), { signal }) - - for await (const result of response) { - // Skip chunks with no choices (common with Azure content filtering) - if (result.choices && result.choices.length > 0) { - yield result - } - } - } -} diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/Bedrock.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/Bedrock.ts deleted file mode 100644 index 1102924128e..00000000000 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/Bedrock.ts +++ /dev/null @@ -1,504 +0,0 @@ -import { - BedrockRuntimeClient, - ContentBlock, - ContentBlockStart, - ConversationRole, - ConverseStreamCommand, - ConverseStreamCommandInput, - ImageFormat, - InvokeModelCommand, - Message, - ToolConfiguration, -} from "@aws-sdk/client-bedrock-runtime" -import { OpenAI } from "openai/index" -import { randomUUID } from "node:crypto" - -import { - ChatCompletion, - ChatCompletionChunk, - ChatCompletionContentPartImage, - ChatCompletionCreateParams, - ChatCompletionCreateParamsNonStreaming, - ChatCompletionCreateParamsStreaming, - Completion, - CompletionCreateParamsNonStreaming, - CompletionCreateParamsStreaming, - Model, -} from "openai/resources/index" - -import { fromNodeProviderChain } from "@aws-sdk/credential-providers" -import { BedrockConfig } from "../types.js" -import { chatChunk, rerank } from "../util.js" -import { BaseLlmApi, CreateRerankResponse, FimCreateParamsStreaming, RerankCreateParams } from "./base.js" - -export class BedrockApi implements BaseLlmApi { - constructor(protected config: BedrockConfig) { - if (config.env?.accessKeyId || config?.env?.secretAccessKey) { - if (!config.env?.accessKeyId) { - throw new Error("accessKeyId is required for Bedrock API. Only found secretAccessKey") - } - if (!config.env?.secretAccessKey) { - throw new Error("secretAccessKey is required for Bedrock API. Only found accessKeyId") - } - } - } - - async getCreds() { - if (this.config?.env?.accessKeyId && this.config?.env?.secretAccessKey) { - return { - accessKeyId: this.config.env.accessKeyId, - secretAccessKey: this.config.env.secretAccessKey, - } - } - const profile = this.config.env?.profile ?? "bedrock" - try { - return await fromNodeProviderChain({ - profile: profile, - ignoreCache: true, - })() - } catch { - console.warn(`AWS profile with name ${profile} not found in ~/.aws/credentials, using default profile`) - } - return await fromNodeProviderChain()() - } - async getClient(): Promise { - const region = this.config.env?.region - - // If apiKey is provided, use bearer token authentication - if (this.config.apiKey) { - return new BedrockRuntimeClient({ - region, - credentials: { - accessKeyId: this.config.apiKey, - secretAccessKey: this.config.apiKey, - }, - }) - } - - // Otherwise use IAM credentials (existing behavior) - const creds = await this.getCreds() - return new BedrockRuntimeClient({ - region, - credentials: creds, - }) - } - - private _oaiPartToBedrockPart( - part: OpenAI.Chat.Completions.ChatCompletionContentPart | OpenAI.Chat.Completions.ChatCompletionContentPartRefusal, - ): ContentBlock { - switch (part.type) { - case "refusal": - return { - text: part.refusal, - } - case "text": - return { - text: part.text, - } - case "input_audio": - throw new Error("Unsupported part type: input_audio") - case "image_url": - default: - try { - const [mimeType, base64Data] = (part as ChatCompletionContentPartImage).image_url.url.split(",") - const format = mimeType.split("/")[1]?.split(";")[0] || "jpeg" - if ( - format === ImageFormat.JPEG || - format === ImageFormat.PNG || - format === ImageFormat.WEBP || - format === ImageFormat.GIF - ) { - return { - image: { - format, - source: { - bytes: Uint8Array.from(Buffer.from(base64Data, "base64")), - }, - }, - } - } else { - console.warn(`Bedrock: skipping unsupported image part format: ${format}`) - return { text: "[Unsupported image format]" } - } - } catch (error) { - console.warn("Bedrock: failed to process image part", error) - return { text: "[Failed to process image]" } - } - } - } - - private _convertMessages( - oaiMessages: OpenAI.Chat.Completions.ChatCompletionMessageParam[], - _availableTools: Set, - ): Message[] { - let currentRole: "user" | "assistant" = "user" - let currentBlocks: ContentBlock[] = [] - const converted: Message[] = [] - - const pushCurrentMessage = () => { - if (currentBlocks.length > 0) { - converted.push({ - role: currentRole, - content: currentBlocks, - }) - currentBlocks = [] - } - } - - const nonSystemMessages = oaiMessages.filter((m) => m.role !== "system") - - for (let idx = 0; idx < nonSystemMessages.length; idx++) { - const message = nonSystemMessages[idx] - - if (message.role === "user" || message.role === "tool") { - // Detect conversational turn change - if (currentRole !== ConversationRole.USER) { - pushCurrentMessage() - currentRole = ConversationRole.USER - } - - // USER messages - if (message.role === "user") { - const content = message.content - if (content) { - if (typeof content === "string") { - currentBlocks.push({ text: content }) - } else { - content.forEach((part) => { - currentBlocks.push(this._oaiPartToBedrockPart(part)) - }) - } - } - } - // TOOL messages - no longer supported, skip - } else if (message.role === "assistant") { - // Detect conversational turn change - if (currentRole !== ConversationRole.ASSISTANT) { - pushCurrentMessage() - currentRole = ConversationRole.ASSISTANT - } - - // ASSISTANT messages - if (typeof message.content === "string") { - const trimmedText = message.content.trim() - if (trimmedText) { - currentBlocks.push({ text: trimmedText }) - } - } else { - message.content?.forEach((part) => { - const text = part.type === "text" ? part.text : part.refusal - const trimmedText = text.trim() - if (trimmedText) { - currentBlocks.push({ text: trimmedText }) - } - }) - } - } - } - - if (currentBlocks.length > 0) { - pushCurrentMessage() - } - - // If caching is enabled, add cache points - // if (this.config.cacheBehavior?.cacheConversation) { - // this._addCachingToLastTwoUserMessages(converted); - // } - - return converted - } - - private _convertBody(oaiBody: ChatCompletionCreateParams): ConverseStreamCommandInput { - // Extract system message - const systemMessage = oaiBody.messages.find((msg) => msg.role === "system")?.content || "" - - const systemMessageText = - typeof systemMessage === "string" - ? systemMessage - : systemMessage.map((part) => (part.type === "text" ? part.text : "[Non-text content]")).join(" ") - - // Check for tools - const availableTools = new Set() - let toolConfig: ToolConfiguration | undefined = undefined - - if (oaiBody.tools && oaiBody.tools.length > 0) { - toolConfig = { - tools: oaiBody.tools.map((tool) => { - // Type guard for function tools - if (tool.type === "function" && "function" in tool) { - return { - toolSpec: { - name: tool.function.name, - description: tool.function.description, - inputSchema: { - json: tool.function.parameters, - }, - }, - } - } else { - throw new Error(`Unsupported tool type in Bedrock: ${tool.type}`) - } - }), - } as ToolConfiguration - - // Add cache point if needed - // if (this.config.cacheBehavior?.cacheSystemMessage) { - // toolConfig!.tools!.push({ cachePoint: { type: "default" } }); - // } - - oaiBody.tools.forEach((tool) => { - if (tool.type === "function" && "function" in tool) { - availableTools.add(tool.function.name) - } - }) - } - - // Convert messages - const convertedMessages = this._convertMessages(oaiBody.messages, availableTools) - - // Build final request body - const body: { - modelId: string - messages: Message[] - inferenceConfig: { - temperature?: number - topP?: number - maxTokens?: number - stopSequences?: string[] | undefined - } - system?: Array<{ text: string } | { cachePoint: { type: "default" } }> - toolConfig?: ToolConfiguration - } = { - modelId: oaiBody.model, - messages: convertedMessages, - inferenceConfig: { - temperature: oaiBody.temperature ?? undefined, - topP: oaiBody.top_p ?? undefined, - maxTokens: oaiBody.max_tokens ?? undefined, - stopSequences: Array.isArray(oaiBody.stop) - ? oaiBody.stop.filter((s) => s.trim() !== "").slice(0, 4) - : oaiBody.stop - ? [oaiBody.stop].filter((s) => s.trim() !== "") - : undefined, - }, - } - - // Add system message if present - if (systemMessageText) { - body.system = [{ text: systemMessageText }] - } - - // Add tool config if present - if (toolConfig) { - body.toolConfig = toolConfig - } - - // Add reasoning if needed - // TODO REASONING - // if (this.c) { - // body.additionalModelRequestFields = { - // thinking: { - // type: "enabled", - // budget_tokens: - // oaiBody.additionalModelRequestFields.reasoningBudgetTokens, - // }, - // }; - // } - - return body - } - - async chatCompletionNonStream( - body: ChatCompletionCreateParamsNonStreaming, - signal: AbortSignal, - ): Promise { - let completion = "" - - for await (const chunk of this.chatCompletionStream( - { - ...body, - stream: true, - }, - signal, - )) { - if (chunk.choices[0].delta.content) { - completion += chunk.choices[0].delta.content - } - } - - return { - id: randomUUID(), - object: "chat.completion", - model: body.model, - created: Date.now(), - choices: [ - { - index: 0, - logprobs: null, - finish_reason: "stop", - message: { - role: "assistant", - content: completion, - refusal: null, - }, - }, - ], - usage: undefined, - } - } - - async *chatCompletionStream( - body: ChatCompletionCreateParamsStreaming, - signal: AbortSignal, - ): AsyncGenerator { - const requestBody = this._convertBody(body) - - try { - const command = new ConverseStreamCommand({ - ...requestBody, - }) - - const client = await this.getClient() - const response = await client.send(command, { abortSignal: signal }) - - if (!response?.stream) { - throw new Error("No stream received from Bedrock API") - } - - for await (const chunk of response.stream) { - if (chunk.contentBlockDelta?.delta) { - type DeltaBlock = { - text?: string - reasoningContent?: { text?: string } - toolUse?: { toolUseId: string; name: string; input: string } - } - const delta = chunk.contentBlockDelta.delta as unknown as DeltaBlock - - // Handle text content - if (delta.text) { - yield chatChunk({ - content: delta.text, - model: body.model, - }) - continue - } - - // Handle thinking content (if reasoning enabled) - if (delta.reasoningContent?.text) { - // TODO reasoning - // Reasoning is not directly supported in OpenAI format, - // but we could add it as a special message - continue - } - - // Skip tool use blocks - } - - if (chunk.contentBlockStart?.start) { - const start: ContentBlockStart = chunk.contentBlockStart.start - - if (start.toolUse) { - // Skip tool use - continue - } - } - } - } catch (error) { - if (error instanceof Error) { - if ("code" in error) { - const code = (error as { code?: string }).code - throw new Error(`AWS Bedrock stream error (${code ?? "UNKNOWN"}): ${error.message}`) - } - throw new Error(`Error processing Bedrock stream: ${error.message}`) - } - throw new Error("Error processing Bedrock stream: Unknown error occurred") - } - } - - completionNonStream(_body: CompletionCreateParamsNonStreaming): Promise { - throw new Error("Bedrock does not support completions API") - } - - completionStream(_body: CompletionCreateParamsStreaming): AsyncGenerator { - throw new Error("Bedrock does not support completions API") - } - - fimStream(_body: FimCreateParamsStreaming): AsyncGenerator { - throw new Error("Bedrock does not support FIM directly") - } - - private async getInvokeModelResponseBody(model: string, jsonBody: object) { - const payload = { - body: JSON.stringify(jsonBody), - modelId: model, - accept: "*/*", - contentType: "application/json", - } - const command = new InvokeModelCommand(payload) - const client = await this.getClient() - const response = await client.send(command) - if (!response.body) { - throw new Error("No response body") - } - const decoder = new TextDecoder() - const decoded = decoder.decode(response.body) - return JSON.parse(decoded) - } - - async rerank(body: RerankCreateParams): Promise { - if (!body.query || !body.documents.length) { - throw new Error("Query and chunks must not be empty") - } - - // Base payload for both models - const payload: { - query: string - documents: string[] - top_n: number - api_version?: number - } = { - query: body.query, - documents: body.documents, - top_n: body.top_k ?? body.documents.length, - } - - // Add api_version for Cohere model - if (body.model.startsWith("cohere.rerank")) { - payload.api_version = 2 - } - - try { - const responseBody = await this.getInvokeModelResponseBody(body.model, payload) - const scores = ( - responseBody.results as Array<{ - index: number - relevance_score: number - }> - ) - .sort((a, b) => a.index - b.index) - .map((result) => result.relevance_score) - - return rerank({ - model: body.model, - usage: { - total_tokens: 0, - }, - data: scores, - }) - } catch (error) { - if (error instanceof Error) { - if ("code" in error) { - // AWS SDK specific errors - const code = (error as { code?: string }).code - throw new Error(`AWS Bedrock rerank error (${code ?? "UNKNOWN"}): ${error.message}`) - } - throw new Error(`Error in BedrockReranker.rerank: ${error.message}`) - } - throw new Error("Error in BedrockReranker.rerank: Unknown error occurred") - } - } - - list(): Promise { - throw new Error("Method not implemented.") - } -} diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/Cohere.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/Cohere.ts deleted file mode 100644 index 741de1c29a9..00000000000 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/Cohere.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { streamJSON } from "../../../fetch/stream.js" -import { OpenAI } from "openai/index" -import { - ChatCompletion, - ChatCompletionChunk, - ChatCompletionCreateParams, - ChatCompletionCreateParamsNonStreaming, - ChatCompletionCreateParamsStreaming, - Completion, - CompletionCreateParamsNonStreaming, - CompletionCreateParamsStreaming, - Model, -} from "openai/resources/index" -import { CohereConfig } from "../types.js" -import { chatCompletion } from "../util.js" -import { EMPTY_CHAT_COMPLETION } from "../util/emptyChatCompletion.js" -import { BaseLlmApi, CreateRerankResponse, FimCreateParamsStreaming, RerankCreateParams } from "./base.js" - -export class CohereApi implements BaseLlmApi { - apiBase: string = "https://api.cohere.com/v1" - - static maxStopSequences = 5 - - constructor(protected config: CohereConfig) { - this.apiBase = config.apiBase ?? this.apiBase - } - - private _convertMessages(msgs: OpenAI.Chat.Completions.ChatCompletionMessageParam[]): any[] { - return msgs.map((m) => ({ - role: m.role === "assistant" ? "CHATBOT" : "USER", - message: m.content, - })) - } - - private _convertBody(oaiBody: ChatCompletionCreateParams) { - return { - message: oaiBody.messages.pop()?.content, - chat_history: this._convertMessages(oaiBody.messages.filter((msg) => msg.role !== "system")), - preamble: oaiBody.messages.find((msg) => msg.role === "system")?.content, - model: oaiBody.model, - stream: oaiBody.stream, - temperature: oaiBody.temperature, - max_tokens: oaiBody.max_tokens, - p: oaiBody.top_p, - stop_sequences: oaiBody.stop?.slice(0, CohereApi.maxStopSequences), - frequency_penalty: oaiBody.frequency_penalty, - presence_penalty: oaiBody.presence_penalty, - } - } - - async chatCompletionNonStream( - body: ChatCompletionCreateParamsNonStreaming, - signal: AbortSignal, - ): Promise { - const headers = { - "Content-Type": "application/json", - Authorization: `Bearer ${this.config.apiKey}`, - } - - const resp = await fetch(new URL("chat", this.apiBase), { - method: "POST", - headers, - body: JSON.stringify(this._convertBody(body)), - signal, - }) - - if (resp.status === 499) { - return EMPTY_CHAT_COMPLETION - } - - const data = (await resp.json()) as any - const { input_tokens, output_tokens } = data.meta.tokens - return chatCompletion({ - model: body.model, - id: data.id, - content: data.text, - usage: { - total_tokens: input_tokens + output_tokens, - completion_tokens: output_tokens, - prompt_tokens: input_tokens, - }, - }) - } - - async *chatCompletionStream( - body: ChatCompletionCreateParamsStreaming, - signal: AbortSignal, - ): AsyncGenerator { - const headers = { - "Content-Type": "application/json", - Authorization: `Bearer ${this.config.apiKey}`, - } - - const resp = await fetch(new URL("chat", this.apiBase), { - method: "POST", - headers, - body: JSON.stringify(this._convertBody(body)), - signal, - }) - - for await (const value of streamJSON(resp as any)) { - if (value.event_type === "text-generation") { - yield { - id: value.id, - object: "chat.completion.chunk", - model: body.model, - created: Date.now(), - choices: [ - { - index: 0, - logprobs: undefined, - finish_reason: null, - delta: { - role: "assistant", - content: value.text, - }, - }, - ], - usage: undefined, - } - } - } - } - completionNonStream(_body: CompletionCreateParamsNonStreaming): Promise { - throw new Error("Method not implemented.") - } - completionStream(_body: CompletionCreateParamsStreaming): AsyncGenerator { - throw new Error("Method not implemented.") - } - fimStream(_body: FimCreateParamsStreaming): AsyncGenerator { - throw new Error("Method not implemented.") - } - async rerank(body: RerankCreateParams): Promise { - const endpoint = new URL("rerank", this.apiBase) - const response = await fetch(endpoint, { - method: "POST", - body: JSON.stringify(body), - headers: { - "Content-Type": "application/json", - Accept: "application/json", - "x-api-key": this.config.apiKey ?? "", - Authorization: `Bearer ${this.config.apiKey}`, - }, - }) - const data = (await response.json()) as any - return { - object: "list", - data: data.results.map((result: any) => ({ - index: result.index, - relevance_score: result.relevance_score, - })), - model: body.model, - usage: { - total_tokens: 0, - }, - } - } - - list(): Promise { - throw new Error("Method not implemented.") - } -} diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/CometAPI.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/CometAPI.ts deleted file mode 100644 index d7e91443fd3..00000000000 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/CometAPI.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { - ChatCompletion, - ChatCompletionChunk, - ChatCompletionCreateParamsNonStreaming, - ChatCompletionCreateParamsStreaming, - Completion, - CompletionCreateParamsNonStreaming, - CompletionCreateParamsStreaming, - Model, -} from "openai/resources/index" -import { CometAPIConfig } from "../types.js" -import { BaseLlmApi, CreateRerankResponse, FimCreateParamsStreaming, RerankCreateParams } from "./base.js" -import { OpenAIApi } from "./OpenAI.js" - -/** - * CometAPI adapter - extends OpenAI adapter since CometAPI is OpenAI-compatible - * - * CometAPI provides access to multiple LLM providers (GPT, Claude, Gemini, etc.) - * through a unified OpenAI-compatible API interface. - */ -export class CometAPIApi extends OpenAIApi implements BaseLlmApi { - // Store the original CometAPI config separately - - constructor(config: CometAPIConfig) { - // CometAPI uses OpenAI-compatible API, so we can reuse OpenAI adapter - // Convert CometAPI config to OpenAI-compatible config for the base class - const openAICompatibleConfig = { - ...config, - provider: "openai" as const, - apiBase: config.apiBase ?? "https://api.cometapi.com/v1/", - } - super(openAICompatibleConfig) - } - - /** - * Override list method to handle CometAPI-specific model filtering - * The core filtering logic is handled in the CometAPI provider class - */ - override async list(): Promise { - try { - return await super.list() - } catch (error) { - // Fallback to empty list if model listing fails - console.warn("CometAPI: Failed to fetch model list", error) - return [] - } - } - - /** - * Chat completion - uses OpenAI-compatible format - */ - override async chatCompletionNonStream( - body: ChatCompletionCreateParamsNonStreaming, - signal: AbortSignal, - ): Promise { - return super.chatCompletionNonStream(body, signal) - } - - /** - * Streaming chat completion - uses OpenAI-compatible format - */ - override async *chatCompletionStream( - body: ChatCompletionCreateParamsStreaming, - signal: AbortSignal, - ): AsyncGenerator { - yield* super.chatCompletionStream(body, signal) - } - - /** - * Legacy completion endpoint support - */ - override completionNonStream(body: CompletionCreateParamsNonStreaming, signal: AbortSignal): Promise { - return super.completionNonStream(body, signal) - } - - /** - * Legacy streaming completion endpoint support - */ - override completionStream(body: CompletionCreateParamsStreaming, signal: AbortSignal): AsyncGenerator { - return super.completionStream(body, signal) - } - - /** - * Fill-in-the-middle completion support - */ - override fimStream(body: FimCreateParamsStreaming, signal: AbortSignal): AsyncGenerator { - return super.fimStream(body, signal) - } - - /** - * Reranking support (if available through CometAPI) - */ - override async rerank(body: RerankCreateParams): Promise { - try { - return await super.rerank(body) - } catch (error) { - throw new Error(`CometAPI reranking not supported: ${error}`) - } - } -} diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/ContinueProxy.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/ContinueProxy.ts deleted file mode 100644 index a6cbe13a464..00000000000 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/ContinueProxy.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { - ChatCompletionCreateParams, - CompletionCreateParamsNonStreaming, - CompletionCreateParamsStreaming, -} from "openai/resources/index" -import { z } from "zod" -import { ContinueProxyConfigSchema } from "../types.js" -import { FimCreateParamsStreaming, RerankCreateParams } from "./base.js" -import { OpenAIApi } from "./OpenAI.js" - -interface ContinueProperties { - apiKeyLocation?: string - envSecretLocations?: Record - apiBase?: string - orgScopeId?: string | null - env?: Record -} - -export class ContinueProxyApi extends OpenAIApi { - // The apiKey and apiBase are set to the values for the proxy, - // but we need to keep track of the actual values that the proxy will use - // to call whatever LLM API is chosen - private actualApiBase?: string - - // Contains extra properties that we pass along to the proxy. Originally from `env` property on LLMOptions - private configEnv?: Record - - // Store the continue proxy config separately - private continueProxyConfig: z.infer - - constructor(config: z.infer) { - // Convert ContinueProxyConfigSchema to OpenAIConfigSchema format - const openaiConfig = { - provider: "openai" as const, - apiKey: config.apiKey, - apiBase: config.env?.proxyUrl - ? new URL("model-proxy/v1/", config.env?.proxyUrl).toString() - : "https://api.continue.dev/model-proxy/v1/", - } - - super(openaiConfig) - - this.continueProxyConfig = config - this.configEnv = config.env - this.actualApiBase = config.apiBase - } - - protected extraBodyProperties(): Record { - const continueProperties: ContinueProperties = { - apiKeyLocation: this.continueProxyConfig.env?.apiKeyLocation, - envSecretLocations: this.continueProxyConfig.env?.envSecretLocations, - apiBase: this.actualApiBase, - orgScopeId: this.continueProxyConfig.env?.orgScopeId ?? null, - env: this.configEnv, - } - return { - continueProperties, - } - } - - private modifyBodyWithContinueProperties>(body: T): T { - return { - ...body, - ...this.extraBodyProperties(), - } - } - - override modifyChatBody(body: T): T { - // First apply OpenAI-specific modifications - const modifiedBody = super.modifyChatBody(body) - // Then add Continue properties - return this.modifyBodyWithContinueProperties(modifiedBody) - } - - override modifyCompletionBody( - body: T, - ): T { - return this.modifyBodyWithContinueProperties(body) - } - - override modifyFimBody(body: T): T { - const modifiedBody = super.modifyFimBody(body) - return this.modifyBodyWithContinueProperties(modifiedBody) - } - - protected override getHeaders(): Record { - return { - "Content-Type": "application/json", - Accept: "application/json", - "x-api-key": this.continueProxyConfig.apiKey ?? "", - Authorization: `Bearer ${this.continueProxyConfig.apiKey}`, - } - } - - override modifyRerankBody(body: T): T { - return { - ...body, - ...this.extraBodyProperties(), - } - } -} diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/DeepSeek.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/DeepSeek.ts deleted file mode 100644 index 8a280b1ce65..00000000000 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/DeepSeek.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { streamSse } from "../../../fetch/stream.js" -import { ChatCompletionChunk, Model } from "openai/resources/index" -import { DeepseekConfig } from "../types.js" -import { chatChunk } from "../util.js" -import { OpenAIApi } from "./OpenAI.js" -import { FimCreateParamsStreaming } from "./base.js" - -const DEEPSEEK_API_BASE = "https://api.deepseek.com/" -export class DeepSeekApi extends OpenAIApi { - constructor(config: DeepseekConfig) { - super({ - ...config, - provider: "openai", - apiBase: config.apiBase ?? DEEPSEEK_API_BASE, - }) - } - - override async *fimStream( - body: FimCreateParamsStreaming, - signal: AbortSignal, - ): AsyncGenerator { - const endpoint = new URL("beta/completions", this.apiBase) - const resp = await fetch(endpoint, { - method: "POST", - body: JSON.stringify({ - model: body.model, - prompt: body.prompt, - suffix: body.suffix, - max_tokens: body.max_tokens, - temperature: body.temperature, - top_p: body.top_p, - frequency_penalty: body.frequency_penalty, - presence_penalty: body.presence_penalty, - stop: body.stop, - stream: true, - }), - headers: { - "Content-Type": "application/json", - Accept: "application/json", - Authorization: `Bearer ${this.config.apiKey}`, - }, - signal, - }) - for await (const chunk of streamSse(resp as any)) { - yield chatChunk({ - content: chunk.choices[0].text, - finish_reason: chunk.finish_reason, - model: body.model, - }) - } - } - - override list(): Promise { - throw new Error("Method not implemented.") - } -} diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/Gemini.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/Gemini.ts deleted file mode 100644 index 2f3f5ec9bc8..00000000000 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/Gemini.ts +++ /dev/null @@ -1,316 +0,0 @@ -import { OpenAI } from "openai/index" -import { - ChatCompletion, - ChatCompletionChunk, - ChatCompletionContentPartImage, - ChatCompletionCreateParams, - ChatCompletionCreateParamsNonStreaming, - ChatCompletionCreateParamsStreaming, - Completion, - CompletionCreateParamsNonStreaming, - CompletionCreateParamsStreaming, - CompletionUsage, - Model, -} from "openai/resources/index" - -import { streamResponse } from "../../../fetch/stream.js" -import { GeminiConfig } from "../types.js" -import { chatChunk, usageChatChunk } from "../util.js" -import { - convertOpenAIToolToGeminiFunction, - GeminiChatContent, - GeminiChatContentPart, - GeminiToolFunctionDeclaration, -} from "../util/gemini-types.js" -import { BaseLlmApi, CreateRerankResponse, FimCreateParamsStreaming, RerankCreateParams } from "./base.js" - -type UsageInfo = Pick - -export class GeminiApi implements BaseLlmApi { - apiBase: string = "https://generativelanguage.googleapis.com/v1beta/" - - constructor(protected config: GeminiConfig) { - this.apiBase = config.apiBase ?? this.apiBase - } - - private _oaiPartToGeminiPart( - part: OpenAI.Chat.Completions.ChatCompletionContentPart | OpenAI.Chat.Completions.ChatCompletionContentPartRefusal, - ): GeminiChatContentPart { - switch (part.type) { - case "refusal": - return { - text: part.refusal, - } - case "text": - return { - text: part.text, - } - case "input_audio": - throw new Error("Unsupported part type: input_audio") - case "image_url": - default: - return { - inlineData: { - mimeType: "image/jpeg", - data: (part as ChatCompletionContentPartImage).image_url?.url.split(",")[1], - }, - } - } - } - - public _convertBody(oaiBody: ChatCompletionCreateParams, url: string) { - const generationConfig: any = {} - - if (oaiBody.top_p) { - generationConfig.topP = oaiBody.top_p - } - if (oaiBody.temperature !== undefined && oaiBody.temperature !== null) { - generationConfig.temperature = oaiBody.temperature - } - if (oaiBody.max_tokens) { - generationConfig.maxOutputTokens = oaiBody.max_tokens - } - if (oaiBody.stop) { - const stop = Array.isArray(oaiBody.stop) ? oaiBody.stop : [oaiBody.stop] - generationConfig.stopSequences = stop.filter((x) => x.trim() !== "") - } - - const isV1API = url.includes("/v1/") - - const contents: (GeminiChatContent | null)[] = oaiBody.messages - .map((msg) => { - if (msg.role === "system" && !isV1API) { - return null // Don't include system message in contents - } - - if (msg.role === "assistant") { - return { - role: "model" as const, - parts: [ - { - text: - typeof msg.content === "string" - ? msg.content - : (msg.content ?? []) - .filter((part) => part.type === "text") - .map((part) => part.text) - .join(""), - }, - ], - } - } - - if (msg.role === "user") { - return { - role: "user" as const, - parts: [ - { - text: - typeof msg.content === "string" - ? msg.content - : msg.content - .filter((part) => part.type === "text") - .map((part) => part.text) - .join(""), - }, - ], - } - } - - if (!msg.content) { - return null - } - - return { - role: "user" as const, - parts: typeof msg.content === "string" ? [{ text: msg.content }] : msg.content.map(this._oaiPartToGeminiPart), - } - }) - .filter((c) => c !== null) - - const sysMsg = oaiBody.messages.find((msg) => msg.role === "system") - const finalBody: any = { - generationConfig, - contents, - // if there is a system message, reformat it for Gemini API - ...(sysMsg && - !isV1API && { - systemInstruction: { parts: [{ text: sysMsg.content }] }, - }), - } - - if (!isV1API) { - // Convert and add tools if present - if (oaiBody.tools?.length) { - // Choosing to map all tools to the functionDeclarations of one tool - // Rather than map each tool to its own tool + functionDeclaration - // Same difference - const functions: GeminiToolFunctionDeclaration[] = [] - oaiBody.tools.forEach((tool) => { - try { - functions.push(convertOpenAIToolToGeminiFunction(tool)) - } catch { - console.warn( - `Failed to convert tool to gemini function definition. Skipping: ${JSON.stringify(tool, null, 2)}`, - ) - } - }) - - if (functions.length) { - finalBody.tools = [ - { - functionDeclarations: functions, - }, - ] - } - } - } - - return finalBody - } - - async chatCompletionNonStream( - body: ChatCompletionCreateParamsNonStreaming, - signal: AbortSignal, - ): Promise { - let completion = "" - let usage: UsageInfo | undefined = undefined - for await (const chunk of this.chatCompletionStream( - { - ...body, - stream: true, - }, - signal, - )) { - if (chunk.choices.length > 0) { - completion += chunk.choices[0].delta.content || "" - } - if (chunk.usage) { - usage = chunk.usage - } - } - return { - id: "", - object: "chat.completion", - model: body.model, - created: Date.now(), - choices: [ - { - index: 0, - logprobs: null, - finish_reason: "stop", - message: { - role: "assistant", - content: completion, - refusal: null, - }, - }, - ], - usage, - } - } - - async *handleStreamResponse(response: Response, model: string) { - let buffer = "" - let usage: UsageInfo | undefined = undefined - for await (const chunk of streamResponse(response)) { - buffer += chunk - if (buffer.startsWith("[")) { - buffer = buffer.slice(1) - } - if (buffer.endsWith("]")) { - buffer = buffer.slice(0, -1) - } - if (buffer.startsWith(",")) { - buffer = buffer.slice(1) - } - - const parts = buffer.split("\n,") - - let foundIncomplete = false - for (let i = 0; i < parts.length; i++) { - const part = parts[i] - let data - try { - data = JSON.parse(part) - } catch { - foundIncomplete = true - continue // yo! - } - if (data.error) { - throw new Error(data.error.message) - } - - // Check for usage metadata - if (data.usageMetadata) { - usage = { - prompt_tokens: data.usageMetadata.promptTokenCount || 0, - completion_tokens: data.usageMetadata.candidatesTokenCount || 0, - total_tokens: data.usageMetadata.totalTokenCount || 0, - } - } - - // In case of max tokens reached, gemini will sometimes return content with no parts, even though that doesn't match the API spec - const contentParts = data?.candidates?.[0]?.content?.parts - if (contentParts) { - for (const part of contentParts) { - if ("text" in part) { - yield chatChunk({ - content: part.text, - model, - }) - } - } - } else { - console.warn("Unexpected response format:", data) - } - } - if (foundIncomplete) { - buffer = parts[parts.length - 1] - } else { - buffer = "" - } - } - - // Emit usage at the end if we have it - if (usage) { - yield usageChatChunk({ - model, - usage, - }) - } - } - - async *chatCompletionStream( - body: ChatCompletionCreateParamsStreaming, - signal: AbortSignal, - ): AsyncGenerator { - const apiURL = new URL( - `models/${body.model}:streamGenerateContent?key=${this.config.apiKey}`, - this.apiBase, - ).toString() - const convertedBody = this._convertBody(body, apiURL) - const resp = await fetch(apiURL, { - method: "POST", - body: JSON.stringify(convertedBody), - signal, - }) - yield* this.handleStreamResponse(resp, body.model) - } - completionNonStream(_body: CompletionCreateParamsNonStreaming): Promise { - throw new Error("Method not implemented.") - } - completionStream(_body: CompletionCreateParamsStreaming): AsyncGenerator { - throw new Error("Method not implemented.") - } - fimStream(_body: FimCreateParamsStreaming): AsyncGenerator { - throw new Error("Method not implemented.") - } - async rerank(_body: RerankCreateParams): Promise { - throw new Error("Method not implemented.") - } - - list(): Promise { - throw new Error("Method not implemented.") - } -} diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/Inception.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/Inception.ts deleted file mode 100644 index 6fbc77b98db..00000000000 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/Inception.ts +++ /dev/null @@ -1,237 +0,0 @@ -import { streamSse } from "../../../fetch/stream.js" -import { - ChatCompletion, - ChatCompletionChunk, - ChatCompletionCreateParamsNonStreaming, - ChatCompletionCreateParamsStreaming, - ChatCompletionMessageParam, - Model, -} from "openai/resources/index" -import { InceptionConfig } from "../types.js" -import { chatChunk } from "../util.js" -import { OpenAIApi } from "./OpenAI.js" -import { FimCreateParamsStreaming } from "./base.js" - -// export type InceptionChatCompletionCreateParamsStreaming = -// ChatCompletionCreateParamsStreaming & { -// nextEdit?: boolean; -// }; - -// export type InceptionChatCompletionCreateParamsNonStreaming = -// ChatCompletionCreateParamsNonStreaming & { -// nextEdit?: boolean; -// }; - -const UNIQUE_TOKEN = "<|!@#IS_NEXT_EDIT!@#|>" -const APPLY_UNIQUE_TOKEN = "<|!@#IS_APPLY!@#|>" -const INCEPTION_API_BASE = "https://api.inceptionlabs.ai/v1/" -export class InceptionApi extends OpenAIApi { - constructor(config: InceptionConfig) { - super({ - ...config, - provider: "openai", - apiBase: config.apiBase ?? INCEPTION_API_BASE, - }) - } - - // Add custom edit completions method. - async *editCompletionStream( - body: ChatCompletionCreateParamsStreaming, - signal: AbortSignal, - ): AsyncGenerator { - yield* this.streamCustomEndpoint("edit/completions", body, signal) - } - - // Add custom edit completions method (non-streaming). - async editCompletionNonStream( - body: ChatCompletionCreateParamsNonStreaming, - signal: AbortSignal, - ): Promise { - return this.nonStreamCustomEndpoint("edit/completions", body, signal) - } - - // Override the regular chat stream method to route to edit endpoint for next edit requests. - override async *chatCompletionStream( - body: ChatCompletionCreateParamsStreaming, - signal: AbortSignal, - ): AsyncGenerator { - if (this.isNextEdit(body.messages)) { - body.messages = this.removeToken(body.messages, UNIQUE_TOKEN) - yield* this.editCompletionStream(body, signal) - } else if (this.isApply(body.messages)) { - body.messages = this.removeToken(body.messages, APPLY_UNIQUE_TOKEN) - yield* this.applyCompletionStream(body, signal) - } else { - yield* super.chatCompletionStream(body, signal) - } - } - - // Override the regular chat non stream method to route to edit endpoint for next edit requests. - override async chatCompletionNonStream( - body: ChatCompletionCreateParamsNonStreaming, - signal: AbortSignal, - ): Promise { - if (this.isNextEdit(body.messages)) { - body.messages = this.removeToken(body.messages, UNIQUE_TOKEN) - return this.editCompletionNonStream(body, signal) - } else if (this.isApply(body.messages)) { - body.messages = this.removeToken(body.messages, APPLY_UNIQUE_TOKEN) - return this.applyCompletionNonStream(body, signal) - } else { - return super.chatCompletionNonStream(body, signal) - } - } - - async *applyCompletionStream( - body: ChatCompletionCreateParamsStreaming, - signal: AbortSignal, - ): AsyncGenerator { - yield* this.streamCustomEndpoint("apply/completions", body, signal) - } - - async applyCompletionNonStream( - body: ChatCompletionCreateParamsNonStreaming, - signal: AbortSignal, - ): Promise { - return this.nonStreamCustomEndpoint("apply/completions", body, signal) - } - - override async *fimStream( - body: FimCreateParamsStreaming, - signal: AbortSignal, - ): AsyncGenerator { - const endpoint = new URL("completions", this.apiBase) - const resp = await fetch(endpoint, { - method: "POST", - body: JSON.stringify({ - model: body.model, - prompt: body.prompt, - suffix: body.suffix.trim() === "" ? "<|endoftext|>" : body.suffix, - max_tokens: body.max_tokens ?? 150, // Only want this for /fim, not chat - temperature: body.temperature, - top_p: body.top_p, - frequency_penalty: body.frequency_penalty, - presence_penalty: body.presence_penalty, - stop: [...(body.stop ?? []), "\n\n", "\n \n"], - stream: true, - }), - headers: { - "Content-Type": "application/json", - Accept: "application/json", - Authorization: `Bearer ${this.config.apiKey}`, - }, - signal, - }) - for await (const chunk of streamSse(resp as any)) { - if (!chunk.choices[0]) { - continue - } - yield chatChunk({ - content: chunk.choices[0].text, - finish_reason: null, - model: body.model, - }) - } - } - - override list(): Promise { - throw new Error("Method not implemented.") - } - - // Check if any message contains the unique next edit token. - private isNextEdit(messages: ChatCompletionMessageParam[]): boolean { - return messages.some((message) => typeof message.content === "string" && message.content.endsWith(UNIQUE_TOKEN)) - } - - private isApply(messages: ChatCompletionMessageParam[]): boolean { - return messages.some( - (message) => typeof message.content === "string" && message.content.endsWith(APPLY_UNIQUE_TOKEN), - ) - } - - // Remove the unique token from messages. - private removeToken(messages: ChatCompletionMessageParam[], token: string): ChatCompletionMessageParam[] { - const lastMessage = messages[messages.length - 1] - - if (typeof lastMessage?.content === "string" && lastMessage.content.endsWith(token)) { - const cleanedMessages = [...messages] - cleanedMessages[cleanedMessages.length - 1] = { - ...lastMessage, - content: lastMessage.content.slice(0, -token.length), - } - return cleanedMessages - } - - return messages - } - - private async *streamCustomEndpoint( - path: string, - body: ChatCompletionCreateParamsStreaming, - signal: AbortSignal, - ): AsyncGenerator { - const endpoint = new URL(path, this.apiBase) - const resp = await fetch(endpoint, { - method: "POST", - body: JSON.stringify({ - model: body.model, - messages: body.messages, - max_tokens: body.max_tokens, - temperature: body.temperature, - top_p: body.top_p, - frequency_penalty: body.frequency_penalty, - presence_penalty: body.presence_penalty, - stop: body.stop, - stream: true, - }), - headers: { - "Content-Type": "application/json", - Accept: "application/json", - Authorization: `Bearer ${this.config.apiKey}`, - }, - signal, - }) - - for await (const chunk of streamSse(resp as any)) { - const deltaContent = chunk.choices?.[0]?.delta?.content - if (deltaContent) { - yield chatChunk({ - content: deltaContent, - finish_reason: chunk.choices[0].finish_reason || null, - model: body.model, - }) - } - } - } - - private async nonStreamCustomEndpoint( - path: string, - body: ChatCompletionCreateParamsNonStreaming, - signal: AbortSignal, - ): Promise { - const endpoint = new URL(path, this.apiBase) - const resp = await fetch(endpoint, { - method: "POST", - body: JSON.stringify({ - model: body.model, - messages: body.messages, - max_tokens: body.max_tokens, - temperature: body.temperature, - top_p: body.top_p, - frequency_penalty: body.frequency_penalty, - presence_penalty: body.presence_penalty, - stop: body.stop, - stream: false, - }), - headers: { - "Content-Type": "application/json", - Accept: "application/json", - Authorization: `Bearer ${this.config.apiKey}`, - }, - signal, - }) - - const data = await resp.json() - return data as ChatCompletion - } -} diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/Jina.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/Jina.ts deleted file mode 100644 index ada927c2554..00000000000 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/Jina.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { - ChatCompletion, - ChatCompletionChunk, - ChatCompletionCreateParamsNonStreaming, - ChatCompletionCreateParamsStreaming, - Completion, - CompletionCreateParamsNonStreaming, - CompletionCreateParamsStreaming, - Model, -} from "openai/resources/index" -import { JinaConfig } from "../types.js" -import { rerank } from "../util.js" -import { BaseLlmApi, CreateRerankResponse, FimCreateParamsStreaming, RerankCreateParams } from "./base.js" - -export class JinaApi implements BaseLlmApi { - apiBase: string = "https://api.jina.ai/v1/" - - constructor(protected config: JinaConfig) { - this.apiBase = config.apiBase ?? this.apiBase - } - - async chatCompletionNonStream(_body: ChatCompletionCreateParamsNonStreaming): Promise { - throw new Error("Method not implemented.") - } - - // eslint-disable-next-line require-yield - async *chatCompletionStream( - _body: ChatCompletionCreateParamsStreaming, - ): AsyncGenerator { - throw new Error("Method not implemented.") - } - async completionNonStream(_body: CompletionCreateParamsNonStreaming): Promise { - throw new Error("Method not implemented.") - } - // eslint-disable-next-line require-yield - async *completionStream(_body: CompletionCreateParamsStreaming): AsyncGenerator { - throw new Error("Method not implemented.") - } - // eslint-disable-next-line require-yield - async *fimStream(_body: FimCreateParamsStreaming): AsyncGenerator { - throw new Error("Method not implemented.") - } - - async rerank(body: RerankCreateParams): Promise { - const endpoint = new URL("rerank", this.apiBase) - const response = await fetch(endpoint, { - method: "POST", - body: JSON.stringify(body), - headers: { - "Content-Type": "application/json", - Accept: "application/json", - "x-api-key": this.config.apiKey ?? "", - Authorization: `Bearer ${this.config.apiKey}`, - }, - }) - const data = (await response.json()) as any - - return rerank({ - model: body.model, - usage: { - total_tokens: 0, - }, - data: data.results.map((result: any) => result.relevance_score), - }) - } - - list(): Promise { - throw new Error("Method not implemented.") - } -} diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/LlamaStack.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/LlamaStack.ts deleted file mode 100644 index 836b68d4666..00000000000 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/LlamaStack.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { streamSse } from "../../../fetch/stream.js" -import { ChatCompletionChunk } from "openai/resources/index" -import { LlamastackConfig } from "../types.js" -import { chatChunk } from "../util.js" -import { OpenAIApi } from "./OpenAI.js" -import { FimCreateParamsStreaming } from "./base.js" - -const Llamastack_API_BASE = "http://localhost:8321/v1/openai/v1/" -export class LlamastackApi extends OpenAIApi { - constructor(config: LlamastackConfig) { - super({ - ...config, - provider: "openai", - apiBase: config.apiBase ?? Llamastack_API_BASE, - apiKey: config.apiKey ?? "dummy", - }) - } - - override async *fimStream( - body: FimCreateParamsStreaming, - signal: AbortSignal, - ): AsyncGenerator { - const endpoint = new URL("completions", this.apiBase) - const resp = await fetch(endpoint, { - method: "POST", - body: JSON.stringify({ - model: body.model, - prompt: body.prompt, - suffix: body.suffix, - max_tokens: body.max_tokens, - max_completion_tokens: (body as any).max_completion_tokens, - temperature: body.temperature, - top_p: body.top_p, - frequency_penalty: body.frequency_penalty, - presence_penalty: body.presence_penalty, - stop: body.stop, - stream: true, - }), - headers: { - "Content-Type": "application/json", - Accept: "application/json", - "x-api-key": this.config.apiKey ?? "", - Authorization: `Bearer ${this.config.apiKey}`, - }, - signal, - }) - for await (const chunk of streamSse(resp as any)) { - yield chatChunk({ - content: chunk.choices[0].text, - finish_reason: chunk.finish_reason, - model: body.model, - }) - } - } -} diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/Mock.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/Mock.ts deleted file mode 100644 index c8a57c0c2a5..00000000000 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/Mock.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { - ChatCompletion, - ChatCompletionChunk, - ChatCompletionCreateParamsNonStreaming, - ChatCompletionCreateParamsStreaming, - Completion, - CompletionCreateParamsNonStreaming, - CompletionCreateParamsStreaming, - Model, -} from "openai/resources/index" -import { chatChunk, chatCompletion } from "../util.js" -import { BaseLlmApi, CreateRerankResponse, FimCreateParamsStreaming, RerankCreateParams } from "./base.js" - -const MOCK_RESPONSE = - "This is a mock response from the OpenAI API. It can be returned all at once or streamed chunk by chunk." - -export class MockApi implements BaseLlmApi { - async chatCompletionNonStream( - body: ChatCompletionCreateParamsNonStreaming, - _signal: AbortSignal, - ): Promise { - await new Promise((resolve) => setTimeout(resolve, 800)) - const lastMessage = body.messages[body.messages.length - 1].content - const content = !lastMessage - ? MOCK_RESPONSE - : typeof lastMessage === "string" - ? lastMessage - : lastMessage[0].type === "text" - ? lastMessage[0].text - : MOCK_RESPONSE - return chatCompletion({ - content, - model: body.model, - }) - } - - async *chatCompletionStream( - body: ChatCompletionCreateParamsStreaming, - _signal: AbortSignal, - ): AsyncGenerator { - const lastMessage = body.messages[body.messages.length - 1].content - const content = !lastMessage - ? MOCK_RESPONSE - : typeof lastMessage === "string" - ? lastMessage - : lastMessage[0].type === "text" - ? lastMessage[0].text - : MOCK_RESPONSE - const chunks = content.split(" ") - for (const chunk of chunks) { - await new Promise((resolve) => setTimeout(resolve, 100)) - yield chatChunk({ - content: chunk + " ", - model: body.model, - }) - } - } - - async completionNonStream(body: CompletionCreateParamsNonStreaming, _signal: AbortSignal): Promise { - await new Promise((resolve) => setTimeout(resolve, 800)) - return { - id: "mock-completion", - object: "text_completion", - created: Date.now(), - model: body.model, - choices: [ - { - text: body.prompt as string, - index: 0, - finish_reason: "stop", - }, - ], - } as Completion - } - - async *completionStream( - body: CompletionCreateParamsStreaming, - _signal: AbortSignal, - ): AsyncGenerator { - const chunks = (body.prompt as string).split(" ") - for (const chunk of chunks) { - await new Promise((resolve) => setTimeout(resolve, 100)) - yield { - id: "mock-chunk", - object: "text_completion", - created: Date.now(), - model: body.model, - choices: [ - { - text: chunk + " ", - index: 0, - finish_reason: "stop", - }, - ], - } as Completion - } - } - - async *fimStream( - body: FimCreateParamsStreaming, - _signal: AbortSignal, - ): AsyncGenerator { - const chunks = (body.prompt as string).split(" ") - for (const chunk of chunks) { - await new Promise((resolve) => setTimeout(resolve, 100)) - yield chatChunk({ - content: chunk + " ", - model: body.model, - }) - } - } - - async rerank(_body: RerankCreateParams): Promise { - throw new Error("Method not implemented.") - } - - async list(): Promise { - return [ - { - id: "mock-model", - created: Date.now(), - object: "model", - owned_by: "mock", - }, - ] - } -} diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/Moonshot.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/Moonshot.ts deleted file mode 100644 index e1c496a076d..00000000000 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/Moonshot.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { streamSse } from "../../../fetch/stream.js" -import { ChatCompletionChunk, Model } from "openai/resources/index" -import { MoonshotConfig } from "../types.js" -import { chatChunk } from "../util.js" -import { OpenAIApi } from "./OpenAI.js" -import { FimCreateParamsStreaming } from "./base.js" - -export class MoonshotApi extends OpenAIApi { - override apiBase: string = "https://api.moonshot.cn/" - constructor(config: MoonshotConfig) { - super({ - ...config, - provider: "openai", - }) - } - - override async *fimStream( - body: FimCreateParamsStreaming, - signal: AbortSignal, - ): AsyncGenerator { - const endpoint = new URL("v1/chat/completions", this.apiBase) - const resp = await fetch(endpoint, { - method: "POST", - body: JSON.stringify({ - model: body.model, - messages: [ - { - role: "user", - content: body.prompt + "[fill]" + body.suffix, - }, - ], - max_tokens: body.max_tokens, - temperature: body.temperature, - top_p: body.top_p, - frequency_penalty: body.frequency_penalty, - presence_penalty: body.presence_penalty, - stop: body.stop, - stream: true, - }), - headers: { - "Content-Type": "application/json", - Accept: "application/json", - Authorization: `Bearer ${this.config.apiKey}`, - }, - signal, - }) - - for await (const chunk of streamSse(resp as any)) { - yield chatChunk({ - content: chunk.choices[0].delta.content, - finish_reason: chunk.finish_reason, - model: body.model, - }) - } - } - override list(): Promise { - throw new Error("Method not implemented.") - } -} diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/OpenAI.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/OpenAI.ts deleted file mode 100644 index f6469030131..00000000000 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/OpenAI.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { streamSse } from "../../../fetch/stream.js" -import { OpenAI } from "openai/index" -import { - ChatCompletion, - ChatCompletionChunk, - ChatCompletionCreateParams, - ChatCompletionCreateParamsNonStreaming, - ChatCompletionCreateParamsStreaming, - Completion, - CompletionCreateParamsNonStreaming, - CompletionCreateParamsStreaming, - Model, -} from "openai/resources/index" -import { z } from "zod" -import { OpenAIConfigSchema } from "../types.js" -import { BaseLlmApi, CreateRerankResponse, FimCreateParamsStreaming, RerankCreateParams } from "./base.js" - -export class OpenAIApi implements BaseLlmApi { - openai: OpenAI - apiBase: string = "https://api.openai.com/v1/" - - constructor(protected config: z.infer) { - this.apiBase = config.apiBase ?? this.apiBase - this.openai = new OpenAI({ - // Necessary because `new OpenAI()` will throw an error if there is no API Key - apiKey: config.apiKey ?? "", - baseURL: this.apiBase, - }) - } - modifyChatBody(body: T): T { - // Add stream_options to include usage in streaming responses - if (body.stream) { - ;(body as any).stream_options = { include_usage: true } - } - - // o-series models - only apply for official OpenAI API - const isOfficialOpenAIAPI = this.apiBase === "https://api.openai.com/v1/" - if (isOfficialOpenAIAPI) { - if (body.model.startsWith("o") || body.model.includes("gpt-5")) { - // a) use max_completion_tokens instead of max_tokens - body.max_completion_tokens = body.max_tokens - body.max_tokens = undefined - - // b) use "developer" message role rather than "system" - body.messages = body.messages.map((message) => { - if (message.role === "system") { - return { ...message, role: "developer" } as any - } - return message - }) - } - } - return body - } - - modifyCompletionBody(body: T): T { - return body - } - - modifyFimBody(body: T): T { - return body - } - - modifyRerankBody(body: T): T { - return body - } - - protected getHeaders(): Record { - return { - "Content-Type": "application/json", - Accept: "application/json", - "x-api-key": this.config.apiKey ?? "", - Authorization: `Bearer ${this.config.apiKey}`, - } - } - - async chatCompletionNonStream( - body: ChatCompletionCreateParamsNonStreaming, - signal: AbortSignal, - ): Promise { - const response = await this.openai.chat.completions.create(this.modifyChatBody(body), { - signal, - }) - return response - } - - async *chatCompletionStream( - body: ChatCompletionCreateParamsStreaming, - signal: AbortSignal, - ): AsyncGenerator { - const response = await this.openai.chat.completions.create(this.modifyChatBody(body), { - signal, - }) - for await (const result of response) { - yield result - } - } - async completionNonStream(body: CompletionCreateParamsNonStreaming, signal: AbortSignal): Promise { - const response = await this.openai.completions.create(this.modifyCompletionBody(body), { signal }) - return response - } - async *completionStream( - body: CompletionCreateParamsStreaming, - signal: AbortSignal, - ): AsyncGenerator { - const response = await this.openai.completions.create(this.modifyCompletionBody(body), { signal }) - for await (const result of response) { - yield result - } - } - async *fimStream( - body: FimCreateParamsStreaming, - signal: AbortSignal, - ): AsyncGenerator { - const endpoint = new URL("fim/completions", this.apiBase) - const modifiedBody = this.modifyFimBody(body) - const resp = await fetch(endpoint, { - method: "POST", - body: JSON.stringify({ - model: modifiedBody.model, - prompt: modifiedBody.prompt, - suffix: modifiedBody.suffix, - max_tokens: modifiedBody.max_tokens, - max_completion_tokens: (modifiedBody as any).max_completion_tokens, - temperature: modifiedBody.temperature, - top_p: modifiedBody.top_p, - frequency_penalty: modifiedBody.frequency_penalty, - presence_penalty: modifiedBody.presence_penalty, - stop: modifiedBody.stop, - stream: true, - }), - headers: this.getHeaders(), - signal, - }) - for await (const chunk of streamSse(resp as any)) { - if (chunk.choices && chunk.choices.length > 0) { - yield chunk - } - } - } - - async rerank(body: RerankCreateParams): Promise { - const endpoint = new URL("rerank", this.apiBase) - const modifiedBody = this.modifyRerankBody(body) - const response = await fetch(endpoint, { - method: "POST", - body: JSON.stringify(modifiedBody), - headers: this.getHeaders(), - }) - const data = await response.json() - return data as any - } - - async list(): Promise { - return (await this.openai.models.list()).data - } -} diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/OpenRouter.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/OpenRouter.ts deleted file mode 100644 index ebea1cac80f..00000000000 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/OpenRouter.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { ChatCompletionCreateParams } from "openai/resources/index" - -import { OpenAIConfig } from "../types.js" -import { OpenAIApi } from "./OpenAI.js" -import { applyAnthropicCachingToOpenRouterBody } from "./OpenRouterCaching.js" - -interface OpenRouterConfig extends OpenAIConfig { - cachingStrategy?: import("./AnthropicCachingStrategies.js").CachingStrategyName -} - -export class OpenRouterApi extends OpenAIApi { - constructor(config: OpenRouterConfig) { - super({ - ...config, - apiBase: config.apiBase ?? "https://openrouter.ai/api/v1/", - }) - } - - private isAnthropicModel(model?: string): boolean { - if (!model) { - return false - } - const modelLower = model.toLowerCase() - return modelLower.includes("claude") - } - - override modifyChatBody(body: T): T { - const modifiedBody = super.modifyChatBody(body) - - if (!this.isAnthropicModel(modifiedBody.model)) { - return modifiedBody - } - - applyAnthropicCachingToOpenRouterBody( - modifiedBody as unknown as ChatCompletionCreateParams, - (this.config as OpenRouterConfig).cachingStrategy ?? "systemAndTools", - ) - - return modifiedBody - } -} diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/OpenRouterCaching.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/OpenRouterCaching.ts deleted file mode 100644 index c0e1520f826..00000000000 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/OpenRouterCaching.ts +++ /dev/null @@ -1,258 +0,0 @@ -import { ContentBlockParam, MessageCreateParams, MessageParam } from "@anthropic-ai/sdk/resources" -import { ChatCompletionCreateParams, ChatCompletionMessageParam } from "openai/resources/index" - -import { CACHING_STRATEGIES, CachingStrategyName } from "./AnthropicCachingStrategies.js" -import { addCacheControlToLastTwoUserMessages, openaiToolToAnthropicTool } from "./AnthropicUtils.js" - -interface SystemMapping { - openaiIndex: number - start: number - length: number - wasString: boolean - originalContent: ChatCompletionMessageParam["content"] - textPartIndices: (number | null)[] -} - -interface MessageMapping { - openaiIndex: number - anthropicIndex: number - role: string - wasString: boolean - originalContent: ChatCompletionMessageParam["content"] - textPartIndices: (number | null)[] -} - -interface ConversionResult { - anthropicBody: MessageCreateParams - systemMappings: SystemMapping[] - messageMappings: MessageMapping[] -} - -const convertContentToBlocks = ( - content: ChatCompletionMessageParam["content"], -): { - blocks: ContentBlockParam[] - textPartIndices: (number | null)[] - wasString: boolean -} => { - if (typeof content === "string" || typeof content === "number") { - const text = String(content) - return { - blocks: [ - { - type: "text", - text, - } as ContentBlockParam, - ], - textPartIndices: [null], - wasString: true, - } - } - - if (!Array.isArray(content)) { - return { - blocks: [], - textPartIndices: [], - wasString: false, - } - } - - const blocks: ContentBlockParam[] = [] - const textPartIndices: (number | null)[] = [] - - content.forEach((part: any, idx: number) => { - if (part?.type === "text") { - blocks.push({ - type: "text", - text: part.text ?? "", - } as ContentBlockParam) - textPartIndices.push(idx) - } else { - blocks.push({ ...(part ?? {}) } as any) - textPartIndices.push(null) - } - }) - - return { - blocks, - textPartIndices, - wasString: false, - } -} - -const convertToAnthropic = (body: ChatCompletionCreateParams): ConversionResult => { - const systemBlocks: ContentBlockParam[] = [] - const systemMappings: SystemMapping[] = [] - const messages: MessageParam[] = [] - const messageMappings: MessageMapping[] = [] - - let systemOffset = 0 - - body.messages.forEach((message, index) => { - const { blocks, textPartIndices, wasString } = convertContentToBlocks(message.content) - - if (message.role === "system") { - const length = blocks.length - systemMappings.push({ - openaiIndex: index, - start: systemOffset, - length, - wasString, - originalContent: message.content, - textPartIndices, - }) - systemBlocks.push(...blocks) - systemOffset += length - } else { - messages.push({ - role: message.role as MessageParam["role"], - content: blocks as any, - }) - messageMappings.push({ - openaiIndex: index, - anthropicIndex: messages.length - 1, - role: message.role, - wasString, - originalContent: message.content, - textPartIndices, - }) - } - }) - - const tools = body.tools?.filter((tool) => tool.type === "function").map((tool) => openaiToolToAnthropicTool(tool)) - - const anthropicBody: MessageCreateParams = { - model: body.model, - messages, - max_tokens: body.max_tokens ?? 1, - system: systemBlocks.length > 0 ? (systemBlocks as any) : undefined, - tools, - } - - return { anthropicBody, systemMappings, messageMappings } -} - -export const applyAnthropicCachingToOpenRouterBody = ( - body: ChatCompletionCreateParams, - strategy: CachingStrategyName, -): void => { - const { anthropicBody, systemMappings, messageMappings } = convertToAnthropic(body) - - const cachingStrategy = CACHING_STRATEGIES[strategy] ?? CACHING_STRATEGIES.systemAndTools - const cachedBody = cachingStrategy({ ...anthropicBody }) - - cachedBody.messages = cachedBody.messages ?? [] - addCacheControlToLastTwoUserMessages(cachedBody.messages) - - const cachedSystem = Array.isArray(cachedBody.system) ? cachedBody.system : [] - - systemMappings.forEach((mapping) => { - const openaiMessage = body.messages[mapping.openaiIndex] as any - if (!openaiMessage) { - return - } - - const slice = cachedSystem.slice(mapping.start, mapping.start + mapping.length) - const hasCache = slice.some((block: any) => block?.cache_control) - - if (!hasCache) { - openaiMessage.content = mapping.originalContent - return - } - - if (mapping.wasString) { - openaiMessage.content = slice.map((block: any) => ({ - type: "text", - text: block?.text ?? "", - ...(block?.cache_control ? { cache_control: block.cache_control } : {}), - })) - return - } - - if (Array.isArray(mapping.originalContent)) { - const newParts = mapping.originalContent.map((part: any) => ({ - ...part, - })) - - slice.forEach((block: any, idx: number) => { - const originalIndex = mapping.textPartIndices[idx] - if (originalIndex === null || originalIndex === undefined || !block?.cache_control) { - return - } - - newParts[originalIndex] = { - ...newParts[originalIndex], - cache_control: block.cache_control, - ...(block.text !== undefined ? { text: block.text } : {}), - } - }) - - openaiMessage.content = newParts - } - }) - - const cachedMessages = cachedBody.messages ?? [] - messageMappings.forEach((mapping) => { - const openaiMessage = body.messages[mapping.openaiIndex] as any - const cachedMessage = cachedMessages[mapping.anthropicIndex] as any - if (!openaiMessage || !cachedMessage) { - return - } - - if (cachedMessage.role !== "user") { - openaiMessage.content = mapping.originalContent - return - } - - const contentArray = Array.isArray(cachedMessage.content) ? cachedMessage.content : [] - const hasCache = contentArray.some((block: any) => block?.cache_control) - - if (!hasCache) { - openaiMessage.content = mapping.originalContent - return - } - - if (mapping.wasString) { - openaiMessage.content = contentArray.map((block: any) => ({ - type: "text", - text: block?.text ?? "", - ...(block?.cache_control ? { cache_control: block.cache_control } : {}), - })) - return - } - - if (Array.isArray(mapping.originalContent)) { - const newParts = mapping.originalContent.map((part: any) => ({ - ...part, - })) - - contentArray.forEach((block: any, idx: number) => { - const originalIndex = mapping.textPartIndices[idx] - if (originalIndex === null || originalIndex === undefined || !block?.cache_control) { - return - } - - newParts[originalIndex] = { - ...newParts[originalIndex], - cache_control: block.cache_control, - ...(block.text !== undefined ? { text: block.text } : {}), - } - }) - - openaiMessage.content = newParts - } - }) - - if (body.tools?.length && cachedBody.tools?.length) { - body.tools = body.tools.map((tool, idx) => { - const cachedTool = (cachedBody.tools ?? [])[idx] as any - if (!cachedTool?.cache_control) { - return tool - } - return { - ...tool, - cache_control: cachedTool.cache_control, - } as any - }) - } -} diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/Relace.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/Relace.ts deleted file mode 100644 index 56f59bb259e..00000000000 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/Relace.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { Completion, CompletionUsage } from "openai/resources/completions.mjs" -import { - ChatCompletion, - ChatCompletionChunk, - ChatCompletionCreateParamsNonStreaming, - ChatCompletionCreateParamsStreaming, -} from "openai/resources/index.mjs" -import { Model } from "openai/resources/models.mjs" -import { z } from "zod" -import { OpenAIConfigSchema } from "../types.js" -import { chatChunk, chatCompletion, usageChatChunk } from "../util.js" -import { BaseLlmApi, CreateRerankResponse } from "./base.js" - -type UsageInfo = Pick - -// Relace only supports apply through a /v1/apply endpoint -export class RelaceApi implements BaseLlmApi { - private apiBase = "https://instantapply.endpoint.relace.run/v1/" - - constructor(private readonly config: z.infer) { - this.apiBase = config.apiBase ?? this.apiBase - if (!this.apiBase.endsWith("/")) { - this.apiBase += "/" - } - this.config = config - } - - async chatCompletionNonStream( - body: ChatCompletionCreateParamsNonStreaming, - signal: AbortSignal, - ): Promise { - let content = "" - let usage: UsageInfo | undefined = undefined - - // Convert the non-streaming params to streaming params - const streamingBody: ChatCompletionCreateParamsStreaming = { - ...body, - stream: true, - } - - for await (const chunk of this.chatCompletionStream(streamingBody, signal)) { - if (chunk.choices.length > 0) { - content += chunk.choices[0]?.delta?.content || "" - } - if (chunk.usage) { - usage = chunk.usage - } - } - - return chatCompletion({ - content, - model: body.model, - usage, - }) - } - - // We convert from what would be sent to OpenAI (a prediction for the existing code and a user message with the new code) - // to Relace's format - async *chatCompletionStream( - body: ChatCompletionCreateParamsStreaming, - signal: AbortSignal, - ): AsyncGenerator { - const headers = { - "Content-Type": "application/json", - Authorization: `Bearer ${this.config.apiKey}`, - } - - const prediction = body.prediction?.content ?? "" - const initialCode = typeof prediction === "string" ? prediction : prediction.map((p) => p.text).join("") - - const userContent = body.messages.find((m) => m.role === "user")?.content - if (!userContent) { - throw new Error("No edit snippet provided.") - } - - const editSnippet = - typeof userContent === "string" - ? userContent - : userContent - .filter((p) => p.type === "text") - .map((p) => p.text) - .join("") - - const data = { - initialCode, - editSnippet, - } - - const url = this.apiBase + "code/apply" - const response = await fetch(url, { - method: "POST", - headers, - body: JSON.stringify(data), - signal, - }) - - if (response.status === 499) { - return // Aborted by user - } - - const result = (await response.json()) as any - const mergedCode = result.mergedCode - - yield chatChunk({ - content: mergedCode, - model: body.model, - }) - - yield usageChatChunk({ - model: body.model, - usage: { - prompt_tokens: result.usage.prompt_tokens || 0, - completion_tokens: result.usage.completion_tokens || 0, - total_tokens: result.usage.total_tokens, - }, - }) - } - - completionNonStream(): Promise { - throw new Error("Relace provider does not support non-streaming completion.") - } - completionStream(): AsyncGenerator { - throw new Error("Relace provider does not support streaming completion.") - } - fimStream(): AsyncGenerator { - throw new Error("Relace provider does not support streaming FIM completion.") - } - rerank(): Promise { - throw new Error("Relace provider does not support reranking.") - } - list(): Promise { - throw new Error("Relace provider does not support model listing.") - } -} diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/VertexAI.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/VertexAI.ts deleted file mode 100644 index 68ebd369866..00000000000 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/VertexAI.ts +++ /dev/null @@ -1,439 +0,0 @@ -import { streamSse } from "../../../fetch/stream.js" -import { AuthClient, GoogleAuth, JWT, auth } from "google-auth-library" -import { - ChatCompletion, - ChatCompletionChunk, - ChatCompletionCreateParams, - ChatCompletionCreateParamsNonStreaming, - ChatCompletionCreateParamsStreaming, - Completion, - CompletionCreateParamsNonStreaming, - CompletionCreateParamsStreaming, - Model, -} from "openai/resources/index" -import { VertexAIConfig } from "../types.js" -import { chatChunk, chatCompletion } from "../util.js" -import { AnthropicApi } from "./Anthropic.js" -import { BaseLlmApi, CreateRerankResponse, FimCreateParamsStreaming } from "./base.js" -import { GeminiApi } from "./Gemini.js" -import { OpenAIApi } from "./OpenAI.js" - -export class VertexAIApi implements BaseLlmApi { - anthropicInstance: AnthropicApi - geminiInstance: GeminiApi - mistralInstance: OpenAIApi - private clientPromise?: Promise - static AUTH_SCOPES = "https://www.googleapis.com/auth/cloud-platform" - - constructor(protected config: VertexAIConfig) { - this.setupAuthentication() - - // These sub-instances are only used to convert and handle responses, - // So do not need apiKey, etc - this.anthropicInstance = new AnthropicApi({ - provider: "anthropic", - apiKey: "dud", - }) - this.geminiInstance = new GeminiApi({ - provider: "gemini", - apiKey: "dud", - }) - this.mistralInstance = new OpenAIApi({ - provider: "mistral", - apiKey: "dud", - }) - } - - private setupAuthentication(): void { - const { apiKey, env } = this.config - const { region, projectId, keyFile, keyJson } = env || {} - - // Validate authentication configuration - if (apiKey) { - // Express mode validation - if (region || projectId || keyFile || keyJson) { - throw new Error( - "VertexAI in express mode (apiKey only) cannot be configured with region, projectId, keyFile, or keyJson", - ) - } - } else { - // Standard mode validation - if (!region || !projectId) { - throw new Error("region and projectId are required for VertexAI (when not using express/apiKey mode)") - } - if (keyFile && keyJson) { - throw new Error("VertexAI credentials can be configured with either keyFile or keyJson but not both") - } - } - - // Set up authentication client - if (keyJson) { - try { - const parsed = JSON.parse(keyJson) - if (!parsed?.private_key) { - throw new Error("VertexAI: keyJson must contain a valid private key") - } - parsed.private_key = parsed.private_key.replace(/\\n/g, "\n") - const jsonClient = auth.fromJSON(parsed) - if (jsonClient instanceof JWT) { - jsonClient.scopes = [VertexAIApi.AUTH_SCOPES] - } else { - throw new Error("VertexAI: keyJson must be a valid JWT") - } - this.clientPromise = Promise.resolve(jsonClient) - } catch { - throw new Error("VertexAI: Failed to parse keyJson") - } - } else if (keyFile) { - if (typeof keyFile !== "string") { - throw new Error("VertexAI: keyFile must be a string") - } - this.clientPromise = new GoogleAuth({ - scopes: VertexAIApi.AUTH_SCOPES, - keyFile, - }) - .getClient() - .catch((e: Error) => { - console.warn(`Failed to load credentials for Vertex AI: ${e.message}`) - }) - } else if (!apiKey) { - // Application Default Credentials - this.clientPromise = new GoogleAuth({ - scopes: VertexAIApi.AUTH_SCOPES, - }) - .getClient() - .catch((e: Error) => { - console.warn(`Failed to load credentials for Vertex AI: ${e.message}`) - }) - } - } - - private getApiBase(): string { - const { apiKey, env } = this.config - - if (this.config.apiBase) { - return this.config.apiBase - } - - if (apiKey) { - // Express mode - return "https://aiplatform.googleapis.com/v1/" - } else { - // Standard mode - const { region, projectId } = env! - return `https://${region}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${region}/` - } - } - - private determineVertexProvider(model: string): "mistral" | "anthropic" | "gemini" | "unknown" { - if (model.includes("mistral") || model.includes("codestral") || model.includes("mixtral")) { - return "mistral" - } else if (model.includes("claude")) { - return "anthropic" - } else if (model.includes("gemini")) { - return "gemini" - } - return "unknown" - } - - private async getAuthHeaders(): Promise> { - const headers: Record = { - "Content-Type": "application/json", - // Accept: "application/json" - } - - // TODO - support anthropic prompt caching with "anthropic-beta" header - - if (this.config.apiKey) { - // Express mode - no Authorization header needed, API key is in URL - return headers - } else { - // Standard mode - use OAuth token - const client = await this.clientPromise - const result = await client?.getAccessToken() - if (!result?.token) { - throw new Error("Could not get an access token. Set up your Google Application Default Credentials.") - } - headers.Authorization = `Bearer ${result.token}` - return headers - } - } - - private buildUrl(endpoint: string): URL { - const apiBase = this.getApiBase() - const url = new URL(endpoint, apiBase) - - if (this.config.apiKey) { - url.searchParams.set("key", this.config.apiKey) - } - - return url - } - - private convertAnthropicBody(oaiBody: ChatCompletionCreateParams): object { - const body = this.anthropicInstance._convertToCleanAnthropicBody(oaiBody) - const { model: _model, ...exceptModel } = body - return { - ...exceptModel, - anthropic_version: "vertex-2023-10-16", - } - } - - private convertGeminiBody(oaiBody: ChatCompletionCreateParams, url: URL): object { - return this.geminiInstance._convertBody(oaiBody, url.toString()) - } - - async chatCompletionNonStream( - body: ChatCompletionCreateParamsNonStreaming, - signal: AbortSignal, - ): Promise { - const vertexProvider = this.determineVertexProvider(body.model) - - if (this.config.apiKey && vertexProvider !== "gemini") { - throw new Error("VertexAI: only gemini models are supported in express (apiKey) mode") - } - - const headers = await this.getAuthHeaders() - let url: URL - let requestBody: any - - switch (vertexProvider) { - case "anthropic": - url = this.buildUrl(`publishers/anthropic/models/${body.model}:rawPredict`) - requestBody = this.convertAnthropicBody(body) - break - case "gemini": - url = this.buildUrl(`publishers/google/models/${body.model}:generateContent`) - requestBody = this.convertGeminiBody(body, url) - break - case "mistral": - url = this.buildUrl(`publishers/mistralai/models/${body.model}:rawPredict`) - requestBody = body - break - default: - throw new Error(`Unsupported model: ${body.model}`) - } - - const response = await fetch(url.toString(), { - method: "POST", - headers, - body: JSON.stringify(requestBody), - signal, - }) - - const data = await response.json() - - if (!response.ok) { - throw new Error(`VertexAI API error: ${response.status} ${response.statusText}\n${JSON.stringify(data)}`) - } - - // Convert response to OpenAI format - switch (vertexProvider) { - case "anthropic": - return chatCompletion({ - content: data.content?.[0]?.text || "", - model: body.model, - }) - case "gemini": - return chatCompletion({ - content: data.candidates?.[0]?.content?.parts?.[0]?.text || "", - model: body.model, - }) - case "mistral": - return chatCompletion({ - content: data.choices?.[0]?.message?.content || "", - model: body.model, - }) - default: - throw new Error(`Unsupported provider: ${vertexProvider}`) - } - } - - async *chatCompletionStream( - body: ChatCompletionCreateParamsStreaming, - signal: AbortSignal, - ): AsyncGenerator { - const vertexProvider = this.determineVertexProvider(body.model) - - if (this.config.apiKey && vertexProvider !== "gemini") { - throw new Error("VertexAI: only gemini models are supported in express (apiKey) mode") - } - - const headers = await this.getAuthHeaders() - let url: URL - let requestBody: any - - switch (vertexProvider) { - case "anthropic": - url = this.buildUrl(`publishers/anthropic/models/${body.model}:streamRawPredict`) - requestBody = this.convertAnthropicBody(body) - break - case "gemini": - url = this.buildUrl(`publishers/google/models/${body.model}:streamGenerateContent`) - requestBody = this.convertGeminiBody(body, url) - break - case "mistral": - url = this.buildUrl(`publishers/mistralai/models/${body.model}:streamRawPredict`) - requestBody = body - break - default: - throw new Error(`Unsupported model: ${body.model}`) - } - - switch (vertexProvider) { - case "mistral": { - const mistralResponse = await this.mistralInstance.openai.chat.completions.create( - this.mistralInstance.modifyChatBody(body), - { - signal, - headers, - }, - ) - for await (const result of mistralResponse) { - yield result - } - break - } - case "anthropic": - case "gemini": { - const response = await fetch(url.toString(), { - method: "POST", - headers, - body: JSON.stringify(requestBody), - signal, - }) - - if (!response.ok) { - const data = await response.json() - throw new Error(`VertexAI API error: ${response.status} ${response.statusText}\n${JSON.stringify(data)}`) - } - - if (response.status === 499) { - return // Aborted by user - } - if (vertexProvider === "gemini") { - yield* this.geminiInstance.handleStreamResponse(response, body.model) - } else { - yield* this.anthropicInstance.handleStreamResponse(response, body.model) - } - break - } - } - } - - async completionNonStream(body: CompletionCreateParamsNonStreaming, signal: AbortSignal): Promise { - // Convert completion to chat completion and back - const promptText = - typeof body.prompt === "string" ? body.prompt : Array.isArray(body.prompt) ? body.prompt.join("") : "" - - const chatBody: ChatCompletionCreateParamsNonStreaming = { - model: body.model, - messages: [{ role: "user", content: promptText }], - max_tokens: body.max_tokens, - temperature: body.temperature, - top_p: body.top_p, - stop: body.stop, - stream: false, - } - - const chatResponse = await this.chatCompletionNonStream(chatBody, signal) - - return { - id: chatResponse.id, - object: "text_completion", - created: chatResponse.created, - model: chatResponse.model, - choices: [ - { - text: chatResponse.choices[0]?.message?.content || "", - index: 0, - logprobs: null, - finish_reason: chatResponse.choices[0]?.finish_reason || null, - }, - ], - usage: chatResponse.usage, - } as Completion - } - - async *completionStream(body: CompletionCreateParamsStreaming, signal: AbortSignal): AsyncGenerator { - // Convert completion to chat completion and back - const promptText = - typeof body.prompt === "string" ? body.prompt : Array.isArray(body.prompt) ? body.prompt.join("") : "" - - const chatBody: ChatCompletionCreateParamsStreaming = { - model: body.model, - messages: [{ role: "user", content: promptText }], - max_tokens: body.max_tokens, - temperature: body.temperature, - top_p: body.top_p, - stop: body.stop, - stream: true, - } - - for await (const chatChunk of this.chatCompletionStream(chatBody, signal)) { - yield { - id: chatChunk.id, - object: "text_completion", - created: chatChunk.created, - model: chatChunk.model, - choices: [ - { - text: chatChunk.choices[0]?.delta?.content || "", - index: 0, - logprobs: null, - finish_reason: chatChunk.choices[0]?.finish_reason || null, - }, - ], - } as Completion - } - } - - async *fimStream(body: FimCreateParamsStreaming, signal: AbortSignal): AsyncGenerator { - // Only Codestral (Mistral) supports FIM in VertexAI - if (!body.model.includes("codestral")) { - throw new Error(`FIM is only supported for Codestral models, got: ${body.model}`) - } - - const headers = await this.getAuthHeaders() - const url = this.buildUrl(`publishers/mistralai/models/${body.model}:streamRawPredict`) - - const requestBody = { - model: body.model, - max_tokens: body.max_tokens, - temperature: body.temperature, - top_p: body.top_p, - stream: body.stream ?? true, - stop: body.stop, - prompt: body.prompt, - suffix: body.suffix, - } - - const response = await fetch(url.toString(), { - method: "POST", - headers, - body: JSON.stringify(requestBody), - signal, - }) - - if (!response.ok) { - throw new Error(`VertexAI API error: ${response.status} ${response.statusText}`) - } - - for await (const chunk of streamSse(response)) { - if (chunk.choices?.[0]?.delta?.content) { - yield chatChunk({ - content: chunk.choices[0].delta.content, - model: body.model, - }) - } - } - } - - async rerank(): Promise { - throw new Error("Reranking is not supported by VertexAI") - } - - async list(): Promise { - throw new Error("VertexAI provider does not support model listing.") - } -} diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/WatsonX.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/WatsonX.ts deleted file mode 100644 index ceced25d0c6..00000000000 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/WatsonX.ts +++ /dev/null @@ -1,322 +0,0 @@ -import { streamSse } from "../../../fetch/stream.js" -import { OpenAI } from "openai/index" -import { - ChatCompletion, - ChatCompletionChunk, - ChatCompletionCreateParamsNonStreaming, - ChatCompletionCreateParamsStreaming, - Completion, - CompletionCreateParamsStreaming, -} from "openai/resources/index" -import { ChatCompletionCreateParams } from "openai/resources/index.js" -import { WatsonXConfig } from "../types.js" -import { chatCompletion } from "../util.js" -import { BaseLlmApi, CreateRerankResponse } from "./base.js" - -export class WatsonXApi implements BaseLlmApi { - apiBase: string - apiVersion: string = "2023-05-29" - projectId?: string - deploymentId?: string - - constructor(protected config: WatsonXConfig) { - this.apiBase = config.apiBase ?? "https://us-south.ml.cloud.ibm.com" - if (!this.apiBase.endsWith("/")) { - this.apiBase += "/" - } - this.apiVersion = config.env.apiVersion ?? this.apiVersion - this.projectId = config.env.projectId - this.deploymentId = config.env.deploymentId - } - - async getBearerToken(): Promise<{ token: string; expiration: number }> { - if (this.apiBase?.includes("cloud.ibm.com")) { - // watsonx SaaS - const wxToken = (await ( - await fetch( - `https://iam.cloud.ibm.com/identity/token?apikey=${this.config.apiKey}&grant_type=urn:ibm:params:oauth:grant-type:apikey`, - { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Accept: "application/json", - }, - }, - ) - ).json()) as any - return { - token: wxToken["access_token"], - expiration: wxToken["expiration"], - } - } else { - // watsonx Software - // if (this.config.env.bearerTokenRequired) { - // In certain WatsonX environments, ZenApiKey authentication is disabled, - // and it's necessary to call this endpoint with username+api_key to get a bearer token. - // See the docs: https://www.ibm.com/docs/en/watsonx/w-and-w/2.1.0?topic=keys-generating-bearer-token - // Ask @sestinj why the rest is commented out. - const base64Decoded = Buffer.from(this.config.apiKey ?? "", "base64").toString() - const [username, api_key] = base64Decoded.split(":") - - const wxToken = (await ( - await fetch(new URL("icp4d-api/v1/authorize", this.apiBase), { - method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - body: JSON.stringify({ - username: username?.trim(), - api_key: api_key?.trim(), - }), - }) - ).json()) as any - - return { - token: wxToken["access_token"] ?? wxToken["token"], - expiration: 0, - } - // } else if (!this.config.apiKey?.includes(":")) { - // // Using ZenApiKey auth - // return { - // token: this.config.apiKey ?? "", - // expiration: -1, - // }; - // } else { - // // Using username/password auth - // const userPass = this.config.apiKey?.split(":"); - // const wxToken = (await ( - // await fetch( - // `${this.apiBase}/icp4d-api/v1/authorize`, - // { - // method: "POST", - // headers: { - // "Content-Type": "application/json", - // Accept: "application/json", - // }, - // body: JSON.stringify({ - // username: userPass[0], - // password: userPass[1], - // }), - // }, - // ) - // ).json()) as any; - // const wxTokenExpiry = (await ( - // await fetch( - // `${this.apiBase}/usermgmt/v1/user/tokenExpiry`, - // { - // method: "GET", - // headers: { - // Accept: "application/json", - // Authorization: `Bearer ${wxToken["token"]}`, - // }, - // }, - // ) - // ).json()) as any; - // return { - // token: wxToken["token"], - // expiration: wxTokenExpiry["exp"], - // }; - // } - } - } - - private getEndpoint(endpoint: string): string { - return `${this.apiBase}ml/v1/${this.deploymentId ? `deployments/${this.deploymentId}/` : ""}text/${endpoint}_stream?version=${this.apiVersion}` - } - - private _convertBody(oaiBody: ChatCompletionCreateParams) { - const stopSequences = oaiBody.stop - ? Array.isArray(oaiBody.stop) - ? oaiBody.stop.filter((s) => s.trim() !== "") - : [oaiBody.stop] - : undefined - - const payload: any = { - messages: oaiBody.messages, - max_tokens: oaiBody.max_tokens ?? 1024, - stop: stopSequences, - frequency_penalty: oaiBody.frequency_penalty, - presence_penalty: oaiBody.presence_penalty, - } - - if (!this.deploymentId) { - payload.model_id = oaiBody.model - payload.project_id = this.projectId - } - - if (oaiBody.temperature !== undefined) { - payload.temperature = oaiBody.temperature - } - - if (oaiBody.top_p !== undefined) { - payload.top_p = oaiBody.top_p - } - - if (oaiBody.tools) { - payload.tools = oaiBody.tools - if (oaiBody.tool_choice) { - payload.tool_choice = oaiBody.tool_choice - } else { - payload.tool_choice_option = "auto" - } - } - - return payload - } - - private async getHeaders(): Promise> { - const bearer = await this.getBearerToken() - // const isZenApiKey = bearer.expiration === -1; - - return { - "Content-Type": "application/json", - // Authorization: `${isZenApiKey ? "ZenApiKey" : "Bearer"} ${bearer.token}`, - Authorization: `Bearer ${bearer.token}`, - } - } - - async chatCompletionNonStream( - body: ChatCompletionCreateParamsNonStreaming, - signal: AbortSignal, - ): Promise { - const generator = this.chatCompletionStream( - { - ...body, - stream: true, - }, - signal, - ) - - let content = "" - for await (const chunk of generator) { - content += chunk.choices[0].delta.content ?? "" - } - return chatCompletion({ - content, - model: body.model, - }) - } - - async *chatCompletionStream( - body: ChatCompletionCreateParamsStreaming, - signal: AbortSignal, - ): AsyncGenerator { - const url = this.getEndpoint("chat") - const headers = await this.getHeaders() - const stringifiedBody = JSON.stringify({ - time_limit: 8000, - ...this._convertBody(body), - stream: true, - }) - const response = await fetch(url, { - method: "POST", - headers, - body: stringifiedBody, - signal, - }) - - if (!response.ok || !response.body) { - throw new Error(`Failed to stream chat completion: ${await response.text()}`) - } - - for await (const value of streamSse(response as any)) { - if (!value.choices?.[0]) { - continue - } - yield value - } - } - - async completionNonStream(): Promise { - throw new Error("Method not implemented.") - } - - async *completionStream( - body: CompletionCreateParamsStreaming, - signal: AbortSignal, - ): AsyncGenerator { - const params = { - decoding_method: body.temperature ? "sample" : "greedy", - max_new_tokens: body.max_tokens ?? 1024, - min_new_tokens: 1, - stop_sequences: body.stop ? (Array.isArray(body.stop) ? body.stop : [body.stop]) : [], - include_stop_sequence: false, - repetition_penalty: body.frequency_penalty || 1, - temperature: body.temperature, - top_p: body.top_p, - top_k: 100, - } - - const payload: any = { - input: body.prompt, - parameters: params, - } - - if (!this.deploymentId) { - payload.model_id = body.model - payload.project_id = this.projectId - } - - const url = this.getEndpoint("generation") - const response = await fetch(url, { - method: "POST", - headers: await this.getHeaders(), - body: JSON.stringify(payload), - signal, - }) - - if (!response.ok || !response.body) { - throw new Error(`Failed to stream completion: ${await response.text()}`) - } - - for await (const value of streamSse(response as any)) { - const lines = value.toString().split("\n") - let generatedChunk = "" - - lines.forEach((line: string) => { - if (line.startsWith("data:")) { - const dataStr = line.replace(/^data:\s+/, "") - try { - const data = JSON.parse(dataStr) - data.results.forEach((result: any) => { - generatedChunk += result.generated_text || "" - }) - } catch { - // parsing error is expected with streaming response - } - } - }) - - if (generatedChunk) { - yield { - id: `watsonx-${Date.now()}`, - object: "text_completion", - created: Date.now(), - model: body.model, - choices: [ - { - text: generatedChunk, - index: 0, - logprobs: null, - finish_reason: "stop", - }, - ], - } - } - } - } - - // eslint-disable-next-line require-yield - async *fimStream(): AsyncGenerator { - throw new Error("Method not implemented.") - } - - async rerank(): Promise { - throw new Error("Method not implemented.") - } - - async list(): Promise { - throw new Error("Method not implemented.") - } -} diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/base.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/base.ts deleted file mode 100644 index 95305a253ed..00000000000 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/apis/base.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { - ChatCompletion, - ChatCompletionChunk, - ChatCompletionCreateParamsNonStreaming, - ChatCompletionCreateParamsStreaming, - Completion, - CompletionCreateParamsNonStreaming, - CompletionCreateParamsStreaming, - Model, -} from "openai/resources/index" - -export interface FimCreateParamsStreaming extends CompletionCreateParamsStreaming { - suffix: string -} - -export interface RerankCreateParams { - query: string - documents: string[] - model: string - top_k?: number -} - -interface CreateRerankItem { - relevance_score: number - index: number -} - -export interface CreateRerankResponse { - object: "list" - data: CreateRerankItem[] - model: string - usage: { - total_tokens: number - } -} - -export interface BaseLlmApi { - // Chat, no stream - chatCompletionNonStream(body: ChatCompletionCreateParamsNonStreaming, signal: AbortSignal): Promise - - // Chat, stream - chatCompletionStream( - body: ChatCompletionCreateParamsStreaming, - signal: AbortSignal, - ): AsyncGenerator - - // Completion, no stream - completionNonStream(body: CompletionCreateParamsNonStreaming, signal: AbortSignal): Promise - - // Completion, stream - completionStream(body: CompletionCreateParamsStreaming, signal: AbortSignal): AsyncGenerator - - // FIM, stream - fimStream(body: FimCreateParamsStreaming, signal: AbortSignal): AsyncGenerator - - // Reranking - rerank(body: RerankCreateParams): Promise - - // List Models - list(): Promise -} diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/index.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/index.ts deleted file mode 100644 index e7dfdc093f8..00000000000 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/index.ts +++ /dev/null @@ -1,162 +0,0 @@ -import dotenv from "dotenv" -import { z } from "zod" -import { AnthropicApi } from "./apis/Anthropic.js" -import { AzureApi } from "./apis/Azure.js" -import { BedrockApi } from "./apis/Bedrock.js" -import { CohereApi } from "./apis/Cohere.js" -import { CometAPIApi } from "./apis/CometAPI.js" -import { ContinueProxyApi } from "./apis/ContinueProxy.js" -import { DeepSeekApi } from "./apis/DeepSeek.js" -import { GeminiApi } from "./apis/Gemini.js" -import { InceptionApi } from "./apis/Inception.js" -import { JinaApi } from "./apis/Jina.js" -import { LlamastackApi } from "./apis/LlamaStack.js" -import { MockApi } from "./apis/Mock.js" -import { MoonshotApi } from "./apis/Moonshot.js" -import { OpenAIApi } from "./apis/OpenAI.js" -import { OpenRouterApi } from "./apis/OpenRouter.js" -import { RelaceApi } from "./apis/Relace.js" -import { VertexAIApi } from "./apis/VertexAI.js" -import { WatsonXApi } from "./apis/WatsonX.js" -import { BaseLlmApi } from "./apis/base.js" -import { LLMConfig, OpenAIConfigSchema } from "./types.js" - -dotenv.config() - -function openAICompatible(apiBase: string, config: z.infer): OpenAIApi { - return new OpenAIApi({ - ...config, - apiBase: config.apiBase ?? apiBase, - }) -} - -/** - * Detects if a HuggingFace API URL is using an OpenAI-compatible router - * @param url The URL to check - * @returns true if the URL appears to be using an OpenAI-compatible router - */ -function isHuggingFaceOpenAICompatible(url: string): boolean { - if (!url) { - return false - } - - // Normalize the URL to lowercase for case-insensitive matching - const normalizedUrl = url.toLowerCase() - - // Check for common OpenAI-compatible patterns - const openAIPatterns = [ - "/v1/", // Standard OpenAI v1 API pattern - "/openai/", // Explicit OpenAI compatibility path - "/v1/chat/completions", // Specific OpenAI chat completions endpoint - "/v1/completions", // OpenAI completions endpoint - "/v1/embeddings", // OpenAI embeddings endpoint - "/v1/models", // OpenAI models endpoint - ] - - // Check if the URL contains any of the OpenAI-compatible patterns - return openAIPatterns.some((pattern) => normalizedUrl.includes(pattern)) -} - -export function constructLlmApi(config: LLMConfig): BaseLlmApi | undefined { - switch (config.provider) { - case "openai": - return new OpenAIApi(config) - case "azure": - return new AzureApi(config) - case "bedrock": - return new BedrockApi(config) - case "cohere": - return new CohereApi(config) - case "cometapi": - return new CometAPIApi(config) - case "anthropic": - return new AnthropicApi(config) - case "gemini": - return new GeminiApi(config) - case "jina": - return new JinaApi(config) - case "deepseek": - return new DeepSeekApi(config) - case "moonshot": - return new MoonshotApi(config) - case "relace": - return new RelaceApi(config) - case "inception": - return new InceptionApi(config) - case "watsonx": - return new WatsonXApi(config) - case "vertexai": - return new VertexAIApi(config) - case "llamastack": - return new LlamastackApi(config) - case "continue-proxy": - return new ContinueProxyApi(config) - case "xAI": - return openAICompatible("https://api.x.ai/v1/", config) - case "voyage": - return openAICompatible("https://api.voyageai.com/v1/", config) - case "mistral": - return openAICompatible("https://api.mistral.ai/v1/", config) - case "deepinfra": - return openAICompatible("https://api.deepinfra.com/v1/openai/", config) - case "vllm": - return openAICompatible("http://localhost:8000/v1/", config) - case "groq": - return openAICompatible("https://api.groq.com/openai/v1/", config) - case "sambanova": - return openAICompatible("https://api.sambanova.ai/v1/", config) - case "text-gen-webui": - return openAICompatible("http://127.0.0.1:5000/v1/", config) - case "cerebras": - return openAICompatible("https://api.cerebras.ai/v1/", config) - case "kindo": - return openAICompatible("https://llm.kindo.ai/v1/", config) - case "msty": - return openAICompatible("http://localhost:10000", config) - case "nvidia": - return openAICompatible("https://integrate.api.nvidia.com/v1/", config) - case "ovhcloud": - return openAICompatible("https://oai.endpoints.kepler.ai.cloud.ovh.net/v1/", config) - case "scaleway": - return openAICompatible("https://api.scaleway.ai/v1/", config) - case "fireworks": - return openAICompatible("https://api.fireworks.ai/inference/v1/", config) - case "together": - return openAICompatible("https://api.together.xyz/v1/", config) - case "ncompass": - return openAICompatible("https://api.ncompass.tech/v1", config) - case "novita": - return openAICompatible("https://api.novita.ai/v3/openai", config) - case "nebius": - return openAICompatible("https://api.studio.nebius.ai/v1/", config) - case "function-network": - return openAICompatible("https://api.function.network/v1/", config) - case "openrouter": - return new OpenRouterApi(config) - case "llama.cpp": - case "llamafile": - return openAICompatible("http://localhost:8000/", config) - case "lmstudio": - return openAICompatible("http://localhost:1234/", config) - case "ollama": - // for openai compaitability, we need to add /v1 to the end of the url - // this is required for cli (for core, endpoints are overriden by core/llm/llms/Ollama.ts) - config.apiBase = config.apiBase && new URL("v1/", config.apiBase).toString() - return openAICompatible("http://localhost:11434/v1/", config) - case "mock": - return new MockApi() - case "huggingface-inference-api": - // Check if it's an OpenAI-compatible router - if (config.apiBase && isHuggingFaceOpenAICompatible(config.apiBase)) { - return openAICompatible(config.apiBase, config) - } - // Return undefined for native HuggingFace endpoints - // (handled by HuggingFaceInferenceAPI class in core) - return undefined - default: - return undefined - } -} - -// export -export type { BaseLlmApi } from "./apis/base.js" diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/types.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/types.ts deleted file mode 100644 index dd92cee612d..00000000000 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/types.ts +++ /dev/null @@ -1,185 +0,0 @@ -import * as z from "zod" - -// Base config objects -const BaseConfig = z.object({ - provider: z.string(), -}) - -const BasePlusConfig = BaseConfig.extend({ - apiBase: z.string().optional(), - apiKey: z.string().optional(), -}) - -// OpenAI and compatible -export const OpenAIConfigSchema = BasePlusConfig.extend({ - provider: z.union([ - z.literal("openai"), - z.literal("mistral"), - z.literal("voyage"), - z.literal("deepinfra"), - z.literal("groq"), - z.literal("nvidia"), - z.literal("ovhcloud"), - z.literal("fireworks"), - z.literal("together"), - z.literal("novita"), - z.literal("nebius"), - z.literal("function-network"), - z.literal("llama.cpp"), - z.literal("llamafile"), - z.literal("lmstudio"), - z.literal("ollama"), - z.literal("cerebras"), - z.literal("kindo"), - z.literal("msty"), - z.literal("openrouter"), - z.literal("sambanova"), - z.literal("text-gen-webui"), - z.literal("vllm"), - z.literal("xAI"), - z.literal("scaleway"), - z.literal("ncompass"), - z.literal("relace"), - z.literal("huggingface-inference-api"), - ]), -}) -export type OpenAIConfig = z.infer - -const _MoonshotConfigSchema = OpenAIConfigSchema.extend({ - provider: z.literal("moonshot"), -}) -export type MoonshotConfig = z.infer - -const _DeepseekConfigSchema = OpenAIConfigSchema.extend({ - provider: z.literal("deepseek"), -}) -export type DeepseekConfig = z.infer - -const _BedrockConfigSchema = OpenAIConfigSchema.extend({ - provider: z.literal("bedrock"), - // cacheBehavior: z.object({ - // cacheSystemMessage: z.boolean().optional(), - // cacheConversation: z.boolean().optional(), - // }).optional(), - env: z - .object({ - region: z.string().optional(), - accessKeyId: z.string().optional(), - secretAccessKey: z.string().optional(), - profile: z.string().optional(), - }) - .optional(), -}) -export type BedrockConfig = z.infer - -const _LlamastackConfigSchema = OpenAIConfigSchema.extend({ - provider: z.literal("llamastack"), -}) -export type LlamastackConfig = z.infer - -export const ContinueProxyConfigSchema = BasePlusConfig.extend({ - provider: z.literal("continue-proxy"), - env: z.object({ - apiKeyLocation: z.string().optional(), - envSecretLocations: z.record(z.string(), z.string()).optional(), - orgScopeId: z.string().nullable(), - proxyUrl: z.string().optional(), - }), -}) - -const _MockConfigSchema = BasePlusConfig.extend({ - provider: z.literal("mock"), -}) - -// Other APIs -const _CohereConfigSchema = OpenAIConfigSchema.extend({ - provider: z.literal("cohere"), -}) -export type CohereConfig = z.infer - -const _CometAPIConfigSchema = OpenAIConfigSchema.extend({ - provider: z.literal("cometapi"), -}) -export type CometAPIConfig = z.infer - -export const AzureConfigSchema = OpenAIConfigSchema.extend({ - provider: z.literal("azure"), - env: z - .object({ - apiVersion: z.string().optional(), - apiType: z - .union([ - z.literal("azure-foundry"), - z.literal("azure-openai"), - z.literal("azure"), // Legacy - ]) - .optional(), - deployment: z.string().optional(), - }) - .optional(), -}) - -const _GeminiConfigSchema = OpenAIConfigSchema.extend({ - provider: z.literal("gemini"), - apiKey: z.string(), -}) -export type GeminiConfig = z.infer - -const _AnthropicConfigSchema = OpenAIConfigSchema.extend({ - provider: z.literal("anthropic"), - apiKey: z.string(), -}) -export type AnthropicConfig = z.infer - -const _WatsonXConfigSchema = BasePlusConfig.extend({ - provider: z.literal("watsonx"), - apiKey: z.string(), - env: z.object({ - apiVersion: z.string().optional(), - projectId: z.string().optional(), - deploymentId: z.string().optional(), - }), -}) -export type WatsonXConfig = z.infer - -const _JinaConfigSchema = OpenAIConfigSchema.extend({ - provider: z.literal("jina"), -}) -export type JinaConfig = z.infer - -const _InceptionConfigSchema = OpenAIConfigSchema.extend({ - provider: z.literal("inception"), -}) -export type InceptionConfig = z.infer - -const _VertexAIConfigSchema = BasePlusConfig.extend({ - provider: z.literal("vertexai"), - env: z - .object({ - region: z.string().optional(), - projectId: z.string().optional(), - keyFile: z.string().optional(), - keyJson: z.string().optional(), - }) - .optional(), -}) -export type VertexAIConfig = z.infer - -// Discriminated union -export type LLMConfig = - | OpenAIConfig - | BedrockConfig - | MoonshotConfig - | DeepseekConfig - | CohereConfig - | z.infer - | GeminiConfig - | AnthropicConfig - | WatsonXConfig - | JinaConfig - | z.infer - | InceptionConfig - | VertexAIConfig - | LlamastackConfig - | z.infer - | CometAPIConfig diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/util.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/util.ts deleted file mode 100644 index 2404509f39e..00000000000 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/util.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { ChatCompletionChunk, CompletionUsage } from "openai/resources/index" - -import { ChatCompletion } from "openai/resources/index.js" -import { CreateRerankResponse } from "./apis/base.js" - -export function chatChunk(options: { - content: string | null | undefined - model: string - finish_reason?: ChatCompletionChunk.Choice["finish_reason"] - id?: string | null - usage?: CompletionUsage -}): ChatCompletionChunk { - return { - choices: [ - { - delta: { - content: options.content, - role: "assistant", - }, - finish_reason: options.finish_reason ?? "stop", - index: 0, - logprobs: null, - }, - ], - usage: options.usage, - created: Date.now(), - id: options.id ?? "", - model: options.model, - object: "chat.completion.chunk", - } -} - -export function usageChatChunk(options: { - model: string - id?: string | null - usage?: CompletionUsage -}): ChatCompletionChunk { - return { - choices: [], - usage: options.usage, - created: Date.now(), - id: options.id ?? "", - model: options.model, - object: "chat.completion.chunk", - } -} - -export function chatCompletion(options: { - content: string | null | undefined - model: string - finish_reason?: ChatCompletion.Choice["finish_reason"] - id?: string | null - usage?: CompletionUsage - index?: number | null -}): ChatCompletion { - return { - choices: [ - { - finish_reason: options.finish_reason ?? "stop", - index: options.index ?? 0, - logprobs: null, - message: { - content: options.content ?? null, - role: "assistant", - refusal: null, - }, - }, - ], - usage: options.usage, - created: Date.now(), - id: options.id ?? "", - model: options.model, - object: "chat.completion", - } -} - -export function rerank(options: { - model: string - data: number[] - usage?: CreateRerankResponse["usage"] -}): CreateRerankResponse { - return { - data: options.data.map((score, index) => ({ - index, - relevance_score: score, - })), - model: options.model, - object: "list" as const, - usage: options.usage ?? { - total_tokens: 0, - }, - } -} diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/util/emptyChatCompletion.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/util/emptyChatCompletion.ts deleted file mode 100644 index cc49014ae76..00000000000 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/util/emptyChatCompletion.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ChatCompletion } from "openai/resources/index" - -export const EMPTY_CHAT_COMPLETION: ChatCompletion = { - choices: [ - { - finish_reason: "stop", - index: 0, - logprobs: null, - message: { - content: null, - role: "assistant", - refusal: null, - }, - }, - ], - usage: undefined, - created: Date.now(), - id: "", - model: "UNSPECIFIED", - object: "chat.completion", -} diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/util/gemini-types.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/util/gemini-types.ts deleted file mode 100644 index 41ce2c09f88..00000000000 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openai-adapters/util/gemini-types.ts +++ /dev/null @@ -1,333 +0,0 @@ -import { ChatCompletionTool } from "openai/resources/index.mjs" - -type GeminiObjectSchemaType = "TYPE_UNSPECIFIED" | "STRING" | "NUMBER" | "INTEGER" | "BOOLEAN" | "ARRAY" | "OBJECT" - -interface GeminiObjectSchema { - type: GeminiObjectSchemaType - format?: string - title?: string - description?: string - nullable?: boolean - enum?: string[] - maxItems?: string - minItems?: string - properties?: Record - required?: string[] - anyOf?: GeminiObjectSchema[] - propertyOrdering?: string[] - items?: GeminiObjectSchema - minimum?: number - maximum?: number -} - -const jsonSchemaTypeToGeminiType = (jsonSchemaType: string): GeminiObjectSchemaType => { - switch (jsonSchemaType.toLowerCase()) { - case "string": - return "STRING" - case "object": - return "OBJECT" - case "number": - return "NUMBER" - case "integer": - return "INTEGER" - case "array": - return "ARRAY" - case "boolean": - return "BOOLEAN" - default: - return "TYPE_UNSPECIFIED" - } -} - -function convertJsonSchemaToGeminiSchema(jsonSchema: any): GeminiObjectSchema { - const jsonSchemaType = jsonSchema["type"] - if (!jsonSchemaType || typeof jsonSchema.type !== "string") { - throw new Error(`Invalid type property in function declaration\n${JSON.stringify(jsonSchema, null, 2)}`) - } - const geminiSchema: GeminiObjectSchema = { - type: jsonSchemaTypeToGeminiType(jsonSchemaType), - } - - // if (jsonSchema.format) geminiSchema.format = jsonSchema.format; - if (jsonSchema.title) geminiSchema.title = jsonSchema.title - if (jsonSchema.description) geminiSchema.description = jsonSchema.description - - // Handle nullable - if (jsonSchemaType === "null" || jsonSchema.nullable) { - geminiSchema.nullable = true - } - - // Handle enum values - if (Array.isArray(jsonSchema.enum)) { - geminiSchema.enum = jsonSchema.enum.map(String) - } - - // Handle array constraints - if (jsonSchemaType === "array") { - if (typeof jsonSchema.maxItems !== "undefined") { - geminiSchema.maxItems = String(jsonSchema.maxItems) - } - if (typeof jsonSchema.minItems !== "undefined") { - geminiSchema.minItems = String(jsonSchema.minItems) - } - // Handle array items - if (jsonSchema.items) { - geminiSchema.items = convertJsonSchemaToGeminiSchema(jsonSchema.items) - } - } - - // Handle numeric constraints - if (typeof jsonSchema.minimum !== "undefined") { - geminiSchema.minimum = Number(jsonSchema.minimum) - } - if (typeof jsonSchema.maximum !== "undefined") { - geminiSchema.maximum = Number(jsonSchema.maximum) - } - - // Handle properties for objects - if (jsonSchema.properties) { - geminiSchema.properties = {} - for (const [key, value] of Object.entries(jsonSchema.properties)) { - geminiSchema.properties[key] = convertJsonSchemaToGeminiSchema(value) - } - } - - // Handle required properties - if (Array.isArray(jsonSchema.required)) { - geminiSchema.required = jsonSchema.required - } - - // Handle anyOf - if (Array.isArray(jsonSchema.anyOf)) { - geminiSchema.anyOf = jsonSchema.anyOf.map(convertJsonSchemaToGeminiSchema) - } - - // TODO/UNSUPPORTED: - // format - // property ordering: - // if (Array.isArray(jsonSchema.propertyOrdering)) { - // geminiSchema.propertyOrdering = jsonSchema.propertyOrdering; - // } - - return geminiSchema -} - -// https://ai.google.dev/api/caching#FunctionDeclaration -// Note "reponse" field (schema showing function output structure) is not supported at the moment -export function convertOpenAIToolToGeminiFunction(tool: ChatCompletionTool): GeminiToolFunctionDeclaration { - // Type guard for function tools - if (tool.type !== "function" || !tool.function) { - throw new Error(`Unsupported tool type: ${tool.type}`) - } - - if (!tool.function.name) { - throw new Error("Function name required") - } - const description = tool.function.description ?? "" - const name = tool.function.name - - const fn: GeminiToolFunctionDeclaration = { - description, - name, - } - - if ( - tool.function.parameters && - "type" in tool.function.parameters && - typeof tool.function.parameters.type === "string" - ) { - // Gemini can't take an empty object - // So if empty object param is present just don't add parameters - if (tool.function.parameters.type === "object") { - if (JSON.stringify(tool.function.parameters.properties) === "{}") { - return fn - } - } - - fn.parameters = convertJsonSchemaToGeminiSchema(tool.function.parameters) - } - - return fn -} - -type GeminiTextContentPart = { - text: string -} - -type GeminiInlineDataContentPart = { - inlineData: { - mimeType: string - data: string - } -} - -type GeminiFunctionCallContentPart = { - functionCall: { - id?: string - name: string - args: unknown - } -} - -type GeminiFunctionResponseContentPart = { - functionResponse: { - id?: string - name: string - response: unknown - } -} - -type GeminiFileDataContentPart = { - fileData: { - fileUri: string - mimeType: string // See possible values here: https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference#filedata - } -} - -type GeminiExecutableCodeContentPart = { - executableCode: { - language: "PYTHON" | "LANGUAGE_UNSPECIFIED" - code: string - } -} - -type GeminiCodeExecutionResultContentPart = { - codeExecutionResult: { - outcome: "OUTCOME_UNSPECIFIED" | "OUTCOME_OK" | "OUTCOME_FAILED" | "OUTCOME_DEADLINE_EXCEEDED" - output: string - } -} - -export type GeminiChatContentPart = - | GeminiTextContentPart - | GeminiInlineDataContentPart - | GeminiFunctionCallContentPart - | GeminiFunctionResponseContentPart - | GeminiFileDataContentPart - | GeminiExecutableCodeContentPart - | GeminiCodeExecutionResultContentPart - -export interface GeminiChatContent { - role?: "user" | "model" - parts: GeminiChatContentPart[] -} - -export interface GeminiToolFunctionDeclaration { - name: string - description: string - parameters?: GeminiObjectSchema - response?: GeminiObjectSchema -} - -interface SafetyRating { - harmCategory: HarmCategory - harmProbability: HarmProbability - blocked: boolean -} - -enum HarmCategory { - HARM_CATEGORY_UNSPECIFIED = "HARM_CATEGORY_UNSPECIFIED", - HARM_CATEGORY_DEROGATORY = "HARM_CATEGORY_DEROGATORY", - HARM_CATEGORY_TOXICITY = "HARM_CATEGORY_TOXICITY", - HARM_CATEGORY_VIOLENCE = "HARM_CATEGORY_VIOLENCE", - HARM_CATEGORY_SEXUAL = "HARM_CATEGORY_SEXUAL", - HARM_CATEGORY_MEDICAL = "HARM_CATEGORY_MEDICAL", - HARM_CATEGORY_DANGEROUS = "HARM_CATEGORY_DANGEROUS", - HARM_CATEGORY_HARASSMENT = "HARM_CATEGORY_HARASSMENT", - HARM_CATEGORY_HATE_SPEECH = "HARM_CATEGORY_HATE_SPEECH", - HARM_CATEGORY_SEXUALLY_EXPLICIT = "HARM_CATEGORY_SEXUALLY_EXPLICIT", - HARM_CATEGORY_DANGEROUS_CONTENT = "HARM_CATEGORY_DANGEROUS_CONTENT", - HARM_CATEGORY_CIVIC_INTEGRITY = "HARM_CATEGORY_CIVIC_INTEGRITY", -} - -enum HarmProbability { - HARM_PROBABILITY_UNSPECIFIED = "HARM_PROBABILITY_UNSPECIFIED", - NEGLIGIBLE = "NEGLIGIBLE", - LOW = "LOW", - MEDIUM = "MEDIUM", - HIGH = "HIGH", -} - -interface Candidate { - content: GeminiChatContent - finishReason: FinishReason - safetyRatings: SafetyRating[] - tokenCount: number - groundingAttribution?: GroundingAttribution - groundingMetadata?: GroundingMetadata - avgLogprobs?: number - logprobs?: LogprobsResult - index: number -} - -enum FinishReason { - FINISH_REASON_UNSPECIFIED = "FINISH_REASON_UNSPECIFIED", - STOP = "STOP", - MAX_TOKENS = "MAX_TOKENS", - SAFETY = "SAFETY", - RECITATION = "RECITATION", - LANGUAGE = "LANGUAGE", - OTHER = "OTHER", - BLOCKLIST = "BLOCKLIST", - PROHIBITED_CONTENT = "PROHIBITED_CONTENT", - SPII = "SPII", - MALFORMED_FUNCTION_CALL = "MALFORMED_FUNCTION_CALL", -} - -interface GroundingAttribution { - attributionSourceId: AttributionSourceId - groundingSourceContent: string -} - -interface AttributionSourceId { - groundingPassage?: GroundingPassageId - semanticRetrieverChunk?: SemanticRetrieverChunk -} - -interface GroundingPassageId { - passageId: string - partIndex: number -} - -interface SemanticRetrieverChunk { - source: string - chunk: string -} - -interface GroundingMetadata { - groundingSupport?: GroundingSupport[] - webSearchQueries?: string[] - searchEntryPoint?: SearchEntryPoint - retrievalMetadata?: RetrievalMetadata -} - -interface SearchEntryPoint { - renderedContent?: string - sdkBlob?: string -} - -interface RetrievalMetadata { - googleSearchDynamicRetrievalScore?: number -} - -interface GroundingSupport { - groundingChunkIndices: number[] - confidenceScores: number[] - segment: Segment -} - -interface Segment { - partIndex: number - startIndex: number - endIndex: number - text: string -} - -interface LogprobsResult { - topCandidates: TopCandidates[] - chosenCandidates: Candidate[] -} - -interface TopCandidates { - candidates: Candidate[] -} diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openaiTypeConverters.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openaiTypeConverters.ts index 47573ad824f..4ae6c57a017 100644 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openaiTypeConverters.ts +++ b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/llm/openaiTypeConverters.ts @@ -1,4 +1,3 @@ -import { FimCreateParamsStreaming } from "./openai-adapters/apis/base" import { ChatCompletion, ChatCompletionChunk, @@ -69,7 +68,7 @@ export function toCompleteBody(prompt: string, options: CompletionOptions): Comp } } -export function toFimBody(prefix: string, suffix: string, options: CompletionOptions): FimCreateParamsStreaming { +export function toFimBody(prefix: string, suffix: string, options: CompletionOptions): Record { return { model: options.model, prompt: prefix, diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/util/treeSitter.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/util/treeSitter.ts index b192201481e..3d3ca9b6d02 100644 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/util/treeSitter.ts +++ b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/util/treeSitter.ts @@ -1,7 +1,11 @@ import fs from "node:fs" import path from "path" -import type { Language, Node as SyntaxNode, Query, Tree } from "web-tree-sitter" +import type Parser from "web-tree-sitter" +type Language = Parser.Language +type SyntaxNode = Parser.SyntaxNode +type Query = Parser.Query +type Tree = Parser.Tree import { SymbolWithRange } from ".." import { getUriFileExtension } from "./uri" diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/vscode-test-harness/src/autocomplete/lsp.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/vscode-test-harness/src/autocomplete/lsp.ts index 01f4347eb89..a037487f2d4 100644 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/vscode-test-harness/src/autocomplete/lsp.ts +++ b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/vscode-test-harness/src/autocomplete/lsp.ts @@ -7,7 +7,8 @@ import { intersection } from "../../../util/ranges" import * as vscode from "vscode" import type { DocumentSymbol, IDE, Range, RangeInFile, RangeInFileWithContents, SignatureHelp } from "../../../" -import type { Node as SyntaxNode } from "web-tree-sitter" +import type Parser from "web-tree-sitter" +type SyntaxNode = Parser.SyntaxNode const FUNCTION_BLOCK_NODE_TYPES = ["block", "statement_block"] const FUNCTION_DECLARATION_NODE_TYPEs = [ "method_definition", diff --git a/packages/kilo-vscode/src/services/autocomplete/docs/TRANSPLANT-PLAN.md b/packages/kilo-vscode/src/services/autocomplete/docs/TRANSPLANT-PLAN.md index c4c7dc5689d..b5d0c291af2 100644 --- a/packages/kilo-vscode/src/services/autocomplete/docs/TRANSPLANT-PLAN.md +++ b/packages/kilo-vscode/src/services/autocomplete/docs/TRANSPLANT-PLAN.md @@ -702,3 +702,60 @@ Use this as the practical step-by-step transplant procedure. 3. (Optional) Webview: - confirm settings update triggers reload - confirm chat textarea completion roundtrip works + +## 11. Clarifications + +These decisions were made before implementation began and override any conflicting guidance in the sections above. + +### 11.1 LLM Provider Architecture + +**Decision: Option A — Via CLI backend (Kilo Gateway).** + +FIM is postponed; only the **holefiller** (chat-completion-based) strategy will be used initially. The LLM provider will route completions through the Kilo Gateway backend. The only supported model for now is `mistralai/codestral-2508`. + +### 11.2 Provider & Model Selection + +**Decision: Hardcoded to Kilo Gateway + `mistralai/codestral-2508`.** + +No profile resolver is needed in phase 1. The provider and model are fixed. + +### 11.3 Feature Scope + +| Feature | Decision | +| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| JetBrains bridge | **Exclude permanently** — delete `AutocompleteJetbrainsBridge.ts` and its tests. This will never be implemented this way. | +| Chat textarea autocomplete | **Include** | +| Code actions | **Include** | +| Continuedev LLM adapters | **Strip to minimum** — remove all adapters not needed for the Kilo Gateway / holefiller path. The module uses `AutocompleteModel`, not the continuedev adapters directly, so most can be removed. | + +### 11.4 Settings Storage + +**Decision: VS Code settings** via `contributes.configuration` in `package.json`. + +### 11.5 Telemetry + +**Decision: Console logging only.** The extension has no telemetry system yet. Keep console logs but do not send actual telemetry events. Implement a no-op `ITelemetryClient`. + +### 11.6 i18n + +**Decision: Wire into `@kilocode/kilo-i18n`.** Use the translations from `src/services/autocomplete/i18n/`. Keys may differ from what `kilo-i18n` uses, so mapping is required. Discard translations for locales not present in `kilo-i18n`. + +### 11.7 File Ignore / Access Control + +**Decision: Dummy `RooIgnoreController`** that allows everything except `.env` files (and similar sensitive defaults). Include a `TODO` comment for proper implementation later. + +### 11.8 Command Prefix + +**Decision: Use `kilo-code.new.autocomplete.*`** to be consistent with the existing extension naming convention. + +### 11.9 NPM Dependencies + +**Decision: Strip unused continuedev code first**, then install only what is actually needed. + +### 11.10 Tree-Sitter WASM Bundling + +**Decision: Bundle in `dist/`** via esbuild copy plugin. + +### 11.11 Singleton vs Dependency Injection + +**Decision: Deferred** — will be determined during implementation based on what works best with the existing `KiloConnectionService` / `KiloProvider` architecture. diff --git a/packages/kilo-vscode/src/services/autocomplete/index.ts b/packages/kilo-vscode/src/services/autocomplete/index.ts index 230de465992..e4b17832377 100644 --- a/packages/kilo-vscode/src/services/autocomplete/index.ts +++ b/packages/kilo-vscode/src/services/autocomplete/index.ts @@ -1,39 +1,38 @@ // kilocode_change - new file import * as vscode from "vscode" import { AutocompleteServiceManager } from "./AutocompleteServiceManager" -import { ClineProvider } from "../../core/webview/ClineProvider" -import { registerAutocompleteJetbrainsBridge } from "./AutocompleteJetbrainsBridge" +import type { KiloConnectionService } from "../cli-backend" -export const registerAutocompleteProvider = (context: vscode.ExtensionContext, cline: ClineProvider) => { - const autocompleteManager = new AutocompleteServiceManager(context, cline) +export const registerAutocompleteProvider = ( + context: vscode.ExtensionContext, + connectionService: KiloConnectionService, +) => { + const autocompleteManager = new AutocompleteServiceManager(context, connectionService) context.subscriptions.push(autocompleteManager) - // Register JetBrains Bridge if applicable - registerAutocompleteJetbrainsBridge(context, cline, autocompleteManager) - // Register AutocompleteServiceManager Commands context.subscriptions.push( - vscode.commands.registerCommand("kilo-code.autocomplete.reload", async () => { + vscode.commands.registerCommand("kilo-code.new.autocomplete.reload", async () => { await autocompleteManager.load() }), ) context.subscriptions.push( - vscode.commands.registerCommand("kilo-code.autocomplete.codeActionQuickFix", async () => { + vscode.commands.registerCommand("kilo-code.new.autocomplete.codeActionQuickFix", async () => { return }), ) context.subscriptions.push( - vscode.commands.registerCommand("kilo-code.autocomplete.generateSuggestions", async () => { + vscode.commands.registerCommand("kilo-code.new.autocomplete.generateSuggestions", async () => { autocompleteManager.codeSuggestion() }), ) context.subscriptions.push( - vscode.commands.registerCommand("kilo-code.autocomplete.showIncompatibilityExtensionPopup", async () => { + vscode.commands.registerCommand("kilo-code.new.autocomplete.showIncompatibilityExtensionPopup", async () => { await autocompleteManager.showIncompatibilityExtensionPopup() }), ) context.subscriptions.push( - vscode.commands.registerCommand("kilo-code.autocomplete.disable", async () => { + vscode.commands.registerCommand("kilo-code.new.autocomplete.disable", async () => { await autocompleteManager.disable() }), ) @@ -44,4 +43,13 @@ export const registerAutocompleteProvider = (context: vscode.ExtensionContext, c providedCodeActionKinds: Object.values(autocompleteManager.codeActionProvider.providedCodeActionKinds), }), ) + + // Re-load when autocomplete settings change (e.g. toggled from webview or VS Code settings UI) + context.subscriptions.push( + vscode.workspace.onDidChangeConfiguration((e) => { + if (e.affectsConfiguration("kilo-code.new.autocomplete")) { + void autocompleteManager.load() + } + }), + ) } diff --git a/packages/kilo-vscode/src/services/autocomplete/shims/FileIgnoreController.ts b/packages/kilo-vscode/src/services/autocomplete/shims/FileIgnoreController.ts new file mode 100644 index 00000000000..46f7d540759 --- /dev/null +++ b/packages/kilo-vscode/src/services/autocomplete/shims/FileIgnoreController.ts @@ -0,0 +1,51 @@ +/** + * Dummy file ignore controller for autocomplete context filtering. + * + * TODO: Implement proper file ignore logic based on .gitignore, .kilocodeignore, + * or CLI backend access control APIs. For now, this allows everything except + * known sensitive file patterns. + */ + +const SENSITIVE_PATTERNS = [".env", ".env.local", ".env.production", ".env.staging", ".env.development", ".env.*"] + +function isSensitiveFile(filePath: string): boolean { + const basename = filePath.split("/").pop() ?? "" + return SENSITIVE_PATTERNS.some((pattern) => { + if (pattern.includes("*")) { + const prefix = pattern.split("*")[0] + return basename.startsWith(prefix) + } + return basename === pattern + }) +} + +export class FileIgnoreController { + async initialize(): Promise { + // No-op for now + } + + /** + * Returns true if the file can be read/used as autocomplete context. + */ + validateAccess(filePath: string): boolean { + return !isSensitiveFile(filePath) + } + + /** + * Filter a list of candidate paths to those allowed. + */ + filterPaths(paths: string[]): string[] { + return paths.filter((p) => this.validateAccess(p)) + } + + /** + * Returns user-facing instructions explaining why access is restricted. + */ + getInstructions(): string | undefined { + return undefined + } + + dispose(): void { + // No-op + } +} diff --git a/packages/kilo-vscode/src/services/autocomplete/shims/TelemetryStub.ts b/packages/kilo-vscode/src/services/autocomplete/shims/TelemetryStub.ts new file mode 100644 index 00000000000..99fe2fea69d --- /dev/null +++ b/packages/kilo-vscode/src/services/autocomplete/shims/TelemetryStub.ts @@ -0,0 +1,18 @@ +/** + * No-op telemetry client for autocomplete. + * + * The extension has no telemetry system yet. This stub logs events + * to the console for debugging but does not send them anywhere. + */ + +export type TelemetryEventName = string + +export interface ITelemetryClient { + captureEvent(event: TelemetryEventName, properties?: Record): void +} + +export class TelemetryStub implements ITelemetryClient { + captureEvent(event: TelemetryEventName, properties?: Record): void { + console.log("[Kilo New] [Telemetry]", event, properties ?? "") + } +} diff --git a/packages/kilo-vscode/src/services/autocomplete/shims/i18n.ts b/packages/kilo-vscode/src/services/autocomplete/shims/i18n.ts new file mode 100644 index 00000000000..b02cb3f6f09 --- /dev/null +++ b/packages/kilo-vscode/src/services/autocomplete/shims/i18n.ts @@ -0,0 +1,18 @@ +// kilocode_change - new file +// i18n bridge for autocomplete module +// Provides a t() function using the autocomplete English dictionary as fallback. +// Can be wired to locale detection later. + +import { dict as enDict } from "../i18n/en" + +const translations: Record = { ...enDict } + +export function t(key: string, vars?: Record): string { + let text = translations[key] ?? key + if (vars) { + for (const [k, v] of Object.entries(vars)) { + text = text.replaceAll(`{{${k}}}`, String(v)) + } + } + return text +} diff --git a/packages/kilo-vscode/src/services/autocomplete/types.ts b/packages/kilo-vscode/src/services/autocomplete/types.ts index cd7725349fd..d32531f55f7 100644 --- a/packages/kilo-vscode/src/services/autocomplete/types.ts +++ b/packages/kilo-vscode/src/services/autocomplete/types.ts @@ -6,7 +6,7 @@ import type { RangeInFile, TabAutocompleteOptions as CoreTabAutocompleteOptions, } from "./continuedev/core" -import { RooIgnoreController } from "../../core/ignore/RooIgnoreController" +import { FileIgnoreController } from "./shims/FileIgnoreController" import { ContextRetrievalService } from "./continuedev/core/autocomplete/context/ContextRetrievalService" import { VsCodeIde } from "./continuedev/core/vscode-test-harness/src/VSCodeIde" import { AutocompleteModel } from "./AutocompleteModel" @@ -288,5 +288,5 @@ export interface AutocompleteContextProvider { contextService: ContextRetrievalService ide: VsCodeIde model: AutocompleteModel - ignoreController?: Promise + ignoreController?: Promise } diff --git a/packages/kilo-vscode/src/services/autocomplete/utils/kilocode-utils.test.ts b/packages/kilo-vscode/src/services/autocomplete/utils/kilocode-utils.test.ts deleted file mode 100644 index 20051355e46..00000000000 --- a/packages/kilo-vscode/src/services/autocomplete/utils/kilocode-utils.test.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { checkKilocodeBalance } from "./kilocode-utils" - -describe("checkKilocodeBalance", () => { - const mockToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbnYiOiJwcm9kdWN0aW9uIn0.test" - const mockOrgId = "org-123" - - beforeEach(() => { - global.fetch = vi.fn() - }) - - afterEach(() => { - vi.restoreAllMocks() - }) - - it("should return true when balance is positive", async () => { - vi.mocked(global.fetch).mockResolvedValueOnce({ - ok: true, - json: async () => ({ balance: 100 }), - } as Response) - - const result = await checkKilocodeBalance(mockToken) - expect(result).toBe(true) - expect(global.fetch).toHaveBeenCalledWith( - "https://api.kilo.ai/api/profile/balance", - expect.objectContaining({ - headers: expect.objectContaining({ - Authorization: `Bearer ${mockToken}`, - }), - }), - ) - }) - - it("should return false when balance is zero", async () => { - vi.mocked(global.fetch).mockResolvedValueOnce({ - ok: true, - json: async () => ({ balance: 0 }), - } as Response) - - const result = await checkKilocodeBalance(mockToken) - expect(result).toBe(false) - }) - - it("should return false when balance is negative", async () => { - vi.mocked(global.fetch).mockResolvedValueOnce({ - ok: true, - json: async () => ({ balance: -10 }), - } as Response) - - const result = await checkKilocodeBalance(mockToken) - expect(result).toBe(false) - }) - - it("should include organization ID in headers when provided", async () => { - vi.mocked(global.fetch).mockResolvedValueOnce({ - ok: true, - json: async () => ({ balance: 100 }), - } as Response) - - const result = await checkKilocodeBalance(mockToken, mockOrgId) - expect(result).toBe(true) - expect(global.fetch).toHaveBeenCalledWith( - "https://api.kilo.ai/api/profile/balance", - expect.objectContaining({ - headers: expect.objectContaining({ - Authorization: `Bearer ${mockToken}`, - "X-KiloCode-OrganizationId": mockOrgId, - }), - }), - ) - }) - - it("should not include organization ID in headers when not provided", async () => { - vi.mocked(global.fetch).mockResolvedValueOnce({ - ok: true, - json: async () => ({ balance: 100 }), - } as Response) - - await checkKilocodeBalance(mockToken) - - const fetchCall = vi.mocked(global.fetch).mock.calls[0] - expect(fetchCall).toBeDefined() - const headers = (fetchCall![1] as RequestInit)?.headers as Record - - expect(headers).toHaveProperty("Authorization") - expect(headers).not.toHaveProperty("X-KiloCode-OrganizationId") - }) - - it("should return false when API request fails", async () => { - vi.mocked(global.fetch).mockResolvedValueOnce({ - ok: false, - } as Response) - - const result = await checkKilocodeBalance(mockToken) - expect(result).toBe(false) - }) - - it("should return false when fetch throws an error", async () => { - vi.mocked(global.fetch).mockRejectedValueOnce(new Error("Network error")) - - const result = await checkKilocodeBalance(mockToken) - expect(result).toBe(false) - }) - - it("should handle missing balance field in response", async () => { - vi.mocked(global.fetch).mockResolvedValueOnce({ - ok: true, - json: async () => ({}), - } as Response) - - const result = await checkKilocodeBalance(mockToken) - expect(result).toBe(false) - }) -}) diff --git a/packages/kilo-vscode/src/services/autocomplete/utils/kilocode-utils.ts b/packages/kilo-vscode/src/services/autocomplete/utils/kilocode-utils.ts deleted file mode 100644 index 6769adc6f29..00000000000 --- a/packages/kilo-vscode/src/services/autocomplete/utils/kilocode-utils.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { getKiloBaseUriFromToken, AUTOCOMPLETE_PROVIDER_MODELS, AutocompleteProviderKey } from "@roo-code/types" - -export { AUTOCOMPLETE_PROVIDER_MODELS } -export type { AutocompleteProviderKey } - -/** - * Check if the Kilocode account has a positive balance - * @param kilocodeToken - The Kilocode JWT token - * @param kilocodeOrganizationId - Optional organization ID to include in headers - * @returns Promise - True if balance > 0, false otherwise - */ -export async function checkKilocodeBalance(kilocodeToken: string, kilocodeOrganizationId?: string): Promise { - try { - const baseUrl = getKiloBaseUriFromToken(kilocodeToken) - - const headers: Record = { - Authorization: `Bearer ${kilocodeToken}`, - } - - if (kilocodeOrganizationId) { - headers["X-KiloCode-OrganizationId"] = kilocodeOrganizationId - } - - const response = await fetch(`${baseUrl}/api/profile/balance`, { - headers, - }) - - if (!response.ok) { - return false - } - - const data = await response.json() - const balance = data.balance ?? 0 - return balance > 0 - } catch (error) { - console.error("Error checking kilocode balance:", error) - return false - } -} diff --git a/packages/kilo-vscode/src/services/cli-backend/http-client.ts b/packages/kilo-vscode/src/services/cli-backend/http-client.ts index 51992e90c39..2deba34f723 100644 --- a/packages/kilo-vscode/src/services/cli-backend/http-client.ts +++ b/packages/kilo-vscode/src/services/cli-backend/http-client.ts @@ -313,6 +313,112 @@ export class HttpClient { } } + // ============================================ + // FIM Completion Methods + // ============================================ + + /** + * Stream a FIM (Fill-in-the-Middle) completion from the Kilo Gateway via the CLI backend. + * The CLI backend handles auth — no API key needed in the extension. + * + * @param prefix - Code before the cursor + * @param suffix - Code after the cursor + * @param onChunk - Callback for each text chunk + * @param options - Optional model, maxTokens, temperature + * @returns Usage metadata (cost, tokens) + */ + async fimCompletion( + prefix: string, + suffix: string, + onChunk: (text: string) => void, + options?: { model?: string; maxTokens?: number; temperature?: number }, + ): Promise<{ cost: number; inputTokens: number; outputTokens: number }> { + const url = `${this.baseUrl}/kilo/fim` + + const response = await fetch(url, { + method: "POST", + headers: { + Authorization: this.authHeader, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + prefix, + suffix, + model: options?.model, + maxTokens: options?.maxTokens, + temperature: options?.temperature, + }), + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`FIM request failed: ${response.status} ${errorText}`) + } + + if (!response.body) { + throw new Error("FIM response has no body") + } + + let cost = 0 + let inputTokens = 0 + let outputTokens = 0 + + // Parse SSE stream + const reader = response.body.getReader() + const decoder = new TextDecoder() + let buffer = "" + + for (;;) { + const { done, value } = await reader.read() + if (done) { + break + } + + buffer += decoder.decode(value, { stream: true }) + + // Process complete SSE lines + const lines = buffer.split("\n") + buffer = lines.pop() ?? "" // Keep incomplete line in buffer + + for (const line of lines) { + if (!line.startsWith("data: ")) { + continue + } + + const data = line.slice(6).trim() + if (data === "[DONE]") { + continue + } + + try { + const parsed = JSON.parse(data) as { + choices?: Array<{ delta?: { content?: string } }> + usage?: { prompt_tokens?: number; completion_tokens?: number } + cost?: number + } + + const content = parsed.choices?.[0]?.delta?.content + if (content) { + onChunk(content) + } + + if (parsed.usage) { + inputTokens = parsed.usage.prompt_tokens ?? 0 + outputTokens = parsed.usage.completion_tokens ?? 0 + } + + if (parsed.cost !== undefined) { + cost = parsed.cost + } + } catch { + // Skip malformed JSON lines + } + } + } + + return { cost, inputTokens, outputTokens } + } + // ============================================ // Auth Methods // ============================================ diff --git a/packages/kilo-vscode/tsconfig.json b/packages/kilo-vscode/tsconfig.json index 1d39abe9f67..4cad76597f6 100644 --- a/packages/kilo-vscode/tsconfig.json +++ b/packages/kilo-vscode/tsconfig.json @@ -13,5 +13,13 @@ // "noUnusedParameters": true, /* Report errors on unused parameters. */ }, "include": ["src/**/*"], - "exclude": ["webview-ui"] + "exclude": [ + "webview-ui", + "src/**/__tests__/**", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/test/**", + "src/**/vitest.setup.ts", + "src/**/testUtils.ts" + ] } diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx index c700f93ef1b..7b1e4f9dcbe 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx @@ -1,29 +1,100 @@ /** * PromptInput component - * Text input with send/abort buttons for the chat interface + * Text input with send/abort buttons and ghost-text autocomplete for the chat interface */ -import { Component, createSignal, Show } from "solid-js" +import { Component, createSignal, onCleanup, Show } from "solid-js" import { Button } from "@kilocode/kilo-ui/button" import { Tooltip } from "@kilocode/kilo-ui/tooltip" import { useSession } from "../../context/session" import { useServer } from "../../context/server" import { useLanguage } from "../../context/language" +import { useVSCode } from "../../context/vscode" import { ModelSelector } from "./ModelSelector" import { ModeSwitcher } from "./ModeSwitcher" +const AUTOCOMPLETE_DEBOUNCE_MS = 500 +const MIN_TEXT_LENGTH = 3 + export const PromptInput: Component = () => { const session = useSession() const server = useServer() const language = useLanguage() + const vscode = useVSCode() const [text, setText] = createSignal("") + const [ghostText, setGhostText] = createSignal("") let textareaRef: HTMLTextAreaElement | undefined + let debounceTimer: ReturnType | undefined + let requestCounter = 0 const isBusy = () => session.status() === "busy" const isDisabled = () => !server.isConnected() const canSend = () => text().trim().length > 0 && !isBusy() && !isDisabled() + // Listen for chat completion results from the extension + const unsubscribe = vscode.onMessage((message) => { + if (message.type === "chatCompletionResult") { + const result = message as { type: "chatCompletionResult"; text: string; requestId: string } + // Only apply if the requestId matches the latest request + const expectedId = `chat-ac-${requestCounter}` + if (result.requestId === expectedId && result.text) { + setGhostText(result.text) + } + } + }) + + onCleanup(() => { + unsubscribe() + if (debounceTimer) { + clearTimeout(debounceTimer) + } + }) + + // Request autocomplete from the extension + const requestAutocomplete = (currentText: string) => { + if (currentText.length < MIN_TEXT_LENGTH || isDisabled()) { + setGhostText("") + return + } + + requestCounter++ + const requestId = `chat-ac-${requestCounter}` + + vscode.postMessage({ + type: "requestChatCompletion", + text: currentText, + requestId, + }) + } + + // Accept the ghost text suggestion + const acceptSuggestion = () => { + const suggestion = ghostText() + if (!suggestion) return + + const newText = text() + suggestion + setText(newText) + setGhostText("") + + // Notify extension of acceptance for telemetry + vscode.postMessage({ + type: "chatCompletionAccepted", + suggestionLength: suggestion.length, + }) + + // Update textarea + if (textareaRef) { + textareaRef.value = newText + adjustHeight() + } + } + + // Dismiss the ghost text + const dismissSuggestion = () => { + setGhostText("") + } + // Auto-resize textarea const adjustHeight = () => { if (!textareaRef) return @@ -35,12 +106,38 @@ export const PromptInput: Component = () => { const target = e.target as HTMLTextAreaElement setText(target.value) adjustHeight() + + // Clear existing ghost text on new input + setGhostText("") + + // Debounce autocomplete request + if (debounceTimer) { + clearTimeout(debounceTimer) + } + debounceTimer = setTimeout(() => { + requestAutocomplete(target.value) + }, AUTOCOMPLETE_DEBOUNCE_MS) } const handleKeyDown = (e: KeyboardEvent) => { + // Tab or ArrowRight to accept ghost text + if ((e.key === "Tab" || e.key === "ArrowRight") && ghostText()) { + e.preventDefault() + acceptSuggestion() + return + } + + // Escape to dismiss ghost text + if (e.key === "Escape" && ghostText()) { + e.preventDefault() + dismissSuggestion() + return + } + // Enter to send (without shift) if (e.key === "Enter" && !e.shiftKey) { e.preventDefault() + dismissSuggestion() handleSend() } } @@ -52,6 +149,7 @@ export const PromptInput: Component = () => { const sel = session.selected() session.sendMessage(message, sel?.providerID, sel?.modelID) setText("") + setGhostText("") // Reset textarea height if (textareaRef) { @@ -66,18 +164,26 @@ export const PromptInput: Component = () => { return (
-