@arnilo/prism 0.0.15 → 0.0.17

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 (119) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/agent-definitions.js +2 -3
  3. package/dist/agent-loops.js +12 -7
  4. package/dist/agent-run-lifecycle.d.ts +1 -2
  5. package/dist/agent-run-lifecycle.js +1 -1
  6. package/dist/agent-run-state.js +29 -4
  7. package/dist/agents.d.ts +1 -1
  8. package/dist/agents.js +163 -61
  9. package/dist/cache-helpers.js +18 -9
  10. package/dist/checkpoints.d.ts +4 -0
  11. package/dist/checkpoints.js +17 -9
  12. package/dist/cli-init.js +3 -7
  13. package/dist/cli-runner.d.ts +2 -6
  14. package/dist/cli-runner.js +71 -33
  15. package/dist/compaction.js +5 -4
  16. package/dist/config.js +7 -4
  17. package/dist/content.js +26 -24
  18. package/dist/context-budget.js +18 -11
  19. package/dist/contracts.d.ts +16 -3
  20. package/dist/contracts.js +4 -1
  21. package/dist/contribution-parsing.js +6 -2
  22. package/dist/contributions.d.ts +2 -0
  23. package/dist/contributions.js +3 -0
  24. package/dist/conversations.js +2 -1
  25. package/dist/credentials.d.ts +8 -2
  26. package/dist/credentials.js +9 -3
  27. package/dist/event-multiplexer.js +18 -4
  28. package/dist/extensions.d.ts +7 -1
  29. package/dist/extensions.js +64 -6
  30. package/dist/feedback.js +12 -10
  31. package/dist/guardrails.d.ts +1 -1
  32. package/dist/guardrails.js +26 -17
  33. package/dist/identity.js +10 -2
  34. package/dist/index.d.ts +82 -83
  35. package/dist/index.js +42 -42
  36. package/dist/input.d.ts +2 -2
  37. package/dist/input.js +50 -27
  38. package/dist/instruction-injection.d.ts +1 -1
  39. package/dist/middleware.js +9 -1
  40. package/dist/models.d.ts +2 -0
  41. package/dist/models.js +3 -0
  42. package/dist/node/agent-definitions.js +16 -8
  43. package/dist/node/contribution-discovery.d.ts +1 -2
  44. package/dist/node/contribution-discovery.js +3 -3
  45. package/dist/node/session-store-jsonl.js +10 -7
  46. package/dist/node/settings.d.ts +1 -1
  47. package/dist/node/settings.js +1 -1
  48. package/dist/node/system-project-prompts.js +2 -4
  49. package/dist/node/trust.js +1 -1
  50. package/dist/persistence-lifecycle.js +1 -3
  51. package/dist/provider-events.js +3 -1
  52. package/dist/provider-request-policy.js +3 -4
  53. package/dist/providers/media.d.ts +1 -1
  54. package/dist/providers/openai-compatible.d.ts +42 -1
  55. package/dist/providers/openai-compatible.js +110 -49
  56. package/dist/providers/openai-primitives.js +7 -7
  57. package/dist/providers/transport.d.ts +6 -0
  58. package/dist/providers/transport.js +21 -0
  59. package/dist/providers.d.ts +2 -0
  60. package/dist/providers.js +3 -0
  61. package/dist/redaction.d.ts +1 -0
  62. package/dist/redaction.js +26 -9
  63. package/dist/resources.d.ts +2 -2
  64. package/dist/resources.js +2 -2
  65. package/dist/retry.d.ts +5 -0
  66. package/dist/retry.js +8 -1
  67. package/dist/rpc.js +42 -9
  68. package/dist/run-ledger.d.ts +6 -0
  69. package/dist/run-ledger.js +16 -13
  70. package/dist/run-limits.js +49 -10
  71. package/dist/secure-agent.js +1 -1
  72. package/dist/security.js +7 -2
  73. package/dist/session-stores.d.ts +1 -1
  74. package/dist/session-stores.js +28 -24
  75. package/dist/structured-output.js +2 -2
  76. package/dist/system-prompts.js +7 -2
  77. package/dist/testing/compaction-conformance.js +5 -1
  78. package/dist/testing/extension-conformance.js +15 -3
  79. package/dist/testing/feedback.d.ts +1 -3
  80. package/dist/testing/feedback.js +1 -1
  81. package/dist/testing/persistence-schema.js +206 -37
  82. package/dist/testing/provider-conformance.js +3 -3
  83. package/dist/testing/run-ledger-conformance.js +1 -1
  84. package/dist/testing/session-store-conformance.js +1 -1
  85. package/dist/testing/tool-conformance.js +30 -5
  86. package/dist/thinking.js +4 -1
  87. package/dist/tools.d.ts +2 -2
  88. package/dist/tools.js +24 -5
  89. package/docs/0.1.0-readiness.md +139 -0
  90. package/docs/agent-events.md +2 -1
  91. package/docs/agent-session-runtime.md +2 -2
  92. package/docs/cli-rpc.md +1 -5
  93. package/docs/coding-agent-tools.md +2 -0
  94. package/docs/compaction-and-retry.md +3 -1
  95. package/docs/contribution-registries.md +1 -0
  96. package/docs/credentials-and-redaction.md +1 -1
  97. package/docs/extensions.md +1 -1
  98. package/docs/guardrails.md +13 -2
  99. package/docs/index.md +4 -2
  100. package/docs/input-and-prompt-assembly.md +3 -3
  101. package/docs/middleware-hooks.md +2 -2
  102. package/docs/migration.md +40 -0
  103. package/docs/performance.md +33 -0
  104. package/docs/providers/openai-compatible.md +28 -1
  105. package/docs/public-contracts.md +2 -2
  106. package/docs/release-and-install.md +127 -17
  107. package/docs/session-stores.md +1 -1
  108. package/package.json +14 -6
  109. package/docs/review-coverage-2026-07-14.md +0 -260
  110. package/docs/review-coverage-2026-07-15.md +0 -193
  111. package/docs/review-coverage-2026-07-17-provider-validation.md +0 -192
  112. package/docs/review-coverage-2026-07-19-phase-3.md +0 -174
  113. package/docs/review-coverage-2026-07-20-phase-4.md +0 -175
  114. package/docs/review-coverage-2026-07-21-phase-5.md +0 -172
  115. package/docs/review-coverage-2026-07-22-phase-6.md +0 -209
  116. package/docs/review-coverage-2026-07-22-phase-7.md +0 -173
  117. package/docs/review-coverage-2026-07-23-phase-8.md +0 -245
  118. package/docs/review-coverage-2026-07-25-phase-9.md +0 -256
  119. package/docs/review-coverage-2026-07-26-phase-10.md +0 -132
