File size: 1,642 Bytes
4fdaf19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/** Shared API client: unwraps the backend envelope, surfaces warnings, and
 * injects BYOK LLM headers when the caller opts to pass keys per-request.
 * All backend responses are { data, artifact_id, provenance, quota, warnings,
 * next_actions } — callers usually want `data`, so `api()` returns the whole
 * envelope and helpers pull what they need. */

export interface Envelope<T = any> {
  data: T;
  artifact_id?: string;
  provenance?: Record<string, unknown>;
  quota?: Record<string, unknown>;
  warnings?: string[];
  next_actions?: { action: string; endpoint: string }[];
}

export class ApiError extends Error {
  status: number;
  constructor(message: string, status: number) {
    super(message);
    this.status = status;
  }
}

export async function api<T = any>(
  path: string,
  options: { method?: string; body?: unknown; headers?: Record<string, string> } = {},
): Promise<Envelope<T>> {
  const response = await fetch(path, {
    method: options.method || (options.body ? 'POST' : 'GET'),
    headers: { 'Content-Type': 'application/json', ...(options.headers || {}) },
    body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
  });
  let payload: any = null;
  try {
    payload = await response.json();
  } catch {
    /* non-JSON */
  }
  if (!response.ok) {
    throw new ApiError(payload?.detail || response.statusText, response.status);
  }
  return payload as Envelope<T>;
}

/** Convenience: return just `data`, throwing on error. */
export async function apiData<T = any>(path: string, options?: Parameters<typeof api>[1]): Promise<T> {
  return (await api<T>(path, options)).data;
}