OmniRoute A2A Server 接入指南:把 AI 路由网关打造成智能路由 Agent
2026/9/11 15:43:47 网站建设 项目流程

OmniRoute A2A Server 接入指南:把 AI 路由网关打造成智能路由 Agent

【免费下载链接】OmniRouteNever stop coding. Free MIT AI gateway: one endpoint, 352 providers (150+ free), 1200+ models Kimi, Claude, GPT, Gemini, GLM, DeepSeek, MiniMax. Works with Claude Code, Codex, Cursor, OpenCode, Cline & Copilot. Quota-aware auto-fallback, RTK+Caveman compression saves 15-95% tokens, MCP/A2A, Desktop/PWA. Built by 550+ contributors项目地址: https://gitcode.com/GitHub_Trending/om/OmniRoute

Agent-to-Agent Protocol v0.3 —— 将 OmniRoute 以「智能路由 Agent」的身份暴露给其他 AI Agent。

本篇指南围绕 OmniRoute 的 A2A(Agent-to-Agent)服务端展开:它以POST /a2a作为 JSON-RPC 2.0 入口、以/api/a2a/*提供 REST 辅助接口,并暴露 6 个可直接调用的技能(Skill)。读完本文,你将掌握 Agent 发现(Agent Card)、鉴权与开关控制、message/send/message/stream/tasks/get/tasks/cancel四个核心 JSON-RPC 方法的完整调用方式,理解任务生命周期与 TTL 清理机制,并能照着步骤在仓库中新增一个自定义 A2A 技能。全文以 docs/frameworks/A2A-SERVER.md 为主体,结合仓库源码给出实现级佐证。

一、A2A 表面的双面结构

OmniRoute 的 A2A 能力由两个互补的入口组成,分别服务不同的调用方:

  • JSON-RPC 2.0POST /a2a,是规范意义上的 A2A 标准入口,专为其他 Agent 设计。路由实现在 src/app/a2a/route.ts,支持message/sendmessage/streamtasks/gettasks/cancel四个方法。
  • REST/api/a2a/*,为仪表盘与外部工具提供辅助能力,包括状态查询、任务列表、任务取消等。

任务的跟踪由A2ATaskManager(src/lib/a2a/taskManager.ts)负责,默认 TTL 为 5 分钟;技能的分发则通过A2A_SKILL_HANDLERS(src/lib/a2a/taskExecution.ts)完成。

从源码结构看,A2A 是 OmniRoute 路由能力的「面向 Agent 的封装层」:它并不自己实现模型推理,而是把收到的消息转交给内部技能模块(如smart-routing技能内部实际调用/v1/chat/completions走完整路由管线),再把结果以 A2A 协议规定的任务与工件(artifact)形式返回给调用方。

二、Agent 发现:Agent Card

A2A 协议要求服务端通过.well-known目录暴露「Agent Card」,让其他 Agent 在调用前先发现其能力、技能与鉴权要求。

curl http://localhost:20128/.well-known/agent.json

返回内容即 OmniRoute 的 Agent Card,描述了网关的能力、技能列表与认证方式。该端点的实现位于 src/app/.well-known/agent.json/route.ts,有两点值得注意:

  • 版本号自动同步:Agent Card 的version字段取自process.env.npm_package_version,与package.json的版本保持自动一致,每次发布无需手工维护(源码中带"1.8.1"兜底值)。
  • 技能动态生成skills数组不仅包含固定的 6 个内置技能,还会通过getFleetSkills()(src/lib/conductor/fleetSkills.ts)合并 OmniConductor 集群的技能(缓存约 60 秒;Hub 未配置或离线时返回[],Agent Card 依然有效)。

Agent Card 的认证部分声明了schemes: ["api-key"]apiKeyHeader: "Authorization",与下面介绍的鉴权方式一致。该响应带有Cache-Control: public, max-age=3600(缓存 3600 秒)。

三、鉴权:Bearer API Key

所有发往/a2a的请求都需要在Authorization头携带 API Key:

Authorization: Bearer YOUR_OMNIROUTE_API_KEY

如果服务器未配置任何 API Key,鉴权将被绕过(keyless 本地优先模式)。完整的判定逻辑在 src/lib/a2a/authenticate.ts 的authenticateA2ARequest()

  1. 若开启了「强制 API Key」特性(isRequireApiKeyEnabled()),则必须提供且校验通过的 OmniRoute Key 才能访问;
  2. 否则若配置了OMNIROUTE_API_KEY环境变量,则用常量时间比较(timingSafeEqual)校验请求携带的 Key;
  3. 两者都不满足时直接放行——即默认的 keyless 本地优先姿态。

同一文件中的resolveA2AOwner()还负责把调用方解析为「所有者 ID」:对 API Key 做 SHA-256 哈希并取前 32 位十六进制前缀。该 owner 会用于任务的作用域隔离(详见「任务可见性与安全模型」一节),确保一个调用方不能读取或取消另一个调用方的任务。

四、开关控制:Endpoints → A2A

A2A 由Endpoints(端点)页面中的 A2A 开关控制,默认是关闭的。开关状态从设置库读取(getSettings()),当a2aEnabled !== true时:

  • GET /api/a2a/status报告status: "disabled"online: false
  • POST /a2a的 JSON-RPC 调用返回HTTP 503,并携带 JSON-RPC 错误码-32000A2A endpoint is disabled. Enable it from the Endpoints page.)。

该判定在 src/app/a2a/route.ts 的rejectIfA2ADisabled()中实现——它在鉴权通过、请求体解析之后立即执行,属于所有方法共用的前置守卫。

五、JSON-RPC 2.0 方法详解

所有调用统一走POST http://localhost:20128/a2a,请求体为标准的 JSON-RPC 2.0 结构:{"jsonrpc": "2.0", "id": "...", "method": "...", "params": {...}}。下面逐个讲解四个方法。

5.1message/send—— 同步执行

向指定技能发送消息并等待完整响应:

curl -X POST http://localhost:20128/a2a \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_KEY" \ -d '{ "jsonrpc": "2.0", "id": "1", "method": "message/send", "params": { "skill": "smart-routing", "messages": [{"role": "user", "content": "Write a hello world in Python"}], "metadata": {"model": "auto", "combo": "fast-coding"} } }'

响应示例:

{ "jsonrpc": "2.0", "id": "1", "result": { "task": { "id": "uuid", "state": "completed" }, "artifacts": [{ "type": "text", "content": "..." }], "metadata": { "routing_explanation": "Selected claude-sonnet via provider \"anthropic\" (latency: 1200ms, cost: $0.003)", "cost_envelope": { "estimated": 0.005, "actual": 0.003, "currency": "USD" }, "resilience_trace": [ { "event": "primary_selected", "provider": "anthropic", "timestamp": "..." } ], "policy_verdict": { "allowed": true, "reason": "within budget and quota limits" } } } }

从路由源码看,message/send的处理链路是:解析并归一化messages(兼容message.content与 legacy 的message.parts两种形态)→ 在A2A_SKILL_HANDLERS中查找技能 →createTask创建任务 →updateTask(task.id, "working")→ 执行技能 handler →updateTask(task.id, "completed", result.artifacts)。若执行抛错,任务会被标记为failed并附上 error 类型工件,返回-32603内部错误码。

值得补充的是参数归一化规则(见 src/app/a2a/route.ts 的toMessageArray()):

  • messages数组中的每条消息,role缺省时补为"user"content为空的消息会被过滤;
  • 也接受单条消息形态{"message": {"role", "content"}}
  • 兼容 legacy 的{"message": {"parts": [...]}},会把各 part(contenttext字段)拼接为整段文本。

5.2message/stream—— SSE 流式执行

message/send参数完全相同,但响应改为 Server-Sent Events(SSE),适合需要实时输出场景的 Agent:

curl -N -X POST http://localhost:20128/a2a \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_KEY" \ -d '{ "jsonrpc": "2.0", "id": "1", "method": "message/stream", "params": { "skill": "smart-routing", "messages": [{"role": "user", "content": "Explain quantum computing"}] } }'

SSE 事件流:

data: {"jsonrpc":"2.0","method":"message/stream","params":{"task":{"id":"...","state":"working"},"chunk":{"type":"text","content":"..."}}} : heartbeat 2026-03-03T17:00:00Z data: {"jsonrpc":"2.0","method":"message/stream","params":{"task":{"id":"...","state":"completed"},"metadata":{...}}}

流式实现集中在 src/lib/a2a/streaming.ts:createA2AStream()接收任务、执行回调(executeA2ATaskWithState)与请求的AbortSignal,并通过onStart/onEnd回调维护A2ATaskManager的活跃流计数(beginStream()/endStream(),供/api/a2a/statusactiveStreams统计使用)。事件中穿插: heartbeat注释行以维持连接活性,任务最终以completed状态事件收尾,携带完整 metadata。

5.3tasks/get—— 查询任务状态

curl -X POST http://localhost:20128/a2a \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_KEY" \ -d '{"jsonrpc":"2.0","id":"2","method":"tasks/get","params":{"taskId":"TASK_UUID"}}'

路由接受params.taskIdparams.id两种字段名。值得注意的是getTask()的过期处理:如果任务已超过expiresAt且仍处于submitted/working状态,查询会把它标记为failed("Task expired")后返回;任务不存在时返回-32601

5.4tasks/cancel—— 取消任务

curl -X POST http://localhost:20128/a2a \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_KEY" \ -d '{"jsonrpc":"2.0","id":"3","method":"tasks/cancel","params":{"taskId":"TASK_UUID"}}'

cancelTask()把任务状态迁移到cancelled(事件消息为 "Cancelled by client")。取消与查询一样受 owner 作用域约束。

5.5 附加:A2A 1.0 方法别名兼容层

从 src/app/a2a/route.ts 的注释可以看出,路由内置了 A2A v1.0 ↔ v0.3 的兼容层:A2A 1.0 将方法改名为SendMessage/SendStreamingMessage,并把同步响应改到task.status.message.parts[].texttask.artifacts之下。该层用V1_METHOD_ALIASES做方法别名映射,用buildV1Task()重塑同步响应结构,因此a2a-sdk 1.x、Hermes 等 1.0 客户端可以直接调用,v0.3 客户端则完全不受影响。v1 方法调用时也支持通过params.message.contextId传递上下文 ID。

六、内置技能一览

OmniRoute 通过 src/lib/a2a/taskExecution.ts 中的A2A_SKILL_HANDLERS暴露6 个 A2A 技能,每个技能模块位于src/lib/a2a/skills/目录,采用动态import()懒加载方式注册:

技能ID描述标签调用示例
Smart Routingsmart-routing使用 OmniRoute 的 combo 引擎 + 评分体系,把提示词路由到最优提供者/组合routing, providers"Route this prompt via the best model"
Quota Managementquota-management报告各提供者的配额状态,帮助调用方决定何时限流/切换quota, providers"Check quota for anthropic"
Provider Discoveryprovider-discovery列出已安装提供者,包含能力、免费档位标记、OAuth 状态providers, discovery"What providers are available?"
Cost Analysiscost-analysis基于目录与近期用量估算请求/会话成本cost, usage"Estimate cost for this conversation"
Health Reporthealth-report汇总各提供者的熔断器、冷却期、锁定状态health, resilience"Show health status of all providers"
List Capabilitieslist-capabilities以 markdown 表格返回完整 Agent Skills 目录及原始 SKILL.md URL,便于上下文注入catalog, discovery, skills"List all OmniRoute capabilities"

文档同时强调:Agent Card 应与实时目录保持对齐,提供者数量与免费/无鉴权元数据均取自运行时注册表(runtime registry),不写死在静态 JSON 中。

6.1smart-routing技能的实现细节

以最核心的smart-routing为例(src/lib/a2a/skills/smartRouting.ts),它内部通过routeFetch()调用 OmniRoute 自身的/v1/chat/completions端点(30 秒 AbortSignal 超时),把完整路由管线作为技能执行体:

  • task.input.metadata读取model(默认"auto")、combo(透传为x-combo请求头)与budget(预算上限);
  • 响应中提取模型输出、provider、实际成本costusage.prompt_tokens,并据此估算成本(prompt_tokens / 1e6 * 3.0的粗略估算公式);
  • 返回四项结构化 metadata:routing_explanation(含延迟与成本的一句话解释)、cost_envelope(estimated / actual / currency)、resilience_traceprimary_selected事件,若触发回退则追加fallback_needed)、policy_verdictallowedreason,预算超限时allowed: false)。

这就是「A2A 技能 = 路由能力的 Agent 化封装」这一设计的最直接体现。

6.2list-capabilities技能详解

list-capabilities对外部 Agent 特别有用——它在发送 API 调用之前,先用它来发现 OmniRoute 暴露了什么。实现位于 src/lib/a2a/skills/listCapabilities.ts,返回结构化 markdown 表格工件:

| ID | Name | Category | Area | Endpoints/Commands | Raw URL | | --- | --- | --- | --- | --- | --- | | omni-auth | Auth & Sessions | api | auth | POST /api/auth/login, ... | https://raw.githubusercontent.com/... | ...

每行包含rawUrl列,Agent 可以立即拉取对应技能的完整 SKILL.md。metadata.totalSkills字段反映目录规模(当前文档记载为 45 项,Agent Card 中描述为 42 项,两者口径略有差异)。相关技能目录可参见 docs/frameworks/AGENT-SKILLS.md。

七、REST 辅助接口

/a2a是规范的 A2A 入口;以下 REST 端点面向仪表盘与外部工具提供辅助访问:

端点方法描述鉴权
/api/a2a/statusGET服务器状态、已注册技能公开
/api/a2a/tasksGET带筛选条件列出任务management
/api/a2a/tasks/[id]GET按 ID 获取任务management
/api/a2a/tasks/[id]/cancelPOST取消运行中的任务management
/.well-known/agent.jsonGETAgent Card(A2A 发现)公开,缓存 3600s
/api/a2a/tasksPOST向 OmniConductor 集群发起入站委派(Conductor PRD RF5)Bearer vsOMNIROUTE_API_KEY+a2aEnabled

其中值得单独说明的是入站 Conductor 委派(POST /api/a2a/tasks:外部 A2A Agent 可以通过 OmniRoute 把编码任务委派给 OmniConductor 集群。请求体结构为:

{ "skill": "conductor" | "conductor-cli-<profile>", "messages": [{ "role": "...", "content": "..." }], "metadata": { "conductor": { "repo": { "url": "...", "base_ref": "..." }, "mode": "...", "cli": "...", "model": "..." } } }

只有 Agent Card 上公布的 Conductor 集群技能才可被委派;metadata.conductor.repo.url为必填(集群工作在 git 仓库上)。该路由使用服务端令牌CONDUCTOR_ORCHESTRATOR_TOKEN(回退CONDUCTOR_HUB_TOKEN)翻译为 Hub 的POST /v1/tasks,返回201 { conductor_task_id, state: "submitted" };任务状态通过 SSE→A2A 镜像(RF1)回流,并可通过GET /api/a2a/tasks?skill=conductor查询。

八、新增一个自定义技能

下面依据文档与源码,给出完整的五步扩展流程:

第一步:创建技能文件

src/lib/a2a/skills/<your-skill>.ts新建文件,导出一个接收任务的异步函数,返回{ artifacts, metadata },形状参考smartRouting.ts

import type { A2ATask } from "../taskManager"; export async function executeYourSkill(task: A2ATask) { // 读取 task.input.messages / task.input.metadata // 执行逻辑… return { artifacts: [{ type: "text", content: "..." }], metadata: { /* 自定义结构化元数据 */ }, }; }

