lazyclaw 6.3.1 → 6.4.0
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/README.ko.md +5 -1
- package/README.md +17 -13
- package/agents.mjs +54 -4
- package/channels-discord/index.mjs +5 -1
- package/channels-email/index.mjs +4 -3
- package/channels-whatsapp/index.mjs +3 -2
- package/chat_window.mjs +11 -2
- package/cli.mjs +103 -3
- package/commands/agents.mjs +6 -1
- package/commands/auth_nodes.mjs +19 -1
- package/commands/automation.mjs +20 -5
- package/commands/channels.mjs +46 -3
- package/commands/chat.mjs +90 -33
- package/commands/chat_hardening.mjs +23 -0
- package/commands/daemon.mjs +99 -1
- package/commands/gateway.mjs +123 -4
- package/commands/mcp.mjs +150 -0
- package/commands/misc.mjs +20 -0
- package/commands/sessions.mjs +68 -8
- package/commands/setup.mjs +44 -8
- package/commands/setup_channels.mjs +219 -47
- package/commands/skills.mjs +19 -1
- package/commands/workflow_named.mjs +137 -0
- package/config-validate.mjs +10 -1
- package/config_features.mjs +45 -0
- package/cron.mjs +19 -8
- package/daemon/lib/auth.mjs +25 -0
- package/daemon/lib/cost.mjs +36 -0
- package/daemon/lib/respond.mjs +20 -1
- package/daemon/lib/team_inbound.mjs +69 -0
- package/daemon/route_table.mjs +5 -1
- package/daemon/routes/_deps.mjs +2 -2
- package/daemon/routes/config.mjs +11 -6
- package/daemon/routes/conversation.mjs +103 -40
- package/daemon/routes/events.mjs +45 -0
- package/daemon/routes/meta.mjs +38 -7
- package/daemon/routes/providers.mjs +26 -6
- package/daemon/routes/workflows.mjs +30 -0
- package/daemon.mjs +26 -2
- package/dotenv_min.mjs +15 -3
- package/gateway/challenge_registry.mjs +90 -0
- package/gateway/device_auth.mjs +71 -97
- package/gateway/http_gateway.mjs +29 -3
- package/lib/args.mjs +69 -3
- package/lib/config.mjs +85 -3
- package/lib/render.mjs +43 -0
- package/lib/service_install.mjs +15 -1
- package/mas/agent_turn.mjs +61 -12
- package/mas/confidence.mjs +20 -1
- package/mas/embedder.mjs +100 -0
- package/mas/events.mjs +57 -0
- package/mas/index_db.mjs +103 -10
- package/mas/learning.mjs +96 -72
- package/mas/mention_router.mjs +48 -90
- package/mas/prompt_stack.mjs +60 -3
- package/mas/recall_blend.mjs +56 -0
- package/mas/redact.mjs +55 -0
- package/mas/router_posting.mjs +96 -0
- package/mas/router_termination.mjs +63 -0
- package/mas/scrub_env.mjs +21 -6
- package/mas/skill_synth.mjs +7 -33
- package/mas/tool_runner.mjs +3 -2
- package/mas/tools/bash.mjs +9 -2
- package/mas/tools/coding.mjs +28 -5
- package/mas/tools/delegation.mjs +34 -4
- package/mas/tools/git.mjs +19 -9
- package/mas/tools/ha.mjs +2 -0
- package/mas/tools/learning.mjs +55 -0
- package/mas/tools/media.mjs +21 -3
- package/mas/tools/os.mjs +70 -16
- package/mas/tools/recall.mjs +28 -2
- package/mcp/client.mjs +8 -1
- package/mcp/server_spawn.mjs +5 -1
- package/memory.mjs +24 -0
- package/package.json +3 -1
- package/providers/anthropic.mjs +101 -6
- package/providers/cache.mjs +9 -1
- package/providers/claude_cli.mjs +104 -22
- package/providers/claude_cli_session.mjs +166 -0
- package/providers/cli_error.mjs +38 -0
- package/providers/cli_login.mjs +179 -0
- package/providers/codex_cli.mjs +66 -11
- package/providers/gemini.mjs +101 -3
- package/providers/gemini_cli.mjs +62 -15
- package/providers/model_catalogue.mjs +33 -2
- package/providers/ollama.mjs +104 -3
- package/providers/openai.mjs +110 -8
- package/providers/openai_compat.mjs +97 -6
- package/providers/orchestrator.mjs +112 -12
- package/providers/registry.mjs +15 -9
- package/providers/retry.mjs +14 -2
- package/providers/tool_use/anthropic.mjs +17 -3
- package/providers/tool_use/claude_cli.mjs +72 -20
- package/providers/tool_use/gemini.mjs +18 -2
- package/providers/tool_use/openai.mjs +28 -5
- package/sandbox/confiners/seatbelt.mjs +37 -12
- package/sandbox/index.mjs +49 -0
- package/sandbox/local.mjs +7 -1
- package/sandbox/spawn.mjs +144 -0
- package/sandbox.mjs +5 -1
- package/scripts/loop-worker.mjs +25 -1
- package/sessions.mjs +0 -0
- package/skills/channel-style.md +20 -0
- package/skills/code-review.md +33 -0
- package/skills/commit-message.md +30 -0
- package/skills/concise.md +24 -0
- package/skills/debug-coach.md +25 -0
- package/skills/explain.md +24 -0
- package/skills/korean.md +25 -0
- package/skills/summarize.md +33 -0
- package/skills.mjs +24 -2
- package/skills_curator.mjs +6 -0
- package/skills_install.mjs +10 -2
- package/tasks.mjs +6 -1
- package/teams.mjs +78 -0
- package/tui/chat_mode_slash.mjs +59 -0
- package/tui/config_picker.mjs +1 -0
- package/tui/editor.mjs +178 -151
- package/tui/editor_keys.mjs +275 -0
- package/tui/hud.mjs +111 -0
- package/tui/login_flow.mjs +113 -0
- package/tui/modal_picker.mjs +10 -1
- package/tui/model_pick.mjs +287 -0
- package/tui/orchestrator_flow.mjs +164 -0
- package/tui/pickers.mjs +196 -80
- package/tui/repl.mjs +106 -44
- package/tui/repl_reset.mjs +37 -0
- package/tui/run_turn.mjs +228 -26
- package/tui/slash_args.mjs +159 -0
- package/tui/slash_commands.mjs +7 -5
- package/tui/slash_dispatcher.mjs +537 -344
- package/tui/slash_popup.mjs +5 -1
- package/tui/splash.mjs +15 -2
- package/tui/status_bar.mjs +26 -0
- package/tui/theme.mjs +28 -0
- package/web/avatars/01.png +0 -0
- package/web/avatars/02.png +0 -0
- package/web/avatars/03.png +0 -0
- package/web/avatars/04.png +0 -0
- package/web/avatars/05.png +0 -0
- package/web/avatars/06.png +0 -0
- package/web/avatars/07.png +0 -0
- package/web/avatars/08.png +0 -0
- package/web/avatars/09.png +0 -0
- package/web/avatars/10.png +0 -0
- package/web/avatars/11.png +0 -0
- package/web/avatars/12.png +0 -0
- package/web/avatars/13.png +0 -0
- package/web/avatars/14.png +0 -0
- package/web/avatars/15.png +0 -0
- package/web/avatars/16.png +0 -0
- package/web/avatars/17.png +0 -0
- package/web/avatars/18.png +0 -0
- package/web/avatars/19.png +0 -0
- package/web/avatars/20.png +0 -0
- package/web/dashboard.css +77 -0
- package/web/dashboard.html +29 -2
- package/web/dashboard.js +296 -33
- package/workflow/builtin_caps.mjs +94 -0
- package/workflow/declarative.mjs +101 -0
- package/workflow/named.mjs +71 -0
- package/workflow/named_cron.mjs +50 -0
- package/workflow/nodes.mjs +67 -0
- package/workflow/run_request.mjs +74 -0
- package/workflow/yaml_min.mjs +97 -0
package/providers/ollama.mjs
CHANGED
|
@@ -45,6 +45,61 @@ class ConnectionError extends Error {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
class TimeoutError extends Error {
|
|
49
|
+
constructor(idleMs) {
|
|
50
|
+
super(`ollama: idle timeout — no stream activity for ${idleMs}ms`);
|
|
51
|
+
this.name = 'TimeoutError';
|
|
52
|
+
this.code = 'TIMEOUT';
|
|
53
|
+
this.idleMs = idleMs;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const DEFAULT_IDLE_TIMEOUT_MS = 120000;
|
|
58
|
+
|
|
59
|
+
// Resolve the IDLE (inter-chunk) timeout once per request: opts override
|
|
60
|
+
// wins, then env LAZYCLAW_REQUEST_TIMEOUT_MS (positive int), else 120s.
|
|
61
|
+
function resolveIdleTimeoutMs(opts) {
|
|
62
|
+
if (Number.isFinite(opts?.idleTimeoutMs) && opts.idleTimeoutMs > 0) return opts.idleTimeoutMs;
|
|
63
|
+
const raw = parseInt(process.env.LAZYCLAW_REQUEST_TIMEOUT_MS ?? '', 10);
|
|
64
|
+
if (Number.isFinite(raw) && raw > 0) return raw;
|
|
65
|
+
return DEFAULT_IDLE_TIMEOUT_MS;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Wrap a chunk iterator with an IDLE timeout: abort only when NO chunk has
|
|
69
|
+
// arrived for idleMs (the timer resets on every received chunk, and also
|
|
70
|
+
// guards the connect/first-byte phase). This is NOT a total-duration cap,
|
|
71
|
+
// so a long but healthy generation that streams steadily is never aborted.
|
|
72
|
+
// `controller` is aborted on idle expiry so the real connection tears down.
|
|
73
|
+
async function* iterateWithIdleTimeout(body, idleMs, controller) {
|
|
74
|
+
const iterator = iterateBody(body)[Symbol.asyncIterator]();
|
|
75
|
+
try {
|
|
76
|
+
while (true) {
|
|
77
|
+
let timer;
|
|
78
|
+
const idle = new Promise((_, reject) => {
|
|
79
|
+
timer = setTimeout(() => {
|
|
80
|
+
controller.abort();
|
|
81
|
+
reject(new TimeoutError(idleMs));
|
|
82
|
+
}, idleMs);
|
|
83
|
+
});
|
|
84
|
+
let step;
|
|
85
|
+
try {
|
|
86
|
+
step = await Promise.race([iterator.next(), idle]);
|
|
87
|
+
} finally {
|
|
88
|
+
clearTimeout(timer);
|
|
89
|
+
}
|
|
90
|
+
if (step.done) return;
|
|
91
|
+
yield step.value;
|
|
92
|
+
}
|
|
93
|
+
} finally {
|
|
94
|
+
// Best-effort, NON-awaited cleanup: on idle abort the underlying reader
|
|
95
|
+
// may be suspended on a read that never settles, so awaiting return()
|
|
96
|
+
// would re-hang us. The controller abort already tears down the socket.
|
|
97
|
+
if (typeof iterator.return === 'function') {
|
|
98
|
+
try { Promise.resolve(iterator.return()).catch(() => {}); } catch { /* ignore */ }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
48
103
|
async function* iterateBody(body) {
|
|
49
104
|
if (body && typeof body.getReader === 'function') {
|
|
50
105
|
const reader = body.getReader();
|
|
@@ -64,6 +119,28 @@ async function* iterateBody(body) {
|
|
|
64
119
|
throw new Error('ollama: response body is not iterable');
|
|
65
120
|
}
|
|
66
121
|
|
|
122
|
+
// The final `done:true` frame carries prompt_eval_count (input) / eval_count
|
|
123
|
+
// (output) and a done_reason. Report tokens via onUsage (cost 0 for a local
|
|
124
|
+
// model) and signal a num_predict/context cut (done_reason 'length') so the
|
|
125
|
+
// caller can warn rather than treat a truncated answer as complete.
|
|
126
|
+
function _emitOllamaDone(obj, opts) {
|
|
127
|
+
const inT = obj.prompt_eval_count;
|
|
128
|
+
const outT = obj.eval_count;
|
|
129
|
+
if ((inT != null || outT != null) && typeof opts.onUsage === 'function') {
|
|
130
|
+
try {
|
|
131
|
+
opts.onUsage({
|
|
132
|
+
inputTokens: inT ?? null,
|
|
133
|
+
outputTokens: outT ?? null,
|
|
134
|
+
totalTokens: (inT ?? 0) + (outT ?? 0),
|
|
135
|
+
totalCostUsd: 0,
|
|
136
|
+
});
|
|
137
|
+
} catch { /* never fail the stream on a usage callback */ }
|
|
138
|
+
}
|
|
139
|
+
if (obj.done_reason === 'length' && typeof opts.onTruncated === 'function') {
|
|
140
|
+
try { opts.onTruncated('length'); } catch { /* never fail the stream on a callback */ }
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
67
144
|
export const ollamaProvider = {
|
|
68
145
|
name: 'ollama',
|
|
69
146
|
/**
|
|
@@ -87,13 +164,28 @@ export const ollamaProvider = {
|
|
|
87
164
|
|
|
88
165
|
if (opts.signal?.aborted) throw new AbortError('aborted before request');
|
|
89
166
|
|
|
167
|
+
// Compose the caller's signal with an internal controller that the idle
|
|
168
|
+
// timeout aborts. The composed signal goes to fetch so a stalled
|
|
169
|
+
// connection (or a user cancel) tears down the real socket.
|
|
170
|
+
const idleMs = resolveIdleTimeoutMs(opts);
|
|
171
|
+
const idleController = new AbortController();
|
|
172
|
+
const fetchSignal = opts.signal
|
|
173
|
+
? (typeof AbortSignal.any === 'function'
|
|
174
|
+
? AbortSignal.any([opts.signal, idleController.signal])
|
|
175
|
+
: idleController.signal)
|
|
176
|
+
: idleController.signal;
|
|
177
|
+
const forwardCallerAbort = () => idleController.abort();
|
|
178
|
+
if (opts.signal && typeof AbortSignal.any !== 'function') {
|
|
179
|
+
opts.signal.addEventListener('abort', forwardCallerAbort, { once: true });
|
|
180
|
+
}
|
|
181
|
+
|
|
90
182
|
let res;
|
|
91
183
|
try {
|
|
92
184
|
res = await fetchFn(`${baseUrl}/api/chat`, {
|
|
93
185
|
method: 'POST',
|
|
94
186
|
headers: { 'content-type': 'application/json' },
|
|
95
187
|
body: JSON.stringify({ model, messages: apiMessages, stream: true }),
|
|
96
|
-
signal:
|
|
188
|
+
signal: fetchSignal,
|
|
97
189
|
});
|
|
98
190
|
} catch (err) {
|
|
99
191
|
// Distinguish "ollama isn't running" from generic network errors so
|
|
@@ -112,7 +204,10 @@ export const ollamaProvider = {
|
|
|
112
204
|
|
|
113
205
|
const decoder = new TextDecoder('utf-8', { fatal: false });
|
|
114
206
|
let buffer = '';
|
|
115
|
-
|
|
207
|
+
try {
|
|
208
|
+
for await (const chunk of iterateWithIdleTimeout(res.body, idleMs, idleController)) {
|
|
209
|
+
// A user cancel surfaces as an idle-controller abort too; map it back
|
|
210
|
+
// to ABORT so it stays distinguishable from a TIMEOUT.
|
|
116
211
|
if (opts.signal?.aborted) throw new AbortError('aborted mid-stream');
|
|
117
212
|
buffer += typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true });
|
|
118
213
|
// Ollama is newline-delimited JSON, not SSE — split on '\n', parse
|
|
@@ -125,7 +220,7 @@ export const ollamaProvider = {
|
|
|
125
220
|
try {
|
|
126
221
|
const obj = JSON.parse(line);
|
|
127
222
|
if (obj?.message?.content) yield obj.message.content;
|
|
128
|
-
if (obj?.done) return;
|
|
223
|
+
if (obj?.done) { _emitOllamaDone(obj, opts); return; }
|
|
129
224
|
if (obj?.error) throw new ApiError(500, obj.error);
|
|
130
225
|
} catch (err) {
|
|
131
226
|
if (err instanceof ApiError) throw err;
|
|
@@ -133,6 +228,11 @@ export const ollamaProvider = {
|
|
|
133
228
|
}
|
|
134
229
|
}
|
|
135
230
|
}
|
|
231
|
+
} finally {
|
|
232
|
+
if (opts.signal && typeof AbortSignal.any !== 'function') {
|
|
233
|
+
opts.signal.removeEventListener('abort', forwardCallerAbort);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
136
236
|
// Any trailing bytes the decoder still holds onto get flushed and
|
|
137
237
|
// parsed once. Empty after a clean stream.
|
|
138
238
|
const tail = decoder.decode();
|
|
@@ -140,6 +240,7 @@ export const ollamaProvider = {
|
|
|
140
240
|
try {
|
|
141
241
|
const obj = JSON.parse(tail);
|
|
142
242
|
if (obj?.message?.content) yield obj.message.content;
|
|
243
|
+
if (obj?.done) _emitOllamaDone(obj, opts);
|
|
143
244
|
} catch { /* malformed tail — drop */ }
|
|
144
245
|
}
|
|
145
246
|
},
|
package/providers/openai.mjs
CHANGED
|
@@ -14,6 +14,13 @@
|
|
|
14
14
|
|
|
15
15
|
const DEFAULT_MAX_TOKENS = 4096;
|
|
16
16
|
|
|
17
|
+
// OpenAI o-series reasoning models (o1/o3/o4...) reject `max_tokens` with
|
|
18
|
+
// HTTP 400 'Unsupported parameter: max_tokens, use max_completion_tokens'.
|
|
19
|
+
// They are surfaced in this provider's own suggestedModels (registry.mjs).
|
|
20
|
+
function isReasoningModel(model) {
|
|
21
|
+
return /^o\d/i.test(String(model || ''));
|
|
22
|
+
}
|
|
23
|
+
|
|
17
24
|
class InvalidApiKeyError extends Error {
|
|
18
25
|
constructor(message = 'invalid OpenAI api key') {
|
|
19
26
|
super(message);
|
|
@@ -41,6 +48,61 @@ class RateLimitError extends Error {
|
|
|
41
48
|
}
|
|
42
49
|
}
|
|
43
50
|
|
|
51
|
+
class TimeoutError extends Error {
|
|
52
|
+
constructor(idleMs) {
|
|
53
|
+
super(`openai: idle timeout — no stream activity for ${idleMs}ms`);
|
|
54
|
+
this.name = 'TimeoutError';
|
|
55
|
+
this.code = 'TIMEOUT';
|
|
56
|
+
this.idleMs = idleMs;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const DEFAULT_IDLE_TIMEOUT_MS = 120000;
|
|
61
|
+
|
|
62
|
+
// Resolve the IDLE (inter-chunk) timeout once per request: opts override
|
|
63
|
+
// wins, then env LAZYCLAW_REQUEST_TIMEOUT_MS (positive int), else 120s.
|
|
64
|
+
function resolveIdleTimeoutMs(opts) {
|
|
65
|
+
if (Number.isFinite(opts?.idleTimeoutMs) && opts.idleTimeoutMs > 0) return opts.idleTimeoutMs;
|
|
66
|
+
const raw = parseInt(process.env.LAZYCLAW_REQUEST_TIMEOUT_MS ?? '', 10);
|
|
67
|
+
if (Number.isFinite(raw) && raw > 0) return raw;
|
|
68
|
+
return DEFAULT_IDLE_TIMEOUT_MS;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Wrap a chunk iterator with an IDLE timeout: abort only when NO chunk has
|
|
72
|
+
// arrived for idleMs (the timer resets on every received chunk, and also
|
|
73
|
+
// guards the connect/first-byte phase). This is NOT a total-duration cap,
|
|
74
|
+
// so a long but healthy generation that streams steadily is never aborted.
|
|
75
|
+
// `controller` is aborted on idle expiry so the real connection tears down.
|
|
76
|
+
async function* iterateWithIdleTimeout(body, idleMs, controller) {
|
|
77
|
+
const iterator = iterateBody(body)[Symbol.asyncIterator]();
|
|
78
|
+
try {
|
|
79
|
+
while (true) {
|
|
80
|
+
let timer;
|
|
81
|
+
const idle = new Promise((_, reject) => {
|
|
82
|
+
timer = setTimeout(() => {
|
|
83
|
+
controller.abort();
|
|
84
|
+
reject(new TimeoutError(idleMs));
|
|
85
|
+
}, idleMs);
|
|
86
|
+
});
|
|
87
|
+
let step;
|
|
88
|
+
try {
|
|
89
|
+
step = await Promise.race([iterator.next(), idle]);
|
|
90
|
+
} finally {
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
}
|
|
93
|
+
if (step.done) return;
|
|
94
|
+
yield step.value;
|
|
95
|
+
}
|
|
96
|
+
} finally {
|
|
97
|
+
// Best-effort, NON-awaited cleanup: on idle abort the underlying reader
|
|
98
|
+
// may be suspended on a read that never settles, so awaiting return()
|
|
99
|
+
// would re-hang us. The controller abort already tears down the socket.
|
|
100
|
+
if (typeof iterator.return === 'function') {
|
|
101
|
+
try { Promise.resolve(iterator.return()).catch(() => {}); } catch { /* ignore */ }
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
44
106
|
class ApiError extends Error {
|
|
45
107
|
constructor(status, body) {
|
|
46
108
|
super(`openai api ${status}: ${String(body).slice(0, 200)}`);
|
|
@@ -120,10 +182,14 @@ export const openaiProvider = {
|
|
|
120
182
|
|
|
121
183
|
const body = {
|
|
122
184
|
model,
|
|
123
|
-
max_tokens: opts.maxTokens || DEFAULT_MAX_TOKENS,
|
|
124
185
|
stream: true,
|
|
125
186
|
messages: apiMessages,
|
|
126
187
|
};
|
|
188
|
+
// Reasoning models take max_completion_tokens; everything else takes
|
|
189
|
+
// max_tokens. Never emit both — the o-series rejects max_tokens.
|
|
190
|
+
const tokenCap = opts.maxTokens || DEFAULT_MAX_TOKENS;
|
|
191
|
+
if (isReasoningModel(model)) body.max_completion_tokens = tokenCap;
|
|
192
|
+
else body.max_tokens = tokenCap;
|
|
127
193
|
// Tool-use passthrough mirrors the anthropic provider: opts.tools is an
|
|
128
194
|
// array of OpenAI-shaped tools. opts.toolChoice maps to tool_choice.
|
|
129
195
|
if (Array.isArray(opts.tools) && opts.tools.length > 0) {
|
|
@@ -140,6 +206,20 @@ export const openaiProvider = {
|
|
|
140
206
|
}
|
|
141
207
|
|
|
142
208
|
if (opts.signal?.aborted) throw new AbortError('aborted before request');
|
|
209
|
+
// Compose the caller's signal with an internal controller that the idle
|
|
210
|
+
// timeout aborts. The composed signal goes to fetch so a stalled
|
|
211
|
+
// connection (or a user cancel) tears down the real socket.
|
|
212
|
+
const idleMs = resolveIdleTimeoutMs(opts);
|
|
213
|
+
const idleController = new AbortController();
|
|
214
|
+
const fetchSignal = opts.signal
|
|
215
|
+
? (typeof AbortSignal.any === 'function'
|
|
216
|
+
? AbortSignal.any([opts.signal, idleController.signal])
|
|
217
|
+
: idleController.signal)
|
|
218
|
+
: idleController.signal;
|
|
219
|
+
const forwardCallerAbort = () => idleController.abort();
|
|
220
|
+
if (opts.signal && typeof AbortSignal.any !== 'function') {
|
|
221
|
+
opts.signal.addEventListener('abort', forwardCallerAbort, { once: true });
|
|
222
|
+
}
|
|
143
223
|
const res = await fetchFn('https://api.openai.com/v1/chat/completions', {
|
|
144
224
|
method: 'POST',
|
|
145
225
|
headers: {
|
|
@@ -147,7 +227,7 @@ export const openaiProvider = {
|
|
|
147
227
|
'authorization': `Bearer ${opts.apiKey}`,
|
|
148
228
|
},
|
|
149
229
|
body: JSON.stringify(body),
|
|
150
|
-
signal:
|
|
230
|
+
signal: fetchSignal,
|
|
151
231
|
});
|
|
152
232
|
|
|
153
233
|
if (!res.ok) {
|
|
@@ -165,6 +245,7 @@ export const openaiProvider = {
|
|
|
165
245
|
// shape that mirrors the anthropic provider's onUsage payload so
|
|
166
246
|
// callers don't have to special-case per provider.
|
|
167
247
|
let usage = null;
|
|
248
|
+
let lastFinish = null;
|
|
168
249
|
// OpenAI streams tool_calls as deltas with an `index` we use as the
|
|
169
250
|
// accumulation key. Each delta may carry a partial id, name, and/or
|
|
170
251
|
// arguments string. We assemble until the stream signals
|
|
@@ -187,7 +268,25 @@ export const openaiProvider = {
|
|
|
187
268
|
});
|
|
188
269
|
} catch { /* never let a callback abort the stream */ }
|
|
189
270
|
};
|
|
190
|
-
|
|
271
|
+
// Fire onUsage + (if truncated) onTruncated EXACTLY ONCE — from the [DONE]
|
|
272
|
+
// sentinel, or from the post-loop flush if the upstream closed after the
|
|
273
|
+
// usage frame without ever sending [DONE].
|
|
274
|
+
let finalized = false;
|
|
275
|
+
const finalize = () => {
|
|
276
|
+
if (finalized) return;
|
|
277
|
+
finalized = true;
|
|
278
|
+
for (const idx of Array.from(toolCallsByIndex.keys())) flushToolCall(idx);
|
|
279
|
+
if (usage && typeof opts.onUsage === 'function') {
|
|
280
|
+
try { opts.onUsage(usage); } catch { /* never let a callback abort */ }
|
|
281
|
+
}
|
|
282
|
+
if (lastFinish === 'length' && typeof opts.onTruncated === 'function') {
|
|
283
|
+
try { opts.onTruncated('length'); } catch { /* ditto */ }
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
try {
|
|
287
|
+
for await (const chunk of iterateWithIdleTimeout(res.body, idleMs, idleController)) {
|
|
288
|
+
// A user cancel surfaces as an idle-controller abort too; map it back
|
|
289
|
+
// to ABORT so it stays distinguishable from a TIMEOUT.
|
|
191
290
|
if (opts.signal?.aborted) throw new AbortError('aborted mid-stream');
|
|
192
291
|
buffer += typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true });
|
|
193
292
|
let consumed = 0;
|
|
@@ -195,11 +294,7 @@ export const openaiProvider = {
|
|
|
195
294
|
consumed = frame.nextCursor;
|
|
196
295
|
if (!frame.data) continue;
|
|
197
296
|
if (frame.data === '[DONE]') {
|
|
198
|
-
|
|
199
|
-
for (const idx of Array.from(toolCallsByIndex.keys())) flushToolCall(idx);
|
|
200
|
-
if (usage && typeof opts.onUsage === 'function') {
|
|
201
|
-
try { opts.onUsage(usage); } catch { /* never let a callback abort */ }
|
|
202
|
-
}
|
|
297
|
+
finalize();
|
|
203
298
|
return;
|
|
204
299
|
}
|
|
205
300
|
try {
|
|
@@ -226,6 +321,7 @@ export const openaiProvider = {
|
|
|
226
321
|
toolCallsByIndex.set(idx, cur);
|
|
227
322
|
}
|
|
228
323
|
}
|
|
324
|
+
if (choice?.finish_reason) lastFinish = choice.finish_reason;
|
|
229
325
|
if (choice?.finish_reason === 'tool_calls') {
|
|
230
326
|
for (const idx of Array.from(toolCallsByIndex.keys())) flushToolCall(idx);
|
|
231
327
|
}
|
|
@@ -235,6 +331,12 @@ export const openaiProvider = {
|
|
|
235
331
|
}
|
|
236
332
|
if (consumed > 0) buffer = buffer.slice(consumed);
|
|
237
333
|
}
|
|
334
|
+
} finally {
|
|
335
|
+
if (opts.signal && typeof AbortSignal.any !== 'function') {
|
|
336
|
+
opts.signal.removeEventListener('abort', forwardCallerAbort);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
finalize(); // flush captured usage/truncation if the stream ended without [DONE]
|
|
238
340
|
const tail = decoder.decode();
|
|
239
341
|
if (tail) buffer += tail;
|
|
240
342
|
},
|
|
@@ -48,6 +48,61 @@ class RateLimitError extends Error {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
class TimeoutError extends Error {
|
|
52
|
+
constructor(idleMs) {
|
|
53
|
+
super(`openai-compat: idle timeout — no stream activity for ${idleMs}ms`);
|
|
54
|
+
this.name = 'TimeoutError';
|
|
55
|
+
this.code = 'TIMEOUT';
|
|
56
|
+
this.idleMs = idleMs;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const DEFAULT_IDLE_TIMEOUT_MS = 120000;
|
|
61
|
+
|
|
62
|
+
// Resolve the IDLE (inter-chunk) timeout once per request: opts override
|
|
63
|
+
// wins, then env LAZYCLAW_REQUEST_TIMEOUT_MS (positive int), else 120s.
|
|
64
|
+
function resolveIdleTimeoutMs(opts) {
|
|
65
|
+
if (Number.isFinite(opts?.idleTimeoutMs) && opts.idleTimeoutMs > 0) return opts.idleTimeoutMs;
|
|
66
|
+
const raw = parseInt(process.env.LAZYCLAW_REQUEST_TIMEOUT_MS ?? '', 10);
|
|
67
|
+
if (Number.isFinite(raw) && raw > 0) return raw;
|
|
68
|
+
return DEFAULT_IDLE_TIMEOUT_MS;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Wrap a chunk iterator with an IDLE timeout: abort only when NO chunk has
|
|
72
|
+
// arrived for idleMs (the timer resets on every received chunk, and also
|
|
73
|
+
// guards the connect/first-byte phase). This is NOT a total-duration cap,
|
|
74
|
+
// so a long but healthy generation that streams steadily is never aborted.
|
|
75
|
+
// `controller` is aborted on idle expiry so the real connection tears down.
|
|
76
|
+
async function* iterateWithIdleTimeout(body, idleMs, controller) {
|
|
77
|
+
const iterator = iterateBody(body)[Symbol.asyncIterator]();
|
|
78
|
+
try {
|
|
79
|
+
while (true) {
|
|
80
|
+
let timer;
|
|
81
|
+
const idle = new Promise((_, reject) => {
|
|
82
|
+
timer = setTimeout(() => {
|
|
83
|
+
controller.abort();
|
|
84
|
+
reject(new TimeoutError(idleMs));
|
|
85
|
+
}, idleMs);
|
|
86
|
+
});
|
|
87
|
+
let step;
|
|
88
|
+
try {
|
|
89
|
+
step = await Promise.race([iterator.next(), idle]);
|
|
90
|
+
} finally {
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
}
|
|
93
|
+
if (step.done) return;
|
|
94
|
+
yield step.value;
|
|
95
|
+
}
|
|
96
|
+
} finally {
|
|
97
|
+
// Best-effort, NON-awaited cleanup: on idle abort the underlying reader
|
|
98
|
+
// may be suspended on a read that never settles, so awaiting return()
|
|
99
|
+
// would re-hang us. The controller abort already tears down the socket.
|
|
100
|
+
if (typeof iterator.return === 'function') {
|
|
101
|
+
try { Promise.resolve(iterator.return()).catch(() => {}); } catch { /* ignore */ }
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
51
106
|
class ApiError extends Error {
|
|
52
107
|
constructor(status, body) {
|
|
53
108
|
super(`openai-compat ${status}: ${String(body).slice(0, 200)}`);
|
|
@@ -175,12 +230,26 @@ export function makeOpenAICompatProvider(cfg) {
|
|
|
175
230
|
if (apiKey) headers['authorization'] = `Bearer ${apiKey}`;
|
|
176
231
|
|
|
177
232
|
if (opts.signal?.aborted) throw new AbortError('aborted before request');
|
|
233
|
+
// Compose the caller's signal with an internal controller that the idle
|
|
234
|
+
// timeout aborts. The composed signal goes to fetch so a stalled
|
|
235
|
+
// connection (or a user cancel) tears down the real socket.
|
|
236
|
+
const idleMs = resolveIdleTimeoutMs(opts);
|
|
237
|
+
const idleController = new AbortController();
|
|
238
|
+
const fetchSignal = opts.signal
|
|
239
|
+
? (typeof AbortSignal.any === 'function'
|
|
240
|
+
? AbortSignal.any([opts.signal, idleController.signal])
|
|
241
|
+
: idleController.signal)
|
|
242
|
+
: idleController.signal;
|
|
243
|
+
const forwardCallerAbort = () => idleController.abort();
|
|
244
|
+
if (opts.signal && typeof AbortSignal.any !== 'function') {
|
|
245
|
+
opts.signal.addEventListener('abort', forwardCallerAbort, { once: true });
|
|
246
|
+
}
|
|
178
247
|
const url = `${baseUrl}/chat/completions`;
|
|
179
248
|
const res = await fetchFn(url, {
|
|
180
249
|
method: 'POST',
|
|
181
250
|
headers,
|
|
182
251
|
body: JSON.stringify(body),
|
|
183
|
-
signal:
|
|
252
|
+
signal: fetchSignal,
|
|
184
253
|
});
|
|
185
254
|
|
|
186
255
|
if (!res.ok) {
|
|
@@ -193,6 +262,7 @@ export function makeOpenAICompatProvider(cfg) {
|
|
|
193
262
|
const decoder = new TextDecoder('utf-8', { fatal: false });
|
|
194
263
|
let buffer = '';
|
|
195
264
|
let usage = null;
|
|
265
|
+
let lastFinish = null;
|
|
196
266
|
const toolCallsByIndex = new Map();
|
|
197
267
|
const flushToolCall = (idx) => {
|
|
198
268
|
const tc = toolCallsByIndex.get(idx);
|
|
@@ -211,7 +281,24 @@ export function makeOpenAICompatProvider(cfg) {
|
|
|
211
281
|
});
|
|
212
282
|
} catch { /* never let a callback abort the stream */ }
|
|
213
283
|
};
|
|
214
|
-
|
|
284
|
+
// Fire onUsage + (if truncated) onTruncated EXACTLY ONCE — from [DONE] or
|
|
285
|
+
// the post-loop flush if the upstream closes without the sentinel.
|
|
286
|
+
let finalized = false;
|
|
287
|
+
const finalize = () => {
|
|
288
|
+
if (finalized) return;
|
|
289
|
+
finalized = true;
|
|
290
|
+
for (const idx of Array.from(toolCallsByIndex.keys())) flushToolCall(idx);
|
|
291
|
+
if (usage && typeof opts.onUsage === 'function') {
|
|
292
|
+
try { opts.onUsage(usage); } catch { /* swallow */ }
|
|
293
|
+
}
|
|
294
|
+
if (lastFinish === 'length' && typeof opts.onTruncated === 'function') {
|
|
295
|
+
try { opts.onTruncated('length'); } catch { /* swallow */ }
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
try {
|
|
299
|
+
for await (const chunk of iterateWithIdleTimeout(res.body, idleMs, idleController)) {
|
|
300
|
+
// A user cancel surfaces as an idle-controller abort too; map it back
|
|
301
|
+
// to ABORT so it stays distinguishable from a TIMEOUT.
|
|
215
302
|
if (opts.signal?.aborted) throw new AbortError('aborted mid-stream');
|
|
216
303
|
buffer += typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true });
|
|
217
304
|
let consumed = 0;
|
|
@@ -219,10 +306,7 @@ export function makeOpenAICompatProvider(cfg) {
|
|
|
219
306
|
consumed = frame.nextCursor;
|
|
220
307
|
if (!frame.data) continue;
|
|
221
308
|
if (frame.data === '[DONE]') {
|
|
222
|
-
|
|
223
|
-
if (usage && typeof opts.onUsage === 'function') {
|
|
224
|
-
try { opts.onUsage(usage); } catch { /* swallow */ }
|
|
225
|
-
}
|
|
309
|
+
finalize();
|
|
226
310
|
return;
|
|
227
311
|
}
|
|
228
312
|
try {
|
|
@@ -247,6 +331,7 @@ export function makeOpenAICompatProvider(cfg) {
|
|
|
247
331
|
toolCallsByIndex.set(idx, cur);
|
|
248
332
|
}
|
|
249
333
|
}
|
|
334
|
+
if (choice?.finish_reason) lastFinish = choice.finish_reason;
|
|
250
335
|
if (choice?.finish_reason === 'tool_calls') {
|
|
251
336
|
for (const idx of Array.from(toolCallsByIndex.keys())) flushToolCall(idx);
|
|
252
337
|
}
|
|
@@ -256,6 +341,12 @@ export function makeOpenAICompatProvider(cfg) {
|
|
|
256
341
|
}
|
|
257
342
|
if (consumed > 0) buffer = buffer.slice(consumed);
|
|
258
343
|
}
|
|
344
|
+
} finally {
|
|
345
|
+
if (opts.signal && typeof AbortSignal.any !== 'function') {
|
|
346
|
+
opts.signal.removeEventListener('abort', forwardCallerAbort);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
finalize(); // flush captured usage/truncation if the stream ended without [DONE]
|
|
259
350
|
const tail = decoder.decode();
|
|
260
351
|
if (tail) buffer += tail;
|
|
261
352
|
},
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
// "gemini:gemini-2.5-pro"
|
|
28
28
|
// ],
|
|
29
29
|
// "maxSubtasks": 5, // optional, default 5
|
|
30
|
-
// "concurrency": 0 // optional
|
|
30
|
+
// "concurrency": 0 // optional; default = min(3, workers), parallel.
|
|
31
|
+
// // 0 or 1 = sequential (visible live streaming)
|
|
31
32
|
// }
|
|
32
33
|
// }
|
|
33
34
|
//
|
|
@@ -86,6 +87,24 @@ function _bestPlanArray(text) {
|
|
|
86
87
|
return null;
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
// Default worker-pool concurrency when cfg.orchestrator.concurrency is unset.
|
|
91
|
+
// Parallel is the point of a worker pool; an unconfigured fleet should not run
|
|
92
|
+
// subtasks one at a time. Clamped to the worker count at the call site, and an
|
|
93
|
+
// explicit 0/1 still selects the sequential live-streaming path.
|
|
94
|
+
const DEFAULT_CONCURRENCY = 3;
|
|
95
|
+
|
|
96
|
+
// Opt-in agentic workers (cfg.orchestrator.agenticWorkers): an EXECUTE worker
|
|
97
|
+
// runs through runAgentTurn so it can actually DO work with its tools (shell,
|
|
98
|
+
// file read, recall) instead of only streaming text. Default OFF — the
|
|
99
|
+
// text-streaming path is byte-stable. Tool calls are confined by the sandbox
|
|
100
|
+
// the caller passes; the loop is bounded by workerMaxIterations.
|
|
101
|
+
const DEFAULT_WORKER_TOOLS = ['bash', 'read', 'grep', 'recall'];
|
|
102
|
+
const DEFAULT_WORKER_MAX_ITERATIONS = 8;
|
|
103
|
+
const AGENTIC_WORKER_ROLE =
|
|
104
|
+
'You are an orchestrator worker. Complete ONLY the assigned subtask. Use your ' +
|
|
105
|
+
'tools (shell, file read, grep, recall) to do real work when useful, then report ' +
|
|
106
|
+
'the result concisely. Do not ask questions — act, then summarise what you found.';
|
|
107
|
+
|
|
89
108
|
const PLANNER_SYSTEM = `You are an orchestrator that decomposes a user request into independent subtasks for parallel worker agents.
|
|
90
109
|
|
|
91
110
|
Rules:
|
|
@@ -262,9 +281,47 @@ export function makeOrchestratorProvider(opts = {}) {
|
|
|
262
281
|
// regardless of which worker finished first.
|
|
263
282
|
// The number is clamped to [1, workers.length] so a runaway value
|
|
264
283
|
// can't accidentally over-subscribe a single worker.
|
|
265
|
-
const rawConcurrency = Number.isFinite(o.concurrency) ? Math.floor(o.concurrency) :
|
|
284
|
+
const rawConcurrency = Number.isFinite(o.concurrency) ? Math.floor(o.concurrency) : DEFAULT_CONCURRENCY;
|
|
266
285
|
const concurrency = Math.max(1, Math.min(rawConcurrency, workers.length));
|
|
267
286
|
yield `### 2. Executing ${trimmed.length} subtask${trimmed.length === 1 ? '' : 's'}${concurrency > 1 ? ` (concurrency=${concurrency}, parallel)` : ''}\n\n`;
|
|
287
|
+
|
|
288
|
+
// Run one worker on one subtask through the agentic tool loop. Confined by
|
|
289
|
+
// the caller's sandbox; bounded by workerMaxIterations; abort-propagating.
|
|
290
|
+
const _runAgenticWorker = async (worker, sub) => {
|
|
291
|
+
const { runAgentTurn } = await import('../mas/agent_turn.mjs');
|
|
292
|
+
const r = await runAgentTurn({
|
|
293
|
+
agent: {
|
|
294
|
+
name: worker.name, provider: worker.name, model: worker.model,
|
|
295
|
+
role: AGENTIC_WORKER_ROLE,
|
|
296
|
+
tools: Array.isArray(o.workerTools) ? o.workerTools : DEFAULT_WORKER_TOOLS,
|
|
297
|
+
},
|
|
298
|
+
userMessage: sub.task,
|
|
299
|
+
configDir: callerOpts.configDir,
|
|
300
|
+
cwd: callerOpts.cwd,
|
|
301
|
+
apiKey: keyResolver(cfg, worker.name),
|
|
302
|
+
fetchImpl: callerOpts.fetchImpl,
|
|
303
|
+
sandbox: callerOpts.sandbox,
|
|
304
|
+
signal: callerOpts.signal,
|
|
305
|
+
maxIterations: Number.isFinite(o.workerMaxIterations) ? o.workerMaxIterations : DEFAULT_WORKER_MAX_ITERATIONS,
|
|
306
|
+
});
|
|
307
|
+
return r.text || '';
|
|
308
|
+
};
|
|
309
|
+
// Worker output as a chunk stream. agenticWorkers OFF (default) → the
|
|
310
|
+
// provider's live token stream, byte-stable. ON → one buffered chunk with
|
|
311
|
+
// the agent turn's final answer (tool work happened inside).
|
|
312
|
+
const _workerChunks = async function* (worker, sub) {
|
|
313
|
+
if (o.agenticWorkers) {
|
|
314
|
+
const text = await _runAgenticWorker(worker, sub);
|
|
315
|
+
if (text) yield text;
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
yield* worker.prov.sendMessage([{ role: 'user', content: sub.task }], {
|
|
319
|
+
apiKey: keyResolver(cfg, worker.name),
|
|
320
|
+
model: worker.model || undefined,
|
|
321
|
+
signal: callerOpts.signal,
|
|
322
|
+
});
|
|
323
|
+
};
|
|
324
|
+
|
|
268
325
|
const results = [];
|
|
269
326
|
if (concurrency <= 1) {
|
|
270
327
|
// Sequential streaming path — historical default, preserved.
|
|
@@ -274,11 +331,7 @@ export function makeOrchestratorProvider(opts = {}) {
|
|
|
274
331
|
yield `**Subtask ${sub.id}** \`${worker.name}${worker.model ? ':' + worker.model : ''}\` — ${sub.task}\n\n`;
|
|
275
332
|
let res = '';
|
|
276
333
|
try {
|
|
277
|
-
for await (const chunk of worker
|
|
278
|
-
apiKey: keyResolver(cfg, worker.name),
|
|
279
|
-
model: worker.model || undefined,
|
|
280
|
-
signal: callerOpts.signal,
|
|
281
|
-
})) {
|
|
334
|
+
for await (const chunk of _workerChunks(worker, sub)) {
|
|
282
335
|
const s = String(chunk);
|
|
283
336
|
res += s;
|
|
284
337
|
yield s;
|
|
@@ -301,11 +354,7 @@ export function makeOrchestratorProvider(opts = {}) {
|
|
|
301
354
|
const chunks = [];
|
|
302
355
|
let error = null;
|
|
303
356
|
try {
|
|
304
|
-
for await (const chunk of worker
|
|
305
|
-
apiKey: keyResolver(cfg, worker.name),
|
|
306
|
-
model: worker.model || undefined,
|
|
307
|
-
signal: callerOpts.signal,
|
|
308
|
-
})) {
|
|
357
|
+
for await (const chunk of _workerChunks(worker, sub)) {
|
|
309
358
|
chunks.push(String(chunk));
|
|
310
359
|
}
|
|
311
360
|
} catch (e) {
|
|
@@ -393,3 +442,54 @@ export function makeOrchestratorProvider(opts = {}) {
|
|
|
393
442
|
},
|
|
394
443
|
};
|
|
395
444
|
}
|
|
445
|
+
|
|
446
|
+
// Minimal one-shot worker dispatch for the `delegate` agent tool.
|
|
447
|
+
//
|
|
448
|
+
// `delegate` hands a single subtask to ONE provider — no plan/synthesis.
|
|
449
|
+
// It reuses the same spec-resolution + key-resolution infra the
|
|
450
|
+
// orchestrator's EXECUTE phase uses (a "<provider>[:<model>]" spec routed
|
|
451
|
+
// through PROVIDERS/PROVIDER_INFO, key via lib/config::_resolveAuthKey),
|
|
452
|
+
// but as a standalone call so the `delegate` tool actually runs.
|
|
453
|
+
//
|
|
454
|
+
// Imports are lazy so this module keeps its leaf static-dep graph (it must
|
|
455
|
+
// NOT statically import ./registry.mjs — that re-forms the registry↔orch
|
|
456
|
+
// cycle; see the header note above).
|
|
457
|
+
//
|
|
458
|
+
// job = { worker: '<provider>[:<model>]', prompt, model? }
|
|
459
|
+
// returns { ok:true, text } | { ok:false, error }
|
|
460
|
+
export async function dispatchWorker(job = {}) {
|
|
461
|
+
const workerSpec = String(job?.worker || '');
|
|
462
|
+
const prompt = String(job?.prompt || '');
|
|
463
|
+
if (!workerSpec || !prompt) return { ok: false, error: 'delegate: worker + prompt required' };
|
|
464
|
+
|
|
465
|
+
let registry, config;
|
|
466
|
+
try {
|
|
467
|
+
registry = await import('./registry.mjs');
|
|
468
|
+
config = await import('../lib/config.mjs');
|
|
469
|
+
} catch (e) {
|
|
470
|
+
return { ok: false, error: `delegate: failed to load provider infra: ${e?.message || String(e)}` };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const lookup = (p) => ({ prov: registry.PROVIDERS[p], info: registry.PROVIDER_INFO[p] });
|
|
474
|
+
const worker = _lookupProvider(workerSpec, lookup);
|
|
475
|
+
if (!worker) return { ok: false, error: `delegate: unknown worker provider "${_parseSpec(workerSpec).provider}"` };
|
|
476
|
+
if (worker.name === 'orchestrator') return { ok: false, error: 'delegate: worker cannot be "orchestrator"' };
|
|
477
|
+
// Explicit job.model overrides the model parsed from the spec.
|
|
478
|
+
const model = String(job?.model || '') || worker.model || '';
|
|
479
|
+
|
|
480
|
+
const cfg = config.readConfig() || {};
|
|
481
|
+
const apiKey = config._resolveAuthKey(cfg, worker.name);
|
|
482
|
+
|
|
483
|
+
let text = '';
|
|
484
|
+
try {
|
|
485
|
+
for await (const chunk of worker.prov.sendMessage([{ role: 'user', content: prompt }], {
|
|
486
|
+
apiKey,
|
|
487
|
+
model: model || undefined,
|
|
488
|
+
})) {
|
|
489
|
+
text += String(chunk);
|
|
490
|
+
}
|
|
491
|
+
} catch (e) {
|
|
492
|
+
return { ok: false, error: `delegate: ${worker.name} error: ${e?.message || String(e)}` };
|
|
493
|
+
}
|
|
494
|
+
return { ok: true, text };
|
|
495
|
+
}
|