@graphorin/provider 0.6.0 → 0.7.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/CHANGELOG.md +42 -0
- package/README.md +8 -3
- package/dist/adapters/llamacpp-server.d.ts +1 -1
- package/dist/adapters/llamacpp-server.js.map +1 -1
- package/dist/adapters/ollama.d.ts.map +1 -1
- package/dist/adapters/ollama.js +19 -6
- package/dist/adapters/ollama.js.map +1 -1
- package/dist/adapters/vercel.d.ts +1 -1
- package/dist/adapters/vercel.d.ts.map +1 -1
- package/dist/adapters/vercel.js +111 -33
- package/dist/adapters/vercel.js.map +1 -1
- package/dist/errors/errors.d.ts +1 -1
- package/dist/errors/errors.js +1 -1
- package/dist/errors/errors.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -8
- package/dist/index.js.map +1 -1
- package/dist/internal/http.js +111 -7
- package/dist/internal/http.js.map +1 -1
- package/dist/internal/openai-shaped.js +21 -4
- package/dist/internal/openai-shaped.js.map +1 -1
- package/dist/middleware/with-fallback.js +1 -1
- package/dist/middleware/with-rate-limit.d.ts +21 -8
- package/dist/middleware/with-rate-limit.d.ts.map +1 -1
- package/dist/middleware/with-rate-limit.js +65 -12
- package/dist/middleware/with-rate-limit.js.map +1 -1
- package/dist/middleware/with-retry.js +1 -1
- package/dist/package.js +6 -0
- package/dist/package.js.map +1 -0
- package/package.json +18 -16
- package/src/adapters/index.ts +14 -0
- package/src/adapters/llamacpp-server.ts +102 -0
- package/src/adapters/ollama.ts +382 -0
- package/src/adapters/openai-compatible.ts +95 -0
- package/src/adapters/vercel-messages.ts +308 -0
- package/src/adapters/vercel.ts +706 -0
- package/src/counters/anthropic-wire.ts +199 -0
- package/src/counters/anthropic.ts +114 -0
- package/src/counters/bedrock.ts +46 -0
- package/src/counters/dispatcher.ts +127 -0
- package/src/counters/global.ts +39 -0
- package/src/counters/google.ts +46 -0
- package/src/counters/heuristic.ts +107 -0
- package/src/counters/index.ts +35 -0
- package/src/counters/js-tiktoken.ts +135 -0
- package/src/counters/serialize.ts +85 -0
- package/src/errors/errors.ts +316 -0
- package/src/errors/index.ts +20 -0
- package/src/index.ts +42 -0
- package/src/internal/abort.ts +30 -0
- package/src/internal/http.ts +388 -0
- package/src/internal/openai-shaped.ts +555 -0
- package/src/internal/sse.ts +112 -0
- package/src/internal/url-utils.ts +20 -0
- package/src/middleware/compose.ts +213 -0
- package/src/middleware/index.ts +37 -0
- package/src/middleware/production-hook.ts +47 -0
- package/src/middleware/with-cost-limit.ts +131 -0
- package/src/middleware/with-cost-tracking.ts +216 -0
- package/src/middleware/with-fallback.ts +157 -0
- package/src/middleware/with-rate-limit.ts +306 -0
- package/src/middleware/with-redaction.ts +671 -0
- package/src/middleware/with-retry.ts +274 -0
- package/src/middleware/with-tracing.ts +117 -0
- package/src/model-tier/classify.ts +125 -0
- package/src/model-tier/index.ts +11 -0
- package/src/provider.ts +121 -0
- package/src/reasoning/apply-policy.ts +89 -0
- package/src/reasoning/classify-contract.ts +120 -0
- package/src/reasoning/index.ts +21 -0
- package/src/reasoning/retention.ts +64 -0
- package/src/tool-examples.ts +54 -0
- package/src/trust/classify-local-provider.ts +254 -0
- package/src/trust/index.ts +14 -0
- package/dist/adapters/index.js +0 -6
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal HTTP helpers shared across the local-LLM adapters.
|
|
3
|
+
*
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ProviderEvent } from '@graphorin/core';
|
|
8
|
+
|
|
9
|
+
import { ProviderHttpError } from '../errors/errors.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* W-095: conversion options for the local-adapter message converters.
|
|
13
|
+
* `multimodal` mirrors the adapter instance's effective
|
|
14
|
+
* `capabilities.multimodal`; `warn` receives ONE message per convert
|
|
15
|
+
* call when parts were dropped (adapters dedupe it to once per
|
|
16
|
+
* instance).
|
|
17
|
+
*
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
export interface ChatMessageConversionOptions {
|
|
21
|
+
readonly multimodal: boolean;
|
|
22
|
+
readonly warn?: (message: string) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const TEXT_ONLY: ChatMessageConversionOptions = { multimodal: false };
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Convert a graphorin `Message` to the OpenAI-compatible chat-completion
|
|
29
|
+
* shape. The shape is the lingua-franca of the bundled local adapters
|
|
30
|
+
* (`llamaCppServerAdapter` and `openAICompatibleAdapter`); the
|
|
31
|
+
* native-Ollama path uses its own conversion.
|
|
32
|
+
*
|
|
33
|
+
* W-095: with `opts.multimodal === true` image parts are emitted as
|
|
34
|
+
* OpenAI `image_url` content parts (bytes as a data URI, `URL`s passed
|
|
35
|
+
* through as strings - the server dereferences; this adapter never
|
|
36
|
+
* fetches). Audio/file parts have no portable wire form on
|
|
37
|
+
* OpenAI-compatible servers and are dropped LOUDLY. With
|
|
38
|
+
* `multimodal: false` (the default) content flattens to a plain string
|
|
39
|
+
* exactly as before, and any dropped non-text part triggers the warn.
|
|
40
|
+
*
|
|
41
|
+
* @internal
|
|
42
|
+
*/
|
|
43
|
+
export function toOpenAIChatMessages(
|
|
44
|
+
messages: ReadonlyArray<{
|
|
45
|
+
readonly role: 'system' | 'user' | 'assistant' | 'tool';
|
|
46
|
+
readonly content: string | ReadonlyArray<unknown>;
|
|
47
|
+
readonly toolCalls?: ReadonlyArray<{
|
|
48
|
+
readonly toolCallId: string;
|
|
49
|
+
readonly toolName: string;
|
|
50
|
+
readonly args: unknown;
|
|
51
|
+
}>;
|
|
52
|
+
readonly toolCallId?: string;
|
|
53
|
+
}>,
|
|
54
|
+
opts: ChatMessageConversionOptions = TEXT_ONLY,
|
|
55
|
+
): ReadonlyArray<Record<string, unknown>> {
|
|
56
|
+
const dropped = new Set<string>();
|
|
57
|
+
const converted = messages.map((msg) => {
|
|
58
|
+
const out: Record<string, unknown> = {
|
|
59
|
+
role: msg.role,
|
|
60
|
+
content:
|
|
61
|
+
typeof msg.content === 'string'
|
|
62
|
+
? msg.content
|
|
63
|
+
: opts.multimodal
|
|
64
|
+
? toOpenAIParts(msg.content, dropped)
|
|
65
|
+
: flattenContent(msg.content, dropped),
|
|
66
|
+
};
|
|
67
|
+
if (msg.toolCalls !== undefined && msg.toolCalls.length > 0) {
|
|
68
|
+
out.tool_calls = msg.toolCalls.map((tc) => ({
|
|
69
|
+
id: tc.toolCallId,
|
|
70
|
+
type: 'function',
|
|
71
|
+
function: {
|
|
72
|
+
name: tc.toolName,
|
|
73
|
+
arguments: typeof tc.args === 'string' ? tc.args : JSON.stringify(tc.args),
|
|
74
|
+
},
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
if (msg.role === 'tool' && msg.toolCallId !== undefined) {
|
|
78
|
+
out.tool_call_id = msg.toolCallId;
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
});
|
|
82
|
+
warnDropped(opts, dropped);
|
|
83
|
+
return converted;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** W-095: build the OpenAI `content` parts array for a vision model. */
|
|
87
|
+
function toOpenAIParts(
|
|
88
|
+
parts: ReadonlyArray<unknown>,
|
|
89
|
+
dropped: Set<string>,
|
|
90
|
+
): ReadonlyArray<Record<string, unknown>> {
|
|
91
|
+
const out: Record<string, unknown>[] = [];
|
|
92
|
+
for (const part of parts) {
|
|
93
|
+
if (typeof part === 'string') {
|
|
94
|
+
out.push({ type: 'text', text: part });
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (typeof part !== 'object' || part === null) continue;
|
|
98
|
+
const obj = part as { type?: string; text?: string; image?: unknown; mimeType?: string };
|
|
99
|
+
if (obj.type === 'text' && typeof obj.text === 'string') {
|
|
100
|
+
out.push({ type: 'text', text: obj.text });
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (obj.type === 'image') {
|
|
104
|
+
const url = imageToUrl(obj.image, obj.mimeType);
|
|
105
|
+
if (url !== undefined) {
|
|
106
|
+
out.push({ type: 'image_url', image_url: { url } });
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (typeof obj.type === 'string') dropped.add(obj.type);
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* W-095: bytes become a `data:` URI (default mime `image/png`); `URL`s
|
|
117
|
+
* pass through as strings - the SERVER dereferences, the adapter makes
|
|
118
|
+
* no network call of its own.
|
|
119
|
+
*/
|
|
120
|
+
function imageToUrl(image: unknown, mimeType?: string): string | undefined {
|
|
121
|
+
if (image instanceof URL) return image.toString();
|
|
122
|
+
if (image instanceof Uint8Array) {
|
|
123
|
+
const mime = mimeType ?? 'image/png';
|
|
124
|
+
return `data:${mime};base64,${Buffer.from(image).toString('base64')}`;
|
|
125
|
+
}
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** W-095: one honest WARN per convert call when parts were dropped. */
|
|
130
|
+
function warnDropped(opts: ChatMessageConversionOptions, dropped: Set<string>): void {
|
|
131
|
+
if (dropped.size === 0 || opts.warn === undefined) return;
|
|
132
|
+
const kinds = [...dropped].sort().join(', ');
|
|
133
|
+
opts.warn(
|
|
134
|
+
opts.multimodal
|
|
135
|
+
? `content parts of kind [${kinds}] have no wire mapping on this adapter and were dropped`
|
|
136
|
+
: `non-text content parts of kind [${kinds}] were dropped - this adapter instance has capabilities.multimodal=false; pass capabilities: { multimodal: true } if the model supports vision`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Convert a graphorin `Message` to Ollama's **native** `/api/chat` shape
|
|
142
|
+
* (PS-13). Unlike the OpenAI form, Ollama's Go server expects `tool_calls`
|
|
143
|
+
* with object `arguments` (a `map[string]any`, never a JSON string) and no
|
|
144
|
+
* `id` / `type` fields - sending those breaks its unmarshaller and any
|
|
145
|
+
* multi-turn replay of assistant tool calls.
|
|
146
|
+
*
|
|
147
|
+
* @internal
|
|
148
|
+
*/
|
|
149
|
+
export function toOllamaChatMessages(
|
|
150
|
+
messages: ReadonlyArray<{
|
|
151
|
+
readonly role: 'system' | 'user' | 'assistant' | 'tool';
|
|
152
|
+
readonly content: string | ReadonlyArray<unknown>;
|
|
153
|
+
readonly toolCalls?: ReadonlyArray<{
|
|
154
|
+
readonly toolCallId: string;
|
|
155
|
+
readonly toolName: string;
|
|
156
|
+
readonly args: unknown;
|
|
157
|
+
}>;
|
|
158
|
+
readonly toolCallId?: string;
|
|
159
|
+
}>,
|
|
160
|
+
opts: ChatMessageConversionOptions = TEXT_ONLY,
|
|
161
|
+
): ReadonlyArray<Record<string, unknown>> {
|
|
162
|
+
const dropped = new Set<string>();
|
|
163
|
+
const converted = messages.map((msg) => {
|
|
164
|
+
let content: string;
|
|
165
|
+
let images: string[] | undefined;
|
|
166
|
+
if (typeof msg.content === 'string') {
|
|
167
|
+
content = msg.content;
|
|
168
|
+
} else if (opts.multimodal) {
|
|
169
|
+
// W-095: Ollama's native API takes a per-message `images` array
|
|
170
|
+
// of RAW base64 strings (no data: prefix). URL images cannot be
|
|
171
|
+
// inlined on this path (the adapter never fetches) - dropped
|
|
172
|
+
// loudly.
|
|
173
|
+
const split = toOllamaContent(msg.content, dropped);
|
|
174
|
+
content = split.text;
|
|
175
|
+
images = split.images.length > 0 ? split.images : undefined;
|
|
176
|
+
} else {
|
|
177
|
+
content = flattenContent(msg.content, dropped);
|
|
178
|
+
}
|
|
179
|
+
const out: Record<string, unknown> = { role: msg.role, content };
|
|
180
|
+
if (images !== undefined) out.images = images;
|
|
181
|
+
if (msg.toolCalls !== undefined && msg.toolCalls.length > 0) {
|
|
182
|
+
out.tool_calls = msg.toolCalls.map((tc) => ({
|
|
183
|
+
function: {
|
|
184
|
+
name: tc.toolName,
|
|
185
|
+
arguments: toArgsObject(tc.args),
|
|
186
|
+
},
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
return out;
|
|
190
|
+
});
|
|
191
|
+
warnDropped(opts, dropped);
|
|
192
|
+
return converted;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** W-095: split parts into flattened text + raw-base64 image payloads. */
|
|
196
|
+
function toOllamaContent(
|
|
197
|
+
parts: ReadonlyArray<unknown>,
|
|
198
|
+
dropped: Set<string>,
|
|
199
|
+
): { readonly text: string; readonly images: string[] } {
|
|
200
|
+
const buffer: string[] = [];
|
|
201
|
+
const images: string[] = [];
|
|
202
|
+
for (const part of parts) {
|
|
203
|
+
if (typeof part === 'string') {
|
|
204
|
+
buffer.push(part);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (typeof part !== 'object' || part === null) continue;
|
|
208
|
+
const obj = part as { type?: string; text?: string; image?: unknown };
|
|
209
|
+
if (obj.type === 'text' && typeof obj.text === 'string') {
|
|
210
|
+
buffer.push(obj.text);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (obj.type === 'image' && obj.image instanceof Uint8Array) {
|
|
214
|
+
images.push(Buffer.from(obj.image).toString('base64'));
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (obj.type === 'image') {
|
|
218
|
+
dropped.add('image (URL - the native Ollama API needs inline bytes)');
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (typeof obj.type === 'string') dropped.add(obj.type);
|
|
222
|
+
}
|
|
223
|
+
return { text: buffer.join(''), images };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Coerce a tool-call `args` value into the object map Ollama expects. Strings
|
|
228
|
+
* (e.g. an OpenAI-style JSON blob produced upstream) are parsed leniently;
|
|
229
|
+
* anything that isn't a JSON object becomes `{}`.
|
|
230
|
+
*/
|
|
231
|
+
function toArgsObject(args: unknown): Record<string, unknown> {
|
|
232
|
+
if (typeof args === 'string') {
|
|
233
|
+
try {
|
|
234
|
+
const parsed: unknown = JSON.parse(args);
|
|
235
|
+
return typeof parsed === 'object' && parsed !== null
|
|
236
|
+
? (parsed as Record<string, unknown>)
|
|
237
|
+
: {};
|
|
238
|
+
} catch {
|
|
239
|
+
return {};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return typeof args === 'object' && args !== null ? (args as Record<string, unknown>) : {};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function flattenContent(parts: ReadonlyArray<unknown>, dropped?: Set<string>): string {
|
|
246
|
+
const buffer: string[] = [];
|
|
247
|
+
for (const part of parts) {
|
|
248
|
+
if (typeof part === 'string') {
|
|
249
|
+
buffer.push(part);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (typeof part === 'object' && part !== null) {
|
|
253
|
+
const obj = part as { type?: string; text?: string };
|
|
254
|
+
if (obj.type === 'text' && typeof obj.text === 'string') {
|
|
255
|
+
buffer.push(obj.text);
|
|
256
|
+
} else if (typeof obj.type === 'string') {
|
|
257
|
+
// W-095: silently vanishing multimodal content was the bug -
|
|
258
|
+
// collect the kind so the caller can WARN once.
|
|
259
|
+
dropped?.add(obj.type);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return buffer.join('');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Wrap a `fetch` call with HTTP error mapping. The helper does not
|
|
268
|
+
* assume any particular streaming format - callers receive the raw
|
|
269
|
+
* `Response` and dispatch on its body.
|
|
270
|
+
*
|
|
271
|
+
* @internal
|
|
272
|
+
*/
|
|
273
|
+
/**
|
|
274
|
+
* Default per-request timeout for the baseUrl adapters (PS-24). Scoped
|
|
275
|
+
* to time-to-response (headers): the timer is cleared the moment the
|
|
276
|
+
* server answers, so long streaming bodies are never killed - only a
|
|
277
|
+
* hung server that never responds. Generous because a cold local
|
|
278
|
+
* llama-server can take tens of seconds to load a model.
|
|
279
|
+
*
|
|
280
|
+
* @stable
|
|
281
|
+
*/
|
|
282
|
+
export const DEFAULT_REQUEST_TIMEOUT_MS = 120_000;
|
|
283
|
+
|
|
284
|
+
export async function callJsonHttp(args: {
|
|
285
|
+
readonly providerName: string;
|
|
286
|
+
readonly url: string;
|
|
287
|
+
readonly headers: Record<string, string>;
|
|
288
|
+
readonly body: unknown;
|
|
289
|
+
readonly signal?: AbortSignal;
|
|
290
|
+
readonly fetchImpl?: typeof fetch;
|
|
291
|
+
/**
|
|
292
|
+
* Time-to-response budget (PS-24). Default
|
|
293
|
+
* {@link DEFAULT_REQUEST_TIMEOUT_MS}; `0` disables.
|
|
294
|
+
*/
|
|
295
|
+
readonly timeoutMs?: number;
|
|
296
|
+
}): Promise<Response> {
|
|
297
|
+
const fetchImpl = args.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
298
|
+
const timeoutMs = args.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
299
|
+
const timeoutCtl = timeoutMs > 0 ? new AbortController() : undefined;
|
|
300
|
+
const timer =
|
|
301
|
+
timeoutCtl !== undefined ? setTimeout(() => timeoutCtl.abort(), timeoutMs) : undefined;
|
|
302
|
+
const signal =
|
|
303
|
+
args.signal !== undefined && timeoutCtl !== undefined
|
|
304
|
+
? AbortSignal.any([args.signal, timeoutCtl.signal])
|
|
305
|
+
: (args.signal ?? timeoutCtl?.signal);
|
|
306
|
+
let resp: Response;
|
|
307
|
+
try {
|
|
308
|
+
resp = await fetchImpl(args.url, {
|
|
309
|
+
method: 'POST',
|
|
310
|
+
headers: args.headers,
|
|
311
|
+
body: JSON.stringify(args.body),
|
|
312
|
+
...(signal !== undefined ? { signal } : {}),
|
|
313
|
+
});
|
|
314
|
+
} catch (cause) {
|
|
315
|
+
if (timeoutCtl?.signal.aborted === true && args.signal?.aborted !== true) {
|
|
316
|
+
throw new ProviderHttpError({
|
|
317
|
+
providerName: args.providerName,
|
|
318
|
+
status: 0,
|
|
319
|
+
message: `request timed out after ${timeoutMs}ms reaching ${args.url}`,
|
|
320
|
+
cause,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
throw new ProviderHttpError({
|
|
324
|
+
providerName: args.providerName,
|
|
325
|
+
status: 0,
|
|
326
|
+
message: `network error reaching ${args.url}`,
|
|
327
|
+
cause,
|
|
328
|
+
});
|
|
329
|
+
} finally {
|
|
330
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
331
|
+
}
|
|
332
|
+
if (!resp.ok) {
|
|
333
|
+
const detail = await safeReadText(resp);
|
|
334
|
+
const headers = pickBackoffHeaders(resp.headers);
|
|
335
|
+
throw new ProviderHttpError({
|
|
336
|
+
providerName: args.providerName,
|
|
337
|
+
status: resp.status,
|
|
338
|
+
message: detail.length > 0 ? detail : resp.statusText,
|
|
339
|
+
...(headers !== undefined ? { headers } : {}),
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
return resp;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Capture the backoff-relevant response headers (`retry-after`,
|
|
347
|
+
* `x-ratelimit-*`) so `withRetry`'s Retry-After hint reader can honour
|
|
348
|
+
* server-provided delays. Returns `undefined` when none are present.
|
|
349
|
+
*/
|
|
350
|
+
function pickBackoffHeaders(headers: Headers): Readonly<Record<string, string>> | undefined {
|
|
351
|
+
const picked: Record<string, string> = {};
|
|
352
|
+
headers.forEach((value, key) => {
|
|
353
|
+
const lower = key.toLowerCase();
|
|
354
|
+
if (lower === 'retry-after' || lower.startsWith('x-ratelimit-')) {
|
|
355
|
+
picked[lower] = value;
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
return Object.keys(picked).length > 0 ? picked : undefined;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function safeReadText(resp: Response): Promise<string> {
|
|
362
|
+
try {
|
|
363
|
+
return await resp.text();
|
|
364
|
+
} catch {
|
|
365
|
+
return '';
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Yield a `stream-start` `ProviderEvent` for an HTTP adapter.
|
|
371
|
+
*
|
|
372
|
+
* @internal
|
|
373
|
+
*/
|
|
374
|
+
export function makeStreamStartEvent(args: {
|
|
375
|
+
readonly providerName: string;
|
|
376
|
+
readonly modelId: string;
|
|
377
|
+
readonly responseId?: string;
|
|
378
|
+
}): ProviderEvent {
|
|
379
|
+
return {
|
|
380
|
+
type: 'stream-start',
|
|
381
|
+
metadata: {
|
|
382
|
+
providerName: args.providerName,
|
|
383
|
+
modelId: args.modelId,
|
|
384
|
+
...(args.responseId !== undefined ? { responseId: args.responseId } : {}),
|
|
385
|
+
createdAt: new Date().toISOString(),
|
|
386
|
+
},
|
|
387
|
+
};
|
|
388
|
+
}
|