@arnilo/prism 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +32 -20
  3. package/dist/agent-loops.d.ts +8 -1
  4. package/dist/agent-loops.js +57 -11
  5. package/dist/agents.js +70 -17
  6. package/dist/checkpoints.d.ts +11 -0
  7. package/dist/checkpoints.js +144 -0
  8. package/dist/compaction.js +9 -1
  9. package/dist/content.d.ts +102 -0
  10. package/dist/content.js +410 -0
  11. package/dist/contracts.d.ts +142 -2
  12. package/dist/event-multiplexer.d.ts +23 -0
  13. package/dist/event-multiplexer.js +136 -0
  14. package/dist/execution-policy.d.ts +28 -0
  15. package/dist/execution-policy.js +24 -0
  16. package/dist/index.d.ts +17 -5
  17. package/dist/index.js +11 -4
  18. package/dist/input.js +11 -1
  19. package/dist/leases.d.ts +8 -0
  20. package/dist/leases.js +111 -0
  21. package/dist/node/agent-definitions.js +3 -5
  22. package/dist/node/config.d.ts +1 -0
  23. package/dist/node/config.js +5 -3
  24. package/dist/node/contribution-discovery.js +5 -8
  25. package/dist/node/session-store-jsonl.js +8 -5
  26. package/dist/node/settings.js +2 -2
  27. package/dist/node/trust.js +2 -4
  28. package/dist/observability.d.ts +3 -0
  29. package/dist/observability.js +18 -0
  30. package/dist/providers/media.d.ts +42 -0
  31. package/dist/providers/media.js +116 -0
  32. package/dist/providers/openai-compatible.js +18 -119
  33. package/dist/providers/openai-primitives.d.ts +9 -0
  34. package/dist/providers/openai-primitives.js +129 -0
  35. package/dist/providers/transport.d.ts +40 -0
  36. package/dist/providers/transport.js +221 -0
  37. package/dist/redaction.js +40 -13
  38. package/dist/resources.d.ts +5 -0
  39. package/dist/resources.js +4 -0
  40. package/dist/structured-output.d.ts +11 -0
  41. package/dist/structured-output.js +59 -0
  42. package/dist/testing/persistence-schema.d.ts +102 -0
  43. package/dist/testing/persistence-schema.js +457 -0
  44. package/dist/testing/provider-conformance.js +10 -1
  45. package/dist/testing/run-ledger-conformance.d.ts +33 -0
  46. package/dist/testing/run-ledger-conformance.js +172 -0
  47. package/dist/testing/session-store-conformance.d.ts +16 -0
  48. package/dist/testing/session-store-conformance.js +73 -0
  49. package/dist/tools.d.ts +17 -0
  50. package/dist/tools.js +29 -2
  51. package/docs/agent-events.md +13 -4
  52. package/docs/agent-loops.md +10 -4
  53. package/docs/agent-session-runtime.md +1 -0
  54. package/docs/cli-rpc.md +3 -0
  55. package/docs/coding-agent-tools.md +242 -0
  56. package/docs/coding-security.md +84 -0
  57. package/docs/credential-storage.md +177 -0
  58. package/docs/credentials-and-redaction.md +2 -1
  59. package/docs/database-persistence.md +44 -2
  60. package/docs/host-security.md +15 -1
  61. package/docs/index.md +28 -11
  62. package/docs/input-and-prompt-assembly.md +6 -5
  63. package/docs/mcp-tools.md +139 -0
  64. package/docs/middleware-hooks.md +2 -0
  65. package/docs/migration.md +21 -28
  66. package/docs/model-registry.md +5 -3
  67. package/docs/multimodal-content.md +148 -0
  68. package/docs/observability.md +163 -0
  69. package/docs/performance.md +40 -1
  70. package/docs/persistence-credentials-multimodality-primitives.md +303 -0
  71. package/docs/postgres-persistence.md +141 -0
  72. package/docs/provider-conformance.md +17 -0
  73. package/docs/provider-layer.md +1 -1
  74. package/docs/provider-primitives.md +281 -0
  75. package/docs/providers/kimi.md +1 -0
  76. package/docs/providers/neuralwatt.md +1 -0
  77. package/docs/providers/openai-compatible.md +2 -1
  78. package/docs/providers/openai.md +8 -1
  79. package/docs/providers/opencode-go.md +1 -0
  80. package/docs/providers/openrouter.md +1 -0
  81. package/docs/providers/zai.md +1 -0
  82. package/docs/public-contracts.md +9 -2
  83. package/docs/release-and-install.md +209 -23
  84. package/docs/resource-loading.md +14 -4
  85. package/docs/review-coverage-2026-07-14.md +260 -0
  86. package/docs/run-ledger-conformance.md +96 -0
  87. package/docs/runs-and-usage.md +2 -0
  88. package/docs/session-store-conformance.md +16 -0
  89. package/docs/session-stores-and-branching.md +1 -0
  90. package/docs/settings-auth-trust-security.md +2 -1
  91. package/docs/sqlite-persistence.md +122 -0
  92. package/docs/structured-output.md +9 -0
  93. package/docs/tool-conformance.md +1 -0
  94. package/docs/tool-execution-primitives.md +374 -0
  95. package/docs/tools.md +40 -1
  96. package/docs/workflow-orchestration-primitives.md +565 -0
  97. package/docs/workflow-tui-primitives.md +5 -0
  98. package/docs/workflows.md +219 -0
  99. package/package.json +34 -5
package/dist/leases.js ADDED
@@ -0,0 +1,111 @@
1
+ export const LEASE_CONFLICT_CODE = "ERR_PRISM_LEASE_CONFLICT";
2
+ export class LeaseConflictError extends Error {
3
+ code = LEASE_CONFLICT_CODE;
4
+ constructor(message) {
5
+ super(message);
6
+ this.name = "LeaseConflictError";
7
+ }
8
+ }
9
+ /** In-process reference implementation. Durable adapters provide cross-process exclusion. */
10
+ export function createMemoryLeaseStore() {
11
+ const records = new Map();
12
+ return {
13
+ async tryAcquireLease(input) {
14
+ validateAcquire(input);
15
+ const id = keyOf(input);
16
+ const current = records.get(id);
17
+ if (current)
18
+ assertOwnership(input, current);
19
+ const now = Date.now();
20
+ if (current && Date.parse(current.expiresAt) > now)
21
+ return null;
22
+ const timestamp = new Date(now).toISOString();
23
+ const record = {
24
+ namespace: input.namespace,
25
+ key: input.key,
26
+ ownerId: input.ownerId,
27
+ token: crypto.randomUUID(),
28
+ fencingToken: (current?.fencingToken ?? 0) + 1,
29
+ acquiredAt: timestamp,
30
+ expiresAt: new Date(now + input.ttlMs).toISOString(),
31
+ updatedAt: timestamp,
32
+ ...ownership(input),
33
+ };
34
+ records.set(id, record);
35
+ return record;
36
+ },
37
+ async renewLease(input) {
38
+ validateClaim(input, true);
39
+ const current = records.get(keyOf(input));
40
+ if (!current)
41
+ return null;
42
+ assertOwnership(input, current);
43
+ const now = Date.now();
44
+ if (current.ownerId !== input.ownerId || current.token !== input.token || Date.parse(current.expiresAt) <= now)
45
+ return null;
46
+ const record = {
47
+ ...current,
48
+ expiresAt: new Date(now + input.ttlMs).toISOString(),
49
+ updatedAt: new Date(now).toISOString(),
50
+ };
51
+ records.set(keyOf(input), record);
52
+ return record;
53
+ },
54
+ async releaseLease(input) {
55
+ validateClaim(input, false);
56
+ const current = records.get(keyOf(input));
57
+ if (!current)
58
+ return false;
59
+ assertOwnership(input, current);
60
+ if (current.ownerId !== input.ownerId || current.token !== input.token)
61
+ return false;
62
+ const now = new Date().toISOString();
63
+ records.set(keyOf(input), { ...current, expiresAt: now, updatedAt: now });
64
+ return true;
65
+ },
66
+ async getLease(input) {
67
+ throwIfAborted(input.signal);
68
+ const current = records.get(keyOf(input));
69
+ if (!current)
70
+ return null;
71
+ assertOwnership(input, current);
72
+ return Date.parse(current.expiresAt) > Date.now() ? current : null;
73
+ },
74
+ };
75
+ }
76
+ function keyOf(input) {
77
+ return `${input.namespace}\0${input.key}`;
78
+ }
79
+ function ownership(input) {
80
+ return {
81
+ ...(input.tenantId === undefined ? {} : { tenantId: input.tenantId }),
82
+ ...(input.accountId === undefined ? {} : { accountId: input.accountId }),
83
+ ...(input.userId === undefined ? {} : { userId: input.userId }),
84
+ };
85
+ }
86
+ function assertOwnership(expected, actual) {
87
+ if (expected.tenantId !== actual.tenantId || expected.accountId !== actual.accountId || expected.userId !== actual.userId) {
88
+ throw new LeaseConflictError("Lease ownership mismatch");
89
+ }
90
+ }
91
+ function validateKey(input) {
92
+ throwIfAborted(input.signal);
93
+ if (!input.namespace || !input.key)
94
+ throw new LeaseConflictError("Lease namespace and key are required");
95
+ }
96
+ function validateAcquire(input) {
97
+ validateKey(input);
98
+ if (!input.ownerId || !Number.isSafeInteger(input.ttlMs) || input.ttlMs < 1)
99
+ throw new LeaseConflictError("Lease ownerId and positive ttlMs are required");
100
+ }
101
+ function validateClaim(input, requireTtl) {
102
+ validateKey(input);
103
+ if (!input.ownerId || !input.token || (requireTtl && (!Number.isSafeInteger(input.ttlMs) || input.ttlMs < 1))) {
104
+ throw new LeaseConflictError("Lease ownerId, token, and positive ttlMs are required");
105
+ }
106
+ }
107
+ function throwIfAborted(signal) {
108
+ if (signal?.aborted)
109
+ throw signal.reason ?? new DOMException("Aborted", "AbortError");
110
+ }
111
+ //# sourceMappingURL=leases.js.map
@@ -7,6 +7,7 @@ import { assertPermission, isTrusted } from "../security.js";
7
7
  import { isPathInsideReal } from "./trust.js";
8
8
  import { readOptionalFile } from "./contribution-discovery.js";
9
9
  import { createSkillRegistry } from "../skills.js";
10
+ import { isNodeErrorCode } from "./config.js";
10
11
  const defaultInclude = {
11
12
  systemPrompt: true,
12
13
  agentPrompt: true,
@@ -329,7 +330,7 @@ async function listSubdirs(dir) {
329
330
  return out;
330
331
  }
331
332
  catch (error) {
332
- if (isMissingFile(error))
333
+ if (isNodeErrorCode(error, "ENOENT"))
333
334
  return [];
334
335
  throw error;
335
336
  }
@@ -339,14 +340,11 @@ async function readOptional(read, path) {
339
340
  return await read(path);
340
341
  }
341
342
  catch (error) {
342
- if (isMissingFile(error))
343
+ if (isNodeErrorCode(error, "ENOENT"))
343
344
  return undefined;
344
345
  throw error;
345
346
  }
346
347
  }
347
- function isMissingFile(error) {
348
- return error instanceof Error && "code" in error && error.code === "ENOENT";
349
- }
350
348
  const NAME_RE = /^[A-Za-z0-9 _-]+$/;
351
349
  function parentDirName(path) {
352
350
  const parts = path.split(/[/\\]+/).filter(Boolean);
@@ -7,3 +7,4 @@ export interface NodeConfigFile {
7
7
  export declare function defaultUserConfigPath(appName?: string): string;
8
8
  export declare function readConfigFile(path: string): Promise<ConfigLayer["config"]>;
9
9
  export declare function loadConfigFiles(files: readonly NodeConfigFile[]): Promise<ConfigLayer[]>;
10
+ export declare function isNodeErrorCode(error: unknown, code: string): boolean;
@@ -11,6 +11,8 @@ export async function readConfigFile(path) {
11
11
  text = await readFile(path, "utf8");
12
12
  }
13
13
  catch (error) {
14
+ if (isNodeErrorCode(error, "ENOENT"))
15
+ throw error;
14
16
  throw new Error(`Failed to read config ${path}: ${errorMessage(error)}`);
15
17
  }
16
18
  let parsed;
@@ -35,15 +37,15 @@ export async function loadConfigFiles(files) {
35
37
  layers.push({ name: file.name, config: await readConfigFile(file.path) });
36
38
  }
37
39
  catch (error) {
38
- if (file.optional && isMissingFile(error))
40
+ if (file.optional && isNodeErrorCode(error, "ENOENT"))
39
41
  continue;
40
42
  throw error;
41
43
  }
42
44
  }
43
45
  return layers;
44
46
  }
