mixdog 0.7.8 → 0.7.12
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/.claude-plugin/marketplace.json +5 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +40 -0
- package/README.md +198 -251
- package/bin/statusline-launcher.mjs +5 -1
- package/bin/statusline-lib.mjs +14 -6
- package/bin/statusline.mjs +14 -6
- package/hooks/lib/settings-loader.cjs +4 -3
- package/hooks/pre-tool-subagent.cjs +7 -2
- package/hooks/session-start.cjs +52 -24
- package/lib/mixdog-debug.cjs +163 -0
- package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
- package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
- package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
- package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
- package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
- package/package.json +1 -1
- package/scripts/builtin-utils-smoke.mjs +14 -8
- package/scripts/bump.mjs +80 -0
- package/scripts/doctor.mjs +8 -3
- package/scripts/mutation-io-smoke.mjs +17 -1
- package/scripts/openai-oauth-catalog-smoke.mjs +53 -0
- package/scripts/permission-eval-smoke.mjs +18 -1
- package/scripts/statusline-launcher-smoke.mjs +2 -2
- package/scripts/webhook-selfheal-smoke.mjs +1 -3
- package/server-main.mjs +57 -3
- package/setup/config-merge.mjs +0 -1
- package/setup/install.mjs +241 -51
- package/setup/mixdog-cli.mjs +30 -3
- package/setup/setup-server.mjs +21 -33
- package/setup/setup.html +46 -11
- package/setup/tui.mjs +35 -316
- package/src/agent/orchestrator/config.mjs +0 -1
- package/src/agent/orchestrator/providers/anthropic-oauth.mjs +2 -5
- package/src/agent/orchestrator/providers/anthropic.mjs +243 -86
- package/src/agent/orchestrator/providers/gemini.mjs +386 -31
- package/src/agent/orchestrator/providers/grok-oauth.mjs +2 -5
- package/src/agent/orchestrator/providers/model-catalog.mjs +146 -13
- package/src/agent/orchestrator/providers/openai-compat-stream.mjs +366 -0
- package/src/agent/orchestrator/providers/openai-compat.mjs +74 -30
- package/src/agent/orchestrator/providers/openai-oauth-ws.mjs +2 -1
- package/src/agent/orchestrator/providers/openai-oauth.mjs +66 -13
- package/src/agent/orchestrator/providers/openai-ws.mjs +23 -0
- package/src/agent/orchestrator/session/manager.mjs +18 -4
- package/src/agent/orchestrator/stall-policy.mjs +6 -0
- package/src/agent/orchestrator/tools/builtin/native-edit-runner.mjs +29 -8
- package/src/agent/orchestrator/tools/graph-manifest.json +11 -11
- package/src/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/channels/index.mjs +27 -8
- package/src/channels/lib/event-queue.mjs +24 -1
- package/src/channels/lib/hook-pipe-server.mjs +21 -8
- package/src/channels/lib/webhook.mjs +142 -20
- package/src/memory/lib/memory-cycle1.mjs +7 -3
- package/src/memory/lib/memory-recall-store.mjs +27 -10
- package/src/search/lib/backends/openai-oauth.mjs +6 -2
- package/src/search/lib/cache.mjs +55 -7
- package/src/shared/config.mjs +1 -1
- package/src/shared/llm/cost.mjs +2 -2
- package/src/shared/open-url.mjs +37 -0
- package/src/shared/seed.mjs +20 -3
- package/src/shared/user-data-guard.mjs +3 -1
- package/scripts/test-config-rmw-restore.mjs +0 -122
- package/setup/wizard.mjs +0 -696
|
@@ -8,10 +8,14 @@ import { withRetry } from './retry-classifier.mjs';
|
|
|
8
8
|
import { traceBridgeUsage, appendBridgeTrace } from '../bridge-trace.mjs';
|
|
9
9
|
import {
|
|
10
10
|
PROVIDER_FIRST_BYTE_TIMEOUT_MS,
|
|
11
|
+
PROVIDER_NONSTREAM_TOTAL_TIMEOUT_MS,
|
|
11
12
|
PROVIDER_GENERATE_TOTAL_TIMEOUT_MS,
|
|
12
13
|
PROVIDER_MAX_BEFORE_WARN_MS,
|
|
14
|
+
PROVIDER_SSE_IDLE_TIMEOUT_MS,
|
|
15
|
+
PROVIDER_SSE_IDLE_WATCHDOG_ENABLED,
|
|
13
16
|
providerTimeoutError,
|
|
14
17
|
resolveTimeoutMs,
|
|
18
|
+
createTimeoutSignal,
|
|
15
19
|
} from '../stall-policy.mjs';
|
|
16
20
|
import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
|
|
17
21
|
|
|
@@ -357,6 +361,323 @@ function runGeminiOperationWithTimeout({ label, timeoutMs, signal, run }) {
|
|
|
357
361
|
});
|
|
358
362
|
}
|
|
359
363
|
|
|
364
|
+
function geminiTruncatedStreamError(message) {
|
|
365
|
+
return Object.assign(
|
|
366
|
+
new Error(message),
|
|
367
|
+
{ name: 'TruncatedStreamError', code: 'TRUNCATED_STREAM', truncatedStream: true },
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Aggregate streamed GenerateContentResponse chunks into one response object
|
|
373
|
+
* (same shape as a non-streaming generateContent JSON body).
|
|
374
|
+
* Mirrors @google/generative-ai aggregateResponses().
|
|
375
|
+
*/
|
|
376
|
+
function aggregateGeminiStreamChunks(responses) {
|
|
377
|
+
const lastResponse = responses[responses.length - 1];
|
|
378
|
+
const aggregatedResponse = {
|
|
379
|
+
promptFeedback: lastResponse?.promptFeedback,
|
|
380
|
+
};
|
|
381
|
+
for (const response of responses) {
|
|
382
|
+
if (response?.candidates) {
|
|
383
|
+
let candidateIndex = 0;
|
|
384
|
+
for (const candidate of response.candidates) {
|
|
385
|
+
if (!aggregatedResponse.candidates) aggregatedResponse.candidates = [];
|
|
386
|
+
if (!aggregatedResponse.candidates[candidateIndex]) {
|
|
387
|
+
aggregatedResponse.candidates[candidateIndex] = { index: candidateIndex };
|
|
388
|
+
}
|
|
389
|
+
const aggCand = aggregatedResponse.candidates[candidateIndex];
|
|
390
|
+
aggCand.citationMetadata = candidate.citationMetadata;
|
|
391
|
+
aggCand.groundingMetadata = candidate.groundingMetadata;
|
|
392
|
+
aggCand.finishReason = candidate.finishReason;
|
|
393
|
+
aggCand.finishMessage = candidate.finishMessage;
|
|
394
|
+
aggCand.safetyRatings = candidate.safetyRatings;
|
|
395
|
+
if (candidate.content?.parts) {
|
|
396
|
+
if (!aggCand.content) {
|
|
397
|
+
aggCand.content = {
|
|
398
|
+
role: candidate.content.role || 'user',
|
|
399
|
+
parts: [],
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
for (const part of candidate.content.parts) {
|
|
403
|
+
const newPart = {};
|
|
404
|
+
if (part.text) newPart.text = part.text;
|
|
405
|
+
if (part.functionCall) newPart.functionCall = part.functionCall;
|
|
406
|
+
if (part.thoughtSignature) newPart.thoughtSignature = part.thoughtSignature;
|
|
407
|
+
if (part.thought_signature) newPart.thought_signature = part.thought_signature;
|
|
408
|
+
if (part.executableCode) newPart.executableCode = part.executableCode;
|
|
409
|
+
if (part.codeExecutionResult) newPart.codeExecutionResult = part.codeExecutionResult;
|
|
410
|
+
if (Object.keys(newPart).length === 0) newPart.text = '';
|
|
411
|
+
aggCand.content.parts.push(newPart);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
candidateIndex++;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if (response?.usageMetadata) aggregatedResponse.usageMetadata = response.usageMetadata;
|
|
418
|
+
}
|
|
419
|
+
return aggregatedResponse;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function assertGeminiStreamCompleted({ sawStreamChunk, finishReason, label }) {
|
|
423
|
+
if (!sawStreamChunk) {
|
|
424
|
+
throw geminiTruncatedStreamError(`${label} truncated: empty stream`);
|
|
425
|
+
}
|
|
426
|
+
if (!finishReason) {
|
|
427
|
+
throw geminiTruncatedStreamError(`${label} truncated: no finishReason`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async function consumeGeminiRestStreamResponse(response, { signal, onStreamDelta, label }) {
|
|
432
|
+
if (!response?.body) throw new Error(`${label}: missing response body`);
|
|
433
|
+
const reader = response.body.getReader();
|
|
434
|
+
const decoder = new TextDecoder();
|
|
435
|
+
let buffer = '';
|
|
436
|
+
const allChunks = [];
|
|
437
|
+
let sawStreamChunk = false;
|
|
438
|
+
let idleTimedOut = false;
|
|
439
|
+
let idleTimer = null;
|
|
440
|
+
let idleReject = null;
|
|
441
|
+
|
|
442
|
+
let firstByteTimer = setTimeout(() => {
|
|
443
|
+
try { reader.cancel('first byte timeout'); } catch {}
|
|
444
|
+
if (idleReject) {
|
|
445
|
+
const e = geminiTimeoutError(`${label} first byte`, GEMINI_FIRST_BYTE_TIMEOUT_MS);
|
|
446
|
+
const r = idleReject; idleReject = null; r(e);
|
|
447
|
+
}
|
|
448
|
+
}, GEMINI_FIRST_BYTE_TIMEOUT_MS);
|
|
449
|
+
if (firstByteTimer.unref) firstByteTimer.unref();
|
|
450
|
+
|
|
451
|
+
const clearFirstByteTimer = () => {
|
|
452
|
+
if (firstByteTimer) {
|
|
453
|
+
clearTimeout(firstByteTimer);
|
|
454
|
+
firstByteTimer = null;
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
const resetIdleTimer = () => {
|
|
459
|
+
if (!PROVIDER_SSE_IDLE_WATCHDOG_ENABLED) return;
|
|
460
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
461
|
+
idleTimer = setTimeout(() => {
|
|
462
|
+
idleTimedOut = true;
|
|
463
|
+
try { reader.cancel('SSE idle timeout'); } catch {}
|
|
464
|
+
if (idleReject) {
|
|
465
|
+
const e = geminiTimeoutError(`${label} SSE idle`, PROVIDER_SSE_IDLE_TIMEOUT_MS);
|
|
466
|
+
const r = idleReject; idleReject = null; r(e);
|
|
467
|
+
}
|
|
468
|
+
}, PROVIDER_SSE_IDLE_TIMEOUT_MS);
|
|
469
|
+
if (idleTimer.unref) idleTimer.unref();
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
const onAbort = () => {
|
|
473
|
+
try {
|
|
474
|
+
const c = reader.cancel('aborted');
|
|
475
|
+
if (c && typeof c.catch === 'function') c.catch(() => {});
|
|
476
|
+
} catch {}
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
if (signal) {
|
|
480
|
+
if (signal.aborted) {
|
|
481
|
+
const reason = signal.reason;
|
|
482
|
+
throw reason instanceof Error ? reason : new Error(`${label} aborted`);
|
|
483
|
+
}
|
|
484
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
try {
|
|
488
|
+
resetIdleTimer();
|
|
489
|
+
while (true) {
|
|
490
|
+
let chunk;
|
|
491
|
+
try {
|
|
492
|
+
chunk = await new Promise((resolve, reject) => {
|
|
493
|
+
idleReject = reject;
|
|
494
|
+
reader.read().then(resolve, reject);
|
|
495
|
+
});
|
|
496
|
+
} catch (err) {
|
|
497
|
+
if (idleTimedOut) {
|
|
498
|
+
throw geminiTimeoutError(`${label} SSE idle`, PROVIDER_SSE_IDLE_TIMEOUT_MS);
|
|
499
|
+
}
|
|
500
|
+
if (signal?.aborted) {
|
|
501
|
+
const reason = signal.reason;
|
|
502
|
+
throw reason instanceof Error ? reason : new Error(`${label} aborted`);
|
|
503
|
+
}
|
|
504
|
+
throw err;
|
|
505
|
+
} finally {
|
|
506
|
+
idleReject = null;
|
|
507
|
+
}
|
|
508
|
+
const { done, value } = chunk;
|
|
509
|
+
if (done) break;
|
|
510
|
+
resetIdleTimer();
|
|
511
|
+
buffer += decoder.decode(value, { stream: true });
|
|
512
|
+
let lineEnd;
|
|
513
|
+
while ((lineEnd = buffer.indexOf('\n')) >= 0) {
|
|
514
|
+
let line = buffer.slice(0, lineEnd);
|
|
515
|
+
buffer = buffer.slice(lineEnd + 1);
|
|
516
|
+
line = line.replace(/\r$/, '');
|
|
517
|
+
if (!line.startsWith('data: ')) continue;
|
|
518
|
+
const data = line.slice(6).trim();
|
|
519
|
+
if (!data || data === '[DONE]') continue;
|
|
520
|
+
let parsed;
|
|
521
|
+
try { parsed = JSON.parse(data); } catch { continue; }
|
|
522
|
+
if (!sawStreamChunk) {
|
|
523
|
+
sawStreamChunk = true;
|
|
524
|
+
clearFirstByteTimer();
|
|
525
|
+
}
|
|
526
|
+
allChunks.push(parsed);
|
|
527
|
+
try { onStreamDelta?.(); } catch {}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (buffer.trim()) {
|
|
531
|
+
const line = buffer.trim().replace(/\r$/, '');
|
|
532
|
+
if (line.startsWith('data: ')) {
|
|
533
|
+
const data = line.slice(6).trim();
|
|
534
|
+
if (data && data !== '[DONE]') {
|
|
535
|
+
try {
|
|
536
|
+
const parsed = JSON.parse(data);
|
|
537
|
+
if (!sawStreamChunk) {
|
|
538
|
+
sawStreamChunk = true;
|
|
539
|
+
clearFirstByteTimer();
|
|
540
|
+
}
|
|
541
|
+
allChunks.push(parsed);
|
|
542
|
+
try { onStreamDelta?.(); } catch {}
|
|
543
|
+
} catch { /* skip malformed tail */ }
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
} finally {
|
|
548
|
+
clearFirstByteTimer();
|
|
549
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
550
|
+
if (signal) signal.removeEventListener('abort', onAbort);
|
|
551
|
+
try { reader.releaseLock(); } catch {}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const aggregated = aggregateGeminiStreamChunks(allChunks);
|
|
555
|
+
const finishReason = aggregated.candidates?.[0]?.finishReason || null;
|
|
556
|
+
assertGeminiStreamCompleted({ sawStreamChunk, finishReason, label });
|
|
557
|
+
return aggregated;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
async function consumeGeminiSdkStream(streamResult, { signal, onStreamDelta, label }) {
|
|
561
|
+
let sawStreamChunk = false;
|
|
562
|
+
let idleTimedOut = false;
|
|
563
|
+
let idleTimer = null;
|
|
564
|
+
let firstByteReject = null;
|
|
565
|
+
let firstByteTimer = null;
|
|
566
|
+
let inFlightReject = null;
|
|
567
|
+
|
|
568
|
+
const armFirstByteTimer = () => {
|
|
569
|
+
if (firstByteTimer) clearTimeout(firstByteTimer);
|
|
570
|
+
firstByteTimer = setTimeout(() => {
|
|
571
|
+
if (firstByteReject) {
|
|
572
|
+
const e = geminiTimeoutError(`${label} first byte`, GEMINI_FIRST_BYTE_TIMEOUT_MS);
|
|
573
|
+
const r = firstByteReject; firstByteReject = null; r(e);
|
|
574
|
+
}
|
|
575
|
+
}, GEMINI_FIRST_BYTE_TIMEOUT_MS);
|
|
576
|
+
if (firstByteTimer.unref) firstByteTimer.unref();
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
const clearFirstByteTimer = () => {
|
|
580
|
+
if (firstByteTimer) {
|
|
581
|
+
clearTimeout(firstByteTimer);
|
|
582
|
+
firstByteTimer = null;
|
|
583
|
+
}
|
|
584
|
+
firstByteReject = null;
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
const resetIdleTimer = () => {
|
|
588
|
+
if (!PROVIDER_SSE_IDLE_WATCHDOG_ENABLED) return;
|
|
589
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
590
|
+
idleTimer = setTimeout(() => {
|
|
591
|
+
idleTimedOut = true;
|
|
592
|
+
if (inFlightReject) {
|
|
593
|
+
const e = geminiTimeoutError(`${label} SSE idle`, PROVIDER_SSE_IDLE_TIMEOUT_MS);
|
|
594
|
+
const r = inFlightReject; inFlightReject = null; r(e);
|
|
595
|
+
}
|
|
596
|
+
}, PROVIDER_SSE_IDLE_TIMEOUT_MS);
|
|
597
|
+
if (idleTimer.unref) idleTimer.unref();
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
if (signal?.aborted) {
|
|
601
|
+
const reason = signal.reason;
|
|
602
|
+
throw reason instanceof Error ? reason : new Error(`${label} aborted`);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const iterator = streamResult.stream[Symbol.asyncIterator]();
|
|
606
|
+
|
|
607
|
+
try {
|
|
608
|
+
armFirstByteTimer();
|
|
609
|
+
resetIdleTimer();
|
|
610
|
+
while (true) {
|
|
611
|
+
if (idleTimedOut) {
|
|
612
|
+
throw geminiTimeoutError(`${label} SSE idle`, PROVIDER_SSE_IDLE_TIMEOUT_MS);
|
|
613
|
+
}
|
|
614
|
+
let step;
|
|
615
|
+
try {
|
|
616
|
+
step = await new Promise((resolve, reject) => {
|
|
617
|
+
inFlightReject = reject;
|
|
618
|
+
if (!sawStreamChunk) firstByteReject = reject;
|
|
619
|
+
iterator.next().then(
|
|
620
|
+
(value) => {
|
|
621
|
+
inFlightReject = null;
|
|
622
|
+
firstByteReject = null;
|
|
623
|
+
resolve(value);
|
|
624
|
+
},
|
|
625
|
+
(err) => {
|
|
626
|
+
inFlightReject = null;
|
|
627
|
+
firstByteReject = null;
|
|
628
|
+
reject(err);
|
|
629
|
+
},
|
|
630
|
+
);
|
|
631
|
+
});
|
|
632
|
+
} catch (err) {
|
|
633
|
+
if (idleTimedOut) {
|
|
634
|
+
throw geminiTimeoutError(`${label} SSE idle`, PROVIDER_SSE_IDLE_TIMEOUT_MS);
|
|
635
|
+
}
|
|
636
|
+
if (signal?.aborted) {
|
|
637
|
+
const reason = signal.reason;
|
|
638
|
+
throw reason instanceof Error ? reason : new Error(`${label} aborted`);
|
|
639
|
+
}
|
|
640
|
+
throw err;
|
|
641
|
+
}
|
|
642
|
+
if (step.done) break;
|
|
643
|
+
if (!sawStreamChunk) {
|
|
644
|
+
sawStreamChunk = true;
|
|
645
|
+
clearFirstByteTimer();
|
|
646
|
+
}
|
|
647
|
+
resetIdleTimer();
|
|
648
|
+
try { onStreamDelta?.(); } catch {}
|
|
649
|
+
}
|
|
650
|
+
if (idleTimedOut) {
|
|
651
|
+
throw geminiTimeoutError(`${label} SSE idle`, PROVIDER_SSE_IDLE_TIMEOUT_MS);
|
|
652
|
+
}
|
|
653
|
+
} catch (err) {
|
|
654
|
+
clearFirstByteTimer();
|
|
655
|
+
if (signal?.aborted) {
|
|
656
|
+
const reason = signal.reason;
|
|
657
|
+
throw reason instanceof Error ? reason : new Error(`${label} aborted`);
|
|
658
|
+
}
|
|
659
|
+
throw err;
|
|
660
|
+
} finally {
|
|
661
|
+
clearFirstByteTimer();
|
|
662
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
let response;
|
|
666
|
+
try {
|
|
667
|
+
response = await streamResult.response;
|
|
668
|
+
} catch (err) {
|
|
669
|
+
if (signal?.aborted) {
|
|
670
|
+
const reason = signal.reason;
|
|
671
|
+
throw reason instanceof Error ? reason : new Error(`${label} aborted`);
|
|
672
|
+
}
|
|
673
|
+
throw err;
|
|
674
|
+
}
|
|
675
|
+
const raw = response?.candidates ? response : (response?.response || response);
|
|
676
|
+
const finishReason = raw?.candidates?.[0]?.finishReason || null;
|
|
677
|
+
assertGeminiStreamCompleted({ sawStreamChunk, finishReason, label });
|
|
678
|
+
return raw;
|
|
679
|
+
}
|
|
680
|
+
|
|
360
681
|
/**
|
|
361
682
|
* Convert JSON Schema type string to Gemini SchemaType.
|
|
362
683
|
* Gemini SDK uses its own enum instead of plain strings.
|
|
@@ -836,6 +1157,7 @@ export class GeminiProvider {
|
|
|
836
1157
|
async _doSend(messages, model, tools, sendOpts) {
|
|
837
1158
|
const opts = sendOpts || {};
|
|
838
1159
|
const signal = opts.signal || null;
|
|
1160
|
+
const onStreamDelta = typeof opts.onStreamDelta === 'function' ? opts.onStreamDelta : null;
|
|
839
1161
|
if (signal?.aborted) {
|
|
840
1162
|
const reason = signal.reason;
|
|
841
1163
|
throw reason instanceof Error ? reason : new Error('Gemini request aborted by session close');
|
|
@@ -884,7 +1206,7 @@ export class GeminiProvider {
|
|
|
884
1206
|
let response;
|
|
885
1207
|
if (cachedContent) {
|
|
886
1208
|
const apiKey = this._getApiKey();
|
|
887
|
-
const genUrl = `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(useModel)}:
|
|
1209
|
+
const genUrl = `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(useModel)}:streamGenerateContent?alt=sse&key=${encodeURIComponent(apiKey)}`;
|
|
888
1210
|
const cachedPrefixContentCount = Number.isFinite(Number(opts.providerState?.gemini?.cachePrefixContentCount))
|
|
889
1211
|
? Math.max(0, Math.min(contents.length, Math.trunc(Number(opts.providerState.gemini.cachePrefixContentCount))))
|
|
890
1212
|
: 0;
|
|
@@ -897,32 +1219,42 @@ export class GeminiProvider {
|
|
|
897
1219
|
cachedContent,
|
|
898
1220
|
};
|
|
899
1221
|
if (toolConfig) body.toolConfig = toolConfig;
|
|
900
|
-
|
|
901
|
-
label: 'Gemini REST
|
|
902
|
-
timeoutMs:
|
|
1222
|
+
response = await runGeminiOperationWithTimeout({
|
|
1223
|
+
label: 'Gemini REST streamGenerateContent total',
|
|
1224
|
+
timeoutMs: PROVIDER_NONSTREAM_TOTAL_TIMEOUT_MS,
|
|
903
1225
|
signal,
|
|
904
1226
|
run: (totalSignal) => withRetry(
|
|
905
|
-
() =>
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1227
|
+
async ({ signal: attemptSignal }) => {
|
|
1228
|
+
try { opts.onStageChange?.('requesting'); } catch {}
|
|
1229
|
+
const openFirstByte = createTimeoutSignal(
|
|
1230
|
+
attemptSignal,
|
|
1231
|
+
GEMINI_FIRST_BYTE_TIMEOUT_MS,
|
|
1232
|
+
'Gemini REST first byte',
|
|
1233
|
+
);
|
|
1234
|
+
let res;
|
|
1235
|
+
try {
|
|
1236
|
+
res = await fetch(genUrl, {
|
|
911
1237
|
method: 'POST',
|
|
912
1238
|
headers: { 'Content-Type': 'application/json' },
|
|
913
1239
|
body: JSON.stringify(body),
|
|
914
|
-
signal:
|
|
1240
|
+
signal: openFirstByte.signal,
|
|
915
1241
|
dispatcher: getLlmDispatcher(),
|
|
916
1242
|
});
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1243
|
+
} finally {
|
|
1244
|
+
openFirstByte.cleanup();
|
|
1245
|
+
}
|
|
1246
|
+
if (!res.ok) {
|
|
1247
|
+
const text = await res.text().catch(() => '');
|
|
1248
|
+
const err = new Error(`Gemini REST streamGenerateContent ${res.status}: ${text.slice(0, 300)}`);
|
|
1249
|
+
err.status = res.status;
|
|
1250
|
+
throw err;
|
|
1251
|
+
}
|
|
1252
|
+
return await consumeGeminiRestStreamResponse(res, {
|
|
1253
|
+
signal: attemptSignal,
|
|
1254
|
+
onStreamDelta,
|
|
1255
|
+
label: 'Gemini REST streamGenerateContent',
|
|
1256
|
+
});
|
|
1257
|
+
},
|
|
926
1258
|
{
|
|
927
1259
|
signal: totalSignal,
|
|
928
1260
|
onRetry: ({ attempt, lastErr }) => {
|
|
@@ -932,7 +1264,6 @@ export class GeminiProvider {
|
|
|
932
1264
|
},
|
|
933
1265
|
),
|
|
934
1266
|
});
|
|
935
|
-
response = fetchResult;
|
|
936
1267
|
} else {
|
|
937
1268
|
const genModel = this.genAI.getGenerativeModel({
|
|
938
1269
|
model: useModel,
|
|
@@ -940,17 +1271,42 @@ export class GeminiProvider {
|
|
|
940
1271
|
tools: geminiTools,
|
|
941
1272
|
...(toolConfig ? { toolConfig } : {}),
|
|
942
1273
|
});
|
|
943
|
-
|
|
944
|
-
label: 'Gemini
|
|
945
|
-
timeoutMs:
|
|
1274
|
+
response = await runGeminiOperationWithTimeout({
|
|
1275
|
+
label: 'Gemini streamGenerateContent total',
|
|
1276
|
+
timeoutMs: PROVIDER_NONSTREAM_TOTAL_TIMEOUT_MS,
|
|
946
1277
|
signal,
|
|
947
1278
|
run: (totalSignal) => withRetry(
|
|
948
|
-
() =>
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1279
|
+
async ({ signal: attemptSignal }) => {
|
|
1280
|
+
try { opts.onStageChange?.('requesting'); } catch {}
|
|
1281
|
+
const openFirstByte = createTimeoutSignal(
|
|
1282
|
+
attemptSignal,
|
|
1283
|
+
GEMINI_FIRST_BYTE_TIMEOUT_MS,
|
|
1284
|
+
'Gemini SDK first byte',
|
|
1285
|
+
);
|
|
1286
|
+
let streamResult;
|
|
1287
|
+
try {
|
|
1288
|
+
try {
|
|
1289
|
+
streamResult = await genModel.generateContentStream(
|
|
1290
|
+
{ contents },
|
|
1291
|
+
{ signal: openFirstByte.signal },
|
|
1292
|
+
);
|
|
1293
|
+
} catch (err) {
|
|
1294
|
+
if (openFirstByte.signal.aborted) {
|
|
1295
|
+
throw openFirstByte.signal.reason instanceof Error
|
|
1296
|
+
? openFirstByte.signal.reason
|
|
1297
|
+
: err;
|
|
1298
|
+
}
|
|
1299
|
+
throw err;
|
|
1300
|
+
}
|
|
1301
|
+
} finally {
|
|
1302
|
+
openFirstByte.cleanup();
|
|
1303
|
+
}
|
|
1304
|
+
return await consumeGeminiSdkStream(streamResult, {
|
|
1305
|
+
signal: attemptSignal,
|
|
1306
|
+
onStreamDelta,
|
|
1307
|
+
label: 'Gemini SDK streamGenerateContent',
|
|
1308
|
+
});
|
|
1309
|
+
},
|
|
954
1310
|
{
|
|
955
1311
|
signal: totalSignal,
|
|
956
1312
|
onRetry: ({ attempt, lastErr }) => {
|
|
@@ -960,7 +1316,6 @@ export class GeminiProvider {
|
|
|
960
1316
|
},
|
|
961
1317
|
),
|
|
962
1318
|
});
|
|
963
|
-
response = result.response;
|
|
964
1319
|
}
|
|
965
1320
|
writeGeminiCacheTrace({
|
|
966
1321
|
opts,
|
|
@@ -710,11 +710,8 @@ export async function loginOAuth() {
|
|
|
710
710
|
url.searchParams.set('plan', 'generic');
|
|
711
711
|
url.searchParams.set('referrer', 'mixdog');
|
|
712
712
|
process.stderr.write(`\n[grok-oauth] Open this URL to log in (consent shows as "Grok Build"):\n${url.toString()}\n\n`);
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
const opener = process.platform === 'win32' ? 'start' : process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
716
|
-
exec(`${opener} "${url.toString()}"`, { windowsHide: true });
|
|
717
|
-
} catch { /* user opens manually */ }
|
|
713
|
+
const { openInBrowser } = await import('../../../shared/open-url.mjs');
|
|
714
|
+
openInBrowser(url.toString());
|
|
718
715
|
|
|
719
716
|
return new Promise((resolve) => {
|
|
720
717
|
const timeout = setTimeout(() => { server.close(); resolve(null); }, LOGIN_TIMEOUT_MS);
|