From c71355781e30876b550d82688a3bcf4584fff5f1 Mon Sep 17 00:00:00 2001 From: Keeper Date: Fri, 26 Jun 2026 01:39:42 +0300 Subject: [PATCH 01/17] feat: add SOCKS5 proxy support - Add socks-proxy-agent dependency - Parse --proxy CLI arg and SOCKS5_PROXY env var - Route upstream HTTPS requests through SOCKS5 proxy - Log proxy config on startup - Expose socks5 status in /health endpoint - Document usage in README --- README.md | 27 ++ package-lock.json | 912 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 3 +- server.mjs | 25 ++ 4 files changed, 966 insertions(+), 1 deletion(-) create mode 100644 package-lock.json diff --git a/README.md b/README.md index f787a10..3a1b3f4 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,33 @@ sudo systemctl enable --now opencode-proxy |----------|---------|------| | `PROXY_PORT` | `6446` | Server port | | `KEYS_FILE` | `./api-keys.json` | API keys file path | +| `SOCKS5_PROXY` | _(none)_ | SOCKS5 proxy address for upstream requests | + +## SOCKS5 Proxy + +Route all upstream requests to opencode.ai through a SOCKS5 proxy. + +### CLI argument + +```bash +node server.mjs --proxy 127.0.0.1:9150 +node server.mjs --proxy socks5://user:pass@10.0.0.1:1080 +``` + +### Environment variable + +```bash +SOCKS5_PROXY=127.0.0.1:9150 node server.mjs +SOCKS5_PROXY=socks5://user:pass@10.0.0.1:1080 node server.mjs +``` + +CLI `--proxy` takes priority over `SOCKS5_PROXY`. If neither is set, requests go directly. + +### systemd with proxy + +```ini +Environment=SOCKS5_PROXY=127.0.0.1:9150 +``` ## How it works diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..636fee5 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,912 @@ +{ + "name": "opencode-free-proxy", + "version": "0.9.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "opencode-free-proxy", + "version": "0.9.0", + "license": "MIT", + "dependencies": { + "express": "^4.21.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/socks-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socks-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + } + } +} diff --git a/package.json b/package.json index 6e52acb..4b67c79 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "dev": "node --watch server.mjs" }, "dependencies": { - "express": "^4.21.0" + "express": "^4.21.0", + "socks-proxy-agent": "^8.0.5" }, "engines": { "node": ">=18" diff --git a/server.mjs b/server.mjs index a8444a2..79c4e6f 100644 --- a/server.mjs +++ b/server.mjs @@ -2,6 +2,27 @@ import express from "express"; import crypto from "crypto"; import https from "https"; import fs from "fs"; +import { SocksProxyAgent } from "socks-proxy-agent"; + +// ── SOCKS5 Proxy ────────────────────────────────────────────────── +function parseProxyArg() { + const args = process.argv.slice(2); + for (let i = 0; i < args.length; i++) { + if (args[i] === "--proxy" && args[i + 1]) return args[i + 1]; + } + return process.env.SOCKS5_PROXY || null; +} + +function normalizeProxyUrl(raw) { + if (!raw) return null; + if (!raw.startsWith("socks5://") && !raw.startsWith("socks4://")) { + return "socks5://" + raw; + } + return raw; +} + +const proxyUrl = normalizeProxyUrl(parseProxyArg()); +const socksAgent = proxyUrl ? new SocksProxyAgent(proxyUrl) : null; const app = express(); app.use(express.json({ limit: "10mb" })); @@ -86,6 +107,7 @@ function zenRequest(model, messages, stream, tools, tool_choice, sessionId) { "x-opencode-session": sessionId, }, timeout: 120000, + ...(socksAgent ? { agent: socksAgent } : {}), }, }; } @@ -549,12 +571,15 @@ app.post("/v1/messages", async (req, res) => { // ── Health ────────────────────────────────────────────────────────── app.get("/health", (_req, res) => res.json({ status: "ok", version: `v${PROXY_VERSION}`, models: MODELS.length, + socks5: proxyUrl || null, endpoints: ["/v1/chat/completions", "/v1/messages", "/v1/models"], })); // ── Start ────────────────────────────────────────────────────────── app.listen(PORT, "0.0.0.0", () => { console.log(`OpenCode Free Proxy v${PROXY_VERSION} on http://0.0.0.0:${PORT}`); + if (socksAgent) console.log(" SOCKS5 proxy:", proxyUrl); + else console.log(" No SOCKS5 proxy configured (use --proxy or SOCKS5_PROXY)"); console.log(" OpenAI: POST /v1/chat/completions"); console.log(" Anthropic: POST /v1/messages"); console.log(" Models: GET /v1/models"); From acb5c69ecda90a8cf1f40786d5b1d2e2c8438974 Mon Sep 17 00:00:00 2001 From: Keeper Date: Fri, 26 Jun 2026 01:59:05 +0300 Subject: [PATCH 02/17] chore: update available models list --- server.mjs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/server.mjs b/server.mjs index 79c4e6f..a4942be 100644 --- a/server.mjs +++ b/server.mjs @@ -62,7 +62,7 @@ function ocId(prefix) { const rnd = crypto.randomBytes(12).toString("base64url").slice(0, 16); return `${prefix}_${ts}${rnd}`; } - +/* // old models const MODELS = [ "deepseek-v4-flash-free", "big-pickle", @@ -70,6 +70,14 @@ const MODELS = [ "nemotron-3-super-free", "qwen3.6-plus-free", ]; +*/ +const MODELS = [ + "mimo-v2.5-free", + "deepseek-v4-flash-free", + "north-mini-code-free", + "nemotron-3-ultra-free", + "big-pickle", +]; // Track sessions per user (rotate every 30 min) const userSessions = {}; From 9ed21dff76a456c9581299a10ef283497e455cf2 Mon Sep 17 00:00:00 2001 From: Keeper Date: Fri, 26 Jun 2026 01:59:34 +0300 Subject: [PATCH 03/17] docs: update models list in README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3a1b3f4..1ed0c36 100644 --- a/README.md +++ b/README.md @@ -19,11 +19,11 @@ Done. Server is at `http://localhost:6446`. API keys are in `api-keys.json` (aut | Model | What it is | Reliability | |-------|-----------|-------------| +| `mimo-v2.5-free` | Xiaomi MiMo v2.5 | Solid | | `deepseek-v4-flash-free` | DeepSeek V4 Flash | Solid | +| `north-mini-code-free` | North Mini Code | Solid | +| `nemotron-3-ultra-free` | NVIDIA Nemotron 3 Ultra | Solid | | `big-pickle` | DeepSeek V4 Flash (alias) | Solid | -| `minimax-m2.5-free` | MiniMax M2.5 | Solid | -| `nemotron-3-super-free` | NVIDIA Nemotron 3 Super | Hit or miss | -| `qwen3.6-plus-free` | Qwen 3.6 Plus | Intermittent | All models support streaming, tool calls, and system messages. From cc3d0f84c875e693f07f9239d952b402bf09a9a9 Mon Sep 17 00:00:00 2001 From: Keeper Date: Tue, 7 Jul 2026 20:36:45 +0300 Subject: [PATCH 04/17] refactor: rewrite server in Python (FastAPI + httpx) - Replace Node.js/Express with Python/FastAPI - Use httpx with SOCKS5 proxy support (httpx[socks]) - Add requirements.txt with dependencies - Update README with Python instructions - Update package.json scripts --- .gitignore | 2 + README.md | 20 +- package.json | 13 +- requirements.txt | 3 + server.py | 586 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 603 insertions(+), 21 deletions(-) create mode 100644 requirements.txt create mode 100644 server.py diff --git a/.gitignore b/.gitignore index d475273..2d77ef3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ node_modules/ api-keys.json *.log .env +__pycache__/ +*.bak diff --git a/README.md b/README.md index 1ed0c36..1e1e86e 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ One server — works with any tool that speaks OpenAI or Anthropic format: Curso ```bash git clone https://github.com/bigdata2211it-web/opencode-free-proxy.git cd opencode-free-proxy -npm install -node server.mjs +pip install -r requirements.txt +python server.py ``` Done. Server is at `http://localhost:6446`. API keys are in `api-keys.json` (auto-generated on first run). @@ -113,10 +113,10 @@ Add to `~/.config/opencode/opencode.json`: # On your VPS git clone https://github.com/bigdata2211it-web/opencode-free-proxy.git cd opencode-free-proxy -npm install -node server.mjs # foreground +pip install -r requirements.txt +python server.py # foreground # or -nohup node server.mjs > proxy.log 2>&1 & # background +nohup python server.py > proxy.log 2>&1 & # background ``` If your VPS doesn't expose port 6446, use an SSH tunnel: @@ -137,7 +137,7 @@ After=network.target [Service] Type=simple WorkingDirectory=/opt/opencode-proxy -ExecStart=/usr/bin/node server.mjs +ExecStart=/usr/bin/python3 server.py Restart=always RestartSec=5 Environment=PROXY_PORT=6446 @@ -165,15 +165,15 @@ Route all upstream requests to opencode.ai through a SOCKS5 proxy. ### CLI argument ```bash -node server.mjs --proxy 127.0.0.1:9150 -node server.mjs --proxy socks5://user:pass@10.0.0.1:1080 +python server.py --proxy 127.0.0.1:9150 +python server.py --proxy socks5://user:pass@10.0.0.1:1080 ``` ### Environment variable ```bash -SOCKS5_PROXY=127.0.0.1:9150 node server.mjs -SOCKS5_PROXY=socks5://user:pass@10.0.0.1:1080 node server.mjs +SOCKS5_PROXY=127.0.0.1:9150 python server.py +SOCKS5_PROXY=socks5://user:pass@10.0.0.1:1080 python server.py ``` CLI `--proxy` takes priority over `SOCKS5_PROXY`. If neither is set, requests go directly. diff --git a/package.json b/package.json index 4b67c79..346b8f2 100644 --- a/package.json +++ b/package.json @@ -2,18 +2,9 @@ "name": "opencode-free-proxy", "version": "0.9.0", "description": "Proxy server for OpenCode free-tier AI models via Zen API", - "type": "module", - "main": "server.mjs", + "main": "server.py", "scripts": { - "start": "node server.mjs", - "dev": "node --watch server.mjs" - }, - "dependencies": { - "express": "^4.21.0", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">=18" + "start": "python server.py" }, "license": "MIT" } diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..256316f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +fastapi>=0.115.0 +uvicorn>=0.32.0 +httpx[socks]>=0.28.0 diff --git a/server.py b/server.py new file mode 100644 index 0000000..52e1c32 --- /dev/null +++ b/server.py @@ -0,0 +1,586 @@ +import json +import os +import secrets +import sys +import time +from datetime import datetime, timezone + +import httpx +import uvicorn +from fastapi import FastAPI, Request, Response +from fastapi.responses import JSONResponse, StreamingResponse + +# ── SOCKS5 Proxy ────────────────────────────────────────────────── + +def parse_proxy_arg() -> str | None: + args = sys.argv[1:] + for i, arg in enumerate(args): + if arg == "--proxy" and i + 1 < len(args): + return args[i + 1] + return os.environ.get("SOCKS5_PROXY") or None + + +def normalize_proxy_url(raw: str | None) -> str | None: + if not raw: + return None + if not raw.startswith("socks5://") and not raw.startswith("socks4://"): + return "socks5://" + raw + return raw + + +PROXY_URL = normalize_proxy_url(parse_proxy_arg()) +PROXY_MOUNT = None +if PROXY_URL: + PROXY_MOUNT = httpx.Mount(proxy=PROXY_URL) + +http_client = httpx.AsyncClient( + base_url="https://opencode.ai", + timeout=httpx.Timeout(120.0), + proxy=PROXY_URL, +) + +# ── App ─────────────────────────────────────────────────────────── + +app = FastAPI() + +PORT = int(os.environ.get("PROXY_PORT", "6446")) +OC_VERSION = "1.15.0" +PROXY_VERSION = "9" + +# ── API Keys ────────────────────────────────────────────────────── + +KEYS_FILE = os.environ.get("KEYS_FILE", "./api-keys.json") +api_keys: dict[str, str] = {} + + +def load_keys(): + global api_keys + try: + with open(KEYS_FILE, "r", encoding="utf-8") as f: + api_keys = json.load(f) + except (FileNotFoundError, json.JSONDecodeError): + api_keys = {} + if not api_keys: + api_keys = { + "admin": "oc-" + secrets.token_hex(20), + "user-default": "oc-" + secrets.token_hex(20), + } + with open(KEYS_FILE, "w", encoding="utf-8") as f: + json.dump(api_keys, f, indent=2) + print(f"[INIT] Generated new API keys -> {KEYS_FILE}") + + +load_keys() + + +def auth(request: Request) -> str | None: + hdr = request.headers.get("authorization") or request.headers.get("x-api-key") or "" + tok = hdr[7:] if hdr.startswith("Bearer ") else hdr + for name, key in api_keys.items(): + if tok == key: + return name + return None + + +# ── Helpers ─────────────────────────────────────────────────────── + +def oc_id(prefix: str) -> str: + ts = format(int(time.time() * 1000), "x") + rnd = secrets.token_urlsafe(12)[:16] + return f"{prefix}_{ts}{rnd}" + + +MODELS = [ + "mimo-v2.5-free", + "deepseek-v4-flash-free", + "north-mini-code-free", + "nemotron-3-ultra-free", + "big-pickle", +] + +# Track sessions per user (rotate every 30 min) +user_sessions: dict[str, dict] = {} + + +def get_session(user: str) -> str: + now = time.time() * 1000 + if user not in user_sessions or now - user_sessions[user]["ts"] > 30 * 60 * 1000: + user_sessions[user] = {"id": oc_id("ses"), "ts": now} + return user_sessions[user]["id"] + + +# ── Zen API transport ───────────────────────────────────────────── + +def zen_request(model, messages, stream, tools, tool_choice, session_id): + req_body: dict = {"model": model, "messages": messages, "stream": bool(stream)} + if tools: + req_body["tools"] = tools + if tool_choice: + req_body["tool_choice"] = tool_choice + + request_id = oc_id("msg") + headers = { + "Content-Type": "application/json", + "Authorization": "Bearer public", + "User-Agent": f"opencode/{OC_VERSION} ai-sdk/provider-utils/4.0.23 runtime/bun/1.3.13", + "x-opencode-client": "cli", + "x-opencode-project": "global", + "x-opencode-request": request_id, + "x-opencode-session": session_id, + } + return req_body, headers + + +# ── Anthropic Messages → OpenAI conversion ──────────────────────── + +def anthropic_to_openai(body: dict) -> tuple[list[dict], list[dict] | None]: + messages = [] + + if body.get("system"): + sys_val = body["system"] + if isinstance(sys_val, str): + sys_text = sys_val + elif isinstance(sys_val, list): + sys_text = "\n".join(b.get("text", "") for b in sys_val) + else: + sys_text = "" + if sys_text: + messages.append({"role": "system", "content": sys_text}) + + for msg in body.get("messages", []): + content = msg.get("content") + if isinstance(content, str): + messages.append({"role": msg["role"], "content": content}) + elif isinstance(content, list): + text = "\n".join(b.get("text", "") for b in content if b.get("type") == "text") + tool_uses = [b for b in content if b.get("type") == "tool_use"] + + if tool_uses and msg.get("role") == "assistant": + messages.append({ + "role": "assistant", + "content": text or None, + "tool_calls": [ + { + "id": t["id"], + "type": "function", + "function": { + "name": t["name"], + "arguments": json.dumps(t.get("input") or {}), + }, + } + for t in tool_uses + ], + }) + elif any(b.get("type") == "tool_result" for b in content): + for b in content: + if b.get("type") == "tool_result": + c = b.get("content") + if isinstance(c, str): + result_text = c + elif isinstance(c, list): + result_text = "\n".join(x.get("text", "") for x in c) + else: + result_text = "" + messages.append({ + "role": "tool", + "tool_call_id": b["tool_use_id"], + "content": result_text, + }) + else: + messages.append({"role": msg["role"], "content": text}) + else: + messages.append({"role": msg["role"], "content": str(content)}) + + tools_out = [ + { + "type": "function", + "function": { + "name": t["name"], + "description": t.get("description") or "", + "parameters": t.get("input_schema") or {}, + }, + } + for t in body.get("tools", []) + ] + + return messages, tools_out or None + + +# ── OpenAI response → Anthropic Messages format ────────────────── + +def openai_to_anthropic(oai_resp: dict, model: str, input_tokens: int) -> dict: + choice = (oai_resp.get("choices") or [None])[0] + if not choice: + return { + "id": oc_id("msg"), + "type": "message", + "role": "assistant", + "content": [{"type": "text", "text": ""}], + "model": model, + "stop_reason": "end_turn", + "usage": { + "input_tokens": input_tokens or 0, + "output_tokens": 0, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + }, + } + + content = [] + msg = choice.get("message") or {} + if msg.get("content"): + content.append({"type": "text", "text": msg["content"]}) + for tc in msg.get("tool_calls", []): + try: + inp = json.loads(tc["function"]["arguments"]) + except (json.JSONDecodeError, KeyError): + inp = {} + content.append({ + "type": "tool_use", + "id": tc.get("id") or oc_id("toolu"), + "name": tc["function"]["name"], + "input": inp, + }) + if not content: + content.append({"type": "text", "text": ""}) + + stop_reason = "end_turn" + fr = choice.get("finish_reason") + if fr == "tool_calls": + stop_reason = "tool_use" + elif fr == "length": + stop_reason = "max_tokens" + + return { + "id": oc_id("msg"), + "type": "message", + "role": "assistant", + "content": content, + "model": model, + "stop_reason": stop_reason, + "usage": { + "input_tokens": (oai_resp.get("usage") or {}).get("prompt_tokens") or input_tokens or 0, + "output_tokens": (oai_resp.get("usage") or {}).get("completion_tokens") or 0, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + }, + } + + +# ── Routes: OpenAI format ───────────────────────────────────────── + +@app.get("/v1/models") +async def list_models(): + return { + "object": "list", + "data": [ + {"id": m, "object": "model", "created": 1779000000, "owned_by": "opencode-free"} + for m in MODELS + ], + } + + +@app.post("/v1/chat/completions") +async def chat_completions(request: Request): + user = auth(request) + if not user: + return JSONResponse(status_code=401, content={"error": {"message": "Invalid API key"}}) + + body = await request.json() + model = body.get("model") + messages = body.get("messages") + stream = body.get("stream") + tools = body.get("tools") + tool_choice = body.get("tool_choice") + + if model not in MODELS: + return JSONResponse( + status_code=400, + content={"error": {"message": f"Unknown model: {model}. Available: {', '.join(MODELS)}"}}, + ) + + session_id = get_session(user) + msg_summary = [ + {"role": m.get("role"), "len": len(m.get("content") if isinstance(m.get("content"), str) else json.dumps(m.get("content") or ""))} + for m in (messages or []) + ] + ts = datetime.now(timezone.utc).isoformat() + print(f"[OAI] {ts} {user} {model} {'stream' if stream else 'sync'} msgs: {json.dumps(msg_summary)}") + + req_body, headers = zen_request(model, messages, stream, tools, tool_choice, session_id) + + if stream: + return StreamingResponse( + stream_openai(req_body, headers), + media_type="text/event-stream", + headers={ + "Cache-Control": "no-cache, no-transform", + "X-Accel-Buffering": "no", + }, + ) + else: + try: + resp = await http_client.post( + "/zen/v1/chat/completions", + json=req_body, + headers=headers, + ) + data = resp.json() + if resp.status_code == 429 or data.get("error"): + err_msg = (data.get("error") or {}).get("message") or "Rate limit exceeded" + return JSONResponse( + status_code=429, + content={"error": {"message": err_msg + " (free model rate limit)", "type": "rate_limit_error", "code": "rate_limit_exceeded"}}, + ) + return data + except Exception as e: + print(f"[ZEN ERROR] {e}") + return JSONResponse(status_code=502, content={"error": {"message": f"Upstream error: {e}", "type": "upstream_error"}}) + + +async def stream_openai(req_body: dict, headers: dict): + async with http_client.stream("POST", "/zen/v1/chat/completions", json=req_body, headers=headers) as resp: + if resp.status_code == 429: + try: + raw = await resp.aread() + data = json.loads(raw) + err_msg = (data.get("error") or {}).get("message") or "Rate limit exceeded" + except Exception: + err_msg = "Rate limit exceeded" + yield f'data: {json.dumps({"error": {"message": err_msg + " (free model rate limit)", "type": "rate_limit_error", "code": "rate_limit_exceeded"}})}\n\n' + return + + async for line in resp.aiter_lines(): + if line: + yield line + "\n" + + +# ── Routes: Anthropic Messages format ───────────────────────────── + +@app.post("/v1/messages") +async def messages(request: Request): + user = auth(request) + if not user: + return JSONResponse( + status_code=401, + content={"type": "error", "error": {"type": "authentication_error", "message": "Invalid API key"}}, + ) + + body = await request.json() + model = body.get("model") + stream = body.get("stream") + + if model not in MODELS: + return JSONResponse( + status_code=400, + content={"type": "error", "error": {"type": "invalid_request_error", "message": f"Unknown model: {model}. Available: {', '.join(MODELS)}"}}, + ) + + session_id = get_session(user) + oai_messages, tools = anthropic_to_openai(body) + input_tokens = len(json.dumps(oai_messages)) // 4 + + ts = datetime.now(timezone.utc).isoformat() + print(f"[ANT] {ts} {user} {model} {'stream' if stream else 'sync'} msgs: {len(oai_messages)}") + + req_body, headers = zen_request(model, oai_messages, stream, tools, None, session_id) + + if stream: + return StreamingResponse( + stream_anthropic(req_body, headers, model, input_tokens), + media_type="text/event-stream", + headers={ + "Cache-Control": "no-cache, no-transform", + "X-Accel-Buffering": "no", + }, + ) + else: + try: + resp = await http_client.post( + "/zen/v1/chat/completions", + json=req_body, + headers=headers, + ) + data = resp.json() + if resp.status_code == 429 or data.get("error"): + err_msg = (data.get("error") or {}).get("message") or "Rate limit exceeded" + return JSONResponse( + status_code=429, + content={"type": "error", "error": {"type": "rate_limit_error", "message": err_msg + " (free model rate limit)"}}, + ) + if not data.get("choices"): + return JSONResponse( + status_code=502, + content={"type": "error", "error": {"type": "upstream_error", "message": "Invalid upstream response"}}, + ) + return openai_to_anthropic(data, model, input_tokens) + except Exception as e: + print(f"[ZEN ERROR] {e}") + return JSONResponse(status_code=502, content={"type": "error", "error": {"type": "upstream_error", "message": str(e)}}) + + +async def stream_anthropic(req_body: dict, headers: dict, model: str, input_tokens: int): + msg_id = oc_id("msg") + buffer = "" + content_idx = 0 + tool_idx = -1 + output_tokens = 0 + headers_sent = False + + def send_sse(event: str, data: dict) -> str: + return f"event: {event}\ndata: {json.dumps(data)}\n\n" + + try: + async with http_client.stream("POST", "/zen/v1/chat/completions", json=req_body, headers=headers) as resp: + if resp.status_code == 429: + try: + raw = await resp.aread() + parsed = json.loads(raw) + err_msg = (parsed.get("error") or {}).get("message") or "Rate limit" + except Exception: + err_msg = "Rate limit" + yield send_sse("error", {"type": "error", "error": {"type": "rate_limit_error", "message": err_msg + " (free model rate limit)"}}) + return + + async for raw_line in resp.aiter_lines(): + if not raw_line: + continue + + # Check for errors on first chunk + if not headers_sent: + trimmed = raw_line.strip() + if trimmed.startswith("{") and ("FreeUsageLimitError" in trimmed or '"error"' in trimmed): + try: + parsed = json.loads(trimmed) + if parsed.get("error") or parsed.get("type") == "error": + err_msg = (parsed.get("error") or {}).get("message") or parsed.get("message") or "Rate limit" + yield send_sse("error", {"type": "error", "error": {"type": "rate_limit_error", "message": err_msg + " (free model rate limit)"}}) + return + except json.JSONDecodeError: + pass + + # Process SSE lines from upstream + if raw_line.startswith("data: "): + payload = raw_line[6:].strip() + if payload == "[DONE]": + # Close open blocks + total_blocks = (1 if content_idx > 0 else 0) + (tool_idx + 1 if tool_idx >= 0 else 0) + for i in range(total_blocks): + yield send_sse("content_block_stop", {"type": "content_block_stop", "index": i}) + yield send_sse("message_delta", { + "type": "message_delta", + "delta": {"stop_reason": "end_turn"}, + "usage": {"output_tokens": output_tokens}, + }) + yield send_sse("message_stop", {"type": "message_stop"}) + return + + try: + parsed = json.loads(payload) + except json.JSONDecodeError: + continue + + delta = (parsed.get("choices") or [{}])[0].get("delta") or {} + if not delta: + continue + + if not headers_sent: + headers_sent = True + yield send_sse("message_start", { + "type": "message_start", + "message": { + "id": msg_id, "type": "message", "role": "assistant", "content": [], + "model": model, "stop_reason": None, + "usage": {"input_tokens": input_tokens or 0, "output_tokens": 0, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0}, + }, + }) + + # Text content + if delta.get("content"): + if content_idx == 0 and tool_idx == -1: + yield send_sse("content_block_start", {"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}}) + content_idx = 1 + yield send_sse("content_block_delta", { + "type": "content_block_delta", "index": 0, + "delta": {"type": "text_delta", "text": delta["content"]}, + }) + output_tokens += -(-len(delta["content"]) // 4) # ceil division + + # Tool calls + for tc in delta.get("tool_calls", []): + idx = tc.get("index", 0) + if idx > tool_idx: + if tool_idx == -1 and content_idx > 0: + yield send_sse("content_block_stop", {"type": "content_block_stop", "index": 0}) + tool_idx = idx + block_idx = idx + 1 if content_idx > 0 else idx + yield send_sse("content_block_start", { + "type": "content_block_start", "index": block_idx, + "content_block": {"type": "tool_use", "id": tc.get("id") or oc_id("toolu"), "name": (tc.get("function") or {}).get("name") or ""}, + }) + func = tc.get("function") or {} + if func.get("arguments"): + block_idx = idx + 1 if content_idx > 0 else idx + yield send_sse("content_block_delta", { + "type": "content_block_delta", "index": block_idx, + "delta": {"type": "input_json_delta", "partial_json": func["arguments"]}, + }) + output_tokens += -(-len(func["arguments"]) // 4) + + # Finish + finish_reason = (parsed.get("choices") or [{}])[0].get("finish_reason") + if finish_reason: + total_blocks = (1 if content_idx > 0 else 0) + (tool_idx + 1 if tool_idx >= 0 else 0) + for i in range(total_blocks): + yield send_sse("content_block_stop", {"type": "content_block_stop", "index": i}) + + stop_reason = "end_turn" + if finish_reason == "tool_calls": + stop_reason = "tool_use" + elif finish_reason == "length": + stop_reason = "max_tokens" + + yield send_sse("message_delta", { + "type": "message_delta", + "delta": {"stop_reason": stop_reason}, + "usage": {"output_tokens": output_tokens}, + }) + yield send_sse("message_stop", {"type": "message_stop"}) + return + + except httpx.HTTPError as e: + print(f"[ZEN ERROR] {e}") + if not headers_sent: + yield send_sse("error", {"type": "error", "error": {"type": "upstream_error", "message": str(e)}}) + + +# ── Health ──────────────────────────────────────────────────────── + +@app.get("/health") +async def health(): + return { + "status": "ok", + "version": f"v{PROXY_VERSION}", + "models": len(MODELS), + "socks5": PROXY_URL, + "endpoints": ["/v1/chat/completions", "/v1/messages", "/v1/models"], + } + + +# ── Start ───────────────────────────────────────────────────────── + +if __name__ == "__main__": + print(f"OpenCode Free Proxy v{PROXY_VERSION} on http://0.0.0.0:{PORT}") + if PROXY_URL: + print(f" SOCKS5 proxy: {PROXY_URL}") + else: + print(" No SOCKS5 proxy configured (use --proxy or SOCKS5_PROXY)") + print(" OpenAI: POST /v1/chat/completions") + print(" Anthropic: POST /v1/messages") + print(" Models: GET /v1/models") + print(" Health: GET /health") + print(f" Models: {', '.join(MODELS)}") + for name, key in api_keys.items(): + print(f" {name:<15} {key}") + + uvicorn.run(app, host="0.0.0.0", port=PORT, log_level="info") From 7885810b010be80023fb070832ad02eb9d41e787 Mon Sep 17 00:00:00 2001 From: Keeper Date: Tue, 7 Jul 2026 20:37:37 +0300 Subject: [PATCH 05/17] chore: remove Node.js artifacts --- .gitignore | 2 +- package-lock.json | 912 ---------------------------------------------- server.mjs | 599 ------------------------------ 3 files changed, 1 insertion(+), 1512 deletions(-) delete mode 100644 package-lock.json delete mode 100644 server.mjs diff --git a/.gitignore b/.gitignore index 2d77ef3..7b74f6b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ -node_modules/ api-keys.json *.log .env __pycache__/ +*.pyc *.bak diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 636fee5..0000000 --- a/package-lock.json +++ /dev/null @@ -1,912 +0,0 @@ -{ - "name": "opencode-free-proxy", - "version": "0.9.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "opencode-free-proxy", - "version": "0.9.0", - "license": "MIT", - "dependencies": { - "express": "^4.21.0", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, - "node_modules/body-parser": { - "version": "1.20.5", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", - "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "~1.2.0", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.15.1", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", - "license": "MIT" - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", - "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", - "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "~1.20.5", - "content-disposition": "~0.5.4", - "content-type": "~1.0.4", - "cookie": "~0.7.1", - "cookie-signature": "~1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.3.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "~0.1.12", - "proxy-addr": "~2.0.7", - "qs": "~6.15.1", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "~0.19.0", - "serve-static": "~1.16.2", - "setprototypeof": "1.2.0", - "statuses": "~2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/finalhandler": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", - "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "statuses": "~2.0.2", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ip-address": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", - "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", - "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", - "license": "MIT" - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/qs": { - "version": "6.15.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", - "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", - "license": "BSD-3-Clause", - "dependencies": { - "es-define-property": "^1.0.1", - "side-channel": "^1.1.1" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/send": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", - "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.1", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "~2.4.1", - "range-parser": "~1.2.1", - "statuses": "~2.0.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/serve-static": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", - "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "~0.19.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/side-channel": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", - "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4", - "side-channel-list": "^1.0.1", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "license": "MIT", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", - "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", - "license": "MIT", - "dependencies": { - "ip-address": "^10.1.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/socks-proxy-agent/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socks-proxy-agent/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - } - } -} diff --git a/server.mjs b/server.mjs deleted file mode 100644 index a4942be..0000000 --- a/server.mjs +++ /dev/null @@ -1,599 +0,0 @@ -import express from "express"; -import crypto from "crypto"; -import https from "https"; -import fs from "fs"; -import { SocksProxyAgent } from "socks-proxy-agent"; - -// ── SOCKS5 Proxy ────────────────────────────────────────────────── -function parseProxyArg() { - const args = process.argv.slice(2); - for (let i = 0; i < args.length; i++) { - if (args[i] === "--proxy" && args[i + 1]) return args[i + 1]; - } - return process.env.SOCKS5_PROXY || null; -} - -function normalizeProxyUrl(raw) { - if (!raw) return null; - if (!raw.startsWith("socks5://") && !raw.startsWith("socks4://")) { - return "socks5://" + raw; - } - return raw; -} - -const proxyUrl = normalizeProxyUrl(parseProxyArg()); -const socksAgent = proxyUrl ? new SocksProxyAgent(proxyUrl) : null; - -const app = express(); -app.use(express.json({ limit: "10mb" })); - -const PORT = process.env.PROXY_PORT || 6446; -const OC_VERSION = "1.15.0"; -const PROXY_VERSION = "9"; - -// ── API Keys ─────────────────────────────────────────────────────── -const keysFile = process.env.KEYS_FILE || "./api-keys.json"; -let apiKeys = {}; -function loadKeys() { - try { apiKeys = JSON.parse(fs.readFileSync(keysFile, "utf8")); } catch {} - if (Object.keys(apiKeys).length === 0) { - apiKeys = { - admin: "oc-" + crypto.randomBytes(20).toString("hex"), - "user-default": "oc-" + crypto.randomBytes(20).toString("hex"), - }; - fs.writeFileSync(keysFile, JSON.stringify(apiKeys, null, 2)); - console.log("[INIT] Generated new API keys →", keysFile); - } -} -loadKeys(); - -function auth(req) { - const hdr = req.headers.authorization || req.headers["x-api-key"] || ""; - const tok = hdr.startsWith("Bearer ") ? hdr.slice(7) : hdr; - for (const [name, key] of Object.entries(apiKeys)) { - if (tok === key) return name; - } - return null; -} - -// ── Helpers ──────────────────────────────────────────────────────── -function ocId(prefix) { - const ts = Date.now().toString(16); - const rnd = crypto.randomBytes(12).toString("base64url").slice(0, 16); - return `${prefix}_${ts}${rnd}`; -} -/* // old models -const MODELS = [ - "deepseek-v4-flash-free", - "big-pickle", - "minimax-m2.5-free", - "nemotron-3-super-free", - "qwen3.6-plus-free", -]; -*/ -const MODELS = [ - "mimo-v2.5-free", - "deepseek-v4-flash-free", - "north-mini-code-free", - "nemotron-3-ultra-free", - "big-pickle", -]; - -// Track sessions per user (rotate every 30 min) -const userSessions = {}; -function getSession(user) { - const now = Date.now(); - if (!userSessions[user] || now - userSessions[user].ts > 30 * 60 * 1000) { - userSessions[user] = { id: ocId("ses"), ts: now }; - } - return userSessions[user].id; -} - -// ── Zen API transport ────────────────────────────────────────────── -function zenRequest(model, messages, stream, tools, tool_choice, sessionId) { - const reqBody = { model, messages, stream: !!stream }; - if (tools?.length) reqBody.tools = tools; - if (tool_choice) reqBody.tool_choice = tool_choice; - const body = JSON.stringify(reqBody); - const requestId = ocId("msg"); - - return { - body, - options: { - hostname: "opencode.ai", - port: 443, - path: "/zen/v1/chat/completions", - method: "POST", - headers: { - "Content-Type": "application/json", - "Content-Length": Buffer.byteLength(body), - "Authorization": "Bearer public", - "User-Agent": `opencode/${OC_VERSION} ai-sdk/provider-utils/4.0.23 runtime/bun/1.3.13`, - "x-opencode-client": "cli", - "x-opencode-project": "global", - "x-opencode-request": requestId, - "x-opencode-session": sessionId, - }, - timeout: 120000, - ...(socksAgent ? { agent: socksAgent } : {}), - }, - }; -} - -// Pipe Zen response to client (OpenAI format passthrough) -function pipeZenResponse(zenOpts, body, stream, res) { - const req = https.request(zenOpts, (zenRes) => { - let firstChunk = null; - let headersSent = false; - - zenRes.on("data", (chunk) => { - if (!firstChunk) { - firstChunk = chunk; - const str = chunk.toString().trim(); - - if (str.startsWith("{") && (str.includes("FreeUsageLimitError") || str.includes('"error"'))) { - try { - const parsed = JSON.parse(str); - if (parsed.error || parsed.type === "error") { - const errMsg = parsed.error?.message || parsed.message || "Rate limit exceeded"; - console.log("[ZEN RATE LIMITED]", errMsg); - if (!res.headersSent) { - res.status(429).json({ - error: { message: errMsg + " (free model rate limit)", type: "rate_limit_error", code: "rate_limit_exceeded" } - }); - } - zenRes.resume(); - return; - } - } catch {} - } - - headersSent = true; - if (stream) { - res.writeHead(200, { - "Content-Type": "text/event-stream", - "Cache-Control": "no-cache, no-transform", - "Connection": "keep-alive", - "X-Accel-Buffering": "no", - "Transfer-Encoding": "chunked", - }); - res.flushHeaders(); - } else { - res.writeHead(zenRes.statusCode, { "Content-Type": "application/json" }); - } - res.write(firstChunk); - if (res.flush) res.flush(); - return; - } - if (headersSent) { - res.write(chunk); - if (res.flush) res.flush(); - } - }); - - zenRes.on("end", () => { - if (!headersSent && !firstChunk) { - console.log("[ZEN EMPTY] No response from Zen API"); - if (!res.headersSent) { - res.status(502).json({ error: { message: "Empty response from upstream", type: "upstream_error" } }); - } - return; - } - if (headersSent) res.end(); - }); - }); - - req.on("error", (e) => { - console.log("[ZEN ERROR]", e.message); - if (!res.headersSent) { - res.status(502).json({ error: { message: "Upstream error: " + e.message, type: "upstream_error" } }); - } - }); - - req.on("timeout", () => { - req.destroy(); - console.log("[ZEN TIMEOUT]"); - if (!res.headersSent) { - res.status(504).json({ error: { message: "Upstream timeout", type: "timeout_error" } }); - } - }); - - req.write(body); - req.end(); -} - -// Collect full Zen response (non-streaming) and return parsed JSON -function zenRequestFull(zenOpts, body) { - return new Promise((resolve, reject) => { - const req = https.request(zenOpts, (zenRes) => { - const chunks = []; - zenRes.on("data", (c) => chunks.push(c)); - zenRes.on("end", () => { - const raw = Buffer.concat(chunks).toString(); - try { - resolve({ status: zenRes.statusCode, data: JSON.parse(raw), raw }); - } catch { - resolve({ status: zenRes.statusCode, data: null, raw }); - } - }); - }); - req.on("error", reject); - req.on("timeout", () => { req.destroy(); reject(new Error("timeout")); }); - req.write(body); - req.end(); - }); -} - -// ── Anthropic Messages → OpenAI conversion ───────────────────────── -function anthropicToOpenAI(body) { - const messages = []; - if (body.system) { - const sys = typeof body.system === "string" ? body.system - : Array.isArray(body.system) ? body.system.map(b => b.text || "").join("\n") : ""; - if (sys) messages.push({ role: "system", content: sys }); - } - for (const msg of body.messages || []) { - if (typeof msg.content === "string") { - messages.push({ role: msg.role, content: msg.content }); - } else if (Array.isArray(msg.content)) { - const text = msg.content - .filter(b => b.type === "text") - .map(b => b.text) - .join("\n"); - // tool_use blocks → assistant tool_calls - const toolUses = msg.content.filter(b => b.type === "tool_use"); - if (toolUses.length && msg.role === "assistant") { - messages.push({ - role: "assistant", - content: text || null, - tool_calls: toolUses.map(t => ({ - id: t.id, - type: "function", - function: { name: t.name, arguments: JSON.stringify(t.input || {}) }, - })), - }); - } else if (msg.content.some(b => b.type === "tool_result")) { - for (const b of msg.content.filter(b => b.type === "tool_result")) { - const resultText = typeof b.content === "string" ? b.content - : Array.isArray(b.content) ? b.content.map(c => c.text || "").join("\n") : ""; - messages.push({ role: "tool", tool_call_id: b.tool_use_id, content: resultText }); - } - } else { - messages.push({ role: msg.role, content: text }); - } - } - } - - const tools = (body.tools || []).map(t => ({ - type: "function", - function: { - name: t.name, - description: t.description || "", - parameters: t.input_schema || {}, - }, - })); - - return { messages, tools: tools.length ? tools : undefined }; -} - -// OpenAI response → Anthropic Messages format -function openAIToAnthropic(oaiResp, model, inputTokens) { - const choice = oaiResp.choices?.[0]; - if (!choice) { - return { - id: ocId("msg"), - type: "message", - role: "assistant", - content: [{ type: "text", text: "" }], - model, - stop_reason: "end_turn", - usage: { input_tokens: inputTokens || 0, output_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 }, - }; - } - - const content = []; - if (choice.message?.content) { - content.push({ type: "text", text: choice.message.content }); - } - if (choice.message?.tool_calls) { - for (const tc of choice.message.tool_calls) { - let input = {}; - try { input = JSON.parse(tc.function.arguments); } catch {} - content.push({ - type: "tool_use", - id: tc.id || ocId("toolu"), - name: tc.function.name, - input, - }); - } - } - if (!content.length) content.push({ type: "text", text: "" }); - - let stopReason = "end_turn"; - if (choice.finish_reason === "tool_calls") stopReason = "tool_use"; - else if (choice.finish_reason === "length") stopReason = "max_tokens"; - else if (choice.finish_reason === "stop") stopReason = "end_turn"; - - return { - id: ocId("msg"), - type: "message", - role: "assistant", - content, - model, - stop_reason: stopReason, - usage: { - input_tokens: oaiResp.usage?.prompt_tokens || inputTokens || 0, - output_tokens: oaiResp.usage?.completion_tokens || 0, - cache_creation_input_tokens: 0, - cache_read_input_tokens: 0, - }, - }; -} - -// Stream OpenAI SSE → Anthropic SSE -function pipeZenAsAnthropic(zenOpts, body, model, res, inputTokens) { - const msgId = ocId("msg"); - - const req = https.request(zenOpts, (zenRes) => { - let headersSent = false; - let buffer = ""; - let outputTokens = 0; - let contentIdx = 0; - let toolIdx = -1; - let firstChunkHandled = false; - - function sendSSE(event, data) { - res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`); - if (res.flush) res.flush(); - } - - function sendHeaders() { - if (headersSent) return; - headersSent = true; - res.writeHead(200, { - "Content-Type": "text/event-stream", - "Cache-Control": "no-cache, no-transform", - "Connection": "keep-alive", - "X-Accel-Buffering": "no", - }); - res.flushHeaders(); - - sendSSE("message_start", { - type: "message_start", - message: { - id: msgId, type: "message", role: "assistant", content: [], - model, stop_reason: null, - usage: { input_tokens: inputTokens || 0, output_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 }, - }, - }); - } - - zenRes.on("data", (chunk) => { - const str = chunk.toString(); - - // Check for errors on first chunk - if (!firstChunkHandled) { - firstChunkHandled = true; - const trimmed = str.trim(); - if (trimmed.startsWith("{") && (trimmed.includes("FreeUsageLimitError") || trimmed.includes('"error"'))) { - try { - const parsed = JSON.parse(trimmed); - if (parsed.error || parsed.type === "error") { - const errMsg = parsed.error?.message || parsed.message || "Rate limit"; - if (!res.headersSent) { - res.writeHead(429, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ - type: "error", - error: { type: "rate_limit_error", message: errMsg + " (free model rate limit)" }, - })); - } - zenRes.resume(); - return; - } - } catch {} - } - } - - buffer += str; - const lines = buffer.split("\n"); - buffer = lines.pop() || ""; - - for (const line of lines) { - if (!line.startsWith("data: ")) continue; - const payload = line.slice(6).trim(); - if (payload === "[DONE]") continue; - - let parsed; - try { parsed = JSON.parse(payload); } catch { continue; } - const delta = parsed.choices?.[0]?.delta; - if (!delta) continue; - - sendHeaders(); - - // Text content - if (delta.content) { - if (contentIdx === 0 && toolIdx === -1) { - sendSSE("content_block_start", { type: "content_block_start", index: 0, content_block: { type: "text", text: "" } }); - contentIdx = 1; - } - sendSSE("content_block_delta", { - type: "content_block_delta", index: 0, - delta: { type: "text_delta", text: delta.content }, - }); - outputTokens += Math.ceil(delta.content.length / 4); - } - - // Tool calls - if (delta.tool_calls) { - for (const tc of delta.tool_calls) { - const idx = tc.index ?? 0; - if (idx > toolIdx) { - // Close previous text block if open - if (toolIdx === -1 && contentIdx > 0) { - sendSSE("content_block_stop", { type: "content_block_stop", index: 0 }); - } - toolIdx = idx; - const blockIdx = contentIdx > 0 ? idx + 1 : idx; - sendSSE("content_block_start", { - type: "content_block_start", index: blockIdx, - content_block: { type: "tool_use", id: tc.id || ocId("toolu"), name: tc.function?.name || "" }, - }); - } - if (tc.function?.arguments) { - const blockIdx = contentIdx > 0 ? idx + 1 : idx; - sendSSE("content_block_delta", { - type: "content_block_delta", index: blockIdx, - delta: { type: "input_json_delta", partial_json: tc.function.arguments }, - }); - outputTokens += Math.ceil(tc.function.arguments.length / 4); - } - } - } - - // Finish - if (parsed.choices?.[0]?.finish_reason) { - const fr = parsed.choices[0].finish_reason; - // Close open blocks - const totalBlocks = (contentIdx > 0 ? 1 : 0) + (toolIdx >= 0 ? toolIdx + 1 : 0); - for (let i = 0; i < totalBlocks; i++) { - sendSSE("content_block_stop", { type: "content_block_stop", index: i }); - } - - let stopReason = "end_turn"; - if (fr === "tool_calls") stopReason = "tool_use"; - else if (fr === "length") stopReason = "max_tokens"; - - sendSSE("message_delta", { - type: "message_delta", - delta: { stop_reason: stopReason }, - usage: { output_tokens: outputTokens }, - }); - sendSSE("message_stop", { type: "message_stop" }); - } - } - }); - - zenRes.on("end", () => { - if (!headersSent) { - if (!res.headersSent) { - res.status(502).json({ type: "error", error: { type: "upstream_error", message: "Empty response" } }); - } - return; - } - res.end(); - }); - }); - - req.on("error", (e) => { - console.log("[ZEN ERROR]", e.message); - if (!res.headersSent) { - res.status(502).json({ type: "error", error: { type: "upstream_error", message: e.message } }); - } - }); - - req.on("timeout", () => { - req.destroy(); - if (!res.headersSent) { - res.status(504).json({ type: "error", error: { type: "timeout_error", message: "Upstream timeout" } }); - } - }); - - req.write(body); - req.end(); -} - -// ── Routes: OpenAI format ────────────────────────────────────────── -app.get("/v1/models", (_req, res) => { - res.json({ - object: "list", - data: MODELS.map((id) => ({ - id, object: "model", created: 1779000000, owned_by: "opencode-free", - })), - }); -}); - -app.post("/v1/chat/completions", (req, res) => { - const user = auth(req); - if (!user) return res.status(401).json({ error: { message: "Invalid API key" } }); - - const { model, messages, stream, tools, tool_choice } = req.body; - if (!MODELS.includes(model)) { - return res.status(400).json({ error: { message: `Unknown model: ${model}. Available: ${MODELS.join(", ")}` } }); - } - - const sessionId = getSession(user); - const msgSummary = (messages || []).map(m => ({ role: m.role, len: (typeof m.content === "string" ? m.content : JSON.stringify(m.content || "")).length })); - console.log("[OAI]", new Date().toISOString(), user, model, stream ? "stream" : "sync", "msgs:", JSON.stringify(msgSummary)); - - const { body, options } = zenRequest(model, messages, stream, tools, tool_choice, sessionId); - pipeZenResponse(options, body, stream, res); -}); - -// ── Routes: Anthropic Messages format ────────────────────────────── -app.post("/v1/messages", async (req, res) => { - const user = auth(req); - if (!user) { - return res.status(401).json({ type: "error", error: { type: "authentication_error", message: "Invalid API key" } }); - } - - const { model, stream } = req.body; - if (!MODELS.includes(model)) { - return res.status(400).json({ - type: "error", - error: { type: "invalid_request_error", message: `Unknown model: ${model}. Available: ${MODELS.join(", ")}` }, - }); - } - - const sessionId = getSession(user); - const { messages, tools } = anthropicToOpenAI(req.body); - const inputTokens = JSON.stringify(messages).length / 4 | 0; - - console.log("[ANT]", new Date().toISOString(), user, model, stream ? "stream" : "sync", "msgs:", messages.length); - - const { body, options } = zenRequest(model, messages, stream, tools, undefined, sessionId); - - if (stream) { - pipeZenAsAnthropic(options, body, model, res, inputTokens); - } else { - try { - const zenResp = await zenRequestFull(options, body); - if (zenResp.status === 429 || zenResp.data?.error) { - const errMsg = zenResp.data?.error?.message || "Rate limit exceeded"; - return res.status(429).json({ - type: "error", error: { type: "rate_limit_error", message: errMsg + " (free model rate limit)" }, - }); - } - if (!zenResp.data?.choices) { - return res.status(502).json({ - type: "error", error: { type: "upstream_error", message: "Invalid upstream response" }, - }); - } - res.json(openAIToAnthropic(zenResp.data, model, inputTokens)); - } catch (e) { - console.log("[ZEN ERROR]", e.message); - res.status(502).json({ type: "error", error: { type: "upstream_error", message: e.message } }); - } - } -}); - -// ── Health ────────────────────────────────────────────────────────── -app.get("/health", (_req, res) => res.json({ - status: "ok", version: `v${PROXY_VERSION}`, models: MODELS.length, - socks5: proxyUrl || null, - endpoints: ["/v1/chat/completions", "/v1/messages", "/v1/models"], -})); - -// ── Start ────────────────────────────────────────────────────────── -app.listen(PORT, "0.0.0.0", () => { - console.log(`OpenCode Free Proxy v${PROXY_VERSION} on http://0.0.0.0:${PORT}`); - if (socksAgent) console.log(" SOCKS5 proxy:", proxyUrl); - else console.log(" No SOCKS5 proxy configured (use --proxy or SOCKS5_PROXY)"); - console.log(" OpenAI: POST /v1/chat/completions"); - console.log(" Anthropic: POST /v1/messages"); - console.log(" Models: GET /v1/models"); - console.log(" Health: GET /health"); - console.log(" Models:", MODELS.join(", ")); - for (const [name, key] of Object.entries(apiKeys)) { - console.log(` ${name.padEnd(15)} ${key}`); - } -}); From 180f82ccf143c7288621b288aab7a595034ea775 Mon Sep 17 00:00:00 2001 From: Keeper Date: Wed, 8 Jul 2026 02:11:41 +0300 Subject: [PATCH 06/17] refactor: unified CLI interface (--port, --host, --proxy, --api-key) --- server.py | 66 ++++++++++++++++++++++--------------------------------- 1 file changed, 26 insertions(+), 40 deletions(-) diff --git a/server.py b/server.py index 52e1c32..978d685 100644 --- a/server.py +++ b/server.py @@ -1,3 +1,4 @@ +import argparse import json import os import secrets @@ -10,15 +11,19 @@ from fastapi import FastAPI, Request, Response from fastapi.responses import JSONResponse, StreamingResponse -# ── SOCKS5 Proxy ────────────────────────────────────────────────── +# ── CLI args ─────────────────────────────────────────────────────── + +def parse_args() -> argparse.Namespace: + p = argparse.ArgumentParser(description="OpenCode Free Proxy") + p.add_argument("--port", type=int, default=None, help="Listen port (default: 6446)") + p.add_argument("--host", default=None, help="Listen host (default: 0.0.0.0)") + p.add_argument("--proxy", default=None, help="SOCKS5 proxy (socks5://host:port)") + p.add_argument("--api-key", default=None, help="API key for client auth") + return p.parse_args() -def parse_proxy_arg() -> str | None: - args = sys.argv[1:] - for i, arg in enumerate(args): - if arg == "--proxy" and i + 1 < len(args): - return args[i + 1] - return os.environ.get("SOCKS5_PROXY") or None +args = parse_args() +# ── SOCKS5 Proxy ────────────────────────────────────────────────── def normalize_proxy_url(raw: str | None) -> str | None: if not raw: @@ -28,10 +33,7 @@ def normalize_proxy_url(raw: str | None) -> str | None: return raw -PROXY_URL = normalize_proxy_url(parse_proxy_arg()) -PROXY_MOUNT = None -if PROXY_URL: - PROXY_MOUNT = httpx.Mount(proxy=PROXY_URL) +PROXY_URL = normalize_proxy_url(args.proxy or os.environ.get("SOCKS5_PROXY")) http_client = httpx.AsyncClient( base_url="https://opencode.ai", @@ -43,42 +45,24 @@ def normalize_proxy_url(raw: str | None) -> str | None: app = FastAPI() -PORT = int(os.environ.get("PROXY_PORT", "6446")) +PORT = args.port or int(os.environ.get("PORT", "6446")) +HOST = args.host or os.environ.get("HOST", "0.0.0.0") OC_VERSION = "1.15.0" PROXY_VERSION = "9" # ── API Keys ────────────────────────────────────────────────────── -KEYS_FILE = os.environ.get("KEYS_FILE", "./api-keys.json") -api_keys: dict[str, str] = {} - - -def load_keys(): - global api_keys - try: - with open(KEYS_FILE, "r", encoding="utf-8") as f: - api_keys = json.load(f) - except (FileNotFoundError, json.JSONDecodeError): - api_keys = {} - if not api_keys: - api_keys = { - "admin": "oc-" + secrets.token_hex(20), - "user-default": "oc-" + secrets.token_hex(20), - } - with open(KEYS_FILE, "w", encoding="utf-8") as f: - json.dump(api_keys, f, indent=2) - print(f"[INIT] Generated new API keys -> {KEYS_FILE}") - - +API_KEY = args.api_key or os.environ.get("LOCAL_KEY") or os.environ.get("API_KEY") load_keys() def auth(request: Request) -> str | None: + if not API_KEY: + return "user" hdr = request.headers.get("authorization") or request.headers.get("x-api-key") or "" tok = hdr[7:] if hdr.startswith("Bearer ") else hdr - for name, key in api_keys.items(): - if tok == key: - return name + if tok == API_KEY: + return "user" return None @@ -570,7 +554,7 @@ async def health(): # ── Start ───────────────────────────────────────────────────────── if __name__ == "__main__": - print(f"OpenCode Free Proxy v{PROXY_VERSION} on http://0.0.0.0:{PORT}") + print(f"OpenCode Free Proxy v{PROXY_VERSION} on http://{HOST}:{PORT}") if PROXY_URL: print(f" SOCKS5 proxy: {PROXY_URL}") else: @@ -580,7 +564,9 @@ async def health(): print(" Models: GET /v1/models") print(" Health: GET /health") print(f" Models: {', '.join(MODELS)}") - for name, key in api_keys.items(): - print(f" {name:<15} {key}") + if API_KEY: + print(f" API key: {API_KEY[:8]}...") + else: + print(" API key: (none - open access)") - uvicorn.run(app, host="0.0.0.0", port=PORT, log_level="info") + uvicorn.run(app, host=HOST, port=PORT, log_level="info") From 8f30de02a11df0b792cb6e05c1fa8c5182cb0a88 Mon Sep 17 00:00:00 2001 From: Keeper Date: Wed, 8 Jul 2026 02:19:53 +0300 Subject: [PATCH 07/17] fix: remove undefined load_keys() call --- server.py | 1 - 1 file changed, 1 deletion(-) diff --git a/server.py b/server.py index 978d685..4f8ef76 100644 --- a/server.py +++ b/server.py @@ -53,7 +53,6 @@ def normalize_proxy_url(raw: str | None) -> str | None: # ── API Keys ────────────────────────────────────────────────────── API_KEY = args.api_key or os.environ.get("LOCAL_KEY") or os.environ.get("API_KEY") -load_keys() def auth(request: Request) -> str | None: From 5a662d7487010b0c3b59571a743239aac1c1a76f Mon Sep 17 00:00:00 2001 From: Keeper Date: Wed, 8 Jul 2026 03:54:59 +0300 Subject: [PATCH 08/17] docs: add cross-references, CLI args, llama-swap integration --- README.md | 48 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 1e1e86e..858f9c7 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,20 @@ Free AI models from [OpenCode](https://opencode.ai) exposed as standard OpenAI a One server — works with any tool that speaks OpenAI or Anthropic format: Cursor, Continue, Cline, Claude Code, aider, opencode CLI, raw `curl`, whatever. +## Related projects + +All three projects share the same CLI interface (`--port`, `--host`, `--proxy`, `--api-key`) and work together through [llama-swap](https://github.com/mmkeeper/llama-swap): + +| Project | Port | What | +|---------|------|------| +| [opencode-free-proxy](https://github.com/mmkeeper/opencode-free-proxy) | 6446 | OpenCode free models (this) | +| [deepseek-free-api](https://github.com/mmkeeper/deepseek-free-api) | 18632 | DeepSeek free API | +| [mimo-free-proxy](https://github.com/mmkeeper/mimo-free-proxy) | 8788 | Xiaomi MiMo free API | + ## 30-second setup ```bash -git clone https://github.com/bigdata2211it-web/opencode-free-proxy.git +git clone https://github.com/mmkeeper/opencode-free-proxy.git cd opencode-free-proxy pip install -r requirements.txt python server.py @@ -15,6 +25,30 @@ python server.py Done. Server is at `http://localhost:6446`. API keys are in `api-keys.json` (auto-generated on first run). +## CLI arguments + +All projects use the same CLI interface: + +```bash +python server.py --port 6446 --host 127.0.0.1 --proxy socks5://127.0.0.1:9150 --api-key sk-my-key +``` + +| Argument | Default | Description | +|----------|---------|-------------| +| `--port` | `6446` | Listen port | +| `--host` | `127.0.0.1` | Listen host | +| `--proxy` | _(none)_ | SOCKS5 proxy (e.g. `socks5://127.0.0.1:9150`) | +| `--api-key` | _(from api-keys.json)_ | API key for client auth | + +## Environment variables + +| Variable | Default | What | +|----------|---------|------| +| `PROXY_PORT` | `6446` | Server port | +| `HOST` | `127.0.0.1` | Listen host | +| `KEYS_FILE` | `./api-keys.json` | API keys file path | +| `SOCKS5_PROXY` | _(none)_ | SOCKS5 proxy address for upstream requests | + ## What you get | Model | What it is | Reliability | @@ -150,14 +184,6 @@ WantedBy=multi-user.target sudo systemctl enable --now opencode-proxy ``` -## Environment variables - -| Variable | Default | What | -|----------|---------|------| -| `PROXY_PORT` | `6446` | Server port | -| `KEYS_FILE` | `./api-keys.json` | API keys file path | -| `SOCKS5_PROXY` | _(none)_ | SOCKS5 proxy address for upstream requests | - ## SOCKS5 Proxy Route all upstream requests to opencode.ai through a SOCKS5 proxy. @@ -184,6 +210,10 @@ CLI `--proxy` takes priority over `SOCKS5_PROXY`. If neither is set, requests go Environment=SOCKS5_PROXY=127.0.0.1:9150 ``` +## llama-swap integration + +This server works with [llama-swap](https://github.com/mmkeeper/llama-swap) as a peer. See `config.yaml` in the llama-swap repo for an example. + ## How it works ``` From fb9ed56d90761995f8ed035a739be7bb36884b3f Mon Sep 17 00:00:00 2001 From: Keeper Date: Wed, 8 Jul 2026 04:08:27 +0300 Subject: [PATCH 09/17] docs: add proxy compatibility columns to model tables --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 858f9c7..efb3150 100644 --- a/README.md +++ b/README.md @@ -51,13 +51,13 @@ python server.py --port 6446 --host 127.0.0.1 --proxy socks5://127.0.0.1:9150 -- ## What you get -| Model | What it is | Reliability | -|-------|-----------|-------------| -| `mimo-v2.5-free` | Xiaomi MiMo v2.5 | Solid | -| `deepseek-v4-flash-free` | DeepSeek V4 Flash | Solid | -| `north-mini-code-free` | North Mini Code | Solid | -| `nemotron-3-ultra-free` | NVIDIA Nemotron 3 Ultra | Solid | -| `big-pickle` | DeepSeek V4 Flash (alias) | Solid | +| Model | What it is | Reliability | Works with proxy | Works without proxy | +|-------|-----------|-------------|------------------|---------------------| +| `mimo-v2.5-free` | Xiaomi MiMo v2.5 | Solid | ✅ | ✅ | +| `deepseek-v4-flash-free` | DeepSeek V4 Flash | Solid | ✅ | ✅ | +| `north-mini-code-free` | North Mini Code | Solid | ✅ | ✅ | +| `nemotron-3-ultra-free` | NVIDIA Nemotron 3 Ultra | Solid | ✅ | ⚠️ sometimes fails | +| `big-pickle` | DeepSeek V4 Flash (alias) | Solid | ✅ | ✅ | All models support streaming, tool calls, and system messages. From fc074bdef4ebecb7a330bc07fabc8292b2b4f21b Mon Sep 17 00:00:00 2001 From: Keeper Date: Thu, 9 Jul 2026 16:34:03 +0300 Subject: [PATCH 10/17] opencode: per-conversation session ID (hash of message history) --- server.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/server.py b/server.py index 4f8ef76..7860a78 100644 --- a/server.py +++ b/server.py @@ -81,15 +81,22 @@ def oc_id(prefix: str) -> str: "big-pickle", ] -# Track sessions per user (rotate every 30 min) -user_sessions: dict[str, dict] = {} +# Session per conversation (hash of previous messages) +import hashlib -def get_session(user: str) -> str: - now = time.time() * 1000 - if user not in user_sessions or now - user_sessions[user]["ts"] > 30 * 60 * 1000: - user_sessions[user] = {"id": oc_id("ses"), "ts": now} - return user_sessions[user]["id"] +def get_session(user: str, messages: list[dict]) -> str: + # Hash all messages except the last (the current user turn) + # This gives a stable ID for the same conversation history + key_parts = [] + for m in (messages or [])[:-1]: + role = m.get("role", "") + content = m.get("content") or "" + if isinstance(content, list): + content = json.dumps(content) + key_parts.append(f"{role}:{content}") + h = hashlib.sha256("|".join(key_parts).encode()).hexdigest()[:16] + return f"ses_{h}" # ── Zen API transport ───────────────────────────────────────────── @@ -282,7 +289,7 @@ async def chat_completions(request: Request): content={"error": {"message": f"Unknown model: {model}. Available: {', '.join(MODELS)}"}}, ) - session_id = get_session(user) + session_id = get_session(user, messages) msg_summary = [ {"role": m.get("role"), "len": len(m.get("content") if isinstance(m.get("content"), str) else json.dumps(m.get("content") or ""))} for m in (messages or []) @@ -359,8 +366,8 @@ async def messages(request: Request): content={"type": "error", "error": {"type": "invalid_request_error", "message": f"Unknown model: {model}. Available: {', '.join(MODELS)}"}}, ) - session_id = get_session(user) oai_messages, tools = anthropic_to_openai(body) + session_id = get_session(user, oai_messages) input_tokens = len(json.dumps(oai_messages)) // 4 ts = datetime.now(timezone.utc).isoformat() From 845087d165f227b0eb50770933d35c87ec58f6fb Mon Sep 17 00:00:00 2001 From: Keeper Date: Thu, 9 Jul 2026 16:38:17 +0300 Subject: [PATCH 11/17] opencode: stable session ID based on conversation start (system + first user msg) --- server.py | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/server.py b/server.py index 7860a78..04a248c 100644 --- a/server.py +++ b/server.py @@ -81,21 +81,32 @@ def oc_id(prefix: str) -> str: "big-pickle", ] -# Session per conversation (hash of previous messages) +# Session per conversation (hash of conversation start) import hashlib def get_session(user: str, messages: list[dict]) -> str: - # Hash all messages except the last (the current user turn) - # This gives a stable ID for the same conversation history - key_parts = [] - for m in (messages or [])[:-1]: - role = m.get("role", "") - content = m.get("content") or "" - if isinstance(content, list): - content = json.dumps(content) - key_parts.append(f"{role}:{content}") - h = hashlib.sha256("|".join(key_parts).encode()).hexdigest()[:16] + # Hash system prompt + first user message to identify the conversation + # This stays stable as the conversation grows + parts = [] + for m in (messages or []): + if m.get("role") == "system": + content = m.get("content") or "" + if isinstance(content, list): + content = json.dumps(content) + parts.append(f"sys:{content}") + break + for m in (messages or []): + if m.get("role") == "user": + content = m.get("content") or "" + if isinstance(content, list): + content = json.dumps(content) + parts.append(f"usr:{content}") + break + if not parts: + # Fallback: empty hash → new session + parts = ["empty"] + h = hashlib.sha256("|".join(parts).encode()).hexdigest()[:16] return f"ses_{h}" From 143027b3df021c656cea87c30d4a2993b24fd5fb Mon Sep 17 00:00:00 2001 From: Keeper Date: Thu, 9 Jul 2026 17:01:58 +0300 Subject: [PATCH 12/17] opencode: hash-based session lookup (prefix matching) --- server.py | 54 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/server.py b/server.py index 04a248c..6a8ac03 100644 --- a/server.py +++ b/server.py @@ -81,33 +81,43 @@ def oc_id(prefix: str) -> str: "big-pickle", ] -# Session per conversation (hash of conversation start) +# Session per conversation (hash-based lookup) import hashlib +# {user: {hash_of_messages: server_session_id}} +_user_sessions: dict[str, dict[str, str]] = {} -def get_session(user: str, messages: list[dict]) -> str: - # Hash system prompt + first user message to identify the conversation - # This stays stable as the conversation grows + +def _hash_messages(messages: list[dict]) -> str: parts = [] for m in (messages or []): - if m.get("role") == "system": - content = m.get("content") or "" - if isinstance(content, list): - content = json.dumps(content) - parts.append(f"sys:{content}") - break - for m in (messages or []): - if m.get("role") == "user": - content = m.get("content") or "" - if isinstance(content, list): - content = json.dumps(content) - parts.append(f"usr:{content}") - break - if not parts: - # Fallback: empty hash → new session - parts = ["empty"] - h = hashlib.sha256("|".join(parts).encode()).hexdigest()[:16] - return f"ses_{h}" + role = m.get("role", "") + content = m.get("content") or "" + if isinstance(content, list): + content = json.dumps(content, ensure_ascii=False) + parts.append(f"{role}:{content}") + return hashlib.sha256("||".join(parts).encode()).hexdigest()[:16] + + +def get_session(user: str, messages: list[dict]) -> str: + if user not in _user_sessions: + _user_sessions[user] = {} + sessions = _user_sessions[user] + + # Check hashes from longest prefix to shortest + for n in range(len(messages), 0, -1): + h = _hash_messages(messages[:n]) + if h in sessions: + # Found! Also store current full hash for faster future lookup + full_h = _hash_messages(messages) + sessions[full_h] = sessions[h] + return sessions[h] + + # No match — new session + new_id = f"ses_{oc_id('ses')}" + full_h = _hash_messages(messages) + sessions[full_h] = new_id + return new_id # ── Zen API transport ───────────────────────────────────────────── From ff8e1b152aa564446b90b9009e67b368d6d8f793 Mon Sep 17 00:00:00 2001 From: Keeper Date: Thu, 9 Jul 2026 17:21:02 +0300 Subject: [PATCH 13/17] opencode: stop streaming after [DONE] (filter upstream extras) --- server.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/server.py b/server.py index 6a8ac03..5f33bde 100644 --- a/server.py +++ b/server.py @@ -108,15 +108,16 @@ def get_session(user: str, messages: list[dict]) -> str: for n in range(len(messages), 0, -1): h = _hash_messages(messages[:n]) if h in sessions: - # Found! Also store current full hash for faster future lookup full_h = _hash_messages(messages) sessions[full_h] = sessions[h] + print(f"[SESSION] match prefix={n}/{len(messages)} -> {sessions[h]}", flush=True) return sessions[h] # No match — new session new_id = f"ses_{oc_id('ses')}" full_h = _hash_messages(messages) sessions[full_h] = new_id + print(f"[SESSION] new {new_id} (msgs={len(messages)})", flush=True) return new_id @@ -350,6 +351,7 @@ async def chat_completions(request: Request): async def stream_openai(req_body: dict, headers: dict): + print(f"[STREAM] session={headers.get('x-opencode-session')} model={req_body.get('model')} msgs={len(req_body.get('messages', []))}", flush=True) async with http_client.stream("POST", "/zen/v1/chat/completions", json=req_body, headers=headers) as resp: if resp.status_code == 429: try: @@ -358,12 +360,24 @@ async def stream_openai(req_body: dict, headers: dict): err_msg = (data.get("error") or {}).get("message") or "Rate limit exceeded" except Exception: err_msg = "Rate limit exceeded" + print(f"[STREAM] 429: {err_msg}", flush=True) yield f'data: {json.dumps({"error": {"message": err_msg + " (free model rate limit)", "type": "rate_limit_error", "code": "rate_limit_exceeded"}})}\n\n' return + if resp.status_code >= 400: + raw = await resp.aread() + print(f"[STREAM] error {resp.status_code}: {raw[:500]}", flush=True) + yield f'data: {json.dumps({"error": {"message": f"Upstream error {resp.status_code}", "type": "upstream_error"}})}\n\n' + return + + chunk_count = 0 async for line in resp.aiter_lines(): if line: + chunk_count += 1 yield line + "\n" + if line.strip() == "data: [DONE]": + break + print(f"[STREAM] done: {chunk_count} chunks", flush=True) # ── Routes: Anthropic Messages format ───────────────────────────── From d2b5134de30e516027a82664859d0a3fe525009c Mon Sep 17 00:00:00 2001 From: Keeper Date: Thu, 9 Jul 2026 18:13:10 +0300 Subject: [PATCH 14/17] opencode: add ocf- prefix to model names --- server.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/server.py b/server.py index 5f33bde..6409002 100644 --- a/server.py +++ b/server.py @@ -74,11 +74,11 @@ def oc_id(prefix: str) -> str: MODELS = [ - "mimo-v2.5-free", - "deepseek-v4-flash-free", - "north-mini-code-free", - "nemotron-3-ultra-free", - "big-pickle", + "ocf-mimo-v2.5-free", + "ocf-deepseek-v4-flash-free", + "ocf-north-mini-code-free", + "ocf-nemotron-3-ultra-free", + "ocf-big-pickle", ] # Session per conversation (hash-based lookup) @@ -123,8 +123,12 @@ def get_session(user: str, messages: list[dict]) -> str: # ── Zen API transport ───────────────────────────────────────────── +def strip_prefix(model: str) -> str: + return model[4:] if model.startswith("ocf-") else model + + def zen_request(model, messages, stream, tools, tool_choice, session_id): - req_body: dict = {"model": model, "messages": messages, "stream": bool(stream)} + req_body: dict = {"model": strip_prefix(model), "messages": messages, "stream": bool(stream)} if tools: req_body["tools"] = tools if tool_choice: From 87cca818508428a742e7397725515b906447349d Mon Sep 17 00:00:00 2001 From: Keeper Date: Thu, 9 Jul 2026 18:45:31 +0300 Subject: [PATCH 15/17] opencode: fix SSE format (double newline) + filter upstream comments + file logging + ocf- prefix + session lookup --- server.py | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/server.py b/server.py index 6409002..925cafc 100644 --- a/server.py +++ b/server.py @@ -8,6 +8,13 @@ import httpx import uvicorn + + +def log(*a): + msg = f"[{time.strftime('%H:%M:%S')}] " + " ".join(str(x) for x in a) + print(msg, flush=True) + with open("proxy.log", "a", encoding="utf-8") as f: + f.write(msg + "\n") from fastapi import FastAPI, Request, Response from fastapi.responses import JSONResponse, StreamingResponse @@ -110,14 +117,14 @@ def get_session(user: str, messages: list[dict]) -> str: if h in sessions: full_h = _hash_messages(messages) sessions[full_h] = sessions[h] - print(f"[SESSION] match prefix={n}/{len(messages)} -> {sessions[h]}", flush=True) + log(f"SESSION match prefix={n}/{len(messages)} -> {sessions[h]}") return sessions[h] # No match — new session new_id = f"ses_{oc_id('ses')}" full_h = _hash_messages(messages) sessions[full_h] = new_id - print(f"[SESSION] new {new_id} (msgs={len(messages)})", flush=True) + log(f"SESSION new {new_id} (msgs={len(messages)})") return new_id @@ -309,6 +316,8 @@ async def chat_completions(request: Request): tools = body.get("tools") tool_choice = body.get("tool_choice") + log(f"REQUEST model={model} stream={stream} msgs={len(messages or [])} tools={len(tools or [])}") + if model not in MODELS: return JSONResponse( status_code=400, @@ -355,7 +364,7 @@ async def chat_completions(request: Request): async def stream_openai(req_body: dict, headers: dict): - print(f"[STREAM] session={headers.get('x-opencode-session')} model={req_body.get('model')} msgs={len(req_body.get('messages', []))}", flush=True) + log(f"STREAM session={headers.get('x-opencode-session')} model={req_body.get('model')} msgs={len(req_body.get('messages', []))}") async with http_client.stream("POST", "/zen/v1/chat/completions", json=req_body, headers=headers) as resp: if resp.status_code == 429: try: @@ -364,24 +373,36 @@ async def stream_openai(req_body: dict, headers: dict): err_msg = (data.get("error") or {}).get("message") or "Rate limit exceeded" except Exception: err_msg = "Rate limit exceeded" - print(f"[STREAM] 429: {err_msg}", flush=True) + log(f"STREAM 429: {err_msg}") yield f'data: {json.dumps({"error": {"message": err_msg + " (free model rate limit)", "type": "rate_limit_error", "code": "rate_limit_exceeded"}})}\n\n' return if resp.status_code >= 400: raw = await resp.aread() - print(f"[STREAM] error {resp.status_code}: {raw[:500]}", flush=True) + log(f"STREAM error {resp.status_code}: {raw[:500]}") yield f'data: {json.dumps({"error": {"message": f"Upstream error {resp.status_code}", "type": "upstream_error"}})}\n\n' return chunk_count = 0 + first_chunks = [] + last_chunks = [] async for line in resp.aiter_lines(): if line: + # Skip SSE comments (lines starting with :) + if line.startswith(":"): + continue chunk_count += 1 - yield line + "\n" + yield line + "\n\n" + if chunk_count <= 3: + first_chunks.append(line[:200]) if line.strip() == "data: [DONE]": break - print(f"[STREAM] done: {chunk_count} chunks", flush=True) + last_chunks.append(line[:200]) + if len(last_chunks) > 5: + last_chunks.pop(0) + log(f"STREAM done: {chunk_count} chunks") + log(f" first: {first_chunks}") + log(f" last: {last_chunks}") # ── Routes: Anthropic Messages format ───────────────────────────── From 9df6de48050839f41c0727309712ddcd537c9ef0 Mon Sep 17 00:00:00 2001 From: Keeper Date: Thu, 9 Jul 2026 18:47:44 +0300 Subject: [PATCH 16/17] opencode: update README - session persistence, ocf- prefix docs --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index efb3150..1619ee3 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,17 @@ python server.py --port 6446 --host 127.0.0.1 --proxy socks5://127.0.0.1:9150 -- All models support streaming, tool calls, and system messages. +### Model name aliases (opencode CLI / Hermes) + +When using the proxy with tools that support provider-prefixed model names (opencode CLI, Hermes), use the `ocf-` prefix: + +| Tool format | Actual model sent to upstream | +|-------------|------------------------------| +| `ocf-mimo-v2.5-free` | `mimo-v2.5-free` | +| `ocf-deepseek-v4-flash-free` | `deepseek-v4-flash-free` | + +The `ocf-` prefix is stripped before forwarding to opencode.ai, so multiple providers (mimo-free-proxy, opencode-free-proxy) can coexist without model name conflicts. + ## API ### OpenAI format — `POST /v1/chat/completions` @@ -226,6 +237,14 @@ Your tool (Cursor, CLI, curl, etc.) opencode.ai/zen/v1/ ← free tier API ``` +### Session persistence + +The proxy maintains conversation sessions automatically. It uses a hash of the message prefix to identify which upstream session to reuse, so multi-turn conversations stay coherent without the client managing session IDs. + +### Model prefix (`ocf-`) + +All models are exposed with an `ocf-` prefix (e.g. `ocf-mimo-v2.5-free`) to avoid conflicts with other providers in tools like Hermes. The prefix is stripped before forwarding to opencode.ai. + The proxy adds `x-opencode-*` authentication headers that the Zen API requires. These were discovered by reverse engineering the opencode binary — without them, even `Authorization: Bearer public` gets rejected with `AuthError`. ### Zen API auth headers (for the curious) From 31f5cb9f7e5220634ab9052511805b2c9196da8f Mon Sep 17 00:00:00 2001 From: Keeper Date: Thu, 9 Jul 2026 20:49:48 +0300 Subject: [PATCH 17/17] opencode: update related projects table with model prefixes --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 1619ee3..d0fb75b 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,11 @@ One server — works with any tool that speaks OpenAI or Anthropic format: Curso All three projects share the same CLI interface (`--port`, `--host`, `--proxy`, `--api-key`) and work together through [llama-swap](https://github.com/mmkeeper/llama-swap): -| Project | Port | What | -|---------|------|------| -| [opencode-free-proxy](https://github.com/mmkeeper/opencode-free-proxy) | 6446 | OpenCode free models (this) | -| [deepseek-free-api](https://github.com/mmkeeper/deepseek-free-api) | 18632 | DeepSeek free API | -| [mimo-free-proxy](https://github.com/mmkeeper/mimo-free-proxy) | 8788 | Xiaomi MiMo free API | +| Project | Port | Prefix | What | +|---------|------|--------|------| +| [opencode-free-proxy](https://github.com/mmkeeper/opencode-free-proxy) | 6446 | `ocf-` | OpenCode free models (this) | +| [deepseek-free-api](https://github.com/mmkeeper/deepseek-free-api) | 18632 | `dsf-` | DeepSeek free API | +| [mimo-free-proxy](https://github.com/mmkeeper/mimo-free-proxy) | 8788 | `mcf-` | Xiaomi MiMo free API | ## 30-second setup