Mona commited on
Commit
430bcda
Β·
1 Parent(s): 513a1ec

feat: add cloud API client with SSE streaming

Browse files

Streaming reasoning client for the control plane:
- Generic API fetch with auth headers and error typing
- Key verification via /v1/auth/verify
- SSE stream parsing with AbortController support
- onChunk and onUsage callbacks for real-time display
- Full response assembly with proper reader cleanup

All LLM reasoning runs remotely. Device never touches provider keys.

Files changed (1) hide show
  1. src/cloud.js +64 -27
src/cloud.js CHANGED
@@ -1,52 +1,89 @@
1
- // All LLM reasoning happens remotely on agent.mona.expert.
2
- // This client sends prompts/steps up and receives streamed reasoning back.
3
- import { CLOUD } from './config.js';
4
 
5
- async function apiFetch(path, { apiKey, method = 'POST', body } = {}) {
6
- const res = await fetch(CLOUD.base + path, {
 
 
 
 
 
 
 
7
  method,
8
  headers: {
9
- 'authorization': `Bearer ${apiKey}`,
10
- 'content-type': 'application/json',
11
- 'x-mona-agent': '0.1.0',
 
12
  },
13
  body: body ? JSON.stringify(body) : undefined,
 
14
  });
 
15
  if (!res.ok) {
16
- const t = await res.text().catch(() => '');
17
- throw new Error(`cloud ${res.status}: ${t.slice(0, 200)}`);
 
 
18
  }
19
  return res;
20
  }
21
 
 
22
  export async function verifyKey(apiKey) {
23
  const res = await apiFetch('/v1/auth/verify', { apiKey, method: 'GET' });
24
- return res.json(); // { agentId, plan, ... }
25
  }
26
 
27
- // Stream reasoning tokens from the cloud brain. onChunk(text) called per token.
28
- export async function think({ apiKey, messages, tools, onChunk }) {
 
 
 
 
29
  const res = await apiFetch('/v1/agent/think', {
30
  apiKey,
31
  body: { messages, tools, stream: true },
 
32
  });
 
33
  const reader = res.body.getReader();
34
  const dec = new TextDecoder();
35
  let buf = '';
36
- while (true) {
37
- const { value, done } = await reader.read();
38
- if (done) break;
39
- buf += dec.decode(value, { stream: true });
40
- const lines = buf.split('\n');
41
- buf = lines.pop() || '';
42
- for (const line of lines) {
43
- if (!line.startsWith('data:')) continue;
44
- const payload = line.slice(5).trim();
45
- if (payload === '[DONE]') return;
46
- try {
47
- const j = JSON.parse(payload);
48
- if (j.delta) onChunk?.(j.delta);
49
- } catch { /* ignore keepalive */ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  }
 
 
51
  }
 
 
52
  }
 
1
+ // Cloud API client β€” all LLM reasoning runs remotely on agent.mona.expert.
2
+ // This device sends prompts up and receives streamed reasoning back.
3
+ // No LLM provider keys are ever stored or used locally.
4
 
5
+ import { CLOUD, DEFAULTS } from './config.js';
6
+ import { log } from './log.js';
7
+
8
+ const UA = `mona-agent/${DEFAULTS.version}`;
9
+
10
+ // ── Generic API fetch ─────────────────────────────────────────────
11
+ async function apiFetch(path, { apiKey, method = 'POST', body, signal } = {}) {
12
+ const url = CLOUD.base + path;
13
+ const res = await fetch(url, {
14
  method,
15
  headers: {
16
+ 'authorization': `Bearer ${apiKey}`,
17
+ 'content-type': 'application/json',
18
+ 'user-agent': UA,
19
+ 'x-mona-agent': DEFAULTS.version,
20
  },
21
  body: body ? JSON.stringify(body) : undefined,
22
+ signal,
23
  });
24
+
25
  if (!res.ok) {
26
+ const text = await res.text().catch(() => '');
27
+ const err = new Error(`Cloud API ${res.status}: ${text.slice(0, 200)}`);
28
+ err.status = res.status;
29
+ throw err;
30
  }
31
  return res;
32
  }
33
 
34
+ // ── Verify API key ────────────────────────────────────────────────
35
  export async function verifyKey(apiKey) {
36
  const res = await apiFetch('/v1/auth/verify', { apiKey, method: 'GET' });
37
+ return res.json();
38
  }
39
 
40
+ // ── Stream reasoning from cloud brain ─────────────────────────────
41
+ // Calls the cloud LLM endpoint and streams tokens back via SSE.
42
+ // onChunk(text) β€” called per delta token
43
+ // onUsage(usage) β€” called with final token counts (if provided)
44
+ // Returns the full assembled response text.
45
+ export async function think({ apiKey, messages, tools, onChunk, onUsage, signal }) {
46
  const res = await apiFetch('/v1/agent/think', {
47
  apiKey,
48
  body: { messages, tools, stream: true },
49
+ signal,
50
  });
51
+
52
  const reader = res.body.getReader();
53
  const dec = new TextDecoder();
54
  let buf = '';
55
+ let full = '';
56
+
57
+ try {
58
+ while (true) {
59
+ const { value, done } = await reader.read();
60
+ if (done) break;
61
+
62
+ buf += dec.decode(value, { stream: true });
63
+ const lines = buf.split('\n');
64
+ buf = lines.pop() || '';
65
+
66
+ for (const line of lines) {
67
+ if (!line.startsWith('data:')) continue;
68
+ const payload = line.slice(5).trim();
69
+ if (payload === '[DONE]') {
70
+ return full;
71
+ }
72
+ try {
73
+ const j = JSON.parse(payload);
74
+ if (j.delta) {
75
+ full += j.delta;
76
+ onChunk?.(j.delta);
77
+ }
78
+ if (j.usage) onUsage?.(j.usage);
79
+ } catch {
80
+ // Skip malformed or keepalive lines
81
+ }
82
+ }
83
  }
84
+ } finally {
85
+ reader.releaseLock();
86
  }
87
+
88
+ return full;
89
  }