mixdog 0.9.50 → 0.9.52
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/package.json +5 -3
- package/scripts/abort-recovery-test.mjs +17 -1
- package/scripts/agent-model-liveness-test.mjs +79 -2
- package/scripts/anthropic-admission-retry-integration-test.mjs +119 -0
- package/scripts/anthropic-transport-policy-test.mjs +466 -0
- package/scripts/atomic-lock-tryonce-test.mjs +60 -1
- package/scripts/build-tui.mjs +13 -1
- package/scripts/channel-daemon-smoke.mjs +630 -10
- package/scripts/code-graph-aggregate-cwd-test.mjs +41 -37
- package/scripts/code-graph-disk-hit-test.mjs +11 -0
- package/scripts/code-graph-root-federation-test.mjs +273 -0
- package/scripts/compact-pressure-test.mjs +55 -1
- package/scripts/compact-smoke.mjs +80 -0
- package/scripts/context-mcp-metering-test.mjs +1350 -19
- package/scripts/deferred-tool-loading-test.mjs +17 -0
- package/scripts/gemini-provider-test.mjs +1053 -0
- package/scripts/hook-bus-test.mjs +23 -0
- package/scripts/internal-tools-normalization-test.mjs +10 -0
- package/scripts/interrupted-turn-history-test.mjs +371 -0
- package/scripts/lifecycle-api-test.mjs +76 -0
- package/scripts/max-output-recovery-test.mjs +55 -0
- package/scripts/mcp-grace-deferred-test.mjs +89 -13
- package/scripts/memory-pg-recovery-test.mjs +59 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +391 -4
- package/scripts/process-lifecycle-test.mjs +389 -0
- package/scripts/provider-admission-scheduler-test.mjs +582 -0
- package/scripts/provider-contract-test.mjs +268 -0
- package/scripts/provider-toolcall-test.mjs +520 -3
- package/scripts/reactive-compact-persist-smoke.mjs +59 -0
- package/scripts/resource-admission-test.mjs +789 -0
- package/scripts/session-bench-cache-break-test.mjs +102 -0
- package/scripts/session-bench.mjs +101 -42
- package/scripts/shell-failure-diagnostics-test.mjs +73 -4
- package/scripts/shell-jobs-windows-hide-test.mjs +1 -1
- package/scripts/smoke-loop-failure-summary-test.mjs +38 -0
- package/scripts/smoke-loop-failure-summary.mjs +16 -0
- package/scripts/smoke-loop.mjs +2 -7
- package/scripts/steering-drain-buckets-test.mjs +18 -0
- package/scripts/tool-failures.mjs +15 -1
- package/scripts/toolcall-args-test.mjs +14 -6
- package/scripts/tui-transcript-perf-test.mjs +43 -7
- package/scripts/web-fetch-routing-test.mjs +158 -0
- package/src/cli.mjs +15 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +26 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +4 -1
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +12 -0
- package/src/runtime/agent/orchestrator/internal-tools.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +331 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +118 -26
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +29 -17
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +136 -38
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +24 -4
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +554 -42
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +67 -18
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +95 -46
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -4
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +54 -14
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +14 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +10 -80
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +89 -17
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +93 -26
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +144 -51
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +22 -8
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +74 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +111 -6
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +13 -17
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +78 -12
- package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +44 -5
- package/src/runtime/agent/orchestrator/providers/registry.mjs +49 -8
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +224 -104
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +127 -55
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +99 -32
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +17 -1
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +38 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +15 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +69 -37
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +8 -28
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +6 -7
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +42 -1
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +220 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +24 -4
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +17 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +89 -22
- package/src/runtime/agent/orchestrator/stall-policy.mjs +2 -12
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +42 -4
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +74 -37
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +223 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +380 -37
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +176 -21
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +108 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +93 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +12 -3
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +124 -14
- package/src/runtime/memory/index.mjs +22 -4
- package/src/runtime/memory/lib/pg/adapter.mjs +84 -10
- package/src/runtime/memory/lib/pg/process.mjs +91 -47
- package/src/runtime/memory/lib/pg/supervisor.mjs +50 -13
- package/src/runtime/search/index.mjs +41 -0
- package/src/runtime/search/lib/http-fetch.mjs +154 -0
- package/src/runtime/search/tool-defs.mjs +23 -0
- package/src/runtime/shared/atomic-file.mjs +28 -150
- package/src/runtime/shared/process-lifecycle.mjs +363 -0
- package/src/runtime/shared/process-shutdown.mjs +27 -4
- package/src/runtime/shared/resource-admission.mjs +359 -0
- package/src/runtime/shared/staged-child-result.mjs +19 -0
- package/src/session-runtime/context-status.mjs +38 -27
- package/src/session-runtime/lifecycle-api.mjs +26 -6
- package/src/session-runtime/provider-request-tools.mjs +72 -0
- package/src/session-runtime/runtime-core.mjs +43 -18
- package/src/session-runtime/session-turn-api.mjs +5 -4
- package/src/session-runtime/tool-catalog.mjs +375 -15
- package/src/standalone/agent-tool.mjs +17 -38
- package/src/standalone/channel-daemon-client.mjs +200 -38
- package/src/standalone/channel-daemon-transport.mjs +136 -9
- package/src/standalone/channel-worker.mjs +79 -12
- package/src/standalone/hook-bus/handlers.mjs +4 -0
- package/src/standalone/hook-bus/rules.mjs +7 -1
- package/src/standalone/hook-bus.mjs +10 -2
- package/src/tui/App.jsx +17 -11
- package/src/tui/app/live-spinner-visibility.mjs +20 -0
- package/src/tui/dist/index.mjs +101 -281
- package/src/tui/engine/session-api-ext.mjs +13 -2
- package/src/tui/engine/session-api.mjs +1 -1
- package/src/tui/engine/turn.mjs +10 -6
- package/src/tui/engine.mjs +13 -4
- package/src/tui/index.jsx +4 -1
- package/src/tui/lib/voice-setup.mjs +16 -0
|
@@ -201,9 +201,10 @@ function cachePath() {
|
|
|
201
201
|
return join(getPluginData(), CATALOG_CACHE_FILE);
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
async function _loadCatalogImpl() {
|
|
204
|
+
async function _loadCatalogImpl(fetchFn = fetch, force = false) {
|
|
205
205
|
// Disk cache first
|
|
206
206
|
try {
|
|
207
|
+
if (force) throw new Error('forced refresh');
|
|
207
208
|
if (existsSync(cachePath())) {
|
|
208
209
|
const raw = JSON.parse(readFileSync(cachePath(), 'utf-8'));
|
|
209
210
|
if (raw?.fetchedAt && (Date.now() - raw.fetchedAt) < CATALOG_TTL_MS && raw.data) {
|
|
@@ -215,7 +216,7 @@ async function _loadCatalogImpl() {
|
|
|
215
216
|
} catch { /* fall through */ }
|
|
216
217
|
// Remote fetch
|
|
217
218
|
try {
|
|
218
|
-
const res = await
|
|
219
|
+
const res = await fetchFn(CATALOG_URL, { signal: AbortSignal.timeout(10_000) });
|
|
219
220
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
220
221
|
const data = await res.json();
|
|
221
222
|
try {
|
|
@@ -226,14 +227,32 @@ async function _loadCatalogImpl() {
|
|
|
226
227
|
return data;
|
|
227
228
|
} catch (err) {
|
|
228
229
|
process.stderr.write(`[model-catalog] fetch failed: ${err.message}\n`);
|
|
230
|
+
if (fetchFn !== fetch) {
|
|
231
|
+
_memCache = _memCache || {};
|
|
232
|
+
_memCacheAt = Date.now();
|
|
233
|
+
}
|
|
229
234
|
return {};
|
|
230
235
|
}
|
|
231
236
|
}
|
|
232
237
|
|
|
233
|
-
async function
|
|
234
|
-
|
|
238
|
+
async function _loadCatalogInjected(fetchFn) {
|
|
239
|
+
try {
|
|
240
|
+
const res = await fetchFn(CATALOG_URL, { signal: AbortSignal.timeout(10_000) });
|
|
241
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
242
|
+
return await res.json();
|
|
243
|
+
} catch (err) {
|
|
244
|
+
process.stderr.write(`[model-catalog] injected fetch failed: ${err.message}\n`);
|
|
245
|
+
return {};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export async function loadCatalog({ fetchFn, force = false } = {}) {
|
|
250
|
+
if (typeof fetchFn === 'function' && fetchFn !== fetch) {
|
|
251
|
+
return _loadCatalogInjected(fetchFn);
|
|
252
|
+
}
|
|
253
|
+
if (!force && _memCache && (Date.now() - _memCacheAt) < CATALOG_TTL_MS) return _memCache;
|
|
235
254
|
if (_loadPromise) return _loadPromise;
|
|
236
|
-
_loadPromise = _loadCatalogImpl().finally(() => { _loadPromise = null; });
|
|
255
|
+
_loadPromise = _loadCatalogImpl(fetchFn, force).finally(() => { _loadPromise = null; });
|
|
237
256
|
return _loadPromise;
|
|
238
257
|
}
|
|
239
258
|
|
|
@@ -255,8 +274,9 @@ let _mdLoadPromise = null;
|
|
|
255
274
|
function mdCachePath() {
|
|
256
275
|
return join(getPluginData(), MODELSDEV_CACHE_FILE);
|
|
257
276
|
}
|
|
258
|
-
async function _loadModelsDevImpl() {
|
|
277
|
+
async function _loadModelsDevImpl(fetchFn = fetch, force = false) {
|
|
259
278
|
try {
|
|
279
|
+
if (force) throw new Error('forced refresh');
|
|
260
280
|
if (existsSync(mdCachePath())) {
|
|
261
281
|
const raw = JSON.parse(readFileSync(mdCachePath(), 'utf-8'));
|
|
262
282
|
if (raw?.fetchedAt && (Date.now() - raw.fetchedAt) < CATALOG_TTL_MS && raw.data) {
|
|
@@ -267,7 +287,7 @@ async function _loadModelsDevImpl() {
|
|
|
267
287
|
}
|
|
268
288
|
} catch { /* fall through to remote */ }
|
|
269
289
|
try {
|
|
270
|
-
const res = await
|
|
290
|
+
const res = await fetchFn(MODELSDEV_URL, { signal: AbortSignal.timeout(10_000) });
|
|
271
291
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
272
292
|
const data = await res.json();
|
|
273
293
|
try {
|
|
@@ -278,13 +298,30 @@ async function _loadModelsDevImpl() {
|
|
|
278
298
|
return data;
|
|
279
299
|
} catch (err) {
|
|
280
300
|
process.stderr.write(`[model-catalog] models.dev fetch failed: ${err.message}\n`);
|
|
301
|
+
if (fetchFn !== fetch) {
|
|
302
|
+
_mdCache = _mdCache || {};
|
|
303
|
+
_mdCacheAt = Date.now();
|
|
304
|
+
}
|
|
281
305
|
return _mdCache || {};
|
|
282
306
|
}
|
|
283
307
|
}
|
|
284
|
-
|
|
285
|
-
|
|
308
|
+
async function _loadModelsDevInjected(fetchFn) {
|
|
309
|
+
try {
|
|
310
|
+
const res = await fetchFn(MODELSDEV_URL, { signal: AbortSignal.timeout(10_000) });
|
|
311
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
312
|
+
return await res.json();
|
|
313
|
+
} catch (err) {
|
|
314
|
+
process.stderr.write(`[model-catalog] models.dev injected fetch failed: ${err.message}\n`);
|
|
315
|
+
return {};
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
export async function loadModelsDevCatalog({ fetchFn, force = false } = {}) {
|
|
319
|
+
if (typeof fetchFn === 'function' && fetchFn !== fetch) {
|
|
320
|
+
return _loadModelsDevInjected(fetchFn);
|
|
321
|
+
}
|
|
322
|
+
if (!force && _mdCache && (Date.now() - _mdCacheAt) < CATALOG_TTL_MS) return _mdCache;
|
|
286
323
|
if (_mdLoadPromise) return _mdLoadPromise;
|
|
287
|
-
_mdLoadPromise = _loadModelsDevImpl().finally(() => { _mdLoadPromise = null; });
|
|
324
|
+
_mdLoadPromise = _loadModelsDevImpl(fetchFn, force).finally(() => { _mdLoadPromise = null; });
|
|
288
325
|
return _mdLoadPromise;
|
|
289
326
|
}
|
|
290
327
|
function warmModelsDevFromDiskSync() {
|
|
@@ -488,11 +525,14 @@ function mergeModelMetadata(base, overlay, opts = {}) {
|
|
|
488
525
|
* entries keep their original shape (no metadata) so callers can distinguish
|
|
489
526
|
* "known in catalog" from "no metadata available".
|
|
490
527
|
*/
|
|
491
|
-
export async function enrichModels(models) {
|
|
528
|
+
export async function enrichModels(models, { fetchFn, force = false } = {}) {
|
|
492
529
|
if (!Array.isArray(models)) return models;
|
|
493
|
-
const catalog = await loadCatalog();
|
|
530
|
+
const catalog = await loadCatalog({ fetchFn, force });
|
|
531
|
+
let modelsDevCatalog = _mdCache;
|
|
494
532
|
if (models.some((m) => _modelsDevProviderId(m?.provider))) {
|
|
495
|
-
try {
|
|
533
|
+
try {
|
|
534
|
+
modelsDevCatalog = await loadModelsDevCatalog({ fetchFn, force });
|
|
535
|
+
} catch { /* optional gap filler */ }
|
|
496
536
|
}
|
|
497
537
|
return models.map(m => {
|
|
498
538
|
const id = m.id || m.name;
|
|
@@ -520,7 +560,7 @@ export async function enrichModels(models) {
|
|
|
520
560
|
let meta = entry ? _normalize(entry) : null;
|
|
521
561
|
const metaFromPricingOverride = entry != null && entry === ov;
|
|
522
562
|
if (m.provider) {
|
|
523
|
-
const row = mappedProvider ?
|
|
563
|
+
const row = mappedProvider ? modelsDevCatalog?.[mappedProvider]?.models?.[id] : null;
|
|
524
564
|
const providerMeta = row ? _normalize(_modelsDevRowToOverride(row)) : null;
|
|
525
565
|
if (providerMeta) meta = mergeModelMetadata(meta, providerMeta, { preserveBaseCosts: metaFromPricingOverride });
|
|
526
566
|
const providerNative = providerCachedModelMetadataSync(m.provider, id);
|
|
@@ -450,9 +450,20 @@ export async function consumeCompatChatCompletionStream(stream, {
|
|
|
450
450
|
}
|
|
451
451
|
}
|
|
452
452
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
453
|
+
// DeepSeek/OpenCode use reasoning_content; newer LM Studio builds
|
|
454
|
+
// use reasoning, and some local compatibility shims expose
|
|
455
|
+
// thinking. They are aliases, never concatenate multiple aliases
|
|
456
|
+
// from the same chunk.
|
|
457
|
+
const reasoningDelta = typeof choice?.delta?.reasoning_content === 'string'
|
|
458
|
+
? choice.delta.reasoning_content
|
|
459
|
+
: typeof choice?.delta?.reasoning === 'string'
|
|
460
|
+
? choice.delta.reasoning
|
|
461
|
+
: typeof choice?.delta?.thinking === 'string'
|
|
462
|
+
? choice.delta.thinking
|
|
463
|
+
: null;
|
|
464
|
+
if (reasoningDelta !== null) {
|
|
465
|
+
reasoningContent += reasoningDelta;
|
|
466
|
+
if (reasoningDelta) {
|
|
456
467
|
reportProgress('reasoning');
|
|
457
468
|
}
|
|
458
469
|
}
|
|
@@ -106,7 +106,10 @@ export function xaiResponsesCacheRouting(opts, params, rawTools, model) {
|
|
|
106
106
|
|
|
107
107
|
export function normalizeXaiReasoningEffort(value) {
|
|
108
108
|
const effort = String(value || '').trim().toLowerCase();
|
|
109
|
-
|
|
109
|
+
// Grok 4.5 accepts low/medium/high. Omit unsupported values (notably
|
|
110
|
+
// `none`) to retain xAI's authoritative model default rather than sending
|
|
111
|
+
// a value the API rejects.
|
|
112
|
+
return ['low', 'medium', 'high'].includes(effort) ? effort : null;
|
|
110
113
|
}
|
|
111
114
|
|
|
112
115
|
export function opencodeGoReasoningEffortValues(modelInfo) {
|
|
@@ -420,85 +423,12 @@ function traceXaiCacheLane(opts, payload) {
|
|
|
420
423
|
}
|
|
421
424
|
|
|
422
425
|
export async function withXaiResponsesCacheLane({ opts, config, cacheRouting, model, transport, previousResponseId, inputCount, signal }, fn) {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
const timeoutMs = xaiResponsesCacheLaneQueueTimeoutMs(opts, config);
|
|
430
|
-
const state = getXaiResponsesCacheLaneState(laneKey, maxInFlight);
|
|
431
|
-
const queued = state.active >= state.maxInFlight;
|
|
432
|
-
if (queued) {
|
|
433
|
-
traceXaiCacheLane(opts, {
|
|
434
|
-
provider: 'xai',
|
|
435
|
-
api: 'responses',
|
|
436
|
-
transport,
|
|
437
|
-
event: 'queued',
|
|
438
|
-
lane_key_hash: traceHash(laneKey),
|
|
439
|
-
lane_shard: shard,
|
|
440
|
-
lane_shards: Number.isFinite(Number(lane?.shards)) ? Number(lane.shards) : null,
|
|
441
|
-
lane_auto: lane?.auto === true,
|
|
442
|
-
lane_seed_hash: lane?.seedHash || null,
|
|
443
|
-
max_in_flight: maxInFlight,
|
|
444
|
-
active: state.active,
|
|
445
|
-
queue_depth: state.queue.length,
|
|
446
|
-
previous_response_used: !!previousResponseId,
|
|
447
|
-
input_count: inputCount,
|
|
448
|
-
});
|
|
449
|
-
}
|
|
450
|
-
const handle = await acquireXaiResponsesCacheLane({ key: laneKey, maxInFlight, signal, timeoutMs });
|
|
451
|
-
const laneMeta = {
|
|
452
|
-
enabled: true,
|
|
453
|
-
laneKeyHash: traceHash(laneKey),
|
|
454
|
-
shard,
|
|
455
|
-
shards: Number.isFinite(Number(lane?.shards)) ? Number(lane.shards) : null,
|
|
456
|
-
auto: lane?.auto === true,
|
|
457
|
-
seedHash: lane?.seedHash || null,
|
|
458
|
-
maxInFlight,
|
|
459
|
-
queued,
|
|
460
|
-
waitMs: handle.waitedMs,
|
|
461
|
-
activeAfterAcquire: handle.activeCount,
|
|
462
|
-
queueDepthAfterAcquire: handle.queueDepth,
|
|
463
|
-
};
|
|
464
|
-
traceXaiCacheLane(opts, {
|
|
465
|
-
provider: 'xai',
|
|
466
|
-
api: 'responses',
|
|
467
|
-
transport,
|
|
468
|
-
event: 'acquired',
|
|
469
|
-
lane_key_hash: laneMeta.laneKeyHash,
|
|
470
|
-
lane_shard: shard,
|
|
471
|
-
lane_shards: laneMeta.shards,
|
|
472
|
-
lane_auto: laneMeta.auto,
|
|
473
|
-
lane_seed_hash: laneMeta.seedHash,
|
|
474
|
-
max_in_flight: maxInFlight,
|
|
475
|
-
wait_ms: laneMeta.waitMs,
|
|
476
|
-
active: laneMeta.activeAfterAcquire,
|
|
477
|
-
queue_depth: laneMeta.queueDepthAfterAcquire,
|
|
478
|
-
previous_response_used: !!previousResponseId,
|
|
479
|
-
input_count: inputCount,
|
|
480
|
-
});
|
|
481
|
-
const startedAt = Date.now();
|
|
482
|
-
try {
|
|
483
|
-
return { value: await fn(laneMeta), laneMeta };
|
|
484
|
-
} finally {
|
|
485
|
-
handle.release();
|
|
486
|
-
traceXaiCacheLane(opts, {
|
|
487
|
-
provider: 'xai',
|
|
488
|
-
api: 'responses',
|
|
489
|
-
transport,
|
|
490
|
-
event: 'released',
|
|
491
|
-
lane_key_hash: laneMeta.laneKeyHash,
|
|
492
|
-
lane_shard: shard,
|
|
493
|
-
lane_shards: laneMeta.shards,
|
|
494
|
-
lane_auto: laneMeta.auto,
|
|
495
|
-
lane_seed_hash: laneMeta.seedHash,
|
|
496
|
-
max_in_flight: maxInFlight,
|
|
497
|
-
held_ms: Date.now() - startedAt,
|
|
498
|
-
previous_response_used: !!previousResponseId,
|
|
499
|
-
input_count: inputCount,
|
|
500
|
-
});
|
|
501
|
-
}
|
|
426
|
+
// Historical prompt-cache lanes formed a second admission queue and could
|
|
427
|
+
// time out while waiting. xAI is now governed exclusively by the common
|
|
428
|
+
// fixed-64 provider/account scheduler, regardless of legacy env/option
|
|
429
|
+
// knobs. Keep this wrapper only as a call-shape compatibility boundary.
|
|
430
|
+
const laneMeta = { enabled: false, maxInFlight: 0 };
|
|
431
|
+
return { value: await fn(laneMeta), laneMeta };
|
|
502
432
|
}
|
|
503
433
|
|
|
504
434
|
export function deterministicUuidFromKey(key) {
|
|
@@ -107,6 +107,72 @@ function assertSafeBaseURL(rawURL, providerName) {
|
|
|
107
107
|
// parsers → openai-compat-wire.mjs. Re-exported below for existing importers.
|
|
108
108
|
export { summarizeTraceMessages, extractCompatCachedTokens } from './openai-compat-trace.mjs';
|
|
109
109
|
export { parseToolCalls, parseResponsesToolCalls } from './openai-compat-wire.mjs';
|
|
110
|
+
|
|
111
|
+
function normalizeReasoningEffort(value, allowed) {
|
|
112
|
+
const effort = String(value ?? '').trim().toLowerCase();
|
|
113
|
+
return allowed.includes(effort) ? effort : null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Keep provider extensions isolated: fields accepted by one OpenAI-compatible
|
|
117
|
+
// backend are frequently rejected by another even when the core Chat schema is
|
|
118
|
+
// shared.
|
|
119
|
+
export function applyCompatProviderChatOptions(params, providerName, opts = {}, config = {}, modelInfo = null) {
|
|
120
|
+
if (providerName === 'xai') {
|
|
121
|
+
const reasoningEffort = normalizeXaiReasoningEffort(opts.xaiReasoningEffort
|
|
122
|
+
?? opts.effort
|
|
123
|
+
?? config?.reasoningEffort
|
|
124
|
+
?? process.env.MIXDOG_XAI_REASONING_EFFORT);
|
|
125
|
+
if (reasoningEffort) params.reasoning_effort = reasoningEffort;
|
|
126
|
+
return params;
|
|
127
|
+
}
|
|
128
|
+
if (providerName === 'deepseek') {
|
|
129
|
+
const rawThinking = opts.deepseekThinking
|
|
130
|
+
?? opts.thinking
|
|
131
|
+
?? config?.thinking;
|
|
132
|
+
const rawEffort = opts.deepseekReasoningEffort
|
|
133
|
+
?? opts.effort
|
|
134
|
+
?? config?.reasoningEffort;
|
|
135
|
+
if (rawThinking !== undefined || rawEffort !== undefined) {
|
|
136
|
+
const disabled = rawThinking === false
|
|
137
|
+
|| String(rawThinking?.type ?? rawThinking ?? rawEffort).trim().toLowerCase() === 'disabled'
|
|
138
|
+
|| String(rawThinking?.type ?? rawThinking ?? rawEffort).trim().toLowerCase() === 'none';
|
|
139
|
+
params.thinking = { type: disabled ? 'disabled' : 'enabled' };
|
|
140
|
+
if (!disabled) {
|
|
141
|
+
const effort = String(rawEffort ?? '').trim().toLowerCase();
|
|
142
|
+
if (effort === 'max' || effort === 'xhigh') params.reasoning_effort = 'max';
|
|
143
|
+
else if (['low', 'medium', 'high'].includes(effort)) params.reasoning_effort = 'high';
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return params;
|
|
147
|
+
}
|
|
148
|
+
if (providerName === 'ollama') {
|
|
149
|
+
const effort = normalizeReasoningEffort(
|
|
150
|
+
opts.ollamaReasoningEffort ?? opts.effort ?? config?.reasoningEffort,
|
|
151
|
+
['none', 'low', 'medium', 'high', 'max'],
|
|
152
|
+
);
|
|
153
|
+
if (effort) params.reasoning_effort = effort;
|
|
154
|
+
return params;
|
|
155
|
+
}
|
|
156
|
+
if (providerName === 'lmstudio') {
|
|
157
|
+
const effort = normalizeReasoningEffort(
|
|
158
|
+
opts.lmStudioReasoningEffort ?? opts.effort ?? config?.reasoningEffort,
|
|
159
|
+
['none', 'low', 'medium', 'high', 'max'],
|
|
160
|
+
);
|
|
161
|
+
if (effort) params.reasoning_effort = effort;
|
|
162
|
+
return params;
|
|
163
|
+
}
|
|
164
|
+
if (providerName === 'opencode-go') {
|
|
165
|
+
const reasoningEffort = normalizeOpencodeGoReasoningEffort(
|
|
166
|
+
opts.effort ?? config?.reasoningEffort,
|
|
167
|
+
modelInfo,
|
|
168
|
+
);
|
|
169
|
+
// OpenCode Go's OpenAI-compatible contract exposes reasoning_effort,
|
|
170
|
+
// not DeepSeek's provider-specific `thinking` extension.
|
|
171
|
+
if (reasoningEffort) params.reasoning_effort = reasoningEffort;
|
|
172
|
+
}
|
|
173
|
+
return params;
|
|
174
|
+
}
|
|
175
|
+
|
|
110
176
|
export class OpenAICompatProvider {
|
|
111
177
|
// Chat Completions prompt_tokens is already the total (includes cached).
|
|
112
178
|
// Covers grok-oauth and all OPENAI_COMPAT_PRESETS. See registry.mjs.
|
|
@@ -150,6 +216,11 @@ export class OpenAICompatProvider {
|
|
|
150
216
|
fetchOptions: { dispatcher: getLlmDispatcher() },
|
|
151
217
|
});
|
|
152
218
|
}
|
|
219
|
+
get _preconnectFn() {
|
|
220
|
+
return typeof this.config?.preconnectFn === 'function'
|
|
221
|
+
? this.config.preconnectFn
|
|
222
|
+
: preconnect;
|
|
223
|
+
}
|
|
153
224
|
reloadApiKey() {
|
|
154
225
|
try {
|
|
155
226
|
const preset = PRESETS[this.name];
|
|
@@ -174,7 +245,13 @@ export class OpenAICompatProvider {
|
|
|
174
245
|
try {
|
|
175
246
|
return await this._doSend(messages, model, tools, sendOpts);
|
|
176
247
|
} catch (err) {
|
|
177
|
-
|
|
248
|
+
const structuredStatus = [err?.status, err?.httpStatus, err?.response?.status]
|
|
249
|
+
.map(value => Number(value))
|
|
250
|
+
.find(value => Number.isFinite(value) && value > 0) || 0;
|
|
251
|
+
const status = structuredStatus > 0
|
|
252
|
+
? structuredStatus
|
|
253
|
+
: (/\b401\b/.test(String(err?.message || '')) ? 401 : 0);
|
|
254
|
+
if (status === 401) {
|
|
178
255
|
if (err.liveTextEmitted === true || err.emittedToolCall === true || err.unsafeToRetry === true) {
|
|
179
256
|
throw err;
|
|
180
257
|
}
|
|
@@ -191,7 +268,11 @@ export class OpenAICompatProvider {
|
|
|
191
268
|
// Re-warm a kept-alive socket to the provider origin before the turn so
|
|
192
269
|
// the request hot path lands on a live socket instead of paying a cold
|
|
193
270
|
// TLS handshake after an idle gap. Fire-and-forget; never awaited.
|
|
194
|
-
|
|
271
|
+
// Tests/local callers can disable this or inject a fail-closed seam;
|
|
272
|
+
// production retains the shared preconnect by default.
|
|
273
|
+
if (this.config?.preconnect !== false) {
|
|
274
|
+
this._preconnectFn(this.baseURL);
|
|
275
|
+
}
|
|
195
276
|
if (this.name === 'xai' && useXaiResponsesApi(opts, this.config)) {
|
|
196
277
|
// Shared Responses transport switch (MIXDOG_OAI_TRANSPORT), capability-
|
|
197
278
|
// gated for xAI/Grok. Provider-local HTTP pins still win: Grok
|
|
@@ -259,20 +340,7 @@ export class OpenAICompatProvider {
|
|
|
259
340
|
if (tools?.length) {
|
|
260
341
|
params.tools = toOpenAITools(tools);
|
|
261
342
|
}
|
|
262
|
-
|
|
263
|
-
const reasoningEffort = normalizeXaiReasoningEffort(opts.xaiReasoningEffort
|
|
264
|
-
?? opts.effort
|
|
265
|
-
?? this.config?.reasoningEffort
|
|
266
|
-
?? process.env.MIXDOG_XAI_REASONING_EFFORT);
|
|
267
|
-
if (reasoningEffort) params.reasoning_effort = reasoningEffort;
|
|
268
|
-
}
|
|
269
|
-
if (this.name === 'opencode-go') {
|
|
270
|
-
const reasoningEffort = normalizeOpencodeGoReasoningEffort(opts.effort ?? this.config?.reasoningEffort, modelInfo);
|
|
271
|
-
if (reasoningEffort) {
|
|
272
|
-
params.reasoning_effort = reasoningEffort;
|
|
273
|
-
params.thinking = { type: 'enabled' };
|
|
274
|
-
}
|
|
275
|
-
}
|
|
343
|
+
applyCompatProviderChatOptions(params, this.name, opts, this.config, modelInfo);
|
|
276
344
|
// Streaming (params.stream = true is always set below): no absolute
|
|
277
345
|
// wall-clock cap on a healthy stream. A fixed total-lifetime timer
|
|
278
346
|
// false-aborts live long-reasoning turns that are still emitting SSE
|
|
@@ -401,7 +469,11 @@ export class OpenAICompatProvider {
|
|
|
401
469
|
// Capture provider reasoning_content so loop.mjs can attach it to the
|
|
402
470
|
// assistant message and echo it back next turn for providers that
|
|
403
471
|
// require or benefit from that official multi-turn shape.
|
|
404
|
-
const capturesReasoningContent = this.name === 'deepseek'
|
|
472
|
+
const capturesReasoningContent = this.name === 'deepseek'
|
|
473
|
+
|| this.name === 'xai'
|
|
474
|
+
|| this.name === 'ollama'
|
|
475
|
+
|| this.name === 'lmstudio'
|
|
476
|
+
|| replaysReasoningContent;
|
|
405
477
|
const reasoningContent = (capturesReasoningContent && typeof assembled.reasoningContent === 'string')
|
|
406
478
|
? assembled.reasoningContent
|
|
407
479
|
: null;
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
traceAgentUsage,
|
|
14
14
|
} from '../agent-trace.mjs';
|
|
15
15
|
import {
|
|
16
|
+
PROVIDER_FIRST_BYTE_TIMEOUT_MS,
|
|
16
17
|
PROVIDER_HTTP_RESPONSE_TIMEOUT_MS,
|
|
17
18
|
PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS,
|
|
18
19
|
PROVIDER_SSE_IDLE_WATCHDOG_ENABLED,
|
|
@@ -20,7 +21,12 @@ import {
|
|
|
20
21
|
createTimeoutSignal,
|
|
21
22
|
createPassthroughSignal,
|
|
22
23
|
} from '../stall-policy.mjs';
|
|
23
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
jitterDelayMs,
|
|
26
|
+
populateHttpStatusFromMessage,
|
|
27
|
+
shouldFallbackTransport,
|
|
28
|
+
sleepWithAbort,
|
|
29
|
+
} from './retry-classifier.mjs';
|
|
24
30
|
import { getLlmDispatcher } from '../../../shared/llm/http-agent.mjs';
|
|
25
31
|
import { makeInvalidToolArgsMarker } from './openai-compat-stream.mjs';
|
|
26
32
|
import { createLeakGuard, createToolCallDedupe, dedupeToolCallList } from './anthropic-leaked-toolcall.mjs';
|
|
@@ -34,6 +40,9 @@ import { createActiveToolItemTracker } from './tool-stream-state.mjs';
|
|
|
34
40
|
// mirrors it so OpenAIDirectProvider can fall back off WebSocket like
|
|
35
41
|
// openai-oauth. Same Responses SSE wire format, only endpoint + auth differ.
|
|
36
42
|
const OPENAI_DIRECT_RESPONSES_URL = 'https://api.openai.com/v1/responses';
|
|
43
|
+
const CODEX_REQUEST_MAX_RETRIES = 4;
|
|
44
|
+
const CODEX_REQUEST_BACKOFF_MS = Object.freeze([200, 400, 800, 1600]);
|
|
45
|
+
const CODEX_RETRY_JITTER_RATIO = 0.1;
|
|
37
46
|
|
|
38
47
|
export function _envFlag(name, fallback = true) {
|
|
39
48
|
const raw = process.env[name];
|
|
@@ -187,6 +196,7 @@ export async function sendViaHttpSse({
|
|
|
187
196
|
iteration,
|
|
188
197
|
useModel,
|
|
189
198
|
fetchFn = fetch,
|
|
199
|
+
_sleepFn,
|
|
190
200
|
} = {}) {
|
|
191
201
|
// P1 audit fix: no fixed wall-clock total cap on the HTTP/SSE fallback
|
|
192
202
|
// stream. The old createTimeoutSignal(..., PROVIDER_GENERATE_TOTAL_TIMEOUT_MS)
|
|
@@ -202,31 +212,60 @@ export async function sendViaHttpSse({
|
|
|
202
212
|
// one still aborts, and
|
|
203
213
|
// (c) externalSignal (client disconnect / replaced-by-newer-request).
|
|
204
214
|
const totalTimeout = createPassthroughSignal(externalSignal);
|
|
205
|
-
const headerTimeout = createTimeoutSignal(
|
|
206
|
-
totalTimeout.signal,
|
|
207
|
-
PROVIDER_HTTP_RESPONSE_TIMEOUT_MS,
|
|
208
|
-
'OpenAI OAuth HTTP fallback initial response',
|
|
209
|
-
);
|
|
210
215
|
const headers = _buildOpenAIHttpFallbackHeaders({ auth, cacheKey });
|
|
211
216
|
const fetchStartedAt = Date.now();
|
|
212
217
|
const responsesUrl = auth?.type === 'openai-direct'
|
|
213
218
|
? OPENAI_DIRECT_RESPONSES_URL
|
|
214
219
|
: CODEX_RESPONSES_URL;
|
|
215
220
|
let response;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
221
|
+
for (let attempt = 0; attempt <= CODEX_REQUEST_MAX_RETRIES; attempt++) {
|
|
222
|
+
const headerTimeout = createTimeoutSignal(
|
|
223
|
+
totalTimeout.signal,
|
|
224
|
+
PROVIDER_HTTP_RESPONSE_TIMEOUT_MS,
|
|
225
|
+
'OpenAI OAuth HTTP fallback initial response',
|
|
226
|
+
);
|
|
227
|
+
let requestError = null;
|
|
228
|
+
try {
|
|
229
|
+
try { onStageChange?.('requesting'); } catch {}
|
|
230
|
+
response = await fetchFn(responsesUrl, {
|
|
231
|
+
method: 'POST',
|
|
232
|
+
headers,
|
|
233
|
+
body: JSON.stringify(body),
|
|
234
|
+
signal: headerTimeout.signal,
|
|
235
|
+
dispatcher: getLlmDispatcher(),
|
|
236
|
+
});
|
|
237
|
+
} catch (err) {
|
|
238
|
+
requestError = headerTimeout.signal?.aborted
|
|
239
|
+
&& headerTimeout.signal.reason instanceof Error
|
|
240
|
+
? headerTimeout.signal.reason
|
|
241
|
+
: err;
|
|
242
|
+
} finally {
|
|
243
|
+
headerTimeout.cleanup();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const retryableStatus = response && response.status >= 500 && response.status <= 599;
|
|
247
|
+
const retryableTransport = !response && requestError
|
|
248
|
+
&& !externalSignal?.aborted
|
|
249
|
+
&& !totalTimeout.signal?.aborted;
|
|
250
|
+
if (attempt < CODEX_REQUEST_MAX_RETRIES && (retryableStatus || retryableTransport)) {
|
|
251
|
+
// A non-success response has not exposed any streamed output. Drain
|
|
252
|
+
// its body before reissuing so the dispatcher can reuse the socket.
|
|
253
|
+
if (response) await response.arrayBuffer().catch(() => {});
|
|
254
|
+
const raw = CODEX_REQUEST_BACKOFF_MS[attempt];
|
|
255
|
+
await sleepWithAbort(
|
|
256
|
+
jitterDelayMs(raw, CODEX_RETRY_JITTER_RATIO),
|
|
257
|
+
externalSignal,
|
|
258
|
+
_sleepFn,
|
|
259
|
+
'OpenAI OAuth HTTP request retry backoff aborted',
|
|
260
|
+
);
|
|
261
|
+
response = undefined;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (requestError) {
|
|
265
|
+
totalTimeout.cleanup();
|
|
266
|
+
throw requestError;
|
|
267
|
+
}
|
|
268
|
+
break;
|
|
230
269
|
}
|
|
231
270
|
|
|
232
271
|
traceAgentFetch({
|
|
@@ -292,6 +331,30 @@ export async function sendViaHttpSse({
|
|
|
292
331
|
// deltas then goes silent trips a short, named terminal failure instead of
|
|
293
332
|
// hanging until the 30-min agent watchdog. Disablable via the shared env.
|
|
294
333
|
let _semanticIdleTimer = null;
|
|
334
|
+
let _firstServerEventTimer = null;
|
|
335
|
+
const firstServerEventOverrideMs = Number(opts?._firstServerEventTimeoutMs);
|
|
336
|
+
const firstServerEventMs = Number.isFinite(firstServerEventOverrideMs) && firstServerEventOverrideMs > 0
|
|
337
|
+
? firstServerEventOverrideMs
|
|
338
|
+
: PROVIDER_FIRST_BYTE_TIMEOUT_MS;
|
|
339
|
+
const _clearFirstServerEvent = () => {
|
|
340
|
+
if (_firstServerEventTimer) {
|
|
341
|
+
clearTimeout(_firstServerEventTimer);
|
|
342
|
+
_firstServerEventTimer = null;
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
const _armFirstServerEvent = () => {
|
|
346
|
+
if (!(firstServerEventMs > 0)) return;
|
|
347
|
+
_clearFirstServerEvent();
|
|
348
|
+
_firstServerEventTimer = setTimeout(() => {
|
|
349
|
+
const err = new Error(`OpenAI OAuth HTTP fallback first server event timed out after ${firstServerEventMs}ms`);
|
|
350
|
+
err.code = 'EPROVIDERTIMEOUT';
|
|
351
|
+
err.firstByteTimeout = true;
|
|
352
|
+
_streamAbortReason = err;
|
|
353
|
+
try { reader.cancel(err).catch(() => {}); } catch {}
|
|
354
|
+
_rejectPendingRead(err);
|
|
355
|
+
}, firstServerEventMs);
|
|
356
|
+
try { _firstServerEventTimer.unref?.(); } catch {}
|
|
357
|
+
};
|
|
295
358
|
const _clearSemanticIdle = () => {
|
|
296
359
|
if (_semanticIdleTimer) { clearTimeout(_semanticIdleTimer); _semanticIdleTimer = null; }
|
|
297
360
|
};
|
|
@@ -508,6 +571,11 @@ export async function sendViaHttpSse({
|
|
|
508
571
|
};
|
|
509
572
|
const handleEvent = (event) => {
|
|
510
573
|
if (!event || typeof event.type !== 'string') return;
|
|
574
|
+
_clearFirstServerEvent();
|
|
575
|
+
// Once any real SSE server event arrives, the fixed initial deadline is
|
|
576
|
+
// satisfied and semantic-idle ownership begins. meaningful() below may
|
|
577
|
+
// immediately re-arm it for a semantic event.
|
|
578
|
+
_armSemanticIdle();
|
|
511
579
|
switch (event.type) {
|
|
512
580
|
case 'response.created':
|
|
513
581
|
if (event.response?.model) model = event.response.model;
|
|
@@ -783,12 +851,10 @@ export async function sendViaHttpSse({
|
|
|
783
851
|
};
|
|
784
852
|
|
|
785
853
|
try {
|
|
786
|
-
//
|
|
787
|
-
//
|
|
788
|
-
//
|
|
789
|
-
|
|
790
|
-
// an empty-partial stall classifies stream_stalled → retry/fallback.
|
|
791
|
-
_armSemanticIdle();
|
|
854
|
+
// Initial wait is governed only by the fixed first-server-event policy.
|
|
855
|
+
// Semantic idle begins in meaningful() after a parsed server event, so
|
|
856
|
+
// a lower semantic-idle override cannot shorten this first-event wait.
|
|
857
|
+
_armFirstServerEvent();
|
|
792
858
|
while (true) {
|
|
793
859
|
if (totalTimeout.signal?.aborted) {
|
|
794
860
|
_clearSemanticIdle();
|
|
@@ -834,6 +900,7 @@ export async function sendViaHttpSse({
|
|
|
834
900
|
throw err;
|
|
835
901
|
} finally {
|
|
836
902
|
_pendingReadReject = null;
|
|
903
|
+
_clearFirstServerEvent();
|
|
837
904
|
_clearSemanticIdle();
|
|
838
905
|
try { reader.releaseLock?.(); } catch {}
|
|
839
906
|
if (_onTotalAbort && totalTimeout.signal) {
|