跳转至

EasyCLIProxyAPI 本地网关架构与多智能体客户端适配

本文目标:全面梳理本地大模型网关从非官方分叉(ZCode-Antigravity)向官方稳定核心(EasyCLIProxyAPI 7.2.149+)迁移的演进历程;详解 Hermes AgentZCode 双端接入使用 Gemini 3.8/3.7 Flash 对话推理、多模态文生图 Skill(gemini-3.1-flash-image / Nano Banana 2)的完整配置步骤、关键配置文件、避坑指南与全流程故障速查表。

实测环境:Windows 11 / EasyCLIProxyAPI 0.2.71 (Core 7.2.149) / Hermes Agent / ZCode 客户端 / Google AI Pro 个人订阅。

一、 本地模型网关的架构演进

在日常使用多种 AI 编程助手(如 Hermes Agent、ZCode、Claude Code、Codex)时,很多开发者选择在本地搭建网关以统一承接 Google Antigravity、Kimi、Claude 等模型渠道。

               ┌───────────────────────┐
               │     Hermes Agent      │ (OpenAI 兼容协议 / 18080)
               └───────────┬───────────┘
               ┌───────────────────────┐
┌─────────────►│    EasyCLIProxyAPI    │◄────────────┐
│              │ (官方核心 v7.2.149)   │             │
│              └───────────┬───────────┘             │
│                          │                         │
│ (Anthropic 协议 / 18080) │                         │
│                          ▼                         │
┌─────────────┐  ┌───────────────────┐ ┌─────────────┴──────────┐
│    ZCode    │  │ 本地代理 127.0.0.1 │ │ 其它 CLI 智能体 (Codex) │
└─────────────┘  └─────────┬─────────┘ └────────────────────────┘
              ┌─────────────────────────┐
              │ Google Antigravity 服务 │
              └─────────────────────────┘

1. 早期分叉分支的局限与风险

早期社区存在基于旧版本修改的中间派生版本(例如 ZCode-Antigravity 7.2.132-zcode)。在实际生产使用中,此类版本暴露出以下问题: - 凭据丢失陷阱:官方二进制在发布期通过 -X ldflags 注入了官方发布的 OAuth Client ID 与 Secret;若在本地直接裸 go build 编译,源码中的变量为空,会导致运行时抛出 500 OAuth client is not configured,随后标记为 503 auth_unavailable,造成大面积鉴权不可用; - 私有接口依赖:派生版本常会添加非官方标准接口(如 /v0/management/api-call),导致外部插件产生私有依赖,一旦版本升级就会彻底失效。

2. 迁移至官方核心的优势

随着上游官方核心演进至 7.2.149(EasyCLIProxyAPI 桌面控制台标配): - 原生多模型支持:上游已原生集成 gemini-3.8-flashgemini-3.8-flash-highgemini-3.7-flash 等全系模型及思考推理链(Thinking Variant); - 官方内嵌安全凭据:官方发行版自带合法 OAuth 认证身份,规避本地编译导致凭据丢失的风险; - 双协议原生互通:同时支持标准 OpenAI 格式(/v1/chat/completions)与 Anthropic Messages 格式(/v1/messages),Hermes Agent 与 ZCode 可共用同一个 127.0.0.1:18080 端口无缝并发调用。

二、 Windows 客户端探查陷阱与目录联接(Junction)

在 EasyCLIProxyAPI 桌面控制台的「智能体配置」中,有时会遇到一个典型问题:本地明明确认已安装了 ZCode 等客户端,但界面却弹出黄色警告“只检测到配置文件,未检测到客户端”,且右下角启动按钮显示“无法启动”被禁用

1. 探查机制排查

反编译与特征码扫描显示,控制台在 Windows 上采用固定硬编码的规范路径来探测客户端可执行文件: - %LOCALAPPDATA%\Programs\<Agent>\<Agent>.exe(用户级安装规范路径) - %ProgramFiles%\<Agent>\<Agent>.exe(系统级 64 位标准安装路径)

