beast-agent 1.9.0 → 2.0.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.
Files changed (52) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +130 -130
  3. package/bin/beast-agent.js +137 -137
  4. package/package.json +1 -1
  5. package/scripts/fix-electron.js +138 -138
  6. package/scripts/release.js +137 -131
  7. package/scripts/swap-electron.js +40 -40
  8. package/src/agent/agentdefs.js +122 -122
  9. package/src/agent/bots.js +580 -580
  10. package/src/agent/bus.js +389 -389
  11. package/src/agent/computeruse.js +200 -200
  12. package/src/agent/config.js +284 -284
  13. package/src/agent/discord.js +332 -332
  14. package/src/agent/engine.js +75 -10
  15. package/src/agent/kb.js +123 -123
  16. package/src/agent/llm.js +430 -430
  17. package/src/agent/logger.js +90 -90
  18. package/src/agent/mcp.js +427 -427
  19. package/src/agent/mem0.js +605 -605
  20. package/src/agent/memory.js +427 -427
  21. package/src/agent/mqueue.js +124 -124
  22. package/src/agent/pdf.js +20 -20
  23. package/src/agent/research.js +133 -133
  24. package/src/agent/scripts/news.py +113 -113
  25. package/src/agent/scripts/stealthsearch.py +30 -30
  26. package/src/agent/scripts/websearch.py +225 -225
  27. package/src/agent/searxng.js +325 -325
  28. package/src/agent/seeds/brainstorming/SKILL.md +90 -90
  29. package/src/agent/seeds/dispatching-parallel-agents/SKILL.md +120 -120
  30. package/src/agent/seeds/executing-plans/SKILL.md +60 -60
  31. package/src/agent/seeds/subagent-driven-development/SKILL.md +167 -167
  32. package/src/agent/seeds/systematic-debugging/SKILL.md +131 -131
  33. package/src/agent/seeds/test-driven-development/SKILL.md +152 -152
  34. package/src/agent/seeds/verification-before-completion/SKILL.md +63 -63
  35. package/src/agent/seeds/writing-plans/SKILL.md +162 -162
  36. package/src/agent/seeds/writing-skills/SKILL.md +229 -229
  37. package/src/agent/skills.js +652 -652
  38. package/src/agent/store.js +378 -378
  39. package/src/agent/telegram.js +155 -155
  40. package/src/agent/tokens.js +39 -39
  41. package/src/agent/usage.js +125 -125
  42. package/src/agent/watchers.js +312 -312
  43. package/src/agent/watext.js +80 -80
  44. package/src/agent/whatsapp.js +555 -555
  45. package/src/cron.js +255 -255
  46. package/src/main.js +348 -6
  47. package/src/preload.js +9 -0
  48. package/src/renderer/browserPreload.js +73 -73
  49. package/src/renderer/i18n.js +4 -0
  50. package/src/renderer/index.html +448 -409
  51. package/src/renderer/renderer.js +824 -41
  52. package/src/renderer/style.css +264 -5
