# qoder-openai-proxy **Repository Path**: shadowedge/qoder-openai-proxy ## Basic Information - **Project Name**: qoder-openai-proxy - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-10-05 - **Last Updated**: 2026-10-05 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # qoder-openai-proxy 把 **Qoder 网关**(`gateway.qoder.com.cn`,私有 COSY 签名 + 私有 SSE 信封)包装成 **标准 OpenAI 接口**,让 Spring AI / LangChain4j / OpenAI SDK / curl 直接调用 `Qwen3.8-Flash`、`Qwen3.8-Max`。 单文件、零 npm 依赖、Node 18+。默认监听 `127.0.0.1:8787`。 ``` 应用 (OpenAI 协议) 本代理 Qoder CN 网关 POST /v1/chat/completions ──▶ 1) 组 Qoder 私有请求体 2) base64 → 自定义字母表乱序编码 (Encode=1) 3) COSY 签名头 (AES key → RSA, MD5 sig) 4) POST ─────────────▶ /algo/api/v2/service/pro/sse/ agent_chat_generation (SSE) ◀── 5) 解析 data:{statusCodeValue,body} 信封 转成 chat.completion.chunk / [DONE] ``` ## 1. 凭据来源(按优先级) | 顺序 | 来源 | 说明 | |---|---|---| | 1 | 环境变量 PAT | `QODERCN_API_KEY` / `QODERCN_PERSONAL_ACCESS_TOKEN` / `QODERCN_PAT`(值形如 `pt-…`)→ 自动 `POST /api/v1/jobToken/exchange` | | 2 | `~/.pi/agent/auth.json` 的 `qoder-cn` 段(**只读**) | 复用你 `pi /login qoder-cn` 的 job token;`refresh` 字段内嵌的 PAT 用于到期后重新交换 | | 3 | `~/.qoder-openai-proxy/cn-creds.json` | 代理自己的凭据缓存(0600),不会污染 pi 的 auth.json | 国际区(`api3.qoder.sh`):`QODER_PROXY_MODE=global`,PAT 变量换成 `QODER_API_KEY` 等。 ## 2. 运行 ```bash cd ~/qoder-openai-proxy # 默认就是前台常驻:打印启动横幅后阻塞在当前终端, # 直到你按 Ctrl+C(收到 SIGINT 会等在途请求跑完再退出,最多 5s) node qoder-proxy.mjs ``` ``` [01:40:04] 模式=cn provider=qoder-cn gateway=https://gateway.qoder.com.cn/ [01:40:04] 凭据就绪: source=pi-auth.json user=user@qoder.com.cn token=jt-ka… 有效期至 … [01:40:04] ✅ OpenAI 兼容端点: http://127.0.0.1:8787/v1 (chat/completions, models, quota, healthz) [01:40:04] Spring AI: spring.ai.openai.base-url=http://127.0.0.1:8787/v1 , api-key=qoder-local [01:40:04] 日志目录: ./logs [01:40:04] 前台保持运行中(心跳 60s)…… 按 Ctrl+C 退出 ^C[01:41:20] 收到 Ctrl+C / SIGINT,正在退出…(在途请求 0 个,最多等 5s) [01:41:20] 👋 已安全退出 ``` 需要放后台时用下面任一方式(前台模式不再受它们影响): ```bash ./start.sh # 后台启动,日志/PID 都在 logs/ 下 ./stop.sh # 停止后台实例 nohup node qoder-proxy.mjs >/dev/null 2>>logs/proxy.err.log & # Git Bash 后台 ./pack.sh # 打产物:命令与制品同目录,见 §2.1 ``` ### 2.1 打包与 npx 运行(`pack.sh`) 零依赖(只用 Node 内置模块),所以 `npx` 能直接跑仓库打出的 tgz: ```bash ./pack.sh --verify # 语法检查 → npm pack → 运行目录 → bundle → 逐种方式真跑一遍 /healthz ``` 产出(全在 `dist/`,已 gitignore): | 产物 | 用途 | | --- | --- | | `qoder-openai-proxy-.tgz` | npm 包:`npx` / `npm i -g` 用 | | `qoder-openai-proxy-/` | **运行目录**:命令 + 制品 + `logs/` 同目录,`cd` 进去直接跑 | | `qoder-openai-proxy--bundle.tar.gz` | 上面那个目录的压缩包(离线拷走解压即跑,里面也带 tgz) | | `qoder-openai-proxy-.sha256` | 两个压缩产物的校验和 | 运行目录里自动生成几类入口(`node_modules/.bin` 用来把命令挂进 `PATH`): ``` qoder-openai-proxy / qoder-proxy # POSIX sh 启动器(日志固定 ./logs) qoder-openai-proxy.cmd / qoder-proxy.cmd # Windows 启动器(cmd 里直接敲) run.sh # 同上别名 start.sh / stop.sh / smoke.sh # 后台启动 / 停止 / 三件套自检 qoder-openai-proxy-.tgz # 制品本身也放了一份 node_modules/.bin/… # npm 风格 shim:PATH 加 ./node_modules/.bin 即可 ``` ```bash # 方式 A:命令与制品同目录(推荐,日志就在旁边) cd dist/qoder-openai-proxy-0.1.0 ./qoder-openai-proxy --port 8787 # 日志 → ./logs/proxy.log PATH="$PWD/node_modules/.bin:$PATH" qoder-openai-proxy --help npm i -g ./qoder-openai-proxy-0.1.0.tgz # 制品就在本目录,就地全局安装 ./start.sh && ./smoke.sh && ./stop.sh # 后台 + 自检 # 方式 B:不装不解压,在 dist/ 里直接 npx 跑 tgz,日志靠 --logs-here 落回 dist/logs cd dist && npx --yes ./qoder-openai-proxy-0.1.0.tgz --logs-here --port=8787 # 方式 C:发布到 registry 后 npx --yes qoder-openai-proxy@0.1.0 # 方式 D:离线机器(有 node 即可) tar -xzf dist/qoder-openai-proxy-0.1.0-bundle.tar.gz cd qoder-openai-proxy-0.1.0 && ./run.sh --port 8787 ``` > 日志落位规则:源码目录 / 运行目录 → `<脚本目录>/logs`;npx 或全局安装(脚本落在 > `node_modules` 里)→ `~/.qoder-openai-proxy/logs`;想让日志跟着启动目录走用 `--logs-here`; > 显式指定用 `--log-dir DIR` 或 `QODER_PROXY_LOG_DIR`。启动器会把 `QODER_PROXY_LOG_DIR` > 钉在自己的同级 `logs/`,所以从别的目录调用命令时,日志依然和制品同目录。 常用打包开关:`--tar` 只出 tgz、`--no-app` 不出运行目录、`--app DIR` 指定运行目录、 `--no-bundle`、`--no-check`、`--clean`、`-V 0.2.0`(临时版本号,不改仓库 `package.json`)、 `-o DIR`(换产物目录)、`--publish`(在暂存目录里发布,仓库保持 `private`)、`--keep-work`。 `./pack.sh --help` 看全部;`--verify` 会真起服务,占用 `--verify-port`(默认 8791 起顺延), 结束后按端口精确收尾,不会碰你 8787 上的实例。 ### 日志 所有日志统一写到 **`logs/`** 目录(脚本同级,无需手动创建,进程会自动 `mkdir -p`): | 文件 | 内容 | |---|---| | `logs/proxy.log` | 运行日志(启动横幅、心跳、凭据刷新、错误诊断),前台/后台都写 | | `logs/proxy.log.1` … | 单文件超过 `QODER_PROXY_LOG_MAX_MB`(默认 10MB)时滚动,保留 `QODER_PROXY_LOG_KEEP` 份 | | `logs/proxy.err.log` | `console.error` / `console.warn`,以及后台启动时的 stderr | | `logs/proxy.pid` | `start.sh` 写入的进程号,供 `stop.sh` 使用 | 日志同时仍然打印到 stdout(方便 `node qoder-proxy.mjs | tee`),`QODER_PROXY_LOG_FILE=0` 可关闭进程内落盘、只留 stdout。 **端口被占用时的行为**(以前会抛 `EADDRINUSE` 堆栈后瞬间退出,现在给中文诊断): ``` [01:40:23] ❌ 端口 127.0.0.1:8787 已被占用(占用进程 PID=264) [01:40:23] 该端口上已经有一个 qoder-openai-proxy 在跑(healthz 已应答)。 [01:40:23] 想接管该端口:QODER_PROXY_KILL_OLD=1 node qoder-proxy.mjs [01:40:23] 或者换端口:QODER_PROXY_PORT=8788 node qoder-proxy.mjs ``` `QODER_PROXY_KILL_OLD=1` 会先确认占用者确实是本代理(探测 `/healthz`),再终止它并重新监听。 自检: ```bash curl http://127.0.0.1:8787/healthz # 凭据来源、有效期 curl http://127.0.0.1:8787/v1/models # 模型目录 curl http://127.0.0.1:8787/v1/quota # 账号额度 ./smoke.sh # 非流式 + 流式 + 工具调用 三件套 ``` ### 环境变量 | 变量 | 默认 | 作用 | |---|---|---| | `QODER_PROXY_PORT` | `8787` | 监听端口 | | `QODER_PROXY_HOST` | `127.0.0.1` | 监听地址(改成 `0.0.0.0` 可局域网用,注意别泄露) | | `QODER_PROXY_MODE` | `cn` | `cn` = gateway.qoder.com.cn,`global` = api3.qoder.sh | | `QODER_PROXY_DEFAULT_MODEL` | 目录第一个 | 请求不带 `model` 时使用 | | `QODER_PROXY_ENABLE_THINKING` | `false` | 未显式传 `enable_thinking` 时是否默认开思考(只管开关,不管强度) | | `QODER_PROXY_DEFAULT_EFFORT` | 空(=目录 `is_default` 档) | 开了思考但未指强度时的默认档位;`omit` = 恢复旧行为(不下发 `reasoning_effort`) | | `QODER_PROXY_LOG` | 空 | `1` 打开详细日志(逐请求状态码 + 心跳统计) | | `QODER_PROXY_LOG_DIR` | `<脚本目录>/logs` | 日志目录(`proxy.log` / `proxy.err.log` / `proxy.pid`);npx/全局安装时默认 `~/.qoder-openai-proxy/logs` | | `QODER_PROXY_LOG_FILE` | 开 | `0` = 不在进程内写文件,只打 stdout(自己重定向时用) | | `QODER_PROXY_LOG_MAX_MB` | `10` | 单个日志文件超过此大小就滚动 | | `QODER_PROXY_LOG_KEEP` | `3` | 保留的滚动文件数(`proxy.log.1` … `.3`) | | `QODER_PROXY_KILL_OLD` | 空 | `1` = 端口被旧实例占用时自动终止旧实例并接管 | 其中 `PORT` / `HOST` / `MODE` / `DEFAULT_MODEL` / `DEFAULT_EFFORT` / `LOG_DIR` / `LOG_FILE` / `LOG` 也都有 等价命令行参数(`--port` / `--host` / `--mode` / `--default-model` / `--default-effort` / `--log-dir` / `--log-file 0|1` / `--verbose`),优先级:命令行 > 环境变量;`npx` 场景下传参更顺手,完整列表看 `--help`。 运行期行为: * 事件循环常驻(60s 心跳定时器),**不会**因为空闲而退出; * 日志同时写 stdout 和 `logs/proxy.log`(`console.error`/`console.warn` 进 `logs/proxy.err.log`); * `logs/` 默认在脚本同级:源码目录就是仓库 `logs/`,`pack.sh` 的运行目录就是该目录 `logs/`, npx/全局安装则落 `~/.qoder-openai-proxy/logs`(详见 §2.1 与 `--logs-here`); * 心跳里顺带检查凭据有效期,剩余 ≤15 分钟自动重新交换/刷新 job token; * `Ctrl+C` / `SIGTERM` / `SIGHUP` → 优雅退出(等 5s 内在途请求); * 管道给 `head`/`less` 时的 `EPIPE` 已忽略,不会把服务带崩。 ## 3. 支持的端点与参数 * `POST /v1/chat/completions` — 流式(`stream:true`)与非流式都支持 * `GET /v1/models`、`GET /v1/quota`、`GET /healthz` * `messages`:`system / developer / user / assistant / tool`,多模态 `image_url`(含 `data:` URL)✅ * `tools` + `tool_calls` 往返 ✅(`finish_reason=tool_calls`) * `tool_choice`:`none` 本地直接不下发 tools;`required` 或指定函数名 → 前插强制 system 提示近似实现(网关无此字段) * `max_tokens` / `max_completion_tokens`(≤131072)、`stream_options.include_usage` * `enable_thinking`(布尔)、`reasoning_effort` / `reasoning.effort` / `thinking_budget` / `thinking:{type,budget_tokens}`:思考开关与强度,详见 §3.1(档位按目录 `thinking_config.efforts` 校验并做别名夹档,本账号支持 `low/medium/xhigh`) * 可选 `session_id` / `user`:稳定映射到 Qoder 的 `session_id`,有利上下文缓存命中 * ❌ `temperature` / `top_p` / `stop` / `n`:Qoder 网关不接受,会被忽略并打日志 * ❌ `/v1/embeddings`:网关无对应端点,返回 501 模型名宽容匹配:`Qwen3.8-Flash` = `qwen3.8-flash` = `qwen3.8flash` = 目录 key。 ### 3.1 设置思考强度(reasoning effort) 强度是**请求体字段**,不是代理配置。代理会把它夹到当前模型支持的档位,再放进 Qoder 私有请求的 `parameters`(`{ enable_thinking, reasoning_effort }`)发给网关。 ```bash # 档位写法全都支持(下面四种等价于 reasoning_effort:"xhigh") curl -sS $BASE/v1/chat/completions -H 'Content-Type: application/json' -d '{ "model":"Qwen3.8-Flash", "messages":[…], "enable_thinking": true, "reasoning_effort": "high" # 或 "reasoning":{"effort":"low"} / "thinking_budget":32000 }' ``` **开关优先级**:`enable_thinking` > 请求里任一强度写法(有=开,`none`/`disabled`=关)> `QODER_PROXY_ENABLE_THINKING`。`enable_thinking:false` 永远赢过强度字段。 **档位优先级**:请求里的强度 > `QODER_PROXY_DEFAULT_EFFORT` > 目录 `is_default` 档(代理会**主动下发**)。 | 写法 | 本代理行为 | |---|---| | `reasoning_effort: "none" / "disabled" / "off"` | 关思考(不是「开但降档」) | | `"auto" / "default"` | 不表态:开关看 `enable_thinking`,档位看默认档 | | `"minimal"` → `low`,`"high"` → `xhigh`,`"max"` → 最高档 | OpenAI 取值自动映射;平局优先升档 | | `"X-High"` / `"extra high"` 等 | 剔掉分隔符后容错匹配 | | `thinking_budget: 512 / 4096 / 32000` | 折算档位:≤1024 最低、≤8192 中间、更大最高(网关不吃 token 数) | | `thinking: {type:"enabled", budget_tokens:n}` / `{type:"disabled"}` | Anthropic 风格也认 | | 乱写的档位(`"turbo"`) | 回落目录默认档,日志留痕 | 档位集合来自 `~/.pi/agent/-models-cache.json` 的 `thinking_config.enabled.efforts`, 可用 `GET /v1/models` 自查(新增 `can_reasoning` / `supported_reasoning_efforts` / `default_reasoning_effort` 字段)。本账号两个模型都是 `low / medium / xhigh`。 实测(`Qwen3.8-Flash`,一道数数字的题,取 2 次 `reasoning_content` 长度): | 强度 | 思考字数 | completion_tokens | 耗时 | |---|---|---|---| | `enable_thinking:false` | 0 | 2 | ~0.7s | | `low` | 180–194 | 165–166 | ~2.4s | | `medium` | 220–265 | 181–210 | ~5s | | `xhigh` | 552–802 | 224–382 | ~4.7s | > ⚠️ **旧版坑(已修)**:只传 `enable_thinking:true` 而不带强度时,旧代码不下发 > `reasoning_effort`,上游经常整段不思考(实测 `reasoning_content` 长度为 0)。现在代理会主动 > 下发目录默认档。想回到旧行为:`QODER_PROXY_DEFAULT_EFFORT=omit`。 > 每次请求的解析结果会在日志里输出一行 `思考强度[模型]: …`(含降档/折算原因)。 自检(不发网络请求,只验解析逻辑):`node test-thinking.mjs`。 ## 4. Spring AI 配置 `pom.xml`(实测 Spring AI GA = `2.0.1`): ```xml org.springframework.ai spring-ai-bom 2.0.1 pomimport org.springframework.ai spring-ai-starter-model-openai ``` `src/main/resources/application.yml`(见 `examples/spring-ai/`): ```yaml spring: ai: openai: base-url: http://127.0.0.1:8787/v1 # 注意带 /v1,Spring AI 2.0 会拼 /chat/completions api-key: qoder-local # 占位即可,代理用本机 Qoder 凭据签名 timeout: 120s chat: model: Qwen3.8-Flash stream-usage: true extra-body: enable_thinking: false model: chat: openai ``` Java 用法(`examples/spring-ai/ChatController.java`)照旧:`ChatClient.Builder` 注入即可, `call()` / `stream()` 都能用。 LangChain4j / OpenAI SDK 同理:`baseUrl=http://127.0.0.1:8787/v1`,`apiKey=any`。 ## 5. 故障排查 | 现象 | 原因 | 处理 | |---|---|---| | `找不到凭据` | 无 env PAT 且 `auth.json` 无 `qoder-cn` | `export QODERCN_API_KEY=pt-…` 或在 pi 里 `/login qoder-cn` | | `401/403` 后自动重试仍失败 | job token 过期且 PAT 失效 | 去 https://qoder.com.cn/account/integrations 重新生成 PAT | | `上游状态 4xx: …` | 模型名不在目录 / 参数超范围 | `curl /v1/models` 看可用 id;`max_tokens ≤ 131072` | | `403 code=112` | 该模型需要付费额度(本账号 `Qwen3.8-Max` 需付费、`Qwen3.8-Flash` 免费可用) | 换免费模型,或到 https://qoder.com.cn/pricing 升级 | | `Unknown Qoder model id` | `~/.pi/agent/qoder-cn-models-cache.json` 缺失或过期 | 用 pi 跑一次(会刷新目录),或 `QODERCN_API_KEY` 让代理走 exchange 后由 pi 刷新 | | 首个 token 很慢 | 开了思考 | 传 `enable_thinking:false`(或 `reasoning_effort:"none"`),别设 `QODER_PROXY_ENABLE_THINKING=true`;要保留思考但省时间就降档 `low` | | 传了 `reasoning_effort` 却没效果 | ① 档位不在目录支持列表 → 日志会有 `思考强度[…]` 一行;② 模型名不在目录(`is_reasoning:false`)会被强制关思考 | `curl $BASE/v1/models` 看 `supported_reasoning_efforts`,用目录里的模型名 | | 想要思考但 `reasoning_content` 是空 | 强度被写成 `none`,或模型不支持思考 | 显式传 `reasoning_effort:"medium"` 并确认 `can_reasoning:true` | | 非流式偶发长时间挂住 | 上游 SSE 未发 `[DONE]` | 客户端设 read-timeout;代理在连接关闭时会 abort 上游 | ## 6. 风险声明(重要) COSY 签名、`QoderMachineType/Os` 指纹、`Encode=1` 混淆体都是 **Qoder 客户端内部协议**, 不是对外 API。此骨架只适合**本机个人自用/实验**: 1. 用于生产服务或对外共享很可能违反 Qoder 服务条款,可能导致账号封禁; 2. 协议随时变更,届时需重新对照 `pi-provider-qoder` 源码移植; 3. 凭据文件权限 0600、默认只监听 `127.0.0.1`,不要把端口暴露到公网; 4. 生产正解仍是阿里云百炼 API Key(OpenAI 兼容模式 `https://dashscope.aliyuncs.com/compatible-mode/v1`)。