mixdog 0.9.52 → 0.9.53
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 +1 -1
- package/scripts/bench-run.mjs +2 -2
- package/scripts/compact-pressure-test.mjs +104 -0
- package/scripts/desktop-session-bridge-test.mjs +704 -0
- package/scripts/freevar-smoke.mjs +7 -4
- package/scripts/lifecycle-api-test.mjs +65 -4
- package/scripts/max-output-recovery-test.mjs +31 -0
- package/scripts/memory-core-input-test.mjs +10 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +63 -3
- package/scripts/openai-ws-early-settle-test.mjs +40 -0
- package/scripts/parent-abort-link-test.mjs +24 -0
- package/scripts/process-lifecycle-test.mjs +80 -22
- package/scripts/provider-contract-test.mjs +257 -0
- package/scripts/provider-toolcall-test.mjs +172 -10
- package/scripts/session-orphan-sweep-test.mjs +27 -1
- package/src/lib/keychain-cjs.cjs +36 -23
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +1 -13
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +7 -8
- package/src/runtime/agent/orchestrator/agent-trace.mjs +33 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +7 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +14 -300
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +2 -4
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +18 -266
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +18 -1
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +15 -130
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +5 -115
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +224 -0
- package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +6 -0
- package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +119 -0
- package/src/runtime/agent/orchestrator/providers/lib/grok-tool-schema.mjs +109 -0
- package/src/runtime/agent/orchestrator/providers/lib/openai-tool-args.mjs +70 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +14 -71
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +47 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +1 -7
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +72 -77
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -20
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +10 -10
- package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +30 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +37 -28
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +5 -7
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +170 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +20 -223
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +11 -17
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +12 -5
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +21 -5
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +3 -58
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +24 -0
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +57 -16
- package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +7 -1
- package/src/runtime/agent/orchestrator/session/store/paths-heartbeat.mjs +52 -0
- package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +62 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +305 -127
- package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +46 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/search-grep-chunks.mjs +173 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/search-input-helpers.mjs +117 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-insights.mjs +199 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-spawn-helpers.mjs +107 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +1 -40
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +19 -277
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +22 -298
- package/src/runtime/agent/orchestrator/tools/lib/shell-spawn-retry.mjs +67 -0
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -71
- package/src/runtime/channels/backends/discord-gateway.mjs +1 -1
- package/src/runtime/channels/backends/discord.mjs +6 -6
- package/src/runtime/channels/lib/config.mjs +15 -2
- package/src/runtime/channels/lib/memory-client.mjs +20 -3
- package/src/runtime/channels/lib/owned-runtime.mjs +19 -12
- package/src/runtime/channels/lib/status-snapshot.mjs +9 -0
- package/src/runtime/channels/lib/tool-dispatch.mjs +9 -5
- package/src/runtime/channels/lib/worker-main.mjs +16 -5
- package/src/runtime/memory/index.mjs +24 -198
- package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -53
- package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +115 -0
- package/src/runtime/memory/lib/memory-port-advertiser.mjs +105 -0
- package/src/runtime/memory/lib/pg/supervisor.mjs +0 -4
- package/src/runtime/memory/lib/query-handlers.mjs +2 -122
- package/src/runtime/memory/lib/query-maintenance-handlers.mjs +126 -0
- package/src/runtime/memory/lib/tool-call-handler.mjs +57 -0
- package/src/runtime/search/lib/http-fetch.mjs +1 -1
- package/src/runtime/shared/config.mjs +58 -13
- package/src/runtime/shared/llm/cost.mjs +14 -4
- package/src/runtime/shared/memory-snapshot.mjs +236 -0
- package/src/runtime/shared/process-lifecycle.mjs +92 -19
- package/src/runtime/shared/process-shutdown.mjs +6 -0
- package/src/runtime/shared/resource-admission.mjs +7 -2
- package/src/session-runtime/channel-config-api.mjs +7 -7
- package/src/session-runtime/config-lifecycle.mjs +20 -17
- package/src/session-runtime/cwd-plugins.mjs +9 -7
- package/src/session-runtime/env.mjs +1 -2
- package/src/session-runtime/hitch-profile.mjs +45 -0
- package/src/session-runtime/lifecycle-api.mjs +36 -9
- package/src/session-runtime/mcp-glue.mjs +6 -11
- package/src/session-runtime/provider-init-key.mjs +17 -0
- package/src/session-runtime/runtime-core.mjs +44 -103
- package/src/session-runtime/runtime-paths.mjs +20 -0
- package/src/session-runtime/runtime-tool-routing.mjs +55 -0
- package/src/session-runtime/tool-catalog-data.mjs +51 -0
- package/src/session-runtime/tool-catalog.mjs +15 -89
- package/src/standalone/agent-tool/spawn-preset.mjs +73 -0
- package/src/standalone/agent-tool/worker-rows.mjs +93 -0
- package/src/standalone/agent-tool.mjs +12 -162
- package/src/standalone/channel-admin.mjs +29 -0
- package/src/tui/App.jsx +11 -5
- package/src/tui/dist/index.mjs +202 -65
- package/src/tui/engine/session-api-ext.mjs +37 -4
- package/src/tui/index.jsx +1 -0
- package/src/tui/lib/voice-setup.mjs +5 -5
- package/scripts/_devtools-stub.mjs +0 -1
- package/src/runtime/lib/keychain-cjs.cjs +0 -288
|
@@ -11,6 +11,20 @@ import { customToolCallFromResponseItem } from './custom-tool-wire.mjs';
|
|
|
11
11
|
import { createLeakGuard, createToolCallDedupe, dedupeToolCallList } from './anthropic-leaked-toolcall.mjs';
|
|
12
12
|
import { randomBytes } from 'crypto';
|
|
13
13
|
import { createActiveToolItemTracker } from './tool-stream-state.mjs';
|
|
14
|
+
import {
|
|
15
|
+
truncatedCompatStreamError,
|
|
16
|
+
makeInvalidToolArgsMarker,
|
|
17
|
+
isInvalidToolArgsMarker,
|
|
18
|
+
formatInvalidToolArgsResult,
|
|
19
|
+
parseCompletedToolCallArgumentsJson,
|
|
20
|
+
} from './lib/openai-tool-args.mjs';
|
|
21
|
+
|
|
22
|
+
export {
|
|
23
|
+
makeInvalidToolArgsMarker,
|
|
24
|
+
isInvalidToolArgsMarker,
|
|
25
|
+
formatInvalidToolArgsResult,
|
|
26
|
+
parseCompletedToolCallArgumentsJson,
|
|
27
|
+
};
|
|
14
28
|
|
|
15
29
|
// Synthesize a native-shaped OpenAI tool call from a recovered leaked call.
|
|
16
30
|
// Matches the `call_...` id scheme the native Responses/Chat paths use so the
|
|
@@ -25,77 +39,6 @@ function synthLeakedOpenAICall(recovered) {
|
|
|
25
39
|
};
|
|
26
40
|
}
|
|
27
41
|
|
|
28
|
-
function truncatedCompatStreamError(label, detail) {
|
|
29
|
-
return Object.assign(
|
|
30
|
-
new Error(`${label} SSE stream truncated${detail ? `: ${detail}` : ''}`),
|
|
31
|
-
{ name: 'TruncatedStreamError', code: 'TRUNCATED_STREAM', truncatedStream: true },
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Invalid-tool-args marker: completed-but-malformed tool_call arguments JSON must NOT throw
|
|
36
|
-
// (kills the turn) NOR be silently swallowed to `{}`. Instead the parse
|
|
37
|
-
// failure is carried as data on the tool call's `arguments` slot so the
|
|
38
|
-
// dispatch loop can turn it into an is_error tool_result and let the model
|
|
39
|
-
// re-issue the call with valid JSON in the SAME turn (follow-up retry).
|
|
40
|
-
// { __invalidToolArgs: true, __rawArguments: <raw string>, __parseError: <msg> }
|
|
41
|
-
export function makeInvalidToolArgsMarker(rawArguments, parseError) {
|
|
42
|
-
return {
|
|
43
|
-
__invalidToolArgs: true,
|
|
44
|
-
__rawArguments: typeof rawArguments === 'string' ? rawArguments : String(rawArguments ?? ''),
|
|
45
|
-
__parseError: typeof parseError === 'string' ? parseError : String(parseError ?? 'parse error'),
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
export function isInvalidToolArgsMarker(value) {
|
|
49
|
-
return !!value && typeof value === 'object' && value.__invalidToolArgs === true;
|
|
50
|
-
}
|
|
51
|
-
/** Model-facing tool_result text for a tool call whose arguments failed to
|
|
52
|
-
* parse; instructs the model to retry with valid JSON in the same turn. */
|
|
53
|
-
export function formatInvalidToolArgsResult(call) {
|
|
54
|
-
const name = call?.name || 'tool';
|
|
55
|
-
const detail = call?.arguments?.__parseError || 'arguments were not valid JSON';
|
|
56
|
-
return `The arguments provided to \`${name}\` are invalid JSON and could not be parsed: ${detail}. Re-issue this tool call with valid JSON arguments.`;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Completed tool_call.arguments must be valid JSON; empty/missing → {}.
|
|
60
|
-
* @param {any} raw - raw arguments value (string or object)
|
|
61
|
-
* @param {string} label - provider label for error messages
|
|
62
|
-
* @param {{id?:string,name?:string,index?:number,finishReason?:string}} [meta] - optional tool-call identity for diagnostics.
|
|
63
|
-
* When `meta.finishReason` is set, a completion/finish signal was observed for
|
|
64
|
-
* the call: a JSON.parse failure is then deterministic bad JSON (permanent),
|
|
65
|
-
* not a mid-stream truncation (retryable). */
|
|
66
|
-
export function parseCompletedToolCallArgumentsJson(raw, label, meta) {
|
|
67
|
-
const text = typeof raw === 'string' ? raw : (raw == null ? '' : String(raw));
|
|
68
|
-
const src = text === '' ? '{}' : text;
|
|
69
|
-
try {
|
|
70
|
-
return JSON.parse(src);
|
|
71
|
-
} catch (err) {
|
|
72
|
-
const preview = text.length <= 64
|
|
73
|
-
? text
|
|
74
|
-
: text.slice(0, 32) + '...' + text.slice(-32);
|
|
75
|
-
const detailParts = [`invalid tool_call arguments JSON: len=${text.length} preview=${JSON.stringify(preview)}`];
|
|
76
|
-
if (meta) {
|
|
77
|
-
const m = {};
|
|
78
|
-
if (meta.id) m.id = meta.id;
|
|
79
|
-
if (meta.name) m.name = meta.name;
|
|
80
|
-
if (meta.index != null) m.index = meta.index;
|
|
81
|
-
if (meta.finishReason) m.finishReason = meta.finishReason;
|
|
82
|
-
detailParts.push(`tool=${JSON.stringify(m)}`);
|
|
83
|
-
}
|
|
84
|
-
// Invariant: a completion/finish signal was observed for this tool call
|
|
85
|
-
// (finish_reason present, or a per-call/response "done" event fired), so
|
|
86
|
-
// the arguments are NOT mid-stream-truncated — they are complete but
|
|
87
|
-
// malformed. Return an invalid-args MARKER (not a throw) so the
|
|
88
|
-
// dispatch loop feeds the parse error back to the model as a
|
|
89
|
-
// tool_result and the model self-corrects in the same turn. Only an
|
|
90
|
-
// unfinished stream (no finishReason) stays the retryable truncation
|
|
91
|
-
// case — that transient behavior is deliberately preserved.
|
|
92
|
-
if (meta?.finishReason) {
|
|
93
|
-
return makeInvalidToolArgsMarker(text, err instanceof Error ? err.message : String(err));
|
|
94
|
-
}
|
|
95
|
-
throw truncatedCompatStreamError(label, detailParts.join(' '));
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
42
|
function firstByteCompatStreamError(label) {
|
|
100
43
|
const err = providerTimeoutError(`${label} first byte`, PROVIDER_FIRST_BYTE_TIMEOUT_MS);
|
|
101
44
|
err.firstByteTimeout = true;
|
|
@@ -3,6 +3,7 @@ import { getAgentApiKey } from '../../../shared/provider-api-key.mjs';
|
|
|
3
3
|
import { withRetry } from './retry-classifier.mjs';
|
|
4
4
|
import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
|
|
5
5
|
import { sendViaWebSocket } from './openai-oauth-ws.mjs';
|
|
6
|
+
import { _combineUsageWithWarmup } from './openai-ws-events.mjs';
|
|
6
7
|
import {
|
|
7
8
|
consumeCompatChatCompletionStream,
|
|
8
9
|
consumeCompatResponsesStream,
|
|
@@ -67,6 +68,36 @@ function loadOpenAI() {
|
|
|
67
68
|
}
|
|
68
69
|
return _OpenAI;
|
|
69
70
|
}
|
|
71
|
+
|
|
72
|
+
function attachCompletedWarmup(err, warmup) {
|
|
73
|
+
if (!err || !warmup?.usage) return err;
|
|
74
|
+
try {
|
|
75
|
+
Object.defineProperty(err, '__warmup', {
|
|
76
|
+
value: warmup,
|
|
77
|
+
configurable: true,
|
|
78
|
+
enumerable: false,
|
|
79
|
+
});
|
|
80
|
+
} catch {}
|
|
81
|
+
return err;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function includeCompletedXaiWarmup(result, warmup) {
|
|
85
|
+
if (!result || !warmup?.usage) return result;
|
|
86
|
+
const usage = _combineUsageWithWarmup(result.usage, warmup.usage, {
|
|
87
|
+
separateMainContext: true,
|
|
88
|
+
});
|
|
89
|
+
const ticks = usage?.raw?.cost_in_usd_ticks;
|
|
90
|
+
const costUsd = typeof ticks === 'number' && ticks >= 0
|
|
91
|
+
? Number((ticks * 1e-10).toFixed(8))
|
|
92
|
+
: undefined;
|
|
93
|
+
return {
|
|
94
|
+
...result,
|
|
95
|
+
usage: usage ? {
|
|
96
|
+
...usage,
|
|
97
|
+
...(costUsd != null ? { costUsd } : {}),
|
|
98
|
+
} : usage,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
70
101
|
export { OPENAI_COMPAT_PRESETS } from './openai-compat-presets.mjs';
|
|
71
102
|
const PRESETS = OPENAI_COMPAT_PRESETS;
|
|
72
103
|
const MODEL_LIST_TIMEOUT_MS = resolveTimeoutMs(
|
|
@@ -257,7 +288,10 @@ export class OpenAICompatProvider {
|
|
|
257
288
|
}
|
|
258
289
|
process.stderr.write(`[provider] Auth error, re-reading provider authentication...\n`);
|
|
259
290
|
this.reloadApiKey();
|
|
260
|
-
|
|
291
|
+
const retryOpts = this.name === 'xai' && err?.__warmup?.usage
|
|
292
|
+
? { ...(sendOpts || {}), _carriedWarmup: err.__warmup }
|
|
293
|
+
: sendOpts;
|
|
294
|
+
return await this._doSend(messages, model, tools, retryOpts);
|
|
261
295
|
}
|
|
262
296
|
throw err;
|
|
263
297
|
}
|
|
@@ -274,6 +308,15 @@ export class OpenAICompatProvider {
|
|
|
274
308
|
this._preconnectFn(this.baseURL);
|
|
275
309
|
}
|
|
276
310
|
if (this.name === 'xai' && useXaiResponsesApi(opts, this.config)) {
|
|
311
|
+
const carriedWarmup = opts._carriedWarmup?.usage ? opts._carriedWarmup : null;
|
|
312
|
+
const sendHttpWithWarmup = async (warmup = carriedWarmup) => {
|
|
313
|
+
try {
|
|
314
|
+
const result = await this._doSendXaiResponses(messages, useModel, tools, opts);
|
|
315
|
+
return includeCompletedXaiWarmup(result, warmup);
|
|
316
|
+
} catch (err) {
|
|
317
|
+
throw attachCompletedWarmup(err, warmup);
|
|
318
|
+
}
|
|
319
|
+
};
|
|
277
320
|
// Shared Responses transport switch (MIXDOG_OAI_TRANSPORT), capability-
|
|
278
321
|
// gated for xAI/Grok. Provider-local HTTP pins still win: Grok
|
|
279
322
|
// proxy-only models set responsesTransport:'http' because the WS
|
|
@@ -315,12 +358,12 @@ export class OpenAICompatProvider {
|
|
|
315
358
|
},
|
|
316
359
|
});
|
|
317
360
|
} catch {}
|
|
318
|
-
return await
|
|
361
|
+
return await sendHttpWithWarmup(err?.__warmup || carriedWarmup);
|
|
319
362
|
}
|
|
320
363
|
throw err;
|
|
321
364
|
}
|
|
322
365
|
}
|
|
323
|
-
return await
|
|
366
|
+
return await sendHttpWithWarmup();
|
|
324
367
|
}
|
|
325
368
|
const signal = opts.signal || null;
|
|
326
369
|
if (signal?.aborted) {
|
|
@@ -788,6 +831,7 @@ export class OpenAICompatProvider {
|
|
|
788
831
|
traceProvider: 'xai',
|
|
789
832
|
logSuppressedReasoningDeltas: false,
|
|
790
833
|
warmupBody,
|
|
834
|
+
_carriedWarmup: opts._carriedWarmup || null,
|
|
791
835
|
// Mirror openai-oauth fast fallback: when the HTTP fallback is
|
|
792
836
|
// enabled (outer catch → _shouldFallbackXaiWsToHttp), a first
|
|
793
837
|
// acquire/first-byte failure should skip remaining WS
|
|
@@ -33,6 +33,7 @@ import { createLeakGuard, createToolCallDedupe, dedupeToolCallList } from './ant
|
|
|
33
33
|
import { customToolCallFromResponseItem } from './custom-tool-wire.mjs';
|
|
34
34
|
import { CODEX_OAUTH_ORIGINATOR, CODEX_RESPONSES_URL, _displayCodexModel } from './openai-oauth.mjs';
|
|
35
35
|
import { createActiveToolItemTracker } from './tool-stream-state.mjs';
|
|
36
|
+
export { envPositiveInt as _envPositiveInt } from './lib/env-utils.mjs';
|
|
36
37
|
|
|
37
38
|
// Public OpenAI Responses API endpoint for the api-key `openai` provider.
|
|
38
39
|
// The openai-direct WS transport hits the same origin (openai-ws-pool
|
|
@@ -50,13 +51,6 @@ export function _envFlag(name, fallback = true) {
|
|
|
50
51
|
return !['0', 'false', 'off', 'no'].includes(String(raw).toLowerCase());
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
export function _envPositiveInt(name, fallback) {
|
|
54
|
-
const raw = process.env[name];
|
|
55
|
-
if (raw == null || raw === '') return fallback;
|
|
56
|
-
const n = Number(raw);
|
|
57
|
-
return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
54
|
// Completed function_call.arguments parse for the OpenAI Responses stream.
|
|
61
55
|
// A function_call item arrives only on a completion/done signal, so a
|
|
62
56
|
// non-empty-but-malformed
|
|
@@ -63,6 +63,7 @@ import {
|
|
|
63
63
|
} from './openai-ws-stream.mjs';
|
|
64
64
|
import { _buildResponseCreateFrame } from './openai-ws-delta.mjs';
|
|
65
65
|
import { resolveOpenAiTransportPolicy } from './openai-transport-policy.mjs';
|
|
66
|
+
import { envPositiveInt } from './lib/env-utils.mjs';
|
|
66
67
|
|
|
67
68
|
// Legacy import paths for scripts/tool-smoke.mjs, mixdog-session-runtime.mjs
|
|
68
69
|
// (drainOpenaiWsPool), scripts/provider-toolcall-test.mjs (parseToolSearchArgs,
|
|
@@ -80,7 +81,6 @@ export {
|
|
|
80
81
|
|
|
81
82
|
globalThis.__mixdogOpenaiWsRuntimeLoaded = true;
|
|
82
83
|
|
|
83
|
-
// --- WS_PRE_RESPONSE_CREATED_MS / WS_INTER_CHUNK_MS: extracted to openai-ws-stream.mjs ---
|
|
84
84
|
// The official Codex Responses policy has one five-retry stream budget shared
|
|
85
85
|
// by connect/handshake and pre-output stream failures.
|
|
86
86
|
const MIDSTREAM_WS_TRANSIENT_RETRY_LIMIT = MIDSTREAM_RETRY_POLICY.ws.transientCloseRetries;
|
|
@@ -103,10 +103,6 @@ const HANDSHAKE_MAX_ATTEMPTS = MIDSTREAM_WS_TRANSIENT_RETRY_LIMIT + 1;
|
|
|
103
103
|
const HANDSHAKE_BACKOFF_BASE_MS = 200;
|
|
104
104
|
const HANDSHAKE_BACKOFF_CAP_MS = 3200;
|
|
105
105
|
|
|
106
|
-
// --- WS pool/handshake/acquire/release: extracted to openai-ws-pool.mjs ---
|
|
107
|
-
|
|
108
|
-
// --- Delta/matching helpers + parseToolSearchArgs: extracted to openai-ws-stream.mjs ---
|
|
109
|
-
// --- Stream consumer (_streamResponse): extracted to openai-ws-stream.mjs ---
|
|
110
106
|
export function _classifyHandshakeError(err) {
|
|
111
107
|
return classifyHandshakeError(err);
|
|
112
108
|
}
|
|
@@ -189,11 +185,6 @@ function _num(value, fallback = 0) {
|
|
|
189
185
|
return Number.isFinite(n) ? n : fallback;
|
|
190
186
|
}
|
|
191
187
|
|
|
192
|
-
function _envPositiveInt(name, fallback) {
|
|
193
|
-
const n = Number(process.env[name]);
|
|
194
|
-
return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
188
|
function _envRatio(name, fallback) {
|
|
198
189
|
const n = Number(process.env[name]);
|
|
199
190
|
return Number.isFinite(n) && n >= 0 && n <= 1 ? n : fallback;
|
|
@@ -409,8 +400,8 @@ function _cacheObservation({ entry, result, continuityResetReason = null }) {
|
|
|
409
400
|
const promptTokens = _num(result?.usage?.promptTokens, 0) || inputTokens;
|
|
410
401
|
const cachedTokens = _num(result?.usage?.cachedTokens, 0);
|
|
411
402
|
const previousMaxCached = _num(entry?.promptCacheMaxCachedTokens, 0);
|
|
412
|
-
const warmThreshold =
|
|
413
|
-
const promptThreshold =
|
|
403
|
+
const warmThreshold = envPositiveInt('MIXDOG_OAI_CACHE_MISS_WARM_TOKENS', 2048);
|
|
404
|
+
const promptThreshold = envPositiveInt('MIXDOG_OAI_CACHE_MISS_PROMPT_TOKENS', 4096);
|
|
414
405
|
const dropRatio = _envRatio('MIXDOG_OAI_CACHE_MISS_DROP_RATIO', 0.6);
|
|
415
406
|
const dropThreshold = Math.floor(previousMaxCached * dropRatio);
|
|
416
407
|
// A full-frame chain break (most commonly compaction/input-prefix rewrite)
|
|
@@ -456,7 +447,7 @@ function _requestInputExtends(previousInput, currentInput) {
|
|
|
456
447
|
);
|
|
457
448
|
}
|
|
458
449
|
|
|
459
|
-
function _cacheContinuityResetReason({ mode, deltaReason, entry, body }) {
|
|
450
|
+
function _cacheContinuityResetReason({ mode, deltaReason, entry, body, traceProvider }) {
|
|
460
451
|
if (mode === 'delta') return null;
|
|
461
452
|
if (deltaReason && !['no_anchor', 'full_forced', 'full_default'].includes(deltaReason)) {
|
|
462
453
|
return deltaReason;
|
|
@@ -465,7 +456,9 @@ function _cacheContinuityResetReason({ mode, deltaReason, entry, body }) {
|
|
|
465
456
|
// full_default. Re-run the two cheap snapshot checks so compaction or any
|
|
466
457
|
// other prompt rewrite still retires the old prompt's cache high-water.
|
|
467
458
|
if (deltaReason !== 'full_default' || !entry?.lastResponseId) return null;
|
|
468
|
-
const currentSansInput = _stableStringify(_sansInput(body
|
|
459
|
+
const currentSansInput = _stableStringify(_sansInput(body, {
|
|
460
|
+
normalizeWarmupGenerate: traceProvider === 'openai-oauth',
|
|
461
|
+
}));
|
|
469
462
|
if (entry.lastRequestSansInput && currentSansInput !== entry.lastRequestSansInput) {
|
|
470
463
|
return 'request_properties_changed';
|
|
471
464
|
}
|
|
@@ -652,6 +645,7 @@ export async function sendViaWebSocket({
|
|
|
652
645
|
_sleepFn = _defaultSleep,
|
|
653
646
|
_sendSpanTraceFn = appendAgentTrace,
|
|
654
647
|
_agentTraceFn = appendAgentTrace,
|
|
648
|
+
_carriedWarmup = null,
|
|
655
649
|
}) {
|
|
656
650
|
// One bounded Codex stream retry budget covers transient handshake and
|
|
657
651
|
// pre-output stream failures. Every retry acquires a fresh connection.
|
|
@@ -659,6 +653,21 @@ export async function sendViaWebSocket({
|
|
|
659
653
|
const MAX_MIDSTREAM_RETRIES = MIDSTREAM_WS_TRANSIENT_RETRY_LIMIT;
|
|
660
654
|
let firstAttemptError = null;
|
|
661
655
|
let firstAttemptClassifier = null;
|
|
656
|
+
// A generate:false prewarm is billable even if its main request later
|
|
657
|
+
// retries on a fresh socket or falls back to HTTP. Retain one completed
|
|
658
|
+
// result across the whole logical send and attach it to terminal errors.
|
|
659
|
+
let completedWarmup = _carriedWarmup?.usage ? _carriedWarmup : null;
|
|
660
|
+
const _stampWarmup = (err) => {
|
|
661
|
+
if (!err || !completedWarmup?.usage) return err;
|
|
662
|
+
try {
|
|
663
|
+
Object.defineProperty(err, '__warmup', {
|
|
664
|
+
value: completedWarmup,
|
|
665
|
+
configurable: true,
|
|
666
|
+
enumerable: false,
|
|
667
|
+
});
|
|
668
|
+
} catch {}
|
|
669
|
+
return err;
|
|
670
|
+
};
|
|
662
671
|
// Known tool names for the leaked-tool-call guard in _streamResponse.
|
|
663
672
|
// Derived from the exact request body so a recovered leaked call only
|
|
664
673
|
// synthesizes when it names a tool actually offered to this request.
|
|
@@ -785,6 +794,7 @@ export async function sendViaWebSocket({
|
|
|
785
794
|
onBackoffSlept: (ms) => { sendSpan.retryBackoffMs += ms; },
|
|
786
795
|
});
|
|
787
796
|
} catch (err) {
|
|
797
|
+
_stampWarmup(err);
|
|
788
798
|
sendSpan.poolAcquireMs += performance.now() - handshakeStart;
|
|
789
799
|
// Provenance only; policy remains provider-owned below. This lets
|
|
790
800
|
// the direct wrapper distinguish an upgrade rejection from an
|
|
@@ -850,7 +860,7 @@ export async function sendViaWebSocket({
|
|
|
850
860
|
} catch (sleepErr) {
|
|
851
861
|
sendSpan.retryBackoffMs += performance.now() - sleepStart;
|
|
852
862
|
emitSendSpan('error');
|
|
853
|
-
throw sleepErr;
|
|
863
|
+
throw _stampWarmup(sleepErr);
|
|
854
864
|
}
|
|
855
865
|
sendSpan.retryBackoffMs += performance.now() - sleepStart;
|
|
856
866
|
continue;
|
|
@@ -951,28 +961,24 @@ export async function sendViaWebSocket({
|
|
|
951
961
|
// codex prewarm gate (client.rs:1686-1688): only when the session
|
|
952
962
|
// has no prior request state. A reused pooled socket with a live
|
|
953
963
|
// chain must go straight to the real request.
|
|
954
|
-
if (warmupBody && typeof warmupBody === 'object' &&
|
|
964
|
+
if (warmupBody && typeof warmupBody === 'object' && !completedWarmup
|
|
965
|
+
&& attemptIndex === 0 && !entry.lastResponseId) {
|
|
955
966
|
const warmupBuildStart = performance.now();
|
|
956
|
-
// Codex
|
|
957
|
-
//
|
|
958
|
-
//
|
|
959
|
-
//
|
|
960
|
-
//
|
|
961
|
-
const
|
|
962
|
-
const
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
requestKind: 'prewarm',
|
|
972
|
-
codexRequestKind: 'prewarm',
|
|
973
|
-
},
|
|
974
|
-
}
|
|
975
|
-
: codexMetadataContext;
|
|
967
|
+
// Codex startup prewarm contains stable instructions/tools but
|
|
968
|
+
// no live user/transcript input. Enforce that at the transport
|
|
969
|
+
// boundary as well as in the OAuth caller so a future caller
|
|
970
|
+
// cannot accidentally duplicate the transcript. WS-only fields
|
|
971
|
+
// are omitted exactly as in Codex's prewarm request.
|
|
972
|
+
const parityWarmupBody = { ...warmupBody, input: [], generate: false };
|
|
973
|
+
const warmupFrame = _buildResponseCreateFrame(parityWarmupBody, { omitTransportFields: true });
|
|
974
|
+
const warmupMetadataContext = {
|
|
975
|
+
...codexMetadataContext,
|
|
976
|
+
sendOpts: {
|
|
977
|
+
...(codexMetadataContext?.sendOpts || {}),
|
|
978
|
+
requestKind: 'prewarm',
|
|
979
|
+
codexRequestKind: 'prewarm',
|
|
980
|
+
},
|
|
981
|
+
};
|
|
976
982
|
const wireWarmupFrame = _withCodexWsClientMetadata(warmupFrame, entry, useCodexWsClientMetadata, warmupMetadataContext);
|
|
977
983
|
wireFrameHadTurnState = !!wireWarmupFrame?.client_metadata?.['x-codex-turn-state'];
|
|
978
984
|
wireFrameMetadataTrace = _metadataTrace(wireWarmupFrame?.client_metadata);
|
|
@@ -1010,8 +1016,15 @@ export async function sendViaWebSocket({
|
|
|
1010
1016
|
if (!warmupResult?.responseId) {
|
|
1011
1017
|
throw new Error('Responses WS warmup completed without response id');
|
|
1012
1018
|
}
|
|
1019
|
+
completedWarmup = {
|
|
1020
|
+
requestBody: parityWarmupBody,
|
|
1021
|
+
responseId: warmupResult.responseId,
|
|
1022
|
+
usage: warmupResult.usage,
|
|
1023
|
+
};
|
|
1013
1024
|
entry.lastResponseId = warmupResult.responseId;
|
|
1014
|
-
entry.lastRequestSansInput = _stableStringify(_sansInput(parityWarmupBody
|
|
1025
|
+
entry.lastRequestSansInput = _stableStringify(_sansInput(parityWarmupBody, {
|
|
1026
|
+
normalizeWarmupGenerate: useCodexWsClientMetadata,
|
|
1027
|
+
}));
|
|
1015
1028
|
const warmupInputArr = Array.isArray(parityWarmupBody.input) ? parityWarmupBody.input : [];
|
|
1016
1029
|
entry.lastRequestInput = _cloneJson(warmupInputArr);
|
|
1017
1030
|
entry.lastResponseItems = _cloneJson(Array.isArray(warmupResult.responseItems) ? warmupResult.responseItems : []);
|
|
@@ -1039,38 +1052,15 @@ export async function sendViaWebSocket({
|
|
|
1039
1052
|
payload: warmupPayload,
|
|
1040
1053
|
});
|
|
1041
1054
|
} catch {}
|
|
1042
|
-
// Do NOT rewrite the main request after warmup (R23 finding).
|
|
1043
|
-
// The old prev_id+no-instructions rewrite made it=1's frame a
|
|
1044
|
-
// different shape from it=2+ full frames, so the first real
|
|
1045
|
-
// full-frame cache write only landed at it=2 and it=3 raced
|
|
1046
|
-
// its propagation (cached=0 early misses). Keeping it=1 as a
|
|
1047
|
-
// normal full frame makes it byte-identical to the warmup's
|
|
1048
|
-
// prefix (instant full hit on the cache warmup just wrote)
|
|
1049
|
-
// and keeps every subsequent frame one consistent shape.
|
|
1050
|
-
// Delta opt-in still chains via entry.lastResponseId above.
|
|
1051
1055
|
}
|
|
1052
1056
|
|
|
1053
|
-
//
|
|
1054
|
-
//
|
|
1055
|
-
//
|
|
1056
|
-
//
|
|
1057
|
-
//
|
|
1058
|
-
// real turn would generate from an empty frame. Keep the warmup state
|
|
1059
|
-
// for cache/trace, but compute the main frame as cold (full input +
|
|
1060
|
-
// instructions).
|
|
1061
|
-
const deltaEntry = warmupResult
|
|
1062
|
-
? {
|
|
1063
|
-
...entry,
|
|
1064
|
-
lastResponseId: null,
|
|
1065
|
-
lastRequestSansInput: null,
|
|
1066
|
-
lastRequestInput: null,
|
|
1067
|
-
lastResponseItems: null,
|
|
1068
|
-
lastInputLen: 0,
|
|
1069
|
-
lastInputPrefixHash: null,
|
|
1070
|
-
}
|
|
1071
|
-
: entry;
|
|
1057
|
+
// A completed generate:false prewarm is a valid continuation
|
|
1058
|
+
// anchor. Compute against its retained empty-input snapshot so the
|
|
1059
|
+
// first real request sends previous_response_id plus exactly the
|
|
1060
|
+
// real incremental input. _computeDelta still retreats to a full
|
|
1061
|
+
// frame on every missing anchor/property/prefix/output mismatch.
|
|
1072
1062
|
const requestBuildStart = performance.now();
|
|
1073
|
-
const delta = _computeDelta({ entry
|
|
1063
|
+
const delta = _computeDelta({ entry, body: requestBody, traceProvider });
|
|
1074
1064
|
({ mode, frame } = delta);
|
|
1075
1065
|
deltaReason = delta.reason || null;
|
|
1076
1066
|
strippedResponseItems = delta.strippedResponseItems || 0;
|
|
@@ -1138,6 +1128,7 @@ export async function sendViaWebSocket({
|
|
|
1138
1128
|
knownToolNames,
|
|
1139
1129
|
});
|
|
1140
1130
|
} catch (err) {
|
|
1131
|
+
_stampWarmup(err);
|
|
1141
1132
|
// Preserve failure provenance for the direct provider. This marker
|
|
1142
1133
|
// is observational for OAuth and does not change its classifier or
|
|
1143
1134
|
// retry budget.
|
|
@@ -1210,7 +1201,7 @@ export async function sendViaWebSocket({
|
|
|
1210
1201
|
} catch (sleepErr) {
|
|
1211
1202
|
sendSpan.retryBackoffMs += performance.now() - sleepStart;
|
|
1212
1203
|
emitSendSpan('error');
|
|
1213
|
-
throw sleepErr;
|
|
1204
|
+
throw _stampWarmup(sleepErr);
|
|
1214
1205
|
}
|
|
1215
1206
|
sendSpan.retryBackoffMs += performance.now() - sleepStart;
|
|
1216
1207
|
continue;
|
|
@@ -1298,10 +1289,13 @@ export async function sendViaWebSocket({
|
|
|
1298
1289
|
deltaReason,
|
|
1299
1290
|
entry,
|
|
1300
1291
|
body: requestBody,
|
|
1292
|
+
traceProvider,
|
|
1301
1293
|
});
|
|
1302
1294
|
if (result.responseId && keepResponseChain) {
|
|
1303
1295
|
entry.lastResponseId = result.responseId;
|
|
1304
|
-
entry.lastRequestSansInput = _stableStringify(_sansInput(requestBody
|
|
1296
|
+
entry.lastRequestSansInput = _stableStringify(_sansInput(requestBody, {
|
|
1297
|
+
normalizeWarmupGenerate: useCodexWsClientMetadata,
|
|
1298
|
+
}));
|
|
1305
1299
|
const inputArr = Array.isArray(requestBody.input) ? requestBody.input : [];
|
|
1306
1300
|
entry.lastRequestInput = _cloneJson(inputArr);
|
|
1307
1301
|
entry.lastResponseItems = _cloneJson(Array.isArray(result.responseItems) ? result.responseItems : []);
|
|
@@ -1329,8 +1323,13 @@ export async function sendViaWebSocket({
|
|
|
1329
1323
|
result,
|
|
1330
1324
|
continuityResetReason: cacheContinuityResetReason,
|
|
1331
1325
|
});
|
|
1332
|
-
if (
|
|
1333
|
-
result.usage = _combineUsageWithWarmup(result.usage,
|
|
1326
|
+
if (completedWarmup?.usage) {
|
|
1327
|
+
result.usage = _combineUsageWithWarmup(result.usage, completedWarmup.usage, {
|
|
1328
|
+
// xAI/Grok prewarm is billable just like Codex prewarm, but it
|
|
1329
|
+
// is not part of the real request's context footprint. Direct
|
|
1330
|
+
// OpenAI intentionally retains its existing usage shape.
|
|
1331
|
+
separateMainContext: useCodexWsClientMetadata || traceProvider === 'xai',
|
|
1332
|
+
});
|
|
1334
1333
|
}
|
|
1335
1334
|
|
|
1336
1335
|
const requestedServiceTier = body?.service_tier || null;
|
|
@@ -1539,14 +1538,10 @@ export async function sendViaWebSocket({
|
|
|
1539
1538
|
releaseWebSocket({ entry, poolKey, keep: keepSocket });
|
|
1540
1539
|
const { responseId: _ignored, responseItems: _responseItemsIgnored, closeSocket: _closeSocketIgnored, ...out } = result;
|
|
1541
1540
|
if (includeResponseId && result.responseId) out.responseId = result.responseId;
|
|
1542
|
-
if (
|
|
1541
|
+
if (completedWarmup) {
|
|
1543
1542
|
try {
|
|
1544
1543
|
Object.defineProperty(out, '__warmup', {
|
|
1545
|
-
value:
|
|
1546
|
-
requestBody,
|
|
1547
|
-
responseId: warmupResult.responseId,
|
|
1548
|
-
usage: warmupResult.usage,
|
|
1549
|
-
},
|
|
1544
|
+
value: completedWarmup,
|
|
1550
1545
|
enumerable: false,
|
|
1551
1546
|
});
|
|
1552
1547
|
} catch {}
|
|
@@ -1558,5 +1553,5 @@ export async function sendViaWebSocket({
|
|
|
1558
1553
|
return out;
|
|
1559
1554
|
}
|
|
1560
1555
|
// Unreachable — the loop either returns or throws above.
|
|
1561
|
-
throw _stampTool(_stampLiveText(firstAttemptError || new Error('sendViaWebSocket: unreachable')));
|
|
1556
|
+
throw _stampWarmup(_stampTool(_stampLiveText(firstAttemptError || new Error('sendViaWebSocket: unreachable'))));
|
|
1562
1557
|
}
|
|
@@ -18,6 +18,7 @@ import { boundProviderAuthPath } from '../../../shared/provider-auth-binding.mjs
|
|
|
18
18
|
import { makeModelCache } from './model-cache.mjs';
|
|
19
19
|
|
|
20
20
|
import { sendViaWebSocket } from './openai-oauth-ws.mjs';
|
|
21
|
+
import { _combineUsageWithWarmup } from './openai-ws-events.mjs';
|
|
21
22
|
import { resolveOpenAiTransportPolicy } from './openai-transport-policy.mjs';
|
|
22
23
|
import {
|
|
23
24
|
buildStableProviderPromptCacheKey,
|
|
@@ -72,7 +73,7 @@ import {
|
|
|
72
73
|
} from './openai-codex-model.mjs';
|
|
73
74
|
export { _displayCodexModel };
|
|
74
75
|
|
|
75
|
-
//
|
|
76
|
+
// Public test/integration entry retained alongside the transport module export.
|
|
76
77
|
export { sendViaHttpSse };
|
|
77
78
|
// --- Constants ---
|
|
78
79
|
const CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
|
|
@@ -351,10 +352,6 @@ async function refreshTokens(refreshToken) {
|
|
|
351
352
|
}
|
|
352
353
|
}
|
|
353
354
|
// --- Build Responses API request ---
|
|
354
|
-
function _cloneJson(value) {
|
|
355
|
-
try { return JSON.parse(JSON.stringify(value)); } catch { return value; }
|
|
356
|
-
}
|
|
357
|
-
|
|
358
355
|
function _contentTextParts(content, type = 'input_text') {
|
|
359
356
|
if (typeof content === 'string') return content ? [{ type, text: content }] : [];
|
|
360
357
|
if (!Array.isArray(content)) {
|
|
@@ -631,8 +628,6 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
|
|
|
631
628
|
return ordered;
|
|
632
629
|
}
|
|
633
630
|
|
|
634
|
-
// --- HTTP/SSE fallback transport: extracted to openai-oauth-http-sse.mjs ---
|
|
635
|
-
|
|
636
631
|
// --- Provider ---
|
|
637
632
|
export class OpenAIOAuthProvider {
|
|
638
633
|
// OpenAI input_tokens already INCLUDES cached_tokens (cached is a subset),
|
|
@@ -895,12 +890,17 @@ export class OpenAIOAuthProvider {
|
|
|
895
890
|
useModel,
|
|
896
891
|
fetchFn: opts._fetchFn,
|
|
897
892
|
});
|
|
893
|
+
if (originalErr?.__warmup?.usage) {
|
|
894
|
+
result.usage = _combineUsageWithWarmup(result.usage, originalErr.__warmup.usage, {
|
|
895
|
+
separateMainContext: true,
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
898
|
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
899
899
|
process.stderr.write(`[agent-trace] provider-send-end elapsed=${Date.now() - _t1}ms result=ok transport=http-fallback\n`);
|
|
900
900
|
}
|
|
901
901
|
return recordLiveModel(result);
|
|
902
902
|
};
|
|
903
|
-
const dispatchWs = (forceFresh = false) => sendWs({
|
|
903
|
+
const dispatchWs = (forceFresh = false, carriedWarmup = null) => sendWs({
|
|
904
904
|
auth,
|
|
905
905
|
body,
|
|
906
906
|
sendOpts: opts,
|
|
@@ -922,18 +922,14 @@ export class OpenAIOAuthProvider {
|
|
|
922
922
|
// the hot WS/cache path for temporary blips while still preventing
|
|
923
923
|
// TUI-level hangs. Sticky HTTP fallback is only armed after this
|
|
924
924
|
// bounded reconnect budget is exhausted.
|
|
925
|
-
//
|
|
926
|
-
//
|
|
927
|
-
//
|
|
928
|
-
//
|
|
929
|
-
// and waiting for completion before the first real turn, exactly
|
|
930
|
-
// like codex prewarm_websocket (client.rs:1673-1705). Cost is one
|
|
931
|
-
// small (~5k tok) generate:false request per fresh socket, far
|
|
932
|
-
// below the 5-12k uncached tokens each early miss burned.
|
|
933
|
-
// Mid-session partial drops are server-side and unaffected.
|
|
925
|
+
// Codex startup-prewarm parity: build from the stable request
|
|
926
|
+
// properties (instructions/tools/etc.) but never send the live
|
|
927
|
+
// transcript/user input. The completed generate:false response is
|
|
928
|
+
// retained by the WS transport and anchors the first real request.
|
|
934
929
|
warmupBody: _envFlag('MIXDOG_OPENAI_OAUTH_WS_WARMUP', true)
|
|
935
|
-
? { ...body, generate: false }
|
|
930
|
+
? { ...body, input: [], generate: false }
|
|
936
931
|
: null,
|
|
932
|
+
_carriedWarmup: carriedWarmup,
|
|
937
933
|
});
|
|
938
934
|
const mustSurfaceFallbackError = (fallbackErr) => externalSignal?.aborted
|
|
939
935
|
|| fallbackErr?.name === 'AbortError'
|
|
@@ -974,7 +970,7 @@ export class OpenAIOAuthProvider {
|
|
|
974
970
|
this._refreshFallbackUntil = 0;
|
|
975
971
|
auth = await this.ensureAuth({ forceRefresh: true, reason: String(status) });
|
|
976
972
|
try {
|
|
977
|
-
const result = await dispatchWs(true);
|
|
973
|
+
const result = await dispatchWs(true, err?.__warmup || null);
|
|
978
974
|
if (process.env.MIXDOG_DEBUG_AGENT) { process.stderr.write(`[agent-trace] provider-send-end elapsed=${Date.now() - _t1}ms result=ok\n`); }
|
|
979
975
|
return recordLiveModel(result);
|
|
980
976
|
} catch (retryErr) {
|
|
@@ -1121,7 +1117,6 @@ export class OpenAIOAuthProvider {
|
|
|
1121
1117
|
}
|
|
1122
1118
|
}
|
|
1123
1119
|
|
|
1124
|
-
// --- OAuth PKCE login flow: extracted to openai-oauth-login.mjs ---
|
|
1125
1120
|
const { beginOAuthLogin, loginOAuth } = createOpenAIOAuthLogin({
|
|
1126
1121
|
clientId: CLIENT_ID,
|
|
1127
1122
|
originator: CODEX_OAUTH_ORIGINATOR,
|
|
@@ -20,16 +20,14 @@ import { createHash } from 'node:crypto';
|
|
|
20
20
|
// If the cached request (sans input) matches the current one and the current
|
|
21
21
|
// input starts with the cached input, return only the tail. Otherwise return
|
|
22
22
|
// the full input (fresh turn).
|
|
23
|
-
export function _sansInput(body) {
|
|
23
|
+
export function _sansInput(body, { normalizeWarmupGenerate = false } = {}) {
|
|
24
24
|
const { input: _ignored, previous_response_id: _prevIgnored, generate, ...rest } = body;
|
|
25
|
-
//
|
|
26
|
-
// marker
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// wire body is untouched — frames are built from the raw body, not this.
|
|
32
|
-
if (generate !== false && generate !== undefined) rest.generate = generate;
|
|
25
|
+
// Only OpenAI OAuth/Codex startup prewarm treats generate:false as a
|
|
26
|
+
// transport marker. Shared xAI callers retain it as a real request
|
|
27
|
+
// property so their existing property guard still falls back to full.
|
|
28
|
+
if (!(normalizeWarmupGenerate && generate === false) && generate !== undefined) {
|
|
29
|
+
rest.generate = generate;
|
|
30
|
+
}
|
|
33
31
|
return rest;
|
|
34
32
|
}
|
|
35
33
|
|
|
@@ -348,7 +346,9 @@ export function _computeDelta({ entry, body, traceProvider }) {
|
|
|
348
346
|
if (!Array.isArray(entry.lastRequestInput)) {
|
|
349
347
|
return { mode: 'full', reason: 'no_input_snapshot', frame: buildFrame(body) };
|
|
350
348
|
}
|
|
351
|
-
const curSans = _stableStringify(_sansInput(body
|
|
349
|
+
const curSans = _stableStringify(_sansInput(body, {
|
|
350
|
+
normalizeWarmupGenerate: traceProvider === 'openai-oauth',
|
|
351
|
+
}));
|
|
352
352
|
if (curSans !== entry.lastRequestSansInput) {
|
|
353
353
|
return { mode: 'full', reason: 'request_properties_changed', frame: buildFrame(body) };
|
|
354
354
|
}
|