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.
Files changed (63) hide show
  1. package/.claude-plugin/marketplace.json +5 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +40 -0
  4. package/README.md +198 -251
  5. package/bin/statusline-launcher.mjs +5 -1
  6. package/bin/statusline-lib.mjs +14 -6
  7. package/bin/statusline.mjs +14 -6
  8. package/hooks/lib/settings-loader.cjs +4 -3
  9. package/hooks/pre-tool-subagent.cjs +7 -2
  10. package/hooks/session-start.cjs +52 -24
  11. package/lib/mixdog-debug.cjs +163 -0
  12. package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
  13. package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
  14. package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
  15. package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
  16. package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
  17. package/package.json +1 -1
  18. package/scripts/builtin-utils-smoke.mjs +14 -8
  19. package/scripts/bump.mjs +80 -0
  20. package/scripts/doctor.mjs +8 -3
  21. package/scripts/mutation-io-smoke.mjs +17 -1
  22. package/scripts/openai-oauth-catalog-smoke.mjs +53 -0
  23. package/scripts/permission-eval-smoke.mjs +18 -1
  24. package/scripts/statusline-launcher-smoke.mjs +2 -2
  25. package/scripts/webhook-selfheal-smoke.mjs +1 -3
  26. package/server-main.mjs +57 -3
  27. package/setup/config-merge.mjs +0 -1
  28. package/setup/install.mjs +241 -51
  29. package/setup/mixdog-cli.mjs +30 -3
  30. package/setup/setup-server.mjs +21 -33
  31. package/setup/setup.html +46 -11
  32. package/setup/tui.mjs +35 -316
  33. package/src/agent/orchestrator/config.mjs +0 -1
  34. package/src/agent/orchestrator/providers/anthropic-oauth.mjs +2 -5
  35. package/src/agent/orchestrator/providers/anthropic.mjs +243 -86
  36. package/src/agent/orchestrator/providers/gemini.mjs +386 -31
  37. package/src/agent/orchestrator/providers/grok-oauth.mjs +2 -5
  38. package/src/agent/orchestrator/providers/model-catalog.mjs +146 -13
  39. package/src/agent/orchestrator/providers/openai-compat-stream.mjs +366 -0
  40. package/src/agent/orchestrator/providers/openai-compat.mjs +74 -30
  41. package/src/agent/orchestrator/providers/openai-oauth-ws.mjs +2 -1
  42. package/src/agent/orchestrator/providers/openai-oauth.mjs +66 -13
  43. package/src/agent/orchestrator/providers/openai-ws.mjs +23 -0
  44. package/src/agent/orchestrator/session/manager.mjs +18 -4
  45. package/src/agent/orchestrator/stall-policy.mjs +6 -0
  46. package/src/agent/orchestrator/tools/builtin/native-edit-runner.mjs +29 -8
  47. package/src/agent/orchestrator/tools/graph-manifest.json +11 -11
  48. package/src/agent/orchestrator/tools/patch-manifest.json +11 -11
  49. package/src/channels/index.mjs +27 -8
  50. package/src/channels/lib/event-queue.mjs +24 -1
  51. package/src/channels/lib/hook-pipe-server.mjs +21 -8
  52. package/src/channels/lib/webhook.mjs +142 -20
  53. package/src/memory/lib/memory-cycle1.mjs +7 -3
  54. package/src/memory/lib/memory-recall-store.mjs +27 -10
  55. package/src/search/lib/backends/openai-oauth.mjs +6 -2
  56. package/src/search/lib/cache.mjs +55 -7
  57. package/src/shared/config.mjs +1 -1
  58. package/src/shared/llm/cost.mjs +2 -2
  59. package/src/shared/open-url.mjs +37 -0
  60. package/src/shared/seed.mjs +20 -3
  61. package/src/shared/user-data-guard.mjs +3 -1
  62. package/scripts/test-config-rmw-restore.mjs +0 -122
  63. 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
- nvidia: {
27
- baseURL: 'https://integrate.api.nvidia.com/v1',
28
- defaultModel: 'meta/llama-3.3-70b-instruct',
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: JSON.parse(tc.function.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: parseJsonObject(item.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, PROVIDER_GENERATE_TOTAL_TIMEOUT_MS, `${this.name} total`);
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
- let response;
1056
+ params.stream = true;
1057
+ params.stream_options = { include_usage: true };
1058
+ let assembled;
1044
1059
  try {
1045
- response = await withRetry(
1046
- ({ signal: attemptSignal }) => this.client.chat.completions.create(params, { 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 = choice ? parseToolCalls(choice) : undefined;
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 choice?.message?.reasoning_content === 'string')
1119
- ? choice.message.reasoning_content
1149
+ const reasoningContent = (capturesReasoningContent && typeof assembled.reasoningContent === 'string')
1150
+ ? assembled.reasoningContent
1120
1151
  : null;
1121
1152
  return {
1122
- content: choice?.message?.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
- // Non-streaming transport: there are no deltas to report, so without
1169
- // an explicit stage the session sits on the loop's per-iteration
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, PROVIDER_GENERATE_TOTAL_TIMEOUT_MS, 'xai responses total');
1221
+ const totalSignal = createTimeoutSignal(signal, PROVIDER_NONSTREAM_TOTAL_TIMEOUT_MS, 'xai responses total');
1193
1222
  try {
1194
1223
  return await withRetry(
1195
- ({ signal: attemptSignal }) => this.client.responses.create(params, { 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
- response = scheduled.value;
1254
+ const streamed = scheduled.value;
1255
+ response = streamed.response;
1211
1256
  cacheLane = cacheLane || scheduled.laneMeta;
1212
- try { opts.onStreamDelta?.(); } catch { /* heartbeat best-effort */ }
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: responseOutputText(response),
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': 'mixdog',
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
- async function _loadCodexModelCache() {
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), and 'priority' is accepted
486
- // but downgraded to 'default' unless the account is entitled to
487
- // priority processing. Keep sending it so entitled accounts benefit.
488
- body.service_tier = 'priority';
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: 'mixdog',
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
- try {
1341
- const { exec } = await import('child_process');
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': 1000000,
361
- 'gpt-5.4-mini': 1000000,
362
- 'gpt-5.4-nano': 1000000,
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: guessContextWindow(modelName),
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
- export function nativeEditBinPath() {
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
- return findCachedPatchBinary(getPluginData()) || NATIVE_EDIT_DEFAULT_BIN;
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
- if (!existsSync(nativeEditBinPath())) return false;
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 (/^(1|true|yes|on|native)$/i.test(mode)) return true;
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
- const fallback = /old_string (?:not found|found \d+ times)|not valid UTF-8|no exact match|not found|server/i.test(msg);
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.6.5",
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.6.5/mixdog-graph-darwin-arm64",
7
- "sha256": "7016c273a07d19ca9e2f56e8fa7f273fdd40fc41bdc7fef206bf23e31a21a736"
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.6.5/mixdog-graph-darwin-x64",
11
- "sha256": "d076e97da4420f49a6c726bc088a3321e2e7f6a9bfb32d39162c8c53045cfcdb"
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.6.5/mixdog-graph-linux-arm64",
15
- "sha256": "74754562b3c080868738c032c5b6e0e13bc53d7a5277002176b036f8d6681f39"
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.6.5/mixdog-graph-linux-x64",
19
- "sha256": "0d8e8bbdd49b18746ed3f972fc3719731a1143ee03ac9e6d86586788b0b431f8"
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.6.5/mixdog-graph-win32-x64.exe",
23
- "sha256": "1a671558e5a5f13c7429ff9987a46ad72a71e52241e200d2da820a13d7cbdae7"
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
  }