45
- function isMissingFile(error) {
46
- return error instanceof Error && error.message.includes("ENOENT");
47
+ export function isNodeErrorCode(error, code) {
48
+ return error instanceof Error && "code" in error && error.code === code;
47
49
  }
48
50
  function errorMessage(error) {
49
51
  return error instanceof Error ? error.message : String(error);
@@ -4,6 +4,7 @@ import { isJsonObject } from "../config.js";
4
4
  import { assertPermission } from "../security.js";
5
5
  import { isPathInsideReal } from "./trust.js";
6
6
  import { parseSkillFile } from "../contribution-parsing.js";
7
+ import { isNodeErrorCode } from "./config.js";
7
8
  /**
8
9
  * Discover contributions on disk. Scans the workspace `.agents/` tree only;
9
10
  * no global root. One `readdir` per kind-root; no `import()`. Inert output —
@@ -34,7 +35,7 @@ async function scanKindRoot(root, kind, options) {
34
35
  names = await readdir(kindDir);
35
36
  }
36
37
  catch (error) {
37
- if (isMissingFile(error))
38
+ if (isNodeErrorCode(error, "ENOENT"))
38
39
  return []; // missing kind root is normal
39
40
  throw error;
40
41
  }
@@ -46,7 +47,7 @@ async function scanKindRoot(root, kind, options) {
46
47
  isDir = (await stat(dir)).isDirectory();
47
48
  }
48
49
  catch (error) {
49
- if (isMissingFile(error))
50
+ if (isNodeErrorCode(error, "ENOENT"))
50
51
  continue;
51
52
  throw error;
52
53
  }
@@ -91,14 +92,13 @@ async function readManifestEntry(dir, fallbackName, kind, origin) {
91
92
  const declaration = parseManifestDeclaration(text, path, kind, fallbackName);
92
93
  return { kind, name: declaration.name, origin, path, declaration };
93
94
  }
94
- // ponytail: Phase 31 — exported so the system/project prompt file loader reuses the scanner's
95
- // ENOENT-tolerant read instead of duplicating it. isMissingFile stays private (only readOptionalFile needs it).
95
+ // ponytail: Phase 31 — exported so the system/project prompt file loader reuses this ENOENT-tolerant read.
96
96
  export async function readOptionalFile(path) {
97
97
  try {
98
98
  return await readFile(path, "utf8");
99
99
  }
100
100
  catch (error) {
101
- if (isMissingFile(error))
101
+ if (isNodeErrorCode(error, "ENOENT"))
102
102
  return undefined;
103
103
  throw error;
104
104
  }
@@ -106,9 +106,6 @@ export async function readOptionalFile(path) {
106
106
  // --- Internal helpers. ---
107
107
  // ponytail: parseSkillFile/parseAgentFile live in ../contribution-parsing.ts (core, fs-free); this
108
108
  // Node module owns directory walking + manifest.json parsing only.
109
- function isMissingFile(error) {
110
- return error instanceof Error && "code" in error && error.code === "ENOENT";
111
- }
112
109
  function parseManifestDeclaration(text, path, dirKind, fallbackName) {
113
110
  let parsed;
114
111
  try {
@@ -1,6 +1,7 @@
1
1
  import { mkdir, readFile, appendFile } from "node:fs/promises";
2
2
  import { dirname } from "node:path";
3
3
  import { SESSION_APPEND_CONFLICT_CODE, SessionAppendConflictError, isSessionEntryKind, SESSION_ENTRY_SCHEMA_VERSION } from "../contracts.js";
4
+ import { isNodeErrorCode } from "./config.js";
4
5
  export function createJsonlSessionStore(pathOrOptions) {
5
6
  const options = typeof pathOrOptions === "string" ? { path: pathOrOptions, createDirectory: true } : pathOrOptions;
6
7
  const path = options.path;
@@ -16,7 +17,12 @@ export function createJsonlSessionStore(pathOrOptions) {
16
17
  appendChain = appendChain.then(async () => {
17
18
  if (options.createDirectory !== false)
18
19
  await mkdir(dirname(path), { recursive: true });
19
- const entries = await readEntries(path);
20
+ const readResult = await readJsonlSessionEntries(path);
21
+ if (readResult.errors.length > 0) {
22
+ const first = readResult.errors[0];
23
+ throw new Error(`Invalid JSONL at line ${first.line}: ${first.message}`);
24
+ }
25
+ const entries = readResult.entries;
20
26
  const dedupKey = appendOptions?.idempotencyKey
21
27
  ? `${entry.sessionId}\u0000${appendOptions.idempotencyKey}\u0000${appendOptions.expectedParentId ?? ""}`
22
28
  : undefined;
@@ -49,7 +55,7 @@ export async function readJsonlSessionEntries(path) {
49
55
  text = await readFile(path, "utf8");
50
56
  }
51
57
  catch (error) {
52
- if (isMissingFile(error))
58
+ if (isNodeErrorCode(error, "ENOENT"))
53
59
  return { entries: [], errors: [] };
54
60
  throw new Error(`Failed to read session store ${path}: ${errorMessage(error)}`);
55
61
  }
@@ -163,9 +169,6 @@ function isAgentEvent(value) {
163
169
  function isPlainObject(value) {
164
170
  return !!value && typeof value === "object" && !Array.isArray(value);
165
171
  }
166
- function isMissingFile(error) {
167
- return error instanceof Error && "code" in error && error.code === "ENOENT";
168
- }
169
172
  function errorMessage(error) {
170
173
  return error instanceof Error ? error.message : String(error);
171
174
  }
@@ -1,7 +1,7 @@
1
1
  import { homedir } from "node:os";
2
2
  import { join } from "node:path";
3
3
  import { mergeConfigLayers } from "../config.js";
4
- import { readConfigFile } from "./config.js";
4
+ import { readConfigFile, isNodeErrorCode } from "./config.js";
5
5
  import { createStaticSettingsProvider } from "../settings.js";
6
6
  export function defaultUserSettingsPath(appName = "prism") {
7
7
  return join(homedir(), ".config", appName, "settings.json");
@@ -16,7 +16,7 @@ export async function loadSettingsFiles(files) {
16
16
  layers.push({ name: file.name, config: await readSettingsFile(file.path) });
17
17
  }
18
18
  catch (error) {
19
- if (file.optional && error instanceof Error && error.message.includes("ENOENT"))
19
+ if (file.optional && isNodeErrorCode(error, "ENOENT"))
20
20
  continue;
21
21
  throw error;
22
22
  }
@@ -1,5 +1,6 @@
1
1
  import { isAbsolute, relative, resolve, dirname, basename, join } from "node:path";
2
2
  import { realpath } from "node:fs/promises";
3
+ import { isNodeErrorCode } from "./config.js";
3
4
  export function isPathInside(root, target) {
4
5
  const from = resolve(root);
5
6
  const to = resolve(target);
@@ -25,7 +26,7 @@ export async function isPathInsideReal(root, target) {
25
26
  to = await realpath(target);
26
27
  }
27
28
  catch (error) {
28
- if (!isMissingFile(error))
29
+ if (!isNodeErrorCode(error, "ENOENT"))
29
30
  return false;
30
31
  const parent = dirname(target);
31
32
  try {
@@ -50,7 +51,4 @@ export function createPathTrustPolicy(options) {
50
51
  },
51
52
  };
52
53
  }
53
- function isMissingFile(error) {
54
- return error instanceof Error && "code" in error && error.code === "ENOENT";
55
- }
56
54
  //# sourceMappingURL=trust.js.map
@@ -0,0 +1,3 @@
1
+ import type { ErrorInfo, ProviderRequest, ProviderTurnMetadata } from "./contracts.js";
2
+ export declare function createProviderTurnMetadata(request: ProviderRequest, providerId: string, fields?: Omit<ProviderTurnMetadata, "providerId" | "model">): ProviderTurnMetadata;
3
+ export declare function readProviderHttpStatus(error?: ErrorInfo): number | undefined;
@@ -0,0 +1,18 @@
1
+ export function createProviderTurnMetadata(request, providerId, fields = {}) {
2
+ return {
3
+ providerId,
4
+ model: request.model,
5
+ requestId: readRequestId(request),
6
+ ...fields,
7
+ };
8
+ }
9
+ export function readProviderHttpStatus(error) {
10
+ return typeof error?.code === "number" ? error.code : undefined;
11
+ }
12
+ function readRequestId(request) {
13
+ const fromMetadata = request.metadata?.requestId;
14
+ if (typeof fromMetadata === "string" && fromMetadata.length > 0)
15
+ return fromMetadata;
16
+ return request.options?.sessionId;
17
+ }
18
+ //# sourceMappingURL=observability.js.map
@@ -0,0 +1,42 @@
1
+ import type { AudioContent, ContentBlock, DocumentContent, FileContent, JsonObject, ModelCapabilities, ModelConfig } from "../contracts.js";
2
+ import { type MediaContentBlock, type ModelInputCapability, type ResolveMediaContentOptions, type ResolvedMediaContent } from "../content.js";
3
+ /** Default upload-cache entry cap per provider media session. */
4
+ export declare const DEFAULT_PROVIDER_UPLOAD_CACHE_ENTRIES = 32;
5
+ /** Inline OpenAI file_data ceiling before preferring Files API upload. */
6
+ export declare const DEFAULT_OPENAI_INLINE_FILE_BYTES: number;
7
+ export interface ProviderMediaScope {
8
+ readonly sessionId?: string;
9
+ readonly runId?: string;
10
+ readonly tenantId?: string;
11
+ }
12
+ export interface BoundedUploadCache<T> {
13
+ get(key: string): T | undefined;
14
+ set(key: string, value: T): void;
15
+ keys(): readonly string[];
16
+ clear(): void;
17
+ }
18
+ export declare function assertProviderMediaCapability(modality: ModelInputCapability, capabilities: ModelCapabilities, model: ModelConfig): void;
19
+ export declare function rejectProviderMediaBlock(part: ContentBlock, capabilities: ModelCapabilities, model: ModelConfig): never;
20
+ export declare function createBoundedUploadCache<T>(maxEntries?: number): BoundedUploadCache<T>;
21
+ export declare function providerUploadCacheKey(scope: ProviderMediaScope, fingerprint: string): string;
22
+ export declare function mediaFingerprint(mediaType: string, bytes: Uint8Array, name?: string): string;
23
+ export declare function bytesToBase64(bytes: Uint8Array): string;
24
+ export declare function isPdfMediaType(mediaType: string): boolean;
25
+ export declare function openAIAudioFormat(mediaType: string): string;
26
+ export declare function serializeOpenAIResponsesInputFile(options: {
27
+ readonly filename: string;
28
+ readonly fileData?: string;
29
+ readonly fileId?: string;
30
+ }): JsonObject;
31
+ export declare function serializeOpenAIResponsesInputAudio(options: {
32
+ readonly data: string;
33
+ readonly format: string;
34
+ }): JsonObject;
35
+ export declare function serializePdfDocumentWireBlock(options: {
36
+ readonly mediaType: string;
37
+ readonly data: string;
38
+ readonly title?: string;
39
+ }): JsonObject;
40
+ export declare function resolveProviderMediaBlock(block: AudioContent | FileContent | DocumentContent, options?: ResolveMediaContentOptions): Promise<ResolvedMediaContent>;
41
+ export declare function defaultProviderFilename(block: MediaContentBlock, fallback: string): string;
42
+ export declare const DEFAULT_PROVIDER_MEDIA_ITEM_BYTES = 10000000;
@@ -0,0 +1,116 @@
1
+ import { createHash } from "node:crypto";
2
+ import { contentBlockInputModality, DEFAULT_MAX_MEDIA_ITEM_BYTES, resolveMediaContentBlock, UnsupportedModalityError, } from "../content.js";
3
+ /** Default upload-cache entry cap per provider media session. */
4
+ export const DEFAULT_PROVIDER_UPLOAD_CACHE_ENTRIES = 32;
5
+ /** Inline OpenAI file_data ceiling before preferring Files API upload. */
6
+ export const DEFAULT_OPENAI_INLINE_FILE_BYTES = 4 * 1024 * 1024;
7
+ export function assertProviderMediaCapability(modality, capabilities, model) {
8
+ const supported = capabilities.input;
9
+ if (supported?.length && !supported.includes(modality)) {
10
+ throw new UnsupportedModalityError(modality, model);
11
+ }
12
+ }
13
+ export function rejectProviderMediaBlock(part, capabilities, model) {
14
+ const modality = contentBlockInputModality(part);
15
+ if (modality)
16
+ assertProviderMediaCapability(modality, capabilities, model);
17
+ throw new Error(`Provider ${model.provider} does not support ${part.type} content blocks`);
18
+ }
19
+ export function createBoundedUploadCache(maxEntries = DEFAULT_PROVIDER_UPLOAD_CACHE_ENTRIES) {
20
+ const map = new Map();
21
+ return {
22
+ get(key) {
23
+ return map.get(key);
24
+ },
25
+ set(key, value) {
26
+ if (map.has(key))
27
+ map.delete(key);
28
+ else if (map.size >= maxEntries) {
29
+ const oldest = map.keys().next().value;
30
+ if (oldest !== undefined)
31
+ map.delete(oldest);
32
+ }
33
+ map.set(key, value);
34
+ },
35
+ keys() {
36
+ return [...map.keys()];
37
+ },
38
+ clear() {
39
+ map.clear();
40
+ },
41
+ };
42
+ }
43
+ export function providerUploadCacheKey(scope, fingerprint) {
44
+ const prefix = [scope.tenantId, scope.sessionId, scope.runId].filter((value) => !!value).join(":");
45
+ return prefix ? `${prefix}:${fingerprint}` : fingerprint;
46
+ }
47
+ export function mediaFingerprint(mediaType, bytes, name) {
48
+ const hash = createHash("sha256").update(bytes).digest("hex");
49
+ return `${mediaType}:${name ?? ""}:${hash}`;
50
+ }
51
+ export function bytesToBase64(bytes) {
52
+ return Buffer.from(bytes).toString("base64");
53
+ }
54
+ export function isPdfMediaType(mediaType) {
55
+ const normalized = mediaType.toLowerCase();
56
+ return normalized === "application/pdf" || normalized.endsWith("+pdf");
57
+ }
58
+ export function openAIAudioFormat(mediaType) {
59
+ const normalized = mediaType.toLowerCase();
60
+ if (normalized.includes("wav"))
61
+ return "wav";
62
+ if (normalized.includes("mpeg") || normalized.includes("mp3"))
63
+ return "mp3";
64
+ if (normalized.includes("ogg"))
65
+ return "ogg";
66
+ if (normalized.includes("flac"))
67
+ return "flac";
68
+ if (normalized.includes("webm"))
69
+ return "webm";
70
+ if (normalized.includes("m4a") || normalized.includes("mp4"))
71
+ return "m4a";
72
+ return "wav";
73
+ }
74
+ export function serializeOpenAIResponsesInputFile(options) {
75
+ if (options.fileId) {
76
+ return { type: "input_file", file_id: options.fileId };
77
+ }
78
+ if (!options.fileData) {
79
+ throw new Error("OpenAI Responses file block requires inline file_data or uploaded file_id");
80
+ }
81
+ return { type: "input_file", filename: options.filename, file_data: options.fileData };
82
+ }
83
+ export function serializeOpenAIResponsesInputAudio(options) {
84
+ return { type: "input_audio", input_audio: { data: options.data, format: options.format } };
85
+ }
86
+ export function serializePdfDocumentWireBlock(options) {
87
+ if (!isPdfMediaType(options.mediaType)) {
88
+ throw new Error(`PDF document blocks require application/pdf media type, got ${options.mediaType}`);
89
+ }
90
+ return {
91
+ type: "document",
92
+ source: { type: "base64", media_type: options.mediaType, data: options.data },
93
+ ...(options.title ? { title: options.title } : {}),
94
+ };
95
+ }
96
+ export async function resolveProviderMediaBlock(block, options = {}) {
97
+ return resolveMediaContentBlock(block, options);
98
+ }
99
+ export function defaultProviderFilename(block, fallback) {
100
+ if ("name" in block && block.name)
101
+ return block.name;
102
+ if (block.type === "document")
103
+ return "document.pdf";
104
+ if (block.type === "audio")
105
+ return `audio.${openAIAudioFormat(declaredMediaType(block))}`;
106
+ return fallback;
107
+ }
108
+ function declaredMediaType(block) {
109
+ if ("mediaType" in block && block.mediaType)
110
+ return block.mediaType;
111
+ if (block.type === "image" && block.mimeType)
112
+ return block.mimeType;
113
+ return "application/octet-stream";
114
+ }
115
+ export const DEFAULT_PROVIDER_MEDIA_ITEM_BYTES = DEFAULT_MAX_MEDIA_ITEM_BYTES;
116
+ //# sourceMappingURL=media.js.map
@@ -1,6 +1,8 @@
1
1
  import { resolveCredentialValue } from "../credentials.js";
2
2
  import { providerDone, providerError, providerTextDelta, providerThinkingDelta, providerToolCall, providerToolCallDelta, providerUsage, toolCallContent, } from "../provider-events.js";
3
- import { redactSecrets } from "../redaction.js";
3
+ import { assertOpenAIChatMessage, applyOpenAIChatStructuredOutput, mapOpenAIChatUsage, serializeOpenAIChatMessage, serializeOpenAITool, } from "./openai-primitives.js";
4
+ import { parseJsonObjectArguments, readBoundedResponseText, readSseData, } from "./transport.js";
5
+ import { assertStructuredOutputRequestSupported } from "../structured-output.js";
4
6
  export function createOpenAICompatibleProvider(options) {
5
7
  const providerId = options.id ?? "openai-compatible";
6
8
  return {
@@ -25,18 +27,18 @@ export function createOpenAICompatibleProvider(options) {
25
27
  signal: request.signal,
26
28
  });
27
29
  if (!response.ok) {
28
- yield providerError(new Error(`OpenAI-compatible request failed: ${response.status} ${await safeText(response)}`), secrets);
30
+ yield providerError(new Error(`OpenAI-compatible request failed: ${response.status} ${await readBoundedResponseText(response, { secrets })}`), secrets);
29
31
  return;
30
32
  }
31
33
  if (!response.body) {
32
34
  yield providerError(new Error("OpenAI-compatible response had no body"), secrets);
33
35
  return;
34
36
  }
35
- for await (const data of readSseData(response.body)) {
37
+ for await (const data of readSseData(response.body, { signal: request.signal })) {
36
38
  if (data === "[DONE]")
37
39
  break;
38
40
  const parsed = JSON.parse(data);
39
- const usage = toUsage(parsed.usage);
41
+ const usage = mapOpenAIChatUsage(parsed.usage);
40
42
  if (usage)
41
43
  yield providerUsage(usage);
42
44
  for (const choice of parsed.choices ?? []) {
@@ -63,8 +65,9 @@ export function createOpenAICompatibleProvider(options) {
63
65
  }
64
66
  }
65
67
  for (const call of tools.values()) {
66
- if (call.id && call.name)
67
- yield providerToolCall(toolCallContent(call.id, call.name, parseArgs(call.argumentsText)));
68
+ if (call.id && call.name) {
69
+ yield providerToolCall(toolCallContent(call.id, call.name, parseJsonObjectArguments(call.argumentsText, { toolName: call.name })));
70
+ }
68
71
  }
69
72
  yield providerDone();
70
73
  }
@@ -75,123 +78,19 @@ export function createOpenAICompatibleProvider(options) {
75
78
  };
76
79
  }
77
80
  function toOpenAIRequest(request) {
78
- return {
81
+ assertStructuredOutputRequestSupported(request.model, request.options);
82
+ const body = {
79
83
  model: request.model.model,
80
- messages: request.messages.map((message) => toOpenAIMessage(message, request.model.capabilities ?? {})),
81
- tools: request.tools?.map(toOpenAITool),
84
+ messages: request.messages.map((message, index) => {
85
+ assertOpenAIChatMessage(message, `messages[${index}]`);
86
+ return serializeOpenAIChatMessage(message, request.model.capabilities ?? {});
87
+ }),
88
+ tools: request.tools?.map(serializeOpenAITool),
82
89
  stream: true,
83
90
  stream_options: { include_usage: true },
84
91
  ...request.model.parameters,
85
92
  };
86
- }
87
- function toOpenAIMessage(message, capabilities = {}) {
88
- if (message.role === "tool") {
89
- const result = message.content.find((part) => part.type === "tool_result");
90
- return {
91
- role: "tool",
92
- tool_call_id: result?.toolCallId ?? "",
93
- content: result ? JSON.stringify(result.result ?? result.error ?? null) : "",
94
- };
95
- }
96
- if (message.role === "assistant") {
97
- const toolCalls = message.content.filter((part) => part.type === "tool_call");
98
- const textParts = message.content.filter((part) => part.type === "text" || part.type === "thinking");
99
- if (toolCalls.length > 0) {
100
- return {
101
- role: "assistant",
102
- content: textParts.map((part) => (part.type === "text" ? part.text : part.text)).join("\n") || null,
103
- tool_calls: toolCalls.map((call) => ({
104
- id: call.id,
105
- type: "function",
106
- function: { name: call.name, arguments: JSON.stringify(call.arguments) },
107
- })),
108
- };
109
- }
110
- }
111
- const content = [];
112
- for (const part of message.content) {
113
- if (part.type === "text" || part.type === "thinking") {
114
- content.push({ type: "text", text: part.text });
115
- }
116
- else if (part.type === "image") {
117
- if (!capabilities.input?.includes("image")) {
118
- throw new Error(`Provider ${message.role} message includes image but model does not declare image input capability`);
119
- }
120
- content.push(toOpenAIImage(part));
121
- }
122
- else if (part.type === "tool_call") {
123
- throw new Error("Provider assistant tool_call blocks must be the only content on the message");
124
- }
125
- else if (part.type === "tool_result") {
126
- throw new Error("Provider tool_result blocks must appear in role=tool messages");
127
- }
128
- }
129
- if (content.length === 1 && content[0].type === "text") {
130
- return { role: message.role, content: content[0].text };
131
- }
132
- return { role: message.role, content };
133
- }
134
- function toOpenAIImage(part) {
135
- const url = part.url ?? (part.data ? `data:${part.mimeType ?? "image/png"};base64,${part.data}` : undefined);
136
- if (!url)
137
- throw new Error("Provider image block missing url or data");
138
- return { type: "image_url", image_url: { url } };
139
- }
140
- function toOpenAITool(tool) {
141
- return {
142
- type: "function",
143
- function: {
144
- name: tool.name,
145
- description: tool.description,
146
- parameters: tool.parameters ?? { type: "object" },
147
- },
148
- };
149
- }
150
- async function* readSseData(body) {
151
- const reader = body.getReader();
152
- const decoder = new TextDecoder();
153
- let buffer = "";
154
- while (true) {
155
- const { done, value } = await reader.read();
156
- if (done)
157
- break;
158
- buffer += decoder.decode(value, { stream: true });
159
- const lines = buffer.split(/\r?\n/);
160
- buffer = lines.pop() ?? "";
161
- for (const line of lines) {
162
- if (line.startsWith("data: "))
163
- yield line.slice(6).trim();
164
- }
165
- }
166
- buffer += decoder.decode();
167
- for (const line of buffer.split(/\r?\n/)) {
168
- if (line.startsWith("data: "))
169
- yield line.slice(6).trim();
170
- }
171
- }
172
- async function safeText(response) {
173
- try {
174
- return redactSecrets(await response.text(), []);
175
- }
176
- catch {
177
- return "";
178
- }
179
- }
180
- function parseArgs(text) {
181
- if (!text)
182
- return {};
183
- const parsed = JSON.parse(text);
184
- return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
185
- }
186
- function toUsage(usage) {
187
- if (!usage)
188
- return undefined;
189
- return {
190
- inputTokens: usage.prompt_tokens,
191
- outputTokens: usage.completion_tokens,
192
- totalTokens: usage.total_tokens,
193
- cacheReadTokens: usage.prompt_tokens_details?.cached_tokens ?? usage.prompt_cache_hit_tokens,
194
- cacheWriteTokens: usage.prompt_tokens_details?.cache_write_tokens,
195
- };
93
+ applyOpenAIChatStructuredOutput(body, request.options?.structuredOutput);
94
+ return body;
196
95
  }
197
96
  //# sourceMappingURL=openai-compatible.js.map