From 2fb39467a71ed921508a4c9e7421598cb1bc6164 Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Wed, 6 May 2026 14:54:21 -0700 Subject: [PATCH] feat(ui/agents): add cloud-agents API client Proxy-routed fetches for agents, sessions, runs, conversation, and followup. Mock-aware: short-circuits to mock-agents.ts when NEXT_PUBLIC_USE_MOCK_AGENTS=true. Centralizes the SSE URL shape via buildRunEventStreamUrl so the hook just opens the EventSource. --- .../src/lib/cloud-agents-client.ts | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 ui/litellm-dashboard/src/lib/cloud-agents-client.ts diff --git a/ui/litellm-dashboard/src/lib/cloud-agents-client.ts b/ui/litellm-dashboard/src/lib/cloud-agents-client.ts new file mode 100644 index 00000000000..21a38a4d360 --- /dev/null +++ b/ui/litellm-dashboard/src/lib/cloud-agents-client.ts @@ -0,0 +1,171 @@ +/** + * Client for the cloud-agents (Cursor SDK) API. + * + * Routes through `proxyBaseUrl` so all calls hit the LiteLLM proxy on the same + * origin. When `NEXT_PUBLIC_USE_MOCK_AGENTS=true`, every method short-circuits + * to `mock-agents.ts` so the dashboard can develop without Epic A merged. + * + * When Epic A lands and exposes `/v1/agents`, `/v1/sessions`, etc., flip the + * env var off and the real fetches kick in. No component changes required. + */ +import { getProxyBaseUrl } from "@/components/networking"; +import { + isMockEnabled, + mockCreateAgent, + mockCreateSession, + mockGetAgent, + mockGetConversation, + mockGetRun, + mockGetSession, + mockListAgents, + mockListSessions, + mockSendFollowup, +} from "@/lib/mock-agents"; +import type { + CloudAgent, + CloudAgentConversationMessage, + CloudAgentRun, + CloudAgentSession, +} from "@/types/cloud-agents"; + +function authHeaders(accessToken: string | null): HeadersInit { + const headers: HeadersInit = { "Content-Type": "application/json" }; + if (accessToken) { + headers["Authorization"] = `Bearer ${accessToken}`; + } + return headers; +} + +async function jsonOrThrow(res: Response, label: string): Promise { + if (!res.ok) { + const text = await res.text().catch(() => ""); + throw new Error(`${label} failed: ${res.status} ${text}`); + } + return (await res.json()) as T; +} + +/* ---------- agents ---------- */ + +export async function listCloudAgents(accessToken: string | null): Promise { + if (isMockEnabled()) return mockListAgents(); + const res = await fetch(`${getProxyBaseUrl()}/v1/agents`, { headers: authHeaders(accessToken) }); + const body = await jsonOrThrow<{ agents: CloudAgent[] }>(res, "listCloudAgents"); + return body.agents; +} + +export async function getCloudAgent( + accessToken: string | null, + agentId: string, +): Promise { + if (isMockEnabled()) return mockGetAgent(agentId); + const res = await fetch(`${getProxyBaseUrl()}/v1/agents/${agentId}`, { + headers: authHeaders(accessToken), + }); + if (res.status === 404) return null; + return jsonOrThrow(res, "getCloudAgent"); +} + +export async function createCloudAgent( + accessToken: string | null, + input: { name: string; model: string; system_prompt?: string }, +): Promise { + if (isMockEnabled()) return mockCreateAgent(input); + const res = await fetch(`${getProxyBaseUrl()}/v1/agents`, { + method: "POST", + headers: authHeaders(accessToken), + body: JSON.stringify(input), + }); + return jsonOrThrow(res, "createCloudAgent"); +} + +/* ---------- sessions ---------- */ + +export async function listCloudSessions( + accessToken: string | null, + agentId: string, +): Promise { + if (isMockEnabled()) return mockListSessions(agentId); + const res = await fetch(`${getProxyBaseUrl()}/v1/sessions?agent_id=${encodeURIComponent(agentId)}`, { + headers: authHeaders(accessToken), + }); + const body = await jsonOrThrow<{ sessions: CloudAgentSession[] }>(res, "listCloudSessions"); + return body.sessions; +} + +export async function getCloudSession( + accessToken: string | null, + sessionId: string, +): Promise { + if (isMockEnabled()) return mockGetSession(sessionId); + const res = await fetch(`${getProxyBaseUrl()}/v1/sessions/${sessionId}`, { + headers: authHeaders(accessToken), + }); + if (res.status === 404) return null; + return jsonOrThrow(res, "getCloudSession"); +} + +export async function createCloudSession( + accessToken: string | null, + input: { agent_id: string; repo_url: string }, +): Promise { + if (isMockEnabled()) return mockCreateSession(input); + const res = await fetch(`${getProxyBaseUrl()}/v1/sessions`, { + method: "POST", + headers: authHeaders(accessToken), + body: JSON.stringify(input), + }); + return jsonOrThrow(res, "createCloudSession"); +} + +export async function getSessionConversation( + accessToken: string | null, + sessionId: string, +): Promise { + if (isMockEnabled()) return mockGetConversation(sessionId); + const res = await fetch(`${getProxyBaseUrl()}/v1/sessions/${sessionId}/conversation`, { + headers: authHeaders(accessToken), + }); + const body = await jsonOrThrow<{ messages: CloudAgentConversationMessage[] }>( + res, + "getSessionConversation", + ); + return body.messages; +} + +export async function sendSessionFollowup( + accessToken: string | null, + sessionId: string, + content: string, +): Promise { + if (isMockEnabled()) return mockSendFollowup(sessionId, content); + const res = await fetch(`${getProxyBaseUrl()}/v1/sessions/${sessionId}/followup`, { + method: "POST", + headers: authHeaders(accessToken), + body: JSON.stringify({ content }), + }); + return jsonOrThrow(res, "sendSessionFollowup"); +} + +/* ---------- runs ---------- */ + +export async function getCloudRun( + accessToken: string | null, + runId: string, +): Promise { + if (isMockEnabled()) return mockGetRun(runId); + const res = await fetch(`${getProxyBaseUrl()}/v1/runs/${runId}`, { + headers: authHeaders(accessToken), + }); + if (res.status === 404) return null; + return jsonOrThrow(res, "getCloudRun"); +} + +/** + * Build the SSE URL for a run's event stream. The hook (`useSessionEventStream`) + * is what actually opens the EventSource — this just centralizes the URL shape + * so the route can change in one place. + */ +export function buildRunEventStreamUrl(sessionId: string, runId: string, sinceSeq?: number): string { + const base = `${getProxyBaseUrl()}/v1/sessions/${sessionId}/runs/${runId}/events`; + return sinceSeq !== undefined ? `${base}?since_seq=${sinceSeq}` : base; +}