From 8dcf445bcaec595acd953d1d63264e09aaac4886 Mon Sep 17 00:00:00 2001 From: Junkui Chen Date: Wed, 3 Jun 2026 11:22:41 +0800 Subject: [PATCH 1/4] refine and update --- .github/workflows/deploy-pages.yml | 40 -- .gitignore | 6 +- README.md | 33 +- deploy/.env.example | 22 + deploy/README.md | 114 +++++ deploy/install-cloudflared-service.cmd | 56 +++ deploy/install-cloudflared.cmd | 30 ++ deploy/install-copilot-api-service.cmd | 143 +++++++ deploy/install-nssm.cmd | 29 ++ deploy/start-all.cmd | 73 ++++ deploy/uninstall-services.cmd | 28 ++ pages/index.html | 556 ------------------------- src/routes/token/route.ts | 16 - src/routes/usage/route.ts | 15 - src/server.ts | 21 +- src/start.ts | 4 +- start-copilot-api.cmd | 2 +- start.bat | 6 +- 18 files changed, 522 insertions(+), 672 deletions(-) delete mode 100644 .github/workflows/deploy-pages.yml create mode 100644 deploy/.env.example create mode 100644 deploy/README.md create mode 100644 deploy/install-cloudflared-service.cmd create mode 100644 deploy/install-cloudflared.cmd create mode 100644 deploy/install-copilot-api-service.cmd create mode 100644 deploy/install-nssm.cmd create mode 100644 deploy/start-all.cmd create mode 100644 deploy/uninstall-services.cmd delete mode 100644 pages/index.html delete mode 100644 src/routes/token/route.ts delete mode 100644 src/routes/usage/route.ts diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml deleted file mode 100644 index e5b05974c..000000000 --- a/.github/workflows/deploy-pages.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Deploy to GitHub Pages - -on: - push: - branches: [ "main" ] - workflow_dispatch: - -# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages -permissions: - contents: read - pages: write - id-token: write - -# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. -# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. -concurrency: - group: "pages" - cancel-in-progress: false - -jobs: - deploy: - runs-on: ubuntu-latest - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Pages - uses: actions/configure-pages@v4 - - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - path: ./pages - - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 577a4f199..a0efba0e7 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,8 @@ node_modules/ .eslintcache # build output -dist/ \ No newline at end of file +dist/ + +# deploy secrets (Cloudflare Tunnel token 等) +deploy/.env +deploy/logs/ \ No newline at end of file diff --git a/README.md b/README.md index 83f7f8417..a4bb735af 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,6 @@ A reverse-engineered proxy for the GitHub Copilot API that exposes it as an Open - **OpenAI & Anthropic Compatibility**: Exposes GitHub Copilot as an OpenAI-compatible (`/v1/chat/completions`, `/v1/models`, `/v1/embeddings`) and Anthropic-compatible (`/v1/messages`) API. - **Claude Code Integration**: Easily configure and launch [Claude Code](https://docs.anthropic.com/en/docs/claude-code/overview) to use Copilot as its backend with a simple command-line flag (`--claude-code`). -- **Usage Dashboard**: A web-based dashboard to monitor your Copilot API usage, view quotas, and see detailed statistics. - **Rate Limit Control**: Manage API usage with rate-limiting options (`--rate-limit`) and a waiting mechanism (`--wait`) to prevent errors from rapid requests. - **Manual Request Approval**: Manually approve or deny each API request for fine-grained control over usage (`--manual`). - **Token Visibility**: Option to display GitHub and Copilot tokens during authentication and refresh for debugging (`--show-token`). @@ -200,15 +199,6 @@ These endpoints are designed to be compatible with the Anthropic Messages API. | `POST /v1/messages` | `POST` | Creates a model response for a given conversation. | | `POST /v1/messages/count_tokens` | `POST` | Calculates the number of tokens for a given set of messages. | -### Usage Monitoring Endpoints - -New endpoints for monitoring your Copilot usage and quotas. - -| Endpoint | Method | Description | -| ------------ | ------ | ------------------------------------------------------------ | -| `GET /usage` | `GET` | Get detailed Copilot usage statistics and quota information. | -| `GET /token` | `GET` | Get the current Copilot token being used by the API. | - ## Example Usage Using with npx: @@ -257,27 +247,6 @@ npx copilot-api@latest debug --json npx copilot-api@latest start --proxy-env ``` -## Using the Usage Viewer - -After starting the server, a URL to the Copilot Usage Dashboard will be displayed in your console. This dashboard is a web interface for monitoring your API usage. - -1. Start the server. For example, using npx: - ```sh - npx copilot-api@latest start - ``` -2. The server will output a URL to the usage viewer. Copy and paste this URL into your browser. It will look something like this: - `https://ericc-ch.github.io/copilot-api?endpoint=http://localhost:4141/usage` - - If you use the `start.bat` script on Windows, this page will open automatically. - -The dashboard provides a user-friendly interface to view your Copilot usage data: - -- **API Endpoint URL**: The dashboard is pre-configured to fetch data from your local server endpoint via the URL query parameter. You can change this URL to point to any other compatible API endpoint. -- **Fetch Data**: Click the "Fetch" button to load or refresh the usage data. The dashboard will automatically fetch data on load. -- **Usage Quotas**: View a summary of your usage quotas for different services like Chat and Completions, displayed with progress bars for a quick overview. -- **Detailed Information**: See the full JSON response from the API for a detailed breakdown of all available usage statistics. -- **URL-based Configuration**: You can also specify the API endpoint directly in the URL using a query parameter. This is useful for bookmarks or sharing links. For example: - `https://ericc-ch.github.io/copilot-api?endpoint=http://your-api-server/usage` - ## Using with Claude Code This proxy can be used to power [Claude Code](https://docs.anthropic.com/en/claude-code), an experimental conversational AI assistant for developers from Anthropic. @@ -368,4 +337,4 @@ bun run start 3. Enter a clear name and paste the script path into the **Command line** field. 4. Save the profile and open it from the dropdown list. 5. Configure `start-copilot-api.cmd` using the same steps. -- You can now start the Copilot API and Claude scripts directly from Windows Terminal. \ No newline at end of file +- You can now start the Copilot API and Claude scripts directly from Windows Terminal. diff --git a/deploy/.env.example b/deploy/.env.example new file mode 100644 index 000000000..7da74190a --- /dev/null +++ b/deploy/.env.example @@ -0,0 +1,22 @@ +# ============================================================ +# 一键部署配置 - 复制为 deploy/.env 后填写实际值 +# ============================================================ + +# Cloudflare Tunnel Token (Zero Trust > Networks > Tunnels > 你的Tunnel > Install connector) +# 形如: eyJhIjoi...一长串... +# 目标域名: xieyan.junquick.com (在 Tunnel 的 Public Hostname 里配置, Service: http://localhost:4141) +CLOUDFLARE_TUNNEL_TOKEN= + +# Copilot API 监听端口 (默认 4141, 一般不用改) +COPILOT_API_PORT=4141 + +# 可选: 直接提供 GitHub Token 跳过交互登录 (留空则首次启动会弹出 device code 登录) +# GH_TOKEN= + +# 可选: 账户类型 individual | business | enterprise +COPILOT_ACCOUNT_TYPE=individual + +# Bearer Token 校验 (强烈建议设置, 否则你的域名暴露后任何人都能白嫖你的 Copilot) +# 随便填一长串, 例如用 PowerShell 生成: [guid]::NewGuid().ToString("N") +# 留空 = 不校验 (仅本地裸跑时建议) +PROXY_AUTH_TOKEN= diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 000000000..c631d9084 --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,114 @@ +# 一键部署 (本地 + Cloudflare Tunnel) + +把本机的 `copilot-api` 通过你自己的域名暴露出去,**无需公网 IP / 无需开端口 / 自动 HTTPS**。 + +## 架构 + +``` +你的域名 (Cloudflare) + │ + ▼ HTTPS + Cloudflare Edge + │ + ▼ 加密隧道 (出站连接, 不需要开端口) + 你本机的 cloudflared + │ + ▼ HTTP localhost:4141 + copilot-api (Bun) +``` + +## 准备工作 (Cloudflare 侧, 5 分钟) + +1. 域名已托管在 Cloudflare (NS 已切过去) +2. 打开 [Cloudflare Zero Trust 后台](https://one.dash.cloudflare.com/) → **Networks → Tunnels → Create a tunnel** +3. 类型选 **Cloudflared**, 起个名字 (例如 `copilot-api`) +4. 创建后会显示一段安装命令, **复制其中的 token**(`--token` 后面那段长字符串) +5. 下一步 **Public Hostnames**: 添加 + - Subdomain: `xieyan` + - Domain: `junquick.com` + - Path: 留空 + - Service Type: `HTTP` + - URL: `localhost:4141` +6. 保存 —— Cloudflare 会自动给 `xieyan.junquick.com` 加 CNAME 记录 + +## 本机一键启动 (3 步) + +```cmd +:: 1. 安装 cloudflared (只装一次) +deploy\install-cloudflared.cmd + +:: 2. 配置 token +copy deploy\.env.example deploy\.env +:: 用记事本打开 deploy\.env, 把 CLOUDFLARE_TUNNEL_TOKEN 填上 + +:: 3. 启动 +deploy\start-all.cmd +``` + +启动后访问 `https://xieyan.junquick.com/` 应该能打通到 `localhost:4141`。 + +测试: +```bash +curl https://xieyan.junquick.com/v1/models +``` + +配置 Claude Code 用这个远程地址: +```json +{ + "env": { + "ANTHROPIC_BASE_URL": "https://xieyan.junquick.com", + "ANTHROPIC_AUTH_TOKEN": "dummy", + "ANTHROPIC_MODEL": "claude-opus-4-6", + "ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-4-5", + "ANTHROPIC_SMALL_FAST_MODEL": "claude-haiku-4-5" + } +} +``` + +## 开机自启 (可选, 推荐) + +把 cloudflared 注册为 Windows 服务,开机后自动连接 Cloudflare: + +```cmd +:: 管理员身份运行 +deploy\install-cloudflared-service.cmd +``` + +> 注意: `copilot-api` 本身不会被注册为服务 —— 因为它首次需要交互式 GitHub 登录。 +> 如果你已经登录过 (token 缓存在 `%LOCALAPPDATA%\copilot-api\` 或类似路径), +> 可以用 [nssm](https://nssm.cc/) 把 `bun run start` 也包成服务。 + +## 文件说明 + +| 文件 | 作用 | +|---|---| +| `.env.example` | 配置模板 | +| `.env` | 你的实际配置 (已 gitignore) | +| `install-cloudflared.cmd` | 用 winget 安装 cloudflared | +| `install-cloudflared-service.cmd` | cloudflared 注册为开机自启服务 (管理员) | +| `install-nssm.cmd` | 用 winget 安装 nssm (service 包装工具) | +| `install-copilot-api-service.cmd` | copilot-api 注册为开机自启服务 (管理员) | +| `uninstall-services.cmd` | 一键卸载上面两个服务 (管理员) | +| `start-all.cmd` | 一键启动 copilot-api + cloudflared (开发用, 不走服务) | +| `logs/` | 服务模式下的 stdout/stderr 日志 | + +## 安全提醒 + +- `CLOUDFLARE_TUNNEL_TOKEN` 等同于隧道的"密钥", 不要提交到 git (已加入 .gitignore) +- **强烈建议在 `deploy/.env` 设置 `PROXY_AUTH_TOKEN`** —— 服务端已内置 Bearer 校验, + 设置后所有请求必须带 `Authorization: Bearer ` (或 Anthropic 风格的 `x-api-key: `), + 否则返回 401。留空则不校验 (危险, 仅本地使用)。 + 生成 token: + ```powershell + [guid]::NewGuid().ToString("N") + ``` +- 客户端配置示例 (Claude Code): + ```json + { + "env": { + "ANTHROPIC_BASE_URL": "https://xieyan.junquick.com", + "ANTHROPIC_AUTH_TOKEN": "你设置的 PROXY_AUTH_TOKEN" + } + } + ``` +- 进阶: 也可以在 Cloudflare Zero Trust 后台加 Access Policy (邮箱白名单 / Google 登录) 做第二层防护 diff --git a/deploy/install-cloudflared-service.cmd b/deploy/install-cloudflared-service.cmd new file mode 100644 index 000000000..25113cf55 --- /dev/null +++ b/deploy/install-cloudflared-service.cmd @@ -0,0 +1,56 @@ +@echo off +REM ============================================================ +REM 把 cloudflared 注册成 Windows 服务 (开机自启, 后台运行) +REM 需要管理员权限 +REM ============================================================ +setlocal + +REM --- 检查管理员 --- +net session >nul 2>&1 +if %ERRORLEVEL% NEQ 0 ( + echo [ERROR] 需要以管理员身份运行此脚本。 + echo 右键 -> 以管理员身份运行 + pause + exit /b 1 +) + +REM --- 加载 .env --- +set "ENV_FILE=%~dp0.env" +if not exist "%ENV_FILE%" ( + echo [ERROR] 找不到 %ENV_FILE% + echo 请先复制 deploy\.env.example 为 deploy\.env 并填写 CLOUDFLARE_TUNNEL_TOKEN + pause + exit /b 1 +) + +for /f "usebackq tokens=1,2 delims==" %%A in ("%ENV_FILE%") do ( + set "%%A=%%B" +) + +if "%CLOUDFLARE_TUNNEL_TOKEN%"=="" ( + echo [ERROR] deploy\.env 中 CLOUDFLARE_TUNNEL_TOKEN 为空 + pause + exit /b 1 +) + +where cloudflared >nul 2>&1 +if %ERRORLEVEL% NEQ 0 ( + echo [ERROR] cloudflared 未安装, 请先运行 install-cloudflared.cmd + pause + exit /b 1 +) + +echo [INFO] 注册 cloudflared 为 Windows 服务... +cloudflared service install %CLOUDFLARE_TUNNEL_TOKEN% +if %ERRORLEVEL%==0 ( + echo. + echo [OK] 服务已安装并启动。可用以下命令管理: + echo net start cloudflared + echo net stop cloudflared + echo sc delete cloudflared :: 卸载 +) else ( + echo [ERROR] 安装失败, 错误码 %ERRORLEVEL% +) + +pause +endlocal diff --git a/deploy/install-cloudflared.cmd b/deploy/install-cloudflared.cmd new file mode 100644 index 000000000..ad178c9a0 --- /dev/null +++ b/deploy/install-cloudflared.cmd @@ -0,0 +1,30 @@ +@echo off +REM ============================================================ +REM 安装 cloudflared (Cloudflare Tunnel 客户端) +REM 优先用 winget; 没有则提示手动下载链接 +REM ============================================================ + +where cloudflared >nul 2>&1 +if %ERRORLEVEL%==0 ( + echo [OK] cloudflared 已安装: + cloudflared --version + goto :eof +) + +echo [INFO] cloudflared 未安装, 尝试用 winget 安装... +where winget >nul 2>&1 +if %ERRORLEVEL%==0 ( + winget install --id Cloudflare.cloudflared -e --accept-source-agreements --accept-package-agreements + if %ERRORLEVEL%==0 ( + echo. + echo [OK] 安装完成。请重开一个终端让 PATH 生效, 然后运行 start-all.cmd + goto :eof + ) +) + +echo. +echo [WARN] 自动安装失败, 请手动下载: +echo https://github.com/cloudflare/cloudflared/releases/latest +echo 下载 cloudflared-windows-amd64.exe, 重命名为 cloudflared.exe, +echo 放到 PATH 路径下 (例如 C:\Windows\System32\) +pause diff --git a/deploy/install-copilot-api-service.cmd b/deploy/install-copilot-api-service.cmd new file mode 100644 index 000000000..f63389a88 --- /dev/null +++ b/deploy/install-copilot-api-service.cmd @@ -0,0 +1,143 @@ +@echo off +REM ============================================================ +REM 把 copilot-api 注册成 Windows 服务 (开机自启 + 崩溃重启) +REM +REM 前置条件: +REM 1. 已运行 deploy\install-nssm.cmd 装好 nssm +REM 2. 已至少手动跑过一次 deploy\start-all.cmd 完成 GitHub device code 登录 +REM (token 缓存到 %LOCALAPPDATA%\copilot-api\, 服务才能无人值守启动) +REM 3. deploy\.env 已配置 PROXY_AUTH_TOKEN +REM +REM 需要管理员权限 +REM ============================================================ +setlocal EnableDelayedExpansion + +REM --- 检查管理员 --- +net session >nul 2>&1 +if %ERRORLEVEL% NEQ 0 ( + echo [ERROR] 需要以管理员身份运行此脚本。 + echo 右键 -^> 以管理员身份运行 + pause + exit /b 1 +) + +REM --- 检查 nssm --- +where nssm >nul 2>&1 +if %ERRORLEVEL% NEQ 0 ( + echo [ERROR] nssm 未安装, 请先运行 deploy\install-nssm.cmd + pause + exit /b 1 +) + +REM --- 检查 bun --- +where bun >nul 2>&1 +if %ERRORLEVEL% NEQ 0 ( + echo [ERROR] bun 未安装, 请先安装: https://bun.com/docs/installation#windows + pause + exit /b 1 +) + +REM --- 解析路径 --- +set "PROJECT_DIR=%~dp0.." +pushd "%PROJECT_DIR%" +set "PROJECT_DIR=%CD%" +popd + +set "ENV_FILE=%~dp0.env" +if not exist "%ENV_FILE%" ( + echo [ERROR] 找不到 %ENV_FILE% + pause + exit /b 1 +) + +REM --- 加载 .env --- +for /f "usebackq tokens=1,2 delims==" %%A in ("%ENV_FILE%") do ( + set "%%A=%%B" +) + +if "%COPILOT_API_PORT%"=="" set "COPILOT_API_PORT=4141" + +if "%PROXY_AUTH_TOKEN%"=="" ( + echo [WARN] PROXY_AUTH_TOKEN 未设置 - 服务将无鉴权运行 ^(任何人能调用^) + echo 强烈建议先在 deploy\.env 设置 PROXY_AUTH_TOKEN, 然后重跑此脚本 + set /p "CONTINUE=确认继续? (y/N): " + if /i not "!CONTINUE!"=="y" exit /b 1 +) + +REM --- 查 bun 完整路径 (nssm 需要绝对路径) --- +for /f "delims=" %%I in ('where bun') do set "BUN_PATH=%%I" & goto :found_bun +:found_bun +echo [INFO] Bun: %BUN_PATH% +echo [INFO] 项目目录: %PROJECT_DIR% +echo [INFO] 端口: %COPILOT_API_PORT% + +REM --- 服务名 --- +set "SVC_NAME=copilot-api" +set "LOG_DIR=%PROJECT_DIR%\deploy\logs" +if not exist "%LOG_DIR%" mkdir "%LOG_DIR%" + +REM --- 如果已存在, 先卸载 --- +sc query %SVC_NAME% >nul 2>&1 +if %ERRORLEVEL%==0 ( + echo [INFO] 服务 %SVC_NAME% 已存在, 先卸载... + nssm stop %SVC_NAME% confirm >nul 2>&1 + nssm remove %SVC_NAME% confirm +) + +REM --- 安装服务 --- +echo [INFO] 安装服务 %SVC_NAME%... +nssm install %SVC_NAME% "%BUN_PATH%" run "%PROJECT_DIR%\src\main.ts" start --port %COPILOT_API_PORT% +if %ERRORLEVEL% NEQ 0 ( + echo [ERROR] nssm install 失败 + pause + exit /b 1 +) + +REM --- 配置服务参数 --- +nssm set %SVC_NAME% AppDirectory "%PROJECT_DIR%" +nssm set %SVC_NAME% DisplayName "Copilot API Proxy" +nssm set %SVC_NAME% Description "GitHub Copilot -> OpenAI/Anthropic compatible proxy" +nssm set %SVC_NAME% Start SERVICE_AUTO_START + +REM --- 环境变量 --- +nssm set %SVC_NAME% AppEnvironmentExtra ^ + NODE_ENV=production ^ + PROXY_AUTH_TOKEN=%PROXY_AUTH_TOKEN% + +REM --- 日志输出 --- +nssm set %SVC_NAME% AppStdout "%LOG_DIR%\copilot-api.out.log" +nssm set %SVC_NAME% AppStderr "%LOG_DIR%\copilot-api.err.log" +nssm set %SVC_NAME% AppRotateFiles 1 +nssm set %SVC_NAME% AppRotateBytes 10485760 + +REM --- 崩溃自动重启 --- +nssm set %SVC_NAME% AppExit Default Restart +nssm set %SVC_NAME% AppRestartDelay 3000 + +REM --- 启动 --- +echo [INFO] 启动服务... +nssm start %SVC_NAME% +timeout /t 3 /nobreak >nul + +echo. +echo ================================================ +echo [OK] copilot-api 服务已安装并启动 +echo ================================================ +echo. +echo 管理命令: +echo net start copilot-api 启动 +echo net stop copilot-api 停止 +echo nssm restart copilot-api 重启 +echo nssm edit copilot-api 图形化编辑配置 +echo nssm remove copilot-api confirm 卸载 +echo. +echo 日志路径: +echo %LOG_DIR%\copilot-api.out.log +echo %LOG_DIR%\copilot-api.err.log +echo. +echo 验证: +echo curl http://localhost:%COPILOT_API_PORT%/ +echo. + +endlocal +pause diff --git a/deploy/install-nssm.cmd b/deploy/install-nssm.cmd new file mode 100644 index 000000000..c42ff607f --- /dev/null +++ b/deploy/install-nssm.cmd @@ -0,0 +1,29 @@ +@echo off +REM ============================================================ +REM 安装 nssm (Non-Sucking Service Manager) +REM 用它把任意命令包成 Windows 服务 +REM ============================================================ + +where nssm >nul 2>&1 +if %ERRORLEVEL%==0 ( + echo [OK] nssm 已安装: + nssm version + goto :eof +) + +echo [INFO] nssm 未安装, 尝试用 winget 安装... +where winget >nul 2>&1 +if %ERRORLEVEL%==0 ( + winget install --id NSSM.NSSM -e --accept-source-agreements --accept-package-agreements + if %ERRORLEVEL%==0 ( + echo. + echo [OK] 安装完成。请重开一个终端让 PATH 生效, 然后运行 install-copilot-api-service.cmd + goto :eof + ) +) + +echo. +echo [WARN] 自动安装失败, 请手动下载: +echo https://nssm.cc/download +echo 解压后把 win64\nssm.exe 放到 PATH 路径下 (例如 C:\Windows\System32\) +pause diff --git a/deploy/start-all.cmd b/deploy/start-all.cmd new file mode 100644 index 000000000..6696d8dc4 --- /dev/null +++ b/deploy/start-all.cmd @@ -0,0 +1,73 @@ +@echo off +REM ============================================================ +REM 一键启动: copilot-api + cloudflared tunnel +REM 适合开发/手动场景。要做开机自启, 请用: +REM install-cloudflared-service.cmd (注册 cloudflared 为服务) +REM ============================================================ +setlocal + +set "PROJECT_DIR=%~dp0.." +set "ENV_FILE=%~dp0.env" + +REM --- 加载 .env --- +if not exist "%ENV_FILE%" ( + echo [ERROR] 找不到 %ENV_FILE% + echo 请先复制 deploy\.env.example 为 deploy\.env 并填写 CLOUDFLARE_TUNNEL_TOKEN + pause + exit /b 1 +) + +for /f "usebackq tokens=1,2 delims==" %%A in ("%ENV_FILE%") do ( + set "%%A=%%B" +) + +if "%CLOUDFLARE_TUNNEL_TOKEN%"=="" ( + echo [ERROR] deploy\.env 中 CLOUDFLARE_TUNNEL_TOKEN 为空 + pause + exit /b 1 +) + +if "%COPILOT_API_PORT%"=="" set "COPILOT_API_PORT=4141" + +REM --- 检查依赖 --- +where bun >nul 2>&1 +if %ERRORLEVEL% NEQ 0 ( + echo [ERROR] bun 未安装, 请先安装: https://bun.com/docs/installation#windows + pause + exit /b 1 +) + +where cloudflared >nul 2>&1 +if %ERRORLEVEL% NEQ 0 ( + echo [ERROR] cloudflared 未安装, 请先运行 deploy\install-cloudflared.cmd + pause + exit /b 1 +) + +REM --- 启动 copilot-api (新窗口) --- +echo [INFO] 启动 copilot-api (端口 %COPILOT_API_PORT%)... +if defined PROXY_AUTH_TOKEN ( + echo [INFO] Bearer 校验: 已启用 +) else ( + echo [WARN] Bearer 校验: 未启用 - 任何能访问域名的人都能调用 API! +) +start "copilot-api" cmd /k "cd /d %PROJECT_DIR% && set PROXY_AUTH_TOKEN=%PROXY_AUTH_TOKEN%&& bun run start --port %COPILOT_API_PORT%" + +REM --- 等几秒让服务起来 --- +echo [INFO] 等待 copilot-api 就绪... +timeout /t 5 /nobreak >nul + +REM --- 启动 cloudflared (新窗口) --- +echo [INFO] 启动 cloudflared tunnel... +start "cloudflared" cmd /k "cloudflared tunnel run --token %CLOUDFLARE_TUNNEL_TOKEN%" + +echo. +echo ================================================ +echo [OK] 已启动两个窗口: +echo 1. copilot-api -> http://localhost:%COPILOT_API_PORT% +echo 2. cloudflared -> 你的域名 (在 Cloudflare 后台配置 Service: +echo http://localhost:%COPILOT_API_PORT%) +echo ================================================ +echo. +echo 关闭对应窗口即可停止服务。 +endlocal diff --git a/deploy/uninstall-services.cmd b/deploy/uninstall-services.cmd new file mode 100644 index 000000000..3d1aa39ae --- /dev/null +++ b/deploy/uninstall-services.cmd @@ -0,0 +1,28 @@ +@echo off +REM ============================================================ +REM 一键卸载两个服务 (copilot-api + cloudflared) +REM 需要管理员权限 +REM ============================================================ +setlocal + +net session >nul 2>&1 +if %ERRORLEVEL% NEQ 0 ( + echo [ERROR] 需要以管理员身份运行 + pause + exit /b 1 +) + +echo [INFO] 停止并卸载 copilot-api... +nssm stop copilot-api confirm >nul 2>&1 +nssm remove copilot-api confirm + +echo. +echo [INFO] 停止并卸载 cloudflared... +net stop cloudflared >nul 2>&1 +cloudflared service uninstall >nul 2>&1 +sc delete cloudflared >nul 2>&1 + +echo. +echo [OK] 两个服务已卸载 +pause +endlocal diff --git a/pages/index.html b/pages/index.html deleted file mode 100644 index 57d16ef02..000000000 --- a/pages/index.html +++ /dev/null @@ -1,556 +0,0 @@ - - - - - - Copilot API Usage Dashboard - - - - - - - - - - - - -
-
- -
-

