mixdog 1.0.12 → 1.1.0

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 (256) hide show
  1. package/README.md +75 -181
  2. package/package.json +4 -2
  3. package/scripts/build-shipped-history.mjs +131 -0
  4. package/scripts/lib/run-node-tests.mjs +235 -13
  5. package/scripts/lib/test-failure-records.mjs +5 -0
  6. package/scripts/lib/test-failure-summary.mjs +9 -3
  7. package/scripts/lib/test-spawn-ledger.mjs +19 -0
  8. package/scripts/prune-embedding-runtime.mjs +9 -5
  9. package/scripts/test-direct.mjs +16 -1
  10. package/src/agents/advisor/AGENT.md +38 -0
  11. package/src/agents/advisor/agent.json +6 -0
  12. package/src/agents/reviewer/AGENT.md +5 -2
  13. package/src/agents/reviewer/agent.json +1 -1
  14. package/src/defaults/shipped-history.json +381 -0
  15. package/src/defaults/skills/goal-management/SKILL.md +11 -5
  16. package/src/defaults/skills/setup/SKILL.md +5 -8
  17. package/src/defaults/skills/setup/references/actions.md +61 -31
  18. package/src/defaults/skills/setup/references/surfaces.md +12 -10
  19. package/src/lib/rules-builder.cjs +1 -1
  20. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +3 -1
  21. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +4 -0
  22. package/src/runtime/agent/orchestrator/config-presets.d.mts +1 -0
  23. package/src/runtime/agent/orchestrator/config-presets.mjs +44 -0
  24. package/src/runtime/agent/orchestrator/config-providers-merge.mjs +9 -1
  25. package/src/runtime/agent/orchestrator/config.mjs +102 -21
  26. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +77 -26
  27. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +44 -14
  28. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +2 -1
  29. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +11 -2
  30. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +2 -0
  31. package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +3 -3
  32. package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +388 -0
  33. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +92 -109
  34. package/src/runtime/agent/orchestrator/providers/custom-reasoning.mjs +107 -0
  35. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +51 -9
  36. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +2 -1
  37. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +14 -9
  38. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +51 -11
  39. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +6 -1
  40. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +7 -5
  41. package/src/runtime/agent/orchestrator/providers/openai-compat-options.mjs +12 -0
  42. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +27 -7
  43. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +12 -2
  44. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +6 -1
  45. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +2 -1
  46. package/src/runtime/agent/orchestrator/providers/quota-reading.mjs +70 -0
  47. package/src/runtime/agent/orchestrator/providers/registry.mjs +38 -3
  48. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +19 -35
  49. package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +5 -0
  50. package/src/runtime/agent/orchestrator/runtime-core/goal-mutations.mjs +0 -17
  51. package/src/runtime/agent/orchestrator/runtime-core/goal-state.mjs +0 -3
  52. package/src/runtime/agent/orchestrator/runtime-core/goal-store.mjs +0 -1
  53. package/src/runtime/agent/orchestrator/runtime-core/goal-text.mjs +1 -1
  54. package/src/runtime/agent/orchestrator/runtime-core/goal-tool-defs.mjs +1 -1
  55. package/src/runtime/agent/orchestrator/runtime-core/goal-tool-exec.mjs +1 -2
  56. package/src/runtime/agent/orchestrator/runtime-core/goal-turns.mjs +18 -4
  57. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +7 -1
  58. package/src/runtime/agent/orchestrator/runtime-core/session-route-policy.mjs +16 -0
  59. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +2 -0
  60. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +2 -2
  61. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +7 -7
  62. package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +6 -0
  63. package/src/runtime/agent/orchestrator/session/loop/step-auto-effort.mjs +28 -3
  64. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +24 -11
  65. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +2 -6
  66. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +63 -3
  67. package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +4 -4
  68. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +1 -0
  69. package/src/runtime/agent/orchestrator/stall-policy.mjs +0 -1
  70. package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +18 -8
  71. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +19 -20
  72. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +6 -11
  73. package/src/runtime/agent/orchestrator/tools/builtin/file-version-test-helpers.mjs +11 -0
  74. package/src/runtime/agent/orchestrator/tools/builtin/file-version.mjs +13 -0
  75. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-single-file-rescue.mjs +8 -5
  76. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-task-output.mjs +25 -12
  77. package/src/runtime/agent/orchestrator/tools/builtin/raw-content-publication.mjs +17 -0
  78. package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +136 -76
  79. package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +15 -17
  80. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -2
  81. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +11 -10
  82. package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +5 -3
  83. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +30 -16
  84. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-helpers.mjs +4 -11
  85. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +7 -3
  86. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-validation.mjs +16 -19
  87. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
  88. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background.mjs +4 -1
  89. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +14 -4
  90. package/src/runtime/agent/orchestrator/tools/patch/apply-patch/codex-batch.mjs +58 -8
  91. package/src/runtime/agent/orchestrator/tools/patch/content-guard.mjs +47 -0
  92. package/src/runtime/agent/orchestrator/tools/patch/delete-quarantine.mjs +155 -0
  93. package/src/runtime/agent/orchestrator/tools/patch/dispatch/native-proofs.mjs +6 -2
  94. package/src/runtime/agent/orchestrator/tools/patch/dispatch/native-snapshots.mjs +6 -2
  95. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +19 -16
  96. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +13 -44
  97. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +2 -2
  98. package/src/runtime/agent/orchestrator/tools/patch/post-patch-excerpts.mjs +5 -2
  99. package/src/runtime/agent/orchestrator/tools/patch/replay-capture.mjs +47 -7
  100. package/src/runtime/agent/orchestrator/tools/patch/rollback-state.mjs +72 -8
  101. package/src/runtime/agent/orchestrator/tools/patch/section-coalesce.mjs +3 -2
  102. package/src/runtime/agent/orchestrator/tools/patch/sequence/run.mjs +9 -1
  103. package/src/runtime/agent/orchestrator/tools/patch/sequence/units.mjs +9 -1
  104. package/src/runtime/agent/orchestrator/tools/patch/sequence.mjs +6 -6
  105. package/src/runtime/agent/orchestrator/tools/patch/ui-diff.mjs +16 -4
  106. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +35 -14
  107. package/src/runtime/agent/orchestrator/tools/patch/wave.mjs +42 -4
  108. package/src/runtime/agent/orchestrator/tools/shell-codepage.mjs +296 -0
  109. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +75 -24
  110. package/src/runtime/channels/lib/config.mjs +3 -8
  111. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +7 -3
  112. package/src/{tui → runtime/channels}/lib/voice-setup.mjs +38 -22
  113. package/src/runtime/channels/lib/webhook/request-handler.mjs +42 -8
  114. package/src/runtime/computer-bridge/actions.mjs +2 -0
  115. package/src/runtime/effort-judge/judge-client.mjs +12 -3
  116. package/src/runtime/effort-judge/model-manifest.json +7 -7
  117. package/src/runtime/effort-judge/previous-human-request.mjs +162 -0
  118. package/src/runtime/effort-judge/verified-context-input.mjs +10 -0
  119. package/src/runtime/memory/lib/embedding-provider.mjs +12 -0
  120. package/src/runtime/memory/lib/http-router/session-routes.mjs +2 -1
  121. package/src/runtime/memory/lib/memory-config-flags.mjs +3 -2
  122. package/src/runtime/memory/lib/memory-embed.mjs +5 -0
  123. package/src/runtime/shared/agent-route-config.mjs +50 -2
  124. package/src/runtime/shared/bounded-file-read.mjs +166 -0
  125. package/src/runtime/shared/builtin-grandfathering.mjs +37 -0
  126. package/src/runtime/shared/config.mjs +24 -4
  127. package/src/runtime/shared/defaults-separation.mjs +263 -0
  128. package/src/runtime/shared/llm/anthropic-effort.mjs +4 -0
  129. package/src/runtime/shared/llm/model-catalog.mjs +23 -0
  130. package/src/runtime/shared/llm/usage-ledger-rollup.mjs +105 -1
  131. package/src/runtime/shared/llm/usage-ledger-worker.mjs +2 -1
  132. package/src/runtime/shared/llm/usage-ledger.mjs +72 -5
  133. package/src/runtime/shared/onnx-runtime-support.mjs +6 -0
  134. package/src/runtime/shared/orchestration.mjs +14 -6
  135. package/src/runtime/shared/projects.mjs +65 -48
  136. package/src/runtime/shared/provider-accounts.mjs +47 -6
  137. package/src/runtime/shared/quota-resume-store.mjs +255 -0
  138. package/src/runtime/shared/shipped-definitions.mjs +275 -0
  139. package/src/runtime/shared/tool-card-model.golden.json +3 -3
  140. package/src/runtime/shared/tool-result-summary.mjs +86 -15
  141. package/src/runtime/shared/transcript-metadata.mjs +2 -0
  142. package/src/runtime/shared/turn-review-reply.mjs +2 -0
  143. package/src/runtime/shared/turn-snapshot.mjs +65 -6
  144. package/src/runtime/shared/turn-worktree-snapshot.mjs +202 -22
  145. package/src/session-runtime/boot/apis.mjs +19 -0
  146. package/src/session-runtime/boot/config.mjs +4 -0
  147. package/src/session-runtime/boot/facade.mjs +4 -1
  148. package/src/session-runtime/boot/infrastructure.mjs +21 -2
  149. package/src/session-runtime/boot/session.mjs +3 -0
  150. package/src/session-runtime/channel-config-api.mjs +5 -0
  151. package/src/session-runtime/config-lifecycle/config-writers.mjs +3 -0
  152. package/src/session-runtime/config-lifecycle.mjs +31 -6
  153. package/src/session-runtime/goal-facade-api.mjs +4 -0
  154. package/src/session-runtime/lifecycle/inheritance.mjs +28 -6
  155. package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +3 -0
  156. package/src/session-runtime/lifecycle/session-switch/resume.mjs +7 -1
  157. package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +6 -0
  158. package/src/session-runtime/lifecycle/teardown/session-close.mjs +14 -2
  159. package/src/session-runtime/media-api.mjs +32 -22
  160. package/src/session-runtime/model-recency.mjs +14 -0
  161. package/src/session-runtime/model-route/route-tuning.mjs +3 -5
  162. package/src/session-runtime/model-route/set-route.mjs +49 -21
  163. package/src/session-runtime/model-route-api.mjs +2 -1
  164. package/src/session-runtime/notification-bus.mjs +31 -6
  165. package/src/session-runtime/provider-auth/login.mjs +6 -0
  166. package/src/session-runtime/provider-models/catalog-load.mjs +3 -1
  167. package/src/session-runtime/provider-models/row-hydration.mjs +15 -14
  168. package/src/session-runtime/quick-model-rows/model-meta.mjs +26 -0
  169. package/src/session-runtime/quick-model-rows/provider-rows.mjs +40 -14
  170. package/src/session-runtime/resource-api.mjs +8 -1
  171. package/src/session-runtime/runtime-core.mjs +4 -0
  172. package/src/session-runtime/runtime-facade.mjs +20 -1
  173. package/src/session-runtime/runtime-review-api.mjs +12 -1
  174. package/src/session-runtime/services/agent-tool/helpers.mjs +4 -0
  175. package/src/session-runtime/services/agent-tool/index-write-queue.mjs +10 -15
  176. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +11 -3
  177. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +2 -0
  178. package/src/session-runtime/services/channel-admin.mjs +87 -13
  179. package/src/session-runtime/services/provider-admin.mjs +256 -63
  180. package/src/session-runtime/services/seeds.mjs +73 -142
  181. package/src/session-runtime/services/usage-dashboard-model.mjs +29 -0
  182. package/src/session-runtime/services/usage-dashboard.mjs +7 -0
  183. package/src/session-runtime/session-address.mjs +125 -0
  184. package/src/session-runtime/session-create.mjs +57 -2
  185. package/src/session-runtime/settings-builtin-tools-api.mjs +8 -0
  186. package/src/session-runtime/settings-compaction-api.mjs +13 -0
  187. package/src/session-runtime/setup-tool/executor.mjs +92 -40
  188. package/src/session-runtime/setup-tool/extended-actions.mjs +64 -5
  189. package/src/session-runtime/setup-tool/settings-contract.mjs +44 -1
  190. package/src/session-runtime/setup-tool/tool-defs.mjs +12 -7
  191. package/src/session-runtime/tool-surface.mjs +16 -9
  192. package/src/session-runtime/turn/session-ops.mjs +3 -0
  193. package/src/session-runtime/turn/share-ops.mjs +4 -1
  194. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +6 -0
  195. package/src/session-runtime/usage-stats-api.mjs +15 -0
  196. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +31 -5
  197. package/src/session-runtime/workflow-agents-api/agent-route.mjs +4 -0
  198. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +27 -7
  199. package/src/standalone/daemon-boot-coordinator.mjs +10 -0
  200. package/src/standalone/daemon-drain-policy.mjs +9 -0
  201. package/src/standalone/daemon-stored-session-views.mjs +7 -8
  202. package/src/standalone/daemon.mjs +21 -5
  203. package/src/standalone/local-session-runtime.mjs +11 -0
  204. package/src/standalone/session-client.mjs +52 -7
  205. package/src/standalone/session-protocol.mjs +8 -0
  206. package/src/standalone/session-service/agent-tree/agent-child.mjs +2 -0
  207. package/src/standalone/session-service/entries/lifecycle.mjs +2 -0
  208. package/src/standalone/session-service/entries/session-loads.mjs +9 -0
  209. package/src/standalone/session-service/projection/revisions.mjs +28 -4
  210. package/src/standalone/session-service/projection.mjs +1 -0
  211. package/src/standalone/session-service/quota-resume.mjs +140 -0
  212. package/src/standalone/session-service/session-calls/session-action.mjs +2 -2
  213. package/src/standalone/session-service/session-calls.mjs +10 -2
  214. package/src/standalone/session-service/stored-reader.mjs +26 -1
  215. package/src/standalone/session-service.mjs +21 -0
  216. package/src/standalone/session-transport/routes.mjs +2 -0
  217. package/src/tui/app/doctor.mjs +7 -11
  218. package/src/tui/app/maintenance-pickers/auto-clear-picker.mjs +3 -5
  219. package/src/tui/app/settings-picker/settings-rows.mjs +4 -6
  220. package/src/tui/app/settings-picker/settings-toggles/store-toggles.mjs +1 -1
  221. package/src/tui/app/slash-dispatch/route-commands.mjs +3 -2
  222. package/src/tui/components/ContextPanel.jsx +5 -1
  223. package/src/tui/components/Spinner.jsx +34 -319
  224. package/src/tui/components/SpinnerByline.jsx +256 -0
  225. package/src/tui/components/use-spinner-progress.mjs +113 -0
  226. package/src/tui/dist/index.mjs +15540 -14452
  227. package/src/tui/session/agent-job-feed/execution-delivery.mjs +13 -1
  228. package/src/tui/session/agent-job-feed/notification-router.mjs +3 -0
  229. package/src/tui/session/agent-job-feed/pending-resume.mjs +14 -1
  230. package/src/tui/session/agent-job-feed.mjs +4 -0
  231. package/src/tui/session/context-state.mjs +13 -1
  232. package/src/tui/session/initial-state.mjs +6 -0
  233. package/src/tui/session/quota-resume.mjs +263 -0
  234. package/src/tui/session/session-api/intake/abort.mjs +39 -35
  235. package/src/tui/session/session-api/intake/submission.mjs +13 -21
  236. package/src/tui/session/session-api/intake.mjs +3 -3
  237. package/src/tui/session/session-api/integrations.mjs +5 -1
  238. package/src/tui/session/session-api/lifecycle/dispose.mjs +1 -0
  239. package/src/tui/session/session-api/lifecycle/inheritance.mjs +2 -2
  240. package/src/tui/session/session-api/lifecycle/session-reset.mjs +4 -0
  241. package/src/tui/session/session-api/lifecycle.mjs +71 -2
  242. package/src/tui/session/session-api/media.mjs +2 -2
  243. package/src/tui/session/session-api/quota-resume.mjs +21 -0
  244. package/src/tui/session/session-api/routes.mjs +2 -0
  245. package/src/tui/session/session-api/settings.mjs +1 -0
  246. package/src/tui/session/session-api.mjs +2 -0
  247. package/src/tui/session/session-flow/auto-clear.mjs +14 -6
  248. package/src/tui/session/session-flow/drain.mjs +52 -5
  249. package/src/tui/session/session-flow/queue/take-entries.mjs +2 -3
  250. package/src/tui/session/session-flow/queue.mjs +3 -0
  251. package/src/tui/session/session-flow/steering.mjs +11 -0
  252. package/src/tui/session/session-flow.mjs +1 -0
  253. package/src/tui/session/tool-approval.mjs +7 -2
  254. package/src/tui/session/transcript-intake.mjs +1 -0
  255. package/src/tui/session/turn.mjs +40 -3
  256. package/src/vendor/statusline/bin/statusline-route.mjs +3 -2
