mixdog 1.0.9 → 1.0.11

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 (165) hide show
  1. package/package.json +3 -3
  2. package/src/rules/agent/40-cycle1-agent.md +19 -23
  3. package/src/rules/lead/LEAD.md +5 -5
  4. package/src/rules/shared/05-tool-calls.md +6 -5
  5. package/src/rules/shared/60-verification.md +2 -2
  6. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +37 -13
  7. package/src/runtime/agent/orchestrator/agent-runtime/title-completion.mjs +3 -0
  8. package/src/runtime/agent/orchestrator/cache-break-trace.mjs +6 -0
  9. package/src/runtime/agent/orchestrator/config-providers-merge.mjs +8 -0
  10. package/src/runtime/agent/orchestrator/mcp/client.mjs +234 -12
  11. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +22 -5
  12. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +7 -8
  13. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +6 -0
  14. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +9 -7
  15. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +2 -0
  16. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +3 -2
  17. package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +77 -0
  18. package/src/runtime/agent/orchestrator/providers/cursor.mjs +11 -5
  19. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +165 -0
  20. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +4 -2
  21. package/src/runtime/agent/orchestrator/providers/gemini-request-body.mjs +4 -6
  22. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +7 -0
  23. package/src/runtime/agent/orchestrator/providers/gemini.mjs +2 -0
  24. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +3 -2
  25. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +2 -0
  26. package/src/runtime/agent/orchestrator/providers/lib/anthropic-image-input.mjs +34 -13
  27. package/src/runtime/agent/orchestrator/providers/lib/oauth-page.mjs +94 -0
  28. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +670 -198
  29. package/src/runtime/agent/orchestrator/providers/mixdog-local.mjs +9 -3
  30. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +23 -4
  31. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +3 -1
  32. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +4 -3
  33. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +4 -0
  34. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +14 -8
  35. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +7 -4
  36. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +24 -9
  37. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +3 -28
  38. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +3 -2
  39. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +2 -0
  40. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -0
  41. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +4 -0
  42. package/src/runtime/agent/orchestrator/providers/provider-base-url.mjs +24 -0
  43. package/src/runtime/agent/orchestrator/providers/registry.mjs +24 -4
  44. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +9 -1
  45. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +3 -2
  46. package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +13 -11
  47. package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +17 -9
  48. package/src/runtime/agent/orchestrator/session/cache/scoped-cache-outcome.mjs +23 -1
  49. package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +40 -71
  50. package/src/runtime/agent/orchestrator/session/cache/tool-result-reuse.mjs +27 -0
  51. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +8 -7
  52. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +54 -12
  53. package/src/runtime/agent/orchestrator/session/context-utils.mjs +27 -5
  54. package/src/runtime/agent/orchestrator/session/eager-dispatch/admission.mjs +8 -51
  55. package/src/runtime/agent/orchestrator/session/image-strip-recovery.mjs +55 -2
  56. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +23 -5
  57. package/src/runtime/agent/orchestrator/session/loop/request-boundary.mjs +2 -0
  58. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +62 -0
  59. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +79 -0
  60. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +10 -2
  61. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +8 -1
  62. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +6 -3
  63. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +10 -3
  64. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +5 -1
  65. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +21 -5
  66. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +20 -4
  67. package/src/runtime/agent/orchestrator/session/store/live-state.mjs +7 -6
  68. package/src/runtime/agent/orchestrator/session/store/serialize.mjs +6 -3
  69. package/src/runtime/agent/orchestrator/session/task-wait-control.mjs +13 -0
  70. package/src/runtime/agent/orchestrator/session/tool-batch/execute-call.mjs +9 -19
  71. package/src/runtime/agent/orchestrator/session/tool-batch/finalize.mjs +85 -2
  72. package/src/runtime/agent/orchestrator/session/tool-batch/pre-dispatch.mjs +10 -6
  73. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +3 -0
  74. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -1
  75. package/src/runtime/agent/orchestrator/tools/builtin/find-files-tool.mjs +3 -3
  76. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-path-fanout.mjs +2 -1
  77. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-pattern-fanout.mjs +2 -1
  78. package/src/runtime/agent/orchestrator/tools/builtin/lib/scoped-search-flight.mjs +20 -0
  79. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +51 -28
  80. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +3 -2
  81. package/src/runtime/agent/orchestrator/tools/builtin/pdf-test-fixtures.mjs +47 -0
  82. package/src/runtime/agent/orchestrator/tools/builtin/read-single-media.mjs +1 -1
  83. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +34 -30
  84. package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +10 -5
  85. package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +18 -7
  86. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +1 -1
  87. package/src/runtime/agent/orchestrator/tools/builtin.mjs +9 -2
  88. package/src/runtime/agent/orchestrator/tools/code-graph/aggregate-roots.mjs +3 -1
  89. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/federation.mjs +3 -1
  90. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/work.mjs +11 -6
  91. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +13 -1
  92. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +15 -1
  93. package/src/runtime/attachments/limits.mjs +20 -0
  94. package/src/runtime/{agent/orchestrator/tools/builtin/read-office-files.mjs → attachments/office-extract.mjs} +356 -125
  95. package/src/runtime/attachments/pdf-extract.mjs +2 -2
  96. package/src/runtime/attachments/store.mjs +232 -72
  97. package/src/runtime/browser-bridge/client.mjs +3 -3
  98. package/src/runtime/effort-judge/compact-tokenizer.mjs +282 -0
  99. package/src/runtime/effort-judge/judge-client.mjs +194 -0
  100. package/src/runtime/effort-judge/judge-worker.mjs +106 -0
  101. package/src/runtime/local-provider/input-capabilities.mjs +30 -12
  102. package/src/runtime/media/jobs.mjs +0 -1
  103. package/src/runtime/media/media-usage.mjs +2 -1
  104. package/src/runtime/memory/lib/embedding-model-config.mjs +6 -6
  105. package/src/runtime/memory/lib/embedding-model-files.mjs +44 -0
  106. package/src/runtime/memory/lib/embedding-tokenizer.mjs +253 -0
  107. package/src/runtime/memory/lib/embedding-worker.mjs +97 -103
  108. package/src/runtime/memory/lib/memory-chunk-quality.mjs +40 -218
  109. package/src/runtime/memory/lib/memory-embed.mjs +0 -9
  110. package/src/runtime/memory/lib/memory.mjs +34 -23
  111. package/src/runtime/memory/tool-defs.mjs +2 -1
  112. package/src/runtime/shared/automation-attachments.mjs +9 -7
  113. package/src/runtime/shared/environment-error.mjs +14 -0
  114. package/src/runtime/shared/error-presentation.mjs +10 -2
  115. package/src/runtime/shared/inline-file-kind.mjs +28 -2
  116. package/src/runtime/shared/llm/model-catalog.mjs +17 -3
  117. package/src/runtime/shared/llm/quota-value-estimate.mjs +33 -18
  118. package/src/runtime/shared/llm/usage-accounting.mjs +8 -5
  119. package/src/runtime/shared/llm/usage-ledger-quota.mjs +29 -10
  120. package/src/runtime/shared/llm/usage-ledger-rollup.mjs +47 -4
  121. package/src/runtime/shared/model-alias.mjs +5 -0
  122. package/src/runtime/shared/read-image-resize.mjs +56 -0
  123. package/src/runtime/shared/tool-card-model/displayed-result.mjs +7 -5
  124. package/src/runtime/shared/tool-card-model.mjs +2 -1
  125. package/src/runtime/shared/tool-result-summary.mjs +120 -59
  126. package/src/runtime/shared/turn-review-reply.mjs +63 -0
  127. package/src/runtime/shared/turn-snapshot.mjs +8 -5
  128. package/src/runtime/shared/turn-worktree-snapshot.mjs +56 -19
  129. package/src/session-runtime/boot/apis.mjs +11 -0
  130. package/src/session-runtime/boot/providers.mjs +2 -0
  131. package/src/session-runtime/boot/session.mjs +5 -0
  132. package/src/session-runtime/boot/shared.mjs +2 -3
  133. package/src/session-runtime/native-web-search/tool-shape.mjs +8 -8
  134. package/src/session-runtime/native-web-search.mjs +6 -8
  135. package/src/session-runtime/provider-auth/login.mjs +26 -1
  136. package/src/session-runtime/provider-models/row-hydration.mjs +9 -0
  137. package/src/session-runtime/quick-model-rows/provider-rows.mjs +17 -2
  138. package/src/session-runtime/services/provider-admin.mjs +215 -3
  139. package/src/session-runtime/services/usage-stats-model.mjs +37 -2
  140. package/src/session-runtime/settings-builtin-tools-api.mjs +29 -4
  141. package/src/session-runtime/setup-tool/executor.mjs +10 -1
  142. package/src/session-runtime/webhook-session-run.mjs +0 -9
  143. package/src/standalone/daemon-stored-session-views.mjs +15 -0
  144. package/src/standalone/session-protocol.mjs +4 -0
  145. package/src/standalone/session-service/entries/session-loads.mjs +11 -3
  146. package/src/standalone/session-service/entries.mjs +1 -0
  147. package/src/standalone/session-service/session-calls/session-action.mjs +31 -3
  148. package/src/standalone/session-service/session-calls/session-turns.mjs +2 -1
  149. package/src/standalone/session-service.mjs +2 -0
  150. package/src/tui/app/model-options.mjs +10 -1
  151. package/src/tui/app/usage-context-panels/context-model.mjs +4 -1
  152. package/src/tui/app/usage-context-panels.mjs +3 -1
  153. package/src/tui/app/use-prompt-queue-history/restore-to-prompt.mjs +1 -0
  154. package/src/tui/components/ContextPanel.jsx +2 -0
  155. package/src/tui/dist/index.mjs +144 -70
  156. package/src/tui/paste-attachments.mjs +2 -1
  157. package/src/tui/session/session-api/intake/abort.mjs +35 -3
  158. package/src/tui/session/session-api/integrations.mjs +14 -59
  159. package/src/tui/session/session-api/media.mjs +1 -1
  160. package/src/tui/session/session-flow/queue/take-entries.mjs +21 -11
  161. package/src/tui/session/session-flow/steering.mjs +17 -3
  162. package/src/tui/session/session-flow.mjs +1 -0
  163. package/src/tui/session/turn.mjs +18 -4
  164. package/src/vendor/statusline/src/gateway/route-meta.mjs +2 -0
  165. package/scripts/memory-layer2-check.mjs +0 -186
