Merge pull request #217 from cynicalight/main

feat: openai auth login via cli
This commit is contained in:
程序员阿江-Relakkes 2026-05-04 21:15:58 +08:00 committed by GitHub
commit 1becbe36a1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 2729 additions and 130 deletions

1
.nvimlog Normal file
View File

@ -0,0 +1 @@
WRN 2026-04-29T11:21:24.001 ?.14533 server_start:199: Failed to start server: operation not permitted: /var/folders/1g/3yj40ngn5b33fs9gg0wzvms00000gn/T/nvim.apple/gwHDxl/nvim.14533.0

View File

@ -71,6 +71,7 @@
"zod": "^4.3.6",
},
"devDependencies": {
"@vitejs/plugin-react": "^6.0.1",
"mermaid": "^11.14.0",
"vitepress": "^1.6.3",
"vitepress-plugin-mermaid": "^2.0.17",
@ -303,6 +304,8 @@
"@pondwader/socks5-server": ["@pondwader/socks5-server@1.0.10", "https://registry.npmmirror.com/@pondwader/socks5-server/-/socks5-server-1.0.10.tgz", {}, "sha512-bQY06wzzR8D2+vVCUoBsr5QS2U6UgPUQRmErNwtsuI6vLcyRKkafjkr3KxbtGFf9aBBIV2mcvlsKD1UYaIV+sg=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.7", "", {}, "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.1", "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", { "os": "android", "cpu": "arm" }, "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.1", "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", { "os": "android", "cpu": "arm64" }, "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA=="],
@ -551,6 +554,8 @@
"@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "https://registry.npmmirror.com/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="],
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="],
"@vitejs/plugin-vue": ["@vitejs/plugin-vue@5.2.4", "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", { "peerDependencies": { "vite": "^5.0.0 || ^6.0.0", "vue": "^3.2.25" } }, "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA=="],
"@vue/compiler-core": ["@vue/compiler-core@3.5.32", "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.32.tgz", { "dependencies": { "@babel/parser": "^7.29.2", "@vue/shared": "3.5.32", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ=="],

View File

@ -28,11 +28,11 @@
"@testing-library/react": "^16.1.0",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@vitejs/plugin-react": "^4.3.4",
"@vitejs/plugin-react": "^6.0.1",
"jsdom": "^25.0.1",
"tailwindcss": "^4.0.0",
"typescript": "^5.9.3",
"vite": "^6.0.7",
"vite": "^8.0.10",
"vitest": "^3.0.4",
},
"optionalDependencies": {
@ -54,36 +54,18 @@
"@babel/code-frame": ["@babel/code-frame@7.29.0", "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.0.tgz", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
"@babel/compat-data": ["@babel/compat-data@7.29.0", "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.29.0.tgz", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
"@babel/core": ["@babel/core@7.29.0", "https://registry.npmmirror.com/@babel/core/-/core-7.29.0.tgz", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
"@babel/generator": ["@babel/generator@7.29.1", "https://registry.npmmirror.com/@babel/generator/-/generator-7.29.1.tgz", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="],
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
"@babel/helpers": ["@babel/helpers@7.29.2", "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.29.2.tgz", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="],
"@babel/parser": ["@babel/parser@7.29.2", "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.2.tgz", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="],
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
"@babel/runtime": ["@babel/runtime@7.29.2", "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.29.2.tgz", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
"@babel/template": ["@babel/template@7.28.6", "https://registry.npmmirror.com/@babel/template/-/template-7.28.6.tgz", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
@ -114,6 +96,12 @@
"@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "https://registry.npmmirror.com/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="],
"@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
"@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
"@emotion/babel-plugin": ["@emotion/babel-plugin@11.13.5", "https://registry.npmmirror.com/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", { "dependencies": { "@babel/helper-module-imports": "^7.16.7", "@babel/runtime": "^7.18.3", "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", "@emotion/serialize": "^1.3.3", "babel-plugin-macros": "^3.1.0", "convert-source-map": "^1.5.0", "escape-string-regexp": "^4.0.0", "find-root": "^1.1.0", "source-map": "^0.5.7", "stylis": "4.2.0" } }, "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ=="],
"@emotion/cache": ["@emotion/cache@11.14.0", "https://registry.npmmirror.com/@emotion/cache/-/cache-11.14.0.tgz", { "dependencies": { "@emotion/memoize": "^0.9.0", "@emotion/sheet": "^1.4.0", "@emotion/utils": "^1.4.2", "@emotion/weak-memoize": "^0.4.0", "stylis": "4.2.0" } }, "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA=="],
@ -206,7 +194,41 @@
"@mermaid-js/parser": ["@mermaid-js/parser@1.1.0", "https://registry.npmmirror.com/@mermaid-js/parser/-/parser-1.1.0.tgz", { "dependencies": { "langium": "^4.0.0" } }, "sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
"@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.17", "", { "os": "android", "cpu": "arm64" }, "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ=="],
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.17", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw=="],
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.17", "", { "os": "darwin", "cpu": "x64" }, "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw=="],
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.17", "", { "os": "freebsd", "cpu": "x64" }, "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw=="],
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm" }, "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ=="],
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q=="],
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg=="],
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA=="],
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "s390x" }, "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA=="],
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "x64" }, "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA=="],
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.17", "", { "os": "linux", "cpu": "x64" }, "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw=="],
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.17", "", { "os": "none", "cpu": "arm64" }, "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA=="],
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.17", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA=="],
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17", "", { "os": "win32", "cpu": "arm64" }, "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA=="],
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.17", "", { "os": "win32", "cpu": "x64" }, "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.7", "", {}, "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.1", "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", { "os": "android", "cpu": "arm" }, "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA=="],
@ -346,16 +368,10 @@
"@testing-library/react": ["@testing-library/react@16.3.2", "https://registry.npmmirror.com/@testing-library/react/-/react-16.3.2.tgz", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g=="],
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
"@types/aria-query": ["@types/aria-query@5.0.4", "https://registry.npmmirror.com/@types/aria-query/-/aria-query-5.0.4.tgz", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
"@types/babel__core": ["@types/babel__core@7.20.5", "https://registry.npmmirror.com/@types/babel__core/-/babel__core-7.20.5.tgz", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
"@types/babel__generator": ["@types/babel__generator@7.27.0", "https://registry.npmmirror.com/@types/babel__generator/-/babel__generator-7.27.0.tgz", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
"@types/babel__template": ["@types/babel__template@7.4.4", "https://registry.npmmirror.com/@types/babel__template/-/babel__template-7.4.4.tgz", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "https://registry.npmmirror.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
"@types/chai": ["@types/chai@5.2.3", "https://registry.npmmirror.com/@types/chai/-/chai-5.2.3.tgz", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
"@types/d3": ["@types/d3@7.4.3", "https://registry.npmmirror.com/@types/d3/-/d3-7.4.3.tgz", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="],
@ -456,7 +472,7 @@
"@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "https://registry.npmmirror.com/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="],
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="],
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="],
"@vitest/expect": ["@vitest/expect@3.2.4", "https://registry.npmmirror.com/@vitest/expect/-/expect-3.2.4.tgz", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="],
@ -494,18 +510,12 @@
"babel-plugin-macros": ["babel-plugin-macros@3.1.0", "https://registry.npmmirror.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", { "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", "resolve": "^1.19.0" } }, "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.14", "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.14.tgz", { "bin": "dist/cli.cjs" }, "sha512-fOVLPAsFTsQfuCkvahZkzq6nf8KvGWanlYoTh0SVA0A/PIUxQGU2AOZAoD95n2gFLVDW/jP6sbGLny95nmEuHA=="],
"browserslist": ["browserslist@4.28.2", "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.2.tgz", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": "cli.js" }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="],
"cac": ["cac@6.7.14", "https://registry.npmmirror.com/cac/-/cac-6.7.14.tgz", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"callsites": ["callsites@3.1.0", "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
"caniuse-lite": ["caniuse-lite@1.0.30001784", "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001784.tgz", {}, "sha512-WU346nBTklUV9YfUl60fqRbU5ZqyXlqvo1SgigE1OAXK5bFL8LL9q1K7aap3N739l4BvNqnkm3YrGHiY9sfUQw=="],
"ccount": ["ccount@2.0.1", "https://registry.npmmirror.com/ccount/-/ccount-2.0.1.tgz", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
"chai": ["chai@5.3.3", "https://registry.npmmirror.com/chai/-/chai-5.3.3.tgz", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="],
@ -536,7 +546,7 @@
"confbox": ["confbox@0.1.8", "https://registry.npmmirror.com/confbox/-/confbox-0.1.8.tgz", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
"convert-source-map": ["convert-source-map@2.0.0", "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"convert-source-map": ["convert-source-map@1.9.0", "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-1.9.0.tgz", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="],
"cose-base": ["cose-base@1.0.3", "https://registry.npmmirror.com/cose-base/-/cose-base-1.0.3.tgz", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="],
@ -652,8 +662,6 @@
"dunder-proto": ["dunder-proto@1.0.1", "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"electron-to-chromium": ["electron-to-chromium@1.5.331", "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.331.tgz", {}, "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q=="],
"enhanced-resolve": ["enhanced-resolve@5.20.1", "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA=="],
"entities": ["entities@6.0.1", "https://registry.npmmirror.com/entities/-/entities-6.0.1.tgz", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
@ -672,8 +680,6 @@
"esbuild": ["esbuild@0.25.12", "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.12.tgz", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": "bin/esbuild" }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
"escalade": ["escalade@3.2.0", "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"escape-string-regexp": ["escape-string-regexp@4.0.0", "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "https://registry.npmmirror.com/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="],
@ -692,8 +698,6 @@
"function-bind": ["function-bind@1.1.2", "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"gensync": ["gensync@1.0.0-beta.2", "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-proto": ["get-proto@1.0.1", "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
@ -762,8 +766,6 @@
"json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "https://registry.npmmirror.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
"json5": ["json5@2.2.3", "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", { "bin": "lib/cli.js" }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"katex": ["katex@0.16.45", "https://registry.npmmirror.com/katex/-/katex-0.16.45.tgz", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA=="],
"khroma": ["khroma@2.1.0", "https://registry.npmmirror.com/khroma/-/khroma-2.1.0.tgz", {}, "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="],
@ -806,7 +808,7 @@
"loupe": ["loupe@3.2.1", "https://registry.npmmirror.com/loupe/-/loupe-3.2.1.tgz", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="],
"lru-cache": ["lru-cache@5.1.1", "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"lru-cache": ["lru-cache@10.4.3", "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"lucide-react": ["lucide-react@0.469.0", "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.469.0.tgz", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw=="],
@ -892,8 +894,6 @@
"nanoid": ["nanoid@3.3.11", "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", { "bin": "bin/nanoid.cjs" }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"node-releases": ["node-releases@2.0.37", "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.37.tgz", {}, "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg=="],
"nwsapi": ["nwsapi@2.2.23", "https://registry.npmmirror.com/nwsapi/-/nwsapi-2.2.23.tgz", {}, "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ=="],
"oniguruma-parser": ["oniguruma-parser@0.12.1", "https://registry.npmmirror.com/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="],
@ -930,7 +930,7 @@
"points-on-path": ["points-on-path@0.2.1", "https://registry.npmmirror.com/points-on-path/-/points-on-path-0.2.1.tgz", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="],
"postcss": ["postcss@8.5.8", "https://registry.npmmirror.com/postcss/-/postcss-8.5.8.tgz", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
"postcss": ["postcss@8.5.13", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag=="],
"postcss-selector-parser": ["postcss-selector-parser@6.0.10", "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="],
@ -950,8 +950,6 @@
"react-is": ["react-is@17.0.2", "https://registry.npmmirror.com/react-is/-/react-is-17.0.2.tgz", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
"react-refresh": ["react-refresh@0.17.0", "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.17.0.tgz", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
"react-shiki": ["react-shiki@0.9.2", "https://registry.npmmirror.com/react-shiki/-/react-shiki-0.9.2.tgz", { "dependencies": { "clsx": "^2.1.1", "dequal": "^2.0.3", "hast-util-to-jsx-runtime": "^2.3.6", "shiki": "^4.0.0", "unist-util-visit": "^5.1.0" }, "peerDependencies": { "@types/react": ">=16.8.0", "@types/react-dom": ">=16.8.0", "react": ">= 16.8.0", "react-dom": ">= 16.8.0" } }, "sha512-TMj2IYm9mWUhWF+OEffdfnaYEpYLr3vs5b8xRcnuRYR4Dkk2TU2zT9RlbSlktl5mLNdiioDnkEqZYzsGYjYQNA=="],
"redent": ["redent@3.0.0", "https://registry.npmmirror.com/redent/-/redent-3.0.0.tgz", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="],
@ -968,6 +966,8 @@
"robust-predicates": ["robust-predicates@3.0.3", "https://registry.npmmirror.com/robust-predicates/-/robust-predicates-3.0.3.tgz", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="],
"rolldown": ["rolldown@1.0.0-rc.17", "", { "dependencies": { "@oxc-project/types": "=0.127.0", "@rolldown/pluginutils": "1.0.0-rc.17" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-x64": "1.0.0-rc.17", "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA=="],
"rollup": ["rollup@4.60.1", "https://registry.npmmirror.com/rollup/-/rollup-4.60.1.tgz", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.1", "@rollup/rollup-android-arm64": "4.60.1", "@rollup/rollup-darwin-arm64": "4.60.1", "@rollup/rollup-darwin-x64": "4.60.1", "@rollup/rollup-freebsd-arm64": "4.60.1", "@rollup/rollup-freebsd-x64": "4.60.1", "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", "@rollup/rollup-linux-arm-musleabihf": "4.60.1", "@rollup/rollup-linux-arm64-gnu": "4.60.1", "@rollup/rollup-linux-arm64-musl": "4.60.1", "@rollup/rollup-linux-loong64-gnu": "4.60.1", "@rollup/rollup-linux-loong64-musl": "4.60.1", "@rollup/rollup-linux-ppc64-gnu": "4.60.1", "@rollup/rollup-linux-ppc64-musl": "4.60.1", "@rollup/rollup-linux-riscv64-gnu": "4.60.1", "@rollup/rollup-linux-riscv64-musl": "4.60.1", "@rollup/rollup-linux-s390x-gnu": "4.60.1", "@rollup/rollup-linux-x64-gnu": "4.60.1", "@rollup/rollup-linux-x64-musl": "4.60.1", "@rollup/rollup-openbsd-x64": "4.60.1", "@rollup/rollup-openharmony-arm64": "4.60.1", "@rollup/rollup-win32-arm64-msvc": "4.60.1", "@rollup/rollup-win32-ia32-msvc": "4.60.1", "@rollup/rollup-win32-x64-gnu": "4.60.1", "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w=="],
"roughjs": ["roughjs@4.6.6", "https://registry.npmmirror.com/roughjs/-/roughjs-4.6.6.tgz", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="],
@ -982,8 +982,6 @@
"scheduler": ["scheduler@0.23.2", "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.2.tgz", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
"semver": ["semver@6.3.1", "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"shiki": ["shiki@4.0.2", "https://registry.npmmirror.com/shiki/-/shiki-4.0.2.tgz", { "dependencies": { "@shikijs/core": "4.0.2", "@shikijs/engine-javascript": "4.0.2", "@shikijs/engine-oniguruma": "4.0.2", "@shikijs/langs": "4.0.2", "@shikijs/themes": "4.0.2", "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ=="],
"siginfo": ["siginfo@2.0.0", "https://registry.npmmirror.com/siginfo/-/siginfo-2.0.0.tgz", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
@ -1022,7 +1020,7 @@
"tinyexec": ["tinyexec@0.3.2", "https://registry.npmmirror.com/tinyexec/-/tinyexec-0.3.2.tgz", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
"tinyglobby": ["tinyglobby@0.2.15", "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
"tinypool": ["tinypool@1.1.1", "https://registry.npmmirror.com/tinypool/-/tinypool-1.1.1.tgz", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="],
@ -1042,6 +1040,8 @@
"ts-dedent": ["ts-dedent@2.2.0", "https://registry.npmmirror.com/ts-dedent/-/ts-dedent-2.2.0.tgz", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"typescript": ["typescript@5.9.3", "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"ufo": ["ufo@1.6.3", "https://registry.npmmirror.com/ufo/-/ufo-1.6.3.tgz", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="],
@ -1056,8 +1056,6 @@
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "https://registry.npmmirror.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
"update-browserslist-db": ["update-browserslist-db@1.2.3", "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
"util-deprecate": ["util-deprecate@1.0.2", "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"uuid": ["uuid@11.1.0", "https://registry.npmmirror.com/uuid/-/uuid-11.1.0.tgz", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="],
@ -1066,7 +1064,7 @@
"vfile-message": ["vfile-message@4.0.3", "https://registry.npmmirror.com/vfile-message/-/vfile-message-4.0.3.tgz", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
"vite": ["vite@6.4.1", "https://registry.npmmirror.com/vite/-/vite-6.4.1.tgz", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx"], "bin": "bin/vite.js" }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
"vite": ["vite@8.0.10", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.10", "rolldown": "1.0.0-rc.17", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw=="],
"vite-node": ["vite-node@3.2.4", "https://registry.npmmirror.com/vite-node/-/vite-node-3.2.4.tgz", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": "vite-node.mjs" }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],
@ -1102,8 +1100,6 @@
"xmlchars": ["xmlchars@2.2.0", "https://registry.npmmirror.com/xmlchars/-/xmlchars-2.2.0.tgz", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="],
"yallist": ["yallist@3.1.1", "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"yaml": ["yaml@2.8.3", "https://registry.npmmirror.com/yaml/-/yaml-2.8.3.tgz", { "bin": "bin.mjs" }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="],
"zustand": ["zustand@5.0.12", "https://registry.npmmirror.com/zustand/-/zustand-5.0.12.tgz", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["immer", "use-sync-external-store"] }, "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g=="],
@ -1112,10 +1108,6 @@
"@antfu/install-pkg/tinyexec": ["tinyexec@1.1.1", "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.1.1.tgz", {}, "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg=="],
"@asamuzakjp/css-color/lru-cache": ["lru-cache@10.4.3", "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"@emotion/babel-plugin/convert-source-map": ["convert-source-map@1.9.0", "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-1.9.0.tgz", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="],
"@emotion/babel-plugin/stylis": ["stylis@4.2.0", "https://registry.npmmirror.com/stylis/-/stylis-4.2.0.tgz", {}, "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="],
"@emotion/cache/stylis": ["stylis@4.2.0", "https://registry.npmmirror.com/stylis/-/stylis-4.2.0.tgz", {}, "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="],
@ -1142,10 +1134,24 @@
"parse-entities/@types/unist": ["@types/unist@2.0.11", "https://registry.npmmirror.com/@types/unist/-/unist-2.0.11.tgz", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
"rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.17", "", {}, "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg=="],
"strip-literal/js-tokens": ["js-tokens@9.0.1", "https://registry.npmmirror.com/js-tokens/-/js-tokens-9.0.1.tgz", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
"vite-node/vite": ["vite@6.4.1", "https://registry.npmmirror.com/vite/-/vite-6.4.1.tgz", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx"], "bin": "bin/vite.js" }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
"vitest/tinyglobby": ["tinyglobby@0.2.15", "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"vitest/vite": ["vite@6.4.1", "https://registry.npmmirror.com/vite/-/vite-6.4.1.tgz", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx"], "bin": "bin/vite.js" }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
"cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "https://registry.npmmirror.com/layout-base/-/layout-base-2.0.1.tgz", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="],
"d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "https://registry.npmmirror.com/d3-path/-/d3-path-1.0.9.tgz", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="],
"vite-node/vite/postcss": ["postcss@8.5.8", "https://registry.npmmirror.com/postcss/-/postcss-8.5.8.tgz", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
"vite-node/vite/tinyglobby": ["tinyglobby@0.2.15", "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"vitest/vite/postcss": ["postcss@8.5.8", "https://registry.npmmirror.com/postcss/-/postcss-8.5.8.tgz", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
}
}

View File

@ -39,11 +39,11 @@
"@testing-library/react": "^16.1.0",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@vitejs/plugin-react": "^4.3.4",
"@vitejs/plugin-react": "^6.0.1",
"jsdom": "^25.0.1",
"tailwindcss": "^4.0.0",
"typescript": "^5.9.3",
"vite": "^6.0.7",
"vite": "^8.0.10",
"vitest": "^3.0.4"
},
"optionalDependencies": {

View File

@ -20,7 +20,6 @@ import { MarkdownRenderer } from '../markdown/MarkdownRenderer'
import {
getFileExtension,
normalizePrismLanguage,
WORKSPACE_PLAIN_TEXT_LINE_THRESHOLD,
WORKSPACE_PREVIEW_LINE_LIMIT,
WorkspaceDiffSurface,
workspacePrismTheme,
@ -321,7 +320,7 @@ function CodeSurface({
const lines = value.split('\n')
const visibleLines = showAllLines ? lines : lines.slice(0, WORKSPACE_PREVIEW_LINE_LIMIT)
const activeQuote = commentLine ? visibleLines[commentLine - 1] ?? '' : ''
const usePlainLargePreview = showAllLines && lines.length > WORKSPACE_PLAIN_TEXT_LINE_THRESHOLD
const usePlainLargePreview = showAllLines && lines.length > WORKSPACE_PREVIEW_LINE_LIMIT
const visibleCode = usePlainLargePreview ? '' : visibleLines.join('\n')
useEffect(() => {

View File

@ -20,13 +20,19 @@ import {
import { getOauthProfileFromOauthToken } from '../../services/oauth/getOauthProfile.js'
import { OAuthService } from '../../services/oauth/index.js'
import type { OAuthTokens } from '../../services/oauth/types.js'
import { OpenAIOAuthService } from '../../services/openaiAuth/index.js'
import { getOpenAIOAuthTokens } from '../../services/openaiAuth/storage.js'
import type { OpenAIOAuthTokens } from '../../services/openaiAuth/types.js'
import {
clearStoredClaudeAIOAuthTokens,
clearOAuthTokenCache,
getAnthropicApiKeyWithSource,
getAuthTokenSource,
getOpenAIAuthOverrideWarning,
getOauthAccountInfo,
getSubscriptionType,
isUsing3PServices,
removeApiKey,
saveOAuthTokensIfNeeded,
validateForceLoginOrg,
} from '../../utils/auth.js'
@ -109,17 +115,75 @@ export async function installOAuthTokens(tokens: OAuthTokens): Promise<void> {
await clearAuthRelatedCaches()
}
export async function installOpenAIOAuthTokens(
tokens: OpenAIOAuthTokens,
): Promise<string | null> {
await removeApiKey()
const clearClaudeOauth = clearStoredClaudeAIOAuthTokens()
if (!clearClaudeOauth.success) {
throw new Error(
clearClaudeOauth.warning ?? 'Failed to disable existing Claude auth state',
)
}
saveGlobalConfig(current => ({
...current,
oauthAccount: undefined,
}))
if (!tokens.refreshToken) {
throw new Error('OpenAI OAuth tokens are incomplete.')
}
await clearAuthRelatedCaches()
return getOpenAIAuthOverrideWarning()
}
export async function authLogin({
email,
sso,
console: useConsole,
claudeai,
openai,
}: {
email?: string
sso?: boolean
console?: boolean
claudeai?: boolean
openai?: boolean
}): Promise<void> {
if (openai) {
if (email || sso || useConsole || claudeai) {
process.stderr.write(
'Error: --openai cannot be combined with --email, --sso, --console, or --claudeai.\n',
)
process.exit(1)
}
const openaiOAuthService = new OpenAIOAuthService()
try {
const tokens = await openaiOAuthService.startOAuthFlow(async url => {
process.stdout.write('Opening browser to sign in to OpenAI…\n')
process.stdout.write(`If the browser did not open, visit: ${url}\n`)
})
const warning = await installOpenAIOAuthTokens(tokens)
process.stdout.write('OpenAI login successful.\n')
if (warning) {
process.stdout.write(`${warning}\n`)
}
process.exit(0)
} catch (err) {
logError(err)
process.stderr.write(`OpenAI login failed: ${errorMessage(err)}\n`)
process.exit(1)
} finally {
openaiOAuthService.cleanup()
}
}
if (useConsole && claudeai) {
process.stderr.write(
'Error: --console and --claudeai cannot be used together.\n',
@ -232,7 +296,52 @@ export async function authLogin({
export async function authStatus(opts: {
json?: boolean
text?: boolean
openai?: boolean
}): Promise<void> {
if (opts.openai) {
const openaiTokens = getOpenAIOAuthTokens()
const loggedIn =
!!openaiTokens &&
openaiTokens.refreshToken.length > 0 &&
openaiTokens.expiresAt > 0
if (opts.text) {
if (!loggedIn) {
process.stdout.write(
'Not logged in to OpenAI. Run claude auth login --openai to authenticate.\n',
)
} else {
process.stdout.write('Provider: openai\n')
if (openaiTokens.email) {
process.stdout.write(`Email: ${openaiTokens.email}\n`)
}
if (openaiTokens.accountId) {
process.stdout.write(`Account ID: ${openaiTokens.accountId}\n`)
}
process.stdout.write(
`Expires At: ${new Date(openaiTokens.expiresAt).toISOString()}\n`,
)
}
} else {
process.stdout.write(
jsonStringify(
{
loggedIn,
authMethod: loggedIn ? 'openai_oauth' : 'none',
provider: 'openai',
email: openaiTokens?.email ?? null,
accountId: openaiTokens?.accountId ?? null,
expiresAt: openaiTokens?.expiresAt ?? null,
},
null,
2,
) + '\n',
)
}
process.exit(loggedIn ? 0 : 1)
}
const { source: authTokenSource, hasToken } = getAuthTokenSource()
const { source: apiKeySource } = getAnthropicApiKeyWithSource()
const hasApiKeyEnvVar =
@ -240,12 +349,25 @@ export async function authStatus(opts: {
const oauthAccount = getOauthAccountInfo()
const subscriptionType = getSubscriptionType()
const using3P = isUsing3PServices()
const openaiTokens = getOpenAIOAuthTokens()
const usingOpenAI =
!!openaiTokens?.refreshToken &&
!hasToken &&
apiKeySource === 'none' &&
!hasApiKeyEnvVar &&
!using3P
const loggedIn =
hasToken || apiKeySource !== 'none' || hasApiKeyEnvVar || using3P
hasToken ||
apiKeySource !== 'none' ||
hasApiKeyEnvVar ||
using3P ||
usingOpenAI
// Determine auth method
let authMethod: string = 'none'
if (using3P) {
if (usingOpenAI) {
authMethod = 'openai_oauth'
} else if (using3P) {
authMethod = 'third_party'
} else if (authTokenSource === 'claude.ai') {
authMethod = 'claude.ai'
@ -311,6 +433,11 @@ export async function authStatus(opts: {
output.orgId = oauthAccount?.organizationUuid ?? null
output.orgName = oauthAccount?.organizationName ?? null
output.subscriptionType = subscriptionType ?? null
} else if (authMethod === 'openai_oauth') {
output.provider = 'openai'
output.email = openaiTokens?.email ?? null
output.accountId = openaiTokens?.accountId ?? null
output.subscriptionType = 'ChatGPT Pro/Plus'
}
process.stdout.write(jsonStringify(output, null, 2) + '\n')
@ -318,7 +445,18 @@ export async function authStatus(opts: {
process.exit(loggedIn ? 0 : 1)
}
export async function authLogout(): Promise<void> {
export async function authLogout(opts?: { openai?: boolean }): Promise<void> {
if (opts?.openai) {
const openaiOAuthService = new OpenAIOAuthService()
const success = openaiOAuthService.logout()
if (!success) {
process.stderr.write('Failed to log out from OpenAI.\n')
process.exit(1)
}
process.stdout.write('Successfully logged out from your OpenAI account.\n')
process.exit(0)
}
try {
await performLogout({ clearOnboarding: false })
} catch {

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,286 @@
import React, { useCallback, useEffect, useRef, useState } from 'react'
import {
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
logEvent,
} from 'src/services/analytics/index.js'
import { installOpenAIOAuthTokens } from '../cli/handlers/auth.js'
import { useTerminalSize } from '../hooks/useTerminalSize.js'
import { setClipboard } from '../ink/termio/osc.js'
import { useTerminalNotification } from '../ink/useTerminalNotification.js'
import { Box, Link, Text } from '../ink.js'
import { useKeybinding } from '../keybindings/useKeybinding.js'
import { getSSLErrorHint } from '../services/api/errorUtils.js'
import { sendNotification } from '../services/notifier.js'
import { OpenAIOAuthService } from '../services/openaiAuth/index.js'
import { getOpenAIOAuthTokens } from '../services/openaiAuth/storage.js'
import { logError } from '../utils/log.js'
import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js'
import { Spinner } from './Spinner.js'
import TextInput from './TextInput.js'
type Props = {
onDone(): void
startingMessage?: string
}
type OpenAILoginStatus =
| { state: 'ready_to_start' }
| { state: 'waiting_for_login'; url: string }
| { state: 'success'; warning?: string | null }
| { state: 'error'; message: string; toRetry?: OpenAILoginStatus }
| { state: 'about_to_retry'; nextState: OpenAILoginStatus }
const PASTE_HERE_MSG = 'Paste code here if prompted > '
export function OpenAILoginFlow({
onDone,
startingMessage,
}: Props): React.ReactNode {
const [oauthService] = useState(() => new OpenAIOAuthService())
const [status, setStatus] = useState<OpenAILoginStatus>({
state: 'ready_to_start',
})
// After a few seconds we suggest the user to copy/paste url if the
// browser did not open automatically.
const [showPastePrompt, setShowPastePrompt] = useState(false)
const [urlCopied, setUrlCopied] = useState(false)
const [pastedCode, setPastedCode] = useState('')
const [cursorOffset, setCursorOffset] = useState(0)
const terminal = useTerminalNotification()
const textInputColumns = useTerminalSize().columns - PASTE_HERE_MSG.length - 1
// Handle Enter to continue on success state
useKeybinding(
'confirm:yes',
() => {
logEvent('tengu_openai_oauth_success', {})
onDone()
},
{
context: 'Confirmation',
isActive: status.state === 'success',
},
)
// Handle Enter to retry on error state
useKeybinding(
'confirm:yes',
() => {
if (status.state === 'error' && status.toRetry) {
setPastedCode('')
setStatus({
state: 'about_to_retry',
nextState: status.toRetry,
})
}
},
{
context: 'Confirmation',
isActive: status.state === 'error' && !!status.toRetry,
},
)
// Clipboard copy: type 'c' to copy the auth URL
useEffect(() => {
if (
pastedCode === 'c' &&
status.state === 'waiting_for_login' &&
showPastePrompt &&
!urlCopied
) {
void setClipboard(status.url).then(raw => {
if (raw) process.stdout.write(raw)
setUrlCopied(true)
setTimeout(setUrlCopied, 2000, false)
})
setPastedCode('')
}
}, [pastedCode, status, showPastePrompt, urlCopied])
// Retry logic
useEffect(() => {
if (status.state === 'about_to_retry') {
const timer = setTimeout(setStatus, 1000, status.nextState)
return () => clearTimeout(timer)
}
}, [status])
const handleSubmitCode = useCallback(
async (value: string, url: string) => {
try {
const [authorizationCode, state] = value.split('#')
if (!authorizationCode || !state) {
setStatus({
state: 'error',
message: 'Invalid code. Please make sure the full code was copied',
toRetry: { state: 'waiting_for_login', url },
})
return
}
logEvent('tengu_openai_oauth_manual_entry', {})
oauthService.handleManualAuthCodeInput({
authorizationCode,
state,
})
} catch (err: unknown) {
logError(err)
setStatus({
state: 'error',
message: (err as Error).message,
toRetry: { state: 'waiting_for_login', url },
})
}
},
[oauthService],
)
const startOAuth = useCallback(async () => {
try {
logEvent('tengu_openai_oauth_flow_start', {})
const tokens = await oauthService.startOAuthFlow(
async url => {
setStatus({ state: 'waiting_for_login', url })
setTimeout(setShowPastePrompt, 3000, true)
},
)
const warning = await installOpenAIOAuthTokens(tokens)
setStatus({ state: 'success', warning })
void sendNotification(
{
message: 'Claude Code Haha OpenAI login successful',
notificationType: 'auth_success',
},
terminal,
)
} catch (error) {
const errorMessage = (error as Error).message
const sslHint = getSSLErrorHint(error)
logError(error)
setStatus({
state: 'error',
message: sslHint ?? errorMessage,
toRetry: { state: 'ready_to_start' },
})
logEvent('tengu_openai_oauth_error', {
error: errorMessage as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
ssl_error: sslHint !== null,
})
}
}, [oauthService, terminal])
const pendingOAuthStartRef = useRef(false)
useEffect(() => {
if (
status.state === 'ready_to_start' &&
!pendingOAuthStartRef.current
) {
pendingOAuthStartRef.current = true
process.nextTick(() => {
void startOAuth()
pendingOAuthStartRef.current = false
})
}
}, [status.state, startOAuth])
// Cleanup OAuth service when component unmounts
useEffect(() => {
return () => {
oauthService.cleanup()
}
}, [oauthService])
const account = getOpenAIOAuthTokens()
return (
<Box flexDirection="column" gap={1}>
{status.state === 'waiting_for_login' && showPastePrompt && (
<Box flexDirection="column" key="urlToCopy" gap={1} paddingBottom={1}>
<Box paddingX={1}>
<Text dimColor>
Browser didn&apos;t open? Use the url below to sign in{' '}
</Text>
{urlCopied ? (
<Text color="success">(Copied!)</Text>
) : (
<Text dimColor>
<KeyboardShortcutHint shortcut="c" action="copy" parens />
</Text>
)}
</Box>
<Link url={status.url}>
<Text dimColor>{status.url}</Text>
</Link>
</Box>
)}
<Box flexDirection="column" gap={1}>
<Text bold>
{startingMessage ??
'Claude Code can also run through your ChatGPT subscription via OpenAI auth.'}
</Text>
{status.state === 'waiting_for_login' && !showPastePrompt && (
<Box>
<Spinner />
<Text>Opening browser to sign in to OpenAI</Text>
</Box>
)}
{status.state === 'waiting_for_login' && showPastePrompt && (
<Box>
<Text>{PASTE_HERE_MSG}</Text>
<TextInput
value={pastedCode}
onChange={setPastedCode}
onSubmit={(value: string) =>
handleSubmitCode(value, status.url)
}
cursorOffset={cursorOffset}
onChangeCursorOffset={setCursorOffset}
columns={textInputColumns}
mask="*"
/>
</Box>
)}
{status.state === 'about_to_retry' && (
<Box flexDirection="column" gap={1}>
<Text color="permission">Retrying</Text>
</Box>
)}
{status.state === 'success' && (
<Box flexDirection="column">
{account?.email ? (
<Text dimColor>
Logged in as <Text>{account.email}</Text>
</Text>
) : null}
{status.warning ? (
<Text color="warning">{status.warning}</Text>
) : null}
<Text color="success">
Login successful. Press <Text bold>Enter</Text> to continue
</Text>
</Box>
)}
{status.state === 'error' && (
<Box flexDirection="column" gap={1}>
<Text color="error">OpenAI OAuth error: {status.message}</Text>
{status.toRetry && (
<Box marginTop={1}>
<Text color="permission">
Press <Text bold>Enter</Text> to retry.
</Text>
</Box>
)}
</Box>
)}
</Box>
</Box>
)
}

View File

@ -0,0 +1,157 @@
import React, { useMemo, useState } from 'react'
import { Box, Link, Text } from '../ink.js'
import { useKeybinding } from '../keybindings/useKeybinding.js'
import { getSettings_DEPRECATED } from '../utils/settings/settings.js'
import { ConsoleOAuthFlow } from './ConsoleOAuthFlow.js'
import { Select } from './CustomSelect/select.js'
import { OpenAILoginFlow } from './OpenAILoginFlow.js'
type Props = {
onDone(): void
startingMessage?: string
}
type LoginSelection = 'claudeai' | 'console' | 'openai' | 'platform' | 'idle'
function PlatformSetupFlow({
onBack,
}: {
onBack(): void
}): React.ReactNode {
useKeybinding(
'confirm:yes',
() => {
onBack()
},
{
context: 'Confirmation',
isActive: true,
},
)
return (
<Box flexDirection="column" gap={1} marginTop={1}>
<Text bold>Using 3rd-party platforms</Text>
<Text>
Claude Code supports Amazon Bedrock, Microsoft Foundry, and Vertex AI.
Set the required environment variables, then restart Claude Code.
</Text>
<Text>
If you are part of an enterprise organization, contact your
administrator for setup instructions.
</Text>
<Box flexDirection="column">
<Text bold>Documentation:</Text>
<Text>
· Amazon Bedrock:{' '}
<Link url="https://code.claude.com/docs/en/amazon-bedrock" />
</Text>
<Text>
· Microsoft Foundry:{' '}
<Link url="https://code.claude.com/docs/en/microsoft-foundry" />
</Text>
<Text>
· Vertex AI:{' '}
<Link url="https://code.claude.com/docs/en/google-vertex-ai" />
</Text>
</Box>
<Text dimColor>
Press <Text bold>Enter</Text> to go back to login options.
</Text>
</Box>
)
}
export function SlashLoginFlow({
onDone,
startingMessage,
}: Props): React.ReactNode {
const settings = getSettings_DEPRECATED() || {}
const forceLoginMethod = settings.forceLoginMethod
const [selection, setSelection] = useState<LoginSelection>(() => {
if (forceLoginMethod === 'claudeai' || forceLoginMethod === 'console') {
return forceLoginMethod
}
return 'idle'
})
const options = useMemo(
() => [
{
label: (
<Text>
Claude account with subscription ·{' '}
<Text dimColor>Pro, Max, Team, or Enterprise</Text>
{'\n'}
</Text>
),
value: 'claudeai' as const,
},
{
label: (
<Text>
Anthropic Console account ·{' '}
<Text dimColor>API usage billing</Text>
{'\n'}
</Text>
),
value: 'console' as const,
},
{
label: (
<Text>
OpenAI account · <Text dimColor>ChatGPT Pro/Plus</Text>
{'\n'}
</Text>
),
value: 'openai' as const,
},
{
label: (
<Text>
3rd-party platform ·{' '}
<Text dimColor>Amazon Bedrock, Microsoft Foundry, or Vertex AI</Text>
{'\n'}
</Text>
),
value: 'platform' as const,
},
],
[],
)
if (selection === 'claudeai' || selection === 'console') {
return (
<ConsoleOAuthFlow
onDone={onDone}
startingMessage={startingMessage}
forceLoginMethod={selection}
/>
)
}
if (selection === 'openai') {
return <OpenAILoginFlow onDone={onDone} startingMessage={startingMessage} />
}
if (selection === 'platform') {
return <PlatformSetupFlow onBack={() => setSelection('idle')} />
}
return (
<Box flexDirection="column" gap={1} marginTop={1}>
<Text bold>
{startingMessage ??
'Claude Code can be used with your Claude subscription, Anthropic Console billing, or your ChatGPT subscription.'}
</Text>
<Text>Select login method:</Text>
<Box>
<Select
options={options}
onChange={value => setSelection(value as LoginSelection)}
/>
</Box>
</Box>
)
}

View File

@ -6,6 +6,7 @@ import {
getApiKeyFromApiKeyHelper,
isAnthropicAuthEnabled,
isClaudeAISubscriber,
isOpenAIAuthActive,
} from '../utils/auth.js'
export type VerificationStatus =
@ -23,7 +24,7 @@ export type ApiKeyVerificationResult = {
export function useApiKeyVerification(): ApiKeyVerificationResult {
const [status, setStatus] = useState<VerificationStatus>(() => {
if (!isAnthropicAuthEnabled() || isClaudeAISubscriber()) {
if (!isAnthropicAuthEnabled() || isClaudeAISubscriber() || isOpenAIAuthActive()) {
return 'valid'
}
// Use skipRetrievingKeyFromApiKeyHelper to avoid executing apiKeyHelper
@ -41,7 +42,7 @@ export function useApiKeyVerification(): ApiKeyVerificationResult {
const [error, setError] = useState<Error | null>(null)
const verify = useCallback(async (): Promise<void> => {
if (!isAnthropicAuthEnabled() || isClaudeAISubscriber()) {
if (!isAnthropicAuthEnabled() || isClaudeAISubscriber() || isOpenAIAuthActive()) {
setStatus('valid')
return
}

View File

@ -4141,16 +4141,18 @@ async function run(): Promise<CommanderCommand> {
// claude auth
const auth = program.command('auth').description('Manage authentication').configureHelp(createSortedHelpConfig());
auth.command('login').description('Sign in to your Anthropic account').option('--email <email>', 'Pre-populate email address on the login page').option('--sso', 'Force SSO login flow').option('--console', 'Use Anthropic Console (API usage billing) instead of Claude subscription').option('--claudeai', 'Use Claude subscription (default)').action(async ({
auth.command('login').description('Sign in to your Anthropic or OpenAI account').option('--email <email>', 'Pre-populate email address on the login page').option('--sso', 'Force SSO login flow').option('--console', 'Use Anthropic Console (API usage billing) instead of Claude subscription').option('--claudeai', 'Use Claude subscription (default)').option('--openai', 'Use OpenAI ChatGPT/Codex OAuth login').action(async ({
email,
sso,
console: useConsole,
claudeai
claudeai,
openai,
}: {
email?: string;
sso?: boolean;
console?: boolean;
claudeai?: boolean;
openai?: boolean;
}) => {
const {
authLogin
@ -4159,23 +4161,27 @@ async function run(): Promise<CommanderCommand> {
email,
sso,
console: useConsole,
claudeai
claudeai,
openai,
});
});
auth.command('status').description('Show authentication status').option('--json', 'Output as JSON (default)').option('--text', 'Output as human-readable text').action(async (opts: {
auth.command('status').description('Show authentication status').option('--json', 'Output as JSON (default)').option('--text', 'Output as human-readable text').option('--openai', 'Show OpenAI OAuth status').action(async (opts: {
json?: boolean;
text?: boolean;
openai?: boolean;
}) => {
const {
authStatus
} = await import('./cli/handlers/auth.js');
await authStatus(opts);
});
auth.command('logout').description('Log out from your Anthropic account').action(async () => {
auth.command('logout').description('Log out from your Anthropic or OpenAI account').option('--openai', 'Log out from OpenAI OAuth only').action(async (opts: {
openai?: boolean;
}) => {
const {
authLogout
} = await import('./cli/handlers/auth.js');
await authLogout();
await authLogout(opts);
});
/**

View File

@ -0,0 +1,159 @@
/**
* Integration tests for /api/haha-openai-oauth/* endpoints.
*/
import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
import * as fs from 'fs/promises'
import * as path from 'path'
import * as os from 'os'
import { handleHahaOpenAIOAuthApi } from '../api/haha-openai-oauth.js'
import { hahaOpenAIOAuthService } from '../services/hahaOpenAIOAuthService.js'
let tmpDir: string
let originalConfigDir: string | undefined
async function setup() {
tmpDir = await fs.mkdtemp(
path.join(os.tmpdir(), 'haha-openai-oauth-api-test-'),
)
originalConfigDir = process.env.CLAUDE_CONFIG_DIR
process.env.CLAUDE_CONFIG_DIR = tmpDir
}
async function teardown() {
if (originalConfigDir === undefined) {
delete process.env.CLAUDE_CONFIG_DIR
} else {
process.env.CLAUDE_CONFIG_DIR = originalConfigDir
}
await fs.rm(tmpDir, { recursive: true, force: true })
}
function buildReq(
method: string,
pathname: string,
body?: unknown,
): { req: Request; url: URL; segments: string[] } {
const url = new URL(`http://localhost:3456${pathname}`)
const req = new Request(url.toString(), {
method,
headers: body ? { 'Content-Type': 'application/json' } : undefined,
body: body ? JSON.stringify(body) : undefined,
})
const segments = url.pathname.split('/').filter(Boolean)
return { req, url, segments }
}
describe('POST /api/haha-openai-oauth/start', () => {
beforeEach(setup)
afterEach(teardown)
test('returns authorize URL with PKCE challenge', async () => {
const { req, url, segments } = buildReq(
'POST',
'/api/haha-openai-oauth/start',
{ serverPort: 54321 },
)
const res = await handleHahaOpenAIOAuthApi(req, url, segments)
expect(res.status).toBe(200)
const data = (await res.json()) as { authorizeUrl: string; state: string }
expect(data.authorizeUrl).toContain('code_challenge_method=S256')
expect(data.authorizeUrl).toContain(
'codex_cli_simplified_flow=true',
)
expect(data.authorizeUrl).toContain(
encodeURIComponent('http://localhost:54321/auth/callback'),
)
expect(data.state).toMatch(/^[A-Za-z0-9_-]+$/)
})
test('400 if serverPort missing', async () => {
const { req, url, segments } = buildReq(
'POST',
'/api/haha-openai-oauth/start',
{},
)
const res = await handleHahaOpenAIOAuthApi(req, url, segments)
expect(res.status).toBe(400)
const body = (await res.json()) as { error: string; message?: string }
expect(body.error).toBe('BAD_REQUEST')
})
})
describe('GET /api/haha-openai-oauth', () => {
beforeEach(setup)
afterEach(teardown)
test('returns loggedIn=false when no token file', async () => {
const { req, url, segments } = buildReq('GET', '/api/haha-openai-oauth')
const res = await handleHahaOpenAIOAuthApi(req, url, segments)
expect(res.status).toBe(200)
const data = (await res.json()) as { loggedIn: boolean }
expect(data.loggedIn).toBe(false)
})
test('returns loggedIn=true + metadata when token saved', async () => {
await hahaOpenAIOAuthService.saveTokens({
accessToken: 'openai-access-token-xxx',
refreshToken: 'openai-refresh-token-xxx',
expiresAt: Date.now() + 3600_000,
email: 'test@example.com',
accountId: 'acct_123',
})
const { req, url, segments } = buildReq('GET', '/api/haha-openai-oauth')
const res = await handleHahaOpenAIOAuthApi(req, url, segments)
expect(res.status).toBe(200)
const data = (await res.json()) as {
loggedIn: boolean
expiresAt: number | null
email: string | null
accountId: string | null
}
expect(data.loggedIn).toBe(true)
expect(data.email).toBe('test@example.com')
expect(data.accountId).toBe('acct_123')
// Never leak token values
expect(JSON.stringify(data)).not.toContain('openai-access-token')
expect(JSON.stringify(data)).not.toContain('openai-refresh-token')
})
test('returns loggedIn=false when stored token is expired and refresh fails', async () => {
await hahaOpenAIOAuthService.saveTokens({
accessToken: 'expired-token',
refreshToken: 'revoked-refresh-token',
expiresAt: Date.now() - 1_000,
email: 'test@example.com',
accountId: 'acct_123',
})
hahaOpenAIOAuthService.setRefreshFn(async () => {
throw new Error('refresh revoked')
})
const { req, url, segments } = buildReq('GET', '/api/haha-openai-oauth')
const res = await handleHahaOpenAIOAuthApi(req, url, segments)
expect(res.status).toBe(200)
expect(await res.json()).toEqual({ loggedIn: false })
})
})
describe('DELETE /api/haha-openai-oauth', () => {
beforeEach(setup)
afterEach(teardown)
test('clears token file', async () => {
await hahaOpenAIOAuthService.saveTokens({
accessToken: 'a',
refreshToken: null,
expiresAt: null,
email: null,
accountId: null,
})
const { req, url, segments } = buildReq('DELETE', '/api/haha-openai-oauth')
const res = await handleHahaOpenAIOAuthApi(req, url, segments)
expect(res.status).toBe(200)
expect(await hahaOpenAIOAuthService.loadTokens()).toBeNull()
})
})

View File

@ -0,0 +1,184 @@
/**
* Unit tests for HahaOpenAIOAuthService haha OpenAI OAuth service
*/
import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
import * as fs from 'fs/promises'
import * as path from 'path'
import * as os from 'os'
import {
HahaOpenAIOAuthService,
type StoredOpenAIOAuthTokens,
} from '../services/hahaOpenAIOAuthService.js'
let tmpDir: string
let originalConfigDir: string | undefined
let service: HahaOpenAIOAuthService
async function setup() {
tmpDir = await fs.mkdtemp(
path.join(os.tmpdir(), 'haha-openai-oauth-test-'),
)
originalConfigDir = process.env.CLAUDE_CONFIG_DIR
process.env.CLAUDE_CONFIG_DIR = tmpDir
service = new HahaOpenAIOAuthService()
}
async function teardown() {
if (originalConfigDir === undefined) {
delete process.env.CLAUDE_CONFIG_DIR
} else {
process.env.CLAUDE_CONFIG_DIR = originalConfigDir
}
await fs.rm(tmpDir, { recursive: true, force: true })
}
describe('HahaOpenAIOAuthService — file storage', () => {
beforeEach(setup)
afterEach(teardown)
test('loadTokens returns null when file does not exist', async () => {
expect(await service.loadTokens()).toBeNull()
})
test('saveTokens writes file with 0600 permissions', async () => {
const tokens: StoredOpenAIOAuthTokens = {
accessToken: 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.mock-access',
refreshToken: 'eyJhbGciOiJSUzI1NiJ9.mock-refresh',
expiresAt: Date.now() + 3600_000,
email: 'test@example.com',
accountId: 'acct_123',
}
await service.saveTokens(tokens)
const oauthPath = path.join(tmpDir, 'cc-haha', 'openai-oauth.json')
const stat = await fs.stat(oauthPath)
if (process.platform !== 'win32') {
expect(stat.mode & 0o777).toBe(0o600)
}
const loaded = await service.loadTokens()
expect(loaded).toEqual(tokens)
})
test('deleteTokens removes file', async () => {
await service.saveTokens({
accessToken: 'a',
refreshToken: null,
expiresAt: null,
email: null,
accountId: null,
})
await service.deleteTokens()
expect(await service.loadTokens()).toBeNull()
})
})
describe('HahaOpenAIOAuthService — session management', () => {
beforeEach(setup)
afterEach(teardown)
test('startSession creates session with PKCE + state', () => {
const session = service.startSession({ serverPort: 54321 })
expect(session.state).toMatch(/^[A-Za-z0-9_-]{43}$/)
expect(session.codeVerifier).toMatch(/^[A-Za-z0-9_-]{43}$/)
expect(session.authorizeUrl).toContain('code_challenge_method=S256')
expect(session.authorizeUrl).toContain(
`state=${encodeURIComponent(session.state)}`,
)
expect(session.authorizeUrl).toContain(
'codex_cli_simplified_flow=true',
)
expect(session.authorizeUrl).toContain(
encodeURIComponent('http://localhost:54321/auth/callback'),
)
})
test('getSession returns stored session by state', () => {
const session = service.startSession({ serverPort: 54321 })
const found = service.getSession(session.state)
expect(found?.codeVerifier).toBe(session.codeVerifier)
})
test('getSession returns null for unknown state', () => {
expect(service.getSession('unknown-state')).toBeNull()
})
test('consumeSession removes session after fetch', () => {
const session = service.startSession({ serverPort: 54321 })
expect(service.consumeSession(session.state)).not.toBeNull()
expect(service.getSession(session.state)).toBeNull()
})
})
describe('HahaOpenAIOAuthService — ensureFreshAccessToken', () => {
beforeEach(setup)
afterEach(teardown)
test('returns null when no token file exists', async () => {
expect(await service.ensureFreshAccessToken()).toBeNull()
})
test('returns token unchanged if not expired', async () => {
const tokens: StoredOpenAIOAuthTokens = {
accessToken: 'still-valid',
refreshToken: 'refresh-xxx',
expiresAt: Date.now() + 30 * 60_000,
email: 'test@example.com',
accountId: 'acct_123',
}
await service.saveTokens(tokens)
expect(await service.ensureFreshAccessToken()).toBe('still-valid')
})
test('returns null when tokens expired and no refresh token', async () => {
await service.saveTokens({
accessToken: 'expired',
refreshToken: null,
expiresAt: Date.now() - 1_000,
email: null,
accountId: null,
})
expect(await service.ensureFreshAccessToken()).toBeNull()
})
test('refreshes token when expired (within 5-min buffer)', async () => {
await service.saveTokens({
accessToken: 'expired',
refreshToken: 'refresh-xxx',
expiresAt: Date.now() + 60_000,
email: 'test@example.com',
accountId: 'acct_123',
})
service.setRefreshFn(async () => ({
access_token: 'new-fresh-token',
refresh_token: 'new-refresh-xxx',
expires_in: 3600,
id_token: 'mock-id-token',
}))
const fresh = await service.ensureFreshAccessToken()
expect(fresh).toBe('new-fresh-token')
const loaded = await service.loadTokens()
expect(loaded?.accessToken).toBe('new-fresh-token')
})
test('returns null when refresh fails', async () => {
await service.saveTokens({
accessToken: 'expired',
refreshToken: 'bad-refresh',
expiresAt: Date.now() + 60_000,
email: null,
accountId: null,
})
service.setRefreshFn(async () => {
throw new Error('401 Unauthorized')
})
expect(await service.ensureFreshAccessToken()).toBeNull()
})
})

View File

@ -281,6 +281,12 @@ describe('openaiResponsesStreamToAnthropic', () => {
const texts = textDeltas.map((e) => (e.data.delta as Record<string, unknown>).text)
expect(texts).toContain('Hello')
expect(texts).toContain(' world')
const msgDelta = events.find((e) => e.event === 'message_delta')!
expect(msgDelta.data.usage).toEqual({
input_tokens: 10,
output_tokens: 5,
})
})
test('function call streaming', async () => {

View File

@ -303,10 +303,33 @@ describe('anthropicToOpenaiResponses', () => {
const result = anthropicToOpenaiResponses(req)
expect(result.model).toBe('gpt-4o')
expect(result.instructions).toBe('Be helpful')
expect(result.store).toBe(false)
expect(result.tools).toBeUndefined()
expect(result.max_output_tokens).toBeUndefined()
expect(result.input).toEqual([{ type: 'message', role: 'user', content: 'Hello' }])
})
test('tools conversion uses top-level name', () => {
const req: AnthropicRequest = {
model: 'gpt-4o',
max_tokens: 100,
messages: [{ role: 'user', content: 'Hi' }],
tools: [{
name: 'get_weather',
description: 'Get weather',
input_schema: { type: 'object', properties: { city: { type: 'string' } } },
}],
}
const result = anthropicToOpenaiResponses(req)
expect(result.tools).toHaveLength(1)
expect(result.tools![0]).toEqual({
type: 'function',
name: 'get_weather',
description: 'Get weather',
parameters: { type: 'object', properties: { city: { type: 'string' } } },
})
})
test('tool_use lifted to function_call', () => {
const req: AnthropicRequest = {
model: 'gpt-4o',

View File

@ -11,6 +11,16 @@ import { handleSettingsApi } from '../api/settings.js'
import { handleModelsApi } from '../api/models.js'
import { handleStatusApi, resetUsage, addUsage } from '../api/status.js'
import { ProviderService } from '../services/providerService.js'
import {
clearOpenAIOAuthTokenCache,
} from '../../services/openaiAuth/storage.js'
import { plainTextStorage } from '../../utils/secureStorage/plainTextStorage.js'
import {
clearKeychainCache,
primeKeychainCacheFromPrefetch,
} from '../../utils/secureStorage/macOsKeychainHelpers.js'
import type { OpenAIOAuthTokens } from '../../services/openaiAuth/types.js'
import { getModelOptions } from '../../utils/model/modelOptions.js'
// ─── Test helpers ─────────────────────────────────────────────────────────────
@ -21,6 +31,12 @@ let originalUserProfile: string | undefined
let originalShell: string | undefined
let originalPath: string | undefined
let originalCliPath: string | undefined
let originalAnthropicApiKey: string | undefined
let originalAnthropicBaseUrl: string | undefined
let originalAnthropicModel: string | undefined
let originalAnthropicDefaultHaikuModel: string | undefined
let originalAnthropicDefaultSonnetModel: string | undefined
let originalAnthropicDefaultOpusModel: string | undefined
async function setup() {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-test-'))
@ -30,14 +46,33 @@ async function setup() {
originalShell = process.env.SHELL
originalPath = process.env.PATH
originalCliPath = process.env.CLAUDE_CLI_PATH
originalAnthropicApiKey = process.env.ANTHROPIC_API_KEY
originalAnthropicBaseUrl = process.env.ANTHROPIC_BASE_URL
originalAnthropicModel = process.env.ANTHROPIC_MODEL
originalAnthropicDefaultHaikuModel = process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL
originalAnthropicDefaultSonnetModel = process.env.ANTHROPIC_DEFAULT_SONNET_MODEL
originalAnthropicDefaultOpusModel = process.env.ANTHROPIC_DEFAULT_OPUS_MODEL
process.env.CLAUDE_CONFIG_DIR = tmpDir
process.env.HOME = tmpDir
process.env.USERPROFILE = tmpDir
process.env.SHELL = '/bin/zsh'
process.env.PATH = ''
delete process.env.ANTHROPIC_API_KEY
delete process.env.ANTHROPIC_BASE_URL
delete process.env.ANTHROPIC_MODEL
delete process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL
delete process.env.ANTHROPIC_DEFAULT_SONNET_MODEL
delete process.env.ANTHROPIC_DEFAULT_OPUS_MODEL
clearKeychainCache()
primeKeychainCacheFromPrefetch(null)
clearOpenAIOAuthTokenCache()
}
async function teardown() {
plainTextStorage.delete()
clearKeychainCache()
clearOpenAIOAuthTokenCache()
if (originalConfigDir !== undefined) {
process.env.CLAUDE_CONFIG_DIR = originalConfigDir
} else {
@ -74,9 +109,50 @@ async function teardown() {
delete process.env.CLAUDE_CLI_PATH
}
if (originalAnthropicApiKey !== undefined) {
process.env.ANTHROPIC_API_KEY = originalAnthropicApiKey
} else {
delete process.env.ANTHROPIC_API_KEY
}
if (originalAnthropicBaseUrl !== undefined) {
process.env.ANTHROPIC_BASE_URL = originalAnthropicBaseUrl
} else {
delete process.env.ANTHROPIC_BASE_URL
}
if (originalAnthropicModel !== undefined) {
process.env.ANTHROPIC_MODEL = originalAnthropicModel
} else {
delete process.env.ANTHROPIC_MODEL
}
if (originalAnthropicDefaultHaikuModel !== undefined) {
process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = originalAnthropicDefaultHaikuModel
} else {
delete process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL
}
if (originalAnthropicDefaultSonnetModel !== undefined) {
process.env.ANTHROPIC_DEFAULT_SONNET_MODEL = originalAnthropicDefaultSonnetModel
} else {
delete process.env.ANTHROPIC_DEFAULT_SONNET_MODEL
}
if (originalAnthropicDefaultOpusModel !== undefined) {
process.env.ANTHROPIC_DEFAULT_OPUS_MODEL = originalAnthropicDefaultOpusModel
} else {
delete process.env.ANTHROPIC_DEFAULT_OPUS_MODEL
}
await fs.rm(tmpDir, { recursive: true, force: true })
}
function saveTestOpenAIOAuthTokens(tokens: OpenAIOAuthTokens) {
plainTextStorage.update({ openaiCodexOauth: tokens })
clearOpenAIOAuthTokenCache()
}
/** 创建一个模拟 Request */
function makeRequest(
method: string,
@ -324,6 +400,34 @@ describe('Models API', () => {
expect(body.models[0].id).toContain('claude')
})
it('GET /api/models should merge env-configured provider models with saved OpenAI OAuth models', async () => {
process.env.ANTHROPIC_API_KEY = 'deepseek-key'
process.env.ANTHROPIC_BASE_URL = 'https://api.deepseek.com/anthropic'
process.env.ANTHROPIC_MODEL = 'deepseek-v4-pro'
process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = 'deepseek-v4-flash'
process.env.ANTHROPIC_DEFAULT_SONNET_MODEL = 'deepseek-v4-pro'
process.env.ANTHROPIC_DEFAULT_OPUS_MODEL = 'deepseek-v4-pro'
saveTestOpenAIOAuthTokens({
accessToken: 'access-token',
refreshToken: 'refresh-token',
expiresAt: Date.now() + 60_000,
})
const { req, url, segments } = makeRequest('GET', '/api/models')
const res = await handleModelsApi(req, url, segments)
expect(res.status).toBe(200)
const body = await res.json()
const ids = body.models.map((model: { id: string }) => model.id)
expect(ids).toContain('deepseek-v4-pro')
expect(ids).toContain('deepseek-v4-flash')
expect(ids).toContain('gpt-5.3-codex')
expect(ids).toContain('gpt-5.4')
expect(ids).toContain('gpt-5.4-mini')
expect(ids.filter((id: string) => id === 'deepseek-v4-pro')).toHaveLength(1)
})
it('GET /api/models/current should return default model when not set', async () => {
const { req, url, segments } = makeRequest('GET', '/api/models/current')
const res = await handleModelsApi(req, url, segments)
@ -333,6 +437,17 @@ describe('Models API', () => {
expect(body.model.id).toBe('claude-opus-4-7')
})
it('GET /api/models/current should respect env-configured default model when no provider is active', async () => {
process.env.ANTHROPIC_MODEL = 'deepseek-v4-pro'
const { req, url, segments } = makeRequest('GET', '/api/models/current')
const res = await handleModelsApi(req, url, segments)
expect(res.status).toBe(200)
const body = await res.json()
expect(body.model.id).toBe('deepseek-v4-pro')
})
it('PUT /api/models/current should switch model', async () => {
const { req, url, segments } = makeRequest('PUT', '/api/models/current', {
modelId: 'claude-opus-4-7',
@ -450,6 +565,37 @@ describe('Models API', () => {
})
})
describe('Model Options', () => {
beforeEach(setup)
afterEach(teardown)
it('should keep OpenAI OAuth models visible alongside env-configured provider models', () => {
process.env.ANTHROPIC_API_KEY = 'deepseek-key'
process.env.ANTHROPIC_BASE_URL = 'https://api.deepseek.com/anthropic'
process.env.ANTHROPIC_MODEL = 'deepseek-v4-pro'
process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = 'deepseek-v4-flash'
process.env.ANTHROPIC_DEFAULT_SONNET_MODEL = 'deepseek-v4-pro'
process.env.ANTHROPIC_DEFAULT_OPUS_MODEL = 'deepseek-v4-pro'
saveTestOpenAIOAuthTokens({
accessToken: 'access-token',
refreshToken: 'refresh-token',
expiresAt: Date.now() + 60_000,
})
const options = getModelOptions()
const values = options
.map(option => option.value)
.filter((value): value is string => typeof value === 'string')
const labels = options.map(option => option.label)
expect(values).toContain('gpt-5.3-codex')
expect(values).toContain('gpt-5.4')
expect(values).toContain('gpt-5.4-mini')
expect(labels).toContain('deepseek-v4-pro')
expect(labels).toContain('deepseek-v4-flash')
})
})
// =============================================================================
// Status API
// =============================================================================

View File

@ -0,0 +1,121 @@
/**
* Haha OpenAI OAuth REST API
*
* POST /api/haha-openai-oauth/start PKCE+state, authorize URL
* GET /callback/openai redirect , token
* GET /api/haha-openai-oauth ( token )
* DELETE /api/haha-openai-oauth , token
*/
import { z } from 'zod'
import { hahaOpenAIOAuthService } from '../services/hahaOpenAIOAuthService.js'
import { ApiError, errorResponse } from '../middleware/errorHandler.js'
const StartRequestSchema = z.object({
serverPort: z.number().int().positive(),
})
function html(body: string): Response {
return new Response(body, {
status: 200,
headers: { 'Content-Type': 'text/html; charset=utf-8' },
})
}
export async function handleHahaOpenAIOAuthApi(
req: Request,
url: URL,
segments: string[],
): Promise<Response> {
try {
const action = segments[2] // segments: ['api', 'haha-openai-oauth', <action?>]
if (action === 'start' && req.method === 'POST') {
let body: unknown
try {
body = await req.json()
} catch {
throw ApiError.badRequest('Invalid JSON body')
}
const parsed = StartRequestSchema.safeParse(body)
if (!parsed.success) {
throw ApiError.badRequest('serverPort (positive integer) required')
}
const session = hahaOpenAIOAuthService.startSession({
serverPort: parsed.data.serverPort,
})
return Response.json({
authorizeUrl: session.authorizeUrl,
state: session.state,
})
}
if (action === undefined && req.method === 'GET') {
const tokens = await hahaOpenAIOAuthService.ensureFreshTokens()
if (!tokens) {
return Response.json({ loggedIn: false })
}
return Response.json({
loggedIn: true,
expiresAt: tokens.expiresAt,
email: tokens.email,
accountId: tokens.accountId,
})
}
if (action === undefined && req.method === 'DELETE') {
await hahaOpenAIOAuthService.deleteTokens()
return Response.json({ ok: true })
}
return Response.json({ error: 'Not Found' }, { status: 404 })
} catch (error) {
return errorResponse(error)
}
}
export async function handleHahaOpenAIOAuthCallback(url: URL): Promise<Response> {
const code = url.searchParams.get('code')
const state = url.searchParams.get('state')
const error = url.searchParams.get('error')
if (error) {
return html(renderCallbackPage(false, `OAuth provider returned: ${error}`))
}
if (!code || !state) {
return html(renderCallbackPage(false, 'Missing code or state parameter'))
}
try {
await hahaOpenAIOAuthService.completeSession(code, state)
return html(renderCallbackPage(true, null))
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
return html(renderCallbackPage(false, msg))
}
}
function renderCallbackPage(success: boolean, errorMsg: string | null): string {
if (success) {
return `<!doctype html>
<html><head><meta charset="utf-8"><title>OpenAI Login Success</title>
<style>body{font-family:-apple-system,sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;background:#fafafa;color:#333}.card{text-align:center;padding:40px;background:white;border-radius:12px;box-shadow:0 4px 16px rgba(0,0,0,.06)}h1{color:#16a34a;margin:0 0 12px}p{color:#666}</style>
</head><body><div class="card"><h1> OpenAI Login Successful</h1><p>You can close this window and return to Claude Code Haha.</p></div>
<script>setTimeout(() => window.close(), 1500)</script>
</body></html>`
}
return `<!doctype html>
<html><head><meta charset="utf-8"><title>OpenAI Login Failed</title>
<style>body{font-family:-apple-system,sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;background:#fafafa;color:#333}.card{text-align:center;padding:40px;background:white;border-radius:12px;box-shadow:0 4px 16px rgba(0,0,0,.06)}h1{color:#dc2626;margin:0 0 12px}pre{color:#666;white-space:pre-wrap;word-break:break-word;text-align:left;background:#f5f5f5;padding:12px;border-radius:6px}</style>
</head><body><div class="card"><h1> OpenAI Login Failed</h1><pre>${escapeHtml(errorMsg ?? 'Unknown error')}</pre></div>
</body></html>`
}
function escapeHtml(s: string): string {
return s
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
}

View File

@ -11,6 +11,8 @@
import { SettingsService } from '../services/settingsService.js'
import { ProviderService } from '../services/providerService.js'
import { ApiError, errorResponse } from '../middleware/errorHandler.js'
import { hasOpenAIAuthLogin } from '../../utils/auth.js'
import { OPENAI_CODEX_MODEL_CATALOG } from '../../services/openaiAuth/models.js'
// ─── Fallback models (used when no provider is configured) ────────────────────
@ -43,6 +45,106 @@ const DEFAULT_EFFORT = 'medium'
const settingsService = new SettingsService()
const providerService = new ProviderService()
type ApiModelInfo = {
id: string
name: string
description: string
context: string
}
function addUniqueModel(
models: ApiModelInfo[],
model: ApiModelInfo | null,
): void {
if (!model || !model.id.trim()) {
return
}
if (models.some(existing => existing.id === model.id)) {
return
}
models.push(model)
}
function buildProviderModelList(models: {
main: string
haiku: string
sonnet: string
opus: string
}): ApiModelInfo[] {
const modelList: ApiModelInfo[] = []
addUniqueModel(modelList, {
id: models.main,
name: models.main,
description: 'Main model',
context: '',
})
addUniqueModel(modelList, models.haiku
? {
id: models.haiku,
name: models.haiku,
description: 'Haiku model',
context: '',
}
: null)
addUniqueModel(modelList, models.sonnet
? {
id: models.sonnet,
name: models.sonnet,
description: 'Sonnet model',
context: '',
}
: null)
addUniqueModel(modelList, models.opus
? {
id: models.opus,
name: models.opus,
description: 'Opus model',
context: '',
}
: null)
return modelList
}
function getEnvConfiguredAnthropicModels(): ApiModelInfo[] {
return buildProviderModelList({
main: process.env.ANTHROPIC_MODEL?.trim() || '',
haiku: process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL?.trim() || '',
sonnet: process.env.ANTHROPIC_DEFAULT_SONNET_MODEL?.trim() || '',
opus: process.env.ANTHROPIC_DEFAULT_OPUS_MODEL?.trim() || '',
})
}
function getOpenAIAuthModels(): ApiModelInfo[] {
if (!hasOpenAIAuthLogin()) {
return []
}
return OPENAI_CODEX_MODEL_CATALOG.map(model => ({
id: model.value,
name: model.label,
description: model.description,
context: '',
}))
}
function getStandaloneModelList(): ApiModelInfo[] {
const models = [...getEnvConfiguredAnthropicModels()]
if (models.length === 0) {
models.push(...DEFAULT_MODELS)
}
for (const model of getOpenAIAuthModels()) {
addUniqueModel(models, model)
}
return models
}
// ─── Router ───────────────────────────────────────────────────────────────────
export async function handleModelsApi(
@ -83,19 +185,13 @@ async function handleModelsList(): Promise<Response> {
const { providers, activeId } = await providerService.listProviders()
const activeProvider = activeId ? providers.find((p) => p.id === activeId) : null
if (activeProvider) {
// Convert ModelMapping to model list for API compatibility
const modelList = [
{ id: activeProvider.models.main, name: activeProvider.models.main, description: 'Main model', context: '' },
...(activeProvider.models.haiku !== activeProvider.models.main ? [{ id: activeProvider.models.haiku, name: activeProvider.models.haiku, description: 'Haiku model', context: '' }] : []),
...(activeProvider.models.sonnet !== activeProvider.models.main ? [{ id: activeProvider.models.sonnet, name: activeProvider.models.sonnet, description: 'Sonnet model', context: '' }] : []),
...(activeProvider.models.opus !== activeProvider.models.main ? [{ id: activeProvider.models.opus, name: activeProvider.models.opus, description: 'Opus model', context: '' }] : []),
]
const modelList = buildProviderModelList(activeProvider.models)
return Response.json({
models: modelList,
provider: { id: activeProvider.id, name: activeProvider.name },
})
}
return Response.json({ models: DEFAULT_MODELS, provider: null })
return Response.json({ models: getStandaloneModelList(), provider: null })
}
async function handleCurrentModel(req: Request): Promise<Response> {
@ -109,6 +205,7 @@ async function handleCurrentModel(req: Request): Promise<Response> {
const explicitModel = (settings.model as string) || ''
const contextTier = (settings.modelContext as string) || undefined
const env = (settings.env as Record<string, string>) || {}
const envModel = process.env.ANTHROPIC_MODEL?.trim() || ''
let currentModelId: string
let currentModelName: string
@ -127,7 +224,7 @@ async function handleCurrentModel(req: Request): Promise<Response> {
}
} else {
// No provider — use settings model with context tier
currentModelId = explicitModel || DEFAULT_MODEL
currentModelId = explicitModel || envModel || DEFAULT_MODEL
currentModelName = currentModelId
}
@ -135,13 +232,8 @@ async function handleCurrentModel(req: Request): Promise<Response> {
// Build available models for name lookup
const availableModels = activeProvider
? [
{ id: activeProvider.models.main, name: activeProvider.models.main, description: 'Main model', context: '' },
...(activeProvider.models.haiku && activeProvider.models.haiku !== activeProvider.models.main ? [{ id: activeProvider.models.haiku, name: activeProvider.models.haiku, description: 'Haiku model', context: '' }] : []),
...(activeProvider.models.sonnet && activeProvider.models.sonnet !== activeProvider.models.main ? [{ id: activeProvider.models.sonnet, name: activeProvider.models.sonnet, description: 'Sonnet model', context: '' }] : []),
...(activeProvider.models.opus && activeProvider.models.opus !== activeProvider.models.main ? [{ id: activeProvider.models.opus, name: activeProvider.models.opus, description: 'Opus model', context: '' }] : []),
]
: DEFAULT_MODELS
? buildProviderModelList(activeProvider.models)
: getStandaloneModelList()
const modelEntry = availableModels.find((m) => m.id === lookupId)
|| availableModels.find((m) => m.id === currentModelId)

View File

@ -14,6 +14,7 @@ import { cronScheduler } from './services/cronScheduler.js'
import { handleProxyRequest } from './proxy/handler.js'
import { ProviderService } from './services/providerService.js'
import { handleHahaOAuthCallback } from './api/haha-oauth.js'
import { handleHahaOpenAIOAuthCallback } from './api/haha-openai-oauth.js'
import { ensureDesktopCliLauncherInstalled } from './services/desktopCliLauncherService.js'
import { enableConfigs } from '../utils/config.js'
import { diagnosticsService } from './services/diagnosticsService.js'
@ -139,6 +140,10 @@ export function startServer(port = PORT, host = HOST) {
return handleHahaOAuthCallback(url)
}
if (url.pathname === '/callback/openai') {
return handleHahaOpenAIOAuthCallback(url)
}
// REST API
if (url.pathname.startsWith('/api/')) {
// Enforce authentication when required

View File

@ -265,7 +265,10 @@ function processEvent(
controller.enqueue(encoder.encode(formatSse('message_delta', {
type: 'message_delta',
delta: { stop_reason: stopReason, stop_sequence: null },
usage: { output_tokens: usage?.output_tokens ?? 0 },
usage: {
input_tokens: usage?.input_tokens ?? 0,
output_tokens: usage?.output_tokens ?? 0,
},
})))
if (!state.messageStopped) {
state.messageStopped = true

View File

@ -10,7 +10,6 @@ import type {
AnthropicMessage,
OpenAIResponsesRequest,
OpenAIResponsesInputItem,
OpenAITool,
OpenAIChatContentPart,
} from './types.js'
@ -29,6 +28,7 @@ export function anthropicToOpenaiResponses(body: AnthropicRequest): OpenAIRespon
model: body.model,
input,
stream: body.stream,
store: false,
}
// system → instructions
@ -51,13 +51,11 @@ export function anthropicToOpenaiResponses(body: AnthropicRequest): OpenAIRespon
if (body.tools && body.tools.length > 0) {
result.tools = body.tools
.filter((t) => t.name !== 'BatchTool')
.map((t): OpenAITool => ({
.map((t) => ({
type: 'function',
function: {
name: t.name,
description: t.description,
parameters: t.input_schema,
},
name: t.name,
description: t.description,
parameters: t.input_schema,
}))
}

View File

@ -110,11 +110,17 @@ export type OpenAIResponsesRequest = {
model: string
input: OpenAIResponsesInputItem[]
instructions?: string
store?: boolean
max_output_tokens?: number
temperature?: number
top_p?: number
stream?: boolean
tools?: OpenAITool[]
tools?: Array<{
type: 'function'
name: string
description?: string
parameters?: Record<string, unknown>
}>
tool_choice?: unknown
reasoning?: { effort?: 'low' | 'medium' | 'high' }
}

View File

@ -18,6 +18,7 @@ import { handlePluginsApi } from './api/plugins.js'
import { handleSkillsApi } from './api/skills.js'
import { handleComputerUseApi } from './api/computer-use.js'
import { handleHahaOAuthApi } from './api/haha-oauth.js'
import { handleHahaOpenAIOAuthApi } from './api/haha-openai-oauth.js'
import { handleMcpApi } from './api/mcp.js'
import { handleDiagnosticsApi } from './api/diagnostics.js'
@ -73,6 +74,9 @@ export async function handleApiRequest(req: Request, url: URL): Promise<Response
case 'haha-oauth':
return handleHahaOAuthApi(req, url, segments)
case 'haha-openai-oauth':
return handleHahaOpenAIOAuthApi(req, url, segments)
case 'adapters':
return handleAdaptersApi(req, url, segments)

View File

@ -0,0 +1,208 @@
/**
* HahaOpenAIOAuthService OpenAI OAuth token
*
* 为什么存在: macOS Keychain ACL .app quarantine
* UI sidecar , CLI OAuth token 403
* service token haha , env CLI
*
* src/services/openaiAuth/client.ts PKCE + token exchange ,
* CLI
*/
import * as fs from 'fs/promises'
import * as os from 'os'
import * as path from 'path'
import {
generateCodeVerifier,
generateCodeChallenge,
generateState,
} from '../../services/oauth/crypto.js'
import {
buildOpenAIAuthorizeUrl,
exchangeOpenAICodeForTokens,
refreshOpenAITokens,
isOpenAITokenExpired,
normalizeOpenAITokens,
OPENAI_CODEX_OAUTH_PORT,
OPENAI_CODEX_REDIRECT_PATH,
} from '../../services/openaiAuth/client.js'
import type {
OpenAIOAuthTokens,
OpenAIOAuthTokenResponse,
} from '../../services/openaiAuth/types.js'
export type StoredOpenAIOAuthTokens = {
accessToken: string
refreshToken: string | null
expiresAt: number | null
email: string | null
accountId: string | null
}
export type OpenAIOAuthSession = {
state: string
codeVerifier: string
authorizeUrl: string
serverPort: number
createdAt: number
}
type OpenAIRefreshFn = (
refreshToken: string,
) => Promise<OpenAIOAuthTokenResponse>
const SESSION_TTL_MS = 5 * 60 * 1000
export class HahaOpenAIOAuthService {
private sessions = new Map<string, OpenAIOAuthSession>()
private refreshFn: OpenAIRefreshFn = refreshOpenAITokens
setRefreshFn(fn: OpenAIRefreshFn): void {
this.refreshFn = fn
}
private getOAuthFilePath(): string {
const configDir =
process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude')
return path.join(configDir, 'cc-haha', 'openai-oauth.json')
}
async loadTokens(): Promise<StoredOpenAIOAuthTokens | null> {
try {
const raw = await fs.readFile(this.getOAuthFilePath(), 'utf-8')
return JSON.parse(raw) as StoredOpenAIOAuthTokens
} catch (err) {
if ((err as NodeJS.ErrnoException).code === 'ENOENT') return null
throw err
}
}
async saveTokens(tokens: StoredOpenAIOAuthTokens): Promise<void> {
const filePath = this.getOAuthFilePath()
await fs.mkdir(path.dirname(filePath), { recursive: true })
const tmp = `${filePath}.tmp.${process.pid}`
await fs.writeFile(tmp, JSON.stringify(tokens, null, 2), { mode: 0o600 })
await fs.rename(tmp, filePath)
}
async deleteTokens(): Promise<void> {
try {
await fs.unlink(this.getOAuthFilePath())
} catch (err) {
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err
}
}
startSession({ serverPort }: { serverPort: number }): OpenAIOAuthSession {
this.pruneExpiredSessions()
const codeVerifier = generateCodeVerifier()
const state = generateState()
const redirectUri = `http://localhost:${serverPort}${OPENAI_CODEX_REDIRECT_PATH}`
const authorizeUrl = buildOpenAIAuthorizeUrl({
redirectUri,
codeVerifier,
state,
})
const session: OpenAIOAuthSession = {
state,
codeVerifier,
authorizeUrl,
serverPort,
createdAt: Date.now(),
}
this.sessions.set(state, session)
return session
}
getSession(state: string): OpenAIOAuthSession | null {
const s = this.sessions.get(state)
if (!s) return null
if (Date.now() - s.createdAt > SESSION_TTL_MS) {
this.sessions.delete(state)
return null
}
return s
}
consumeSession(state: string): OpenAIOAuthSession | null {
const s = this.getSession(state)
if (s) this.sessions.delete(state)
return s
}
private pruneExpiredSessions(): void {
const now = Date.now()
for (const [state, s] of this.sessions.entries()) {
if (now - s.createdAt > SESSION_TTL_MS) this.sessions.delete(state)
}
}
async completeSession(
authorizationCode: string,
state: string,
): Promise<StoredOpenAIOAuthTokens> {
const session = this.consumeSession(state)
if (!session) {
throw new Error('OpenAI OAuth session not found or expired')
}
const redirectUri = `http://localhost:${session.serverPort}${OPENAI_CODEX_REDIRECT_PATH}`
const response = await exchangeOpenAICodeForTokens({
code: authorizationCode,
redirectUri,
codeVerifier: session.codeVerifier,
})
const normalized = normalizeOpenAITokens(response)
const tokens: StoredOpenAIOAuthTokens = {
accessToken: normalized.accessToken,
refreshToken: normalized.refreshToken,
expiresAt: normalized.expiresAt,
email: normalized.email ?? null,
accountId: normalized.accountId ?? null,
}
await this.saveTokens(tokens)
return tokens
}
async ensureFreshTokens(): Promise<StoredOpenAIOAuthTokens | null> {
const tokens = await this.loadTokens()
if (!tokens) return null
if (tokens.expiresAt === null) return tokens
if (!isOpenAITokenExpired(tokens.expiresAt)) return tokens
if (!tokens.refreshToken) return null
try {
const refreshed = await this.refreshFn(tokens.refreshToken)
const normalized = normalizeOpenAITokens(refreshed)
const updated: StoredOpenAIOAuthTokens = {
accessToken: normalized.accessToken,
refreshToken: normalized.refreshToken ?? tokens.refreshToken,
expiresAt: normalized.expiresAt,
email: normalized.email ?? tokens.email,
accountId: normalized.accountId ?? tokens.accountId,
}
await this.saveTokens(updated)
return updated
} catch (err) {
console.error(
'[HahaOpenAIOAuthService] token refresh failed:',
err instanceof Error ? err.message : err,
)
return null
}
}
async ensureFreshAccessToken(): Promise<string | null> {
const tokens = await this.ensureFreshTokens()
return tokens?.accessToken ?? null
}
}
export const hahaOpenAIOAuthService = new HahaOpenAIOAuthService()

View File

@ -22,6 +22,12 @@ import {
getSessionId,
} from '../../bootstrap/state.js'
import { getOauthConfig } from '../../constants/oauth.js'
import {
buildOpenAICodexFetch,
OPENAI_OAUTH_DUMMY_KEY,
shouldUseOpenAICodexAuth,
} from '../openaiAuth/fetch.js'
import { isOpenAIResponsesModel } from '../openaiAuth/models.js'
import { isDebugToStdErr, logForDebugging } from '../../utils/debug.js'
import {
getAWSRegion,
@ -132,11 +138,21 @@ export async function getAnthropicClient({
await checkAndRefreshOAuthTokenIfNeeded()
logForDebugging('[API:auth] OAuth token check complete')
if (!isClaudeAISubscriber()) {
const isOpenAIModel = model ? isOpenAIResponsesModel(model) : false
const usingOpenAICodex =
shouldUseOpenAICodexAuth() &&
!isClaudeAISubscriber() &&
(isOpenAIModel ||
(!process.env.ANTHROPIC_AUTH_TOKEN &&
!(apiKey || getAnthropicApiKey())))
if (!isClaudeAISubscriber() && !usingOpenAICodex) {
await configureApiKeyHeaders(defaultHeaders, getIsNonInteractiveSession())
}
const resolvedFetch = buildFetch(fetchOverride, source)
const resolvedFetch = usingOpenAICodex
? buildOpenAICodexFetch(fetchOverride, source)
: buildFetch(fetchOverride, source)
const ARGS = {
defaultHeaders,
@ -299,7 +315,11 @@ export async function getAnthropicClient({
// Determine authentication method based on available tokens
const clientConfig: ConstructorParameters<typeof Anthropic>[0] = {
apiKey: isClaudeAISubscriber() ? null : apiKey || getAnthropicApiKey(),
apiKey: isClaudeAISubscriber()
? null
: usingOpenAICodex
? OPENAI_OAUTH_DUMMY_KEY
: apiKey || getAnthropicApiKey(),
authToken: isClaudeAISubscriber()
? getClaudeAIOAuthTokens()?.accessToken
: undefined,

View File

@ -0,0 +1,141 @@
import { generateCodeChallenge } from '../oauth/crypto.js'
import type {
OpenAIJwtClaims,
OpenAIOAuthTokenResponse,
OpenAIOAuthTokens,
} from './types.js'
export const OPENAI_AUTH_ISSUER = 'https://auth.openai.com'
export const OPENAI_CODEX_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann'
export const OPENAI_CODEX_API_ENDPOINT =
'https://chatgpt.com/backend-api/codex/responses'
export const OPENAI_CODEX_OAUTH_PORT = 1455
export const OPENAI_CODEX_REDIRECT_PATH = '/auth/callback'
const DEFAULT_TOKEN_LIFETIME_MS = 3600 * 1000
export function buildOpenAIAuthorizeUrl(input: {
redirectUri: string
codeVerifier: string
state: string
}): string {
const params = new URLSearchParams({
response_type: 'code',
client_id: OPENAI_CODEX_CLIENT_ID,
redirect_uri: input.redirectUri,
scope: 'openid profile email offline_access',
code_challenge: generateCodeChallenge(input.codeVerifier),
code_challenge_method: 'S256',
id_token_add_organizations: 'true',
codex_cli_simplified_flow: 'true',
state: input.state,
originator: 'opencode',
})
return `${OPENAI_AUTH_ISSUER}/oauth/authorize?${params.toString()}`
}
export async function exchangeOpenAICodeForTokens(input: {
code: string
redirectUri: string
codeVerifier: string
}): Promise<OpenAIOAuthTokenResponse> {
const response = await fetch(`${OPENAI_AUTH_ISSUER}/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: input.code,
redirect_uri: input.redirectUri,
client_id: OPENAI_CODEX_CLIENT_ID,
code_verifier: input.codeVerifier,
}).toString(),
})
if (!response.ok) {
throw new Error(`OpenAI token exchange failed: ${response.status}`)
}
return (await response.json()) as OpenAIOAuthTokenResponse
}
export async function refreshOpenAITokens(
refreshToken: string,
): Promise<OpenAIOAuthTokenResponse> {
const response = await fetch(`${OPENAI_AUTH_ISSUER}/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: OPENAI_CODEX_CLIENT_ID,
}).toString(),
})
if (!response.ok) {
throw new Error(`OpenAI token refresh failed: ${response.status}`)
}
return (await response.json()) as OpenAIOAuthTokenResponse
}
export function parseOpenAIJwtClaims(
token?: string,
): OpenAIJwtClaims | undefined {
if (!token) return undefined
const parts = token.split('.')
if (parts.length !== 3) return undefined
try {
return JSON.parse(Buffer.from(parts[1]!, 'base64url').toString())
} catch {
return undefined
}
}
export function extractOpenAIAccountId(
claims?: OpenAIJwtClaims,
): string | undefined {
if (!claims) return undefined
return (
claims.chatgpt_account_id ||
claims['https://api.openai.com/auth']?.chatgpt_account_id ||
claims.organizations?.[0]?.id
)
}
export function normalizeOpenAITokens(
response: OpenAIOAuthTokenResponse,
): OpenAIOAuthTokens {
const claims =
parseOpenAIJwtClaims(response.id_token) ??
parseOpenAIJwtClaims(response.access_token)
return {
accessToken: response.access_token,
refreshToken: response.refresh_token,
expiresAt: Date.now() + (response.expires_in ?? 3600) * 1000,
idToken: response.id_token,
accountId: extractOpenAIAccountId(claims),
email: claims?.email,
}
}
export function isOpenAITokenExpired(expiresAt: number): boolean {
return expiresAt - Date.now() <= 5 * 60 * 1000
}
export function withRefreshedAccessToken(
existing: OpenAIOAuthTokens,
refreshed: OpenAIOAuthTokenResponse,
): OpenAIOAuthTokens {
const next = normalizeOpenAITokens(refreshed)
return {
...next,
accountId: next.accountId ?? existing.accountId,
email: next.email ?? existing.email,
idToken: next.idToken ?? existing.idToken,
}
}

View File

@ -0,0 +1,132 @@
import type { ClientOptions } from '@anthropic-ai/sdk'
import { randomUUID } from 'crypto'
import { OPENAI_CODEX_API_ENDPOINT } from './client.js'
import { ensureFreshOpenAITokens } from './index.js'
import { resolveOpenAICodexModel } from './models.js'
import { getOpenAIOAuthTokens } from './storage.js'
import { anthropicToOpenaiResponses } from '../../server/proxy/transform/anthropicToOpenaiResponses.js'
import { openaiResponsesToAnthropic } from '../../server/proxy/transform/openaiResponsesToAnthropic.js'
import { openaiResponsesStreamToAnthropic } from '../../server/proxy/streaming/openaiResponsesStreamToAnthropic.js'
import type { AnthropicRequest } from '../../server/proxy/transform/types.js'
import { logForDebugging } from '../../utils/debug.js'
export const OPENAI_OAUTH_DUMMY_KEY = 'openai-oauth-dummy-key'
export function shouldUseOpenAICodexAuth(): boolean {
const openaiTokens = getOpenAIOAuthTokens()
return !!openaiTokens?.refreshToken
}
export function buildOpenAICodexFetch(
fetchOverride: ClientOptions['fetch'],
source: string | undefined,
): ClientOptions['fetch'] {
const inner = fetchOverride ?? globalThis.fetch
return async (input, init) => {
const url = input instanceof Request ? new URL(input.url) : new URL(String(input))
if (!url.pathname.endsWith('/v1/messages')) {
return inner(input, init)
}
const originalBody = await readAnthropicBody(input, init)
const mappedModel = resolveOpenAICodexModel(originalBody.model)
const transformedBody = anthropicToOpenaiResponses({
...originalBody,
model: mappedModel,
})
const tokens = await ensureFreshOpenAITokens()
if (!tokens) {
throw new Error(
'OpenAI OAuth token is missing or expired. Run claude auth login --openai again.',
)
}
const headers = new Headers()
headers.set('Content-Type', 'application/json')
headers.set('Authorization', `Bearer ${tokens.accessToken}`)
if (tokens.accountId) {
headers.set('ChatGPT-Account-Id', tokens.accountId)
}
logForDebugging(
`[API REQUEST] ${url.pathname} remapped_to=OpenAI/Codex model=${mappedModel} source=${source ?? 'unknown'} request_id=${randomUUID()}`,
)
const upstream = await inner(OPENAI_CODEX_API_ENDPOINT, {
method: 'POST',
headers,
body: JSON.stringify(transformedBody),
signal: init?.signal,
})
if (!upstream.ok) {
const errorText = await upstream.text().catch(() => '')
return Response.json(
{
type: 'error',
error: {
type: 'api_error',
message: `OpenAI upstream returned HTTP ${upstream.status}: ${errorText.slice(0, 500)}`,
},
},
{ status: upstream.status },
)
}
if (transformedBody.stream) {
if (!upstream.body) {
return Response.json(
{
type: 'error',
error: {
type: 'api_error',
message: 'OpenAI upstream returned no body for stream',
},
},
{ status: 502 },
)
}
return new Response(
openaiResponsesStreamToAnthropic(upstream.body, mappedModel),
{
status: 200,
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
},
},
)
}
const responseBody = await upstream.json()
return Response.json(
openaiResponsesToAnthropic(responseBody, mappedModel),
)
}
}
async function readAnthropicBody(
input: RequestInfo | URL,
init?: RequestInit,
): Promise<AnthropicRequest> {
const directBody = init?.body
if (typeof directBody === 'string') {
return JSON.parse(directBody) as AnthropicRequest
}
if (directBody instanceof Uint8Array || directBody instanceof ArrayBuffer) {
return JSON.parse(Buffer.from(directBody).toString('utf8')) as AnthropicRequest
}
if (input instanceof Request) {
return (await input.clone().json()) as AnthropicRequest
}
throw new Error('Unable to read Anthropic request body for OpenAI/Codex transformation')
}

View File

@ -0,0 +1,210 @@
import { openBrowser } from '../../utils/browser.js'
import { AuthCodeListener } from '../oauth/auth-code-listener.js'
import { generateCodeVerifier, generateState } from '../oauth/crypto.js'
import {
buildOpenAIAuthorizeUrl,
exchangeOpenAICodeForTokens,
isOpenAITokenExpired,
OPENAI_CODEX_OAUTH_PORT,
OPENAI_CODEX_REDIRECT_PATH,
refreshOpenAITokens,
normalizeOpenAITokens,
withRefreshedAccessToken,
} from './client.js'
import {
clearOpenAIOAuthTokenCache,
deleteOpenAIOAuthTokens,
getOpenAIOAuthTokensAsync,
saveOpenAIOAuthTokens,
} from './storage.js'
import type { OpenAIOAuthTokens } from './types.js'
const HTML_SUCCESS = `<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>cc-haha OpenAI Authorization Successful</title>
<style>
body { font-family: system-ui, -apple-system, sans-serif; display:flex; justify-content:center; align-items:center; height:100vh; margin:0; background:#131010; color:#f1ecec; }
.container { text-align:center; padding:2rem; }
p { color:#b7b1b1; }
</style>
</head>
<body>
<div class="container">
<h1>Authorization Successful</h1>
<p>You can close this window and return to Claude Code Haha.</p>
</div>
<script>setTimeout(() => window.close(), 2000)</script>
</body>
</html>`
const HTML_ERROR = `<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>cc-haha OpenAI Authorization Failed</title>
</head>
<body>
<h1>Authorization Failed</h1>
<p>You can close this window and return to Claude Code Haha.</p>
</body>
</html>`
export class OpenAIOAuthService {
private codeVerifier: string
private authCodeListener: AuthCodeListener | null = null
private port: number | null = null
private manualAuthCodeResolver: ((authorizationCode: string) => void) | null =
null
constructor() {
this.codeVerifier = generateCodeVerifier()
}
async startOAuthFlow(
authURLHandler: (url: string, automaticUrl?: string) => Promise<void>,
options?: {
skipBrowserOpen?: boolean
},
): Promise<OpenAIOAuthTokens> {
this.authCodeListener = new AuthCodeListener(OPENAI_CODEX_REDIRECT_PATH)
this.port = await this.authCodeListener.start(OPENAI_CODEX_OAUTH_PORT)
const state = generateState()
const redirectUri = `http://localhost:${this.port}${OPENAI_CODEX_REDIRECT_PATH}`
const authorizeUrl = buildOpenAIAuthorizeUrl({
redirectUri,
codeVerifier: this.codeVerifier,
state,
})
const authorizationCode = await this.waitForAuthorizationCode(
state,
async () => {
if (options?.skipBrowserOpen) {
await authURLHandler(authorizeUrl, authorizeUrl)
} else {
await authURLHandler(authorizeUrl)
await openBrowser(authorizeUrl)
}
},
)
try {
const response = await exchangeOpenAICodeForTokens({
code: authorizationCode,
redirectUri,
codeVerifier: this.codeVerifier,
})
const tokens = normalizeOpenAITokens(response)
const storage = saveOpenAIOAuthTokens(tokens)
if (!storage.success) {
throw new Error(storage.warning ?? 'Failed to persist OpenAI OAuth tokens')
}
this.authCodeListener?.handleSuccessRedirect([], (res) => {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
res.end(HTML_SUCCESS)
})
return tokens
} catch (error) {
this.authCodeListener?.handleSuccessRedirect([], (res) => {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
res.end(HTML_ERROR)
})
throw error
} finally {
this.cleanup()
}
}
private async waitForAuthorizationCode(
state: string,
onReady: () => Promise<void>,
): Promise<string> {
return new Promise((resolve, reject) => {
this.manualAuthCodeResolver = resolve
this.authCodeListener
?.waitForAuthorization(state, onReady)
.then((authorizationCode) => {
this.manualAuthCodeResolver = null
resolve(authorizationCode)
})
.catch((error) => {
this.manualAuthCodeResolver = null
reject(error)
})
})
}
/**
* Handle manual auth code input e.g., user pastes the authorization code
* from the browser into the desktop UI when automatic redirect didn't work.
*/
handleManualAuthCodeInput(params: {
authorizationCode: string
state: string
}): void {
if (this.manualAuthCodeResolver) {
this.manualAuthCodeResolver(params.authorizationCode)
this.manualAuthCodeResolver = null
this.authCodeListener?.close()
}
}
async ensureFreshTokens(): Promise<OpenAIOAuthTokens | null> {
clearOpenAIOAuthTokenCache()
const tokens = await getOpenAIOAuthTokensAsync()
if (!tokens) return null
if (!isOpenAITokenExpired(tokens.expiresAt)) return tokens
const refreshed = await refreshOpenAITokens(tokens.refreshToken)
const updated = withRefreshedAccessToken(tokens, refreshed)
const storage = saveOpenAIOAuthTokens(updated)
if (!storage.success) {
throw new Error(storage.warning ?? 'Failed to persist refreshed OpenAI tokens')
}
return updated
}
async ensureFreshAccessToken(): Promise<string | null> {
const tokens = await this.ensureFreshTokens()
return tokens?.accessToken ?? null
}
logout(): boolean {
clearOpenAIOAuthTokenCache()
return deleteOpenAIOAuthTokens()
}
cleanup(): void {
this.authCodeListener?.close()
this.authCodeListener = null
this.port = null
this.manualAuthCodeResolver = null
}
}
export async function ensureFreshOpenAITokens(): Promise<OpenAIOAuthTokens | null> {
clearOpenAIOAuthTokenCache()
const tokens = await getOpenAIOAuthTokensAsync()
if (!tokens) return null
if (!isOpenAITokenExpired(tokens.expiresAt)) return tokens
try {
const refreshed = await refreshOpenAITokens(tokens.refreshToken)
const updated = withRefreshedAccessToken(tokens, refreshed)
const storage = saveOpenAIOAuthTokens(updated)
if (!storage.success) {
throw new Error(storage.warning ?? 'Failed to persist refreshed OpenAI tokens')
}
return updated
} catch {
return null
}
}

View File

@ -0,0 +1,42 @@
import { describe, expect, test } from 'bun:test'
import {
OPENAI_DEFAULT_MAIN_MODEL,
OPENAI_GPT5_LARGE_CONTEXT_WINDOW,
OPENAI_GPT5_STANDARD_CONTEXT_WINDOW,
getOpenAIContextWindowForModel,
isOpenAIResponsesModel,
resolveOpenAICodexModel,
} from './models.js'
describe('openai auth model resolution', () => {
test('does not treat opus as an OpenAI Responses model', () => {
expect(isOpenAIResponsesModel('opus')).toBe(false)
})
test('accepts gpt and o-series models', () => {
expect(isOpenAIResponsesModel('gpt-5.4')).toBe(true)
expect(isOpenAIResponsesModel('o3-mini')).toBe(true)
})
test('maps opus aliases to the OpenAI default model', () => {
expect(resolveOpenAICodexModel('opus')).toBe(OPENAI_DEFAULT_MAIN_MODEL)
})
test('maps large-context GPT-5 models to a 1.05M context window', () => {
expect(getOpenAIContextWindowForModel('gpt-5.5')).toBe(
OPENAI_GPT5_LARGE_CONTEXT_WINDOW,
)
expect(getOpenAIContextWindowForModel('gpt-5.4')).toBe(
OPENAI_GPT5_LARGE_CONTEXT_WINDOW,
)
})
test('maps standard GPT-5 models to a 400k context window', () => {
expect(getOpenAIContextWindowForModel('gpt-5.3-codex')).toBe(
OPENAI_GPT5_STANDARD_CONTEXT_WINDOW,
)
expect(getOpenAIContextWindowForModel('gpt-5.4-mini')).toBe(
OPENAI_GPT5_STANDARD_CONTEXT_WINDOW,
)
})
})

View File

@ -0,0 +1,137 @@
export const OPENAI_DEFAULT_MAIN_MODEL = 'gpt-5.3-codex'
export const OPENAI_DEFAULT_SONNET_MODEL = 'gpt-5.4'
export const OPENAI_DEFAULT_HAIKU_MODEL = 'gpt-5.4-mini'
export const OPENAI_GPT5_STANDARD_CONTEXT_WINDOW = 400_000
export const OPENAI_GPT5_LARGE_CONTEXT_WINDOW = 1_050_000
export type OpenAIModelCatalogEntry = {
value: string
label: string
description: string
descriptionForModel?: string
}
export const OPENAI_CODEX_MODEL_CATALOG: OpenAIModelCatalogEntry[] = [
{
value: OPENAI_DEFAULT_MAIN_MODEL,
label: 'GPT-5.3 Codex',
description: 'Best for coding and agentic work',
descriptionForModel: 'GPT-5.3 Codex - best for coding and agentic work',
},
{
value: OPENAI_DEFAULT_SONNET_MODEL,
label: 'GPT-5.4',
description: 'Strong general-purpose model',
descriptionForModel: 'GPT-5.4 - strong general-purpose model',
},
{
value: 'gpt-5.5',
label: 'GPT-5.5',
description: 'Latest general-purpose model',
descriptionForModel: 'GPT-5.5 - latest general-purpose model',
},
{
value: OPENAI_DEFAULT_HAIKU_MODEL,
label: 'GPT-5.4 Mini',
description: 'Fastest for quick tasks',
descriptionForModel: 'GPT-5.4 Mini - fastest for quick tasks',
},
]
export function isOpenAIResponsesModel(model: string): boolean {
const normalized = model.trim().toLowerCase()
return normalized.startsWith('gpt-') || /^o\d/.test(normalized)
}
export function resolveOpenAICodexModel(model: string): string {
if (process.env.OPENAI_CODEX_MODEL?.trim()) {
return process.env.OPENAI_CODEX_MODEL.trim()
}
const normalized = model.trim().toLowerCase()
if (isOpenAIResponsesModel(normalized)) {
return model
}
if (normalized.includes('haiku')) {
return (
process.env.OPENAI_CODEX_HAIKU_MODEL?.trim() ||
OPENAI_DEFAULT_HAIKU_MODEL
)
}
if (normalized.includes('sonnet')) {
return (
process.env.OPENAI_CODEX_SONNET_MODEL?.trim() ||
OPENAI_DEFAULT_SONNET_MODEL
)
}
if (normalized.includes('opus')) {
return (
process.env.OPENAI_CODEX_OPUS_MODEL?.trim() || OPENAI_DEFAULT_MAIN_MODEL
)
}
return OPENAI_DEFAULT_MAIN_MODEL
}
export function getOpenAIModelDisplayName(model: string): string | null {
switch (model.trim().toLowerCase()) {
case 'gpt-5.3-codex':
return 'GPT-5.3 Codex'
case 'gpt-5.5':
return 'GPT-5.5'
case 'gpt-5.4':
return 'GPT-5.4'
case 'gpt-5.4-mini':
return 'GPT-5.4 Mini'
case 'gpt-5.2':
return 'GPT-5.2'
case 'gpt-5.2-codex':
return 'GPT-5.2 Codex'
case 'gpt-5.1-codex':
return 'GPT-5.1 Codex'
case 'gpt-5.1-codex-max':
return 'GPT-5.1 Codex Max'
case 'gpt-5.1-codex-mini':
return 'GPT-5.1 Codex Mini'
default:
return null
}
}
export function getOpenAIContextWindowForModel(model: string): number | null {
const normalized = model.trim().toLowerCase()
// Current OpenAI model docs split GPT-5-family context windows into
// standard 400k models and larger 1.05M models.
if (
normalized === 'gpt-5.5' ||
normalized === 'gpt-5.5-pro' ||
normalized === 'gpt-5.4' ||
normalized === 'gpt-5.4-pro'
) {
return OPENAI_GPT5_LARGE_CONTEXT_WINDOW
}
if (
normalized === 'gpt-5.4-mini' ||
normalized === 'gpt-5.4-nano' ||
normalized === 'gpt-5.3-codex' ||
normalized === 'gpt-5.2' ||
normalized === 'gpt-5.2-codex' ||
normalized === 'gpt-5.1' ||
normalized === 'gpt-5.1-codex' ||
normalized === 'gpt-5.1-codex-max' ||
normalized === 'gpt-5.1-codex-mini' ||
normalized === 'gpt-5-codex' ||
normalized === 'gpt-5' ||
normalized === 'gpt-5-mini' ||
normalized === 'gpt-5-nano'
) {
return OPENAI_GPT5_STANDARD_CONTEXT_WINDOW
}
return null
}

View File

@ -0,0 +1,71 @@
import memoize from 'lodash-es/memoize.js'
import { getSecureStorage } from '../../utils/secureStorage/index.js'
import { errorMessage } from '../../utils/errors.js'
import { logError } from '../../utils/log.js'
import type { OpenAIOAuthTokens } from './types.js'
const STORAGE_KEY = 'openaiCodexOauth'
type SecureStorageShape = Record<string, unknown> & {
openaiCodexOauth?: OpenAIOAuthTokens
}
export function saveOpenAIOAuthTokens(tokens: OpenAIOAuthTokens): {
success: boolean
warning?: string
} {
try {
const storage = getSecureStorage()
const data = (storage.read() ?? {}) as SecureStorageShape
data[STORAGE_KEY] = tokens
const result = storage.update(data)
clearOpenAIOAuthTokenCache()
return result
} catch (error) {
logError(error)
return {
success: false,
warning: `Failed to save OpenAI OAuth tokens: ${errorMessage(error)}`,
}
}
}
export const getOpenAIOAuthTokens = memoize((): OpenAIOAuthTokens | null => {
try {
const storage = getSecureStorage()
const data = storage.read() as SecureStorageShape | null
return data?.openaiCodexOauth ?? null
} catch (error) {
logError(error)
return null
}
})
export async function getOpenAIOAuthTokensAsync(): Promise<OpenAIOAuthTokens | null> {
try {
const storage = getSecureStorage()
const data = (await storage.readAsync()) as SecureStorageShape | null
return data?.openaiCodexOauth ?? null
} catch (error) {
logError(error)
return null
}
}
export function clearOpenAIOAuthTokenCache(): void {
getOpenAIOAuthTokens.cache?.clear?.()
}
export function deleteOpenAIOAuthTokens(): boolean {
try {
const storage = getSecureStorage()
const data = (storage.read() ?? {}) as SecureStorageShape
delete data[STORAGE_KEY]
const result = storage.update(data)
clearOpenAIOAuthTokenCache()
return result.success
} catch (error) {
logError(error)
return false
}
}

View File

@ -0,0 +1,24 @@
export type OpenAIOAuthTokenResponse = {
access_token: string
refresh_token: string
id_token?: string
expires_in?: number
}
export type OpenAIOAuthTokens = {
accessToken: string
refreshToken: string
expiresAt: number
idToken?: string
accountId?: string
email?: string
}
export type OpenAIJwtClaims = {
chatgpt_account_id?: string
organizations?: Array<{ id: string }>
email?: string
'https://api.openai.com/auth'?: {
chatgpt_account_id?: string
}
}

View File

@ -0,0 +1,42 @@
import { describe, expect, test } from 'bun:test'
import { getCurrentUsage } from '../tokens.js'
describe('getCurrentUsage', () => {
test('skips zero placeholder usage and returns the latest meaningful usage', () => {
const messages = [
{
type: 'assistant',
message: {
model: 'gpt-5.5',
content: [{ type: 'text', text: 'older' }],
usage: {
input_tokens: 123,
output_tokens: 45,
cache_creation_input_tokens: 6,
cache_read_input_tokens: 7,
},
},
},
{
type: 'assistant',
message: {
model: 'gpt-5.5',
content: [{ type: 'text', text: 'placeholder' }],
usage: {
input_tokens: 0,
output_tokens: 0,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0,
},
},
},
] as const
expect(getCurrentUsage(messages as never)).toEqual({
input_tokens: 123,
output_tokens: 45,
cache_creation_input_tokens: 6,
cache_read_input_tokens: 7,
})
})
})

View File

@ -19,6 +19,7 @@ import {
getMockSubscriptionType,
shouldUseMockSubscription,
} from '../services/mockRateLimits.js'
import { getOpenAIOAuthTokens } from '../services/openaiAuth/storage.js'
import {
isOAuthTokenExpired,
refreshOAuthToken,
@ -1183,6 +1184,27 @@ export async function removeApiKey(): Promise<void> {
clearLegacyApiKeyPrefetch()
}
export function clearStoredClaudeAIOAuthTokens(): {
success: boolean
warning?: string
} {
try {
const secureStorage = getSecureStorage()
const storageData = secureStorage.read() || {}
if ('claudeAiOauth' in storageData) {
delete storageData.claudeAiOauth
}
const result = secureStorage.update(storageData)
clearOAuthTokenCache()
return result
} catch (error) {
logError(error)
return { success: false, warning: 'Failed to clear stored Claude OAuth tokens' }
}
}
async function maybeRemoveApiKeyFromMacOSKeychain(): Promise<void> {
try {
await maybeRemoveApiKeyFromMacOSKeychainThrows()
@ -1856,11 +1878,72 @@ export function isConsumerSubscriber(): boolean {
}
export type UserAccountInfo = {
provider?: 'anthropic' | 'openai'
subscription?: string
tokenSource?: string
apiKeySource?: ApiKeySource
organization?: string
email?: string
accountId?: string
}
export function getOpenAISubscriptionName(): string {
return 'ChatGPT Pro/Plus'
}
export function getOpenAIAuthOverrideWarning(): string | null {
if (isUsing3PServices()) {
return 'OpenAI login is saved, but this shell is configured for a third-party provider.'
}
const { source: authTokenSource } = getAuthTokenSource()
if (authTokenSource !== 'none') {
return `OpenAI login is saved, but this shell is still using ${authTokenSource} for Anthropic-compatible auth.`
}
const { source: apiKeySource } = getAnthropicApiKeyWithSource({
skipRetrievingKeyFromApiKeyHelper: true,
})
if (apiKeySource !== 'none') {
return `OpenAI login is saved, but this shell is still using ${apiKeySource} for Anthropic-compatible auth.`
}
return null
}
export function hasOpenAIAuthLogin(): boolean {
const openaiTokens = getOpenAIOAuthTokens()
return !!openaiTokens?.refreshToken && !isUsing3PServices()
}
export function isOpenAIAuthActive(): boolean {
return getOpenAIAccountInformationIfActive() !== undefined
}
function getOpenAIAccountInformationIfActive(input?: {
authTokenSource?: ReturnType<typeof getAuthTokenSource>['source']
apiKeySource?: ApiKeySource
}): UserAccountInfo | undefined {
const openaiTokens = getOpenAIOAuthTokens()
if (!openaiTokens?.refreshToken || isUsing3PServices()) {
return undefined
}
const authTokenSource = input?.authTokenSource ?? getAuthTokenSource().source
const apiKeySource =
input?.apiKeySource ?? getAnthropicApiKeyWithSource().source
if (authTokenSource !== 'none' || apiKeySource !== 'none') {
return undefined
}
return {
provider: 'openai',
subscription: getOpenAISubscriptionName(),
tokenSource: 'OpenAI OAuth',
email: openaiTokens.email,
accountId: openaiTokens.accountId,
}
}
export function getAccountInformation() {
@ -1870,7 +1953,17 @@ export function getAccountInformation() {
return undefined
}
const { source: authTokenSource } = getAuthTokenSource()
const { key: apiKey, source: apiKeySource } = getAnthropicApiKeyWithSource()
const openAIAccountInfo = getOpenAIAccountInformationIfActive({
authTokenSource,
apiKeySource,
})
if (openAIAccountInfo) {
return openAIAccountInfo
}
const accountInfo: UserAccountInfo = {}
accountInfo.provider = 'anthropic'
if (
authTokenSource === 'CLAUDE_CODE_OAUTH_TOKEN' ||
authTokenSource === 'CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR'
@ -1881,7 +1974,6 @@ export function getAccountInformation() {
} else {
accountInfo.tokenSource = authTokenSource
}
const { key: apiKey, source: apiKeySource } = getAnthropicApiKeyWithSource()
if (apiKey) {
accountInfo.apiKeySource = apiKeySource
}

View File

@ -1,11 +1,12 @@
// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
import { CONTEXT_1M_BETA_HEADER } from '../constants/betas.js'
import { getOpenAIContextWindowForModel } from '../services/openaiAuth/models.js'
import { getGlobalConfig } from './config.js'
import { isEnvTruthy } from './envUtils.js'
import { getCanonicalName } from './model/model.js'
import { getModelCapability } from './model/modelCapabilities.js'
// Model context window size (200k tokens for all models right now)
// Default fallback when the model-specific capability is unknown.
export const MODEL_CONTEXT_WINDOW_DEFAULT = 200_000
// Maximum output tokens for compact operations
@ -71,6 +72,11 @@ export function getContextWindowForModel(
return 1_000_000
}
const openAIContextWindow = getOpenAIContextWindowForModel(model)
if (openAIContextWindow) {
return openAIContextWindow
}
const cap = getModelCapability(model)
if (cap?.max_input_tokens && cap.max_input_tokens >= 100_000) {
if (

View File

@ -10,6 +10,7 @@ import {
getSubscriptionType,
isClaudeAISubscriber,
isMaxSubscriber,
isOpenAIAuthActive,
isProSubscriber,
isTeamPremiumSubscriber,
} from '../auth.js'
@ -27,6 +28,10 @@ import { getAPIProvider } from './providers.js'
import { LIGHTNING_BOLT } from '../../constants/figures.js'
import { isModelAllowed } from './modelAllowlist.js'
import { type ModelAlias, isModelAlias } from './aliases.js'
import {
getOpenAIModelDisplayName,
resolveOpenAICodexModel,
} from '../../services/openaiAuth/models.js'
import { capitalize } from '../stringUtils.js'
export type ModelShortName = string
@ -103,6 +108,9 @@ export function getBestModel(): ModelName {
// @[MODEL LAUNCH]: Update the default Opus model (3P providers may lag so keep defaults unchanged).
export function getDefaultOpusModel(): ModelName {
if (isOpenAIAuthActive()) {
return resolveOpenAICodexModel('opus')
}
if (process.env.ANTHROPIC_DEFAULT_OPUS_MODEL) {
return process.env.ANTHROPIC_DEFAULT_OPUS_MODEL
}
@ -117,6 +125,9 @@ export function getDefaultOpusModel(): ModelName {
// @[MODEL LAUNCH]: Update the default Sonnet model (3P providers may lag so keep defaults unchanged).
export function getDefaultSonnetModel(): ModelName {
if (isOpenAIAuthActive()) {
return resolveOpenAICodexModel('sonnet')
}
if (process.env.ANTHROPIC_DEFAULT_SONNET_MODEL) {
return process.env.ANTHROPIC_DEFAULT_SONNET_MODEL
}
@ -129,6 +140,9 @@ export function getDefaultSonnetModel(): ModelName {
// @[MODEL LAUNCH]: Update the default Haiku model (3P providers may lag so keep defaults unchanged).
export function getDefaultHaikuModel(): ModelName {
if (isOpenAIAuthActive()) {
return resolveOpenAICodexModel('haiku')
}
if (process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL) {
return process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL
}
@ -188,6 +202,10 @@ export function getDefaultMainLoopModelSetting(): ModelName | ModelAlias {
)
}
if (isOpenAIAuthActive()) {
return resolveOpenAICodexModel('opus')
}
// Max users get Opus as default
if (isMaxSubscriber()) {
return getDefaultOpusModel() + (isOpus1mMergeEnabled() ? '[1m]' : '')
@ -290,6 +308,12 @@ export function getCanonicalName(fullModelName: ModelName): ModelShortName {
export function getClaudeAiUserDefaultModelDescription(
fastMode = false,
): string {
if (isOpenAIAuthActive()) {
const defaultModel = renderDefaultModelSetting(
getDefaultMainLoopModelSetting(),
)
return `${defaultModel} · OpenAI/Codex default`
}
if (isMaxSubscriber() || isTeamPremiumSubscriber()) {
if (isOpus1mMergeEnabled()) {
return `Opus 4.7 with 1M context · Most capable for complex work${fastMode ? getOpus46PricingSuffix(true) : ''}`
@ -351,6 +375,10 @@ export function renderModelSetting(setting: ModelName | ModelAlias): string {
* if the model is not recognized as a public model.
*/
export function getPublicModelDisplayName(model: ModelName): string | null {
const openAIModelName = getOpenAIModelDisplayName(model)
if (openAIModelName) {
return openAIModelName
}
switch (model) {
case getModelStrings().opus46:
return 'Opus 4.7'
@ -572,6 +600,10 @@ export function modelDisplayString(model: ModelSetting): string {
// @[MODEL LAUNCH]: Add a marketing name mapping for the new model below.
export function getMarketingNameForModel(modelId: string): string | undefined {
const openAIModelName = getOpenAIModelDisplayName(modelId)
if (openAIModelName) {
return openAIModelName
}
if (getAPIProvider() === 'foundry') {
// deployment ID is user-defined in Foundry, so it may have no relation to the actual model
return undefined

View File

@ -1,8 +1,10 @@
// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
import { getInitialMainLoopModel } from '../../bootstrap/state.js'
import {
hasOpenAIAuthLogin,
isClaudeAISubscriber,
isMaxSubscriber,
isOpenAIAuthActive,
isTeamPremiumSubscriber,
} from '../auth.js'
import { getModelStrings } from './modelStrings.js'
@ -14,7 +16,7 @@ import {
} from '../modelCost.js'
import { getSettings_DEPRECATED } from '../settings/settings.js'
import { checkOpus1mAccess, checkSonnet1mAccess } from './check1mAccess.js'
import { getAPIProvider } from './providers.js'
import { getAPIProvider, isFirstPartyAnthropicBaseUrl } from './providers.js'
import { isModelAllowed } from './modelAllowlist.js'
import {
getCanonicalName,
@ -32,6 +34,7 @@ import {
} from './model.js'
import { has1mContext } from '../context.js'
import { getGlobalConfig } from '../config.js'
import { OPENAI_CODEX_MODEL_CATALOG } from '../../services/openaiAuth/models.js'
// @[MODEL LAUNCH]: Update all the available and default model option strings below.
@ -42,6 +45,25 @@ export type ModelOption = {
descriptionForModel?: string
}
function hasAnthropicCompatibleThirdPartyConfig(): boolean {
return getAPIProvider() !== 'firstParty' || !isFirstPartyAnthropicBaseUrl()
}
function pushUniqueOption(
options: ModelOption[],
option: ModelOption | undefined,
): void {
if (!option) {
return
}
if (options.some(existing => existing.value === option.value)) {
return
}
options.push(option)
}
export function getDefaultOptionForUser(fastMode = false): ModelOption {
if (process.env.USER_TYPE === 'ant') {
const currentModel = renderDefaultModelSetting(
@ -55,6 +77,18 @@ export function getDefaultOptionForUser(fastMode = false): ModelOption {
}
}
if (isOpenAIAuthActive()) {
const currentModel = renderDefaultModelSetting(
getDefaultMainLoopModelSetting(),
)
return {
value: null,
label: 'Default (recommended)',
description: `Use the OpenAI/Codex default model (currently ${currentModel})`,
descriptionForModel: `OpenAI/Codex default model (currently ${currentModel})`,
}
}
// Subscribers
if (isClaudeAISubscriber()) {
return {
@ -65,7 +99,7 @@ export function getDefaultOptionForUser(fastMode = false): ModelOption {
}
// PAYG
const is3P = getAPIProvider() !== 'firstParty'
const is3P = hasAnthropicCompatibleThirdPartyConfig()
return {
value: null,
label: 'Default (recommended)',
@ -74,7 +108,7 @@ export function getDefaultOptionForUser(fastMode = false): ModelOption {
}
function getCustomSonnetOption(): ModelOption | undefined {
const is3P = getAPIProvider() !== 'firstParty'
const is3P = hasAnthropicCompatibleThirdPartyConfig()
const customSonnetModel = process.env.ANTHROPIC_DEFAULT_SONNET_MODEL
// When a 3P user has a custom sonnet model string, show it directly
if (is3P && customSonnetModel) {
@ -94,7 +128,7 @@ function getCustomSonnetOption(): ModelOption | undefined {
// @[MODEL LAUNCH]: Update or add model option functions (getSonnetXXOption, getOpusXXOption, etc.)
// with the new model's label and description. These appear in the /model picker.
function getSonnet46Option(): ModelOption {
const is3P = getAPIProvider() !== 'firstParty'
const is3P = hasAnthropicCompatibleThirdPartyConfig()
return {
value: is3P ? getModelStrings().sonnet46 : 'sonnet',
label: 'Sonnet',
@ -105,7 +139,7 @@ function getSonnet46Option(): ModelOption {
}
function getCustomOpusOption(): ModelOption | undefined {
const is3P = getAPIProvider() !== 'firstParty'
const is3P = hasAnthropicCompatibleThirdPartyConfig()
const customOpusModel = process.env.ANTHROPIC_DEFAULT_OPUS_MODEL
// When a 3P user has a custom opus model string, show it directly
if (is3P && customOpusModel) {
@ -141,7 +175,7 @@ function getOpus46Option(fastMode = false): ModelOption {
}
export function getSonnet46_1MOption(): ModelOption {
const is3P = getAPIProvider() !== 'firstParty'
const is3P = hasAnthropicCompatibleThirdPartyConfig()
return {
value: is3P ? getModelStrings().sonnet46 + '[1m]' : 'sonnet[1m]',
label: 'Sonnet (1M context)',
@ -152,7 +186,7 @@ export function getSonnet46_1MOption(): ModelOption {
}
export function getOpus46_1MOption(fastMode = false): ModelOption {
const is3P = getAPIProvider() !== 'firstParty'
const is3P = hasAnthropicCompatibleThirdPartyConfig()
return {
value: is3P ? getModelStrings().opus46 + '[1m]' : 'opus[1m]',
label: 'Opus (1M context)',
@ -163,7 +197,7 @@ export function getOpus46_1MOption(fastMode = false): ModelOption {
}
function getCustomHaikuOption(): ModelOption | undefined {
const is3P = getAPIProvider() !== 'firstParty'
const is3P = hasAnthropicCompatibleThirdPartyConfig()
const customHaikuModel = process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL
// When a 3P user has a custom haiku model string, show it directly
if (is3P && customHaikuModel) {
@ -179,7 +213,7 @@ function getCustomHaikuOption(): ModelOption | undefined {
}
function getHaiku45Option(): ModelOption {
const is3P = getAPIProvider() !== 'firstParty'
const is3P = hasAnthropicCompatibleThirdPartyConfig()
return {
value: 'haiku',
label: 'Haiku',
@ -217,7 +251,7 @@ function getMaxOpusOption(fastMode = false): ModelOption {
}
export function getMaxSonnet46_1MOption(): ModelOption {
const is3P = getAPIProvider() !== 'firstParty'
const is3P = hasAnthropicCompatibleThirdPartyConfig()
const billingInfo = isClaudeAISubscriber() ? ' · Billed as extra usage' : ''
return {
value: 'sonnet[1m]',
@ -236,7 +270,7 @@ export function getMaxOpus46_1MOption(fastMode = false): ModelOption {
}
function getMergedOpus1MOption(fastMode = false): ModelOption {
const is3P = getAPIProvider() !== 'firstParty'
const is3P = hasAnthropicCompatibleThirdPartyConfig()
return {
value: is3P ? getModelStrings().opus46 + '[1m]' : 'opus[1m]',
label: 'Opus (1M context)',
@ -295,6 +329,27 @@ function getAzureOpenAICodexOptions(): ModelOption[] {
]
}
function getOpenAIModelOptions(): ModelOption[] {
const options = [
getDefaultOptionForUser(),
...OPENAI_CODEX_MODEL_CATALOG.map(model => ({
value: model.value,
label: model.label,
description: model.description,
descriptionForModel: model.descriptionForModel,
})),
]
if (!hasAnthropicCompatibleThirdPartyConfig()) {
return options
}
pushUniqueOption(options, getCustomSonnetOption())
pushUniqueOption(options, getCustomOpusOption())
pushUniqueOption(options, getCustomHaikuOption())
return options
}
// @[MODEL LAUNCH]: Update the model picker lists below to include/reorder options for the new model.
// Each user tier (ant, Max/Team Premium, Pro/Team Standard/Enterprise, PAYG 1P, PAYG 3P) has its own list.
function getModelOptionsBase(fastMode = false): ModelOption[] {
@ -320,6 +375,10 @@ function getModelOptionsBase(fastMode = false): ModelOption[] {
]
}
if (hasOpenAIAuthLogin()) {
return getOpenAIModelOptions(fastMode)
}
if (isClaudeAISubscriber()) {
if (isMaxSubscriber() || isTeamPremiumSubscriber()) {
// Max and Team Premium users: Opus is default, show Sonnet as alternative
@ -357,7 +416,7 @@ function getModelOptionsBase(fastMode = false): ModelOption[] {
}
// PAYG 1P API: Default (Sonnet) + Sonnet 1M + Opus 4.7 + Opus 1M + Haiku
if (getAPIProvider() === 'firstParty') {
if (!hasAnthropicCompatibleThirdPartyConfig()) {
const payg1POptions = [getDefaultOptionForUser(fastMode)]
if (checkSonnet1mAccess()) {
payg1POptions.push(getSonnet46_1MOption())
@ -379,7 +438,7 @@ function getModelOptionsBase(fastMode = false): ModelOption[] {
const customSonnet = getCustomSonnetOption()
if (customSonnet !== undefined) {
payg3pOptions.push(customSonnet)
pushUniqueOption(payg3pOptions, customSonnet)
} else {
// Add Sonnet 4.6 since Sonnet 4.5 is the default
payg3pOptions.push(getSonnet46Option())
@ -390,7 +449,7 @@ function getModelOptionsBase(fastMode = false): ModelOption[] {
const customOpus = getCustomOpusOption()
if (customOpus !== undefined) {
payg3pOptions.push(customOpus)
pushUniqueOption(payg3pOptions, customOpus)
} else {
// Add Opus 4.1, Opus 4.7 and Opus 4.7 1M
payg3pOptions.push(getOpus41Option()) // This is the default opus
@ -401,7 +460,7 @@ function getModelOptionsBase(fastMode = false): ModelOption[] {
}
const customHaiku = getCustomHaikuOption()
if (customHaiku !== undefined) {
payg3pOptions.push(customHaiku)
pushUniqueOption(payg3pOptions, customHaiku)
} else {
payg3pOptions.push(getHaikuOption())
}

View File

@ -3,6 +3,7 @@ import { MODEL_ALIASES } from './aliases.js'
import { isModelAllowed } from './modelAllowlist.js'
import { getAPIProvider } from './providers.js'
import { sideQuery } from '../sideQuery.js'
import { isOpenAIAuthActive } from '../auth.js'
import {
NotFoundError,
APIError,
@ -10,6 +11,7 @@ import {
AuthenticationError,
} from '@anthropic-ai/sdk'
import { getModelStrings } from './modelStrings.js'
import { isOpenAIResponsesModel } from '../../services/openaiAuth/models.js'
// Cache valid models to avoid repeated API calls
const validModelCache = new Map<string, boolean>()
@ -109,6 +111,11 @@ export async function validateModel(
return { valid: true }
}
if (isOpenAIAuthActive() && isOpenAIResponsesModel(normalizedModel)) {
validModelCache.set(normalizedModel, true)
return { valid: true }
}
// Check cache first
if (validModelCache.has(normalizedModel)) {
return { valid: true }

File diff suppressed because one or more lines are too long

View File

@ -145,12 +145,30 @@ export function getCurrentUsage(messages: Message[]): {
const message = messages[i]
const usage = message ? getTokenUsage(message) : undefined
if (usage) {
return {
const normalizedUsage = {
input_tokens: usage.input_tokens,
output_tokens: usage.output_tokens,
cache_creation_input_tokens: usage.cache_creation_input_tokens ?? 0,
cache_read_input_tokens: usage.cache_read_input_tokens ?? 0,
}
// Zero-token usage is used in a few recovery/compaction paths as a
// placeholder for "stale or unavailable", not as a real token count.
if (
normalizedUsage.input_tokens === 0 &&
normalizedUsage.output_tokens === 0 &&
normalizedUsage.cache_creation_input_tokens === 0 &&
normalizedUsage.cache_read_input_tokens === 0
) {
continue
}
return {
input_tokens: normalizedUsage.input_tokens,
output_tokens: normalizedUsage.output_tokens,
cache_creation_input_tokens: normalizedUsage.cache_creation_input_tokens,
cache_read_input_tokens: normalizedUsage.cache_read_input_tokens,
}
}
}
return null