The platform no longer exposes a public /api/* REST surface. The original HTTP route handlers are preserved verbatim as the single source of truth inside an internal, never-socket-bound FastAPI app (rpc_app), and are now reached only via RPC-over-WebSocket dispatch. - realtime router: anonymous connections allowed only to /api/auth/login and /api/auth/signup; all other RPCs/subscribes refused until token auth. - RPC dispatch re-injects the bearer token and X-Request-ID, then forwards through httpx.ASGITransport against the internal app (no network hop). - Public app now serves only /, /health, /health/ready and /api/ws; docs/openapi disabled. - Frontend: apiRequest routes /api/* to ws.rpc(); /health probes keep fetch. RealtimeClient gains rpc() with id correlation, timeout and AbortSignal. - Tests repointed to get_rpc_app(); anonymous-auth behaviour covered. Result: zero REST on the public surface; 264 backend tests + frontend typecheck pass.
601 lines
23 KiB
TypeScript
601 lines
23 KiB
TypeScript
/**
|
|
* Typed API client.
|
|
*
|
|
* One place knows the base URL, the bearer token and the error envelope. Every
|
|
* call returns parsed JSON or throws `ApiError`, so components never have to
|
|
* inspect status codes.
|
|
*/
|
|
|
|
import type { ApiErrorBody } from "@agy/contracts";
|
|
import { getRealtimeClient } from "@/lib/ws";
|
|
|
|
/**
|
|
* Resolves the API base URL.
|
|
* In a browser:
|
|
* - If NEXT_PUBLIC_API_URL is configured and not pointing to localhost on a remote host, use it.
|
|
* - Otherwise default to relative path "" so requests go directly to the current origin (e.g. /api/*)
|
|
* behind reverse proxies (Caddy), preventing ERR_CONNECTION_REFUSED on clients.
|
|
* In SSR / Node / tests:
|
|
* - Fall back to NEXT_PUBLIC_API_URL or "http://localhost:8000".
|
|
*/
|
|
export function getApiBaseUrl(): string {
|
|
const configured = process.env.NEXT_PUBLIC_API_URL?.trim();
|
|
if (typeof window !== "undefined") {
|
|
const isLocalhost =
|
|
window.location.hostname === "localhost" ||
|
|
window.location.hostname === "127.0.0.1";
|
|
if (!configured || (!isLocalhost && configured.includes("localhost"))) {
|
|
return "";
|
|
}
|
|
return configured;
|
|
}
|
|
return configured || "http://localhost:8000";
|
|
}
|
|
|
|
export const API_BASE_URL = getApiBaseUrl();
|
|
|
|
export class ApiError extends Error {
|
|
readonly status: number;
|
|
readonly code: string;
|
|
readonly details?: unknown;
|
|
/**
|
|
* The server's correlation id for this failure. It is also the
|
|
* `X-Request-ID` response header, and it identifies one row in the platform
|
|
* error console — so a bug report that includes it is immediately actionable.
|
|
*/
|
|
readonly requestId?: string;
|
|
|
|
constructor(
|
|
status: number,
|
|
code: string,
|
|
message: string,
|
|
details?: unknown,
|
|
requestId?: string,
|
|
) {
|
|
super(message);
|
|
this.name = "ApiError";
|
|
this.status = status;
|
|
this.code = code;
|
|
this.details = details;
|
|
this.requestId = requestId;
|
|
}
|
|
}
|
|
|
|
const TOKEN_STORAGE_KEY = "agy.access_token";
|
|
|
|
export function getStoredToken(): string | null {
|
|
if (typeof window === "undefined") return null;
|
|
try {
|
|
return window.localStorage.getItem(TOKEN_STORAGE_KEY);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function setStoredToken(token: string | null): void {
|
|
if (typeof window === "undefined") return;
|
|
try {
|
|
if (token) window.localStorage.setItem(TOKEN_STORAGE_KEY, token);
|
|
else window.localStorage.removeItem(TOKEN_STORAGE_KEY);
|
|
} catch {
|
|
/* storage disabled — the session simply will not survive a reload */
|
|
}
|
|
}
|
|
|
|
export interface RequestOptions {
|
|
method?: "GET" | "POST" | "PATCH" | "PUT" | "DELETE";
|
|
body?: unknown;
|
|
token?: string | null;
|
|
signal?: AbortSignal;
|
|
}
|
|
|
|
export async function apiRequest<T>(path: string, options: RequestOptions = {}): Promise<T> {
|
|
const { method = "GET", body, signal } = options;
|
|
|
|
// Liveness and readiness probes stay on plain HTTP: they answer during a DB
|
|
// outage and are used by deploy orchestration (and the browser status check),
|
|
// so they must not depend on the authenticated socket. Every application call
|
|
// travels over the WebSocket as an RPC instead (see lib/ws.ts) — one
|
|
// connection for both live feeds and request/response.
|
|
if (path === "/health" || path === "/health/ready") {
|
|
return fetchHealth<T>(path, method, body, signal);
|
|
}
|
|
|
|
const client = getRealtimeClient();
|
|
return client.rpc<T>(method, path, body === undefined ? null : body, { signal });
|
|
}
|
|
|
|
/** Plain-HTTP implementation kept only for the two health probes. */
|
|
async function fetchHealth<T>(
|
|
path: string,
|
|
method: string,
|
|
body: unknown,
|
|
signal?: AbortSignal,
|
|
): Promise<T> {
|
|
const headers: Record<string, string> = { Accept: "application/json" };
|
|
if (body !== undefined) headers["Content-Type"] = "application/json";
|
|
|
|
let response: Response;
|
|
try {
|
|
response = await fetch(`${getApiBaseUrl()}${path}`, {
|
|
method,
|
|
headers,
|
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
signal,
|
|
cache: "no-store",
|
|
});
|
|
} catch (error) {
|
|
throw new ApiError(0, "network_error", "ارتباط با سرور برقرار نشد.", error);
|
|
}
|
|
|
|
if (response.status === 204) return undefined as T;
|
|
|
|
const text = await response.text();
|
|
const payload = text ? (JSON.parse(text) as unknown) : null;
|
|
|
|
if (!response.ok) {
|
|
const envelope = payload as ApiErrorBody | null;
|
|
const code = envelope?.error?.code ?? "http_error";
|
|
const message = envelope?.error?.message ?? `خطای سرور (${response.status})`;
|
|
const requestId =
|
|
envelope?.error?.request_id ?? response.headers.get("X-Request-ID") ?? undefined;
|
|
throw new ApiError(response.status, code, message, envelope?.error?.details, requestId);
|
|
}
|
|
|
|
return payload as T;
|
|
}
|
|
|
|
export const api = {
|
|
health: () => apiRequest<{ status: string }>("/health"),
|
|
readiness: () => apiRequest<import("@agy/contracts").ReadinessResponse>("/health/ready"),
|
|
signup: (body: { email: string; password: string; name?: string }) =>
|
|
apiRequest<import("@agy/contracts").TokenResponse>("/api/auth/signup", {
|
|
method: "POST",
|
|
body,
|
|
}),
|
|
login: (body: { email: string; password: string }) =>
|
|
apiRequest<import("@agy/contracts").TokenResponse>("/api/auth/login", {
|
|
method: "POST",
|
|
body,
|
|
}),
|
|
logout: () => apiRequest<void>("/api/auth/logout", { method: "POST" }),
|
|
me: () => apiRequest<import("@agy/contracts").MeResponse>("/api/auth/me"),
|
|
listOrganizations: () =>
|
|
apiRequest<import("@agy/contracts").OrganizationWithRole[]>("/api/organizations"),
|
|
createOrganization: (name: string) =>
|
|
apiRequest<import("@agy/contracts").OrganizationWithRole>("/api/organizations", {
|
|
method: "POST",
|
|
body: { name },
|
|
}),
|
|
listMembers: (orgId: string) =>
|
|
apiRequest<import("@agy/contracts").Member[]>(`/api/organizations/${orgId}/members`),
|
|
createInvite: (orgId: string, body: { email: string; role: import("@agy/contracts").Role }) =>
|
|
apiRequest<import("@agy/contracts").Invite>(`/api/organizations/${orgId}/invites`, {
|
|
method: "POST",
|
|
body,
|
|
}),
|
|
updateMember: (
|
|
orgId: string,
|
|
memberId: string,
|
|
body: {
|
|
role?: import("@agy/contracts").Role;
|
|
status?: import("@agy/contracts").MembershipStatus;
|
|
},
|
|
) =>
|
|
apiRequest<import("@agy/contracts").Member>(
|
|
`/api/organizations/${orgId}/members/${memberId}`,
|
|
{ method: "PATCH", body },
|
|
),
|
|
removeMember: (orgId: string, memberId: string) =>
|
|
apiRequest<void>(`/api/organizations/${orgId}/members/${memberId}`, {
|
|
method: "DELETE",
|
|
}),
|
|
listAudit: (orgId: string) =>
|
|
apiRequest<import("@agy/contracts").AuditEvent[]>(`/api/organizations/${orgId}/audit`),
|
|
|
|
// --- projects -------------------------------------------------------------
|
|
listProjects: (orgId: string) =>
|
|
apiRequest<import("@agy/contracts").Project[]>(`/api/organizations/${orgId}/projects`),
|
|
createProject: (orgId: string, body: { name: string; description?: string }) =>
|
|
apiRequest<import("@agy/contracts").Project>(`/api/organizations/${orgId}/projects`, {
|
|
method: "POST",
|
|
body,
|
|
}),
|
|
getProject: (orgId: string, projectId: string) =>
|
|
apiRequest<import("@agy/contracts").Project>(
|
|
`/api/organizations/${orgId}/projects/${projectId}`,
|
|
),
|
|
|
|
// --- repository -----------------------------------------------------------
|
|
ensureRepo: (orgId: string, projectId: string) =>
|
|
apiRequest<import("@agy/contracts").Repository>(
|
|
`/api/organizations/${orgId}/projects/${projectId}/repo/ensure`,
|
|
{ method: "POST" },
|
|
),
|
|
getRepo: (orgId: string, projectId: string) =>
|
|
apiRequest<import("@agy/contracts").Repository>(
|
|
`/api/organizations/${orgId}/projects/${projectId}/repo`,
|
|
),
|
|
getRepoStatus: (orgId: string, projectId: string) =>
|
|
apiRequest<import("@agy/contracts").RepoStatus>(
|
|
`/api/organizations/${orgId}/projects/${projectId}/repo/status`,
|
|
),
|
|
|
|
// --- preview domains ------------------------------------------------------
|
|
listPreviewDomains: (orgId: string, projectId: string) =>
|
|
apiRequest<import("@agy/contracts").PreviewDomain[]>(
|
|
`/api/organizations/${orgId}/projects/${projectId}/preview/domains`,
|
|
),
|
|
createPreviewDomain: (
|
|
orgId: string,
|
|
projectId: string,
|
|
body: { subdomain: string; port?: number },
|
|
) =>
|
|
apiRequest<import("@agy/contracts").PreviewDomain>(
|
|
`/api/organizations/${orgId}/projects/${projectId}/preview/domains`,
|
|
{ method: "POST", body },
|
|
),
|
|
deletePreviewDomain: (orgId: string, projectId: string, domainId: string) =>
|
|
apiRequest<void>(
|
|
`/api/organizations/${orgId}/projects/${projectId}/preview/domains/${domainId}`,
|
|
{ method: "DELETE" },
|
|
),
|
|
|
|
// --- workforce ------------------------------------------------------------
|
|
listAgentTemplates: (orgId: string) =>
|
|
apiRequest<import("@agy/contracts").AgentTemplate[]>(
|
|
`/api/organizations/${orgId}/agent-templates`,
|
|
),
|
|
listRoutingChoices: (orgId: string) =>
|
|
apiRequest<import("@agy/contracts").RoutingChoice[]>(
|
|
`/api/organizations/${orgId}/routing-choices`,
|
|
),
|
|
listEmployees: (orgId: string) =>
|
|
apiRequest<import("@agy/contracts").Employee[]>(`/api/organizations/${orgId}/employees`),
|
|
createEmployee: (
|
|
orgId: string,
|
|
body: {
|
|
name: string;
|
|
role_key: import("@agy/contracts").RoleKey;
|
|
template_id?: string | null;
|
|
project_id?: string | null;
|
|
routing_policy_id?: string | null;
|
|
},
|
|
) =>
|
|
apiRequest<import("@agy/contracts").Employee>(`/api/organizations/${orgId}/employees`, {
|
|
method: "POST",
|
|
body,
|
|
}),
|
|
getEmployee: (orgId: string, employeeId: string) =>
|
|
apiRequest<import("@agy/contracts").EmployeeDetail>(
|
|
`/api/organizations/${orgId}/employees/${employeeId}`,
|
|
),
|
|
updateEmployee: (
|
|
orgId: string,
|
|
employeeId: string,
|
|
body: {
|
|
name?: string;
|
|
status?: import("@agy/contracts").EmployeeStatus;
|
|
project_id?: string | null;
|
|
routing_policy_id?: string | null;
|
|
},
|
|
) =>
|
|
apiRequest<import("@agy/contracts").Employee>(
|
|
`/api/organizations/${orgId}/employees/${employeeId}`,
|
|
{ method: "PATCH", body },
|
|
),
|
|
addPromptVersion: (orgId: string, employeeId: string, content: string) =>
|
|
apiRequest<import("@agy/contracts").PromptVersion>(
|
|
`/api/organizations/${orgId}/employees/${employeeId}/prompt-versions`,
|
|
{ method: "POST", body: { content } },
|
|
),
|
|
|
|
// --- manager chat ---------------------------------------------------------
|
|
listThreads: (orgId: string) =>
|
|
apiRequest<import("@agy/contracts").Thread[]>(`/api/organizations/${orgId}/threads`),
|
|
createThread: (orgId: string, body: { title?: string; project_id?: string | null }) =>
|
|
apiRequest<import("@agy/contracts").Thread>(`/api/organizations/${orgId}/threads`, {
|
|
method: "POST",
|
|
body,
|
|
}),
|
|
getThread: (orgId: string, threadId: string) =>
|
|
apiRequest<import("@agy/contracts").ThreadDetail>(
|
|
`/api/organizations/${orgId}/threads/${threadId}`,
|
|
),
|
|
postMessage: (orgId: string, threadId: string, body: { content: string; employee_id?: string | null }) =>
|
|
apiRequest<import("@agy/contracts").PostMessageResponse>(
|
|
`/api/organizations/${orgId}/threads/${threadId}/messages`,
|
|
{ method: "POST", body },
|
|
),
|
|
|
|
// --- tasks & runs ---------------------------------------------------------
|
|
listTasks: (orgId: string, status?: string) =>
|
|
apiRequest<import("@agy/contracts").Task[]>(
|
|
`/api/organizations/${orgId}/tasks${status ? `?status=${status}` : ""}`,
|
|
),
|
|
createTask: (
|
|
orgId: string,
|
|
body: {
|
|
title: string;
|
|
description?: string;
|
|
project_id?: string | null;
|
|
thread_id?: string | null;
|
|
assigned_employee_id?: string | null;
|
|
},
|
|
) =>
|
|
apiRequest<import("@agy/contracts").Task>(`/api/organizations/${orgId}/tasks`, {
|
|
method: "POST",
|
|
body,
|
|
}),
|
|
getTask: (orgId: string, taskId: string) =>
|
|
apiRequest<import("@agy/contracts").TaskDetail>(
|
|
`/api/organizations/${orgId}/tasks/${taskId}`,
|
|
),
|
|
startRun: (orgId: string, taskId: string, employeeId?: string | null) =>
|
|
apiRequest<import("@agy/contracts").RunDetail>(
|
|
`/api/organizations/${orgId}/tasks/${taskId}/runs`,
|
|
{ method: "POST", body: { employee_id: employeeId ?? null } },
|
|
),
|
|
getRun: (orgId: string, runId: string) =>
|
|
apiRequest<import("@agy/contracts").RunDetail>(`/api/organizations/${orgId}/runs/${runId}`),
|
|
cancelRun: (orgId: string, runId: string) =>
|
|
apiRequest<import("@agy/contracts").RunDetail>(
|
|
`/api/organizations/${orgId}/runs/${runId}/cancel`,
|
|
{ method: "POST" },
|
|
),
|
|
listPendingApprovals: (orgId: string) =>
|
|
apiRequest<import("@agy/contracts").ToolCall[]>(
|
|
`/api/organizations/${orgId}/approvals/pending`,
|
|
),
|
|
|
|
// --- billing --------------------------------------------------------------
|
|
billingSummary: (orgId: string) =>
|
|
apiRequest<import("@agy/contracts").BillingSummary>(
|
|
`/api/organizations/${orgId}/billing/summary`,
|
|
),
|
|
billingUsageEvents: (orgId: string, limit = 100) =>
|
|
apiRequest<import("@agy/contracts").UsageEvent[]>(
|
|
`/api/organizations/${orgId}/billing/usage-events?limit=${limit}`,
|
|
),
|
|
billingInvoices: (orgId: string) =>
|
|
apiRequest<import("@agy/contracts").Invoice[]>(
|
|
`/api/organizations/${orgId}/billing/invoices`,
|
|
),
|
|
billingPayments: (orgId: string) =>
|
|
apiRequest<import("@agy/contracts").Payment[]>(
|
|
`/api/organizations/${orgId}/billing/payments`,
|
|
),
|
|
billingAddCredit: (orgId: string, amount: number) =>
|
|
apiRequest<import("@agy/contracts").Wallet>(
|
|
`/api/organizations/${orgId}/billing/add-credit`,
|
|
{ method: "POST", body: { amount } },
|
|
),
|
|
billingCheckout: (orgId: string, amount: number) =>
|
|
apiRequest<{ payment_id: string; checkout_url: string; amount: number }>(
|
|
`/api/organizations/${orgId}/billing/checkout`,
|
|
{ method: "POST", body: { amount } },
|
|
),
|
|
|
|
// --- tools & approvals ----------------------------------------------------
|
|
listTools: (orgId: string) =>
|
|
apiRequest<import("@agy/contracts").ToolSpec[]>(`/api/organizations/${orgId}/tools`),
|
|
setToolPermission: (orgId: string, employeeId: string, toolKey: string, allowed: boolean) =>
|
|
apiRequest<import("@agy/contracts").ToolPermission>(
|
|
`/api/organizations/${orgId}/employees/${employeeId}/tools/${encodeURIComponent(toolKey)}`,
|
|
{ method: "PUT", body: { allowed } },
|
|
),
|
|
decideApproval: (orgId: string, toolCallId: string, approve: boolean, note = "") =>
|
|
apiRequest<import("@agy/contracts").ToolCall>(
|
|
`/api/organizations/${orgId}/approvals/${toolCallId}/decide`,
|
|
{ method: "POST", body: { approve, note } },
|
|
),
|
|
|
|
// --- sandbox --------------------------------------------------------------
|
|
// Every call here is metered into the usage ledger server-side. The client
|
|
// never sends a timeout or an output cap: limits come from policy only.
|
|
sandboxTemplates: (orgId: string) =>
|
|
apiRequest<import("@agy/contracts").SandboxTemplate[]>(
|
|
`/api/organizations/${orgId}/sandbox/templates`,
|
|
),
|
|
sandboxLimits: (orgId: string, projectId?: string) =>
|
|
apiRequest<import("@agy/contracts").SandboxResourceLimit>(
|
|
`/api/organizations/${orgId}/sandbox/limits${
|
|
projectId ? `?project_id=${encodeURIComponent(projectId)}` : ""
|
|
}`,
|
|
),
|
|
sandboxWorkspaces: (orgId: string, projectId: string) =>
|
|
apiRequest<import("@agy/contracts").SandboxWorkspace[]>(
|
|
`/api/organizations/${orgId}/sandbox/workspaces?project_id=${encodeURIComponent(projectId)}`,
|
|
),
|
|
sandboxCreateWorkspace: (orgId: string, body: { project_id: string; template_id: string }) =>
|
|
apiRequest<import("@agy/contracts").SandboxWorkspace>(
|
|
`/api/organizations/${orgId}/sandbox/workspaces`,
|
|
{ method: "POST", body },
|
|
),
|
|
sandboxDestroyWorkspace: (orgId: string, workspaceId: string) =>
|
|
apiRequest<import("@agy/contracts").SandboxWorkspace>(
|
|
`/api/organizations/${orgId}/sandbox/workspaces/${workspaceId}`,
|
|
{ method: "DELETE" },
|
|
),
|
|
sandboxStartSession: (orgId: string, workspaceId: string) =>
|
|
apiRequest<import("@agy/contracts").SandboxSession>(
|
|
`/api/organizations/${orgId}/sandbox/sessions`,
|
|
{ method: "POST", body: { workspace_id: workspaceId } },
|
|
),
|
|
sandboxStopSession: (orgId: string, sessionId: string) =>
|
|
apiRequest<import("@agy/contracts").SandboxSession>(
|
|
`/api/organizations/${orgId}/sandbox/sessions/${sessionId}/stop`,
|
|
{ method: "POST" },
|
|
),
|
|
sandboxRunCommand: (orgId: string, sessionId: string, command: string) =>
|
|
apiRequest<import("@agy/contracts").SandboxCommandRun>(
|
|
`/api/organizations/${orgId}/sandbox/sessions/${sessionId}/commands`,
|
|
{ method: "POST", body: { command } },
|
|
),
|
|
sandboxCommands: (orgId: string, sessionId: string) =>
|
|
apiRequest<import("@agy/contracts").SandboxCommandRun[]>(
|
|
`/api/organizations/${orgId}/sandbox/sessions/${sessionId}/commands`,
|
|
),
|
|
sandboxReadFile: (orgId: string, sessionId: string, path: string) =>
|
|
apiRequest<import("@agy/contracts").SandboxFileRead>(
|
|
`/api/organizations/${orgId}/sandbox/sessions/${sessionId}/files/read`,
|
|
{ method: "POST", body: { path } },
|
|
),
|
|
sandboxWriteFile: (orgId: string, sessionId: string, path: string, content: string) =>
|
|
apiRequest<void>(
|
|
`/api/organizations/${orgId}/sandbox/sessions/${sessionId}/files/write`,
|
|
{ method: "POST", body: { path, content } },
|
|
),
|
|
sandboxSnapshot: (orgId: string, sessionId: string, name: string) =>
|
|
apiRequest<import("@agy/contracts").SandboxSnapshot>(
|
|
`/api/organizations/${orgId}/sandbox/sessions/${sessionId}/snapshots`,
|
|
{ method: "POST", body: { name } },
|
|
),
|
|
sandboxPreviewUrl: (orgId: string, sessionId: string, port: number) =>
|
|
apiRequest<import("@agy/contracts").SandboxPreviewUrl>(
|
|
`/api/organizations/${orgId}/sandbox/sessions/${sessionId}/preview-url?port=${port}`,
|
|
{ method: "POST" },
|
|
),
|
|
|
|
// --- platform owner -------------------------------------------------------
|
|
// agy accounts: OAuth login instead of API keys.
|
|
agyAccounts: () =>
|
|
apiRequest<import("@agy/contracts").AgyAccount[]>("/api/admin/agy/accounts"),
|
|
agyStartLogin: () =>
|
|
apiRequest<{ login_id: string; auth_url: string }>(
|
|
"/api/admin/agy/accounts/login/start",
|
|
{ method: "POST" },
|
|
),
|
|
agyCompleteLogin: (body: { login_id: string; callback_url: string }) =>
|
|
apiRequest<import("@agy/contracts").AgyAccount>(
|
|
"/api/admin/agy/accounts/login/complete",
|
|
{ method: "POST", body },
|
|
),
|
|
agySetAccountActive: (accountId: string, active: boolean) =>
|
|
apiRequest<import("@agy/contracts").AgyAccount>(
|
|
`/api/admin/agy/accounts/${accountId}/${active ? "activate" : "deactivate"}`,
|
|
{ method: "POST" },
|
|
),
|
|
agyDeleteAccount: (accountId: string) =>
|
|
apiRequest<null>(`/api/admin/agy/accounts/${accountId}`, { method: "DELETE" }),
|
|
agyTestAccount: (accountId: string) =>
|
|
apiRequest<{
|
|
ok: boolean;
|
|
detail: string;
|
|
model_internal_name: string;
|
|
input_tokens: number;
|
|
output_tokens: number;
|
|
}>(`/api/admin/agy/accounts/${accountId}/test`, { method: "POST" }),
|
|
// Live model catalog of one account — fetched from Google on every call, so
|
|
// the model dropdown always matches that subscription's entitlement along with
|
|
// its supported reasoning efforts.
|
|
agyAccountModels: async (accountId: string) => {
|
|
const raw = await apiRequest<(import("@agy/contracts").AgyAccountModel | string)[]>(
|
|
`/api/admin/agy/accounts/${accountId}/models`,
|
|
);
|
|
return raw.map((item): import("@agy/contracts").AgyAccountModel => {
|
|
if (typeof item === "string") {
|
|
return {
|
|
id: item,
|
|
display_name: item,
|
|
supports_thinking: false,
|
|
supported_efforts: [],
|
|
};
|
|
}
|
|
return item;
|
|
});
|
|
},
|
|
adminModels: () => apiRequest<import("@agy/contracts").AdminModel[]>("/api/admin/models"),
|
|
adminCreateModel: (body: {
|
|
internal_name: string;
|
|
display_name_for_admin?: string;
|
|
capabilities: import("@agy/contracts").Capability[];
|
|
input_price_per_1m?: number;
|
|
output_price_per_1m?: number;
|
|
reasoning_effort?: string;
|
|
}) =>
|
|
apiRequest<import("@agy/contracts").AdminModel>("/api/admin/models", {
|
|
method: "POST",
|
|
body,
|
|
}),
|
|
adminRoutingPolicies: () =>
|
|
apiRequest<import("@agy/contracts").AdminRoutingPolicy[]>("/api/admin/routing-policies"),
|
|
adminCreateRoutingPolicy: (body: {
|
|
name: string;
|
|
mode: import("@agy/contracts").RoutingMode;
|
|
description?: string;
|
|
is_default?: boolean;
|
|
}) =>
|
|
apiRequest<import("@agy/contracts").AdminRoutingPolicy>("/api/admin/routing-policies", {
|
|
method: "POST",
|
|
body,
|
|
}),
|
|
adminRateCards: () =>
|
|
apiRequest<import("@agy/contracts").AdminRateCard[]>("/api/admin/rate-cards"),
|
|
adminUpsertRateCard: (body: {
|
|
unit: import("@agy/contracts").RateUnit;
|
|
name?: string;
|
|
customer_price: number;
|
|
internal_cost_estimate?: number;
|
|
is_active?: boolean;
|
|
}) =>
|
|
apiRequest<import("@agy/contracts").AdminRateCard>("/api/admin/rate-cards", {
|
|
method: "PUT",
|
|
body,
|
|
}),
|
|
adminOrganizations: () =>
|
|
apiRequest<import("@agy/contracts").AdminOrganization[]>("/api/admin/organizations"),
|
|
adminUsage: () => apiRequest<import("@agy/contracts").AdminUsageRow[]>("/api/admin/usage"),
|
|
adminValidateModelChoice: (mode: import("@agy/contracts").RoutingMode) =>
|
|
apiRequest<import("@agy/contracts").ModelChoice>(
|
|
`/api/admin/validate-model-choice?mode=${encodeURIComponent(mode)}`,
|
|
{ method: "POST" },
|
|
),
|
|
|
|
// --- error console --------------------------------------------------------
|
|
// The console exists so a failure that already happened can be diagnosed
|
|
// without reproducing it. Grouped by fingerprint, so a bug that fired a
|
|
// thousand times is one line.
|
|
adminErrorSummary: () =>
|
|
apiRequest<import("@agy/contracts").ErrorSummary>("/api/admin/errors/summary"),
|
|
adminErrorGroups: (query: import("@agy/contracts").ErrorQuery = {}) =>
|
|
apiRequest<import("@agy/contracts").ErrorGroup[]>(
|
|
`/api/admin/errors/groups${errorQueryString(query)}`,
|
|
),
|
|
adminErrors: (query: import("@agy/contracts").ErrorQuery = {}) =>
|
|
apiRequest<import("@agy/contracts").ErrorListResponse>(
|
|
`/api/admin/errors${errorQueryString(query)}`,
|
|
),
|
|
adminError: (errorId: string) =>
|
|
apiRequest<import("@agy/contracts").ErrorEvent>(
|
|
`/api/admin/errors/${encodeURIComponent(errorId)}`,
|
|
),
|
|
adminResolveError: (errorId: string, note = "") =>
|
|
apiRequest<import("@agy/contracts").ResolveErrorResponse>(
|
|
`/api/admin/errors/${encodeURIComponent(errorId)}/resolve`,
|
|
{ method: "POST", body: { note } },
|
|
),
|
|
adminResolveErrorGroup: (fingerprint: string, note = "") =>
|
|
apiRequest<import("@agy/contracts").ResolveErrorResponse>(
|
|
`/api/admin/errors/groups/${encodeURIComponent(fingerprint)}/resolve`,
|
|
{ method: "POST", body: { note } },
|
|
),
|
|
adminReopenErrorGroup: (fingerprint: string) =>
|
|
apiRequest<import("@agy/contracts").ResolveErrorResponse>(
|
|
`/api/admin/errors/groups/${encodeURIComponent(fingerprint)}/reopen`,
|
|
{ method: "POST" },
|
|
),
|
|
};
|
|
|
|
/** Serialise the console's filters, dropping anything unset. */
|
|
function errorQueryString(query: import("@agy/contracts").ErrorQuery): string {
|
|
const params = new URLSearchParams();
|
|
for (const [key, value] of Object.entries(query)) {
|
|
if (value !== undefined && value !== null && value !== "") {
|
|
params.set(key, String(value));
|
|
}
|
|
}
|
|
const encoded = params.toString();
|
|
return encoded ? `?${encoded}` : "";
|
|
}
|