# agent-proxy **Repository Path**: mkee/agent-proxy ## Basic Information - **Project Name**: agent-proxy - **Description**: 局域网大模型代理工具 - **Primary Language**: C++ - **License**: MulanPSL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-16 - **Last Updated**: 2026-09-16 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # agent-proxy 本地多上游大模型代理。零第三方依赖的 C++ 实现:入站用 Winsock,出站用 WinHTTP(原生支持 HTTPS)。 调用方只需面向本代理的一个 OpenAI 兼容地址,由代理按「模型别名」或「路径前缀」路由到不同上游,并用配置中的真实 `api_key` 替换调用方凭据。 > 当前版本(v1)仅支持 **Chat Completions**。 > 面向使用者的操作指南(配置、启动、接入客户端、故障排查)见 [`docs/USER_GUIDE.md`](docs/USER_GUIDE.md)。 ## 特性 - 多个上游地址与 API Key 本地配置,调用方无需持有上游密钥 - 两种选路方式:模型别名映射、`/{provider}/...` 路径前缀直连 - 流式(SSE / `stream: true`)逐块透传,非流式按 `Content-Length` 透传 - 模型别名可自动改写为上游真实模型名(body 原文其他字段保持不变) - 别名未命中时可按 `providers[].models` 匹配,再回落到 `default_provider` - 专属 `User-Agent` 标识(可全局或按 provider 覆盖),不再转发客户端的 SDK UA - 自动填充稳定的会话 ID 请求头(默认 `x-opencode-session`),可按 provider 限定生效范围,便于上游做路由与提示词缓存 - 可选的调用方鉴权(`server.api_keys`) - 内置 CORS,`OPTIONS` 预检直接返回 - 静态链接,产物不依赖 ucrt64 运行库 DLL(仅依赖系统 DLL) ## 环境要求 - Windows - MSYS2(默认路径 `D:\msys64`)中的 `ucrt64` 工具链:`g++` 15+(C++17) ## 构建 ```bat build.bat ``` 产物:`build\agent-proxy.exe` 也可用 Makefile(需要 MSYS2 的 `make`): ```bash make # 编译 make clean # 清理 make run # 编译并运行 config.json ``` 主要编译参数:`-std=c++17 -O2 -Wall -Wextra -static`,链接 `-lwinhttp -lws2_32`。 ## 快速开始 ```bat copy config.example.json config.json rem 编辑 config.json,填入上游 base_url / api_key / 别名 build\agent-proxy.exe config.json ``` 不带配置文件参数时,按以下顺序查找 `config.json`:**exe 同目录 → exe 上级目录 → 当前工作目录**(因此 `build\agent-proxy.exe` 不加参数也会读到项目根目录的配置)。可用 `--port N` 临时覆盖端口。 启动后: ``` agent-proxy listening on http://0.0.0.0:8787 ``` 测试: ```bash curl http://127.0.0.1:8787/health curl http://127.0.0.1:8787/v1/models ``` ## 端点 | 方法 | 路径 | 说明 | | --- | --- | --- | | POST | `/v1/chat/completions` | 按请求体 `model` 查别名路由 | | POST | `/{provider}/v1/chat/completions` | 直连指定 provider,body 原样透传 | | POST | `/{provider}/chat/completions` | 同上(简写) | | GET | `/v1/models` | 输出别名与各 provider 模型列表 | | GET | `/health`、`/healthz` | 健康检查 | | OPTIONS | 任意 | CORS 预检,返回 204 | ### 路由规则 `POST /v1/chat/completions` 时,按以下顺序解析 `model`: 1. `aliases[]` 中 `name` 命中 → 使用其 `provider`;若 `model` 非空且与请求不同,则改写请求体中的 `model` 2. 某个 `providers[].models` 命中 → 使用该 provider,模型名不变 3. 回落到 `server.default_provider`;未配置该项时使用 `providers` 列表中的第一个,模型名不变 4. 都不满足 → `400` 上游最终地址为 `provider.base_url + provider.chat_path`(`chat_path` 默认 `/chat/completions`)。若 `base_url` 末尾已带 `/chat/completions` 或多余的 `/`,会被自动规整,避免路径重复。 ### 请求头处理 | 请求头 | 行为 | | --- | --- | | `Authorization` | 丢弃,替换为 provider 的 `api_key`(`api_key` 为空则不发送) | | `User-Agent` | 丢弃,替换为配置的专属标识 | | `Content-Type`、`Accept` | 透传(缺省分别为 `application/json`) | | `Accept-Encoding` | 固定 `identity`,避免压缩流破坏 SSE 透传 | | `session.header` | 客户端已带则透传,否则按该 provider 的 `session` 配置自动填充 | | `providers[].extra_headers` | 追加,同名时覆盖以上默认值 | ## 配置说明 ```json { "server": { "...": "..." }, "session": { "...": "..." }, "providers": [ "..." ], "aliases": [ "..." ] } ``` `server` 与 `providers` 必需,`aliases`、`session` 可选(`session` 不写即不追加会话请求头)。 ### server | 字段 | 类型 | 默认 | 说明 | | --- | --- | --- | --- | | `host` | string | `0.0.0.0` | 监听地址,`0.0.0.0`/`*` 为全部网卡 | | `port` | int | `8787` | 监听端口 | | `api_keys` | string[] | `[]` | 调用方密钥;为空则不鉴权 | | `default_provider` | string | 空 | 别名/模型都未命中时的回落 provider;**未填写时自动使用 `providers` 列表中的第一个** | | `user_agent` | string | `agent-proxy/1.0` | 出站 User-Agent | | `max_body_bytes` | int | `67108864` | 请求体上限(64 MB) | | `resolve_timeout_ms` | int | `15000` | DNS 解析超时 | | `connect_timeout_ms` | int | `15000` | 连接超时 | | `send_timeout_ms` | int | `120000` | 发送超时 | | `receive_timeout_ms` | int | `600000` | 接收超时(长推理可调大) | ### session | 字段 | 类型 | 默认 | 说明 | | --- | --- | --- | --- | | `auto_fill` | bool | `false` | 客户端未带该头时是否自动填充。默认关闭,需显式开启 | | `header` | string | `x-opencode-session` | 请求头名 | | `id` | string | 空 | 固定会话 ID;非空时始终使用该值 | | `prefix` | string | `sess-` | 自动生成 ID 的前缀 | 自动生成的规则:取 `model + system 消息 + 首条 user 消息`(各截断 4096 字节)做 FNV-1a64 哈希,得到 `sess-<16 位十六进制>`。 同一对话多轮追加历史不改变前缀,因此 ID 稳定;不同对话 ID 不同。请求体超过 4 MB 或无法解析 `messages` 时退化为请求体前 4096 字节的哈希。 客户端自带该头时始终原样透传,不受 `auto_fill` 影响。 `auto_fill` 默认关闭。推荐直接在需要它的 provider 上开启,例如只对 `https://opencode.ai/zen/go/v1` 追加该请求头: ```json { "providers": [ { "id": "opencode", "base_url": "https://opencode.ai/zen/go/v1", "api_key": "sk-xxx", "session": { "auto_fill": true, "header": "x-opencode-session", "prefix": "sess-" } } ] } ``` 顶层的 `session` 是全局默认值,可用于给所有 provider 统一开启;`providers[].session` 只覆盖写出的字段,未声明的 provider 沿用全局。两者都不写时,所有 provider 默认不填充。 ### providers | 字段 | 类型 | 必需 | 默认 | 说明 | | --- | --- | --- | --- | --- | | `id` | string | 是 | - | provider 标识,用于别名与路径前缀 | | `base_url` | string | 是 | - | 上游 API 根地址,通常含 `/v1` | | `api_key` | string | 否 | 空 | 上游密钥;为空则不发送鉴权头 | | `auth_header` | string | 否 | `Authorization` | 鉴权头名 | | `auth_scheme` | string | 否 | `Bearer` | 鉴权前缀;为空则直接使用 key | | `chat_path` | string | 否 | `/chat/completions` | 追加到 `base_url` 的路径 | | `user_agent` | string | 否 | 继承 `server.user_agent` | 该 provider 单独使用的 UA | | `models` | string[] | 否 | `[]` | 该 provider 支持的模型名 | | `extra_headers` | object | 否 | `{}` | 追加的请求头 | | `session` | object | 否 | 继承全局 | 该 provider 专用的会话头配置,字段同全局 `session`(未写的字段继承全局) | ### aliases | 字段 | 类型 | 必需 | 说明 | | --- | --- | --- | --- | | `name` | string | 是 | 对外暴露的模型名 | | `provider` | string | 是 | 目标 provider 的 `id` | | `model` | string | 否 | 上游真实模型名;为空则沿用请求中的 `model` | 同名别名会覆盖 `providers[].models` 的匹配结果。别名 `provider` 必须存在,否则启动报错。 ### 完整示例 见 [`config.example.json`](config.example.json)。 ## 调用方鉴权 `server.api_keys` 非空时,除 `/health` 外的接口都要求: ``` Authorization: Bearer ``` 校验失败返回 `401`。注意该头用于**调用方 → 代理**鉴权,代理转发时会替换为上游密钥。 ## 客户端接入 代理地址:`http://127.0.0.1:8787/v1`,`apiKey` 填任意非空字符串(未开启鉴权时)或 `api_keys` 中的值。 ### curl ```bash curl http://127.0.0.1:8787/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hi"}]}' # 流式 curl -N http://127.0.0.1:8787/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"model":"gpt-4o-mini","stream":true,"messages":[{"role":"user","content":"hi"}]}' ``` ### Python ```python from openai import OpenAI client = OpenAI(base_url="http://127.0.0.1:8787/v1", api_key="sk-local") resp = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": "hi"}], ) print(resp.choices[0].message.content) ``` ### Node.js ```js import OpenAI from "openai"; const client = new OpenAI({ baseURL: "http://127.0.0.1:8787/v1", apiKey: "sk-local" }); const resp = await client.chat.completions.create({ model: "gpt-4o-mini", messages: [{ role: "user", content: "hi" }], }); console.log(resp.choices[0].message.content); ``` ### 通用环境变量 ```bat set OPENAI_BASE_URL=http://127.0.0.1:8787/v1 set OPENAI_API_KEY=sk-local ``` ### opencode `opencode.json`(项目根或 `~/.config/opencode/opencode.json`): ```json { "$schema": "https://opencode.ai/config.json", "provider": { "local-proxy": { "npm": "@ai-sdk/openai-compatible", "name": "Local Agent Proxy", "options": { "baseURL": "http://127.0.0.1:8787/v1", "apiKey": "sk-local" }, "models": { "gpt-4o-mini": { "name": "gpt-4o-mini (proxy)" }, "deepseek-chat": { "name": "deepseek-chat (proxy)" } } } } } ``` `models` 的键需与 `GET /v1/models` 返回的 id 一致。 ## 错误响应 ```json { "error": { "message": "unknown provider: nope", "type": "proxy_error", "code": 404 } } ``` | 状态码 | 场景 | | --- | --- | | 400 | 请求体为空、缺少 `model`、无法解析路由、没有可用 provider | | 401 | 需要鉴权但未提供或密钥错误 | | 404 | 未知 provider 或路径 | | 405 | 路由存在但方法不是 POST | | 413 | 请求体超过 `max_body_bytes` | | 502 | 上游请求失败(连接、超时、TLS 等) | 上游返回的非 2xx 响应(如 400/429)会连同状态码、`Content-Type` 和响应体原样透传。 ## 目录结构 ``` src/json.* 迷你 JSON 解析/序列化,定位顶层 model 字段 src/util.* 字符串、UTF-8 转换、哈希、状态码 src/config.* 配置加载与校验 src/upstream.* WinHTTP 出站客户端(流式回调) src/server.* Winsock HTTP 服务(线程/连接,chunked 流式下发) src/proxy.* 路由、鉴权、请求头处理、转发 src/main.cpp 入口,Ctrl+C 优雅退出 config.example.json build.bat / Makefile docs/USER_GUIDE.md tools/md2docx.py 把使用手册 markdown 转成 docx(可选) ``` ## 已知限制 - 仅支持 Chat Completions,不含 embeddings / images / audio / responses 等 - 每个连接处理一个请求后关闭(`Connection: close`),未实现 keep-alive - 无内置请求日志与指标(可自行在 `proxy.cpp` 中扩展) - 会话 ID 为内容指纹,不维护服务端状态;客户端截断历史会改变 ID