mixdog 1.0.10 → 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 (155) 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/mcp/client.mjs +234 -12
  10. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +22 -5
  11. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +7 -8
  12. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +6 -0
  13. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +9 -7
  14. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +2 -0
  15. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +3 -2
  16. package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +77 -0
  17. package/src/runtime/agent/orchestrator/providers/cursor.mjs +11 -5
  18. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +3 -1
  19. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +4 -2
  20. package/src/runtime/agent/orchestrator/providers/gemini-request-body.mjs +4 -6
  21. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +7 -0
  22. package/src/runtime/agent/orchestrator/providers/gemini.mjs +2 -0
  23. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +3 -2
  24. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +2 -0
  25. package/src/runtime/agent/orchestrator/providers/lib/anthropic-image-input.mjs +34 -13
  26. package/src/runtime/agent/orchestrator/providers/lib/oauth-page.mjs +94 -0
  27. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +670 -198
  28. package/src/runtime/agent/orchestrator/providers/mixdog-local.mjs +9 -3
  29. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +3 -1
  30. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +1 -1
  31. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +4 -0
  32. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +14 -8
  33. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +7 -4
  34. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +24 -9
  35. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +2 -0
  36. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +3 -2
  37. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +2 -0
  38. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -0
  39. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +4 -0
  40. package/src/runtime/agent/orchestrator/providers/registry.mjs +10 -0
  41. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +9 -1
  42. package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +13 -11
  43. package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +17 -9
  44. package/src/runtime/agent/orchestrator/session/cache/scoped-cache-outcome.mjs +23 -1
  45. package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +40 -71
  46. package/src/runtime/agent/orchestrator/session/cache/tool-result-reuse.mjs +27 -0
  47. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +8 -7
  48. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +54 -12
  49. package/src/runtime/agent/orchestrator/session/context-utils.mjs +27 -5
  50. package/src/runtime/agent/orchestrator/session/eager-dispatch/admission.mjs +8 -51
  51. package/src/runtime/agent/orchestrator/session/image-strip-recovery.mjs +55 -2
  52. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +23 -5
  53. package/src/runtime/agent/orchestrator/session/loop/request-boundary.mjs +2 -0
  54. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +62 -0
  55. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +79 -0
  56. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +10 -2
  57. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +8 -1
  58. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +6 -3
  59. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +10 -3
  60. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +5 -1
  61. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +21 -5
  62. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +20 -4
  63. package/src/runtime/agent/orchestrator/session/store/live-state.mjs +7 -6
  64. package/src/runtime/agent/orchestrator/session/store/serialize.mjs +6 -3
  65. package/src/runtime/agent/orchestrator/session/task-wait-control.mjs +13 -0
  66. package/src/runtime/agent/orchestrator/session/tool-batch/execute-call.mjs +9 -19
  67. package/src/runtime/agent/orchestrator/session/tool-batch/finalize.mjs +85 -2
  68. package/src/runtime/agent/orchestrator/session/tool-batch/pre-dispatch.mjs +10 -6
  69. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +3 -0
  70. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -1
  71. package/src/runtime/agent/orchestrator/tools/builtin/find-files-tool.mjs +3 -3
  72. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-path-fanout.mjs +2 -1
  73. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-pattern-fanout.mjs +2 -1
  74. package/src/runtime/agent/orchestrator/tools/builtin/lib/scoped-search-flight.mjs +20 -0
  75. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +51 -28
  76. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +3 -2
  77. package/src/runtime/agent/orchestrator/tools/builtin/pdf-test-fixtures.mjs +47 -0
  78. package/src/runtime/agent/orchestrator/tools/builtin/read-single-media.mjs +1 -1
  79. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +34 -30
  80. package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +10 -5
  81. package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +18 -7
  82. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +1 -1
  83. package/src/runtime/agent/orchestrator/tools/builtin.mjs +9 -2
  84. package/src/runtime/agent/orchestrator/tools/code-graph/aggregate-roots.mjs +3 -1
  85. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/federation.mjs +3 -1
  86. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/work.mjs +11 -6
  87. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +13 -1
  88. package/src/runtime/attachments/limits.mjs +20 -0
  89. package/src/runtime/{agent/orchestrator/tools/builtin/read-office-files.mjs → attachments/office-extract.mjs} +356 -125
  90. package/src/runtime/attachments/pdf-extract.mjs +2 -2
  91. package/src/runtime/attachments/store.mjs +232 -72
  92. package/src/runtime/browser-bridge/client.mjs +3 -3
  93. package/src/runtime/effort-judge/compact-tokenizer.mjs +282 -0
  94. package/src/runtime/effort-judge/judge-client.mjs +194 -0
  95. package/src/runtime/effort-judge/judge-worker.mjs +106 -0
  96. package/src/runtime/local-provider/input-capabilities.mjs +30 -12
  97. package/src/runtime/media/jobs.mjs +0 -1
  98. package/src/runtime/media/media-usage.mjs +2 -1
  99. package/src/runtime/memory/lib/embedding-model-config.mjs +6 -6
  100. package/src/runtime/memory/lib/embedding-model-files.mjs +44 -0
  101. package/src/runtime/memory/lib/embedding-tokenizer.mjs +253 -0
  102. package/src/runtime/memory/lib/embedding-worker.mjs +97 -103
  103. package/src/runtime/memory/lib/memory-chunk-quality.mjs +40 -218
  104. package/src/runtime/memory/lib/memory-embed.mjs +0 -9
  105. package/src/runtime/memory/lib/memory.mjs +34 -23
  106. package/src/runtime/memory/tool-defs.mjs +2 -1
  107. package/src/runtime/shared/automation-attachments.mjs +9 -7
  108. package/src/runtime/shared/environment-error.mjs +14 -0
  109. package/src/runtime/shared/error-presentation.mjs +10 -2
  110. package/src/runtime/shared/inline-file-kind.mjs +28 -2
  111. package/src/runtime/shared/llm/model-catalog.mjs +17 -3
  112. package/src/runtime/shared/llm/quota-value-estimate.mjs +33 -18
  113. package/src/runtime/shared/llm/usage-accounting.mjs +8 -5
  114. package/src/runtime/shared/llm/usage-ledger-quota.mjs +29 -10
  115. package/src/runtime/shared/llm/usage-ledger-rollup.mjs +47 -4
  116. package/src/runtime/shared/read-image-resize.mjs +56 -0
  117. package/src/runtime/shared/tool-card-model/displayed-result.mjs +7 -5
  118. package/src/runtime/shared/tool-card-model.mjs +2 -1
  119. package/src/runtime/shared/tool-result-summary.mjs +120 -59
  120. package/src/runtime/shared/turn-review-reply.mjs +63 -0
  121. package/src/runtime/shared/turn-snapshot.mjs +8 -5
  122. package/src/runtime/shared/turn-worktree-snapshot.mjs +56 -19
  123. package/src/session-runtime/boot/apis.mjs +5 -0
  124. package/src/session-runtime/boot/providers.mjs +2 -0
  125. package/src/session-runtime/boot/session.mjs +5 -0
  126. package/src/session-runtime/boot/shared.mjs +2 -3
  127. package/src/session-runtime/native-web-search/tool-shape.mjs +8 -8
  128. package/src/session-runtime/native-web-search.mjs +6 -8
  129. package/src/session-runtime/provider-models/row-hydration.mjs +9 -0
  130. package/src/session-runtime/services/provider-admin.mjs +17 -3
  131. package/src/session-runtime/services/usage-stats-model.mjs +37 -2
  132. package/src/session-runtime/settings-builtin-tools-api.mjs +29 -4
  133. package/src/session-runtime/setup-tool/executor.mjs +10 -1
  134. package/src/session-runtime/webhook-session-run.mjs +0 -9
  135. package/src/standalone/daemon-stored-session-views.mjs +15 -0
  136. package/src/standalone/session-service/entries/session-loads.mjs +11 -3
  137. package/src/standalone/session-service/entries.mjs +1 -0
  138. package/src/standalone/session-service/session-calls/session-action.mjs +31 -3
  139. package/src/standalone/session-service/session-calls/session-turns.mjs +2 -1
  140. package/src/standalone/session-service.mjs +2 -0
  141. package/src/tui/app/usage-context-panels/context-model.mjs +4 -1
  142. package/src/tui/app/usage-context-panels.mjs +3 -1
  143. package/src/tui/app/use-prompt-queue-history/restore-to-prompt.mjs +1 -0
  144. package/src/tui/components/ContextPanel.jsx +2 -0
  145. package/src/tui/dist/index.mjs +135 -69
  146. package/src/tui/paste-attachments.mjs +2 -1
  147. package/src/tui/session/session-api/intake/abort.mjs +35 -3
  148. package/src/tui/session/session-api/integrations.mjs +2 -59
  149. package/src/tui/session/session-api/media.mjs +1 -1
  150. package/src/tui/session/session-flow/queue/take-entries.mjs +21 -11
  151. package/src/tui/session/session-flow/steering.mjs +17 -3
  152. package/src/tui/session/session-flow.mjs +1 -0
  153. package/src/tui/session/turn.mjs +18 -4
  154. package/src/vendor/statusline/src/gateway/route-meta.mjs +2 -0
  155. package/scripts/memory-layer2-check.mjs +0 -186