@@ -78,9 +78,8 @@ const _oauthRefreshes = new Map();
78
78
  // This picks up cross-process refresh_token rotation rather than replaying
79
79
  // a stale single-use token.
80
80
 
81
- // Anthropic OAuth contract for first-party OAuth clients: Opus/Sonnet
82
- // requests are gated on this exact system-prompt prefix. Haiku is not
83
- // gated and ignores this prefix.
81
+ // Anthropic OAuth contract for first-party OAuth clients: Claude requests
82
+ // are gated on this exact system-prompt prefix (Haiku 5.5 included).
84
83
  const CLAUDE_CODE_SYSTEM_PREFIX = "You are Claude Code, Anthropic's official CLI for Claude.";
85
84
  const OAUTH_BETA_HEADERS =
86
85
  'oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,extended-cache-ttl-2025-04-11';
@@ -90,11 +89,9 @@ function logQuiet(line) {
90
89
  }
91
90
 
92
91
  function requiresSystemPrefix(model) {
93
- // High-tier Claude OAuth models require the first-party system prefix for
94
- // OAuth pool routing. Haiku does not; keep every other Claude family (Opus,
95
- // Sonnet, Fable, and future non-Haiku families) on the prefixed path.
96
- const id = String(model || '').toLowerCase();
97
- return /^claude-/.test(id) && !/^claude-haiku(?:-|$)/.test(id);
92
+ // Every Claude OAuth model needs the first-party system prefix for OAuth
93
+ // pool routing; without it the server answers 429 rate_limit_error "Error".
94
+ return /^claude-/.test(String(model || '').toLowerCase());
98
95
  }
