pi-provider-utils 0.0.0
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/LICENSE +21 -0
- package/README.md +155 -0
- package/package.json +65 -0
- package/src/__tests__/agent-paths.test.ts +255 -0
- package/src/__tests__/providers.test.ts +187 -0
- package/src/__tests__/streams.test.ts +345 -0
- package/src/agent-paths.ts +148 -0
- package/src/providers.ts +95 -0
- package/src/streams.ts +180 -0
package/src/streams.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stream and error primitives for extension-owned provider wrappers.
|
|
3
|
+
*
|
|
4
|
+
* Both pi-credential-vault and pi-multicodex build custom streamSimple
|
|
5
|
+
* implementations that resolve credentials, forward events, and surface
|
|
6
|
+
* errors as AssistantMessage events. This module provides the shared
|
|
7
|
+
* building blocks so each package only owns its domain-specific logic.
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
type Api,
|
|
11
|
+
type AssistantMessage,
|
|
12
|
+
type AssistantMessageEvent,
|
|
13
|
+
type AssistantMessageEventStream,
|
|
14
|
+
createAssistantMessageEventStream,
|
|
15
|
+
type Model,
|
|
16
|
+
} from "@mariozechner/pi-ai";
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Error normalization
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Normalize an unknown thrown value into a human-readable error string.
|
|
24
|
+
*
|
|
25
|
+
* Handles Error instances, plain strings, and arbitrary values.
|
|
26
|
+
*/
|
|
27
|
+
export function normalizeUnknownError(error: unknown): string {
|
|
28
|
+
if (error instanceof Error) return error.message;
|
|
29
|
+
if (typeof error === "string") return error;
|
|
30
|
+
return JSON.stringify(error);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// AssistantMessage construction
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build an error AssistantMessage for a model.
|
|
39
|
+
*
|
|
40
|
+
* Produces a zero-usage message with `stopReason: "error"` that can be
|
|
41
|
+
* used inside error events on an AssistantMessageEventStream.
|
|
42
|
+
*/
|
|
43
|
+
export function createErrorAssistantMessage(
|
|
44
|
+
model: Model<Api>,
|
|
45
|
+
message: string,
|
|
46
|
+
): AssistantMessage {
|
|
47
|
+
return {
|
|
48
|
+
role: "assistant" as const,
|
|
49
|
+
content: [],
|
|
50
|
+
api: model.api,
|
|
51
|
+
provider: model.provider,
|
|
52
|
+
model: model.id,
|
|
53
|
+
usage: {
|
|
54
|
+
input: 0,
|
|
55
|
+
output: 0,
|
|
56
|
+
cacheRead: 0,
|
|
57
|
+
cacheWrite: 0,
|
|
58
|
+
totalTokens: 0,
|
|
59
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
60
|
+
},
|
|
61
|
+
stopReason: "error" as const,
|
|
62
|
+
errorMessage: message,
|
|
63
|
+
timestamp: Date.now(),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Stream helpers
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Push an error event into an AssistantMessageEventStream.
|
|
73
|
+
*/
|
|
74
|
+
export function pushErrorEvent(
|
|
75
|
+
stream: AssistantMessageEventStream,
|
|
76
|
+
model: Model<Api>,
|
|
77
|
+
message: string,
|
|
78
|
+
): void {
|
|
79
|
+
stream.push({
|
|
80
|
+
type: "error",
|
|
81
|
+
reason: "error",
|
|
82
|
+
error: createErrorAssistantMessage(model, message),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Create a stream that immediately emits a single error event.
|
|
88
|
+
*
|
|
89
|
+
* Useful when an extension detects a problem before starting the real
|
|
90
|
+
* provider request (e.g., missing credentials or unavailable backend).
|
|
91
|
+
*/
|
|
92
|
+
export function createImmediateErrorStream(
|
|
93
|
+
model: Model<Api>,
|
|
94
|
+
message: string,
|
|
95
|
+
): AssistantMessageEventStream {
|
|
96
|
+
const stream = createAssistantMessageEventStream();
|
|
97
|
+
pushErrorEvent(stream, model, message);
|
|
98
|
+
return stream;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Pipe all events from a source stream into a target stream, then end it.
|
|
103
|
+
*
|
|
104
|
+
* push() marks the target as done when it sees a terminal event (done/error),
|
|
105
|
+
* which prevents further pushes. end() is still needed to flush any
|
|
106
|
+
* consumers waiting on the target's async iterator after the last event.
|
|
107
|
+
*/
|
|
108
|
+
export async function pipeAssistantStream(
|
|
109
|
+
source: AssistantMessageEventStream,
|
|
110
|
+
target: AssistantMessageEventStream,
|
|
111
|
+
): Promise<void> {
|
|
112
|
+
for await (const event of source) {
|
|
113
|
+
target.push(event);
|
|
114
|
+
}
|
|
115
|
+
target.end();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Rewrite the provider field on an AssistantMessageEvent.
|
|
120
|
+
*
|
|
121
|
+
* Extensions that re-register a provider under a different ID use this
|
|
122
|
+
* to ensure downstream consumers see the extension's provider ID instead
|
|
123
|
+
* of the internal one used for the actual API call.
|
|
124
|
+
*/
|
|
125
|
+
export function rewriteProviderOnEvent(
|
|
126
|
+
event: AssistantMessageEvent,
|
|
127
|
+
provider: string,
|
|
128
|
+
): AssistantMessageEvent {
|
|
129
|
+
if ("partial" in event) {
|
|
130
|
+
return { ...event, partial: { ...event.partial, provider } };
|
|
131
|
+
}
|
|
132
|
+
if (event.type === "done") {
|
|
133
|
+
return { ...event, message: { ...event.message, provider } };
|
|
134
|
+
}
|
|
135
|
+
if (event.type === "error") {
|
|
136
|
+
return { ...event, error: { ...event.error, provider } };
|
|
137
|
+
}
|
|
138
|
+
return event;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// Abort controller helpers
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Create an AbortController that aborts when the given signal fires.
|
|
147
|
+
*
|
|
148
|
+
* Returns a new controller whose signal can be passed to provider calls.
|
|
149
|
+
* Aborting the returned controller does not abort the parent signal.
|
|
150
|
+
*/
|
|
151
|
+
export function createLinkedAbortController(
|
|
152
|
+
signal?: AbortSignal,
|
|
153
|
+
): AbortController {
|
|
154
|
+
const controller = new AbortController();
|
|
155
|
+
if (signal?.aborted) {
|
|
156
|
+
controller.abort();
|
|
157
|
+
return controller;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
signal?.addEventListener("abort", () => controller.abort(), { once: true });
|
|
161
|
+
return controller;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Create a linked AbortController that auto-aborts after a timeout.
|
|
166
|
+
*
|
|
167
|
+
* Returns the controller and a `clear()` function that cancels the timer.
|
|
168
|
+
* Always call `clear()` when the operation finishes to prevent leaks.
|
|
169
|
+
*/
|
|
170
|
+
export function createTimeoutController(
|
|
171
|
+
signal: AbortSignal | undefined,
|
|
172
|
+
timeoutMs: number,
|
|
173
|
+
): { controller: AbortController; clear: () => void } {
|
|
174
|
+
const controller = createLinkedAbortController(signal);
|
|
175
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
176
|
+
return {
|
|
177
|
+
controller,
|
|
178
|
+
clear: () => clearTimeout(timeout),
|
|
179
|
+
};
|
|
180
|
+
}
|