mixdog 0.7.8 → 0.7.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +5 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +40 -0
- package/README.md +198 -251
- package/bin/statusline-launcher.mjs +5 -1
- package/bin/statusline-lib.mjs +14 -6
- package/bin/statusline.mjs +14 -6
- package/hooks/lib/settings-loader.cjs +4 -3
- package/hooks/pre-tool-subagent.cjs +7 -2
- package/hooks/session-start.cjs +52 -24
- package/lib/mixdog-debug.cjs +163 -0
- package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
- package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
- package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
- package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
- package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
- package/package.json +1 -1
- package/scripts/builtin-utils-smoke.mjs +14 -8
- package/scripts/bump.mjs +80 -0
- package/scripts/doctor.mjs +8 -3
- package/scripts/mutation-io-smoke.mjs +17 -1
- package/scripts/openai-oauth-catalog-smoke.mjs +53 -0
- package/scripts/permission-eval-smoke.mjs +18 -1
- package/scripts/statusline-launcher-smoke.mjs +2 -2
- package/scripts/webhook-selfheal-smoke.mjs +1 -3
- package/server-main.mjs +57 -3
- package/setup/config-merge.mjs +0 -1
- package/setup/install.mjs +241 -51
- package/setup/mixdog-cli.mjs +30 -3
- package/setup/setup-server.mjs +21 -33
- package/setup/setup.html +46 -11
- package/setup/tui.mjs +35 -316
- package/src/agent/orchestrator/config.mjs +0 -1
- package/src/agent/orchestrator/providers/anthropic-oauth.mjs +2 -5
- package/src/agent/orchestrator/providers/anthropic.mjs +243 -86
- package/src/agent/orchestrator/providers/gemini.mjs +386 -31
- package/src/agent/orchestrator/providers/grok-oauth.mjs +2 -5
- package/src/agent/orchestrator/providers/model-catalog.mjs +146 -13
- package/src/agent/orchestrator/providers/openai-compat-stream.mjs +366 -0
- package/src/agent/orchestrator/providers/openai-compat.mjs +74 -30
- package/src/agent/orchestrator/providers/openai-oauth-ws.mjs +2 -1
- package/src/agent/orchestrator/providers/openai-oauth.mjs +66 -13
- package/src/agent/orchestrator/providers/openai-ws.mjs +23 -0
- package/src/agent/orchestrator/session/manager.mjs +18 -4
- package/src/agent/orchestrator/stall-policy.mjs +6 -0
- package/src/agent/orchestrator/tools/builtin/native-edit-runner.mjs +29 -8
- package/src/agent/orchestrator/tools/graph-manifest.json +11 -11
- package/src/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/channels/index.mjs +27 -8
- package/src/channels/lib/event-queue.mjs +24 -1
- package/src/channels/lib/hook-pipe-server.mjs +21 -8
- package/src/channels/lib/webhook.mjs +142 -20
- package/src/memory/lib/memory-cycle1.mjs +7 -3
- package/src/memory/lib/memory-recall-store.mjs +27 -10
- package/src/search/lib/backends/openai-oauth.mjs +6 -2
- package/src/search/lib/cache.mjs +55 -7
- package/src/shared/config.mjs +1 -1
- package/src/shared/llm/cost.mjs +2 -2
- package/src/shared/open-url.mjs +37 -0
- package/src/shared/seed.mjs +20 -3
- package/src/shared/user-data-guard.mjs +3 -1
- package/scripts/test-config-rmw-restore.mjs +0 -122
- package/setup/wizard.mjs +0 -696
|
@@ -3,10 +3,16 @@ import { createHash } from 'crypto';
|
|
|
3
3
|
import { loadConfig } from '../config.mjs';
|
|
4
4
|
import { withRetry } from './retry-classifier.mjs';
|
|
5
5
|
import { sendViaWebSocket } from './openai-oauth-ws.mjs';
|
|
6
|
+
import {
|
|
7
|
+
consumeCompatChatCompletionStream,
|
|
8
|
+
consumeCompatResponsesStream,
|
|
9
|
+
parseCompletedToolCallArgumentsJson,
|
|
10
|
+
} from './openai-compat-stream.mjs';
|
|
6
11
|
import { appendBridgeTrace, traceBridgeUsage } from '../bridge-trace.mjs';
|
|
7
12
|
import { resolveProviderCacheKey } from '../smart-bridge/cache-strategy.mjs';
|
|
8
13
|
import {
|
|
9
14
|
PROVIDER_FIRST_BYTE_TIMEOUT_MS,
|
|
15
|
+
PROVIDER_NONSTREAM_TOTAL_TIMEOUT_MS,
|
|
10
16
|
PROVIDER_GENERATE_TOTAL_TIMEOUT_MS,
|
|
11
17
|
createTimeoutSignal,
|
|
12
18
|
resolveTimeoutMs,
|
|
@@ -23,9 +29,16 @@ export const OPENAI_COMPAT_PRESETS = {
|
|
|
23
29
|
baseURL: 'https://api.x.ai/v1',
|
|
24
30
|
defaultModel: 'grok-4.3',
|
|
25
31
|
},
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
// OpenCode Go — low-cost coding-model subscription gateway. The Go
|
|
33
|
+
// gateway exposes a unified OpenAI-compatible /chat/completions surface
|
|
34
|
+
// that transparently fronts every Go model (GLM / Kimi / DeepSeek / MiMo
|
|
35
|
+
// and the anthropic-native MiniMax / Qwen), including tool-calling and
|
|
36
|
+
// server-side prefix caching (cached_tokens), so no separate Anthropic
|
|
37
|
+
// transport is needed. Auth is a single OPENCODE_API_KEY (Bearer).
|
|
38
|
+
// listModels() pulls the live roster from {baseURL}/models.
|
|
39
|
+
'opencode-go': {
|
|
40
|
+
baseURL: 'https://opencode.ai/zen/go/v1',
|
|
41
|
+
defaultModel: 'glm-5.2',
|
|
29
42
|
},
|
|
30
43
|
ollama: {
|
|
31
44
|
baseURL: 'http://localhost:11434/v1',
|
|
@@ -841,7 +854,7 @@ function toResponsesTools(tools) {
|
|
|
841
854
|
parameters: t.inputSchema,
|
|
842
855
|
}));
|
|
843
856
|
}
|
|
844
|
-
function parseToolCalls(choice) {
|
|
857
|
+
function parseToolCalls(choice, label) {
|
|
845
858
|
const calls = choice.message?.tool_calls;
|
|
846
859
|
if (!calls?.length)
|
|
847
860
|
return undefined;
|
|
@@ -850,7 +863,7 @@ function parseToolCalls(choice) {
|
|
|
850
863
|
.map((tc) => ({
|
|
851
864
|
id: tc.id,
|
|
852
865
|
name: tc.function.name,
|
|
853
|
-
arguments:
|
|
866
|
+
arguments: parseCompletedToolCallArgumentsJson(tc.function.arguments, label),
|
|
854
867
|
}));
|
|
855
868
|
}
|
|
856
869
|
function parseJsonObject(value) {
|
|
@@ -861,14 +874,14 @@ function parseJsonObject(value) {
|
|
|
861
874
|
return {};
|
|
862
875
|
}
|
|
863
876
|
}
|
|
864
|
-
function parseResponsesToolCalls(response) {
|
|
877
|
+
function parseResponsesToolCalls(response, label) {
|
|
865
878
|
const out = [];
|
|
866
879
|
for (const item of response?.output || []) {
|
|
867
880
|
if (item?.type !== 'function_call') continue;
|
|
868
881
|
out.push({
|
|
869
882
|
id: item.call_id || item.id,
|
|
870
883
|
name: item.name,
|
|
871
|
-
arguments:
|
|
884
|
+
arguments: parseCompletedToolCallArgumentsJson(item.arguments, label),
|
|
872
885
|
});
|
|
873
886
|
}
|
|
874
887
|
return out.length ? out : undefined;
|
|
@@ -1025,7 +1038,7 @@ export class OpenAICompatProvider {
|
|
|
1025
1038
|
?? process.env.MIXDOG_XAI_REASONING_EFFORT);
|
|
1026
1039
|
if (reasoningEffort) params.reasoning_effort = reasoningEffort;
|
|
1027
1040
|
}
|
|
1028
|
-
const totalSignal = createTimeoutSignal(signal,
|
|
1041
|
+
const totalSignal = createTimeoutSignal(signal, PROVIDER_NONSTREAM_TOTAL_TIMEOUT_MS, `${this.name} total`);
|
|
1029
1042
|
const cacheRouting = this.name === 'xai'
|
|
1030
1043
|
? xaiCacheRouting(opts, params, tools || [], useModel)
|
|
1031
1044
|
: null;
|
|
@@ -1040,14 +1053,31 @@ export class OpenAICompatProvider {
|
|
|
1040
1053
|
// their own load balancers and emit 5xx / "overloaded" under burst
|
|
1041
1054
|
// traffic. The withRetry wrapper preserves abort behavior via
|
|
1042
1055
|
// mergedSignal and only retries when classifyError() says transient.
|
|
1043
|
-
|
|
1056
|
+
params.stream = true;
|
|
1057
|
+
params.stream_options = { include_usage: true };
|
|
1058
|
+
let assembled;
|
|
1044
1059
|
try {
|
|
1045
|
-
|
|
1046
|
-
({ signal: attemptSignal }) =>
|
|
1060
|
+
assembled = await withRetry(
|
|
1061
|
+
async ({ signal: attemptSignal }) => {
|
|
1062
|
+
try { opts.onStageChange?.('requesting'); } catch { /* heartbeat best-effort */ }
|
|
1063
|
+
const stream = await withRetry(
|
|
1064
|
+
({ signal: openSignal }) => this.client.chat.completions.create(params, { signal: openSignal }),
|
|
1065
|
+
{
|
|
1066
|
+
signal: attemptSignal,
|
|
1067
|
+
perAttemptTimeoutMs: PROVIDER_FIRST_BYTE_TIMEOUT_MS,
|
|
1068
|
+
perAttemptLabel: `${this.name} first byte`,
|
|
1069
|
+
},
|
|
1070
|
+
);
|
|
1071
|
+
try { opts.onStageChange?.('streaming'); } catch { /* heartbeat best-effort */ }
|
|
1072
|
+
return consumeCompatChatCompletionStream(stream, {
|
|
1073
|
+
signal: attemptSignal,
|
|
1074
|
+
label: this.name,
|
|
1075
|
+
onStreamDelta: opts.onStreamDelta,
|
|
1076
|
+
parseToolCalls,
|
|
1077
|
+
});
|
|
1078
|
+
},
|
|
1047
1079
|
{
|
|
1048
1080
|
signal: totalSignal.signal,
|
|
1049
|
-
perAttemptTimeoutMs: PROVIDER_FIRST_BYTE_TIMEOUT_MS,
|
|
1050
|
-
perAttemptLabel: `${this.name} first byte`,
|
|
1051
1081
|
onRetry: ({ attempt, lastErr, delayMs, delayReason }) => {
|
|
1052
1082
|
const delayLabel = Number.isFinite(Number(delayMs)) ? `, delay ${delayMs}ms${delayReason ? ` (${delayReason})` : ''}` : '';
|
|
1053
1083
|
process.stderr.write(`[${this.name}] retry attempt ${attempt + 1} after ${lastErr?.message || lastErr?.code || 'transient error'}${delayLabel}\n`);
|
|
@@ -1057,8 +1087,9 @@ export class OpenAICompatProvider {
|
|
|
1057
1087
|
} finally {
|
|
1058
1088
|
totalSignal.cleanup();
|
|
1059
1089
|
}
|
|
1090
|
+
const response = assembled.response;
|
|
1060
1091
|
const choice = response.choices[0];
|
|
1061
|
-
const toolCalls =
|
|
1092
|
+
const toolCalls = assembled.toolCalls;
|
|
1062
1093
|
// Capture finish_reason early so we can refuse to return an
|
|
1063
1094
|
// incomplete completion as final content. OpenAI-compat backends use
|
|
1064
1095
|
// `length` (max_tokens / model context overflow) and `content_filter`
|
|
@@ -1115,11 +1146,11 @@ export class OpenAICompatProvider {
|
|
|
1115
1146
|
// assistant message and echo it back next turn for providers that
|
|
1116
1147
|
// require or benefit from that official multi-turn shape.
|
|
1117
1148
|
const capturesReasoningContent = this.name === 'deepseek' || this.name === 'xai';
|
|
1118
|
-
const reasoningContent = (capturesReasoningContent && typeof
|
|
1119
|
-
?
|
|
1149
|
+
const reasoningContent = (capturesReasoningContent && typeof assembled.reasoningContent === 'string')
|
|
1150
|
+
? assembled.reasoningContent
|
|
1120
1151
|
: null;
|
|
1121
1152
|
return {
|
|
1122
|
-
content:
|
|
1153
|
+
content: assembled.content || '',
|
|
1123
1154
|
model: response.model,
|
|
1124
1155
|
toolCalls,
|
|
1125
1156
|
stopReason,
|
|
@@ -1165,17 +1196,15 @@ export class OpenAICompatProvider {
|
|
|
1165
1196
|
};
|
|
1166
1197
|
if (previousResponseId) params.previous_response_id = previousResponseId;
|
|
1167
1198
|
if (tools?.length) params.tools = toResponsesTools(tools);
|
|
1168
|
-
//
|
|
1169
|
-
//
|
|
1170
|
-
// 'connecting' reset for the whole generation (bridge list shows a
|
|
1171
|
-
// working session as stuck). Report 'requesting' for the in-flight
|
|
1172
|
-
// window and fire one delta on arrival to feed the stall watchdog.
|
|
1199
|
+
// SSE transport: report 'requesting' until the stream opens, then
|
|
1200
|
+
// per-chunk onStreamDelta feeds the bridge stall watchdog.
|
|
1173
1201
|
try { opts.onStageChange?.('requesting'); } catch { /* heartbeat best-effort */ }
|
|
1174
1202
|
const reasoningEffort = normalizeXaiReasoningEffort(opts.xaiReasoningEffort
|
|
1175
1203
|
?? opts.effort
|
|
1176
1204
|
?? this.config?.reasoningEffort
|
|
1177
1205
|
?? process.env.MIXDOG_XAI_REASONING_EFFORT);
|
|
1178
1206
|
if (reasoningEffort) params.reasoning = { effort: reasoningEffort };
|
|
1207
|
+
params.stream = true;
|
|
1179
1208
|
let response;
|
|
1180
1209
|
let cacheLane = null;
|
|
1181
1210
|
const scheduled = await withXaiResponsesCacheLane({
|
|
@@ -1189,14 +1218,29 @@ export class OpenAICompatProvider {
|
|
|
1189
1218
|
signal,
|
|
1190
1219
|
}, async (laneMeta) => {
|
|
1191
1220
|
cacheLane = laneMeta;
|
|
1192
|
-
const totalSignal = createTimeoutSignal(signal,
|
|
1221
|
+
const totalSignal = createTimeoutSignal(signal, PROVIDER_NONSTREAM_TOTAL_TIMEOUT_MS, 'xai responses total');
|
|
1193
1222
|
try {
|
|
1194
1223
|
return await withRetry(
|
|
1195
|
-
({ signal: attemptSignal }) =>
|
|
1224
|
+
async ({ signal: attemptSignal }) => {
|
|
1225
|
+
const stream = await withRetry(
|
|
1226
|
+
({ signal: openSignal }) => this.client.responses.create(params, { signal: openSignal }),
|
|
1227
|
+
{
|
|
1228
|
+
signal: attemptSignal,
|
|
1229
|
+
perAttemptTimeoutMs: PROVIDER_FIRST_BYTE_TIMEOUT_MS,
|
|
1230
|
+
perAttemptLabel: 'xai responses first byte',
|
|
1231
|
+
},
|
|
1232
|
+
);
|
|
1233
|
+
try { opts.onStageChange?.('streaming'); } catch { /* heartbeat best-effort */ }
|
|
1234
|
+
return consumeCompatResponsesStream(stream, {
|
|
1235
|
+
signal: attemptSignal,
|
|
1236
|
+
label: 'xai:responses',
|
|
1237
|
+
onStreamDelta: opts.onStreamDelta,
|
|
1238
|
+
parseResponsesToolCalls,
|
|
1239
|
+
responseOutputText,
|
|
1240
|
+
});
|
|
1241
|
+
},
|
|
1196
1242
|
{
|
|
1197
1243
|
signal: totalSignal.signal,
|
|
1198
|
-
perAttemptTimeoutMs: PROVIDER_FIRST_BYTE_TIMEOUT_MS,
|
|
1199
|
-
perAttemptLabel: 'xai responses first byte',
|
|
1200
1244
|
onRetry: ({ attempt, lastErr, delayMs, delayReason }) => {
|
|
1201
1245
|
const delayLabel = Number.isFinite(Number(delayMs)) ? `, delay ${delayMs}ms${delayReason ? ` (${delayReason})` : ''}` : '';
|
|
1202
1246
|
process.stderr.write(`[xai:responses] retry attempt ${attempt + 1} after ${lastErr?.message || lastErr?.code || 'transient error'}${delayLabel}\n`);
|
|
@@ -1207,10 +1251,10 @@ export class OpenAICompatProvider {
|
|
|
1207
1251
|
totalSignal.cleanup();
|
|
1208
1252
|
}
|
|
1209
1253
|
});
|
|
1210
|
-
|
|
1254
|
+
const streamed = scheduled.value;
|
|
1255
|
+
response = streamed.response;
|
|
1211
1256
|
cacheLane = cacheLane || scheduled.laneMeta;
|
|
1212
|
-
|
|
1213
|
-
const toolCalls = parseResponsesToolCalls(response);
|
|
1257
|
+
const toolCalls = streamed.toolCalls;
|
|
1214
1258
|
writeXaiResponsesCacheTrace({
|
|
1215
1259
|
model: useModel,
|
|
1216
1260
|
opts,
|
|
@@ -1254,7 +1298,7 @@ export class OpenAICompatProvider {
|
|
|
1254
1298
|
});
|
|
1255
1299
|
}
|
|
1256
1300
|
return {
|
|
1257
|
-
content:
|
|
1301
|
+
content: streamed.content,
|
|
1258
1302
|
model: response.model || useModel,
|
|
1259
1303
|
toolCalls,
|
|
1260
1304
|
providerState: {
|
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
} from '../stall-policy.mjs';
|
|
44
44
|
|
|
45
45
|
const CODEX_WS_URL = 'wss://chatgpt.com/backend-api/codex/responses';
|
|
46
|
+
const CODEX_OAUTH_ORIGINATOR = 'codex_cli_rs';
|
|
46
47
|
const OPENAI_WS_URL = 'wss://api.openai.com/v1/responses';
|
|
47
48
|
const XAI_WS_URL = 'wss://api.x.ai/v1/responses';
|
|
48
49
|
const WS_IDLE_MS = 5 * 60_000;
|
|
@@ -202,7 +203,7 @@ function _buildHandshakeHeaders({ auth, sessionToken, turnState, cacheKey }) {
|
|
|
202
203
|
: {
|
|
203
204
|
'Authorization': `Bearer ${auth.access_token}`,
|
|
204
205
|
'chatgpt-account-id': auth.account_id || '',
|
|
205
|
-
'originator':
|
|
206
|
+
'originator': CODEX_OAUTH_ORIGINATOR,
|
|
206
207
|
'OpenAI-Beta': 'responses_websockets=2026-02-06',
|
|
207
208
|
};
|
|
208
209
|
if (sessionToken) {
|
|
@@ -33,6 +33,7 @@ import { populateHttpStatusFromMessage } from './retry-classifier.mjs';
|
|
|
33
33
|
import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
|
|
34
34
|
// --- Constants ---
|
|
35
35
|
const CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
|
|
36
|
+
const CODEX_OAUTH_ORIGINATOR = 'codex_cli_rs';
|
|
36
37
|
const TOKEN_URL = 'https://auth.openai.com/oauth/token';
|
|
37
38
|
const CODEX_RESPONSES_URL = 'https://chatgpt.com/backend-api/codex/responses';
|
|
38
39
|
// Version string baked into the models endpoint query — Codex rejects the
|
|
@@ -69,26 +70,33 @@ async function _resolveCodexClientVersion() {
|
|
|
69
70
|
return CODEX_CLIENT_VERSION_FLOOR;
|
|
70
71
|
}
|
|
71
72
|
const CODEX_MODEL_CACHE_TTL_MS = 24 * 60 * 60_000;
|
|
73
|
+
const CODEX_MODEL_CACHE_SCHEMA_VERSION = 2;
|
|
72
74
|
const TOKEN_REFRESH_SKEW_MS = 5 * 60_000;
|
|
73
75
|
|
|
74
76
|
function _codexModelCachePath() {
|
|
75
77
|
return join(getPluginData(), 'openai-oauth-models.json');
|
|
76
78
|
}
|
|
77
79
|
|
|
78
|
-
|
|
80
|
+
function _loadCodexModelCacheSync() {
|
|
79
81
|
const path = _codexModelCachePath();
|
|
80
82
|
if (!existsSync(path)) return null;
|
|
81
83
|
try {
|
|
82
84
|
const raw = JSON.parse(readFileSync(path, 'utf-8'));
|
|
85
|
+
if (raw?.version !== CODEX_MODEL_CACHE_SCHEMA_VERSION) return null;
|
|
83
86
|
if (!raw?.fetchedAt || !Array.isArray(raw.models)) return null;
|
|
84
87
|
if (Date.now() - raw.fetchedAt > CODEX_MODEL_CACHE_TTL_MS) return null;
|
|
85
88
|
return raw.models;
|
|
86
89
|
} catch { return null; }
|
|
87
90
|
}
|
|
88
91
|
|
|
92
|
+
async function _loadCodexModelCache() {
|
|
93
|
+
return _loadCodexModelCacheSync();
|
|
94
|
+
}
|
|
95
|
+
|
|
89
96
|
async function _saveCodexModelCache(models) {
|
|
90
97
|
try {
|
|
91
98
|
writeJsonAtomicSync(_codexModelCachePath(), {
|
|
99
|
+
version: CODEX_MODEL_CACHE_SCHEMA_VERSION,
|
|
92
100
|
fetchedAt: Date.now(),
|
|
93
101
|
models,
|
|
94
102
|
}, { lock: true, fsyncDir: true });
|
|
@@ -112,6 +120,34 @@ function _codexCatalogHas(id) {
|
|
|
112
120
|
return _inMemoryCodexCatalog.some(m => m.id === id);
|
|
113
121
|
}
|
|
114
122
|
|
|
123
|
+
function _findCachedCodexModel(id) {
|
|
124
|
+
if (!id) return null;
|
|
125
|
+
if (!Array.isArray(_inMemoryCodexCatalog)) {
|
|
126
|
+
_inMemoryCodexCatalog = _loadCodexModelCacheSync();
|
|
127
|
+
}
|
|
128
|
+
if (!Array.isArray(_inMemoryCodexCatalog)) return null;
|
|
129
|
+
return _inMemoryCodexCatalog.find(m => m?.id === id) || null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function _codexServiceTiers(modelInfo) {
|
|
133
|
+
return Array.isArray(modelInfo?.serviceTiers) ? modelInfo.serviceTiers : [];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function _codexModelBlocksServiceTier(id, serviceTier) {
|
|
137
|
+
if (serviceTier !== 'priority') return false;
|
|
138
|
+
const family = _codexFamily(id);
|
|
139
|
+
return family === 'gpt-mini' || family === 'gpt-nano' || family === 'gpt-codex';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function codexModelSupportsServiceTier(id, serviceTier) {
|
|
143
|
+
if (_codexModelBlocksServiceTier(id, serviceTier)) return false;
|
|
144
|
+
const info = _findCachedCodexModel(id);
|
|
145
|
+
if (!info) return true;
|
|
146
|
+
const tiers = _codexServiceTiers(info);
|
|
147
|
+
if (!tiers.length) return false;
|
|
148
|
+
return tiers.some(t => t?.id === serviceTier);
|
|
149
|
+
}
|
|
150
|
+
|
|
115
151
|
// Codex returns dated ids (gpt-5.4-mini-2026-03-17). Strip the trailing
|
|
116
152
|
// -YYYY-MM-DD to get the version alias (gpt-5.4-mini). Unknown shapes pass
|
|
117
153
|
// through unchanged.
|
|
@@ -123,6 +159,18 @@ function _displayCodexModel(id) {
|
|
|
123
159
|
function _normalizeCodexModel(m) {
|
|
124
160
|
const id = m?.slug || m?.id;
|
|
125
161
|
const family = _codexFamily(id);
|
|
162
|
+
const serviceTiers = Array.isArray(m?.service_tiers)
|
|
163
|
+
? m.service_tiers
|
|
164
|
+
.map(t => ({
|
|
165
|
+
id: String(t?.id || '').trim(),
|
|
166
|
+
name: String(t?.name || '').trim(),
|
|
167
|
+
description: String(t?.description || '').trim(),
|
|
168
|
+
}))
|
|
169
|
+
.filter(t => t.id)
|
|
170
|
+
: [];
|
|
171
|
+
const additionalSpeedTiers = Array.isArray(m?.additional_speed_tiers)
|
|
172
|
+
? m.additional_speed_tiers.map(t => String(t || '').trim()).filter(Boolean)
|
|
173
|
+
: [];
|
|
126
174
|
// Codex doesn't use dated ids — everything is effectively a version alias.
|
|
127
175
|
return {
|
|
128
176
|
id,
|
|
@@ -130,12 +178,17 @@ function _normalizeCodexModel(m) {
|
|
|
130
178
|
display: m?.display_name || id,
|
|
131
179
|
family,
|
|
132
180
|
provider: 'openai-oauth',
|
|
133
|
-
contextWindow: m?.context_window || 1000000,
|
|
181
|
+
contextWindow: m?.context_window || m?.max_context_window || 1000000,
|
|
182
|
+
maxContextWindow: m?.max_context_window || null,
|
|
134
183
|
outputTokens: m?.auto_compact_token_limit || 32768,
|
|
184
|
+
autoCompactTokenLimit: m?.auto_compact_token_limit || null,
|
|
135
185
|
tier: 'version',
|
|
136
186
|
latest: false,
|
|
137
187
|
description: m?.description || '',
|
|
138
188
|
reasoningLevels: (m?.supported_reasoning_levels || []).map(r => r.effort),
|
|
189
|
+
serviceTiers,
|
|
190
|
+
defaultServiceTier: m?.default_service_tier || null,
|
|
191
|
+
additionalSpeedTiers,
|
|
139
192
|
};
|
|
140
193
|
}
|
|
141
194
|
|
|
@@ -482,10 +535,11 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
|
|
|
482
535
|
if (opts.fast === true) {
|
|
483
536
|
// 'priority' is the only fast-class value the Codex OAuth backend
|
|
484
537
|
// accepts on the wire: 'fast' is hard-rejected ("Unsupported
|
|
485
|
-
// service_tier: fast", probed 2026-06-11)
|
|
486
|
-
//
|
|
487
|
-
|
|
488
|
-
|
|
538
|
+
// service_tier: fast", probed 2026-06-11). Match official Codex:
|
|
539
|
+
// only send the request value when the model catalog advertises it.
|
|
540
|
+
if (codexModelSupportsServiceTier(model, 'priority')) {
|
|
541
|
+
body.service_tier = 'priority';
|
|
542
|
+
}
|
|
489
543
|
}
|
|
490
544
|
// Add tools
|
|
491
545
|
if (tools?.length) {
|
|
@@ -564,7 +618,7 @@ function _buildOpenAIHttpFallbackHeaders({ auth, cacheKey }) {
|
|
|
564
618
|
'Content-Type': 'application/json',
|
|
565
619
|
Accept: 'text/event-stream',
|
|
566
620
|
'OpenAI-Beta': 'responses=experimental',
|
|
567
|
-
originator:
|
|
621
|
+
originator: CODEX_OAUTH_ORIGINATOR,
|
|
568
622
|
'chatgpt-account-id': auth.account_id || '',
|
|
569
623
|
'x-client-request-id': randomBytes(16).toString('hex'),
|
|
570
624
|
};
|
|
@@ -954,6 +1008,9 @@ export class OpenAIOAuthProvider {
|
|
|
954
1008
|
// request skips the cold TLS handshake. Best-effort; never throws.
|
|
955
1009
|
preconnect('https://chatgpt.com');
|
|
956
1010
|
}
|
|
1011
|
+
getCachedModelInfo(model) {
|
|
1012
|
+
return _findCachedCodexModel(model);
|
|
1013
|
+
}
|
|
957
1014
|
async ensureAuth({ forceRefresh = false, reason = 'preemptive' } = {}) {
|
|
958
1015
|
if (!this.tokens) this.tokens = loadTokens();
|
|
959
1016
|
if (!this.tokens)
|
|
@@ -1308,7 +1365,6 @@ export class OpenAIOAuthProvider {
|
|
|
1308
1365
|
|
|
1309
1366
|
const AUTHORIZE_URL = 'https://auth.openai.com/oauth/authorize';
|
|
1310
1367
|
const CODEX_OAUTH_SCOPE = 'openid profile email offline_access api.connectors.read api.connectors.invoke';
|
|
1311
|
-
const CODEX_OAUTH_ORIGINATOR = 'codex_cli_rs';
|
|
1312
1368
|
const CALLBACK_HOST = '127.0.0.1';
|
|
1313
1369
|
const CALLBACK_PORT = 1455;
|
|
1314
1370
|
const CALLBACK_PATH = '/auth/callback';
|
|
@@ -1337,11 +1393,8 @@ export async function loginOAuth() {
|
|
|
1337
1393
|
url.searchParams.set('state', state);
|
|
1338
1394
|
url.searchParams.set('originator', CODEX_OAUTH_ORIGINATOR);
|
|
1339
1395
|
process.stderr.write(`\n[openai-oauth] Open this URL to log in to ChatGPT (Codex):\n${url.toString()}\n\n`);
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
const opener = process.platform === 'win32' ? 'start' : process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
1343
|
-
exec(`${opener} "${url.toString()}"`, { windowsHide: true });
|
|
1344
|
-
} catch { /* user opens manually */ }
|
|
1396
|
+
const { openInBrowser } = await import('../../../shared/open-url.mjs');
|
|
1397
|
+
openInBrowser(url.toString());
|
|
1345
1398
|
|
|
1346
1399
|
return new Promise((resolve) => {
|
|
1347
1400
|
const timeout = setTimeout(() => { server.close(); resolve(null); }, LOGIN_TIMEOUT_MS);
|
|
@@ -16,6 +16,24 @@ import { sendViaWebSocket } from './openai-oauth-ws.mjs';
|
|
|
16
16
|
import { buildRequestBody } from './openai-oauth.mjs';
|
|
17
17
|
import { resolveProviderCacheKey } from '../smart-bridge/cache-strategy.mjs';
|
|
18
18
|
|
|
19
|
+
const OPENAI_DIRECT_PRIORITY_MODEL_PATTERNS = Object.freeze([
|
|
20
|
+
/^gpt-5\.5(?:-\d{4}|$)/,
|
|
21
|
+
/^gpt-5\.4(?:-\d{4}|$)/,
|
|
22
|
+
/^gpt-5\.4-mini(?:-\d{4}|$)/,
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
export function openAiDirectSupportsPriority(model) {
|
|
26
|
+
const id = String(model || '').trim();
|
|
27
|
+
return OPENAI_DIRECT_PRIORITY_MODEL_PATTERNS.some(re => re.test(id));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function applyOpenAIDirectFastTier(body, model, opts) {
|
|
31
|
+
if (opts?.fast === true && openAiDirectSupportsPriority(model)) {
|
|
32
|
+
body.service_tier = 'priority';
|
|
33
|
+
}
|
|
34
|
+
return body;
|
|
35
|
+
}
|
|
36
|
+
|
|
19
37
|
export class OpenAIDirectProvider {
|
|
20
38
|
// input_tokens INCLUDES cached tokens (OpenAI convention). See registry.mjs.
|
|
21
39
|
static inputExcludesCache = false;
|
|
@@ -38,6 +56,11 @@ export class OpenAIDirectProvider {
|
|
|
38
56
|
const apiKey = this._ensureKey();
|
|
39
57
|
const useModel = model || 'gpt-5.5';
|
|
40
58
|
const body = buildRequestBody(messages, useModel, tools, sendOpts);
|
|
59
|
+
// Public OpenAI API priority support is documented separately from the
|
|
60
|
+
// Codex OAuth catalog. Keep this provider's service-tier decision local
|
|
61
|
+
// so gpt-5.4-mini can opt into Priority even when the Codex catalog does
|
|
62
|
+
// not advertise a Fast tier for its OAuth endpoint.
|
|
63
|
+
applyOpenAIDirectFastTier(body, useModel, opts);
|
|
41
64
|
// Public Responses API supports prompt_cache_retention='24h' at no
|
|
42
65
|
// extra cost (same cached_input_tokens billing as the default 5–10
|
|
43
66
|
// min in-memory cache). Codex/oauth rejects the parameter, so it's
|
|
@@ -357,9 +357,10 @@ let nextId = Date.now();
|
|
|
357
357
|
// without buying anything.
|
|
358
358
|
const CONTEXT_WINDOWS = {
|
|
359
359
|
// OpenAI GPT-5.x family
|
|
360
|
-
'gpt-5.5':
|
|
361
|
-
'gpt-5.4
|
|
362
|
-
'gpt-5.4-
|
|
360
|
+
'gpt-5.5': 272000,
|
|
361
|
+
'gpt-5.4': 272000,
|
|
362
|
+
'gpt-5.4-mini': 272000,
|
|
363
|
+
'gpt-5.4-nano': 272000,
|
|
363
364
|
// Anthropic Claude 4.x
|
|
364
365
|
'claude-opus-4-8': 1000000,
|
|
365
366
|
'claude-opus-4-7': 1000000,
|
|
@@ -378,6 +379,18 @@ function guessContextWindow(model) {
|
|
|
378
379
|
return 8192;
|
|
379
380
|
return 128000;
|
|
380
381
|
}
|
|
382
|
+
function positiveContextWindow(value) {
|
|
383
|
+
const n = Number(value);
|
|
384
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : null;
|
|
385
|
+
}
|
|
386
|
+
function resolveSessionContextWindow(provider, model) {
|
|
387
|
+
const info = typeof provider?.getCachedModelInfo === 'function'
|
|
388
|
+
? provider.getCachedModelInfo(model)
|
|
389
|
+
: null;
|
|
390
|
+
return positiveContextWindow(info?.contextWindow)
|
|
391
|
+
|| positiveContextWindow(info?.context_window)
|
|
392
|
+
|| guessContextWindow(model);
|
|
393
|
+
}
|
|
381
394
|
// Provider-scoped unified cache key. Goal: all orchestrator-internal
|
|
382
395
|
// dispatches (bridge/maintenance/mcp/scheduler/webhook) targeting the
|
|
383
396
|
// same provider land in a single server-side cache shard, so the
|
|
@@ -902,7 +915,7 @@ export function createSession(opts) {
|
|
|
902
915
|
provider: providerName,
|
|
903
916
|
model: modelName,
|
|
904
917
|
messages,
|
|
905
|
-
contextWindow:
|
|
918
|
+
contextWindow: resolveSessionContextWindow(provider, modelName),
|
|
906
919
|
tools,
|
|
907
920
|
preset: toolPreset,
|
|
908
921
|
presetName: presetObj?.name || null,
|
|
@@ -1417,6 +1430,7 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
1417
1430
|
runtime.session = session;
|
|
1418
1431
|
if (!provider)
|
|
1419
1432
|
throw new Error(`Provider "${session.provider}" not available`);
|
|
1433
|
+
session.contextWindow = resolveSessionContextWindow(provider, session.model);
|
|
1420
1434
|
// Cap caller-supplied / prefetched context so an oversized
|
|
1421
1435
|
// payload can't blow the session token budget before the
|
|
1422
1436
|
// first model call. 32 KB ~ 8k tokens at the 4 B/tok
|
|
@@ -70,6 +70,12 @@ export const PROVIDER_GENERATE_TOTAL_TIMEOUT_MS = resolveTimeoutMs(
|
|
|
70
70
|
{ minMs: PROVIDER_FIRST_BYTE_TIMEOUT_MS, maxMs: PROVIDER_MAX_BEFORE_WARN_MS },
|
|
71
71
|
);
|
|
72
72
|
|
|
73
|
+
export const PROVIDER_NONSTREAM_TOTAL_TIMEOUT_MS = resolveTimeoutMs(
|
|
74
|
+
['MIXDOG_NONSTREAM_TOTAL_TIMEOUT_MS', 'MIXDOG_COMPAT_NONSTREAM_TOTAL_TIMEOUT_MS'],
|
|
75
|
+
480_000,
|
|
76
|
+
{ minMs: PROVIDER_GENERATE_TOTAL_TIMEOUT_MS, maxMs: STALL_ABORT_MS },
|
|
77
|
+
);
|
|
78
|
+
|
|
73
79
|
export const PROVIDER_CACHE_CREATE_TIMEOUT_MS = resolveTimeoutMs(
|
|
74
80
|
'MIXDOG_PROVIDER_CACHE_CREATE_TIMEOUT_MS',
|
|
75
81
|
Math.min(120_000, PROVIDER_GENERATE_TOTAL_TIMEOUT_MS),
|
|
@@ -19,21 +19,35 @@ export function nativeEditMode() {
|
|
|
19
19
|
return String(process.env.MIXDOG_EDIT_NATIVE || 'auto').toLowerCase();
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
function nativeEditBinCandidate() {
|
|
23
23
|
const override = process.env.MIXDOG_EDIT_NATIVE_BIN || process.env.MIXDOG_PATCH_NATIVE_BIN;
|
|
24
|
-
if (override) return override;
|
|
25
|
-
if (existsSync(NATIVE_EDIT_DEFAULT_BIN)) return NATIVE_EDIT_DEFAULT_BIN;
|
|
26
|
-
|
|
24
|
+
if (override) return { path: override, kind: 'override' };
|
|
25
|
+
if (existsSync(NATIVE_EDIT_DEFAULT_BIN)) return { path: NATIVE_EDIT_DEFAULT_BIN, kind: 'local' };
|
|
26
|
+
const cached = findCachedPatchBinary(getPluginData());
|
|
27
|
+
if (cached) return { path: cached, kind: 'cached' };
|
|
28
|
+
return { path: NATIVE_EDIT_DEFAULT_BIN, kind: 'missing' };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function nativeEditBinPath() {
|
|
32
|
+
return nativeEditBinCandidate().path;
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
export function nativeEditShouldAttempt({ editSnapshot, oldStr, newStr, preloadedContent, preloadedRawBuf }) {
|
|
30
36
|
const mode = nativeEditMode();
|
|
31
37
|
if (/^(0|false|no|off|js|legacy)$/i.test(mode)) return false;
|
|
32
|
-
|
|
38
|
+
const forcedNative = /^(1|true|yes|on|native)$/i.test(mode);
|
|
39
|
+
const candidate = nativeEditBinCandidate();
|
|
40
|
+
if (!existsSync(candidate.path)) return false;
|
|
41
|
+
// Cached release prebuilds are guaranteed valid for apply_patch, but older
|
|
42
|
+
// manifests (currently v0.6.5 in clean CI) predate the EDIT server protocol.
|
|
43
|
+
// In auto mode, native edit is only an acceleration, so require either a
|
|
44
|
+
// local cargo build or an explicit override. If a user forces native mode,
|
|
45
|
+
// still try the cached binary and surface any protocol failure.
|
|
46
|
+
if (candidate.kind === 'cached' && !forcedNative) return false;
|
|
33
47
|
if (!snapshotCoversFullFile(editSnapshot)) return false;
|
|
34
48
|
if (preloadedContent !== null || preloadedRawBuf !== null) return false;
|
|
35
49
|
if (typeof oldStr !== 'string' || oldStr.length === 0 || typeof newStr !== 'string') return false;
|
|
36
|
-
if (
|
|
50
|
+
if (forcedNative) return true;
|
|
37
51
|
// auto: the persistent server removed per-call spawn cost, so route edits to
|
|
38
52
|
// native edit2 by default (B3). Same-size edits keep the JS in-place partial
|
|
39
53
|
// write, which rewrites bytes in place instead of the whole file.
|
|
@@ -44,6 +58,7 @@ export function nativeEditShouldAttempt({ editSnapshot, oldStr, newStr, preloade
|
|
|
44
58
|
}
|
|
45
59
|
|
|
46
60
|
export async function runNativeExactEdit({ fullPath, oldStr, newStr, replaceAll, signal = null }) {
|
|
61
|
+
const forcedNative = /^(1|true|yes|on|native)$/i.test(nativeEditMode());
|
|
47
62
|
if (signal?.aborted) {
|
|
48
63
|
return { ok: false, fallback: false, error: signal.reason?.message || signal.reason || 'native edit aborted' };
|
|
49
64
|
}
|
|
@@ -82,8 +97,14 @@ export async function runNativeExactEdit({ fullPath, oldStr, newStr, replaceAll,
|
|
|
82
97
|
}
|
|
83
98
|
const msg = String(err?.message || err);
|
|
84
99
|
// Tier misses and not-found map to a JS fallback; transport/spawn errors
|
|
85
|
-
// also fall back so a server hiccup never blocks an edit.
|
|
86
|
-
|
|
100
|
+
// also fall back so a server hiccup never blocks an edit. Older cached
|
|
101
|
+
// mixdog-patch binaries (for example the v0.6.5 release prebuilds used
|
|
102
|
+
// by clean CI before a local cargo build exists) support APPLY but not
|
|
103
|
+
// the EDIT server protocol, and answer EDIT with the APPLY parser's
|
|
104
|
+
// "bad header" error. In auto mode that means "native edit unavailable",
|
|
105
|
+
// not "the edit is invalid", so fall through to the JS editor. When the
|
|
106
|
+
// user explicitly forces native mode, keep surfacing the native failure.
|
|
107
|
+
const fallback = !forcedNative && /old_string (?:not found|found \d+ times)|not valid UTF-8|no exact match|not found|server|bad header|bad edit header/i.test(msg);
|
|
87
108
|
return { ok: false, fallback, error: msg };
|
|
88
109
|
}
|
|
89
110
|
}
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.7.12",
|
|
3
3
|
"_comment": "Rewritten by .github/workflows/graph-release.yml on each tagged release. assets maps platformKey (process.platform-process.arch, e.g. win32-x64, linux-x64, darwin-arm64) to { url, sha256 } of the mixdog-graph binary on the GitHub release. A local cargo build under native/mixdog-graph/target/release always takes precedence at runtime. (v0.5.236 entries were filled manually after CI's commit step hit detached HEAD; the workflow now checks out ref: main so future releases self-update.)",
|
|
4
4
|
"assets": {
|
|
5
5
|
"darwin-arm64": {
|
|
6
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.
|
|
7
|
-
"sha256": "
|
|
6
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.12/mixdog-graph-darwin-arm64",
|
|
7
|
+
"sha256": "75bfdd200b2f8553b72dc877ec2637208f581800083d1ee5f9caf33f87792bf7"
|
|
8
8
|
},
|
|
9
9
|
"darwin-x64": {
|
|
10
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.
|
|
11
|
-
"sha256": "
|
|
10
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.12/mixdog-graph-darwin-x64",
|
|
11
|
+
"sha256": "04742fbb4cbe09bb76943f312ee129c05814543e7bc9d37e1241fb4e65b97137"
|
|
12
12
|
},
|
|
13
13
|
"linux-arm64": {
|
|
14
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.
|
|
15
|
-
"sha256": "
|
|
14
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.12/mixdog-graph-linux-arm64",
|
|
15
|
+
"sha256": "4b3edcd7be1ffec7184c48fe6bc7d6bce42f2ea67d4709f44d4402e6b48564f2"
|
|
16
16
|
},
|
|
17
17
|
"linux-x64": {
|
|
18
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.
|
|
19
|
-
"sha256": "
|
|
18
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.12/mixdog-graph-linux-x64",
|
|
19
|
+
"sha256": "4394bb7884a8706dd6a4eea55f8755c76ba584cd02248863802d94acc3e1413c"
|
|
20
20
|
},
|
|
21
21
|
"win32-x64": {
|
|
22
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.
|
|
23
|
-
"sha256": "
|
|
22
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.12/mixdog-graph-win32-x64.exe",
|
|
23
|
+
"sha256": "cbfe189d690085aee1dfd70f5c0b9c26c260d0a080914cbeb504c84510ec3a5a"
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
}
|