* feat(api-keys): add rename support in permissions modal Add an editable key name field at the top of the permissions modal, allowing users to rename API keys alongside existing permission settings. The backend already supported name updates via PATCH /api/keys/:id — this wires the UI to send the name field and refreshes the key list on success. Changes: - Add keyName state and text input to PermissionsModal - Update handleUpdatePermissions to validate and send name in PATCH body - Add integration test for rename via PATCH (valid, empty, too-long names) - Update E2E mock to handle PATCH requests * chore(release): bump version to 3.7.6 * chore(release): v3.7.6 — merge API key rename feature and sync docs * chore(release): expand contributor credits to 155 PRs across full project history - Expanded acknowledgment table from 29 to 53 contributors - Added 100+ previously uncredited PRs from project inception through v3.7.5 - Moved contributor credits section to v3.7.6 (current release) - Synced llm.txt version to 3.7.6 * fix: resolve security ReDoS in codex and bugs #1797 #1789 * feat(dashboard): implement remaining v3.7.6 dashboard features and fixes * fix(xiaomi-mimo): update models to V2.5, fix Token Plan validation and default region (#1823) Integrated into release/v3.7.6 * fix(dashboard): correct loadPresets ReferenceError in CostOverviewTab * fix(codex): omit compact client metadata (#1822) Integrated into release/v3.7.6 * feat(chatgpt-web): support thinking_effort (Standard/Extended) for thinking-capable models (#1821) Integrated into release/v3.7.6 * Fix endpoint visibility, A2A status, and API catalog (#1806) Integrated into release/v3.7.6 * fix(analytics): use pure SQL aggregations — no history rows loaded (#1802) Integrated into release/v3.7.6 * fix(stability): resolve codex input validation, enable combo circuit breaker, and fix broken unit tests * docs(changelog): update for stability bug fixes #1804 #1805 * fix: clear active requests and recover providers (#1824) Integrated into release/v3.7.6 * feat: inject fallback tool names to prevent upstream 400 errors (#1775) * feat: auto-restore probe-failed database to prevent data loss (#1810) * fix: safely cast inputs to strings before calling trim() to avoid crashes on numeric fields in proxy modal (#1825) * chore(release): v3.7.6 — final stability patches for production * test: update expected db probe-failure error message for auto-restore feature * chore(workflow): mandate implementation plan generation in resolve-issues * docs(changelog): rewrite v3.7.6 with complete commit-accurate entries * feat(analytics): add cost-based usage insights and activity streaks Expand usage analytics to report total cost, per-series cost totals, API key counts, and current activity streaks using pricing-aware token calculations. Also make probe-failed database recovery choose the newest backup by its embedded timestamp instead of filesystem mtime so auto-restore selects the intended snapshot reliably. * fix(mitm): enforce transparent interception on port 443 only Reject non-443 MITM port updates in the settings API and normalize stored configuration back to the required transparent interception port. Lock the dashboard port field to 443, update the validation copy, and add integration coverage to prevent stale custom ports from being accepted or surfaced. * docs(changelog): update for analytics and mitm features --------- Co-authored-by: Andrew Munsell <andrew@wizardapps.net> Co-authored-by: Antigravity Assistant <bot@antigravity.local> Co-authored-by: Gi99lin <74502520+Gi99lin@users.noreply.github.com> Co-authored-by: Sergey Morozov <tr0st@bk.ru> Co-authored-by: payne <baboialex95@gmail.com> Co-authored-by: Randi <55005611+rdself@users.noreply.github.com> Co-authored-by: Paijo <14921983+oyi77@users.noreply.github.com> Co-authored-by: ipanghu <bypanghu@163.com>
OmniRoute A2A Server
Agent-to-Agent Protocol v0.3 — Enables any AI agent to use OmniRoute as an intelligent routing agent via JSON-RPC 2.0.
The A2A Server exposes OmniRoute as a first-class agent that other agents can discover, delegate tasks to, and collaborate with using the A2A Protocol.
Architecture
┌──────────────────────────────────────────────────────────────────┐
│ Orchestrator Agent │
│ (LangChain, CrewAI, AutoGen, Custom Agent) │
└──────────────────────┬───────────────────────────────────────────┘
│ 1. GET /.well-known/agent.json (discover)
│ 2. POST /a2a (JSON-RPC 2.0)
▼
┌──────────────────────────────────────────────────────────────────┐
│ OmniRoute A2A Server │
│ ┌────────────────┐ ┌────────────────┐ ┌───────────────────┐ │
│ │ Task Manager │ │ Skill Engine │ │ SSE Streaming │ │
│ │ (lifecycle) │──│ (registry) │──│ (real-time) │ │
│ └────────────────┘ └────────┬───────┘ └───────────────────┘ │
│ │ │
│ Skills: │ │
│ ├─ smart-routing ──────────┤ ┌────────────────────────────┐ │
│ └─ quota-management ───────┘ │ Routing Decision Logger │ │
│ └────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
│
▼ OmniRoute Gateway (internal)
/v1/chat/completions, /api/combos, /api/usage/quota
Quick Start
Agent Discovery
Every A2A-compatible agent exposes an Agent Card at /.well-known/agent.json:
curl http://localhost:20128/.well-known/agent.json
Response:
{
"name": "OmniRoute",
"description": "Intelligent AI gateway with auto-routing across 50+ providers",
"url": "http://localhost:20128/a2a",
"version": "1.8.1",
"capabilities": {
"streaming": true,
"pushNotifications": false
},
"skills": [
{
"id": "smart-routing",
"name": "Smart Routing",
"description": "Routes prompts through OmniRoute intelligent pipeline",
"tags": ["routing", "llm", "multi-provider", "cost-optimization"],
"examples": [
"Write a hello world in Python",
"Explain quantum computing using the cheapest provider"
]
},
{
"id": "quota-management",
"name": "Quota Management",
"description": "Natural-language queries about provider quotas",
"tags": ["quota", "analytics", "cost"],
"examples": [
"Which provider has the most quota remaining?",
"Suggest a free combo for coding"
]
}
],
"authentication": {
"schemes": ["bearer"],
"apiKeyHeader": "Authorization"
}
}
JSON-RPC 2.0 Methods
message/send — Synchronous Execution
Send a message to a skill and receive the complete response.
curl -X POST http://localhost:20128/a2a \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_KEY" \
-d '{
"jsonrpc": "2.0",
"id": "1",
"method": "message/send",
"params": {
"skill": "smart-routing",
"messages": [{"role": "user", "content": "Write a Python hello world"}],
"metadata": {"model": "auto", "combo": "fast-coding"}
}
}'
Response:
{
"jsonrpc": "2.0",
"id": "1",
"result": {
"task": { "id": "a1b2c3d4-...", "state": "completed" },
"artifacts": [{ "type": "text", "content": "print('Hello, World!')" }],
"metadata": {
"routing_explanation": "Selected claude-sonnet via provider \"anthropic\" (latency: 1200ms, cost: $0.0030)",
"cost_envelope": { "estimated": 0.005, "actual": 0.003, "currency": "USD" },
"resilience_trace": [
{ "event": "primary_selected", "provider": "anthropic", "timestamp": "2026-03-04T..." }
],
"policy_verdict": { "allowed": true, "reason": "within budget and quota limits" }
}
}
}
message/stream — SSE Streaming
Same as message/send but returns Server-Sent Events for real-time streaming.
curl -N -X POST http://localhost:20128/a2a \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_KEY" \
-d '{
"jsonrpc": "2.0",
"id": "1",
"method": "message/stream",
"params": {
"skill": "smart-routing",
"messages": [{"role": "user", "content": "Explain quantum computing"}]
}
}'
SSE Events:
data: {"jsonrpc":"2.0","method":"message/stream","params":{"task":{"id":"...","state":"working"},"chunk":{"type":"text","content":"Quantum computing..."}}}
: heartbeat 2026-03-04T21:00:00Z
data: {"jsonrpc":"2.0","method":"message/stream","params":{"task":{"id":"...","state":"completed"},"metadata":{...}}}
tasks/get — Query Task Status
curl -X POST http://localhost:20128/a2a \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_KEY" \
-d '{"jsonrpc":"2.0","id":"2","method":"tasks/get","params":{"taskId":"TASK_UUID"}}'
tasks/cancel — Cancel a Running Task
curl -X POST http://localhost:20128/a2a \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_KEY" \
-d '{"jsonrpc":"2.0","id":"3","method":"tasks/cancel","params":{"taskId":"TASK_UUID"}}'
Skills Reference
smart-routing
Routes prompts through OmniRoute's intelligent pipeline with full observability.
Parameters (in metadata):
| Parameter | Type | Default | Description |
|---|---|---|---|
model |
string |
"auto" |
Target model (e.g., claude-sonnet-4, gpt-4o, auto) |
combo |
string |
active combo | Specific combo to route through |
budget |
number |
none | Maximum cost in USD for this request |
role |
string |
none | Task role hint: coding, review, planning, analysis, debugging, documentation |
Returns:
| Field | Description |
|---|---|
artifacts[].content |
The LLM response text |
metadata.routing_explanation |
Human-readable explanation of routing decision |
metadata.cost_envelope |
Estimated vs actual cost with currency |
metadata.resilience_trace |
Array of events (primary_selected, fallback_needed, etc.) |
metadata.policy_verdict |
Whether the request was allowed and why |
quota-management
Answers natural-language queries about provider quotas.
Query types (inferred from message content):
| Query Pattern | Response Type |
|---|---|
Contains "ranking", "most quota", "best" |
Providers ranked by remaining quota |
Contains "free", "suggest" |
Lists free combos or suggests free-tier providers |
| Default | Full quota summary with warnings for low-quota providers |
Task Lifecycle
submitted ──→ working ──→ completed
──→ failed
──────────→ cancelled
| State | Description |
|---|---|
submitted |
Task created, queued for execution |
working |
Skill handler is executing |
completed |
Execution succeeded, artifacts available |
failed |
Execution failed or task expired (TTL: 5 min default) |
cancelled |
Cancelled by client via tasks/cancel |
- Terminal states:
completed,failed,cancelled(no further transitions) - Expired tasks in
submittedorworkingare auto-marked asfailed - Tasks are garbage-collected after 2× TTL
Client Examples
Python — Orchestrator Agent
"""
A2A Client — Python example.
Discovers OmniRoute agent, sends a task, and processes the result.
"""
import requests
import json
BASE_URL = "http://localhost:20128"
API_KEY = "your-api-key"
HEADERS = {
"Content-Type": "application/json",
"Authorization": f"Bearer {API_KEY}",
}
# 1. Discover agent capabilities
agent_card = requests.get(f"{BASE_URL}/.well-known/agent.json").json()
print(f"Agent: {agent_card['name']} v{agent_card['version']}")
print(f"Skills: {[s['id'] for s in agent_card['skills']]}")
# 2. Send a smart-routing task
response = requests.post(f"{BASE_URL}/a2a", headers=HEADERS, json={
"jsonrpc": "2.0",
"id": "task-1",
"method": "message/send",
"params": {
"skill": "smart-routing",
"messages": [{"role": "user", "content": "Write a Python quicksort implementation"}],
"metadata": {
"model": "auto",
"combo": "fast-coding",
"budget": 0.10,
}
}
})
result = response.json()["result"]
print(f"\n📝 Response: {result['artifacts'][0]['content'][:200]}...")
print(f"🔀 Routing: {result['metadata']['routing_explanation']}")
print(f"💰 Cost: ${result['metadata']['cost_envelope']['actual']}")
print(f"🛡️ Policy: {result['metadata']['policy_verdict']['reason']}")
# 3. Query quota status
quota_resp = requests.post(f"{BASE_URL}/a2a", headers=HEADERS, json={
"jsonrpc": "2.0",
"id": "task-2",
"method": "message/send",
"params": {
"skill": "quota-management",
"messages": [{"role": "user", "content": "Which provider has the most quota remaining?"}],
}
})
quota_result = quota_resp.json()["result"]
print(f"\n📊 Quota: {quota_result['artifacts'][0]['content']}")
TypeScript — Multi-Agent Orchestrator
/**
* A2A Client — TypeScript example.
* Shows agent discovery, task delegation, and streaming.
*/
const BASE_URL = "http://localhost:20128";
const API_KEY = "your-api-key";
interface JsonRpcResponse<T = any> {
jsonrpc: "2.0";
id: string | number;
result?: T;
error?: { code: number; message: string };
}
async function a2aCall<T>(method: string, params: Record<string, any>): Promise<T> {
const resp = await fetch(`${BASE_URL}/a2a`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${API_KEY}`,
},
body: JSON.stringify({
jsonrpc: "2.0",
id: `${method}-${Date.now()}`,
method,
params,
}),
});
const json: JsonRpcResponse<T> = await resp.json();
if (json.error) throw new Error(`[${json.error.code}] ${json.error.message}`);
return json.result!;
}
// ── Agent Discovery ──
const agentCard = await fetch(`${BASE_URL}/.well-known/agent.json`).then((r) => r.json());
console.log(`Connected to: ${agentCard.name} (${agentCard.skills.length} skills)`);
// ── Smart Routing: Send a coding task ──
const routingResult = await a2aCall("message/send", {
skill: "smart-routing",
messages: [{ role: "user", content: "Implement a Redis cache wrapper in TypeScript" }],
metadata: { model: "claude-sonnet-4", role: "coding" },
});
console.log("Response:", routingResult.artifacts[0].content);
console.log("Provider:", routingResult.metadata.routing_explanation);
// ── Quota Management: Find free alternatives ──
const quotaResult = await a2aCall("message/send", {
skill: "quota-management",
messages: [{ role: "user", content: "Suggest free combos for documentation" }],
});
console.log("Free combos:", quotaResult.artifacts[0].content);
// ── Streaming: Real-time response ──
const streamResp = await fetch(`${BASE_URL}/a2a`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${API_KEY}`,
},
body: JSON.stringify({
jsonrpc: "2.0",
id: "stream-1",
method: "message/stream",
params: {
skill: "smart-routing",
messages: [{ role: "user", content: "Explain microservices architecture" }],
},
}),
});
const reader = streamResp.body!.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
for (const line of chunk.split("\n")) {
if (line.startsWith("data: ")) {
const event = JSON.parse(line.slice(6));
if (event.params.chunk) {
process.stdout.write(event.params.chunk.content);
}
if (event.params.task.state === "completed") {
console.log("\n✅ Stream completed");
}
}
}
}
Python — LangChain A2A Integration
"""
LangChain integration — Use OmniRoute A2A as a custom LLM.
"""
from langchain.llms.base import BaseLLM
from langchain.schema import LLMResult, Generation
import requests
from typing import List, Optional
class OmniRouteA2A(BaseLLM):
base_url: str = "http://localhost:20128"
api_key: str = ""
model: str = "auto"
combo: Optional[str] = None
@property
def _llm_type(self) -> str:
return "omniroute-a2a"
def _call(self, prompt: str, stop: Optional[List[str]] = None, **kwargs) -> str:
response = requests.post(
f"{self.base_url}/a2a",
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {self.api_key}",
},
json={
"jsonrpc": "2.0",
"id": "langchain-1",
"method": "message/send",
"params": {
"skill": "smart-routing",
"messages": [{"role": "user", "content": prompt}],
"metadata": {
"model": self.model,
**({"combo": self.combo} if self.combo else {}),
},
},
},
)
result = response.json()["result"]
return result["artifacts"][0]["content"]
def _generate(self, prompts: List[str], stop=None, **kwargs) -> LLMResult:
return LLMResult(
generations=[[Generation(text=self._call(p, stop))] for p in prompts]
)
# Usage
llm = OmniRouteA2A(
base_url="http://localhost:20128",
api_key="your-key",
model="auto",
combo="fast-coding",
)
result = llm("Write a Python function to merge two sorted lists")
print(result)
Go — A2A Client
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
const baseURL = "http://localhost:20128"
const apiKey = "your-api-key"
type JsonRpcRequest struct {
Jsonrpc string `json:"jsonrpc"`
ID string `json:"id"`
Method string `json:"method"`
Params interface{} `json:"params"`
}
type JsonRpcResponse struct {
Jsonrpc string `json:"jsonrpc"`
ID string `json:"id"`
Result interface{} `json:"result"`
Error *struct {
Code int `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
func a2aCall(method string, params interface{}) (*JsonRpcResponse, error) {
body, _ := json.Marshal(JsonRpcRequest{
Jsonrpc: "2.0",
ID: "go-1",
Method: method,
Params: params,
})
req, _ := http.NewRequest("POST", baseURL+"/a2a", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+apiKey)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
var result JsonRpcResponse
json.Unmarshal(data, &result)
return &result, nil
}
func main() {
// Discover agent
resp, _ := http.Get(baseURL + "/.well-known/agent.json")
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println("Agent Card:", string(body))
// Send smart-routing task
result, _ := a2aCall("message/send", map[string]interface{}{
"skill": "smart-routing",
"messages": []map[string]string{{"role": "user", "content": "Hello from Go!"}},
"metadata": map[string]interface{}{"model": "auto"},
})
out, _ := json.MarshalIndent(result.Result, "", " ")
fmt.Println("Result:", string(out))
}
Use Cases
🤖 Use Case 1: Multi-Agent Coding Pipeline
An orchestrator agent delegates code generation to OmniRoute, then passes the output to a review agent.
def coding_pipeline(task: str):
# Step 1: Generate code via OmniRoute A2A
code_result = a2a_send("smart-routing", [
{"role": "user", "content": f"Write production-quality code: {task}"}
], metadata={"model": "auto", "role": "coding"})
code = code_result["artifacts"][0]["content"]
# Step 2: Review the code via OmniRoute A2A (different model)
review_result = a2a_send("smart-routing", [
{"role": "user", "content": f"Review this code for bugs and improvements:\n\n{code}"}
], metadata={"model": "auto", "role": "review"})
review = review_result["artifacts"][0]["content"]
# Step 3: Check costs
print(f"Code cost: ${code_result['metadata']['cost_envelope']['actual']}")
print(f"Review cost: ${review_result['metadata']['cost_envelope']['actual']}")
return {"code": code, "review": review}
💡 Use Case 2: Quota-Aware Agent Swarm
Multiple agents share quota through OmniRoute, using the quota skill to coordinate.
async def quota_aware_agent(agent_name: str, task: str):
# Check quota before starting
quota = a2a_send("quota-management", [
{"role": "user", "content": "Which provider has the most quota remaining?"}
])
print(f"[{agent_name}] {quota['artifacts'][0]['content']}")
# Send request with budget constraint
result = a2a_send("smart-routing", [
{"role": "user", "content": task}
], metadata={"budget": 0.05})
policy = result["metadata"]["policy_verdict"]
if not policy["allowed"]:
print(f"[{agent_name}] ⚠️ Budget exceeded: {policy['reason']}")
# Fall back to free combo
quota = a2a_send("quota-management", [
{"role": "user", "content": "Suggest free combos"}
])
print(f"[{agent_name}] Free alternatives: {quota['artifacts'][0]['content']}")
return result
📊 Use Case 3: Real-Time Streaming Dashboard
A monitoring agent streams responses and displays progress in real-time.
async function streamingDashboard(prompt: string) {
const response = await fetch(`${BASE_URL}/a2a`, {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${API_KEY}` },
body: JSON.stringify({
jsonrpc: "2.0",
id: "dash-1",
method: "message/stream",
params: { skill: "smart-routing", messages: [{ role: "user", content: prompt }] },
}),
});
let totalChunks = 0;
const reader = response.body!.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
for (const line of decoder.decode(value).split("\n")) {
if (line.startsWith("data: ")) {
const event = JSON.parse(line.slice(6));
const state = event.params.task.state;
if (state === "working" && event.params.chunk) {
totalChunks++;
process.stdout.write(
`\r[Chunk ${totalChunks}] ${event.params.chunk.content.slice(0, 50)}...`
);
}
if (state === "completed") {
const meta = event.params.metadata;
console.log(
`\n✅ Done | Cost: $${meta?.cost_envelope?.actual || 0} | Route: ${meta?.routing_explanation || "N/A"}`
);
}
if (state === "failed") {
console.error(`\n❌ Failed: ${event.params.metadata?.error}`);
}
}
}
}
}
🔁 Use Case 4: Task Polling Pattern
For long-running tasks, poll the task status instead of waiting synchronously.
import time
def poll_task(task_id: str, timeout: int = 60):
"""Poll task status until completion or timeout."""
start = time.time()
while time.time() - start < timeout:
result = requests.post(f"{BASE_URL}/a2a", headers=HEADERS, json={
"jsonrpc": "2.0",
"id": "poll-1",
"method": "tasks/get",
"params": {"taskId": task_id},
}).json()
task = result["result"]["task"]
state = task["state"]
print(f" Task {task_id[:8]}... state={state}")
if state in ("completed", "failed", "cancelled"):
return task
time.sleep(2)
# Timeout — cancel the task
requests.post(f"{BASE_URL}/a2a", headers=HEADERS, json={
"jsonrpc": "2.0",
"id": "cancel-1",
"method": "tasks/cancel",
"params": {"taskId": task_id},
})
raise TimeoutError(f"Task {task_id} timed out after {timeout}s")
Error Codes
| Code | Constant | Meaning |
|---|---|---|
| -32700 | — | Parse error (invalid JSON) |
| -32600 | INVALID_REQUEST |
Invalid JSON-RPC request or unauthorized |
| -32601 | METHOD_NOT_FOUND |
Unknown method or skill |
| -32602 | INVALID_PARAMS |
Missing or invalid parameters |
| -32603 | INTERNAL_ERROR |
Skill execution failed |
| -32001 | TASK_NOT_FOUND |
Task ID not found |
| -32002 | TASK_ALREADY_COMPLETED |
Cannot modify a completed task |
| -32003 | UNAUTHORIZED |
Invalid or missing API key |
| -32004 | BUDGET_EXCEEDED |
Request exceeds configured budget |
| -32005 | PROVIDER_UNAVAILABLE |
No available providers |
Authentication
All /a2a requests require a Bearer token via the Authorization header:
Authorization: Bearer YOUR_OMNIROUTE_API_KEY
If no API key is configured on the server (OMNIROUTE_API_KEY is empty), authentication is bypassed.
File Structure
src/lib/a2a/
├── taskManager.ts # Task lifecycle (create/update/cancel/list), TTL, cleanup
├── taskExecution.ts # Generic task executor with state management
├── streaming.ts # SSE stream formatting, heartbeat, chunk/completion events
├── routingLogger.ts # Routing decision logger (stats, history, retention)
└── skills/
├── smartRouting.ts # Smart routing skill (routes via /v1/chat/completions)
└── quotaManagement.ts # Quota management skill (natural-language quota queries)
src/app/a2a/
└── route.ts # Next.js API route handler (JSON-RPC 2.0 dispatch)
open-sse/mcp-server/
└── schemas/a2a.ts # Zod schemas (AgentCard, Task, JSON-RPC, SSE events)
Comparison: MCP vs A2A
| Feature | MCP Server | A2A Server |
|---|---|---|
| Protocol | Model Context Protocol | Agent-to-Agent Protocol v0.3 |
| Transport | stdio / HTTP | HTTP (JSON-RPC 2.0) |
| Discovery | Tool listing via MCP | /.well-known/agent.json |
| Granularity | 16 individual tools | 2 high-level skills |
| Best for | IDE agents (Cursor, VS Code) | Multi-agent systems (LangChain, CrewAI) |
| Streaming | Not supported | SSE via message/stream |
| Task tracking | No | Full lifecycle (submitted → completed) |
| Observability | Audit log per tool call | Cost envelope + resilience trace + policy verdict |
License
Part of OmniRoute — MIT License.