99
96
 
100
97
  function buildOAuthBetaHeaders(body, { fastMode = false, toolSearch = false, model, opts = {} } = {}) {
@@ -272,6 +269,8 @@ export function _buildRequestBodyForCacheSmoke(messages, model, tools = [], send
272
269
  // --- Provider ---
273
270
 
274
271
  export class AnthropicOAuthProvider {
272
+ // Takes a PDF as a native document block (decided by this adapter's wire protocol).
273
+ nativePdf = true;
275
274
  // input_tokens EXCLUDES cache_read_input_tokens (separate field) — add the
276
275
  // cache back for the real context footprint. See registry.mjs.
277
276
  static inputExcludesCache = true;
@@ -22,9 +22,12 @@ import { getLlmDispatcher } from '../../../shared/llm/http-agent.mjs';
22
22
  import { loadAnthropic, _normalizeAnthropicModel, _setApiKeyCatalogMirror } from './anthropic-messages.mjs';
23
23
  import { ANTHROPIC_VERSION, MODELS } from './lib/anthropic-models.mjs';
24
24
  import { streamCallbacks } from './lib/send-callbacks.mjs';
25
+ import { preparePdfTextForProvider } from './media-normalization.mjs';
25
26
  export { _test, _toAnthropicMessagesForTest } from './anthropic-messages.mjs';
26
27
 
27
28
  export class AnthropicProvider {
29
+ // Takes a PDF as a native document block (decided by this adapter's wire protocol).
30
+ nativePdf = true;
28
31
  // Anthropic reports usage.input_tokens EXCLUDING cache_read/cache_creation
29
32
  // (those are separate fields), so the live context-window footprint must
30
33
  // add cache_read back. See providerInputExcludesCache() in registry.mjs.
@@ -77,6 +80,9 @@ export class AnthropicProvider {
77
80
  // dispatch with an aborted-mid-flight tool_use would otherwise hit
78
81
  // the provider as a hard 400 (`tool_use ids ... without tool_result`).
79
82
  messages = sanitizeToolPairs(messages);
83
+ // The flag alone decides: an instance that takes no native PDF (a custom
84
+ // gateway, OpenCode Go's Anthropic route) gets the PDF as text.
85
+ messages = await preparePdfTextForProvider(messages, this);
80
86
  try {
81
87
  return await this._doSend(messages, model, tools, sendOpts);
82
88
  } catch (err) {
@@ -11,6 +11,7 @@ import { createServer } from 'node:http';
11
11
  import { randomBytes } from 'node:crypto';
12
12
  import { setTimeout as delay } from 'node:timers/promises';
13
13
  import { createOAuthPkce, parseOAuthCodeInput } from './lib/oauth-pkce.mjs';
14
+ import { OAUTH_PAGE_CONTENT_TYPE, oauthErrorHtml, oauthSuccessHtml } from './lib/oauth-page.mjs';
14
15
  import {
15
16
  AUTH_URL,
16
17
  CALLBACK_HOST,
@@ -34,9 +35,10 @@ import {
34
35
  } from './antigravity-oauth-tokens.mjs';
35
36
 
36
37
  const ONBOARD_INTERVAL_MS = 1_000;
37
- const CALLBACK_FAILURE_HTML =
38
- '<html><body><h2>Antigravity sign-in was not completed.</h2>' +
39
- '<p>Return to Mixdog for the error details and any account verification link.</p></body></html>';
38
+ const CALLBACK_FAILURE_HTML = oauthErrorHtml(
39
+ 'Antigravity sign-in was not completed.',
40
+ 'Return to Mixdog for the error details and any account verification link.'
41
+ );
40
42
 
41
43
  function extractProjectId(payload) {
42
44
  return typeof payload?.cloudaicompanionProject === 'string' ? payload.cloudaicompanionProject : '';
@@ -353,7 +355,7 @@ export async function beginOAuthLogin({
353
355
  const authorizationError = requestUrl.searchParams.get('error');
354
356
  if (authorizationError) {
355
357
  const detail = requestUrl.searchParams.get('error_description') || authorizationError;
356
- res.writeHead(400, { 'Content-Type': 'text/html' });
358
+ res.writeHead(400, { 'Content-Type': OAUTH_PAGE_CONTENT_TYPE });
357
359
  res.end(CALLBACK_FAILURE_HTML);
358
360
  // A late denial must not cancel an already accepted code.
359
361
  if (!exchangePromise) {
@@ -369,12 +371,12 @@ export async function beginOAuthLogin({
369
371
  }
370
372
  try {
371
373
  await acceptCode(code);
372
- res.writeHead(200, { 'Content-Type': 'text/html' });
373
- res.end('<html><body><h2>Antigravity connected.</h2><p>You can close this tab.</p></body></html>');
374
+ res.writeHead(200, { 'Content-Type': OAUTH_PAGE_CONTENT_TYPE });
375
+ res.end(oauthSuccessHtml('Antigravity'));
374
376
  } catch {
375
377
  // The shared exchange reports its error through waitForCallback.
376
378
  // Do not interpolate provider-controlled text into browser HTML.
377
- res.writeHead(500, { 'Content-Type': 'text/html' });
379
+ res.writeHead(500, { 'Content-Type': OAUTH_PAGE_CONTENT_TYPE });
378
380
  res.end(CALLBACK_FAILURE_HTML);
379
381
  }
380
382
  });
@@ -63,6 +63,8 @@ async function storedAuth(options) {
63
63
  }
64
64
 
65
65
  export class AntigravityOAuthProvider {
66
+ // Takes a PDF as a native document block (decided by this adapter's wire protocol).
67
+ nativePdf = true;
66
68
  // usageMetadata.promptTokenCount is the total, cached tokens included.
67
69
  static inputExcludesCache = false;
68
70
  name = 'antigravity-oauth';
@@ -1,6 +1,6 @@
1
1
  import { randomUUID, createHash } from 'node:crypto';
2
2
  import { createProviderReplay, providerReplayItems } from './lib/provider-replay.mjs';
3
- import { toGeminiContents, toGeminiTools, toGeminiToolConfig } from './gemini-schema.mjs';
3
+ import { toGeminiContents, toGeminiRequestTools, toGeminiToolConfig } from './gemini-schema.mjs';
4
4
  import { geminiThinkingConfig } from './gemini-thinking.mjs';
5
5
 
6
6
  const THOUGHT_SIGNATURE_SENTINEL = 'skip_thought_signature_validator';
@@ -105,8 +105,9 @@ export function buildAntigravityRequest(messages, model, tools, opts = {}, proje
105
105
  };
106
106
  const request = { contents };
107
107
  if (systemText) request.systemInstruction = { role: 'user', parts: [{ text: systemText }] };
108
+ const requestTools = toGeminiRequestTools(tools, opts.nativeTools);
109
+ if (requestTools.length) request.tools = requestTools;
108
110
  if (tools?.length) {
109
- request.tools = [toGeminiTools(tools)];
110
111
  request.toolConfig = toGeminiToolConfig(opts.toolChoice) || {
111
112
  functionCallingConfig: { mode: antigravityFunctionCallingMode() },
112
113
  };
@@ -0,0 +1,77 @@
1
+ // Auto effort: turns the effort judge's difficulty distribution into this
2
+ // turn's effort by moving along the model's effort ladder from a base effort.
3
+ // Every turn starts again from the base, so a run of hard turns never
4
+ // ratchets the effort upward.
5
+ //
6
+ // The base is `medium` on every model: raised only when the request needs it,
7
+ // lowered when it is easy. The ladder is the model's supported efforts from
8
+ // `low` up to the tier below the top: `none`/`minimal` switch thinking off and
9
+ // `max`/`ultra` are reserved for an explicit user choice, so auto never picks
10
+ // them. A chosen `max`/`ultra` is left untouched.
11
+
12
+ const EFFORT_ORDER = Object.freeze(['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra']);
13
+ const FLOOR_INDEX = EFFORT_ORDER.indexOf('low');
14
+ const RESERVED_TOP = new Set(['max', 'ultra']);
15
+
16
+ export const AUTO_EFFORT_BASE = 'medium';
17
+
18
+ /** The effort auto moves from: `medium`, unless the user chose `max`/`ultra`. */
19
+ export function autoEffortBase(chosen) {
20
+ const effort = String(chosen ?? '')
21
+ .trim()
22
+ .toLowerCase();
23
+ return RESERVED_TOP.has(effort) ? effort : AUTO_EFFORT_BASE;
24
+ }
25
+
26
+ export const AUTO_EFFORT_MODES = Object.freeze(['off', 'observe', 'on']);
27
+
28
+ // Below this top-class probability the judge is unsure and the default stays.
29
+ export const AUTO_EFFORT_MIN_CONFIDENCE = 0.4;
30
+ // xhigh costs far more than high, so "very hard" needs a firmer judge;
31
+ // below this it is treated as "hard". 0.65 minimised the eval cost with a
32
+ // false xhigh weighted three times a missed one.
33
+ export const AUTO_EFFORT_VERY_HARD_CONFIDENCE = 0.65;
34
+
35
+ // The judge rates four levels, each one ladder step from the base: easy -1
36
+ // (low), normal 0 (medium), hard +1 (high), very hard +2 (xhigh).
37
+ export const AUTO_EFFORT_LEVELS = Object.freeze(['easy', 'normal', 'hard', 'very hard']);
38
+ const STEP_OF_LEVEL = [-1, 0, 1, 2];
39
+
40
+ export function normalizeAutoEffortMode(value) {
41
+ const mode = String(value ?? '')
42
+ .trim()
43
+ .toLowerCase();
44
+ return AUTO_EFFORT_MODES.includes(mode) ? mode : 'off';
45
+ }
46
+
47
+ /** The efforts auto may choose for a model, in ascending order. */
48
+ export function autoEffortLadder(options) {
49
+ const supported = new Set((options || []).map((value) => String(value).trim().toLowerCase()));
50
+ return EFFORT_ORDER.filter(
51
+ (level, index) => supported.has(level) && index >= FLOOR_INDEX && !RESERVED_TOP.has(level)
52
+ );
53
+ }
54
+
55
+ /** Ladder step for a judge distribution over the four levels (index 0 = easy). */
56
+ export function judgedStep(probs) {
57
+ const values = Array.isArray(probs) && probs.length === AUTO_EFFORT_LEVELS.length ? probs.map(Number) : null;
58
+ if (!values || values.some((value) => !Number.isFinite(value))) return { step: 0, level: null, confidence: 0 };
59
+ const confidence = Math.max(...values);
60
+ if (confidence < AUTO_EFFORT_MIN_CONFIDENCE) return { step: 0, level: null, confidence };
61
+ let level = values.indexOf(confidence);
62
+ if (level === 3 && confidence < AUTO_EFFORT_VERY_HARD_CONFIDENCE) level = 2;
63
+ return { step: STEP_OF_LEVEL[level], level, confidence };
64
+ }
65
+
66
+ /**
67
+ * This turn's effort, or null when auto does not apply (the default lies
68
+ * outside the ladder). `effort === base` when the judge keeps the default.
69
+ */
70
+ export function resolveAutoEffort({ base, options, probs }) {
71
+ const ladder = autoEffortLadder(options);
72
+ const at = ladder.indexOf(String(base ?? '').toLowerCase());
73
+ if (at < 0) return null;
74
+ const { step, level, confidence } = judgedStep(probs);
75
+ const effort = ladder[Math.max(0, Math.min(ladder.length - 1, at + step))];
76
+ return { base: ladder[at], effort, step, level, confidence: Number(confidence.toFixed(3)) };
77
+ }
@@ -7,6 +7,7 @@ import {
7
7
  toOpenAIMessages,
8
8
  toOpenAITools,
9
9
  } from './openai-compat-wire.mjs';
10
+ import { preparePdfTextForProvider } from './media-normalization.mjs';
10
11
  import { consumeCompatChatCompletionStream } from './openai-compat-stream.mjs';
11
12
  import { ensureChatToolPairs } from './lib/wire-pairing.mjs';
12
13
  import { cursorTokenExpiry, exchangeCursorToken, resolveCursorOAuthAccessToken } from './cursor-auth.mjs';
@@ -28,10 +29,10 @@ async function loadCursorRuntime() {
28
29
  return runtimePromise;
29
30
  }
30
31
 
31
- function toCursorMessages(messages, providerName) {
32
+ function toCursorMessages(messages, providerName, { nativePdf } = {}) {
32
33
  const output = [];
33
34
  for (const message of messages || []) {
34
- const converted = toOpenAIMessages([message], providerName);
35
+ const converted = toOpenAIMessages([message], providerName, { nativePdf });
35
36
  if (message?.role !== 'tool') {
36
37
  output.push(...converted);
37
38
  continue;
@@ -544,6 +545,9 @@ function selectCursorVariant(group, { effort = null, fast = false } = {}) {
544
545
  }
545
546
 
546
547
  class CursorProviderBase {
548
+ // Cursor's chat wire has no document block: PDFs lower to text.
549
+ nativePdf = false;
550
+
547
551
  static inputExcludesCache = false;
548
552
  name;
549
553
  config;
@@ -614,7 +618,7 @@ class CursorProviderBase {
614
618
  // Wire-level pairing guard: a call whose result never committed
615
619
  // (cancel/abort) is hard-rejected unpaired, so synthesize the
616
620
  // missing tool messages on the assembled array.
617
- messages: ensureChatToolPairs(toCursorMessages(messages, this.name)),
621
+ messages: ensureChatToolPairs(toCursorMessages(messages, this.name, { nativePdf: this.nativePdf })),
618
622
  mixdog_session_id: sessionScope,
619
623
  stream: true,
620
624
  stream_options: { include_usage: true },
@@ -690,10 +694,12 @@ class CursorProviderBase {
690
694
  sendOpts.sessionId || sendOpts.providerCacheKey || sendOpts.promptCacheKey || `cursor-call:${randomUUID()}`
691
695
  );
692
696
  const openAiTools = tools?.length ? toOpenAITools(tools) : undefined;
693
- const dispatch = () => {
697
+ const dispatch = async () => {
694
698
  noteRequestServiceTier(cursorSelectionIsFast(cursorSelection) ? 'fast' : '');
699
+ // Every attempt (retries included) prepares right before it lowers.
700
+ const prepared = await preparePdfTextForProvider(messages, this);
695
701
  return this._dispatchChat(
696
- this._chatBody(messages, { cursorSelection, sessionScope, openAiTools, toolChoice: sendOpts.toolChoice }),
702
+ this._chatBody(prepared, { cursorSelection, sessionScope, openAiTools, toolChoice: sendOpts.toolChoice }),
697
703
  { runtime, accessToken, signal, sendOpts, openAiTools }
698
704
  );
699
705
  };
@@ -0,0 +1,165 @@
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 Object.assign(new Error(`${provider.name} models ${res.status}`), { status: 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
+ // An unknown third-party gateway behind the Anthropic wire: PDFs lower to text.
147
+ provider.nativePdf = false;
148
+ } else {
149
+ const { OpenAICompatProvider } = await import('./openai-compat.mjs');
150
+ const Base =
151
+ cfg.protocol === 'openai-responses'
152
+ ? class extends OpenAICompatProvider {
153
+ _doSend(messages, model, tools, sendOpts) {
154
+ return super._doSend(messages, model, tools, { ...(sendOpts || {}), compatWireApi: 'responses' });
155
+ }
156
+ }
157
+ : OpenAICompatProvider;
158
+ const Custom = withConfiguredModels(Base, (self) => listCompatModels(self, { throwOnError: true }));
159
+ provider = new Custom(providerId, engine);
160
+ if (cfg.models.length) provider.defaultModel = cfg.models[0].id;
161
+ }
162
+ provider._customModels = cfg.models.map((m) => modelRow(providerId, m));
163
+ provider.customConfig = cfg;
164
+ return provider;
165
+ }
@@ -88,11 +88,13 @@ function validSnapshot(value, provider, model, mode) {
88
88
  );
89
89
  }
90
90
 
91
- export function prepareTurnEffortConfiguration(session, provider) {
91
+ // `turnEffort` overrides the session's saved effort for this turn only (auto
92
+ // effort); the saved default itself is never changed here.
93
+ export function prepareTurnEffortConfiguration(session, provider, turnEffort) {
92
94
  const config = provider?.config || {};
93
95
  const opts = { ...config, modelParameters: session.modelParameters || {} };
94
96
  const mode = effortConfigurationMode(session.provider, session.model, opts);
95
- const effort = normalizedEffort(session.provider, session.model, session.effort);
97
+ const effort = normalizedEffort(session.provider, session.model, turnEffort ?? session.effort);
96
98
  if (!mode || !EFFORTS.has(effort)) return null;
97
99
  const first = (session.messages || []).find((message) =>
98
100
  validSnapshot(message?.meta?.[META_KEY], session.provider, session.model, mode)
@@ -9,7 +9,7 @@
9
9
  * gemini-schema.mjs.
10
10
  */
11
11
  import { geminiThinkingConfig } from './gemini-thinking.mjs';
12
- import { toGeminiContents, toGeminiNativeTools, toGeminiToolConfig, toGeminiTools } from './gemini-schema.mjs';
12
+ import { toGeminiContents, toGeminiRequestTools, toGeminiToolConfig } from './gemini-schema.mjs';
13
13
 
14
14
  // Request pieces shared by the cached REST path and the SDK path.
15
15
  export function buildGeminiRequest(messages, useModel, tools, opts) {
@@ -29,11 +29,9 @@ export function buildGeminiRequest(messages, useModel, tools, opts) {
29
29
  const contents = toGeminiContents(chatMsgs, useModel);
30
30
  if (!contents.length) throw new Error('No messages to send');
31
31
 
32
- const nativeGeminiTools = toGeminiNativeTools(opts.nativeTools);
33
- const functionGeminiTools = tools?.length ? [toGeminiTools(tools)] : [];
34
- const geminiTools =
35
- nativeGeminiTools.length || functionGeminiTools.length ? [...nativeGeminiTools, ...functionGeminiTools] : undefined;
36
- const toolConfig = functionGeminiTools.length ? toGeminiToolConfig(opts.toolChoice) : undefined;
32
+ const requestTools = toGeminiRequestTools(tools, opts.nativeTools);
33
+ const geminiTools = requestTools.length ? requestTools : undefined;
34
+ const toolConfig = tools?.length ? toGeminiToolConfig(opts.toolChoice) : undefined;
37
35
  return { generationConfig, systemInstruction, contents, geminiTools, toolConfig };
38
36
  }
39
37
 
@@ -593,6 +593,13 @@ export function toGeminiNativeTools(nativeTools) {
593
593
  return out;
594
594
  }
595
595
 
596
+ // The request `tools` array: hosted tools first, then one function-declaration
597
+ // entry when function tools are present. Empty when neither is present.
598
+ export function toGeminiRequestTools(tools, nativeTools) {
599
+ const functionTools = tools?.length ? [toGeminiTools(tools)] : [];
600
+ return [...toGeminiNativeTools(nativeTools), ...functionTools];
601
+ }
602
+
596
603
  export function collectGeminiGroundingSources(candidate) {
597
604
  const out = [];
598
605
  const seen = new Set();
@@ -131,6 +131,8 @@ function throwIfGeminiAborted(signal) {
131
131
  }
132
132
 
133
133
  export class GeminiProvider {
134
+ // Takes a PDF as a native document block (decided by this adapter's wire protocol).
135
+ nativePdf = true;
134
136
  // promptTokenCount is the total (cachedContentTokenCount is a subset), so
135
137
  // input already includes cache. See registry.mjs.
136
138
  static inputExcludesCache = false;
@@ -4,6 +4,7 @@ import { createServer } from 'node:http';
4
4
  import { randomBytes } from 'node:crypto';
5
5
  import { getLlmDispatcher } from '../../../shared/llm/http-agent.mjs';
6
6
  import { createOAuthPkce, parseOAuthCodeInput } from './lib/oauth-pkce.mjs';
7
+ import { OAUTH_PAGE_CONTENT_TYPE, oauthSuccessHtml } from './lib/oauth-page.mjs';
7
8
  import {
8
9
  CLIENT_ID,
9
10
  SCOPE,
@@ -119,8 +120,8 @@ export async function beginOAuthLogin() {
119
120
  res.end('Invalid');
120
121
  return;
121
122
  }
122
- res.writeHead(200, { 'Content-Type': 'text/html' });
123
- res.end('<html><body><h2>Grok login successful! You can close this tab.</h2></body></html>');
123
+ res.writeHead(200, { 'Content-Type': OAUTH_PAGE_CONTENT_TYPE });
124
+ res.end(oauthSuccessHtml('Grok'));
124
125
  try {
125
126
  const tokens = await exchangeAuthorizationCode({ discovery, pkce, code });
126
127
  finish(tokens);
@@ -259,6 +259,8 @@ async function ensureLatestGrokModel(provider) {
259
259
  let _modelRefreshInFlight = null;
260
260
 
261
261
  export class GrokOAuthProvider {
262
+ // No document block on this wire: a PDF is lowered to text before the request is built.
263
+ nativePdf = false;
262
264
  // OpenAI-compatible usage: prompt_tokens includes cached. See registry.mjs.
263
265
  static inputExcludesCache = false;
264
266
  name = 'grok-oauth';
@@ -2,14 +2,16 @@ import {
2
2
  API_IMAGE_MAX_BASE64_SIZE,
3
3
  IMAGE_MAX_WIDTH,
4
4
  IMAGE_MAX_HEIGHT,
5
- imageMetadataText,
6
5
  resizeImageBuffer,
6
+ supersedingImageMetadataText,
7
7
  } from '../../tools/builtin/read-image-resize.mjs';
8
8
 
9
+ import { isEnvironmentError } from '../../../../shared/environment-error.mjs';
10
+
9
11
  const MAX_INPUT_BASE64_SIZE = Math.ceil((64 * 1024 * 1024) / 3) * 4;
10
12
 
11
13
  export class AnthropicImagePreparationError extends Error {
12
- constructor(path, detail, cause) {
14
+ constructor(path, detail, cause, failingImageData) {
13
15
  super(
14
16
  `Anthropic image preparation failed at ${path}: ${detail}. The original image and conversation were not changed.`,
15
17
  { cause }
@@ -17,12 +19,28 @@ export class AnthropicImagePreparationError extends Error {
17
19
  this.name = 'AnthropicImagePreparationError';
18
20
  this.code = 'ANTHROPIC_IMAGE_PREPARATION_FAILED';
19
21
  this.status = 400;
22
+ // Identifies the rejected image for recovery; never part of the message.
23
+ Object.defineProperty(this, 'failingImageData', { value: failingImageData, enumerable: false });
24
+ }
25
+ }
26
+
27
+ // The image may be fine; this machine cannot resize it (broken sharp runtime,
28
+ // out of memory...). Not an image rejection: recovery must not strip anything.
29
+ export class AnthropicImageRuntimeError extends Error {
30
+ constructor(path, cause) {
31
+ super(
32
+ `Anthropic image preparation unavailable at ${path}: image resizing is unavailable; repair the sharp runtime. The original image and conversation were not changed.`,
33
+ { cause }
34
+ );
35
+ this.name = 'AnthropicImageRuntimeError';
36
+ this.code = 'ANTHROPIC_IMAGE_RUNTIME_UNAVAILABLE';
37
+ this.status = 400;
20
38
  }
21
39
  }
22
40
 
23
41
  // Work on the lowered request, not stored history: this covers attachments,
24
- // tool results, replayed images, and provider switches alike. Keep the 2000px
25
- // ceiling independent of image count so crossing 20 images never invalidates
42
+ // tool results, replayed images, and provider switches alike. Keep the
43
+ // IMAGE_MAX_WIDTH x IMAGE_MAX_HEIGHT display box (1568px) independent of image count so crossing 20 images never invalidates
26
44
  // an earlier image or changes its cached rendition.
27
45
  export async function prepareAnthropicImages(messages, { signal } = {}) {
28
46
  if (!Array.isArray(messages)) return messages;
@@ -64,12 +82,16 @@ export async function prepareAnthropicImages(messages, { signal } = {}) {
64
82
  { profile: 'anthropic' }
65
83
  );
66
84
  } catch (error) {
67
- throw new AnthropicImagePreparationError(partPath, 'image could not be decoded; reattach a valid image', error);
85
+ if (isEnvironmentError(error)) throw new AnthropicImageRuntimeError(partPath, error);
86
+ throw new AnthropicImagePreparationError(
87
+ partPath,
88
+ 'image could not be decoded; reattach a valid image',
89
+ error,
90
+ source.data
91
+ );
68
92
  }
69
93
  signal?.throwIfAborted();
70
- if (!resized) {
71
- throw new AnthropicImagePreparationError(partPath, 'image resizing is unavailable; repair the sharp runtime');
72
- }
94
+ if (!resized) throw new AnthropicImageRuntimeError(partPath);
73
95
  const dims = resized.dimensions;
74
96
  if (
75
97
  !dims?.displayWidth ||
@@ -80,7 +102,9 @@ export async function prepareAnthropicImages(messages, { signal } = {}) {
80
102
  ) {
81
103
  throw new AnthropicImagePreparationError(
82
104
  partPath,
83
- 'image still exceeds the supported size; use a smaller image'
105
+ 'image still exceeds the supported size; use a smaller image',
106
+ undefined,
107
+ source.data
84
108
  );
85
109
  }
86
110
  if (resized.data === source.data && resized.mimeType === source.media_type) {
@@ -89,10 +113,7 @@ export async function prepareAnthropicImages(messages, { signal } = {}) {
89
113
  }
90
114
  changed = true;
91
115
  if (dims.originalWidth !== dims.displayWidth || dims.originalHeight !== dims.displayHeight) {
92
- next.push({
93
- type: 'text',
94
- text: `${imageMetadataText(dims)} These display dimensions supersede any earlier display-size annotation for the following image.`,
95
- });
116
+ next.push({ type: 'text', text: supersedingImageMetadataText(dims) });
96
117
  }
97
118
  // Cache markers stay attached to the same logical block. No
98
119
  // sanitizer or reordering runs after this byte-preparation pass.