Merge Azure OpenAI auth into local main

Remote main already contains PR #217. Local main had provider context-window and auth-strategy work, so this merge keeps both context resolvers and reconciles the desktop Bun lockfile with the merged desktop package manifest. The accidental remote .nvimlog artifact is excluded from the merge result.

Constraint: Local main was 15 commits ahead and 8 commits behind origin/main after PR #217 was merged remotely
Rejected: Fast-forward pull | local main had unpushed commits
Rejected: Keep remote desktop lockfile verbatim | failed bun install --frozen-lockfile against the merged package.json
Confidence: high
Scope-risk: broad
Directive: Keep provider-configured context windows ahead of OpenAI OAuth model windows so explicit user/provider settings win
Tested: bun install --frozen-lockfile; cd desktop && bun install --frozen-lockfile; git diff --check; bun run check:server; bun run check:desktop; ALLOW_CLI_CORE_CHANGE=1 bun run quality:pr
Not-tested: live provider OAuth login with a real OpenAI account
This commit is contained in:
程序员阿江(Relakkes) 2026-05-04 21:24:22 +08:00
commit 8a81636757
38 changed files with 2723 additions and 130 deletions

View File

@ -29,11 +29,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": {
@ -56,36 +56,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=="],
@ -116,6 +98,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", "https://registry.npmmirror.com/@emnapi/core/-/core-1.10.0.tgz", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
"@emnapi/runtime": ["@emnapi/runtime@1.10.0", "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.10.0.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", { "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=="],
@ -208,7 +196,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", "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", { "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", "https://registry.npmmirror.com/@oxc-project/types/-/types-0.127.0.tgz", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.17", "https://registry.npmmirror.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz", { "os": "android", "cpu": "arm64" }, "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ=="],
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.17", "https://registry.npmmirror.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw=="],
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.17", "https://registry.npmmirror.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw=="],
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.17", "https://registry.npmmirror.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw=="],
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17", "https://registry.npmmirror.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz", { "os": "linux", "cpu": "arm" }, "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ=="],
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17", "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz", { "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", "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg=="],
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17", "https://registry.npmmirror.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz", { "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", "https://registry.npmmirror.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA=="],
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.17", "https://registry.npmmirror.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz", { "os": "linux", "cpu": "x64" }, "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA=="],
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.17", "https://registry.npmmirror.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz", { "os": "linux", "cpu": "x64" }, "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw=="],
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.17", "https://registry.npmmirror.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz", { "os": "none", "cpu": "arm64" }, "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA=="],
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.17", "https://registry.npmmirror.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz", { "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", "https://registry.npmmirror.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA=="],
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.17", "https://registry.npmmirror.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz", { "os": "win32", "cpu": "x64" }, "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.7", "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", {}, "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=="],
@ -350,16 +372,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", "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", { "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=="],
@ -460,7 +476,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", "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", { "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=="],
@ -498,10 +514,6 @@
"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=="],
@ -510,8 +522,6 @@
"camelcase": ["camelcase@5.3.1", "https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="],
"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=="],
@ -548,7 +558,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=="],
@ -668,8 +678,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=="],
"emoji-regex": ["emoji-regex@8.0.0", "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"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=="],
@ -690,8 +698,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=="],
@ -712,8 +718,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-caller-file": ["get-caller-file@2.0.5", "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
"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=="],
@ -786,8 +790,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=="],
@ -832,7 +834,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=="],
@ -918,8 +920,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=="],
@ -966,7 +966,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", "https://registry.npmmirror.com/postcss/-/postcss-8.5.13.tgz", { "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=="],
@ -988,8 +988,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=="],
@ -1010,6 +1008,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", "https://registry.npmmirror.com/rolldown/-/rolldown-1.0.0-rc.17.tgz", { "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=="],
@ -1024,8 +1024,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=="],
"set-blocking": ["set-blocking@2.0.0", "https://registry.npmmirror.com/set-blocking/-/set-blocking-2.0.0.tgz", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="],
"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=="],
@ -1070,7 +1068,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", "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.16.tgz", { "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=="],
@ -1090,6 +1088,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", "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", {}, "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=="],
@ -1104,8 +1104,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=="],
@ -1114,7 +1112,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", "https://registry.npmmirror.com/vite/-/vite-8.0.10.tgz", { "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=="],
@ -1156,8 +1154,6 @@
"y18n": ["y18n@4.0.3", "https://registry.npmmirror.com/y18n/-/y18n-4.0.3.tgz", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="],
"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=="],
"yargs": ["yargs@15.4.1", "https://registry.npmmirror.com/yargs/-/yargs-15.4.1.tgz", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="],
@ -1170,10 +1166,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=="],
@ -1200,12 +1192,26 @@
"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", "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz", {}, "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=="],
"wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"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

@ -40,11 +40,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,12 +1,13 @@
// 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'
import { getConfiguredOrBuiltInModelContextWindow } from './model/modelContextWindows.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
@ -83,6 +84,11 @@ export function getContextWindowForModel(
return configuredWindow
}
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