@tjamescouch/gro 1.3.5 → 1.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/drivers/anthropic.js +256 -0
- package/dist/drivers/index.js +2 -0
- package/dist/drivers/streaming-openai.js +262 -0
- package/dist/drivers/types.js +1 -0
- package/dist/errors.js +79 -0
- package/dist/logger.js +30 -0
- package/dist/main.js +867 -0
- package/dist/mcp/client.js +130 -0
- package/dist/mcp/index.js +1 -0
- package/dist/memory/advanced-memory.js +210 -0
- package/dist/memory/agent-memory.js +52 -0
- package/dist/memory/agenthnsw.js +86 -0
- package/{src/memory/index.ts → dist/memory/index.js} +0 -1
- package/dist/memory/simple-memory.js +34 -0
- package/dist/memory/vector-index.js +7 -0
- package/dist/package.json +22 -0
- package/dist/session.js +110 -0
- package/dist/tools/agentpatch.js +91 -0
- package/dist/tools/bash.js +61 -0
- package/dist/tools/version.js +76 -0
- package/dist/utils/rate-limiter.js +46 -0
- package/{src/utils/retry.ts → dist/utils/retry.js} +8 -12
- package/dist/utils/timed-fetch.js +25 -0
- package/gro +0 -0
- package/package.json +13 -2
- package/.github/workflows/ci.yml +0 -20
- package/src/drivers/anthropic.ts +0 -281
- package/src/drivers/index.ts +0 -5
- package/src/drivers/streaming-openai.ts +0 -258
- package/src/drivers/types.ts +0 -39
- package/src/errors.ts +0 -97
- package/src/logger.ts +0 -28
- package/src/main.ts +0 -905
- package/src/mcp/client.ts +0 -163
- package/src/mcp/index.ts +0 -2
- package/src/memory/advanced-memory.ts +0 -263
- package/src/memory/agent-memory.ts +0 -61
- package/src/memory/agenthnsw.ts +0 -122
- package/src/memory/simple-memory.ts +0 -41
- package/src/memory/vector-index.ts +0 -30
- package/src/session.ts +0 -150
- package/src/tools/agentpatch.ts +0 -89
- package/src/tools/bash.ts +0 -61
- package/src/tools/version.ts +0 -98
- package/src/utils/rate-limiter.ts +0 -60
- package/src/utils/timed-fetch.ts +0 -29
- package/tests/errors.test.ts +0 -246
- package/tests/memory.test.ts +0 -186
- package/tests/rate-limiter.test.ts +0 -76
- package/tests/retry.test.ts +0 -138
- package/tests/timed-fetch.test.ts +0 -104
- package/tsconfig.json +0 -13
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic Messages API driver.
|
|
3
|
+
* Direct HTTP — no SDK dependency.
|
|
4
|
+
*/
|
|
5
|
+
import { Logger } from "../logger.js";
|
|
6
|
+
import { rateLimiter } from "../utils/rate-limiter.js";
|
|
7
|
+
import { timedFetch } from "../utils/timed-fetch.js";
|
|
8
|
+
import { MAX_RETRIES, isRetryable, retryDelay, sleep } from "../utils/retry.js";
|
|
9
|
+
import { groError, asError, isGroError, errorLogFields } from "../errors.js";
|
|
10
|
+
/**
|
|
11
|
+
* Convert tool definitions from OpenAI format to Anthropic format.
|
|
12
|
+
* OpenAI: { type: "function", function: { name, description, parameters } }
|
|
13
|
+
* Anthropic: { name, description, input_schema }
|
|
14
|
+
*/
|
|
15
|
+
function convertToolDefs(tools) {
|
|
16
|
+
return tools.map(t => {
|
|
17
|
+
if (t.type === "function" && t.function) {
|
|
18
|
+
return {
|
|
19
|
+
type: "custom",
|
|
20
|
+
name: t.function.name,
|
|
21
|
+
description: t.function.description || "",
|
|
22
|
+
input_schema: t.function.parameters || { type: "object", properties: {} },
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
// Already in Anthropic format — ensure type is set
|
|
26
|
+
if (!t.type)
|
|
27
|
+
return { type: "custom", ...t };
|
|
28
|
+
return t;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Convert internal messages (OpenAI-style) to Anthropic Messages API format.
|
|
33
|
+
*
|
|
34
|
+
* Key differences:
|
|
35
|
+
* - Assistant tool calls become content blocks with type "tool_use"
|
|
36
|
+
* - Tool result messages become user messages with type "tool_result" content blocks
|
|
37
|
+
* - Anthropic requires strictly alternating user/assistant roles
|
|
38
|
+
*/
|
|
39
|
+
function convertMessages(messages) {
|
|
40
|
+
let systemPrompt;
|
|
41
|
+
const apiMessages = [];
|
|
42
|
+
for (const m of messages) {
|
|
43
|
+
if (m.role === "system") {
|
|
44
|
+
systemPrompt = systemPrompt ? systemPrompt + "\n" + m.content : m.content;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (m.role === "assistant") {
|
|
48
|
+
const content = [];
|
|
49
|
+
if (m.content)
|
|
50
|
+
content.push({ type: "text", text: m.content });
|
|
51
|
+
// Convert OpenAI-style tool_calls to Anthropic tool_use blocks
|
|
52
|
+
const toolCalls = m.tool_calls;
|
|
53
|
+
if (Array.isArray(toolCalls)) {
|
|
54
|
+
for (const tc of toolCalls) {
|
|
55
|
+
let input;
|
|
56
|
+
try {
|
|
57
|
+
input = JSON.parse(tc.function.arguments || "{}");
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
input = {};
|
|
61
|
+
}
|
|
62
|
+
content.push({
|
|
63
|
+
type: "tool_use",
|
|
64
|
+
id: tc.id,
|
|
65
|
+
name: tc.function.name,
|
|
66
|
+
input,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (content.length > 0) {
|
|
71
|
+
apiMessages.push({ role: "assistant", content });
|
|
72
|
+
}
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (m.role === "tool") {
|
|
76
|
+
// Tool results must be in a user message with tool_result content blocks
|
|
77
|
+
const block = {
|
|
78
|
+
type: "tool_result",
|
|
79
|
+
tool_use_id: m.tool_call_id,
|
|
80
|
+
content: m.content,
|
|
81
|
+
};
|
|
82
|
+
// Group consecutive tool results into a single user message
|
|
83
|
+
const last = apiMessages[apiMessages.length - 1];
|
|
84
|
+
if (last && last.role === "user" && Array.isArray(last.content) &&
|
|
85
|
+
last.content.length > 0 && last.content[0].type === "tool_result") {
|
|
86
|
+
last.content.push(block);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
apiMessages.push({ role: "user", content: [block] });
|
|
90
|
+
}
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
// Regular user messages
|
|
94
|
+
apiMessages.push({ role: "user", content: m.content });
|
|
95
|
+
}
|
|
96
|
+
return { system: systemPrompt, apiMessages };
|
|
97
|
+
}
|
|
98
|
+
/** Pattern matching transient network errors that should be retried */
|
|
99
|
+
const TRANSIENT_ERROR_RE = /fetch timeout|fetch failed|ECONNREFUSED|ECONNRESET|ETIMEDOUT|ENETUNREACH|EAI_AGAIN|socket hang up/i;
|
|
100
|
+
/** Parse response content blocks into text + tool calls + token usage */
|
|
101
|
+
function parseResponseContent(data, onToken) {
|
|
102
|
+
let text = "";
|
|
103
|
+
const toolCalls = [];
|
|
104
|
+
for (const block of data.content ?? []) {
|
|
105
|
+
if (block.type === "text") {
|
|
106
|
+
text += block.text;
|
|
107
|
+
if (onToken) {
|
|
108
|
+
try {
|
|
109
|
+
onToken(block.text);
|
|
110
|
+
}
|
|
111
|
+
catch { }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else if (block.type === "tool_use") {
|
|
115
|
+
toolCalls.push({
|
|
116
|
+
id: block.id,
|
|
117
|
+
type: "custom",
|
|
118
|
+
function: {
|
|
119
|
+
name: block.name,
|
|
120
|
+
arguments: JSON.stringify(block.input),
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const usage = data.usage ? {
|
|
126
|
+
inputTokens: data.usage.input_tokens ?? 0,
|
|
127
|
+
outputTokens: data.usage.output_tokens ?? 0,
|
|
128
|
+
} : undefined;
|
|
129
|
+
return { text, toolCalls, usage };
|
|
130
|
+
}
|
|
131
|
+
export function makeAnthropicDriver(cfg) {
|
|
132
|
+
const base = (cfg.baseUrl ?? "https://api.anthropic.com").replace(/\/+$/, "");
|
|
133
|
+
const endpoint = `${base}/v1/messages`;
|
|
134
|
+
const model = cfg.model ?? "claude-sonnet-4-20250514";
|
|
135
|
+
const maxTokens = cfg.maxTokens ?? 4096;
|
|
136
|
+
const timeoutMs = cfg.timeoutMs ?? 2 * 60 * 60 * 1000;
|
|
137
|
+
async function chat(messages, opts) {
|
|
138
|
+
await rateLimiter.limit("llm-ask", 1);
|
|
139
|
+
const onToken = opts?.onToken;
|
|
140
|
+
const resolvedModel = opts?.model ?? model;
|
|
141
|
+
const { system: systemPrompt, apiMessages } = convertMessages(messages);
|
|
142
|
+
const body = {
|
|
143
|
+
model: resolvedModel,
|
|
144
|
+
thinking: {
|
|
145
|
+
type: "adaptive"
|
|
146
|
+
},
|
|
147
|
+
max_tokens: maxTokens,
|
|
148
|
+
messages: apiMessages,
|
|
149
|
+
};
|
|
150
|
+
if (systemPrompt)
|
|
151
|
+
body.system = systemPrompt;
|
|
152
|
+
// Tools support — convert from OpenAI format to Anthropic format
|
|
153
|
+
if (Array.isArray(opts?.tools) && opts.tools.length) {
|
|
154
|
+
body.tools = convertToolDefs(opts.tools);
|
|
155
|
+
}
|
|
156
|
+
const headers = {
|
|
157
|
+
"Content-Type": "application/json",
|
|
158
|
+
"x-api-key": cfg.apiKey,
|
|
159
|
+
"anthropic-version": "2023-06-01",
|
|
160
|
+
};
|
|
161
|
+
const RETRYABLE_STATUS = new Set([429, 503, 529]);
|
|
162
|
+
let requestId;
|
|
163
|
+
try {
|
|
164
|
+
let res;
|
|
165
|
+
for (let attempt = 0;; attempt++) {
|
|
166
|
+
res = await timedFetch(endpoint, {
|
|
167
|
+
method: "POST",
|
|
168
|
+
headers,
|
|
169
|
+
body: JSON.stringify(body),
|
|
170
|
+
where: "driver:anthropic",
|
|
171
|
+
timeoutMs,
|
|
172
|
+
});
|
|
173
|
+
if (res.ok)
|
|
174
|
+
break;
|
|
175
|
+
if (isRetryable(res.status) && attempt < MAX_RETRIES) {
|
|
176
|
+
const delay = retryDelay(attempt);
|
|
177
|
+
Logger.warn(`Anthropic ${res.status}, retry ${attempt + 1}/${MAX_RETRIES} in ${Math.round(delay)}ms`);
|
|
178
|
+
await sleep(delay);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const text = await res.text().catch(() => "");
|
|
182
|
+
const ge = groError("provider_error", `Anthropic API failed (${res.status}): ${text}`, {
|
|
183
|
+
provider: "anthropic",
|
|
184
|
+
model: resolvedModel,
|
|
185
|
+
request_id: requestId,
|
|
186
|
+
retryable: RETRYABLE_STATUS.has(res.status),
|
|
187
|
+
cause: new Error(text),
|
|
188
|
+
});
|
|
189
|
+
Logger.error("Anthropic driver error:", errorLogFields(ge));
|
|
190
|
+
throw ge;
|
|
191
|
+
}
|
|
192
|
+
const data = await res.json();
|
|
193
|
+
return parseResponseContent(data, onToken);
|
|
194
|
+
}
|
|
195
|
+
catch (e) {
|
|
196
|
+
if (isGroError(e))
|
|
197
|
+
throw e; // already wrapped above
|
|
198
|
+
// Classify the error: fetch timeouts and network errors are transient
|
|
199
|
+
const errMsg = asError(e).message;
|
|
200
|
+
const isTransient = TRANSIENT_ERROR_RE.test(errMsg);
|
|
201
|
+
if (isTransient) {
|
|
202
|
+
// Retry transient network errors (e.g. auth proxy down during container restart)
|
|
203
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
204
|
+
const delay = retryDelay(attempt);
|
|
205
|
+
Logger.warn(`Transient error: ${errMsg.substring(0, 120)}, retry ${attempt + 1}/${MAX_RETRIES} in ${Math.round(delay)}ms`);
|
|
206
|
+
await sleep(delay);
|
|
207
|
+
try {
|
|
208
|
+
const retryRes = await timedFetch(endpoint, {
|
|
209
|
+
method: "POST",
|
|
210
|
+
headers,
|
|
211
|
+
body: JSON.stringify(body),
|
|
212
|
+
where: "driver:anthropic",
|
|
213
|
+
timeoutMs,
|
|
214
|
+
});
|
|
215
|
+
if (!retryRes.ok) {
|
|
216
|
+
const text = await retryRes.text().catch(() => "");
|
|
217
|
+
if (isRetryable(retryRes.status) && attempt < MAX_RETRIES - 1)
|
|
218
|
+
continue;
|
|
219
|
+
throw groError("provider_error", `Anthropic API failed (${retryRes.status}): ${text}`, {
|
|
220
|
+
provider: "anthropic", model: resolvedModel, retryable: false, cause: new Error(text),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
// Success on retry — parse and return
|
|
224
|
+
const data = await retryRes.json();
|
|
225
|
+
Logger.info(`Recovered from transient error after ${attempt + 1} retries`);
|
|
226
|
+
return parseResponseContent(data, onToken);
|
|
227
|
+
}
|
|
228
|
+
catch (retryErr) {
|
|
229
|
+
if (isGroError(retryErr))
|
|
230
|
+
throw retryErr;
|
|
231
|
+
if (attempt === MAX_RETRIES - 1) {
|
|
232
|
+
// Exhausted retries — throw with context
|
|
233
|
+
const ge = groError("provider_error", `Anthropic driver error (after ${MAX_RETRIES} retries): ${errMsg}`, {
|
|
234
|
+
provider: "anthropic", model: resolvedModel, request_id: requestId,
|
|
235
|
+
retryable: false, cause: e,
|
|
236
|
+
});
|
|
237
|
+
Logger.error("Anthropic driver error (retries exhausted):", errorLogFields(ge));
|
|
238
|
+
throw ge;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// Non-transient error — throw immediately
|
|
244
|
+
const ge = groError("provider_error", `Anthropic driver error: ${errMsg}`, {
|
|
245
|
+
provider: "anthropic",
|
|
246
|
+
model: resolvedModel,
|
|
247
|
+
request_id: requestId,
|
|
248
|
+
retryable: false,
|
|
249
|
+
cause: e,
|
|
250
|
+
});
|
|
251
|
+
Logger.error("Anthropic driver error:", errorLogFields(ge));
|
|
252
|
+
throw ge;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return { chat };
|
|
256
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming OpenAI-compatible chat driver.
|
|
3
|
+
* Works with OpenAI, Anthropic (via proxy), LM Studio, Ollama, etc.
|
|
4
|
+
*/
|
|
5
|
+
import { Logger } from "../logger.js";
|
|
6
|
+
import { asError } from "../errors.js";
|
|
7
|
+
import { rateLimiter } from "../utils/rate-limiter.js";
|
|
8
|
+
import { timedFetch } from "../utils/timed-fetch.js";
|
|
9
|
+
import { MAX_RETRIES, isRetryable, retryDelay, sleep } from "../utils/retry.js";
|
|
10
|
+
function yieldToLoop() {
|
|
11
|
+
return new Promise((resolve) => typeof globalThis.setImmediate === "function"
|
|
12
|
+
? globalThis.setImmediate(resolve)
|
|
13
|
+
: setTimeout(resolve, 0));
|
|
14
|
+
}
|
|
15
|
+
class YieldBudget {
|
|
16
|
+
constructor(byteBudget = 1024, msBudget = 8) {
|
|
17
|
+
this.byteBudget = byteBudget;
|
|
18
|
+
this.msBudget = msBudget;
|
|
19
|
+
this.bytesSince = 0;
|
|
20
|
+
this.last = Date.now();
|
|
21
|
+
}
|
|
22
|
+
async maybe(extraBytes = 0) {
|
|
23
|
+
this.bytesSince += extraBytes;
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
if (this.bytesSince >= this.byteBudget || (now - this.last) >= this.msBudget) {
|
|
26
|
+
this.bytesSince = 0;
|
|
27
|
+
this.last = now;
|
|
28
|
+
await yieldToLoop();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function makeStreamingOpenAiDriver(cfg) {
|
|
33
|
+
const base = cfg.baseUrl.replace(/\/+$/, "");
|
|
34
|
+
const endpoint = `${base}/v1/chat/completions`;
|
|
35
|
+
const defaultTimeout = cfg.timeoutMs ?? 2 * 60 * 60 * 1000;
|
|
36
|
+
async function chat(messages, opts) {
|
|
37
|
+
await rateLimiter.limit("llm-ask", 1);
|
|
38
|
+
Logger.debug("streaming messages out", messages);
|
|
39
|
+
const controller = new AbortController();
|
|
40
|
+
const userSignal = opts?.signal;
|
|
41
|
+
const linkAbort = () => controller.abort();
|
|
42
|
+
if (userSignal) {
|
|
43
|
+
if (userSignal.aborted)
|
|
44
|
+
controller.abort();
|
|
45
|
+
else
|
|
46
|
+
userSignal.addEventListener("abort", linkAbort, { once: true });
|
|
47
|
+
}
|
|
48
|
+
const timer = setTimeout(() => controller.abort(), defaultTimeout);
|
|
49
|
+
const model = opts?.model ?? cfg.model;
|
|
50
|
+
const tools = Array.isArray(opts?.tools) && opts.tools.length ? opts.tools : undefined;
|
|
51
|
+
const onToken = opts?.onToken;
|
|
52
|
+
const onReasoningToken = opts?.onReasoningToken;
|
|
53
|
+
const onToolCallDelta = opts?.onToolCallDelta;
|
|
54
|
+
const headers = { "Content-Type": "application/json" };
|
|
55
|
+
if (cfg.apiKey)
|
|
56
|
+
headers["Authorization"] = `Bearer ${cfg.apiKey}`;
|
|
57
|
+
const payload = { model, messages, stream: true };
|
|
58
|
+
if (tools) {
|
|
59
|
+
payload.tools = tools;
|
|
60
|
+
payload.tool_choice = "auto";
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
let res;
|
|
64
|
+
for (let attempt = 0;; attempt++) {
|
|
65
|
+
res = await timedFetch(endpoint, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers,
|
|
68
|
+
body: JSON.stringify(payload),
|
|
69
|
+
signal: controller.signal,
|
|
70
|
+
where: "driver:openai:stream",
|
|
71
|
+
timeoutMs: defaultTimeout,
|
|
72
|
+
});
|
|
73
|
+
if (res.ok)
|
|
74
|
+
break;
|
|
75
|
+
if (isRetryable(res.status) && attempt < MAX_RETRIES) {
|
|
76
|
+
const delay = retryDelay(attempt);
|
|
77
|
+
Logger.warn(`OpenAI ${res.status}, retry ${attempt + 1}/${MAX_RETRIES} in ${Math.round(delay)}ms`);
|
|
78
|
+
await sleep(delay);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const text = await res.text().catch(() => "");
|
|
82
|
+
throw new Error(`OpenAI chat (stream) failed (${res.status}): ${text}`);
|
|
83
|
+
}
|
|
84
|
+
const ct = (res.headers.get("content-type") || "").toLowerCase();
|
|
85
|
+
if (!ct.includes("text/event-stream")) {
|
|
86
|
+
const data = await res.json().catch(() => ({}));
|
|
87
|
+
const choice = data?.choices?.[0];
|
|
88
|
+
const msg = choice?.message || {};
|
|
89
|
+
const content = typeof msg?.content === "string" ? msg.content : "";
|
|
90
|
+
const toolCalls = Array.isArray(msg?.tool_calls) ? msg.tool_calls : [];
|
|
91
|
+
if (content && onToken)
|
|
92
|
+
onToken(content);
|
|
93
|
+
const usage = data?.usage ? {
|
|
94
|
+
inputTokens: data.usage.prompt_tokens ?? 0,
|
|
95
|
+
outputTokens: data.usage.completion_tokens ?? 0,
|
|
96
|
+
} : undefined;
|
|
97
|
+
return { text: content, reasoning: msg?.reasoning || undefined, toolCalls, usage };
|
|
98
|
+
}
|
|
99
|
+
// SSE streaming
|
|
100
|
+
const decoder = new TextDecoder("utf-8");
|
|
101
|
+
const yb = new YieldBudget(1024, 8);
|
|
102
|
+
let buf = "";
|
|
103
|
+
let fullText = "";
|
|
104
|
+
let fullReasoning = "";
|
|
105
|
+
let streamUsage;
|
|
106
|
+
const toolByIndex = new Map();
|
|
107
|
+
const pumpEvent = async (rawEvent) => {
|
|
108
|
+
const dataLines = rawEvent
|
|
109
|
+
.split("\n")
|
|
110
|
+
.map((l) => l.trim())
|
|
111
|
+
.filter((l) => l.startsWith("data:"))
|
|
112
|
+
.map((l) => l.replace(/^data:\s?/, ""));
|
|
113
|
+
if (!dataLines.length)
|
|
114
|
+
return;
|
|
115
|
+
const joined = dataLines.join("\n").trim();
|
|
116
|
+
if (!joined || joined === "[DONE]")
|
|
117
|
+
return;
|
|
118
|
+
let payload;
|
|
119
|
+
try {
|
|
120
|
+
payload = JSON.parse(joined);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
// Capture usage from final streaming chunk (if stream_options.include_usage was set)
|
|
126
|
+
if (payload?.usage) {
|
|
127
|
+
streamUsage = {
|
|
128
|
+
inputTokens: payload.usage.prompt_tokens ?? 0,
|
|
129
|
+
outputTokens: payload.usage.completion_tokens ?? 0,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const delta = payload?.choices?.[0]?.delta;
|
|
133
|
+
if (!delta)
|
|
134
|
+
return;
|
|
135
|
+
if (typeof delta.content === "string" && delta.content.length) {
|
|
136
|
+
fullText += delta.content;
|
|
137
|
+
if (onToken) {
|
|
138
|
+
let s = delta.content;
|
|
139
|
+
while (s.length) {
|
|
140
|
+
const piece = s.slice(0, 512);
|
|
141
|
+
s = s.slice(512);
|
|
142
|
+
try {
|
|
143
|
+
onToken(piece);
|
|
144
|
+
}
|
|
145
|
+
catch { }
|
|
146
|
+
await yb.maybe(piece.length);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
await yb.maybe(delta.content.length);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (typeof delta.reasoning === "string" && delta.reasoning.length) {
|
|
154
|
+
fullReasoning += delta.reasoning;
|
|
155
|
+
if (onReasoningToken) {
|
|
156
|
+
let s = delta.reasoning;
|
|
157
|
+
while (s.length) {
|
|
158
|
+
const piece = s.slice(0, 512);
|
|
159
|
+
s = s.slice(512);
|
|
160
|
+
try {
|
|
161
|
+
onReasoningToken(piece);
|
|
162
|
+
}
|
|
163
|
+
catch { }
|
|
164
|
+
await yb.maybe(piece.length);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
await yb.maybe(delta.reasoning.length);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (Array.isArray(delta.tool_calls)) {
|
|
172
|
+
for (const item of delta.tool_calls) {
|
|
173
|
+
const idx = typeof item?.index === "number" ? item.index : 0;
|
|
174
|
+
const prev = toolByIndex.get(idx) ?? {
|
|
175
|
+
id: "", type: "function", function: { name: "", arguments: "" }
|
|
176
|
+
};
|
|
177
|
+
if (typeof item.id === "string" && item.id)
|
|
178
|
+
prev.id = item.id;
|
|
179
|
+
if (typeof item.type === "string" && item.type)
|
|
180
|
+
prev.type = item.type;
|
|
181
|
+
const f = item?.function ?? {};
|
|
182
|
+
if (typeof f.name === "string" && f.name)
|
|
183
|
+
prev.function.name += f.name;
|
|
184
|
+
if (typeof f.arguments === "string" && f.arguments)
|
|
185
|
+
prev.function.arguments += f.arguments;
|
|
186
|
+
toolByIndex.set(idx, prev);
|
|
187
|
+
if (onToolCallDelta) {
|
|
188
|
+
try {
|
|
189
|
+
onToolCallDelta(prev);
|
|
190
|
+
}
|
|
191
|
+
catch { }
|
|
192
|
+
}
|
|
193
|
+
await yb.maybe(64);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
const body = res.body;
|
|
198
|
+
if (body && typeof body.getReader === "function") {
|
|
199
|
+
const reader = body.getReader();
|
|
200
|
+
try {
|
|
201
|
+
while (true) {
|
|
202
|
+
const { value, done } = await reader.read();
|
|
203
|
+
if (done)
|
|
204
|
+
break;
|
|
205
|
+
buf += decoder.decode(value, { stream: true });
|
|
206
|
+
await yb.maybe(value?.byteLength ?? 0);
|
|
207
|
+
let sepIdx;
|
|
208
|
+
while ((sepIdx = buf.indexOf("\n\n")) !== -1) {
|
|
209
|
+
const rawEvent = buf.slice(0, sepIdx).trim();
|
|
210
|
+
buf = buf.slice(sepIdx + 2);
|
|
211
|
+
if (rawEvent)
|
|
212
|
+
await pumpEvent(rawEvent);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (buf.trim())
|
|
216
|
+
await pumpEvent(buf.trim());
|
|
217
|
+
}
|
|
218
|
+
finally {
|
|
219
|
+
reader.cancel().catch(() => { });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
else if (body && typeof body[Symbol.asyncIterator] === "function") {
|
|
223
|
+
for await (const chunk of body) {
|
|
224
|
+
buf += decoder.decode(chunk, { stream: true });
|
|
225
|
+
await yb.maybe(chunk.byteLength);
|
|
226
|
+
let sepIdx;
|
|
227
|
+
while ((sepIdx = buf.indexOf("\n\n")) !== -1) {
|
|
228
|
+
const rawEvent = buf.slice(0, sepIdx).trim();
|
|
229
|
+
buf = buf.slice(sepIdx + 2);
|
|
230
|
+
if (rawEvent)
|
|
231
|
+
await pumpEvent(rawEvent);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (buf.trim())
|
|
235
|
+
await pumpEvent(buf.trim());
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
const txt = await res.text();
|
|
239
|
+
for (const part of txt.split("\n\n").map((s) => s.trim())) {
|
|
240
|
+
if (part)
|
|
241
|
+
await pumpEvent(part);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const toolCalls = Array.from(toolByIndex.entries())
|
|
245
|
+
.sort((a, b) => a[0] - b[0])
|
|
246
|
+
.map(([, v]) => v);
|
|
247
|
+
return { text: fullText, reasoning: fullReasoning || undefined, toolCalls, usage: streamUsage };
|
|
248
|
+
}
|
|
249
|
+
catch (e) {
|
|
250
|
+
const wrapped = asError(e);
|
|
251
|
+
if (wrapped.name === "AbortError")
|
|
252
|
+
Logger.debug("timeout(stream)", { ms: defaultTimeout });
|
|
253
|
+
throw wrapped;
|
|
254
|
+
}
|
|
255
|
+
finally {
|
|
256
|
+
clearTimeout(timer);
|
|
257
|
+
if (userSignal)
|
|
258
|
+
userSignal.removeEventListener("abort", linkAbort);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return { chat };
|
|
262
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured error types for gro.
|
|
3
|
+
*
|
|
4
|
+
* All error boundaries should wrap errors using GroError instead of
|
|
5
|
+
* stringifying with e.message. This preserves stack traces, enables
|
|
6
|
+
* retry logic, and provides consistent logging fields.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Create a GroError with structured fields.
|
|
10
|
+
*/
|
|
11
|
+
export function groError(kind, message, opts = {}) {
|
|
12
|
+
const err = new Error(message);
|
|
13
|
+
err.kind = kind;
|
|
14
|
+
err.retryable = opts.retryable ?? false;
|
|
15
|
+
if (opts.provider)
|
|
16
|
+
err.provider = opts.provider;
|
|
17
|
+
if (opts.model)
|
|
18
|
+
err.model = opts.model;
|
|
19
|
+
if (opts.request_id)
|
|
20
|
+
err.request_id = opts.request_id;
|
|
21
|
+
if (opts.latency_ms !== undefined)
|
|
22
|
+
err.latency_ms = opts.latency_ms;
|
|
23
|
+
if (opts.cause !== undefined)
|
|
24
|
+
err.cause = opts.cause;
|
|
25
|
+
return err;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Normalize an unknown thrown value into an Error.
|
|
29
|
+
* Handles strings, objects, nulls — the full JS throw spectrum.
|
|
30
|
+
*/
|
|
31
|
+
export function asError(e) {
|
|
32
|
+
if (e instanceof Error)
|
|
33
|
+
return e;
|
|
34
|
+
if (typeof e === "string")
|
|
35
|
+
return new Error(e.slice(0, 1024));
|
|
36
|
+
if (e === null || e === undefined)
|
|
37
|
+
return new Error("Unknown error");
|
|
38
|
+
try {
|
|
39
|
+
const s = String(e);
|
|
40
|
+
return new Error(s.length > 1024 ? s.slice(0, 1024) + "..." : s);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return new Error("Unknown error (unstringifiable)");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Check if an error is a GroError with structured fields.
|
|
48
|
+
*/
|
|
49
|
+
export function isGroError(e) {
|
|
50
|
+
return e instanceof Error && "kind" in e && "retryable" in e;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Format a GroError for structured logging.
|
|
54
|
+
* Returns a plain object suitable for JSON.stringify or structured loggers.
|
|
55
|
+
*/
|
|
56
|
+
export function errorLogFields(e) {
|
|
57
|
+
const fields = {
|
|
58
|
+
kind: e.kind,
|
|
59
|
+
message: e.message,
|
|
60
|
+
retryable: e.retryable,
|
|
61
|
+
};
|
|
62
|
+
if (e.provider)
|
|
63
|
+
fields.provider = e.provider;
|
|
64
|
+
if (e.model)
|
|
65
|
+
fields.model = e.model;
|
|
66
|
+
if (e.request_id)
|
|
67
|
+
fields.request_id = e.request_id;
|
|
68
|
+
if (e.latency_ms !== undefined)
|
|
69
|
+
fields.latency_ms = e.latency_ms;
|
|
70
|
+
if (e.cause) {
|
|
71
|
+
const cause = asError(e.cause);
|
|
72
|
+
fields.cause_message = cause.message;
|
|
73
|
+
if (cause.stack)
|
|
74
|
+
fields.cause_stack = cause.stack;
|
|
75
|
+
}
|
|
76
|
+
if (e.stack)
|
|
77
|
+
fields.stack = e.stack;
|
|
78
|
+
return fields;
|
|
79
|
+
}
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const C = {
|
|
2
|
+
reset: "\x1b[0m",
|
|
3
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
4
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
5
|
+
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
6
|
+
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
7
|
+
blue: (s) => `\x1b[34m${s}\x1b[0m`,
|
|
8
|
+
magenta: (s) => `\x1b[35m${s}\x1b[0m`,
|
|
9
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
10
|
+
gray: (s) => `\x1b[90m${s}\x1b[0m`,
|
|
11
|
+
};
|
|
12
|
+
function writeRaw(s) {
|
|
13
|
+
const g = globalThis;
|
|
14
|
+
if (g?.Bun?.stdout?.write) {
|
|
15
|
+
g.Bun.stdout.write(s);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
g?.process?.stdout?.write?.(s);
|
|
19
|
+
}
|
|
20
|
+
export class Logger {
|
|
21
|
+
static info(...a) { console.log(...a); }
|
|
22
|
+
static warn(...a) { console.warn(...a); }
|
|
23
|
+
static error(...a) { console.error(...a); }
|
|
24
|
+
static debug(...a) {
|
|
25
|
+
if ((process.env.GRO_LOG_LEVEL ?? "").toUpperCase() === "DEBUG")
|
|
26
|
+
console.log(...a);
|
|
27
|
+
}
|
|
28
|
+
static streamInfo(s) { writeRaw(s); }
|
|
29
|
+
static endStreamLine(suffix = "") { writeRaw(suffix + "\n"); }
|
|
30
|
+
}
|