[Bug]: A2A agent-permission 403 is not logged (no failure callback, no SpendLogs row), unlike the model-access 403

该问题出现在 LiteLLM 的 A2A 路由中:调用者没有目标 agent 权限时,`/a2a/{agent}` 返回 403([Bug]: A2A agent-permission 403 is not logged (no failure callback, no SpendLogs row)

快速结论:该问题出现在 LiteLLM 的 A2A 路由中:调用者没有目标 agent 权限时,`/a2a/{agent}` 返回 403([Bug]: A2A agent-permission 403 is not logged (no failure callback, no SpendLogs row), unlike the model-access 403),但这个拒绝既不触发失败回调,也不写入 SpendLogs;同类模型权限 403 则会正常记录。优先排查 `a2a_endpoints.py` 中权限检查的抛出位置,以及 `except HTTPException: raise` 是否绕过了 `post_call_failure_hook`。

适用环境:Issue 确认复现于 LiteLLM v1.108.0-rc.1(`ghcr.io/berriai/litellm:v1.108.0-rc.1`),v1.109.0-dev.2(`main-latest`)和 v1.106.0 也存在同样现象;代码在 main `a713ed6` 上未变。使用 Postgres 17 存储 key 与 SpendLogs;代理端 agent 为基于 Python `a2a-sdk` 1.1.0 的最小 A2A 1.0 echo agent。Issue 未说明操作系统、CUDA、PyTorch 或显卡信息。

最快修复方案:暂无确认的一步修复方案。Issue 讨论给出的可优先尝试方向是:在 `a2a_endpoints.py` 的 `except HTTPException:` 分支中补调 `post_call_failure_hook`,并把 `user_api_key_dict.invoked_agent_id = agent.agent_id` 的赋值移到 `is_agent_allowed` 权限检查之前。这两处改动均未在 Issue 中标注为已合并或已验证。

注意事项:仅补调 hook 而不移动 `invoked_agent_id` 赋值,日志会记录调用者和花费,但不会记录被拒绝的 agent,无法满足“管理员能看到谁尝试访问哪个 agent”的预期。未知 agent 的 404 在 `#L725` 通过直接返回 `_jsonrpc_error(…)` 而不是抛异常,需要单独处理,否则两种拒绝的日志行为仍不一致。该 403 是 `HTTPException` 而非 `ProxyException`,由 `StarletteHTTPException` 处理器返回 `{“detail”: …}`,这一点是否应改为 `ProxyException` 尚未定论。

问题场景

在 LiteLLM 代理中配置了两个 A2A agent(如 `plain` 和 `static`),某把 key 的 `object_permission.agents` 只授权了其中一个 agent。当这把 key 通过 `POST /a2a/{agent}` 调用未授权的 agent 时,代理返回 403,但配置的 `CustomLogger`(实现了 `async_log_success_event`、`async_log_failure_event` 和 `async_post_call_failure_hook`)没有收到任何失败回调,`LiteLLM_SpendLogs` 中也没有对应的失败记录。作为对照,同一把 key 调用未授权模型时,`key_model_access_denied` 会触发失败回调并写入 SpendLogs;在 `/a2a/{agent}` 上因预算不足被拒绝也能正常记录。只有 agent 权限拒绝不可见。

报错原文

[Bug]: A2A agent-permission 403 is not logged (no failure callback, no SpendLogs row), unlike the model-access 403

{"detail":"Agent 'plain' is not allowed for your key/team. Contact proxy admin for access."}
403

key_model_access_denied

原因分析

根据 Issue 讨论,原因可以归结为权限 403 的抛出位置与失败回调路径之间存在错位,具体有三个关键点:

  • 权限检查发生在日志对象创建之前。`is_agent_allowed` 在 `a2a_endpoints.py` 的 `#L729-L737` 抛出 `HTTPException(403)`,早于 `common_processing_pre_call_logic`(`#L800`)执行。因此请求此时还没有创建日志对象,也不在代理的 pre-call/logging 路径上。
  • 异常分支跳过了失败回调。`#L1039-L1040` 的 `except HTTPException: raise` 直接重新抛出,没有调用 `post_call_failure_hook`;而 `#L1041` 的通用 `except Exception` 分支会调用该 hook。所以问题不是“拒绝事件类型不可记录”,而是这次抛出落在 hook 调用和日志对象创建之外。
  • 目标 agent 标识在权限检查之后才绑定。`user_api_key_dict.invoked_agent_id = agent.agent_id` 在 `#L739` 赋值,晚于权限检查。`metadata[“agent_id”]`、`target_agent_id` 和 `billing_agent_id` 都由 `invoked_agent_id` 派生(`litellm_pre_call_utils.py:2115-2122`)。若只补 hook 而不移动赋值,拒绝日志将缺少被拒 agent 信息。

讨论还指出,`post_call_failure_hook` 本身支持“日志对象尚不存在”的情况:`_handle_logging_proxy_only_error`(`litellm/proxy/utils.py`)中的 `if litellm_logging_obj is None:` 分支会调用 `function_setup(…)`、绑定新的 `Logging`,并在请求数据上标记 `REJECTED_BEFORE_REQUEST_SETUP`。同时,`/a2a/{agent_id}` 属于 `LiteLLMRoutes.agent_inference_routes`(`litellm/proxy/_types.py:498`),可通过 `RouteChecks.is_llm_api_route` 检查,且 `HTTPException` 在 `_PROXY_ONLY_LLM_API_ERRORS` 中,因此从 `except HTTPException:` 分支调用 hook 在机制上可行。

环境排查

  • LiteLLM 版本:Issue 明确 v1.108.0-rc.1、v1.109.0-dev.2(`main-latest`)、v1.106.0 均复现;main `a713ed6` 代码未变。
  • 确认 `/a2a/{agent}` 路由是否启用了 A2A 端点,并已配置 `agents` 列表。
  • 确认 key 的 `object_permission.agents` 是否授权了目标 agent,以及对照用的模型权限拒绝是否正常记录。
  • 确认自定义回调是否已通过 `litellm_settings.callbacks` 加载,并实现了 `async_log_failure_event` 与 `async_post_call_failure_hook`。
  • 确认 SpendLogs 使用的 Postgres 连接与 `proxy_batch_write_at` 等批写配置是否正常;Issue 使用 Postgres 17,但未说明其与静默问题直接相关。
  • 缺省项目(操作系统、CUDA、PyTorch、显卡)在 Issue 中无证据,不需要纳入排查。

解决步骤

  1. 先按 Issue 的复现步骤构造对照:用只授权 `static` agent 的 key 分别调用 `/a2a/plain` 和 `/v1/chat/completions`(未授权模型),确认前者 403 无失败回调、无 SpendLogs 行,后者 403 有记录,以定位问题范围。
  2. 检查 `litellm/proxy/agent_endpoints/a2a_endpoints.py` 中 `is_agent_allowed` 的位置,确认 `HTTPException(403)` 是否在 `common_processing_pre_call_logic` 之前抛出。
  3. 检查 `#L1039-L1040` 的 `except HTTPException: raise`,确认它没有调用 `post_call_failure_hook`;对照 `#L1041` 的通用异常分支。
  4. 可优先尝试在 `except HTTPException:` 分支中补调 `post_call_failure_hook`,并确认 `_is_proxy_only_llm_api_error` 对该请求返回 True,使合成日志对象能写入 SpendLogs。
  5. 可优先尝试把 `user_api_key_dict.invoked_agent_id = agent.agent_id` 的赋值移到 `is_agent_allowed` 之前;agent 对象在 `#L725` 已解析,无需新增查询。
  6. 如果希望未知 agent 的 404 也进入同一日志路径,需要单独改造 `#L725` 处直接返回 `_jsonrpc_error(…)` 的逻辑,使其与 403 的日志行为一致。
  7. 修改后重新运行复现步骤,确认 403 会触发 `async_log_failure_event` 和 `async_post_call_failure_hook`,并在 SpendLogs 中写入带 key、user 和 agent 的失败行。

验证方法

用未授权 agent 的 key 再次请求 `/a2a/plain`,检查:自定义回调文件中出现失败事件与失败 hook 调用;`LiteLLM_SpendLogs` 产生一条失败记录;该记录包含调用者 key、user 以及被拒绝的 agent(`plain`)。同时确认未授权模型的 `key_model_access_denied` 仍正常记录,未授权 agent 的 404 若也纳入修复,则应产生同类日志。Issue 中未给出合并后的验证结果,上述验证需在应用修复后自行执行。

参考来源

BerriAI/litellm #49425

GamsGo AI

AI 工具推荐

想把多个 AI 模型放在一个入口?

GamsGo AI 集成 ChatGPT、DeepSeek、Gemini、Claude、Midjourney、Veo 等常用模型,适合写作、绘图、视频和日常 AI 工作流。

了解 GamsGo AI

推广链接:通过此链接购买,我可能获得佣金,不影响你的价格。

这个方案解决了吗?

celebrityanime
celebrityanime
文章: 31852

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注