mirror of
https://github.com/BerriAI/litellm.git
synced 2025-12-06 11:33:26 +08:00
[Docs] Agent Gateway (#17454)
* init litellm A2a client * simpler a2a client interface * test a2a * move a2a invoking tests * test fix * ensure a2a send message is tracked n logs * rename tags * add streaming handlng * add a2a invocation * add a2a invocation i cost calc * test_a2a_logging_payload * update invoke_agent_a2a * test_invoke_agent_a2a_adds_litellm_data * add A2a agent * fix endpoints on A2a * UI allow testing a2a endpoints * add agent imgs * add a2a as an endpoint * add a2a * docs a2a invoke * docs a2a * docs A2a invoke
This commit is contained in:
198
docs/my-website/docs/a2a.md
Normal file
198
docs/my-website/docs/a2a.md
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
import Tabs from '@theme/Tabs';
|
||||||
|
import TabItem from '@theme/TabItem';
|
||||||
|
import Image from '@theme/IdealImage';
|
||||||
|
|
||||||
|
# /a2a - Agent Gateway (A2A Protocol)
|
||||||
|
|
||||||
|
| Feature | Supported |
|
||||||
|
|---------|-----------|
|
||||||
|
| Logging | ✅ |
|
||||||
|
| Load Balancing | ✅ |
|
||||||
|
| Streaming | ✅ |
|
||||||
|
|
||||||
|
:::tip
|
||||||
|
|
||||||
|
LiteLLM follows the [A2A (Agent-to-Agent) Protocol](https://github.com/google/A2A) for invoking agents.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Adding your Agent
|
||||||
|
|
||||||
|
You can add A2A-compatible agents through the LiteLLM Admin UI.
|
||||||
|
|
||||||
|
1. Navigate to the **Agents** tab
|
||||||
|
2. Click **Add Agent**
|
||||||
|
3. Enter the agent name (e.g., `ij-local`) and the URL of your A2A agent
|
||||||
|
|
||||||
|
<Image
|
||||||
|
img={require('../img/add_agent_1.png')}
|
||||||
|
style={{width: '80%', display: 'block', margin: '0'}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
The URL should be the invocation URL for your A2A agent (e.g., `http://localhost:10001`).
|
||||||
|
|
||||||
|
## Invoking your Agents
|
||||||
|
|
||||||
|
Use the [A2A Python SDK](https://pypi.org/project/a2a/) to invoke agents through LiteLLM:
|
||||||
|
|
||||||
|
- `base_url`: Your LiteLLM proxy URL + `/a2a/{agent_name}`
|
||||||
|
- `headers`: Include your LiteLLM Virtual Key for authentication
|
||||||
|
|
||||||
|
```python showLineNumbers title="invoke_a2a_agent.py"
|
||||||
|
from uuid import uuid4
|
||||||
|
import httpx
|
||||||
|
import asyncio
|
||||||
|
from a2a.client import A2ACardResolver, A2AClient
|
||||||
|
from a2a.types import MessageSendParams, SendMessageRequest
|
||||||
|
|
||||||
|
# === CONFIGURE THESE ===
|
||||||
|
LITELLM_BASE_URL = "http://localhost:4000" # Your LiteLLM proxy URL
|
||||||
|
LITELLM_VIRTUAL_KEY = "sk-1234" # Your LiteLLM Virtual Key
|
||||||
|
LITELLM_AGENT_NAME = "ij-local" # Agent name registered in LiteLLM
|
||||||
|
# =======================
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
base_url = f"{LITELLM_BASE_URL}/a2a/{LITELLM_AGENT_NAME}"
|
||||||
|
headers = {"Authorization": f"Bearer {LITELLM_VIRTUAL_KEY}"}
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(headers=headers) as httpx_client:
|
||||||
|
# Resolve agent card and create client
|
||||||
|
resolver = A2ACardResolver(httpx_client=httpx_client, base_url=base_url)
|
||||||
|
agent_card = await resolver.get_agent_card()
|
||||||
|
client = A2AClient(httpx_client=httpx_client, agent_card=agent_card)
|
||||||
|
|
||||||
|
# Send a message
|
||||||
|
request = SendMessageRequest(
|
||||||
|
id=str(uuid4()),
|
||||||
|
params=MessageSendParams(
|
||||||
|
message={
|
||||||
|
"role": "user",
|
||||||
|
"parts": [{"kind": "text", "text": "Hello, what can you do?"}],
|
||||||
|
"messageId": uuid4().hex,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
response = await client.send_message(request)
|
||||||
|
print(response.model_dump(mode="json", exclude_none=True))
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
|
```
|
||||||
|
|
||||||
|
### Streaming Responses
|
||||||
|
|
||||||
|
For streaming responses, use `send_message_streaming`:
|
||||||
|
|
||||||
|
```python showLineNumbers title="invoke_a2a_agent_streaming.py"
|
||||||
|
from uuid import uuid4
|
||||||
|
import httpx
|
||||||
|
import asyncio
|
||||||
|
from a2a.client import A2ACardResolver, A2AClient
|
||||||
|
from a2a.types import MessageSendParams, SendStreamingMessageRequest
|
||||||
|
|
||||||
|
# === CONFIGURE THESE ===
|
||||||
|
LITELLM_BASE_URL = "http://localhost:4000" # Your LiteLLM proxy URL
|
||||||
|
LITELLM_VIRTUAL_KEY = "sk-1234" # Your LiteLLM Virtual Key
|
||||||
|
LITELLM_AGENT_NAME = "ij-local" # Agent name registered in LiteLLM
|
||||||
|
# =======================
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
base_url = f"{LITELLM_BASE_URL}/a2a/{LITELLM_AGENT_NAME}"
|
||||||
|
headers = {"Authorization": f"Bearer {LITELLM_VIRTUAL_KEY}"}
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(headers=headers) as httpx_client:
|
||||||
|
# Resolve agent card and create client
|
||||||
|
resolver = A2ACardResolver(httpx_client=httpx_client, base_url=base_url)
|
||||||
|
agent_card = await resolver.get_agent_card()
|
||||||
|
client = A2AClient(httpx_client=httpx_client, agent_card=agent_card)
|
||||||
|
|
||||||
|
# Send a streaming message
|
||||||
|
request = SendStreamingMessageRequest(
|
||||||
|
id=str(uuid4()),
|
||||||
|
params=MessageSendParams(
|
||||||
|
message={
|
||||||
|
"role": "user",
|
||||||
|
"parts": [{"kind": "text", "text": "Hello, what can you do?"}],
|
||||||
|
"messageId": uuid4().hex,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Stream the response
|
||||||
|
async for chunk in client.send_message_streaming(request):
|
||||||
|
print(chunk.model_dump(mode="json", exclude_none=True))
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tracking Agent Logs
|
||||||
|
|
||||||
|
After invoking an agent, you can view the request logs in the LiteLLM **Logs** tab.
|
||||||
|
|
||||||
|
The logs show:
|
||||||
|
- **Request/Response content** sent to and received from the agent
|
||||||
|
- **User, Key, Team** information for tracking who made the request
|
||||||
|
- **Latency and cost** metrics
|
||||||
|
|
||||||
|
<Image
|
||||||
|
img={require('../img/agent2.png')}
|
||||||
|
style={{width: '100%', display: 'block', margin: '2rem auto'}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### Endpoint
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /a2a/{agent_name}/message/send
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
Include your LiteLLM Virtual Key in the `Authorization` header:
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer sk-your-litellm-key
|
||||||
|
```
|
||||||
|
|
||||||
|
### Request Format
|
||||||
|
|
||||||
|
LiteLLM follows the [A2A JSON-RPC 2.0 specification](https://github.com/google/A2A):
|
||||||
|
|
||||||
|
```json title="Request Body"
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "unique-request-id",
|
||||||
|
"method": "message/send",
|
||||||
|
"params": {
|
||||||
|
"message": {
|
||||||
|
"role": "user",
|
||||||
|
"parts": [{"kind": "text", "text": "Your message here"}],
|
||||||
|
"messageId": "unique-message-id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response Format
|
||||||
|
|
||||||
|
```json title="Response"
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "unique-request-id",
|
||||||
|
"result": {
|
||||||
|
"kind": "task",
|
||||||
|
"id": "task-id",
|
||||||
|
"contextId": "context-id",
|
||||||
|
"status": {"state": "completed", "timestamp": "2025-01-01T00:00:00Z"},
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"artifactId": "artifact-id",
|
||||||
|
"name": "response",
|
||||||
|
"parts": [{"kind": "text", "text": "Agent response here"}]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
0
docs/my-website/img/add_agent1.png
Normal file
0
docs/my-website/img/add_agent1.png
Normal file
BIN
docs/my-website/img/add_agent_1.png
Normal file
BIN
docs/my-website/img/add_agent_1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 288 KiB |
BIN
docs/my-website/img/agent2.png
Normal file
BIN
docs/my-website/img/agent2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 806 KiB |
@@ -316,6 +316,7 @@ const sidebars = {
|
|||||||
slug: "/supported_endpoints",
|
slug: "/supported_endpoints",
|
||||||
},
|
},
|
||||||
items: [
|
items: [
|
||||||
|
"a2a",
|
||||||
"assistants",
|
"assistants",
|
||||||
{
|
{
|
||||||
type: "category",
|
type: "category",
|
||||||
|
|||||||
@@ -115,6 +115,16 @@ async def get_agent_card(
|
|||||||
tags=["[beta] A2A Agents"],
|
tags=["[beta] A2A Agents"],
|
||||||
dependencies=[Depends(user_api_key_auth)],
|
dependencies=[Depends(user_api_key_auth)],
|
||||||
)
|
)
|
||||||
|
@router.post(
|
||||||
|
"/a2a/{agent_id}/message/send",
|
||||||
|
tags=["[beta] A2A Agents"],
|
||||||
|
dependencies=[Depends(user_api_key_auth)],
|
||||||
|
)
|
||||||
|
@router.post(
|
||||||
|
"/v1/a2a/{agent_id}/message/send",
|
||||||
|
tags=["[beta] A2A Agents"],
|
||||||
|
dependencies=[Depends(user_api_key_auth)],
|
||||||
|
)
|
||||||
async def invoke_agent_a2a(
|
async def invoke_agent_a2a(
|
||||||
agent_id: str,
|
agent_id: str,
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|||||||
@@ -17,7 +17,8 @@
|
|||||||
"rerank": "Supports /rerank endpoint",
|
"rerank": "Supports /rerank endpoint",
|
||||||
"ocr": "Supports /ocr endpoint",
|
"ocr": "Supports /ocr endpoint",
|
||||||
"search": "Supports /search endpoint",
|
"search": "Supports /search endpoint",
|
||||||
"skills": "Supports /skills endpoint"
|
"skills": "Supports /skills endpoint",
|
||||||
|
"a2a_(Agent Gateway)": "Supports /a2a/{agent}/message/send endpoint (A2A Protocol)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -35,7 +36,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ai21": {
|
"ai21": {
|
||||||
@@ -51,7 +53,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ai21_chat": {
|
"ai21_chat": {
|
||||||
@@ -67,7 +70,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"anthropic": {
|
"anthropic": {
|
||||||
@@ -84,7 +88,8 @@
|
|||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": true,
|
"batches": true,
|
||||||
"rerank": false,
|
"rerank": false,
|
||||||
"skills": true
|
"skills": true,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"anthropic_text": {
|
"anthropic_text": {
|
||||||
@@ -101,7 +106,8 @@
|
|||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": true,
|
"batches": true,
|
||||||
"rerank": false,
|
"rerank": false,
|
||||||
"skills": true
|
"skills": true,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"assemblyai": {
|
"assemblyai": {
|
||||||
@@ -117,7 +123,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"auto_router": {
|
"auto_router": {
|
||||||
@@ -133,7 +140,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bedrock": {
|
"bedrock": {
|
||||||
@@ -149,7 +157,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": true
|
"rerank": true,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sagemaker": {
|
"sagemaker": {
|
||||||
@@ -165,7 +174,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"azure": {
|
"azure": {
|
||||||
@@ -181,7 +191,8 @@
|
|||||||
"audio_speech": true,
|
"audio_speech": true,
|
||||||
"moderations": true,
|
"moderations": true,
|
||||||
"batches": true,
|
"batches": true,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"azure_ai": {
|
"azure_ai": {
|
||||||
@@ -198,7 +209,8 @@
|
|||||||
"moderations": true,
|
"moderations": true,
|
||||||
"batches": true,
|
"batches": true,
|
||||||
"rerank": false,
|
"rerank": false,
|
||||||
"ocr": true
|
"ocr": true,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"azure_ai/doc-intelligence": {
|
"azure_ai/doc-intelligence": {
|
||||||
@@ -231,7 +243,8 @@
|
|||||||
"audio_speech": true,
|
"audio_speech": true,
|
||||||
"moderations": true,
|
"moderations": true,
|
||||||
"batches": true,
|
"batches": true,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"baseten": {
|
"baseten": {
|
||||||
@@ -247,7 +260,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bytez": {
|
"bytez": {
|
||||||
@@ -263,7 +277,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cerebras": {
|
"cerebras": {
|
||||||
@@ -279,7 +294,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"clarifai": {
|
"clarifai": {
|
||||||
@@ -295,7 +311,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cloudflare": {
|
"cloudflare": {
|
||||||
@@ -311,7 +328,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"codestral": {
|
"codestral": {
|
||||||
@@ -327,7 +345,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cohere": {
|
"cohere": {
|
||||||
@@ -343,7 +362,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": true
|
"rerank": true,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cohere_chat": {
|
"cohere_chat": {
|
||||||
@@ -359,7 +379,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cometapi": {
|
"cometapi": {
|
||||||
@@ -375,7 +396,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"compactifai": {
|
"compactifai": {
|
||||||
@@ -391,7 +413,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"custom": {
|
"custom": {
|
||||||
@@ -407,7 +430,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"custom_openai": {
|
"custom_openai": {
|
||||||
@@ -423,7 +447,8 @@
|
|||||||
"audio_speech": true,
|
"audio_speech": true,
|
||||||
"moderations": true,
|
"moderations": true,
|
||||||
"batches": true,
|
"batches": true,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dashscope": {
|
"dashscope": {
|
||||||
@@ -439,7 +464,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"databricks": {
|
"databricks": {
|
||||||
@@ -455,7 +481,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dataforseo": {
|
"dataforseo": {
|
||||||
@@ -488,7 +515,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"deepgram": {
|
"deepgram": {
|
||||||
@@ -504,7 +532,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"deepinfra": {
|
"deepinfra": {
|
||||||
@@ -520,7 +549,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"deepseek": {
|
"deepseek": {
|
||||||
@@ -536,7 +566,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"elevenlabs": {
|
"elevenlabs": {
|
||||||
@@ -552,7 +583,8 @@
|
|||||||
"audio_speech": true,
|
"audio_speech": true,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"exa_ai": {
|
"exa_ai": {
|
||||||
@@ -585,7 +617,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fal_ai": {
|
"fal_ai": {
|
||||||
@@ -601,7 +634,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"featherless_ai": {
|
"featherless_ai": {
|
||||||
@@ -617,7 +651,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fireworks_ai": {
|
"fireworks_ai": {
|
||||||
@@ -633,7 +668,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"firecrawl": {
|
"firecrawl": {
|
||||||
@@ -666,7 +702,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"galadriel": {
|
"galadriel": {
|
||||||
@@ -682,7 +719,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"github_copilot": {
|
"github_copilot": {
|
||||||
@@ -698,7 +736,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"github": {
|
"github": {
|
||||||
@@ -714,7 +753,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vertex_ai": {
|
"vertex_ai": {
|
||||||
@@ -731,7 +771,8 @@
|
|||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false,
|
"rerank": false,
|
||||||
"ocr": true
|
"ocr": true,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vertex_ai/chirp": {
|
"vertex_ai/chirp": {
|
||||||
@@ -763,7 +804,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"gradient_ai": {
|
"gradient_ai": {
|
||||||
@@ -779,7 +821,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"groq": {
|
"groq": {
|
||||||
@@ -795,7 +838,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"heroku": {
|
"heroku": {
|
||||||
@@ -811,7 +855,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"hosted_vllm": {
|
"hosted_vllm": {
|
||||||
@@ -827,7 +872,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"huggingface": {
|
"huggingface": {
|
||||||
@@ -843,7 +889,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": true
|
"rerank": true,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"hyperbolic": {
|
"hyperbolic": {
|
||||||
@@ -859,7 +906,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"watsonx": {
|
"watsonx": {
|
||||||
@@ -875,7 +923,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"infinity": {
|
"infinity": {
|
||||||
@@ -923,7 +972,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lemonade": {
|
"lemonade": {
|
||||||
@@ -939,7 +989,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"litellm_proxy": {
|
"litellm_proxy": {
|
||||||
@@ -955,7 +1006,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"llamafile": {
|
"llamafile": {
|
||||||
@@ -971,7 +1023,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lm_studio": {
|
"lm_studio": {
|
||||||
@@ -987,7 +1040,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"maritalk": {
|
"maritalk": {
|
||||||
@@ -1003,7 +1057,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"meta_llama": {
|
"meta_llama": {
|
||||||
@@ -1019,7 +1074,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mistral": {
|
"mistral": {
|
||||||
@@ -1036,7 +1092,8 @@
|
|||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false,
|
"rerank": false,
|
||||||
"ocr": true
|
"ocr": true,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"moonshot": {
|
"moonshot": {
|
||||||
@@ -1052,7 +1109,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"docker_model_runner": {
|
"docker_model_runner": {
|
||||||
@@ -1068,7 +1126,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"morph": {
|
"morph": {
|
||||||
@@ -1084,7 +1143,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nebius": {
|
"nebius": {
|
||||||
@@ -1100,7 +1160,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nlp_cloud": {
|
"nlp_cloud": {
|
||||||
@@ -1116,7 +1177,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"novita": {
|
"novita": {
|
||||||
@@ -1132,7 +1194,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nscale": {
|
"nscale": {
|
||||||
@@ -1148,7 +1211,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nvidia_nim": {
|
"nvidia_nim": {
|
||||||
@@ -1164,7 +1228,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"oci": {
|
"oci": {
|
||||||
@@ -1180,7 +1245,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ollama": {
|
"ollama": {
|
||||||
@@ -1196,7 +1262,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ollama_chat": {
|
"ollama_chat": {
|
||||||
@@ -1212,7 +1279,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"oobabooga": {
|
"oobabooga": {
|
||||||
@@ -1228,7 +1296,8 @@
|
|||||||
"audio_speech": true,
|
"audio_speech": true,
|
||||||
"moderations": true,
|
"moderations": true,
|
||||||
"batches": true,
|
"batches": true,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"openai": {
|
"openai": {
|
||||||
@@ -1244,7 +1313,8 @@
|
|||||||
"audio_speech": true,
|
"audio_speech": true,
|
||||||
"moderations": true,
|
"moderations": true,
|
||||||
"batches": true,
|
"batches": true,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"openai_like": {
|
"openai_like": {
|
||||||
@@ -1276,7 +1346,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ovhcloud": {
|
"ovhcloud": {
|
||||||
@@ -1292,7 +1363,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"parallel_ai": {
|
"parallel_ai": {
|
||||||
@@ -1326,7 +1398,8 @@
|
|||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false,
|
"rerank": false,
|
||||||
"search": true
|
"search": true,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"petals": {
|
"petals": {
|
||||||
@@ -1342,7 +1415,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"publicai": {
|
"publicai": {
|
||||||
@@ -1358,7 +1432,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"predibase": {
|
"predibase": {
|
||||||
@@ -1374,7 +1449,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"recraft": {
|
"recraft": {
|
||||||
@@ -1406,7 +1482,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"runwayml": {
|
"runwayml": {
|
||||||
@@ -1439,7 +1516,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"searxng": {
|
"searxng": {
|
||||||
@@ -1472,7 +1550,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"snowflake": {
|
"snowflake": {
|
||||||
@@ -1488,7 +1567,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"text-completion-codestral": {
|
"text-completion-codestral": {
|
||||||
@@ -1504,7 +1584,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"text-completion-openai": {
|
"text-completion-openai": {
|
||||||
@@ -1520,7 +1601,8 @@
|
|||||||
"audio_speech": true,
|
"audio_speech": true,
|
||||||
"moderations": true,
|
"moderations": true,
|
||||||
"batches": true,
|
"batches": true,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"together_ai": {
|
"together_ai": {
|
||||||
@@ -1536,7 +1618,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"topaz": {
|
"topaz": {
|
||||||
@@ -1552,7 +1635,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tavily": {
|
"tavily": {
|
||||||
@@ -1585,7 +1669,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"v0": {
|
"v0": {
|
||||||
@@ -1601,7 +1686,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vercel_ai_gateway": {
|
"vercel_ai_gateway": {
|
||||||
@@ -1617,7 +1703,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vllm": {
|
"vllm": {
|
||||||
@@ -1633,7 +1720,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"volcengine": {
|
"volcengine": {
|
||||||
@@ -1649,7 +1737,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"voyage": {
|
"voyage": {
|
||||||
@@ -1681,7 +1770,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"watsonx_text": {
|
"watsonx_text": {
|
||||||
@@ -1697,7 +1787,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"xai": {
|
"xai": {
|
||||||
@@ -1713,7 +1804,8 @@
|
|||||||
"audio_speech": false,
|
"audio_speech": false,
|
||||||
"moderations": false,
|
"moderations": false,
|
||||||
"batches": false,
|
"batches": false,
|
||||||
"rerank": false
|
"rerank": false,
|
||||||
|
"a2a": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"xinference": {
|
"xinference": {
|
||||||
|
|||||||
@@ -0,0 +1,231 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { Tooltip, Button } from "antd";
|
||||||
|
import {
|
||||||
|
CheckCircleOutlined,
|
||||||
|
ClockCircleOutlined,
|
||||||
|
LoadingOutlined,
|
||||||
|
ExclamationCircleOutlined,
|
||||||
|
CopyOutlined,
|
||||||
|
DownOutlined,
|
||||||
|
RightOutlined,
|
||||||
|
LinkOutlined,
|
||||||
|
FileTextOutlined,
|
||||||
|
RobotOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
|
|
||||||
|
export interface A2ATaskMetadata {
|
||||||
|
taskId?: string;
|
||||||
|
contextId?: string;
|
||||||
|
status?: {
|
||||||
|
state?: string;
|
||||||
|
timestamp?: string;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface A2AMetricsProps {
|
||||||
|
a2aMetadata?: A2ATaskMetadata;
|
||||||
|
timeToFirstToken?: number;
|
||||||
|
totalLatency?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusIcon = (state?: string) => {
|
||||||
|
switch (state) {
|
||||||
|
case "completed":
|
||||||
|
return <CheckCircleOutlined className="text-green-500" />;
|
||||||
|
case "working":
|
||||||
|
case "submitted":
|
||||||
|
return <LoadingOutlined className="text-blue-500" />;
|
||||||
|
case "failed":
|
||||||
|
case "canceled":
|
||||||
|
return <ExclamationCircleOutlined className="text-red-500" />;
|
||||||
|
default:
|
||||||
|
return <ClockCircleOutlined className="text-gray-500" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusColor = (state?: string) => {
|
||||||
|
switch (state) {
|
||||||
|
case "completed":
|
||||||
|
return "bg-green-100 text-green-700";
|
||||||
|
case "working":
|
||||||
|
case "submitted":
|
||||||
|
return "bg-blue-100 text-blue-700";
|
||||||
|
case "failed":
|
||||||
|
case "canceled":
|
||||||
|
return "bg-red-100 text-red-700";
|
||||||
|
default:
|
||||||
|
return "bg-gray-100 text-gray-700";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTimestamp = (timestamp?: string) => {
|
||||||
|
if (!timestamp) return null;
|
||||||
|
try {
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
||||||
|
} catch {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const truncateId = (id?: string, length = 8) => {
|
||||||
|
if (!id) return null;
|
||||||
|
return id.length > length ? `${id.substring(0, length)}…` : id;
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyToClipboard = (text: string) => {
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
};
|
||||||
|
|
||||||
|
const A2AMetrics: React.FC<A2AMetricsProps> = ({ a2aMetadata, timeToFirstToken, totalLatency }) => {
|
||||||
|
const [showDetails, setShowDetails] = useState(false);
|
||||||
|
|
||||||
|
if (!a2aMetadata && !timeToFirstToken && !totalLatency) return null;
|
||||||
|
|
||||||
|
const { taskId, contextId, status, metadata } = a2aMetadata || {};
|
||||||
|
const formattedTime = formatTimestamp(status?.timestamp);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="a2a-metrics mt-3 pt-2 border-t border-gray-200 text-xs">
|
||||||
|
{/* A2A Metadata Header */}
|
||||||
|
<div className="flex items-center mb-2 text-gray-600">
|
||||||
|
<RobotOutlined className="mr-1.5 text-blue-500" />
|
||||||
|
<span className="font-medium text-gray-700">A2A Metadata</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main metrics row */}
|
||||||
|
<div className="flex flex-wrap items-center gap-2 text-gray-500 ml-4">
|
||||||
|
{/* Status badge */}
|
||||||
|
{status?.state && (
|
||||||
|
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${getStatusColor(status.state)}`}>
|
||||||
|
{getStatusIcon(status.state)}
|
||||||
|
<span className="ml-1 capitalize">{status.state}</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Timestamp */}
|
||||||
|
{formattedTime && (
|
||||||
|
<Tooltip title={status?.timestamp}>
|
||||||
|
<span className="flex items-center">
|
||||||
|
<ClockCircleOutlined className="mr-1" />
|
||||||
|
{formattedTime}
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Latency */}
|
||||||
|
{totalLatency !== undefined && (
|
||||||
|
<Tooltip title="Total latency">
|
||||||
|
<span className="flex items-center text-blue-600">
|
||||||
|
<ClockCircleOutlined className="mr-1" />
|
||||||
|
{(totalLatency / 1000).toFixed(2)}s
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Time to first token */}
|
||||||
|
{timeToFirstToken !== undefined && (
|
||||||
|
<Tooltip title="Time to first token">
|
||||||
|
<span className="flex items-center text-green-600">
|
||||||
|
TTFT: {(timeToFirstToken / 1000).toFixed(2)}s
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* IDs row */}
|
||||||
|
<div className="flex flex-wrap items-center gap-3 text-gray-500 ml-4 mt-1.5">
|
||||||
|
{/* Task ID */}
|
||||||
|
{taskId && (
|
||||||
|
<Tooltip title={`Click to copy: ${taskId}`}>
|
||||||
|
<span
|
||||||
|
className="flex items-center cursor-pointer hover:text-gray-700"
|
||||||
|
onClick={() => copyToClipboard(taskId)}
|
||||||
|
>
|
||||||
|
<FileTextOutlined className="mr-1" />
|
||||||
|
Task: {truncateId(taskId)}
|
||||||
|
<CopyOutlined className="ml-1 text-gray-400 hover:text-gray-600" />
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Context/Session ID */}
|
||||||
|
{contextId && (
|
||||||
|
<Tooltip title={`Click to copy: ${contextId}`}>
|
||||||
|
<span
|
||||||
|
className="flex items-center cursor-pointer hover:text-gray-700"
|
||||||
|
onClick={() => copyToClipboard(contextId)}
|
||||||
|
>
|
||||||
|
<LinkOutlined className="mr-1" />
|
||||||
|
Session: {truncateId(contextId)}
|
||||||
|
<CopyOutlined className="ml-1 text-gray-400 hover:text-gray-600" />
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Details toggle */}
|
||||||
|
{(metadata || status?.message) && (
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
className="text-xs text-blue-500 hover:text-blue-700 p-0 h-auto"
|
||||||
|
onClick={() => setShowDetails(!showDetails)}
|
||||||
|
>
|
||||||
|
{showDetails ? <DownOutlined /> : <RightOutlined />}
|
||||||
|
<span className="ml-1">Details</span>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Expandable details panel */}
|
||||||
|
{showDetails && (
|
||||||
|
<div className="mt-2 ml-4 p-3 bg-gray-50 rounded-md text-gray-600 border border-gray-200">
|
||||||
|
{/* Status message */}
|
||||||
|
{status?.message && (
|
||||||
|
<div className="mb-2">
|
||||||
|
<span className="font-medium text-gray-700">Status Message:</span>
|
||||||
|
<span className="ml-2">{status.message}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Full IDs */}
|
||||||
|
{taskId && (
|
||||||
|
<div className="mb-1.5 flex items-center">
|
||||||
|
<span className="font-medium text-gray-700 w-24">Task ID:</span>
|
||||||
|
<code className="ml-2 px-2 py-1 bg-white border border-gray-200 rounded text-xs font-mono">{taskId}</code>
|
||||||
|
<CopyOutlined
|
||||||
|
className="ml-2 cursor-pointer text-gray-400 hover:text-blue-500"
|
||||||
|
onClick={() => copyToClipboard(taskId)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{contextId && (
|
||||||
|
<div className="mb-1.5 flex items-center">
|
||||||
|
<span className="font-medium text-gray-700 w-24">Session ID:</span>
|
||||||
|
<code className="ml-2 px-2 py-1 bg-white border border-gray-200 rounded text-xs font-mono">{contextId}</code>
|
||||||
|
<CopyOutlined
|
||||||
|
className="ml-2 cursor-pointer text-gray-400 hover:text-blue-500"
|
||||||
|
onClick={() => copyToClipboard(contextId)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Metadata fields */}
|
||||||
|
{metadata && Object.keys(metadata).length > 0 && (
|
||||||
|
<div className="mt-3">
|
||||||
|
<span className="font-medium text-gray-700">Custom Metadata:</span>
|
||||||
|
<pre className="mt-1.5 p-2 bg-white border border-gray-200 rounded text-xs font-mono overflow-x-auto whitespace-pre-wrap">
|
||||||
|
{JSON.stringify(metadata, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default A2AMetrics;
|
||||||
@@ -51,6 +51,10 @@ import { fetchAvailableModels, ModelGroup } from "../llm_calls/fetch_models";
|
|||||||
import { makeOpenAIImageEditsRequest } from "../llm_calls/image_edits";
|
import { makeOpenAIImageEditsRequest } from "../llm_calls/image_edits";
|
||||||
import { makeOpenAIImageGenerationRequest } from "../llm_calls/image_generation";
|
import { makeOpenAIImageGenerationRequest } from "../llm_calls/image_generation";
|
||||||
import { makeOpenAIResponsesRequest } from "../llm_calls/responses_api";
|
import { makeOpenAIResponsesRequest } from "../llm_calls/responses_api";
|
||||||
|
import { Agent, fetchAvailableAgents } from "../llm_calls/fetch_agents";
|
||||||
|
import { makeA2AStreamMessageRequest } from "../llm_calls/a2a_send_message";
|
||||||
|
import A2AMetrics from "./A2AMetrics";
|
||||||
|
import { A2ATaskMetadata } from "./types";
|
||||||
import MCPEventsDisplay, { MCPEvent } from "./MCPEventsDisplay";
|
import MCPEventsDisplay, { MCPEvent } from "./MCPEventsDisplay";
|
||||||
import { EndpointType, getEndpointType } from "./mode_endpoint_mapping";
|
import { EndpointType, getEndpointType } from "./mode_endpoint_mapping";
|
||||||
import ReasoningContent from "./ReasoningContent";
|
import ReasoningContent from "./ReasoningContent";
|
||||||
@@ -124,6 +128,8 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||||||
const [selectedModel, setSelectedModel] = useState<string | undefined>(undefined);
|
const [selectedModel, setSelectedModel] = useState<string | undefined>(undefined);
|
||||||
const [showCustomModelInput, setShowCustomModelInput] = useState<boolean>(false);
|
const [showCustomModelInput, setShowCustomModelInput] = useState<boolean>(false);
|
||||||
const [modelInfo, setModelInfo] = useState<ModelGroup[]>([]);
|
const [modelInfo, setModelInfo] = useState<ModelGroup[]>([]);
|
||||||
|
const [agentInfo, setAgentInfo] = useState<Agent[]>([]);
|
||||||
|
const [selectedAgent, setSelectedAgent] = useState<string | undefined>(undefined);
|
||||||
const customModelTimeout = useRef<NodeJS.Timeout | null>(null);
|
const customModelTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||||
const [endpointType, setEndpointType] = useState<string>(
|
const [endpointType, setEndpointType] = useState<string>(
|
||||||
() => sessionStorage.getItem("endpointType") || EndpointType.CHAT,
|
() => sessionStorage.getItem("endpointType") || EndpointType.CHAT,
|
||||||
@@ -340,6 +346,29 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||||||
loadMCPTools();
|
loadMCPTools();
|
||||||
}, [accessToken, userID, userRole, apiKeySource, apiKey, token]);
|
}, [accessToken, userID, userRole, apiKeySource, apiKey, token]);
|
||||||
|
|
||||||
|
// Fetch agents when A2A endpoint is selected
|
||||||
|
useEffect(() => {
|
||||||
|
const userApiKey = apiKeySource === "session" ? accessToken : apiKey;
|
||||||
|
if (!userApiKey || endpointType !== EndpointType.A2A_AGENTS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadAgents = async () => {
|
||||||
|
try {
|
||||||
|
const agents = await fetchAvailableAgents(userApiKey);
|
||||||
|
setAgentInfo(agents);
|
||||||
|
// Clear selection if current agent not in list
|
||||||
|
if (selectedAgent && !agents.some((a) => a.agent_name === selectedAgent)) {
|
||||||
|
setSelectedAgent(undefined);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching agents:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadAgents();
|
||||||
|
}, [accessToken, apiKeySource, apiKey, endpointType]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Scroll to the bottom of the chat whenever chatHistory updates
|
// Scroll to the bottom of the chat whenever chatHistory updates
|
||||||
if (chatEndRef.current) {
|
if (chatEndRef.current) {
|
||||||
@@ -469,6 +498,23 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateA2AMetadata = (a2aMetadata: A2ATaskMetadata) => {
|
||||||
|
console.log("Received A2A metadata:", a2aMetadata);
|
||||||
|
setChatHistory((prevHistory) => {
|
||||||
|
const lastMessage = prevHistory[prevHistory.length - 1];
|
||||||
|
|
||||||
|
if (lastMessage && lastMessage.role === "assistant") {
|
||||||
|
const updatedMessage = {
|
||||||
|
...lastMessage,
|
||||||
|
a2aMetadata,
|
||||||
|
};
|
||||||
|
return [...prevHistory.slice(0, prevHistory.length - 1), updatedMessage];
|
||||||
|
}
|
||||||
|
|
||||||
|
return prevHistory;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const updateTotalLatency = (totalLatency: number) => {
|
const updateTotalLatency = (totalLatency: number) => {
|
||||||
setChatHistory((prevHistory) => {
|
setChatHistory((prevHistory) => {
|
||||||
const lastMessage = prevHistory[prevHistory.length - 1];
|
const lastMessage = prevHistory[prevHistory.length - 1];
|
||||||
@@ -684,6 +730,12 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For A2A agents, require agent selection
|
||||||
|
if (endpointType === EndpointType.A2A_AGENTS && !selectedAgent) {
|
||||||
|
NotificationsManager.fromBackend("Please select an agent to send a message");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!token || !userRole || !userID) {
|
if (!token || !userRole || !userID) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -908,6 +960,20 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle A2A agent calls (separate from model-based calls) - use streaming
|
||||||
|
if (endpointType === EndpointType.A2A_AGENTS && selectedAgent) {
|
||||||
|
await makeA2AStreamMessageRequest(
|
||||||
|
selectedAgent,
|
||||||
|
inputMessage,
|
||||||
|
(chunk, model) => updateTextUI("assistant", chunk, model),
|
||||||
|
effectiveApiKey,
|
||||||
|
signal,
|
||||||
|
updateTimingData,
|
||||||
|
updateTotalLatency,
|
||||||
|
updateA2AMetadata,
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (signal.aborted) {
|
if (signal.aborted) {
|
||||||
console.log("Request was cancelled");
|
console.log("Request was cancelled");
|
||||||
@@ -1038,11 +1104,13 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||||||
endpointType={endpointType}
|
endpointType={endpointType}
|
||||||
onEndpointChange={(value) => {
|
onEndpointChange={(value) => {
|
||||||
setEndpointType(value);
|
setEndpointType(value);
|
||||||
// Clear model selection when switching endpoint type
|
// Clear model/agent selection when switching endpoint type
|
||||||
setSelectedModel(undefined);
|
setSelectedModel(undefined);
|
||||||
|
setSelectedAgent(undefined);
|
||||||
setShowCustomModelInput(false);
|
setShowCustomModelInput(false);
|
||||||
try {
|
try {
|
||||||
sessionStorage.removeItem("selectedModel");
|
sessionStorage.removeItem("selectedModel");
|
||||||
|
sessionStorage.removeItem("selectedAgent");
|
||||||
} catch {}
|
} catch {}
|
||||||
}}
|
}}
|
||||||
className="mb-4"
|
className="mb-4"
|
||||||
@@ -1077,103 +1145,145 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
{/* Model Selector - shown when NOT using A2A Agents */}
|
||||||
<Text className="font-medium block mb-2 text-gray-700 flex items-center justify-between">
|
{endpointType !== EndpointType.A2A_AGENTS && (
|
||||||
<span className="flex items-center">
|
<div>
|
||||||
<RobotOutlined className="mr-2" /> Select Model
|
<Text className="font-medium block mb-2 text-gray-700 flex items-center justify-between">
|
||||||
</span>
|
<span className="flex items-center">
|
||||||
{isChatModel() ? (
|
<RobotOutlined className="mr-2" /> Select Model
|
||||||
<Popover
|
</span>
|
||||||
content={
|
{isChatModel() ? (
|
||||||
<AdditionalModelSettings
|
<Popover
|
||||||
temperature={temperature}
|
content={
|
||||||
maxTokens={maxTokens}
|
<AdditionalModelSettings
|
||||||
useAdvancedParams={useAdvancedParams}
|
temperature={temperature}
|
||||||
onTemperatureChange={setTemperature}
|
maxTokens={maxTokens}
|
||||||
onMaxTokensChange={setMaxTokens}
|
useAdvancedParams={useAdvancedParams}
|
||||||
onUseAdvancedParamsChange={setUseAdvancedParams}
|
onTemperatureChange={setTemperature}
|
||||||
|
onMaxTokensChange={setMaxTokens}
|
||||||
|
onUseAdvancedParamsChange={setUseAdvancedParams}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title="Model Settings"
|
||||||
|
trigger="click"
|
||||||
|
placement="right"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<SettingOutlined />}
|
||||||
|
className="text-gray-500 hover:text-gray-700"
|
||||||
/>
|
/>
|
||||||
}
|
</Popover>
|
||||||
title="Model Settings"
|
) : (
|
||||||
trigger="click"
|
<Tooltip title="Advanced parameters are only supported for chat models currently">
|
||||||
placement="right"
|
<Button
|
||||||
>
|
type="text"
|
||||||
<Button
|
size="small"
|
||||||
type="text"
|
icon={<SettingOutlined />}
|
||||||
size="small"
|
className="text-gray-300 cursor-not-allowed"
|
||||||
icon={<SettingOutlined />}
|
disabled
|
||||||
className="text-gray-500 hover:text-gray-700"
|
/>
|
||||||
/>
|
</Tooltip>
|
||||||
</Popover>
|
)}
|
||||||
) : (
|
</Text>
|
||||||
<Tooltip title="Advanced parameters are only supported for chat models currently">
|
<Select
|
||||||
<Button
|
value={selectedModel}
|
||||||
type="text"
|
placeholder="Select a Model"
|
||||||
size="small"
|
onChange={onModelChange}
|
||||||
icon={<SettingOutlined />}
|
options={[
|
||||||
className="text-gray-300 cursor-not-allowed"
|
...Array.from(
|
||||||
disabled
|
new Set(
|
||||||
/>
|
modelInfo
|
||||||
</Tooltip>
|
.filter((option) => {
|
||||||
)}
|
if (!option.mode) {
|
||||||
</Text>
|
//If no mode, show all models
|
||||||
<Select
|
return true;
|
||||||
value={selectedModel}
|
}
|
||||||
placeholder="Select a Model"
|
const optionEndpoint = getEndpointType(option.mode);
|
||||||
onChange={onModelChange}
|
// Show chat models for responses/anthropic_messages endpoints as they are compatible
|
||||||
options={[
|
if (
|
||||||
...Array.from(
|
endpointType === EndpointType.RESPONSES ||
|
||||||
new Set(
|
endpointType === EndpointType.ANTHROPIC_MESSAGES
|
||||||
modelInfo
|
) {
|
||||||
.filter((option) => {
|
return optionEndpoint === endpointType || optionEndpoint === EndpointType.CHAT;
|
||||||
if (!option.mode) {
|
}
|
||||||
//If no mode, show all models
|
// Show image models for image_edits endpoint as they are compatible
|
||||||
return true;
|
if (endpointType === EndpointType.IMAGE_EDITS) {
|
||||||
}
|
return optionEndpoint === endpointType || optionEndpoint === EndpointType.IMAGE;
|
||||||
const optionEndpoint = getEndpointType(option.mode);
|
}
|
||||||
// Show chat models for responses/anthropic_messages endpoints as they are compatible
|
return optionEndpoint === endpointType;
|
||||||
if (
|
})
|
||||||
endpointType === EndpointType.RESPONSES ||
|
.map((option) => option.model_group),
|
||||||
endpointType === EndpointType.ANTHROPIC_MESSAGES
|
),
|
||||||
) {
|
).map((model_group, index) => ({
|
||||||
return optionEndpoint === endpointType || optionEndpoint === EndpointType.CHAT;
|
value: model_group,
|
||||||
}
|
label: model_group,
|
||||||
// Show image models for image_edits endpoint as they are compatible
|
key: index,
|
||||||
if (endpointType === EndpointType.IMAGE_EDITS) {
|
})),
|
||||||
return optionEndpoint === endpointType || optionEndpoint === EndpointType.IMAGE;
|
{ value: "custom", label: "Enter custom model", key: "custom" },
|
||||||
}
|
]}
|
||||||
return optionEndpoint === endpointType;
|
style={{ width: "100%" }}
|
||||||
})
|
showSearch={true}
|
||||||
.map((option) => option.model_group),
|
className="rounded-md"
|
||||||
),
|
|
||||||
).map((model_group, index) => ({
|
|
||||||
value: model_group,
|
|
||||||
label: model_group,
|
|
||||||
key: index,
|
|
||||||
})),
|
|
||||||
{ value: "custom", label: "Enter custom model", key: "custom" },
|
|
||||||
]}
|
|
||||||
style={{ width: "100%" }}
|
|
||||||
showSearch={true}
|
|
||||||
className="rounded-md"
|
|
||||||
/>
|
|
||||||
{showCustomModelInput && (
|
|
||||||
<TextInput
|
|
||||||
className="mt-2"
|
|
||||||
placeholder="Enter custom model name"
|
|
||||||
onValueChange={(value) => {
|
|
||||||
// Using setTimeout to create a simple debounce effect
|
|
||||||
if (customModelTimeout.current) {
|
|
||||||
clearTimeout(customModelTimeout.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
customModelTimeout.current = setTimeout(() => {
|
|
||||||
setSelectedModel(value);
|
|
||||||
}, 500); // 500ms delay after typing stops
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
{showCustomModelInput && (
|
||||||
</div>
|
<TextInput
|
||||||
|
className="mt-2"
|
||||||
|
placeholder="Enter custom model name"
|
||||||
|
onValueChange={(value) => {
|
||||||
|
// Using setTimeout to create a simple debounce effect
|
||||||
|
if (customModelTimeout.current) {
|
||||||
|
clearTimeout(customModelTimeout.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
customModelTimeout.current = setTimeout(() => {
|
||||||
|
setSelectedModel(value);
|
||||||
|
}, 500); // 500ms delay after typing stops
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Agent Selector - shown ONLY for A2A Agents endpoint */}
|
||||||
|
{endpointType === EndpointType.A2A_AGENTS && (
|
||||||
|
<div>
|
||||||
|
<Text className="font-medium block mb-2 text-gray-700 flex items-center">
|
||||||
|
<RobotOutlined className="mr-2" /> Select Agent
|
||||||
|
</Text>
|
||||||
|
<Select
|
||||||
|
value={selectedAgent}
|
||||||
|
placeholder="Select an Agent"
|
||||||
|
onChange={(value) => setSelectedAgent(value)}
|
||||||
|
options={agentInfo.map((agent) => ({
|
||||||
|
value: agent.agent_name,
|
||||||
|
label: agent.agent_name || agent.agent_id,
|
||||||
|
key: agent.agent_id,
|
||||||
|
}))}
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
showSearch={true}
|
||||||
|
className="rounded-md"
|
||||||
|
optionLabelProp="label"
|
||||||
|
>
|
||||||
|
{agentInfo.map((agent) => (
|
||||||
|
<Select.Option key={agent.agent_id} value={agent.agent_name} label={agent.agent_name || agent.agent_id}>
|
||||||
|
<div className="flex flex-col py-1">
|
||||||
|
<span className="font-medium">{agent.agent_name || agent.agent_id}</span>
|
||||||
|
{agent.agent_card_params?.description && (
|
||||||
|
<span className="text-xs text-gray-500 mt-1">{agent.agent_card_params.description}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
{agentInfo.length === 0 && (
|
||||||
|
<Text className="text-xs text-gray-500 mt-2 block">
|
||||||
|
No agents found. Create agents via /v1/agents endpoint.
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Text className="font-medium block mb-2 text-gray-700 flex items-center">
|
<Text className="font-medium block mb-2 text-gray-700 flex items-center">
|
||||||
@@ -1440,7 +1550,8 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{message.role === "assistant" &&
|
{message.role === "assistant" &&
|
||||||
(message.timeToFirstToken || message.totalLatency || message.usage) && (
|
(message.timeToFirstToken || message.totalLatency || message.usage) &&
|
||||||
|
!message.a2aMetadata && (
|
||||||
<ResponseMetrics
|
<ResponseMetrics
|
||||||
timeToFirstToken={message.timeToFirstToken}
|
timeToFirstToken={message.timeToFirstToken}
|
||||||
totalLatency={message.totalLatency}
|
totalLatency={message.totalLatency}
|
||||||
@@ -1448,6 +1559,15 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||||||
toolName={message.toolName}
|
toolName={message.toolName}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* A2A Metrics - show for A2A agent responses */}
|
||||||
|
{message.role === "assistant" && message.a2aMetadata && (
|
||||||
|
<A2AMetrics
|
||||||
|
a2aMetadata={message.a2aMetadata}
|
||||||
|
timeToFirstToken={message.timeToFirstToken}
|
||||||
|
totalLatency={message.totalLatency}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1685,13 +1805,15 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||||||
endpointType === EndpointType.RESPONSES ||
|
endpointType === EndpointType.RESPONSES ||
|
||||||
endpointType === EndpointType.ANTHROPIC_MESSAGES
|
endpointType === EndpointType.ANTHROPIC_MESSAGES
|
||||||
? "Type your message... (Shift+Enter for new line)"
|
? "Type your message... (Shift+Enter for new line)"
|
||||||
: endpointType === EndpointType.IMAGE_EDITS
|
: endpointType === EndpointType.A2A_AGENTS
|
||||||
? "Describe how you want to edit the image..."
|
? "Send a message to the A2A agent..."
|
||||||
: endpointType === EndpointType.SPEECH
|
: endpointType === EndpointType.IMAGE_EDITS
|
||||||
? "Enter text to convert to speech..."
|
? "Describe how you want to edit the image..."
|
||||||
: endpointType === EndpointType.TRANSCRIPTION
|
: endpointType === EndpointType.SPEECH
|
||||||
? "Optional: Add context or prompt for transcription..."
|
? "Enter text to convert to speech..."
|
||||||
: "Describe the image you want to generate..."
|
: endpointType === EndpointType.TRANSCRIPTION
|
||||||
|
? "Optional: Add context or prompt for transcription..."
|
||||||
|
: "Describe the image you want to generate..."
|
||||||
}
|
}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
|
|||||||
@@ -42,4 +42,5 @@ export const ENDPOINT_OPTIONS = [
|
|||||||
{ value: EndpointType.EMBEDDINGS, label: "/v1/embeddings" },
|
{ value: EndpointType.EMBEDDINGS, label: "/v1/embeddings" },
|
||||||
{ value: EndpointType.SPEECH, label: "/v1/audio/speech" },
|
{ value: EndpointType.SPEECH, label: "/v1/audio/speech" },
|
||||||
{ value: EndpointType.TRANSCRIPTION, label: "/v1/audio/transcriptions" },
|
{ value: EndpointType.TRANSCRIPTION, label: "/v1/audio/transcriptions" },
|
||||||
|
{ value: EndpointType.A2A_AGENTS, label: "/v1/a2a/message/send" },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export enum EndpointType {
|
|||||||
EMBEDDINGS = "embeddings",
|
EMBEDDINGS = "embeddings",
|
||||||
SPEECH = "speech",
|
SPEECH = "speech",
|
||||||
TRANSCRIPTION = "transcription",
|
TRANSCRIPTION = "transcription",
|
||||||
|
A2A_AGENTS = "a2a_agents",
|
||||||
// add additional endpoint types if required
|
// add additional endpoint types if required
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -70,12 +70,24 @@ export interface VectorStoreSearchResponse {
|
|||||||
data: VectorStoreSearchResult[];
|
data: VectorStoreSearchResult[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface A2ATaskMetadata {
|
||||||
|
taskId?: string;
|
||||||
|
contextId?: string;
|
||||||
|
status?: {
|
||||||
|
state?: string;
|
||||||
|
timestamp?: string;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface MessageType {
|
export interface MessageType {
|
||||||
role: string;
|
role: string;
|
||||||
content: string | MultimodalContent[];
|
content: string | MultimodalContent[];
|
||||||
model?: string;
|
model?: string;
|
||||||
isImage?: boolean;
|
isImage?: boolean;
|
||||||
isAudio?: boolean;
|
isAudio?: boolean;
|
||||||
|
isEmbeddings?: boolean;
|
||||||
reasoningContent?: string;
|
reasoningContent?: string;
|
||||||
timeToFirstToken?: number;
|
timeToFirstToken?: number;
|
||||||
totalLatency?: number;
|
totalLatency?: number;
|
||||||
@@ -93,6 +105,7 @@ export interface MessageType {
|
|||||||
detail: string;
|
detail: string;
|
||||||
};
|
};
|
||||||
searchResults?: VectorStoreSearchResponse[];
|
searchResults?: VectorStoreSearchResponse[];
|
||||||
|
a2aMetadata?: A2ATaskMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MultimodalContent {
|
export interface MultimodalContent {
|
||||||
|
|||||||
@@ -0,0 +1,413 @@
|
|||||||
|
// a2a_send_message.tsx
|
||||||
|
// A2A Protocol (JSON-RPC 2.0) implementation for sending messages to agents
|
||||||
|
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import { getProxyBaseUrl } from "../../networking";
|
||||||
|
import { A2ATaskMetadata } from "../chat_ui/types";
|
||||||
|
|
||||||
|
interface A2AMessagePart {
|
||||||
|
kind: "text";
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface A2AMessage {
|
||||||
|
kind: "message";
|
||||||
|
messageId: string;
|
||||||
|
role: "user" | "agent";
|
||||||
|
parts: A2AMessagePart[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface A2AJsonRpcRequest {
|
||||||
|
jsonrpc: "2.0";
|
||||||
|
id: string;
|
||||||
|
method: string;
|
||||||
|
params: {
|
||||||
|
message: A2AMessage;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface A2AJsonRpcResponse {
|
||||||
|
jsonrpc: "2.0";
|
||||||
|
id: string;
|
||||||
|
result?: {
|
||||||
|
kind?: string;
|
||||||
|
parts?: A2AMessagePart[];
|
||||||
|
id?: string;
|
||||||
|
contextId?: string;
|
||||||
|
status?: {
|
||||||
|
state?: string;
|
||||||
|
timestamp?: string;
|
||||||
|
message?: {
|
||||||
|
parts?: A2AMessagePart[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
artifacts?: Array<{
|
||||||
|
artifactId?: string;
|
||||||
|
name?: string;
|
||||||
|
parts?: A2AMessagePart[];
|
||||||
|
}>;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
error?: {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts A2A task metadata from the response result.
|
||||||
|
*/
|
||||||
|
const extractA2AMetadata = (result: A2AJsonRpcResponse["result"]): A2ATaskMetadata | undefined => {
|
||||||
|
if (!result) return undefined;
|
||||||
|
|
||||||
|
const metadata: A2ATaskMetadata = {};
|
||||||
|
|
||||||
|
// Extract task ID
|
||||||
|
if (result.id) {
|
||||||
|
metadata.taskId = result.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract context/session ID
|
||||||
|
if (result.contextId) {
|
||||||
|
metadata.contextId = result.contextId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract status
|
||||||
|
if (result.status) {
|
||||||
|
metadata.status = {
|
||||||
|
state: result.status.state,
|
||||||
|
timestamp: result.status.timestamp,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extract status message text if present
|
||||||
|
if (result.status.message?.parts) {
|
||||||
|
const statusText = result.status.message.parts
|
||||||
|
.filter((p: any) => p.kind === "text" && p.text)
|
||||||
|
.map((p: any) => p.text)
|
||||||
|
.join(" ");
|
||||||
|
if (statusText) {
|
||||||
|
metadata.status.message = statusText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract custom metadata
|
||||||
|
if (result.metadata && typeof result.metadata === "object") {
|
||||||
|
metadata.metadata = result.metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(metadata).length > 0 ? metadata : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a message to an A2A agent using the JSON-RPC 2.0 protocol.
|
||||||
|
* Uses the non-streaming message/send method.
|
||||||
|
*/
|
||||||
|
export const makeA2ASendMessageRequest = async (
|
||||||
|
agentId: string,
|
||||||
|
message: string,
|
||||||
|
onTextUpdate: (chunk: string, model?: string) => void,
|
||||||
|
accessToken: string,
|
||||||
|
signal?: AbortSignal,
|
||||||
|
onTimingData?: (timeToFirstToken: number) => void,
|
||||||
|
onTotalLatency?: (totalLatency: number) => void,
|
||||||
|
onA2AMetadata?: (metadata: A2ATaskMetadata) => void,
|
||||||
|
): Promise<void> => {
|
||||||
|
const proxyBaseUrl = getProxyBaseUrl();
|
||||||
|
const url = proxyBaseUrl
|
||||||
|
? `${proxyBaseUrl}/a2a/${agentId}/message/send`
|
||||||
|
: `/a2a/${agentId}/message/send`;
|
||||||
|
|
||||||
|
const requestId = uuidv4();
|
||||||
|
const messageId = uuidv4().replace(/-/g, "");
|
||||||
|
|
||||||
|
const jsonRpcRequest: A2AJsonRpcRequest = {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
id: requestId,
|
||||||
|
method: "message/send",
|
||||||
|
params: {
|
||||||
|
message: {
|
||||||
|
kind: "message",
|
||||||
|
messageId: messageId,
|
||||||
|
role: "user",
|
||||||
|
parts: [{ kind: "text", text: message }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const startTime = performance.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(jsonRpcRequest),
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
const timeToFirstToken = performance.now() - startTime;
|
||||||
|
if (onTimingData) {
|
||||||
|
onTimingData(timeToFirstToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.error?.message || errorData.detail || `HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsonRpcResponse: A2AJsonRpcResponse = await response.json();
|
||||||
|
|
||||||
|
const totalLatency = performance.now() - startTime;
|
||||||
|
if (onTotalLatency) {
|
||||||
|
onTotalLatency(totalLatency);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonRpcResponse.error) {
|
||||||
|
throw new Error(jsonRpcResponse.error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract text and metadata from response
|
||||||
|
const result = jsonRpcResponse.result;
|
||||||
|
if (result) {
|
||||||
|
let responseText = "";
|
||||||
|
|
||||||
|
// Extract and send A2A metadata
|
||||||
|
const a2aMetadata = extractA2AMetadata(result);
|
||||||
|
if (a2aMetadata && onA2AMetadata) {
|
||||||
|
onA2AMetadata(a2aMetadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A2A Task response format with artifacts array
|
||||||
|
// Extract text from artifacts[*].parts[*] where kind === "text"
|
||||||
|
if (result.artifacts && Array.isArray(result.artifacts)) {
|
||||||
|
for (const artifact of result.artifacts) {
|
||||||
|
if (artifact.parts && Array.isArray(artifact.parts)) {
|
||||||
|
for (const part of artifact.parts) {
|
||||||
|
if (part.kind === "text" && part.text) {
|
||||||
|
responseText += part.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback: direct parts array (simpler response format)
|
||||||
|
else if (result.parts && Array.isArray(result.parts)) {
|
||||||
|
for (const part of result.parts) {
|
||||||
|
if (part.kind === "text" && part.text) {
|
||||||
|
responseText += part.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback: status.message.parts format
|
||||||
|
else if (result.status?.message?.parts) {
|
||||||
|
for (const part of result.status.message.parts) {
|
||||||
|
if (part.kind === "text" && part.text) {
|
||||||
|
responseText += part.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responseText) {
|
||||||
|
onTextUpdate(responseText, `a2a_agent/${agentId}`);
|
||||||
|
} else {
|
||||||
|
// Fallback: show raw result if we couldn't parse it
|
||||||
|
console.warn("Could not extract text from A2A response, showing raw JSON:", result);
|
||||||
|
onTextUpdate(JSON.stringify(result, null, 2), `a2a_agent/${agentId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (signal?.aborted) {
|
||||||
|
console.log("A2A request was cancelled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.error("A2A send message error:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a streaming message to an A2A agent using the JSON-RPC 2.0 protocol.
|
||||||
|
* Uses the message/stream method with NDJSON responses.
|
||||||
|
*/
|
||||||
|
export const makeA2AStreamMessageRequest = async (
|
||||||
|
agentId: string,
|
||||||
|
message: string,
|
||||||
|
onTextUpdate: (chunk: string, model?: string) => void,
|
||||||
|
accessToken: string,
|
||||||
|
signal?: AbortSignal,
|
||||||
|
onTimingData?: (timeToFirstToken: number) => void,
|
||||||
|
onTotalLatency?: (totalLatency: number) => void,
|
||||||
|
onA2AMetadata?: (metadata: A2ATaskMetadata) => void,
|
||||||
|
): Promise<void> => {
|
||||||
|
const proxyBaseUrl = getProxyBaseUrl();
|
||||||
|
const url = proxyBaseUrl
|
||||||
|
? `${proxyBaseUrl}/a2a/${agentId}`
|
||||||
|
: `/a2a/${agentId}`;
|
||||||
|
|
||||||
|
const requestId = uuidv4();
|
||||||
|
const messageId = uuidv4().replace(/-/g, "");
|
||||||
|
|
||||||
|
const jsonRpcRequest: A2AJsonRpcRequest = {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
id: requestId,
|
||||||
|
method: "message/stream",
|
||||||
|
params: {
|
||||||
|
message: {
|
||||||
|
kind: "message",
|
||||||
|
messageId: messageId,
|
||||||
|
role: "user",
|
||||||
|
parts: [{ kind: "text", text: message }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const startTime = performance.now();
|
||||||
|
let firstChunkReceived = false;
|
||||||
|
let latestMetadata: A2ATaskMetadata | undefined;
|
||||||
|
let accumulatedText = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(jsonRpcRequest),
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.error?.message || errorData.detail || `HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = response.body?.getReader();
|
||||||
|
if (!reader) {
|
||||||
|
throw new Error("No response body");
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let buffer = "";
|
||||||
|
let done = false;
|
||||||
|
|
||||||
|
while (!done) {
|
||||||
|
const readResult = await reader.read();
|
||||||
|
done = readResult.done;
|
||||||
|
const value = readResult.value;
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
buffer += decoder.decode(value, { stream: true });
|
||||||
|
const lines = buffer.split("\n");
|
||||||
|
buffer = lines.pop() || "";
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (!line.trim()) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const chunk = JSON.parse(line);
|
||||||
|
|
||||||
|
if (!firstChunkReceived) {
|
||||||
|
firstChunkReceived = true;
|
||||||
|
const timeToFirstToken = performance.now() - startTime;
|
||||||
|
if (onTimingData) {
|
||||||
|
onTimingData(timeToFirstToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle streaming chunks - extract text from various A2A formats
|
||||||
|
const result = chunk.result;
|
||||||
|
if (result) {
|
||||||
|
// Extract metadata from each chunk (keep latest)
|
||||||
|
const chunkMetadata = extractA2AMetadata(result);
|
||||||
|
if (chunkMetadata) {
|
||||||
|
latestMetadata = { ...latestMetadata, ...chunkMetadata };
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunkKind = result.kind;
|
||||||
|
|
||||||
|
// Handle artifact-update chunks (streaming response content)
|
||||||
|
// Note: streaming uses "artifact" (singular), not "artifacts" (plural)
|
||||||
|
if (chunkKind === "artifact-update" && result.artifact) {
|
||||||
|
const artifact = result.artifact;
|
||||||
|
if (artifact.parts && Array.isArray(artifact.parts)) {
|
||||||
|
for (const part of artifact.parts) {
|
||||||
|
if (part.kind === "text" && part.text) {
|
||||||
|
// Accumulate actual response content
|
||||||
|
accumulatedText += part.text;
|
||||||
|
onTextUpdate(accumulatedText, `a2a_agent/${agentId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Handle non-streaming Task response format with artifacts array (plural)
|
||||||
|
else if (result.artifacts && Array.isArray(result.artifacts)) {
|
||||||
|
for (const artifact of result.artifacts) {
|
||||||
|
if (artifact.parts && Array.isArray(artifact.parts)) {
|
||||||
|
for (const part of artifact.parts) {
|
||||||
|
if (part.kind === "text" && part.text) {
|
||||||
|
accumulatedText += part.text;
|
||||||
|
onTextUpdate(accumulatedText, `a2a_agent/${agentId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Handle status-update chunks (progress messages like "Processing request...")
|
||||||
|
// Only show these temporarily if we haven't received actual content yet
|
||||||
|
else if (chunkKind === "status-update" && result.status?.message?.parts) {
|
||||||
|
// Skip status messages once we have real content
|
||||||
|
if (!accumulatedText) {
|
||||||
|
for (const part of result.status.message.parts) {
|
||||||
|
if (part.kind === "text" && part.text) {
|
||||||
|
// Show as temporary status - will be replaced when real content arrives
|
||||||
|
onTextUpdate(part.text, `a2a_agent/${agentId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Direct parts array (fallback)
|
||||||
|
else if (result.parts && Array.isArray(result.parts)) {
|
||||||
|
for (const part of result.parts) {
|
||||||
|
if (part.kind === "text" && part.text) {
|
||||||
|
accumulatedText += part.text;
|
||||||
|
onTextUpdate(accumulatedText, `a2a_agent/${agentId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chunk.error) {
|
||||||
|
throw new Error(chunk.error.message);
|
||||||
|
}
|
||||||
|
} catch (parseError) {
|
||||||
|
// Only warn if it's not a JSON parse error on an empty/partial line
|
||||||
|
if (line.trim().length > 0) {
|
||||||
|
console.warn("Failed to parse A2A streaming chunk:", line, parseError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalLatency = performance.now() - startTime;
|
||||||
|
if (onTotalLatency) {
|
||||||
|
onTotalLatency(totalLatency);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send final metadata after streaming completes
|
||||||
|
if (latestMetadata && onA2AMetadata) {
|
||||||
|
onA2AMetadata(latestMetadata);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (signal?.aborted) {
|
||||||
|
console.log("A2A streaming request was cancelled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.error("A2A stream message error:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
// fetch_agents.tsx
|
||||||
|
|
||||||
|
import { getProxyBaseUrl } from "../../networking";
|
||||||
|
|
||||||
|
export interface Agent {
|
||||||
|
agent_id: string;
|
||||||
|
agent_name: string;
|
||||||
|
description?: string;
|
||||||
|
agent_card_params?: {
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
url?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches available A2A agents from /v1/agents endpoint.
|
||||||
|
*/
|
||||||
|
export const fetchAvailableAgents = async (accessToken: string): Promise<Agent[]> => {
|
||||||
|
try {
|
||||||
|
const proxyBaseUrl = getProxyBaseUrl();
|
||||||
|
const url = proxyBaseUrl ? `${proxyBaseUrl}/v1/agents` : `/v1/agents`;
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.detail || "Failed to fetch agents");
|
||||||
|
}
|
||||||
|
|
||||||
|
const agents: Agent[] = await response.json();
|
||||||
|
console.log("Fetched agents:", agents);
|
||||||
|
|
||||||
|
// Sort agents alphabetically by name
|
||||||
|
agents.sort((a, b) => {
|
||||||
|
const nameA = a.agent_name || a.agent_id;
|
||||||
|
const nameB = b.agent_name || b.agent_id;
|
||||||
|
return nameA.localeCompare(nameB);
|
||||||
|
});
|
||||||
|
|
||||||
|
return agents;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching agents:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user