feat: add new free provider opencode
Browse files- bun.lock +31 -0
- src/commands/login/login.tsx +19 -1
- src/components/OpenCodeLoginFlow.tsx +243 -0
- src/services/api/client.ts +22 -4
- src/services/api/opencodeClient.ts +190 -0
- src/utils/auth.ts +22 -0
- src/utils/config.ts +5 -1
- src/utils/model/model.ts +30 -0
- src/utils/model/modelOptions.ts +30 -0
- src/utils/model/modelStrings.ts +8 -0
- src/utils/model/providers.ts +30 -7
bun.lock
CHANGED
|
@@ -42,6 +42,7 @@
|
|
| 42 |
"@opentelemetry/semantic-conventions": "^1.40.0",
|
| 43 |
"@smithy/core": "^3.23.13",
|
| 44 |
"@smithy/node-http-handler": "^4.5.1",
|
|
|
|
| 45 |
"@yukiakai/tls-fetch": "^2.0.2",
|
| 46 |
"ajv": "^8.18.0",
|
| 47 |
"asciichart": "^1.5.25",
|
|
@@ -484,6 +485,8 @@
|
|
| 484 |
|
| 485 |
"@smithy/uuid": ["@smithy/uuid@1.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g=="],
|
| 486 |
|
|
|
|
|
|
|
| 487 |
"@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
|
| 488 |
|
| 489 |
"@types/lodash": ["@types/lodash@4.17.24", "", {}, "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ=="],
|
|
@@ -522,8 +525,12 @@
|
|
| 522 |
|
| 523 |
"asciichart": ["asciichart@1.5.25", "", {}, "sha512-PNxzXIPPOtWq8T7bgzBtk9cI2lgS4SJZthUHEiQ1aoIc3lNzGfUvIvo9LiAnq26TACo9t1/4qP6KTGAUbzX9Xg=="],
|
| 524 |
|
|
|
|
|
|
|
| 525 |
"auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="],
|
| 526 |
|
|
|
|
|
|
|
| 527 |
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
| 528 |
|
| 529 |
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
|
@@ -578,6 +585,8 @@
|
|
| 578 |
|
| 579 |
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
| 580 |
|
|
|
|
|
|
|
| 581 |
"commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="],
|
| 582 |
|
| 583 |
"content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
|
|
@@ -608,6 +617,8 @@
|
|
| 608 |
|
| 609 |
"define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="],
|
| 610 |
|
|
|
|
|
|
|
| 611 |
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
| 612 |
|
| 613 |
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
|
@@ -638,6 +649,8 @@
|
|
| 638 |
|
| 639 |
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
| 640 |
|
|
|
|
|
|
|
| 641 |
"es-toolkit": ["es-toolkit@1.45.1", "", {}, "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw=="],
|
| 642 |
|
| 643 |
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
|
@@ -682,6 +695,10 @@
|
|
| 682 |
|
| 683 |
"flora-colossus": ["flora-colossus@2.0.0", "", { "dependencies": { "debug": "^4.3.4", "fs-extra": "^10.1.0" } }, "sha512-dz4HxH6pOvbUzZpZ/yXhafjbR2I8cenK5xL0KtBFb7U2ADsR+OwXifnxZjij/pZWF775uSCMzWVd+jDik2H2IA=="],
|
| 684 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 685 |
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
|
| 686 |
|
| 687 |
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
|
@@ -728,6 +745,8 @@
|
|
| 728 |
|
| 729 |
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
| 730 |
|
|
|
|
|
|
|
| 731 |
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
| 732 |
|
| 733 |
"highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="],
|
|
@@ -780,6 +799,8 @@
|
|
| 780 |
|
| 781 |
"jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="],
|
| 782 |
|
|
|
|
|
|
|
| 783 |
"json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="],
|
| 784 |
|
| 785 |
"json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="],
|
|
@@ -924,6 +945,8 @@
|
|
| 924 |
|
| 925 |
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
| 926 |
|
|
|
|
|
|
|
| 927 |
"qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="],
|
| 928 |
|
| 929 |
"qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="],
|
|
@@ -1384,6 +1407,8 @@
|
|
| 1384 |
|
| 1385 |
"@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.2", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q=="],
|
| 1386 |
|
|
|
|
|
|
|
| 1387 |
"@typespec/ts-http-runtime/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
| 1388 |
|
| 1389 |
"cli-highlight/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
|
@@ -1398,6 +1423,8 @@
|
|
| 1398 |
|
| 1399 |
"external-editor/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
|
| 1400 |
|
|
|
|
|
|
|
| 1401 |
"gaxios/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
| 1402 |
|
| 1403 |
"gtoken/gaxios": ["gaxios@6.7.1", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "is-stream": "^2.0.0", "node-fetch": "^2.6.9", "uuid": "^9.0.1" } }, "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ=="],
|
|
@@ -1532,6 +1559,8 @@
|
|
| 1532 |
|
| 1533 |
"@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow=="],
|
| 1534 |
|
|
|
|
|
|
|
| 1535 |
"@typespec/ts-http-runtime/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
| 1536 |
|
| 1537 |
"cli-highlight/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
|
@@ -1546,6 +1575,8 @@
|
|
| 1546 |
|
| 1547 |
"cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
| 1548 |
|
|
|
|
|
|
|
| 1549 |
"gaxios/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
| 1550 |
|
| 1551 |
"gtoken/gaxios/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
|
|
|
| 42 |
"@opentelemetry/semantic-conventions": "^1.40.0",
|
| 43 |
"@smithy/core": "^3.23.13",
|
| 44 |
"@smithy/node-http-handler": "^4.5.1",
|
| 45 |
+
"@tavily/core": "^0.6.1",
|
| 46 |
"@yukiakai/tls-fetch": "^2.0.2",
|
| 47 |
"ajv": "^8.18.0",
|
| 48 |
"asciichart": "^1.5.25",
|
|
|
|
| 485 |
|
| 486 |
"@smithy/uuid": ["@smithy/uuid@1.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g=="],
|
| 487 |
|
| 488 |
+
"@tavily/core": ["@tavily/core@0.6.4", "", { "dependencies": { "axios": "^1.7.7", "https-proxy-agent": "^7.0.6", "js-tiktoken": "^1.0.14" } }, "sha512-PppC0p2SwkoImLiYFT/uqDyWKPivpVsIM16HUf1Apmtbqg1YhI7Yg5Hq6eYSojC6COVCGXE4CotBnWqUmrai+A=="],
|
| 489 |
+
|
| 490 |
"@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
|
| 491 |
|
| 492 |
"@types/lodash": ["@types/lodash@4.17.24", "", {}, "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ=="],
|
|
|
|
| 525 |
|
| 526 |
"asciichart": ["asciichart@1.5.25", "", {}, "sha512-PNxzXIPPOtWq8T7bgzBtk9cI2lgS4SJZthUHEiQ1aoIc3lNzGfUvIvo9LiAnq26TACo9t1/4qP6KTGAUbzX9Xg=="],
|
| 527 |
|
| 528 |
+
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
| 529 |
+
|
| 530 |
"auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="],
|
| 531 |
|
| 532 |
+
"axios": ["axios@1.16.0", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } }, "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w=="],
|
| 533 |
+
|
| 534 |
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
| 535 |
|
| 536 |
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
|
|
|
| 585 |
|
| 586 |
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
| 587 |
|
| 588 |
+
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
| 589 |
+
|
| 590 |
"commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="],
|
| 591 |
|
| 592 |
"content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
|
|
|
|
| 617 |
|
| 618 |
"define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="],
|
| 619 |
|
| 620 |
+
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
| 621 |
+
|
| 622 |
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
| 623 |
|
| 624 |
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
|
|
|
| 649 |
|
| 650 |
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
| 651 |
|
| 652 |
+
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
|
| 653 |
+
|
| 654 |
"es-toolkit": ["es-toolkit@1.45.1", "", {}, "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw=="],
|
| 655 |
|
| 656 |
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
|
|
|
| 695 |
|
| 696 |
"flora-colossus": ["flora-colossus@2.0.0", "", { "dependencies": { "debug": "^4.3.4", "fs-extra": "^10.1.0" } }, "sha512-dz4HxH6pOvbUzZpZ/yXhafjbR2I8cenK5xL0KtBFb7U2ADsR+OwXifnxZjij/pZWF775uSCMzWVd+jDik2H2IA=="],
|
| 697 |
|
| 698 |
+
"follow-redirects": ["follow-redirects@1.16.0", "", {}, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="],
|
| 699 |
+
|
| 700 |
+
"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=="],
|
| 701 |
+
|
| 702 |
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
|
| 703 |
|
| 704 |
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
|
|
|
| 745 |
|
| 746 |
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
| 747 |
|
| 748 |
+
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
| 749 |
+
|
| 750 |
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
| 751 |
|
| 752 |
"highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="],
|
|
|
|
| 799 |
|
| 800 |
"jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="],
|
| 801 |
|
| 802 |
+
"js-tiktoken": ["js-tiktoken@1.0.21", "", { "dependencies": { "base64-js": "^1.5.1" } }, "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g=="],
|
| 803 |
+
|
| 804 |
"json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="],
|
| 805 |
|
| 806 |
"json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="],
|
|
|
|
| 945 |
|
| 946 |
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
| 947 |
|
| 948 |
+
"proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="],
|
| 949 |
+
|
| 950 |
"qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="],
|
| 951 |
|
| 952 |
"qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="],
|
|
|
|
| 1407 |
|
| 1408 |
"@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.2", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q=="],
|
| 1409 |
|
| 1410 |
+
"@tavily/core/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
| 1411 |
+
|
| 1412 |
"@typespec/ts-http-runtime/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
| 1413 |
|
| 1414 |
"cli-highlight/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
|
|
|
| 1423 |
|
| 1424 |
"external-editor/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
|
| 1425 |
|
| 1426 |
+
"form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
| 1427 |
+
|
| 1428 |
"gaxios/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
| 1429 |
|
| 1430 |
"gtoken/gaxios": ["gaxios@6.7.1", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "is-stream": "^2.0.0", "node-fetch": "^2.6.9", "uuid": "^9.0.1" } }, "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ=="],
|
|
|
|
| 1559 |
|
| 1560 |
"@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow=="],
|
| 1561 |
|
| 1562 |
+
"@tavily/core/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
| 1563 |
+
|
| 1564 |
"@typespec/ts-http-runtime/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
| 1565 |
|
| 1566 |
"cli-highlight/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
|
|
|
| 1575 |
|
| 1576 |
"cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
| 1577 |
|
| 1578 |
+
"form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
| 1579 |
+
|
| 1580 |
"gaxios/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
| 1581 |
|
| 1582 |
"gtoken/gaxios/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
src/commands/login/login.tsx
CHANGED
|
@@ -24,6 +24,7 @@ import { Dialog } from '../../components/design-system/Dialog.js';
|
|
| 24 |
import { OpenAILoginFlow } from '../../components/OpenAILoginFlow.js'
|
| 25 |
import { OpenRouterLoginFlow } from '../../components/OpenRouterLoginFlow.js'
|
| 26 |
import { LocalLoginFlow } from '../../components/LocalLoginFlow.js'
|
|
|
|
| 27 |
|
| 28 |
import { useMainLoopModel } from '../../hooks/useMainLoopModel.js';
|
| 29 |
|
|
@@ -47,7 +48,7 @@ import {
|
|
| 47 |
import { resetUserCache } from '../../utils/user.js';
|
| 48 |
|
| 49 |
// TODO
|
| 50 |
-
type AuthProviderChoice = 'anthropic' | 'openai' | 'openrouter' | 'local'
|
| 51 |
|
| 52 |
/* 第一层: 入口函数 call()
|
| 53 |
* CLI 执行 /login 真正被调用的函数
|
|
@@ -174,6 +175,18 @@ export function Login(props: {
|
|
| 174 |
),
|
| 175 |
value: 'openrouter',
|
| 176 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
{
|
| 178 |
label: (
|
| 179 |
<Text>
|
|
@@ -224,6 +237,11 @@ export function Login(props: {
|
|
| 224 |
onDone={onFlowDone}
|
| 225 |
startingMessage="Better-Clawd can use OpenRouter with your OpenRouter API key."
|
| 226 |
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
) : selectedProvider === 'local' ? (
|
| 228 |
<LocalLoginFlow
|
| 229 |
onDone={onFlowDone}
|
|
|
|
| 24 |
import { OpenAILoginFlow } from '../../components/OpenAILoginFlow.js'
|
| 25 |
import { OpenRouterLoginFlow } from '../../components/OpenRouterLoginFlow.js'
|
| 26 |
import { LocalLoginFlow } from '../../components/LocalLoginFlow.js'
|
| 27 |
+
import { OpenCodeLoginFlow } from '../../components/OpenCodeLoginFlow.js'
|
| 28 |
|
| 29 |
import { useMainLoopModel } from '../../hooks/useMainLoopModel.js';
|
| 30 |
|
|
|
|
| 48 |
import { resetUserCache } from '../../utils/user.js';
|
| 49 |
|
| 50 |
// TODO
|
| 51 |
+
type AuthProviderChoice = 'anthropic' | 'openai' | 'openrouter' | 'local' | 'opencode'
|
| 52 |
|
| 53 |
/* 第一层: 入口函数 call()
|
| 54 |
* CLI 执行 /login 真正被调用的函数
|
|
|
|
| 175 |
),
|
| 176 |
value: 'openrouter',
|
| 177 |
},
|
| 178 |
+
{
|
| 179 |
+
label: (
|
| 180 |
+
<Text>
|
| 181 |
+
OpenCode Zen{' '}
|
| 182 |
+
<Text dimColor={true}>
|
| 183 |
+
Free models (Big Pickle, GPT 5 Nano) or Zen API key
|
| 184 |
+
</Text>
|
| 185 |
+
{'\n'}
|
| 186 |
+
</Text>
|
| 187 |
+
),
|
| 188 |
+
value: 'opencode',
|
| 189 |
+
},
|
| 190 |
{
|
| 191 |
label: (
|
| 192 |
<Text>
|
|
|
|
| 237 |
onDone={onFlowDone}
|
| 238 |
startingMessage="Better-Clawd can use OpenRouter with your OpenRouter API key."
|
| 239 |
/>
|
| 240 |
+
) : selectedProvider === 'opencode' ? (
|
| 241 |
+
<OpenCodeLoginFlow
|
| 242 |
+
onDone={onFlowDone}
|
| 243 |
+
startingMessage="Better-Clawd can use OpenCode Zen free models or with a Zen API key."
|
| 244 |
+
/>
|
| 245 |
) : selectedProvider === 'local' ? (
|
| 246 |
<LocalLoginFlow
|
| 247 |
onDone={onFlowDone}
|
src/components/OpenCodeLoginFlow.tsx
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from 'react'
|
| 2 |
+
import { useState, useEffect } from 'react'
|
| 3 |
+
import { saveOpenCodeApiKey, getOpenCodeApiKey, getOpenCodeModelName } from '../utils/auth.js'
|
| 4 |
+
import { Box, Text } from '../ink.js'
|
| 5 |
+
import TextInput from './TextInput.js'
|
| 6 |
+
import { Select } from './CustomSelect/select.js'
|
| 7 |
+
|
| 8 |
+
type OpenCodeLoginFlowProps = {
|
| 9 |
+
onDone: () => void
|
| 10 |
+
startingMessage?: string
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
type LoginMode = 'menu' | 'free_models' | 'api_key'
|
| 14 |
+
|
| 15 |
+
const FREE_MODELS = [
|
| 16 |
+
{ label: 'Big Pickle (推荐)', value: 'big-pickle', description: '旗舰模型,限时免费,适合复杂任务' },
|
| 17 |
+
{ label: 'GPT 5 Nano', value: 'gpt-5-nano', description: '永久免费,轻量快速,隐私安全' },
|
| 18 |
+
{ label: 'MiniMax M2.5 Free', value: 'minimax-m2.5-free', description: '限时免费,编码推理强' },
|
| 19 |
+
{ label: 'GLM 4.7 Free', value: 'glm-4.7-free', description: '限时免费,智谱开源模型' },
|
| 20 |
+
{ label: 'Kimi K2.5 Free', value: 'kimi-k2.5-free', description: '限时免费,月之暗面模型' },
|
| 21 |
+
{ label: 'Nemotron 3 Super Free', value: 'nemotron-3-super-free', description: '限时免费,NVIDIA 模型,100万上下文' },
|
| 22 |
+
]
|
| 23 |
+
|
| 24 |
+
export function OpenCodeLoginFlow({
|
| 25 |
+
onDone,
|
| 26 |
+
startingMessage,
|
| 27 |
+
}: OpenCodeLoginFlowProps): React.ReactNode {
|
| 28 |
+
const [mode, setMode] = useState<LoginMode>('menu')
|
| 29 |
+
const [isBusy, setIsBusy] = useState(false)
|
| 30 |
+
const [status, setStatus] = useState<string | null>(null)
|
| 31 |
+
const [inputValue, setInputValue] = useState('')
|
| 32 |
+
const [cursorOffset, setCursorOffset] = useState(0)
|
| 33 |
+
const [selectedModel, setSelectedModel] = useState('big-pickle')
|
| 34 |
+
const [existingApiKey, setExistingApiKey] = useState<string | null>(null)
|
| 35 |
+
const [existingModelName, setExistingModelName] = useState<string | null>(null)
|
| 36 |
+
|
| 37 |
+
useEffect(() => {
|
| 38 |
+
const key = getOpenCodeApiKey()
|
| 39 |
+
if (key) {
|
| 40 |
+
setExistingApiKey(key)
|
| 41 |
+
}
|
| 42 |
+
const model = getOpenCodeModelName()
|
| 43 |
+
if (model) {
|
| 44 |
+
setExistingModelName(model)
|
| 45 |
+
setSelectedModel(model)
|
| 46 |
+
}
|
| 47 |
+
}, [])
|
| 48 |
+
|
| 49 |
+
const menuOptions = [
|
| 50 |
+
{
|
| 51 |
+
label: (
|
| 52 |
+
<Text>
|
| 53 |
+
Use free models{' '}
|
| 54 |
+
<Text dimColor={true}>No API key required, select a model</Text>
|
| 55 |
+
{'\n'}
|
| 56 |
+
</Text>
|
| 57 |
+
),
|
| 58 |
+
value: 'free_models',
|
| 59 |
+
},
|
| 60 |
+
{
|
| 61 |
+
label: (
|
| 62 |
+
<Text>
|
| 63 |
+
Paste OpenCode Zen API key{' '}
|
| 64 |
+
<Text dimColor={true}>Access paid models, pay-as-you-go</Text>
|
| 65 |
+
{'\n'}
|
| 66 |
+
</Text>
|
| 67 |
+
),
|
| 68 |
+
value: 'api_key',
|
| 69 |
+
},
|
| 70 |
+
] as const
|
| 71 |
+
|
| 72 |
+
async function handleMenuSelection(value: string): Promise<void> {
|
| 73 |
+
setStatus(null)
|
| 74 |
+
|
| 75 |
+
if (value === 'api_key') {
|
| 76 |
+
setInputValue('')
|
| 77 |
+
setCursorOffset(0)
|
| 78 |
+
setMode('api_key')
|
| 79 |
+
return
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
if (value === 'free_models') {
|
| 83 |
+
setMode('free_models')
|
| 84 |
+
return
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
async function handleFreeModelSelect(): Promise<void> {
|
| 89 |
+
setIsBusy(true)
|
| 90 |
+
setStatus(null)
|
| 91 |
+
try {
|
| 92 |
+
// 免费模型不需要 API Key,直接保存模型名称
|
| 93 |
+
await saveOpenCodeApiKey('', selectedModel)
|
| 94 |
+
onDone()
|
| 95 |
+
} catch (error) {
|
| 96 |
+
setStatus(error instanceof Error ? error.message : String(error))
|
| 97 |
+
} finally {
|
| 98 |
+
setIsBusy(false)
|
| 99 |
+
}
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
async function handleSubmit(value?: string): Promise<void> {
|
| 103 |
+
const trimmed = value?.trim() || ''
|
| 104 |
+
const keyToSave = trimmed || existingApiKey
|
| 105 |
+
|
| 106 |
+
if (!keyToSave) {
|
| 107 |
+
return
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
setIsBusy(true)
|
| 111 |
+
setStatus(null)
|
| 112 |
+
try {
|
| 113 |
+
await saveOpenCodeApiKey(keyToSave)
|
| 114 |
+
onDone()
|
| 115 |
+
} catch (error) {
|
| 116 |
+
setStatus(error instanceof Error ? error.message : String(error))
|
| 117 |
+
} finally {
|
| 118 |
+
setIsBusy(false)
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
if (isBusy) {
|
| 123 |
+
return (
|
| 124 |
+
<Box flexDirection="column" gap={1}>
|
| 125 |
+
<Text>Configuring OpenCode Zen for Better-Clawd...</Text>
|
| 126 |
+
<Text dimColor={true}>
|
| 127 |
+
OpenCode Zen provides free models with no API key required.
|
| 128 |
+
</Text>
|
| 129 |
+
</Box>
|
| 130 |
+
)
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
if (mode === 'free_models') {
|
| 134 |
+
return (
|
| 135 |
+
<Box flexDirection="column" gap={1}>
|
| 136 |
+
<Text bold={true}>
|
| 137 |
+
{startingMessage ?? 'Select a free model from OpenCode Zen.'}
|
| 138 |
+
</Text>
|
| 139 |
+
<Text dimColor={true}>
|
| 140 |
+
Free models are limited-time offers except GPT 5 Nano (permanently free).
|
| 141 |
+
{'\n'}
|
| 142 |
+
No API key needed — just start coding!
|
| 143 |
+
</Text>
|
| 144 |
+
<Text>Select model:</Text>
|
| 145 |
+
<Select
|
| 146 |
+
options={FREE_MODELS.map(model => ({
|
| 147 |
+
label: (
|
| 148 |
+
<Text>
|
| 149 |
+
{model.label}{' '}
|
| 150 |
+
<Text dimColor={true}>{model.description}</Text>
|
| 151 |
+
{'\n'}
|
| 152 |
+
</Text>
|
| 153 |
+
),
|
| 154 |
+
value: model.value,
|
| 155 |
+
}))}
|
| 156 |
+
onChange={value => {
|
| 157 |
+
setSelectedModel(value)
|
| 158 |
+
// 选择后直接提交
|
| 159 |
+
void handleFreeModelSelect()
|
| 160 |
+
}}
|
| 161 |
+
/>
|
| 162 |
+
<Box flexDirection="row" gap={2} marginTop={1}>
|
| 163 |
+
<Text color="subtle">Select a model to continue</Text>
|
| 164 |
+
<Text color="subtle">·</Text>
|
| 165 |
+
<Text color="subtle">Esc to cancel</Text>
|
| 166 |
+
</Box>
|
| 167 |
+
{status ? <Text color="error">{status}</Text> : null}
|
| 168 |
+
<Box marginTop={1}>
|
| 169 |
+
<TextInput
|
| 170 |
+
value=""
|
| 171 |
+
onChange={() => {}}
|
| 172 |
+
onSubmit={handleFreeModelSelect}
|
| 173 |
+
onExit={() => setMode('menu')}
|
| 174 |
+
cursorOffset={0}
|
| 175 |
+
onChangeCursorOffset={() => {}}
|
| 176 |
+
columns={72}
|
| 177 |
+
focus={true}
|
| 178 |
+
placeholder="Or press Enter to use selected model"
|
| 179 |
+
/>
|
| 180 |
+
</Box>
|
| 181 |
+
</Box>
|
| 182 |
+
)
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
if (mode === 'api_key') {
|
| 186 |
+
return (
|
| 187 |
+
<Box flexDirection="column" gap={1}>
|
| 188 |
+
<Text>
|
| 189 |
+
OpenCode Zen API keys use pay-as-you-go billing.
|
| 190 |
+
{'\n'}
|
| 191 |
+
Sign up at opencode.ai/zen to get your key.
|
| 192 |
+
</Text>
|
| 193 |
+
<Box>
|
| 194 |
+
<Text>Paste your OpenCode Zen API key:</Text>
|
| 195 |
+
<TextInput
|
| 196 |
+
value={inputValue}
|
| 197 |
+
onChange={setInputValue}
|
| 198 |
+
onSubmit={handleSubmit}
|
| 199 |
+
onExit={() => {
|
| 200 |
+
setMode('menu')
|
| 201 |
+
setInputValue('')
|
| 202 |
+
setCursorOffset(0)
|
| 203 |
+
}}
|
| 204 |
+
cursorOffset={cursorOffset}
|
| 205 |
+
onChangeCursorOffset={setCursorOffset}
|
| 206 |
+
columns={72}
|
| 207 |
+
mask="*"
|
| 208 |
+
focus={true}
|
| 209 |
+
placeholder={existingApiKey || undefined}
|
| 210 |
+
/>
|
| 211 |
+
</Box>
|
| 212 |
+
{status ? <Text color="error">{status}</Text> : null}
|
| 213 |
+
<Text dimColor={true}>
|
| 214 |
+
Press <Text bold={true}>Enter</Text> to save, or <Text bold={true}>Esc</Text>{' '}
|
| 215 |
+
to cancel.
|
| 216 |
+
</Text>
|
| 217 |
+
</Box>
|
| 218 |
+
)
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
return (
|
| 222 |
+
<Box flexDirection="column" gap={1}>
|
| 223 |
+
<Text bold={true}>
|
| 224 |
+
{startingMessage ??
|
| 225 |
+
'Better-Clawd can use OpenCode Zen free models or with a standard OpenCode Zen API key.'}
|
| 226 |
+
</Text>
|
| 227 |
+
<Text dimColor={true}>
|
| 228 |
+
OpenCode Zen provides free models out of the box — no API key required.
|
| 229 |
+
{'\n'}
|
| 230 |
+
Free models include Big Pickle, GPT 5 Nano, and more.
|
| 231 |
+
</Text>
|
| 232 |
+
{status ? <Text color="error">{status}</Text> : null}
|
| 233 |
+
<Box>
|
| 234 |
+
<Select
|
| 235 |
+
options={menuOptions}
|
| 236 |
+
onChange={value => {
|
| 237 |
+
void handleMenuSelection(value)
|
| 238 |
+
}}
|
| 239 |
+
/>
|
| 240 |
+
</Box>
|
| 241 |
+
</Box>
|
| 242 |
+
)
|
| 243 |
+
}
|
src/services/api/client.ts
CHANGED
|
@@ -12,6 +12,8 @@ import {
|
|
| 12 |
getOpenRouterApiKey,
|
| 13 |
getOpenAIApiKey,
|
| 14 |
getLocalBaseUrl,
|
|
|
|
|
|
|
| 15 |
} from 'src/utils/auth.js'
|
| 16 |
import { getUserAgent } from 'src/utils/http.js'
|
| 17 |
import { getSmallFastModel } from 'src/utils/model/model.js'
|
|
@@ -20,8 +22,10 @@ import {
|
|
| 20 |
isFirstPartyAnthropicBaseUrl,
|
| 21 |
getOpenRouterBaseUrl,
|
| 22 |
getOpenAIBaseUrl,
|
|
|
|
| 23 |
} from 'src/utils/model/providers.js'
|
| 24 |
import { getProxyFetchOptions } from 'src/utils/proxy.js'
|
|
|
|
| 25 |
import {
|
| 26 |
getIsNonInteractiveSession,
|
| 27 |
getSessionId,
|
|
@@ -141,7 +145,14 @@ export async function getAnthropicClient({
|
|
| 141 |
await configureApiKeyHeaders(defaultHeaders, getIsNonInteractiveSession())
|
| 142 |
}
|
| 143 |
|
| 144 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
|
| 146 |
const ARGS = {
|
| 147 |
defaultHeaders,
|
|
@@ -303,7 +314,6 @@ export async function getAnthropicClient({
|
|
| 303 |
}
|
| 304 |
|
| 305 |
// Determine authentication method based on available tokens
|
| 306 |
-
const provider = getAPIProvider()
|
| 307 |
let clientConfig: ConstructorParameters<typeof Anthropic>[0] = {
|
| 308 |
apiKey: isClaudeAISubscriber() ? null : apiKey || getAnthropicApiKey(),
|
| 309 |
authToken: isClaudeAISubscriber()
|
|
@@ -345,6 +355,14 @@ export async function getAnthropicClient({
|
|
| 345 |
}
|
| 346 |
}
|
| 347 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 348 |
return new Anthropic(clientConfig)
|
| 349 |
}
|
| 350 |
|
|
@@ -354,8 +372,8 @@ async function configureApiKeyHeaders(
|
|
| 354 |
): Promise<void> {
|
| 355 |
const provider = getAPIProvider()
|
| 356 |
|
| 357 |
-
// Skip for OpenRouter, OpenAI, and
|
| 358 |
-
if (provider === 'openrouter' || provider === 'openai' || provider === 'local') {
|
| 359 |
return
|
| 360 |
}
|
| 361 |
|
|
|
|
| 12 |
getOpenRouterApiKey,
|
| 13 |
getOpenAIApiKey,
|
| 14 |
getLocalBaseUrl,
|
| 15 |
+
getOpenCodeApiKey,
|
| 16 |
+
getOpenCodeModelName,
|
| 17 |
} from 'src/utils/auth.js'
|
| 18 |
import { getUserAgent } from 'src/utils/http.js'
|
| 19 |
import { getSmallFastModel } from 'src/utils/model/model.js'
|
|
|
|
| 22 |
isFirstPartyAnthropicBaseUrl,
|
| 23 |
getOpenRouterBaseUrl,
|
| 24 |
getOpenAIBaseUrl,
|
| 25 |
+
getOpencodeBaseUrl,
|
| 26 |
} from 'src/utils/model/providers.js'
|
| 27 |
import { getProxyFetchOptions } from 'src/utils/proxy.js'
|
| 28 |
+
import { createOpenCodeFetchOverride } from './opencodeClient.js'
|
| 29 |
import {
|
| 30 |
getIsNonInteractiveSession,
|
| 31 |
getSessionId,
|
|
|
|
| 145 |
await configureApiKeyHeaders(defaultHeaders, getIsNonInteractiveSession())
|
| 146 |
}
|
| 147 |
|
| 148 |
+
// For OpenCode provider, use custom fetch to convert Anthropic format to OpenAI format
|
| 149 |
+
const provider = getAPIProvider()
|
| 150 |
+
let opencodeFetchOverride: ClientOptions['fetch'] | undefined
|
| 151 |
+
if (provider === 'opencode' && model) {
|
| 152 |
+
opencodeFetchOverride = createOpenCodeFetchOverride(model)
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
const resolvedFetch = buildFetch(fetchOverride || opencodeFetchOverride, source)
|
| 156 |
|
| 157 |
const ARGS = {
|
| 158 |
defaultHeaders,
|
|
|
|
| 314 |
}
|
| 315 |
|
| 316 |
// Determine authentication method based on available tokens
|
|
|
|
| 317 |
let clientConfig: ConstructorParameters<typeof Anthropic>[0] = {
|
| 318 |
apiKey: isClaudeAISubscriber() ? null : apiKey || getAnthropicApiKey(),
|
| 319 |
authToken: isClaudeAISubscriber()
|
|
|
|
| 355 |
}
|
| 356 |
}
|
| 357 |
|
| 358 |
+
// Handle OpenCode Zen - uses custom fetch override to convert Anthropic format to OpenAI format
|
| 359 |
+
// No need to set baseURL here as the fetch override handles the endpoint directly
|
| 360 |
+
if (provider === 'opencode') {
|
| 361 |
+
// The fetch override will handle the API conversion
|
| 362 |
+
// Just set a dummy apiKey to satisfy the SDK
|
| 363 |
+
clientConfig.apiKey = 'opencode-zen'
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
return new Anthropic(clientConfig)
|
| 367 |
}
|
| 368 |
|
|
|
|
| 372 |
): Promise<void> {
|
| 373 |
const provider = getAPIProvider()
|
| 374 |
|
| 375 |
+
// Skip for OpenRouter, OpenAI, Local, and OpenCode - they use apiKey parameter instead
|
| 376 |
+
if (provider === 'openrouter' || provider === 'openai' || provider === 'local' || provider === 'opencode') {
|
| 377 |
return
|
| 378 |
}
|
| 379 |
|
src/services/api/opencodeClient.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { getOpenCodeApiKey, getOpenCodeModelName } from '../../utils/auth.js'
|
| 2 |
+
import {
|
| 3 |
+
convertAnthropicMessagesToOpenAI,
|
| 4 |
+
convertAnthropicToolsToOpenAI,
|
| 5 |
+
convertOpenAIStreamToAnthropic,
|
| 6 |
+
type AnthropicMessage,
|
| 7 |
+
} from './copilotClient.js'
|
| 8 |
+
|
| 9 |
+
const OPENCODE_BASE_URL = 'https://opencode.ai/zen/v1'
|
| 10 |
+
|
| 11 |
+
function normalizeBaseUrl(url: string): string {
|
| 12 |
+
return url.replace(/\/$/, '')
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
function chatCompletionsUrl(base: string): string {
|
| 16 |
+
const b = normalizeBaseUrl(base)
|
| 17 |
+
if (b.endsWith('/v1')) {
|
| 18 |
+
return `${b}/chat/completions`
|
| 19 |
+
}
|
| 20 |
+
return `${b}/v1/chat/completions`
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
export function createOpenCodeFetchOverride(
|
| 24 |
+
model: string,
|
| 25 |
+
): (input: RequestInfo | URL, init?: RequestInit) => Promise<Response> {
|
| 26 |
+
const apiKey = getOpenCodeApiKey() || ''
|
| 27 |
+
const modelName = getOpenCodeModelName() || model || 'big-pickle'
|
| 28 |
+
const endpoint = chatCompletionsUrl(apiKey ? OPENCODE_BASE_URL : OPENCODE_BASE_URL)
|
| 29 |
+
|
| 30 |
+
return async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
| 31 |
+
const url = input instanceof URL ? input.href : typeof input === 'string' ? input : input.url
|
| 32 |
+
|
| 33 |
+
if (!url.includes('/messages') && !url.includes('/v1/')) {
|
| 34 |
+
return fetch(input, init)
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
if (url.includes('/count_tokens') || url.includes('/models')) {
|
| 38 |
+
return new Response(JSON.stringify({ input_tokens: 0 }), {
|
| 39 |
+
status: 200,
|
| 40 |
+
headers: { 'Content-Type': 'application/json' },
|
| 41 |
+
})
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
let anthropicBody: Record<string, unknown> = {}
|
| 45 |
+
if (init?.body) {
|
| 46 |
+
try {
|
| 47 |
+
anthropicBody = JSON.parse(
|
| 48 |
+
typeof init.body === 'string' ? init.body : new TextDecoder().decode(init.body as ArrayBuffer),
|
| 49 |
+
)
|
| 50 |
+
} catch {
|
| 51 |
+
return fetch(input, init)
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
const systemBlocks = anthropicBody.system as
|
| 56 |
+
| Array<{ type: string; text: string }>
|
| 57 |
+
| string
|
| 58 |
+
| undefined
|
| 59 |
+
let systemPrompt = ''
|
| 60 |
+
if (typeof systemBlocks === 'string') {
|
| 61 |
+
systemPrompt = systemBlocks
|
| 62 |
+
} else if (Array.isArray(systemBlocks)) {
|
| 63 |
+
systemPrompt = systemBlocks
|
| 64 |
+
.filter(b => b.type === 'text')
|
| 65 |
+
.map(b => b.text)
|
| 66 |
+
.join('\n\n')
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
const anthropicMessages = (anthropicBody.messages || []) as AnthropicMessage[]
|
| 70 |
+
const openaiMessages = convertAnthropicMessagesToOpenAI(anthropicMessages, systemPrompt)
|
| 71 |
+
|
| 72 |
+
const anthropicTools = (anthropicBody.tools || []) as Array<{
|
| 73 |
+
name: string
|
| 74 |
+
description?: string
|
| 75 |
+
input_schema?: Record<string, unknown>
|
| 76 |
+
}>
|
| 77 |
+
const openaiTools = anthropicTools.length > 0 ? convertAnthropicToolsToOpenAI(anthropicTools) : undefined
|
| 78 |
+
|
| 79 |
+
const isStreaming = anthropicBody.stream === true
|
| 80 |
+
|
| 81 |
+
const requestBody: Record<string, unknown> = {
|
| 82 |
+
model: modelName,
|
| 83 |
+
messages: openaiMessages,
|
| 84 |
+
stream: isStreaming,
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
if (anthropicBody.max_tokens) {
|
| 88 |
+
requestBody.max_tokens = anthropicBody.max_tokens
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
if (openaiTools && openaiTools.length > 0) {
|
| 92 |
+
requestBody.tools = openaiTools
|
| 93 |
+
requestBody.tool_choice = 'auto'
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
const headers: Record<string, string> = {
|
| 97 |
+
'Content-Type': 'application/json',
|
| 98 |
+
'User-Agent': 'claude-code/2.1.88',
|
| 99 |
+
}
|
| 100 |
+
if (apiKey) {
|
| 101 |
+
headers.Authorization = `Bearer ${apiKey}`
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
const openaiResponse = await fetch(endpoint, {
|
| 105 |
+
method: 'POST',
|
| 106 |
+
headers,
|
| 107 |
+
body: JSON.stringify(requestBody),
|
| 108 |
+
signal: init?.signal,
|
| 109 |
+
})
|
| 110 |
+
|
| 111 |
+
if (!openaiResponse.ok) {
|
| 112 |
+
return openaiResponse
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
if (!isStreaming) {
|
| 116 |
+
const data = (await openaiResponse.json()) as {
|
| 117 |
+
id: string
|
| 118 |
+
choices: Array<{
|
| 119 |
+
message: {
|
| 120 |
+
role: string
|
| 121 |
+
content: string | null
|
| 122 |
+
tool_calls?: Array<{
|
| 123 |
+
id: string
|
| 124 |
+
function: { name: string; arguments: string }
|
| 125 |
+
}>
|
| 126 |
+
}
|
| 127 |
+
finish_reason: string
|
| 128 |
+
}>
|
| 129 |
+
usage?: { prompt_tokens: number; completion_tokens: number }
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
const choice = data.choices[0]
|
| 133 |
+
const anthropicContent: Array<{
|
| 134 |
+
type: string
|
| 135 |
+
text?: string
|
| 136 |
+
id?: string
|
| 137 |
+
name?: string
|
| 138 |
+
input?: unknown
|
| 139 |
+
}> = []
|
| 140 |
+
|
| 141 |
+
if (choice?.message?.content) {
|
| 142 |
+
anthropicContent.push({ type: 'text', text: choice.message.content })
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
if (choice?.message?.tool_calls) {
|
| 146 |
+
for (const tc of choice.message.tool_calls) {
|
| 147 |
+
anthropicContent.push({
|
| 148 |
+
type: 'tool_use',
|
| 149 |
+
id: tc.id,
|
| 150 |
+
name: tc.function.name,
|
| 151 |
+
input: JSON.parse(tc.function.arguments || '{}'),
|
| 152 |
+
})
|
| 153 |
+
}
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
const anthropicResponse = {
|
| 157 |
+
id: data.id || `msg_opencode_${Date.now()}`,
|
| 158 |
+
type: 'message',
|
| 159 |
+
role: 'assistant',
|
| 160 |
+
content: anthropicContent,
|
| 161 |
+
model: modelName,
|
| 162 |
+
stop_reason: choice?.finish_reason === 'tool_calls' ? 'tool_use' : 'end_turn',
|
| 163 |
+
usage: {
|
| 164 |
+
input_tokens: data.usage?.prompt_tokens || 0,
|
| 165 |
+
output_tokens: data.usage?.completion_tokens || 0,
|
| 166 |
+
},
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
return new Response(JSON.stringify(anthropicResponse), {
|
| 170 |
+
status: 200,
|
| 171 |
+
headers: { 'Content-Type': 'application/json' },
|
| 172 |
+
})
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
if (!openaiResponse.body) {
|
| 176 |
+
return openaiResponse
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
const transformStream = convertOpenAIStreamToAnthropic(openaiResponse.body, modelName)
|
| 180 |
+
|
| 181 |
+
return new Response(transformStream, {
|
| 182 |
+
status: 200,
|
| 183 |
+
headers: {
|
| 184 |
+
'Content-Type': 'text/event-stream',
|
| 185 |
+
'Cache-Control': 'no-cache',
|
| 186 |
+
Connection: 'keep-alive',
|
| 187 |
+
},
|
| 188 |
+
})
|
| 189 |
+
}
|
| 190 |
+
}
|
src/utils/auth.ts
CHANGED
|
@@ -430,6 +430,28 @@ export async function saveOpenRouterApiKey(apiKey: string): Promise<void> {
|
|
| 430 |
clearStoredProviderCache()
|
| 431 |
}
|
| 432 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 433 |
export async function saveLocalModelConfig(baseUrl: string, modelName: string): Promise<void> {
|
| 434 |
if (!baseUrl.trim()) {
|
| 435 |
throw new Error('Base URL cannot be empty')
|
|
|
|
| 430 |
clearStoredProviderCache()
|
| 431 |
}
|
| 432 |
|
| 433 |
+
export async function saveOpenCodeApiKey(apiKey: string, modelName?: string): Promise<void> {
|
| 434 |
+
saveGlobalConfig(current => ({
|
| 435 |
+
...current,
|
| 436 |
+
authProvider: 'opencode',
|
| 437 |
+
openCodeApiKey: apiKey || undefined,
|
| 438 |
+
openCodeModelName: modelName || undefined,
|
| 439 |
+
}))
|
| 440 |
+
// Clear provider cache so it will be re-read on next access
|
| 441 |
+
const { clearStoredProviderCache } = await import('./model/providers.js')
|
| 442 |
+
clearStoredProviderCache()
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
+
export function getOpenCodeApiKey(): null | string {
|
| 446 |
+
const config = getGlobalConfig()
|
| 447 |
+
return config.openCodeApiKey || null
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
+
export function getOpenCodeModelName(): null | string {
|
| 451 |
+
const config = getGlobalConfig()
|
| 452 |
+
return config.openCodeModelName || null
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
export async function saveLocalModelConfig(baseUrl: string, modelName: string): Promise<void> {
|
| 456 |
if (!baseUrl.trim()) {
|
| 457 |
throw new Error('Base URL cannot be empty')
|
src/utils/config.ts
CHANGED
|
@@ -187,7 +187,7 @@ export type GlobalConfig = {
|
|
| 187 |
apiKeyHelper?: string
|
| 188 |
|
| 189 |
// Authentication provider configuration
|
| 190 |
-
authProvider?: 'anthropic' | 'openai' | 'openrouter' | 'local'
|
| 191 |
|
| 192 |
// OpenRouter API key
|
| 193 |
openRouterApiKey?: string
|
|
@@ -200,6 +200,10 @@ export type GlobalConfig = {
|
|
| 200 |
localBaseUrl?: string
|
| 201 |
localModelName?: string
|
| 202 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
projects?: Record<string, ProjectConfig>
|
| 204 |
numStartups: number
|
| 205 |
installMethod?: InstallMethod
|
|
|
|
| 187 |
apiKeyHelper?: string
|
| 188 |
|
| 189 |
// Authentication provider configuration
|
| 190 |
+
authProvider?: 'anthropic' | 'openai' | 'openrouter' | 'local' | 'opencode'
|
| 191 |
|
| 192 |
// OpenRouter API key
|
| 193 |
openRouterApiKey?: string
|
|
|
|
| 200 |
localBaseUrl?: string
|
| 201 |
localModelName?: string
|
| 202 |
|
| 203 |
+
// OpenCode Zen configuration
|
| 204 |
+
openCodeApiKey?: string
|
| 205 |
+
openCodeModelName?: string
|
| 206 |
+
|
| 207 |
projects?: Record<string, ProjectConfig>
|
| 208 |
numStartups: number
|
| 209 |
installMethod?: InstallMethod
|
src/utils/model/model.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
| 13 |
isMaxSubscriber,
|
| 14 |
isProSubscriber,
|
| 15 |
isTeamPremiumSubscriber,
|
|
|
|
| 16 |
} from '../auth.js'
|
| 17 |
import {
|
| 18 |
has1mContext,
|
|
@@ -248,6 +249,35 @@ export function getDefaultMainLoopModelSetting(): ModelName | ModelAlias {
|
|
| 248 |
return 'default'
|
| 249 |
}
|
| 250 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
// Ants default to defaultModel from flag config, or Opus 1M if not configured
|
| 252 |
if (process.env.USER_TYPE === 'ant') {
|
| 253 |
return (
|
|
|
|
| 13 |
isMaxSubscriber,
|
| 14 |
isProSubscriber,
|
| 15 |
isTeamPremiumSubscriber,
|
| 16 |
+
getOpenCodeModelName,
|
| 17 |
} from '../auth.js'
|
| 18 |
import {
|
| 19 |
has1mContext,
|
|
|
|
| 249 |
return 'default'
|
| 250 |
}
|
| 251 |
|
| 252 |
+
// Check if using OpenCode Zen provider
|
| 253 |
+
let isOpenCodeProvider = apiProvider === 'opencode'
|
| 254 |
+
|
| 255 |
+
// If getAPIProvider() returns null, check the config file directly
|
| 256 |
+
if (!isOpenCodeProvider && apiProvider === null) {
|
| 257 |
+
try {
|
| 258 |
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
| 259 |
+
const { readFileSync } = require('fs') as typeof import('fs')
|
| 260 |
+
const { getGlobalClaudeFile } = require('./env.js') as typeof import('./env.js')
|
| 261 |
+
const raw = readFileSync(getGlobalClaudeFile(), 'utf8')
|
| 262 |
+
const config = JSON.parse(raw) as {
|
| 263 |
+
authProvider?: string
|
| 264 |
+
openCodeModelName?: string
|
| 265 |
+
}
|
| 266 |
+
isOpenCodeProvider = config.authProvider === 'opencode' && !!config.openCodeModelName
|
| 267 |
+
} catch {
|
| 268 |
+
// Ignore errors, fall through to default behavior
|
| 269 |
+
}
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
if (isOpenCodeProvider) {
|
| 273 |
+
const openCodeModelName = getOpenCodeModelName()
|
| 274 |
+
if (openCodeModelName) {
|
| 275 |
+
return openCodeModelName
|
| 276 |
+
}
|
| 277 |
+
// Fallback to Big Pickle if no model is configured
|
| 278 |
+
return 'big-pickle'
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
// Ants default to defaultModel from flag config, or Opus 1M if not configured
|
| 282 |
if (process.env.USER_TYPE === 'ant') {
|
| 283 |
return (
|
src/utils/model/modelOptions.ts
CHANGED
|
@@ -68,6 +68,7 @@ export function getDefaultOptionForUser(fastMode = false): ModelOption {
|
|
| 68 |
const provider = getAPIProvider()
|
| 69 |
const isOpenAI = provider === 'openai'
|
| 70 |
const isLocal = provider === 'local'
|
|
|
|
| 71 |
const is3P = provider !== 'firstParty'
|
| 72 |
|
| 73 |
// Check if we should use local model
|
|
@@ -92,6 +93,35 @@ export function getDefaultOptionForUser(fastMode = false): ModelOption {
|
|
| 92 |
}
|
| 93 |
}
|
| 94 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
if (localModelName) {
|
| 96 |
return {
|
| 97 |
value: null,
|
|
|
|
| 68 |
const provider = getAPIProvider()
|
| 69 |
const isOpenAI = provider === 'openai'
|
| 70 |
const isLocal = provider === 'local'
|
| 71 |
+
const isOpenCode = provider === 'opencode'
|
| 72 |
const is3P = provider !== 'firstParty'
|
| 73 |
|
| 74 |
// Check if we should use local model
|
|
|
|
| 93 |
}
|
| 94 |
}
|
| 95 |
|
| 96 |
+
// Check if we should use OpenCode Zen model
|
| 97 |
+
let openCodeModelName: string | null = null
|
| 98 |
+
if (isOpenCode || provider === null) {
|
| 99 |
+
try {
|
| 100 |
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
| 101 |
+
const { readFileSync } = require('fs') as typeof import('fs')
|
| 102 |
+
const { getGlobalClaudeFile } = require('../env.js') as typeof import('../env.js')
|
| 103 |
+
const raw = readFileSync(getGlobalClaudeFile(), 'utf8')
|
| 104 |
+
const config = JSON.parse(raw) as {
|
| 105 |
+
authProvider?: string
|
| 106 |
+
openCodeModelName?: string
|
| 107 |
+
}
|
| 108 |
+
if (config.authProvider === 'opencode' && config.openCodeModelName) {
|
| 109 |
+
openCodeModelName = config.openCodeModelName
|
| 110 |
+
}
|
| 111 |
+
} catch {
|
| 112 |
+
// Ignore errors
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
if (openCodeModelName) {
|
| 117 |
+
return {
|
| 118 |
+
value: null,
|
| 119 |
+
label: 'Default (recommended)',
|
| 120 |
+
description: `Use the default OpenCode Zen model (currently ${openCodeModelName})`,
|
| 121 |
+
descriptionForModel: `Default OpenCode Zen model (currently ${openCodeModelName})`,
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
if (localModelName) {
|
| 126 |
return {
|
| 127 |
value: null,
|
src/utils/model/modelStrings.ts
CHANGED
|
@@ -31,6 +31,14 @@ function getBuiltinModelStrings(provider: APIProvider): ModelStrings {
|
|
| 31 |
return out as ModelStrings
|
| 32 |
}
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
if (provider === 'openrouter') {
|
| 35 |
const out = getBuiltinModelStrings('firstParty') as Record<string, string>
|
| 36 |
out.sonnet46 =
|
|
|
|
| 31 |
return out as ModelStrings
|
| 32 |
}
|
| 33 |
|
| 34 |
+
if (provider === 'opencode') {
|
| 35 |
+
const out = getBuiltinModelStrings('firstParty') as Record<string, string>
|
| 36 |
+
out.haiku45 = process.env.OPENCODE_HAIKU_MODEL || 'gpt-5-nano'
|
| 37 |
+
out.sonnet46 = process.env.OPENCODE_SONNET_MODEL || 'big-pickle'
|
| 38 |
+
out.opus46 = process.env.OPENCODE_OPUS_MODEL || 'big-pickle'
|
| 39 |
+
return out as ModelStrings
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
if (provider === 'openrouter') {
|
| 43 |
const out = getBuiltinModelStrings('firstParty') as Record<string, string>
|
| 44 |
out.sonnet46 =
|
src/utils/model/providers.ts
CHANGED
|
@@ -7,6 +7,7 @@ export type APIProvider =
|
|
| 7 |
| 'openrouter'
|
| 8 |
| 'openai'
|
| 9 |
| 'local'
|
|
|
|
| 10 |
| 'bedrock'
|
| 11 |
| 'vertex'
|
| 12 |
| 'foundry'
|
|
@@ -25,12 +26,14 @@ function getStoredProviderPreference(): APIProvider | null {
|
|
| 25 |
const { readFileSync } = require('fs') as typeof import('fs')
|
| 26 |
const raw = readFileSync(getGlobalClaudeFile(), 'utf8')
|
| 27 |
const config = JSON.parse(raw) as {
|
| 28 |
-
authProvider?: 'anthropic' | 'openrouter' | 'openai' | 'local'
|
| 29 |
openRouterApiKey?: string
|
| 30 |
openAiApiKey?: string
|
| 31 |
openAiAccessToken?: string
|
| 32 |
localBaseUrl?: string
|
| 33 |
localModelName?: string
|
|
|
|
|
|
|
| 34 |
}
|
| 35 |
|
| 36 |
let result: APIProvider | null
|
|
@@ -47,6 +50,9 @@ function getStoredProviderPreference(): APIProvider | null {
|
|
| 47 |
case 'local':
|
| 48 |
result = config.localBaseUrl ? 'local' : null
|
| 49 |
break
|
|
|
|
|
|
|
|
|
|
| 50 |
case 'anthropic':
|
| 51 |
result = 'firstParty'
|
| 52 |
break
|
|
@@ -82,6 +88,8 @@ function getExplicitProviderOverride(): APIProvider | null {
|
|
| 82 |
return 'openai'
|
| 83 |
case 'local':
|
| 84 |
return 'local'
|
|
|
|
|
|
|
| 85 |
case 'bedrock':
|
| 86 |
return 'bedrock'
|
| 87 |
case 'vertex':
|
|
@@ -133,6 +141,19 @@ export function isOpenAIConfigured(): boolean {
|
|
| 133 |
return getStoredProviderPreference() === 'openai'
|
| 134 |
}
|
| 135 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
export function getOpenRouterBaseUrl(): string {
|
| 137 |
const configuredBaseUrl = process.env.OPENROUTER_BASE_URL
|
| 138 |
const fallbackBaseUrl = 'https://openrouter.ai/api'
|
|
@@ -176,11 +197,13 @@ export function getAPIProvider(): APIProvider | null {
|
|
| 176 |
? 'vertex'
|
| 177 |
: isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY)
|
| 178 |
? 'foundry'
|
| 179 |
-
:
|
| 180 |
-
? '
|
| 181 |
-
:
|
| 182 |
-
? '
|
| 183 |
-
:
|
|
|
|
|
|
|
| 184 |
}
|
| 185 |
|
| 186 |
export function getAPIProviderForStatsig(): AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS {
|
|
@@ -190,7 +213,7 @@ export function getAPIProviderForStatsig(): AnalyticsMetadata_I_VERIFIED_THIS_IS
|
|
| 190 |
export function isAnthropicCompatibleProvider(
|
| 191 |
provider: APIProvider = getAPIProvider(),
|
| 192 |
): boolean {
|
| 193 |
-
return provider !== 'openai'
|
| 194 |
}
|
| 195 |
|
| 196 |
/**
|
|
|
|
| 7 |
| 'openrouter'
|
| 8 |
| 'openai'
|
| 9 |
| 'local'
|
| 10 |
+
| 'opencode'
|
| 11 |
| 'bedrock'
|
| 12 |
| 'vertex'
|
| 13 |
| 'foundry'
|
|
|
|
| 26 |
const { readFileSync } = require('fs') as typeof import('fs')
|
| 27 |
const raw = readFileSync(getGlobalClaudeFile(), 'utf8')
|
| 28 |
const config = JSON.parse(raw) as {
|
| 29 |
+
authProvider?: 'anthropic' | 'openrouter' | 'openai' | 'local' | 'opencode'
|
| 30 |
openRouterApiKey?: string
|
| 31 |
openAiApiKey?: string
|
| 32 |
openAiAccessToken?: string
|
| 33 |
localBaseUrl?: string
|
| 34 |
localModelName?: string
|
| 35 |
+
openCodeApiKey?: string
|
| 36 |
+
openCodeModelName?: string
|
| 37 |
}
|
| 38 |
|
| 39 |
let result: APIProvider | null
|
|
|
|
| 50 |
case 'local':
|
| 51 |
result = config.localBaseUrl ? 'local' : null
|
| 52 |
break
|
| 53 |
+
case 'opencode':
|
| 54 |
+
result = 'opencode'
|
| 55 |
+
break
|
| 56 |
case 'anthropic':
|
| 57 |
result = 'firstParty'
|
| 58 |
break
|
|
|
|
| 88 |
return 'openai'
|
| 89 |
case 'local':
|
| 90 |
return 'local'
|
| 91 |
+
case 'opencode':
|
| 92 |
+
return 'opencode'
|
| 93 |
case 'bedrock':
|
| 94 |
return 'bedrock'
|
| 95 |
case 'vertex':
|
|
|
|
| 141 |
return getStoredProviderPreference() === 'openai'
|
| 142 |
}
|
| 143 |
|
| 144 |
+
export function isOpencodeConfigured(): boolean {
|
| 145 |
+
if (getExplicitProviderOverride() === 'opencode') {
|
| 146 |
+
return true
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
// Check config file
|
| 150 |
+
return getStoredProviderPreference() === 'opencode'
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
export function getOpencodeBaseUrl(): string {
|
| 154 |
+
return process.env.OPENCODE_BASE_URL ?? 'https://opencode.ai/zen/v1'
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
export function getOpenRouterBaseUrl(): string {
|
| 158 |
const configuredBaseUrl = process.env.OPENROUTER_BASE_URL
|
| 159 |
const fallbackBaseUrl = 'https://openrouter.ai/api'
|
|
|
|
| 197 |
? 'vertex'
|
| 198 |
: isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY)
|
| 199 |
? 'foundry'
|
| 200 |
+
: isOpencodeConfigured()
|
| 201 |
+
? 'opencode'
|
| 202 |
+
: isOpenAIConfigured()
|
| 203 |
+
? 'openai'
|
| 204 |
+
: isOpenRouterConfigured()
|
| 205 |
+
? 'openrouter'
|
| 206 |
+
: getStoredProviderPreference()
|
| 207 |
}
|
| 208 |
|
| 209 |
export function getAPIProviderForStatsig(): AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS {
|
|
|
|
| 213 |
export function isAnthropicCompatibleProvider(
|
| 214 |
provider: APIProvider = getAPIProvider(),
|
| 215 |
): boolean {
|
| 216 |
+
return provider !== 'openai' && provider !== 'opencode'
|
| 217 |
}
|
| 218 |
|
| 219 |
/**
|