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.
- package/package.json +3 -3
- package/src/rules/agent/40-cycle1-agent.md +19 -23
- package/src/rules/lead/LEAD.md +5 -5
- package/src/rules/shared/05-tool-calls.md +6 -5
- package/src/rules/shared/60-verification.md +2 -2
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +37 -13
- package/src/runtime/agent/orchestrator/agent-runtime/title-completion.mjs +3 -0
- package/src/runtime/agent/orchestrator/cache-break-trace.mjs +6 -0
- package/src/runtime/agent/orchestrator/config-providers-merge.mjs +8 -0
- package/src/runtime/agent/orchestrator/mcp/client.mjs +234 -12
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +22 -5
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +7 -8
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +6 -0
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +9 -7
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +3 -2
- package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +77 -0
- package/src/runtime/agent/orchestrator/providers/cursor.mjs +11 -5
- package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +165 -0
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +4 -2
- package/src/runtime/agent/orchestrator/providers/gemini-request-body.mjs +4 -6
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +3 -2
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-image-input.mjs +34 -13
- package/src/runtime/agent/orchestrator/providers/lib/oauth-page.mjs +94 -0
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +670 -198
- package/src/runtime/agent/orchestrator/providers/mixdog-local.mjs +9 -3
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +23 -4
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +4 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +4 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +14 -8
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +7 -4
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +24 -9
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +3 -28
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +3 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +4 -0
- package/src/runtime/agent/orchestrator/providers/provider-base-url.mjs +24 -0
- package/src/runtime/agent/orchestrator/providers/registry.mjs +24 -4
- package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +9 -1
- package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +3 -2
- package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +13 -11
- package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +17 -9
- package/src/runtime/agent/orchestrator/session/cache/scoped-cache-outcome.mjs +23 -1
- package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +40 -71
- package/src/runtime/agent/orchestrator/session/cache/tool-result-reuse.mjs +27 -0
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +8 -7
- package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +54 -12
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +27 -5
- package/src/runtime/agent/orchestrator/session/eager-dispatch/admission.mjs +8 -51
- package/src/runtime/agent/orchestrator/session/image-strip-recovery.mjs +55 -2
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +23 -5
- package/src/runtime/agent/orchestrator/session/loop/request-boundary.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +62 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +79 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +10 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +8 -1
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +6 -3
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +21 -5
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +20 -4
- package/src/runtime/agent/orchestrator/session/store/live-state.mjs +7 -6
- package/src/runtime/agent/orchestrator/session/store/serialize.mjs +6 -3
- package/src/runtime/agent/orchestrator/session/task-wait-control.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/tool-batch/execute-call.mjs +9 -19
- package/src/runtime/agent/orchestrator/session/tool-batch/finalize.mjs +85 -2
- package/src/runtime/agent/orchestrator/session/tool-batch/pre-dispatch.mjs +10 -6
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +3 -0
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/builtin/find-files-tool.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-path-fanout.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-pattern-fanout.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/lib/scoped-search-flight.mjs +20 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +51 -28
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/builtin/pdf-test-fixtures.mjs +47 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-media.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +34 -30
- package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +10 -5
- package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +18 -7
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +9 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/aggregate-roots.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/federation.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/work.mjs +11 -6
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +13 -1
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +15 -1
- package/src/runtime/attachments/limits.mjs +20 -0
- package/src/runtime/{agent/orchestrator/tools/builtin/read-office-files.mjs → attachments/office-extract.mjs} +356 -125
- package/src/runtime/attachments/pdf-extract.mjs +2 -2
- package/src/runtime/attachments/store.mjs +232 -72
- package/src/runtime/browser-bridge/client.mjs +3 -3
- package/src/runtime/effort-judge/compact-tokenizer.mjs +282 -0
- package/src/runtime/effort-judge/judge-client.mjs +194 -0
- package/src/runtime/effort-judge/judge-worker.mjs +106 -0
- package/src/runtime/local-provider/input-capabilities.mjs +30 -12
- package/src/runtime/media/jobs.mjs +0 -1
- package/src/runtime/media/media-usage.mjs +2 -1
- package/src/runtime/memory/lib/embedding-model-config.mjs +6 -6
- package/src/runtime/memory/lib/embedding-model-files.mjs +44 -0
- package/src/runtime/memory/lib/embedding-tokenizer.mjs +253 -0
- package/src/runtime/memory/lib/embedding-worker.mjs +97 -103
- package/src/runtime/memory/lib/memory-chunk-quality.mjs +40 -218
- package/src/runtime/memory/lib/memory-embed.mjs +0 -9
- package/src/runtime/memory/lib/memory.mjs +34 -23
- package/src/runtime/memory/tool-defs.mjs +2 -1
- package/src/runtime/shared/automation-attachments.mjs +9 -7
- package/src/runtime/shared/environment-error.mjs +14 -0
- package/src/runtime/shared/error-presentation.mjs +10 -2
- package/src/runtime/shared/inline-file-kind.mjs +28 -2
- package/src/runtime/shared/llm/model-catalog.mjs +17 -3
- package/src/runtime/shared/llm/quota-value-estimate.mjs +33 -18
- package/src/runtime/shared/llm/usage-accounting.mjs +8 -5
- package/src/runtime/shared/llm/usage-ledger-quota.mjs +29 -10
- package/src/runtime/shared/llm/usage-ledger-rollup.mjs +47 -4
- package/src/runtime/shared/model-alias.mjs +5 -0
- package/src/runtime/shared/read-image-resize.mjs +56 -0
- package/src/runtime/shared/tool-card-model/displayed-result.mjs +7 -5
- package/src/runtime/shared/tool-card-model.mjs +2 -1
- package/src/runtime/shared/tool-result-summary.mjs +120 -59
- package/src/runtime/shared/turn-review-reply.mjs +63 -0
- package/src/runtime/shared/turn-snapshot.mjs +8 -5
- package/src/runtime/shared/turn-worktree-snapshot.mjs +56 -19
- package/src/session-runtime/boot/apis.mjs +11 -0
- package/src/session-runtime/boot/providers.mjs +2 -0
- package/src/session-runtime/boot/session.mjs +5 -0
- package/src/session-runtime/boot/shared.mjs +2 -3
- package/src/session-runtime/native-web-search/tool-shape.mjs +8 -8
- package/src/session-runtime/native-web-search.mjs +6 -8
- package/src/session-runtime/provider-auth/login.mjs +26 -1
- package/src/session-runtime/provider-models/row-hydration.mjs +9 -0
- package/src/session-runtime/quick-model-rows/provider-rows.mjs +17 -2
- package/src/session-runtime/services/provider-admin.mjs +215 -3
- package/src/session-runtime/services/usage-stats-model.mjs +37 -2
- package/src/session-runtime/settings-builtin-tools-api.mjs +29 -4
- package/src/session-runtime/setup-tool/executor.mjs +10 -1
- package/src/session-runtime/webhook-session-run.mjs +0 -9
- package/src/standalone/daemon-stored-session-views.mjs +15 -0
- package/src/standalone/session-protocol.mjs +4 -0
- package/src/standalone/session-service/entries/session-loads.mjs +11 -3
- package/src/standalone/session-service/entries.mjs +1 -0
- package/src/standalone/session-service/session-calls/session-action.mjs +31 -3
- package/src/standalone/session-service/session-calls/session-turns.mjs +2 -1
- package/src/standalone/session-service.mjs +2 -0
- package/src/tui/app/model-options.mjs +10 -1
- package/src/tui/app/usage-context-panels/context-model.mjs +4 -1
- package/src/tui/app/usage-context-panels.mjs +3 -1
- package/src/tui/app/use-prompt-queue-history/restore-to-prompt.mjs +1 -0
- package/src/tui/components/ContextPanel.jsx +2 -0
- package/src/tui/dist/index.mjs +144 -70
- package/src/tui/paste-attachments.mjs +2 -1
- package/src/tui/session/session-api/intake/abort.mjs +35 -3
- package/src/tui/session/session-api/integrations.mjs +14 -59
- package/src/tui/session/session-api/media.mjs +1 -1
- package/src/tui/session/session-flow/queue/take-entries.mjs +21 -11
- package/src/tui/session/session-flow/steering.mjs +17 -3
- package/src/tui/session/session-flow.mjs +1 -0
- package/src/tui/session/turn.mjs +18 -4
- package/src/vendor/statusline/src/gateway/route-meta.mjs +2 -0
- 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:
|
|
82
|
-
//
|
|
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
|
-
//
|
|
94
|
-
//
|
|
95
|
-
|
|
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
|
-
'
|
|
39
|
-
'
|
|
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':
|
|
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':
|
|
373
|
-
res.end('
|
|
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':
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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,
|
|
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
|
|
33
|
-
const
|
|
34
|
-
const
|
|
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':
|
|
123
|
-
res.end('
|
|
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
|
|
25
|
-
//
|
|
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
|
|
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.
|