mixdog 1.0.8 → 1.0.10

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 (124) hide show
  1. package/package.json +2 -1
  2. package/scripts/lib/run-node-tests.mjs +13 -0
  3. package/scripts/session-efficiency-diag.mjs +283 -0
  4. package/src/defaults/skills/computer-use/SKILL.md +3 -3
  5. package/src/defaults/skills/local-provider/SKILL.md +2 -1
  6. package/src/defaults/skills/setup/SKILL.md +4 -4
  7. package/src/defaults/skills/setup/references/actions.md +1 -3
  8. package/src/defaults/skills/setup/references/surfaces.md +2 -3
  9. package/src/headless-exec.mjs +1 -0
  10. package/src/rules/shared/70-delivery.md +2 -2
  11. package/src/runtime/agent/orchestrator/config-providers-merge.mjs +8 -0
  12. package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +10 -0
  13. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +1 -1
  14. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  15. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +8 -4
  16. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request.mjs +10 -1
  17. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +1 -1
  18. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +18 -3
  19. package/src/runtime/agent/orchestrator/providers/client-version-store.mjs +50 -0
  20. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +27 -4
  21. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +9 -5
  22. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +163 -0
  23. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +18 -4
  24. package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +2 -2
  25. package/src/runtime/agent/orchestrator/providers/lib/anthropic-models.mjs +7 -1
  26. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +23 -4
  27. package/src/runtime/agent/orchestrator/providers/npm-cli-version.mjs +25 -2
  28. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +7 -1
  29. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +4 -0
  30. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +3 -2
  31. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +8 -0
  32. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +1 -28
  33. package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +17 -3
  34. package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +7 -1
  35. package/src/runtime/agent/orchestrator/providers/provider-base-url.mjs +24 -0
  36. package/src/runtime/agent/orchestrator/providers/registry.mjs +14 -4
  37. package/src/runtime/agent/orchestrator/providers/retry-classification.mjs +16 -0
  38. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +0 -20
  39. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +3 -2
  40. package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +12 -10
  41. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +3 -1
  42. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +1 -1
  43. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +15 -3
  44. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +11 -2
  45. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +5 -1
  46. package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +23 -9
  47. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +15 -6
  48. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +4 -4
  49. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +22 -1
  50. package/src/runtime/channels/lib/voice-transcription.mjs +30 -4
  51. package/src/runtime/computer-bridge/action-schema.mjs +3 -3
  52. package/src/runtime/computer-bridge/actions.mjs +4 -0
  53. package/src/runtime/computer-bridge/bridge-env.test-support.mjs +22 -0
  54. package/src/runtime/computer-bridge/client.mjs +10 -183
  55. package/src/runtime/computer-bridge/core-actions.mjs +2 -1
  56. package/src/runtime/computer-bridge/error-recovery.mjs +46 -34
  57. package/src/runtime/computer-bridge/result-canonical.mjs +181 -0
  58. package/src/runtime/local-provider/asset-installer.mjs +20 -10
  59. package/src/runtime/local-provider/catalog.mjs +4 -7
  60. package/src/runtime/local-provider/data/manifest.json +14 -1
  61. package/src/runtime/local-provider/hardware.mjs +36 -5
  62. package/src/runtime/local-provider/resumable-installations.mjs +2 -2
  63. package/src/runtime/local-provider/server.mjs +1 -1
  64. package/src/runtime/memory/index.mjs +2 -4
  65. package/src/runtime/memory/lib/core-memory-file.mjs +3 -8
  66. package/src/runtime/memory/lib/core-memory-store.mjs +14 -61
  67. package/src/runtime/memory/lib/cycle-scheduler/backlog-probe.mjs +5 -9
  68. package/src/runtime/memory/lib/cycle1/cycle1-plan.mjs +7 -0
  69. package/src/runtime/memory/lib/cycle1/cycle1-rows.mjs +43 -16
  70. package/src/runtime/memory/lib/cycle1/cycle1-window.mjs +11 -2
  71. package/src/runtime/memory/lib/embedding-provider.mjs +2 -5
  72. package/src/runtime/memory/lib/embedding-reindex.mjs +1 -5
  73. package/src/runtime/memory/lib/http-router/lifecycle-routes.mjs +0 -1
  74. package/src/runtime/memory/lib/memory-action-handlers/maintenance-actions.mjs +1 -1
  75. package/src/runtime/memory/lib/memory-schema/core-and-meta.mjs +4 -22
  76. package/src/runtime/memory/lib/memory.mjs +79 -32
  77. package/src/runtime/memory/lib/query-maintenance-handlers.mjs +0 -2
  78. package/src/runtime/office/com/com-adapter.mjs +3 -0
  79. package/src/runtime/office/pdf/pdf-adapter.mjs +50 -14
  80. package/src/runtime/office/pdf/pdf-render.mjs +2 -6
  81. package/src/runtime/office/pdf/pdf-security.mjs +8 -2
  82. package/src/runtime/office/portable/font-provisioner.mjs +28 -0
  83. package/src/runtime/office/portable/portable-docx-edits.mjs +28 -1
  84. package/src/runtime/office/portable/portable-soffice.mjs +44 -7
  85. package/src/runtime/shared/llm/anthropic-betas.mjs +7 -1
  86. package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +1 -1
  87. package/src/runtime/shared/llm/default-context-window.mjs +42 -0
  88. package/src/runtime/shared/llm/model-catalog-projection.mjs +2 -0
  89. package/src/runtime/shared/llm/model-catalog.mjs +2 -0
  90. package/src/runtime/shared/model-alias.mjs +5 -0
  91. package/src/runtime/shared/plugin-paths.mjs +9 -2
  92. package/src/runtime/shared/worker-exec-argv.mjs +25 -0
  93. package/src/runtime/web-search/lib/formatter.mjs +43 -18
  94. package/src/session-runtime/automation-agents.mjs +88 -0
  95. package/src/session-runtime/automation-workflow.mjs +19 -6
  96. package/src/session-runtime/boot/apis.mjs +6 -1
  97. package/src/session-runtime/cwd-plugins/core-memory-context.mjs +14 -12
  98. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +4 -25
  99. package/src/session-runtime/model-recency.mjs +10 -2
  100. package/src/session-runtime/model-route/set-route.mjs +7 -1
  101. package/src/session-runtime/provider-auth/login.mjs +26 -1
  102. package/src/session-runtime/quick-model-rows/provider-rows.mjs +17 -2
  103. package/src/session-runtime/resource-api.mjs +8 -2
  104. package/src/session-runtime/resource-plugins-api.mjs +14 -2
  105. package/src/session-runtime/route-state.mjs +6 -2
  106. package/src/session-runtime/schedule-session-run.mjs +7 -1
  107. package/src/session-runtime/services/agent-tool/render.mjs +0 -5
  108. package/src/session-runtime/services/provider-admin.mjs +201 -3
  109. package/src/session-runtime/settings-builtin-tools-api.mjs +0 -16
  110. package/src/session-runtime/setup-tool/executor.mjs +7 -13
  111. package/src/session-runtime/setup-tool/tool-defs.mjs +2 -3
  112. package/src/session-runtime/tool-policy-refresh.mjs +0 -2
  113. package/src/session-runtime/webhook-session-run.mjs +10 -11
  114. package/src/session-runtime/workflow-agents-api/agent-route.mjs +5 -0
  115. package/src/standalone/daemon-agent-control.mjs +8 -1
  116. package/src/standalone/daemon.mjs +4 -0
  117. package/src/standalone/session-protocol.mjs +4 -0
  118. package/src/tui/app/model-options.mjs +10 -1
  119. package/src/tui/dist/index.mjs +12 -3
  120. package/src/tui/session/agent-job-feed/notification-router.mjs +7 -0
  121. package/src/tui/session/live-share.mjs +16 -1
  122. package/src/tui/session/notification-plan.mjs +4 -0
  123. package/src/tui/session/session-api/integrations.mjs +12 -0
  124. package/src/session-runtime/bridge-first-use-gate.mjs +0 -72
