mixdog 0.9.53 → 0.9.55
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 +2 -1
- package/scripts/agent-model-liveness-test.mjs +68 -0
- package/scripts/anthropic-transport-policy-test.mjs +260 -0
- package/scripts/desktop-session-bridge-test.mjs +47 -0
- package/scripts/gemini-provider-test.mjs +396 -1
- package/scripts/grok-oauth-refresh-race-test.mjs +76 -1
- package/scripts/interrupted-turn-history-test.mjs +28 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +81 -1
- package/scripts/pending-completion-drop-test.mjs +160 -4
- package/scripts/pending-messages-lock-nonblocking-test.mjs +62 -0
- package/scripts/process-lifecycle-test.mjs +18 -4
- package/scripts/prompt-input-parity-test.mjs +145 -0
- package/scripts/provider-admission-scheduler-test.mjs +4 -5
- package/scripts/provider-contract-test.mjs +91 -33
- package/scripts/provider-toolcall-test.mjs +97 -17
- package/scripts/streaming-tail-window-test.mjs +146 -0
- package/scripts/tui-runtime-load-bench-entry.jsx +608 -0
- package/scripts/tui-runtime-load-bench.mjs +45 -0
- package/scripts/tui-store-frame-batch-test.mjs +99 -0
- package/scripts/tui-transcript-perf-test.mjs +367 -2
- package/scripts/write-backpressure-test.mjs +147 -0
- package/src/cli.mjs +5 -5
- package/src/lib/keychain-cjs.cjs +114 -25
- package/src/repl.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +62 -25
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +8 -2
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +50 -23
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +15 -4
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +136 -27
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +104 -20
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +96 -75
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +40 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +5 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +35 -4
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +49 -9
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +14 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +9 -4
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +53 -13
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +145 -23
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +20 -4
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +316 -63
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +23 -1
- package/src/runtime/agent/orchestrator/session/store.mjs +46 -1
- package/src/runtime/agent/orchestrator/stall-policy.mjs +6 -13
- package/src/runtime/memory/lib/trace-store.mjs +66 -4
- package/src/runtime/shared/buffered-appender.mjs +83 -4
- package/src/runtime/shared/memory-snapshot.mjs +45 -36
- package/src/runtime/shared/process-lifecycle.mjs +166 -45
- package/src/runtime/shared/process-shutdown.mjs +2 -1
- package/src/session-runtime/model-route-api.mjs +4 -1
- package/src/session-runtime/native-search.mjs +4 -2
- package/src/session-runtime/provider-auth-api.mjs +8 -1
- package/src/session-runtime/provider-models.mjs +8 -3
- package/src/session-runtime/resource-api.mjs +2 -1
- package/src/session-runtime/runtime-core.mjs +32 -0
- package/src/session-runtime/session-turn-api.mjs +1 -0
- package/src/session-runtime/warmup-schedulers.mjs +5 -1
- package/src/standalone/agent-tool.mjs +19 -1
- package/src/tui/App.jsx +10 -4
- package/src/tui/app/text-layout.mjs +9 -1
- package/src/tui/app/transcript-window.mjs +146 -60
- package/src/tui/app/use-mouse-input.mjs +16 -8
- package/src/tui/app/use-transcript-scroll.mjs +71 -19
- package/src/tui/app/use-transcript-window.mjs +21 -10
- package/src/tui/components/PromptInput.jsx +13 -6
- package/src/tui/dist/index.mjs +1094 -232
- package/src/tui/engine/context-state.mjs +31 -31
- package/src/tui/engine/frame-batched-store.mjs +75 -0
- package/src/tui/engine/session-api-ext.mjs +6 -1
- package/src/tui/engine/session-api.mjs +9 -3
- package/src/tui/engine/session-flow.mjs +54 -27
- package/src/tui/engine/turn.mjs +44 -3
- package/src/tui/engine.mjs +515 -36
- package/src/tui/input-editing.mjs +33 -13
- package/src/tui/markdown/measure-rendered-rows.mjs +117 -5
- package/vendor/ink/build/ink.js +8 -16
|
@@ -5,20 +5,16 @@
|
|
|
5
5
|
* https://auth.x.ai/.well-known/openid-configuration). Credentials come from
|
|
6
6
|
* Mixdog's own token store (grok-oauth.json).
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* grok-build and grok-composer-2.5-fast, which api.x.ai does not publish.
|
|
12
|
-
* The proxy version-gates /responses (HTTP 426); we clear it with the Grok
|
|
13
|
-
* CLI client headers (proxyHeaders, real local version from version.json).
|
|
8
|
+
* Every OAuth inference request routes through cli-chat-proxy.grok.com/v1,
|
|
9
|
+
* matching Grok Build's session-auth contract. Model discovery still merges
|
|
10
|
+
* api.x.ai and proxy catalogs because each publishes a different subset.
|
|
14
11
|
*
|
|
15
12
|
* Inference is delegated to an inner OpenAICompatProvider('xai') — the only
|
|
16
|
-
* preset wired for the Responses API — with the
|
|
17
|
-
*
|
|
18
|
-
* OAuth access token. The model catalog is the union of both endpoints.
|
|
13
|
+
* preset wired for the Responses API — with the proxy URL + CLI headers
|
|
14
|
+
* injected via config.extraHeaders, bearer swapped for the OAuth access token.
|
|
19
15
|
*/
|
|
20
16
|
import { createServer } from 'http';
|
|
21
|
-
import { randomBytes, createHash } from 'crypto';
|
|
17
|
+
import { randomBytes, randomUUID, createHash } from 'crypto';
|
|
22
18
|
import { readFileSync, existsSync, mkdirSync, statSync, unlinkSync } from 'fs';
|
|
23
19
|
import { join, resolve } from 'path';
|
|
24
20
|
import { getPluginData } from '../config.mjs';
|
|
@@ -81,33 +77,33 @@ function grokCliVersion() {
|
|
|
81
77
|
// Headers the Grok CLI sends to clear the proxy version gate — extracted from
|
|
82
78
|
// the grok binary: x-grok-client-version (the actual 426 gate),
|
|
83
79
|
// x-grok-client-identifier, and a matching User-Agent.
|
|
84
|
-
function proxyHeaders() {
|
|
80
|
+
function proxyHeaders({ model, sendOpts, userId } = {}) {
|
|
85
81
|
const v = grokCliVersion();
|
|
86
|
-
|
|
82
|
+
const headers = {
|
|
87
83
|
'x-grok-client-version': v,
|
|
88
84
|
'x-grok-client-identifier': GROK_CLIENT_IDENTIFIER,
|
|
89
85
|
'User-Agent': `xai-grok-build/${v}`,
|
|
90
86
|
};
|
|
87
|
+
const sessionId = String(sendOpts?.sessionId || sendOpts?.session?.id || '').trim();
|
|
88
|
+
const requestId = String(sendOpts?.requestId || '').trim() || (sendOpts ? randomUUID() : '');
|
|
89
|
+
const turnIndex = sendOpts?.iteration;
|
|
90
|
+
// Mixdog has an authoritative session id but no distinct Grok conversation
|
|
91
|
+
// id. Do not synthesize the latter from the former.
|
|
92
|
+
if (sessionId) headers['x-grok-session-id'] = sessionId;
|
|
93
|
+
if (requestId) headers['x-grok-req-id'] = requestId;
|
|
94
|
+
if (model) headers['x-grok-model-override'] = String(model);
|
|
95
|
+
if (turnIndex != null && Number.isFinite(Number(turnIndex))) {
|
|
96
|
+
headers['x-grok-turn-idx'] = String(turnIndex);
|
|
97
|
+
}
|
|
98
|
+
if (userId) headers['x-grok-user-id'] = String(userId);
|
|
99
|
+
return headers;
|
|
91
100
|
}
|
|
92
101
|
|
|
93
|
-
function resolveGrokOAuthResponsesTransport(
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
?? config?.transport
|
|
99
|
-
?? process.env.MIXDOG_GROK_OAUTH_RESPONSES_TRANSPORT
|
|
100
|
-
?? process.env.MIXDOG_GROK_OAUTH_TRANSPORT
|
|
101
|
-
?? '';
|
|
102
|
-
const mode = String(raw).trim().toLowerCase();
|
|
103
|
-
if (mode) return mode;
|
|
104
|
-
// No Grok-specific override: leave the field unset so ALL api.x.ai-capable
|
|
105
|
-
// Grok models honor the shared MIXDOG_OAI_TRANSPORT switch. Proxy-only
|
|
106
|
-
// grok-build is included: the shared xAI WebSocket connector targets the
|
|
107
|
-
// fixed wss://api.x.ai/v1/responses endpoint (not the proxy baseURL), and a
|
|
108
|
-
// live probe confirmed grok-build serves there with the OAuth token. No
|
|
109
|
-
// implicit HTTP pin — the proxy baseURL is only used when HTTP is selected.
|
|
110
|
-
return undefined;
|
|
102
|
+
function resolveGrokOAuthResponsesTransport() {
|
|
103
|
+
// The OAuth bearer is session auth and must never escape to api.x.ai.
|
|
104
|
+
// Mixdog's xAI WebSocket connector has a fixed api.x.ai endpoint, so OAuth
|
|
105
|
+
// inference is pinned to the proxy's reference HTTP/SSE transport.
|
|
106
|
+
return 'http';
|
|
111
107
|
}
|
|
112
108
|
|
|
113
109
|
// Retired model aliases xAI no longer exposes by their old ids. The live
|
|
@@ -221,13 +217,18 @@ function _identityFromAccessToken(token) {
|
|
|
221
217
|
const parts = String(token || '').split('.');
|
|
222
218
|
if (parts.length !== 3) return {};
|
|
223
219
|
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString('utf-8'));
|
|
220
|
+
const principalId = payload?.principal_id || payload?.principalId || '';
|
|
221
|
+
const principalType = payload?.principal_type || payload?.principalType || '';
|
|
224
222
|
const userId = payload?.user_id
|
|
225
223
|
|| payload?.userId
|
|
226
|
-
||
|
|
227
|
-
|| payload?.principalId
|
|
224
|
+
|| principalId
|
|
228
225
|
|| payload?.sub
|
|
229
226
|
|| '';
|
|
230
|
-
return
|
|
227
|
+
return {
|
|
228
|
+
...(userId ? { user_id: String(userId) } : {}),
|
|
229
|
+
...(principalType ? { principal_type: String(principalType) } : {}),
|
|
230
|
+
...(principalId ? { principal_id: String(principalId) } : {}),
|
|
231
|
+
};
|
|
231
232
|
} catch { return {}; }
|
|
232
233
|
}
|
|
233
234
|
|
|
@@ -243,12 +244,15 @@ function _loadOwnTokens() {
|
|
|
243
244
|
try {
|
|
244
245
|
const raw = JSON.parse(readFileSync(path, 'utf-8'));
|
|
245
246
|
if (!raw?.access_token || !raw?.refresh_token) return null;
|
|
247
|
+
const identity = _identityFromAccessToken(raw.access_token);
|
|
246
248
|
return {
|
|
247
249
|
access_token: raw.access_token,
|
|
248
250
|
refresh_token: raw.refresh_token,
|
|
249
251
|
expires_at: _normalizeExpiresAt(raw.expires_at ?? raw.expiresAt) || _expiryFromAccessToken(raw.access_token),
|
|
250
252
|
token_endpoint: raw.token_endpoint || null,
|
|
251
|
-
user_id: raw.user_id || raw.userId ||
|
|
253
|
+
user_id: raw.user_id || raw.userId || identity.user_id || '',
|
|
254
|
+
principal_type: raw.principal_type || raw.principalType || identity.principal_type || '',
|
|
255
|
+
principal_id: raw.principal_id || raw.principalId || identity.principal_id || '',
|
|
252
256
|
source: 'own',
|
|
253
257
|
mtimeMs: _mtimeMs(path),
|
|
254
258
|
};
|
|
@@ -261,12 +265,15 @@ function loadTokens() {
|
|
|
261
265
|
}
|
|
262
266
|
|
|
263
267
|
function saveTokens(tokens) {
|
|
268
|
+
const identity = _identityFromAccessToken(tokens.access_token);
|
|
264
269
|
writeJsonAtomicSync(getOwnTokenPath(), {
|
|
265
270
|
access_token: tokens.access_token,
|
|
266
271
|
refresh_token: tokens.refresh_token,
|
|
267
272
|
expires_at: tokens.expires_at || 0,
|
|
268
273
|
token_endpoint: tokens.token_endpoint || null,
|
|
269
|
-
user_id: tokens.user_id || tokens.userId ||
|
|
274
|
+
user_id: tokens.user_id || tokens.userId || identity.user_id || undefined,
|
|
275
|
+
principal_type: tokens.principal_type || tokens.principalType || identity.principal_type || undefined,
|
|
276
|
+
principal_id: tokens.principal_id || tokens.principalId || identity.principal_id || undefined,
|
|
270
277
|
}, { lock: true, fsyncDir: true, mode: 0o600, secret: true });
|
|
271
278
|
}
|
|
272
279
|
|
|
@@ -331,14 +338,17 @@ async function _postRefresh(tokens) {
|
|
|
331
338
|
: (await fetchDiscovery()).token_endpoint;
|
|
332
339
|
const timeout = createTimeoutSignal(null, TOKEN_TIMEOUT_MS, 'grok-oauth refresh');
|
|
333
340
|
try {
|
|
341
|
+
const body = new URLSearchParams({
|
|
342
|
+
grant_type: 'refresh_token',
|
|
343
|
+
client_id: CLIENT_ID,
|
|
344
|
+
refresh_token: tokens.refresh_token,
|
|
345
|
+
});
|
|
346
|
+
if (tokens.principal_type) body.set('principal_type', tokens.principal_type);
|
|
347
|
+
if (tokens.principal_id) body.set('principal_id', tokens.principal_id);
|
|
334
348
|
const res = await fetch(tokenEndpoint, {
|
|
335
349
|
method: 'POST',
|
|
336
350
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
337
|
-
body
|
|
338
|
-
grant_type: 'refresh_token',
|
|
339
|
-
client_id: CLIENT_ID,
|
|
340
|
-
refresh_token: tokens.refresh_token,
|
|
341
|
-
}),
|
|
351
|
+
body,
|
|
342
352
|
// Never follow a redirect on a secret-bearing request: a trusted
|
|
343
353
|
// token endpoint that 307/308-redirects would replay the
|
|
344
354
|
// refresh_token to the redirect target. Fail loud instead.
|
|
@@ -350,13 +360,12 @@ async function _postRefresh(tokens) {
|
|
|
350
360
|
let json = null;
|
|
351
361
|
try { json = text ? JSON.parse(text) : null; } catch { /* handled below */ }
|
|
352
362
|
if (!res.ok) {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
const
|
|
356
|
-
|| /invalid_grant|revoked|reused/i.test(text);
|
|
363
|
+
const oauthError = String(json?.error || '').toLowerCase();
|
|
364
|
+
const isInvalidGrant = oauthError === 'invalid_grant';
|
|
365
|
+
const isTerminalRefresh = isInvalidGrant || oauthError === 'invalid_client';
|
|
357
366
|
throw Object.assign(
|
|
358
367
|
new Error(`[grok-oauth] token refresh ${res.status}: ${_scrubTokens(text).slice(0, 200)}`),
|
|
359
|
-
{ isInvalidGrant },
|
|
368
|
+
{ isInvalidGrant, isTerminalRefresh, oauthError: oauthError || null },
|
|
360
369
|
);
|
|
361
370
|
}
|
|
362
371
|
const accessToken = json?.access_token;
|
|
@@ -371,6 +380,8 @@ async function _postRefresh(tokens) {
|
|
|
371
380
|
: _normalizeExpiresAt(json?.expires_at),
|
|
372
381
|
token_endpoint: tokenEndpoint,
|
|
373
382
|
user_id: tokens.user_id || tokens.userId || _identityFromAccessToken(accessToken).user_id || '',
|
|
383
|
+
principal_type: json?.principal_type || tokens.principal_type || tokens.principalType || '',
|
|
384
|
+
principal_id: json?.principal_id || tokens.principal_id || tokens.principalId || '',
|
|
374
385
|
};
|
|
375
386
|
saveTokens(refreshed);
|
|
376
387
|
return { ...refreshed, source: 'own', mtimeMs: _mtimeMs(getOwnTokenPath()) };
|
|
@@ -379,6 +390,20 @@ async function _postRefresh(tokens) {
|
|
|
379
390
|
}
|
|
380
391
|
}
|
|
381
392
|
|
|
393
|
+
async function _postRefreshWithRetry(tokens) {
|
|
394
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
395
|
+
try {
|
|
396
|
+
return await _postRefresh(tokens);
|
|
397
|
+
} catch (err) {
|
|
398
|
+
if (err?.isTerminalRefresh || attempt === 2) throw err;
|
|
399
|
+
const baseMs = Math.min(2_000, 200 * (2 ** attempt));
|
|
400
|
+
const delayMs = Math.round(baseMs + Math.random() * baseMs);
|
|
401
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
throw new Error('[grok-oauth] unreachable refresh retry state');
|
|
405
|
+
}
|
|
406
|
+
|
|
382
407
|
// Mixdog processes share one grok-oauth.json and xAI rotates refresh tokens
|
|
383
408
|
// single-use. Hold a cross-process lease across the re-read, exchange, and
|
|
384
409
|
// atomic save so only one process can spend a generation.
|
|
@@ -403,7 +428,7 @@ async function refreshTokens(tokens, { force = false } = {}) {
|
|
|
403
428
|
|
|
404
429
|
const current = disk || tokens;
|
|
405
430
|
try {
|
|
406
|
-
return await
|
|
431
|
+
return await _postRefreshWithRetry(current);
|
|
407
432
|
} catch (err) {
|
|
408
433
|
if (err?.isInvalidGrant) {
|
|
409
434
|
// A writer that does not participate in this lease may still
|
|
@@ -416,7 +441,7 @@ async function refreshTokens(tokens, { force = false } = {}) {
|
|
|
416
441
|
&& (!rotated.expires_at || rotated.expires_at >= validAfter)) {
|
|
417
442
|
return rotated;
|
|
418
443
|
}
|
|
419
|
-
return await
|
|
444
|
+
return await _postRefreshWithRetry(rotated);
|
|
420
445
|
}
|
|
421
446
|
}
|
|
422
447
|
throw err;
|
|
@@ -660,27 +685,16 @@ export class GrokOAuthProvider {
|
|
|
660
685
|
}
|
|
661
686
|
|
|
662
687
|
// Build (or rebuild on token change) the inner OpenAI-compatible provider
|
|
663
|
-
// that owns request shaping.
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
// Proxy-only models (grok-composer*, grok-build) live on the grok-build
|
|
667
|
-
// CLI proxy: different baseURL + the Grok CLI client headers. Everything
|
|
668
|
-
// else stays on api.x.ai. The cache key includes the route so an
|
|
669
|
-
// interleaved sequence of api/proxy calls each get the right inner.
|
|
670
|
-
const proxy = isProxyOnlyModel(model);
|
|
671
|
-
const key = `${proxy ? 'proxy' : 'api'}:${token}`;
|
|
688
|
+
// that owns request shaping. Every OAuth model uses the Grok CLI proxy.
|
|
689
|
+
_ensureInner(token, model, requestHeaders = proxyHeaders({ model })) {
|
|
690
|
+
const key = `proxy:${token}:${JSON.stringify(requestHeaders)}`;
|
|
672
691
|
if (this._inner && this._innerKey === key) return this._inner;
|
|
673
692
|
this._inner = new OpenAICompatProvider('xai', {
|
|
674
693
|
...this.config,
|
|
675
694
|
apiKey: token,
|
|
676
|
-
baseURL:
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
// Grok-specific http setting is the escape hatch back to the proxy.
|
|
680
|
-
responsesTransport: resolveGrokOAuthResponsesTransport(this.config, proxy),
|
|
681
|
-
// Proxy-only models additionally need the Grok CLI client headers to
|
|
682
|
-
// clear the proxy version gate (HTTP 426 otherwise).
|
|
683
|
-
...(proxy ? { extraHeaders: proxyHeaders() } : {}),
|
|
695
|
+
baseURL: PROXY_BASE_URL,
|
|
696
|
+
responsesTransport: resolveGrokOAuthResponsesTransport(),
|
|
697
|
+
extraHeaders: requestHeaders,
|
|
684
698
|
});
|
|
685
699
|
this._innerKey = key;
|
|
686
700
|
return this._inner;
|
|
@@ -693,13 +707,18 @@ export class GrokOAuthProvider {
|
|
|
693
707
|
const warm = typeof this.config?.preconnectFn === 'function'
|
|
694
708
|
? this.config.preconnectFn
|
|
695
709
|
: preconnect;
|
|
696
|
-
warm(
|
|
710
|
+
warm(PROXY_BASE_URL);
|
|
697
711
|
}
|
|
698
712
|
const useModel = normalizeGrokModelId(
|
|
699
713
|
model || await ensureLatestGrokModel(this),
|
|
700
714
|
);
|
|
701
715
|
const tokens = await this.ensureAuth();
|
|
702
|
-
const
|
|
716
|
+
const requestHeaders = proxyHeaders({
|
|
717
|
+
model: useModel,
|
|
718
|
+
sendOpts,
|
|
719
|
+
userId: tokens.user_id,
|
|
720
|
+
});
|
|
721
|
+
const inner = this._ensureInner(tokens.access_token, useModel, requestHeaders);
|
|
703
722
|
const grokTools = normalizeGrokToolSchemas(tools);
|
|
704
723
|
try {
|
|
705
724
|
// Call _doSend directly, bypassing OpenAICompatProvider.send()'s
|
|
@@ -710,23 +729,22 @@ export class GrokOAuthProvider {
|
|
|
710
729
|
// catalog to this token — no single-model lock.
|
|
711
730
|
return await inner._doSend(messages, useModel, grokTools, sendOpts);
|
|
712
731
|
} catch (err) {
|
|
713
|
-
// Refresh-and-retry
|
|
732
|
+
// Refresh-and-retry on a server-rejected OAuth session (401/403).
|
|
714
733
|
// Resolve the status from the structured field (falling back to the
|
|
715
734
|
// shared classifier that derives it from the error text) rather than
|
|
716
|
-
// ad-hoc string matching.
|
|
717
|
-
// account's tier lacks the model) — refreshing the same grant can't
|
|
718
|
-
// fix it, so it must surface unretried.
|
|
735
|
+
// ad-hoc string matching.
|
|
719
736
|
populateHttpStatusFromMessage(err);
|
|
720
|
-
|
|
721
|
-
|
|
737
|
+
const rejectedStatus = Number(err?.httpStatus || err?.status);
|
|
738
|
+
if (rejectedStatus === 401 || rejectedStatus === 403) {
|
|
739
|
+
// A stream-level rejection after text/tool dispatch cannot be safely
|
|
722
740
|
// replayed: the client has already observed output or may have
|
|
723
741
|
// executed a side-effecting tool.
|
|
724
742
|
if (err.liveTextEmitted === true || err.emittedToolCall === true || err.unsafeToRetry === true) {
|
|
725
743
|
throw err;
|
|
726
744
|
}
|
|
727
|
-
process.stderr.write(
|
|
745
|
+
process.stderr.write(`[grok-oauth] ${rejectedStatus}, force-refreshing token...\n`);
|
|
728
746
|
const fresh = await this.ensureAuth({ forceRefresh: true });
|
|
729
|
-
const retryInner = this._ensureInner(fresh.access_token, useModel);
|
|
747
|
+
const retryInner = this._ensureInner(fresh.access_token, useModel, requestHeaders);
|
|
730
748
|
const retryOpts = err?.__warmup?.usage
|
|
731
749
|
? { ...(sendOpts || {}), _carriedWarmup: err.__warmup }
|
|
732
750
|
: sendOpts;
|
|
@@ -882,6 +900,7 @@ async function exchangeAuthorizationCode({ discovery, pkce, code }) {
|
|
|
882
900
|
if (!json.access_token || !json.refresh_token) {
|
|
883
901
|
throw new Error('[grok-oauth] token exchange response missing access_token or refresh_token');
|
|
884
902
|
}
|
|
903
|
+
const identity = _identityFromAccessToken(json.access_token);
|
|
885
904
|
const tokens = {
|
|
886
905
|
access_token: json.access_token,
|
|
887
906
|
refresh_token: json.refresh_token,
|
|
@@ -889,7 +908,9 @@ async function exchangeAuthorizationCode({ discovery, pkce, code }) {
|
|
|
889
908
|
? Date.now() + json.expires_in * 1000
|
|
890
909
|
: _normalizeExpiresAt(json.expires_at),
|
|
891
910
|
token_endpoint: discovery.token_endpoint,
|
|
892
|
-
user_id:
|
|
911
|
+
user_id: identity.user_id || '',
|
|
912
|
+
principal_type: json.principal_type || identity.principal_type || '',
|
|
913
|
+
principal_id: json.principal_id || identity.principal_id || '',
|
|
893
914
|
};
|
|
894
915
|
saveTokens(tokens);
|
|
895
916
|
return tokens;
|
|
@@ -33,7 +33,7 @@ export function resolveAnthropicCacheTtls(opts) {
|
|
|
33
33
|
tools: pick('tools', null),
|
|
34
34
|
system: pick('system', ANTHROPIC_CACHE_TTL_STABLE),
|
|
35
35
|
tier3: pick('tier3', ANTHROPIC_CACHE_TTL_STABLE),
|
|
36
|
-
messages: pick('messages',
|
|
36
|
+
messages: pick('messages', ANTHROPIC_CACHE_TTL_VOLATILE),
|
|
37
37
|
};
|
|
38
38
|
const ttlRank = (ttl) => (ttl === ANTHROPIC_CACHE_TTL_STABLE ? 2 : 1);
|
|
39
39
|
let minRank = Infinity;
|
|
@@ -55,6 +55,45 @@ export function clampAnthropicThinkingBudget(value, maxTokens) {
|
|
|
55
55
|
return Math.max(1024, Math.min(desired, ceiling));
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
export function normalizeAnthropicNonStreamingResponse(message, fallbackModel = '') {
|
|
59
|
+
const blocks = Array.isArray(message?.content) ? message.content : [];
|
|
60
|
+
const text = blocks
|
|
61
|
+
.filter((block) => block?.type === 'text')
|
|
62
|
+
.map((block) => String(block.text || ''))
|
|
63
|
+
.join('');
|
|
64
|
+
const toolCalls = blocks
|
|
65
|
+
.filter((block) => block?.type === 'tool_use')
|
|
66
|
+
.map((block) => ({
|
|
67
|
+
id: block.id,
|
|
68
|
+
name: block.name,
|
|
69
|
+
arguments: block.input && typeof block.input === 'object' ? block.input : {},
|
|
70
|
+
}));
|
|
71
|
+
const thinkingBlocks = blocks.filter((block) => (
|
|
72
|
+
block?.type === 'thinking' || block?.type === 'redacted_thinking'
|
|
73
|
+
));
|
|
74
|
+
const usage = message?.usage || {};
|
|
75
|
+
const input = Number(usage.input_tokens) || 0;
|
|
76
|
+
const cacheRead = Number(usage.cache_read_input_tokens) || 0;
|
|
77
|
+
const cacheWrite = Number(usage.cache_creation_input_tokens) || 0;
|
|
78
|
+
return {
|
|
79
|
+
content: text,
|
|
80
|
+
model: message?.model || fallbackModel,
|
|
81
|
+
toolCalls: toolCalls.length ? toolCalls : undefined,
|
|
82
|
+
stopReason: message?.stop_reason || null,
|
|
83
|
+
hasThinkingContent: thinkingBlocks.length > 0,
|
|
84
|
+
contentBlockTypes: blocks.map((block) => block?.type).filter(Boolean),
|
|
85
|
+
thinkingBlocks: thinkingBlocks.length ? thinkingBlocks : undefined,
|
|
86
|
+
usage: {
|
|
87
|
+
inputTokens: input,
|
|
88
|
+
outputTokens: Number(usage.output_tokens) || 0,
|
|
89
|
+
cachedTokens: cacheRead,
|
|
90
|
+
cacheWriteTokens: cacheWrite,
|
|
91
|
+
promptTokens: input + cacheRead + cacheWrite,
|
|
92
|
+
raw: usage,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
58
97
|
export function sanitizeAnthropicInputSchema(schema, toolName, logTag) {
|
|
59
98
|
if (!schema || typeof schema !== 'object') {
|
|
60
99
|
return { type: 'object', properties: {} };
|
|
@@ -765,6 +765,10 @@ function handleCompatResponsesStreamEvent(event, state, { label, parseResponsesT
|
|
|
765
765
|
const msg = event.response?.error?.message || event.error?.message || event.message || 'response.failed';
|
|
766
766
|
const err = new Error(`xAI Responses stream response.failed: ${msg}`);
|
|
767
767
|
populateHttpStatusFromMessage(err, msg);
|
|
768
|
+
// xAI's reference sampler treats protocol-level failed/error
|
|
769
|
+
// events as synthetic HTTP 500s. Gate by the xAI stream label so
|
|
770
|
+
// shared compat consumers retain their existing classification.
|
|
771
|
+
if (String(label || '').toLowerCase().startsWith('xai')) err.httpStatus = 500;
|
|
768
772
|
throw err;
|
|
769
773
|
}
|
|
770
774
|
case 'response.incomplete': {
|
|
@@ -788,6 +792,7 @@ function handleCompatResponsesStreamEvent(event, state, { label, parseResponsesT
|
|
|
788
792
|
const msg = event.message || event.error?.message || 'unknown';
|
|
789
793
|
const err = new Error(`xAI Responses stream error: ${msg}`);
|
|
790
794
|
populateHttpStatusFromMessage(err, msg);
|
|
795
|
+
if (String(label || '').toLowerCase().startsWith('xai')) err.httpStatus = 500;
|
|
791
796
|
throw err;
|
|
792
797
|
}
|
|
793
798
|
default:
|
|
@@ -329,32 +329,61 @@ export function toXaiResponsesInput(messages, providerState, options = {}) {
|
|
|
329
329
|
let startIndex = 0;
|
|
330
330
|
let resetReason = null;
|
|
331
331
|
let previousResponseId = typeof state?.previousResponseId === 'string' ? state.previousResponseId : null;
|
|
332
|
+
let statelessContinuation = state?.store === false;
|
|
332
333
|
const expectedModel = options.model ? String(options.model) : '';
|
|
333
334
|
const stateModel = state?.model ? String(state.model) : '';
|
|
334
335
|
const seen = Number.isInteger(state?.seenMessageCount) ? state.seenMessageCount : null;
|
|
335
|
-
if (previousResponseId && expectedModel && stateModel && stateModel !== expectedModel) {
|
|
336
|
+
if ((previousResponseId || statelessContinuation) && expectedModel && stateModel && stateModel !== expectedModel) {
|
|
336
337
|
previousResponseId = null;
|
|
338
|
+
statelessContinuation = false;
|
|
337
339
|
resetReason = 'model_changed';
|
|
338
340
|
}
|
|
339
|
-
if (previousResponseId && (seen == null || seen < 0 || seen > messages.length)) {
|
|
341
|
+
if ((previousResponseId || statelessContinuation) && (seen == null || seen < 0 || seen > messages.length)) {
|
|
340
342
|
previousResponseId = null;
|
|
343
|
+
statelessContinuation = false;
|
|
341
344
|
resetReason = seen == null ? 'missing_seen_message_count' : 'seen_message_count_out_of_range';
|
|
342
345
|
}
|
|
343
346
|
if (previousResponseId) {
|
|
344
347
|
startIndex = Math.max(0, Math.min(seen, messages.length));
|
|
345
348
|
if (messages[startIndex]?.role === 'assistant') startIndex += 1;
|
|
349
|
+
} else if (statelessContinuation) {
|
|
350
|
+
// store=false responses cannot be looked up by id. The reference
|
|
351
|
+
// ConversationRequest sends the complete retained conversation on
|
|
352
|
+
// every turn, so replay from the beginning rather than treating the
|
|
353
|
+
// encrypted reasoning item like stored server-side continuation.
|
|
354
|
+
startIndex = 0;
|
|
346
355
|
}
|
|
347
|
-
const dropToolHistory = !previousResponseId
|
|
356
|
+
const dropToolHistory = !previousResponseId
|
|
357
|
+
&& !statelessContinuation
|
|
358
|
+
&& (resetReason !== null || !state?.previousResponseId);
|
|
348
359
|
const input = [];
|
|
360
|
+
const reasoningByMessageIndex = new Map();
|
|
361
|
+
if (statelessContinuation) {
|
|
362
|
+
const history = Array.isArray(state?.encryptedReasoningHistory)
|
|
363
|
+
? state.encryptedReasoningHistory
|
|
364
|
+
: (Array.isArray(state?.encryptedReasoningItems)
|
|
365
|
+
? [{ messageIndex: seen, items: state.encryptedReasoningItems }]
|
|
366
|
+
: []);
|
|
367
|
+
for (const entry of history) {
|
|
368
|
+
const index = Number(entry?.messageIndex);
|
|
369
|
+
if (!Number.isInteger(index) || index < 0 || !Array.isArray(entry?.items)) continue;
|
|
370
|
+
const bucket = reasoningByMessageIndex.get(index) || [];
|
|
371
|
+
bucket.push(...entry.items.map((item) => ({ ...item })));
|
|
372
|
+
reasoningByMessageIndex.set(index, bucket);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
349
375
|
const pendingToolMedia = [];
|
|
350
376
|
const customToolCallNameById = new Map();
|
|
351
377
|
const flushToolMedia = () => {
|
|
352
378
|
if (!pendingToolMedia.length) return;
|
|
353
379
|
input.push({ role: 'user', content: pendingToolMedia.splice(0) });
|
|
354
380
|
};
|
|
355
|
-
for (
|
|
381
|
+
for (let messageIndex = startIndex; messageIndex < messages.length; messageIndex += 1) {
|
|
382
|
+
const m = messages[messageIndex];
|
|
356
383
|
if (!includeSystem && m.role === 'system') continue;
|
|
357
384
|
if (m.role !== 'tool') flushToolMedia();
|
|
385
|
+
const reasoningItems = reasoningByMessageIndex.get(messageIndex);
|
|
386
|
+
if (reasoningItems) input.push(...reasoningItems);
|
|
358
387
|
if (dropToolHistory && m.role === 'tool') continue;
|
|
359
388
|
if (dropToolHistory && m.role === 'assistant' && Array.isArray(m.toolCalls) && m.toolCalls.length > 0) {
|
|
360
389
|
if (m.content) input.push({ role: 'assistant', content: normalizeContentForOpenAIResponses(m.content, { role: 'assistant' }) });
|
|
@@ -365,5 +394,7 @@ export function toXaiResponsesInput(messages, providerState, options = {}) {
|
|
|
365
394
|
else input.push(converted);
|
|
366
395
|
}
|
|
367
396
|
flushToolMedia();
|
|
397
|
+
const trailingReasoning = reasoningByMessageIndex.get(messages.length);
|
|
398
|
+
if (trailingReasoning) input.push(...trailingReasoning);
|
|
368
399
|
return { input, previousResponseId, startIndex, continuationResetReason: resetReason };
|
|
369
400
|
}
|
|
@@ -147,7 +147,9 @@ export function useXaiResponsesWebSocket(opts, config) {
|
|
|
147
147
|
?? config?.transport
|
|
148
148
|
?? process.env.MIXDOG_XAI_RESPONSES_TRANSPORT
|
|
149
149
|
?? process.env.MIXDOG_XAI_TRANSPORT;
|
|
150
|
-
|
|
150
|
+
// Reference xAI/Grok parity is HTTP/SSE. WebSocket remains available only
|
|
151
|
+
// when a caller explicitly selects a WS transport.
|
|
152
|
+
if (raw == null || raw === '') return false;
|
|
151
153
|
const transport = String(raw).trim().toLowerCase();
|
|
152
154
|
return !['0', 'false', 'off', 'http', 'https', 'responses-http', 'sdk'].includes(transport);
|
|
153
155
|
}
|
|
@@ -98,6 +98,13 @@ function includeCompletedXaiWarmup(result, warmup) {
|
|
|
98
98
|
} : usage,
|
|
99
99
|
};
|
|
100
100
|
}
|
|
101
|
+
|
|
102
|
+
function encryptedXaiReasoningItems(output) {
|
|
103
|
+
if (!Array.isArray(output)) return [];
|
|
104
|
+
return output
|
|
105
|
+
.filter((item) => item?.type === 'reasoning' && typeof item?.encrypted_content === 'string' && item.encrypted_content)
|
|
106
|
+
.map((item) => ({ ...item }));
|
|
107
|
+
}
|
|
101
108
|
export { OPENAI_COMPAT_PRESETS } from './openai-compat-presets.mjs';
|
|
102
109
|
const PRESETS = OPENAI_COMPAT_PRESETS;
|
|
103
110
|
const MODEL_LIST_TIMEOUT_MS = resolveTimeoutMs(
|
|
@@ -575,7 +582,12 @@ export class OpenAICompatProvider {
|
|
|
575
582
|
const params = {
|
|
576
583
|
model: useModel,
|
|
577
584
|
input,
|
|
578
|
-
|
|
585
|
+
// xAI's reference sampler is ZDR-safe by default and asks the
|
|
586
|
+
// service to return opaque reasoning state for continuation.
|
|
587
|
+
// This method is xAI-only; other compat providers retain their
|
|
588
|
+
// existing request bodies.
|
|
589
|
+
store: false,
|
|
590
|
+
include: ['reasoning.encrypted_content'],
|
|
579
591
|
prompt_cache_key: cacheRouting.key,
|
|
580
592
|
};
|
|
581
593
|
if (previousResponseId) params.previous_response_id = previousResponseId;
|
|
@@ -709,6 +721,15 @@ export class OpenAICompatProvider {
|
|
|
709
721
|
// provider requirement found — chain_continuous trace will surface
|
|
710
722
|
// any rejection as a provider-side drop.)
|
|
711
723
|
const nextPreviousResponseId = response.id;
|
|
724
|
+
const encryptedReasoningItems = encryptedXaiReasoningItems(response.output);
|
|
725
|
+
const encryptedReasoningHistory = [
|
|
726
|
+
...(Array.isArray(opts.providerState?.xaiResponses?.encryptedReasoningHistory)
|
|
727
|
+
? opts.providerState.xaiResponses.encryptedReasoningHistory
|
|
728
|
+
: []),
|
|
729
|
+
...(encryptedReasoningItems.length
|
|
730
|
+
? [{ messageIndex: Array.isArray(messages) ? messages.length : 0, items: encryptedReasoningItems }]
|
|
731
|
+
: []),
|
|
732
|
+
];
|
|
712
733
|
const searchSources = collectCompatResponseSearchSources(response);
|
|
713
734
|
return {
|
|
714
735
|
content: streamed.content,
|
|
@@ -724,9 +745,17 @@ export class OpenAICompatProvider {
|
|
|
724
745
|
providerState: {
|
|
725
746
|
...(opts.providerState || {}),
|
|
726
747
|
xaiResponses: {
|
|
727
|
-
previousResponseId:
|
|
748
|
+
previousResponseId: null,
|
|
749
|
+
responseId: nextPreviousResponseId,
|
|
750
|
+
store: false,
|
|
751
|
+
encryptedReasoningItems,
|
|
752
|
+
encryptedReasoningHistory,
|
|
728
753
|
seenMessageCount: Array.isArray(messages) ? messages.length : 0,
|
|
729
|
-
|
|
754
|
+
// The proxy may return a deployment alias (for example
|
|
755
|
+
// grok-4.5-build) for a requested public model id. Chain
|
|
756
|
+
// compatibility is keyed to the requested id; storing the
|
|
757
|
+
// alias here would falsely reset the next tool-result turn.
|
|
758
|
+
model: useModel,
|
|
730
759
|
updatedAt: Date.now(),
|
|
731
760
|
},
|
|
732
761
|
},
|
|
@@ -765,10 +794,8 @@ export class OpenAICompatProvider {
|
|
|
765
794
|
const params = {
|
|
766
795
|
model: useModel,
|
|
767
796
|
input,
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
// in our live probes unless the chain is stored.
|
|
771
|
-
store: true,
|
|
797
|
+
store: false,
|
|
798
|
+
include: ['reasoning.encrypted_content'],
|
|
772
799
|
prompt_cache_key: cacheRouting.key,
|
|
773
800
|
};
|
|
774
801
|
const instructions = xaiSystemInstructions(messages);
|
|
@@ -846,6 +873,15 @@ export class OpenAICompatProvider {
|
|
|
846
873
|
const responseId = result.responseId || previousResponseId || null;
|
|
847
874
|
// Same rationale as the HTTP path above: `length` stop keeps the chain.
|
|
848
875
|
const nextPreviousResponseId = responseId;
|
|
876
|
+
const encryptedReasoningItems = encryptedXaiReasoningItems(result.responseItems);
|
|
877
|
+
const encryptedReasoningHistory = [
|
|
878
|
+
...(Array.isArray(opts.providerState?.xaiResponses?.encryptedReasoningHistory)
|
|
879
|
+
? opts.providerState.xaiResponses.encryptedReasoningHistory
|
|
880
|
+
: []),
|
|
881
|
+
...(encryptedReasoningItems.length
|
|
882
|
+
? [{ messageIndex: Array.isArray(messages) ? messages.length : 0, items: encryptedReasoningItems }]
|
|
883
|
+
: []),
|
|
884
|
+
];
|
|
849
885
|
const rawUsage = result.usage?.raw || result.usage || null;
|
|
850
886
|
const traceParams = result.__warmup?.requestBody || params;
|
|
851
887
|
writeXaiResponsesCacheTrace({
|
|
@@ -899,9 +935,13 @@ export class OpenAICompatProvider {
|
|
|
899
935
|
providerState: {
|
|
900
936
|
...(opts.providerState || {}),
|
|
901
937
|
xaiResponses: {
|
|
902
|
-
previousResponseId:
|
|
938
|
+
previousResponseId: null,
|
|
939
|
+
responseId: nextPreviousResponseId,
|
|
940
|
+
store: false,
|
|
941
|
+
encryptedReasoningItems,
|
|
942
|
+
encryptedReasoningHistory,
|
|
903
943
|
seenMessageCount: Array.isArray(messages) ? messages.length : 0,
|
|
904
|
-
model:
|
|
944
|
+
model: useModel,
|
|
905
945
|
updatedAt: Date.now(),
|
|
906
946
|
transport: 'websocket',
|
|
907
947
|
},
|
|
@@ -162,8 +162,10 @@ function _buildOpenAIHttpFallbackHeaders({ auth, cacheKey }) {
|
|
|
162
162
|
|
|
163
163
|
// WS→HTTP/SSE fallback predicate → shared shouldFallbackTransport
|
|
164
164
|
// (retry-classifier.mjs). The per-provider env flag is computed here and passed
|
|
165
|
-
// as `enabled`;
|
|
165
|
+
// as `enabled`; OAuth rate limits are terminal and must never trigger a
|
|
166
|
+
// transport fallback, even if a caller has marked a prior WS attempt exhausted.
|
|
166
167
|
export function _shouldUseOpenAIHttpFallback(err, externalSignal) {
|
|
168
|
+
if (Number(err?.httpStatus || 0) === 429) return false;
|
|
167
169
|
return shouldFallbackTransport(err, {
|
|
168
170
|
signal: externalSignal,
|
|
169
171
|
enabled: _envFlag('MIXDOG_OPENAI_OAUTH_HTTP_FALLBACK', true),
|
|
@@ -220,7 +222,17 @@ export async function sendViaHttpSse({
|
|
|
220
222
|
);
|
|
221
223
|
let requestError = null;
|
|
222
224
|
try {
|
|
223
|
-
|
|
225
|
+
// Keep the established stage name for consumers, while making each
|
|
226
|
+
// bounded retry an observable liveness heartbeat. Session runtime
|
|
227
|
+
// liveness updates lastProgressAt for every stage callback,
|
|
228
|
+
// including a repeated `requesting` stage.
|
|
229
|
+
try {
|
|
230
|
+
onStageChange?.('requesting', {
|
|
231
|
+
attempt: attempt + 1,
|
|
232
|
+
maxAttempts: CODEX_REQUEST_MAX_RETRIES + 1,
|
|
233
|
+
retry: attempt > 0,
|
|
234
|
+
});
|
|
235
|
+
} catch {}
|
|
224
236
|
response = await fetchFn(responsesUrl, {
|
|
225
237
|
method: 'POST',
|
|
226
238
|
headers,
|