@@ -1,13 +1,12 @@
1
+ import { readdir, readFile, stat } from "node:fs/promises";
1
2
  import { join } from "node:path";
2
- import { readFile } from "node:fs/promises";
3
- import { readdir, stat } from "node:fs/promises";
4
- import { parseSkillFile, splitFrontmatter } from "../contribution-parsing.js";
5
3
  import { resolveAgentDefinition } from "../agent-definitions.js";
4
+ import { parseSkillFile, splitFrontmatter } from "../contribution-parsing.js";
6
5
  import { assertPermission, isTrusted } from "../security.js";
7
- import { isPathInsideReal } from "./trust.js";
8
- import { readOptionalFile } from "./contribution-discovery.js";
9
6
  import { createSkillRegistry } from "../skills.js";
10
7
  import { isNodeErrorCode } from "./config.js";
8
+ import { readOptionalFile } from "./contribution-discovery.js";
9
+ import { isPathInsideReal } from "./trust.js";
11
10
  const defaultInclude = {
12
11
  systemPrompt: true,
13
12
  agentPrompt: true,
@@ -288,7 +287,13 @@ function resolveSkillContexts(skills, contextProviders) {
288
287
  function collectMetadata(front) {
289
288
  const metadata = {};
290
289
  for (const [key, value] of front) {
291
- if (key !== "name" && key !== "description" && key !== "model" && key !== "tools" && key !== "skills" && key !== "context" && key !== "instructions") {
290
+ if (key !== "name" &&
291
+ key !== "description" &&
292
+ key !== "model" &&
293
+ key !== "tools" &&
294
+ key !== "skills" &&
295
+ key !== "context" &&
296
+ key !== "instructions") {
292
297
  metadata[key] = value;
293
298
  }
294
299
  }
@@ -305,7 +310,10 @@ function getStringList(front, key) {
305
310
  if (Array.isArray(value))
306
311
  return value.map(String);
307
312
  if (typeof value === "string")
308
- return value.split(",").map((s) => s.trim()).filter(Boolean);
313
+ return value
314
+ .split(",")
315
+ .map((s) => s.trim())
316
+ .filter(Boolean);
309
317
  return undefined;
310
318
  }
311
319
  function bodyAsInstructions(text, path) {
@@ -348,7 +356,7 @@ async function readOptional(read, path) {
348
356
  const NAME_RE = /^[A-Za-z0-9 _-]+$/;
349
357
  function parentDirName(path) {
350
358
  const parts = path.split(/[/\\]+/).filter(Boolean);
351
- return parts.length >= 2 ? parts[parts.length - 2] : parts[parts.length - 1] ?? "";
359
+ return parts.length >= 2 ? parts[parts.length - 2] : (parts[parts.length - 1] ?? "");
352
360
  }
353
361
  /** Parse a `CONTEXT.md` into a static {@link ContextProvider}. The markdown
354
362
  * body becomes the context block content; frontmatter `name` overrides the
@@ -1,6 +1,5 @@
1
1
  import type { ContributionFileKind, DiscoveredContribution } from "../contracts.js";
2
- import type { PermissionPolicy } from "../security.js";
3
- import type { TrustPolicy } from "../security.js";
2
+ import type { PermissionPolicy, TrustPolicy } from "../security.js";
4
3
  export interface DiscoveryOptions {
5
4
  readonly kinds: readonly ContributionFileKind[];
6
5
  /** Workspace root. Scans `<root>/.agents/<kind>s/<name>/`. Gated by `trust`. */
@@ -1,10 +1,10 @@
1
1
  import { readdir, readFile, stat } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { isJsonObject } from "../config.js";
4
- import { assertPermission } from "../security.js";
5
- import { isPathInsideReal } from "./trust.js";
6
4
  import { parseSkillFile } from "../contribution-parsing.js";
5
+ import { assertPermission } from "../security.js";
7
6
  import { isNodeErrorCode } from "./config.js";
7
+ import { isPathInsideReal } from "./trust.js";
8
8
  /**
9
9
  * Discover contributions on disk. Scans the workspace `.agents/` tree only;
10
10
  * no global root. One `readdir` per kind-root; no `import()`. Inert output —
@@ -72,7 +72,7 @@ async function readEntry(dir, fallbackName, kind, origin) {
72
72
  return readManifestEntry(dir, fallbackName, kind, origin);
73
73
  }
74
74
  }
75
- async function readSkillEntry(dir, fallbackName, origin) {
75
+ async function readSkillEntry(dir, _fallbackName, origin) {
76
76
  const path = join(dir, "SKILL.md");
77
77
  if (!(await isPathInsideReal(dir, path)))
78
78
  return undefined;
@@ -1,6 +1,6 @@
1
- import { mkdir, readFile, appendFile } from "node:fs/promises";
1
+ import { appendFile, mkdir, readFile } from "node:fs/promises";
2
2
  import { dirname } from "node:path";
3
- import { SESSION_APPEND_CONFLICT_CODE, SessionAppendConflictError, SessionSearchUnsupportedError, isSessionEntryKind, SESSION_ENTRY_SCHEMA_VERSION, } from "../contracts.js";
3
+ import { isSessionEntryKind, SESSION_APPEND_CONFLICT_CODE, SESSION_ENTRY_SCHEMA_VERSION, SessionAppendConflictError, SessionSearchUnsupportedError, } from "../contracts.js";
4
4
  import { isNodeErrorCode } from "./config.js";
5
5
  export function createJsonlSessionStore(pathOrOptions) {
6
6
  const options = typeof pathOrOptions === "string" ? { path: pathOrOptions, createDirectory: true } : pathOrOptions;
@@ -93,7 +93,10 @@ function parseEntry(line, lineNumber) {
93
93
  }
94
94
  function validateSessionEntry(value, lineNumber, raw) {
95
95
  if (!isBasicSessionEntry(value)) {
96
- return { ok: false, error: { line: lineNumber, message: "Invalid session entry: expected object with id, sessionId, timestamp, and kind", raw } };
96
+ return {
97
+ ok: false,
98
+ error: { line: lineNumber, message: "Invalid session entry: expected object with id, sessionId, timestamp, and kind", raw },
99
+ };
97
100
  }
98
101
  const entry = value;
99
102
  if (entry.parentId !== undefined && typeof entry.parentId !== "string") {
@@ -149,10 +152,10 @@ function isBasicSessionEntry(value) {
149
152
  if (!value || typeof value !== "object" || Array.isArray(value))
150
153
  return false;
151
154
  const entry = value;
152
- return typeof entry.id === "string"
153
- && typeof entry.sessionId === "string"
154
- && typeof entry.timestamp === "string"
155
- && typeof entry.kind === "string";
155
+ return (typeof entry.id === "string" &&
156
+ typeof entry.sessionId === "string" &&
157
+ typeof entry.timestamp === "string" &&
158
+ typeof entry.kind === "string");
156
159
  }
157
160
  function isMessage(value) {
158
161
  if (!value || typeof value !== "object" || Array.isArray(value))
@@ -1,6 +1,6 @@
1
1
  import { type ConfigLayer } from "../config.js";
2
- import { type NodeConfigFile } from "./config.js";
3
2
  import type { SettingsProvider } from "../contracts.js";
3
+ import { type NodeConfigFile } from "./config.js";
4
4
  export type NodeSettingsFile = NodeConfigFile;
5
5
  export declare function defaultUserSettingsPath(appName?: string): string;
6
6
  export declare function readSettingsFile(path: string): Promise<ConfigLayer["config"]>;
@@ -1,8 +1,8 @@
1
1
  import { homedir } from "node:os";
2
2
  import { join } from "node:path";
3
3
  import { mergeConfigLayers } from "../config.js";
4
- import { readConfigFile, isNodeErrorCode } from "./config.js";
5
4
  import { createStaticSettingsProvider } from "../settings.js";
5
+ import { isNodeErrorCode, readConfigFile } from "./config.js";
6
6
  export function defaultUserSettingsPath(appName = "prism") {
7
7
  return join(homedir(), ".config", appName, "settings.json");
8
8
  }
@@ -14,10 +14,8 @@ import { readOptionalFile } from "./contribution-discovery.js";
14
14
  * singletons don't fit `discoverContributions`' named-subdir scanner, so this is a
15
15
  * sibling loader mirroring `src/node/instruction-injectors.ts`'s per-concern adapter shape. */
16
16
  export async function loadSystemPromptFiles(options) {
17
- const agentsPath = options.agentsMdPath
18
- ?? (options.workspaceRoot !== undefined ? join(options.workspaceRoot, "AGENTS.md") : undefined);
19
- const systemPath = options.systemMdPath
20
- ?? (options.globalRoot !== undefined ? join(options.globalRoot, ".prism", "agent", "SYSTEM.md") : undefined);
17
+ const agentsPath = options.agentsMdPath ?? (options.workspaceRoot !== undefined ? join(options.workspaceRoot, "AGENTS.md") : undefined);
18
+ const systemPath = options.systemMdPath ?? (options.globalRoot !== undefined ? join(options.globalRoot, ".prism", "agent", "SYSTEM.md") : undefined);
21
19
  const out = [];
22
20
  if (systemPath !== undefined) {
23
21
  const layer = await readSystemFile(systemPath, options.permission);
@@ -1,5 +1,5 @@
1
- import { isAbsolute, relative, resolve, dirname, basename, join } from "node:path";
2
1
  import { realpath } from "node:fs/promises";
2
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
3
3
  import { isNodeErrorCode } from "./config.js";
4
4
  export function isPathInside(root, target) {
5
5
  const from = resolve(root);
@@ -64,9 +64,7 @@ export function createMemoryPersistenceLifecycle() {
64
64
  async applyRetention(input) {
65
65
  throwIfAborted(input.signal);
66
66
  assertOwnership(input);
67
- const held = new Set([...holds.values()]
68
- .filter((hold) => sameOwnership(input, hold) && hold.resourceKind === "session")
69
- .map((hold) => hold.resourceId));
67
+ const held = new Set([...holds.values()].filter((hold) => sameOwnership(input, hold) && hold.resourceKind === "session").map((hold) => hold.resourceId));
70
68
  const candidates = input.candidates ?? [];
71
69
  const deletedIds = [];
72
70
  const skippedHeld = [];
@@ -35,7 +35,9 @@ export function reconstructToolCallDeltas(events) {
35
35
  partial.argumentsText += event.argumentsText;
36
36
  partials.set(event.index, partial);
37
37
  }
38
- return [...partials.entries()].sort(([a], [b]) => a - b).map(([index, partial]) => {
38
+ return [...partials.entries()]
39
+ .sort(([a], [b]) => a - b)
40
+ .map(([index, partial]) => {
39
41
  if (!partial.id || !partial.name) {
40
42
  throw new ProviderTransportError("incomplete_delta", `Incomplete tool call delta at index ${index}`);
41
43
  }
@@ -50,10 +50,9 @@ export function mergeProviderRequestOptions(base, patch) {
50
50
  cache: {
51
51
  ...base.cache,
52
52
  ...patch.cache,
53
- breakpoints: base.cache?.breakpoints || patch.cache?.breakpoints ? [
54
- ...(base.cache?.breakpoints ?? []),
55
- ...(patch.cache?.breakpoints ?? []),
56
- ] : undefined,
53
+ breakpoints: base.cache?.breakpoints || patch.cache?.breakpoints
54
+ ? [...(base.cache?.breakpoints ?? []), ...(patch.cache?.breakpoints ?? [])]
55
+ : undefined,
57
56
  },
58
57
  };
59
58
  }
@@ -1,5 +1,5 @@
1
+ import { type MediaContentBlock, type ModelInputCapability, type ResolvedMediaContent, type ResolveMediaContentOptions } from "../content.js";
1
2
  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
3
  /** Default upload-cache entry cap per provider media session. */
4
4
  export declare const DEFAULT_PROVIDER_UPLOAD_CACHE_ENTRIES = 32;
5
5
  /** Inline OpenAI file_data ceiling before preferring Files API upload. */
@@ -1,4 +1,4 @@
1
- import type { AIProvider, ProviderRequest } from "../contracts.js";
1
+ import type { AIProvider, JsonObject, Message, ProviderEvent, ProviderRequest, Usage } from "../contracts.js";
2
2
  import { type CredentialValueSource } from "../credentials.js";
3
3
  export interface OpenAICompatibleProviderOptions {
4
4
  readonly id?: string;
@@ -9,5 +9,46 @@ export interface OpenAICompatibleProviderOptions {
9
9
  readonly chatCompletionsUrl?: string | ((request: ProviderRequest) => string);
10
10
  /** Default `bearer`. Azure resource keys use `api-key`; host-signed fetches may use `none`. */
11
11
  readonly authStyle?: "bearer" | "api-key" | "none";
12
+ /** Extra provider-specific body fields (thinking/reasoning/cache); merged over the base body. */
13
+ readonly buildBodyExtra?: (request: ProviderRequest) => JsonObject | undefined;
14
+ /** Transform messages before serialization (e.g. cache-control markers). Defaults to `request.messages`. */
15
+ readonly mapMessages?: (request: ProviderRequest) => readonly Message[];
16
+ /** Custom message serializer (e.g. Z.AI `reasoning_content` replay). Defaults to assert + `serializeOpenAIChatMessage`. */
17
+ readonly serializeMessage?: (message: Message, request: ProviderRequest) => JsonObject;
18
+ /** Custom usage mapping (e.g. OpenRouter cost fields). Defaults to `mapOpenAIChatUsage`. */
19
+ readonly mapUsage?: (usage: unknown) => Usage | undefined;
20
+ /** Extra request headers (merged over caller headers; provider auth/content-type still win). */
21
+ readonly extraHeaders?: (request: ProviderRequest) => Record<string, string>;
22
+ /** Final body transform applied last (token limits, compat stripping). Wins over everything. */
23
+ readonly transformBody?: (body: JsonObject, request: ProviderRequest) => JsonObject;
24
+ /** Require `[DONE]` and a `finish_reason` before emitting `done`; truncated streams yield an error. `done` then carries the final usage. */
25
+ readonly strictCompletion?: boolean;
26
+ /** Emit the final stream usage on the `done` event (without strict completion checks). */
27
+ readonly doneUsage?: boolean;
28
+ /** Prefix for HTTP error messages (default `OpenAI-compatible request failed`). */
29
+ readonly requestFailedPrefix?: string;
30
+ /** Custom HTTP error mapping (e.g. NeuralWatt retry classification). Receives the response and redacted body text. */
31
+ readonly mapHttpError?: (response: Response, bodyText: string, secrets: readonly (string | undefined)[]) => Error;
32
+ /** Handle SSE comment lines in the stream (e.g. NeuralWatt energy/cost telemetry). */
33
+ readonly onComment?: (text: string) => ProviderEvent | undefined;
12
34
  }
35
+ export interface OpenAIChatEventsOptions {
36
+ readonly signal?: AbortSignal;
37
+ /** Require `[DONE]` and a `finish_reason`; `done` then carries the final usage. */
38
+ readonly strictCompletion?: boolean;
39
+ /** Emit the final stream usage on the `done` event. */
40
+ readonly doneUsage?: boolean;
41
+ readonly mapUsage?: (usage: unknown) => Usage | undefined;
42
+ /** Handle an SSE comment line (text after `:`), e.g. NeuralWatt `: energy` / `: cost` telemetry. Returned events are yielded in stream order before the data of the same SSE event. */
43
+ readonly onComment?: (text: string) => ProviderEvent | undefined;
44
+ }
45
+ /**
46
+ * Shared OpenAI Chat Completions SSE stream loop: maps `data:` frames to Prism
47
+ * `ProviderEvent` values (text/thinking deltas, tool-call fragments, usage, done/error).
48
+ */
49
+ export declare function openAIChatEvents(body: ReadableStream<Uint8Array>, options?: OpenAIChatEventsOptions): AsyncIterable<ProviderEvent>;
13
50
  export declare function createOpenAICompatibleProvider(options: OpenAICompatibleProviderOptions): AIProvider;
51
+ /** Subset of factory options that shape the request body. */
52
+ export type OpenAIChatBodyOptions = Pick<OpenAICompatibleProviderOptions, "mapMessages" | "serializeMessage" | "buildBodyExtra" | "transformBody">;
53
+ /** Base Chat Completions request body builder, exported for provider packages keeping public body helpers. */
54
+ export declare function buildOpenAIChatBody(request: ProviderRequest, options?: OpenAIChatBodyOptions): JsonObject;
@@ -1,28 +1,110 @@
1
1
  import { resolveCredentialValue } from "../credentials.js";
2
2
  import { providerDone, providerError, providerTextDelta, providerThinkingDelta, providerToolCall, providerToolCallDelta, providerUsage, toolCallFromArgumentsText, } from "../provider-events.js";
3
- import { assertOpenAIChatMessage, applyOpenAIChatStructuredOutput, mapOpenAIChatUsage, serializeOpenAIChatMessage, serializeOpenAITool, } from "./openai-primitives.js";
4
- import { ProviderTransportError, readBoundedResponseText, readSseData, } from "./transport.js";
5
3
  import { assertStructuredOutputRequestSupported } from "../structured-output.js";
4
+ import { applyOpenAIChatStructuredOutput, assertOpenAIChatMessage, mapOpenAIChatUsage, serializeOpenAIChatMessage, serializeOpenAITool, } from "./openai-primitives.js";
5
+ import { httpStatusError, ProviderTransportError, readBoundedResponseText, readSseEvents } from "./transport.js";
6
+ /**
7
+ * Shared OpenAI Chat Completions SSE stream loop: maps `data:` frames to Prism
8
+ * `ProviderEvent` values (text/thinking deltas, tool-call fragments, usage, done/error).
9
+ */
10
+ export async function* openAIChatEvents(body, options = {}) {
11
+ const tools = new Map();
12
+ let usage;
13
+ let sawDoneMarker = false;
14
+ let sawFinishReason = false;
15
+ for await (const sseEvent of readSseEvents(body, { signal: options.signal })) {
16
+ if (options.onComment && sseEvent.comments?.length) {
17
+ for (const text of sseEvent.comments) {
18
+ const commentEvent = options.onComment(text);
19
+ if (commentEvent)
20
+ yield commentEvent;
21
+ }
22
+ }
23
+ const data = sseEvent.data.trim();
24
+ if (!data)
25
+ continue;
26
+ if (data === "[DONE]") {
27
+ sawDoneMarker = true;
28
+ break;
29
+ }
30
+ let parsed;
31
+ try {
32
+ parsed = JSON.parse(data);
33
+ }
34
+ catch (error) {
35
+ // Malformed chunks are terminal: yield the error instead of crashing the generator.
36
+ yield providerError(error, []);
37
+ return;
38
+ }
39
+ const mapped = (options.mapUsage ?? mapOpenAIChatUsage)(parsed.usage);
40
+ if (mapped) {
41
+ usage = mapped;
42
+ yield providerUsage(mapped);
43
+ }
44
+ for (const choice of parsed.choices ?? []) {
45
+ if (choice.finish_reason)
46
+ sawFinishReason = true;
47
+ const delta = choice.delta ?? {};
48
+ if (typeof delta.content === "string" && delta.content)
49
+ yield providerTextDelta(delta.content);
50
+ const thinking = delta.reasoning ?? delta.reasoning_content;
51
+ if (typeof thinking === "string" && thinking) {
52
+ yield providerThinkingDelta(thinking);
53
+ }
54
+ for (const tool of delta.tool_calls ?? []) {
55
+ const index = tool.index ?? 0;
56
+ const current = tools.get(index) ?? { argumentsText: "" };
57
+ current.id = tool.id ?? current.id;
58
+ current.name = tool.function?.name ?? current.name;
59
+ current.argumentsText += tool.function?.arguments ?? "";
60
+ tools.set(index, current);
61
+ yield providerToolCallDelta({
62
+ index,
63
+ id: tool.id,
64
+ name: tool.function?.name,
65
+ argumentsText: tool.function?.arguments,
66
+ });
67
+ }
68
+ }
69
+ }
70
+ const incomplete = [...tools.entries()].find(([, call]) => !call.id || !call.name);
71
+ if (incomplete) {
72
+ yield providerError(new ProviderTransportError("incomplete_delta", `Incomplete tool call delta at index ${incomplete[0]}`));
73
+ return;
74
+ }
75
+ if (options.strictCompletion && (!sawDoneMarker || !sawFinishReason)) {
76
+ // Truncated streams must fail loudly — emitting done would mark partial output as succeeded.
77
+ yield providerError(new Error(`Chat stream ended without completion evidence ` +
78
+ `([DONE]: ${sawDoneMarker ? "received" : "missing"}, ` +
79
+ `finish_reason: ${sawFinishReason ? "received" : "missing"})`));
80
+ return;
81
+ }
82
+ for (const call of tools.values()) {
83
+ yield providerToolCall(toolCallFromArgumentsText(call.id, call.name, call.argumentsText));
84
+ }
85
+ yield providerDone(options.strictCompletion || options.doneUsage ? usage : undefined);
86
+ }
6
87
  export function createOpenAICompatibleProvider(options) {
7
88
  const providerId = options.id ?? "openai-compatible";
8
89
  return {
9
90
  id: providerId,
10
91
  async *generate(request) {
92
+ if (request.signal?.aborted)
93
+ throw request.signal.reason ?? new Error("aborted");
11
94
  const apiKey = await resolveCredentialValue(options.apiKey, {
12
95
  name: "apiKey",
13
96
  provider: providerId,
14
97
  });
15
98
  const fetchImpl = options.fetch ?? fetch;
16
99
  const secrets = [apiKey];
17
- const tools = new Map();
18
100
  try {
19
101
  const url = typeof options.chatCompletionsUrl === "function"
20
102
  ? options.chatCompletionsUrl(request)
21
- : options.chatCompletionsUrl
22
- ?? `${options.baseUrl.replace(/\/$/, "")}/chat/completions`;
103
+ : (options.chatCompletionsUrl ?? `${options.baseUrl.replace(/\/+$/, "")}/chat/completions`);
23
104
  const authStyle = options.authStyle ?? "bearer";
24
105
  const headers = {
25
106
  ...Object.fromEntries(Object.entries(request.options?.headers ?? {}).filter((entry) => typeof entry[1] === "string")),
107
+ ...options.extraHeaders?.(request),
26
108
  "content-type": "application/json",
27
109
  };
28
110
  if (apiKey && authStyle === "api-key")
@@ -32,56 +114,28 @@ export function createOpenAICompatibleProvider(options) {
32
114
  const response = await fetchImpl(url, {
33
115
  method: "POST",
34
116
  headers,
35
- body: JSON.stringify(toOpenAIRequest(request)),
117
+ body: JSON.stringify(toOpenAIRequest(request, options)),
36
118
  signal: request.signal,
37
119
  });
38
120
  if (!response.ok) {
39
- yield providerError(new Error(`OpenAI-compatible request failed: ${response.status} ${await readBoundedResponseText(response, { secrets })}`), secrets);
121
+ const bodyText = await readBoundedResponseText(response, { secrets });
122
+ const error = options.mapHttpError
123
+ ? options.mapHttpError(response, bodyText, secrets)
124
+ : httpStatusError(options.requestFailedPrefix ?? "OpenAI-compatible request failed", response, bodyText);
125
+ yield providerError(error, secrets);
40
126
  return;
41
127
  }
42
128
  if (!response.body) {
43
129
  yield providerError(new Error("OpenAI-compatible response had no body"), secrets);
44
130
  return;
45
131
  }
46
- for await (const data of readSseData(response.body, { signal: request.signal })) {
47
- if (data === "[DONE]")
48
- break;
49
- const parsed = JSON.parse(data);
50
- const usage = mapOpenAIChatUsage(parsed.usage);
51
- if (usage)
52
- yield providerUsage(usage);
53
- for (const choice of parsed.choices ?? []) {
54
- const delta = choice.delta ?? {};
55
- if (typeof delta.content === "string" && delta.content)
56
- yield providerTextDelta(delta.content);
57
- if (typeof delta.reasoning_content === "string" && delta.reasoning_content) {
58
- yield providerThinkingDelta(delta.reasoning_content);
59
- }
60
- for (const tool of delta.tool_calls ?? []) {
61
- const index = tool.index ?? 0;
62
- const current = tools.get(index) ?? { argumentsText: "" };
63
- current.id = tool.id ?? current.id;
64
- current.name = tool.function?.name ?? current.name;
65
- current.argumentsText += tool.function?.arguments ?? "";
66
- tools.set(index, current);
67
- yield providerToolCallDelta({
68
- index,
69
- id: tool.id,
70
- name: tool.function?.name,
71
- argumentsText: tool.function?.arguments,
72
- });
73
- }
74
- }
75
- }
76
- const incomplete = [...tools.entries()].find(([, call]) => !call.id || !call.name);
77
- if (incomplete) {
78
- yield providerError(new ProviderTransportError("incomplete_delta", `Incomplete tool call delta at index ${incomplete[0]}`), secrets);
79
- return;
80
- }
81
- for (const call of tools.values()) {
82
- yield providerToolCall(toolCallFromArgumentsText(call.id, call.name, call.argumentsText));
83
- }
84
- yield providerDone();
132
+ yield* openAIChatEvents(response.body, {
133
+ signal: request.signal,
134
+ strictCompletion: options.strictCompletion,
135
+ doneUsage: options.doneUsage,
136
+ mapUsage: options.mapUsage,
137
+ onComment: options.onComment,
138
+ });
85
139
  }
86
140
  catch (error) {
87
141
  yield providerError(error, secrets);
@@ -89,11 +143,13 @@ export function createOpenAICompatibleProvider(options) {
89
143
  },
90
144
  };
91
145
  }
92
- function toOpenAIRequest(request) {
146
+ function toOpenAIRequest(request, options) {
93
147
  assertStructuredOutputRequestSupported(request.model, request.options);
94
148
  const body = {
95
149
  model: request.model.model,
96
- messages: request.messages.map((message, index) => {
150
+ messages: (options.mapMessages?.(request) ?? request.messages).map((message, index) => {
151
+ if (options.serializeMessage)
152
+ return options.serializeMessage(message, request);
97
153
  assertOpenAIChatMessage(message, `messages[${index}]`);
98
154
  return serializeOpenAIChatMessage(message, request.model.capabilities ?? {});
99
155
  }),
@@ -103,6 +159,11 @@ function toOpenAIRequest(request) {
103
159
  ...request.model.parameters,
104
160
  };
105
161
  applyOpenAIChatStructuredOutput(body, request.options?.structuredOutput);
106
- return body;
162
+ const merged = { ...body, ...options.buildBodyExtra?.(request) };
163
+ return options.transformBody ? options.transformBody(merged, request) : merged;
164
+ }
165
+ /** Base Chat Completions request body builder, exported for provider packages keeping public body helpers. */
166
+ export function buildOpenAIChatBody(request, options = {}) {
167
+ return toOpenAIRequest(request, options);
107
168
  }
108
169
  //# sourceMappingURL=openai-compatible.js.map
@@ -110,13 +110,13 @@ export function mapOpenAIChatUsage(usage) {
110
110
  if (!usage || typeof usage !== "object" || Array.isArray(usage))
111
111
  return undefined;
112
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
- && wire.prompt_tokens_details?.cache_creation_input_tokens === undefined) {
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
+ wire.prompt_tokens_details?.cache_creation_input_tokens === undefined) {
120
120
  return undefined;
121
121
  }
122
122
  return {
@@ -20,6 +20,12 @@ export declare class ProviderTransportError extends Error {
20
20
  readonly limitBytes?: number;
21
21
  constructor(code: ProviderTransportErrorCode, message: string, limitBytes?: number);
22
22
  }
23
+ /** HTTP failure error carrying the status as numeric `code` plus any `Retry-After`
24
+ * hint as `retryAfterMs`, so retry policies classify transience and pace retries
25
+ * without parsing message text. Both fields flow into `ErrorInfo` via errorToErrorInfo. */
26
+ export declare function httpStatusError(prefix: string, response: Response, bodyText: string): Error;
27
+ /** Parse a `Retry-After` header (delay-seconds or HTTP-date) into milliseconds. */
28
+ export declare function parseRetryAfterMs(value: string | null, now?: number): number | undefined;
23
29
  export interface ReadSseEventsOptions extends BoundedStreamLimits {
24
30
  readonly signal?: AbortSignal;
25
31
  }
@@ -13,6 +13,27 @@ export class ProviderTransportError extends Error {
13
13
  this.limitBytes = limitBytes;
14
14
  }
15
15
  }
16
+ /** HTTP failure error carrying the status as numeric `code` plus any `Retry-After`
17
+ * hint as `retryAfterMs`, so retry policies classify transience and pace retries
18
+ * without parsing message text. Both fields flow into `ErrorInfo` via errorToErrorInfo. */
19
+ export function httpStatusError(prefix, response, bodyText) {
20
+ const error = new Error(`${prefix}: ${response.status} ${bodyText}`);
21
+ error.code = response.status;
22
+ const hint = parseRetryAfterMs(response.headers.get("retry-after"));
23
+ if (hint !== undefined)
24
+ error.retryAfterMs = hint;
25
+ return error;
26
+ }
27
+ /** Parse a `Retry-After` header (delay-seconds or HTTP-date) into milliseconds. */
28
+ export function parseRetryAfterMs(value, now = Date.now()) {
29
+ if (!value)
30
+ return undefined;
31
+ const seconds = Number(value);
32
+ if (Number.isFinite(seconds) && seconds >= 0)
33
+ return seconds * 1000;
34
+ const date = Date.parse(value);
35
+ return Number.isNaN(date) ? undefined : Math.max(0, date - now);
36
+ }
16
37
  function resolveLimits(options) {
17
38
  return {
18
39
  maxEventBytes: options?.maxEventBytes ?? DEFAULT_MAX_EVENT_BYTES,
@@ -2,6 +2,8 @@ import type { AIProvider, ModelConfig, ProviderResolver } from "./contracts.js";
2
2
  import { type DuplicateRegistrationOptions } from "./registry-options.js";
3
3
  export interface ProviderRegistry {
4
4
  register(provider: AIProvider): void;
5
+ /** Remove a provider; returns false when the id was not registered. */
6
+ unregister(id: string): boolean;
5
7
  get(id: string): AIProvider | undefined;
6
8
  resolve(model: Pick<ModelConfig, "provider"> | string): AIProvider;
7
9
  list(): readonly AIProvider[];
package/dist/providers.js CHANGED
@@ -6,6 +6,9 @@ export function createProviderRegistry(providers = [], options = {}) {
6
6
  assertCanRegister(byId, provider.id, "provider", provider.id, options.duplicate);
7
7
  byId.set(provider.id, provider);
8
8
  },
9
+ unregister(id) {
10
+ return byId.delete(id);
11
+ },
9
12
  get(id) {
10
13
  return byId.get(id);
11
14
  },
@@ -8,5 +8,6 @@ export declare function redactAgentEvent(event: AgentEvent, redactor?: SecretRed
8
8
  export declare function redactSessionEntry(entry: SessionEntry, redactor?: SecretRedactor): SessionEntry;
9
9
  export declare function redactProviderRequest(request: ProviderRequest, redactor?: SecretRedactor): ProviderRequest;
10
10
  export declare function redactRunLedgerRecord<T extends RunLedgerRecord>(record: T, redactor?: SecretRedactor): T;
11
+ export declare function resolveRedactor(redactor?: SecretRedactor, secrets?: readonly (string | undefined)[]): SecretRedactor | undefined;
11
12
  export declare function redactSecrets<T>(value: T, secrets: readonly (string | undefined)[]): T;
12
13
  export declare function errorToErrorInfo(error: unknown, secrets?: readonly (string | undefined)[]): ErrorInfo;