@@ -0,0 +1,50 @@
1
+ // Last live client version each provider saw, kept on disk under one key per
2
+ // client. A new or offline process starts from the newest version this machine
3
+ // has observed instead of a floor that only moves with a release. Skipped
4
+ // entirely when MIXDOG_DISABLE_LIVE_CLI_VERSIONS=1.
5
+
6
+ import { readFileSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { updateJsonAtomicSync } from '../../../shared/atomic-file.mjs';
9
+ import { resolvePluginData } from '../../../shared/plugin-paths.mjs';
10
+
11
+ const LAST_KNOWN_FILE_NAME = 'client-versions.json';
12
+ const LAST_KNOWN_SCHEMA_VERSION = 1;
13
+ const LAST_KNOWN_VALUE = /^[\w.+-]{1,64}$/;
14
+
15
+ const liveLookupDisabled = () => process.env.MIXDOG_DISABLE_LIVE_CLI_VERSIONS === '1';
16
+ const lastKnownPath = () => join(resolvePluginData(), LAST_KNOWN_FILE_NAME);
17
+
18
+ /** The last live version remembered under `key`, or null. */
19
+ export function readLastKnownVersion(key) {
20
+ if (!key || liveLookupDisabled()) return null;
21
+ try {
22
+ const raw = JSON.parse(readFileSync(lastKnownPath(), 'utf-8'));
23
+ if (raw?.version !== LAST_KNOWN_SCHEMA_VERSION) return null;
24
+ const value = raw.entries?.[key]?.value;
25
+ return typeof value === 'string' && LAST_KNOWN_VALUE.test(value) ? value : null;
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ /** Remember a live version; `null` forgets it. Best-effort: never throws. */
32
+ export function rememberLastKnownVersion(key, value) {
33
+ if (!key || liveLookupDisabled()) return;
34
+ if (value != null && !LAST_KNOWN_VALUE.test(String(value))) return;
35
+ try {
36
+ updateJsonAtomicSync(
37
+ lastKnownPath(),
38
+ (current) => {
39
+ const entries = current?.version === LAST_KNOWN_SCHEMA_VERSION ? { ...current.entries } : {};
40
+ if ((entries[key]?.value ?? null) === value) return undefined;
41
+ if (value == null) delete entries[key];
42
+ else entries[key] = { value, fetchedAt: Date.now() };
43
+ return { version: LAST_KNOWN_SCHEMA_VERSION, entries };
44
+ },
45
+ { lock: true }
46
+ );
47
+ } catch {
48
+ /* a read-only data directory must not break the lookup */
49
+ }
50
+ }
@@ -12,13 +12,16 @@
12
12
  * cache-node routing, so mixdog mirrors both.
13
13
  */
14
14
  import os from 'node:os';
15
+ import { readLastKnownVersion, rememberLastKnownVersion } from './client-version-store.mjs';
15
16
 
16
- // Offline fallback only; live value refreshes from npm (24h TTL, in-process).
17
+ // Offline fallback only; live value refreshes from npm (24h TTL) and the last
18
+ // live answer is kept on disk, so the floor is only a first-run default.
17
19
  // The backend gates model exposure AND per-request model access on the client
18
20
  // version (gpt-6-sol/luna require >= 0.155.0 per the published model catalog,
19
21
  // verified 2026-09-22), so keep this at the current release when bumping.
20
22
  const CODEX_CLIENT_VERSION_FLOOR = '0.155.1';
21
23
  const VERSION_TTL_MS = 24 * 60 * 60_000;
24
+ const LAST_KNOWN_KEY = 'codex-cli';
22
25
  let _cache = { value: null, fetchedAt: 0 };
23
26
  let _refreshInFlight = null;
24
27
 
@@ -32,14 +35,16 @@ async function _refresh() {
32
35
  const v = String(j?.version || '').trim();
33
36
  if (/^\d+\.\d+\.\d+/.test(v)) {
34
37
  _cache = { value: v, fetchedAt: Date.now() };
38
+ rememberLastKnownVersion(LAST_KNOWN_KEY, v);
35
39
  return v;
36
40
  }
37
41
  }
38
42
  } catch {
39
43
  /* offline — keep floor */
40
44
  }
41
- _cache = { value: CODEX_CLIENT_VERSION_FLOOR, fetchedAt: Date.now() };
42
- return CODEX_CLIENT_VERSION_FLOOR;
45
+ const fallback = _offlineVersion();
46
+ _cache = { value: fallback, fetchedAt: Date.now() };
47
+ return fallback;
43
48
  }
44
49
 
45
50
  /**
@@ -50,7 +55,25 @@ async function _refresh() {
50
55
  function codexClientVersionSync() {
51
56
  if (_cacheFresh()) return _cache.value;
52
57
  _ensureRefresh();
53
- return _cache.value || CODEX_CLIENT_VERSION_FLOOR;
58
+ return _cache.value || _offlineVersion();
59
+ }
60
+
61
+ // The newest version this machine has seen, never below the shipped floor.
62
+ function _offlineVersion() {
63
+ const lastKnown = readLastKnownVersion(LAST_KNOWN_KEY);
64
+ const newer = lastKnown && _compareVersion(lastKnown, CODEX_CLIENT_VERSION_FLOOR) > 0;
65
+ return newer ? lastKnown : CODEX_CLIENT_VERSION_FLOOR;
66
+ }
67
+
68
+ // Numeric x.y.z order; a pre-release suffix on the patch part is ignored.
69
+ function _compareVersion(a, b) {
70
+ const parts = (version) => String(version).split('.').map((part) => Number.parseInt(part, 10) || 0);
71
+ const [pa, pb] = [parts(a), parts(b)];
72
+ for (let i = 0; i < 3; i += 1) {
73
+ const delta = (pa[i] || 0) - (pb[i] || 0);
74
+ if (delta) return delta;
75
+ }
76
+ return 0;
54
77
  }
55
78
 
56
79
  function _cacheFresh() {
@@ -4,13 +4,17 @@ import { createRemoteVersionSource } from './npm-cli-version.mjs';
4
4
  // documented version endpoint; the official installer script embeds the
5
5
  // current build (…/lab/<build>/…), parsed best-effort. Any format change or
6
6
  // failure falls back to the floor.
7
- export const CURSOR_CLIENT_VERSION_FLOOR = 'cli-2026.08.11-e8db854';
7
+ export const CURSOR_CLIENT_VERSION_FLOOR = 'cli-2026.10.01-e373342';
8
8
  const BUILD_PATTERN = /downloads\.cursor\.com\/lab\/(\d{4}\.\d{2}\.\d{2}-[0-9a-f]{6,40})\//;
9
9
 
10
- const live = createRemoteVersionSource('https://cursor.com/install', async (res) => {
11
- const match = (await res.text()).match(BUILD_PATTERN);
12
- return match ? `cli-${match[1]}` : null;
13
- });
10
+ const live = createRemoteVersionSource(
11
+ 'https://cursor.com/install',
12
+ async (res) => {
13
+ const match = (await res.text()).match(BUILD_PATTERN);
14
+ return match ? `cli-${match[1]}` : null;
15
+ },
16
+ { persistKey: 'cursor-cli' }
17
+ );
14
18
  let envVersion;
15
19
 
16
20
  function envOverride() {
@@ -0,0 +1,163 @@
1
+ // User-defined ("custom") providers. A custom provider is stored as a
2
+ // secret-free config entry keyed by a generated `custom-<uuid>` id; the API key
3
+ // lives in the keychain under that same id and is injected at load time. The
4
+ // runtime reuses the existing OpenAI-compat / Anthropic engines, constructed
5
+ // under the custom id so model routes and provider identity stay `custom-…`.
6
+
7
+ import { ANTHROPIC_VERSION } from './lib/anthropic-models.mjs';
8
+ import { assertSafeBaseURL } from './provider-base-url.mjs';
9
+ import { listCompatModels } from './openai-compat-models.mjs';
10
+
11
+ export const CUSTOM_PROVIDER_PROTOCOLS = Object.freeze(['openai-chat', 'openai-responses', 'anthropic']);
12
+
13
+ function fail(message) {
14
+ throw new Error(`[custom-provider] ${message}`);
15
+ }
16
+
17
+ function text(value) {
18
+ return typeof value === 'string' ? value.trim() : '';
19
+ }
20
+
21
+ function positiveInt(value, label) {
22
+ if (value === undefined || value === null || value === '') return undefined;
23
+ const n = Number(value);
24
+ if (!Number.isInteger(n) || n <= 0) fail(`${label} must be a positive integer`);
25
+ return n;
26
+ }
27
+
28
+ function normalizeBaseURL(raw) {
29
+ const value = text(raw);
30
+ if (!value) fail('baseURL is required');
31
+ let url;
32
+ try {
33
+ url = new URL(value);
34
+ } catch {
35
+ fail(`invalid baseURL: ${value}`);
36
+ }
37
+ if (url.protocol !== 'https:' && url.protocol !== 'http:') fail('baseURL must use http or https');
38
+ if (url.username || url.password) fail('baseURL must not embed credentials');
39
+ if (url.search || url.hash || /[?#]/.test(value)) fail('baseURL must not contain a query or hash');
40
+ return assertSafeBaseURL(value.replace(/\/+$/, ''), 'custom');
41
+ }
42
+
43
+ function normalizeModels(raw, requireModels) {
44
+ if (raw === undefined || raw === null) raw = [];
45
+ if (!Array.isArray(raw)) fail('models must be an array');
46
+ const seen = new Set();
47
+ const models = raw.map((entry) => {
48
+ const id = text(entry?.id);
49
+ if (!id) fail('every model needs an id');
50
+ if (seen.has(id)) fail(`duplicate model id: ${id}`);
51
+ seen.add(id);
52
+ const name = text(entry.name);
53
+ const contextWindow = positiveInt(entry.contextWindow, `model ${id} contextWindow`);
54
+ const maxOutputTokens = positiveInt(entry.maxOutputTokens, `model ${id} maxOutputTokens`);
55
+ return {
56
+ id,
57
+ ...(name ? { name } : {}),
58
+ ...(contextWindow ? { contextWindow } : {}),
59
+ ...(maxOutputTokens ? { maxOutputTokens } : {}),
60
+ };
61
+ });
62
+ if (requireModels && models.length === 0) fail('at least one model is required');
63
+ return models;
64
+ }
65
+
66
+ export function normalizeCustomProviderConfig(input, { requireModels = true } = {}) {
67
+ if (!input || typeof input !== 'object') fail('config must be an object');
68
+ const name = text(input.name);
69
+ if (!name) fail('name is required');
70
+ const protocol = text(input.protocol);
71
+ if (!protocol) fail('protocol is required');
72
+ if (!CUSTOM_PROVIDER_PROTOCOLS.includes(protocol)) fail(`unsupported protocol: ${protocol}`);
73
+ return {
74
+ type: 'custom',
75
+ name,
76
+ protocol,
77
+ baseURL: normalizeBaseURL(input.baseURL),
78
+ models: normalizeModels(input.models, requireModels),
79
+ enabled: true,
80
+ };
81
+ }
82
+
83
+ function modelRow(id, m) {
84
+ return {
85
+ id: m.id,
86
+ name: m.name || m.id,
87
+ provider: id,
88
+ contextWindow: m.contextWindow || 0,
89
+ outputTokens: m.maxOutputTokens || null,
90
+ };
91
+ }
92
+
93
+ // Configured models win; an empty list falls back to protocol discovery.
94
+ function withConfiguredModels(Base, discover) {
95
+ return class extends Base {
96
+ _customModels = [];
97
+ async listModels() {
98
+ if (!this._customModels.length) return discover(this);
99
+ if ('_enrichedModels' in this) this._enrichedModels = this._customModels;
100
+ return this._customModels.map((m) => ({ ...m }));
101
+ }
102
+ async isAvailable() {
103
+ return this._customModels.length ? true : super.isAvailable();
104
+ }
105
+ getCachedModelInfo(model) {
106
+ return this._customModels.find((m) => m.id === model) || super.getCachedModelInfo?.(model) || null;
107
+ }
108
+ };
109
+ }
110
+
111
+ async function discoverAnthropicModels(provider) {
112
+ const res = await fetch(`${provider.config.baseURL}/v1/models`, {
113
+ method: 'GET',
114
+ signal: AbortSignal.timeout(10_000),
115
+ headers: { 'x-api-key': provider.apiKey || '', 'anthropic-version': ANTHROPIC_VERSION },
116
+ });
117
+ if (!res.ok) throw new Error(`${provider.name} models ${res.status}`);
118
+ const data = await res.json();
119
+ const items = Array.isArray(data?.data) ? data.data : [];
120
+ return items
121
+ .filter((m) => m?.id)
122
+ .map((m) => ({
123
+ id: m.id,
124
+ name: m.display_name || m.id,
125
+ provider: provider.name,
126
+ contextWindow: Number(m.max_input_tokens) > 0 ? Number(m.max_input_tokens) : 0,
127
+ outputTokens: Number(m.max_tokens) > 0 ? Number(m.max_tokens) : null,
128
+ }));
129
+ }
130
+
131
+ export async function createCustomProvider(id, config) {
132
+ const providerId = text(id);
133
+ if (!/^custom-.+/.test(providerId)) fail(`invalid custom provider id: ${id}`);
134
+ const cfg = normalizeCustomProviderConfig(config, { requireModels: false });
135
+ const engine = {
136
+ baseURL: cfg.baseURL,
137
+ apiKey: config.apiKey || undefined,
138
+ ...(config.preconnect === false ? { preconnect: false } : {}),
139
+ };
140
+ let provider;
141
+ if (cfg.protocol === 'anthropic') {
142
+ const { AnthropicProvider } = await import('./anthropic.mjs');
143
+ const Custom = withConfiguredModels(AnthropicProvider, discoverAnthropicModels);
144
+ // The Anthropic SDK appends /v1/messages itself; accept a /v1 base URL.
145
+ provider = new Custom({ ...engine, name: providerId, baseURL: cfg.baseURL.replace(/\/v1$/i, '') });
146
+ } else {
147
+ const { OpenAICompatProvider } = await import('./openai-compat.mjs');
148
+ const Base =
149
+ cfg.protocol === 'openai-responses'
150
+ ? class extends OpenAICompatProvider {
151
+ _doSend(messages, model, tools, sendOpts) {
152
+ return super._doSend(messages, model, tools, { ...(sendOpts || {}), compatWireApi: 'responses' });
153
+ }
154
+ }
155
+ : OpenAICompatProvider;
156
+ const Custom = withConfiguredModels(Base, (self) => listCompatModels(self, { throwOnError: true }));
157
+ provider = new Custom(providerId, engine);
158
+ if (cfg.models.length) provider.defaultModel = cfg.models[0].id;
159
+ }
160
+ provider._customModels = cfg.models.map((m) => modelRow(providerId, m));
161
+ provider.customConfig = cfg;
162
+ return provider;
163
+ }
@@ -1,10 +1,19 @@
1
1
  import { normalizeAnthropicEffortInput } from './anthropic-effort.mjs';
2
+ import { codexModelSupportsEffortUpdates } from './openai-oauth-catalog.mjs';
2
3
 
3
4
  export const EFFORT_CONFIGURATION_BETA = 'mid-conversation-output-config-2026-07-01';
4
- const ANTHROPIC_MODELS = new Set(['claude-fable-5-1', 'claude-mythos-5-1', 'claude-opus-5', 'claude-opus-5-5']);
5
+ const ANTHROPIC_MODELS = new Set([
6
+ 'claude-fable-5-1',
7
+ 'claude-mythos-5-1',
8
+ 'claude-opus-5',
9
+ 'claude-opus-5-5',
10
+ 'claude-sonnet-5-5',
11
+ ]);
5
12
  // https://developers.openai.com/api/docs/guides/reasoning#change-reasoning-mid-conversation
6
- // — the GPT-6 model family, standard single-agent mode.
7
- const OPENAI_MODELS = new Set(['gpt-6-astra', 'gpt-6-sol', 'gpt-6-luna']);
13
+ // — the GPT-6 model family, standard single-agent mode. The public API
14
+ // publishes no per-model flag, so this list is its whole answer; the OAuth
15
+ // backend's catalog carries one and only falls back here without it.
16
+ const OPENAI_MODELS = new Set(['gpt-6-astra', 'gpt-6-sol', 'gpt-6-luna', 'gpt-6-1-sol']);
8
17
  const OPENAI_PROVIDERS = new Set(['openai', 'openai-oauth']);
9
18
  const ANTHROPIC_PROVIDERS = new Set(['anthropic', 'anthropic-oauth']);
10
19
  const EFFORTS = new Set(['minimal', 'low', 'medium', 'high', 'xhigh', 'max']);
@@ -35,11 +44,16 @@ function modelKey(model) {
35
44
  .replace(/\./g, '-');
36
45
  }
37
46
 
47
+ function openAiSupportsEffortUpdates(provider, model, id) {
48
+ const declared = provider === 'openai-oauth' ? codexModelSupportsEffortUpdates(String(model || '').trim()) : null;
49
+ return declared ?? OPENAI_MODELS.has(id);
50
+ }
51
+
38
52
  // Explicit protocol capabilities, not a prediction about future model families.
39
53
  export function effortConfigurationMode(provider, model, opts = {}) {
40
54
  const id = modelKey(model);
41
55
  if (opts.effortConfigurationEnabled === false || Number(opts.thinkingBudgetTokens) > 0) return null;
42
- if (OPENAI_PROVIDERS.has(provider) && OPENAI_MODELS.has(id)) {
56
+ if (OPENAI_PROVIDERS.has(provider) && openAiSupportsEffortUpdates(provider, model, id)) {
43
57
  const parameters = opts.modelParameters || {};
44
58
  const mode = opts.reasoning?.mode ?? parameters.reasoning_mode ?? parameters.mode ?? 'standard';
45
59
  if (mode !== 'standard' || opts.multiAgent === true || parameters.multi_agent === true) return null;
@@ -3,9 +3,9 @@ import { createNpmVersionSource, maxSemver } from './npm-cli-version.mjs';
3
3
  // Grok CLI client version for the cli-chat-proxy version gate (HTTP 426).
4
4
  // Effective = env override, else max(floor, learned-from-426, live npm latest
5
5
  // of the official @xai-official/grok CLI).
6
- export const GROK_CLI_VERSION_FLOOR = '0.2.87';
6
+ export const GROK_CLI_VERSION_FLOOR = '1.0.46';
7
7
 
8
- const live = createNpmVersionSource('@xai-official/grok');
8
+ const live = createNpmVersionSource('@xai-official/grok', { persistKey: 'grok-cli' });
9
9
  let envVersion;
10
10
  let learned = null;
11
11
 
@@ -3,6 +3,12 @@
3
3
  export const MODELS = [
4
4
  { id: 'claude-opus-5-5', name: 'Claude Opus 5.5', provider: 'anthropic', family: 'opus', contextWindow: 1000000 },
5
5
  { id: 'claude-fable-5-1', name: 'Claude Fable 5.1', provider: 'anthropic', family: 'fable', contextWindow: 1000000 },
6
- { id: 'claude-sonnet-5', name: 'Claude Sonnet 5', provider: 'anthropic', family: 'sonnet', contextWindow: 1000000 },
6
+ {
7
+ id: 'claude-sonnet-5-5',
8
+ name: 'Claude Sonnet 5.5',
9
+ provider: 'anthropic',
10
+ family: 'sonnet',
11
+ contextWindow: 1000000,
12
+ },
7
13
  ];
8
14
  export const ANTHROPIC_VERSION = '2023-06-01';
@@ -11,6 +11,7 @@
11
11
  // a filtered + deduped subset of the SAME row objects (never mutated).
12
12
 
13
13
  import { getModelsDevProviderModelsSync, getModelsDevRowSync } from './model-catalog.mjs';
14
+ import { isRollingModelAlias } from '../../../shared/model-alias.mjs';
14
15
 
15
16
  // Id-level fallback for coding-unfit SKUs when the catalog row is missing.
16
17
  // search-preview (grounded-search) and audio/realtime preview SKUs are
@@ -213,6 +214,14 @@ function _stalenessFamily(row, id) {
213
214
  return family || null;
214
215
  }
215
216
 
217
+ // Explicit preview/experimental markers in the id. A stable
218
+ // model is not superseded by these; the catalog carries no structured status.
219
+ const PREVIEW_VARIANT_RE = /(^|[-_.:/\s])(preview|exp|experimental)([-_.:/\s]|$)/i;
220
+
221
+ function _isPreviewVariant(id) {
222
+ return PREVIEW_VARIANT_RE.test(String(id || ''));
223
+ }
224
+
216
225
  function _staleMonths() {
217
226
  const raw = process.env.MIXDOG_MODEL_STALE_MONTHS;
218
227
  if (raw == null || raw === '') return 9;
@@ -246,19 +255,28 @@ function _applyAutoStaleness(kept, provider, testCatalog) {
246
255
  // ANY model (kept or not) in the same family with a strictly newer date.
247
256
  // Compute the newest release_date per family from the FULL catalog so a
248
257
  // superseding model that isn't in this list still counts.
258
+ // Rolling aliases never count. Preview/experimental entries count only
259
+ // against other preview/experimental rows, never against a stable one.
249
260
  const familyNewest = new Map();
261
+ const familyNewestStable = new Map();
262
+ const bump = (map, fam, ep) => {
263
+ const prev = map.get(fam);
264
+ if (prev == null || ep > prev) map.set(fam, ep);
265
+ };
250
266
  for (const [id, cr] of Object.entries(catModels)) {
251
267
  const fam = _stalenessFamily(cr, id);
252
268
  const ep = _releaseEpoch(cr);
253
- if (!fam || ep == null) continue;
254
- const prev = familyNewest.get(fam);
255
- if (prev == null || ep > prev) familyNewest.set(fam, ep);
269
+ if (!fam || ep == null || isRollingModelAlias(id)) continue;
270
+ bump(familyNewest, fam, ep);
271
+ if (!_isPreviewVariant(id)) bump(familyNewestStable, fam, ep);
256
272
  }
257
273
 
258
274
  const dropped = new Set();
259
275
  for (const m of meta) {
260
276
  if (!m.cat || m.family == null || m.epoch == null) continue; // fallback: keep
261
- const newest = familyNewest.get(m.family);
277
+ if (isRollingModelAlias(m.row.id)) continue;
278
+ const newestByFamily = _isPreviewVariant(m.row.id) ? familyNewest : familyNewestStable;
279
+ const newest = newestByFamily.get(m.family);
262
280
  if (newest != null && newest > m.epoch) dropped.add(m.row);
263
281
  }
264
282
  // Never empty a family the user relies on: if supersession removed every
@@ -286,6 +304,7 @@ function _applyAutoStaleness(kept, provider, testCatalog) {
286
304
  for (const m of meta) {
287
305
  if (dropped.has(m.row)) continue;
288
306
  if (!m.cat || m.epoch == null) continue; // fallback: keep
307
+ if (isRollingModelAlias(m.row.id)) continue;
289
308
  if (m.epoch < cutoff) cutDropped.add(m.row);
290
309
  }
291
310
  // If applying the absolute cut would empty the entire provider list, skip
@@ -3,6 +3,12 @@
3
3
  // Sync accessor never blocks (kicks a background refresh); warm() is the
4
4
  // awaitable cold-start path. Failures resolve to null so callers fall back to
5
5
  // their floor. MIXDOG_DISABLE_LIVE_CLI_VERSIONS=1 turns the lookup off.
6
+ //
7
+ // A source given a `persistKey` also remembers its last live answer on disk,
8
+ // so a new process (or an offline one) starts from the newest version this
9
+ // machine has seen rather than from a floor that only moves with a release.
10
+
11
+ import { readLastKnownVersion, rememberLastKnownVersion } from './client-version-store.mjs';
6
12
 
7
13
  const SEMVER = /^\d+\.\d+\.\d+$/;
8
14
  const DEFAULT_TTL_MS = 24 * 60 * 60_000;
@@ -43,10 +49,22 @@ export function createNpmVersionSource(pkg, options) {
43
49
  * Generic source: GET `url`, `parse(res)` returns the version string or null
44
50
  * (unrecognised format). Same TTL/in-flight/timeout/disable rules as npm.
45
51
  */
46
- export function createRemoteVersionSource(url, parse, { ttlMs = DEFAULT_TTL_MS, timeoutMs = TIMEOUT_MS } = {}) {
52
+ export function createRemoteVersionSource(
53
+ url,
54
+ parse,
55
+ { ttlMs = DEFAULT_TTL_MS, timeoutMs = TIMEOUT_MS, persistKey = null } = {}
56
+ ) {
47
57
  let value = null;
48
58
  let expiresAt = 0;
49
59
  let inFlight = null;
60
+ let lastKnownLoaded = false;
61
+
62
+ // Stale on purpose: it answers until the refresh lands, never instead of it.
63
+ function loadLastKnown() {
64
+ if (lastKnownLoaded) return;
65
+ lastKnownLoaded = true;
66
+ if (!value) value = readLastKnownVersion(persistKey);
67
+ }
50
68
 
51
69
  async function refresh() {
52
70
  let next = null;
@@ -56,7 +74,10 @@ export function createRemoteVersionSource(url, parse, { ttlMs = DEFAULT_TTL_MS,
56
74
  } catch {
57
75
  /* offline or slow — keep previous/floor */
58
76
  }
59
- if (next) value = next;
77
+ if (next) {
78
+ value = next;
79
+ rememberLastKnownVersion(persistKey, next);
80
+ }
60
81
  expiresAt = Date.now() + (next ? ttlMs : FAILURE_TTL_MS);
61
82
  return value;
62
83
  }
@@ -76,11 +97,13 @@ export function createRemoteVersionSource(url, parse, { ttlMs = DEFAULT_TTL_MS,
76
97
  return {
77
98
  /** Cached live version or null; refreshes in the background when stale. */
78
99
  sync() {
100
+ loadLastKnown();
79
101
  if (!fresh()) ensureRefresh();
80
102
  return value;
81
103
  },
82
104
  /** Awaitable, never rejects; bounded by the fetch timeout. */
83
105
  warm() {
106
+ loadLastKnown();
84
107
  return fresh() ? Promise.resolve(value) : ensureRefresh();
85
108
  },
86
109
  };
@@ -154,7 +154,13 @@ function openAIOAuthState() {
154
154
  }
155
155
 
156
156
  function grokOAuthState() {
157
- const paths = [process.env.GROK_OAUTH_CREDENTIALS_PATH, join(resolvePluginData(), 'grok-oauth.json')];
157
+ // Same account/path precedence as the token store (getOwnTokenPath): a login
158
+ // lands in the selected account's pool file, never the legacy root file.
159
+ const paths = [
160
+ boundProviderAuthPath('grok-oauth') ||
161
+ process.env.GROK_OAUTH_CREDENTIALS_PATH ||
162
+ join(resolvePluginData(), 'grok-oauth.json'),
163
+ ];
158
164
  return memoProbe('grok-oauth', paths, () =>
159
165
  resolveProbeState(paths, (docs) => docs.some((own) => !!(own?.access_token && own?.refresh_token)))
160
166
  );
@@ -86,6 +86,10 @@ export function _normalizeCodexModel(m) {
86
86
  supportVerbosity: m?.support_verbosity === true,
87
87
  defaultVerbosity: m?.default_verbosity || null,
88
88
  supportsReasoningSummaries: m?.supports_reasoning_summaries === true,
89
+ // Absent on an older catalog: the caller then falls back to its own list.
90
+ ...(typeof m?.supports_reasoning_effort_updates === 'boolean'
91
+ ? { supportsReasoningEffortUpdates: m.supports_reasoning_effort_updates }
92
+ : {}),
89
93
  ...(typeof m?.supports_image_generation === 'boolean'
90
94
  ? { supportsImageGeneration: m.supports_image_generation }
91
95
  : {}),
@@ -28,7 +28,7 @@ export async function fetchCompatModelItems(provider) {
28
28
  }
29
29
  }
30
30
 
31
- export async function listCompatModels(provider) {
31
+ export async function listCompatModels(provider, { throwOnError = false } = {}) {
32
32
  try {
33
33
  const list = await provider._fetchModelItems();
34
34
  const models = [];
@@ -56,7 +56,8 @@ export async function listCompatModels(provider) {
56
56
  const enriched = sanitizeModelList(await enrichModels(filtered), { provider: provider.name });
57
57
  provider._enrichedModels = enriched;
58
58
  return enriched;
59
- } catch {
59
+ } catch (error) {
60
+ if (throwOnError) throw error;
60
61
  return [];
61
62
  }
62
63
  }
@@ -14,6 +14,7 @@ import { appendAgentTrace } from '../agent-trace.mjs';
14
14
  import { resolveProviderCacheKey, resolveProviderPromptCacheLane } from '../agent-runtime/cache-strategy.mjs';
15
15
  import { shouldFallbackTransport } from './retry-classifier.mjs';
16
16
  import { envFlag as _envFlag } from '../../../shared/env.mjs';
17
+ import { getModelsDevRowSync } from '../../../shared/llm/model-catalog.mjs';
17
18
  import {
18
19
  traceHash,
19
20
  stableTraceStringify,
@@ -161,6 +162,13 @@ export function xaiModelSupportsReasoningEffort(model) {
161
162
  .toLowerCase();
162
163
  if (!id) return true;
163
164
  if (!id.startsWith('grok')) return true;
165
+ // The catalog answers first: a listed model takes the parameter exactly
166
+ // when it publishes effort values (grok-4.3 does, grok-4.20-*-reasoning
167
+ // does not), so a new release needs no edit here.
168
+ const listed = getModelsDevRowSync(id, 'xai');
169
+ if (listed) {
170
+ return (listed.reasoning_options || []).some((option) => option?.type === 'effort' && option.values?.length > 0);
171
+ }
164
172
  // grok-4.5 / grok-4.6 and anything newer in that line.
165
173
  return /grok[-_]?4\.(?:[5-9]|\d{2,})/.test(id);
166
174
  }
@@ -4,6 +4,7 @@ import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs
4
4
  import { _combineUsageWithWarmup } from './openai-ws-events.mjs';
5
5
  import { appendAgentTrace } from '../agent-trace.mjs';
6
6
  import { OPENAI_COMPAT_PRESETS } from './openai-compat-presets.mjs';
7
+ import { assertSafeBaseURL } from './provider-base-url.mjs';
7
8
  import { resolveResponsesTransportPolicy, RESPONSES_TRANSPORT_CAPABILITIES } from './openai-transport-policy.mjs';
8
9
  import { toResponsesTools, toXaiResponsesInput } from './openai-compat-wire.mjs';
9
10
  import {
@@ -74,34 +75,6 @@ export { compatReportedCostUsd } from './openai-compat-response-normalization.mj
74
75
 
75
76
  const PRESETS = OPENAI_COMPAT_PRESETS;
76
77
 
77
- // SSRF guard for provider baseURL. config.baseURL comes from user JSON;
78
- // reject non-http(s) schemes (file:/data:/ftp:/etc.) and require https for
79
- // any non-localhost host. The managed Local Provider and other loopback hosts
80
- // may use http. Throws a clear config error — no silent
81
- // fallback — so misconfig surfaces immediately instead of leaking apiKey.
82
- function assertSafeBaseURL(rawURL, providerName) {
83
- let parsed;
84
- try {
85
- parsed = new URL(String(rawURL));
86
- } catch {
87
- throw new Error(`[provider:${providerName}] invalid baseURL: ${rawURL}`);
88
- }
89
- const scheme = parsed.protocol.toLowerCase();
90
- if (scheme !== 'https:' && scheme !== 'http:') {
91
- throw new Error(`[provider:${providerName}] baseURL scheme not allowed: ${parsed.protocol} (only http/https)`);
92
- }
93
- if (scheme === 'http:') {
94
- const host = parsed.hostname.toLowerCase();
95
- const isLocal = host === 'localhost' || host === '127.0.0.1' || host === '::1';
96
- if (!isLocal) {
97
- throw new Error(
98
- `[provider:${providerName}] baseURL must use https for non-localhost host (got ${parsed.protocol}//${parsed.hostname})`
99
- );
100
- }
101
- }
102
- return rawURL;
103
- }
104
-
105
78
  export class OpenAICompatProvider {
106
79
  // Chat Completions prompt_tokens is already the total (includes cached).
107
80
  // Covers grok-oauth and all OPENAI_COMPAT_PRESETS. See registry.mjs.
@@ -1,13 +1,27 @@
1
+ import { getModelMetadataSync } from '../../../shared/llm/model-catalog.mjs';
2
+
1
3
  // Public Responses contracts are independent of the OAuth backend.
2
4
  // Match documented model IDs and dated snapshots, not unknown future families.
3
5
  // GPT-5.6 and later: prompt_cache_options caching and Fast mode.
4
- const GPT_56_PLUS_MODELS = /^(?:gpt-6-(?:astra|sol|luna)|gpt-5\.6-(?:sol|terra|luna))(?:-\d{4}-\d{2}-\d{2})?$/;
6
+ const GPT_56_PLUS_MODELS =
7
+ /^(?:gpt-6-(?:astra|sol|luna)|gpt-6\.1-sol|gpt-5\.6-(?:sol|terra|luna))(?:-\d{4}-\d{2}-\d{2})?$/;
8
+
9
+ // OpenAI bills prompt-cache writes from GPT-5.6 on, and exactly those models
10
+ // take prompt_cache_options and Fast mode. The id pattern is the offline
11
+ // floor; a published cache-write price admits a later model without an edit
12
+ // here, and a family no catalog lists yet stays out.
13
+ // https://developers.openai.com/api/docs/guides/prompt-caching
14
+ function isGpt56PlusModel(model) {
15
+ const id = String(model || '').trim();
16
+ if (!id) return false;
17
+ return GPT_56_PLUS_MODELS.test(id) || getModelMetadataSync(id, 'openai')?.cacheWriteCostPerM > 0;
18
+ }
5
19
  // Earlier models documented for Priority processing (now Fast mode).
6
20
  const EARLIER_FAST_MODELS = /^gpt-5\.(?:5|4|4-mini)(?:-\d{4}|$)/;
7
21
 
8
22
  export function openAiDirectSupportsFast(model) {
9
23
  const id = String(model?.id || model || '').trim();
10
- return GPT_56_PLUS_MODELS.test(id) || EARLIER_FAST_MODELS.test(id);
24
+ return isGpt56PlusModel(id) || EARLIER_FAST_MODELS.test(id);
11
25
  }
12
26
 
13
27
  export function applyOpenAIDirectCachePolicy(body, model, storeResponses) {
@@ -17,7 +31,7 @@ export function applyOpenAIDirectCachePolicy(body, model, storeResponses) {
17
31
  // Preserve the existing opt-out: no explicit cache-retention hint when
18
32
  // response storage is disabled. The provider's default cache still applies.
19
33
  if (!storeResponses) return body;
20
- if (GPT_56_PLUS_MODELS.test(String(model || '').trim())) {
34
+ if (isGpt56PlusModel(model)) {
21
35
  body.prompt_cache_options = { ttl: '30m' };
22
36
  } else {
23
37
  body.prompt_cache_retention = '24h';