Skip to content

LiteLLM and A2A ​

Two ways for another AI system to call a Console agent without custom code: through a LiteLLM proxy, which exposes Console's OpenAI-compatible endpoint under your own model names, and through A2A (Agent2Agent), the JSON-RPC protocol that agent frameworks use to talk to each other.

LiteLLM proxyA2A
Use it whenA central LLM proxy already fronts your teams, and agents should look like one more model there.The caller is an agent framework that discovers agents through an agent card and sends tasks.
Wire formatOpenAI chat completions / ResponsesJSON-RPC 2.0 over HTTP, with SSE for streaming
DiscoveryGET /models lists agent:<key>Agent card at /.well-known/agent-card.json

LiteLLM proxy ​

Console speaks the OpenAI protocol, so LiteLLM treats it as an openai/ provider with a custom api_base. The model name is the Console id, agent:<key>.

yaml
# config.yaml
model_list:
  - model_name: support-bot                  # the name your users call on the proxy
    litellm_params:
      model: openai/agent:support-bot        # "openai/" selects the provider; the rest is sent as `model`
      api_base: https://console.example.com/api/client/v1
      api_key: os.environ/CONSOLE_API_KEY    # a cpeer_ token with the agents service

  - model_name: report-writer
    litellm_params:
      model: openai/agent:report-writer
      api_base: https://console.example.com/api/client/v1
      api_key: os.environ/CONSOLE_API_KEY
      timeout: 900                           # seconds; agents can take longer than a plain completion

litellm_settings:
  drop_params: true                          # LiteLLM may add params an agent ignores; this keeps requests tidy
bash
export CONSOLE_API_KEY="cpeer_..."
litellm --config config.yaml

Call it like any model on the proxy:

bash
curl http://localhost:4000/v1/chat/completions \
  -H "Authorization: Bearer $LITELLM_KEY" -H "Content-Type: application/json" \
  -d '{ "model": "support-bot", "messages": [{ "role": "user", "content": "Where is order 1234?" }] }'

Notes:

  • Declare agents explicitly, as above, instead of relying on model discovery. Console lists both models and agents on /models; an explicit model_list keeps your proxy namespace deliberate.
  • The agent keeps its own instructions, tools and knowledge. temperature, top_p, max_tokens, tools and system messages sent through the proxy are ignored (listed in X-Cognipeer-Ignored-Params).
  • Streaming works: set stream: true on the proxy call. A closed client connection cancels the run.
  • Sessions: the proxy resends the history, and Console's transcript match continues the same session (how). To pin one, forward the header with LiteLLM's extra_headers/headers per request: X-Cognipeer-Session-Id.
  • Structured output passes through: response_format with a json_schema on the proxy call reaches the agent, and the content is the JSON string.
  • Extra Console fields (variables, service_tier: "flex", metadata, version) can be sent as extra_body on the proxy call.
  • The Responses route (/v1/responses) works the same way for LiteLLM versions that proxy it; a 202 (turn still running after the wait window) is returned as is.
  • Each turn is recorded in Console as a run (channel: chat_completions), visible in Sessions and Tracing, whatever the proxy logs on its side.

A2A ​

Console publishes an agent over A2A when you switch on Publish via A2A in the agent's Publish tab (how). The token endpoint is per agent:

URL
Agent cardGET /api/client/v1/a2a/{agentKey}/.well-known/agent-card.json
JSON-RPCPOST /api/client/v1/a2a/{agentKey}

Authenticate with Authorization: Bearer cpeer_…. (An agent can also be published on an unauthenticated public link; that route is always blocking.)

The agent card ​

bash
curl -s "$CONSOLE_URL/api/client/v1/a2a/support-bot/.well-known/agent-card.json" \
  -H "Authorization: Bearer $COGNIPEER_API_TOKEN" | jq '{name, url, capabilities, defaultInputModes, defaultOutputModes}'

The card advertises capabilities.streaming: true, input modes text/plain and application/json (plus file MIME types when the agent accepts attachments), and output modes text/plain and application/json. Register this URL in the framework that will call the agent.

message/send ​

bash
curl -s -X POST "$CONSOLE_URL/api/client/v1/a2a/support-bot" \
  -H "Authorization: Bearer $COGNIPEER_API_TOKEN" -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0", "id": 1, "method": "message/send",
    "params": {
      "message": {
        "role": "user", "messageId": "m-1",
        "parts": [{ "kind": "text", "text": "Where is order 1234?" }]
      }
    }
  }'