若用户将客户端安装在非系统盘(例如 D:\zcode\ZCode.exe),控制台仅能在用户目录(~/.zcode)找到配置,却无法在默认路径找到程序实体,因而禁用启动逻辑。

2. 优雅解决方案:NTFS 目录联接(Junction)

无需搬迁文件或重装软件,只需以管理员权限在命令行中建立 NTFS 目录联接即可:

:: 映射到用户本地应用规范路径
mklink /J "%LOCALAPPDATA%\Programs\ZCode" "D:\zcode"

:: 映射到系统 Program Files 规范路径
mklink /J "%ProgramFiles%\ZCode" "D:\zcode"

原理解析: mklink /J 是目录级重解析点(reparse point),效果上类似路径别名——与文件级硬链接是不同机制 - 建立联接后,控制台在标准路径即可瞬间探测到 ZCode.exe,版本信息立刻正常显示,“无法启动”按钮随之恢复为正常启动控制。

三、 Hermes Agent 接入使用 Gemini 配置实战

Hermes Agent 底层采用标准 OpenAI 兼容格式对接本地网关,主要通过配置文件 ~/.hermes/config.yaml 管理。

1. 主模型与辅助模型配置

config.yaml 中配置默认主力模型与提供商:

model:
  default: gemini-3.8-flash
  provider: cpa-gui
  base_url: http://127.0.0.1:18080/v1

auxiliary:
  vision:
    provider: cpa-gui
    model: gemini-3.8-flash

agent:
  reasoning_effort: ultra # 开启 Gemini 3.8 Flash Ultra 思考链

2. 自定义提供商注册 (custom_providers)

确保在 custom_providers 列表内注册统一且唯一的 cpa-gui 项:

custom_providers:
  - name: cpa-gui
    base_url: http://127.0.0.1:18080/v1
    api_key: <YOUR_GATEWAY_KEY> # 取自 EasyCLIProxyAPI 的 api-keys
    api_mode: chat_completions
    model: gemini-3.8-flash
    models_discovered: true
    models:
      gemini-3.8-flash: {}
      gemini-3.7-flash: {}
      gemini-3.6-flash: {}
      gemini-3.1-pro-low: {}
      gemini-web-search: {}
      claude-sonnet-4-6: {}
      claude-opus-4-6-thinking: {}
      gpt-oss-120b-medium: {}

3. 注意点与防坑准则

  • 严禁重复定义提供商:旧版配置常遗留 Local (127.0.0.1:18080)。若与 cpa-gui 同时存在,二者打向同一端口,会导致桌面 GUI 的模型下拉框内出现两套完全重合的模型列表。必须清理掉冗余项;
  • 凭据池同步清理:编辑 config.yaml 去除重复项后,需同步检查 ~/.hermes/auth.json 中的 credential_pool,删除废弃条目;
  • 浏览器沙箱强制隔离:配置 browser.use_real_profile: false,避免 Agent 浏览器操作污染甚至清空日常 Edge/Chrome 的扩展注册表。

[!WARNING] 历史存档(2026-09-12) 第四、五节以 ZCode 客户端为配置载体。ZCode 已于 2026-09-09 弃用并卸载~/.zcode 相关路径已不存在;但网关侧的双协议适配(/v1/messages Anthropic 格式转换)仍然有效,适用于任何 Anthropic 协议客户端。以下按写作时点存档。

四、 ZCode 客户端接入使用 Gemini 配置实战