@@ -632,7 +632,7 @@ async function exchangeAuthorizationCode({ pkce, code, state, redirectUri }) {
632
632
  };
633
633
  }
634
634
 
635
- export async function beginOAuthLogin({ openBrowserFn = null } = {}) {
635
+ export async function beginOAuthLogin({ openBrowserFn = null, openBrowser = true } = {}) {
636
636
  const pkce = createOAuthPkce();
637
637
  const state = randomBytes(32).toString('base64url');
638
638
  const buildUrl = (redirectUri) => {
@@ -650,6 +650,7 @@ export async function beginOAuthLogin({ openBrowserFn = null } = {}) {
650
650
  const url = buildUrl(OAUTH_REDIRECT_URI);
651
651
  const manualUrl = buildUrl(OAUTH_MANUAL_REDIRECT_URI);
652
652
  const openLoginUrl = async (targetUrl, label = 'login') => {
653
+ if (!openBrowser) return;
653
654
  try {
654
655
  const openInBrowser = openBrowserFn || (await import('../../../shared/open-url.mjs')).openInBrowser;
655
656
  await openInBrowser(targetUrl.toString());
@@ -7,6 +7,7 @@ import { createAbortController } from '../../../shared/abort-controller.mjs';
7
7
  import { ANTHROPIC_MAX_MIDSTREAM_RETRIES, parseSSEStream } from './anthropic-sse.mjs';
8
8
  import { buildAnthropicBetaHeaders } from './anthropic-betas.mjs';
9
9
  import { buildAnthropicApiRequest } from './anthropic-api-request.mjs';
10
+ import { isCustomProviderName } from './custom-reasoning.mjs';
10
11
  import { armFirstByteWatchdog, createAnthropicApiTransport } from './anthropic-api-transport.mjs';
11
12
  import { createAnthropicApiRecovery } from './anthropic-api-recovery.mjs';
12
13
  import { buildAnthropicTurnResult } from './anthropic-turn-result.mjs';
@@ -47,12 +48,18 @@ export class AnthropicProvider {
47
48
  // off the client defaults and add it only on turns that actually serialize
48
49
  // defer_loading tools.
49
50
  _createClient(apiKey) {
50
- const betaHeaders = this.config.disableBetaHeaders ? null : buildAnthropicBetaHeaders();
51
+ // A custom gateway gets no first-party default betas and only its own
52
+ // credential: never the SDK's env/credential-file fallbacks, and none at
53
+ // all when keyless (config.keyless).
54
+ const custom = isCustomProviderName(this.name);
55
+ const keyless = custom && this.config.keyless === true;
56
+ const betaHeaders = this.config.disableBetaHeaders || custom ? null : buildAnthropicBetaHeaders();
51
57
  return new (loadAnthropic())({
52
- apiKey,
58
+ ...(custom ? { apiKey: keyless ? '' : apiKey || '', authToken: null } : { apiKey }),
53
59
  ...(this.config.baseURL ? { baseURL: this.config.baseURL } : {}),
54
60
  defaultHeaders: {
55
61
  ...(betaHeaders ? { 'anthropic-beta': betaHeaders } : {}),
62
+ ...(keyless ? { 'x-api-key': null, authorization: null } : {}),
56
63
  ...(this.config.extraHeaders || {}),
57
64
  },
58
65
  maxRetries: 0,
@@ -114,6 +121,8 @@ export class AnthropicProvider {
114
121
  tools,
115
122
  opts,
116
123
  fastModeLatched: this.fastModeBetaHeaderLatched,
124
+ // Custom providers carry per-model reasoning settings on their own rows.
125
+ modelInfo: isCustomProviderName(this.name) ? this.getCachedModelInfo?.(useModel) || null : null,
117
126
  });
118
127
  this.fastModeBetaHeaderLatched = fastModeLatched;
119
128
 
@@ -287,6 +287,7 @@ export async function beginOAuthLogin({
287
287
  onProgress = null,
288
288
  createServerFn = createServer,
289
289
  openBrowserFn = null,
290
+ openBrowser = true,
290
291
  } = {}) {
291
292
  const controller = new AbortController();
292
293
  const pkce = createOAuthPkce();
@@ -398,6 +399,7 @@ export async function beginOAuthLogin({
398
399
  server.listen(CALLBACK_PORT, CALLBACK_HOST, async () => {
399
400
  if (settled) return;
400
401
  process.stderr.write(`\n[antigravity-oauth] Open this URL to log in:\n${url.toString()}\n\n`);
402
+ if (!openBrowser) return;
401
403
  try {
402
404
  if (openBrowserFn) await openBrowserFn(url.toString());
403
405
  else {
@@ -242,13 +242,13 @@ async function pollCursorOAuth(
242
242
  throw new Error('Cursor OAuth timed out');
243
243
  }
244
244
 
245
- export async function beginCursorOAuthLogin(options = {}) {
245
+ export async function beginCursorOAuthLogin({ openBrowser = true, ...pollOptions } = {}) {
246
246
  const params = generateCursorOAuthParams();
247
247
  const controller = new AbortController();
248
248
  process.stderr.write(`\n[cursor-oauth] Open the Cursor sign-in page:\n${params.loginUrl}\n\n`);
249
- openInBrowser(params.loginUrl);
249
+ if (openBrowser) openInBrowser(params.loginUrl);
250
250
  const waitForCallback = pollCursorOAuth(params.uuid, params.verifier, {
251
- ...options,
251
+ ...pollOptions,
252
252
  signal: controller.signal,
253
253
  }).then((tokens) => {
254
254
  saveCredentials(tokens);
@@ -0,0 +1,388 @@
1
+ // Custom provider config (schema v2): normalization, legacy migration,
2
+ // validation and the pure layering of per-model overrides on top of the
3
+ // auto-discovered (catalog-enriched) model list. No I/O.
4
+
5
+ import { CUSTOM_EFFORT_LEVELS, normalizeCustomEffort } from './custom-reasoning.mjs';
6
+ import { assertSafeBaseURL } from './provider-base-url.mjs';
7
+
8
+ export const CUSTOM_PROVIDER_PROTOCOLS = Object.freeze(['openai-chat', 'openai-responses', 'anthropic']);
9
+ export const CUSTOM_PROVIDER_SCHEMA_VERSION = 2;
10
+ // Ranked weakest → strongest.
11
+ export const CUSTOM_REASONING_LEVELS = CUSTOM_EFFORT_LEVELS;
12
+ export const CUSTOM_REASONING_WIRE_FORMATS = Object.freeze(['standard', 'deepseek']);
13
+ export const CUSTOM_PROVIDER_AUTH_MODES = Object.freeze(['key', 'none']);
14
+ export const CUSTOM_MODEL_OVERRIDE_LIMIT = 500;
15
+
16
+ const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
17
+ const MAX_MODEL_ID = 256;
18
+ const MAX_NAME = 200;
19
+ const MAX_WIRE_VALUE = 64;
20
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: rejecting control characters is the point.
21
+ const CONTROL_CHARS = /[\u0000-\u001f\u007f]/;
22
+
23
+ function fail(message) {
24
+ throw new Error(`[custom-provider] ${message}`);
25
+ }
26
+
27
+ function text(value) {
28
+ return typeof value === 'string' ? value.trim() : '';
29
+ }
30
+
31
+ function isPlainObject(value) {
32
+ return !!value && typeof value === 'object' && !Array.isArray(value);
33
+ }
34
+
35
+ function boundedText(value, label, max) {
36
+ if (typeof value !== 'string') fail(`${label} must be a string`);
37
+ const out = value.trim();
38
+ if (out.length > max) fail(`${label} must be at most ${max} characters`);
39
+ if (CONTROL_CHARS.test(out)) fail(`${label} must not contain control characters`);
40
+ return out;
41
+ }
42
+
43
+ // Legacy (unversioned) input stays tolerant: blank → unset, numeric strings ok.
44
+ // An integer beyond the safe range is dropped (unset), so migration only ever
45
+ // emits values the v2 loader accepts.
46
+ function lenientPositiveInt(value, label) {
47
+ if (value === undefined || value === null || value === '') return undefined;
48
+ const n = Number(value);
49
+ if (!Number.isInteger(n) || n <= 0) fail(`${label} must be a positive integer`);
50
+ return Number.isSafeInteger(n) ? n : undefined;
51
+ }
52
+
53
+ // v2: a JSON number that is a safe positive integer, or absent.
54
+ function strictPositiveInt(value, label) {
55
+ if (value === undefined) return undefined;
56
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value <= 0)
57
+ fail(`${label} must be a positive integer`);
58
+ return value;
59
+ }
60
+
61
+ const OVERRIDE_KEYS = new Set(['manual', 'hidden', 'name', 'contextWindow', 'maxOutputTokens', 'reasoning']);
62
+ const REASONING_KEYS = new Set(['levels', 'wireFormat', 'wireValues', 'replay']);
63
+ const DISCOVERY_KEYS = new Set(['newModelsHidden']);
64
+ // v2 config fields, plus the transport fields callers pass alongside them and
65
+ // the normalizer ignores: `id` / `apiKey` (setup input, validated by the
66
+ // service/IPC) and `preconnect` (runtime construction flag).
67
+ const V2_TOP_LEVEL_KEYS = new Set([
68
+ 'type',
69
+ 'schemaVersion',
70
+ 'name',
71
+ 'protocol',
72
+ 'baseURL',
73
+ 'enabled',
74
+ 'auth',
75
+ 'discovery',
76
+ 'modelOverrides',
77
+ 'id',
78
+ 'apiKey',
79
+ 'preconnect',
80
+ ]);
81
+ const MAX_BASE_URL = 2048;
82
+
83
+ function onlyKnownKeys(value, allowed, label) {
84
+ for (const key of Object.keys(value)) if (!allowed.has(key)) fail(`${label} has unsupported field: ${key}`);
85
+ }
86
+
87
+ function optionalBoolean(value, label) {
88
+ if (value === undefined) return undefined;
89
+ if (typeof value !== 'boolean') fail(`${label} must be a boolean`);
90
+ return value;
91
+ }
92
+
93
+ function normalizeBaseURL(raw) {
94
+ const value = text(raw);
95
+ if (!value) fail('baseURL is required');
96
+ if (value.length > MAX_BASE_URL) fail(`baseURL must be at most ${MAX_BASE_URL} characters`);
97
+ let url;
98
+ try {
99
+ url = new URL(value);
100
+ } catch {
101
+ fail(`invalid baseURL: ${value}`);
102
+ }
103
+ if (url.protocol !== 'https:' && url.protocol !== 'http:') fail('baseURL must use http or https');
104
+ if (url.username || url.password) fail('baseURL must not embed credentials');
105
+ if (url.search || url.hash || /[?#]/.test(value)) fail('baseURL must not contain a query or hash');
106
+ return assertSafeBaseURL(value.replace(/\/+$/, ''), 'custom');
107
+ }
108
+
109
+ function modelIdKey(raw, label = 'model id') {
110
+ const id = typeof raw === 'string' ? raw : '';
111
+ if (!id.trim()) fail(`every ${label} must be a non-empty string`);
112
+ if (id !== id.trim()) fail(`${label} must not have surrounding whitespace: ${JSON.stringify(id)}`);
113
+ if (id.length > MAX_MODEL_ID) fail(`${label} must be at most ${MAX_MODEL_ID} characters`);
114
+ if (CONTROL_CHARS.test(id)) fail(`${label} must not contain control characters`);
115
+ if (UNSAFE_KEYS.has(id)) fail(`unsafe ${label}: ${id}`);
116
+ return id;
117
+ }
118
+
119
+ /** Canonical level for `raw` (alias-aware), or null when unknown. */
120
+ export function canonicalReasoningLevel(raw) {
121
+ return typeof raw === 'string' ? normalizeCustomEffort(raw) : null;
122
+ }
123
+
124
+ const rank = (level) => CUSTOM_REASONING_LEVELS.indexOf(level);
125
+ const byRank = (a, b) => rank(a) - rank(b);
126
+
127
+ function normalizeLevels(raw, label) {
128
+ if (!Array.isArray(raw)) fail(`${label} must be an array`);
129
+ const seen = new Set();
130
+ for (const entry of raw) {
131
+ const level = canonicalReasoningLevel(entry);
132
+ if (!level) fail(`${label} has unknown level: ${String(entry)}`);
133
+ if (seen.has(level)) fail(`${label} has duplicate level: ${level}`);
134
+ seen.add(level);
135
+ }
136
+ return [...seen].sort(byRank);
137
+ }
138
+
139
+ function normalizeReasoning(raw, label, protocol) {
140
+ if (!isPlainObject(raw)) fail(`${label} must be an object`);
141
+ onlyKnownKeys(raw, REASONING_KEYS, label);
142
+ const out = {};
143
+ if (raw.levels !== undefined) out.levels = normalizeLevels(raw.levels, `${label}.levels`);
144
+ if (raw.wireFormat !== undefined) {
145
+ const wireFormat = text(raw.wireFormat);
146
+ if (!CUSTOM_REASONING_WIRE_FORMATS.includes(wireFormat)) fail(`${label}.wireFormat is invalid: ${raw.wireFormat}`);
147
+ if (wireFormat === 'deepseek' && protocol !== 'openai-chat')
148
+ fail(`${label}.wireFormat 'deepseek' requires protocol 'openai-chat'`);
149
+ out.wireFormat = wireFormat;
150
+ }
151
+ if (raw.wireValues !== undefined) out.wireValues = normalizeWireValues(raw.wireValues, label, out.levels);
152
+ const replay = optionalBoolean(raw.replay, `${label}.replay`);
153
+ if (replay !== undefined) out.replay = replay;
154
+ return out;
155
+ }
156
+
157
+ // `levels` (canonical) ties every key to a declared level; null skips the tie.
158
+ function normalizeWireValues(raw, label, levels) {
159
+ if (!isPlainObject(raw)) fail(`${label}.wireValues must be an object`);
160
+ const wireValues = {};
161
+ for (const key of Object.keys(raw)) {
162
+ const level = canonicalReasoningLevel(key);
163
+ if (!level) fail(`${label}.wireValues has unknown level: ${key}`);
164
+ if (levels && !levels.includes(level)) fail(`${label}.wireValues level ${level} is not in levels`);
165
+ if (level in wireValues) fail(`${label}.wireValues has duplicate level: ${level}`);
166
+ const value = boundedText(raw[key], `${label}.wireValues.${level}`, MAX_WIRE_VALUE);
167
+ if (!value) fail(`${label}.wireValues.${level} must be non-empty`);
168
+ wireValues[level] = value;
169
+ }
170
+ return Object.fromEntries(Object.entries(wireValues).sort(([a], [b]) => byRank(a, b)));
171
+ }
172
+
173
+ /**
174
+ * Shape check for a tri-state reasoning patch (null deletes a field): the
175
+ * stored-override rules for every supplied value, minus the levels ↔
176
+ * wireValues tie, which the caller settles after merging (pruning).
177
+ */
178
+ export function assertCustomReasoningPatch(raw) {
179
+ const label = 'reasoning patch';
180
+ if (!isPlainObject(raw)) fail(`${label} must be an object`);
181
+ onlyKnownKeys(raw, REASONING_KEYS, label);
182
+ const supplied = (key) => raw[key] !== undefined && raw[key] !== null;
183
+ if (supplied('levels')) normalizeLevels(raw.levels, `${label}.levels`);
184
+ if (supplied('wireFormat') && !CUSTOM_REASONING_WIRE_FORMATS.includes(text(raw.wireFormat)))
185
+ fail(`${label}.wireFormat is invalid: ${String(raw.wireFormat)}`);
186
+ if (supplied('wireValues')) normalizeWireValues(raw.wireValues, label, null);
187
+ if (supplied('replay')) optionalBoolean(raw.replay, `${label}.replay`);
188
+ }
189
+
190
+ // `legacy` marks overrides generated by migrateLegacy from an unversioned
191
+ // config, whose tolerant value rules are kept; everything else is v2-strict.
192
+ function normalizeOverride(id, raw, protocol, legacy) {
193
+ const label = `modelOverrides[${id}]`;
194
+ if (!isPlainObject(raw)) fail(`${label} must be an object`);
195
+ onlyKnownKeys(raw, OVERRIDE_KEYS, label);
196
+ const positiveInt = legacy ? lenientPositiveInt : strictPositiveInt;
197
+ const out = {};
198
+ const manual = optionalBoolean(raw.manual, `${label}.manual`);
199
+ if (manual) out.manual = true;
200
+ const hidden = optionalBoolean(raw.hidden, `${label}.hidden`);
201
+ if (hidden !== undefined) out.hidden = hidden;
202
+ if (raw.name !== undefined) {
203
+ const name = boundedText(raw.name, `${label}.name`, MAX_NAME);
204
+ if (!name && !legacy) fail(`${label}.name must be non-empty`);
205
+ if (name) out.name = name;
206
+ }
207
+ const contextWindow = positiveInt(raw.contextWindow, `${label}.contextWindow`);
208
+ if (contextWindow) out.contextWindow = contextWindow;
209
+ const maxOutputTokens = positiveInt(raw.maxOutputTokens, `${label}.maxOutputTokens`);
210
+ if (maxOutputTokens) out.maxOutputTokens = maxOutputTokens;
211
+ if (raw.reasoning !== undefined) {
212
+ const reasoning = normalizeReasoning(raw.reasoning, `${label}.reasoning`, protocol);
213
+ if (Object.keys(reasoning).length) out.reasoning = reasoning;
214
+ }
215
+ return out;
216
+ }
217
+
218
+ function normalizeOverrides(raw, protocol, legacy) {
219
+ if (raw === undefined) return {};
220
+ if (!isPlainObject(raw)) fail('modelOverrides must be an object');
221
+ const keys = Object.keys(raw);
222
+ if (keys.length > CUSTOM_MODEL_OVERRIDE_LIMIT)
223
+ fail(`at most ${CUSTOM_MODEL_OVERRIDE_LIMIT} model overrides are allowed`);
224
+ const out = {};
225
+ for (const key of keys) {
226
+ const id = modelIdKey(key, 'modelOverrides key');
227
+ out[id] = normalizeOverride(id, raw[key], protocol, legacy);
228
+ }
229
+ return out;
230
+ }
231
+
232
+ // Legacy (pre-v2) `models: [{ id, name?, contextWindow?, maxOutputTokens? }]`
233
+ // become manual, visible overrides; their presence hides newly discovered ids.
234
+ function migrateLegacy(input) {
235
+ const raw = input.models === undefined || input.models === null ? [] : input.models;
236
+ if (!Array.isArray(raw)) fail('models must be an array');
237
+ if (raw.length > CUSTOM_MODEL_OVERRIDE_LIMIT) fail(`at most ${CUSTOM_MODEL_OVERRIDE_LIMIT} models are allowed`);
238
+ const modelOverrides = {};
239
+ for (const entry of raw) {
240
+ const id = modelIdKey(text(entry?.id), 'model id');
241
+ if (Object.hasOwn(modelOverrides, id)) fail(`duplicate model id: ${id}`);
242
+ const override = { manual: true, hidden: false };
243
+ const name = text(entry.name);
244
+ if (name) override.name = name;
245
+ if (entry.contextWindow !== undefined) override.contextWindow = entry.contextWindow;
246
+ if (entry.maxOutputTokens !== undefined) override.maxOutputTokens = entry.maxOutputTokens;
247
+ modelOverrides[id] = override;
248
+ }
249
+ return {
250
+ ...input,
251
+ // Legacy entries predate keyless auth; a caller-stated auth still applies.
252
+ auth: input.auth ?? 'key',
253
+ discovery: { newModelsHidden: raw.length > 0 },
254
+ modelOverrides,
255
+ };
256
+ }
257
+
258
+ /**
259
+ * Normalized, secret-free v2 config. Legacy (no schemaVersion) input is
260
+ * migrated deterministically; unknown future versions are rejected.
261
+ */
262
+ export function normalizeCustomProviderConfig(input) {
263
+ if (!isPlainObject(input)) fail('config must be an object');
264
+ let source = input;
265
+ const version = input.schemaVersion;
266
+ const legacy = version === undefined || version === null;
267
+ if (legacy) {
268
+ // v2 fields without the version would be silently dropped by migration.
269
+ if (input.modelOverrides !== undefined || input.discovery !== undefined)
270
+ fail('modelOverrides and discovery require schemaVersion 2');
271
+ source = migrateLegacy(input);
272
+ } else if (Number.isInteger(version) && version > CUSTOM_PROVIDER_SCHEMA_VERSION)
273
+ fail(`unsupported schemaVersion ${version}; update mixdog to use this provider`);
274
+ else if (version !== CUSTOM_PROVIDER_SCHEMA_VERSION) fail(`invalid schemaVersion: ${String(version)}`);
275
+ if (!legacy) {
276
+ // v2 top level is strict (same accept set as the desktop IPC validator):
277
+ // no unknown fields, exact enum strings, boolean `enabled`. An omitted
278
+ // `auth` means 'key' here; edits keep the stored value in the service.
279
+ onlyKnownKeys(input, V2_TOP_LEVEL_KEYS, 'config');
280
+ if (input.type !== undefined && input.type !== 'custom') fail(`invalid type: ${String(input.type)}`);
281
+ if (input.protocol !== undefined && typeof input.protocol !== 'string') fail('protocol must be a string');
282
+ if (typeof input.protocol === 'string' && input.protocol !== input.protocol.trim())
283
+ fail(`unsupported protocol: ${input.protocol}`);
284
+ if (input.auth !== undefined && !CUSTOM_PROVIDER_AUTH_MODES.includes(input.auth))
285
+ fail(`invalid auth: ${String(input.auth)}`);
286
+ optionalBoolean(input.enabled, 'enabled');
287
+ optionalBoolean(input.preconnect, 'preconnect');
288
+ }
289
+ const name = boundedText(source.name ?? '', 'name', MAX_NAME);
290
+ if (!name) fail('name is required');
291
+ const protocol = text(source.protocol);
292
+ if (!protocol) fail('protocol is required');
293
+ if (!CUSTOM_PROVIDER_PROTOCOLS.includes(protocol)) fail(`unsupported protocol: ${protocol}`);
294
+ const auth = source.auth === undefined || source.auth === null ? 'key' : text(source.auth);
295
+ if (!CUSTOM_PROVIDER_AUTH_MODES.includes(auth)) fail(`invalid auth: ${String(source.auth)}`);
296
+ const discovery = source.discovery === undefined ? {} : source.discovery;
297
+ if (!isPlainObject(discovery)) fail('discovery must be an object');
298
+ onlyKnownKeys(discovery, DISCOVERY_KEYS, 'discovery');
299
+ const newModelsHidden = optionalBoolean(discovery.newModelsHidden, 'discovery.newModelsHidden') ?? false;
300
+ return {
301
+ type: 'custom',
302
+ schemaVersion: CUSTOM_PROVIDER_SCHEMA_VERSION,
303
+ name,
304
+ protocol,
305
+ baseURL: normalizeBaseURL(source.baseURL),
306
+ enabled: source.enabled !== false,
307
+ auth,
308
+ discovery: { newModelsHidden },
309
+ modelOverrides: normalizeOverrides(source.modelOverrides, protocol, legacy),
310
+ };
311
+ }
312
+
313
+ function catalogLevels(row) {
314
+ const options = Array.isArray(row?.reasoningOptions) ? row.reasoningOptions : [];
315
+ const levels = new Set();
316
+ for (const option of options) {
317
+ if (option?.type !== 'effort' || !Array.isArray(option.values)) continue;
318
+ for (const value of option.values) {
319
+ const level = canonicalReasoningLevel(value);
320
+ if (level) levels.add(level);
321
+ }
322
+ }
323
+ return levels.size ? [...levels].sort(byRank) : null;
324
+ }
325
+
326
+ function effectiveRow(base, override, { providerId, discovered, newModelsHidden }) {
327
+ const ov = override || {};
328
+ const reasoning = ov.reasoning || {};
329
+ const {
330
+ reasoningOptions: _options,
331
+ reasoningLevels: _levels,
332
+ reasoningContentField,
333
+ customReasoning: _custom,
334
+ ...rest
335
+ } = base;
336
+ const levels = reasoning.levels ?? catalogLevels(base);
337
+ const replay = reasoning.replay ?? reasoningContentField === 'reasoning_content';
338
+ const wireValues = {};
339
+ for (const [level, value] of Object.entries(reasoning.wireValues || {})) {
340
+ if (!levels || levels.includes(level)) wireValues[level] = value;
341
+ }
342
+ const hidden = 'hidden' in ov ? ov.hidden : discovered && !override && newModelsHidden;
343
+ return {
344
+ ...rest,
345
+ id: base.id,
346
+ name: ov.name ?? (base.name || base.id),
347
+ provider: providerId || base.provider,
348
+ contextWindow: ov.contextWindow ?? (Number(base.contextWindow) > 0 ? Number(base.contextWindow) : 0),
349
+ outputTokens: ov.maxOutputTokens ?? (Number(base.outputTokens) > 0 ? Number(base.outputTokens) : null),
350
+ hidden,
351
+ manual: ov.manual === true,
352
+ supportsReasoning: levels ? levels.some((level) => level !== 'none') : base.supportsReasoning === true,
353
+ ...(levels
354
+ ? {
355
+ reasoningOptions: levels.length ? [{ type: 'effort', values: [...levels] }] : [],
356
+ reasoningLevels: [...levels],
357
+ }
358
+ : {}),
359
+ customReasoning: { wireFormat: reasoning.wireFormat ?? 'standard', wireValues, replay },
360
+ ...(replay ? { reasoningContentField: 'reasoning_content' } : {}),
361
+ };
362
+ }
363
+
364
+ /**
365
+ * Pure layering: discovered rows (catalog-enriched) as base → overrides by
366
+ * property presence → manual overrides missing from discovery appended.
367
+ * `config` is a normalized v2 config. Hidden rows are kept.
368
+ */
369
+ export function effectiveCustomModelRows(config, discoveredRows, { providerId } = {}) {
370
+ const overrides = config?.modelOverrides || {};
371
+ const newModelsHidden = config?.discovery?.newModelsHidden === true;
372
+ const rows = [];
373
+ const seen = new Set();
374
+ for (const row of Array.isArray(discoveredRows) ? discoveredRows : []) {
375
+ const id = typeof row?.id === 'string' ? row.id : '';
376
+ if (!id || seen.has(id)) continue;
377
+ seen.add(id);
378
+ const override = Object.hasOwn(overrides, id) ? overrides[id] : undefined;
379
+ rows.push(effectiveRow(row, override, { providerId, discovered: true, newModelsHidden }));
380
+ }
381
+ for (const [id, override] of Object.entries(overrides)) {
382
+ if (!override?.manual || seen.has(id)) continue;
383
+ seen.add(id);
384
+ const base = { id, name: id, provider: providerId, contextWindow: 0, outputTokens: null };
385
+ rows.push(effectiveRow(base, override, { providerId, discovered: false, newModelsHidden }));
386
+ }
387
+ return rows;
388
+ }