@nirmos/sdk 0.1.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/README.md +62 -0
- package/dist/cjs/client.js +61 -0
- package/dist/cjs/client.js.map +1 -0
- package/dist/cjs/core/errors.js +143 -0
- package/dist/cjs/core/errors.js.map +1 -0
- package/dist/cjs/core/metadata.js +104 -0
- package/dist/cjs/core/metadata.js.map +1 -0
- package/dist/cjs/core/transport.js +276 -0
- package/dist/cjs/core/transport.js.map +1 -0
- package/dist/cjs/core/types.js +3 -0
- package/dist/cjs/core/types.js.map +1 -0
- package/dist/cjs/gateway/chat-stream.js +262 -0
- package/dist/cjs/gateway/chat-stream.js.map +1 -0
- package/dist/cjs/gateway/chat.js +102 -0
- package/dist/cjs/gateway/chat.js.map +1 -0
- package/dist/cjs/gateway/embeddings.js +33 -0
- package/dist/cjs/gateway/embeddings.js.map +1 -0
- package/dist/cjs/gateway/gateway.js +18 -0
- package/dist/cjs/gateway/gateway.js.map +1 -0
- package/dist/cjs/gateway/images.js +36 -0
- package/dist/cjs/gateway/images.js.map +1 -0
- package/dist/cjs/gateway/mappers.js +184 -0
- package/dist/cjs/gateway/mappers.js.map +1 -0
- package/dist/cjs/gateway/types.js +3 -0
- package/dist/cjs/gateway/types.js.map +1 -0
- package/dist/cjs/index.js +25 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/prompts/cache.js +48 -0
- package/dist/cjs/prompts/cache.js.map +1 -0
- package/dist/cjs/prompts/mappers.js +242 -0
- package/dist/cjs/prompts/mappers.js.map +1 -0
- package/dist/cjs/prompts/prompts.js +121 -0
- package/dist/cjs/prompts/prompts.js.map +1 -0
- package/dist/cjs/prompts/render.js +86 -0
- package/dist/cjs/prompts/render.js.map +1 -0
- package/dist/cjs/prompts/types.js +3 -0
- package/dist/cjs/prompts/types.js.map +1 -0
- package/dist/cjs/prompts/versions.js +51 -0
- package/dist/cjs/prompts/versions.js.map +1 -0
- package/dist/esm/client.d.ts +28 -0
- package/dist/esm/client.d.ts.map +1 -0
- package/dist/esm/client.js +58 -0
- package/dist/esm/client.js.map +1 -0
- package/dist/esm/core/errors.d.ts +72 -0
- package/dist/esm/core/errors.d.ts.map +1 -0
- package/dist/esm/core/errors.js +125 -0
- package/dist/esm/core/errors.js.map +1 -0
- package/dist/esm/core/metadata.d.ts +11 -0
- package/dist/esm/core/metadata.d.ts.map +1 -0
- package/dist/esm/core/metadata.js +95 -0
- package/dist/esm/core/metadata.js.map +1 -0
- package/dist/esm/core/transport.d.ts +28 -0
- package/dist/esm/core/transport.d.ts.map +1 -0
- package/dist/esm/core/transport.js +272 -0
- package/dist/esm/core/transport.js.map +1 -0
- package/dist/esm/core/types.d.ts +43 -0
- package/dist/esm/core/types.d.ts.map +1 -0
- package/dist/esm/core/types.js +2 -0
- package/dist/esm/core/types.js.map +1 -0
- package/dist/esm/gateway/chat-stream.d.ts +9 -0
- package/dist/esm/gateway/chat-stream.d.ts.map +1 -0
- package/dist/esm/gateway/chat-stream.js +258 -0
- package/dist/esm/gateway/chat-stream.js.map +1 -0
- package/dist/esm/gateway/chat.d.ts +12 -0
- package/dist/esm/gateway/chat.d.ts.map +1 -0
- package/dist/esm/gateway/chat.js +98 -0
- package/dist/esm/gateway/chat.js.map +1 -0
- package/dist/esm/gateway/embeddings.d.ts +9 -0
- package/dist/esm/gateway/embeddings.d.ts.map +1 -0
- package/dist/esm/gateway/embeddings.js +29 -0
- package/dist/esm/gateway/embeddings.js.map +1 -0
- package/dist/esm/gateway/gateway.d.ts +12 -0
- package/dist/esm/gateway/gateway.d.ts.map +1 -0
- package/dist/esm/gateway/gateway.js +14 -0
- package/dist/esm/gateway/gateway.js.map +1 -0
- package/dist/esm/gateway/images.d.ts +9 -0
- package/dist/esm/gateway/images.d.ts.map +1 -0
- package/dist/esm/gateway/images.js +32 -0
- package/dist/esm/gateway/images.js.map +1 -0
- package/dist/esm/gateway/mappers.d.ts +11 -0
- package/dist/esm/gateway/mappers.d.ts.map +1 -0
- package/dist/esm/gateway/mappers.js +174 -0
- package/dist/esm/gateway/mappers.js.map +1 -0
- package/dist/esm/gateway/types.d.ts +168 -0
- package/dist/esm/gateway/types.d.ts.map +1 -0
- package/dist/esm/gateway/types.js +2 -0
- package/dist/esm/gateway/types.js.map +1 -0
- package/dist/esm/index.d.ts +8 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +4 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/prompts/cache.d.ts +13 -0
- package/dist/esm/prompts/cache.d.ts.map +1 -0
- package/dist/esm/prompts/cache.js +44 -0
- package/dist/esm/prompts/cache.js.map +1 -0
- package/dist/esm/prompts/mappers.d.ts +8 -0
- package/dist/esm/prompts/mappers.d.ts.map +1 -0
- package/dist/esm/prompts/mappers.js +235 -0
- package/dist/esm/prompts/mappers.js.map +1 -0
- package/dist/esm/prompts/prompts.d.ts +17 -0
- package/dist/esm/prompts/prompts.d.ts.map +1 -0
- package/dist/esm/prompts/prompts.js +117 -0
- package/dist/esm/prompts/prompts.js.map +1 -0
- package/dist/esm/prompts/render.d.ts +3 -0
- package/dist/esm/prompts/render.d.ts.map +1 -0
- package/dist/esm/prompts/render.js +83 -0
- package/dist/esm/prompts/render.js.map +1 -0
- package/dist/esm/prompts/types.d.ts +119 -0
- package/dist/esm/prompts/types.d.ts.map +1 -0
- package/dist/esm/prompts/types.js +2 -0
- package/dist/esm/prompts/types.js.map +1 -0
- package/dist/esm/prompts/versions.d.ts +12 -0
- package/dist/esm/prompts/versions.d.ts.map +1 -0
- package/dist/esm/prompts/versions.js +47 -0
- package/dist/esm/prompts/versions.js.map +1 -0
- package/package.json +50 -0
- package/src/client.ts +89 -0
- package/src/core/errors.ts +160 -0
- package/src/core/metadata.ts +141 -0
- package/src/core/transport.ts +331 -0
- package/src/core/types.ts +51 -0
- package/src/gateway/chat-stream.ts +307 -0
- package/src/gateway/chat.ts +134 -0
- package/src/gateway/embeddings.ts +44 -0
- package/src/gateway/gateway.ts +17 -0
- package/src/gateway/images.ts +41 -0
- package/src/gateway/mappers.ts +234 -0
- package/src/gateway/types.ts +206 -0
- package/src/index.ts +83 -0
- package/src/prompts/cache.ts +50 -0
- package/src/prompts/mappers.ts +318 -0
- package/src/prompts/prompts.ts +188 -0
- package/src/prompts/render.ts +117 -0
- package/src/prompts/types.ts +148 -0
- package/src/prompts/versions.ts +80 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import {
|
|
2
|
+
APIError,
|
|
3
|
+
AuthenticationError,
|
|
4
|
+
ConflictError,
|
|
5
|
+
NetworkError,
|
|
6
|
+
NotFoundError,
|
|
7
|
+
PermissionDeniedError,
|
|
8
|
+
RateLimitError,
|
|
9
|
+
RequestAbortedError,
|
|
10
|
+
ServerError,
|
|
11
|
+
TimeoutError,
|
|
12
|
+
ValidationError,
|
|
13
|
+
} from "./errors.js";
|
|
14
|
+
import {
|
|
15
|
+
asUnknownRecord,
|
|
16
|
+
extractMetadata,
|
|
17
|
+
readBoolean,
|
|
18
|
+
readString,
|
|
19
|
+
} from "./metadata.js";
|
|
20
|
+
import type { HeadersInput, RequestOptions } from "./types.js";
|
|
21
|
+
|
|
22
|
+
export interface TransportConfig {
|
|
23
|
+
apiKey: string;
|
|
24
|
+
baseUrl: string;
|
|
25
|
+
timeoutMs: number;
|
|
26
|
+
maxRetries: number;
|
|
27
|
+
fetch: typeof globalThis.fetch;
|
|
28
|
+
headers?: HeadersInput;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface TransportRequest {
|
|
32
|
+
method: "GET" | "POST" | "PATCH" | "PUT" | "DELETE";
|
|
33
|
+
path: string;
|
|
34
|
+
query?: Readonly<Record<string, string | number | boolean | undefined>>;
|
|
35
|
+
body?: unknown;
|
|
36
|
+
options?: RequestOptions;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface TransportResponse<T> {
|
|
40
|
+
data: T;
|
|
41
|
+
response: Response;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const SDK_VERSION = "1.0.0";
|
|
45
|
+
|
|
46
|
+
export class HttpTransport {
|
|
47
|
+
readonly #config: TransportConfig;
|
|
48
|
+
|
|
49
|
+
constructor(config: TransportConfig) {
|
|
50
|
+
this.#config = config;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async request<T>(request: TransportRequest): Promise<TransportResponse<T>> {
|
|
54
|
+
const response = await this.fetch(request);
|
|
55
|
+
const data = await parseResponseBody(response);
|
|
56
|
+
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
throw createApiError(response, data);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { data: data as T, response };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async ensureSuccess(response: Response): Promise<Response> {
|
|
65
|
+
if (response.ok) return response;
|
|
66
|
+
const data = await parseResponseBody(response);
|
|
67
|
+
throw createApiError(response, data);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async fetch(request: TransportRequest): Promise<Response> {
|
|
71
|
+
const maxRetries = request.options?.maxRetries ?? this.#config.maxRetries;
|
|
72
|
+
const retryableMethod =
|
|
73
|
+
request.method === "GET" || request.options?.idempotencyKey !== undefined;
|
|
74
|
+
let attempt = 0;
|
|
75
|
+
|
|
76
|
+
while (true) {
|
|
77
|
+
try {
|
|
78
|
+
const response = await this.#fetchOnce(request);
|
|
79
|
+
if (
|
|
80
|
+
retryableMethod &&
|
|
81
|
+
attempt < maxRetries &&
|
|
82
|
+
isRetryableStatus(response.status)
|
|
83
|
+
) {
|
|
84
|
+
await response.body?.cancel();
|
|
85
|
+
await waitBeforeRetry(response, attempt, request.options?.signal);
|
|
86
|
+
attempt += 1;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
return response;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (
|
|
92
|
+
retryableMethod &&
|
|
93
|
+
attempt < maxRetries &&
|
|
94
|
+
error instanceof NetworkError
|
|
95
|
+
) {
|
|
96
|
+
await wait(250 * 2 ** attempt, request.options?.signal);
|
|
97
|
+
attempt += 1;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async #fetchOnce(request: TransportRequest): Promise<Response> {
|
|
106
|
+
const timeoutMs = request.options?.timeoutMs ?? this.#config.timeoutMs;
|
|
107
|
+
const controller = new AbortController();
|
|
108
|
+
let timedOut = false;
|
|
109
|
+
let externallyAborted = false;
|
|
110
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
111
|
+
let responseOwnsCleanup = false;
|
|
112
|
+
let cleanedUp = false;
|
|
113
|
+
|
|
114
|
+
const abortFromSignal = () => {
|
|
115
|
+
externallyAborted = true;
|
|
116
|
+
controller.abort(request.options?.signal?.reason);
|
|
117
|
+
cleanup();
|
|
118
|
+
};
|
|
119
|
+
const cleanup = () => {
|
|
120
|
+
if (cleanedUp) return;
|
|
121
|
+
cleanedUp = true;
|
|
122
|
+
if (timer) clearTimeout(timer);
|
|
123
|
+
request.options?.signal?.removeEventListener("abort", abortFromSignal);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
if (request.options?.signal?.aborted) {
|
|
127
|
+
throw new RequestAbortedError({
|
|
128
|
+
cause: request.options.signal.reason,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
request.options?.signal?.addEventListener("abort", abortFromSignal, {
|
|
133
|
+
once: true,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (timeoutMs > 0) {
|
|
137
|
+
timer = setTimeout(() => {
|
|
138
|
+
timedOut = true;
|
|
139
|
+
controller.abort();
|
|
140
|
+
cleanup();
|
|
141
|
+
}, timeoutMs);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const response = await this.#config.fetch(this.#url(request), {
|
|
146
|
+
method: request.method,
|
|
147
|
+
headers: this.#headers(request),
|
|
148
|
+
body:
|
|
149
|
+
request.body === undefined ? undefined : JSON.stringify(request.body),
|
|
150
|
+
signal: controller.signal,
|
|
151
|
+
});
|
|
152
|
+
if (!response.body) return response;
|
|
153
|
+
|
|
154
|
+
responseOwnsCleanup = true;
|
|
155
|
+
const reader = response.body.getReader();
|
|
156
|
+
const body = new ReadableStream<Uint8Array>({
|
|
157
|
+
async pull(streamController) {
|
|
158
|
+
try {
|
|
159
|
+
const { done, value } = await reader.read();
|
|
160
|
+
if (done) {
|
|
161
|
+
cleanup();
|
|
162
|
+
streamController.close();
|
|
163
|
+
} else {
|
|
164
|
+
streamController.enqueue(value);
|
|
165
|
+
}
|
|
166
|
+
} catch (error) {
|
|
167
|
+
cleanup();
|
|
168
|
+
if (timedOut) {
|
|
169
|
+
streamController.error(
|
|
170
|
+
new TimeoutError(timeoutMs, { cause: error }),
|
|
171
|
+
);
|
|
172
|
+
} else if (externallyAborted || request.options?.signal?.aborted) {
|
|
173
|
+
streamController.error(new RequestAbortedError({ cause: error }));
|
|
174
|
+
} else {
|
|
175
|
+
streamController.error(new NetworkError(error));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
async cancel(reason) {
|
|
180
|
+
try {
|
|
181
|
+
await reader.cancel(reason);
|
|
182
|
+
} finally {
|
|
183
|
+
cleanup();
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
return new Response(body, {
|
|
189
|
+
headers: response.headers,
|
|
190
|
+
status: response.status,
|
|
191
|
+
statusText: response.statusText,
|
|
192
|
+
});
|
|
193
|
+
} catch (error) {
|
|
194
|
+
if (timedOut) throw new TimeoutError(timeoutMs, { cause: error });
|
|
195
|
+
if (externallyAborted || request.options?.signal?.aborted) {
|
|
196
|
+
throw new RequestAbortedError({ cause: error });
|
|
197
|
+
}
|
|
198
|
+
throw new NetworkError(error);
|
|
199
|
+
} finally {
|
|
200
|
+
if (!responseOwnsCleanup) cleanup();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
#url(request: TransportRequest): string {
|
|
205
|
+
const url = new URL(
|
|
206
|
+
request.path.replace(/^\//, ""),
|
|
207
|
+
`${this.#config.baseUrl}/`,
|
|
208
|
+
);
|
|
209
|
+
for (const [key, value] of Object.entries(request.query ?? {})) {
|
|
210
|
+
if (value !== undefined) url.searchParams.set(key, String(value));
|
|
211
|
+
}
|
|
212
|
+
return url.toString();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
#headers(request: TransportRequest): Headers {
|
|
216
|
+
const headers = createHeaders(this.#config.headers);
|
|
217
|
+
headers.set("accept", "application/json");
|
|
218
|
+
headers.set("x-api-key", this.#config.apiKey);
|
|
219
|
+
headers.set("x-nirmos-sdk-version", SDK_VERSION);
|
|
220
|
+
if (request.body !== undefined) {
|
|
221
|
+
headers.set("content-type", "application/json");
|
|
222
|
+
}
|
|
223
|
+
createHeaders(request.options?.headers).forEach((value, key) => {
|
|
224
|
+
if (key.toLowerCase() !== "x-api-key") headers.set(key, value);
|
|
225
|
+
});
|
|
226
|
+
if (request.options?.idempotencyKey) {
|
|
227
|
+
headers.set("idempotency-key", request.options.idempotencyKey);
|
|
228
|
+
}
|
|
229
|
+
return headers;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function parseResponseBody(response: Response): Promise<unknown> {
|
|
234
|
+
if (response.status === 204) return undefined;
|
|
235
|
+
const text = await response.text();
|
|
236
|
+
if (!text) return undefined;
|
|
237
|
+
try {
|
|
238
|
+
return JSON.parse(text) as unknown;
|
|
239
|
+
} catch {
|
|
240
|
+
return text;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function createApiError(response: Response, payload: unknown): APIError {
|
|
245
|
+
const root = asUnknownRecord(payload);
|
|
246
|
+
const error = asUnknownRecord(root.error);
|
|
247
|
+
const metadata = extractMetadata(response, payload);
|
|
248
|
+
const message =
|
|
249
|
+
(readString(error, "message") ??
|
|
250
|
+
readString(root, "message") ??
|
|
251
|
+
response.statusText) ||
|
|
252
|
+
"Nirmos API request failed.";
|
|
253
|
+
const code = readString(error, "code", "type") ?? readString(root, "code");
|
|
254
|
+
const retryable =
|
|
255
|
+
readBoolean(error, "retryable") ?? isRetryableStatus(response.status);
|
|
256
|
+
const retryAfterMs = parseRetryAfter(response.headers.get("retry-after"));
|
|
257
|
+
const options = {
|
|
258
|
+
status: response.status,
|
|
259
|
+
code,
|
|
260
|
+
retryable,
|
|
261
|
+
retryAfterMs,
|
|
262
|
+
details: error.details,
|
|
263
|
+
metadata,
|
|
264
|
+
requestId: metadata.requestId,
|
|
265
|
+
traceId: metadata.traceId,
|
|
266
|
+
headers: response.headers,
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
if (response.status === 401) return new AuthenticationError(message, options);
|
|
270
|
+
if (response.status === 403)
|
|
271
|
+
return new PermissionDeniedError(message, options);
|
|
272
|
+
if (response.status === 404) return new NotFoundError(message, options);
|
|
273
|
+
if (response.status === 409) return new ConflictError(message, options);
|
|
274
|
+
if (response.status === 422 || response.status === 400) {
|
|
275
|
+
return new ValidationError(message, options);
|
|
276
|
+
}
|
|
277
|
+
if (response.status === 429) return new RateLimitError(message, options);
|
|
278
|
+
if (response.status >= 500) return new ServerError(message, options);
|
|
279
|
+
return new APIError(message, options);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function isRetryableStatus(status: number): boolean {
|
|
283
|
+
return status === 408 || status === 429 || status >= 500;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function parseRetryAfter(value: string | null): number | undefined {
|
|
287
|
+
if (!value) return undefined;
|
|
288
|
+
const seconds = Number(value);
|
|
289
|
+
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1_000);
|
|
290
|
+
const date = Date.parse(value);
|
|
291
|
+
return Number.isNaN(date) ? undefined : Math.max(0, date - Date.now());
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function waitBeforeRetry(
|
|
295
|
+
response: Response,
|
|
296
|
+
attempt: number,
|
|
297
|
+
signal?: AbortSignal,
|
|
298
|
+
): Promise<void> {
|
|
299
|
+
const retryAfter = parseRetryAfter(response.headers.get("retry-after"));
|
|
300
|
+
await wait(retryAfter ?? 250 * 2 ** attempt, signal);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function wait(milliseconds: number, signal?: AbortSignal): Promise<void> {
|
|
304
|
+
if (signal?.aborted) {
|
|
305
|
+
throw new RequestAbortedError({ cause: signal.reason });
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
await new Promise<void>((resolve, reject) => {
|
|
309
|
+
const finish = () => {
|
|
310
|
+
signal?.removeEventListener("abort", abort);
|
|
311
|
+
resolve();
|
|
312
|
+
};
|
|
313
|
+
const timer = setTimeout(finish, milliseconds);
|
|
314
|
+
const abort = () => {
|
|
315
|
+
clearTimeout(timer);
|
|
316
|
+
signal?.removeEventListener("abort", abort);
|
|
317
|
+
reject(new RequestAbortedError({ cause: signal?.reason }));
|
|
318
|
+
};
|
|
319
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function createHeaders(input?: HeadersInput): Headers {
|
|
324
|
+
if (!input) return new Headers();
|
|
325
|
+
if (Array.isArray(input)) {
|
|
326
|
+
return new Headers(
|
|
327
|
+
input.map(([key, value]) => [key, value] as [string, string]),
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
return new Headers(input as HeadersInit);
|
|
331
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export type JsonPrimitive = string | number | boolean | null;
|
|
2
|
+
|
|
3
|
+
export type JsonValue =
|
|
4
|
+
JsonPrimitive | { readonly [key: string]: JsonValue } | readonly JsonValue[];
|
|
5
|
+
|
|
6
|
+
export type HeadersInput =
|
|
7
|
+
| Headers
|
|
8
|
+
| Readonly<Record<string, string>>
|
|
9
|
+
| readonly (readonly [string, string])[];
|
|
10
|
+
|
|
11
|
+
export interface RequestOptions {
|
|
12
|
+
/** Cancels this request when aborted. */
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
/** Overrides the client timeout for this request. Set to 0 to disable. */
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
/** Adds headers to this request. Client authentication headers cannot be overridden. */
|
|
17
|
+
headers?: HeadersInput;
|
|
18
|
+
/** Enables safe retries for a mutation when the API supports this key. */
|
|
19
|
+
idempotencyKey?: string;
|
|
20
|
+
/** Overrides the configured retry count for this request. */
|
|
21
|
+
maxRetries?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface RequestMetadata {
|
|
25
|
+
requestId?: string;
|
|
26
|
+
traceId?: string;
|
|
27
|
+
provider?: string;
|
|
28
|
+
model?: string;
|
|
29
|
+
latencyMs?: number;
|
|
30
|
+
cache?: {
|
|
31
|
+
status?: string;
|
|
32
|
+
layer?: string;
|
|
33
|
+
remainingTtlSeconds?: number;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface Usage {
|
|
38
|
+
inputTokens: number;
|
|
39
|
+
outputTokens: number;
|
|
40
|
+
totalTokens: number;
|
|
41
|
+
cachedTokens?: number;
|
|
42
|
+
reasoningTokens?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface Page<T> {
|
|
46
|
+
data: readonly T[];
|
|
47
|
+
hasMore: boolean;
|
|
48
|
+
nextCursor?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type ExtensibleString<T extends string> = T | (string & {});
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { NirmosError, StreamError } from "../core/errors.js";
|
|
2
|
+
import {
|
|
3
|
+
asUnknownRecord,
|
|
4
|
+
compactMetadata,
|
|
5
|
+
extractMetadata,
|
|
6
|
+
readArray,
|
|
7
|
+
readNumber,
|
|
8
|
+
readString,
|
|
9
|
+
} from "../core/metadata.js";
|
|
10
|
+
import type { RequestMetadata, Usage } from "../core/types.js";
|
|
11
|
+
import { normalizeToolCall, normalizeUsage } from "./mappers.js";
|
|
12
|
+
import type {
|
|
13
|
+
ChatChoice,
|
|
14
|
+
ChatCompletion,
|
|
15
|
+
ChatMessage,
|
|
16
|
+
ChatStreamEvent,
|
|
17
|
+
FunctionToolCall,
|
|
18
|
+
} from "./types.js";
|
|
19
|
+
|
|
20
|
+
type MutableChoice = {
|
|
21
|
+
index: number;
|
|
22
|
+
role: ChatMessage["role"];
|
|
23
|
+
content: string;
|
|
24
|
+
finishReason: ChatChoice["finishReason"];
|
|
25
|
+
toolCalls: Map<number, FunctionToolCall>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export class ChatStream implements AsyncIterable<ChatStreamEvent> {
|
|
29
|
+
readonly #response: Response;
|
|
30
|
+
readonly #metadata: RequestMetadata;
|
|
31
|
+
readonly #choices = new Map<number, MutableChoice>();
|
|
32
|
+
#id = "";
|
|
33
|
+
#model = "";
|
|
34
|
+
#createdAt = 0;
|
|
35
|
+
#usage: Usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
36
|
+
#iterator?: AsyncGenerator<ChatStreamEvent>;
|
|
37
|
+
#done = false;
|
|
38
|
+
#consuming = false;
|
|
39
|
+
#completion?: ChatCompletion;
|
|
40
|
+
#error?: unknown;
|
|
41
|
+
|
|
42
|
+
constructor(response: Response) {
|
|
43
|
+
if (!response.body) {
|
|
44
|
+
throw new StreamError("Streaming response did not include a body.");
|
|
45
|
+
}
|
|
46
|
+
this.#response = response;
|
|
47
|
+
this.#metadata = compactMetadata(extractMetadata(response));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
[Symbol.asyncIterator](): AsyncIterator<ChatStreamEvent> {
|
|
51
|
+
if (this.#consuming && !this.#done) {
|
|
52
|
+
throw new StreamError("Chat stream can only be consumed once.");
|
|
53
|
+
}
|
|
54
|
+
this.#consuming = true;
|
|
55
|
+
this.#iterator ??= this.#events();
|
|
56
|
+
return this.#iterator;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async finalResponse(): Promise<ChatCompletion> {
|
|
60
|
+
if (this.#error) throw this.#error;
|
|
61
|
+
if (!this.#done) {
|
|
62
|
+
if (this.#consuming) {
|
|
63
|
+
throw new StreamError(
|
|
64
|
+
"Wait for active stream iteration to finish before requesting finalResponse().",
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
for await (const event of this) {
|
|
68
|
+
// Drain stream and assemble normalized completion.
|
|
69
|
+
void event;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (this.#error) throw this.#error;
|
|
73
|
+
if (!this.#completion) {
|
|
74
|
+
throw new StreamError("Chat stream ended before completion.", {
|
|
75
|
+
requestId: this.#metadata.requestId,
|
|
76
|
+
traceId: this.#metadata.traceId,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return this.#completion;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async text(): Promise<string> {
|
|
83
|
+
return (await this.finalResponse()).outputText;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async *#events(): AsyncGenerator<ChatStreamEvent> {
|
|
87
|
+
try {
|
|
88
|
+
for await (const data of parseSse(this.#response.body!)) {
|
|
89
|
+
if (data === "[DONE]") break;
|
|
90
|
+
let payload: unknown;
|
|
91
|
+
try {
|
|
92
|
+
payload = JSON.parse(data) as unknown;
|
|
93
|
+
} catch (cause) {
|
|
94
|
+
throw new StreamError("Received invalid JSON in chat stream.", {
|
|
95
|
+
cause,
|
|
96
|
+
requestId: this.#metadata.requestId,
|
|
97
|
+
traceId: this.#metadata.traceId,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const root = asUnknownRecord(payload);
|
|
102
|
+
const error = asUnknownRecord(root.error);
|
|
103
|
+
if (Object.keys(error).length > 0) {
|
|
104
|
+
throw new StreamError(
|
|
105
|
+
readString(error, "message") ?? "Nirmos stream failed.",
|
|
106
|
+
{
|
|
107
|
+
code: readString(error, "code"),
|
|
108
|
+
requestId: this.#metadata.requestId,
|
|
109
|
+
traceId: this.#metadata.traceId,
|
|
110
|
+
},
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
this.#id = readString(root, "id") ?? this.#id;
|
|
115
|
+
this.#model = readString(root, "model") ?? this.#model;
|
|
116
|
+
this.#createdAt =
|
|
117
|
+
readNumber(root, "created", "created_at", "createdAt") ??
|
|
118
|
+
this.#createdAt;
|
|
119
|
+
if (root.usage !== undefined) this.#usage = normalizeUsage(root.usage);
|
|
120
|
+
|
|
121
|
+
for (const rawChoice of readArray(root, "choices")) {
|
|
122
|
+
const choice = asUnknownRecord(rawChoice);
|
|
123
|
+
const index = readNumber(choice, "index") ?? 0;
|
|
124
|
+
const delta = asUnknownRecord(choice.delta);
|
|
125
|
+
const mutable = this.#choice(index);
|
|
126
|
+
mutable.role =
|
|
127
|
+
(readString(delta, "role") as ChatMessage["role"] | undefined) ??
|
|
128
|
+
mutable.role;
|
|
129
|
+
const finishReason = readString(
|
|
130
|
+
choice,
|
|
131
|
+
"finish_reason",
|
|
132
|
+
"finishReason",
|
|
133
|
+
);
|
|
134
|
+
if (finishReason) {
|
|
135
|
+
mutable.finishReason =
|
|
136
|
+
finishReason === "tool_calls"
|
|
137
|
+
? "toolCalls"
|
|
138
|
+
: finishReason === "content_filter"
|
|
139
|
+
? "contentFilter"
|
|
140
|
+
: finishReason;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const content = readString(delta, "content");
|
|
144
|
+
if (content) {
|
|
145
|
+
mutable.content += content;
|
|
146
|
+
yield {
|
|
147
|
+
type: "content.delta",
|
|
148
|
+
id: this.#id,
|
|
149
|
+
model: this.#model,
|
|
150
|
+
choiceIndex: index,
|
|
151
|
+
delta: content,
|
|
152
|
+
metadata: this.#metadata,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
for (const rawToolCall of readArray(
|
|
157
|
+
delta,
|
|
158
|
+
"tool_calls",
|
|
159
|
+
"toolCalls",
|
|
160
|
+
)) {
|
|
161
|
+
const toolRecord = asUnknownRecord(rawToolCall);
|
|
162
|
+
const toolIndex = readNumber(toolRecord, "index") ?? 0;
|
|
163
|
+
const incoming = normalizeToolCall(toolRecord);
|
|
164
|
+
const current = mutable.toolCalls.get(toolIndex) ?? {
|
|
165
|
+
id: "",
|
|
166
|
+
type: "function" as const,
|
|
167
|
+
function: { name: "", arguments: "" },
|
|
168
|
+
};
|
|
169
|
+
const merged: FunctionToolCall = {
|
|
170
|
+
id: incoming.id || current.id,
|
|
171
|
+
type: "function",
|
|
172
|
+
function: {
|
|
173
|
+
name: incoming.function.name || current.function.name,
|
|
174
|
+
arguments:
|
|
175
|
+
current.function.arguments + incoming.function.arguments,
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
mutable.toolCalls.set(toolIndex, merged);
|
|
179
|
+
yield {
|
|
180
|
+
type: "toolCall.delta",
|
|
181
|
+
id: this.#id,
|
|
182
|
+
model: this.#model,
|
|
183
|
+
choiceIndex: index,
|
|
184
|
+
toolCall: incoming,
|
|
185
|
+
metadata: this.#metadata,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
this.#completion = this.#buildCompletion();
|
|
192
|
+
this.#done = true;
|
|
193
|
+
yield {
|
|
194
|
+
type: "completion.completed",
|
|
195
|
+
id: this.#completion.id,
|
|
196
|
+
model: this.#completion.model,
|
|
197
|
+
completion: this.#completion,
|
|
198
|
+
metadata: this.#metadata,
|
|
199
|
+
};
|
|
200
|
+
} catch (error) {
|
|
201
|
+
this.#error =
|
|
202
|
+
error instanceof NirmosError
|
|
203
|
+
? error
|
|
204
|
+
: new StreamError("Chat stream ended unexpectedly.", {
|
|
205
|
+
cause: error,
|
|
206
|
+
requestId: this.#metadata.requestId,
|
|
207
|
+
traceId: this.#metadata.traceId,
|
|
208
|
+
});
|
|
209
|
+
throw this.#error;
|
|
210
|
+
} finally {
|
|
211
|
+
this.#done = true;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
#choice(index: number): MutableChoice {
|
|
216
|
+
let choice = this.#choices.get(index);
|
|
217
|
+
if (!choice) {
|
|
218
|
+
choice = {
|
|
219
|
+
index,
|
|
220
|
+
role: "assistant",
|
|
221
|
+
content: "",
|
|
222
|
+
finishReason: null,
|
|
223
|
+
toolCalls: new Map(),
|
|
224
|
+
};
|
|
225
|
+
this.#choices.set(index, choice);
|
|
226
|
+
}
|
|
227
|
+
return choice;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
#buildCompletion(): ChatCompletion {
|
|
231
|
+
const choices: ChatChoice[] = [...this.#choices.values()]
|
|
232
|
+
.sort((left, right) => left.index - right.index)
|
|
233
|
+
.map((choice) => ({
|
|
234
|
+
index: choice.index,
|
|
235
|
+
message: {
|
|
236
|
+
role: choice.role,
|
|
237
|
+
content: choice.content,
|
|
238
|
+
...(choice.toolCalls.size > 0
|
|
239
|
+
? { toolCalls: [...choice.toolCalls.values()] }
|
|
240
|
+
: {}),
|
|
241
|
+
},
|
|
242
|
+
finishReason: choice.finishReason,
|
|
243
|
+
}));
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
id: this.#id || this.#metadata.requestId || "",
|
|
247
|
+
createdAt: this.#createdAt,
|
|
248
|
+
model: this.#model || this.#metadata.model || "",
|
|
249
|
+
choices,
|
|
250
|
+
outputText: choices
|
|
251
|
+
.map((choice) =>
|
|
252
|
+
typeof choice.message.content === "string"
|
|
253
|
+
? choice.message.content
|
|
254
|
+
: "",
|
|
255
|
+
)
|
|
256
|
+
.join(""),
|
|
257
|
+
usage: this.#usage,
|
|
258
|
+
metadata: this.#metadata,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function* parseSse(
|
|
264
|
+
body: ReadableStream<Uint8Array>,
|
|
265
|
+
): AsyncGenerator<string> {
|
|
266
|
+
const reader = body.getReader();
|
|
267
|
+
const decoder = new TextDecoder();
|
|
268
|
+
let buffer = "";
|
|
269
|
+
let dataLines: string[] = [];
|
|
270
|
+
let completed = false;
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
while (true) {
|
|
274
|
+
const { done, value } = await reader.read();
|
|
275
|
+
buffer += decoder.decode(value, { stream: !done });
|
|
276
|
+
const lines = buffer.split(/\r?\n/);
|
|
277
|
+
buffer = done ? "" : (lines.pop() ?? "");
|
|
278
|
+
|
|
279
|
+
for (const line of lines) {
|
|
280
|
+
if (line === "") {
|
|
281
|
+
if (dataLines.length > 0) {
|
|
282
|
+
yield dataLines.join("\n");
|
|
283
|
+
dataLines = [];
|
|
284
|
+
}
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (line.startsWith(":")) continue;
|
|
288
|
+
if (line.startsWith("data:")) {
|
|
289
|
+
dataLines.push(line.slice(5).replace(/^ /, ""));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (done) {
|
|
294
|
+
completed = true;
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (buffer.startsWith("data:")) {
|
|
300
|
+
dataLines.push(buffer.slice(5).replace(/^ /, ""));
|
|
301
|
+
}
|
|
302
|
+
if (dataLines.length > 0) yield dataLines.join("\n");
|
|
303
|
+
} finally {
|
|
304
|
+
if (!completed) await reader.cancel();
|
|
305
|
+
reader.releaseLock();
|
|
306
|
+
}
|
|
307
|
+
}
|