@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.
- package/CHANGELOG.md +31 -0
- package/dist/agent-definitions.js +2 -3
- package/dist/agent-loops.js +12 -7
- package/dist/agent-run-lifecycle.d.ts +1 -2
- package/dist/agent-run-lifecycle.js +1 -1
- package/dist/agent-run-state.js +29 -4
- package/dist/agents.d.ts +1 -1
- package/dist/agents.js +163 -61
- package/dist/cache-helpers.js +18 -9
- package/dist/checkpoints.d.ts +4 -0
- package/dist/checkpoints.js +17 -9
- package/dist/cli-init.js +3 -7
- package/dist/cli-runner.d.ts +2 -6
- package/dist/cli-runner.js +71 -33
- package/dist/compaction.js +5 -4
- package/dist/config.js +7 -4
- package/dist/content.js +26 -24
- package/dist/context-budget.js +18 -11
- package/dist/contracts.d.ts +16 -3
- package/dist/contracts.js +4 -1
- package/dist/contribution-parsing.js +6 -2
- package/dist/contributions.d.ts +2 -0
- package/dist/contributions.js +3 -0
- package/dist/conversations.js +2 -1
- package/dist/credentials.d.ts +8 -2
- package/dist/credentials.js +9 -3
- package/dist/event-multiplexer.js +18 -4
- package/dist/extensions.d.ts +7 -1
- package/dist/extensions.js +64 -6
- package/dist/feedback.js +12 -10
- package/dist/guardrails.d.ts +1 -1
- package/dist/guardrails.js +26 -17
- package/dist/identity.js +10 -2
- package/dist/index.d.ts +82 -83
- package/dist/index.js +42 -42
- package/dist/input.d.ts +2 -2
- package/dist/input.js +50 -27
- package/dist/instruction-injection.d.ts +1 -1
- package/dist/middleware.js +9 -1
- package/dist/models.d.ts +2 -0
- package/dist/models.js +3 -0
- package/dist/node/agent-definitions.js +16 -8
- package/dist/node/contribution-discovery.d.ts +1 -2
- package/dist/node/contribution-discovery.js +3 -3
- package/dist/node/session-store-jsonl.js +10 -7
- package/dist/node/settings.d.ts +1 -1
- package/dist/node/settings.js +1 -1
- package/dist/node/system-project-prompts.js +2 -4
- package/dist/node/trust.js +1 -1
- package/dist/persistence-lifecycle.js +1 -3
- package/dist/provider-events.js +3 -1
- package/dist/provider-request-policy.js +3 -4
- package/dist/providers/media.d.ts +1 -1
- package/dist/providers/openai-compatible.d.ts +42 -1
- package/dist/providers/openai-compatible.js +110 -49
- package/dist/providers/openai-primitives.js +7 -7
- package/dist/providers/transport.d.ts +6 -0
- package/dist/providers/transport.js +21 -0
- package/dist/providers.d.ts +2 -0
- package/dist/providers.js +3 -0
- package/dist/redaction.d.ts +1 -0
- package/dist/redaction.js +26 -9
- package/dist/resources.d.ts +2 -2
- package/dist/resources.js +2 -2
- package/dist/retry.d.ts +5 -0
- package/dist/retry.js +8 -1
- package/dist/rpc.js +42 -9
- package/dist/run-ledger.d.ts +6 -0
- package/dist/run-ledger.js +16 -13
- package/dist/run-limits.js +49 -10
- package/dist/secure-agent.js +1 -1
- package/dist/security.js +7 -2
- package/dist/session-stores.d.ts +1 -1
- package/dist/session-stores.js +28 -24
- package/dist/structured-output.js +2 -2
- package/dist/system-prompts.js +7 -2
- package/dist/testing/compaction-conformance.js +5 -1
- package/dist/testing/extension-conformance.js +15 -3
- package/dist/testing/feedback.d.ts +1 -3
- package/dist/testing/feedback.js +1 -1
- package/dist/testing/persistence-schema.js +206 -37
- package/dist/testing/provider-conformance.js +3 -3
- package/dist/testing/run-ledger-conformance.js +1 -1
- package/dist/testing/session-store-conformance.js +1 -1
- package/dist/testing/tool-conformance.js +30 -5
- package/dist/thinking.js +4 -1
- package/dist/tools.d.ts +2 -2
- package/dist/tools.js +24 -5
- package/docs/0.1.0-readiness.md +139 -0
- package/docs/agent-events.md +2 -1
- package/docs/agent-session-runtime.md +2 -2
- package/docs/cli-rpc.md +1 -5
- package/docs/coding-agent-tools.md +2 -0
- package/docs/compaction-and-retry.md +3 -1
- package/docs/contribution-registries.md +1 -0
- package/docs/credentials-and-redaction.md +1 -1
- package/docs/extensions.md +1 -1
- package/docs/guardrails.md +13 -2
- package/docs/index.md +4 -2
- package/docs/input-and-prompt-assembly.md +3 -3
- package/docs/middleware-hooks.md +2 -2
- package/docs/migration.md +40 -0
- package/docs/performance.md +33 -0
- package/docs/providers/openai-compatible.md +28 -1
- package/docs/public-contracts.md +2 -2
- package/docs/release-and-install.md +127 -17
- package/docs/session-stores.md +1 -1
- package/package.json +14 -6
- package/docs/review-coverage-2026-07-14.md +0 -260
- package/docs/review-coverage-2026-07-15.md +0 -193
- package/docs/review-coverage-2026-07-17-provider-validation.md +0 -192
- package/docs/review-coverage-2026-07-19-phase-3.md +0 -174
- package/docs/review-coverage-2026-07-20-phase-4.md +0 -175
- package/docs/review-coverage-2026-07-21-phase-5.md +0 -172
- package/docs/review-coverage-2026-07-22-phase-6.md +0 -209
- package/docs/review-coverage-2026-07-22-phase-7.md +0 -173
- package/docs/review-coverage-2026-07-23-phase-8.md +0 -245
- package/docs/review-coverage-2026-07-25-phase-9.md +0 -256
- 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" &&
|
|
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
|
|
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,
|
|
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
|
|
1
|
+
import { appendFile, mkdir, readFile } from "node:fs/promises";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
|
-
import {
|
|
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 {
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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))
|
package/dist/node/settings.d.ts
CHANGED
|
@@ -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"]>;
|
package/dist/node/settings.js
CHANGED
|
@@ -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
|
-
|
|
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);
|
package/dist/node/trust.js
CHANGED
|
@@ -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 = [];
|
package/dist/provider-events.js
CHANGED
|
@@ -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()]
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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,
|
package/dist/providers.d.ts
CHANGED
|
@@ -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
|
},
|
package/dist/redaction.d.ts
CHANGED
|
@@ -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;
|