Skip to content

feat(a2a): reach Microsoft Foundry agents with Entra auth and versioned card discovery - #41511

Merged
mateo-berri merged 11 commits into
mainfrom
litellm_foundry_a2a_entra_agents
Sep 18, 2026
Merged

mateo-berri merged 11 commits into
mainfrom
litellm_foundry_a2a_entra_agents

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • The A2A relay cannot reach Microsoft Foundry agents at all
  • Foundry serves its card only at agentCard/v1.0, never the well-known paths
  • Foundry accepts only an Entra ID bearer, which the relay never mints
  • Foundry defaults to a non-blocking send, so chat completions came back empty
  • Chat completions never sent a registered agent's api_key or headers
  • Chat completions sent messages without kind, which Foundry rejects
  • Foundry has no message/stream, so streaming chat completions came back empty
  • The legacy azure_ai/agents/<name> provider only speaks the Assistants API

How it solves it:

  • New agent_card_path litellm_param, plus agentCard/v1.0 as a third discovery probe
  • Flat Entra fields on the agent mint a bearer for https://ai.azure.com/.default
  • The bearer rides message/send, message/stream, tasks/* and chat completions
  • Chat completions ask for a blocking send and get the answer in one response
  • The chat route finds the registered agent, so its api_key and headers are sent, with the Entra bearer winning over a static one stored next to the Entra fields
  • Every A2A message carries "kind": "message"
  • A card whose capabilities block does not say streaming: true turns a streaming chat completion into one blocking chunk
  • A JSON-RPC error inside a stream fails the call instead of yielding nothing
  • Entra credential fields stay out of the chat bridge's logged parameters

User Flow

Before: a platform admin registers a Microsoft Foundry agent, and every call to it fails inside the gateway before a request ever reaches the agent

  1. The admin adds the agent under a2a_agents in config.yaml with the Foundry A2A endpoint as the card url and the service principal's tenant_id, client_id and client_secret in litellm_params, then restarts the proxy
  2. They GET https://litellm-domain/v1/agents and see the agent listed by name
  3. A developer sends POST https://litellm-domain/a2a/foundry-agent with a JSON-RPC message/send body
  4. They get HTTP 500: -32603 Internal error: Failed to fetch agent card from https://<account>.services.ai.azure.com/api/projects/<project>/agents/<agent>/endpoint/protocols/a2a/.well-known/agent.json (HTTP 401)
  5. They retry with the A2A-Version: 1.0 header and a SendMessage body and get the same HTTP 500
  6. They send POST https://litellm-domain/v1/chat/completions with "model": "a2a/foundry-agent" and get HTTP 401 Access denied due to invalid subscription key or wrong API endpoint
  7. They send the same request with "stream": true and get the same HTTP 401
  8. They fall back to the legacy azure_ai/agents/<agent> model and get HTTP 400 Invalid 'assistant_id': '<agent>'. Expected an ID that begins with 'asst'

After: the same agent answers through the gateway on the A2A route and on chat completions

  1. The admin adds the agent under a2a_agents with the same card url, the same Entra fields, and agent_card_path: agentCard/v1.0 in litellm_params, then restarts the proxy
  2. They GET https://litellm-domain/v1/agents and see the agent listed by name
  3. A developer sends POST https://litellm-domain/a2a/foundry-agent with a JSON-RPC message/send body and "configuration": {"blocking": true}
  4. They get HTTP 200 with a completed task whose artifact carries the agent's answer
  5. They retry with the A2A-Version: 1.0 header and a SendMessage body and get HTTP 200 with a completed task; a GetTask with that task id returns the same task
  6. They send POST https://litellm-domain/v1/chat/completions with "model": "a2a/foundry-agent" and get HTTP 200 with the agent's answer in choices[0].message.content
  7. They send the same request with "stream": true and get an SSE stream: one chunk carrying the whole answer, one with "finish_reason": "stop", then [DONE]
  8. Leaving agent_card_path out still works: discovery reaches agentCard/v1.0 after the two well-known probes 404

Relevant issues

Affected release

Linear ticket

Resolves LIT-5122

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • The handful of test files covering my change pass locally, e.g. uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*, make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more
  • My PR passes all required CI/CD checks (e.g., lint, schema.d.ts sync check, etc.)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

Screenshots / Proof of Fix

Every block below was run at the commit its heading names: a proxy booted on this config from a worktree at that commit, against the same live Microsoft Foundry agent (a prompt agent that answers arithmetic and appends FOUNDRY-A2A-OK). The four AZURE_FOUNDRY_A2A_QA_* variables hold the Foundry project's service principal. foundry-agent names the card path; foundry-agent-discovered leaves discovery to the probes; foundry-agent-static-headers adds a stored headers entry, foundry-agent-with-api-key a wrong static api_key and foundry-agent-lowercase-auth a wrong authorization header, each next to the same Entra fields; those three are only used by the two sections at the end, and the Before side at the merge base ran with the first three agents, since the last two were added with 1c40e60. The registered card is Foundry's own agentCard/v1.0, with capabilities.streaming: false

general_settings:
  master_key: sk-1234
agents:
- agent_name: foundry-agent
  agent_card_params: &card
    name: litellm-a2a-qa
    version: 1.0.0
    protocolVersion: '1.0'
    url: https://<account>.services.ai.azure.com/api/projects/<project>/agents/litellm-a2a-qa/endpoint/protocols/a2a
    capabilities:
      streaming: false
      pushNotifications: false
    defaultInputModes: [text]
    defaultOutputModes: [text]
    skills:
    - id: answer
      name: Answer
      description: Answers a question in one sentence
      tags: [qa]
  litellm_params:
    tenant_id: os.environ/AZURE_FOUNDRY_A2A_QA_TENANT_ID
    client_id: os.environ/AZURE_FOUNDRY_A2A_QA_CLIENT_ID
    client_secret: os.environ/AZURE_FOUNDRY_A2A_QA_CLIENT_SECRET
    agent_card_path: agentCard/v1.0
- agent_name: foundry-agent-discovered
  agent_card_params: *card
  litellm_params:
    tenant_id: os.environ/AZURE_FOUNDRY_A2A_QA_TENANT_ID
    client_id: os.environ/AZURE_FOUNDRY_A2A_QA_CLIENT_ID
    client_secret: os.environ/AZURE_FOUNDRY_A2A_QA_CLIENT_SECRET
- agent_name: foundry-agent-static-headers
  agent_card_params: *card
  litellm_params:
    tenant_id: os.environ/AZURE_FOUNDRY_A2A_QA_TENANT_ID
    client_id: os.environ/AZURE_FOUNDRY_A2A_QA_CLIENT_ID
    client_secret: os.environ/AZURE_FOUNDRY_A2A_QA_CLIENT_SECRET
    agent_card_path: agentCard/v1.0
    headers:
      X-Rig: static
- agent_name: foundry-agent-with-api-key
  agent_card_params: *card
  litellm_params:
    tenant_id: os.environ/AZURE_FOUNDRY_A2A_QA_TENANT_ID
    client_id: os.environ/AZURE_FOUNDRY_A2A_QA_CLIENT_ID
    client_secret: os.environ/AZURE_FOUNDRY_A2A_QA_CLIENT_SECRET
    agent_card_path: agentCard/v1.0
    api_key: not-a-foundry-token
- agent_name: foundry-agent-lowercase-auth
  agent_card_params: *card
  litellm_params:
    tenant_id: os.environ/AZURE_FOUNDRY_A2A_QA_TENANT_ID
    client_id: os.environ/AZURE_FOUNDRY_A2A_QA_CLIENT_ID
    client_secret: os.environ/AZURE_FOUNDRY_A2A_QA_CLIENT_SECRET
    agent_card_path: agentCard/v1.0
    headers:
      authorization: Bearer not-a-foundry-token

Each side is one proxy process booted with --num_workers 2 and no database, the same topology on every side, started from its worktree with PYTHONPATH set to it; the run prints the proxy's working directory and where that environment resolves litellm, and both sit inside the worktree. The curls below show the proxy on port 4000 with master key sk-1234; the run used a random port and its own key

Before (16bbff6)

Registered agents

  1. List the agents (names and card protocol version):
    curl http://localhost:4000/v1/agents -H 'Authorization: Bearer sk-1234'
    
    [('foundry-agent', '1.0'), ('foundry-agent-discovered', '1.0'), ('foundry-agent-static-headers', '1.0')]
    

0.3 message/send through the relay

  1. Blocking send to the agent registered with agent_card_path:
    curl -X POST http://localhost:4000/a2a/foundry-agent -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":"1","method":"message/send","params":{"message":{"kind":"message","messageId":"m1","role":"user","parts":[{"kind":"text","text":"What is 2+2? Reply with the number."}]},"configuration":{"blocking":true}}}'
    
    {"jsonrpc":"2.0","id":"1","error":{"code":-32603,"message":"Internal error: Failed to fetch agent card from https://<account>.services.ai.azure.com/api/projects/<project>/agents/litellm-a2a-qa/endpoint/protocols/a2a/.well-known/agent.json (HTTP 401): Client error '401 Unauthorized' for url 'https://<account>.services.ai.azure.com/api/projects/<project>/agents/litellm-a2a-qa/endpoint/protocols/a2a/.well-known/agent.json'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401"}}
    HTTP 500
    

1.0 SendMessage through the relay

  1. Same question with the A2A-Version: 1.0 header and the 1.0 body:
    curl -X POST http://localhost:4000/a2a/foundry-agent -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -H 'A2A-Version: 1.0' -d '{"jsonrpc":"2.0","id":"2","method":"SendMessage","params":{"message":{"messageId":"m2","role":"ROLE_USER","parts":[{"text":"What is 2+2? Reply with the number."}]},"configuration":{"blocking":true}}}'
    
    {"jsonrpc":"2.0","id":"2","error":{"code":-32603,"message":"Internal error: Failed to fetch agent card from https://<account>.services.ai.azure.com/api/projects/<project>/agents/litellm-a2a-qa/endpoint/protocols/a2a/.well-known/agent.json (HTTP 401): Client error '401 Unauthorized' for url 'https://<account>.services.ai.azure.com/api/projects/<project>/agents/litellm-a2a-qa/endpoint/protocols/a2a/.well-known/agent.json'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401"}}
    HTTP 500
    

Discovery without agent_card_path

  1. 0.3 blocking send to the agent registered without agent_card_path:
    curl -X POST http://localhost:4000/a2a/foundry-agent-discovered -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":"1","method":"message/send","params":{"message":{"kind":"message","messageId":"m1","role":"user","parts":[{"kind":"text","text":"What is 2+2? Reply with the number."}]},"configuration":{"blocking":true}}}'
    
    {"jsonrpc":"2.0","id":"1","error":{"code":-32603,"message":"Internal error: Failed to fetch agent card from https://<account>.services.ai.azure.com/api/projects/<project>/agents/litellm-a2a-qa/endpoint/protocols/a2a/.well-known/agent.json (HTTP 401): Client error '401 Unauthorized' for url 'https://<account>.services.ai.azure.com/api/projects/<project>/agents/litellm-a2a-qa/endpoint/protocols/a2a/.well-known/agent.json'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401"}}
    HTTP 500
    
  2. 1.0 SendMessage to the same agent:
    curl -X POST http://localhost:4000/a2a/foundry-agent-discovered -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -H 'A2A-Version: 1.0' -d '{"jsonrpc":"2.0","id":"2","method":"SendMessage","params":{"message":{"messageId":"m2","role":"ROLE_USER","parts":[{"text":"What is 2+2? Reply with the number."}]},"configuration":{"blocking":true}}}'
    
    {"jsonrpc":"2.0","id":"2","error":{"code":-32603,"message":"Internal error: Failed to fetch agent card from https://<account>.services.ai.azure.com/api/projects/<project>/agents/litellm-a2a-qa/endpoint/protocols/a2a/.well-known/agent.json (HTTP 401): Client error '401 Unauthorized' for url 'https://<account>.services.ai.azure.com/api/projects/<project>/agents/litellm-a2a-qa/endpoint/protocols/a2a/.well-known/agent.json'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401"}}
    HTTP 500
    

Non-blocking message/send then tasks/get

  1. Send without a blocking configuration:
    curl -X POST http://localhost:4000/a2a/foundry-agent -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":"3","method":"message/send","params":{"message":{"kind":"message","messageId":"m3","role":"user","parts":[{"kind":"text","text":"What is 3+3? Reply with the number."}]}}}'
    
    {"jsonrpc":"2.0","id":"3","error":{"code":-32603,"message":"Internal error: Failed to fetch agent card from https://<account>.services.ai.azure.com/api/projects/<project>/agents/litellm-a2a-qa/endpoint/protocols/a2a/.well-known/agent.json (HTTP 401): Client error '401 Unauthorized' for url 'https://<account>.services.ai.azure.com/api/projects/<project>/agents/litellm-a2a-qa/endpoint/protocols/a2a/.well-known/agent.json'
    For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401"}}
    task id: 
    
  2. Fetch the task by its id four seconds later:
    curl -X POST http://localhost:4000/a2a/foundry-agent -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":"4","method":"tasks/get","params":{"id":"<task id from step 1>"}}'
    
    {"jsonrpc":"2.0","id":"4","error":{"code":-32603,"message":"Internal error: Client error '401 Unauthorized' for url 'https://<account>.services.ai.azure.com/api/projects/<project>/agents/litellm-a2a-qa/endpoint/protocols/a2a'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401"}}
    HTTP 500
    

1.0 multi-turn on the contextId

  1. Continue the conversation from the 1.0 SendMessage case:
    curl -X POST http://localhost:4000/a2a/foundry-agent -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -H 'A2A-Version: 1.0' -d '{"jsonrpc":"2.0","id":"5","method":"SendMessage","params":{"message":{"messageId":"m5","role":"ROLE_USER","contextId":"<contextId from the 1.0 SendMessage case>","parts":[{"text":"Add 10 to your previous answer. Reply with the number."}]},"configuration":{"blocking":true}}}'
    
    contextId: 
    {"jsonrpc":"2.0","id":"5","error":{"code":-32603,"message":"Internal error: Failed to fetch agent card from https://<account>.services.ai.azure.com/api/projects/<project>/agents/litellm-a2a-qa/endpoint/protocols/a2a/.well-known/agent.json (HTTP 401): Client error '401 Unauthorized' for url 'https://<account>.services.ai.azure.com/api/projects/<project>/agents/litellm-a2a-qa/endpoint/protocols/a2a/.well-known/agent.json'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401"}}
    HTTP 500
    

Chat completions

  1. Non-streaming chat completion to the agent registered with agent_card_path:
    curl -X POST http://localhost:4000/v1/chat/completions -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"model":"a2a/foundry-agent","messages":[{"role":"user","content":"What is 2+2? Reply with the number."}]}'
    
    {"error":{"message":"litellm.AuthenticationError: A2aException - {\"error\":{\"code\":\"401\",\"message\":\"Access denied due to invalid subscription key or wrong API endpoint. Make sure to provide a valid key for an active subscription and use a correct regional API endpoint for your resource.\"}}","type":"authentication_error","param":null,"code":"401"}}
    HTTP 401
    
  2. Same to the agent registered without it:
    curl -X POST http://localhost:4000/v1/chat/completions -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"model":"a2a/foundry-agent-discovered","messages":[{"role":"user","content":"What is 5+5? Reply with the number."}]}'
    
    {"error":{"message":"litellm.AuthenticationError: A2aException - {\"error\":{\"code\":\"401\",\"message\":\"Access denied due to invalid subscription key or wrong API endpoint. Make sure to provide a valid key for an active subscription and use a correct regional API endpoint for your resource.\"}}","type":"authentication_error","param":null,"code":"401"}}
    HTTP 401
    

Streaming chat completions

  1. The same request with "stream": true:
    curl -X POST http://localhost:4000/v1/chat/completions -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"model":"a2a/foundry-agent","stream":true,"messages":[{"role":"user","content":"What is 6+1? Reply with the number."}]}'
    
    {"error":{"message":"litellm.AuthenticationError: A2aException - {\"error\":{\"code\":\"401\",\"message\":\"Access denied due to invalid subscription key or wrong API endpoint. Make sure to provide a valid key for an active subscription and use a correct regional API endpoint for your resource.\"}}","type":"authentication_error","param":null,"code":"401"}}
    HTTP 401
    

The card the gateway serves

  1. Fetch the card the proxy publishes for the agent:
    curl http://localhost:4000/a2a/foundry-agent/.well-known/agent-card.json -H 'Authorization: Bearer sk-1234'
    
    {"name":"litellm-a2a-qa","description":"LiteLLM QA prompt agent exposed over A2A","version":"1.0.0","supportedInterfaces":[{"url":"http://localhost:4000/a2a/foundry-agent","protocolBinding":"JSONRPC","protocolVersion":"1.0"},{"url":"https://<account>.services.ai.azure.com/api/projects/<project>/agents/litellm-a2a-qa/endpoint/protocols/a2a","protocolBinding":"JSONRPC","protocolVersion":"0.3"},{"url":"https://<account>.services.ai.azure.com/api/projects/<project>/agents/li
    

After (e5744c5)

Registered agents

  1. List the agents (names and card protocol version):
    curl http://localhost:4000/v1/agents -H 'Authorization: Bearer sk-1234'
    
    [('foundry-agent', '1.0'), ('foundry-agent-discovered', '1.0'), ('foundry-agent-static-headers', '1.0'), ('foundry-agent-with-api-key', '1.0'), ('foundry-agent-lowercase-auth', '1.0')]
    

0.3 message/send through the relay

  1. Blocking send to the agent registered with agent_card_path:
    curl -X POST http://localhost:4000/a2a/foundry-agent -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":"1","method":"message/send","params":{"message":{"kind":"message","messageId":"m1","role":"user","parts":[{"kind":"text","text":"What is 2+2? Reply with the number."}]},"configuration":{"blocking":true}}}'
    
    {"id":"1","jsonrpc":"2.0","result":{"task":{"id":"resp_0a3b2686b3fee55a006aad807f83a48190a8c9cf8a3c88501a","contextId":"ctxt_f7667846ef6841dc8d0515a30b52cfb2","status":{"state":"TASK_STATE_COMPLETED","timestamp":"2026-09-18T18:18:40Z"},"artifacts":[{"artifactId":"msg_0a3b2686b3fee55a006aad808032048190b67aa8251132bf05","parts":[{"text":"4 FOUNDRY-A2A-OK"}]}]}}}
    HTTP 200
    

1.0 SendMessage through the relay

  1. Same question with the A2A-Version: 1.0 header and the 1.0 body:
    curl -X POST http://localhost:4000/a2a/foundry-agent -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -H 'A2A-Version: 1.0' -d '{"jsonrpc":"2.0","id":"2","method":"SendMessage","params":{"message":{"messageId":"m2","role":"ROLE_USER","parts":[{"text":"What is 2+2? Reply with the number."}]},"configuration":{"blocking":true}}}'
    
    {"id":"2","jsonrpc":"2.0","result":{"task":{"id":"resp_0acdfd74e1e7f899006aad80827d248190babebc879a5d7b1e","contextId":"ctxt_530fe4d143e849a0acf4ad336e946b1d","status":{"state":"TASK_STATE_COMPLETED","timestamp":"2026-09-18T18:18:43Z"},"artifacts":[{"artifactId":"msg_0acdfd74e1e7f899006aad808346d08190b16b41bfd779ab87","parts":[{"text":"4 FOUNDRY-A2A-OK"}]}]}}}
    HTTP 200
    

Discovery without agent_card_path

  1. 0.3 blocking send to the agent registered without agent_card_path:
    curl -X POST http://localhost:4000/a2a/foundry-agent-discovered -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":"1","method":"message/send","params":{"message":{"kind":"message","messageId":"m1","role":"user","parts":[{"kind":"text","text":"What is 2+2? Reply with the number."}]},"configuration":{"blocking":true}}}'
    
    {"id":"1","jsonrpc":"2.0","result":{"task":{"id":"resp_05a5f1133c98c8cb006aad8085750c8190b1ceb2d8ce78e812","contextId":"ctxt_3edb4306a4854d24ab11af028086e852","status":{"state":"TASK_STATE_COMPLETED","timestamp":"2026-09-18T18:18:46Z"},"artifacts":[{"artifactId":"msg_05a5f1133c98c8cb006aad808612cc819097eb705bf80eb27f","parts":[{"text":"The answer is 4. FOUNDRY-A2A-OK"}]}]}}}
    HTTP 200
    
  2. 1.0 SendMessage to the same agent:
    curl -X POST http://localhost:4000/a2a/foundry-agent-discovered -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -H 'A2A-Version: 1.0' -d '{"jsonrpc":"2.0","id":"2","method":"SendMessage","params":{"message":{"messageId":"m2","role":"ROLE_USER","parts":[{"text":"What is 2+2? Reply with the number."}]},"configuration":{"blocking":true}}}'
    
    {"id":"2","jsonrpc":"2.0","result":{"task":{"id":"resp_0077f3bc6c49e0bc006aad808978808190becbc26365893167","contextId":"ctxt_14b054f4d68344b99881ebacb3373146","status":{"state":"TASK_STATE_COMPLETED","timestamp":"2026-09-18T18:18:50Z"},"artifacts":[{"artifactId":"msg_0077f3bc6c49e0bc006aad808a198c81908c4f9069e2a5c457","parts":[{"text":"The answer is 4. FOUNDRY-A2A-OK"}]}]}}}
    HTTP 200
    

Non-blocking message/send then tasks/get

  1. Send without a blocking configuration:
    curl -X POST http://localhost:4000/a2a/foundry-agent -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":"3","method":"message/send","params":{"message":{"kind":"message","messageId":"m3","role":"user","parts":[{"kind":"text","text":"What is 3+3? Reply with the number."}]}}}'
    
    {"id":"3","jsonrpc":"2.0","result":{"task":{"id":"resp_009b445f80c98f85006aad808bec60819082629f1faa2b4361","contextId":"ctxt_01846d7ba1c8434c93c38a6e0633ec8e","status":{"state":"TASK_STATE_COMPLETED","timestamp":"2026-09-18T18:18:53Z"},"artifacts":[{"artifactId":"msg_009b445f80c98f85006aad808c99448190816e4ecd52dcd6a7","parts":[{"text":"6 FOUNDRY-A2A-OK"}]}]}}}
    
  2. Fetch the task by its id four seconds later:
    curl -X POST http://localhost:4000/a2a/foundry-agent -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":"4","method":"tasks/get","params":{"id":"<task id from step 1>"}}'
    
    {"id":"4","jsonrpc":"2.0","result":{"id":"resp_009b445f80c98f85006aad808bec60819082629f1faa2b4361","contextId":"ctxt_01846d7ba1c8434c93c38a6e0633ec8e","status":{"state":"TASK_STATE_COMPLETED","timestamp":"2026-09-18T18:18:53Z"},"artifacts":[{"artifactId":"msg_009b445f80c98f85006aad808c99448190816e4ecd52dcd6a7","parts":[{"text":"6 FOUNDRY-A2A-OK"}]}]}}
    HTTP 200
    

1.0 multi-turn on the contextId

  1. Continue the conversation from the 1.0 SendMessage case:
    curl -X POST http://localhost:4000/a2a/foundry-agent -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -H 'A2A-Version: 1.0' -d '{"jsonrpc":"2.0","id":"5","method":"SendMessage","params":{"message":{"messageId":"m5","role":"ROLE_USER","contextId":"<contextId from the 1.0 SendMessage case>","parts":[{"text":"Add 10 to your previous answer. Reply with the number."}]},"configuration":{"blocking":true}}}'
    
    {"id":"5","jsonrpc":"2.0","result":{"task":{"id":"resp_0acdfd74e1e7f899006aad809369bc8190b4ce7adf2a85c87c","contextId":"ctxt_530fe4d143e849a0acf4ad336e946b1d","status":{"state":"TASK_STATE_COMPLETED","timestamp":"2026-09-18T18:19:00Z"},"artifacts":[{"artifactId":"msg_0acdfd74e1e7f899006aad80941dd48190bd50e77703bb5551","parts":[{"text":"14 FOUNDRY-A2A-OK"}]}]}}}
    HTTP 200
    

Chat completions

  1. Non-streaming chat completion to the agent registered with agent_card_path:
    curl -X POST http://localhost:4000/v1/chat/completions -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"model":"a2a/foundry-agent","messages":[{"role":"user","content":"What is 2+2? Reply with the number."}]}'
    
    {"id":"3876b7b9-a9c4-437f-9505-902282d7bdf5","created":1789755541,"model":"a2a/foundry-agent","object":"chat.completion","choices":[{"finish_reason":"stop","index":0,"message":{"content":"4 FOUNDRY-A2A-OK","role":"assistant"}}],"usage":{"completion_tokens":10,"prompt_tokens":19,"total_tokens":29}}
    HTTP 200
    
  2. Same to the agent registered without it:
    curl -X POST http://localhost:4000/v1/chat/completions -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"model":"a2a/foundry-agent-discovered","messages":[{"role":"user","content":"What is 5+5? Reply with the number."}]}'
    
    {"id":"0de1d0a1-062d-4492-9e0c-7fe97f2d9fee","created":1789755544,"model":"a2a/foundry-agent-discovered","object":"chat.completion","choices":[{"finish_reason":"stop","index":0,"message":{"content":"10 FOUNDRY-A2A-OK","role":"assistant"}}],"usage":{"completion_tokens":10,"prompt_tokens":19,"total_tokens":29}}
    HTTP 200
    

Streaming chat completions

  1. The same request with "stream": true:
    curl -X POST http://localhost:4000/v1/chat/completions -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"model":"a2a/foundry-agent","stream":true,"messages":[{"role":"user","content":"What is 6+1? Reply with the number."}]}'
    
    data: {"id":"535762fe-43e0-4e08-8f63-b81c57d92d01","object":"chat.completion.chunk","created":1789755549,"model":"a2a/foundry-agent","choices":[{"index":0,"delta":{"role":"assistant","content":"7 FOUNDRY-A2A-OK"}}]}
    data: {"id":"535762fe-43e0-4e08-8f63-b81c57d92d01","object":"chat.completion.chunk","created":1789755549,"model":"a2a/foundry-agent","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
    data: [DONE]
    HTTP 200
    

The card the gateway serves

  1. Fetch the card the proxy publishes for the agent:
    curl http://localhost:4000/a2a/foundry-agent/.well-known/agent-card.json -H 'Authorization: Bearer sk-1234'
    
    {"name":"litellm-a2a-qa","description":"LiteLLM QA prompt agent exposed over A2A","version":"1.0.0","supportedInterfaces":[{"url":"http://localhost:4000/a2a/foundry-agent","protocolBinding":"JSONRPC","protocolVersion":"1.0"},{"url":"https://<account>.services.ai.azure.com/api/projects/<project>/agents/litellm-a2a-qa/endpoint/protocols/a2a","protocolBinding":"JSONRPC","protocolVersion":"0.3"},{"url":"https://<account>.services.ai.azure.com/api/projects/<project>/agents/li
    

Stored agent headers stay as configured

This leg covers decision 23 (commit 4669041), so its before side is the PR's tip before that commit, c2f77fd, where the agent already answers, and not the merge base, where an Entra-authenticated Foundry agent cannot be reached through a2a/ at all. Same config, same topology, same requests on both sides. The listing masks the value of any key that looks sensitive, so a leaked bearer shows as the masked Authorization entry. At the merge base the same three steps give six HTTP 401s and unchanged stored headers, so the write only ever existed on the PR's earlier tip

Before (c2f77fd)
  1. Read the headers stored on the agent (litellm_params.headers of foundry-agent-static-headers in the listing), four reads in a row against the two-worker proxy:
    for i in 1 2 3 4; do
      curl -s http://localhost:4000/v1/agents -H 'Authorization: Bearer sk-1234' \
        | python3 -c 'import json,sys; [print(json.dumps(a["litellm_params"].get("headers"))) for a in json.load(sys.stdin) if a["agent_name"] == "foundry-agent-static-headers"]'
    done
    
    {"X-Rig": "static"}
    {"X-Rig": "static"}
    {"X-Rig": "static"}
    {"X-Rig": "static"}
    
  2. Send six chat completions through that agent:
    for i in 1 2 3 4 5 6; do
      curl -s -w ' HTTP %{http_code}' -X POST http://localhost:4000/v1/chat/completions -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' \
        -d '{"model":"a2a/foundry-agent-static-headers","messages":[{"role":"user","content":"What is 2+2? Reply with the number."}]}' \
        | python3 -c 'import json,sys; body, _, code = sys.stdin.read().rpartition(" HTTP "); print("assistant:", json.dumps(json.loads(body)["choices"][0]["message"]["content"]), "| HTTP", code)'
    done
    
    assistant: "FOUNDRY-A2A-OK The answer is 4." | HTTP 200
    assistant: "The answer is 4. FOUNDRY-A2A-OK" | HTTP 200
    assistant: "4 FOUNDRY-A2A-OK" | HTTP 200
    assistant: "4 FOUNDRY-A2A-OK" | HTTP 200
    assistant: "4 FOUNDRY-A2A-OK" | HTTP 200
    assistant: "4 FOUNDRY-A2A-OK" | HTTP 200
    
  3. Read the stored headers again, four reads in a row:
    for i in 1 2 3 4; do
      curl -s http://localhost:4000/v1/agents -H 'Authorization: Bearer sk-1234' \
        | python3 -c 'import json,sys; [print(json.dumps(a["litellm_params"].get("headers"))) for a in json.load(sys.stdin) if a["agent_name"] == "foundry-agent-static-headers"]'
    done
    
    {"X-Rig": "static", "Content-Type": "application/json", "Authorization": "REDACTED_BY_LITELM"}
    {"X-Rig": "static", "Content-Type": "application/json", "Authorization": "REDACTED_BY_LITELM"}
    {"X-Rig": "static", "Content-Type": "application/json", "Authorization": "REDACTED_BY_LITELM"}
    {"X-Rig": "static", "Content-Type": "application/json", "Authorization": "REDACTED_BY_LITELM"}
    
After (e5744c5)
  1. Read the headers stored on the agent (litellm_params.headers of foundry-agent-static-headers in the listing), four reads in a row against the two-worker proxy:
    for i in 1 2 3 4; do
      curl -s http://localhost:4000/v1/agents -H 'Authorization: Bearer sk-1234' \
        | python3 -c 'import json,sys; [print(json.dumps(a["litellm_params"].get("headers"))) for a in json.load(sys.stdin) if a["agent_name"] == "foundry-agent-static-headers"]'
    done
    
    {"X-Rig": "static"}
    {"X-Rig": "static"}
    {"X-Rig": "static"}
    {"X-Rig": "static"}
    
  2. Send six chat completions through that agent:
    for i in 1 2 3 4 5 6; do
      curl -s -w ' HTTP %{http_code}' -X POST http://localhost:4000/v1/chat/completions -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' \
        -d '{"model":"a2a/foundry-agent-static-headers","messages":[{"role":"user","content":"What is 2+2? Reply with the number."}]}' \
        | python3 -c 'import json,sys; body, _, code = sys.stdin.read().rpartition(" HTTP "); print("assistant:", json.dumps(json.loads(body)["choices"][0]["message"]["content"]), "| HTTP", code)'
    done
    
    assistant: "4 FOUNDRY-A2A-OK" | HTTP 200
    assistant: "4 FOUNDRY-A2A-OK" | HTTP 200
    assistant: "The answer is 4. FOUNDRY-A2A-OK" | HTTP 200
    assistant: "2+2 is 4. FOUNDRY-A2A-OK" | HTTP 200
    assistant: "The answer is 4. FOUNDRY-A2A-OK" | HTTP 200
    assistant: "The answer is 4. FOUNDRY-A2A-OK" | HTTP 200
    
  3. Read the stored headers again, four reads in a row:
    for i in 1 2 3 4; do
      curl -s http://localhost:4000/v1/agents -H 'Authorization: Bearer sk-1234' \
        | python3 -c 'import json,sys; [print(json.dumps(a["litellm_params"].get("headers"))) for a in json.load(sys.stdin) if a["agent_name"] == "foundry-agent-static-headers"]'
    done
    
    {"X-Rig": "static"}
    {"X-Rig": "static"}
    {"X-Rig": "static"}
    {"X-Rig": "static"}
    

An api_key or authorization header stored next to the Entra fields

This section covers decision 24 (commit 1c40e60), so its before side is the PR's tip before that commit, 4669041, where an Entra agent already answers on both routes, and not the merge base, where no Entra agent can be reached at all. Two more agents carry a wrong static credential next to the same Entra fields: foundry-agent-with-api-key stores api_key: not-a-foundry-token and foundry-agent-lowercase-auth stores headers: {authorization: Bearer not-a-foundry-token}. At 4669041 the relay already let the minted bearer win (decisions 7 and 20) while the chat route sent the stored value, so /a2a/<agent> answered and a2a/<agent> chat completions failed with Foundry's 401: Received 401 from a service request for the stored key, and Error parsing client JWT for the stored header, since both authorization lines went out. Same config and topology on both sides, four calls per step so both workers answer

Before (4669041)
  1. Four blocking message/send calls through /a2a/foundry-agent-with-api-key, the agent with a stored api_key:
    for i in 1 2 3 4; do
      curl -s -w ' HTTP %{http_code}' -X POST http://localhost:4000/a2a/foundry-agent-with-api-key -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' \
        -d '{"jsonrpc":"2.0","id":"1","method":"message/send","params":{"message":{"kind":"message","messageId":"m1","role":"user","parts":[{"kind":"text","text":"What is 2+2? Reply with the number."}]},"configuration":{"blocking":true}}}' \
        | python3 -c 'import json,sys; body, _, code = sys.stdin.read().rpartition(" HTTP "); d = json.loads(body); r = d.get("result") or {}; t = r.get("task") or r; print("task state:", (t.get("status") or {}).get("state"), "| error:", json.dumps(d.get("error")), "| HTTP", code)'
    done
    
    task state: TASK_STATE_COMPLETED | error: null | HTTP 200
    task state: TASK_STATE_COMPLETED | error: null | HTTP 200
    task state: TASK_STATE_COMPLETED | error: null | HTTP 200
    task state: TASK_STATE_COMPLETED | error: null | HTTP 200
    
  2. Four chat completions with "model": "a2a/foundry-agent-with-api-key":
    for i in 1 2 3 4; do
      curl -s -w ' HTTP %{http_code}' -X POST http://localhost:4000/v1/chat/completions -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' \
        -d '{"model":"a2a/foundry-agent-with-api-key","messages":[{"role":"user","content":"What is 2+2? Reply with the number."}]}' \
        | python3 -c 'import json,sys; body, _, code = sys.stdin.read().rpartition(" HTTP "); d = json.loads(body); print("assistant:", json.dumps(d["choices"][0]["message"]["content"]) if "choices" in d else body, "| HTTP", code)'
    done
    
    {"error":{"message":"litellm.APIConnectionError: A2aException - A2A error: Received 401 from a service request","type":"internal_server_error","param":null,"code":"500"}} | HTTP 500
    {"error":{"message":"litellm.APIConnectionError: A2aException - A2A error: Received 401 from a service request","type":"internal_server_error","param":null,"code":"500"}} | HTTP 500
    {"error":{"message":"litellm.APIConnectionError: A2aException - A2A error: Received 401 from a service request","type":"internal_server_error","param":null,"code":"500"}} | HTTP 500
    {"error":{"message":"litellm.APIConnectionError: A2aException - A2A error: Received 401 from a service request","type":"internal_server_error","param":null,"code":"500"}} | HTTP 500
    
  3. Four blocking message/send calls through /a2a/foundry-agent-lowercase-auth, the agent with a stored authorization header:
    for i in 1 2 3 4; do
      curl -s -w ' HTTP %{http_code}' -X POST http://localhost:4000/a2a/foundry-agent-lowercase-auth -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' \
        -d '{"jsonrpc":"2.0","id":"1","method":"message/send","params":{"message":{"kind":"message","messageId":"m1","role":"user","parts":[{"kind":"text","text":"What is 2+2? Reply with the number."}]},"configuration":{"blocking":true}}}' \
        | python3 -c 'import json,sys; body, _, code = sys.stdin.read().rpartition(" HTTP "); d = json.loads(body); r = d.get("result") or {}; t = r.get("task") or r; print("task state:", (t.get("status") or {}).get("state"), "| error:", json.dumps(d.get("error")), "| HTTP", code)'
    done
    
    task state: TASK_STATE_COMPLETED | error: null | HTTP 200
    task state: TASK_STATE_COMPLETED | error: null | HTTP 200
    task state: TASK_STATE_COMPLETED | error: null | HTTP 200
    task state: TASK_STATE_COMPLETED | error: null | HTTP 200
    
  4. Four chat completions with "model": "a2a/foundry-agent-lowercase-auth":
    for i in 1 2 3 4; do
      curl -s -w ' HTTP %{http_code}' -X POST http://localhost:4000/v1/chat/completions -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' \
        -d '{"model":"a2a/foundry-agent-lowercase-auth","messages":[{"role":"user","content":"What is 2+2? Reply with the number."}]}' \
        | python3 -c 'import json,sys; body, _, code = sys.stdin.read().rpartition(" HTTP "); d = json.loads(body); print("assistant:", json.dumps(d["choices"][0]["message"]["content"]) if "choices" in d else body, "| HTTP", code)'
    done
    
    {"error":{"message":"litellm.APIConnectionError: A2aException - A2A error: Error parsing client JWT: ","type":"internal_server_error","param":null,"code":"500"}} | HTTP 500
    {"error":{"message":"litellm.APIConnectionError: A2aException - A2A error: Error parsing client JWT: ","type":"internal_server_error","param":null,"code":"500"}} | HTTP 500
    {"error":{"message":"litellm.APIConnectionError: A2aException - A2A error: Error parsing client JWT: ","type":"internal_server_error","param":null,"code":"500"}} | HTTP 500
    {"error":{"message":"litellm.APIConnectionError: A2aException - A2A error: Error parsing client JWT: ","type":"internal_server_error","param":null,"code":"500"}} | HTTP 500
    
After (e5744c5)
  1. Four blocking message/send calls through /a2a/foundry-agent-with-api-key, the agent with a stored api_key:
    for i in 1 2 3 4; do
      curl -s -w ' HTTP %{http_code}' -X POST http://localhost:4000/a2a/foundry-agent-with-api-key -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' \
        -d '{"jsonrpc":"2.0","id":"1","method":"message/send","params":{"message":{"kind":"message","messageId":"m1","role":"user","parts":[{"kind":"text","text":"What is 2+2? Reply with the number."}]},"configuration":{"blocking":true}}}' \
        | python3 -c 'import json,sys; body, _, code = sys.stdin.read().rpartition(" HTTP "); d = json.loads(body); r = d.get("result") or {}; t = r.get("task") or r; print("task state:", (t.get("status") or {}).get("state"), "| error:", json.dumps(d.get("error")), "| HTTP", code)'
    done
    
    task state: TASK_STATE_COMPLETED | error: null | HTTP 200
    task state: TASK_STATE_COMPLETED | error: null | HTTP 200
    task state: TASK_STATE_COMPLETED | error: null | HTTP 200
    task state: TASK_STATE_COMPLETED | error: null | HTTP 200
    
  2. Four chat completions with "model": "a2a/foundry-agent-with-api-key":
    for i in 1 2 3 4; do
      curl -s -w ' HTTP %{http_code}' -X POST http://localhost:4000/v1/chat/completions -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' \
        -d '{"model":"a2a/foundry-agent-with-api-key","messages":[{"role":"user","content":"What is 2+2? Reply with the number."}]}' \
        | python3 -c 'import json,sys; body, _, code = sys.stdin.read().rpartition(" HTTP "); d = json.loads(body); print("assistant:", json.dumps(d["choices"][0]["message"]["content"]) if "choices" in d else body, "| HTTP", code)'
    done
    
    assistant: "4 FOUNDRY-A2A-OK" | HTTP 200
    assistant: "The answer is 4. FOUNDRY-A2A-OK" | HTTP 200
    assistant: "The answer is 4. FOUNDRY-A2A-OK" | HTTP 200
    assistant: "4 FOUNDRY-A2A-OK" | HTTP 200
    
  3. Four blocking message/send calls through /a2a/foundry-agent-lowercase-auth, the agent with a stored authorization header:
    for i in 1 2 3 4; do
      curl -s -w ' HTTP %{http_code}' -X POST http://localhost:4000/a2a/foundry-agent-lowercase-auth -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' \
        -d '{"jsonrpc":"2.0","id":"1","method":"message/send","params":{"message":{"kind":"message","messageId":"m1","role":"user","parts":[{"kind":"text","text":"What is 2+2? Reply with the number."}]},"configuration":{"blocking":true}}}' \
        | python3 -c 'import json,sys; body, _, code = sys.stdin.read().rpartition(" HTTP "); d = json.loads(body); r = d.get("result") or {}; t = r.get("task") or r; print("task state:", (t.get("status") or {}).get("state"), "| error:", json.dumps(d.get("error")), "| HTTP", code)'
    done
    
    task state: TASK_STATE_COMPLETED | error: null | HTTP 200
    task state: TASK_STATE_COMPLETED | error: null | HTTP 200
    task state: TASK_STATE_COMPLETED | error: null | HTTP 200
    task state: TASK_STATE_COMPLETED | error: null | HTTP 200
    
  4. Four chat completions with "model": "a2a/foundry-agent-lowercase-auth":
    for i in 1 2 3 4; do
      curl -s -w ' HTTP %{http_code}' -X POST http://localhost:4000/v1/chat/completions -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' \
        -d '{"model":"a2a/foundry-agent-lowercase-auth","messages":[{"role":"user","content":"What is 2+2? Reply with the number."}]}' \
        | python3 -c 'import json,sys; body, _, code = sys.stdin.read().rpartition(" HTTP "); d = json.loads(body); print("assistant:", json.dumps(d["choices"][0]["message"]["content"]) if "choices" in d else body, "| HTTP", code)'
    done
    
    assistant: "4 FOUNDRY-A2A-OK" | HTTP 200
    assistant: "4 FOUNDRY-A2A-OK" | HTTP 200
    assistant: "4 FOUNDRY-A2A-OK" | HTTP 200
    assistant: "FOUNDRY-A2A-OK The answer is 4." | HTTP 200
    

Observed during the run, beyond the flow above:

  • No-capabilities agents stream to HTTP 500 on Foundry: PR causes
  • SDK stream path errors instead of empty stream: PR causes
  • SDK card discovery raises A2AAgentCardDiscoveryError now: PR causes
  • 0.3 message/send answers in the 1.0 task shape: PR leaves
  • Served card rewrites only the first interface url: PR leaves
  • Foundry rejects tasks/cancel with -32004: PR leaves
  • Listing masks client_secret; tenant and client ids shown: PR leaves

Type

🆕 New Feature

Decisions

Ordered by how much they shape the change. Each names the alternative it beat

  1. Card discovery adds an agent_card_path litellm_param and agentCard/v1.0 as a third probe after the two well-known paths. Foundry configs should set agent_card_path: agentCard/v1.0 to skip the two 404 probes. Alternative: probe only, which costs two failed round trips per request on Foundry; or trust the registered card and skip discovery, which changes how every agent behaves today
  2. Discovery failure raises A2AAgentCardDiscoveryError, which names every probed path and its error. Alternative: keep reporting only the last probe's error, which hid the 401 Foundry returns on the well-known path
  3. Entra credentials are flat fields on the agent's litellm_params (tenant_id + client_id + client_secret, or azure_ad_token, or client_id + azure_username + azure_password, plus an optional azure_scope), the same names the azure_ai provider uses. Alternative: a nested azure_entra block nobody else reads, or reusing api_key for a pasted bearer, which cannot mint or refresh
  4. The Entra path opens only when a credential-bearing field (client_secret, azure_ad_token, azure_password) is set on the agent, and never reads the process-wide AZURE_* variables. Alternative: the legacy get_azure_ad_token fallback chain, which would authenticate every agent on a box that has AZURE_CLIENT_SECRET exported and could send one agent's identity to another agent. Managed identity and DefaultAzureCredential are not wired for agents in this PR
  5. The default scope is https://ai.azure.com/.default, the Foundry Agent Consumer scope, overridable with azure_scope. Alternative: the legacy https://cognitiveservices.azure.com/.default, which Foundry rejects with 401
  6. When one agent carries several credential shapes, a service principal wins, then username and password, then an OIDC token, then a static azure_ad_token. Alternative: static token first, where a stale pasted token would beat a renewable principal
  7. On the relay, databricks_oauth is checked before Entra, and the minted bearer overrides an Authorization set in static_headers. Alternative: static headers win, where a stale header would shadow a fresh token
  8. Bridge agents (any agent with custom_llm_provider) are left out of the relay's Entra mint; their Entra fields belong to the model provider they bridge to. Alternative: mint for every agent, which would send a Foundry bearer to a chat model provider
  9. Entra fields stay out of optional_params on the chat route and out of the streaming logging object's litellm_params, which now carries only the pricing keys. Alternative: rely on the downstream secret redaction, which runs after raw kwargs reach callbacks
  10. Chat completions send configuration.blocking: true on message/send, so Foundry answers in the same response. Alternative: poll tasks/get until the task completes, which needs a timeout policy and more round trips. acceptedOutputModes is left unset because Foundry ignores it
  11. The chat route looks the agent up by the provider-stripped name, since completion() strips a2a/ before dispatch, and treats the empty header dict completion() hands it as "no caller headers". Alternative: re-add the prefix inside _complete_a2a, which hides the contract, or have the proxy's a2a_routing inject api_key and headers, which duplicates the lookup
  12. The chat route mints the token synchronously inside the request, the way the azure_ai provider does; the credential caches the token in memory, so only the first call reaches Entra. Alternative: thread an async mint through _complete_a2a, a wider change to the sync and async dispatch
  13. aget_agent_card now sends the agent's extra headers on the card fetch, because Foundry wants the bearer on the card too. Alternative: keep the card fetch unauthenticated, which 401s on Foundry
  14. An absolute URL in agent_card_path is joined onto the base as a relative path by the SDK (it becomes {base}/https://...); documented rather than special-cased, since no known agent needs it
  15. No new token cache: the Entra path reuses the azure_ai provider's cached credential provider
  16. Every A2A message the chat route builds carries "kind": "message", the 0.3 discriminator Foundry requires. Alternative: add it only when the card names Foundry, but the a2a-sdk sends it on every message too, so the relay already did
  17. A streaming chat completion to an agent whose registered card carries a capabilities block with no truthy streaming runs as a blocking message/send and is replayed as one chunk, with usage estimated the same way the non-streaming route does; a card with no capabilities block at all keeps message/stream, so config.yaml agents registered before cards carried capabilities behave as before. The block is read the way the A2A spec and the a2a-sdk client read it, absent meaning false, because POST /v1/agents stores the upstream card through merge_agent_card, which keeps only truthy capabilities and turns Foundry's streaming: false into an empty block: the first cut required a literal false and only ever fired for hand-written config.yaml agents. Alternatives: treat a missing block as no streaming too, the spec's default, which changes every existing agent; keep streaming: false on the stored card, which changes the card the proxy serves for every agent registered through the API; retry with message/send after a -32004 error, which costs a failed round trip on every call; always message/send, which drops streaming for agents that support it
  18. A JSON-RPC error object inside a stream fails the call with an HTTP 500 that carries the agent's message. Alternative: keep swallowing it as an empty chunk, which is how Foundry's This operation is not supported turned into an empty stream
  19. The relay's minted bearer is added inside the one forwarded-header build, last, so the override in decision 7 has a single place. The MappingProxyType returns in the Entra helpers and the _card_http_kwargs helper come from the repo's mutability lint, not from behavior
  20. A client header the admin forwards as authorization, through extra_headers or the x-a2a-<agent>-authorization convention, is dropped whatever its casing when the relay mints a bearer, so the backend gets one Authorization line. Alternative: override on the exact name only, where authorization and Authorization both go out and the backend picks one
  21. A2AAgentCardDiscoveryError carries the first non-404 probe status, else 404, so the relay's HTTP status tells an auth failure (Foundry's 401 on the well-known paths) from a missing card. Alternatives: the last probe's status, which the a2a-sdk's AgentCardResolutionError reported at the base and which hid the 401 behind the final 404; always 404, which e243237 did and made every discovery failure look like a missing agent
  22. The relay gets the hop's Entra bearer from one helper under llms/a2a, resolve_a2a_hop_auth_header, with the mint injected so its tests need no monkeypatch; the endpoint keeps only the databricks_oauth check it already had. Alternatives: branch on the Azure helpers inside the endpoint, which the first cut did and the repo's provider-code rule flags; move the Databricks check under llms/ too, which pulls pre-existing proxy code into the PR
  23. The chat route copies the registered agent's headers before use and the header build returns a new dict instead of writing into its input, so the bearer one caller supplies (an explicit api_key or the Entra token minted for that request) is written to that request alone. Alternative: keep handing the stored dict through, which wrote each caller's bearer onto the shared in-memory agent, where the next caller with no key of their own sent it (Bugbot's finding on c2f77fd, pinned by the two-caller test in tests/test_litellm/test_a2a_registry_lookup.py)
  24. On the chat route the Entra fields own the bearer over a static credential stored next to them: a stored api_key is ignored and a stored authorization header is dropped in any casing when the agent authenticates with Entra, which is what decisions 7, 8 and 20 already did on the relay; a bridge agent (custom_llm_provider set) keeps its stored values (commit 1c40e60). Alternatives: let the stored api_key win, which 4669041 did and which sent Foundry the static key the Entra fields were configured to replace (the legacy provider's behavior the ticket asks not to inherit); an api_key passed on the request itself still wins, the lookup's existing contract, so a caller who supplies a bearer gets the bearer they supplied
  25. Discovery reads the failed probes' status codes through a runtime_checkable Protocol (_HasStatusCode, an object with a status_code attribute) instead of isinstance(error, AgentCardResolutionError) (commit d4b5f05). The SDK class is imported inside try/except ImportError so basedpyright reports it possibly unbound and never narrows error through it, which put three reportAttributeAccessIssue hits on the new hunk and pushed the rule over the budget main carries; the resolver raises only AgentCardResolutionError, so the Protocol matches the same errors. Alternatives: keep the class check and suppress the three hits (a suppression per access on a hunk that is otherwise clean); getattr(error, "status_code", None), which types the value as Any and moves the cost to reportAny; import the class unconditionally, which makes the module fail to import without the a2a-sdk installed, where today only the resolver call fails
  26. An agent whose azure_ad_token is an oidc/ federated token is exchanged only with the tenant_id and client_id stored on that agent, and without both the call fails with the credential help, which now names the requirement (commit e5744c5, from bugbot's review of d4b5f05). The shared OIDC helper fills a missing id from the host's AZURE_TENANT_ID and AZURE_CLIENT_ID, so the previous tip could exchange the host's federated token for the host's identity at that agent's URL, the process-wide fallback decision 7 rules out. Alternatives: pass the ids through and document the fallback (the legacy provider's behavior the ticket asks not to inherit, and the docs already promise the host variables are never read for an agent); treat an oidc/ token without ids as a static bearer (sends the literal oidc/... string to Foundry, a 401 with no hint)

Caveats (if any)

Low

  • Agents registered in config.yaml without a capabilities block still get message/stream on streaming chat completions, the tail of decision 17
    • On Foundry that is now HTTP 500 This operation is not supported. Through the proxy the merge base never got that far (HTTP 401, no bearer is sent), and with a bearer supplied on the SDK path it answered an empty stream, so the only path this changes was already unusable and now names its cause
    • The documented Foundry entry carries capabilities.streaming: false, and agents registered through POST /v1/agents always carry the block; left as is because the fix (a missing block meaning no streaming) turns every existing config.yaml agent that streams today into a fake stream
  • The SDK path outside the proxy (litellm.completion(model="a2a/...", api_base=..., api_key=..., stream=True)) has no registry and so no card, and sends message/stream
    • On Foundry that is now the This operation is not supported error instead of an empty stream; stream=False works; left as is for the same reason, a retry as a blocking send on that error would add a hidden second request to every agent's failure path
  • Managed identity and DefaultAzureCredential are not wired for agents
    • A service principal (what the ticket asks for), username and password, OIDC token, or static bearer are; a managed identity leg cannot run outside Azure, so it is not claimed
  • The relay discovers the card on every request, so no agent_card_path costs two 404 probes per call, on top of the per-request discovery the relay already did
    • agent_card_path: agentCard/v1.0 skips them; a card cache is a follow-up
  • A 0.3 message/send through the relay answers in the 1.0 task shape (result.task), pre-existing
  • The served card rewrites only the first supportedInterfaces url to the proxy, pre-existing
  • A fake-streamed chat completion is logged as stream: true, the shape the client asked for, so spend rows do not say the backend call was blocking
  • The Responses API agent_reference route is unchanged and tracked separately
  • Docs for agent_card_path, the Entra fields, and the blocking fallback: docs(a2a): register Microsoft Foundry agents over A2A with Entra credentials litellm-docs#1508
  • User Flow Before step 8, the HTTP 400 Invalid 'assistant_id' from the legacy azure_ai/agents/<agent> model, is not in the posted legs: through the proxy the QA service principal gets HTTP 401 from the legacy threads API before the assistant id is checked, and the 400 was reproduced once on the SDK path with a user identity; the PR does not touch that path
  • CircleCI at e5744c5 is red on four non-required build_and_test jobs (litellm_utils_testing, pass_through_unit_testing, logging_testing, ocr_testing), each red on main's own pipeline 89764 and fixed on main since (fix(proxy): forward every method on the typesafe pass-through route #41723, test(logging): add azure_spillover to the GCS pub/sub spend-log golden #41729 and the OCR changes in fix(ocr): keep a downloaded document inlined when callbacks intercept the request (#41550 follow-up) #41719 and refactor(rust): isolate legacy callback contract #41752; main's 89780 is 44 of 44 green), so they turn green on the merge commit; merging main into the branch first would only reset the bot reviews and the per-commit QA

Live PR risk

Base 16bbff6 and the tip ran through the same legs in the same order, the tip at 4669041, again at 1c40e60, again at d4b5f05 and again at e5744c5 (18 leg headers on each re-run, every status line the same as at 4669041), each as one proxy with 2 uvicorn workers and no database, started from its own worktree with PYTHONPATH set to it, on the same config (three Foundry agents: one with agent_card_path and a capabilities block, one without agent_card_path, one without capabilities), driven through the same legs in the same order. The SDK legs (litellm.completion with and without stream, and aget_agent_card) ran on the base tree and on all four tips too, each printing the litellm it imported. The first run at e5744c5 hit a DNS outage on the QA machine mid-chain (the proxy answered nodename nor servname provided for the Foundry host and the cost map fetch failed the same way), so that run was discarded and the whole chain, base legs excluded, ran again from a fresh proxy once names resolved; every posted leg at this tip is from that second run

Merge ref: main has moved since the merge base and touched litellm/main.py and litellm/llms/azure_ai/common_utils.py, so the legs ran again on local merges of each tip into main, all clean: 4669041 into 356b8d4 (the risk legs, the Proof of Fix legs and the SDK legs), 1c40e60 into 57d41bd (the same legs plus the mixed-credential section; that merge is now on the branch as c29d1c2) d4b5f05 into 03ba563 and e5744c5 into d42f448 (the same legs, never pushed). Every status line matched the tip's on every merged tree, the stored headers stayed as configured, and the PR's mapped unit test files pass on each, 165 tests on the last

Breaking: none observed. Every relay route (message/send in the 0.3 and 1.0 shapes, message/stream, tasks/get, the served card, /v1/agents), chat completions with and without stream, and discovery with and without agent_card_path answer at the tip as the User Flow says, and the base fails them the way the Before section shows

Backward incompatible:

  • aget_agent_card (also exported as litellm.a2a_protocol.aget_agent_card and used by litellm/a2a_protocol/client.py) raises A2AAgentCardDiscoveryError, a litellm A2AError, with the first non-404 probe status, where the base let the a2a-sdk's AgentCardResolutionError for the last probe escape. Observed with the same call on both trees: base a2a.client.errors.AgentCardResolutionError status_code=401 naming only /.well-known/agent.json, tip litellm.a2a_protocol.exceptions.A2AAgentCardDiscoveryError status_code=401 naming all three probes
  • Every chat-completion message/send now carries "kind": "message" and configuration.blocking: true (decisions 10 and 16); the a2a-sdk sends the same kind on the relay, and a blocking send is what an agent that answers inline already did
  • A registered card whose capabilities block has no truthy streaming turns a streaming chat completion into one blocking chunk (decision 17); a JSON-RPC error inside a stream raises instead of yielding an empty chunk (decision 18); a minted bearer replaces a forwarded client authorization in any casing (decision 20)

Regression risk, reachable but not driven live at the tip:

  • Agents registered through POST /v1/agents, the DB-backed path where merge_agent_card stores the upstream card, were not driven live; the shape it produces is what decision 17 was written for and is pinned in tests/test_litellm/proxy/agent_endpoints/test_a2a_endpoints.py
  • Agents with databricks_oauth, and the relay's static-header forwarding for agents without Entra fields, are unit-tested in the same file, not driven live
  • The Responses API agent_reference route is untouched by the diff and was not driven

Dependency graph, every dependent of a changed symbol and how it was covered:

  • aget_agent_card, LiteLLMA2ACardResolver, A2AAgentCardDiscoveryError: the relay's per-request discovery (live, the relay legs), the SDK client in a2a_protocol/client.py (live, the card fetch above), POST /v1/agents registration (unit tests). No dashboard reference: the two agent_card_path hits in ui/litellm-dashboard/src/components/public_model_hub.tsx are comments about the a2a-sdk defaults
  • agent_card_path litellm_param (AGENT_CARD_PATH_PARAM): read by a2a_protocol/main.py and the completion bridge handler; a new key with no prior meaning
  • A2AConfig.resolve_agent_config_from_registry, transform_request, A2AModelResponseIterator.chunk_parser: litellm.main._complete_a2a for every a2a/ chat completion (live, the chat legs with and without stream, on all three agents) and litellm_completion_bridge/handler.py (unit tests in test_completion_bridge_streaming.py)
  • A2AConfig.validate_environment and resolve_agent_config_from_registry after 4669041, which got its own graph walk: the header build now returns a new dict and the registry lookup copies the stored headers. Each has one A2A caller (llm_http_handler.py for the header build, litellm.main._complete_a2a for the lookup) and both use the return value, so nothing relied on the old in-place write; every chat leg drives both, and the stored-headers section in Proof of Fix shows the effect
  • a2a_hop_uses_entra, _agent_authenticates_with_entra, _registry_api_key and _registry_headers after 1c40e60, which got its own graph walk: the first is read by resolve_a2a_hop_auth_header alone, whose one caller is the relay's _resolve_backend_auth_header (live, every relay leg); the other three are read by resolve_agent_config_from_registry alone, whose one caller is litellm.main._complete_a2a (live, every chat leg and the mixed-credential section of Proof of Fix). The stored dict is copied, not written, which the stored-headers section shows. No string dispatch, subclass, dashboard or config-key reference reaches any of the four, and the precedence of an api_key passed on the request is unchanged (the parametrized test in tests/test_litellm/test_a2a_registry_lookup.py pins all three agent shapes)
  • _HasStatusCode after d4b5f05, which got its own graph walk: a module-private Protocol read by _discovery_status_code alone, whose one caller is _get_agent_card_from_first_reachable_path, so the only dependent path is discovery's status choice when every probe fails, driven live by the relay legs without agent_card_path (two 404 probes before the third path answers) and by the SDK card fetch that gets a 401 on both trees; tests/test_litellm/a2a_protocol/test_card_resolver.py pins the first non-404 status and the all-404 case. No other module referenced AgentCardResolutionError, so dropping its import changes no other path
  • get_azure_ai_agent_entra_token after e5744c5, the last commit, which got its own graph walk: two callers, resolve_azure_ai_agent_auth_header (the relay's default resolve_entra_header, live on every Entra relay leg) and A2AConfig on the chat route (live on every Entra chat leg), and both reach the changed lines only for an agent whose azure_ad_token starts with oidc/, which no QA agent does, so every posted leg still takes the service principal branch and its status lines are unchanged at this tip; the OIDC branch is pinned by the two new tests in tests/test_litellm/llms/azure_ai/test_azure_ai_entra_auth.py (host ids set and never used; agent ids and the Foundry scope reach the exchange). AZURE_ENTRA_CREDENTIAL_HELP has no other reader
  • _forwarding_headers and _resolve_backend_auth_header in a2a_endpoints.py, plus resolve_a2a_hop_auth_header (added in c2f77fd to litellm/llms/a2a/common_utils.py, with its own graph walk: one caller, the relay's header build; its module now imports llms/azure_ai/common_utils at load, which proxy/utils.py, the chat transformation, the guardrail handler, the streaming iterator and the AgentCore transformation already reached through the chat transformation): every relay request (live, the relay legs); the casing override is pinned in test_a2a_endpoints.py and the bridge-agent exclusion in tests/test_litellm/llms/a2a/test_common_utils.py
  • get_azure_ai_agent_entra_token, has_azure_entra_params, AZURE_ENTRA_LITELLM_PARAM_KEYS in azure_ai/common_utils.py: additions only, read by the chat transformation and, through resolve_a2a_hop_auth_header, the relay; the azure_ai provider's own Entra path is unchanged
  • Masked values a writer could get back: GET /v1/agents shows client_secret as REDACTED_BY_LITELM on both trees and never carries the raw secret (checked by value on four reads per side; tenant_id and client_id are shown, they are identifiers). The agent writers restore the stored value when that marker comes back (_restore_redacted_litellm_params in agent_registry.py); the masking and the restore both pre-date the PR, which does not touch that file, and work on key names, so they cover the new fields. Traced, not driven: the writers need a database
  • Legacy suites found by the provider string: tests/agent_tests/test_a2a_agent.py (updated in this PR) ran green in the agent_testing job at the tip; tests/llm_translation/test_a2a.py skips itself when no local agent listens (4 skipped at the tip, the same as on main); the local-only suites under tests/agent_tests/local_only_agent_tests/ need a LangGraph server and an AgentCore runtime and were not run. The workflow's reds at the tip are classified below
  • Docs: docs(a2a): register Microsoft Foundry agents over A2A with Entra credentials litellm-docs#1508

Not verified: the DB-registered agent path, databricks_oauth agents, the Responses agent_reference route, managed identity / DefaultAzureCredential (not wired, decision 4), and the local-only a2a suites that need a LangGraph server and an AgentCore runtime

CircleCI at 1c40e60 (pipeline 89752, workflow 88a135df) and at 4669041 (pipeline 89742): the integration workflow is green and build_and_test is red on the same eight jobs, on the same tests. None is caused by this PR: each failing test was fixed on main after the merge base 16bbff6, and every fix is an ancestor of 57d41bd, the main merged into the branch as c29d1c2

CircleCI at d4b5f05 (pipeline 89767, workflow dbf1742b): the integration workflow is green (7 of 7 jobs) and build_and_test is red on four of 44 jobs, none caused by this PR: each of them is red on main's own pipeline 89764 (03ba563) and reproduces on a pure checkout of main

  • litellm_utils_testing (test_models_by_provider, expects a typesafe provider key) and pass_through_unit_testing (test_pass_through_routes_support_all_methods, expects /typesafe/{endpoint:path} to take every HTTP method): both fail the same way on main's own pipeline 89764 and on a local run of the newest main 03ba563 merged with the tip
  • ocr_testing (test_basic_ocr_with_url[True] and [False], invalid OCR document data URI): fails the same way on main's pipeline 89764
  • logging_testing (test_async_gcs_pub_sub_v1, Extra key in actual: metadata.azure_spillover): reproduces with the identical message on a pure checkout of main 02a20fe and on 03ba563 merged with the tip; the key comes from main's 7b855bd, which updated the spend-log golden but not the GCS Pub/Sub one, and the PR touches nothing under logging

agent_testing, the job that runs this PR's A2A suite, is green at d4b5f05. None of the red jobs is in main's ruleset required_status_checks, and the 28 required GitHub Actions checks, osv-scan included, all pass at d4b5f05

CircleCI at e5744c5 (pipeline 89774, workflow cd7223c1, the tip): the integration workflow is green (7 of 7 jobs) and build_and_test is red on the same four of 44 jobs as at d4b5f05, none caused by this PR: each is red on main's own pipeline 89764 (03ba563) and green on main's pipeline 89780 (44 of 44, 06:09Z), because main fixed every one of them after the last main merged into this branch (57d41bd, on the branch as c29d1c2)

agent_testing, the job that runs this PR's A2A suite, is green at the tip. None of the red jobs is in main's ruleset required_status_checks, and the 28 required GitHub Actions checks, osv-scan included, all pass at the tip. Merging main into the branch only to turn CircleCI green was weighed and skipped: it resets the bot reviews and the per-commit QA without changing what ships, and the four tests already pass on the local merge of the tip into main

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

Note

High Risk
Touches proxy authentication, credential handling, and core A2A/chat request shaping; behavior changes include discovery errors, blocking sends, streaming fallback, and what gets logged.

Overview
Adds Microsoft Foundry–compatible A2A support: agent cards can be fetched via optional agent_card_path or a new /agentCard/v1.0 probe (after the two well-known paths), with A2AAgentCardDiscoveryError reporting every failed attempt and a sensible HTTP status.

Entra ID for A2A hops is minted from each agent’s own litellm_params (default scope https://ai.azure.com/.default), forwarded on relay routes (message/*, tasks/*) and chat completions, without using host AZURE_* env vars or leaking secrets into streaming logs. Completion-bridge agents (custom_llm_provider set) skip hop-level Entra minting; minted Authorization wins over forwarded authorization headers.

Chat completions over a2a/ now resolve registry config by stripped agent name, apply registered api_key/headers/Entra, send kind: message, use blocking message/send for non-streaming, and fake-stream when the card’s capabilities omit truthy streaming. JSON-RPC errors in A2A streams surface as A2AError instead of empty output. Header/registry handling avoids mutating shared agent header dicts across callers.

Reviewed by Cursor Bugbot for commit e5744c5. Bugbot is set up for automated code reviews on this repo. Configure here.

…ed card discovery

Foundry serves its agent card only at agentCard/v1.0, accepts only an Entra ID
bearer, and defaults to a non-blocking send, so the A2A relay and the chat
completions route could not use it.

The relay gains an agent_card_path litellm_param plus agentCard/v1.0 as a third
discovery probe, mints a bearer from flat Entra fields on the agent
(tenant_id, client_id, client_secret, azure_ad_token, azure_username,
azure_password, azure_scope) for https://ai.azure.com/.default, and sends it on
the card fetch, message/send, message/stream, tasks/* and the chat bridge.

Chat completions look the registered agent up by its provider-stripped name so
its api_key and headers reach the request, tag every message with its kind, ask
for a blocking send, fall back to a blocking send when the registered card says
streaming: false, and fail the call on a JSON-RPC error inside a stream instead
of yielding an empty one. Entra fields stay out of the chat bridge's logged
parameters.

Resolves LIT-5122
@devin-ai-integration

devin-ai-integration Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@codspeed

codspeed Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_foundry_a2a_entra_agents (e5744c5) with main (c25c098)

Open in CodSpeed

@greptile-apps

greptile-apps Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

The PR appears safe to merge; all previous findings are resolved or withdrawn, and no actionable new regression remains.

Summary

Adds Microsoft Foundry support to the A2A relay and chat-completion integration.

  • Adds versioned agent-card discovery and explicit agent_card_path handling.
  • Mints agent-scoped Entra credentials and forwards them without mutating shared registered headers.
  • Adds blocking and non-streaming fallback behavior for compatible chat-completion responses.
  • Preserves upstream discovery status information and surfaces JSON-RPC stream errors.
  • The latest change prevents incomplete OIDC agent configurations from borrowing process-wide Azure tenant or client identity.

Reviews (7) · Last reviewed commit: "fix(azure_ai): mint an oidc Entra token ..."

Comment thread litellm/a2a_protocol/card_resolver.py Outdated
Comment thread litellm/a2a_protocol/card_resolver.py
Comment thread litellm/proxy/agent_endpoints/a2a_endpoints.py Outdated
Comment thread tests/test_litellm/a2a_protocol/test_card_resolver.py
@codecov

codecov Bot commented Sep 16, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.23077% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/llms/azure_ai/common_utils.py 97.29% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ mateo-berri
❌ devin-ai-integration[bot]
You have signed the CLA already but the status is still pending? Let us recheck it.

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Fixed the base-class patching and the narrative docstrings. Rebutted the 404 status change and the provider placement findings with evidence in their threads

…ect the card client in tests

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Pushed 75eec87: discovery errors keep the upstream status, agent_tests mock accepts relative_card_path, added an all-404 regression test. Placement finding rebutted above

@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

Comment thread litellm/llms/a2a/chat/transformation.py
Comment thread litellm/proxy/agent_endpoints/a2a_endpoints.py Outdated
@mateo-berri mateo-berri added run-ci and removed run-ci labels Sep 17, 2026
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread litellm/llms/a2a/chat/transformation.py
…reaches the next

The chat route handed the registry's stored headers dict straight to validate_environment, which wrote the caller's bearer into it, so the next caller of the same agent with no key of their own sent the previous caller's token. The registry lookup now copies the stored headers and validate_environment returns a new dict instead of mutating its input. A regression test drives two completions through one registered agent and asserts the second carries no Authorization and the stored agent is unchanged.
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

@mateo-berri mateo-berri added run-ci and removed run-ci labels Sep 17, 2026
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@mateo-berri mateo-berri added run-ci and removed run-ci labels Sep 17, 2026

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

basedpyright cannot narrow the probe error through
isinstance(error, AgentCardResolutionError) while that class is imported
inside try/except ImportError, which left three reportAttributeAccessIssue
errors over the budget main now carries. A runtime-checkable Protocol with
the same status_code contract carries the narrowing instead, so the check
no longer depends on the possibly unbound SDK name and the import goes.
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-berri mateo-berri added run-ci and removed run-ci labels Sep 18, 2026
@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread litellm/llms/azure_ai/common_utils.py
The OIDC branch of the agent token mint handed a missing tenant_id or
client_id to the shared helper, which fills them from the host's
AZURE_TENANT_ID and AZURE_CLIENT_ID, so an agent carrying only an
oidc/ token could be authenticated with the host's identity. The
branch now needs both ids on the agent and otherwise fails with the
credential help, which names the requirement
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-berri mateo-berri added run-ci and removed run-ci labels Sep 18, 2026
@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit e5744c5. Configure here.

@mateo-berri mateo-berri left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@mateo-berri
mateo-berri merged commit 88c9dd1 into main Sep 18, 2026
137 of 142 checks passed
@mateo-berri
mateo-berri deleted the litellm_foundry_a2a_entra_agents branch September 18, 2026 18:44
@mateo-berri mateo-berri mentioned this pull request Sep 18, 2026
6 tasks done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants