agent-accelerator 0.5.5 → 0.5.6
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/README.md +10 -4
- package/package.json +1 -1
- package/src/agent/loop.ts +1 -1
- package/src/providers/google.ts +6 -39
- package/src/providers/openai-compat.ts +31 -104
- package/src/providers/openai.ts +30 -103
- package/src/providers/openrouter.ts +102 -175
- package/src/providers/shared.ts +134 -5
- package/src/providers.ts +21 -6
- package/src/session/store.ts +3 -3
- package/src/types/message.ts +1 -1
- package/src/utils/errors.ts +4 -1
- package/src/utils/headers.ts +1 -1
- package/src/utils/media.ts +10 -1
package/src/providers/shared.ts
CHANGED
|
@@ -9,6 +9,8 @@ import { toConciseProviderError } from "../utils/errors.ts";
|
|
|
9
9
|
import { withRetries } from "../utils/retry.ts";
|
|
10
10
|
import { redactHeadersCentral } from "../utils/cache.ts";
|
|
11
11
|
import { TRUNCATED_ARGS_KEY, isTruncatedArguments } from "../types/tool.ts";
|
|
12
|
+
import { SSEParser } from "../streaming/sse-parser.ts";
|
|
13
|
+
import type { AssistantMessageEventStream } from "../streaming/event-stream.ts";
|
|
12
14
|
|
|
13
15
|
/** Parses a JSON tool-argument string; non-objects degrade to the truncated-arguments marker. */
|
|
14
16
|
export function parseArguments(raw: string | undefined): Record<string, unknown> {
|
|
@@ -43,17 +45,17 @@ export function redactedHeaders(headers: Record<string, string>): Record<string,
|
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
/**
|
|
46
|
-
* Extracts `{ message, code, errorType, raw }` from a non-2xx JSON body. Handles
|
|
47
|
-
* OpenAI `type`, OpenRouter `metadata.error_type` + `metadata.raw`, and Google
|
|
48
|
+
* Extracts `{ message, code, errorType, raw, limitSource }` from a non-2xx JSON body. Handles
|
|
49
|
+
* OpenAI `type`, OpenRouter `metadata.error_type` + `metadata.raw` + `metadata.limit_source`, and Google
|
|
48
50
|
* `status` code shapes; unknown bodies degrade to a 300-char slice.
|
|
49
51
|
*/
|
|
50
52
|
export function readErrorPayload(
|
|
51
53
|
bodyText: string
|
|
52
|
-
): { message: string; code?: string | number; errorType?: string; raw?: string } {
|
|
54
|
+
): { message: string; code?: string | number; errorType?: string; raw?: string; limitSource?: string } {
|
|
53
55
|
try {
|
|
54
56
|
const parsed: unknown = JSON.parse(bodyText);
|
|
55
57
|
const first = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
56
|
-
const err = (first as { error?: { message?: string; code?: string | number; type?: string; status?: string | number; metadata?: { error_type?: string; raw?: unknown } } })?.error;
|
|
58
|
+
const err = (first as { error?: { message?: string; code?: string | number; type?: string; status?: string | number; metadata?: { error_type?: string; raw?: unknown; limit_source?: unknown } } })?.error;
|
|
57
59
|
if (err && typeof err.message === "string") {
|
|
58
60
|
const code = err.code ?? err.status;
|
|
59
61
|
const errorType =
|
|
@@ -67,6 +69,7 @@ export function readErrorPayload(
|
|
|
67
69
|
...(code !== undefined ? { code } : {}),
|
|
68
70
|
...(typeof errorType === "string" ? { errorType } : {}),
|
|
69
71
|
...(raw ? { raw } : {}),
|
|
72
|
+
...(typeof err.metadata?.limit_source === "string" ? { limitSource: err.metadata.limit_source } : {}),
|
|
70
73
|
};
|
|
71
74
|
}
|
|
72
75
|
return { message: bodyText.slice(0, 300) };
|
|
@@ -180,6 +183,12 @@ export function hasTruncatedToolArguments(args: unknown): boolean {
|
|
|
180
183
|
return isTruncatedArguments(args);
|
|
181
184
|
}
|
|
182
185
|
|
|
186
|
+
// True when a stream with no terminal event left a tool call whose arguments never fully arrived (none at all, or cut mid-JSON); in: the adapter's assembled calls and their records, out: its incomplete_stream guard.
|
|
187
|
+
export function hasUnfinishedToolCall(calls: Iterable<{ startArgs: string; deltaArgs: string }>, toolCalls: Array<{ arguments: unknown }>): boolean {
|
|
188
|
+
for (const c of calls) if (!c.startArgs.trim() && !c.deltaArgs.trim()) return true;
|
|
189
|
+
return toolCalls.some((c) => isTruncatedArguments(c.arguments));
|
|
190
|
+
}
|
|
191
|
+
|
|
183
192
|
/** Short hash for tool-ID clamping (FNV-1a, 8 hex chars, no deps). */
|
|
184
193
|
export function shortHashId(value: string): string {
|
|
185
194
|
let h = 0x811c9dc5;
|
|
@@ -198,7 +207,10 @@ export function shortHashId(value: string): string {
|
|
|
198
207
|
export function clampToolCallId(id: string): string {
|
|
199
208
|
const clean = String(id ?? "").replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
200
209
|
if (!clean.includes("|")) {
|
|
201
|
-
|
|
210
|
+
if (clean.length <= 40) return clean || "call_unknown";
|
|
211
|
+
// A hard cut would give two long ids with the same first 40 characters the same id; a hash of the whole id keeps them apart.
|
|
212
|
+
const hash = shortHashId(id);
|
|
213
|
+
return `${clean.slice(0, 40 - hash.length - 1)}_${hash}`;
|
|
202
214
|
}
|
|
203
215
|
const sep = clean.indexOf("|");
|
|
204
216
|
const callId = clean.slice(0, sep).replace(/_+$/, "") || "call";
|
|
@@ -210,6 +222,16 @@ export function clampToolCallId(id: string): string {
|
|
|
210
222
|
return `${prefix}_${hash}`;
|
|
211
223
|
}
|
|
212
224
|
|
|
225
|
+
// The finish reason of a turn that produced tool calls: `tool_calls`, except that a turn cut off by the output limit (`length`) keeps saying so when one of its calls was cut mid-JSON; in: the wire finish reason + the assembled calls, out: each streaming adapter.
|
|
226
|
+
export function toolTurnFinishReason(finishReason: string, toolCalls: Array<{ arguments: unknown }>): string {
|
|
227
|
+
return finishReason === "length" && toolCalls.some((c) => isTruncatedArguments(c.arguments)) ? "length" : "tool_calls";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// The JSON text of a tool call's arguments for a history message: `{}` for a call whose streamed arguments were cut off (the internal `__truncated_raw` marker was never the model's own output and is never resent); in: a stored tool call, out: each adapter's history builder.
|
|
231
|
+
export function toolArgumentsJson(args: unknown): string {
|
|
232
|
+
return isTruncatedArguments(args) ? "{}" : JSON.stringify(args || {});
|
|
233
|
+
}
|
|
234
|
+
|
|
213
235
|
/** Plain lower-cased header record from a fetch `Headers` object. */
|
|
214
236
|
export function headersToRecord(headers: Headers): Record<string, string> {
|
|
215
237
|
const out: Record<string, string> = {};
|
|
@@ -282,3 +304,110 @@ export function retryOptionsFrom(options: {
|
|
|
282
304
|
retryBudgetMs: options?.retryBudgetMs,
|
|
283
305
|
};
|
|
284
306
|
}
|
|
307
|
+
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
// Streaming scaffold shared by the SSE adapters (abort linking, request timer, read loop, end-of-stream check, failure)
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
|
|
312
|
+
// Ties a provider stream to the caller's signal and to the stream's own cancel: one controller that aborts when either does; in: an adapter's stream(), out: the controller its request uses and the `release` its finally block calls.
|
|
313
|
+
export function linkStreamAbort(eventStream: AssistantMessageEventStream, signal: AbortSignal | undefined): { linked: AbortController; release: () => void } {
|
|
314
|
+
const linked = new AbortController();
|
|
315
|
+
const forwardUserAbort = (): void => {
|
|
316
|
+
try {
|
|
317
|
+
linked.abort((signal as { reason?: unknown } | undefined)?.reason);
|
|
318
|
+
} catch {
|
|
319
|
+
try {
|
|
320
|
+
linked.abort();
|
|
321
|
+
} catch {}
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
if (signal?.aborted) forwardUserAbort();
|
|
325
|
+
else signal?.addEventListener("abort", forwardUserAbort, { once: true });
|
|
326
|
+
const removeStreamCancel = eventStream.onCancel(() => {
|
|
327
|
+
try {
|
|
328
|
+
linked.abort();
|
|
329
|
+
} catch {}
|
|
330
|
+
});
|
|
331
|
+
return {
|
|
332
|
+
linked,
|
|
333
|
+
release: () => {
|
|
334
|
+
try {
|
|
335
|
+
signal?.removeEventListener("abort", forwardUserAbort);
|
|
336
|
+
} catch {}
|
|
337
|
+
try {
|
|
338
|
+
removeStreamCancel();
|
|
339
|
+
} catch {}
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Runs one request attempt under a timer that aborts `controller` after `ms` (none when unset); in: an adapter's fetchOnce, out: its Response.
|
|
345
|
+
export async function withAbortTimer<T>(controller: AbortController, ms: number | undefined, run: () => Promise<T>): Promise<T> {
|
|
346
|
+
const timer =
|
|
347
|
+
ms && ms > 0
|
|
348
|
+
? setTimeout(() => {
|
|
349
|
+
try {
|
|
350
|
+
controller.abort();
|
|
351
|
+
} catch {}
|
|
352
|
+
}, ms)
|
|
353
|
+
: undefined;
|
|
354
|
+
try {
|
|
355
|
+
return await run();
|
|
356
|
+
} finally {
|
|
357
|
+
if (timer) clearTimeout(timer);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Reads an SSE response body to its end, handing each `data:` payload to `onData`; stops with an AbortError as soon as the stream is aborted or cancelled (cancelling the body, whose rejection is absorbed); in: the opened Response body of an SSE adapter, out: calls to onData.
|
|
362
|
+
export async function readSseData(
|
|
363
|
+
body: ReadableStream<Uint8Array>,
|
|
364
|
+
linked: AbortController,
|
|
365
|
+
eventStream: AssistantMessageEventStream,
|
|
366
|
+
onData: (data: string) => void
|
|
367
|
+
): Promise<void> {
|
|
368
|
+
const parser = new SSEParser();
|
|
369
|
+
const reader = body.getReader();
|
|
370
|
+
const decoder = new TextDecoder();
|
|
371
|
+
const abort = (): Error => Object.assign(new Error("Stream aborted"), { name: "AbortError" });
|
|
372
|
+
linked.signal.addEventListener("abort", () => void reader.cancel().catch(() => {}), { once: true }); // an aborted body is already errored: its cancel() rejects, and nobody else listens
|
|
373
|
+
while (true) {
|
|
374
|
+
if (linked.signal.aborted || eventStream.isCancelled()) {
|
|
375
|
+
try {
|
|
376
|
+
await reader.cancel();
|
|
377
|
+
} catch {}
|
|
378
|
+
throw abort();
|
|
379
|
+
}
|
|
380
|
+
const { done, value } = await reader.read();
|
|
381
|
+
if (done) break;
|
|
382
|
+
for (const m of parser.feed(decoder.decode(value, { stream: true }))) onData(m.data);
|
|
383
|
+
}
|
|
384
|
+
for (const m of parser.flush()) onData(m.data);
|
|
385
|
+
try {
|
|
386
|
+
reader.releaseLock();
|
|
387
|
+
} catch {}
|
|
388
|
+
if (linked.signal.aborted || eventStream.isCancelled()) throw abort();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// The error to fail with when an SSE stream ended without its final event while carrying no content or an unfinished tool call (checked only for real SSE payloads, not JSON mocks); in: the adapter's state after reading, out: `eventStream.fail`, else undefined.
|
|
392
|
+
export function incompleteStreamCheck(a: {
|
|
393
|
+
provider: string;
|
|
394
|
+
modelId: string;
|
|
395
|
+
hasTerminal: boolean;
|
|
396
|
+
empty: boolean;
|
|
397
|
+
unfinishedCall: boolean;
|
|
398
|
+
responseHeaders: Record<string, string>;
|
|
399
|
+
framesSeen: number;
|
|
400
|
+
badFrames: number;
|
|
401
|
+
}): Error | undefined {
|
|
402
|
+
if (a.hasTerminal || !(a.empty || a.unfinishedCall)) return undefined;
|
|
403
|
+
const contentType = Object.entries(a.responseHeaders).find(([k]) => k.toLowerCase() === "content-type")?.[1];
|
|
404
|
+
const isSSEPayload = (typeof contentType === "string" && (contentType.includes("event-stream") || contentType.startsWith("text/"))) || a.framesSeen > 0 || a.badFrames > 0;
|
|
405
|
+
return isSSEPayload ? incompleteStreamError(a.provider, a.modelId) : undefined;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Fails a stream with the right error: an abort (the signal, the stream's cancel, or an AbortError) becomes the plain `AbortError`, anything else passes through; in: an adapter's catch block, out: the stream's result() rejection.
|
|
409
|
+
export function failStream(eventStream: AssistantMessageEventStream, linked: AbortController, err: unknown): void {
|
|
410
|
+
const raw = err instanceof Error ? err : new Error(String(err));
|
|
411
|
+
const isAbort = linked.signal.aborted || eventStream.isCancelled() || (raw as { name?: string }).name === "AbortError";
|
|
412
|
+
eventStream.fail(isAbort ? Object.assign(new Error("Stream aborted"), { name: "AbortError" }) : raw);
|
|
413
|
+
}
|
package/src/providers.ts
CHANGED
|
@@ -248,17 +248,17 @@ export function resolveCache(
|
|
|
248
248
|
return undefined;
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
-
/** Warns once per provider/model that per-media `resolution` is ignored (no such control on that wire); in: an adapter's request messages, out: telemetry bus only. */
|
|
252
|
-
export function warnIgnoredMediaResolution(provider: string, messages: Message[], modelRef: string): void {
|
|
251
|
+
/** Warns once per provider/model that per-media `resolution` is ignored (no such control on that wire for the given kinds of media); in: an adapter's request messages, out: telemetry bus only. */
|
|
252
|
+
export function warnIgnoredMediaResolution(provider: string, messages: Message[], modelRef: string, kinds: Array<"image" | "video"> = ["image", "video"]): void {
|
|
253
253
|
const used = messages.some(
|
|
254
|
-
(m) => Array.isArray(m.content) && m.content.some((p) => (p.type === "image" || p.type === "video") && p.resolution)
|
|
254
|
+
(m) => Array.isArray(m.content) && m.content.some((p) => (p.type === "image" || p.type === "video") && kinds.includes(p.type) && p.resolution)
|
|
255
255
|
);
|
|
256
256
|
if (!used) return;
|
|
257
257
|
emitProviderWarning({
|
|
258
258
|
provider,
|
|
259
259
|
capability: "media resolution",
|
|
260
260
|
requested: modelRef,
|
|
261
|
-
reason: "this provider has no per-media detail control
|
|
261
|
+
reason: "this provider has no per-media detail control for this kind of media.",
|
|
262
262
|
fallback: "the provider's default detail",
|
|
263
263
|
});
|
|
264
264
|
}
|
|
@@ -399,6 +399,7 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
|
399
399
|
* @example `parseStreamedToolArguments("{}", '{"location":"Paris"}')`
|
|
400
400
|
*/
|
|
401
401
|
export function parseStreamedToolArguments(startText: string, deltaText: string): Record<string, unknown> {
|
|
402
|
+
if (!startText.trim() && !deltaText.trim()) return {}; // no argument text at all is a call that takes none
|
|
402
403
|
const candidates = [startText + deltaText, deltaText, startText].filter((c) => c && c.trim());
|
|
403
404
|
for (const candidate of candidates) {
|
|
404
405
|
try {
|
|
@@ -422,6 +423,7 @@ export function parseStreamedToolArguments(startText: string, deltaText: string)
|
|
|
422
423
|
* @example `selectRawArguments("{}", '{"location":"Paris"}') // '{"location":"Paris"}'`
|
|
423
424
|
*/
|
|
424
425
|
export function selectRawArguments(startText: string, deltaText: string): string {
|
|
426
|
+
if (!startText.trim() && !deltaText.trim()) return "{}";
|
|
425
427
|
const candidates = [startText + deltaText, deltaText, startText].filter((c) => c && c.trim());
|
|
426
428
|
for (const candidate of candidates) {
|
|
427
429
|
try {
|
|
@@ -445,6 +447,8 @@ export interface GenerationParams {
|
|
|
445
447
|
seed?: number;
|
|
446
448
|
}
|
|
447
449
|
|
|
450
|
+
/** Most stop sequences a Chat Completions request may carry (OpenRouter chat-completion api-ref `maxItems`, OpenAI the same). */
|
|
451
|
+
const MAX_CHAT_STOP = 4;
|
|
448
452
|
const finiteNumber = (v: unknown): v is number => typeof v === "number" && Number.isFinite(v);
|
|
449
453
|
|
|
450
454
|
/** Normalizes the public `stop` option (`string | string[]`) to a clean array, or undefined. */
|
|
@@ -460,11 +464,22 @@ export function normalizeStop(stop: string | string[] | undefined): string[] | u
|
|
|
460
464
|
* `max_tokens`, `stop`, `seed`. Only set values are
|
|
461
465
|
* emitted, so strict endpoints never see anything the caller did not ask for.
|
|
462
466
|
*/
|
|
463
|
-
export function mapGenerationParamsToChat(params: GenerationParams | undefined): Record<string, unknown> {
|
|
467
|
+
export function mapGenerationParamsToChat(params: GenerationParams | undefined, provider = "chat", modelRef = ""): Record<string, unknown> {
|
|
464
468
|
const out: Record<string, unknown> = {};
|
|
465
469
|
if (!params) return out;
|
|
466
470
|
if (finiteNumber(params.maxOutputTokens) && params.maxOutputTokens > 0) out["max_tokens"] = Math.floor(params.maxOutputTokens);
|
|
467
|
-
if (params.stop && params.stop.length > 0)
|
|
471
|
+
if (params.stop && params.stop.length > 0) {
|
|
472
|
+
out["stop"] = params.stop.slice(0, MAX_CHAT_STOP);
|
|
473
|
+
if (params.stop.length > MAX_CHAT_STOP) {
|
|
474
|
+
emitProviderWarning({
|
|
475
|
+
provider,
|
|
476
|
+
capability: "stop sequences",
|
|
477
|
+
requested: `${params.stop.length} sequences (${modelRef})`,
|
|
478
|
+
reason: `Chat Completions allows at most ${MAX_CHAT_STOP} stop sequences.`,
|
|
479
|
+
fallback: `the first ${MAX_CHAT_STOP}`,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
}
|
|
468
483
|
if (finiteNumber(params.seed)) out["seed"] = Math.floor(params.seed);
|
|
469
484
|
return out;
|
|
470
485
|
}
|
package/src/session/store.ts
CHANGED
|
@@ -1391,7 +1391,7 @@ function resolveEffectivePricing(
|
|
|
1391
1391
|
/**
|
|
1392
1392
|
* Prices one usage snapshot with integer micro-USD accumulation (Q-36).
|
|
1393
1393
|
*
|
|
1394
|
-
* -
|
|
1394
|
+
* - A provider-reported `usage.cost.totalCost` (a number, 0 included: free or cached) wins (source `provider`).
|
|
1395
1395
|
* - Else catalog pricing via `resolveEffectivePricing` (source `catalog`).
|
|
1396
1396
|
* - Else returns `undefined` (source `unknown` is implicit) — unknown stays
|
|
1397
1397
|
* unknown, never a fabricated 0 object.
|
|
@@ -1408,7 +1408,7 @@ export function priceUsage(
|
|
|
1408
1408
|
opts?: { serviceTier?: ServiceTier | string; modelId?: string }
|
|
1409
1409
|
): PricedCost | undefined {
|
|
1410
1410
|
const reported = usage?.cost;
|
|
1411
|
-
if (reported && typeof reported.totalCost === "number" && reported.totalCost
|
|
1411
|
+
if (reported && typeof reported.totalCost === "number" && reported.totalCost >= 0) {
|
|
1412
1412
|
const micro = (d?: number): number => (typeof d === "number" && d > 0 ? Math.round(d * 1_000_000) : 0);
|
|
1413
1413
|
const inputMicroUSD = micro(reported.inputCost);
|
|
1414
1414
|
const outputMicroUSD = micro(reported.outputCost);
|
|
@@ -1492,7 +1492,7 @@ export function addCostToUsage(
|
|
|
1492
1492
|
spec?: ModelSpec,
|
|
1493
1493
|
opts?: { serviceTier?: ServiceTier | string }
|
|
1494
1494
|
): TokenUsage["cost"] | undefined {
|
|
1495
|
-
if (usage?.cost && typeof usage.cost.totalCost === "number" && usage.cost.totalCost
|
|
1495
|
+
if (usage?.cost && typeof usage.cost.totalCost === "number" && usage.cost.totalCost >= 0) {
|
|
1496
1496
|
return usage.cost;
|
|
1497
1497
|
}
|
|
1498
1498
|
const priced = priceUsage(usage, spec, opts);
|
package/src/types/message.ts
CHANGED
|
@@ -9,7 +9,7 @@ export interface TextPart {
|
|
|
9
9
|
thoughtSignature?: string;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
/** Detail level a provider spends on one image or video (more detail = more input tokens). Google
|
|
12
|
+
/** Detail level a provider spends on one image or video (more detail = more input tokens). Google (images and videos) and OpenRouter (images, as `image_url.detail`) honour it; other providers ignore it with a warning. */
|
|
13
13
|
export type MediaResolution = "low" | "medium" | "high" | "ultra_high";
|
|
14
14
|
|
|
15
15
|
/** Image content accepted as a URL, data, path, or binary value. */
|
package/src/utils/errors.ts
CHANGED
|
@@ -113,7 +113,8 @@ const PART_TO_MODALITY: Record<string, "image" | "audio" | "video" | "pdf"> = {
|
|
|
113
113
|
|
|
114
114
|
/**
|
|
115
115
|
* Fails fast — before any network call — when the catalog knows the model
|
|
116
|
-
* lacks a requested input modality (image/audio/video/document
|
|
116
|
+
* lacks a requested input modality (image/audio/video/document; files are
|
|
117
|
+
* not checked on OpenRouter, which parses them for any model). Unknown
|
|
117
118
|
* models (custom providers, dynamic routers) are skipped: the provider
|
|
118
119
|
* verdict stands and surfaces via toConciseProviderError instead.
|
|
119
120
|
*/
|
|
@@ -130,6 +131,8 @@ export function assertModalitiesSupported(
|
|
|
130
131
|
if (mapped) needed.add(mapped);
|
|
131
132
|
}
|
|
132
133
|
}
|
|
134
|
+
// OpenRouter reads a file with the model's native support or its own parser, so the catalog's "pdf" flag never gates a file there.
|
|
135
|
+
if (providerId === "openrouter") needed.delete("pdf");
|
|
133
136
|
if (needed.size === 0) return;
|
|
134
137
|
let supported: readonly string[] | undefined;
|
|
135
138
|
try {
|
package/src/utils/headers.ts
CHANGED
|
@@ -23,7 +23,7 @@ export function getPackageVersion(): string {
|
|
|
23
23
|
const v = (globalThis as any).process?.env?.npm_package_version;
|
|
24
24
|
if (typeof v === "string" && v) return v;
|
|
25
25
|
} catch {}
|
|
26
|
-
return "0.5.
|
|
26
|
+
return "0.5.6";
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
function getAgentAccelUserAgent(): string {
|
package/src/utils/media.ts
CHANGED
|
@@ -26,6 +26,11 @@ export function inferMimeType(input: string, fallback = "application/octet-strea
|
|
|
26
26
|
if (clean.endsWith(".mp3")) return "audio/mp3";
|
|
27
27
|
if (clean.endsWith(".wav")) return "audio/wav";
|
|
28
28
|
if (clean.endsWith(".ogg")) return "audio/ogg";
|
|
29
|
+
if (clean.endsWith(".flac")) return "audio/flac";
|
|
30
|
+
if (clean.endsWith(".m4a")) return "audio/m4a";
|
|
31
|
+
if (clean.endsWith(".aac")) return "audio/aac";
|
|
32
|
+
if (clean.endsWith(".aiff") || clean.endsWith(".aif")) return "audio/aiff";
|
|
33
|
+
if (clean.endsWith(".mpeg") || clean.endsWith(".mpg")) return "video/mpeg";
|
|
29
34
|
if (clean.endsWith(".mp4")) return "video/mp4";
|
|
30
35
|
if (clean.endsWith(".webm")) return "video/webm";
|
|
31
36
|
if (clean.endsWith(".mov")) return "video/quicktime";
|
|
@@ -53,8 +58,12 @@ export function sniffMimeType(bytes: Uint8Array): string | undefined {
|
|
|
53
58
|
if (ascii(bytes, 0, "RIFF") && ascii(bytes, 8, "WAVE")) return "audio/wav";
|
|
54
59
|
if (ascii(bytes, 0, "%PDF")) return "application/pdf";
|
|
55
60
|
if (ascii(bytes, 0, "OggS")) return "audio/ogg";
|
|
61
|
+
if (ascii(bytes, 0, "fLaC")) return "audio/flac";
|
|
62
|
+
if (ascii(bytes, 0, "FORM") && ascii(bytes, 8, "AIF")) return "audio/aiff";
|
|
63
|
+
if (bytes[0] === 0xff && ((bytes[1] ?? 0) & 0xf6) === 0xf0) return "audio/aac"; // ADTS: sync bits plus layer 00 (MP3 frames have a non-zero layer)
|
|
56
64
|
if (ascii(bytes, 0, "ID3") || (bytes[0] === 0xff && ((bytes[1] ?? 0) & 0xe0) === 0xe0)) return "audio/mp3";
|
|
57
|
-
if (ascii(bytes, 4, "ftyp")) return ascii(bytes, 8, "qt") ? "video/quicktime" : "video/mp4";
|
|
65
|
+
if (ascii(bytes, 4, "ftyp")) return ascii(bytes, 8, "M4A") || ascii(bytes, 8, "M4B") ? "audio/m4a" : ascii(bytes, 8, "qt") ? "video/quicktime" : "video/mp4";
|
|
66
|
+
if (bytes[0] === 0 && bytes[1] === 0 && bytes[2] === 1 && bytes[3] === 0xba) return "video/mpeg"; // MPEG program stream
|
|
58
67
|
if (bytes[0] === 0x1a && bytes[1] === 0x45 && bytes[2] === 0xdf && bytes[3] === 0xa3) return "video/webm";
|
|
59
68
|
return undefined;
|
|
60
69
|
}
|