melchizedek-agents 1.13.0 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +4 -1
- package/CHANGELOG.md +18 -0
- package/DOCUMENTATION.md +1 -1
- package/dist/lib/a2a/app.d.ts +5 -1
- package/dist/lib/a2a/app.js +5 -1
- package/dist/lib/a2a/executor.d.ts +14 -4
- package/dist/lib/a2a/executor.js +61 -8
- package/dist/lib/index.d.ts +2 -0
- package/dist/lib/index.js +2 -0
- package/dist/lib/models/attachments.d.ts +42 -0
- package/dist/lib/models/attachments.js +80 -0
- package/dist/lib/models/chatCompletionsAdapter.js +6 -1
- package/dist/lib/models/claudeAdapter.js +10 -0
- package/dist/lib/observability/tracer.js +13 -2
- package/dist/scripts/a2a_server.js +5 -2
- package/package.json +1 -1
package/.env.example
CHANGED
|
@@ -295,7 +295,10 @@ PUBLIC_URL=
|
|
|
295
295
|
# OpenAPI tools: the exact environment variables an `auth:` may name. Unset, any
|
|
296
296
|
# variable but the framework's own settings (this file's) is allowed.
|
|
297
297
|
# OPENAPI_CREDENTIAL_ENVS=WEATHER_API_KEY,CRM_TOKEN
|
|
298
|
-
#
|
|
298
|
+
# Inline files in one message, decoded bytes; 0 refuses file parts. A file by URL is always refused.
|
|
299
|
+
# A2A_MAX_FILE_BYTES=10485760
|
|
300
|
+
# Set by default to hold A2A_MAX_FILE_BYTES as base64.
|
|
301
|
+
# A2A_BODY_LIMIT=15mb
|
|
299
302
|
# How long SIGTERM waits for running tasks before canceling them.
|
|
300
303
|
# A2A_SHUTDOWN_GRACE_MS=25000
|
|
301
304
|
# Of that, how long /readyz fails while requests are still served, so a load
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,24 @@ Consumers of the package read this file; it records changes to the
|
|
|
4
4
|
**published API surface** (the exports map in `package.json`, the bins,
|
|
5
5
|
the starter pack and the templates), not the repo's full history.
|
|
6
6
|
|
|
7
|
+
## 1.14.0 — 2026-11-01
|
|
8
|
+
|
|
9
|
+
**Files in a message.** The A2A server takes inline file parts (`raw`, or 0.3's
|
|
10
|
+
`file.bytes`): images and documents the agent's model reads go to it as blobs,
|
|
11
|
+
text types as a text part, and anything else ends `rejected` naming what it
|
|
12
|
+
does read. `A2A_MAX_FILE_BYTES` (default 10 MiB) caps one message's files, and
|
|
13
|
+
0 restores the old refusal; `A2A_BODY_LIMIT` now defaults to what holds that
|
|
14
|
+
limit as base64. A file by URL is still refused. The agent card's
|
|
15
|
+
`defaultInputModes` lists the types.
|
|
16
|
+
|
|
17
|
+
**`attachmentSupport(model)`** (from the barrel) says which file types a
|
|
18
|
+
model's path takes and how large, for a caller that converts a file first.
|
|
19
|
+
|
|
20
|
+
**Claude reads PDFs.** A `application/pdf` blob is a `document` block; it was
|
|
21
|
+
dropped. The chat-completions adapters no longer label a non-image blob as an
|
|
22
|
+
image, which failed the whole request; it is left out with a warning. A failed
|
|
23
|
+
call's recorded request keeps a file's size, not its bytes.
|
|
24
|
+
|
|
7
25
|
## 1.13.0 — 2026-10-24
|
|
8
26
|
|
|
9
27
|
**A Prompt Writer template.** `templates/prompt_writer.yaml` writes
|
package/DOCUMENTATION.md
CHANGED
|
@@ -1056,7 +1056,7 @@ running". The lock is in-process by default and a Postgres advisory lock
|
|
|
1056
1056
|
with `DATABASE_URL`, so it holds across instances. `tasks/cancel` stops a running task, including the model call
|
|
1057
1057
|
in flight, and the task ends `canceled`. Final states: `completed`,
|
|
1058
1058
|
`failed` (the message names the stage and the provider's reason),
|
|
1059
|
-
`canceled`, `rejected` (a file
|
|
1059
|
+
`canceled`, `rejected` (a file its model does not read, a file by URL, an empty message, or the server at
|
|
1060
1060
|
capacity). Message parts may be `text` or `data` (sent to the model as
|
|
1061
1061
|
JSON); `file` parts are refused.
|
|
1062
1062
|
|
package/dist/lib/a2a/app.d.ts
CHANGED
|
@@ -309,8 +309,10 @@ export interface A2AAppOptions {
|
|
|
309
309
|
};
|
|
310
310
|
/** Express `trust proxy` setting (hop count, boolean, or subnet list). */
|
|
311
311
|
trustProxy?: number | boolean | string;
|
|
312
|
-
/** JSON body size limit, e.g. "1mb". */
|
|
312
|
+
/** JSON body size limit, e.g. "1mb". An inline file is base64 in this body: leave room for `maxFileBytes`. */
|
|
313
313
|
bodyLimit?: string;
|
|
314
|
+
/** Largest total of inline files in one message, decoded bytes. Undefined or 0 refuses file parts. */
|
|
315
|
+
maxFileBytes?: number;
|
|
314
316
|
/**
|
|
315
317
|
* Allowlist of agent ids reachable at /:agentId/*. Undefined = any id with
|
|
316
318
|
* a file in the deployment's own agents directory. Ids listed here may also
|
|
@@ -386,6 +388,8 @@ export declare function compileAgentCard(config: SyndicateYamlConfig, opts: {
|
|
|
386
388
|
identity?: IdentityScheme;
|
|
387
389
|
version: string;
|
|
388
390
|
byok?: boolean;
|
|
391
|
+
/** The server takes file parts: the card names what this agent's model reads. */
|
|
392
|
+
files?: boolean;
|
|
389
393
|
}): AgentCard;
|
|
390
394
|
/**
|
|
391
395
|
* The app name a syndicate's sessions and memory are stored under: its
|
package/dist/lib/a2a/app.js
CHANGED
|
@@ -73,6 +73,7 @@ import { setCredentialHosts } from '../tools/credentialHosts.js';
|
|
|
73
73
|
import { syndicateCredentialHostProblems } from '../tools/credentialUses.js';
|
|
74
74
|
import { syndicateOAuthHostProblems } from '../tools/oauthTools.js';
|
|
75
75
|
import { credentialValue } from '../models/credentialSource.js';
|
|
76
|
+
import { attachmentSupport } from '../models/attachments.js';
|
|
76
77
|
const SURFACE_HEADERS = [
|
|
77
78
|
['x-surface', 'name'],
|
|
78
79
|
['x-surface-guild', 'guild'],
|
|
@@ -183,6 +184,7 @@ const scopeUserBuilder = async () => {
|
|
|
183
184
|
*/
|
|
184
185
|
export function compileAgentCard(config, opts) {
|
|
185
186
|
const base = `${opts.baseUrl.replace(/\/$/, '')}${opts.routePrefix ?? ''}`;
|
|
187
|
+
const fileSupport = opts.files ? attachmentSupport(config.orchestrator?.model) : undefined;
|
|
186
188
|
const securitySchemes = {};
|
|
187
189
|
if (opts.byok) {
|
|
188
190
|
securitySchemes.apiKey = {
|
|
@@ -242,7 +244,7 @@ export function compileAgentCard(config, opts) {
|
|
|
242
244
|
},
|
|
243
245
|
securitySchemes,
|
|
244
246
|
securityRequirements: Object.keys(requirement).length ? [{ schemes: requirement }] : [],
|
|
245
|
-
defaultInputModes: ['text/plain', 'application/json'],
|
|
247
|
+
defaultInputModes: ['text/plain', 'application/json', ...(fileSupport ? [...fileSupport.images, ...fileSupport.documents, ...fileSupport.media] : [])],
|
|
246
248
|
defaultOutputModes: ['text/plain'],
|
|
247
249
|
skills: (config.subagents ?? []).map((sub) => ({
|
|
248
250
|
id: sub.name,
|
|
@@ -541,6 +543,7 @@ export async function createA2AApp(options) {
|
|
|
541
543
|
memoryService: services.memoryService,
|
|
542
544
|
compileFor,
|
|
543
545
|
taskTimeoutMs: options.taskTimeoutMs,
|
|
546
|
+
maxFileBytes: options.maxFileBytes,
|
|
544
547
|
streamText: options.streamText,
|
|
545
548
|
turnLock,
|
|
546
549
|
turnLockWaitMs: options.turnLockWaitMs,
|
|
@@ -556,6 +559,7 @@ export async function createA2AApp(options) {
|
|
|
556
559
|
});
|
|
557
560
|
const version = executor.configHashFor().slice(0, 12);
|
|
558
561
|
const cardFor = (base) => compileAgentCard(cfg, {
|
|
562
|
+
files: !!options.maxFileBytes,
|
|
559
563
|
baseUrl: base,
|
|
560
564
|
routePrefix,
|
|
561
565
|
bearer: !!options.serverSecret,
|
|
@@ -26,6 +26,7 @@ import type { TaskRecord } from '../observability/metrics.ts';
|
|
|
26
26
|
import type { AuditSink } from '../observability/audit.ts';
|
|
27
27
|
import type { Policy } from './policy.ts';
|
|
28
28
|
import type { Metadata } from '../observability/metadata.ts';
|
|
29
|
+
import type { AttachmentSupport } from '../models/attachments.ts';
|
|
29
30
|
/** Per-request caller context, set by the server's identity middleware. */
|
|
30
31
|
export interface A2AContext {
|
|
31
32
|
/** The caller's provider key in BYOK mode (or from resolveRequest); '' otherwise. */
|
|
@@ -108,17 +109,24 @@ export declare function describeTurnError(error: {
|
|
|
108
109
|
}): string;
|
|
109
110
|
/** The failed task's message for a turn result. */
|
|
110
111
|
export declare function describeFailedTurn(result: SyndicateTurnResult): string;
|
|
112
|
+
/** What this server takes as a file part: the bytes it allows and what the agent's model reads. */
|
|
113
|
+
export interface FilePartPolicy {
|
|
114
|
+
/** Largest total of files in one message, decoded bytes. */
|
|
115
|
+
maxBytes: number;
|
|
116
|
+
support: AttachmentSupport;
|
|
117
|
+
}
|
|
111
118
|
/**
|
|
112
119
|
* A2A message parts → model parts. Text passes through; a `data` part
|
|
113
120
|
* becomes its JSON as text (callers inject structured payloads this way).
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
121
|
+
* An inline file (`raw` content, or 0.3's `file.bytes`) becomes a blob part
|
|
122
|
+
* the agent's model reads, or text when it is text, within `files`; anything
|
|
123
|
+
* else is refused with a clear error rather than silently blanked. A file by
|
|
124
|
+
* URL is always refused: fetching one server-side is an SSRF surface.
|
|
117
125
|
*
|
|
118
126
|
* Accepts A2A 1.0 parts (`content.$case`) and, for callers that hand in
|
|
119
127
|
* plain objects, the 0.3 shapes (`kind: 'text' | 'data' | 'file'`).
|
|
120
128
|
*/
|
|
121
|
-
export declare function a2aPartsToMessage(rawParts: unknown[]): {
|
|
129
|
+
export declare function a2aPartsToMessage(rawParts: unknown[], files?: FilePartPolicy): {
|
|
122
130
|
parts: MessagePart[];
|
|
123
131
|
refused?: string;
|
|
124
132
|
};
|
|
@@ -132,6 +140,8 @@ export interface ExecutorOptions {
|
|
|
132
140
|
compileFor: (ctx: A2AContext) => CompileOptions;
|
|
133
141
|
/** Wall-clock budget per task in ms; 0 or undefined = none. */
|
|
134
142
|
taskTimeoutMs?: number;
|
|
143
|
+
/** Largest total of inline files in one message, decoded bytes; 0 or undefined refuses file parts. */
|
|
144
|
+
maxFileBytes?: number;
|
|
135
145
|
/** Shared across every executor on the server. */
|
|
136
146
|
limiter: TaskLimiter;
|
|
137
147
|
/** The agent id this executor serves ('' for the default syndicate). */
|
package/dist/lib/a2a/executor.js
CHANGED
|
@@ -27,6 +27,7 @@ import { isWorkflowSyndicate } from '../workflow.js';
|
|
|
27
27
|
import { describeInput } from '../runtime/syndicateTurn.js';
|
|
28
28
|
import { describeConsent } from '../runtime/credentials.js';
|
|
29
29
|
import { MAX_META_KEYS, metaAttributes, requestMetadata } from '../observability/metadata.js';
|
|
30
|
+
import { attachmentSupport, takesBlob } from '../models/attachments.js';
|
|
30
31
|
export const requestContextStorage = new AsyncLocalStorage();
|
|
31
32
|
/** The app name every A2A session and memory silo is stored under. */
|
|
32
33
|
export const A2A_APP_NAME = 'melchizedek-a2a';
|
|
@@ -143,19 +144,62 @@ export function describeFailedTurn(result) {
|
|
|
143
144
|
}
|
|
144
145
|
return `Error: [${error.code}] The agent run failed — ${describeTurnError(error)}`;
|
|
145
146
|
}
|
|
147
|
+
/** Types read as text whatever the model: they go to it as a text part. */
|
|
148
|
+
const TEXT_FILE = /^(text\/[\w.+-]+|application\/(json|xml|yaml|x-yaml|x-ndjson|csv))$/;
|
|
149
|
+
/** A file's name as the model reads it: printable and short, since the caller chose it. */
|
|
150
|
+
const fileLabel = (name) => (typeof name === 'string' ? name : '').replace(/[^\p{L}\p{N} ._()-]/gu, '_').slice(0, 120) || 'file';
|
|
151
|
+
/** One inline file as model parts, or why it is refused. */
|
|
152
|
+
function filePart(bytes, declared, name, policy) {
|
|
153
|
+
const type = String(declared ?? '').split(';')[0].trim().toLowerCase();
|
|
154
|
+
const shown = type.replace(/[^\x20-\x7e]/g, '?').slice(0, 64) || 'a file with no type';
|
|
155
|
+
if (TEXT_FILE.test(type)) {
|
|
156
|
+
let text;
|
|
157
|
+
try {
|
|
158
|
+
text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return `The file part declared as ${shown} is not UTF-8 text.`;
|
|
162
|
+
}
|
|
163
|
+
return { text: `<attached_file name="${fileLabel(name)}" type="${shown}">\n${text}\n</attached_file>` };
|
|
164
|
+
}
|
|
165
|
+
if (!takesBlob(policy.support, type))
|
|
166
|
+
return `This agent's model does not read ${shown}. It reads ${[...policy.support.images, ...policy.support.documents, ...policy.support.media].join(', ')} and text.`;
|
|
167
|
+
const limit = policy.support.images.includes(type) ? policy.support.maxImageBytes : policy.support.maxFileBytes;
|
|
168
|
+
if (bytes.length > limit)
|
|
169
|
+
return `A ${shown} file may be at most ${limit} bytes for this agent's model; this one is ${bytes.length}.`;
|
|
170
|
+
return { inlineData: { mimeType: type, data: bytes.toString('base64') } };
|
|
171
|
+
}
|
|
146
172
|
/**
|
|
147
173
|
* A2A message parts → model parts. Text passes through; a `data` part
|
|
148
174
|
* becomes its JSON as text (callers inject structured payloads this way).
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
175
|
+
* An inline file (`raw` content, or 0.3's `file.bytes`) becomes a blob part
|
|
176
|
+
* the agent's model reads, or text when it is text, within `files`; anything
|
|
177
|
+
* else is refused with a clear error rather than silently blanked. A file by
|
|
178
|
+
* URL is always refused: fetching one server-side is an SSRF surface.
|
|
152
179
|
*
|
|
153
180
|
* Accepts A2A 1.0 parts (`content.$case`) and, for callers that hand in
|
|
154
181
|
* plain objects, the 0.3 shapes (`kind: 'text' | 'data' | 'file'`).
|
|
155
182
|
*/
|
|
156
|
-
export function a2aPartsToMessage(rawParts) {
|
|
183
|
+
export function a2aPartsToMessage(rawParts, files) {
|
|
157
184
|
const parts = [];
|
|
158
|
-
const refusal = 'File parts are not supported by this server; send text or data parts.';
|
|
185
|
+
const refusal = files ? 'A file part given by URL is not fetched by this server; send its bytes.' : 'File parts are not supported by this server; send text or data parts.';
|
|
186
|
+
let total = 0;
|
|
187
|
+
/** Adds one inline file; a string is why the message is refused. */
|
|
188
|
+
const addFile = (value, declared, name) => {
|
|
189
|
+
if (!files)
|
|
190
|
+
return refusal;
|
|
191
|
+
const bytes = typeof value === 'string' ? Buffer.from(value, 'base64') : value instanceof Uint8Array ? Buffer.from(value) : null;
|
|
192
|
+
if (!bytes || bytes.length === 0)
|
|
193
|
+
return 'A file part has no bytes.';
|
|
194
|
+
total += bytes.length;
|
|
195
|
+
if (total > files.maxBytes)
|
|
196
|
+
return `The files in one message may total at most ${files.maxBytes} bytes on this server.`;
|
|
197
|
+
const part = filePart(bytes, declared, name, files);
|
|
198
|
+
if (typeof part === 'string')
|
|
199
|
+
return part;
|
|
200
|
+
parts.push(part);
|
|
201
|
+
return undefined;
|
|
202
|
+
};
|
|
159
203
|
for (const raw of rawParts) {
|
|
160
204
|
if (typeof raw === 'string') {
|
|
161
205
|
parts.push({ text: raw });
|
|
@@ -168,8 +212,13 @@ export function a2aPartsToMessage(rawParts) {
|
|
|
168
212
|
parts.push({ text: content.value });
|
|
169
213
|
else if (content.$case === 'data')
|
|
170
214
|
parts.push({ text: JSON.stringify(content.value) });
|
|
171
|
-
else if (content.$case === 'url'
|
|
215
|
+
else if (content.$case === 'url')
|
|
172
216
|
return { parts, refused: refusal };
|
|
217
|
+
else if (content.$case === 'raw') {
|
|
218
|
+
const refused = addFile(content.value, p.mediaType, p.filename);
|
|
219
|
+
if (refused)
|
|
220
|
+
return { parts, refused };
|
|
221
|
+
}
|
|
173
222
|
continue;
|
|
174
223
|
}
|
|
175
224
|
const kind = p.kind ?? (p.text !== undefined ? 'text' : p.data !== undefined ? 'data' : p.file !== undefined ? 'file' : undefined);
|
|
@@ -181,7 +230,11 @@ export function a2aPartsToMessage(rawParts) {
|
|
|
181
230
|
parts.push({ text: JSON.stringify(p.data) });
|
|
182
231
|
}
|
|
183
232
|
else if (kind === 'file') {
|
|
184
|
-
|
|
233
|
+
if (p.file?.bytes === undefined)
|
|
234
|
+
return { parts, refused: refusal };
|
|
235
|
+
const refused = addFile(p.file.bytes, p.file.mimeType, p.file.name);
|
|
236
|
+
if (refused)
|
|
237
|
+
return { parts, refused };
|
|
185
238
|
}
|
|
186
239
|
}
|
|
187
240
|
return { parts };
|
|
@@ -606,7 +659,7 @@ export class SyndicateExecutor {
|
|
|
606
659
|
throw new Error('No authentication context available.');
|
|
607
660
|
ctxForReport = ctx;
|
|
608
661
|
const rawParts = message?.parts ?? message?.content ?? [];
|
|
609
|
-
const mapped = a2aPartsToMessage(rawParts);
|
|
662
|
+
const mapped = a2aPartsToMessage(rawParts, this.opts.maxFileBytes ? { maxBytes: this.opts.maxFileBytes, support: attachmentSupport(this.opts.config.orchestrator?.model, { callerKey: !!ctx.apiKey }) } : undefined);
|
|
610
663
|
const refused = mapped.refused;
|
|
611
664
|
let parts = mapped.parts;
|
|
612
665
|
if (refused) {
|
package/dist/lib/index.d.ts
CHANGED
|
@@ -68,6 +68,8 @@ export type { MemoryStore } from './memory/store.ts';
|
|
|
68
68
|
export { validateSyndicateConfig, syndicateJsonSchema, SyndicateValidationError } from './syndicateSchema.ts';
|
|
69
69
|
export { PROVIDERS, providerForModel, providerKeyPresent, providerStatuses, logProviderStatuses, resolveModel, describeCapabilities, capabilitySummary, planTransport, gatewayConfig, gatewayProblem, gatewayUsable, GATEWAYS, } from './models/registry.ts';
|
|
70
70
|
export { loadEnv } from './loadEnv.ts';
|
|
71
|
+
export { attachmentSupport, takesBlob } from './models/attachments.ts';
|
|
72
|
+
export type { AttachmentSupport } from './models/attachments.ts';
|
|
71
73
|
export { setCredentialSource, credentialValue } from './models/credentialSource.ts';
|
|
72
74
|
export type { CredentialSource } from './models/credentialSource.ts';
|
|
73
75
|
export { PROJECT_MANIFEST, projectManifestSchema, ProjectManifestError, findProject, findProjectRoot, loadProject, parseProjectManifest, resolveAgentsDir, listProjectSyndicates, checkProject, type Project, type ProjectManifest, type ProjectCheck, } from './project.ts';
|
package/dist/lib/index.js
CHANGED
|
@@ -54,6 +54,8 @@ export { PROVIDERS, providerForModel, providerKeyPresent, providerStatuses, logP
|
|
|
54
54
|
// subpath until 0.16.0.
|
|
55
55
|
describeCapabilities, capabilitySummary, planTransport, gatewayConfig, gatewayProblem, gatewayUsable, GATEWAYS, } from './models/registry.js';
|
|
56
56
|
export { loadEnv } from './loadEnv.js';
|
|
57
|
+
// Which files a model's path takes in a user message, and how large.
|
|
58
|
+
export { attachmentSupport, takesBlob } from './models/attachments.js';
|
|
57
59
|
// Where credentials are read, by environment variable name (ADR 0133).
|
|
58
60
|
export { setCredentialSource, credentialValue } from './models/credentialSource.js';
|
|
59
61
|
// Project manifest, melchizedek.json (ADR 0138).
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/models/attachments.ts — which files a model's path takes in a user
|
|
3
|
+
* message, and how large.
|
|
4
|
+
*
|
|
5
|
+
* WHY this file exists:
|
|
6
|
+
* A file reaches a model as a blob part, and what an adapter can send
|
|
7
|
+
* differs by provider: Gemini takes audio and video, the Messages API and
|
|
8
|
+
* the Responses API take PDFs, a chat-completions path takes images only. A
|
|
9
|
+
* caller that converts or shrinks a file first (the desktop app), and the
|
|
10
|
+
* A2A server that refuses one, read the same table, so neither learns of a
|
|
11
|
+
* mismatch from a provider's 400.
|
|
12
|
+
*
|
|
13
|
+
* Limits are the provider's published ones, set a little under where the
|
|
14
|
+
* base64 a request carries is what is counted. `maxRequestBytes` is decoded
|
|
15
|
+
* bytes across a whole request, and a conversation's earlier files are in it.
|
|
16
|
+
*/
|
|
17
|
+
export interface AttachmentSupport {
|
|
18
|
+
/** Image types sent as images. */
|
|
19
|
+
images: readonly string[];
|
|
20
|
+
/** Document types sent as documents; anything else must arrive as text. */
|
|
21
|
+
documents: readonly string[];
|
|
22
|
+
/** Audio and video types sent as they are. */
|
|
23
|
+
media: readonly string[];
|
|
24
|
+
/** The longest image edge worth sending, in pixels: past it the provider shrinks the image or refuses it. */
|
|
25
|
+
maxImageEdge: number;
|
|
26
|
+
/** Largest image, decoded bytes. */
|
|
27
|
+
maxImageBytes: number;
|
|
28
|
+
/** Largest document or media file, decoded bytes. */
|
|
29
|
+
maxFileBytes: number;
|
|
30
|
+
/** Largest total of files in one request, decoded bytes. */
|
|
31
|
+
maxRequestBytes: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* What `model` takes on the path it will actually run on. A gateway speaks
|
|
35
|
+
* chat completions whichever provider is behind it, so a direct-only type is
|
|
36
|
+
* not offered there. An id no path serves reads as the plain table.
|
|
37
|
+
*/
|
|
38
|
+
export declare function attachmentSupport(model: unknown, opts?: {
|
|
39
|
+
callerKey?: boolean;
|
|
40
|
+
}): AttachmentSupport;
|
|
41
|
+
/** Whether a media type is sent as a blob on this path (not whether it fits). */
|
|
42
|
+
export declare function takesBlob(support: AttachmentSupport, mediaType: string): boolean;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/models/attachments.ts — which files a model's path takes in a user
|
|
3
|
+
* message, and how large.
|
|
4
|
+
*
|
|
5
|
+
* WHY this file exists:
|
|
6
|
+
* A file reaches a model as a blob part, and what an adapter can send
|
|
7
|
+
* differs by provider: Gemini takes audio and video, the Messages API and
|
|
8
|
+
* the Responses API take PDFs, a chat-completions path takes images only. A
|
|
9
|
+
* caller that converts or shrinks a file first (the desktop app), and the
|
|
10
|
+
* A2A server that refuses one, read the same table, so neither learns of a
|
|
11
|
+
* mismatch from a provider's 400.
|
|
12
|
+
*
|
|
13
|
+
* Limits are the provider's published ones, set a little under where the
|
|
14
|
+
* base64 a request carries is what is counted. `maxRequestBytes` is decoded
|
|
15
|
+
* bytes across a whole request, and a conversation's earlier files are in it.
|
|
16
|
+
*/
|
|
17
|
+
import { planTransport } from './gateway.js';
|
|
18
|
+
const MB = 1024 * 1024;
|
|
19
|
+
/** Any path that takes a data URI: the chat-completions adapters, Ollama and the gateways. */
|
|
20
|
+
const PLAIN = {
|
|
21
|
+
images: ['image/jpeg', 'image/png'],
|
|
22
|
+
documents: [],
|
|
23
|
+
media: [],
|
|
24
|
+
maxImageEdge: 1568,
|
|
25
|
+
maxImageBytes: 8 * MB,
|
|
26
|
+
maxFileBytes: 0,
|
|
27
|
+
maxRequestBytes: 14 * MB,
|
|
28
|
+
};
|
|
29
|
+
const DIRECT = {
|
|
30
|
+
anthropic: {
|
|
31
|
+
images: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
|
|
32
|
+
documents: ['application/pdf'],
|
|
33
|
+
media: [],
|
|
34
|
+
maxImageEdge: 1568,
|
|
35
|
+
maxImageBytes: 3.75 * MB,
|
|
36
|
+
maxFileBytes: 22 * MB,
|
|
37
|
+
maxRequestBytes: 22 * MB,
|
|
38
|
+
},
|
|
39
|
+
openai: {
|
|
40
|
+
images: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
|
|
41
|
+
documents: ['application/pdf'],
|
|
42
|
+
media: [],
|
|
43
|
+
maxImageEdge: 2048,
|
|
44
|
+
maxImageBytes: 20 * MB,
|
|
45
|
+
maxFileBytes: 32 * MB,
|
|
46
|
+
maxRequestBytes: 36 * MB,
|
|
47
|
+
},
|
|
48
|
+
gemini: {
|
|
49
|
+
images: ['image/jpeg', 'image/png', 'image/webp', 'image/heic', 'image/heif'],
|
|
50
|
+
documents: ['application/pdf'],
|
|
51
|
+
media: [
|
|
52
|
+
'audio/wav', 'audio/mp3', 'audio/mpeg', 'audio/aiff', 'audio/aac', 'audio/ogg', 'audio/flac',
|
|
53
|
+
'video/mp4', 'video/mpeg', 'video/quicktime', 'video/webm',
|
|
54
|
+
],
|
|
55
|
+
maxImageEdge: 3072,
|
|
56
|
+
maxImageBytes: 14 * MB,
|
|
57
|
+
maxFileBytes: 14 * MB,
|
|
58
|
+
maxRequestBytes: 14 * MB,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* What `model` takes on the path it will actually run on. A gateway speaks
|
|
63
|
+
* chat completions whichever provider is behind it, so a direct-only type is
|
|
64
|
+
* not offered there. An id no path serves reads as the plain table.
|
|
65
|
+
*/
|
|
66
|
+
export function attachmentSupport(model, opts = {}) {
|
|
67
|
+
if (typeof model !== 'string' || !model)
|
|
68
|
+
return PLAIN;
|
|
69
|
+
try {
|
|
70
|
+
const plan = planTransport(model, opts);
|
|
71
|
+
return (plan.transport === 'direct' && DIRECT[plan.provider]) || PLAIN;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return PLAIN;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** Whether a media type is sent as a blob on this path (not whether it fits). */
|
|
78
|
+
export function takesBlob(support, mediaType) {
|
|
79
|
+
return support.images.includes(mediaType) || support.documents.includes(mediaType) || support.media.includes(mediaType);
|
|
80
|
+
}
|
|
@@ -837,7 +837,12 @@ export class ChatCompletionsAdapter {
|
|
|
837
837
|
else if (part.data) {
|
|
838
838
|
// Inline only: a data URI for vision models. A URL blob is not
|
|
839
839
|
// sent (Moonshot takes no public image URLs).
|
|
840
|
-
|
|
840
|
+
const type = !part.mimeType || part.mimeType === 'application/octet-stream' ? 'image/png' : part.mimeType;
|
|
841
|
+
// This wire has images only; a PDF labelled as one would fail the whole request.
|
|
842
|
+
if (type.startsWith('image/'))
|
|
843
|
+
images.push({ type: 'image_url', image_url: { url: `data:${type};base64,${part.data}` } });
|
|
844
|
+
else
|
|
845
|
+
console.warn(`⚠ A file part (${type.replace(/[^\x20-\x7e]/g, '?').slice(0, 64)}) is not sent: this path takes images only.`);
|
|
841
846
|
}
|
|
842
847
|
}
|
|
843
848
|
const text = texts.join('\n');
|
|
@@ -105,6 +105,16 @@ function imageBlockFor(part, urlImages) {
|
|
|
105
105
|
const named = (type) => type.replace(/[^\x20-\x7e]/g, '?').slice(0, 64);
|
|
106
106
|
const declared = String(part.mimeType ?? '').toLowerCase();
|
|
107
107
|
const untyped = UNTYPED.has(declared);
|
|
108
|
+
if (declared === 'application/pdf') {
|
|
109
|
+
if (part.data !== undefined)
|
|
110
|
+
return part.data ? { type: 'document', source: { type: 'base64', media_type: 'application/pdf', data: part.data } } : undefined;
|
|
111
|
+
const uri = String(part.url ?? '');
|
|
112
|
+
if (!uri)
|
|
113
|
+
return undefined;
|
|
114
|
+
if (!/^https:\/\//i.test(uri))
|
|
115
|
+
return `non-https URL (${/^[a-z][a-z0-9+.-]*:/i.exec(uri)?.[0] ?? 'no scheme'})`;
|
|
116
|
+
return urlImages ? { type: 'document', source: { type: 'url', url: uri } } : URL_SOURCE;
|
|
117
|
+
}
|
|
108
118
|
if (part.data !== undefined) {
|
|
109
119
|
if (!part.data)
|
|
110
120
|
return undefined;
|
|
@@ -628,7 +628,7 @@ function safePayloadJson(value) {
|
|
|
628
628
|
/** What `llm.payload.request` records for a failed call: the ModelRequest less its signal, else the older `llmRequest`. */
|
|
629
629
|
function payloadRequest(meta) {
|
|
630
630
|
if (meta.request === undefined)
|
|
631
|
-
return meta.llmRequest;
|
|
631
|
+
return withoutFileBytes(meta.llmRequest);
|
|
632
632
|
let request;
|
|
633
633
|
try {
|
|
634
634
|
request = typeof meta.request === 'function' ? meta.request() : meta.request;
|
|
@@ -637,7 +637,18 @@ function payloadRequest(meta) {
|
|
|
637
637
|
return undefined;
|
|
638
638
|
}
|
|
639
639
|
const { signal: _signal, ...recorded } = request;
|
|
640
|
-
return recorded;
|
|
640
|
+
return withoutFileBytes(recorded);
|
|
641
|
+
}
|
|
642
|
+
/** A copy with each file's bytes replaced by their count: a request's base64 would fill the payload's cap and say nothing a reader can use. */
|
|
643
|
+
function withoutFileBytes(value) {
|
|
644
|
+
if (Array.isArray(value))
|
|
645
|
+
return value.map(withoutFileBytes);
|
|
646
|
+
if (!value || typeof value !== 'object')
|
|
647
|
+
return value;
|
|
648
|
+
const v = value;
|
|
649
|
+
if (typeof v.mimeType === 'string' && typeof v.data === 'string')
|
|
650
|
+
return { ...v, data: `[${Math.floor((v.data.length * 3) / 4)} bytes not recorded]` };
|
|
651
|
+
return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, withoutFileBytes(x)]));
|
|
641
652
|
}
|
|
642
653
|
/**
|
|
643
654
|
* Providers throw errors whose message IS the API's JSON error body
|
|
@@ -46,7 +46,8 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
46
46
|
* A2A_AUTH_FAILURE_MAX failed authentications per window per IP (30)
|
|
47
47
|
* A2A_TRUST_PROXY Express trust-proxy: hop count, true/false, or subnets
|
|
48
48
|
* (false; required with PUBLIC_URL)
|
|
49
|
-
*
|
|
49
|
+
* A2A_MAX_FILE_BYTES inline files in one message, decoded bytes (10485760; 0 refuses file parts)
|
|
50
|
+
* A2A_BODY_LIMIT JSON body limit ("1mb", or "15mb" with file parts on)
|
|
50
51
|
* A2A_KEY_MODE server (default): models run on the server's keys;
|
|
51
52
|
* byok: the caller's X-API-Key pays. Under A2A_AUTH=secret
|
|
52
53
|
* the mode also scopes data: X-User-Id in server mode,
|
|
@@ -479,7 +480,9 @@ export async function startServer(syndicateName = 'syndicate.yaml', extensions =
|
|
|
479
480
|
authFailureLimit: { windowMs: 15 * 60 * 1000, max: envInt('A2A_AUTH_FAILURE_MAX', 30) },
|
|
480
481
|
trustProxy: envTrustProxy(),
|
|
481
482
|
...(limitStore ? { limitStore } : {}),
|
|
482
|
-
|
|
483
|
+
maxFileBytes: envInt('A2A_MAX_FILE_BYTES', 10 * 1024 * 1024),
|
|
484
|
+
// Base64 is 4/3 of the bytes; the body must hold what the file limit allows.
|
|
485
|
+
bodyLimit: process.env.A2A_BODY_LIMIT?.trim() || `${Math.max(1, Math.ceil((envInt('A2A_MAX_FILE_BYTES', 10 * 1024 * 1024) * 4) / 3 / (1024 * 1024)) + 1)}mb`,
|
|
483
486
|
servedAgents,
|
|
484
487
|
registryAgents,
|
|
485
488
|
keyMode,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "melchizedek-agents",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
4
4
|
"description": "A multi-model, multi-agent orchestration framework — YAML-defined syndicates on the engine's own agent loop, Supabase or Postgres session persistence, pgvector long-term memory. Companion repo to the lyceumagents.com curriculum.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|