ZCode 客户端与 OpenAI 格式不同,其底层采用的是 Anthropic Messages 协议(/v1/messages。EasyCLIProxyAPI 官方核心原生支持此格式转换。

1. 配置文件双层定位

ZCode 的配置分为两层,建议同步配置: 1. 全局默认配置C:\Users\<用户名>\.zcode\v2\config.json 2. 工作区定制配置<项目根目录>\.zcode\v2\config.json(若存在)

2. 提供商注入 (zcode-antigravity-local)

config.jsonprovider 字典中注入 Google 本地网关节点:

{
  "provider": {
    "zcode-antigravity-local": {
      "name": "Google",
      "kind": "anthropic",
      "options": {
        "apiKey": "<YOUR_GATEWAY_KEY>",
        "baseURL": "http://127.0.0.1:18080",
        "apiKeyRequired": true
      },
      "enabled": true,
      "source": "custom",
      "x-zcode-antigravity-managed": 1,
      "models": {
        "gemini-3.8-flash": {
          "name": "Gemini 3.8 Flash",
          "limit": { "context": 1048576 },
          "modalities": {
            "input": ["text", "image", "audio", "video"],
            "output": ["text"]
          },
          "reasoning": {
            "enabled": true,
            "variants": ["low", "medium", "high"],
            "defaultVariant": "high"
          },
          "zcode": { "priority": 200 }
        },
        "gemini-3.7-flash": {
          "name": "Gemini 3.7 Flash",
          "limit": { "context": 1048576 },
          "modalities": {
            "input": ["text", "image", "audio", "video"],
            "output": ["text"]
          },
          "reasoning": {
            "enabled": true,
            "variants": ["low", "medium", "high"],
            "defaultVariant": "high"
          },
          "zcode": { "priority": 201 }
        },
        "gemini-3.1-pro-low": {
          "name": "Gemini 3.1 Pro (Low)",
          "limit": { "context": 1048576 },
          "modalities": {
            "input": ["text", "image", "audio", "video"],
            "output": ["text"]
          },
          "reasoning": { "enabled": true, "variants": ["low", "medium", "high"], "defaultVariant": "low" },
          "zcode": { "priority": 203 }
        },
        "gemini-web-search": {
          "name": "Gemini Web Search (Google)",
          "limit": { "context": 1048576 },
          "modalities": {
            "input": ["text", "image", "audio", "video"],
            "output": ["text"]
          },
          "zcode": { "priority": 204 }
        }
      }
    }
  }
}

3. 模型列表置顶展示

~/.zcode/v2/model-provider-display-order.json 中,将 \"zcode-antigravity-local\" 放置在 providerIds 数组的首位

{
  "providerIds": [
    "zcode-antigravity-local",
    "builtin:bigmodel",
    "builtin:zai"
  ]
}

这样启动 ZCode 后,顶部模型下拉框首项即为 Google 官方 Gemini 全系模型。

五、 多模态扩展:ZCode 接入 Gemini 原生生图 Skill (gemini-3.1-flash-image / Nano Banana 2)

很多用户在配置了 Gemini 模型后,让智能体画图却发现智能体只输出文字描述,甚至产生幻觉。这是由于 Tool-Calling 机制与模型多模态能力脱节导致的。

[用户发出画图指令]
[ZCode Agent] ──(自动触发)──> [Skill: gemini-image-gen]
                                     ▼ (执行 Python 脚本)
                      [generate_image.py]
                                     ▼ (POST /v1/chat/completions)
                   [EasyCLIProxyAPI 网关] (http://127.0.0.1:18080)
                                     ▼ (调用 Google 远端接口)
                         [gemini-3.1-flash-image]
                                     ▼ (返回包含 data:image/jpeg;base64 的 JSON)
                      [Python 脚本解码并保存]
                   [本地文件: ./generated_images/xxx.jpg]
                      [ZCode 渲染展示 Markdown 预览]

1. 主控模型与生图后端解耦

  • 主控对话模型 (Chat Controller):负责理解自然语言、优化扩展 Prompt,触发工具调用(如 gemini-3.8-flash / gemini-3.7-flash);
  • 生图后端 (Image Generation Backend):负责计算并生成图片 Base64 数据(gemini-3.1-flash-image,即 Nano Banana 2)。
  • 无论主控模型使用的是哪个模型,只要挂载了生图 Skill,都能自由调用生图后端。

2. 为什么走 /v1/chat/completions 而不是 /v1/images/generations

Google 的 gemini-3.1-flash-image 在 Antigravity 中是作为多模态补全模型注册的。它不支持标准 OpenAI 的 /v1/images/generations 端点(调用会报 400: Model is not supported on /v1/images/generations),必须向 /v1/chat/completions 发起对话补全请求,并从返回结果的 choices[0].message.images 数组中提取 data:image/jpeg;base64 编码。

3. 创建 gemini-image-gen Skill

在 ZCode 用户全局技能目录 ~/.zcode/skills/gemini-image-gen/(或项目级目录)下创建两份文件:

(1) SKILL.md(向 Agent 注册调用规范)

---
name: gemini-image-gen
description: Use this skill whenever the user asks to generate, draw, paint, or render an image, illustration, anime art, or photo using Google Gemini / Imagen 3 backend.
---

# Gemini Image Generation Skill

Use this skill to generate high quality images using Google's Imagen 3 / Gemini Image API and save them directly to the local workspace.

## How to execute

Run the generation script via Bash tool:

```bash
python "C:/Users/<用户名>/.zcode/skills/gemini-image-gen/generate_image.py" --prompt "YOUR_DETAILED_PROMPT" --output-dir "generated_images" --output-name "custom_name"
```

### Parameters
- `--prompt` (必填): 详细的英文提示词,包含主体、画风、材质、光影及构图。
- `--output-dir`: 保存目标文件夹(默认 `./generated_images`)。
- `--output-name`: 自定义保存文件名(不含扩展名)。
- `--model`: 默认为 `gemini-3.1-flash-image`
### When Executing
1. 调用系统命令执行上述脚本。
2. 读取脚本输出的 JSON 结果。
3. 成功后以 Markdown 图片/链接格式返回给用户展示:`![image](path/to/image.jpg)`。

(2) generate_image.py(请求网关并保存图片)

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Gemini / Nano Banana 2 Image Generation Script for EasyCLIProxyAPI Bridge
"""

import os
import sys
import json
import base64
import argparse
from datetime import datetime
import requests

LOCAL_BRIDGE_URL = "http://127.0.0.1:18080"
LOCAL_BRIDGE_KEY = "<YOUR_GATEWAY_KEY>"

def generate_via_antigravity(prompt, base_url, api_key, model="gemini-3.1-flash-image", timeout=60):
    url = f"{base_url}/v1/chat/completions"
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json"
    }
    payload = {
        "model": model,
        "messages": [{"role": "user", "content": prompt}]
    }
    response = requests.post(url, json=payload, headers=headers, timeout=timeout)
    if response.status_code != 200:
        raise Exception(f"Antigravity Bridge Error ({response.status_code}): {response.text}")

    data = response.json()
    choices = data.get("choices", [])
    if not choices:
        raise Exception("No choices returned by Antigravity Bridge")

    msg = choices[0].get("message", {})
    images = []

    # 提取 choices[0].message.images 中的 Base64
    if "images" in msg and msg["images"]:
        for img_obj in msg["images"]:
            url_val = img_obj.get("image_url", {}).get("url", "")
            if url_val.startswith("data:image"):
                header, b64_data = url_val.split(",", 1)
                mime = "image/jpeg" if "jpeg" in header or "jpg" in header else "image/png"
                images.append((base64.b64decode(b64_data), mime))
            elif url_val.startswith("http"):
                r = requests.get(url_val, timeout=30)
                images.append((r.content, "image/jpeg"))

    if not images:
        raise Exception(f"No image was generated. Model reply: {msg.get('content')}")

    return images

def main():
    parser = argparse.ArgumentParser(description="Generate images via Gemini / Antigravity Bridge")
    parser.add_argument("--prompt", "-p", required=True, help="Image generation prompt")
    parser.add_argument("--model", "-m", default="gemini-3.1-flash-image", help="Model name")
    parser.add_argument("--output-dir", "-o", default="generated_images", help="Output directory")
    parser.add_argument("--output-name", "-n", default=None, help="Output file name")
    parser.add_argument("--base-url", default=LOCAL_BRIDGE_URL, help="Antigravity bridge base URL")
    parser.add_argument("--api-key", "-k", default=LOCAL_BRIDGE_KEY, help="Antigravity bridge API key")

    args = parser.parse_args()
    os.makedirs(args.output_dir, exist_ok=True)

    try:
        images = generate_via_antigravity(args.prompt, args.base_url, args.api_key, model=args.model)
        saved_paths = []
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        for i, (img_bytes, mime) in enumerate(images):
            ext = "png" if "png" in mime else "jpg"
            filename = f"{args.output_name}.{ext}" if args.output_name else f"gemini_image_{timestamp}_{i+1}.{ext}"
            filepath = os.path.abspath(os.path.join(args.output_dir, filename))
            with open(filepath, "wb") as f:
                f.write(img_bytes)
            saved_paths.append(filepath)

        print(json.dumps({
            "status": "success",
            "model": args.model,
            "prompt": args.prompt,
            "images": saved_paths
        }, ensure_ascii=False, indent=2))

    except Exception as e:
        print(json.dumps({
            "status": "error",
            "error_type": "GENERATION_FAILED",
            "message": str(e)
        }, ensure_ascii=False, indent=2))
        sys.exit(1)

if __name__ == "__main__":
    main()

六、 全流程避坑与常见故障速查表(血泪经验汇编)

故障现象 触发时机 / 原因 避坑方案与解决对策
HTTP 500 后变 503 auth_unavailable 本地直接裸 go build 编译 CLIProxyAPI 二进制,丢失了官方发布期通过 -X ldflags 注入的 OAuth Client 凭据。 严禁用本地裸构建覆盖官方核心。直接使用 EasyCLIProxyAPI 官方预编译的 cpa-core\cli-proxy-api.exe(7.2.149+)。
“只检测到配置文件,未检测到客户端” EasyCLIProxyAPI 控制台硬编码探查系统盘规范路径,而 ZCode 安装在 D:\zcode %LOCALAPPDATA%\Programs\ZCode%ProgramFiles%\ZCode 建立 NTFS 目录联接(mklink /J)。
Hermes 模型下拉列表重复翻倍 config.yaml 中同时保留了旧网关名称(Local (127.0.0.1:18080))与新网关名称(cpa-gui)。 清理 config.yamlauth.json,统一规范化为单实例 cpa-gui
日常浏览器扩展和脚本全清空 Hermes 开启 browser.use_real_profile: true(历史版本会把无扩展内存状态写回日常配置)。⚠️ 勘误(2026-09-12 对照 v0.21.1 源码):现行实现为快照副本隔离(~/.hermes/browser-profile/),写回路径已与日常配置隔离,故障链不成立 建议保持 use_real_profile: false(纵深防御)
两端查看的 Google 配额完全不一致 通用 Google 生产端点 cloudcode-pa 与 Antigravity 专有端点 daily-cloudcode-pa 属于云端解耦配额池。 查询 Antigravity 实际调用消耗时,必须指定 daily-cloudcode-pa.googleapis.com 端点
HTTP 403: IP banned due to too many failed attempts 前端微件使用普通 API Key 频繁轮询 /v0/management/ 高权限管理接口,触发了防爆破 30 分钟 IP 熔断。 数据面与管理面隔离;获取配额改走本地轻量 Python 微服务,绝不高频撞击管理接口。
刷新配额点击无反应 / 误以为卡死 内存防抖缓存瞬间命中,且界面缺乏加载动画与完成时间戳。 后端增加 ?force=1 穿透参数;前端配套 SVG 旋转 Spinner、✓ 已刷新 徽章变形与 Toast 弹窗反馈。
400: Model is not supported on /v1/images/generations Antigravity 桥接中的 gemini-3.1-flash-image 是对话多模态格式,不支持标准 OpenAI 生图端点。 将请求端点由 /v1/images/generations 改为 /v1/chat/completions,并在返回的 choices[0].message.images 中提取 Base64。
400: User location is not supported for the API use 直连 Google AI Studio 时,国内出口代理 IP 处于未获支持的地区。 通过本地 EasyCLIProxyAPI 网关桥接服务中转,自动规避原生地域检测。
401: Invalid API key 生图脚本中的 API Key 与网关 config.tomlconfig.yamlapi-keys 不匹配。 统一提取网关中配置的明文密钥(如 <YOUR_GATEWAY_KEY>)。