@alvin0/ai-agent-sdk-provider-http 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/dist/index.js ADDED
@@ -0,0 +1,1757 @@
1
+ import { CONTEXT_WINDOW_EXCEEDED_CODE, MODEL_ERROR_CODES, ModelAdapter, ModelError, ProviderRequestId, QUOTA_EXCEEDED_CODE, assertUsableApiKey, attributionHeaders, contentHasImage, createOperationId, detachedFrozen, isContextWindowExceededError, isQuotaExceededError, isSpanId, isTraceId, resolveRetryPolicy, safeErrorRecord, validateUsageCounters, waitForSettlement } from "@alvin0/ai-agent-sdk-core";
2
+ import { createParser } from "eventsource-parser";
3
+ import { AgentSdkError, CREDENTIAL_CAPABILITY_API_VERSION } from "@alvin0/ai-agent-sdk-core/provider";
4
+
5
+ //#region src/common/config.ts
6
+ /** Runtime wire-protocol contract version supported by this package. */
7
+ const HTTP_PROTOCOL_API_VERSION = 1;
8
+ /** Stable support-safe errors owned by the runtime HTTP extension path. */
9
+ const HTTP_PROVIDER_ERROR_CODES = Object.freeze({
10
+ PROTOCOL_API_UNSUPPORTED: "HTTP_PROTOCOL_API_UNSUPPORTED",
11
+ HEADER_INVALID: "HTTP_HEADER_INVALID",
12
+ HEADER_RESERVED: "HTTP_HEADER_RESERVED",
13
+ HEADER_COLLISION: "HTTP_HEADER_COLLISION",
14
+ WIRE_BODY_INVALID: "HTTP_WIRE_BODY_INVALID",
15
+ WIRE_BODY_TOO_LARGE: "HTTP_WIRE_BODY_TOO_LARGE",
16
+ STREAM_MEDIA_TYPE_INVALID: "HTTP_STREAM_MEDIA_TYPE_INVALID",
17
+ SSE_LIMIT_EXCEEDED: "HTTP_SSE_LIMIT_EXCEEDED",
18
+ REDIRECT_REJECTED: "HTTP_REDIRECT_REJECTED"
19
+ });
20
+ const HTTP_PROTOCOL_LIMITS = Object.freeze({
21
+ idBytes: 128,
22
+ dialectDepth: 16,
23
+ dialectNodes: 4096,
24
+ dialectObjectFields: 1024,
25
+ dialectArrayItems: 4096,
26
+ dialectKeyBytes: 1024,
27
+ dialectBytes: 1048576
28
+ });
29
+ /** Structural limits for detached runtime-provider configuration snapshots. */
30
+ const HTTP_RUNTIME_OPTION_LIMITS = Object.freeze({
31
+ maxDepth: 16,
32
+ maxNodes: 16384,
33
+ maxObjectFields: 4096,
34
+ maxArrayItems: 4096,
35
+ maxKeyBytes: 1024,
36
+ maxBytes: 4194304
37
+ });
38
+ /** Bounds for a failure envelope received across a package/runtime boundary. */
39
+ const HTTP_FOREIGN_FAILURE_LIMITS = Object.freeze({
40
+ messageBytes: 2048,
41
+ codeBytes: 128,
42
+ requestIdBytes: 1024
43
+ });
44
+
45
+ //#endregion
46
+ //#region src/stream/parser.ts
47
+ /** Internal bounded parser used by the HTTP transport. */
48
+ async function* parseSseBounded(stream, onActivity, teardownTimeoutMs, limits) {
49
+ const pending = [];
50
+ let emitted = 0;
51
+ const parser = createParser({
52
+ maxBufferSize: limits.maxEventChars,
53
+ onError(error) {
54
+ if (error.type === "max-buffer-size-exceeded") throw limitError("character");
55
+ },
56
+ onEvent(event) {
57
+ emitted++;
58
+ if (emitted > limits.maxEvents || event.data.length > limits.maxEventChars) throw limitError(emitted > limits.maxEvents ? "event-count" : "character");
59
+ pending.push({
60
+ event: event.event,
61
+ data: event.data
62
+ });
63
+ },
64
+ onComment() {
65
+ onActivity?.();
66
+ }
67
+ });
68
+ const decoder = new TextDecoder();
69
+ const reader = stream.getReader();
70
+ let drained = false;
71
+ let primaryFailure;
72
+ try {
73
+ while (true) {
74
+ const { done, value } = await reader.read();
75
+ if (done) break;
76
+ if (value !== void 0 && value.byteLength > 0) onActivity?.();
77
+ if (value !== void 0) parser.feed(decoder.decode(value, { stream: true }));
78
+ yield* drainBatch(pending);
79
+ }
80
+ const tail = decoder.decode();
81
+ if (tail.length > 0) {
82
+ parser.feed(tail);
83
+ yield* drainBatch(pending);
84
+ }
85
+ drained = true;
86
+ } catch (error) {
87
+ primaryFailure = error;
88
+ throw error;
89
+ } finally {
90
+ if (drained) reader.releaseLock();
91
+ else {
92
+ let cancellationFailure;
93
+ const cancellation = reader.cancel().catch((error) => {
94
+ cancellationFailure = error;
95
+ });
96
+ const settled = await waitForSettlement(cancellation, teardownTimeoutMs);
97
+ if (primaryFailure === void 0) {
98
+ if (!settled) throw new Error(`SSE body ignored cancellation for more than ${teardownTimeoutMs}ms`);
99
+ if (cancellationFailure !== void 0) throw cancellationFailure;
100
+ }
101
+ }
102
+ }
103
+ }
104
+ /** Cursor iteration avoids repeated array compaction from Array.shift(). */
105
+ function* drainBatch(pending) {
106
+ for (let index = 0; index < pending.length; index++) {
107
+ const event = pending[index];
108
+ if (event !== void 0) yield event;
109
+ }
110
+ pending.length = 0;
111
+ }
112
+ function limitError(kind) {
113
+ return new ModelError(`provider SSE event buffer ${kind} limit exceeded`, HTTP_PROVIDER_ERROR_CODES.SSE_LIMIT_EXCEEDED);
114
+ }
115
+
116
+ //#endregion
117
+ //#region src/stream/config.ts
118
+ const DEFAULT_MAX_SSE_EVENTS = 1e5;
119
+ const DEFAULT_MAX_SSE_EVENT_CHARS = 1048576;
120
+ const DEFAULT_SSE_TEARDOWN_TIMEOUT_MS = 3e4;
121
+
122
+ //#endregion
123
+ //#region src/stream/idle-deadline.ts
124
+ /**
125
+ * Create one idle timer for a physical provider attempt.
126
+ *
127
+ * The parser calls `activity` while one `iterator.next()` is pending. Resetting
128
+ * the same timer lets comment-only heartbeats keep that read alive without
129
+ * manufacturing protocol events. A primary timeout is never replaced by a
130
+ * secondary iterator-cancellation failure.
131
+ */
132
+ function createStreamIdleDeadline(timeoutMs, displayName, teardownTimeoutMs) {
133
+ let timer;
134
+ let expired = false;
135
+ let rejectExpiry;
136
+ const expiry = new Promise((_resolve, reject) => {
137
+ rejectExpiry = reject;
138
+ });
139
+ expiry.catch(() => void 0);
140
+ const activity = () => {
141
+ if (expired) return;
142
+ if (timer !== void 0) clearTimeout(timer);
143
+ timer = setTimeout(() => {
144
+ expired = true;
145
+ rejectExpiry?.(new ModelError(`${displayName} stream idle for more than ${timeoutMs}ms`, MODEL_ERROR_CODES.TIMEOUT));
146
+ }, timeoutMs);
147
+ };
148
+ const dispose = () => {
149
+ if (timer !== void 0) clearTimeout(timer);
150
+ timer = void 0;
151
+ };
152
+ const guard = async function* (source) {
153
+ const iterator = source[Symbol.asyncIterator]();
154
+ let exhausted = false;
155
+ let primaryFailure;
156
+ activity();
157
+ try {
158
+ while (true) {
159
+ const result = await Promise.race([iterator.next(), expiry]);
160
+ if (result.done === true) {
161
+ exhausted = true;
162
+ return;
163
+ }
164
+ yield result.value;
165
+ }
166
+ } catch (error) {
167
+ primaryFailure = error;
168
+ throw error;
169
+ } finally {
170
+ dispose();
171
+ if (!exhausted) {
172
+ const close = iterator.return?.bind(iterator);
173
+ if (close !== void 0) {
174
+ let closeFailure;
175
+ const closing = Promise.resolve().then(async () => {
176
+ await close();
177
+ }).catch((error) => {
178
+ closeFailure = error;
179
+ });
180
+ const settled = await waitForSettlement(closing, teardownTimeoutMs);
181
+ if (primaryFailure === void 0) {
182
+ if (!settled) throw new ModelError(`${displayName} stream teardown exceeded ${teardownTimeoutMs}ms`, MODEL_ERROR_CODES.TEARDOWN_TIMEOUT);
183
+ if (closeFailure !== void 0) throw closeFailure;
184
+ }
185
+ }
186
+ }
187
+ }
188
+ };
189
+ return Object.freeze({
190
+ activity,
191
+ guard
192
+ });
193
+ }
194
+
195
+ //#endregion
196
+ //#region src/stream/terminal.ts
197
+ /**
198
+ * Enforce the provider-neutral stream terminal contract.
199
+ *
200
+ * The finish chunk is held until the translator ends. That makes it impossible
201
+ * for a custom protocol to expose a finish and then append more output. Earlier
202
+ * output remains streaming; a truncated response after visible output is still
203
+ * surfaced and therefore cannot be retried by the outer retry adapter.
204
+ */
205
+ async function* requireTerminalFinish(source, displayName) {
206
+ let finish;
207
+ for await (const chunk of source) {
208
+ if (finish !== void 0) throw new ModelError(`${displayName} protocol emitted output after its terminal finish`, MODEL_ERROR_CODES.MALFORMED_RESPONSE);
209
+ if (chunk.type === "finish") {
210
+ finish = chunk;
211
+ continue;
212
+ }
213
+ yield chunk;
214
+ }
215
+ if (finish === void 0) throw new ModelError(`${displayName} response ended before a terminal finish`, MODEL_ERROR_CODES.STREAM_CLOSED);
216
+ yield finish;
217
+ }
218
+
219
+ //#endregion
220
+ //#region src/common/failure.ts
221
+ const ENCODER$1 = new TextEncoder();
222
+ const INVALID_FIELD = Symbol("invalid failure field");
223
+ /** Read an own data property without invoking getters or inherited state. */
224
+ function ownDataProbe(source, key) {
225
+ try {
226
+ const descriptor = Object.getOwnPropertyDescriptor(source, key);
227
+ if (descriptor === void 0) return {
228
+ present: false,
229
+ data: true
230
+ };
231
+ if (!("value" in descriptor)) return {
232
+ present: true,
233
+ data: false
234
+ };
235
+ return {
236
+ present: true,
237
+ data: true,
238
+ value: descriptor.value
239
+ };
240
+ } catch {
241
+ return {
242
+ present: true,
243
+ data: false
244
+ };
245
+ }
246
+ }
247
+ function boundedString(value, maxBytes) {
248
+ return typeof value === "string" && value.length > 0 && ENCODER$1.encode(value).byteLength <= maxBytes;
249
+ }
250
+ function optionalFailureField(source, key) {
251
+ const field = ownDataProbe(source, key);
252
+ return field.data ? field.value : INVALID_FIELD;
253
+ }
254
+ /**
255
+ * Validate the data twin carried by a ModelError from another core copy/realm.
256
+ * A lone outer code is deliberately insufficient: retry policy may trust a code
257
+ * only when the bounded inner envelope exists and agrees with it.
258
+ */
259
+ function probeFailureEnvelope(value) {
260
+ if (typeof value !== "object" && typeof value !== "function" || value === null) return { kind: "absent" };
261
+ const outerCode = ownDataProbe(value, "code");
262
+ const carried = ownDataProbe(value, "failure");
263
+ if (!outerCode.present && !carried.present) return { kind: "absent" };
264
+ if (!outerCode.data || !carried.data || !boundedString(outerCode.value, HTTP_FOREIGN_FAILURE_LIMITS.codeBytes) || typeof carried.value !== "object" || carried.value === null || Array.isArray(carried.value)) return { kind: "invalid" };
265
+ const message = optionalFailureField(carried.value, "message");
266
+ const code = optionalFailureField(carried.value, "code");
267
+ const status = optionalFailureField(carried.value, "status");
268
+ const providerRetryAfterMs = optionalFailureField(carried.value, "providerRetryAfterMs");
269
+ const requestId = optionalFailureField(carried.value, "requestId");
270
+ if (message === INVALID_FIELD || code === INVALID_FIELD || status === INVALID_FIELD || providerRetryAfterMs === INVALID_FIELD || requestId === INVALID_FIELD || !boundedString(message, HTTP_FOREIGN_FAILURE_LIMITS.messageBytes) || !boundedString(code, HTTP_FOREIGN_FAILURE_LIMITS.codeBytes) || code !== outerCode.value || status !== void 0 && (!Number.isSafeInteger(status) || status < 100 || status > 599) || providerRetryAfterMs !== void 0 && (!Number.isFinite(providerRetryAfterMs) || providerRetryAfterMs <= 0) || requestId !== void 0 && !boundedString(requestId, HTTP_FOREIGN_FAILURE_LIMITS.requestIdBytes)) return { kind: "invalid" };
271
+ return {
272
+ kind: "valid",
273
+ failure: Object.freeze({
274
+ message,
275
+ code,
276
+ ...status === void 0 ? {} : { status },
277
+ ...providerRetryAfterMs === void 0 ? {} : { providerRetryAfterMs },
278
+ ...requestId === void 0 ? {} : { requestId }
279
+ })
280
+ };
281
+ }
282
+ /** Normalize a foreign provider failure without relying on package class identity. */
283
+ function normalizeHttpBoundaryError(value, fallbackMessage) {
284
+ const envelope = probeFailureEnvelope(value);
285
+ if (envelope.kind === "absent") return new ModelError(fallbackMessage, MODEL_ERROR_CODES.TRANSPORT, { cause: value });
286
+ if (envelope.kind === "invalid") return new ModelError("provider supplied an invalid failure envelope", MODEL_ERROR_CODES.UNKNOWN, { cause: value });
287
+ const failure = envelope.failure;
288
+ return new ModelError(failure.message, failure.code, {
289
+ cause: value,
290
+ ...failure.status === void 0 ? {} : { status: failure.status },
291
+ ...failure.providerRetryAfterMs === void 0 ? {} : { providerRetryAfterMs: failure.providerRetryAfterMs },
292
+ ...failure.requestId === void 0 ? {} : { requestId: failure.requestId }
293
+ });
294
+ }
295
+
296
+ //#endregion
297
+ //#region src/common/header-layers.ts
298
+ const DEFAULT_TRANSPORT_HEADERS = Object.freeze({
299
+ "content-type": "application/json",
300
+ accept: "text/event-stream"
301
+ });
302
+ const HEADER_NAME = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
303
+ const FORBIDDEN_TRANSPORT_NAMES = /* @__PURE__ */ new Set([
304
+ "connection",
305
+ "content-length",
306
+ "host",
307
+ "proxy-authorization",
308
+ "proxy-authenticate",
309
+ "te",
310
+ "trailer",
311
+ "transfer-encoding",
312
+ "upgrade"
313
+ ]);
314
+ const TRANSPORT_OWNED_NAMES = /* @__PURE__ */ new Set(["accept", "content-type"]);
315
+ const SDK_OWNED_NAMES = /* @__PURE__ */ new Set(["user-agent"]);
316
+ const SDK_OWNED_PREFIXES = ["x-ai-agent-sdk-"];
317
+ const SENSITIVE_NAME = /authorization|api[-_]?key|token|secret|cookie|account[-_]?id|signature/i;
318
+ /** Conservative fallback used in addition to exact authentication provenance. */
319
+ function isSensitiveHeaderName(name) {
320
+ return SENSITIVE_NAME.test(name);
321
+ }
322
+ /** Detach one layer without applying ownership rules that need all layers present. */
323
+ function captureHeaderLayer(input) {
324
+ const output = Object.create(null);
325
+ const source = headerRecord(input.headers);
326
+ for (const key of Reflect.ownKeys(source)) {
327
+ if (typeof key !== "string") throw headerError("Header names must be strings", "HEADER_INVALID");
328
+ const descriptor = Object.getOwnPropertyDescriptor(source, key);
329
+ if (descriptor === void 0 || !("value" in descriptor)) throw headerError("Header values must not use accessors", "HEADER_INVALID");
330
+ const name = key.toLowerCase();
331
+ validateHeaderShape(name, descriptor.value);
332
+ if (Object.hasOwn(output, name)) throw headerError("Header names must be unique case-insensitively", "HEADER_COLLISION");
333
+ output[name] = descriptor.value;
334
+ }
335
+ return Object.freeze({
336
+ layer: input.layer,
337
+ headers: Object.freeze(output)
338
+ });
339
+ }
340
+ /** Validate five case-insensitive ownership layers and return one detached snapshot. */
341
+ function mergeHeaderLayers(layers) {
342
+ const output = Object.create(null);
343
+ const owners = /* @__PURE__ */ new Map();
344
+ const sensitive = /* @__PURE__ */ new Set();
345
+ for (const raw of layers) {
346
+ const input = captureHeaderLayer(raw);
347
+ for (const [name, value] of Object.entries(input.headers)) {
348
+ const first = owners.get(name);
349
+ if (first !== void 0) throw headerError(`Header ownership collision between ${first} and ${input.layer}`, "HEADER_COLLISION");
350
+ validateHeaderOwnership(name, input.layer);
351
+ owners.set(name, input.layer);
352
+ output[name] = value;
353
+ if (input.layer === "auth") sensitive.add(name);
354
+ }
355
+ }
356
+ return Object.freeze({
357
+ headers: Object.freeze(output),
358
+ sensitiveHeaderNames: Object.freeze([...sensitive])
359
+ });
360
+ }
361
+ function headerRecord(value) {
362
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw headerError("Headers must be a record", "HEADER_INVALID");
363
+ const prototype = Object.getPrototypeOf(value);
364
+ if (prototype !== Object.prototype && prototype !== null) throw headerError("Headers must be a plain record", "HEADER_INVALID");
365
+ return value;
366
+ }
367
+ function validateHeaderShape(name, value) {
368
+ if (!HEADER_NAME.test(name) || name.length > 256 || typeof value !== "string" || value.length > 16384 || /[\r\n\0]/.test(value)) throw headerError("Header name or value is invalid", "HEADER_INVALID");
369
+ }
370
+ function validateHeaderOwnership(name, layer) {
371
+ if (FORBIDDEN_TRANSPORT_NAMES.has(name) || name.startsWith("sec-") || name.startsWith("proxy-")) throw headerError("Header name is reserved by the transport", "HEADER_RESERVED");
372
+ if (TRANSPORT_OWNED_NAMES.has(name) && layer !== "transport") throw headerError("Header name is owned by the transport layer", "HEADER_RESERVED");
373
+ if (SDK_OWNED_NAMES.has(name) && layer !== "sdk-attribution") throw headerError("Header name is owned by SDK attribution", "HEADER_RESERVED");
374
+ if (SDK_OWNED_PREFIXES.some((prefix) => name.startsWith(prefix)) && layer !== "sdk-attribution") throw headerError("Header prefix is owned by SDK attribution", "HEADER_RESERVED");
375
+ if (isSensitiveHeaderName(name) && layer !== "auth") throw headerError("Credential headers must be supplied by auth", "HEADER_RESERVED");
376
+ }
377
+ function headerError(message, key) {
378
+ return new AgentSdkError(message, HTTP_PROVIDER_ERROR_CODES[key]);
379
+ }
380
+
381
+ //#endregion
382
+ //#region src/base/http-errors.ts
383
+ /**
384
+ * The HTTP-to-taxonomy mapping every provider shares.
385
+ *
386
+ * Kept here rather than per provider because the interesting decisions are
387
+ * genuinely vendor-independent: a 429 that means "slow down" versus one that
388
+ * means "your balance is gone", and a 400 that means "your prompt is too long"
389
+ * versus one that means "your schema is wrong". Both distinctions are invisible
390
+ * in the status code and both change what the caller should do, so getting them
391
+ * right once is worth more than getting them right three times.
392
+ *
393
+ * @module ai-agent-sdk/providers/base/http-errors
394
+ */
395
+ /**
396
+ * Map an HTTP status plus whatever the provider said into a stable code.
397
+ *
398
+ * `detail` should be the provider's error `code`, `type`, and `message` joined
399
+ * into one string — the wording classifiers need all three because providers
400
+ * disagree about which field carries the useful part.
401
+ * @param status - status of a non-2xx response.
402
+ * @param detail - provider error text, joined; empty string when the body was unparseable.
403
+ * @returns the normalized code.
404
+ */
405
+ function httpErrorCode(status, detail = "") {
406
+ if (status === 401 || status === 403) return MODEL_ERROR_CODES.AUTH;
407
+ if (status === 413) return MODEL_ERROR_CODES.INVALID_REQUEST;
408
+ if (isQuotaExceededError(detail)) return QUOTA_EXCEEDED_CODE;
409
+ if (status === 429) return MODEL_ERROR_CODES.RATE_LIMIT;
410
+ if (status === 400 || status === 422) return isContextWindowExceededError(detail) ? CONTEXT_WINDOW_EXCEEDED_CODE : MODEL_ERROR_CODES.INVALID_REQUEST;
411
+ if (status === 404) return MODEL_ERROR_CODES.INVALID_REQUEST;
412
+ if (status >= 500) return MODEL_ERROR_CODES.SERVER;
413
+ return `HTTP_${status}`;
414
+ }
415
+ /**
416
+ * Parse a `retry-after` header into milliseconds.
417
+ *
418
+ * The header comes in two forms — delta-seconds and an HTTP date — and both are
419
+ * used in practice. A date already in the past yields `undefined` rather than a
420
+ * negative delay.
421
+ * @param value - the raw header value, or `null` when absent.
422
+ * @returns a positive finite delay, or `undefined` when absent or unusable.
423
+ */
424
+ function retryAfterMs(value) {
425
+ if (value === null) return void 0;
426
+ const trimmed = value.trim();
427
+ if (/^\d+$/.test(trimmed)) {
428
+ const delay = Number(trimmed) * 1e3;
429
+ return Number.isFinite(delay) && delay > 0 ? delay : void 0;
430
+ }
431
+ const delay = Date.parse(trimmed) - Date.now();
432
+ return Number.isFinite(delay) && delay > 0 ? delay : void 0;
433
+ }
434
+ /** Header names providers use for their request correlation id, in priority order. */
435
+ const REQUEST_ID_HEADERS = [
436
+ "request-id",
437
+ "x-request-id",
438
+ "x-requestid",
439
+ "cf-ray"
440
+ ];
441
+ /**
442
+ * Extract a provider request id for diagnostics.
443
+ *
444
+ * Worth capturing even though nothing programmatic reads it: when a provider is
445
+ * misbehaving, this id is what their support needs to find the request.
446
+ * @param headers - the response headers.
447
+ * @returns the first non-empty id found, or `undefined`.
448
+ */
449
+ function requestIdFrom(headers) {
450
+ for (const name of REQUEST_ID_HEADERS) {
451
+ const value = headers.get(name);
452
+ if (value !== null && value.length > 0) return ProviderRequestId(value);
453
+ }
454
+ }
455
+ /** Read a string property from an unknown object without trusting its shape. */
456
+ function stringField(source, key) {
457
+ if (typeof source !== "object" || source === null) return void 0;
458
+ const value = source[key];
459
+ return typeof value === "string" && value.length > 0 ? value : void 0;
460
+ }
461
+ /**
462
+ * Reduce a provider error body to a message and a classifier detail string.
463
+ *
464
+ * Handles the two shapes both providers use — `{error: {...}}` and a bare
465
+ * `{type, message}` — and tolerates a body that is not JSON at all, which is what
466
+ * a gateway or load balancer in front of the provider will return.
467
+ * @param raw - the response body as text.
468
+ * @returns the message and joined detail.
469
+ */
470
+ function parseErrorBody(raw) {
471
+ let parsed;
472
+ try {
473
+ parsed = JSON.parse(raw);
474
+ } catch {
475
+ return {
476
+ message: void 0,
477
+ detail: raw.slice(0, 2048)
478
+ };
479
+ }
480
+ const error = typeof parsed === "object" && parsed !== null && "error" in parsed ? parsed.error : parsed;
481
+ const code = stringField(error, "code");
482
+ const type = stringField(error, "type");
483
+ const message = stringField(error, "message");
484
+ const detailField = stringField(error, "detail") ?? stringField(parsed, "detail");
485
+ const parts = [
486
+ code,
487
+ type,
488
+ message ?? detailField
489
+ ].filter((part) => part !== void 0);
490
+ return {
491
+ message: message ?? detailField,
492
+ detail: parts.join(" ")
493
+ };
494
+ }
495
+
496
+ //#endregion
497
+ //#region src/base/transport.ts
498
+ function boundedResponseBody(source, maxBytes, maxChunks, displayName, signal) {
499
+ let bytes = 0;
500
+ let chunks = 0;
501
+ return source.pipeThrough(new TransformStream({ transform(chunk, controller) {
502
+ chunks++;
503
+ bytes += chunk.byteLength;
504
+ if (chunks > maxChunks) throw new ModelError(`${displayName} response exceeds the ${maxChunks}-chunk limit`, MODEL_ERROR_CODES.TRANSPORT);
505
+ if (bytes > maxBytes) throw new ModelError(`${displayName} response exceeds the ${maxBytes}-byte limit`, MODEL_ERROR_CODES.TRANSPORT);
506
+ controller.enqueue(chunk);
507
+ } }), signal === void 0 ? void 0 : { signal });
508
+ }
509
+ async function readBoundedText(response, maxBytes, signal) {
510
+ if (response.body === null) return "";
511
+ const reader = response.body.getReader();
512
+ const decoder = new TextDecoder();
513
+ let bytes = 0;
514
+ let text = "";
515
+ try {
516
+ while (true) {
517
+ const { done, value } = await raceWithSignal(reader.read(), signal);
518
+ if (done) break;
519
+ if (value === void 0) continue;
520
+ const remaining = maxBytes - bytes;
521
+ if (remaining <= 0) {
522
+ await waitForSettlement(reader.cancel().catch(() => void 0), 3e4);
523
+ return `${text}\n[error body truncated at ${maxBytes} bytes]`;
524
+ }
525
+ const kept = value.byteLength <= remaining ? value : value.subarray(0, remaining);
526
+ bytes += kept.byteLength;
527
+ text += decoder.decode(kept, { stream: true });
528
+ if (kept.byteLength !== value.byteLength) {
529
+ await waitForSettlement(reader.cancel().catch(() => void 0), 3e4);
530
+ return `${text}${decoder.decode()}\n[error body truncated at ${maxBytes} bytes]`;
531
+ }
532
+ }
533
+ return text + decoder.decode();
534
+ } finally {
535
+ reader.releaseLock();
536
+ }
537
+ }
538
+ /** Reject every redirect shape exposed by Web fetch without following a second hop. */
539
+ async function rejectProviderRedirect(response, requestedUrl) {
540
+ const redirectedStatus = response.status >= 300 && response.status < 400;
541
+ const finalUrlChanged = response.url.length > 0 && response.url !== requestedUrl;
542
+ if (response.type !== "opaqueredirect" && response.redirected !== true && !redirectedStatus && !finalUrlChanged) return;
543
+ if (response.body !== null) await waitForSettlement(response.body.cancel().catch(() => void 0), 3e4);
544
+ throw new ModelError("provider transport rejected a redirect before following it", HTTP_PROVIDER_ERROR_CODES.REDIRECT_REJECTED, response.status === 0 ? void 0 : { status: response.status });
545
+ }
546
+ async function raceWithSignal(pending, signal) {
547
+ if (signal.aborted) {
548
+ pending.catch(() => void 0);
549
+ throw signal.reason ?? /* @__PURE__ */ new Error("operation aborted");
550
+ }
551
+ return await new Promise((resolve, reject) => {
552
+ const onAbort = () => {
553
+ signal.removeEventListener("abort", onAbort);
554
+ reject(signal.reason ?? /* @__PURE__ */ new Error("operation aborted"));
555
+ };
556
+ signal.addEventListener("abort", onAbort, { once: true });
557
+ pending.then((value) => {
558
+ signal.removeEventListener("abort", onAbort);
559
+ resolve(value);
560
+ }, (error) => {
561
+ signal.removeEventListener("abort", onAbort);
562
+ reject(error);
563
+ });
564
+ });
565
+ }
566
+ /** HTTP-specific ownership cleanup; generic Promise races must not dispose values. */
567
+ async function cancelResponseBody(response) {
568
+ try {
569
+ if (response.body === null || response.body.locked) return;
570
+ await waitForSettlement(Promise.resolve().then(() => response.body.cancel()), 3e4);
571
+ } catch {}
572
+ }
573
+ async function* withAbortSignal(iterable, signal) {
574
+ const iterator = iterable[Symbol.asyncIterator]();
575
+ let exhausted = false;
576
+ try {
577
+ while (true) {
578
+ const next = await raceWithSignal(iterator.next(), signal);
579
+ if (next.done === true) {
580
+ exhausted = true;
581
+ return;
582
+ }
583
+ yield next.value;
584
+ }
585
+ } finally {
586
+ if (!exhausted) {
587
+ const close = iterator.return?.bind(iterator);
588
+ if (close !== void 0) {
589
+ const closing = Promise.resolve().then(async () => {
590
+ await close();
591
+ });
592
+ await waitForSettlement(closing, 3e4);
593
+ }
594
+ }
595
+ }
596
+ }
597
+ function positiveInteger(value, name) {
598
+ if (!Number.isSafeInteger(value) || value < 1) throw new RangeError(`${name} must be a positive safe integer`);
599
+ return value;
600
+ }
601
+ function positiveFinite$1(value, name) {
602
+ if (!Number.isFinite(value) || value <= 0) throw new RangeError(`${name} must be a positive finite number`);
603
+ return value;
604
+ }
605
+ function endpointUrl(baseUrl, path, allowInsecureHttp) {
606
+ let base;
607
+ try {
608
+ base = new URL(baseUrl);
609
+ } catch (error) {
610
+ throw new ModelError("provider baseUrl is not a valid absolute URL", MODEL_ERROR_CODES.INVALID_REQUEST, { cause: error });
611
+ }
612
+ if (base.username.length > 0 || base.password.length > 0) throw new ModelError("provider baseUrl must not contain credentials", MODEL_ERROR_CODES.INVALID_REQUEST);
613
+ if (base.search.length > 0 || base.hash.length > 0) throw new ModelError("provider baseUrl must not contain a query or fragment", MODEL_ERROR_CODES.INVALID_REQUEST);
614
+ if (base.protocol !== "https:" && !(allowInsecureHttp && base.protocol === "http:")) throw new ModelError("provider baseUrl must use HTTPS unless allowInsecureHttp is explicitly enabled", MODEL_ERROR_CODES.INVALID_REQUEST);
615
+ const normalizedBase = base.href.replace(/\/+$/, "");
616
+ let endpoint;
617
+ try {
618
+ endpoint = new URL(`${normalizedBase}${path}`);
619
+ } catch (error) {
620
+ throw new ModelError("provider endpoint path produced an invalid URL", MODEL_ERROR_CODES.INVALID_REQUEST, { cause: error });
621
+ }
622
+ if (endpoint.origin !== base.origin) throw new ModelError("provider endpoint path must remain on the configured origin", MODEL_ERROR_CODES.INVALID_REQUEST);
623
+ return endpoint;
624
+ }
625
+ function safeProviderFailure(failure) {
626
+ return Object.freeze({
627
+ type: "ModelError",
628
+ message: "provider attempt failed; inspect the stable code and request ID",
629
+ code: failure.code,
630
+ ...failure.status === void 0 ? {} : { status: failure.status }
631
+ });
632
+ }
633
+ function redactHeaders(headers, sensitiveHeaderNames = []) {
634
+ const provenance = new Set(sensitiveHeaderNames.map((name) => name.toLowerCase()));
635
+ return Object.fromEntries(Object.entries(headers).map(([name, value]) => [name, provenance.has(name.toLowerCase()) || isSensitiveHeaderName(name) ? "[REDACTED]" : value]));
636
+ }
637
+ function requestLogId() {
638
+ return globalThis.crypto?.randomUUID?.() ?? `request-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
639
+ }
640
+ function catalogModelInfo(provider, model) {
641
+ return {
642
+ provider,
643
+ id: model.id,
644
+ name: model.name ?? model.id,
645
+ ...model.description === void 0 ? {} : { description: model.description },
646
+ inputModalities: model.inputModalities ?? ["text"],
647
+ ...model.outputModalities === void 0 ? {} : { outputModalities: model.outputModalities },
648
+ ...model.nativeTools === void 0 ? {} : { nativeTools: model.nativeTools }
649
+ };
650
+ }
651
+ /** Resolve exact metadata from an advisory catalog without opening a connection. */
652
+ function resolvedCatalogModelInfo(provider, modelId, models, defaultMaxTokens, defaultContextWindow) {
653
+ const configured = models.find((entry) => entry.id === modelId);
654
+ return {
655
+ ...configured === void 0 ? {
656
+ provider,
657
+ id: modelId,
658
+ name: modelId,
659
+ inputModalities: ["text"]
660
+ } : catalogModelInfo(provider, configured),
661
+ context: { contextWindow: configured?.contextWindow ?? defaultContextWindow },
662
+ defaultMaxTokens: configured?.maxTokens ?? defaultMaxTokens,
663
+ maxOutputTokens: configured?.maxTokens ?? defaultMaxTokens,
664
+ ...configured?.reasoning === void 0 ? {} : { reasoning: configured.reasoning },
665
+ ...configured?.outputModalities === void 0 ? {} : { outputModalities: configured.outputModalities }
666
+ };
667
+ }
668
+ function abortError(displayName, cause) {
669
+ return new ModelError(`${displayName} request aborted by caller`, MODEL_ERROR_CODES.ABORTED, { cause });
670
+ }
671
+
672
+ //#endregion
673
+ //#region src/base/http-adapter.ts
674
+ /**
675
+ * The single HTTP/SSE pipeline every provider in this package runs through.
676
+ *
677
+ * This is a template method, and that is the point. `stream()` is implemented
678
+ * HERE and is not an extension point: a provider cannot accidentally ship its own
679
+ * fetch loop that forgets attribution headers, mishandles abort, leaks a response
680
+ * body, or invents its own error codes. What a provider supplies is only the four
681
+ * things that are genuinely vendor-specific:
682
+ *
683
+ * - {@link HttpModelAdapter.connect} — where to send it and with what credentials
684
+ * - {@link HttpModelAdapter.endpointPath} — the path under the base URL
685
+ * - {@link HttpModelAdapter.buildBody} — normalized request to wire JSON
686
+ * - {@link HttpModelAdapter.translate} — wire SSE events to `StreamChunk`s
687
+ *
688
+ * Everything else — connection snapshotting, the catalog, modality checks, the
689
+ * request, HTTP error mapping, `retry-after`, request ids, SSE decoding, the idle
690
+ * bound, and teardown — is shared and happens exactly once, here.
691
+ *
692
+ * @module ai-agent-sdk/providers/base/http-adapter
693
+ */
694
+ /** Default idle bound: five minutes without a single byte is a hung stream. */
695
+ const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 3e5;
696
+ /** Default end-to-end bound once provider request construction begins. */
697
+ const DEFAULT_REQUEST_TIMEOUT_MS = 6e5;
698
+ /** Default serialized request ceiling. */
699
+ const DEFAULT_MAX_REQUEST_BYTES = 33554432;
700
+ /** Default cumulative successful response-body ceiling. */
701
+ const DEFAULT_MAX_RESPONSE_BYTES = 33554432;
702
+ /** Default number of raw response chunks accepted from one request. */
703
+ const DEFAULT_MAX_RESPONSE_CHUNKS = 1e5;
704
+ /** Default error body retained for classification and diagnostics. */
705
+ const DEFAULT_MAX_ERROR_BODY_BYTES = 1048576;
706
+ /** Default diagnostic observer deadline; logging must never gate dispatch indefinitely. */
707
+ const DEFAULT_REQUEST_LOGGER_TIMEOUT_MS = 5e3;
708
+ /** Base for every HTTP provider adapter in this package. */
709
+ var HttpModelAdapter = class extends ModelAdapter {
710
+ /**
711
+ * Extra headers merged in by the base pipeline. Override to change `accept`.
712
+ * @returns headers applied beneath {@link HttpConnection.headers}.
713
+ */
714
+ baseHeaders() {
715
+ return {
716
+ "content-type": "application/json",
717
+ "accept": "text/event-stream"
718
+ };
719
+ }
720
+ /**
721
+ * Observe an exact, credential-redacted wire request before dispatch.
722
+ *
723
+ * The default is a no-op so library users do not silently persist prompts.
724
+ * Implementations should treat this as diagnostics, not a dispatch veto.
725
+ * @deprecated High-risk compatibility diagnostics; prefer structured observation.
726
+ */
727
+ observeRequest(_record) {}
728
+ /**
729
+ * Map a non-2xx response to a stable code. Override only to add codes this
730
+ * provider reports that the shared mapping cannot infer from the status.
731
+ */
732
+ providerErrorCode(status, detail) {
733
+ return httpErrorCode(status, detail);
734
+ }
735
+ providerInfo(provider) {
736
+ return {
737
+ id: provider,
738
+ name: this.displayName
739
+ };
740
+ }
741
+ async listModels(provider, signal) {
742
+ return this.captureConnection(await this.connect(provider, signal)).models.map((model) => catalogModelInfo(provider, model));
743
+ }
744
+ async resolveModel(provider, model, signal) {
745
+ const connection = this.captureConnection(await this.connect(provider, signal));
746
+ return this.decorateModel(this.modelInfoFor(connection, provider, model), connection);
747
+ }
748
+ async prepareCall(provider, model, signal, context) {
749
+ context?.declareProviderAttemptAccounting?.();
750
+ const connection = this.captureConnection(await this.connect(provider, signal, context));
751
+ const info = this.decorateModel(this.modelInfoFor(connection, provider, model), connection);
752
+ const wireBody = {};
753
+ return {
754
+ model: info,
755
+ stream: (options, invocation = context) => this.run(options, connection, info, invocation, wireBody)
756
+ };
757
+ }
758
+ /**
759
+ * Stream one model call.
760
+ *
761
+ * Intentionally NOT an extension point — see the module note. Providers
762
+ * customize behaviour through the abstract members instead.
763
+ */
764
+ stream(options, context) {
765
+ return this.runResolving(options, context);
766
+ }
767
+ /** Resolve a connection first, for the un-prepared entry point. */
768
+ async *runResolving(options, context) {
769
+ context?.declareProviderAttemptAccounting?.();
770
+ const connection = this.captureConnection(await this.connect(options.provider, options.signal, context));
771
+ const info = this.decorateModel(this.modelInfoFor(connection, options.provider, options.model), connection);
772
+ yield* this.run(options, connection, info, context, {});
773
+ }
774
+ /** Resolve exact-model metadata from the catalog, falling back to config defaults. */
775
+ modelInfoFor(connection, provider, model) {
776
+ return resolvedCatalogModelInfo(provider, model, connection.models, connection.defaultMaxTokens, connection.defaultContextWindow);
777
+ }
778
+ /** Decorate resolved metadata without reopening the captured connection generation. */
779
+ decorateModel(info, _connection) {
780
+ return info;
781
+ }
782
+ /** Capture legacy subclass transport/auth layers once; configured adapters already return all five. */
783
+ captureConnection(connection) {
784
+ const transport = this.baseHeaders();
785
+ if (Reflect.ownKeys(transport).length === 0) return connection;
786
+ const merged = mergeHeaderLayers([
787
+ {
788
+ layer: "transport",
789
+ headers: transport
790
+ },
791
+ {
792
+ layer: "sdk-attribution",
793
+ headers: attributionHeaders()
794
+ },
795
+ {
796
+ layer: "auth",
797
+ headers: connection.headers
798
+ }
799
+ ]);
800
+ return Object.freeze({
801
+ ...connection,
802
+ headers: merged.headers,
803
+ sensitiveHeaderNames: Object.freeze([.../* @__PURE__ */ new Set([...connection.sensitiveHeaderNames ?? [], ...merged.sensitiveHeaderNames])])
804
+ });
805
+ }
806
+ /**
807
+ * The shared pipeline: guard, build, send, classify, decode, bound, translate.
808
+ */
809
+ async *run(options, connection, model, context, wireBodyCache = {}) {
810
+ context?.declareProviderAttemptAccounting?.();
811
+ if (options.messages.some((message) => contentHasImage(message.content)) && model.inputModalities?.includes("image") !== true) throw new ModelError(`${this.displayName} model "${options.model}" does not accept image input`, MODEL_ERROR_CODES.UNSUPPORTED_CONTENT);
812
+ const request = {
813
+ options,
814
+ model,
815
+ connection,
816
+ maxTokens: options.maxTokens ?? model.defaultMaxTokens ?? connection.defaultMaxTokens
817
+ };
818
+ const consumer = new AbortController();
819
+ const requestTimeoutMs = positiveFinite$1(connection.requestTimeoutMs ?? 6e5, "requestTimeoutMs");
820
+ const timeout = AbortSignal.timeout(requestTimeoutMs);
821
+ const signal = AbortSignal.any([
822
+ consumer.signal,
823
+ timeout,
824
+ ...options.signal === void 0 ? [] : [options.signal]
825
+ ]);
826
+ const maxRequestBytes = positiveInteger(connection.maxRequestBytes ?? 33554432, "maxRequestBytes");
827
+ const maxResponseBytes = positiveInteger(connection.maxResponseBytes ?? 33554432, "maxResponseBytes");
828
+ const maxResponseChunks = positiveInteger(connection.maxResponseChunks ?? 1e5, "maxResponseChunks");
829
+ const maxSseEvents = positiveInteger(connection.maxSseEvents ?? 1e5, "maxSseEvents");
830
+ const maxSseEventChars = positiveInteger(connection.maxSseEventChars ?? 1048576, "maxSseEventChars");
831
+ const maxErrorBodyBytes = positiveInteger(connection.maxErrorBodyBytes ?? 1048576, "maxErrorBodyBytes");
832
+ const requestLoggerTimeoutMs = positiveFinite$1(connection.requestLoggerTimeoutMs ?? 5e3, "requestLoggerTimeoutMs");
833
+ let admissionFailure;
834
+ let ownedResponse;
835
+ try {
836
+ signal.throwIfAborted();
837
+ const preparedBody = await (wireBodyCache.prepared ??= this.prepareWireBody(request, maxRequestBytes, signal));
838
+ const wireBody = preparedBody.value;
839
+ const body = preparedBody.encoded;
840
+ const bodyBytes = preparedBody.bytes;
841
+ const endpoint = endpointUrl(connection.baseUrl, this.endpointPath(request), connection.allowInsecureHttp ?? false);
842
+ const url = endpoint.href;
843
+ const origin = endpoint.origin;
844
+ const headers = connection.headers;
845
+ try {
846
+ const loggerSignal = AbortSignal.any([signal, AbortSignal.timeout(requestLoggerTimeoutMs)]);
847
+ await raceWithSignal(Promise.resolve(this.observeRequest({
848
+ schemaVersion: 1,
849
+ type: "provider-request",
850
+ id: requestLogId(),
851
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
852
+ provider: options.provider,
853
+ model: options.model,
854
+ method: "POST",
855
+ url,
856
+ headers: redactHeaders(headers, connection.sensitiveHeaderNames),
857
+ body: wireBody,
858
+ bodyBytes
859
+ })), loggerSignal);
860
+ } catch {}
861
+ let attempt;
862
+ let dispatchState = "not-sent";
863
+ let httpStatus;
864
+ let providerRequestId;
865
+ let attemptStatus = "unknown";
866
+ let attemptUsage;
867
+ let attemptError;
868
+ try {
869
+ signal.throwIfAborted();
870
+ try {
871
+ attempt = await context?.startProviderAttempt?.({
872
+ provider: options.provider,
873
+ model: options.model,
874
+ method: "POST",
875
+ origin
876
+ }, signal);
877
+ } catch (error) {
878
+ admissionFailure = { value: error };
879
+ throw error;
880
+ }
881
+ signal.throwIfAborted();
882
+ dispatchState = "unknown";
883
+ const pendingResponse = (connection.fetch ?? globalThis.fetch)(url, {
884
+ method: "POST",
885
+ headers,
886
+ body,
887
+ signal,
888
+ redirect: "manual"
889
+ });
890
+ pendingResponse.then((response) => {
891
+ if (signal.aborted) return cancelResponseBody(response);
892
+ }, () => void 0);
893
+ const response = await raceWithSignal(pendingResponse, signal);
894
+ ownedResponse = response;
895
+ signal.throwIfAborted();
896
+ dispatchState = "sent";
897
+ httpStatus = response.status;
898
+ providerRequestId = requestIdFrom(response.headers);
899
+ await rejectProviderRedirect(response, url);
900
+ if (!response.ok) throw await this.httpFailure(response, origin, maxErrorBodyBytes, signal);
901
+ if (response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() !== "text/event-stream") throw new ModelError(`${this.displayName} response is not text/event-stream`, HTTP_PROVIDER_ERROR_CODES.STREAM_MEDIA_TYPE_INVALID);
902
+ if (response.body === null) throw new ModelError(`${this.displayName} returned no response body`, MODEL_ERROR_CODES.STREAM_CLOSED);
903
+ const declaredLength = response.headers.get("content-length");
904
+ if (declaredLength !== null && /^\d+$/.test(declaredLength) && Number(declaredLength) > maxResponseBytes) throw new ModelError(`${this.displayName} response exceeds the ${maxResponseBytes}-byte limit`, MODEL_ERROR_CODES.TRANSPORT);
905
+ const idleDeadline = createStreamIdleDeadline(connection.streamIdleTimeoutMs, this.displayName, 3e4);
906
+ const events = parseSseBounded(boundedResponseBody(response.body, maxResponseBytes, maxResponseChunks, this.displayName, signal), idleDeadline.activity, 3e4, {
907
+ maxEvents: maxSseEvents,
908
+ maxEventChars: maxSseEventChars
909
+ });
910
+ const translated = requireTerminalFinish(this.translate(events, request), this.displayName);
911
+ for await (const chunk of withAbortSignal(idleDeadline.guard(translated), signal)) {
912
+ if (chunk.type === "usage") {
913
+ attemptUsage = chunk.usage;
914
+ const validated = validateUsageCounters(chunk.usage, true);
915
+ if (!validated.complete) continue;
916
+ yield {
917
+ type: "usage",
918
+ usage: validated.reported
919
+ };
920
+ continue;
921
+ }
922
+ if (chunk.type === "finish") {
923
+ attemptStatus = chunk.reason.kind === "aborted" ? "aborted" : chunk.reason.kind === "error" ? "error" : "success";
924
+ if (chunk.reason.kind === "error" || chunk.reason.kind === "aborted") attemptError = safeProviderFailure(chunk.reason.failure);
925
+ }
926
+ yield chunk;
927
+ }
928
+ } catch (error) {
929
+ if (admissionFailure !== void 0 && error === admissionFailure.value) throw error;
930
+ const mapped = timeout.aborted && options.signal?.aborted !== true ? new ModelError(`${this.displayName} request exceeded its ${requestTimeoutMs}ms time limit`, MODEL_ERROR_CODES.TIMEOUT, { cause: error }) : signal.aborted ? abortError(this.displayName, error) : normalizeHttpBoundaryError(error, `${this.displayName} request to ${origin} failed`);
931
+ attemptStatus = mapped.code === MODEL_ERROR_CODES.ABORTED ? "aborted" : "error";
932
+ attemptError = safeProviderFailure(mapped.failure);
933
+ throw mapped;
934
+ } finally {
935
+ attempt?.end({
936
+ status: attemptStatus,
937
+ dispatchState,
938
+ ...attemptUsage === void 0 ? {} : { reported: attemptUsage },
939
+ ...httpStatus === void 0 ? {} : { httpStatus },
940
+ ...providerRequestId === void 0 ? {} : { providerRequestId },
941
+ ...attemptError === void 0 ? {} : { error: attemptError }
942
+ });
943
+ }
944
+ } catch (error) {
945
+ if (options.signal?.aborted === true) throw abortError(this.displayName, error);
946
+ if (timeout.aborted) throw new ModelError(`${this.displayName} request exceeded its ${requestTimeoutMs}ms time limit`, MODEL_ERROR_CODES.TIMEOUT, { cause: error });
947
+ if (admissionFailure !== void 0 && error === admissionFailure.value) throw error;
948
+ throw normalizeHttpBoundaryError(error, `${this.displayName} stream failed`);
949
+ } finally {
950
+ consumer.abort(/* @__PURE__ */ new Error(`${this.displayName} stream consumer stopped`));
951
+ if (ownedResponse !== void 0) await cancelResponseBody(ownedResponse);
952
+ }
953
+ }
954
+ async prepareWireBody(request, maxRequestBytes, signal) {
955
+ signal.throwIfAborted();
956
+ const value = await raceWithSignal(Promise.resolve(this.buildBody(request)), signal);
957
+ const encoded = JSON.stringify(value);
958
+ const bytes = new TextEncoder().encode(encoded).byteLength;
959
+ if (bytes > maxRequestBytes) throw new ModelError(`${this.displayName} request exceeds the ${maxRequestBytes}-byte limit`, MODEL_ERROR_CODES.INVALID_REQUEST);
960
+ return Object.freeze({
961
+ value,
962
+ encoded,
963
+ bytes
964
+ });
965
+ }
966
+ /** Turn a non-2xx response into a fully populated {@link ModelError}. */
967
+ async httpFailure(response, url, maxBytes, signal) {
968
+ let raw = "";
969
+ try {
970
+ raw = await readBoundedText(response, maxBytes, signal);
971
+ } catch {}
972
+ const { message, detail } = parseErrorBody(raw);
973
+ const delay = retryAfterMs(response.headers.get("retry-after"));
974
+ const id = requestIdFrom(response.headers);
975
+ return new ModelError(message ?? `${this.displayName} error (HTTP ${response.status}) from ${url}`, this.providerErrorCode(response.status, detail), {
976
+ cause: new Error(raw.length > 0 ? raw : `HTTP ${response.status}`),
977
+ status: response.status,
978
+ ...delay === void 0 ? {} : { providerRetryAfterMs: delay },
979
+ ...id === void 0 ? {} : { requestId: id }
980
+ });
981
+ }
982
+ };
983
+
984
+ //#endregion
985
+ //#region src/common/data.ts
986
+ const ENCODER = new TextEncoder();
987
+ /** Read an own data property without evaluating accessors or inherited state. */
988
+ function ownData(source, key, required = true) {
989
+ const descriptor = Object.getOwnPropertyDescriptor(source, key);
990
+ if (descriptor === void 0) {
991
+ if (!required) return void 0;
992
+ throw new TypeError(`Missing ${String(key)}`);
993
+ }
994
+ if (!("value" in descriptor)) throw new TypeError(`${String(key)} must not be an accessor`);
995
+ return descriptor.value;
996
+ }
997
+ /** Require a plain configuration object. */
998
+ function plainObject(value, label) {
999
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw new TypeError(`${label} must be an object`);
1000
+ const prototype = Object.getPrototypeOf(value);
1001
+ if (prototype !== Object.prototype && prototype !== null) throw new TypeError(`${label} must be a plain object`);
1002
+ return value;
1003
+ }
1004
+ /** Capture a method once while retaining its original receiver. */
1005
+ function capturedMethod(source, key) {
1006
+ const value = ownData(source, key);
1007
+ if (typeof value !== "function") throw new TypeError(`${String(key)} must be a function`);
1008
+ return (...args) => Reflect.apply(value, source, args);
1009
+ }
1010
+ /** Capture an optional method once while retaining its original receiver. */
1011
+ function optionalCapturedMethod(source, key) {
1012
+ const value = ownData(source, key, false);
1013
+ if (value === void 0) return void 0;
1014
+ if (typeof value !== "function") throw new TypeError(`${String(key)} must be a function`);
1015
+ return (...args) => Reflect.apply(value, source, args);
1016
+ }
1017
+ /** Validate one bounded non-empty UTF-8 identifier. */
1018
+ function boundedIdentifier(value, maxBytes, label) {
1019
+ if (typeof value !== "string" || value.length === 0 || value.trim() !== value || ENCODER.encode(value).byteLength > maxBytes) throw new TypeError(`${label} must be a bounded non-empty string`);
1020
+ return value;
1021
+ }
1022
+
1023
+ //#endregion
1024
+ //#region src/common/json-snapshot.ts
1025
+ /** Clone bounded JSON data without invoking accessors, prototypes, or serialization hooks. */
1026
+ function snapshotJsonObject(value, limits) {
1027
+ let nodes = 0;
1028
+ const seen = /* @__PURE__ */ new Set();
1029
+ const encoder = new TextEncoder();
1030
+ const clone = (input, depth) => {
1031
+ nodes++;
1032
+ if (nodes > limits.maxNodes || depth > limits.maxDepth) throw new TypeError("JSON data exceeds its structural bound");
1033
+ if (input === null || typeof input === "boolean" || typeof input === "string") return input;
1034
+ if (typeof input === "number") {
1035
+ if (!Number.isFinite(input)) throw new TypeError("JSON number must be finite");
1036
+ return input;
1037
+ }
1038
+ if (Array.isArray(input)) return cloneArray(input, depth);
1039
+ if (input === null || typeof input !== "object") throw new TypeError("JSON data contains an unsupported value");
1040
+ return cloneRecord(input, depth);
1041
+ };
1042
+ const cloneArray = (source, depth) => {
1043
+ if (seen.has(source)) throw new TypeError("JSON data is cyclic");
1044
+ const length = ownValue(source, "length");
1045
+ if (!Number.isSafeInteger(length) || Number(length) < 0 || Number(length) > limits.maxArrayItems) throw new TypeError("JSON array exceeds its item bound");
1046
+ seen.add(source);
1047
+ try {
1048
+ const result = [];
1049
+ for (let index = 0; index < Number(length); index++) result.push(clone(ownValue(source, String(index)), depth + 1));
1050
+ return Object.freeze(result);
1051
+ } finally {
1052
+ seen.delete(source);
1053
+ }
1054
+ };
1055
+ const cloneRecord = (source, depth) => {
1056
+ const prototype = Object.getPrototypeOf(source);
1057
+ if (prototype !== Object.prototype && prototype !== null) throw new TypeError("JSON object must be plain");
1058
+ if (seen.has(source)) throw new TypeError("JSON data is cyclic");
1059
+ const keys = Reflect.ownKeys(source);
1060
+ if (keys.some((key) => typeof key !== "string") || keys.length > limits.maxObjectFields) throw new TypeError("JSON object exceeds its field bound");
1061
+ seen.add(source);
1062
+ try {
1063
+ const result = Object.create(null);
1064
+ for (const key of keys) {
1065
+ if (encoder.encode(key).byteLength > limits.maxKeyBytes) throw new TypeError("JSON object key exceeds its byte bound");
1066
+ result[key] = clone(ownValue(source, key), depth + 1);
1067
+ }
1068
+ return Object.freeze(result);
1069
+ } finally {
1070
+ seen.delete(source);
1071
+ }
1072
+ };
1073
+ const snapshot = clone(value, 0);
1074
+ if (snapshot === null || Array.isArray(snapshot) || typeof snapshot !== "object") throw new TypeError("Expected a JSON object");
1075
+ if (encoder.encode(JSON.stringify(snapshot)).byteLength > limits.maxBytes) throw new TypeError("JSON data exceeds its byte bound");
1076
+ return snapshot;
1077
+ }
1078
+ function ownValue(source, key) {
1079
+ const descriptor = Object.getOwnPropertyDescriptor(source, key);
1080
+ if (descriptor === void 0) throw new TypeError("JSON arrays must not be sparse");
1081
+ if (!("value" in descriptor)) throw new TypeError("JSON data must not use accessors");
1082
+ return descriptor.value;
1083
+ }
1084
+
1085
+ //#endregion
1086
+ //#region src/protocol/definition.ts
1087
+ /**
1088
+ * Stamp a protocol definition without allocating transport state or performing I/O.
1089
+ * All executable properties are captured once and retain the author's receiver.
1090
+ */
1091
+ function defineWireProtocol(definition) {
1092
+ const source = plainObject(definition, "wire protocol definition");
1093
+ const id = boundedIdentifier(ownData(source, "id"), HTTP_PROTOCOL_LIMITS.idBytes, "protocol id");
1094
+ const defaultDialect = snapshotJsonObject(ownData(source, "defaultDialect"), {
1095
+ maxDepth: HTTP_PROTOCOL_LIMITS.dialectDepth,
1096
+ maxNodes: HTTP_PROTOCOL_LIMITS.dialectNodes,
1097
+ maxObjectFields: HTTP_PROTOCOL_LIMITS.dialectObjectFields,
1098
+ maxArrayItems: HTTP_PROTOCOL_LIMITS.dialectArrayItems,
1099
+ maxKeyBytes: HTTP_PROTOCOL_LIMITS.dialectKeyBytes,
1100
+ maxBytes: HTTP_PROTOCOL_LIMITS.dialectBytes
1101
+ });
1102
+ const endpointPath = capturedMethod(source, "endpointPath");
1103
+ const protocolHeaders = optionalCapturedMethod(source, "protocolHeaders");
1104
+ const serialize = capturedMethod(source, "serialize");
1105
+ const translate = capturedMethod(source, "translate");
1106
+ return Object.freeze({
1107
+ kind: "http-wire-protocol",
1108
+ apiVersion: 1,
1109
+ id,
1110
+ defaultDialect,
1111
+ endpointPath,
1112
+ ...protocolHeaders === void 0 ? {} : { protocolHeaders },
1113
+ serialize,
1114
+ translate
1115
+ });
1116
+ }
1117
+
1118
+ //#endregion
1119
+ //#region src/protocol/protocol.ts
1120
+ /**
1121
+ * Merge an endpoint's partial dialect over a protocol's defaults.
1122
+ *
1123
+ * `undefined` entries are dropped rather than applied, so an override object built
1124
+ * with optional fields cannot accidentally erase a default.
1125
+ * @param protocol - the protocol supplying defaults.
1126
+ * @param overrides - the endpoint's partial override.
1127
+ * @returns the effective, frozen dialect.
1128
+ */
1129
+ function resolveDialect(protocol, overrides) {
1130
+ if (overrides === void 0) return protocol.defaultDialect;
1131
+ const applied = Object.fromEntries(Object.entries(overrides).filter(([, value]) => value !== void 0));
1132
+ return Object.freeze({
1133
+ ...protocol.defaultDialect,
1134
+ ...applied
1135
+ });
1136
+ }
1137
+
1138
+ //#endregion
1139
+ //#region src/observation/operations.ts
1140
+ const SAFE_OPERATION_ERROR_TYPES = /* @__PURE__ */ new Set([
1141
+ "AbortError",
1142
+ "AgentSdkError",
1143
+ "CodexRefreshError",
1144
+ "Error",
1145
+ "ModelError",
1146
+ "RangeError",
1147
+ "TypeError"
1148
+ ]);
1149
+ const SAFE_OPERATION_ERROR_CODES = /* @__PURE__ */ new Set([
1150
+ "ABORTED",
1151
+ "CODEX_AUTH_FAILED",
1152
+ "CODEX_AUTH_MALFORMED",
1153
+ "CODEX_REAUTH_REQUIRED",
1154
+ "CODEX_REFRESH_TRANSIENT",
1155
+ "INVALID_CREDENTIAL",
1156
+ "MISSING_CREDENTIAL",
1157
+ "TIMEOUT"
1158
+ ]);
1159
+ /** Observe one nested provider operation without exposing its sensitive values. */
1160
+ async function observeProviderOperation(context, input, task) {
1161
+ const port = context?.observation;
1162
+ const parent = context?.correlation;
1163
+ const resource = context?.resource;
1164
+ const scope = context?.scope;
1165
+ if (port === void 0 || parent?.runId === void 0 || resource === void 0 || scope === void 0 || !isTraceId(parent.traceId) || !isSpanId(parent.spanId) || parent.parentSpanId !== null && !isSpanId(parent.parentSpanId)) return await task();
1166
+ const correlationParent = parent;
1167
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
1168
+ let correlation = correlationParent;
1169
+ let span;
1170
+ try {
1171
+ span = port.openSpan({
1172
+ name: input.spanName,
1173
+ runId: parent.runId,
1174
+ parent: correlationParent,
1175
+ startedAt,
1176
+ monotonicMs: scope.monotonicMs()
1177
+ });
1178
+ correlation = span.correlation;
1179
+ } catch {}
1180
+ const capture = (phase, data) => {
1181
+ const event = {
1182
+ schemaVersion: 1,
1183
+ eventId: createOperationId(),
1184
+ sequence: scope.nextSequence(),
1185
+ name: input.name,
1186
+ phase,
1187
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
1188
+ monotonicMs: scope.monotonicMs(),
1189
+ priority: "critical",
1190
+ resource,
1191
+ correlation,
1192
+ data
1193
+ };
1194
+ try {
1195
+ port.capture(event);
1196
+ } catch {}
1197
+ };
1198
+ capture("start", input.data);
1199
+ try {
1200
+ const result = await task();
1201
+ const endedAt = (/* @__PURE__ */ new Date()).toISOString();
1202
+ try {
1203
+ span?.end("success", endedAt, scope.monotonicMs());
1204
+ } catch {}
1205
+ capture("end", {
1206
+ ...input.data,
1207
+ status: "success"
1208
+ });
1209
+ return result;
1210
+ } catch (error) {
1211
+ const endedAt = (/* @__PURE__ */ new Date()).toISOString();
1212
+ try {
1213
+ span?.end("error", endedAt, scope.monotonicMs());
1214
+ } catch {}
1215
+ const safe = safeErrorRecord(error);
1216
+ const type = SAFE_OPERATION_ERROR_TYPES.has(safe.type) ? safe.type : "Error";
1217
+ capture("end", {
1218
+ ...input.data,
1219
+ status: "error",
1220
+ error: {
1221
+ type,
1222
+ message: input.failureMessage,
1223
+ ...safe.code !== void 0 && SAFE_OPERATION_ERROR_CODES.has(safe.code) ? { code: safe.code } : {},
1224
+ ...safe.status === void 0 ? {} : { status: safe.status },
1225
+ ...safe.retryable === void 0 ? {} : { retryable: safe.retryable }
1226
+ }
1227
+ });
1228
+ throw error;
1229
+ }
1230
+ }
1231
+ function observeCredentialOperation(context, provider, operation, task) {
1232
+ return observeProviderOperation(context, {
1233
+ name: "sdk.credential.operation",
1234
+ spanName: "sdk.credential.operation",
1235
+ data: {
1236
+ provider,
1237
+ operation
1238
+ },
1239
+ failureMessage: "credential operation failed"
1240
+ }, task);
1241
+ }
1242
+ function observeModelCatalogOperation(context, provider, origin, task) {
1243
+ return observeProviderOperation(context, {
1244
+ name: "sdk.integration.request",
1245
+ spanName: "sdk.integration.request",
1246
+ data: {
1247
+ integration: "model-catalog",
1248
+ provider,
1249
+ operation: "discover",
1250
+ origin
1251
+ },
1252
+ failureMessage: "model catalog operation failed"
1253
+ }, task);
1254
+ }
1255
+
1256
+ //#endregion
1257
+ //#region src/configurable/http-provider.ts
1258
+ const DEFAULT_CATALOG_TTL_MS = 3e5;
1259
+ const DEFAULT_CATALOG_STALE_TTL_MS = 0;
1260
+ const DEFAULT_CATALOG_FAILURE_BACKOFF_MS = 5e3;
1261
+ const DEFAULT_MAX_CATALOG_MODELS = 2048;
1262
+ const DEFAULT_MAX_CATALOG_BYTES = 4194304;
1263
+ /** Resolve one credential source, with a useful label on failure. */
1264
+ async function credential(source, displayName, label, signal, context) {
1265
+ const value = typeof source === "function" ? await source(signal, context) : source;
1266
+ return assertUsableApiKey(value, displayName, label);
1267
+ }
1268
+ /**
1269
+ * A provider whose every endpoint fact is configuration.
1270
+ *
1271
+ * Kept private: the exported surface is {@link createHttpProvider}, so this class
1272
+ * is free to change and callers cannot come to depend on its shape.
1273
+ */
1274
+ var ConfiguredHttpAdapter = class extends HttpModelAdapter {
1275
+ displayName;
1276
+ options;
1277
+ dialect;
1278
+ retry;
1279
+ catalog;
1280
+ catalogFailureAt;
1281
+ constructor(options) {
1282
+ super();
1283
+ const maxCatalogModels = positiveSafeInteger(options.maxCatalogModels ?? DEFAULT_MAX_CATALOG_MODELS, "maxCatalogModels");
1284
+ const maxCatalogBytes = positiveSafeInteger(options.maxCatalogBytes ?? DEFAULT_MAX_CATALOG_BYTES, "maxCatalogBytes");
1285
+ const models = options.models === void 0 ? void 0 : boundedCatalog(options.models, maxCatalogModels, maxCatalogBytes);
1286
+ this.options = Object.freeze({
1287
+ ...options,
1288
+ catalogTtlMs: positiveFinite(options.catalogTtlMs ?? DEFAULT_CATALOG_TTL_MS, "catalogTtlMs"),
1289
+ catalogStaleTtlMs: nonNegativeFinite(options.catalogStaleTtlMs ?? DEFAULT_CATALOG_STALE_TTL_MS, "catalogStaleTtlMs"),
1290
+ catalogFailureBackoffMs: nonNegativeFinite(options.catalogFailureBackoffMs ?? DEFAULT_CATALOG_FAILURE_BACKOFF_MS, "catalogFailureBackoffMs"),
1291
+ maxCatalogModels,
1292
+ maxCatalogBytes,
1293
+ auth: Object.freeze({ ...options.auth }),
1294
+ ...models === void 0 ? {} : { models },
1295
+ ...options.headers === void 0 || typeof options.headers === "function" ? {} : { headers: Object.freeze({ ...options.headers }) },
1296
+ ...options.baseHeaders === void 0 ? {} : { baseHeaders: Object.freeze({ ...options.baseHeaders }) }
1297
+ });
1298
+ this.displayName = options.displayName;
1299
+ this.dialect = resolveDialect(options.protocol, options.dialect);
1300
+ this.retry = resolveRetryPolicy(options.retryPolicy, `${options.displayName}.retryPolicy`);
1301
+ }
1302
+ providerRetryPolicy() {
1303
+ return this.retry;
1304
+ }
1305
+ listModels(provider, signal) {
1306
+ if (!this.hasStaticCatalog()) return super.listModels(provider, signal);
1307
+ signal?.throwIfAborted();
1308
+ return Promise.resolve(this.staticModels(provider));
1309
+ }
1310
+ resolveModel(provider, model, signal) {
1311
+ if (!this.hasStaticCatalog()) return super.resolveModel(provider, model, signal);
1312
+ signal?.throwIfAborted();
1313
+ return Promise.resolve(this.decorateModel(resolvedCatalogModelInfo(provider, model, this.options.models ?? [], this.options.defaultMaxTokens ?? 8192, this.options.defaultContextWindow ?? 128e3)));
1314
+ }
1315
+ async modelCatalog(provider, options = {}) {
1316
+ if (this.hasStaticCatalog()) {
1317
+ options.signal?.throwIfAborted();
1318
+ return Object.freeze({
1319
+ provider: Object.freeze({
1320
+ id: provider,
1321
+ name: this.displayName
1322
+ }),
1323
+ state: "static",
1324
+ revision: "http-static",
1325
+ models: this.staticModels(provider),
1326
+ observedAt: (/* @__PURE__ */ new Date()).toISOString()
1327
+ });
1328
+ }
1329
+ const snapshot = await super.modelCatalog(provider, options);
1330
+ if (this.catalogFailureAt !== void 0) throw new Error("HTTP provider model catalog is unavailable");
1331
+ return snapshot;
1332
+ }
1333
+ decorateModel(base) {
1334
+ return this.options.describeModel?.(base, this.dialect) ?? base;
1335
+ }
1336
+ hasStaticCatalog() {
1337
+ return this.options.models !== void 0 || this.options.discoverModels === void 0;
1338
+ }
1339
+ staticModels(provider) {
1340
+ return Object.freeze((this.options.models ?? []).map((model) => Object.freeze(catalogModelInfo(provider, model))));
1341
+ }
1342
+ /** Resolve the authentication headers for one operation. */
1343
+ async authHeaders(provider, signal, context) {
1344
+ const auth = this.options.auth;
1345
+ switch (auth.kind) {
1346
+ case "none": return { headers: {} };
1347
+ case "bearer": return { headers: { authorization: `Bearer ${await observeCredentialOperation(context, provider, "resolve", () => credential(auth.token, this.displayName, auth.label ?? "the `auth.token` option", signal, context))}` } };
1348
+ case "header": {
1349
+ const value = await observeCredentialOperation(context, provider, "resolve", () => credential(auth.value, this.displayName, auth.label ?? `the \`${auth.name}\` credential`, signal, context));
1350
+ return { headers: { [auth.name]: value } };
1351
+ }
1352
+ case "dynamic": return { headers: await observeCredentialOperation(context, provider, "resolve", async () => await auth.resolve(signal, context, provider)) };
1353
+ default: return { headers: {} };
1354
+ }
1355
+ }
1356
+ async connect(provider, signal, context) {
1357
+ const timeoutMs = positiveFinite(this.options.requestTimeoutMs ?? 6e5, "requestTimeoutMs");
1358
+ const timeout = AbortSignal.timeout(timeoutMs);
1359
+ const operationSignal = signal === void 0 ? timeout : AbortSignal.any([signal, timeout]);
1360
+ const baseUrl = this.options.baseUrl.replace(/\/+$/, "");
1361
+ const extra = typeof this.options.headers === "function" ? this.options.headers() : this.options.headers ?? {};
1362
+ const publicLayers = [
1363
+ captureHeaderLayer({
1364
+ layer: "transport",
1365
+ headers: this.options.baseHeaders ?? DEFAULT_TRANSPORT_HEADERS
1366
+ }),
1367
+ captureHeaderLayer({
1368
+ layer: "sdk-attribution",
1369
+ headers: attributionHeaders()
1370
+ }),
1371
+ captureHeaderLayer({
1372
+ layer: "wire-protocol",
1373
+ headers: this.options.protocol.protocolHeaders?.(this.dialect) ?? {}
1374
+ }),
1375
+ captureHeaderLayer({
1376
+ layer: "endpoint",
1377
+ headers: extra
1378
+ })
1379
+ ];
1380
+ mergeHeaderLayers(publicLayers);
1381
+ const auth = await raceAbort(this.authHeaders(provider, operationSignal, context), operationSignal);
1382
+ const merged = mergeHeaderLayers([...publicLayers, captureHeaderLayer({
1383
+ layer: "auth",
1384
+ headers: auth.headers
1385
+ })]);
1386
+ const headers = merged.headers;
1387
+ return {
1388
+ baseUrl,
1389
+ headers,
1390
+ sensitiveHeaderNames: merged.sensitiveHeaderNames,
1391
+ streamIdleTimeoutMs: this.options.streamIdleTimeoutMs ?? 3e5,
1392
+ requestTimeoutMs: this.options.requestTimeoutMs ?? 6e5,
1393
+ maxRequestBytes: this.options.maxRequestBytes ?? 33554432,
1394
+ maxResponseBytes: this.options.maxResponseBytes ?? 33554432,
1395
+ maxResponseChunks: this.options.maxResponseChunks ?? 1e5,
1396
+ ...this.options.maxSseEvents === void 0 ? {} : { maxSseEvents: this.options.maxSseEvents },
1397
+ ...this.options.maxSseEventChars === void 0 ? {} : { maxSseEventChars: this.options.maxSseEventChars },
1398
+ maxErrorBodyBytes: this.options.maxErrorBodyBytes ?? 1048576,
1399
+ ...this.options.allowInsecureHttp === void 0 ? {} : { allowInsecureHttp: this.options.allowInsecureHttp },
1400
+ ...this.options.fetch === void 0 ? {} : { fetch: this.options.fetch },
1401
+ ...this.options.requestLoggerTimeoutMs === void 0 ? {} : { requestLoggerTimeoutMs: this.options.requestLoggerTimeoutMs },
1402
+ retryPolicy: this.retry,
1403
+ models: this.options.models ?? await this.resolveCatalog(provider, baseUrl, headers, operationSignal, context),
1404
+ defaultMaxTokens: this.options.defaultMaxTokens ?? 8192,
1405
+ defaultContextWindow: this.options.defaultContextWindow ?? 128e3
1406
+ };
1407
+ }
1408
+ /** Run the discovery hook, memoized, tolerating failure. */
1409
+ async resolveCatalog(provider, baseUrl, headers, signal, context) {
1410
+ const discover = this.options.discoverModels;
1411
+ if (discover === void 0) return [];
1412
+ const ttl = this.options.catalogTtlMs ?? DEFAULT_CATALOG_TTL_MS;
1413
+ const staleTtl = this.options.catalogStaleTtlMs ?? DEFAULT_CATALOG_STALE_TTL_MS;
1414
+ const failureBackoff = this.options.catalogFailureBackoffMs ?? DEFAULT_CATALOG_FAILURE_BACKOFF_MS;
1415
+ const now = Date.now();
1416
+ const cached = this.catalog;
1417
+ if (cached !== void 0 && now - cached.fetchedAt < ttl) return cached.models;
1418
+ if (this.catalogFailureAt !== void 0 && now - this.catalogFailureAt < failureBackoff) return staleCatalog(cached, now, ttl, staleTtl);
1419
+ try {
1420
+ const models = boundedCatalog(await observeModelCatalogOperation(context, provider, new URL(baseUrl).origin, async () => {
1421
+ const pending = discover({
1422
+ baseUrl,
1423
+ headers,
1424
+ provider,
1425
+ ...context === void 0 ? {} : { context },
1426
+ ...signal === void 0 ? {} : { signal }
1427
+ });
1428
+ return signal === void 0 ? await pending : await raceAbort(pending, signal);
1429
+ }), this.options.maxCatalogModels ?? DEFAULT_MAX_CATALOG_MODELS, this.options.maxCatalogBytes ?? DEFAULT_MAX_CATALOG_BYTES);
1430
+ this.catalog = {
1431
+ models,
1432
+ fetchedAt: Date.now()
1433
+ };
1434
+ this.catalogFailureAt = void 0;
1435
+ return models;
1436
+ } catch (error) {
1437
+ if (signal?.aborted === true) throw signal.reason ?? error;
1438
+ this.catalogFailureAt = Date.now();
1439
+ return staleCatalog(cached, this.catalogFailureAt, ttl, staleTtl);
1440
+ }
1441
+ }
1442
+ baseHeaders() {
1443
+ return {};
1444
+ }
1445
+ observeRequest(record) {
1446
+ return this.options.requestLogger?.(record);
1447
+ }
1448
+ providerErrorCode(status, detail) {
1449
+ return this.options.errorCode?.(status, detail) ?? super.providerErrorCode(status, detail);
1450
+ }
1451
+ endpointPath(request) {
1452
+ return this.options.protocol.endpointPath(request, this.dialect);
1453
+ }
1454
+ buildBody(request) {
1455
+ return this.options.protocol.serialize(request, this.dialect);
1456
+ }
1457
+ translate(events, request) {
1458
+ return this.options.protocol.translate(events, request, this.displayName);
1459
+ }
1460
+ };
1461
+ function positiveFinite(value, field) {
1462
+ if (!Number.isFinite(value) || value <= 0) throw new RangeError(`${field} must be a positive finite number`);
1463
+ return value;
1464
+ }
1465
+ function positiveSafeInteger(value, field) {
1466
+ if (!Number.isSafeInteger(value) || value <= 0) throw new RangeError(`${field} must be a positive safe integer`);
1467
+ return value;
1468
+ }
1469
+ function nonNegativeFinite(value, field) {
1470
+ if (!Number.isFinite(value) || value < 0) throw new RangeError(`${field} must be a non-negative finite number`);
1471
+ return value;
1472
+ }
1473
+ function staleCatalog(cached, now, ttl, staleTtl) {
1474
+ if (cached === void 0 || now - cached.fetchedAt >= ttl + staleTtl) return [];
1475
+ return cached.models;
1476
+ }
1477
+ /** Validate resource bounds before retaining a provider-controlled catalog. */
1478
+ function boundedCatalog(value, maxModels, maxBytes) {
1479
+ if (!Array.isArray(value)) throw new TypeError("model catalog must be an array");
1480
+ if (value.length > maxModels) throw new RangeError(`model catalog exceeds maxCatalogModels (${maxModels})`);
1481
+ let encoded;
1482
+ try {
1483
+ encoded = JSON.stringify(value);
1484
+ } catch (error) {
1485
+ throw new TypeError("model catalog must be JSON-serializable", { cause: error });
1486
+ }
1487
+ if (new TextEncoder().encode(encoded).byteLength > maxBytes) throw new RangeError(`model catalog exceeds maxCatalogBytes (${maxBytes})`);
1488
+ return detachedFrozen(value);
1489
+ }
1490
+ function raceAbort(pending, signal) {
1491
+ if (signal.aborted) return Promise.reject(signal.reason ?? /* @__PURE__ */ new Error("HTTP provider operation aborted"));
1492
+ return new Promise((resolve, reject) => {
1493
+ const abort = () => {
1494
+ cleanup();
1495
+ reject(signal.reason ?? /* @__PURE__ */ new Error("HTTP provider operation aborted"));
1496
+ };
1497
+ const cleanup = () => signal.removeEventListener("abort", abort);
1498
+ signal.addEventListener("abort", abort, { once: true });
1499
+ pending.then((value) => {
1500
+ cleanup();
1501
+ resolve(value);
1502
+ }, (error) => {
1503
+ cleanup();
1504
+ reject(error);
1505
+ });
1506
+ });
1507
+ }
1508
+ /**
1509
+ * Create a provider adapter from configuration.
1510
+ * @param options - protocol, endpoint, credential, and optional capability hooks.
1511
+ * @returns an adapter ready for `registry.registerAdapter`.
1512
+ */
1513
+ function createHttpProvider(options) {
1514
+ return new ConfiguredHttpAdapter(options);
1515
+ }
1516
+
1517
+ //#endregion
1518
+ //#region src/common/wire-body.ts
1519
+ const WIRE_BODY_LIMITS = Object.freeze({
1520
+ maxDepth: 64,
1521
+ maxNodes: 2e5,
1522
+ maxObjectFields: 1e5,
1523
+ maxArrayItems: 1e5,
1524
+ maxKeyBytes: 16384
1525
+ });
1526
+ /** Validate and detach one synchronous protocol JSON object before dispatch. */
1527
+ function snapshotWireBody(value, maxBytes) {
1528
+ if (isThenable(value)) throw new ModelError("runtime wire protocol serialize() must return synchronously", HTTP_PROVIDER_ERROR_CODES.WIRE_BODY_INVALID);
1529
+ try {
1530
+ return snapshotJsonObject(value, {
1531
+ ...WIRE_BODY_LIMITS,
1532
+ maxBytes
1533
+ });
1534
+ } catch (error) {
1535
+ const tooLarge = error instanceof Error && /byte bound/.test(error.message);
1536
+ throw new ModelError(tooLarge ? "runtime wire body exceeds maxRequestBytes" : "runtime wire body is not bounded JSON", tooLarge ? HTTP_PROVIDER_ERROR_CODES.WIRE_BODY_TOO_LARGE : HTTP_PROVIDER_ERROR_CODES.WIRE_BODY_INVALID, { cause: error });
1537
+ }
1538
+ }
1539
+ function isThenable(value) {
1540
+ if (value === null || typeof value !== "object" && typeof value !== "function") return false;
1541
+ const descriptor = Object.getOwnPropertyDescriptor(value, "then");
1542
+ if (descriptor !== void 0 && !("value" in descriptor)) return true;
1543
+ return descriptor !== void 0 && typeof descriptor.value === "function";
1544
+ }
1545
+
1546
+ //#endregion
1547
+ //#region src/configurable/runtime-provider.ts
1548
+ const NEVER_ABORTED_SIGNAL = new AbortController().signal;
1549
+ const NULL_LOGGER = Object.freeze({
1550
+ child: () => NULL_LOGGER,
1551
+ trace: () => void 0,
1552
+ debug: () => void 0,
1553
+ info: () => void 0,
1554
+ warn: () => void 0,
1555
+ error: () => void 0,
1556
+ fatal: () => void 0
1557
+ });
1558
+ /** Create the versioned HTTP extension adapter without performing credential or network I/O. */
1559
+ function createRuntimeHttpProvider(options) {
1560
+ const source = plainObject(options, "runtime HTTP provider options");
1561
+ const displayName = boundedIdentifier(ownData(source, "displayName"), 256, "displayName");
1562
+ const baseUrl = captureBaseUrl(ownData(source, "baseUrl"));
1563
+ const protocol = boundedRuntimeProtocol(captureRuntimeProtocol(ownData(source, "protocol")));
1564
+ const auth = captureRuntimeAuth(ownData(source, "auth"), baseUrl);
1565
+ const discover = optionalCapturedMethod(source, "discoverModels");
1566
+ const fetch = optionalCapturedMethod(source, "fetch");
1567
+ const describeModel = optionalCapturedMethod(source, "describeModel");
1568
+ const errorCode = optionalCapturedMethod(source, "errorCode");
1569
+ const requestLogger = optionalCapturedMethod(source, "requestLogger");
1570
+ const headers = captureHeaders(source);
1571
+ const legacy = {
1572
+ displayName,
1573
+ protocol,
1574
+ baseUrl: baseUrl.href,
1575
+ auth,
1576
+ ...copyOptional(source, "allowInsecureHttp"),
1577
+ ...copyJsonOptional(source, "models", "models"),
1578
+ ...copyJsonOptional(source, "dialect", "dialect"),
1579
+ ...fetch === void 0 ? {} : { fetch },
1580
+ ...headers === void 0 ? {} : { headers },
1581
+ ...copyOptional(source, "catalogTtlMs"),
1582
+ ...copyOptional(source, "catalogStaleTtlMs"),
1583
+ ...copyOptional(source, "catalogFailureBackoffMs"),
1584
+ ...copyOptional(source, "maxCatalogModels"),
1585
+ ...copyOptional(source, "maxCatalogBytes"),
1586
+ ...describeModel === void 0 ? {} : { describeModel },
1587
+ ...copyOptional(source, "defaultMaxTokens"),
1588
+ ...copyOptional(source, "defaultContextWindow"),
1589
+ ...copyOptional(source, "streamIdleTimeoutMs"),
1590
+ ...copyOptional(source, "requestTimeoutMs"),
1591
+ ...copyOptional(source, "maxRequestBytes"),
1592
+ ...copyOptional(source, "maxResponseBytes"),
1593
+ ...copyOptional(source, "maxResponseChunks"),
1594
+ ...copyOptional(source, "maxSseEvents"),
1595
+ ...copyOptional(source, "maxSseEventChars"),
1596
+ ...copyOptional(source, "maxErrorBodyBytes"),
1597
+ ...copyOptional(source, "requestLoggerTimeoutMs"),
1598
+ ...copyJsonOptional(source, "retryPolicy", "retryPolicy"),
1599
+ ...errorCode === void 0 ? {} : { errorCode },
1600
+ ...copyHeaderOptional(source, "baseHeaders", "transport"),
1601
+ ...requestLogger === void 0 ? {} : { requestLogger },
1602
+ ...discover === void 0 ? {} : { discoverModels: async (context) => discover({
1603
+ provider: context.provider ?? "",
1604
+ baseUrl: new URL(context.baseUrl),
1605
+ headers: context.headers,
1606
+ signal: context.signal ?? NEVER_ABORTED_SIGNAL,
1607
+ ...context.context === void 0 ? {} : { context: context.context }
1608
+ }) }
1609
+ };
1610
+ return createHttpProvider(legacy);
1611
+ }
1612
+ function boundedRuntimeProtocol(protocol) {
1613
+ return Object.freeze({
1614
+ ...protocol,
1615
+ serialize(request, dialect) {
1616
+ return snapshotWireBody(protocol.serialize(request, dialect), request.connection.maxRequestBytes ?? 33554432);
1617
+ }
1618
+ });
1619
+ }
1620
+ function captureRuntimeProtocol(value) {
1621
+ try {
1622
+ const source = plainObject(value, "runtime wire protocol");
1623
+ if (ownData(source, "kind") !== "http-wire-protocol" || ownData(source, "apiVersion") !== 1) throw new TypeError("unsupported protocol marker");
1624
+ const endpointPath = capturedMethod(source, "endpointPath");
1625
+ const protocolHeaders = optionalCapturedMethod(source, "protocolHeaders");
1626
+ const serialize = capturedMethod(source, "serialize");
1627
+ const translate = capturedMethod(source, "translate");
1628
+ return defineWireProtocol({
1629
+ id: ownData(source, "id"),
1630
+ defaultDialect: ownData(source, "defaultDialect"),
1631
+ endpointPath,
1632
+ ...protocolHeaders === void 0 ? {} : { protocolHeaders },
1633
+ serialize,
1634
+ translate
1635
+ });
1636
+ } catch (error) {
1637
+ throw new AgentSdkError("Runtime HTTP protocol is incompatible", HTTP_PROVIDER_ERROR_CODES.PROTOCOL_API_UNSUPPORTED, { cause: error });
1638
+ }
1639
+ }
1640
+ function captureRuntimeAuth(value, baseUrl) {
1641
+ const source = plainObject(value, "runtime HTTP auth");
1642
+ const kind = ownData(source, "kind");
1643
+ if (kind === "none") return Object.freeze({ kind });
1644
+ if (kind === "bearer") return Object.freeze({
1645
+ kind,
1646
+ token: captureCredential(ownData(source, "token")),
1647
+ ...copyOptional(source, "label")
1648
+ });
1649
+ if (kind === "header") {
1650
+ const name = boundedIdentifier(ownData(source, "name"), 256, "auth header name");
1651
+ return Object.freeze({
1652
+ kind,
1653
+ name,
1654
+ value: captureCredential(ownData(source, "value")),
1655
+ ...copyOptional(source, "label")
1656
+ });
1657
+ }
1658
+ if (kind === "dynamic") {
1659
+ const resolve = capturedMethod(source, "resolve");
1660
+ return Object.freeze({
1661
+ kind,
1662
+ resolve: async (signal, context, provider = "") => ({ ...await resolve({
1663
+ provider,
1664
+ baseUrl,
1665
+ signal: signal ?? NEVER_ABORTED_SIGNAL,
1666
+ ...context === void 0 ? {} : { context }
1667
+ }) })
1668
+ });
1669
+ }
1670
+ throw new AgentSdkError("Runtime HTTP auth is invalid", HTTP_PROVIDER_ERROR_CODES.HEADER_INVALID);
1671
+ }
1672
+ function captureCredential(input) {
1673
+ if (typeof input === "string") return input;
1674
+ const source = typeof input === "function" ? input : plainObject(input, "credential source");
1675
+ if (ownData(source, "kind") !== "credential-source" || ownData(source, "apiVersion") !== CREDENTIAL_CAPABILITY_API_VERSION) throw new AgentSdkError("Credential source is incompatible", "CREDENTIAL_SOURCE_INVALID");
1676
+ const resolve = capturedMethod(source, "resolve");
1677
+ return (signal, context) => resolve({
1678
+ signal: signal ?? NEVER_ABORTED_SIGNAL,
1679
+ logger: context?.logger ?? NULL_LOGGER
1680
+ });
1681
+ }
1682
+ function captureBaseUrl(value) {
1683
+ if (value instanceof URL) return new URL(value.href);
1684
+ if (typeof value === "string") return new URL(value);
1685
+ throw new TypeError("baseUrl must be an absolute URL or URL string");
1686
+ }
1687
+ function copyOptional(source, key) {
1688
+ const value = ownData(source, key, false);
1689
+ return value === void 0 ? {} : { [key]: value };
1690
+ }
1691
+ function copyJsonOptional(source, key, envelopeKey) {
1692
+ const value = ownData(source, key, false);
1693
+ if (value === void 0) return {};
1694
+ const snapshot = snapshotJsonObject({ [envelopeKey]: value }, HTTP_RUNTIME_OPTION_LIMITS);
1695
+ return { [key]: snapshot[envelopeKey] };
1696
+ }
1697
+ function copyHeaderOptional(source, key, layer) {
1698
+ const value = ownData(source, key, false);
1699
+ return value === void 0 ? {} : { [key]: snapshotHeaders(value, layer) };
1700
+ }
1701
+ function captureHeaders(source) {
1702
+ const value = ownData(source, "headers", false);
1703
+ if (value === void 0) return void 0;
1704
+ if (typeof value !== "function") return snapshotHeaders(value, "endpoint");
1705
+ const captured = (...args) => Reflect.apply(value, source, args);
1706
+ return () => snapshotHeaders(captured(), "endpoint");
1707
+ }
1708
+ function snapshotHeaders(value, layer) {
1709
+ return mergeHeaderLayers([{
1710
+ layer,
1711
+ headers: value
1712
+ }]).headers;
1713
+ }
1714
+
1715
+ //#endregion
1716
+ //#region src/stream/sse.ts
1717
+ /**
1718
+ * Decode an SSE byte stream into events.
1719
+ *
1720
+ * All the genuinely hard framing work  Echunk reassembly, UTF-8 sequences split
1721
+ * across reads, CRLF and BOM handling, comment and unknown-field skipping,
1722
+ * joining multiple `data:` lines of one event  Ebelongs to `eventsource-parser`.
1723
+ *
1724
+ * Note what is deliberately NOT decided here. OpenAI terminates with a literal
1725
+ * `data: [DONE]` sentinel; Anthropic terminates with a named `message_stop` event
1726
+ * and sends no sentinel at all. Baking in either rule would make the parser lie
1727
+ * about the other, so termination is the adapter's call and this generator simply
1728
+ * runs to the end of the body.
1729
+ *
1730
+ * The callback-based parser is used rather than `EventSourceParserStream` so the
1731
+ * SDK does not require `TextDecoderStream` to exist  Eit is absent on some
1732
+ * runtimes this package should still work on.
1733
+ *
1734
+ * @module @alvin0/ai-agent-sdk-provider-http/sse
1735
+ */
1736
+ /**
1737
+ * Parse an SSE byte stream into events, in arrival order.
1738
+ *
1739
+ * Framing is spec-strict: an event dispatches only on its blank-line terminator,
1740
+ * so an unterminated tail at EOF is truncation rather than a flushable payload.
1741
+ * @param stream - raw SSE bytes, as `Response.body` provides them. Reads may split
1742
+ * anywhere, including mid-codepoint; the streaming decoder handles that.
1743
+ * @param onActivity - called on every frame INCLUDING comments. Providers send
1744
+ * comment-only keepalives during long pauses, so a liveness watchdog has to
1745
+ * count them as activity even though they carry no data.
1746
+ * @returns each event in arrival order; returns normally at end of body.
1747
+ */
1748
+ async function* parseSse(stream, onActivity, teardownTimeoutMs = DEFAULT_SSE_TEARDOWN_TIMEOUT_MS) {
1749
+ yield* parseSseBounded(stream, onActivity, teardownTimeoutMs, {
1750
+ maxEvents: DEFAULT_MAX_SSE_EVENTS,
1751
+ maxEventChars: DEFAULT_MAX_SSE_EVENT_CHARS
1752
+ });
1753
+ }
1754
+
1755
+ //#endregion
1756
+ export { DEFAULT_MAX_ERROR_BODY_BYTES, DEFAULT_MAX_REQUEST_BYTES, DEFAULT_MAX_RESPONSE_BYTES, DEFAULT_MAX_RESPONSE_CHUNKS, DEFAULT_REQUEST_LOGGER_TIMEOUT_MS, DEFAULT_REQUEST_TIMEOUT_MS, DEFAULT_STREAM_IDLE_TIMEOUT_MS, HTTP_PROTOCOL_API_VERSION, HTTP_PROVIDER_ERROR_CODES, HttpModelAdapter, createHttpProvider, createRuntimeHttpProvider, defineWireProtocol, httpErrorCode, observeCredentialOperation, observeModelCatalogOperation, parseErrorBody, parseSse, redactHeaders, requestIdFrom, resolveDialect, retryAfterMs };
1757
+ //# sourceMappingURL=index.js.map