The answer is a Task: status.state is completed (the answer is in artifacts[0].parts[0].text), or input-required when the agent paused for an approval or a question (metadata.pendingActions), canceled when the run was stopped while the call waited, or failed when the run failed after it started (the status message holds the error and metadata has errorType, errorCode and the error's other fields, for example the agent quota refusal). A refusal before the run exists (a running turn on the session, a concurrency or spend limit) is a JSON-RPC error instead, and a session the request created for it is removed again. Pass message.contextId (the contextId of an earlier task) to continue the same conversation.

A blocking call that is still running when the wait window ends returns a Task in state working: poll it with tasks/get. A client that drops the connection does not cancel the task.

Non-blocking: blocking: false and tasks/get ​

Add configuration.blocking: false and Console queues the run and answers at once with a Task in submitted (or working). The task id is the run id (run_…). (Authenticated route only.)

bash
# Start
curl -s -X POST "$CONSOLE_URL/api/client/v1/a2a/report-writer" \
  -H "Authorization: Bearer $COGNIPEER_API_TOKEN" -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0", "id": 1, "method": "message/send",
    "params": {
      "message": { "role": "user", "messageId": "m-2", "parts": [{ "kind": "text", "text": "Write the weekly report" }] },
      "configuration": { "blocking": false }
    }
  }' | jq '.result | {id, contextId, state: .status.state}'

# Poll
curl -s -X POST "$CONSOLE_URL/api/client/v1/a2a/report-writer" \
  -H "Authorization: Bearer $COGNIPEER_API_TOKEN" -H "Content-Type: application/json" \
  -d '{ "jsonrpc": "2.0", "id": 2, "method": "tasks/get", "params": { "id": "run_65f1c0a2b9d3e4f5a6b7c8d9" } }' \
  | jq '.result.status.state'

message/stream ​

Server-Sent Events, one JSON-RPC response per event: first the Task (submitted), then TaskStatusUpdateEvents (working) and TaskArtifactUpdateEvents (the first text chunk creates the artifact with append: false, later chunks append, an empty last chunk has lastChunk: true), and a final status event with final: true and one of completed, input-required, failed, canceled.

A dropped connection does not cancel the task. It keeps running, and tasks/resubscribe reattaches to it. The task id is the first event, so you have it from the start.

bash
curl -N -X POST "$CONSOLE_URL/api/client/v1/a2a/support-bot" \
  -H "Authorization: Bearer $COGNIPEER_API_TOKEN" -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0", "id": 3, "method": "message/stream",
    "params": { "message": { "role": "user", "messageId": "m-3", "parts": [{ "kind": "text", "text": "Draft a status update" }] } }
  }'

tasks/resubscribe ​

bash
curl -N -X POST "$CONSOLE_URL/api/client/v1/a2a/support-bot" \
  -H "Authorization: Bearer $COGNIPEER_API_TOKEN" -H "Content-Type: application/json" \
  -d '{ "jsonrpc": "2.0", "id": 4, "method": "tasks/resubscribe", "params": { "id": "run_65f1c0a2b9d3e4f5a6b7c8d9" } }'

The stream starts with the task as it is now, then the status and artifact updates that follow, up to the final status event. For a task that has already finished, or one waiting on input (input-required), you get the Task and its final status event at once. One resubscribe stream follows a task for up to 60 minutes; if the task is still running then, the stream closes without a final event, so resubscribe again. Use it after a lost connection instead of starting the turn again.

Input parts and output artifacts ​

PartInputOutput
textThe user message.The answer, as a text artifact.
filefile.bytes (base64) or file.uri (http or https, fetched safely by Console) becomes a session file. The agent needs attachments enabled.Files the agent created appear as file artifacts with a download uri.
dataJSON. When its keys match the agent's declared variables they are used as variables (checked before the model runs); otherwise it is appended to the message as a JSON block.A data artifact with the validated object when the agent has structured output.
json
{ "kind": "file", "file": { "name": "contract.pdf", "mimeType": "application/pdf", "uri": "https://files.example.com/contract.pdf" } }

Cancel and errors ​

tasks/cancel with { "id": "run_…" } stops the task with the same semantics as every other cancel: a queued task is cancelled at once; a running one at its next checkpoint, and a task waiting on a person is settled immediately.

A JSON-RPC error is an HTTP 200 with an error object, as everywhere in A2A: -32602 invalid params (including an unknown contextId), -32001 task not found, -32601 unknown method, -32002 the task cannot be canceled (TaskNotCancelable: it finished or is not running; it was -32004 before), -32010 the caller may not do this to a task that waits on a person. Old task ids (task_<conversation>_<n>) still resolve on the token route. A remote file uri that does not arrive within the outbound timeout is -32602 (attachment_unreachable).

The public link. On /api/public/a2a/<tenantId>/<slug> every caller is one anonymous identity, so a task and its conversation are bound to the caller that created them by a random nonce. The first reply carries id: run_<runId>.<nonce> and contextId: <sessionId>.<nonce>; send them back whole in tasks/get, tasks/cancel, tasks/resubscribe and message.contextId. An id without the right nonce (or an old task_… id) answers -32001, and a contextId without it answers -32602 (unknown contextId). A public conversation started before the nonce existed can no longer be reached from the public link. The variables of a public caller's data part are treated as external content: they reach the run in the message and never in the system prompt.

Studio · Pulse · Console · Agent SDK and more — the Cognipeer documentation hub