chenbhao commited on
Commit
fae9128
·
1 Parent(s): d092f6f

feat: add new free provider opencode

Browse files
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
- const resolvedFetch = buildFetch(fetchOverride, source)
 
 
 
 
 
 
 
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 Local - they use apiKey parameter instead
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
- : isOpenAIConfigured()
180
- ? 'openai'
181
- : isOpenRouterConfigured()
182
- ? 'openrouter'
183
- : getStoredProviderPreference()
 
 
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
  /**