- - - - - - Copilot API Usage Dashboard -

-

- Should be the same as the one in VSCode -

-
- - -
-
- - - -
-
- - -
-
-
- - - - - - diff --git a/src/routes/token/route.ts b/src/routes/token/route.ts deleted file mode 100644 index dd0456d9a..000000000 --- a/src/routes/token/route.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Hono } from "hono" - -import { state } from "~/lib/state" - -export const tokenRoute = new Hono() - -tokenRoute.get("/", (c) => { - try { - return c.json({ - token: state.copilotToken, - }) - } catch (error) { - console.error("Error fetching token:", error) - return c.json({ error: "Failed to fetch token", token: null }, 500) - } -}) diff --git a/src/routes/usage/route.ts b/src/routes/usage/route.ts deleted file mode 100644 index 3e9473236..000000000 --- a/src/routes/usage/route.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Hono } from "hono" - -import { getCopilotUsage } from "~/services/github/get-copilot-usage" - -export const usageRoute = new Hono() - -usageRoute.get("/", async (c) => { - try { - const usage = await getCopilotUsage() - return c.json(usage) - } catch (error) { - console.error("Error fetching Copilot usage:", error) - return c.json({ error: "Failed to fetch Copilot usage" }, 500) - } -}) diff --git a/src/server.ts b/src/server.ts index 462a278f3..57ad63c2f 100644 --- a/src/server.ts +++ b/src/server.ts @@ -6,21 +6,34 @@ import { completionRoutes } from "./routes/chat-completions/route" import { embeddingRoutes } from "./routes/embeddings/route" import { messageRoutes } from "./routes/messages/route" import { modelRoutes } from "./routes/models/route" -import { tokenRoute } from "./routes/token/route" -import { usageRoute } from "./routes/usage/route" export const server = new Hono() server.use(logger()) server.use(cors()) +// Optional Bearer-token gate for public exposure (e.g. via Cloudflare Tunnel). +// Enabled only when PROXY_AUTH_TOKEN is set; "/" health check stays open. +const PROXY_AUTH_TOKEN = process.env.PROXY_AUTH_TOKEN +if (PROXY_AUTH_TOKEN) { + server.use("*", async (c, next) => { + if (c.req.path === "/") return next() + const header = c.req.header("authorization") ?? "" + const token = header.startsWith("Bearer ") ? header.slice(7) : "" + // x-api-key fallback for Anthropic SDK style clients + const apiKey = c.req.header("x-api-key") ?? "" + if (token !== PROXY_AUTH_TOKEN && apiKey !== PROXY_AUTH_TOKEN) { + return c.json({ error: "Unauthorized" }, 401) + } + return next() + }) +} + server.get("/", (c) => c.text("Server running")) server.route("/chat/completions", completionRoutes) server.route("/models", modelRoutes) server.route("/embeddings", embeddingRoutes) -server.route("/usage", usageRoute) -server.route("/token", tokenRoute) // Compatibility with tools that expect v1/ prefix server.route("/v1/chat/completions", completionRoutes) diff --git a/src/start.ts b/src/start.ts index f27cb5847..7105fa88b 100644 --- a/src/start.ts +++ b/src/start.ts @@ -127,9 +127,7 @@ export async function runServer(options: RunServerOptions): Promise { } } - consola.box( - `🌐 Usage Viewer: https://ericc-ch.github.io/copilot-api?endpoint=${serverUrl}/usage`, - ) + consola.box(`Server running at ${serverUrl}`) const idleTimeout = process.env.IDLE_TIMEOUT ? diff --git a/start-copilot-api.cmd b/start-copilot-api.cmd index 492e05f58..84492c237 100644 --- a/start-copilot-api.cmd +++ b/start-copilot-api.cmd @@ -1,6 +1,6 @@ @echo off echo ================================================ -echo GitHub Copilot API Server with Usage Viewer +echo GitHub Copilot API Server echo Start Copilot API Server at %~dp0 echo ================================================ echo. diff --git a/start.bat b/start.bat index c621304bd..9c358379b 100644 --- a/start.bat +++ b/start.bat @@ -1,6 +1,6 @@ @echo off echo ================================================ -echo GitHub Copilot API Server with Usage Viewer +echo GitHub Copilot API Server echo ================================================ echo. @@ -11,10 +11,8 @@ if not exist node_modules ( ) echo Starting server... -echo The usage viewer page will open automatically after the server starts echo. -start "" "https://ericc-ch.github.io/copilot-api?endpoint=http://localhost:4141/usage" bun run dev -pause \ No newline at end of file +pause From 6d72e927fe8f62d0911253195ef78d607df538fd Mon Sep 17 00:00:00 2001 From: partychen Date: Thu, 18 Jun 2026 10:50:07 +0800 Subject: [PATCH 2/4] Apply model mapping in count_tokens handler and add opus-4-8 mapping The count_tokens handler was missing the model mapping step that the messages and chat-completions handlers already had, so requests using a mapped source name (e.g. claude-opus-4-8) failed model lookup and returned a default token count, surfacing as "model not found" in clients. Add the same mapping logic and register claude-opus-4-8. Co-Authored-By: Claude --- .env | 2 +- src/routes/messages/count-tokens-handler.ts | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.env b/.env index e637682be..87e4061fb 100644 --- a/.env +++ b/.env @@ -14,4 +14,4 @@ IDLE_TIMEOUT=255 # Format: "source1:target1,source2:target2" # Example: "claude-sonnet-4-20250514:claude-opus-4,gpt-4:gpt-4-turbo" # MODEL_MAPPINGS="claude-sonnet-4-5-20250929:claude-sonnet-4.5,claude-opus-4-5-20251101:claude-opus-4.5,claude-haiku-4-5-20251001:claude-haiku-4.5" -MODEL_MAPPINGS="claude-opus-4-6:claude-opus-4.6,claude-sonnet-4-5:claude-sonnet-4.5,claude-opus-4-5:claude-opus-4.5,claude-haiku-4-5:claude-haiku-4.5" +MODEL_MAPPINGS="claude-opus-4-8:claude-opus-4.8,claude-opus-4-6:claude-opus-4.6,claude-sonnet-4-5:claude-sonnet-4.5,claude-opus-4-5:claude-opus-4.5,claude-haiku-4-5:claude-haiku-4.5" diff --git a/src/routes/messages/count-tokens-handler.ts b/src/routes/messages/count-tokens-handler.ts index 2ec849cb8..65aaa0821 100644 --- a/src/routes/messages/count-tokens-handler.ts +++ b/src/routes/messages/count-tokens-handler.ts @@ -2,6 +2,7 @@ import type { Context } from "hono" import consola from "consola" +import { applyModelMapping, getModelMappings } from "~/lib/model-mapping" import { state } from "~/lib/state" import { getTokenCount } from "~/lib/tokenizer" @@ -15,7 +16,19 @@ export async function handleCountTokens(c: Context) { try { const anthropicBeta = c.req.header("anthropic-beta") - const anthropicPayload = await c.req.json() + let anthropicPayload = await c.req.json() + + const mappings = getModelMappings() + if (mappings.size > 0) { + const { model, mapped } = applyModelMapping( + anthropicPayload.model, + mappings, + state.verbose, + ) + if (mapped) { + anthropicPayload = { ...anthropicPayload, model } + } + } const openAIPayload = translateToOpenAI(anthropicPayload) From f6afbdd4d374b09eeda0d48fe1019ef4cca9054d Mon Sep 17 00:00:00 2001 From: partychen Date: Thu, 18 Jun 2026 11:22:05 +0800 Subject: [PATCH 3/4] Fix assistant-prefill 400 on tool-terminated conversations GitHub Copilot's claude-opus-4.8 rejects requests whose final upstream message is not a user message ("assistant message prefill"). A trailing Anthropic tool_result translates to an OpenAI `tool` role message, so conversations ending in a tool call hit a 400. Append a placeholder user message when the translated conversation does not end with a user turn. Also switch local Claude CLI config to claude-opus-4-8. Co-Authored-By: Claude --- settings.json | 8 ++++---- src/routes/messages/non-stream-translation.ts | 9 +++++++++ start-claude.cmd | 4 ++-- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/settings.json b/settings.json index 90974fe69..92eaefd8a 100644 --- a/settings.json +++ b/settings.json @@ -2,11 +2,11 @@ "env": { "ANTHROPIC_BASE_URL": "http://localhost:4141", "ANTHROPIC_AUTH_TOKEN": "dummy", - "ANTHROPIC_MODEL": "claude-opus-4-6", + "ANTHROPIC_MODEL": "claude-opus-4-8", "ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-4-5", "ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-haiku-4-5", - "ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-opus-4-6", - "CLAUDE_CODE_SUBAGENT_MODEL": "claude-opus-4-6", + "ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-opus-4-8", + "CLAUDE_CODE_SUBAGENT_MODEL": "claude-opus-4-8", "CLAUDE_CODE_ATTRIBUTION_HEADER": "0", "CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD": "1" }, @@ -15,6 +15,6 @@ "WebSearch" ] }, - "model": "claude-opus-4-6", + "model": "claude-opus-4-8", "autoUpdatesChannel": "latest" } \ No newline at end of file diff --git a/src/routes/messages/non-stream-translation.ts b/src/routes/messages/non-stream-translation.ts index dc41e6382..81fd19310 100644 --- a/src/routes/messages/non-stream-translation.ts +++ b/src/routes/messages/non-stream-translation.ts @@ -68,6 +68,15 @@ function translateAnthropicMessagesToOpenAI( : handleAssistantMessage(message), ) + // Some upstream models (e.g. claude-opus-4.8 via GitHub Copilot) reject + // requests whose final message is not a user message ("assistant message + // prefill"). A trailing tool result translates to a `tool` role message, so + // append a placeholder user message to keep the conversation user-terminated. + const lastMessage = otherMessages.at(-1) + if (lastMessage && lastMessage.role !== "user") { + otherMessages.push({ role: "user", content: "Continue." }) + } + return [...systemMessages, ...otherMessages] } diff --git a/start-claude.cmd b/start-claude.cmd index 61dd2469d..292229f6c 100644 --- a/start-claude.cmd +++ b/start-claude.cmd @@ -1,11 +1,11 @@ @ECHO OFF set ANTHROPIC_BASE_URL=http://localhost:4141 set ANTHROPIC_AUTH_TOKEN=dummy -set ANTHROPIC_MODEL=claude-opus-4-6 +set ANTHROPIC_MODEL=claude-opus-4-8 set ANTHROPIC_DEFAULT_SONNET_MODEL=claude-sonnet-4-5 set ANTHROPIC_SMALL_FAST_MODEL=claude-sonnet-4-5 set ANTHROPIC_DEFAULT_HAIKU_MODEL=claude-haiku-4-5 -set CLAUDE_CODE_SUBAGENT_MODEL=claude-opus-4-6 +set CLAUDE_CODE_SUBAGENT_MODEL=claude-opus-4-8 set CLAUDE_CODE_ATTRIBUTION_HEADER=0 set CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1 set CLAUDE_AUTOCOMPACT_PCT_OVERRIDE=95 From e7787931c479d6632543d94128fedc9ebe96af45 Mon Sep 17 00:00:00 2001 From: partychen Date: Thu, 18 Jun 2026 12:59:35 +0800 Subject: [PATCH 4/4] Only inject Continue placeholder on assistant-terminated turns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The assistant-prefill fix in f6afbdd used the condition `role !== "user"`, which also matched tool-terminated turns. In the Claude Code agent loop, a turn whose final user message carries only a tool_result translates to an OpenAI `tool` role message, so every tool round hit the condition and got a spurious `{role:"user", content:"Continue."}` appended — making the model respond to "Continue." instead of the real tool result. Verified directly against api.githubcopilot.com that claude-opus-4.8 returns 200 for tool-terminated conversations and only 400s on assistant-terminated ones ("This model does not support assistant message prefill. The conversation must end with a user message."). Narrow the condition to `role === "assistant"` so only genuine prefill is patched. Add regression tests covering both: tool_result-terminated must NOT append Continue, assistant-terminated must. Co-Authored-By: Claude Opus 4.8 --- src/routes/messages/non-stream-translation.ts | 12 ++-- tests/anthropic-request.test.ts | 67 +++++++++++++++++++ 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/src/routes/messages/non-stream-translation.ts b/src/routes/messages/non-stream-translation.ts index 81fd19310..fba5f70be 100644 --- a/src/routes/messages/non-stream-translation.ts +++ b/src/routes/messages/non-stream-translation.ts @@ -69,11 +69,15 @@ function translateAnthropicMessagesToOpenAI( ) // Some upstream models (e.g. claude-opus-4.8 via GitHub Copilot) reject - // requests whose final message is not a user message ("assistant message - // prefill"). A trailing tool result translates to a `tool` role message, so - // append a placeholder user message to keep the conversation user-terminated. + // requests that end with an assistant message ("assistant message prefill"): + // "This model does not support assistant message prefill. The conversation + // must end with a user message." + // A `tool` role message is a VALID terminator (verified: tool-terminated + // requests return 200), so only patch the genuine assistant-prefill case. + // Patching tool-terminated turns would inject a spurious "Continue." into + // every agent tool round. const lastMessage = otherMessages.at(-1) - if (lastMessage && lastMessage.role !== "user") { + if (lastMessage && lastMessage.role === "assistant") { otherMessages.push({ role: "user", content: "Continue." }) } diff --git a/tests/anthropic-request.test.ts b/tests/anthropic-request.test.ts index 06c663778..72f0602a9 100644 --- a/tests/anthropic-request.test.ts +++ b/tests/anthropic-request.test.ts @@ -156,6 +156,73 @@ describe("Anthropic to OpenAI translation logic", () => { expect(assistantMessage?.content).toContain("2+2 equals 4.") }) + test("should NOT append 'Continue.' when conversation ends in a tool_result", () => { + // Agent loop turn: the final user message carries only a tool_result, which + // translates to a `tool` role message. That is a valid terminator for + // Copilot (returns 200), so the proxy must not inject a placeholder user + // turn — doing so would forge a "Continue." on every tool round. + const anthropicPayload: AnthropicMessagesPayload = { + model: "claude-opus-4-8", + messages: [ + { role: "user", content: "What's the weather?" }, + { + role: "assistant", + content: [ + { type: "text", text: "Let me check the weather for you." }, + { + type: "tool_use", + id: "call_123", + name: "get_weather", + input: { location: "New York" }, + }, + ], + }, + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "call_123", + content: "Sunny, 75F", + }, + ], + }, + ], + max_tokens: 100, + } + const openAIPayload = translateToOpenAI(anthropicPayload) + expect(isValidChatCompletionRequest(openAIPayload)).toBe(true) + + const lastMessage = openAIPayload.messages.at(-1) + expect(lastMessage?.role).toBe("tool") + // No forged user turn should have been appended. + expect( + openAIPayload.messages.some( + (m) => m.role === "user" && m.content === "Continue.", + ), + ).toBe(false) + }) + + test("should append 'Continue.' when conversation ends in an assistant message", () => { + // Genuine assistant prefill: Copilot's claude-opus-4.8 rejects this with a + // 400 unless the conversation ends with a user message. The proxy appends a + // placeholder user turn to keep it valid. + const anthropicPayload: AnthropicMessagesPayload = { + model: "claude-opus-4-8", + messages: [ + { role: "user", content: "Complete this sentence with one word." }, + { role: "assistant", content: "The capital of France is" }, + ], + max_tokens: 100, + } + const openAIPayload = translateToOpenAI(anthropicPayload) + expect(isValidChatCompletionRequest(openAIPayload)).toBe(true) + + const lastMessage = openAIPayload.messages.at(-1) + expect(lastMessage?.role).toBe("user") + expect(lastMessage?.content).toBe("Continue.") + }) + test("should handle thinking blocks with tool calls", () => { const anthropicPayload: AnthropicMessagesPayload = { model: "claude-3-5-sonnet-20241022",