@arnilo/prism 0.0.3 → 0.0.5

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 (131) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +62 -26
  3. package/dist/agent-loops.d.ts +8 -1
  4. package/dist/agent-loops.js +57 -11
  5. package/dist/agents.js +212 -32
  6. package/dist/checkpoints.d.ts +11 -0
  7. package/dist/checkpoints.js +144 -0
  8. package/dist/cli-init.d.ts +41 -0
  9. package/dist/cli-init.js +390 -0
  10. package/dist/cli-runner.d.ts +7 -1
  11. package/dist/cli-runner.js +13 -1
  12. package/dist/compaction.js +9 -1
  13. package/dist/content.d.ts +121 -0
  14. package/dist/content.js +538 -0
  15. package/dist/contracts.d.ts +236 -11
  16. package/dist/contracts.js +8 -0
  17. package/dist/event-multiplexer.d.ts +23 -0
  18. package/dist/event-multiplexer.js +136 -0
  19. package/dist/execution-policy.d.ts +28 -0
  20. package/dist/execution-policy.js +24 -0
  21. package/dist/feedback.d.ts +48 -0
  22. package/dist/feedback.js +230 -0
  23. package/dist/index.d.ts +20 -6
  24. package/dist/index.js +13 -5
  25. package/dist/input.js +11 -1
  26. package/dist/leases.d.ts +8 -0
  27. package/dist/leases.js +111 -0
  28. package/dist/node/agent-definitions.js +3 -5
  29. package/dist/node/config.d.ts +1 -0
  30. package/dist/node/config.js +5 -3
  31. package/dist/node/contribution-discovery.js +5 -8
  32. package/dist/node/session-store-jsonl.js +8 -5
  33. package/dist/node/settings.js +2 -2
  34. package/dist/node/trust.js +2 -4
  35. package/dist/observability.d.ts +3 -0
  36. package/dist/observability.js +18 -0
  37. package/dist/providers/media.d.ts +44 -0
  38. package/dist/providers/media.js +126 -0
  39. package/dist/providers/openai-compatible.js +18 -119
  40. package/dist/providers/openai-primitives.d.ts +9 -0
  41. package/dist/providers/openai-primitives.js +129 -0
  42. package/dist/providers/transport.d.ts +40 -0
  43. package/dist/providers/transport.js +221 -0
  44. package/dist/redaction.js +40 -13
  45. package/dist/resources.d.ts +5 -0
  46. package/dist/resources.js +4 -0
  47. package/dist/structured-output.d.ts +11 -0
  48. package/dist/structured-output.js +59 -0
  49. package/dist/testing/feedback.d.ts +6 -0
  50. package/dist/testing/feedback.js +37 -0
  51. package/dist/testing/persistence-schema.d.ts +102 -0
  52. package/dist/testing/persistence-schema.js +487 -0
  53. package/dist/testing/provider-conformance.js +10 -1
  54. package/dist/testing/run-ledger-conformance.d.ts +33 -0
  55. package/dist/testing/run-ledger-conformance.js +178 -0
  56. package/dist/testing/session-store-conformance.d.ts +16 -0
  57. package/dist/testing/session-store-conformance.js +73 -0
  58. package/dist/tools.d.ts +17 -0
  59. package/dist/tools.js +29 -2
  60. package/docs/a2a.md +73 -0
  61. package/docs/agent-events.md +17 -10
  62. package/docs/agent-loops.md +11 -5
  63. package/docs/agent-session-runtime.md +15 -16
  64. package/docs/cli-rpc.md +36 -5
  65. package/docs/coding-agent-tools.md +43 -9
  66. package/docs/coding-security.md +88 -0
  67. package/docs/compaction-observational-memory.md +2 -0
  68. package/docs/context-and-skills.md +1 -0
  69. package/docs/credential-storage.md +177 -0
  70. package/docs/credentials-and-redaction.md +4 -3
  71. package/docs/database-persistence.md +52 -7
  72. package/docs/evaluations.md +122 -0
  73. package/docs/extensions.md +2 -2
  74. package/docs/host-security.md +33 -2
  75. package/docs/index.md +46 -18
  76. package/docs/input-and-prompt-assembly.md +6 -5
  77. package/docs/mcp-tools.md +184 -0
  78. package/docs/middleware-hooks.md +2 -0
  79. package/docs/migration.md +51 -28
  80. package/docs/model-registry.md +5 -3
  81. package/docs/multimodal-content.md +156 -0
  82. package/docs/observability.md +171 -0
  83. package/docs/performance.md +249 -1
  84. package/docs/persistence-credentials-multimodality-primitives.md +303 -0
  85. package/docs/postgres-persistence.md +143 -0
  86. package/docs/provider-conformance.md +18 -0
  87. package/docs/provider-layer.md +1 -1
  88. package/docs/provider-packages.md +2 -0
  89. package/docs/provider-primitives.md +281 -0
  90. package/docs/providers/ai-sdk.md +113 -0
  91. package/docs/providers/kimi.md +1 -0
  92. package/docs/providers/neuralwatt.md +1 -0
  93. package/docs/providers/openai-compatible.md +2 -1
  94. package/docs/providers/openai.md +8 -1
  95. package/docs/providers/opencode-go.md +1 -0
  96. package/docs/providers/openrouter.md +1 -0
  97. package/docs/providers/zai.md +1 -0
  98. package/docs/public-contracts.md +13 -5
  99. package/docs/rag.md +113 -0
  100. package/docs/release-and-install.md +237 -30
  101. package/docs/resource-loading.md +14 -4
  102. package/docs/review-coverage-2026-07-14.md +260 -0
  103. package/docs/review-coverage-2026-07-15.md +193 -0
  104. package/docs/run-ledger-conformance.md +96 -0
  105. package/docs/runs-and-usage.md +43 -4
  106. package/docs/server.md +139 -0
  107. package/docs/session-store-conformance.md +16 -0
  108. package/docs/session-stores-and-branching.md +1 -0
  109. package/docs/settings-auth-trust-security.md +6 -5
  110. package/docs/sqlite-persistence.md +123 -0
  111. package/docs/structured-output.md +9 -0
  112. package/docs/supervisors.md +71 -0
  113. package/docs/tool-conformance.md +1 -0
  114. package/docs/tool-execution-primitives.md +374 -0
  115. package/docs/tools.md +39 -1
  116. package/docs/workflow-orchestration-primitives.md +581 -0
  117. package/docs/workflow-tui-primitives.md +5 -0
  118. package/docs/workflows.md +293 -0
  119. package/docs/working-and-semantic-memory.md +169 -0
  120. package/package.json +43 -5
  121. package/templates/init/README.md.tmpl +28 -0
  122. package/templates/init/env.example.tmpl +1 -0
  123. package/templates/init/gitignore.tmpl +11 -0
  124. package/templates/init/optional/evals-example.ts.tmpl +17 -0
  125. package/templates/init/optional/workflows-example.ts.tmpl +27 -0
  126. package/templates/init/package.json.tmpl +22 -0
  127. package/templates/init/providers.json +76 -0
  128. package/templates/init/src/agent.ts.tmpl +10 -0
  129. package/templates/init/src/index.ts.tmpl +12 -0
  130. package/templates/init/src/tests/agent.test.ts.tmpl +24 -0
  131. package/templates/init/tsconfig.json.tmpl +15 -0
