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
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { inspectPdfBuffer } from '../../../attachments/pdf-extract.mjs';
|
|
3
|
+
import { resolvedPdfJs } from '../../../attachments/pdfjs-runtime.mjs';
|
|
4
|
+
import { MAX_PDF_PAGES } from '../../../attachments/limits.mjs';
|
|
5
|
+
import { tryExtractOoxmlTextFromBuffer } from '../../../attachments/office-extract.mjs';
|
|
2
6
|
import {
|
|
3
7
|
attachmentTextForPart,
|
|
4
8
|
isAttachmentReference,
|
|
5
|
-
|
|
9
|
+
isPermanentAttachmentError,
|
|
10
|
+
providerTakesNativePdf,
|
|
6
11
|
readAttachmentBase64,
|
|
12
|
+
storeInlineImagePart,
|
|
13
|
+
storeInlineDocumentPart,
|
|
7
14
|
} from '../../../attachments/store.mjs';
|
|
8
15
|
import { base64ByteLength, inlineFileKind } from '../../../shared/inline-file-kind.mjs';
|
|
16
|
+
import { isEnvironmentError } from '../../../shared/environment-error.mjs';
|
|
9
17
|
|
|
10
18
|
const DEFAULT_IMAGE_MIME = 'image/png';
|
|
11
19
|
|
|
@@ -59,7 +67,7 @@ function fileInfo(block) {
|
|
|
59
67
|
let filename = '';
|
|
60
68
|
if (typeof block.title === 'string' && block.title) filename = block.title;
|
|
61
69
|
else if (typeof block.filename === 'string' && block.filename) filename = block.filename;
|
|
62
|
-
return { data: source.data, mimeType, filename };
|
|
70
|
+
return { data: source.data, mimeType: bytesDecideMime(mimeType, source.data), filename, part: block };
|
|
63
71
|
}
|
|
64
72
|
if (block.type !== 'file') return null;
|
|
65
73
|
let data = '';
|
|
@@ -71,7 +79,18 @@ function fileInfo(block) {
|
|
|
71
79
|
.trim()
|
|
72
80
|
.toLowerCase() || 'application/pdf';
|
|
73
81
|
const filename = typeof block.filename === 'string' && block.filename ? block.filename : '';
|
|
74
|
-
|
|
82
|
+
const file = { data, mimeType: bytesDecideMime(mimeType, data), filename, part: block };
|
|
83
|
+
// A reference is already the content hash: memo keys use it instead of
|
|
84
|
+
// hashing the whole base64 payload again.
|
|
85
|
+
if (isAttachmentReference(block)) file.ref = block.attachmentRef;
|
|
86
|
+
return file;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// The PDF magic header outranks the transport label, so a mislabelled PDF
|
|
90
|
+
// carries the same type, label and price whether it is an inline part or the
|
|
91
|
+
// reference the store wrote for it (which records application/pdf).
|
|
92
|
+
function bytesDecideMime(mimeType, data) {
|
|
93
|
+
return inlineFileKind(mimeType, data) === 'pdf' ? 'application/pdf' : mimeType;
|
|
75
94
|
}
|
|
76
95
|
|
|
77
96
|
function fileLabel(file) {
|
|
@@ -88,9 +107,307 @@ function fileDecodedText(file) {
|
|
|
88
107
|
// API rejects.
|
|
89
108
|
function fileFallbackText(file, kind) {
|
|
90
109
|
if (kind === 'text') return `--- ${fileLabel(file)} ---\n${fileDecodedText(file)}`;
|
|
110
|
+
if (kind === 'office') {
|
|
111
|
+
const extracted = officeTextForFile(file);
|
|
112
|
+
if (extracted !== null) return `--- ${fileLabel(file)} ---\n${extracted}`;
|
|
113
|
+
}
|
|
91
114
|
return `[file not sent inline: ${fileLabel(file)} — this type has no inline form; open it from disk with read]`;
|
|
92
115
|
}
|
|
93
116
|
|
|
117
|
+
function fileKind(file) {
|
|
118
|
+
return inlineFileKind(file.mimeType, file.data, file.filename);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Extraction limits are fixed constants and the output depends only on the
|
|
122
|
+
// file's bytes and filename, so every turn, reload and restart lowers the same
|
|
123
|
+
// history to the same request. The memos below are speed caches keyed by
|
|
124
|
+
// content hash (or by the part itself): a cold memo recomputes the identical
|
|
125
|
+
// text, and nothing correctness-related depends on one.
|
|
126
|
+
//
|
|
127
|
+
// CACHE WARNING: this text is part of every already-sent turn's provider
|
|
128
|
+
// prefix. Changing the Office extractor (attachments/office-extract.mjs), the
|
|
129
|
+
// PDF extractor (pdf-extract.mjs / pdf.js), the limits below or the label
|
|
130
|
+
// format changes how OLD turns lower and invalidates the provider prompt cache
|
|
131
|
+
// for every session that holds such a file.
|
|
132
|
+
const OFFICE_TEXT_MAX_BYTES = 256 * 1024;
|
|
133
|
+
// Inflated size caps for lowering: a part or container beyond them has no text
|
|
134
|
+
// form (the fixed description is used), whatever its extractable content.
|
|
135
|
+
const OFFICE_PART_MAX_BYTES = 8 * 1024 * 1024;
|
|
136
|
+
const OFFICE_CONTAINER_MAX_BYTES = 32 * 1024 * 1024;
|
|
137
|
+
// The most text a PDF contributes; token estimates cap a text-lowered PDF here.
|
|
138
|
+
export const PDF_TEXT_MAX_BYTES = 256 * 1024;
|
|
139
|
+
const MEMO_MAX_ENTRIES = 64;
|
|
140
|
+
const officeTextMemo = new Map();
|
|
141
|
+
const pdfTextMemo = new Map();
|
|
142
|
+
// An inline part never changes, so its extraction is remembered by the part
|
|
143
|
+
// itself instead of re-hashing its whole base64 payload on every lowering.
|
|
144
|
+
let inlineOfficeText = new WeakMap();
|
|
145
|
+
|
|
146
|
+
function memoized(memo, key, compute) {
|
|
147
|
+
if (memo.has(key)) {
|
|
148
|
+
const hit = memo.get(key);
|
|
149
|
+
memo.delete(key);
|
|
150
|
+
memo.set(key, hit);
|
|
151
|
+
return hit;
|
|
152
|
+
}
|
|
153
|
+
const value = compute();
|
|
154
|
+
memo.set(key, value);
|
|
155
|
+
if (memo.size > MEMO_MAX_ENTRIES) memo.delete(memo.keys().next().value);
|
|
156
|
+
return value;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Extracted text, or null when the container cannot be read (the caller then
|
|
160
|
+
// uses the plain description).
|
|
161
|
+
function officeTextForFile(file) {
|
|
162
|
+
const compute = () =>
|
|
163
|
+
tryExtractOoxmlTextFromBuffer(Buffer.from(file.data, 'base64'), file.filename, {
|
|
164
|
+
maxOutputBytes: OFFICE_TEXT_MAX_BYTES,
|
|
165
|
+
maxPartBytes: OFFICE_PART_MAX_BYTES,
|
|
166
|
+
maxContainerBytes: OFFICE_CONTAINER_MAX_BYTES,
|
|
167
|
+
});
|
|
168
|
+
if (file.ref) return memoized(officeTextMemo, `ref:${file.ref}\0${file.filename || ''}`, compute);
|
|
169
|
+
if (file.part && inlineOfficeText.has(file.part)) return inlineOfficeText.get(file.part);
|
|
170
|
+
const key = createHash('sha256').update(file.filename || '').update('\0').update(file.data).digest('hex');
|
|
171
|
+
const text = memoized(officeTextMemo, key, compute);
|
|
172
|
+
if (file.part) inlineOfficeText.set(file.part, text);
|
|
173
|
+
return text;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Empties the extraction speed caches (tests: cold-process equivalence). */
|
|
177
|
+
export function _clearLoweringMemos() {
|
|
178
|
+
officeTextMemo.clear();
|
|
179
|
+
pdfTextMemo.clear();
|
|
180
|
+
inlineOfficeText = new WeakMap();
|
|
181
|
+
inlinePdfKeys = new WeakMap();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// pdf.js is asynchronous while lowering is synchronous, so a provider that
|
|
185
|
+
// takes a PDF as text gets it from an awaited pre-send step
|
|
186
|
+
// (preparePdfTextForProvider) that returns a copy of the request's messages
|
|
187
|
+
// with every PDF part already replaced by its text part. Lowering therefore
|
|
188
|
+
// never reads an extraction cache: a PDF part that still reaches it for such a
|
|
189
|
+
// provider is a bug and throws. Extraction runs in-process (it awaits pdf.js, it
|
|
190
|
+
// does not block the event loop for the whole document), and a PDF that cannot be
|
|
191
|
+
// read — encrypted, damaged — yields the fixed text below, which is a property
|
|
192
|
+
// of the bytes.
|
|
193
|
+
const PDF_UNREADABLE_TEXT = '[PDF text unavailable: this PDF could not be read]';
|
|
194
|
+
|
|
195
|
+
// The content hash of the PDF's bytes — a reference already is one — so an
|
|
196
|
+
// inline part and the reference stored for it share one cache entry.
|
|
197
|
+
// An inline part never changes, so its hash is taken once per part.
|
|
198
|
+
let inlinePdfKeys = new WeakMap();
|
|
199
|
+
function pdfKey(file) {
|
|
200
|
+
if (file.ref) return file.ref;
|
|
201
|
+
const known = file.part ? inlinePdfKeys.get(file.part) : undefined;
|
|
202
|
+
if (known) return known;
|
|
203
|
+
const key = createHash('sha256').update(Buffer.from(file.data, 'base64')).digest('hex');
|
|
204
|
+
if (file.part) inlinePdfKeys.set(file.part, key);
|
|
205
|
+
return key;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
let pdfExtractionCount = 0;
|
|
209
|
+
|
|
210
|
+
/** Number of PDF extractions actually run in this process (tests). */
|
|
211
|
+
export function _pdfExtractionCount() {
|
|
212
|
+
return pdfExtractionCount;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// A PDF the parser rejects is a property of the bytes: fixed text. A system
|
|
216
|
+
// error or an allocation failure is not the bytes' fault and must never become
|
|
217
|
+
// text.
|
|
218
|
+
async function extractPdfText(data) {
|
|
219
|
+
pdfExtractionCount += 1;
|
|
220
|
+
// Loading pdf.js says nothing about the PDF: a failure here propagates (the
|
|
221
|
+
// send fails and retries later with the real text) and nothing is cached.
|
|
222
|
+
await resolvedPdfJs();
|
|
223
|
+
await import('unpdf');
|
|
224
|
+
try {
|
|
225
|
+
// A PDF longer than MAX_PDF_PAGES is read for its first MAX_PDF_PAGES pages.
|
|
226
|
+
const { text } = await inspectPdfBuffer(Buffer.from(data, 'base64'), {
|
|
227
|
+
extractText: true,
|
|
228
|
+
maxPages: Infinity,
|
|
229
|
+
pageRange: { from: 1, to: MAX_PDF_PAGES },
|
|
230
|
+
maxOutputBytes: PDF_TEXT_MAX_BYTES,
|
|
231
|
+
});
|
|
232
|
+
return text;
|
|
233
|
+
} catch (error) {
|
|
234
|
+
if (isEnvironmentError(error)) throw error;
|
|
235
|
+
return PDF_UNREADABLE_TEXT;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// The cache holds the extraction promise, so concurrent sends of one PDF share
|
|
240
|
+
// a single extraction; a rejection is dropped so the next send retries.
|
|
241
|
+
function pdfTextFor(file) {
|
|
242
|
+
const key = pdfKey(file);
|
|
243
|
+
const pending = memoized(pdfTextMemo, key, () => extractPdfText(file.data));
|
|
244
|
+
pending.catch(() => {
|
|
245
|
+
if (pdfTextMemo.get(key) === pending) pdfTextMemo.delete(key);
|
|
246
|
+
});
|
|
247
|
+
return pending;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function pdfNotPrepared() {
|
|
251
|
+
return new Error(
|
|
252
|
+
'internal error: a PDF part reached text lowering unprepared (await preparePdfTextForProvider before building the request)'
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function* walkContentParts(content) {
|
|
257
|
+
if (Array.isArray(content)) {
|
|
258
|
+
for (const part of content) yield* walkContentParts(part);
|
|
259
|
+
} else if (content && typeof content === 'object') {
|
|
260
|
+
if (Array.isArray(content.content)) yield* walkContentParts(content.content);
|
|
261
|
+
else yield content;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* The text a text-only provider receives for a file/document part: exactly the
|
|
267
|
+
* string every other provider's lowering produces (text files decoded, Office
|
|
268
|
+
* files extracted, anything else the shared "[file not sent inline: …]"
|
|
269
|
+
* description, an unreadable reference its placeholder). A PDF is converted
|
|
270
|
+
* beforehand by preparePdfTextForProvider, so one reaching here is a bug.
|
|
271
|
+
* Null when the part is not a file at all.
|
|
272
|
+
*/
|
|
273
|
+
export function textFormOfFilePart(part) {
|
|
274
|
+
if (part?.type !== 'file' && part?.type !== 'document') return null;
|
|
275
|
+
try {
|
|
276
|
+
const file = fileInfo(part);
|
|
277
|
+
if (!file) return null;
|
|
278
|
+
const kind = fileKind(file);
|
|
279
|
+
if (kind === 'pdf') throw pdfNotPrepared();
|
|
280
|
+
return fileFallbackText(file, kind);
|
|
281
|
+
} catch (error) {
|
|
282
|
+
const unavailable = unavailableAttachmentText(part, error);
|
|
283
|
+
if (unavailable === null) throw error;
|
|
284
|
+
return unavailable;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Fixed text per media part type: repeated turns must lower identically.
|
|
289
|
+
const MEDIA_LABEL = {
|
|
290
|
+
image: 'image',
|
|
291
|
+
image_url: 'image',
|
|
292
|
+
input_image: 'image',
|
|
293
|
+
audio: 'audio',
|
|
294
|
+
input_audio: 'audio',
|
|
295
|
+
video: 'video',
|
|
296
|
+
file: 'file',
|
|
297
|
+
document: 'document',
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
/** The fixed text that stands for a media part the model cannot take. */
|
|
301
|
+
export function omittedMediaText(partType) {
|
|
302
|
+
return `[${MEDIA_LABEL[partType]} omitted: local model is text-only]`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const OMITTED_MEDIA_TEXT_RE = /^\[\w+ omitted: local model is text-only\]$/;
|
|
306
|
+
const NOT_SENT_FILE_TEXT_RE = /^\[file not sent inline:/;
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Whether a lowered text only says that media was left out (omittedMediaText,
|
|
310
|
+
* or a file's "not sent inline" description) rather than carrying content.
|
|
311
|
+
*/
|
|
312
|
+
export function isUnsentMediaText(text) {
|
|
313
|
+
return OMITTED_MEDIA_TEXT_RE.test(text) || NOT_SENT_FILE_TEXT_RE.test(text);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Replaces the media parts with fixed text, recursing into nested
|
|
318
|
+
* content. A file or document with a text form becomes that text (the same
|
|
319
|
+
* string every provider gets); everything else becomes omittedMediaText.
|
|
320
|
+
* Returns the same array when nothing changed.
|
|
321
|
+
*/
|
|
322
|
+
export function degradeMediaParts(parts) {
|
|
323
|
+
let changed = false;
|
|
324
|
+
const next = parts.map((part) => {
|
|
325
|
+
if (part && typeof part === 'object' && Object.hasOwn(MEDIA_LABEL, part.type)) {
|
|
326
|
+
changed = true;
|
|
327
|
+
return { type: 'text', text: textFormOfFilePart(part) ?? omittedMediaText(part.type) };
|
|
328
|
+
}
|
|
329
|
+
if (part && typeof part === 'object' && Array.isArray(part.content)) {
|
|
330
|
+
const inner = degradeMediaParts(part.content);
|
|
331
|
+
if (inner !== part.content) {
|
|
332
|
+
changed = true;
|
|
333
|
+
return { ...part, content: inner };
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return part;
|
|
337
|
+
});
|
|
338
|
+
return changed ? next : parts;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function replacePdfParts(content, texts) {
|
|
342
|
+
if (Array.isArray(content)) {
|
|
343
|
+
let changed = false;
|
|
344
|
+
const next = content.map((part) => {
|
|
345
|
+
const replaced = replacePdfParts(part, texts);
|
|
346
|
+
if (replaced !== part) changed = true;
|
|
347
|
+
return replaced;
|
|
348
|
+
});
|
|
349
|
+
return changed ? next : content;
|
|
350
|
+
}
|
|
351
|
+
if (content && typeof content === 'object') {
|
|
352
|
+
if (texts.has(content)) return { type: 'text', text: texts.get(content) };
|
|
353
|
+
if (Array.isArray(content.content)) {
|
|
354
|
+
const inner = replacePdfParts(content.content, texts);
|
|
355
|
+
return inner === content.content ? content : { ...content, content: inner };
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return content;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Awaited pre-send step for a provider that takes a PDF as text (nativePdf ===
|
|
363
|
+
* false): returns the messages with every PDF part — inline or stored — replaced
|
|
364
|
+
* by its `--- label ---\n<text>` text part, so the synchronous lowering after it
|
|
365
|
+
* sees no PDF. Returns the same array when there is nothing to convert. Call it
|
|
366
|
+
* where a request is built (every attempt), never from estimation. A blob that
|
|
367
|
+
* is permanently gone is left alone (it lowers to its placeholder); a transient
|
|
368
|
+
* read error or a pdf.js environment failure propagates and nothing is cached.
|
|
369
|
+
*/
|
|
370
|
+
export async function preparePdfTextForProvider(messages, provider) {
|
|
371
|
+
if (providerTakesNativePdf(provider) || !Array.isArray(messages)) return messages;
|
|
372
|
+
const found = [];
|
|
373
|
+
for (const message of messages) {
|
|
374
|
+
for (const part of walkContentParts(message?.content)) {
|
|
375
|
+
if (part.type !== 'file' && part.type !== 'document') continue;
|
|
376
|
+
let file;
|
|
377
|
+
try {
|
|
378
|
+
file = fileInfo(part);
|
|
379
|
+
} catch (error) {
|
|
380
|
+
if (unavailableAttachmentText(part, error) === null) throw error;
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
if (file && fileKind(file) === 'pdf') found.push({ part, file });
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (!found.length) return messages;
|
|
387
|
+
const extracted = await Promise.all(found.map(({ file }) => pdfTextFor(file)));
|
|
388
|
+
const texts = new Map(found.map(({ part, file }, i) => [part, `--- ${fileLabel(file)} ---\n${extracted[i]}`]));
|
|
389
|
+
return messages.map((message) => {
|
|
390
|
+
// A message whose content is one bare part becomes a one-part array.
|
|
391
|
+
if (texts.has(message?.content)) return { ...message, content: [{ type: 'text', text: texts.get(message.content) }] };
|
|
392
|
+
const content = replacePdfParts(message?.content, texts);
|
|
393
|
+
return content === message?.content ? message : { ...message, content };
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// A referenced blob that is gone or fails its content check lowers to this
|
|
398
|
+
// fixed placeholder (null: the error is something else and must propagate).
|
|
399
|
+
// Only permanent failures (gone, corrupt, impossible size) are rewritten: a
|
|
400
|
+
// transient EBUSY/EPERM/EACCES/EMFILE keeps throwing, so one flaky read never
|
|
401
|
+
// changes what an already-sent turn lowers to. The text names the stored
|
|
402
|
+
// filename (or type) only — no path, no error text.
|
|
403
|
+
function unavailableAttachmentText(part, error) {
|
|
404
|
+
if (!isAttachmentReference(part) || !isPermanentAttachmentError(error)) return null;
|
|
405
|
+
const name = String(part.filename || '')
|
|
406
|
+
.split(/[\\/]/)
|
|
407
|
+
.pop();
|
|
408
|
+
return `[attachment unavailable: ${name || part.mimeType || part.mediaType || part.type}]`;
|
|
409
|
+
}
|
|
410
|
+
|
|
94
411
|
function imageUrlFromPart(block) {
|
|
95
412
|
if (!block || typeof block !== 'object') return null;
|
|
96
413
|
if (block.type === 'image_url' || block.type === 'input_image') {
|
|
@@ -189,7 +506,19 @@ function contentParts(content) {
|
|
|
189
506
|
return null;
|
|
190
507
|
}
|
|
191
508
|
|
|
192
|
-
|
|
509
|
+
// Provider lowering text for a part. A referenced blob that is gone or fails
|
|
510
|
+
// its integrity check lowers to a fixed placeholder instead of failing the send.
|
|
511
|
+
function jsonFallbackFromPart(block, nativePdf = true) {
|
|
512
|
+
try {
|
|
513
|
+
return rawJsonFallbackFromPart(block, nativePdf);
|
|
514
|
+
} catch (error) {
|
|
515
|
+
const unavailable = unavailableAttachmentText(block, error);
|
|
516
|
+
if (unavailable === null) throw error;
|
|
517
|
+
return unavailable;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function rawJsonFallbackFromPart(block, nativePdf = true) {
|
|
193
522
|
const text = textFromPart(block);
|
|
194
523
|
if (text) return text;
|
|
195
524
|
if (!block || typeof block !== 'object') return block == null ? '' : String(block);
|
|
@@ -202,8 +531,12 @@ function jsonFallbackFromPart(block) {
|
|
|
202
531
|
if (file) {
|
|
203
532
|
// A PDF rides as native media, so its bytes stay out of the text. Anything
|
|
204
533
|
// else has no media form here and contributes its text or its description.
|
|
205
|
-
const kind =
|
|
206
|
-
|
|
534
|
+
const kind = fileKind(file);
|
|
535
|
+
if (kind === 'pdf') {
|
|
536
|
+
if (!nativePdf) throw pdfNotPrepared();
|
|
537
|
+
return '';
|
|
538
|
+
}
|
|
539
|
+
return fileFallbackText(file, kind);
|
|
207
540
|
}
|
|
208
541
|
return stringifyFallback(block);
|
|
209
542
|
}
|
|
@@ -286,6 +619,13 @@ export function contentImageDescriptors(content) {
|
|
|
286
619
|
// documents that travel as native media are billed here; a text-ish file is
|
|
287
620
|
// already priced as the text it becomes, and a binary file only costs its
|
|
288
621
|
// one-line description.
|
|
622
|
+
// The page count (recorded by intake and by the read tool) rides on the part
|
|
623
|
+
// itself, so an inline part and its stored reference carry the same one.
|
|
624
|
+
function pdfDescriptor(sizeBytes, part) {
|
|
625
|
+
const pageCount = Math.floor(Number(part?.pageCount));
|
|
626
|
+
return { mimeType: 'application/pdf', sizeBytes, ...(pageCount > 0 ? { pageCount } : {}) };
|
|
627
|
+
}
|
|
628
|
+
|
|
289
629
|
export function contentFileDescriptors(content) {
|
|
290
630
|
const parts = contentParts(content);
|
|
291
631
|
if (!parts) return [];
|
|
@@ -293,30 +633,44 @@ export function contentFileDescriptors(content) {
|
|
|
293
633
|
if (part?.type === 'file' && isAttachmentReference(part)) {
|
|
294
634
|
const mimeType = String(part.mimeType || part.mediaType || 'application/pdf');
|
|
295
635
|
if (inlineFileKind(mimeType, '') !== 'pdf') return [];
|
|
296
|
-
return [
|
|
636
|
+
return [pdfDescriptor(Number(part.sizeBytes) || 0, part)];
|
|
297
637
|
}
|
|
298
638
|
const file = fileInfo(part);
|
|
299
|
-
if (!file ||
|
|
300
|
-
|
|
639
|
+
if (!file || fileKind(file) !== 'pdf') return [];
|
|
640
|
+
// The bytes decide the kind (see inlineFileKind), so a mislabelled PDF is
|
|
641
|
+
// priced under the type it is stored as.
|
|
642
|
+
return [pdfDescriptor(base64ByteLength(file.data), part)];
|
|
301
643
|
});
|
|
302
644
|
}
|
|
303
645
|
|
|
304
646
|
// Wire bytes of the media one content carries: the base64 payload of an inline
|
|
305
647
|
// image or document, or the base64 size a stored attachment expands to (from
|
|
306
648
|
// its recorded size, never by reading the blob).
|
|
307
|
-
export function contentMediaBytes(content) {
|
|
649
|
+
export function contentMediaBytes(content, { nativePdf = true } = {}) {
|
|
308
650
|
const parts = contentParts(content);
|
|
309
651
|
if (!parts) return 0;
|
|
310
652
|
let bytes = 0;
|
|
653
|
+
// Text and Office files (and a PDF on a provider that takes it as text)
|
|
654
|
+
// travel as text, so they are not media. Only a PDF counts, and the same way
|
|
655
|
+
// for an inline part and its reference: by MIME type, never by reading a blob.
|
|
656
|
+
const countsAsMedia = (mimeType, data) => nativePdf && inlineFileKind(mimeType, data) === 'pdf';
|
|
311
657
|
for (const part of parts) {
|
|
312
658
|
if (!part || typeof part !== 'object') continue;
|
|
313
659
|
if (isAttachmentReference(part)) {
|
|
314
|
-
if (part.type === 'image'
|
|
660
|
+
if (part.type === 'image') bytes += Math.ceil((Number(part.sizeBytes) || 0) / 3) * 4;
|
|
661
|
+
else if (part.type === 'file' && countsAsMedia(String(part.mimeType || part.mediaType || 'application/pdf'), '')) {
|
|
662
|
+
bytes += Math.ceil((Number(part.sizeBytes) || 0) / 3) * 4;
|
|
663
|
+
}
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
const image = imageInfo(part) || geminiInlineInfo(part);
|
|
667
|
+
if (image) {
|
|
668
|
+
bytes += image.data.length;
|
|
315
669
|
continue;
|
|
316
670
|
}
|
|
317
|
-
const
|
|
318
|
-
if (
|
|
319
|
-
bytes +=
|
|
671
|
+
const file = fileInfo(part);
|
|
672
|
+
if (file) {
|
|
673
|
+
if (countsAsMedia(file.mimeType, '')) bytes += file.data.length;
|
|
320
674
|
continue;
|
|
321
675
|
}
|
|
322
676
|
const url = imageUrlFromPart(part);
|
|
@@ -325,6 +679,11 @@ export function contentMediaBytes(content) {
|
|
|
325
679
|
return bytes;
|
|
326
680
|
}
|
|
327
681
|
|
|
682
|
+
// Images plus the documents that travel as native document blocks.
|
|
683
|
+
export function contentMediaCount(content, { nativePdf = true } = {}) {
|
|
684
|
+
return contentImageDescriptors(content).length + (nativePdf ? contentFileDescriptors(content).length : 0);
|
|
685
|
+
}
|
|
686
|
+
|
|
328
687
|
function projectContentText(content, fallback, projectPart) {
|
|
329
688
|
if (typeof content === 'string') return content;
|
|
330
689
|
const parts = contentParts(content);
|
|
@@ -333,20 +692,20 @@ function projectContentText(content, fallback, projectPart) {
|
|
|
333
692
|
return text || fallback;
|
|
334
693
|
}
|
|
335
694
|
|
|
336
|
-
export function contentToText(content, fallback = '') {
|
|
337
|
-
return projectContentText(content, fallback, jsonFallbackFromPart);
|
|
695
|
+
export function contentToText(content, fallback = '', { nativePdf = true } = {}) {
|
|
696
|
+
return projectContentText(content, fallback, (part) => jsonFallbackFromPart(part, nativePdf));
|
|
338
697
|
}
|
|
339
698
|
|
|
340
699
|
// Context estimation's text projection. A referenced file or text whose blob
|
|
341
700
|
// is missing is priced as the part's own metadata, like any part with no
|
|
342
701
|
// readable payload; a referenced image already projects to '' and is priced
|
|
343
702
|
// by its descriptor. Provider sends keep contentToText and the normalizers
|
|
344
|
-
// below, which
|
|
703
|
+
// below, which lower a missing blob to "[attachment unavailable: …]" text.
|
|
345
704
|
function estimatePartText(part) {
|
|
346
705
|
try {
|
|
347
|
-
return
|
|
706
|
+
return rawJsonFallbackFromPart(part);
|
|
348
707
|
} catch (error) {
|
|
349
|
-
if (!isAttachmentReference(part) || !
|
|
708
|
+
if (!isAttachmentReference(part) || !isPermanentAttachmentError(error)) throw error;
|
|
350
709
|
return stringifyFallback(part);
|
|
351
710
|
}
|
|
352
711
|
}
|
|
@@ -370,10 +729,84 @@ function storedHistoryImagePlaceholder(part) {
|
|
|
370
729
|
return `[Image omitted from stored history${mimeType ? `: ${mimeType}` : ''}]`;
|
|
371
730
|
}
|
|
372
731
|
|
|
732
|
+
// Live parts keep their identity across saves, so each inline image is hashed
|
|
733
|
+
// and written once and every projection returns the same stored part.
|
|
734
|
+
const storedImageParts = new WeakMap();
|
|
735
|
+
|
|
736
|
+
// One part the store refuses (over the size cap, disk error) must never fail the
|
|
737
|
+
// whole session save: the on-disk copy carries its placeholder text instead.
|
|
738
|
+
// Only successes are memoized — a failure is retried on the next save (a
|
|
739
|
+
// transient disk error must not pin the placeholder) and the live part is never
|
|
740
|
+
// touched, so provider lowering keeps sending the real bytes.
|
|
741
|
+
const loggedStoreFailures = new Set();
|
|
742
|
+
|
|
743
|
+
function storeOrNull(store, part) {
|
|
744
|
+
try {
|
|
745
|
+
return store(part);
|
|
746
|
+
} catch (error) {
|
|
747
|
+
const key = `${part?.type}:${error?.message || error}`;
|
|
748
|
+
if (!loggedStoreFailures.has(key)) {
|
|
749
|
+
loggedStoreFailures.add(key);
|
|
750
|
+
console.error(`[attachments] inline ${part?.type} part saved as a placeholder: ${error?.message || error}`);
|
|
751
|
+
}
|
|
752
|
+
return null;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function memoizedStore(memo, store, part) {
|
|
757
|
+
if (memo.has(part)) return memo.get(part);
|
|
758
|
+
const stored = storeOrNull(store, part);
|
|
759
|
+
if (stored) memo.set(part, stored);
|
|
760
|
+
return stored;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function storedImagePart(part) {
|
|
764
|
+
return memoizedStore(storedImageParts, storeInlineImagePart, part);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
const storedDocumentParts = new WeakMap();
|
|
768
|
+
|
|
769
|
+
function storedDocumentPart(part) {
|
|
770
|
+
return memoizedStore(storedDocumentParts, storeInlineDocumentPart, part);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Whether stored-history projection would rewrite this content (it still holds
|
|
775
|
+
* inline media or URL/file-id images). Shape test only: nothing is written and
|
|
776
|
+
* nothing throws.
|
|
777
|
+
*/
|
|
778
|
+
export function contentCarriesLiveMedia(content) {
|
|
779
|
+
if (typeof content === 'string') return false;
|
|
780
|
+
const parts = contentParts(content);
|
|
781
|
+
return Boolean(parts?.some(partCarriesLiveMedia));
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function partCarriesLiveMedia(part) {
|
|
785
|
+
if (!part || typeof part !== 'object' || isAttachmentReference(part)) return false;
|
|
786
|
+
if (
|
|
787
|
+
part.type === 'image' ||
|
|
788
|
+
part.type === 'image_url' ||
|
|
789
|
+
part.type === 'input_image' ||
|
|
790
|
+
imageUrlFromPart(part) ||
|
|
791
|
+
imageFileIdFromPart(part) ||
|
|
792
|
+
imageFileUriFromPart(part) ||
|
|
793
|
+
geminiInlineInfo(part) ||
|
|
794
|
+
fileInfo(part)
|
|
795
|
+
) {
|
|
796
|
+
return true;
|
|
797
|
+
}
|
|
798
|
+
return Array.isArray(part.content) && contentCarriesLiveMedia(part.content);
|
|
799
|
+
}
|
|
800
|
+
|
|
373
801
|
function sanitizePartForStoredHistory(part) {
|
|
374
802
|
if (typeof part === 'string') return part;
|
|
375
803
|
if (!part || typeof part !== 'object') return part;
|
|
376
804
|
if (isAttachmentReference(part)) return part;
|
|
805
|
+
// An inline base64 image persists as a content-addressed reference: the
|
|
806
|
+
// reloaded transcript lowers to the same provider bytes, so evicting or
|
|
807
|
+
// reloading the session keeps the provider prompt cache intact.
|
|
808
|
+
const storedImage = part.type === 'image' ? storedImagePart(part) : null;
|
|
809
|
+
if (storedImage) return storedImage;
|
|
377
810
|
if (
|
|
378
811
|
part.type === 'image' ||
|
|
379
812
|
part.type === 'image_url' ||
|
|
@@ -387,6 +820,8 @@ function sanitizePartForStoredHistory(part) {
|
|
|
387
820
|
}
|
|
388
821
|
const file = fileInfo(part);
|
|
389
822
|
if (file) {
|
|
823
|
+
const storedDocument = storedDocumentPart(part);
|
|
824
|
+
if (storedDocument) return storedDocument;
|
|
390
825
|
return { type: 'text', text: `[File omitted from stored history: ${file.filename || file.mimeType}]` };
|
|
391
826
|
}
|
|
392
827
|
if (Array.isArray(part.content)) {
|
|
@@ -414,120 +849,140 @@ export function normalizeContentForAnthropic(content) {
|
|
|
414
849
|
const parts = contentParts(content);
|
|
415
850
|
if (!parts) return content;
|
|
416
851
|
return parts.map((part) => {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
let out;
|
|
424
|
-
if (kind === 'pdf') {
|
|
425
|
-
out = {
|
|
426
|
-
type: 'document',
|
|
427
|
-
source: { type: 'base64', media_type: 'application/pdf', data: file.data },
|
|
428
|
-
...title,
|
|
429
|
-
};
|
|
430
|
-
} else if (kind === 'text') {
|
|
431
|
-
out = {
|
|
432
|
-
type: 'document',
|
|
433
|
-
source: { type: 'text', media_type: 'text/plain', data: fileDecodedText(file) },
|
|
434
|
-
...title,
|
|
435
|
-
};
|
|
436
|
-
} else {
|
|
437
|
-
out = { type: 'text', text: fileFallbackText(file, kind) };
|
|
438
|
-
}
|
|
439
|
-
if (part.cache_control) out.cache_control = part.cache_control;
|
|
440
|
-
return out;
|
|
852
|
+
try {
|
|
853
|
+
return normalizeAnthropicPart(part);
|
|
854
|
+
} catch (error) {
|
|
855
|
+
const unavailable = unavailableAttachmentText(part, error);
|
|
856
|
+
if (unavailable === null) throw error;
|
|
857
|
+
return { type: 'text', text: unavailable };
|
|
441
858
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function normalizeAnthropicPart(part) {
|
|
863
|
+
const file = fileInfo(part);
|
|
864
|
+
if (file) {
|
|
865
|
+
// A base64 document block is a PDF contract; a text document carries its
|
|
866
|
+
// decoded text, and anything else is described rather than sent.
|
|
867
|
+
const kind = fileKind(file);
|
|
868
|
+
const title = file.filename ? { title: file.filename } : {};
|
|
869
|
+
let out;
|
|
870
|
+
if (kind === 'pdf') {
|
|
871
|
+
out = {
|
|
872
|
+
type: 'document',
|
|
873
|
+
source: { type: 'base64', media_type: 'application/pdf', data: file.data },
|
|
874
|
+
...title,
|
|
451
875
|
};
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
return { type: 'image', source: { type: 'file', file_id: fileId } };
|
|
458
|
-
}
|
|
459
|
-
const url = imageUrlFromPart(part);
|
|
460
|
-
const dataUrlInfo = imageInfoFromDataUrl(url);
|
|
461
|
-
if (dataUrlInfo) {
|
|
462
|
-
return {
|
|
463
|
-
type: 'image',
|
|
464
|
-
source: {
|
|
465
|
-
type: 'base64',
|
|
466
|
-
media_type: dataUrlInfo.mimeType,
|
|
467
|
-
data: dataUrlInfo.data,
|
|
468
|
-
},
|
|
876
|
+
} else if (kind === 'text') {
|
|
877
|
+
out = {
|
|
878
|
+
type: 'document',
|
|
879
|
+
source: { type: 'text', media_type: 'text/plain', data: fileDecodedText(file) },
|
|
880
|
+
...title,
|
|
469
881
|
};
|
|
882
|
+
} else {
|
|
883
|
+
out = { type: 'text', text: fileFallbackText(file, kind) };
|
|
470
884
|
}
|
|
471
|
-
if (
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
885
|
+
if (part.cache_control) out.cache_control = part.cache_control;
|
|
886
|
+
return out;
|
|
887
|
+
}
|
|
888
|
+
const info = imageInfo(part);
|
|
889
|
+
if (info) {
|
|
890
|
+
const out = {
|
|
891
|
+
type: 'image',
|
|
892
|
+
source: {
|
|
893
|
+
type: 'base64',
|
|
894
|
+
media_type: info.mimeType,
|
|
895
|
+
data: info.data,
|
|
896
|
+
},
|
|
897
|
+
};
|
|
898
|
+
if (part.cache_control) out.cache_control = part.cache_control;
|
|
899
|
+
return out;
|
|
900
|
+
}
|
|
901
|
+
const fileId = imageFileIdFromPart(part);
|
|
902
|
+
if (fileId) {
|
|
903
|
+
return { type: 'image', source: { type: 'file', file_id: fileId } };
|
|
904
|
+
}
|
|
905
|
+
const url = imageUrlFromPart(part);
|
|
906
|
+
const dataUrlInfo = imageInfoFromDataUrl(url);
|
|
907
|
+
if (dataUrlInfo) {
|
|
908
|
+
return {
|
|
909
|
+
type: 'image',
|
|
910
|
+
source: {
|
|
911
|
+
type: 'base64',
|
|
912
|
+
media_type: dataUrlInfo.mimeType,
|
|
913
|
+
data: dataUrlInfo.data,
|
|
914
|
+
},
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
if (url) {
|
|
918
|
+
return { type: 'image', source: { type: 'url', url } };
|
|
919
|
+
}
|
|
920
|
+
if (part?.type === 'image') {
|
|
921
|
+
if (part.source?.type === 'url' && typeof part.source.url === 'string') return part;
|
|
922
|
+
if (part.source?.type === 'file' && typeof part.source.file_id === 'string') return part;
|
|
923
|
+
return { type: 'text', text: `[unsupported image content: ${stringifyFallback(part)}]` };
|
|
924
|
+
}
|
|
925
|
+
if (part?.type === 'tool_result') {
|
|
926
|
+
let nested;
|
|
927
|
+
if (Array.isArray(part.content)) nested = normalizeContentForAnthropic(part.content);
|
|
928
|
+
else if (typeof part.content === 'string') nested = part.content;
|
|
929
|
+
else nested = part.content == null ? '' : stringifyFallback(part.content);
|
|
930
|
+
return { ...part, content: nested };
|
|
931
|
+
}
|
|
932
|
+
if (part?.type === 'input_text' || part?.type === 'output_text') {
|
|
933
|
+
return { type: 'text', text: part.text || '' };
|
|
934
|
+
}
|
|
935
|
+
if (part?.type === 'text' && isAttachmentReference(part)) {
|
|
936
|
+
return { type: 'text', text: attachmentTextForPart(part) };
|
|
937
|
+
}
|
|
938
|
+
return part;
|
|
494
939
|
}
|
|
495
940
|
|
|
496
|
-
|
|
941
|
+
// `nativePdf: false` marks a provider that takes a PDF as text: its PDFs were
|
|
942
|
+
// converted by preparePdfTextForProvider, so a PDF part reaching these
|
|
943
|
+
// normalizers is an error rather than a file block the provider cannot read.
|
|
944
|
+
export function normalizeContentForOpenAIChat(content, { role = 'user', nativePdf = true } = {}) {
|
|
497
945
|
const parts = contentParts(content);
|
|
498
946
|
if (!parts) return content;
|
|
499
947
|
const out = [];
|
|
500
948
|
for (const part of parts) {
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
949
|
+
try {
|
|
950
|
+
const file = fileInfo(part);
|
|
951
|
+
if (file) {
|
|
952
|
+
const kind = fileKind(file);
|
|
953
|
+
if (kind === 'pdf') {
|
|
954
|
+
if (!nativePdf) throw pdfNotPrepared();
|
|
955
|
+
out.push({
|
|
956
|
+
type: 'file',
|
|
957
|
+
file: { filename: file.filename || 'document.pdf', file_data: `data:application/pdf;base64,${file.data}` },
|
|
958
|
+
});
|
|
959
|
+
} else {
|
|
960
|
+
out.push({ type: 'text', text: fileFallbackText(file, kind) });
|
|
961
|
+
}
|
|
962
|
+
continue;
|
|
511
963
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
964
|
+
const fileId = imageFileIdFromPart(part);
|
|
965
|
+
if (fileId) {
|
|
966
|
+
out.push({ type: 'text', text: `[unsupported image file_id for OpenAI Chat-compatible request: ${fileId}]` });
|
|
967
|
+
continue;
|
|
968
|
+
}
|
|
969
|
+
const fileUri = imageFileUriFromPart(part);
|
|
970
|
+
if (fileUri) {
|
|
971
|
+
out.push({ type: 'image_url', image_url: { url: fileUri.fileUri } });
|
|
972
|
+
continue;
|
|
973
|
+
}
|
|
974
|
+
const url = imageUrlFromPart(part);
|
|
975
|
+
if (url) {
|
|
976
|
+
out.push({ type: 'image_url', image_url: { url } });
|
|
977
|
+
continue;
|
|
978
|
+
}
|
|
979
|
+
const text = jsonFallbackFromPart(part, nativePdf);
|
|
980
|
+
if (text) out.push({ type: 'text', text });
|
|
981
|
+
} catch (error) {
|
|
982
|
+
const unavailable = unavailableAttachmentText(part, error);
|
|
983
|
+
if (unavailable === null) throw error;
|
|
984
|
+
out.push({ type: 'text', text: unavailable });
|
|
528
985
|
}
|
|
529
|
-
const text = jsonFallbackFromPart(part);
|
|
530
|
-
if (text) out.push({ type: 'text', text });
|
|
531
986
|
}
|
|
532
987
|
if (role !== 'user')
|
|
533
988
|
return out
|
|
@@ -537,7 +992,7 @@ export function normalizeContentForOpenAIChat(content, { role = 'user' } = {}) {
|
|
|
537
992
|
return out.length ? out : contentToText(content, '');
|
|
538
993
|
}
|
|
539
994
|
|
|
540
|
-
export function normalizeContentForOpenAIResponses(content, { role = 'user' } = {}) {
|
|
995
|
+
export function normalizeContentForOpenAIResponses(content, { role = 'user', nativePdf = true } = {}) {
|
|
541
996
|
const textType = role === 'assistant' ? 'output_text' : 'input_text';
|
|
542
997
|
if (typeof content === 'string') return content ? [{ type: textType, text: content }] : [];
|
|
543
998
|
const parts = contentParts(content);
|
|
@@ -547,37 +1002,44 @@ export function normalizeContentForOpenAIResponses(content, { role = 'user' } =
|
|
|
547
1002
|
}
|
|
548
1003
|
const out = [];
|
|
549
1004
|
for (const part of parts) {
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
1005
|
+
try {
|
|
1006
|
+
const file = fileInfo(part);
|
|
1007
|
+
if (file) {
|
|
1008
|
+
const kind = fileKind(file);
|
|
1009
|
+
if (kind === 'pdf') {
|
|
1010
|
+
if (!nativePdf) throw pdfNotPrepared();
|
|
1011
|
+
out.push({
|
|
1012
|
+
type: 'input_file',
|
|
1013
|
+
filename: file.filename || 'document.pdf',
|
|
1014
|
+
file_data: `data:application/pdf;base64,${file.data}`,
|
|
1015
|
+
});
|
|
1016
|
+
} else {
|
|
1017
|
+
out.push({ type: textType, text: fileFallbackText(file, kind) });
|
|
1018
|
+
}
|
|
1019
|
+
continue;
|
|
561
1020
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
1021
|
+
const fileId = imageFileIdFromPart(part);
|
|
1022
|
+
if (fileId) {
|
|
1023
|
+
out.push({ type: 'input_image', file_id: fileId });
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1026
|
+
const fileUri = imageFileUriFromPart(part);
|
|
1027
|
+
if (fileUri) {
|
|
1028
|
+
out.push({ type: 'input_image', image_url: fileUri.fileUri });
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
const url = imageUrlFromPart(part);
|
|
1032
|
+
if (url) {
|
|
1033
|
+
out.push({ type: 'input_image', image_url: url });
|
|
1034
|
+
continue;
|
|
1035
|
+
}
|
|
1036
|
+
const text = jsonFallbackFromPart(part, nativePdf);
|
|
1037
|
+
if (text) out.push({ type: textType, text });
|
|
1038
|
+
} catch (error) {
|
|
1039
|
+
const unavailable = unavailableAttachmentText(part, error);
|
|
1040
|
+
if (unavailable === null) throw error;
|
|
1041
|
+
out.push({ type: textType, text: unavailable });
|
|
578
1042
|
}
|
|
579
|
-
const text = jsonFallbackFromPart(part);
|
|
580
|
-
if (text) out.push({ type: textType, text });
|
|
581
1043
|
}
|
|
582
1044
|
return out;
|
|
583
1045
|
}
|
|
@@ -591,45 +1053,51 @@ export function normalizeContentForGeminiParts(content) {
|
|
|
591
1053
|
}
|
|
592
1054
|
const out = [];
|
|
593
1055
|
for (const part of parts) {
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
1056
|
+
try {
|
|
1057
|
+
const file = fileInfo(part);
|
|
1058
|
+
if (file) {
|
|
1059
|
+
const kind = fileKind(file);
|
|
1060
|
+
if (kind === 'pdf') out.push({ inlineData: { mimeType: 'application/pdf', data: file.data } });
|
|
1061
|
+
else out.push({ text: fileFallbackText(file, kind) });
|
|
1062
|
+
continue;
|
|
1063
|
+
}
|
|
1064
|
+
const inlineInfo = geminiInlineInfo(part);
|
|
1065
|
+
if (inlineInfo) {
|
|
1066
|
+
out.push({ inlineData: { mimeType: inlineInfo.mimeType, data: inlineInfo.data } });
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
const fileUri = imageFileUriFromPart(part);
|
|
1070
|
+
if (fileUri) {
|
|
1071
|
+
out.push({ fileData: { mimeType: fileUri.mimeType, fileUri: fileUri.fileUri } });
|
|
1072
|
+
continue;
|
|
1073
|
+
}
|
|
1074
|
+
const fileId = imageFileIdFromPart(part);
|
|
1075
|
+
if (fileId) {
|
|
1076
|
+
out.push({ text: `[unsupported image file_id for Gemini request: ${fileId}]` });
|
|
1077
|
+
continue;
|
|
1078
|
+
}
|
|
1079
|
+
const info = imageInfo(part);
|
|
1080
|
+
if (info) {
|
|
1081
|
+
out.push({ inlineData: { mimeType: info.mimeType, data: info.data } });
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
const url = imageUrlFromPart(part);
|
|
1085
|
+
const dataUrlInfo = imageInfoFromDataUrl(url);
|
|
1086
|
+
if (dataUrlInfo) {
|
|
1087
|
+
out.push({ inlineData: { mimeType: dataUrlInfo.mimeType, data: dataUrlInfo.data } });
|
|
1088
|
+
continue;
|
|
1089
|
+
}
|
|
1090
|
+
if (url && !url.startsWith('data:')) {
|
|
1091
|
+
out.push({ fileData: { mimeType: DEFAULT_IMAGE_MIME, fileUri: url } });
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
const text = jsonFallbackFromPart(part);
|
|
1095
|
+
if (text) out.push({ text });
|
|
1096
|
+
} catch (error) {
|
|
1097
|
+
const unavailable = unavailableAttachmentText(part, error);
|
|
1098
|
+
if (unavailable === null) throw error;
|
|
1099
|
+
out.push({ text: unavailable });
|
|
630
1100
|
}
|
|
631
|
-
const text = jsonFallbackFromPart(part);
|
|
632
|
-
if (text) out.push({ text });
|
|
633
1101
|
}
|
|
634
1102
|
return out;
|
|
635
1103
|
}
|
|
@@ -642,14 +1110,14 @@ function mediaPartsOnly(parts, allowedTypes) {
|
|
|
642
1110
|
return parts.filter((part) => allowedTypes.has(part?.type));
|
|
643
1111
|
}
|
|
644
1112
|
|
|
645
|
-
export function splitToolContentForOpenAIChat(content) {
|
|
1113
|
+
export function splitToolContentForOpenAIChat(content, { nativePdf = true } = {}) {
|
|
646
1114
|
const mediaContent = mediaPartsOnly(
|
|
647
|
-
normalizeContentForOpenAIChat(content, { role: 'user' }),
|
|
1115
|
+
normalizeContentForOpenAIChat(content, { role: 'user', nativePdf }),
|
|
648
1116
|
OPENAI_CHAT_MEDIA_TYPES
|
|
649
1117
|
);
|
|
650
|
-
if (!mediaContent.length) return { output: contentToText(content, ''), mediaContent: null };
|
|
1118
|
+
if (!mediaContent.length) return { output: contentToText(content, '', { nativePdf }), mediaContent: null };
|
|
651
1119
|
return {
|
|
652
|
-
output: contentToText(content, '[tool result included media content in the following user message]'),
|
|
1120
|
+
output: contentToText(content, '[tool result included media content in the following user message]', { nativePdf }),
|
|
653
1121
|
mediaContent,
|
|
654
1122
|
};
|
|
655
1123
|
}
|
|
@@ -666,15 +1134,16 @@ export function splitToolContentForOpenAIResponses(content) {
|
|
|
666
1134
|
};
|
|
667
1135
|
}
|
|
668
1136
|
|
|
669
|
-
export function splitToolContentForXaiResponses(content) {
|
|
670
|
-
const normalized = normalizeContentForOpenAIResponses(content, { role: 'user' });
|
|
1137
|
+
export function splitToolContentForXaiResponses(content, { nativePdf = true } = {}) {
|
|
1138
|
+
const normalized = normalizeContentForOpenAIResponses(content, { role: 'user', nativePdf });
|
|
671
1139
|
const mediaContent = mediaPartsOnly(normalized, new Set(['input_image']));
|
|
672
1140
|
if (!mediaContent.length) {
|
|
673
1141
|
const hasDocument = normalized.some((part) => part?.type === 'input_file');
|
|
674
1142
|
return {
|
|
675
1143
|
output: contentToText(
|
|
676
1144
|
content,
|
|
677
|
-
hasDocument ? '[tool result included document content unavailable to xAI Responses]' : ''
|
|
1145
|
+
hasDocument ? '[tool result included document content unavailable to xAI Responses]' : '',
|
|
1146
|
+
{ nativePdf }
|
|
678
1147
|
),
|
|
679
1148
|
mediaContent: null,
|
|
680
1149
|
};
|
|
@@ -682,7 +1151,7 @@ export function splitToolContentForXaiResponses(content) {
|
|
|
682
1151
|
return {
|
|
683
1152
|
// xAI documents function_call_output as text/JSON; media remains a
|
|
684
1153
|
// following user input, without replaying the tool-result text there.
|
|
685
|
-
output: contentToText(content, '[tool result included image content in the following user message]'),
|
|
1154
|
+
output: contentToText(content, '[tool result included image content in the following user message]', { nativePdf }),
|
|
686
1155
|
mediaContent,
|
|
687
1156
|
};
|
|
688
1157
|
}
|
|
@@ -691,7 +1160,10 @@ export function splitToolContentForGemini(content) {
|
|
|
691
1160
|
if (!contentHasImage(content)) {
|
|
692
1161
|
// Raw passthrough is for text and plain objects only: a file part would
|
|
693
1162
|
// serialize its base64 payload straight into the function response.
|
|
694
|
-
|
|
1163
|
+
// Decided by part shape: classifying a stored file must not read its blob.
|
|
1164
|
+
const hasFile = contentParts(content)?.some(
|
|
1165
|
+
(part) => part?.type === 'file' || (part?.type === 'document' && part.source?.type === 'base64')
|
|
1166
|
+
);
|
|
695
1167
|
return { response: { result: hasFile ? contentToText(content, '') : content }, mediaParts: [] };
|
|
696
1168
|
}
|
|
697
1169
|
return {
|