package/src/agent/llm.js CHANGED
@@ -1,430 +1,430 @@
1
- 'use strict';
2
-
3
- /* OpenAI-compatible streaming chat client with SSE parsing.
4
- Geçici sağlayıcı hatalarında (5xx/429) üstel beklemeyle retry; İNTERNET
5
- KOPMASINDA (fetch failed) çok daha sabırlı: 6 deneme + internet dönene
6
- dek bekleme (main'deki net izleyicisinden beslenir). Akış ortasında kopan
7
- bağlantıda: hiç veri akmadıysa sessiz baştan dener, kısmi metin geldiyse
8
- 'length' gibi döner → chatStreamAuto kaldığı yerden DEVAM ETTİRİR. */
9
-
10
- const RETRYABLE_STATUS = new Set([408, 409, 425, 429, 500, 502, 503, 504]);
11
- const MAX_RETRIES = 3; /* sağlayıcı geçici HTTP hataları */
12
- const NET_MAX_RETRIES = 6; /* bağlantı kopması — daha sabırlı */
13
- const NET_BACKOFF = [1000, 2000, 4000, 8000, 15000, 25000]; /* ≈55 sn toplam */
14
- const NET_WAIT_CAP_MS = 120000; /* internet dönene dek bekleme üst sınırı */
15
-
16
- /* main process'teki net izleyicisi besler: () => boolean | Promise<boolean> */
17
- let _netProbe = null;
18
- function setNetProbe(fn) {
19
- _netProbe = typeof fn === 'function' ? fn : null;
20
- }
21
-
22
- /* fetch failed / ENOTFOUND / ECONNRESET / socket hang up / terminated —
23
- HTTP yanıtı ALINAMADI demektir (status yok) */
24
- function isNetworkError(e) {
25
- if (!e) return false;
26
- if (e.name === 'AbortError') return false;
27
- if (e.status !== undefined) return false;
28
- return true;
29
- }
30
-
31
- /* internet yoksa dönene kadar bekle (en fazla NET_WAIT_CAP_MS); probe yoksa
32
- ya da durum bilinmiyorsa hemen geç */
33
- async function waitForNet(signal) {
34
- if (!_netProbe) return true;
35
- const t0 = Date.now();
36
- for (;;) {
37
- if (signal && signal.aborted) {
38
- const e = new Error('iptal');
39
- e.name = 'AbortError';
40
- throw e;
41
- }
42
- let online;
43
- try { online = await _netProbe(); } catch { online = null; }
44
- if (online !== false) return true; /* false DEĞİLSE devam (bilinmiyor = iyimser) */
45
- if (Date.now() - t0 > NET_WAIT_CAP_MS) return false;
46
- await sleep(2000, signal);
47
- }
48
- }
49
-
50
- const HINTS = {
51
- 401: 'API anahtarı geçersiz',
52
- 402: 'kredi/bakiye bitti',
53
- 403: 'erişim reddedildi (anahtar yetkisi)',
54
- 404: 'model adı ya da endpoint yanlış',
55
- 429: 'hız limitine takıldın',
56
- 500: 'sağlayıcı iç hatası',
57
- 502: 'sağlayıcı upstream bağlantısı koptu',
58
- 503: 'sağlayıcı şu an kullanılamıyor (aşırı yük veya kapalı)',
59
- 504: 'sağlayıcı zaman aşımı',
60
- };
61
-
62
- function friendlyError(status, statusText, detail) {
63
- const hint = HINTS[status];
64
- let msg = `HTTP ${status} ${statusText}`;
65
- if (hint) msg += ` — ${hint}`;
66
- if (detail) msg += `\n${detail}`;
67
- if ([500, 502, 503, 504].includes(status)) {
68
- msg += '\n(geçici olabilir: birkaç saniye sonra tekrar dene ya da model seçiciyi açıp başka bir model dene)';
69
- }
70
- return msg;
71
- }
72
-
73
- /* opencode cache disiplini (provider/transform.ts:1262-1276 port): destekleyen
74
- sağlayıcılara oturum-sabit önbellek anahtarı gönderilir — aynı önek tekrar
75
- kullanıldığında girdi tokenleri cache'ten okunur (~%90 ucuz + hızlı TTFT) */
76
- const CACHE_KEY_PROVIDER_RE = /openai|azure|xai|deepseek|cerebras|deepinfra|mistral|venice/i;
77
- const CACHE_KEY_MODEL_RE = /openai|gpt-|^o[134]|xai|grok|deepseek|cerebras|deepinfra|mistral|venice/i;
78
- function wantsCacheKey(sel) {
79
- if (!sel) return false;
80
- return (
81
- CACHE_KEY_PROVIDER_RE.test(String(sel.providerId || '')) ||
82
- CACHE_KEY_MODEL_RE.test(String(sel.model || ''))
83
- );
84
- }
85
-
86
- /* Retry-After başlığı: saniye ("2"), milisaniye ("120ms" / retry-after-ms)
87
- ya da HTTP-tarih olabilir (opencode session/retry.ts port) */
88
- function parseRetryAfter(v) {
89
- if (v == null) return null;
90
- const s = String(v).trim();
91
- if (!s) return null;
92
- const ms = /^(\d+)\s*ms$/i.exec(s);
93
- if (ms) return Number(ms[1]);
94
- if (/^\d+(\.\d+)?$/.test(s)) return Number(s) * 1000;
95
- const d = Date.parse(s);
96
- if (!Number.isNaN(d)) return Math.max(0, d - Date.now());
97
- return null;
98
- }
99
-
100
- const RETRY_AFTER_CAP_MS = 30000; /* opencode retry.ts:39 — headersız bekleme tavanı */
101
- function clampMs(v) {
102
- return Math.min(RETRY_AFTER_CAP_MS, Math.max(0, Number(v) || 0));
103
- }
104
-
105
- function sleep(ms, signal) {
106
- return new Promise((resolve, reject) => {
107
- const t = setTimeout(resolve, ms);
108
- if (signal) {
109
- signal.addEventListener(
110
- 'abort',
111
- () => {
112
- clearTimeout(t);
113
- const e = new Error('iptal');
114
- e.name = 'AbortError';
115
- reject(e);
116
- },
117
- { once: true }
118
- );
119
- }
120
- });
121
- }
122
-
123
- /* fn: denenecek istek. Sağlayıcı geçici HTTP hatalarında 0.8s→1.6s→3.2s;
124
- İNTERNET KOPMASINDA (status yok) 6 denemeye kadar 1s→2s→4s→8s→15s→25s ve
125
- net izleyicisi "çevrimdışı" diyorsa internet dönene kadar bekler. */
126
- async function withRetries(fn, { signal, onRetry } = {}) {
127
- let attempt = 0;
128
- for (;;) {
129
- try {
130
- return await fn();
131
- } catch (e) {
132
- const aborted = e && (e.name === 'AbortError' || (signal && signal.aborted));
133
- const status = e && e.status;
134
- const netErr = !aborted && status === undefined;
135
- const retriable = !aborted && (status === undefined || RETRYABLE_STATUS.has(status));
136
- if (!retriable) throw e;
137
- const cap = netErr ? NET_MAX_RETRIES : MAX_RETRIES;
138
- if (attempt >= cap) throw e;
139
- if (netErr && _netProbe) {
140
- /* internet kopmuş: dönene kadar bekle — kısa kopmalarda görev ölmez */
141
- await waitForNet(signal);
142
- }
143
- attempt++;
144
- try {
145
- onRetry && onRetry(attempt, status);
146
- } catch {}
147
- /* sağlayıcı retry-after dediye: backoff yerine ona uy (30 sn tavan);
148
- opencode session/retry.ts:26-31 ile aynı politika */
149
- let wait;
150
- if (e.retryAfterMs) wait = clampMs(e.retryAfterMs);
151
- else
152
- wait = netErr
153
- ? NET_BACKOFF[Math.min(attempt - 1, NET_BACKOFF.length - 1)]
154
- : 800 * Math.pow(2, attempt - 1);
155
- await sleep(wait, signal);
156
- }
157
- }
158
- }
159
-
160
- async function openChat(sel, body, { stream, signal, omitReasoning } = {}) {
161
- const payload = {
162
- model: sel.model,
163
- messages: body.messages,
164
- stream,
165
- ...(body.tools && body.tools.length ? { tools: body.tools } : {}),
166
- temperature: body.temperature ?? 0.6,
167
- };
168
- /* Düşünme (reasoning) seviyesi: OpenAI-style reasoning_effort —
169
- OpenRouter ve GPT-5 ailesi dahil uyumlu sağlayıcılar kabul eder. */
170
- if (body.reasoningEffort && !omitReasoning) {
171
- payload.reasoning_effort = String(body.reasoningEffort);
172
- }
173
- /* prompt cache anahtarı: oturum başına sabit → sağlayıcı önek önbelleği tutar */
174
- if (body.cacheKey && wantsCacheKey(sel)) {
175
- payload.prompt_cache_key = String(body.cacheKey);
176
- }
177
- return fetch(sel.url, {
178
- method: 'POST',
179
- signal,
180
- headers: {
181
- 'Content-Type': 'application/json',
182
- Authorization: `Bearer ${sel.key}`,
183
- 'HTTP-Referer': 'https://localhost/beast',
184
- 'X-Title': 'Beast Agent',
185
- },
186
- body: JSON.stringify(payload),
187
- });
188
- }
189
-
190
- async function chatStream(sel, body, { signal, onDelta, onRetry } = {}) {
191
- try {
192
- return await streamOnce(sel, body, { signal, onDelta });
193
- } catch (e) {
194
- /* Model reasoning_effort'u desteklemiyorsa (400) parametreyi atlayıp
195
- TEK seferlik sessiz retry — kullanıcı hatayı görmez. */
196
- if (
197
- body.reasoningEffort &&
198
- e && e.status === 400 &&
199
- /reason|effort|thinking/i.test(String(e.message || ''))
200
- ) {
201
- return await streamOnce(sel, body, { signal, onDelta }, true);
202
- }
203
- throw e;
204
- }
205
- }
206
-
207
- async function streamOnce(sel, body, { signal, onDelta, onRetry } = {}, omitReasoning = false) {
208
- /* akış durumu dışarıda tutulur: gövde okunurken bağlantı koparsa
209
- ne kadarı geldiğine bakılır (kısmi metin → devam; boş/araç → baştan) */
210
- const state = { content: '', reasoning: '', toolCalls: [], usage: null, finishReason: null };
211
- let dropAttempt = 0;
212
-
213
- for (;;) {
214
- const res = await withRetries(
215
- async () => {
216
- const r = await openChat(sel, body, { stream: true, signal, omitReasoning });
217
- if (!r.ok) {
218
- let detail = '';
219
- try {
220
- detail = (await r.text()).slice(0, 300);
221
- } catch {}
222
- const err = new Error(friendlyError(r.status, r.statusText, detail));
223
- err.status = r.status;
224
- err.retryAfterMs = parseRetryAfter(
225
- r.headers && (r.headers.get('retry-after-ms') || r.headers.get('retry-after'))
226
- );
227
- throw err;
228
- }
229
- return r;
230
- },
231
- { signal, onRetry }
232
- );
233
-
234
- try {
235
- const reader = res.body.getReader();
236
- const decoder = new TextDecoder();
237
- let buf = '';
238
- /* SERT İPTAL: abort gelirse gövdeyi fiziksel kapat (reader.cancel) —
239
- sağlayıcı/fetch abort'u gövdeye taşımasa bile okuma döngüsü ANINDA
240
- biter, token akışı kesilir */
241
- let onAbort = null;
242
- if (signal) {
243
- onAbort = () => {
244
- try {
245
- const p = reader.cancel();
246
- if (p && typeof p.catch === 'function') p.catch(() => {});
247
- } catch {}
248
- };
249
- signal.addEventListener('abort', onAbort, { once: true });
250
- }
251
- try {
252
- /* tek satır SSE olayını işle — hem okuma döngüsü hem SON FLUSH kullanır */
253
- const handleLine = (line) => {
254
- line = line.trim();
255
- if (!line.startsWith('data:')) return;
256
- const data = line.slice(5).trim();
257
- if (!data || data === '[DONE]') return;
258
-
259
- let json;
260
- try {
261
- json = JSON.parse(data);
262
- } catch {
263
- return;
264
- }
265
-
266
- if (json.usage) state.usage = json.usage;
267
- const ch = json.choices && json.choices[0];
268
- if (!ch) return;
269
- if (ch.finish_reason) state.finishReason = ch.finish_reason;
270
- const d = ch.delta || {};
271
- if (d.content) {
272
- state.content += d.content;
273
- onDelta && onDelta(d.content, state.content);
274
- }
275
- if (d.reasoning_content) state.reasoning += d.reasoning_content;
276
- if (d.reasoning) state.reasoning += d.reasoning;
277
- for (const tc of d.tool_calls || []) {
278
- const i = typeof tc.index === 'number' ? tc.index : state.toolCalls.length;
279
- while (state.toolCalls.length <= i) {
280
- state.toolCalls.push({ id: '', type: 'function', function: { name: '', arguments: '' } });
281
- }
282
- if (tc.id) state.toolCalls[i].id = tc.id;
283
- if (tc.function) {
284
- if (tc.function.name) state.toolCalls[i].function.name += tc.function.name;
285
- if (tc.function.arguments) state.toolCalls[i].function.arguments += tc.function.arguments;
286
- }
287
- }
288
- };
289
-
290
- while (true) {
291
- const { done, value } = await reader.read();
292
- if (signal && signal.aborted) {
293
- const e = new Error('iptal');
294
- e.name = 'AbortError';
295
- throw e;
296
- }
297
- if (done) break;
298
- buf += decoder.decode(value, { stream: true });
299
-
300
- let idx;
301
- while ((idx = buf.indexOf('\n')) !== -1) {
302
- const line = buf.slice(0, idx);
303
- buf = buf.slice(idx + 1);
304
- handleLine(line);
305
- }
306
- }
307
-
308
- /* SON FLUSH — KRİTİK: akış son parçası '\n' ile bitmeyebilir.
309
- Okuma bitince kalan buf'u Decoder'dan boşaltıp işlemezsek
310
- cevabın SON kelimesi/cümlesi kaybolur (SSE sonu öyle gelir). */
311
- buf += decoder.decode();
312
- if (buf.trim()) handleLine(buf);
313
-
314
- return { ...state };
315
- } finally {
316
- if (signal && onAbort) {
317
- try { signal.removeEventListener('abort', onAbort); } catch {}
318
- }
319
- }
320
- } catch (e) {
321
- /* okuma sırasında bağlantı koptu */
322
- const aborted = e && (e.name === 'AbortError' || (signal && signal.aborted));
323
- if (!aborted && isNetworkError(e)) {
324
- if (state.content && !state.toolCalls.length) {
325
- /* kısmi metin geldi, araç çağrısı yok → hata balonu YOK:
326
- 'length' gibi dön, chatStreamAuto CONTINUE_PROMPT ile kaldığı
327
- yerden sürdürür ve metni birleştirir */
328
- return { ...state, finishReason: 'length' };
329
- }
330
- /* hiç veri akmadı YA DA araç çağrısı yarım kaldı → BAŞTAN dene */
331
- if (dropAttempt >= NET_MAX_RETRIES) throw e;
332
- dropAttempt++;
333
- try { onRetry && onRetry(dropAttempt, undefined); } catch {}
334
- if (_netProbe) await waitForNet(signal);
335
- await sleep(NET_BACKOFF[Math.min(dropAttempt - 1, NET_BACKOFF.length - 1)], signal);
336
- continue; /* yeni stream — UI'daki kısmi token'lar nihai mesajla ezilir */
337
- }
338
- throw e;
339
- }
340
- }
341
- }
342
-
343
- /* usage toplama: devam turları gerçek faturalamayı yansıtsın (prompt yeniden sayılır) */
344
- function sumUsage(a, b) {
345
- if (!a) return b || null;
346
- if (!b) return a;
347
- const out = {};
348
- for (const k of new Set([...Object.keys(a), ...Object.keys(b)])) {
349
- if (typeof a[k] === 'number' && typeof b[k] === 'number') out[k] = a[k] + b[k];
350
- else out[k] = b[k] !== undefined ? b[k] : a[k];
351
- }
352
- return out;
353
- }
354
-
355
- /* OTOMATİK DEVAM: model cevabı çıkış limiti (finish_reason='length') yüzünden
356
- kelime ortasında kesilirse — özellikle free modellerin düşük varsayılan
357
- limitlerinde olur — yarım cümle kullanıcıya gitmesin. Kaldığı yerden devam
358
- istenip metin birleştirilir (en fazla 2 devam turu). Araç çağrılı yanıtlarda
359
- ve içerik hiç gelmediyse devreye girmez; orijinal mesaj dizisi DEĞİŞTİRİLMEZ. */
360
- const CONTINUE_PROMPT =
361
- 'Yanıtın çıkış limiti nedeniyle ortadan kesildi. Kaldığın yerden DEVAM ET — baştan başlama, özetleme, açıklama yapma; yalnızca kesilen kısmın DEVAMINI yaz.';
362
-
363
- async function chatStreamAuto(sel, body, opts = {}) {
364
- const first = await chatStream(sel, body, opts);
365
- if (first.finishReason !== 'length' || !first.content || (first.toolCalls && first.toolCalls.length)) {
366
- return first;
367
- }
368
- let full = first.content;
369
- let last = first;
370
- for (let i = 0; i < 2; i++) {
371
- const msgs2 = body.messages.slice();
372
- msgs2.push({ role: 'assistant', content: full });
373
- msgs2.push({ role: 'user', content: CONTINUE_PROMPT });
374
- let cont;
375
- try {
376
- cont = await chatStream(sel, { ...body, messages: msgs2 }, opts);
377
- } catch {
378
- break; // devam turu patlarsa elimizdeki kısmi yanıtla döneriz
379
- }
380
- if (!cont.content || (cont.toolCalls && cont.toolCalls.length)) break;
381
- full += cont.content;
382
- last = { ...cont, content: full, usage: sumUsage(first.usage, cont.usage) };
383
- if (cont.finishReason !== 'length') break;
384
- }
385
- return last;
386
- }
387
-
388
- async function chatOnce(sel, body, { signal, onRetry } = {}) {
389
- const res = await withRetries(
390
- async () => {
391
- const r = await openChat(sel, body, { stream: false, signal });
392
- if (!r.ok) {
393
- let detail = '';
394
- try {
395
- detail = (await r.text()).slice(0, 300);
396
- } catch {}
397
- const err = new Error(friendlyError(r.status, r.statusText, detail));
398
- err.status = r.status;
399
- err.retryAfterMs = parseRetryAfter(
400
- r.headers && (r.headers.get('retry-after-ms') || r.headers.get('retry-after'))
401
- );
402
- throw err;
403
- }
404
- return r;
405
- },
406
- { signal, onRetry }
407
- );
408
- const json = await res.json();
409
- const msg = json.choices?.[0]?.message || {};
410
- return {
411
- content: msg.content || '',
412
- reasoning: msg.reasoning_content || msg.reasoning || '',
413
- toolCalls: msg.tool_calls || [],
414
- usage: json.usage || null,
415
- finishReason: json.choices?.[0]?.finish_reason,
416
- };
417
- }
418
-
419
- module.exports = {
420
- chatStream,
421
- chatStreamAuto,
422
- chatOnce,
423
- withRetries,
424
- friendlyError,
425
- sumUsage,
426
- setNetProbe,
427
- isNetworkError,
428
- wantsCacheKey,
429
- parseRetryAfter,
430
- };
1
+ 'use strict';
2
+
3
+ /* OpenAI-compatible streaming chat client with SSE parsing.
4
+ Geçici sağlayıcı hatalarında (5xx/429) üstel beklemeyle retry; İNTERNET
5
+ KOPMASINDA (fetch failed) çok daha sabırlı: 6 deneme + internet dönene
6
+ dek bekleme (main'deki net izleyicisinden beslenir). Akış ortasında kopan
7
+ bağlantıda: hiç veri akmadıysa sessiz baştan dener, kısmi metin geldiyse
8
+ 'length' gibi döner → chatStreamAuto kaldığı yerden DEVAM ETTİRİR. */
9
+
10
+ const RETRYABLE_STATUS = new Set([408, 409, 425, 429, 500, 502, 503, 504]);
11
+ const MAX_RETRIES = 3; /* sağlayıcı geçici HTTP hataları */
12
+ const NET_MAX_RETRIES = 6; /* bağlantı kopması — daha sabırlı */
13
+ const NET_BACKOFF = [1000, 2000, 4000, 8000, 15000, 25000]; /* ≈55 sn toplam */
14
+ const NET_WAIT_CAP_MS = 120000; /* internet dönene dek bekleme üst sınırı */
15
+
16
+ /* main process'teki net izleyicisi besler: () => boolean | Promise<boolean> */
17
+ let _netProbe = null;
18
+ function setNetProbe(fn) {
19
+ _netProbe = typeof fn === 'function' ? fn : null;
20
+ }
21
+
22
+ /* fetch failed / ENOTFOUND / ECONNRESET / socket hang up / terminated —
23
+ HTTP yanıtı ALINAMADI demektir (status yok) */
24
+ function isNetworkError(e) {
25
+ if (!e) return false;
26
+ if (e.name === 'AbortError') return false;
27
+ if (e.status !== undefined) return false;
28
+ return true;
29
+ }
30
+
31
+ /* internet yoksa dönene kadar bekle (en fazla NET_WAIT_CAP_MS); probe yoksa
32
+ ya da durum bilinmiyorsa hemen geç */
33
+ async function waitForNet(signal) {
34
+ if (!_netProbe) return true;
35
+ const t0 = Date.now();
36
+ for (;;) {
37
+ if (signal && signal.aborted) {
38
+ const e = new Error('iptal');
39
+ e.name = 'AbortError';
40
+ throw e;
41
+ }
42
+ let online;
43
+ try { online = await _netProbe(); } catch { online = null; }
44
+ if (online !== false) return true; /* false DEĞİLSE devam (bilinmiyor = iyimser) */
45
+ if (Date.now() - t0 > NET_WAIT_CAP_MS) return false;
46
+ await sleep(2000, signal);
47
+ }
48
+ }
49
+
50
+ const HINTS = {
51
+ 401: 'API anahtarı geçersiz',
52
+ 402: 'kredi/bakiye bitti',
53
+ 403: 'erişim reddedildi (anahtar yetkisi)',
54
+ 404: 'model adı ya da endpoint yanlış',
55
+ 429: 'hız limitine takıldın',
56
+ 500: 'sağlayıcı iç hatası',
57
+ 502: 'sağlayıcı upstream bağlantısı koptu',
58
+ 503: 'sağlayıcı şu an kullanılamıyor (aşırı yük veya kapalı)',
59
+ 504: 'sağlayıcı zaman aşımı',
60
+ };
61
+
62
+ function friendlyError(status, statusText, detail) {
63
+ const hint = HINTS[status];
64
+ let msg = `HTTP ${status} ${statusText}`;
65
+ if (hint) msg += ` — ${hint}`;
66
+ if (detail) msg += `\n${detail}`;
67
+ if ([500, 502, 503, 504].includes(status)) {
68
+ msg += '\n(geçici olabilir: birkaç saniye sonra tekrar dene ya da model seçiciyi açıp başka bir model dene)';
69
+ }
70
+ return msg;
71
+ }
72
+
73
+ /* opencode cache disiplini (provider/transform.ts:1262-1276 port): destekleyen
74
+ sağlayıcılara oturum-sabit önbellek anahtarı gönderilir — aynı önek tekrar
75
+ kullanıldığında girdi tokenleri cache'ten okunur (~%90 ucuz + hızlı TTFT) */
76
+ const CACHE_KEY_PROVIDER_RE = /openai|azure|xai|deepseek|cerebras|deepinfra|mistral|venice/i;
77
+ const CACHE_KEY_MODEL_RE = /openai|gpt-|^o[134]|xai|grok|deepseek|cerebras|deepinfra|mistral|venice/i;
78
+ function wantsCacheKey(sel) {
79
+ if (!sel) return false;
80
+ return (
81
+ CACHE_KEY_PROVIDER_RE.test(String(sel.providerId || '')) ||
82
+ CACHE_KEY_MODEL_RE.test(String(sel.model || ''))
83
+ );
84
+ }
85
+
86
+ /* Retry-After başlığı: saniye ("2"), milisaniye ("120ms" / retry-after-ms)
87
+ ya da HTTP-tarih olabilir (opencode session/retry.ts port) */
88
+ function parseRetryAfter(v) {
89
+ if (v == null) return null;
90
+ const s = String(v).trim();
91
+ if (!s) return null;
92
+ const ms = /^(\d+)\s*ms$/i.exec(s);
93
+ if (ms) return Number(ms[1]);
94
+ if (/^\d+(\.\d+)?$/.test(s)) return Number(s) * 1000;
95
+ const d = Date.parse(s);
96
+ if (!Number.isNaN(d)) return Math.max(0, d - Date.now());
97
+ return null;
98
+ }
99
+
100
+ const RETRY_AFTER_CAP_MS = 30000; /* opencode retry.ts:39 — headersız bekleme tavanı */
101
+ function clampMs(v) {
102
+ return Math.min(RETRY_AFTER_CAP_MS, Math.max(0, Number(v) || 0));
103
+ }
104
+
105
+ function sleep(ms, signal) {
106
+ return new Promise((resolve, reject) => {
107
+ const t = setTimeout(resolve, ms);
108
+ if (signal) {
109
+ signal.addEventListener(
110
+ 'abort',
111
+ () => {
112
+ clearTimeout(t);
113
+ const e = new Error('iptal');
114
+ e.name = 'AbortError';
115
+ reject(e);
116
+ },
117
+ { once: true }
118
+ );
119
+ }
120
+ });
121
+ }
122
+
123
+ /* fn: denenecek istek. Sağlayıcı geçici HTTP hatalarında 0.8s→1.6s→3.2s;
124
+ İNTERNET KOPMASINDA (status yok) 6 denemeye kadar 1s→2s→4s→8s→15s→25s ve
125
+ net izleyicisi "çevrimdışı" diyorsa internet dönene kadar bekler. */
126
+ async function withRetries(fn, { signal, onRetry } = {}) {
127
+ let attempt = 0;
128
+ for (;;) {
129
+ try {
130
+ return await fn();
131
+ } catch (e) {
132
+ const aborted = e && (e.name === 'AbortError' || (signal && signal.aborted));
133
+ const status = e && e.status;
134
+ const netErr = !aborted && status === undefined;
135
+ const retriable = !aborted && (status === undefined || RETRYABLE_STATUS.has(status));
136
+ if (!retriable) throw e;
137
+ const cap = netErr ? NET_MAX_RETRIES : MAX_RETRIES;
138
+ if (attempt >= cap) throw e;
139
+ if (netErr && _netProbe) {
140
+ /* internet kopmuş: dönene kadar bekle — kısa kopmalarda görev ölmez */
141
+ await waitForNet(signal);
142
+ }
143
+ attempt++;
144
+ try {
145
+ onRetry && onRetry(attempt, status);
146
+ } catch {}
147
+ /* sağlayıcı retry-after dediye: backoff yerine ona uy (30 sn tavan);
148
+ opencode session/retry.ts:26-31 ile aynı politika */
149
+ let wait;
150
+ if (e.retryAfterMs) wait = clampMs(e.retryAfterMs);
151
+ else
152
+ wait = netErr
153
+ ? NET_BACKOFF[Math.min(attempt - 1, NET_BACKOFF.length - 1)]
154
+ : 800 * Math.pow(2, attempt - 1);
155
+ await sleep(wait, signal);
156
+ }
157
+ }
158
+ }
159
+
160
+ async function openChat(sel, body, { stream, signal, omitReasoning } = {}) {
161
+ const payload = {
162
+ model: sel.model,
163
+ messages: body.messages,
164
+ stream,
165
+ ...(body.tools && body.tools.length ? { tools: body.tools } : {}),
166
+ temperature: body.temperature ?? 0.6,
167
+ };
168
+ /* Düşünme (reasoning) seviyesi: OpenAI-style reasoning_effort —
169
+ OpenRouter ve GPT-5 ailesi dahil uyumlu sağlayıcılar kabul eder. */
170
+ if (body.reasoningEffort && !omitReasoning) {
171
+ payload.reasoning_effort = String(body.reasoningEffort);
172
+ }
173
+ /* prompt cache anahtarı: oturum başına sabit → sağlayıcı önek önbelleği tutar */
174
+ if (body.cacheKey && wantsCacheKey(sel)) {
175
+ payload.prompt_cache_key = String(body.cacheKey);
176
+ }
177
+ return fetch(sel.url, {
178
+ method: 'POST',
179
+ signal,
180
+ headers: {
181
+ 'Content-Type': 'application/json',
182
+ Authorization: `Bearer ${sel.key}`,
183
+ 'HTTP-Referer': 'https://localhost/beast',
184
+ 'X-Title': 'Beast Agent',
185
+ },
186
+ body: JSON.stringify(payload),
187
+ });
188
+ }
189
+
190
+ async function chatStream(sel, body, { signal, onDelta, onRetry } = {}) {
191
+ try {
192
+ return await streamOnce(sel, body, { signal, onDelta });
193
+ } catch (e) {
194
+ /* Model reasoning_effort'u desteklemiyorsa (400) parametreyi atlayıp
195
+ TEK seferlik sessiz retry — kullanıcı hatayı görmez. */
196
+ if (
197
+ body.reasoningEffort &&
198
+ e && e.status === 400 &&
199
+ /reason|effort|thinking/i.test(String(e.message || ''))
200
+ ) {
201
+ return await streamOnce(sel, body, { signal, onDelta }, true);
202
+ }
203
+ throw e;
204
+ }
205
+ }
206
+
207
+ async function streamOnce(sel, body, { signal, onDelta, onRetry } = {}, omitReasoning = false) {
208
+ /* akış durumu dışarıda tutulur: gövde okunurken bağlantı koparsa
209
+ ne kadarı geldiğine bakılır (kısmi metin → devam; boş/araç → baştan) */
210
+ const state = { content: '', reasoning: '', toolCalls: [], usage: null, finishReason: null };
211
+ let dropAttempt = 0;
212
+
213
+ for (;;) {
214
+ const res = await withRetries(
215
+ async () => {
216
+ const r = await openChat(sel, body, { stream: true, signal, omitReasoning });
217
+ if (!r.ok) {
218
+ let detail = '';
219
+ try {
220
+ detail = (await r.text()).slice(0, 300);
221
+ } catch {}
222
+ const err = new Error(friendlyError(r.status, r.statusText, detail));
223
+ err.status = r.status;
224
+ err.retryAfterMs = parseRetryAfter(
225
+ r.headers && (r.headers.get('retry-after-ms') || r.headers.get('retry-after'))
226
+ );
227
+ throw err;
228
+ }
229
+ return r;
230
+ },
231
+ { signal, onRetry }
232
+ );
233
+
234
+ try {
235
+ const reader = res.body.getReader();
236
+ const decoder = new TextDecoder();
237
+ let buf = '';
238
+ /* SERT İPTAL: abort gelirse gövdeyi fiziksel kapat (reader.cancel) —
239
+ sağlayıcı/fetch abort'u gövdeye taşımasa bile okuma döngüsü ANINDA
240
+ biter, token akışı kesilir */
241
+ let onAbort = null;
242
+ if (signal) {
243
+ onAbort = () => {
244
+ try {
245
+ const p = reader.cancel();
246
+ if (p && typeof p.catch === 'function') p.catch(() => {});
247
+ } catch {}
248
+ };
249
+ signal.addEventListener('abort', onAbort, { once: true });
250
+ }
251
+ try {
252
+ /* tek satır SSE olayını işle — hem okuma döngüsü hem SON FLUSH kullanır */
253
+ const handleLine = (line) => {
254
+ line = line.trim();
255
+ if (!line.startsWith('data:')) return;
256
+ const data = line.slice(5).trim();
257
+ if (!data || data === '[DONE]') return;
258
+
259
+ let json;
260
+ try {
261
+ json = JSON.parse(data);
262
+ } catch {
263
+ return;
264
+ }
265
+
266
+ if (json.usage) state.usage = json.usage;
267
+ const ch = json.choices && json.choices[0];
268
+ if (!ch) return;
269
+ if (ch.finish_reason) state.finishReason = ch.finish_reason;
270
+ const d = ch.delta || {};
271
+ if (d.content) {
272
+ state.content += d.content;
273
+ onDelta && onDelta(d.content, state.content);
274
+ }
275
+ if (d.reasoning_content) state.reasoning += d.reasoning_content;
276
+ if (d.reasoning) state.reasoning += d.reasoning;
277
+ for (const tc of d.tool_calls || []) {
278
+ const i = typeof tc.index === 'number' ? tc.index : state.toolCalls.length;
279
+ while (state.toolCalls.length <= i) {
280
+ state.toolCalls.push({ id: '', type: 'function', function: { name: '', arguments: '' } });
281
+ }
282
+ if (tc.id) state.toolCalls[i].id = tc.id;
283
+ if (tc.function) {
284
+ if (tc.function.name) state.toolCalls[i].function.name += tc.function.name;
285
+ if (tc.function.arguments) state.toolCalls[i].function.arguments += tc.function.arguments;
286
+ }
287
+ }
288
+ };
289
+
290
+ while (true) {
291
+ const { done, value } = await reader.read();
292
+ if (signal && signal.aborted) {
293
+ const e = new Error('iptal');
294
+ e.name = 'AbortError';
295
+ throw e;
296
+ }
297
+ if (done) break;
298
+ buf += decoder.decode(value, { stream: true });
299
+
300
+ let idx;
301
+ while ((idx = buf.indexOf('\n')) !== -1) {
302
+ const line = buf.slice(0, idx);
303
+ buf = buf.slice(idx + 1);
304
+ handleLine(line);
305
+ }
306
+ }
307
+
308
+ /* SON FLUSH — KRİTİK: akış son parçası '\n' ile bitmeyebilir.
309
+ Okuma bitince kalan buf'u Decoder'dan boşaltıp işlemezsek
310
+ cevabın SON kelimesi/cümlesi kaybolur (SSE sonu öyle gelir). */
311
+ buf += decoder.decode();
312
+ if (buf.trim()) handleLine(buf);
313
+
314
+ return { ...state };
315
+ } finally {
316
+ if (signal && onAbort) {
317
+ try { signal.removeEventListener('abort', onAbort); } catch {}
318
+ }
319
+ }
320
+ } catch (e) {
321
+ /* okuma sırasında bağlantı koptu */
322
+ const aborted = e && (e.name === 'AbortError' || (signal && signal.aborted));
323
+ if (!aborted && isNetworkError(e)) {
324
+ if (state.content && !state.toolCalls.length) {
325
+ /* kısmi metin geldi, araç çağrısı yok → hata balonu YOK:
326
+ 'length' gibi dön, chatStreamAuto CONTINUE_PROMPT ile kaldığı
327
+ yerden sürdürür ve metni birleştirir */
328
+ return { ...state, finishReason: 'length' };
329
+ }
330
+ /* hiç veri akmadı YA DA araç çağrısı yarım kaldı → BAŞTAN dene */
331
+ if (dropAttempt >= NET_MAX_RETRIES) throw e;
332
+ dropAttempt++;
333
+ try { onRetry && onRetry(dropAttempt, undefined); } catch {}
334
+ if (_netProbe) await waitForNet(signal);
335
+ await sleep(NET_BACKOFF[Math.min(dropAttempt - 1, NET_BACKOFF.length - 1)], signal);
336
+ continue; /* yeni stream — UI'daki kısmi token'lar nihai mesajla ezilir */
337
+ }
338
+ throw e;
339
+ }
340
+ }
341
+ }
342
+
343
+ /* usage toplama: devam turları gerçek faturalamayı yansıtsın (prompt yeniden sayılır) */
344
+ function sumUsage(a, b) {
345
+ if (!a) return b || null;
346
+ if (!b) return a;
347
+ const out = {};
348
+ for (const k of new Set([...Object.keys(a), ...Object.keys(b)])) {
349
+ if (typeof a[k] === 'number' && typeof b[k] === 'number') out[k] = a[k] + b[k];
350
+ else out[k] = b[k] !== undefined ? b[k] : a[k];
351
+ }
352
+ return out;
353
+ }
354
+
355
+ /* OTOMATİK DEVAM: model cevabı çıkış limiti (finish_reason='length') yüzünden
356
+ kelime ortasında kesilirse — özellikle free modellerin düşük varsayılan
357
+ limitlerinde olur — yarım cümle kullanıcıya gitmesin. Kaldığı yerden devam
358
+ istenip metin birleştirilir (en fazla 2 devam turu). Araç çağrılı yanıtlarda
359
+ ve içerik hiç gelmediyse devreye girmez; orijinal mesaj dizisi DEĞİŞTİRİLMEZ. */
360
+ const CONTINUE_PROMPT =
361
+ 'Yanıtın çıkış limiti nedeniyle ortadan kesildi. Kaldığın yerden DEVAM ET — baştan başlama, özetleme, açıklama yapma; yalnızca kesilen kısmın DEVAMINI yaz.';
362
+
363
+ async function chatStreamAuto(sel, body, opts = {}) {
364
+ const first = await chatStream(sel, body, opts);
365
+ if (first.finishReason !== 'length' || !first.content || (first.toolCalls && first.toolCalls.length)) {
366
+ return first;
367
+ }
368
+ let full = first.content;
369
+ let last = first;
370
+ for (let i = 0; i < 2; i++) {
371
+ const msgs2 = body.messages.slice();
372
+ msgs2.push({ role: 'assistant', content: full });
373
+ msgs2.push({ role: 'user', content: CONTINUE_PROMPT });
374
+ let cont;
375
+ try {
376
+ cont = await chatStream(sel, { ...body, messages: msgs2 }, opts);
377
+ } catch {
378
+ break; // devam turu patlarsa elimizdeki kısmi yanıtla döneriz
379
+ }
380
+ if (!cont.content || (cont.toolCalls && cont.toolCalls.length)) break;
381
+ full += cont.content;
382
+ last = { ...cont, content: full, usage: sumUsage(first.usage, cont.usage) };
383
+ if (cont.finishReason !== 'length') break;
384
+ }
385
+ return last;
386
+ }
387
+
388
+ async function chatOnce(sel, body, { signal, onRetry } = {}) {
389
+ const res = await withRetries(
390
+ async () => {
391
+ const r = await openChat(sel, body, { stream: false, signal });
392
+ if (!r.ok) {
393
+ let detail = '';
394
+ try {
395
+ detail = (await r.text()).slice(0, 300);
396
+ } catch {}
397
+ const err = new Error(friendlyError(r.status, r.statusText, detail));
398
+ err.status = r.status;
399
+ err.retryAfterMs = parseRetryAfter(
400
+ r.headers && (r.headers.get('retry-after-ms') || r.headers.get('retry-after'))
401
+ );
402
+ throw err;
403
+ }
404
+ return r;
405
+ },
406
+ { signal, onRetry }
407
+ );
408
+ const json = await res.json();
409
+ const msg = json.choices?.[0]?.message || {};
410
+ return {
411
+ content: msg.content || '',
412
+ reasoning: msg.reasoning_content || msg.reasoning || '',
413
+ toolCalls: msg.tool_calls || [],
414
+ usage: json.usage || null,
415
+ finishReason: json.choices?.[0]?.finish_reason,
416
+ };
417
+ }
418
+
419
+ module.exports = {
420
+ chatStream,
421
+ chatStreamAuto,
422
+ chatOnce,
423
+ withRetries,
424
+ friendlyError,
425
+ sumUsage,
426
+ setNetProbe,
427
+ isNetworkError,
428
+ wantsCacheKey,
429
+ parseRetryAfter,
430
+ };