第二步:注册 Handler

在 src/lib/a2a/taskExecution.ts 的A2A_SKILL_HANDLERS中追加条目:

export const A2A_SKILL_HANDLERS = { // ...existing skills "your-skill": async (task) => { const skillModule = await import("./skills/yourSkill"); return skillModule.executeYourSkill(task); }, };

第三步:在 Agent Card 中暴露

在 src/app/.well-known/agent.json/route.ts 的skills数组中追加:

{ "id": "your-skill", "name": "Your Skill", "description": "Brief, intent-focused description", "tags": ["routing", "quota"], "examples": ["Sample natural-language invocation"] }

第四步:编写测试

tests/unit/下新增a2a-<your-skill>.test.ts,覆盖 happy path 与 error path 两条路径(仓库现有 A2A 相关单测可作为参照)。

第五步:更新文档

在本文(A2A-SERVER.md)的「Available Skills」表格中登记新技能。

九、任务 TTL 与生命周期

9.1 TTL 与清理

任务在expiresAt之后过期,ttlMinutes默认5 分钟,配置于 src/lib/a2a/taskManager.ts 的A2ATaskManager构造函数(constructor(ttlMinutes: number = 5, ...))。如需定制,可 forkA2ATaskManager的实例化并传入不同值,例如new A2ATaskManager(15)表示 15 分钟 TTL。

构造函数内部启动一个后台清理间隔,每 60 秒扫描一次(setInterval(() => this.cleanupExpired(), 60_000),并对unref()以免阻止进程退出)。cleanupExpired()的清理逻辑包含两条规则:

  1. 已过期且仍处于submitted/working的任务 → 标记为failed(事件消息 "TTL expired");
  2. 处于终态(completed/failed/cancelled)且超过2 倍 TTL未再更新的任务 → 从内存 Map 中移除。

从源码看,任务管理还集成了可选的 SQLite 历史持久化(src/lib/db/a2aTasks.ts):每次状态变更都会 best-effort 写入历史表并追加事件,失败只记日志、绝不拖垮写入路径;历史记录按OMNIROUTE_A2A_HISTORY_RETENTION_DAYS环境变量保留(默认 30 天),由maybePurge()每 24 小时最多清理一次。

9.2 任务状态机

submitted → working → completed → failed → cancelled
  • 任务默认 5 分钟过期(见上文 TTL 小节);
  • 终态:completedfailedcancelled
  • 每个状态迁移都会写入事件日志(task.events),同时通过emit("agent.task.updated", ...)发布事件(best-effort,监听器抛错不打断写入路径)。

状态机在 src/lib/a2a/taskManager.ts 中由VALID_TRANSITIONS严格约束——非法迁移(如completed → working)会直接抛出Invalid transition错误。

9.3 任务可见性与安全模型

src/lib/a2a/taskManager.ts 中任务结构包含可选的owner字段(即调用方 API Key 的哈希,见鉴权一节)。可见性规则为:

  • 带 owner 的任务仅对同一 owner 可见:查询、取消、列表都会用isVisibleTo()过滤;
  • 无 owner 的任务(keyless 本地优先姿态下创建的)对所有调用方可见,保持原有行为;
  • 取消他人任务时返回与「任务不存在」相同的错误,防止 IDOR 探测区分「存在但非你的」与「不存在」。

十、错误码对照

JSON-RPC 错误码遵循标准约定,同时扩展了 A2A 特有码:

代码含义
-32700解析错误(非法 JSON)
-32600无效请求 / 未授权
-32601方法或技能不存在
-32602参数无效
-32603内部错误
-32000A2A 端点已禁用

从 src/app/a2a/route.ts 的jsonRpcError()看,错误响应的 HTTP 状态码也做了映射:-32600→ 400、-32601→ 404、-32603→ 500,其余(含-32000禁用态)→ 200(但禁用态由rejectIfA2ADisabled()单独返回 HTTP 503)。因此判断 A2A 是否可用,应以「HTTP 状态码 + JSON-RPC 错误码」两者结合为准。

十一、集成示例

Python(requests)

import requests resp = requests.post("http://localhost:20128/a2a", json={ "jsonrpc": "2.0", "id": "1", "method": "message/send", "params": { "skill": "smart-routing", "messages": [{"role": "user", "content": "Hello"}] } }, headers={"Authorization": "Bearer YOUR_KEY"}) result = resp.json()["result"] print(result["artifacts"][0]["content"]) print(result["metadata"]["routing_explanation"])

TypeScript(fetch)

const resp = await fetch("http://localhost:20128/a2a", { method: "POST", headers: { "Content-Type": "application/json", Authorization: "Bearer YOUR_KEY", }, body: JSON.stringify({ jsonrpc: "2.0", id: "1", method: "message/send", params: { skill: "smart-routing", messages: [{ role: "user", content: "Hello" }], }, }), }); const { result } = await resp.json(); console.log(result.metadata.routing_explanation);

十二、关键环境变量与调参速查

结合上述源码分析,整理 A2A 模块相关的可调项:

默认值说明
任务 TTL5 分钟A2ATaskManager构造参数,可 fork 实例化传值
清理间隔60 秒后台cleanupExpired()扫描周期
OMNIROUTE_API_KEY未设置设置后/a2a要求 Bearer 鉴权;未设置则 keyless 放行
OMNIROUTE_A2A_HISTORY_RETENTION_DAYS30A2A 任务历史的 SQLite 保留天数
OMNIROUTE_A2A_MEMORY_HITS开启设为0时关闭 A2A 任务的记忆命中收集(仅观测,不注入提示词)
CONDUCTOR_ORCHESTRATOR_TOKEN入站 Conductor 委派的 Hub 令牌(回退CONDUCTOR_HUB_TOKEN

十三、相关文档延伸

  • Agent Skills 目录与 SKILL.md 组织方式:docs/frameworks/AGENT-SKILLS.md
  • A2A 核心源码:入口路由 src/app/a2a/route.ts、任务管理 src/lib/a2a/taskManager.ts、技能分发 src/lib/a2a/taskExecution.ts、鉴权 src/lib/a2a/authenticate.ts、流式输出 src/lib/a2a/streaming.ts
  • 技能实现目录:src/lib/a2a/skills/
  • Agent Card 端点:src/app/.well-known/agent.json/route.ts

【免费下载链接】OmniRouteNever stop coding. Free MIT AI gateway: one endpoint, 352 providers (150+ free), 1200+ models Kimi, Claude, GPT, Gemini, GLM, DeepSeek, MiniMax. Works with Claude Code, Codex, Cursor, OpenCode, Cline & Copilot. Quota-aware auto-fallback, RTK+Caveman compression saves 15-95% tokens, MCP/A2A, Desktop/PWA. Built by 550+ contributors项目地址: https://gitcode.com/GitHub_Trending/om/OmniRoute

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询