@@ -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
  };
@@ -114,7 +114,7 @@ async function discoverAnthropicModels(provider) {
114
114
  signal: AbortSignal.timeout(10_000),
115
115
  headers: { 'x-api-key': provider.apiKey || '', 'anthropic-version': ANTHROPIC_VERSION },
116
116
  });
117
- if (!res.ok) throw new Error(`${provider.name} models ${res.status}`);
117
+ if (!res.ok) throw Object.assign(new Error(`${provider.name} models ${res.status}`), { status: res.status });
118
118
  const data = await res.json();
119
119
  const items = Array.isArray(data?.data) ? data.data : [];
120
120
  return items
@@ -143,6 +143,8 @@ export async function createCustomProvider(id, config) {
143
143
  const Custom = withConfiguredModels(AnthropicProvider, discoverAnthropicModels);
144
144
  // The Anthropic SDK appends /v1/messages itself; accept a /v1 base URL.
145
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;
146
148
  } else {
147
149
  const { OpenAICompatProvider } = await import('./openai-compat.mjs');
148
150
  const Base =
@@ -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.
@@ -0,0 +1,94 @@
1
+ // Shared browser pages for the loopback OAuth callbacks (success / failure).
2
+
3
+ const LOGO_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="44 44 168 168" aria-hidden="true">
4
+ <defs>
5
+ <linearGradient id="titanium-a" x1="0%" y1="0%" x2="100%" y2="100%">
6
+ <stop offset="0%" stop-color="#ffffff"/><stop offset="50%" stop-color="#94a3b8"/><stop offset="100%" stop-color="#ffffff"/>
7
+ </linearGradient>
8
+ <linearGradient id="titanium-b" x1="0%" y1="0%" x2="100%" y2="100%">
9
+ <stop offset="0%" stop-color="#cbd5e1"/><stop offset="50%" stop-color="#475569"/><stop offset="100%" stop-color="#cbd5e1"/>
10
+ </linearGradient>
11
+ <linearGradient id="titanium-c" x1="0%" y1="0%" x2="100%" y2="100%">
12
+ <stop offset="0%" stop-color="#94a3b8"/><stop offset="50%" stop-color="#334155"/><stop offset="100%" stop-color="#94a3b8"/>
13
+ </linearGradient>
14
+ </defs>
15
+ <g fill="none" stroke-width="22" stroke-linecap="round">
16
+ <path d="M116.2 61A68 68 0 0 1 191.9 104.7" stroke="url(#titanium-a)"/>
17
+ <path d="M116.2 61A68 68 0 0 1 191.9 104.7" transform="rotate(120 128 128)" stroke="url(#titanium-b)"/>
18
+ <path d="M116.2 61A68 68 0 0 1 191.9 104.7" transform="rotate(240 128 128)" stroke="url(#titanium-c)"/>
19
+ </g>
20
+ <polygon points="128,112 133,123 144,128 133,133 128,144 123,133 112,128 123,123" fill="#cbd5e1"/>
21
+ <circle cx="128" cy="128" r="3.5" fill="#07080b"/>
22
+ <circle cx="128" cy="128" r="1.5" fill="#ffffff"/>
23
+ </svg>`;
24
+
25
+ function escapeHtml(value) {
26
+ return String(value)
27
+ .replaceAll('&', '&amp;')
28
+ .replaceAll('<', '&lt;')
29
+ .replaceAll('>', '&gt;')
30
+ .replaceAll('"', '&quot;')
31
+ .replaceAll("'", '&#39;');
32
+ }
33
+
34
+ function renderPage({ heading, message, details }) {
35
+ const title = escapeHtml(heading);
36
+ return `<!doctype html>
37
+ <html lang="en">
38
+ <head>
39
+ <meta charset="utf-8" />
40
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
41
+ <title>${title} · Mixdog</title>
42
+ <style>
43
+ * { box-sizing: border-box; }
44
+ html { color-scheme: dark; }
45
+ body {
46
+ margin: 0;
47
+ min-height: 100vh;
48
+ display: flex;
49
+ align-items: center;
50
+ justify-content: center;
51
+ padding: 24px;
52
+ background: #07080b;
53
+ color: #f8fafc;
54
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
55
+ text-align: center;
56
+ }
57
+ main { width: 100%; max-width: 560px; display: flex; flex-direction: column; align-items: center; }
58
+ .logo { width: 72px; height: 72px; margin-bottom: 24px; }
59
+ .logo svg { width: 100%; height: 100%; display: block; }
60
+ h1 { margin: 0 0 10px; font-size: 28px; line-height: 1.15; font-weight: 650; }
61
+ p { margin: 0; line-height: 1.7; color: #94a3b8; font-size: 15px; }
62
+ .details {
63
+ margin-top: 16px;
64
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
65
+ font-size: 13px;
66
+ color: #94a3b8;
67
+ white-space: pre-wrap;
68
+ word-break: break-word;
69
+ }
70
+ </style>
71
+ </head>
72
+ <body>
73
+ <main>
74
+ <div class="logo">${LOGO_SVG}</div>
75
+ <h1>${title}</h1>
76
+ <p>${escapeHtml(message)}</p>
77
+ ${details ? `<div class="details">${escapeHtml(details)}</div>` : ''}
78
+ </main>
79
+ </body>
80
+ </html>`;
81
+ }
82
+
83
+ export const OAUTH_PAGE_CONTENT_TYPE = 'text/html; charset=utf-8';
84
+
85
+ export function oauthSuccessHtml(providerName) {
86
+ return renderPage({
87
+ heading: 'Authentication successful',
88
+ message: `Signed in to ${providerName}. You can close this tab and return to Mixdog.`,
89
+ });
90
+ }
91
+
92
+ export function oauthErrorHtml(message, details) {
93
+ return renderPage({ heading: 'Authentication failed', message, details });
94
+ }