@arnilo/prism 0.0.16 → 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 +20 -0
- package/dist/agent-run-state.js +13 -1
- package/dist/agents.js +42 -10
- package/dist/checkpoints.d.ts +4 -0
- package/dist/checkpoints.js +12 -0
- package/dist/cli-runner.d.ts +1 -5
- package/dist/cli-runner.js +5 -28
- package/dist/context-budget.js +6 -3
- package/dist/contracts.d.ts +13 -0
- package/dist/contributions.d.ts +2 -0
- package/dist/contributions.js +3 -0
- package/dist/credentials.d.ts +7 -1
- package/dist/credentials.js +6 -2
- package/dist/event-multiplexer.js +17 -1
- package/dist/extensions.d.ts +7 -1
- package/dist/extensions.js +64 -6
- package/dist/feedback.js +1 -1
- package/dist/guardrails.js +9 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/input.js +11 -4
- package/dist/middleware.js +9 -1
- package/dist/models.d.ts +2 -0
- package/dist/models.js +3 -0
- package/dist/providers/openai-compatible.d.ts +42 -1
- package/dist/providers/openai-compatible.js +109 -47
- 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.js +21 -7
- package/dist/retry.d.ts +5 -0
- package/dist/retry.js +8 -1
- package/dist/run-ledger.d.ts +6 -0
- package/dist/run-ledger.js +3 -9
- package/dist/session-stores.js +15 -11
- 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 +2 -2
- package/docs/input-and-prompt-assembly.md +3 -3
- package/docs/middleware-hooks.md +2 -2
- package/docs/migration.md +11 -0
- package/docs/providers/openai-compatible.md +28 -1
- package/docs/public-contracts.md +1 -1
- package/docs/release-and-install.md +36 -15
- package/docs/session-stores.md +1 -1
- package/package.json +1 -1
package/dist/feedback.js
CHANGED
|
@@ -216,7 +216,7 @@ function freezeRecord(record) {
|
|
|
216
216
|
});
|
|
217
217
|
}
|
|
218
218
|
function cloneFrozenJsonObject(value) {
|
|
219
|
-
const cloned =
|
|
219
|
+
const cloned = structuredClone(value);
|
|
220
220
|
if (!cloned || typeof cloned !== "object" || Array.isArray(cloned))
|
|
221
221
|
throw new RunFeedbackError("metadata must be a JSON object");
|
|
222
222
|
return deepFreeze(cloned);
|
package/dist/guardrails.js
CHANGED
|
@@ -5,7 +5,9 @@ export class GuardrailError extends Error {
|
|
|
5
5
|
code;
|
|
6
6
|
record;
|
|
7
7
|
constructor(record) {
|
|
8
|
-
super(record.action === "interrupt"
|
|
8
|
+
super(record.action === "interrupt"
|
|
9
|
+
? `Guardrail interruption is unavailable at stage "${record.stage}"; interrupt suspends only at the input stage of durable runs`
|
|
10
|
+
: "Guardrail blocked run");
|
|
9
11
|
this.name = "GuardrailError";
|
|
10
12
|
this.code = record.action === "interrupt" ? "ERR_PRISM_GUARDRAIL_INTERRUPT_UNAVAILABLE" : "ERR_PRISM_GUARDRAIL_BLOCKED";
|
|
11
13
|
this.record = record;
|
|
@@ -88,8 +90,12 @@ async function evaluate(guardrail, options, signal) {
|
|
|
88
90
|
}
|
|
89
91
|
return record(guardrail.name, options.stage, decision.action, decision.reason, decision.metadata, options.redactor);
|
|
90
92
|
}
|
|
91
|
-
catch {
|
|
92
|
-
|
|
93
|
+
catch (error) {
|
|
94
|
+
// Keep the cause diagnosable without leaking internals: message only, bounded and
|
|
95
|
+
// passed through the same redact+bound metadata path as guardrail-provided metadata.
|
|
96
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
97
|
+
const redacted = options.redactor?.redact(message) ?? message;
|
|
98
|
+
return record(guardrail.name, options.stage, "tripwire", "guardrail_failed", { error: boundText(redacted, MAX_REASON_BYTES) }, options.redactor);
|
|
93
99
|
}
|
|
94
100
|
}
|
|
95
101
|
function resolveConcurrency(value) {
|
package/dist/index.d.ts
CHANGED
|
@@ -35,7 +35,7 @@ export type { EventMultiplexer, EventMultiplexerOptions, EventOverflowInfo, Even
|
|
|
35
35
|
export { createEventMultiplexer } from "./event-multiplexer.js";
|
|
36
36
|
export type { ExecutionAction, ExecutionDecision, ExecutionPolicy, ExecutionRisk } from "./execution-policy.js";
|
|
37
37
|
export { applyExecutionDecision, assertExecutionAllowed, checkExecution, ExecutionDeniedError } from "./execution-policy.js";
|
|
38
|
-
export type { ExtensionErrorPolicy, ExtensionEventBus, ExtensionEventHandler, ExtensionKernel, ExtensionKernelOptions, ExtensionLoadPolicy, } from "./extensions.js";
|
|
38
|
+
export type { ExtensionErrorPolicy, ExtensionEventBus, ExtensionEventHandler, ExtensionKernel, ExtensionKernelOptions, ExtensionLoadPolicy, LoadedExtension, } from "./extensions.js";
|
|
39
39
|
export { createExtensionEventBus, createExtensionKernel } from "./extensions.js";
|
|
40
40
|
export type { MemoryRunFeedbackStoreOptions, PrepareRunFeedbackOptions, RunFeedbackLimits, RunFeedbackRun, RunFeedbackRunResolver, } from "./feedback.js";
|
|
41
41
|
export { createMemoryRunFeedbackStore, prepareRunFeedback, RunFeedbackError, requireRunFeedbackOwnership, runFeedbackPageLimit, } from "./feedback.js";
|
|
@@ -94,5 +94,5 @@ export { createToolParameterValidator, createToolRegistry, dispatchToolCall, fil
|
|
|
94
94
|
export type { ResolvedUseCaseModel, ResolveUseCaseModelInput, UseCaseModelBinding, } from "./use-case-model.js";
|
|
95
95
|
export { resolveUseCaseModel, resolveUseCaseModelBinding, useCaseCredentialProviderId, } from "./use-case-model.js";
|
|
96
96
|
export declare const name = "prism";
|
|
97
|
-
export declare const version = "0.0.
|
|
97
|
+
export declare const version = "0.0.17";
|
|
98
98
|
export declare const description = "Agent harness for AI providers, agents, sessions, and tools.";
|
package/dist/index.js
CHANGED
|
@@ -51,6 +51,6 @@ export { applyThinkingLevel, isThinkingLevel, normalizeThinkingLevel, THINKING_L
|
|
|
51
51
|
export { createToolParameterValidator, createToolRegistry, dispatchToolCall, filterTools } from "./tools.js";
|
|
52
52
|
export { resolveUseCaseModel, resolveUseCaseModelBinding, useCaseCredentialProviderId, } from "./use-case-model.js";
|
|
53
53
|
export const name = "prism";
|
|
54
|
-
export const version = "0.0.
|
|
54
|
+
export const version = "0.0.17";
|
|
55
55
|
export const description = "Agent harness for AI providers, agents, sessions, and tools.";
|
|
56
56
|
//# sourceMappingURL=index.js.map
|
package/dist/input.js
CHANGED
|
@@ -9,8 +9,9 @@ export function createDefaultInputBuilder() {
|
|
|
9
9
|
name: "default-input",
|
|
10
10
|
async build(input, context = {}) {
|
|
11
11
|
const groups = await buildDefaultInputMessageGroups(input, context);
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
// `input_assembly` middleware is applied by assembleProviderInput after build(),
|
|
13
|
+
// never here: builders must not be a security seam.
|
|
14
|
+
return flattenInputGroups(groups, context.inputLayout ?? "legacy");
|
|
14
15
|
},
|
|
15
16
|
};
|
|
16
17
|
}
|
|
@@ -35,7 +36,10 @@ export function createDefaultPromptBuilder() {
|
|
|
35
36
|
return {
|
|
36
37
|
name: "default-prompt",
|
|
37
38
|
async build(request) {
|
|
38
|
-
|
|
39
|
+
// Tool-capable models receive schemas via request.tools; the text list only serves
|
|
40
|
+
// text-only (or unknown-capability) models — duplicating it doubles tool tokens per turn.
|
|
41
|
+
const tools = request.model?.capabilities?.tools === true ? undefined : request.tools;
|
|
42
|
+
return [...contextMessages(request.context), ...skillMessages(request.skills), ...toolMessages(tools), ...request.messages];
|
|
39
43
|
},
|
|
40
44
|
};
|
|
41
45
|
}
|
|
@@ -113,6 +117,9 @@ export async function assembleProviderInput(options) {
|
|
|
113
117
|
else {
|
|
114
118
|
const inputBuilder = options.inputBuilder ?? createDefaultInputBuilder();
|
|
115
119
|
messages = await inputBuilder.build(options.input, buildContext);
|
|
120
|
+
// Runtime-owned: input_assembly middleware always runs, regardless of which builder is installed.
|
|
121
|
+
if (buildContext.middleware)
|
|
122
|
+
messages = await buildContext.middleware.run("input_assembly", messages);
|
|
116
123
|
context = await resolveContextProviders({
|
|
117
124
|
providers: options.contextProviders,
|
|
118
125
|
messages,
|
|
@@ -139,7 +146,7 @@ export async function assembleProviderInput(options) {
|
|
|
139
146
|
metadata: options.metadata,
|
|
140
147
|
signal: options.signal,
|
|
141
148
|
};
|
|
142
|
-
const providerMessages = await promptBuilder.build({ ...promptRequest, tools });
|
|
149
|
+
const providerMessages = await promptBuilder.build({ ...promptRequest, tools, model: options.model });
|
|
143
150
|
assertMessagesSupportModelCapabilities(options.model, providerMessages);
|
|
144
151
|
const metadata = budgetReport ? { ...options.metadata, [CONTEXT_BUDGET_REPORT_METADATA_KEY]: budgetReport } : options.metadata;
|
|
145
152
|
return {
|
package/dist/middleware.js
CHANGED
|
@@ -21,10 +21,13 @@ export function createMiddlewareRegistry(options = {}) {
|
|
|
21
21
|
},
|
|
22
22
|
async run(hook, value) {
|
|
23
23
|
let current = value;
|
|
24
|
-
for (const middleware of byHook.get(hook) ?? []) {
|
|
24
|
+
for (const [index, middleware] of (byHook.get(hook) ?? []).entries()) {
|
|
25
25
|
try {
|
|
26
26
|
let calledNext = false;
|
|
27
27
|
const next = async (nextValue) => {
|
|
28
|
+
// Double next() forks the chain value nondeterministically — always a bug.
|
|
29
|
+
if (calledNext)
|
|
30
|
+
throw new Error(`Middleware hook "${hook}" #${index}: next() called more than once`);
|
|
28
31
|
calledNext = true;
|
|
29
32
|
current = nextValue;
|
|
30
33
|
return current;
|
|
@@ -32,6 +35,11 @@ export function createMiddlewareRegistry(options = {}) {
|
|
|
32
35
|
const result = await middleware(current, next);
|
|
33
36
|
if (!calledNext)
|
|
34
37
|
current = result;
|
|
38
|
+
else if (result !== undefined && result !== current) {
|
|
39
|
+
// next(v) already committed the chain value; a conflicting return is silently
|
|
40
|
+
// discarded. Ambiguous rather than certainly-wrong, so diagnose, don't throw.
|
|
41
|
+
await options.onError?.(middlewareError(new Error(`Middleware hook "${hook}" #${index}: called next(value) and returned a different value; the next() value wins, the return is discarded`), hook, secrets));
|
|
42
|
+
}
|
|
35
43
|
}
|
|
36
44
|
catch (error) {
|
|
37
45
|
if (errorPolicy === "throw")
|
package/dist/models.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import type { ModelConfig } from "./contracts.js";
|
|
|
2
2
|
import { type DuplicateRegistrationOptions } from "./registry-options.js";
|
|
3
3
|
export interface ModelRegistry {
|
|
4
4
|
register(model: ModelConfig): void;
|
|
5
|
+
/** Remove a model; returns false when it was not registered. */
|
|
6
|
+
unregister(provider: string, model: string): boolean;
|
|
5
7
|
get(provider: string, model: string): ModelConfig | undefined;
|
|
6
8
|
resolve(provider: string, model: string): ModelConfig;
|
|
7
9
|
list(): readonly ModelConfig[];
|
package/dist/models.js
CHANGED
|
@@ -8,6 +8,9 @@ export function createModelRegistry(models = [], options = {}) {
|
|
|
8
8
|
assertCanRegister(byId, id, "model", `${model.provider}/${model.model}`, options.duplicate);
|
|
9
9
|
byId.set(id, model);
|
|
10
10
|
},
|
|
11
|
+
unregister(provider, model) {
|
|
12
|
+
return byId.delete(key(provider, model));
|
|
13
|
+
},
|
|
11
14
|
get(provider, model) {
|
|
12
15
|
return byId.get(key(provider, model));
|
|
13
16
|
},
|
|
@@ -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;
|
|
@@ -2,26 +2,109 @@ import { resolveCredentialValue } from "../credentials.js";
|
|
|
2
2
|
import { providerDone, providerError, providerTextDelta, providerThinkingDelta, providerToolCall, providerToolCallDelta, providerUsage, toolCallFromArgumentsText, } from "../provider-events.js";
|
|
3
3
|
import { assertStructuredOutputRequestSupported } from "../structured-output.js";
|
|
4
4
|
import { applyOpenAIChatStructuredOutput, assertOpenAIChatMessage, mapOpenAIChatUsage, serializeOpenAIChatMessage, serializeOpenAITool, } from "./openai-primitives.js";
|
|
5
|
-
import { ProviderTransportError, readBoundedResponseText,
|
|
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 ?? `${options.baseUrl.replace(
|
|
103
|
+
: (options.chatCompletionsUrl ?? `${options.baseUrl.replace(/\/+$/, "")}/chat/completions`);
|
|
22
104
|
const authStyle = options.authStyle ?? "bearer";
|
|
23
105
|
const headers = {
|
|
24
106
|
...Object.fromEntries(Object.entries(request.options?.headers ?? {}).filter((entry) => typeof entry[1] === "string")),
|
|
107
|
+
...options.extraHeaders?.(request),
|
|
25
108
|
"content-type": "application/json",
|
|
26
109
|
};
|
|
27
110
|
if (apiKey && authStyle === "api-key")
|
|
@@ -31,56 +114,28 @@ export function createOpenAICompatibleProvider(options) {
|
|
|
31
114
|
const response = await fetchImpl(url, {
|
|
32
115
|
method: "POST",
|
|
33
116
|
headers,
|
|
34
|
-
body: JSON.stringify(toOpenAIRequest(request)),
|
|
117
|
+
body: JSON.stringify(toOpenAIRequest(request, options)),
|
|
35
118
|
signal: request.signal,
|
|
36
119
|
});
|
|
37
120
|
if (!response.ok) {
|
|
38
|
-
|
|
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);
|
|
39
126
|
return;
|
|
40
127
|
}
|
|
41
128
|
if (!response.body) {
|
|
42
129
|
yield providerError(new Error("OpenAI-compatible response had no body"), secrets);
|
|
43
130
|
return;
|
|
44
131
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
for (const choice of parsed.choices ?? []) {
|
|
53
|
-
const delta = choice.delta ?? {};
|
|
54
|
-
if (typeof delta.content === "string" && delta.content)
|
|
55
|
-
yield providerTextDelta(delta.content);
|
|
56
|
-
if (typeof delta.reasoning_content === "string" && delta.reasoning_content) {
|
|
57
|
-
yield providerThinkingDelta(delta.reasoning_content);
|
|
58
|
-
}
|
|
59
|
-
for (const tool of delta.tool_calls ?? []) {
|
|
60
|
-
const index = tool.index ?? 0;
|
|
61
|
-
const current = tools.get(index) ?? { argumentsText: "" };
|
|
62
|
-
current.id = tool.id ?? current.id;
|
|
63
|
-
current.name = tool.function?.name ?? current.name;
|
|
64
|
-
current.argumentsText += tool.function?.arguments ?? "";
|
|
65
|
-
tools.set(index, current);
|
|
66
|
-
yield providerToolCallDelta({
|
|
67
|
-
index,
|
|
68
|
-
id: tool.id,
|
|
69
|
-
name: tool.function?.name,
|
|
70
|
-
argumentsText: tool.function?.arguments,
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
const incomplete = [...tools.entries()].find(([, call]) => !call.id || !call.name);
|
|
76
|
-
if (incomplete) {
|
|
77
|
-
yield providerError(new ProviderTransportError("incomplete_delta", `Incomplete tool call delta at index ${incomplete[0]}`), secrets);
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
for (const call of tools.values()) {
|
|
81
|
-
yield providerToolCall(toolCallFromArgumentsText(call.id, call.name, call.argumentsText));
|
|
82
|
-
}
|
|
83
|
-
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
|
+
});
|
|
84
139
|
}
|
|
85
140
|
catch (error) {
|
|
86
141
|
yield providerError(error, secrets);
|
|
@@ -88,11 +143,13 @@ export function createOpenAICompatibleProvider(options) {
|
|
|
88
143
|
},
|
|
89
144
|
};
|
|
90
145
|
}
|
|
91
|
-
function toOpenAIRequest(request) {
|
|
146
|
+
function toOpenAIRequest(request, options) {
|
|
92
147
|
assertStructuredOutputRequestSupported(request.model, request.options);
|
|
93
148
|
const body = {
|
|
94
149
|
model: request.model.model,
|
|
95
|
-
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);
|
|
96
153
|
assertOpenAIChatMessage(message, `messages[${index}]`);
|
|
97
154
|
return serializeOpenAIChatMessage(message, request.model.capabilities ?? {});
|
|
98
155
|
}),
|
|
@@ -102,6 +159,11 @@ function toOpenAIRequest(request) {
|
|
|
102
159
|
...request.model.parameters,
|
|
103
160
|
};
|
|
104
161
|
applyOpenAIChatStructuredOutput(body, request.options?.structuredOutput);
|
|
105
|
-
|
|
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);
|
|
106
168
|
}
|
|
107
169
|
//# sourceMappingURL=openai-compatible.js.map
|
|
@@ -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.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
const REDACTED = "[REDACTED]";
|
|
2
|
+
// Depth bound matching agent-run-state.ts; hostile deep structures yield a placeholder
|
|
3
|
+
// instead of a stack overflow.
|
|
4
|
+
const MAX_REDACT_DEPTH = 32;
|
|
2
5
|
export function createSecretRedactor(secrets) {
|
|
3
6
|
return { redact: (value) => redactSecrets(value, secrets) };
|
|
4
7
|
}
|
|
@@ -43,11 +46,13 @@ export function redactSecrets(value, secrets) {
|
|
|
43
46
|
// ponytail: active-path WeakSet marks only ancestor cycles as [Circular]; shared
|
|
44
47
|
// references (diamonds) are visited again on separate branches. Map/Set normalize
|
|
45
48
|
// to JSON-shaped output; string keys are redacted like values.
|
|
46
|
-
const redact = (input, active = new WeakSet()) => {
|
|
49
|
+
const redact = (input, active = new WeakSet(), depth = 0) => {
|
|
47
50
|
if (typeof input === "string")
|
|
48
51
|
return redactString(input);
|
|
49
52
|
if (input === null || typeof input !== "object")
|
|
50
53
|
return input;
|
|
54
|
+
if (depth >= MAX_REDACT_DEPTH)
|
|
55
|
+
return "[MaxDepth]";
|
|
51
56
|
if (input instanceof Date || input instanceof RegExp)
|
|
52
57
|
return input;
|
|
53
58
|
if (ArrayBuffer.isView(input) || input instanceof ArrayBuffer)
|
|
@@ -57,18 +62,18 @@ export function redactSecrets(value, secrets) {
|
|
|
57
62
|
active.add(input);
|
|
58
63
|
try {
|
|
59
64
|
if (Array.isArray(input))
|
|
60
|
-
return input.map((item) => redact(item, active));
|
|
65
|
+
return input.map((item) => redact(item, active, depth + 1));
|
|
61
66
|
if (input instanceof Map) {
|
|
62
67
|
const out = {};
|
|
63
68
|
for (const [key, item] of input)
|
|
64
|
-
assignKey(out, redactKey(key), redact(item, active));
|
|
69
|
+
assignKey(out, redactKey(key), redact(item, active, depth + 1));
|
|
65
70
|
return out;
|
|
66
71
|
}
|
|
67
72
|
if (input instanceof Set)
|
|
68
|
-
return [...input].map((item) => redact(item, active));
|
|
73
|
+
return [...input].map((item) => redact(item, active, depth + 1));
|
|
69
74
|
const out = {};
|
|
70
75
|
for (const [key, item] of Object.entries(input))
|
|
71
|
-
assignKey(out, redactKey(key), redact(item, active));
|
|
76
|
+
assignKey(out, redactKey(key), redact(item, active, depth + 1));
|
|
72
77
|
return out;
|
|
73
78
|
}
|
|
74
79
|
finally {
|
|
@@ -79,18 +84,21 @@ export function redactSecrets(value, secrets) {
|
|
|
79
84
|
}
|
|
80
85
|
export function errorToErrorInfo(error, secrets = []) {
|
|
81
86
|
const code = readErrorCode(error);
|
|
87
|
+
const retry = readRetryAfterMs(error);
|
|
88
|
+
const retryAfter = retry !== undefined ? { retryAfterMs: retry } : {};
|
|
82
89
|
if (error instanceof Error) {
|
|
83
90
|
return {
|
|
84
91
|
name: error.name,
|
|
85
92
|
message: redactSecrets(error.message, secrets),
|
|
86
93
|
code,
|
|
94
|
+
...retryAfter,
|
|
87
95
|
cause: error.cause ? redactSecrets(String(error.cause), secrets) : undefined,
|
|
88
96
|
};
|
|
89
97
|
}
|
|
90
98
|
if (error && typeof error === "object" && "message" in error) {
|
|
91
|
-
return { message: redactSecrets(String(error.message), secrets), code };
|
|
99
|
+
return { message: redactSecrets(String(error.message), secrets), code, ...retryAfter };
|
|
92
100
|
}
|
|
93
|
-
return { message: redactSecrets(String(error), secrets), code };
|
|
101
|
+
return { message: redactSecrets(String(error), secrets), code, ...retryAfter };
|
|
94
102
|
}
|
|
95
103
|
function readErrorCode(error) {
|
|
96
104
|
if (!error || typeof error !== "object" || !("code" in error))
|
|
@@ -98,4 +106,10 @@ function readErrorCode(error) {
|
|
|
98
106
|
const code = error.code;
|
|
99
107
|
return typeof code === "string" || typeof code === "number" ? code : undefined;
|
|
100
108
|
}
|
|
109
|
+
function readRetryAfterMs(error) {
|
|
110
|
+
if (!error || typeof error !== "object" || !("retryAfterMs" in error))
|
|
111
|
+
return undefined;
|
|
112
|
+
const value = error.retryAfterMs;
|
|
113
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
114
|
+
}
|
|
101
115
|
//# sourceMappingURL=redaction.js.map
|
package/dist/retry.d.ts
CHANGED
|
@@ -5,6 +5,11 @@ export interface DefaultRetryPolicyOptions {
|
|
|
5
5
|
readonly baseDelayMs?: number;
|
|
6
6
|
readonly maxDelayMs?: number;
|
|
7
7
|
readonly transientCodes?: readonly (string | number)[];
|
|
8
|
+
/** Symmetric jitter fraction applied to computed delays (default 0.25 → ±25%).
|
|
9
|
+
* Prevents thundering-herd retries under a shared outage. Set 0 to disable. */
|
|
10
|
+
readonly jitter?: number;
|
|
11
|
+
/** Random source for jitter (tests); defaults to Math.random. */
|
|
12
|
+
readonly random?: () => number;
|
|
8
13
|
}
|
|
9
14
|
export declare function createDefaultRetryPolicy(options?: DefaultRetryPolicyOptions): RetryPolicy;
|
|
10
15
|
export declare function isTransientErrorInfo(error: ErrorInfo, transientCodes?: ReadonlySet<string | number>): boolean;
|
package/dist/retry.js
CHANGED
|
@@ -22,13 +22,20 @@ export function createDefaultRetryPolicy(options = {}) {
|
|
|
22
22
|
const maxAttempts = Math.max(1, options.maxAttempts ?? 3);
|
|
23
23
|
const baseDelayMs = Math.max(0, options.baseDelayMs ?? 100);
|
|
24
24
|
const maxDelayMs = Math.max(baseDelayMs, options.maxDelayMs ?? 1000);
|
|
25
|
+
const jitter = Math.min(1, Math.max(0, options.jitter ?? 0.25));
|
|
26
|
+
const random = options.random ?? Math.random;
|
|
25
27
|
const transientCodes = new Set([...TRANSIENT_CODES, ...(options.transientCodes ?? [])]);
|
|
26
28
|
return {
|
|
27
29
|
name: options.name ?? "default-retry",
|
|
28
30
|
decide(context) {
|
|
29
31
|
if (context.signal?.aborted || context.attempt >= maxAttempts || !isTransientErrorInfo(context.error, transientCodes))
|
|
30
32
|
return { retry: false };
|
|
31
|
-
|
|
33
|
+
// Provider backpressure hint (Retry-After) wins over computed backoff, always
|
|
34
|
+
// capped at maxDelayMs so a hostile/huge hint cannot pin a run.
|
|
35
|
+
const hint = context.error.retryAfterMs;
|
|
36
|
+
const base = Math.min(hint !== undefined && Number.isFinite(hint) && hint >= 0 ? hint : baseDelayMs * 2 ** Math.max(0, context.attempt - 1), maxDelayMs);
|
|
37
|
+
const delayMs = jitter > 0 ? Math.round(base * (1 - jitter + random() * 2 * jitter)) : base;
|
|
38
|
+
return { retry: true, delayMs };
|
|
32
39
|
},
|
|
33
40
|
};
|
|
34
41
|
}
|
package/dist/run-ledger.d.ts
CHANGED
|
@@ -6,9 +6,15 @@ export declare const HARD_LEDGER_BATCH_BYTES: number;
|
|
|
6
6
|
export declare const DEFAULT_LEDGER_BATCH_DELAY_MS = 25;
|
|
7
7
|
export declare const HARD_LEDGER_BATCH_DELAY_MS = 60000;
|
|
8
8
|
export interface BatchedRunLedgerOptions {
|
|
9
|
+
/** Flush triggers, not write grouping: a pending flush fires once the buffer reaches
|
|
10
|
+
* this many entries. Writes still go to the target one at a time (RunLedger has no
|
|
11
|
+
* batch append); these bounds shape flush *timing*, not per-write coalescing. */
|
|
9
12
|
readonly maxBatchEntries?: number;
|
|
13
|
+
/** Flush trigger: pending flush fires once buffered bytes reach this bound. */
|
|
10
14
|
readonly maxBatchBytes?: number;
|
|
15
|
+
/** Backpressure bound: enqueue flushes synchronously before exceeding this. */
|
|
11
16
|
readonly maxBufferedEntries?: number;
|
|
17
|
+
/** Backpressure bound (bytes): enqueue flushes synchronously before exceeding this. */
|
|
12
18
|
readonly maxBufferedBytes?: number;
|
|
13
19
|
readonly maxDelayMs?: number;
|
|
14
20
|
readonly durability?: RunLedgerDurability;
|
package/dist/run-ledger.js
CHANGED
|
@@ -58,20 +58,14 @@ export function createBatchedRunLedger(target, options = {}) {
|
|
|
58
58
|
const flush = () => {
|
|
59
59
|
cancelTimer();
|
|
60
60
|
const operation = flushChain.then(async () => {
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
// One write per record — RunLedger has no batch append API. maxBatchEntries/
|
|
62
|
+
// maxBatchBytes only decide when a flush fires (see enqueue), never how writes group.
|
|
63
63
|
while (queue.length) {
|
|
64
64
|
const item = queue[0];
|
|
65
|
-
|
|
66
|
-
entries = 0;
|
|
67
|
-
bytes = 0;
|
|
68
|
-
}
|
|
69
|
-
await write(item);
|
|
65
|
+
await write(item); // shifts only after success — a failed write stays buffered for retry
|
|
70
66
|
queue.shift();
|
|
71
67
|
bufferedBytes -= item.bytes;
|
|
72
68
|
flushed += 1;
|
|
73
|
-
entries += 1;
|
|
74
|
-
bytes += item.bytes;
|
|
75
69
|
}
|
|
76
70
|
return status();
|
|
77
71
|
});
|