@@ -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,44 @@
1
+ import type { AudioContent, ContentBlock, DocumentContent, FileContent, JsonObject, Message, 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
+ /** Resolve every media block once and enforce aggregate request bounds before provider I/O. */
42
+ export declare function resolveProviderMediaMessages(messages: readonly Message[], model: ModelConfig, options?: ResolveMediaContentOptions): Promise<ReadonlyMap<MediaContentBlock, ResolvedMediaContent>>;
43
+ export declare function defaultProviderFilename(block: MediaContentBlock, fallback: string): string;
44
+ export declare const DEFAULT_PROVIDER_MEDIA_ITEM_BYTES = 10000000;
@@ -0,0 +1,126 @@
1
+ import { createHash } from "node:crypto";
2
+ import { assertMessagesSupportModelCapabilities, contentBlockInputModality, DEFAULT_MAX_MEDIA_ITEM_BYTES, resolveMediaContentBlock, resolveMediaContentBlocks, 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
+ /** Resolve every media block once and enforce aggregate request bounds before provider I/O. */
100
+ export async function resolveProviderMediaMessages(messages, model, options = {}) {
101
+ assertMessagesSupportModelCapabilities(model, messages);
102
+ const blocks = messages.flatMap((message) => message.content.filter(isMediaContentBlock));
103
+ const resolved = await resolveMediaContentBlocks(blocks, options);
104
+ return new Map(blocks.map((block, index) => [block, resolved[index]]));
105
+ }
106
+ function isMediaContentBlock(block) {
107
+ return contentBlockInputModality(block) !== undefined;
108
+ }
109
+ export function defaultProviderFilename(block, fallback) {
110
+ if ("name" in block && block.name)
111
+ return block.name;
112
+ if (block.type === "document")
113
+ return "document.pdf";
114
+ if (block.type === "audio")
115
+ return `audio.${openAIAudioFormat(declaredMediaType(block))}`;
116
+ return fallback;
117
+ }
118
+ function declaredMediaType(block) {
119
+ if ("mediaType" in block && block.mediaType)
120
+ return block.mediaType;
121
+ if (block.type === "image" && block.mimeType)
122
+ return block.mimeType;
123
+ return "application/octet-stream";
124
+ }
125
+ export const DEFAULT_PROVIDER_MEDIA_ITEM_BYTES = DEFAULT_MAX_MEDIA_ITEM_BYTES;
126
+ //# 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
@@ -0,0 +1,9 @@
1
+ import type { JsonObject, Message, ModelCapabilities, StructuredOutputOptions, ToolDefinition, Usage } from "../contracts.js";
2
+ export declare function assertOpenAIChatMessage(message: unknown, path: string): asserts message is Message;
3
+ export declare function serializeOpenAIChatStructuredOutput(options: StructuredOutputOptions): JsonObject;
4
+ export declare function serializeOpenAIResponsesStructuredOutput(options: StructuredOutputOptions): JsonObject;
5
+ export declare function applyOpenAIChatStructuredOutput(body: Record<string, unknown>, structuredOutput?: StructuredOutputOptions): void;
6
+ export declare function applyOpenAIResponsesStructuredOutput(body: Record<string, unknown>, structuredOutput?: StructuredOutputOptions): void;
7
+ export declare function serializeOpenAITool(tool: ToolDefinition): JsonObject;
8
+ export declare function serializeOpenAIChatMessage(message: Message, capabilities?: ModelCapabilities): JsonObject;
9
+ export declare function mapOpenAIChatUsage(usage: unknown): Usage | undefined;
@@ -0,0 +1,129 @@
1
+ export function assertOpenAIChatMessage(message, path) {
2
+ if (!message || typeof message !== "object") {
3
+ throw new Error(`Invalid provider message at ${path}: expected object`);
4
+ }
5
+ const candidate = message;
6
+ if (typeof candidate.role !== "string") {
7
+ throw new Error(`Invalid provider message at ${path}: expected role`);
8
+ }
9
+ if (!Array.isArray(candidate.content)) {
10
+ throw new Error(`Invalid provider message at ${path}: expected content array`);
11
+ }
12
+ }
13
+ export function serializeOpenAIChatStructuredOutput(options) {
14
+ return {
15
+ type: "json_schema",
16
+ json_schema: {
17
+ name: options.name,
18
+ schema: options.schema,
19
+ ...(options.strict === undefined ? {} : { strict: options.strict }),
20
+ },
21
+ };
22
+ }
23
+ export function serializeOpenAIResponsesStructuredOutput(options) {
24
+ return {
25
+ format: {
26
+ type: "json_schema",
27
+ name: options.name,
28
+ schema: options.schema,
29
+ ...(options.strict === undefined ? {} : { strict: options.strict }),
30
+ },
31
+ };
32
+ }
33
+ export function applyOpenAIChatStructuredOutput(body, structuredOutput) {
34
+ if (!structuredOutput)
35
+ return;
36
+ body.response_format = serializeOpenAIChatStructuredOutput(structuredOutput);
37
+ }
38
+ export function applyOpenAIResponsesStructuredOutput(body, structuredOutput) {
39
+ if (!structuredOutput)
40
+ return;
41
+ body.text = serializeOpenAIResponsesStructuredOutput(structuredOutput);
42
+ }
43
+ export function serializeOpenAITool(tool) {
44
+ return {
45
+ type: "function",
46
+ function: {
47
+ name: tool.name,
48
+ description: tool.description,
49
+ parameters: tool.parameters ?? { type: "object" },
50
+ },
51
+ };
52
+ }
53
+ export function serializeOpenAIChatMessage(message, capabilities = {}) {
54
+ if (message.role === "tool") {
55
+ const result = message.content.find((part) => part.type === "tool_result");
56
+ return {
57
+ role: "tool",
58
+ tool_call_id: result?.toolCallId ?? "",
59
+ content: result ? JSON.stringify(result.result ?? result.error ?? null) : "",
60
+ };
61
+ }
62
+ if (message.role === "assistant") {
63
+ const toolCalls = message.content.filter((part) => part.type === "tool_call");
64
+ const textParts = message.content.filter((part) => part.type === "text" || part.type === "thinking");
65
+ if (toolCalls.length > 0) {
66
+ return {
67
+ role: "assistant",
68
+ content: textParts.map((part) => part.text).join("\n") || null,
69
+ tool_calls: toolCalls.map((call) => ({
70
+ id: call.id,
71
+ type: "function",
72
+ function: { name: call.name, arguments: JSON.stringify(call.arguments) },
73
+ })),
74
+ };
75
+ }
76
+ }
77
+ const content = [];
78
+ for (const part of message.content) {
79
+ if (part.type === "text" || part.type === "thinking") {
80
+ content.push({ type: "text", text: part.text });
81
+ }
82
+ else if (part.type === "image") {
83
+ if (!capabilities.input?.includes("image")) {
84
+ throw new Error(`Provider ${message.role} message includes image but model does not declare image input capability`);
85
+ }
86
+ content.push(serializeOpenAIImage(part));
87
+ }
88
+ else if (part.type === "audio" || part.type === "file" || part.type === "document") {
89
+ throw new Error(`OpenAI Chat Completions does not support ${part.type} content blocks`);
90
+ }
91
+ else if (part.type === "tool_call") {
92
+ throw new Error("Provider assistant tool_call blocks must be the only content on the message");
93
+ }
94
+ else if (part.type === "tool_result") {
95
+ throw new Error("Provider tool_result blocks must appear in role=tool messages");
96
+ }
97
+ }
98
+ if (content.length === 1 && content[0].type === "text") {
99
+ return { role: message.role, content: content[0].text };
100
+ }
101
+ return { role: message.role, content };
102
+ }
103
+ function serializeOpenAIImage(part) {
104
+ const url = part.url ?? (part.data ? `data:${part.mimeType ?? "image/png"};base64,${part.data}` : undefined);
105
+ if (!url)
106
+ throw new Error("Provider image block missing url or data");
107
+ return { type: "image_url", image_url: { url } };
108
+ }
109
+ export function mapOpenAIChatUsage(usage) {
110
+ if (!usage || typeof usage !== "object" || Array.isArray(usage))
111
+ return undefined;
112
+ const wire = usage;
113
+ if (wire.prompt_tokens === undefined
114
+ && wire.completion_tokens === undefined
115
+ && wire.total_tokens === undefined
116
+ && wire.prompt_cache_hit_tokens === undefined
117
+ && wire.prompt_tokens_details?.cached_tokens === undefined
118
+ && wire.prompt_tokens_details?.cache_write_tokens === undefined) {
119
+ return undefined;
120
+ }
121
+ return {
122
+ inputTokens: wire.prompt_tokens,
123
+ outputTokens: wire.completion_tokens,
124
+ totalTokens: wire.total_tokens,
125
+ cacheReadTokens: wire.prompt_tokens_details?.cached_tokens ?? wire.prompt_cache_hit_tokens,
126
+ cacheWriteTokens: wire.prompt_tokens_details?.cache_write_tokens,
127
+ };
128
+ }
129
+ //# sourceMappingURL=openai-primitives.js.map
@@ -0,0 +1,40 @@
1
+ import type { JsonObject } from "../contracts.js";
2
+ export declare const DEFAULT_MAX_EVENT_BYTES = 262144;
3
+ export declare const DEFAULT_MAX_BUFFER_BYTES = 524288;
4
+ export declare const DEFAULT_MAX_RESPONSE_BODY_BYTES = 65536;
5
+ export declare const DEFAULT_MAX_ARGUMENT_BYTES = 262144;
6
+ export interface BoundedStreamLimits {
7
+ readonly maxEventBytes?: number;
8
+ readonly maxBufferBytes?: number;
9
+ readonly maxResponseBodyBytes?: number;
10
+ }
11
+ export interface SseEvent {
12
+ readonly id?: string;
13
+ readonly event?: string;
14
+ readonly data: string;
15
+ readonly comments?: readonly string[];
16
+ }
17
+ export type ProviderTransportErrorCode = "sse_buffer_overflow" | "sse_event_overflow" | "response_body_overflow" | "aborted" | "invalid_json_arguments";
18
+ export declare class ProviderTransportError extends Error {
19
+ readonly code: ProviderTransportErrorCode;
20
+ readonly limitBytes?: number;
21
+ constructor(code: ProviderTransportErrorCode, message: string, limitBytes?: number);
22
+ }
23
+ export interface ReadSseEventsOptions extends BoundedStreamLimits {
24
+ readonly signal?: AbortSignal;
25
+ }
26
+ export interface ReadBoundedResponseTextOptions extends BoundedStreamLimits {
27
+ readonly secrets?: readonly (string | undefined)[];
28
+ }
29
+ export interface ParseJsonObjectArgumentsOptions {
30
+ readonly toolName?: string;
31
+ readonly maxBytes?: number;
32
+ }
33
+ /** Incremental O(bytes) SSE parser with finite buffer/event limits. */
34
+ export declare function readSseEvents(body: ReadableStream<Uint8Array>, options?: ReadSseEventsOptions): AsyncGenerator<SseEvent>;
35
+ /** Yields joined `data:` payloads for each SSE event (migration helper over {@link readSseEvents}). */
36
+ export declare function readSseData(body: ReadableStream<Uint8Array>, options?: ReadSseEventsOptions): AsyncGenerator<string>;
37
+ /** Read a response body with a hard byte ceiling; redacts optional secrets. */
38
+ export declare function readBoundedResponseText(response: Response, options?: ReadBoundedResponseTextOptions): Promise<string>;
39
+ /** Parse streamed tool arguments as a JSON object; throws {@link ProviderTransportError} on invalid input. */
40
+ export declare function parseJsonObjectArguments(text: string, options?: ParseJsonObjectArgumentsOptions): JsonObject;