@arnilo/prism 0.0.16 → 0.0.18
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 +33 -0
- package/README.md +9 -2
- 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 +10 -7
- 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 +12 -5
- 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/0.1.0-readiness.md +35 -21
- package/docs/agent-events.md +2 -1
- package/docs/agent-session-runtime.md +3 -3
- package/docs/cli-rpc.md +1 -5
- package/docs/coding-agent-tools.md +10 -6
- 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 -4
- package/docs/input-and-prompt-assembly.md +6 -8
- package/docs/mcp-tools.md +3 -3
- package/docs/middleware-hooks.md +2 -2
- package/docs/migration.md +24 -1
- package/docs/provider-caching.md +1 -1
- package/docs/provider-conformance.md +1 -1
- package/docs/provider-packages.md +1 -1
- package/docs/providers/ai-sdk.md +2 -1
- package/docs/providers/openai-compatible.md +28 -1
- package/docs/public-contracts.md +2 -2
- package/docs/release-and-install.md +59 -17
- package/docs/session-stores.md +1 -1
- package/package.json +1 -1
package/dist/extensions.js
CHANGED
|
@@ -45,90 +45,148 @@ export function createExtensionKernel(options = {}) {
|
|
|
45
45
|
const middleware = options.middleware ?? createMiddlewareRegistry({ ...options, onError: events.emit });
|
|
46
46
|
const errorPolicy = options.errorPolicy ?? "event";
|
|
47
47
|
const secrets = options.secrets ?? [];
|
|
48
|
-
|
|
48
|
+
// Per-extension tracked API: every registration records an undo so a dispose handle
|
|
49
|
+
// (or a failed setup) can unwind exactly what that extension added.
|
|
50
|
+
const createApi = (track) => ({
|
|
49
51
|
registries,
|
|
50
52
|
middleware,
|
|
51
|
-
on
|
|
53
|
+
on(type, handler) {
|
|
54
|
+
const off = events.on(type, handler);
|
|
55
|
+
track?.(off);
|
|
56
|
+
return off;
|
|
57
|
+
},
|
|
52
58
|
emit: events.emit,
|
|
53
|
-
use
|
|
59
|
+
use(hook, mw) {
|
|
60
|
+
const off = middleware.use(hook, mw);
|
|
61
|
+
track?.(off);
|
|
62
|
+
return off;
|
|
63
|
+
},
|
|
54
64
|
registerProvider(provider) {
|
|
55
65
|
registries.providers.register(provider);
|
|
66
|
+
track?.(() => registries.providers.unregister(provider.id));
|
|
56
67
|
},
|
|
57
68
|
registerModel(model) {
|
|
58
69
|
registries.models.register(model);
|
|
70
|
+
track?.(() => registries.models.unregister(model.provider, model.model));
|
|
59
71
|
},
|
|
60
72
|
registerTool(tool) {
|
|
61
73
|
registries.tools.register(tool.name, tool);
|
|
74
|
+
track?.(() => registries.tools.unregister(tool.name));
|
|
62
75
|
},
|
|
63
76
|
registerContextProvider(provider) {
|
|
64
77
|
registries.contextProviders.register(provider.name, provider);
|
|
78
|
+
track?.(() => registries.contextProviders.unregister(provider.name));
|
|
65
79
|
},
|
|
66
80
|
registerSkill(skill) {
|
|
67
81
|
registries.skills.register(skill.name, skill);
|
|
82
|
+
track?.(() => registries.skills.unregister(skill.name));
|
|
68
83
|
},
|
|
69
84
|
registerCommand(command) {
|
|
70
85
|
registries.commands.register(command.name, command);
|
|
86
|
+
track?.(() => registries.commands.unregister(command.name));
|
|
71
87
|
},
|
|
72
88
|
registerAgent(agent) {
|
|
73
89
|
registries.agents.register(agent.name, agent);
|
|
90
|
+
track?.(() => registries.agents.unregister(agent.name));
|
|
74
91
|
},
|
|
75
92
|
registerInputBuilder(builder) {
|
|
76
93
|
registries.inputBuilders.register(builder.name, builder);
|
|
94
|
+
track?.(() => registries.inputBuilders.unregister(builder.name));
|
|
77
95
|
},
|
|
78
96
|
registerPromptBuilder(builder) {
|
|
79
97
|
registries.promptBuilders.register(builder.name, builder);
|
|
98
|
+
track?.(() => registries.promptBuilders.unregister(builder.name));
|
|
80
99
|
},
|
|
81
100
|
registerCompactionStrategy(strategy) {
|
|
82
101
|
registries.compactionStrategies.register(strategy.name, strategy);
|
|
102
|
+
track?.(() => registries.compactionStrategies.unregister(strategy.name));
|
|
83
103
|
},
|
|
84
104
|
registerRetryPolicy(policy) {
|
|
85
105
|
registries.retryPolicies.register(policy.name, policy);
|
|
106
|
+
track?.(() => registries.retryPolicies.unregister(policy.name));
|
|
86
107
|
},
|
|
87
108
|
registerStoreFactory(factory) {
|
|
88
109
|
registries.storeFactories.register(factory.name, factory);
|
|
110
|
+
track?.(() => registries.storeFactories.unregister(factory.name));
|
|
89
111
|
},
|
|
90
112
|
registerResourceLoader(key, loader) {
|
|
91
113
|
registries.resourceLoaders.register(key, loader);
|
|
114
|
+
track?.(() => registries.resourceLoaders.unregister(key));
|
|
92
115
|
},
|
|
93
116
|
registerSettingsProvider(key, provider) {
|
|
94
117
|
registries.settingsProviders.register(key, provider);
|
|
118
|
+
track?.(() => registries.settingsProviders.unregister(key));
|
|
95
119
|
},
|
|
96
120
|
registerCredentialResolver(key, resolver) {
|
|
97
121
|
registries.credentialResolvers.register(key, resolver);
|
|
122
|
+
track?.(() => registries.credentialResolvers.unregister(key));
|
|
98
123
|
},
|
|
99
124
|
registerProviderPackage(providerPackage) {
|
|
100
125
|
registries.providerPackages.register(providerPackage.name, providerPackage);
|
|
126
|
+
track?.(() => registries.providerPackages.unregister(providerPackage.name));
|
|
101
127
|
},
|
|
102
128
|
registerAuthMethod(method) {
|
|
103
|
-
|
|
129
|
+
const key = authMethodKey(method);
|
|
130
|
+
registries.authMethods.register(key, method);
|
|
131
|
+
track?.(() => registries.authMethods.unregister(key));
|
|
104
132
|
},
|
|
105
133
|
registerProviderRequestPolicy(policy) {
|
|
106
134
|
registries.providerRequestPolicies.register(policy.name, policy);
|
|
135
|
+
track?.(() => registries.providerRequestPolicies.unregister(policy.name));
|
|
107
136
|
},
|
|
108
137
|
registerSystemPromptContribution(contribution) {
|
|
109
|
-
|
|
138
|
+
const key = systemPromptContributionKey(contribution);
|
|
139
|
+
registries.systemPromptContributions.register(key, contribution);
|
|
140
|
+
track?.(() => registries.systemPromptContributions.unregister(key));
|
|
110
141
|
},
|
|
111
142
|
registerInstructionInjector(injector) {
|
|
112
143
|
registries.instructionInjectors.register(injector.name, injector);
|
|
144
|
+
track?.(() => registries.instructionInjectors.unregister(injector.name));
|
|
113
145
|
},
|
|
146
|
+
});
|
|
147
|
+
const unwind = (undo) => {
|
|
148
|
+
for (const fn of undo.reverse()) {
|
|
149
|
+
try {
|
|
150
|
+
fn();
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// best-effort: one stuck undo must not block the rest
|
|
154
|
+
}
|
|
155
|
+
}
|
|
114
156
|
};
|
|
115
157
|
return {
|
|
116
158
|
registries,
|
|
117
159
|
middleware,
|
|
118
160
|
events,
|
|
119
161
|
async load(extensions) {
|
|
162
|
+
const loaded = [];
|
|
120
163
|
for (const extension of extensions) {
|
|
164
|
+
const undo = [];
|
|
121
165
|
try {
|
|
122
166
|
await assertPermission(options.permission, { kind: "extension", action: "setup", target: extension.name });
|
|
123
167
|
await assertExtensionLoadPolicy(options.loadPolicy, extension);
|
|
124
|
-
await extension.setup(
|
|
168
|
+
await extension.setup(createApi((fn) => undo.push(fn)));
|
|
125
169
|
}
|
|
126
170
|
catch (error) {
|
|
171
|
+
// A failed setup must not leave partial contributions behind.
|
|
172
|
+
unwind(undo);
|
|
127
173
|
if (errorPolicy === "throw")
|
|
128
174
|
throw error;
|
|
129
175
|
await events.emit(extensionError(error, extension.name, secrets));
|
|
176
|
+
continue;
|
|
130
177
|
}
|
|
178
|
+
let disposed = false;
|
|
179
|
+
loaded.push({
|
|
180
|
+
name: extension.name,
|
|
181
|
+
dispose() {
|
|
182
|
+
if (disposed)
|
|
183
|
+
return;
|
|
184
|
+
disposed = true;
|
|
185
|
+
unwind(undo);
|
|
186
|
+
},
|
|
187
|
+
});
|
|
131
188
|
}
|
|
189
|
+
return loaded;
|
|
132
190
|
},
|
|
133
191
|
};
|
|
134
192
|
}
|
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.18";
|
|
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.18";
|
|
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 ?? "cache_aware");
|
|
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
|
}
|
|
@@ -76,7 +80,7 @@ export async function assembleProviderInput(options) {
|
|
|
76
80
|
? composeSystemPrompt(injectorContribs.instructions, { base: options.systemInstructions })
|
|
77
81
|
: options.systemInstructions;
|
|
78
82
|
const buildContext = { ...options, ...baseContext, systemInstructions };
|
|
79
|
-
const layout = buildContext.inputLayout ?? "
|
|
83
|
+
const layout = buildContext.inputLayout ?? "cache_aware";
|
|
80
84
|
let messages;
|
|
81
85
|
let context;
|
|
82
86
|
let skills = options.skills;
|
|
@@ -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
|
},
|