@providerprotocol/ai 0.0.39 → 0.0.40
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 +269 -34
- package/dist/anthropic/index.d.ts +3 -3
- package/dist/anthropic/index.js +7 -5
- package/dist/anthropic/index.js.map +1 -1
- package/dist/cerebras/index.d.ts +3 -3
- package/dist/cerebras/index.js +7 -5
- package/dist/cerebras/index.js.map +1 -1
- package/dist/{chunk-WU4U6IHF.js → chunk-6QCV4WXF.js} +4 -13
- package/dist/chunk-6QCV4WXF.js.map +1 -0
- package/dist/{chunk-5XPRVUOK.js → chunk-AC3VHSZJ.js} +2 -2
- package/dist/{chunk-5XPRVUOK.js.map → chunk-AC3VHSZJ.js.map} +1 -1
- package/dist/{chunk-ZDYEDI2A.js → chunk-CWGTARDE.js} +2 -2
- package/dist/{chunk-KNBODIQU.js → chunk-DI47UY2H.js} +2 -2
- package/dist/{chunk-KNBODIQU.js.map → chunk-DI47UY2H.js.map} +1 -1
- package/dist/{chunk-IDZR4ROP.js → chunk-EHR3LIPS.js} +2 -2
- package/dist/{chunk-IDZR4ROP.js.map → chunk-EHR3LIPS.js.map} +1 -1
- package/dist/chunk-EY2LLDGY.js +94 -0
- package/dist/chunk-EY2LLDGY.js.map +1 -0
- package/dist/{chunk-MJI74VEJ.js → chunk-F5ENANMJ.js} +18 -2
- package/dist/chunk-F5ENANMJ.js.map +1 -0
- package/dist/chunk-IKJH5ZSJ.js +1 -0
- package/dist/chunk-IKJH5ZSJ.js.map +1 -0
- package/dist/{chunk-IIMTP3XC.js → chunk-KBI45OXI.js} +2 -2
- package/dist/{chunk-SAMIK4WZ.js → chunk-KVUOTFYZ.js} +2 -2
- package/dist/{chunk-U6M3MXNI.js → chunk-L6QWKFGE.js} +3 -2
- package/dist/chunk-L6QWKFGE.js.map +1 -0
- package/dist/{chunk-RDC5GYST.js → chunk-N4LAFGLX.js} +7 -7
- package/dist/{chunk-ZKNPQBIE.js → chunk-R3T2IYOU.js} +5 -3
- package/dist/{chunk-ZKNPQBIE.js.map → chunk-R3T2IYOU.js.map} +1 -1
- package/dist/chunk-U2G5PHHL.js +25 -0
- package/dist/chunk-U2G5PHHL.js.map +1 -0
- package/dist/{chunk-SBGZJVTJ.js → chunk-VQZPADW6.js} +100 -33
- package/dist/chunk-VQZPADW6.js.map +1 -0
- package/dist/{chunk-O32SBS6S.js → chunk-XTWBAL42.js} +2 -2
- package/dist/{chunk-O32SBS6S.js.map → chunk-XTWBAL42.js.map} +1 -1
- package/dist/{chunk-WNB5PSY6.js → chunk-ZMESKGUY.js} +2 -2
- package/dist/{chunk-7ULSRWDH.js → chunk-ZSZVWLGE.js} +2 -2
- package/dist/{embedding-iNQCeXfk.d.ts → embedding-ts1npsDg.d.ts} +1 -1
- package/dist/google/index.d.ts +38 -4
- package/dist/google/index.js +5 -4
- package/dist/google/index.js.map +1 -1
- package/dist/groq/index.d.ts +3 -3
- package/dist/groq/index.js +7 -5
- package/dist/groq/index.js.map +1 -1
- package/dist/http/index.d.ts +5 -5
- package/dist/http/index.js +19 -22
- package/dist/{image-stream-ARno6XlS.d.ts → image-stream-BPml2YZZ.d.ts} +1 -1
- package/dist/index.d.ts +8 -8
- package/dist/index.js +306 -112
- package/dist/index.js.map +1 -1
- package/dist/{llm-CZqlijjK.d.ts → llm-BWLaTzzY.d.ts} +75 -29
- package/dist/middleware/logging/index.d.ts +3 -3
- package/dist/middleware/logging/index.js +3 -0
- package/dist/middleware/logging/index.js.map +1 -1
- package/dist/middleware/parsed-object/index.d.ts +3 -3
- package/dist/middleware/parsed-object/index.js +5 -1
- package/dist/middleware/parsed-object/index.js.map +1 -1
- package/dist/middleware/persistence/index.d.ts +3 -3
- package/dist/middleware/persistence/index.js +3 -2
- package/dist/middleware/persistence/index.js.map +1 -1
- package/dist/middleware/pipeline/index.d.ts +195 -0
- package/dist/middleware/pipeline/index.js +61 -0
- package/dist/middleware/pipeline/index.js.map +1 -0
- package/dist/middleware/pubsub/index.d.ts +13 -11
- package/dist/middleware/pubsub/index.js +31 -5
- package/dist/middleware/pubsub/index.js.map +1 -1
- package/dist/middleware/pubsub/server/express/index.d.ts +3 -3
- package/dist/middleware/pubsub/server/express/index.js +2 -2
- package/dist/middleware/pubsub/server/fastify/index.d.ts +3 -3
- package/dist/middleware/pubsub/server/fastify/index.js +2 -2
- package/dist/middleware/pubsub/server/h3/index.d.ts +3 -3
- package/dist/middleware/pubsub/server/h3/index.js +2 -2
- package/dist/middleware/pubsub/server/index.d.ts +50 -9
- package/dist/middleware/pubsub/server/index.js +5 -5
- package/dist/middleware/pubsub/server/index.js.map +1 -1
- package/dist/middleware/pubsub/server/webapi/index.d.ts +3 -3
- package/dist/middleware/pubsub/server/webapi/index.js +2 -2
- package/dist/moonshot/index.d.ts +3 -3
- package/dist/moonshot/index.js +7 -5
- package/dist/moonshot/index.js.map +1 -1
- package/dist/ollama/index.d.ts +24 -4
- package/dist/ollama/index.js +5 -4
- package/dist/ollama/index.js.map +1 -1
- package/dist/openai/index.d.ts +65 -4
- package/dist/openai/index.js +7 -5
- package/dist/openai/index.js.map +1 -1
- package/dist/openrouter/index.d.ts +4 -4
- package/dist/openrouter/index.js +7 -5
- package/dist/openrouter/index.js.map +1 -1
- package/dist/proxy/index.d.ts +5 -5
- package/dist/proxy/index.js +16 -15
- package/dist/proxy/index.js.map +1 -1
- package/dist/proxy/server/express/index.d.ts +8 -9
- package/dist/proxy/server/express/index.js +4 -3
- package/dist/proxy/server/fastify/index.d.ts +8 -9
- package/dist/proxy/server/fastify/index.js +4 -3
- package/dist/proxy/server/h3/index.d.ts +8 -9
- package/dist/proxy/server/h3/index.js +4 -3
- package/dist/proxy/server/index.d.ts +5 -5
- package/dist/proxy/server/index.js +14 -13
- package/dist/proxy/server/webapi/index.d.ts +8 -9
- package/dist/proxy/server/webapi/index.js +4 -3
- package/dist/responses/index.d.ts +3 -3
- package/dist/responses/index.js +7 -5
- package/dist/responses/index.js.map +1 -1
- package/dist/retry-DVfdPLIB.d.ts +322 -0
- package/dist/{stream-DVVUIKpz.d.ts → stream-bBd_4Ipu.d.ts} +27 -4
- package/dist/{tool-D22EhP5F.d.ts → tool-BmAfKNBq.d.ts} +1 -1
- package/dist/{types-CyXF0J7C.d.ts → types-nTwlpyJE.d.ts} +13 -1
- package/dist/utils/index.d.ts +66 -2
- package/dist/utils/index.js +13 -0
- package/dist/xai/index.d.ts +3 -3
- package/dist/xai/index.js +7 -5
- package/dist/xai/index.js.map +1 -1
- package/package.json +6 -1
- package/dist/chunk-ARVM24K2.js +0 -128
- package/dist/chunk-ARVM24K2.js.map +0 -1
- package/dist/chunk-MJI74VEJ.js.map +0 -1
- package/dist/chunk-SBGZJVTJ.js.map +0 -1
- package/dist/chunk-U6M3MXNI.js.map +0 -1
- package/dist/chunk-WU4U6IHF.js.map +0 -1
- package/dist/chunk-Y5H7C5J4.js +0 -263
- package/dist/chunk-Y5H7C5J4.js.map +0 -1
- package/dist/retry-C1eJbEMV.d.ts +0 -531
- /package/dist/{chunk-ZDYEDI2A.js.map → chunk-CWGTARDE.js.map} +0 -0
- /package/dist/{chunk-IIMTP3XC.js.map → chunk-KBI45OXI.js.map} +0 -0
- /package/dist/{chunk-SAMIK4WZ.js.map → chunk-KVUOTFYZ.js.map} +0 -0
- /package/dist/{chunk-RDC5GYST.js.map → chunk-N4LAFGLX.js.map} +0 -0
- /package/dist/{chunk-WNB5PSY6.js.map → chunk-ZMESKGUY.js.map} +0 -0
- /package/dist/{chunk-7ULSRWDH.js.map → chunk-ZSZVWLGE.js.map} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@providerprotocol/ai",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.40",
|
|
4
4
|
"description": "UPP: Unified Provider Protocol for AI inference",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://providerprotocol.org",
|
|
@@ -92,6 +92,11 @@
|
|
|
92
92
|
"import": "./dist/middleware/persistence/index.js",
|
|
93
93
|
"default": "./dist/middleware/persistence/index.js"
|
|
94
94
|
},
|
|
95
|
+
"./middleware/pipeline": {
|
|
96
|
+
"types": "./dist/middleware/pipeline/index.d.ts",
|
|
97
|
+
"import": "./dist/middleware/pipeline/index.js",
|
|
98
|
+
"default": "./dist/middleware/pipeline/index.js"
|
|
99
|
+
},
|
|
95
100
|
"./middleware/pubsub": {
|
|
96
101
|
"types": "./dist/middleware/pubsub/index.d.ts",
|
|
97
102
|
"import": "./dist/middleware/pubsub/index.js",
|
package/dist/chunk-ARVM24K2.js
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ErrorCode,
|
|
3
|
-
UPPError
|
|
4
|
-
} from "./chunk-COS4ON4G.js";
|
|
5
|
-
|
|
6
|
-
// src/http/keys.ts
|
|
7
|
-
var RoundRobinKeys = class {
|
|
8
|
-
keys;
|
|
9
|
-
index = 0;
|
|
10
|
-
/**
|
|
11
|
-
* Creates a new RoundRobinKeys instance.
|
|
12
|
-
*
|
|
13
|
-
* @param keys - Array of API keys to rotate through
|
|
14
|
-
* @throws {Error} When the keys array is empty
|
|
15
|
-
*/
|
|
16
|
-
constructor(keys) {
|
|
17
|
-
if (keys.length === 0) {
|
|
18
|
-
throw new Error("RoundRobinKeys requires at least one key");
|
|
19
|
-
}
|
|
20
|
-
this.keys = keys;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Returns the next key in the rotation sequence.
|
|
24
|
-
*
|
|
25
|
-
* @returns The next API key in round-robin order
|
|
26
|
-
*/
|
|
27
|
-
getKey() {
|
|
28
|
-
const key = this.keys[this.index];
|
|
29
|
-
this.index = (this.index + 1) % this.keys.length;
|
|
30
|
-
return key;
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
var WeightedKeys = class {
|
|
34
|
-
entries;
|
|
35
|
-
totalWeight;
|
|
36
|
-
/**
|
|
37
|
-
* Creates a new WeightedKeys instance.
|
|
38
|
-
*
|
|
39
|
-
* @param keys - Array of key-weight pairs defining selection probabilities
|
|
40
|
-
* @throws {Error} When the keys array is empty
|
|
41
|
-
*/
|
|
42
|
-
constructor(keys) {
|
|
43
|
-
if (keys.length === 0) {
|
|
44
|
-
throw new Error("WeightedKeys requires at least one key");
|
|
45
|
-
}
|
|
46
|
-
this.entries = keys;
|
|
47
|
-
this.totalWeight = keys.reduce((sum, k) => sum + k.weight, 0);
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Returns a randomly selected key based on configured weights.
|
|
51
|
-
*
|
|
52
|
-
* @returns An API key selected with probability proportional to its weight
|
|
53
|
-
*/
|
|
54
|
-
getKey() {
|
|
55
|
-
const random = Math.random() * this.totalWeight;
|
|
56
|
-
let cumulative = 0;
|
|
57
|
-
for (const entry of this.entries) {
|
|
58
|
-
cumulative += entry.weight;
|
|
59
|
-
if (random <= cumulative) {
|
|
60
|
-
return entry.key;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return this.entries[this.entries.length - 1].key;
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
var DynamicKey = class {
|
|
67
|
-
selector;
|
|
68
|
-
/**
|
|
69
|
-
* Creates a new DynamicKey instance.
|
|
70
|
-
*
|
|
71
|
-
* @param selector - Function that returns an API key (sync or async)
|
|
72
|
-
*/
|
|
73
|
-
constructor(selector) {
|
|
74
|
-
this.selector = selector;
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Invokes the selector function to retrieve the current key.
|
|
78
|
-
*
|
|
79
|
-
* @returns Promise resolving to the selected API key
|
|
80
|
-
*/
|
|
81
|
-
async getKey() {
|
|
82
|
-
return this.selector();
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
function maskApiKey(key) {
|
|
86
|
-
if (key.length <= 8) {
|
|
87
|
-
return "***";
|
|
88
|
-
}
|
|
89
|
-
return `${key.slice(0, 4)}...${key.slice(-4)}`;
|
|
90
|
-
}
|
|
91
|
-
function isKeyStrategy(value) {
|
|
92
|
-
return typeof value === "object" && value !== null && "getKey" in value && typeof value.getKey === "function";
|
|
93
|
-
}
|
|
94
|
-
async function resolveApiKey(config, envVar, provider = "unknown", modality = "llm") {
|
|
95
|
-
const { apiKey } = config;
|
|
96
|
-
if (apiKey !== void 0) {
|
|
97
|
-
if (typeof apiKey === "string") {
|
|
98
|
-
return apiKey;
|
|
99
|
-
}
|
|
100
|
-
if (typeof apiKey === "function") {
|
|
101
|
-
return apiKey();
|
|
102
|
-
}
|
|
103
|
-
if (isKeyStrategy(apiKey)) {
|
|
104
|
-
return apiKey.getKey();
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
if (envVar) {
|
|
108
|
-
const envValue = process.env[envVar];
|
|
109
|
-
if (envValue) {
|
|
110
|
-
return envValue;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
throw new UPPError(
|
|
114
|
-
envVar ? `API key not found. Set ${envVar} environment variable or provide apiKey in config.` : "API key not found. Provide apiKey in config.",
|
|
115
|
-
ErrorCode.AuthenticationFailed,
|
|
116
|
-
provider,
|
|
117
|
-
modality
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export {
|
|
122
|
-
RoundRobinKeys,
|
|
123
|
-
WeightedKeys,
|
|
124
|
-
DynamicKey,
|
|
125
|
-
maskApiKey,
|
|
126
|
-
resolveApiKey
|
|
127
|
-
};
|
|
128
|
-
//# sourceMappingURL=chunk-ARVM24K2.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/http/keys.ts"],"sourcesContent":["/**\n * API key management strategies for load balancing and dynamic key selection.\n * @module http/keys\n */\n\nimport type { ProviderConfig, KeyStrategy } from '../types/provider.ts';\nimport { ErrorCode, UPPError, type Modality } from '../types/errors.ts';\n\n/**\n * Distributes API requests across multiple keys using round-robin selection.\n *\n * Each call to {@link getKey} returns the next key in sequence, cycling back to\n * the first key after reaching the end. This provides even distribution of requests\n * across all available keys, which is useful for:\n * - Spreading rate limits across multiple API keys\n * - Load balancing between different accounts\n * - Maximizing throughput when multiple keys are available\n *\n * @implements {KeyStrategy}\n *\n * @example\n * ```typescript\n * const keys = new RoundRobinKeys([\n * 'sk-key-1',\n * 'sk-key-2',\n * 'sk-key-3'\n * ]);\n *\n * keys.getKey(); // Returns 'sk-key-1'\n * keys.getKey(); // Returns 'sk-key-2'\n * keys.getKey(); // Returns 'sk-key-3'\n * keys.getKey(); // Returns 'sk-key-1' (cycles back)\n * ```\n */\nexport class RoundRobinKeys implements KeyStrategy {\n private keys: string[];\n private index = 0;\n\n /**\n * Creates a new RoundRobinKeys instance.\n *\n * @param keys - Array of API keys to rotate through\n * @throws {Error} When the keys array is empty\n */\n constructor(keys: string[]) {\n if (keys.length === 0) {\n throw new Error('RoundRobinKeys requires at least one key');\n }\n this.keys = keys;\n }\n\n /**\n * Returns the next key in the rotation sequence.\n *\n * @returns The next API key in round-robin order\n */\n getKey(): string {\n const key = this.keys[this.index]!;\n this.index = (this.index + 1) % this.keys.length;\n return key;\n }\n}\n\n/**\n * Selects API keys using weighted random probability.\n *\n * Each key is assigned a weight that determines its probability of being selected.\n * Higher weights mean higher selection probability. This is useful for:\n * - Preferring higher-tier API keys with better rate limits\n * - Gradually migrating traffic between old and new keys\n * - A/B testing different API accounts\n * - Directing more traffic to keys with higher quotas\n *\n * The selection probability for each key is: weight / totalWeight\n *\n * @implements {KeyStrategy}\n *\n * @example\n * ```typescript\n * const keys = new WeightedKeys([\n * { key: 'sk-premium', weight: 70 }, // 70% of requests\n * { key: 'sk-standard', weight: 20 }, // 20% of requests\n * { key: 'sk-backup', weight: 10 } // 10% of requests\n * ]);\n *\n * // Configure provider with weighted key selection\n * const provider = createOpenAI({\n * apiKey: keys\n * });\n * ```\n */\nexport class WeightedKeys implements KeyStrategy {\n private entries: Array<{ key: string; weight: number }>;\n private totalWeight: number;\n\n /**\n * Creates a new WeightedKeys instance.\n *\n * @param keys - Array of key-weight pairs defining selection probabilities\n * @throws {Error} When the keys array is empty\n */\n constructor(keys: Array<{ key: string; weight: number }>) {\n if (keys.length === 0) {\n throw new Error('WeightedKeys requires at least one key');\n }\n this.entries = keys;\n this.totalWeight = keys.reduce((sum, k) => sum + k.weight, 0);\n }\n\n /**\n * Returns a randomly selected key based on configured weights.\n *\n * @returns An API key selected with probability proportional to its weight\n */\n getKey(): string {\n const random = Math.random() * this.totalWeight;\n let cumulative = 0;\n\n for (const entry of this.entries) {\n cumulative += entry.weight;\n if (random <= cumulative) {\n return entry.key;\n }\n }\n\n return this.entries[this.entries.length - 1]!.key;\n }\n}\n\n/**\n * Provides dynamic key selection using custom logic.\n *\n * This strategy delegates key selection to a user-provided function, enabling\n * advanced scenarios such as:\n * - Fetching keys from a secrets manager (AWS Secrets Manager, HashiCorp Vault)\n * - Rotating keys based on external state or configuration\n * - Selecting keys based on request context or time of day\n * - Implementing custom load balancing algorithms\n *\n * The selector function can be synchronous or asynchronous.\n *\n * @implements {KeyStrategy}\n *\n * @example\n * ```typescript\n * // Fetch key from environment based on current mode\n * const dynamicKey = new DynamicKey(() => {\n * return process.env.NODE_ENV === 'production'\n * ? process.env.PROD_API_KEY!\n * : process.env.DEV_API_KEY!;\n * });\n *\n * // Async key fetching from a secrets manager\n * const vaultKey = new DynamicKey(async () => {\n * const secret = await vault.read('secret/openai');\n * return secret.data.apiKey;\n * });\n *\n * // Time-based key rotation\n * const timedKey = new DynamicKey(() => {\n * const hour = new Date().getHours();\n * return hour < 12 ? morningKey : afternoonKey;\n * });\n * ```\n */\nexport class DynamicKey implements KeyStrategy {\n private selector: () => string | Promise<string>;\n\n /**\n * Creates a new DynamicKey instance.\n *\n * @param selector - Function that returns an API key (sync or async)\n */\n constructor(selector: () => string | Promise<string>) {\n this.selector = selector;\n }\n\n /**\n * Invokes the selector function to retrieve the current key.\n *\n * @returns Promise resolving to the selected API key\n */\n async getKey(): Promise<string> {\n return this.selector();\n }\n}\n\n/**\n * Masks an API key for safe logging.\n * Shows first 4 and last 4 characters with ellipsis, or '***' for short keys.\n *\n * @param key - The API key to mask\n * @returns Masked key like \"sk-ab...yz12\" or \"***\" for short keys\n *\n * @example\n * ```typescript\n * maskApiKey('sk-abc123def456xyz789'); // 'sk-a...z789'\n * maskApiKey('short'); // '***'\n * ```\n */\nexport function maskApiKey(key: string): string {\n if (key.length <= 8) {\n return '***';\n }\n return `${key.slice(0, 4)}...${key.slice(-4)}`;\n}\n\n/**\n * Type guard to check if a value implements the KeyStrategy interface.\n *\n * @param value - The value to check\n * @returns True if the value has a getKey method\n */\nfunction isKeyStrategy(value: unknown): value is KeyStrategy {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'getKey' in value &&\n typeof (value as KeyStrategy).getKey === 'function'\n );\n}\n\n/**\n * Resolves an API key from provider configuration with multiple fallback options.\n *\n * This function handles various key specification methods in priority order:\n * 1. Direct string key in config.apiKey\n * 2. Function returning a key (sync or async) in config.apiKey\n * 3. KeyStrategy instance in config.apiKey (RoundRobinKeys, WeightedKeys, DynamicKey)\n * 4. Environment variable fallback (if envVar parameter is provided)\n *\n * @param config - Provider configuration containing the apiKey option\n * @param envVar - Optional environment variable name to check as fallback\n * @param provider - Provider identifier for error context (default: 'unknown')\n * @param modality - Request modality for error context (default: 'llm')\n * @returns The resolved API key string\n *\n * @throws {UPPError} AUTHENTICATION_FAILED - When no valid key is found\n *\n * @example\n * ```typescript\n * // Direct key in config\n * const key1 = await resolveApiKey({ apiKey: 'sk-...' }, 'OPENAI_API_KEY', 'openai');\n *\n * // Function-based key\n * const key2 = await resolveApiKey({ apiKey: () => getKeyFromVault() }, undefined, 'anthropic');\n *\n * // KeyStrategy instance\n * const key3 = await resolveApiKey({\n * apiKey: new RoundRobinKeys(['sk-1', 'sk-2', 'sk-3'])\n * }, 'OPENAI_API_KEY', 'openai');\n *\n * // Environment variable fallback\n * const key4 = await resolveApiKey({}, 'ANTHROPIC_API_KEY', 'anthropic');\n * ```\n */\nexport async function resolveApiKey(\n config: ProviderConfig,\n envVar?: string,\n provider = 'unknown',\n modality: Modality = 'llm'\n): Promise<string> {\n const { apiKey } = config;\n\n if (apiKey !== undefined) {\n if (typeof apiKey === 'string') {\n return apiKey;\n }\n\n if (typeof apiKey === 'function') {\n return apiKey();\n }\n\n if (isKeyStrategy(apiKey)) {\n return apiKey.getKey();\n }\n }\n\n if (envVar) {\n const envValue = process.env[envVar];\n if (envValue) {\n return envValue;\n }\n }\n\n throw new UPPError(\n envVar\n ? `API key not found. Set ${envVar} environment variable or provide apiKey in config.`\n : 'API key not found. Provide apiKey in config.',\n ErrorCode.AuthenticationFailed,\n provider,\n modality\n );\n}\n"],"mappings":";;;;;;AAkCO,IAAM,iBAAN,MAA4C;AAAA,EACzC;AAAA,EACA,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQhB,YAAY,MAAgB;AAC1B,QAAI,KAAK,WAAW,GAAG;AACrB,YAAM,IAAI,MAAM,0CAA0C;AAAA,IAC5D;AACA,SAAK,OAAO;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,SAAiB;AACf,UAAM,MAAM,KAAK,KAAK,KAAK,KAAK;AAChC,SAAK,SAAS,KAAK,QAAQ,KAAK,KAAK,KAAK;AAC1C,WAAO;AAAA,EACT;AACF;AA8BO,IAAM,eAAN,MAA0C;AAAA,EACvC;AAAA,EACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQR,YAAY,MAA8C;AACxD,QAAI,KAAK,WAAW,GAAG;AACrB,YAAM,IAAI,MAAM,wCAAwC;AAAA,IAC1D;AACA,SAAK,UAAU;AACf,SAAK,cAAc,KAAK,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,QAAQ,CAAC;AAAA,EAC9D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,SAAiB;AACf,UAAM,SAAS,KAAK,OAAO,IAAI,KAAK;AACpC,QAAI,aAAa;AAEjB,eAAW,SAAS,KAAK,SAAS;AAChC,oBAAc,MAAM;AACpB,UAAI,UAAU,YAAY;AACxB,eAAO,MAAM;AAAA,MACf;AAAA,IACF;AAEA,WAAO,KAAK,QAAQ,KAAK,QAAQ,SAAS,CAAC,EAAG;AAAA,EAChD;AACF;AAsCO,IAAM,aAAN,MAAwC;AAAA,EACrC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOR,YAAY,UAA0C;AACpD,SAAK,WAAW;AAAA,EAClB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,SAA0B;AAC9B,WAAO,KAAK,SAAS;AAAA,EACvB;AACF;AAeO,SAAS,WAAW,KAAqB;AAC9C,MAAI,IAAI,UAAU,GAAG;AACnB,WAAO;AAAA,EACT;AACA,SAAO,GAAG,IAAI,MAAM,GAAG,CAAC,CAAC,MAAM,IAAI,MAAM,EAAE,CAAC;AAC9C;AAQA,SAAS,cAAc,OAAsC;AAC3D,SACE,OAAO,UAAU,YACjB,UAAU,QACV,YAAY,SACZ,OAAQ,MAAsB,WAAW;AAE7C;AAoCA,eAAsB,cACpB,QACA,QACA,WAAW,WACX,WAAqB,OACJ;AACjB,QAAM,EAAE,OAAO,IAAI;AAEnB,MAAI,WAAW,QAAW;AACxB,QAAI,OAAO,WAAW,UAAU;AAC9B,aAAO;AAAA,IACT;AAEA,QAAI,OAAO,WAAW,YAAY;AAChC,aAAO,OAAO;AAAA,IAChB;AAEA,QAAI,cAAc,MAAM,GAAG;AACzB,aAAO,OAAO,OAAO;AAAA,IACvB;AAAA,EACF;AAEA,MAAI,QAAQ;AACV,UAAM,WAAW,QAAQ,IAAI,MAAM;AACnC,QAAI,UAAU;AACZ,aAAO;AAAA,IACT;AAAA,EACF;AAEA,QAAM,IAAI;AAAA,IACR,SACI,0BAA0B,MAAM,uDAChC;AAAA,IACJ,UAAU;AAAA,IACV;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/types/stream.ts"],"sourcesContent":["/**\n * @fileoverview Streaming types for real-time LLM responses.\n *\n * Defines the event types and interfaces for streaming LLM inference,\n * including text deltas, tool call deltas, and control events.\n *\n * @module types/stream\n */\n\nimport type { Turn } from './turn.ts';\n\n/**\n * Stream event type constants.\n *\n * Use these constants instead of raw strings for type-safe event handling:\n *\n * @example\n * ```typescript\n * import { StreamEventType } from 'upp';\n *\n * for await (const event of stream) {\n * if (event.type === StreamEventType.TextDelta) {\n * process.stdout.write(event.delta.text ?? '');\n * }\n * }\n * ```\n */\nexport const StreamEventType = {\n /** Incremental text output */\n TextDelta: 'text_delta',\n /** Incremental reasoning/thinking output */\n ReasoningDelta: 'reasoning_delta',\n /** Incremental image data */\n ImageDelta: 'image_delta',\n /** Incremental audio data */\n AudioDelta: 'audio_delta',\n /** Incremental video data */\n VideoDelta: 'video_delta',\n /** Incremental tool call data (arguments being streamed) */\n ToolCallDelta: 'tool_call_delta',\n /** Incremental structured object data (for structured output responses) */\n ObjectDelta: 'object_delta',\n /** Tool execution has started (may be emitted after completion in some implementations) */\n ToolExecutionStart: 'tool_execution_start',\n /** Tool execution has completed */\n ToolExecutionEnd: 'tool_execution_end',\n /** Beginning of a message */\n MessageStart: 'message_start',\n /** End of a message */\n MessageStop: 'message_stop',\n /** Beginning of a content block */\n ContentBlockStart: 'content_block_start',\n /** End of a content block */\n ContentBlockStop: 'content_block_stop',\n} as const;\n\n/**\n * Stream event type discriminator union.\n *\n * This type is derived from {@link StreamEventType} constants. Use `StreamEventType.TextDelta`\n * for constants or `type MyType = StreamEventType` for type annotations.\n */\nexport type StreamEventType = (typeof StreamEventType)[keyof typeof StreamEventType];\n\n/**\n * Event delta data payload.\n *\n * Contains the type-specific data for a streaming event.\n * Different fields are populated depending on the event type:\n *\n * | Event Type | Fields |\n * |------------|--------|\n * | `text_delta` | `text` |\n * | `reasoning_delta` | `text` |\n * | `object_delta` | `text` |\n * | `image_delta` | `data` |\n * | `audio_delta` | `data` |\n * | `video_delta` | `data` |\n * | `tool_call_delta` | `toolCallId`, `toolName`, `argumentsJson` |\n * | `tool_execution_start` | `toolCallId`, `toolName`, `timestamp` |\n * | `tool_execution_end` | `toolCallId`, `toolName`, `result`, `isError`, `timestamp` |\n * | `message_start` | (none) |\n * | `message_stop` | (none) |\n * | `content_block_start` | (none) |\n * | `content_block_stop` | (none) |\n */\nexport interface EventDelta {\n /** Incremental text content (text_delta, reasoning_delta, object_delta) */\n text?: string;\n\n /** Incremental binary data (image_delta, audio_delta, video_delta) */\n data?: Uint8Array;\n\n /** Tool call identifier (tool_call_delta, tool_execution_start/end) */\n toolCallId?: string;\n\n /** Tool name (tool_call_delta, tool_execution_start/end) */\n toolName?: string;\n\n /** Incremental JSON arguments string (tool_call_delta) */\n argumentsJson?: string;\n\n /** Tool execution result (tool_execution_end) */\n result?: unknown;\n\n /** Whether tool execution resulted in an error (tool_execution_end) */\n isError?: boolean;\n\n /** Timestamp in milliseconds (tool_execution_start/end) */\n timestamp?: number;\n}\n\n/**\n * A single streaming event from the LLM.\n *\n * Events are emitted in order as the model generates output,\n * allowing for real-time display of responses.\n *\n * @example\n * ```typescript\n * import { StreamEventType } from 'upp';\n *\n * for await (const event of stream) {\n * if (event.type === StreamEventType.TextDelta) {\n * process.stdout.write(event.delta.text ?? '');\n * } else if (event.type === StreamEventType.ToolCallDelta) {\n * console.log('Tool:', event.delta.toolName);\n * }\n * }\n * ```\n */\nexport interface StreamEvent {\n /** Event type discriminator */\n type: StreamEventType;\n\n /** Index of the content block this event belongs to */\n index: number;\n\n /** Event-specific data payload */\n delta: EventDelta;\n}\n\n/**\n * Stream result - an async iterable that also provides the final turn.\n *\n * Allows consuming streaming events while also awaiting the complete\n * Turn result after streaming finishes. Implements `PromiseLike<Turn>`\n * for direct awaiting with automatic stream consumption.\n *\n * @typeParam TData - Type of the structured output data\n *\n * @example\n * ```typescript\n * import { StreamEventType } from 'upp';\n *\n * const stream = instance.stream('Tell me a story');\n *\n * // Option 1: Consume streaming events manually\n * for await (const event of stream) {\n * if (event.type === StreamEventType.TextDelta) {\n * process.stdout.write(event.delta.text ?? '');\n * }\n * }\n * const turn = await stream.turn;\n *\n * // Option 2: Just await the turn (auto-drains the stream)\n * const turn = await instance.stream('Tell me a story');\n *\n * // Option 3: Fire-and-forget with callback\n * instance.stream('Tell me a story').then(turn => saveToDB(turn));\n * ```\n */\nexport interface StreamResult<TData = unknown>\n extends AsyncIterable<StreamEvent>, PromiseLike<Turn<TData>> {\n /**\n * Promise that resolves to the complete Turn after streaming finishes.\n * Rejects if the stream is aborted or terminated early.\n *\n * Accessing `turn` auto-drains the stream if it has not been iterated yet.\n */\n readonly turn: Promise<Turn<TData>>;\n\n /**\n * Aborts the stream, stopping further events and cancelling the request.\n * This will cause {@link StreamResult.turn} to reject.\n */\n abort(): void;\n}\n\n/**\n * Creates a StreamResult from an async generator and completion promise.\n *\n * @typeParam TData - Type of the structured output data\n * @param generator - Async generator that yields stream events\n * @param turnPromiseOrFactory - Promise or factory that resolves to the complete Turn\n * @param abortController - Controller for aborting the stream\n * @returns A StreamResult that can be iterated and awaited\n *\n * @example\n * ```typescript\n * const abortController = new AbortController();\n * const stream = createStreamResult(\n * eventGenerator(),\n * turnPromise,\n * abortController\n * );\n *\n * // Can be awaited directly (auto-drains)\n * const turn = await stream;\n *\n * // Or iterated manually\n * for await (const event of stream) { ... }\n * const turn = await stream.turn;\n * ```\n */\nexport function createStreamResult<TData = unknown>(\n generator: AsyncGenerator<StreamEvent, void, unknown>,\n turnPromiseOrFactory: Promise<Turn<TData>> | (() => Promise<Turn<TData>>),\n abortController: AbortController\n): StreamResult<TData> {\n let cachedTurn: Promise<Turn<TData>> | null = null;\n let drainStarted = false;\n let iteratorStarted = false;\n\n const getTurn = (): Promise<Turn<TData>> => {\n if (typeof turnPromiseOrFactory === 'function') {\n if (!cachedTurn) {\n cachedTurn = turnPromiseOrFactory();\n }\n return cachedTurn;\n }\n return turnPromiseOrFactory;\n };\n\n const drain = (): void => {\n if (drainStarted) return;\n drainStarted = true;\n void (async () => {\n try {\n let done = false;\n while (!done) {\n const result = await generator.next();\n done = result.done ?? false;\n }\n } catch {\n // Errors are surfaced via turn promise\n }\n })();\n };\n\n return {\n [Symbol.asyncIterator]() {\n iteratorStarted = true;\n return generator;\n },\n get turn() {\n if (!iteratorStarted) {\n drain();\n }\n return getTurn();\n },\n abort() {\n abortController.abort();\n },\n then<TResult1 = Turn<TData>, TResult2 = never>(\n onfulfilled?: ((value: Turn<TData>) => TResult1 | PromiseLike<TResult1>) | null,\n onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null\n ): Promise<TResult1 | TResult2> {\n drain();\n return getTurn().then(onfulfilled, onrejected);\n },\n };\n}\n\n/**\n * Creates a text delta stream event.\n *\n * @param text - The incremental text content\n * @param index - Content block index (default: 0)\n * @returns A text_delta StreamEvent\n */\nexport function textDelta(text: string, index = 0): StreamEvent {\n return {\n type: StreamEventType.TextDelta,\n index,\n delta: { text },\n };\n}\n\n/**\n * Creates a tool call delta stream event.\n *\n * @param toolCallId - Unique identifier for the tool call\n * @param toolName - Name of the tool being called\n * @param argumentsJson - Incremental JSON arguments string\n * @param index - Content block index (default: 0)\n * @returns A tool_call_delta StreamEvent\n */\nexport function toolCallDelta(\n toolCallId: string,\n toolName: string,\n argumentsJson: string,\n index = 0\n): StreamEvent {\n return {\n type: StreamEventType.ToolCallDelta,\n index,\n delta: { toolCallId, toolName, argumentsJson },\n };\n}\n\n/**\n * Creates an object delta stream event for structured output responses.\n *\n * @param text - The incremental text content\n * @param index - Content block index (default: 0)\n * @returns An object_delta StreamEvent\n */\nexport function objectDelta(text: string, index = 0): StreamEvent {\n return {\n type: StreamEventType.ObjectDelta,\n index,\n delta: { text },\n };\n}\n\n/**\n * Creates a message start stream event.\n *\n * @returns A message_start StreamEvent\n */\nexport function messageStart(): StreamEvent {\n return {\n type: StreamEventType.MessageStart,\n index: 0,\n delta: {},\n };\n}\n\n/**\n * Creates a message stop stream event.\n *\n * @returns A message_stop StreamEvent\n */\nexport function messageStop(): StreamEvent {\n return {\n type: StreamEventType.MessageStop,\n index: 0,\n delta: {},\n };\n}\n\n/**\n * Creates a content block start stream event.\n *\n * @param index - The content block index starting\n * @returns A content_block_start StreamEvent\n */\nexport function contentBlockStart(index: number): StreamEvent {\n return {\n type: StreamEventType.ContentBlockStart,\n index,\n delta: {},\n };\n}\n\n/**\n * Creates a content block stop stream event.\n *\n * @param index - The content block index stopping\n * @returns A content_block_stop StreamEvent\n */\nexport function contentBlockStop(index: number): StreamEvent {\n return {\n type: StreamEventType.ContentBlockStop,\n index,\n delta: {},\n };\n}\n\n/**\n * Creates a tool execution start stream event.\n *\n * @param toolCallId - Unique identifier for the tool call\n * @param toolName - Name of the tool being executed\n * @param timestamp - Start timestamp in milliseconds\n * @param index - Content block index (default: 0)\n * @returns A tool_execution_start StreamEvent\n */\nexport function toolExecutionStart(\n toolCallId: string,\n toolName: string,\n timestamp: number,\n index = 0\n): StreamEvent {\n return {\n type: StreamEventType.ToolExecutionStart,\n index,\n delta: { toolCallId, toolName, timestamp },\n };\n}\n\n/**\n * Creates a tool execution end stream event.\n *\n * @param toolCallId - Unique identifier for the tool call\n * @param toolName - Name of the tool that was executed\n * @param result - The result from the tool execution\n * @param isError - Whether the execution resulted in an error\n * @param timestamp - End timestamp in milliseconds\n * @param index - Content block index (default: 0)\n * @returns A tool_execution_end StreamEvent\n */\nexport function toolExecutionEnd(\n toolCallId: string,\n toolName: string,\n result: unknown,\n isError: boolean,\n timestamp: number,\n index = 0\n): StreamEvent {\n return {\n type: StreamEventType.ToolExecutionEnd,\n index,\n delta: { toolCallId, toolName, result, isError, timestamp },\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/http/errors.ts","../src/http/fetch.ts"],"sourcesContent":["/**\n * HTTP error handling and normalization utilities.\n * @module http/errors\n */\n\nimport {\n UPPError,\n ErrorCode,\n type Modality,\n} from '../types/errors.ts';\nimport { toError } from '../utils/error.ts';\n\n/**\n * Maps HTTP status codes to standardized UPP error codes.\n *\n * This function provides consistent error categorization across all providers:\n * - 400 -> INVALID_REQUEST (bad request format or parameters)\n * - 401, 403 -> AUTHENTICATION_FAILED (invalid or missing credentials)\n * - 404 -> MODEL_NOT_FOUND (requested model does not exist)\n * - 408 -> TIMEOUT (request timed out)\n * - 413 -> CONTEXT_LENGTH_EXCEEDED (input too long)\n * - 429 -> RATE_LIMITED (too many requests)\n * - 5xx -> PROVIDER_ERROR (server-side issues)\n *\n * @param status - HTTP status code from the response\n * @returns The corresponding UPP ErrorCode\n *\n * @example\n * ```typescript\n * const errorCode = statusToErrorCode(429);\n * // Returns 'RATE_LIMITED'\n *\n * const serverError = statusToErrorCode(503);\n * // Returns 'PROVIDER_ERROR'\n * ```\n */\nexport function statusToErrorCode(status: number): ErrorCode {\n switch (status) {\n case 400:\n return ErrorCode.InvalidRequest;\n case 402:\n return ErrorCode.QuotaExceeded;\n case 401:\n case 403:\n return ErrorCode.AuthenticationFailed;\n case 404:\n return ErrorCode.ModelNotFound;\n case 408:\n return ErrorCode.Timeout;\n case 409:\n return ErrorCode.InvalidRequest;\n case 422:\n return ErrorCode.InvalidRequest;\n case 413:\n return ErrorCode.ContextLengthExceeded;\n case 451:\n return ErrorCode.ContentFiltered;\n case 429:\n return ErrorCode.RateLimited;\n case 500:\n case 502:\n case 503:\n case 504:\n return ErrorCode.ProviderError;\n default:\n return ErrorCode.ProviderError;\n }\n}\n\n/**\n * Normalizes HTTP error responses into standardized UPPError objects.\n *\n * This function performs several operations:\n * 1. Maps the HTTP status code to an appropriate ErrorCode\n * 2. Attempts to extract a meaningful error message from the response body\n * 3. Handles various provider-specific error response formats\n *\n * Supported error message formats:\n * - `{ error: { message: \"...\" } }` (OpenAI, Anthropic)\n * - `{ message: \"...\" }` (simple format)\n * - `{ error: { error: { message: \"...\" } } }` (nested format)\n * - `{ detail: \"...\" }` (FastAPI style)\n * - Plain text body (if under 200 characters)\n *\n * @param response - The HTTP Response object with non-2xx status\n * @param provider - Provider identifier for error context\n * @param modality - Request modality for error context\n * @returns A UPPError with normalized code and message\n *\n * @example\n * ```typescript\n * if (!response.ok) {\n * const error = await normalizeHttpError(response, 'openai', 'llm');\n * // error.code might be 'RATE_LIMITED' for 429\n * // error.message contains provider's error message\n * throw error;\n * }\n * ```\n */\nexport async function normalizeHttpError(\n response: Response,\n provider: string,\n modality: Modality\n): Promise<UPPError> {\n const code = statusToErrorCode(response.status);\n let message = `HTTP ${response.status}: ${response.statusText}`;\n let bodyReadError: Error | undefined;\n\n try {\n const body = await response.text();\n if (body) {\n try {\n const json = JSON.parse(body);\n const extractedMessage =\n json.error?.message ||\n json.message ||\n json.error?.error?.message ||\n json.detail;\n\n if (extractedMessage) {\n message = extractedMessage;\n }\n } catch {\n if (body.length < 200) {\n message = body;\n }\n }\n }\n } catch (error) {\n bodyReadError = toError(error);\n }\n\n return new UPPError(message, code, provider, modality, response.status, bodyReadError);\n}\n\n/**\n * Creates a UPPError for network failures (DNS, connection, etc.).\n *\n * Use this when the request fails before receiving any HTTP response,\n * such as DNS resolution failures, connection refused, or network unreachable.\n *\n * @param error - The underlying Error that caused the failure\n * @param provider - Provider identifier for error context\n * @param modality - Request modality for error context\n * @returns A UPPError with NETWORK_ERROR code and the original error attached\n */\nexport function networkError(\n error: Error,\n provider: string,\n modality: Modality\n): UPPError {\n return new UPPError(\n `Network error: ${error.message}`,\n ErrorCode.NetworkError,\n provider,\n modality,\n undefined,\n error\n );\n}\n\n/**\n * Creates a UPPError for request timeout.\n *\n * Use this when the request exceeds the configured timeout duration\n * and is aborted by the AbortController.\n *\n * @param timeout - The timeout duration in milliseconds that was exceeded\n * @param provider - Provider identifier for error context\n * @param modality - Request modality for error context\n * @returns A UPPError with TIMEOUT code\n */\nexport function timeoutError(\n timeout: number,\n provider: string,\n modality: Modality\n): UPPError {\n return new UPPError(\n `Request timed out after ${timeout}ms`,\n ErrorCode.Timeout,\n provider,\n modality\n );\n}\n\n/**\n * Creates a UPPError for user-initiated request cancellation.\n *\n * Use this when the request is aborted via a user-provided AbortSignal,\n * distinct from timeout-based cancellation.\n *\n * @param provider - Provider identifier for error context\n * @param modality - Request modality for error context\n * @returns A UPPError with CANCELLED code\n */\nexport function cancelledError(provider: string, modality: Modality): UPPError {\n return new UPPError(\n 'Request was cancelled',\n ErrorCode.Cancelled,\n provider,\n modality\n );\n}\n","/**\n * HTTP fetch utilities with retry, timeout, and error normalization.\n * @module http/fetch\n */\n\nimport type { ProviderConfig, RetryStrategy } from '../types/provider.ts';\nimport type { Modality } from '../types/errors.ts';\nimport { UPPError } from '../types/errors.ts';\nimport {\n normalizeHttpError,\n networkError,\n timeoutError,\n cancelledError,\n} from './errors.ts';\nimport { toError } from '../utils/error.ts';\n\n/** Default request timeout in milliseconds (2 minutes). */\nconst DEFAULT_TIMEOUT = 120000;\nconst MAX_RETRY_AFTER_SECONDS = 3600;\n\n/**\n * Warns when a non-TLS URL is used with a provider.\n * Only warns in non-production, excludes localhost for local development.\n */\nexport function warnInsecureUrl(url: string, provider: string): void {\n if (\n process.env.NODE_ENV !== 'production' &&\n url.startsWith('http://') &&\n !url.includes('localhost') &&\n !url.includes('127.0.0.1') &&\n !url.includes('[::1]')\n ) {\n console.warn(\n `[UPP] Provider \"${provider}\" using non-TLS URL: ${url}. ` +\n 'API keys may be exposed to network interception.'\n );\n }\n}\n\ntype ForkableRetryStrategy = RetryStrategy & {\n fork: () => RetryStrategy | undefined;\n};\n\nfunction hasFork(strategy: RetryStrategy | undefined): strategy is ForkableRetryStrategy {\n return !!strategy && typeof (strategy as { fork?: unknown }).fork === 'function';\n}\n\n/**\n * Delays execution for a specified duration.\n *\n * @param ms - Duration to sleep in milliseconds\n * @returns Promise that resolves after the specified delay\n */\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Parses Retry-After header values into seconds.\n *\n * Supports both delta-seconds and HTTP-date formats.\n */\nfunction parseRetryAfter(headerValue: string | null, maxSeconds: number): number | null {\n if (!headerValue) {\n return null;\n }\n\n const seconds = parseInt(headerValue, 10);\n if (!Number.isNaN(seconds)) {\n return Math.min(maxSeconds, Math.max(0, seconds));\n }\n\n const dateMillis = Date.parse(headerValue);\n if (Number.isNaN(dateMillis)) {\n return null;\n }\n\n const deltaMs = dateMillis - Date.now();\n if (deltaMs <= 0) {\n return 0;\n }\n\n const deltaSeconds = Math.ceil(deltaMs / 1000);\n return Math.min(maxSeconds, Math.max(0, deltaSeconds));\n}\n\n/**\n * Executes a fetch request with configurable timeout.\n *\n * Creates an AbortController to cancel the request if it exceeds the timeout.\n * Properly handles both user-provided abort signals and timeout-based cancellation,\n * throwing appropriate error types for each case.\n *\n * @param fetchFn - The fetch function to use (allows custom implementations)\n * @param url - The URL to fetch\n * @param init - Standard fetch RequestInit options\n * @param timeout - Maximum time in milliseconds before aborting\n * @param provider - Provider identifier for error context\n * @param modality - Request modality for error context\n * @returns The Response from the fetch call\n *\n * @throws {UPPError} TIMEOUT - When the timeout is exceeded\n * @throws {UPPError} CANCELLED - When cancelled via user-provided signal\n * @throws {Error} Network errors are passed through unchanged\n */\nasync function fetchWithTimeout(\n fetchFn: typeof fetch,\n url: string,\n init: RequestInit,\n timeout: number,\n provider: string,\n modality: Modality\n): Promise<Response> {\n const existingSignal = init.signal;\n\n // Check if already aborted before starting\n if (existingSignal?.aborted) {\n throw cancelledError(provider, modality);\n }\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeout);\n\n const onAbort = () => controller.abort();\n if (existingSignal) {\n existingSignal.addEventListener('abort', onAbort, { once: true });\n }\n\n try {\n const response = await fetchFn(url, {\n ...init,\n signal: controller.signal,\n });\n return response;\n } catch (error) {\n if (toError(error).name === 'AbortError') {\n if (existingSignal?.aborted) {\n throw cancelledError(provider, modality);\n }\n throw timeoutError(timeout, provider, modality);\n }\n throw error;\n } finally {\n clearTimeout(timeoutId);\n if (existingSignal) {\n existingSignal.removeEventListener('abort', onAbort);\n }\n }\n}\n\n/**\n * Executes an HTTP fetch request with automatic retry, timeout handling, and error normalization.\n *\n * This function wraps the standard fetch API with additional capabilities:\n * - Configurable timeout with automatic request cancellation (per attempt)\n * - Retry strategy support (exponential backoff, linear, token bucket, etc.)\n * - Pre-request delay support for rate limiting strategies\n * - Automatic Retry-After header parsing and handling\n * - Error normalization to UPPError format\n *\n * @param url - The URL to fetch\n * @param init - Standard fetch RequestInit options (method, headers, body, etc.)\n * @param config - Provider configuration containing fetch customization, timeout, and retry strategy\n * @param provider - Provider identifier for error context (e.g., 'openai', 'anthropic')\n * @param modality - Request modality for error context (e.g., 'llm', 'embedding', 'image')\n * @returns The successful Response object\n *\n * @throws {UPPError} RATE_LIMITED - When rate limited and retries exhausted\n * @throws {UPPError} NETWORK_ERROR - When a network failure occurs\n * @throws {UPPError} TIMEOUT - When the request times out\n * @throws {UPPError} CANCELLED - When the request is aborted via signal\n * @throws {UPPError} Various codes based on HTTP status (see statusToErrorCode)\n *\n * @example\n * ```typescript\n * const response = await doFetch(\n * 'https://api.openai.com/v1/chat/completions',\n * {\n * method: 'POST',\n * headers: { 'Authorization': 'Bearer sk-...' },\n * body: JSON.stringify({ model: 'gpt-4', messages: [] })\n * },\n * { timeout: 30000, retryStrategy: new ExponentialBackoff() },\n * 'openai',\n * 'llm'\n * );\n * ```\n */\nexport async function doFetch(\n url: string,\n init: RequestInit,\n config: ProviderConfig,\n provider: string,\n modality: Modality\n): Promise<Response> {\n const fetchFn = config.fetch ?? fetch;\n const timeout = config.timeout ?? DEFAULT_TIMEOUT;\n const baseStrategy = config.retryStrategy;\n const strategy = hasFork(baseStrategy) ? baseStrategy.fork() : baseStrategy;\n\n // Warn about potential security issue with non-TLS URLs\n warnInsecureUrl(url, provider);\n\n let attempt = 0;\n\n while (true) {\n attempt++;\n\n if (strategy?.beforeRequest) {\n const delay = await strategy.beforeRequest();\n if (delay > 0) {\n await sleep(delay);\n }\n }\n\n let response: Response;\n try {\n response = await fetchWithTimeout(\n fetchFn,\n url,\n init,\n timeout,\n provider,\n modality\n );\n } catch (error) {\n if (error instanceof UPPError) {\n if (strategy) {\n const delay = await strategy.onRetry(error, attempt);\n if (delay !== null) {\n await sleep(delay);\n continue;\n }\n }\n throw error;\n }\n\n const uppError = networkError(toError(error), provider, modality);\n\n if (strategy) {\n const delay = await strategy.onRetry(uppError, attempt);\n if (delay !== null) {\n await sleep(delay);\n continue;\n }\n }\n\n throw uppError;\n }\n\n if (!response.ok) {\n const error = await normalizeHttpError(response, provider, modality);\n\n const retryAfterSeconds = parseRetryAfter(\n response.headers.get('Retry-After'),\n config.retryAfterMaxSeconds ?? MAX_RETRY_AFTER_SECONDS\n );\n if (retryAfterSeconds !== null && strategy && 'setRetryAfter' in strategy) {\n (strategy as { setRetryAfter: (s: number) => void }).setRetryAfter(\n retryAfterSeconds\n );\n }\n\n if (strategy) {\n const delay = await strategy.onRetry(error, attempt);\n if (delay !== null) {\n await sleep(delay);\n continue;\n }\n }\n\n throw error;\n }\n\n strategy?.reset?.();\n\n return response;\n }\n}\n\n/**\n * Executes an HTTP fetch request for streaming responses.\n *\n * Unlike {@link doFetch}, this function returns the response immediately without\n * checking the HTTP status. This is necessary for Server-Sent Events (SSE) and\n * other streaming protocols where error information may be embedded in the stream.\n *\n * The caller is responsible for:\n * - Checking response.ok and handling HTTP errors\n * - Parsing the response stream (e.g., using parseSSEStream)\n * - Handling stream-specific error conditions\n *\n * Retries are not performed for streaming requests since partial data may have\n * already been consumed by the caller.\n *\n * @param url - The URL to fetch\n * @param init - Standard fetch RequestInit options\n * @param config - Provider configuration containing fetch customization and timeout\n * @param provider - Provider identifier for error context\n * @param modality - Request modality for error context\n * @returns The Response object (may have non-2xx status)\n *\n * @throws {UPPError} NETWORK_ERROR - When a network failure occurs\n * @throws {UPPError} TIMEOUT - When the request times out\n * @throws {UPPError} CANCELLED - When the request is aborted via signal\n *\n * @example\n * ```typescript\n * const response = await doStreamFetch(\n * 'https://api.openai.com/v1/chat/completions',\n * {\n * method: 'POST',\n * headers: { 'Authorization': 'Bearer sk-...' },\n * body: JSON.stringify({ model: 'gpt-4', messages: [], stream: true })\n * },\n * { timeout: 120000 },\n * 'openai',\n * 'llm'\n * );\n *\n * if (!response.ok) {\n * throw await normalizeHttpError(response, 'openai', 'llm');\n * }\n *\n * for await (const event of parseSSEStream(response.body!)) {\n * console.log(event);\n * }\n * ```\n */\nexport async function doStreamFetch(\n url: string,\n init: RequestInit,\n config: ProviderConfig,\n provider: string,\n modality: Modality\n): Promise<Response> {\n const fetchFn = config.fetch ?? fetch;\n const timeout = config.timeout ?? DEFAULT_TIMEOUT;\n const baseStrategy = config.retryStrategy;\n const strategy = hasFork(baseStrategy) ? baseStrategy.fork() : baseStrategy;\n\n if (strategy?.beforeRequest) {\n const delay = await strategy.beforeRequest();\n if (delay > 0) {\n await sleep(delay);\n }\n }\n\n try {\n const response = await fetchWithTimeout(\n fetchFn,\n url,\n init,\n timeout,\n provider,\n modality\n );\n return response;\n } catch (error) {\n if (error instanceof UPPError) {\n throw error;\n }\n throw networkError(toError(error), provider, modality);\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/middleware/pubsub/server/shared.ts"],"sourcesContent":["/**\n * @fileoverview Shared utilities for pub-sub server adapters.\n *\n * @module middleware/pubsub/server/shared\n * @internal\n */\n\nimport type { StreamEvent } from '../../../types/stream.ts';\nimport type { PubSubAdapter } from '../types.ts';\nimport { serializeStreamEvent } from '../../../stream/serialization.ts';\n\n/**\n * Writer interface for abstracting how data is written to responses.\n * @internal\n */\nexport interface StreamWriter {\n write(data: string): void;\n end(): void;\n}\n\n/**\n * Options for runSubscriberStream.\n * @internal\n */\nexport interface StreamOptions {\n signal?: AbortSignal;\n}\n\n/**\n * Formats a stream event as an SSE data line.\n */\nexport function formatSSE(event: StreamEvent): string {\n const serialized = serializeStreamEvent(event);\n return `data: ${JSON.stringify(serialized)}\\n\\n`;\n}\n\n/**\n * Core subscriber stream logic shared across all adapters.\n *\n * Handles:\n * 1. Subscribing to live events and completion signal\n * 2. Replaying buffered events (empty if stream just started)\n * 3. Processing live events until completion signal\n * 4. Final cleanup\n * 5. Client disconnect via AbortSignal\n *\n * @internal\n */\nexport async function runSubscriberStream(\n streamId: string,\n adapter: PubSubAdapter,\n writer: StreamWriter,\n options: StreamOptions = {}\n): Promise<void> {\n const { signal } = options;\n\n if (signal?.aborted) {\n writer.end();\n return;\n }\n\n try {\n if (signal?.aborted) {\n writer.end();\n return;\n }\n\n const queue: Array<{ event: StreamEvent; cursor: number | null }> = [];\n let resolveWait: (() => void) | null = null;\n let completed = false;\n let lastSentCursor = -1;\n let finalData: unknown = undefined;\n\n const onEvent = (event: StreamEvent, cursor?: number): void => {\n queue.push({ event, cursor: cursor ?? null });\n resolveWait?.();\n };\n\n const onComplete = (): void => {\n completed = true;\n resolveWait?.();\n };\n\n const onFinalData = (data: unknown): void => {\n finalData = data;\n };\n\n const unsubscribe = adapter.subscribe(streamId, onEvent, onComplete, onFinalData);\n\n const onAbort = (): void => {\n completed = true;\n resolveWait?.();\n };\n signal?.addEventListener('abort', onAbort);\n\n const drainQueue = (): void => {\n while (queue.length > 0 && !signal?.aborted) {\n const item = queue.shift();\n if (!item) break;\n const { event, cursor } = item;\n if (cursor !== null && cursor <= lastSentCursor) continue;\n writer.write(formatSSE(event));\n if (cursor !== null && cursor > lastSentCursor) {\n lastSentCursor = cursor;\n }\n }\n };\n\n const dropReplayDuplicates = (): void => {\n if (queue.length === 0) return;\n const filtered: Array<{ event: StreamEvent; cursor: number | null }> = [];\n for (const item of queue) {\n if (item.cursor !== null && item.cursor <= lastSentCursor) continue;\n filtered.push(item);\n }\n queue.length = 0;\n queue.push(...filtered);\n };\n\n const waitForSignal = (): Promise<void> => new Promise((resolve) => {\n let settled = false;\n\n const settle = (): void => {\n if (settled) return;\n settled = true;\n resolveWait = null;\n resolve();\n };\n\n resolveWait = settle;\n\n if (completed || signal?.aborted || queue.length > 0) {\n settle();\n }\n });\n\n try {\n const events = await adapter.getEvents(streamId);\n\n for (const event of events) {\n if (signal?.aborted) break;\n writer.write(formatSSE(event));\n }\n\n lastSentCursor = events.length - 1;\n dropReplayDuplicates();\n\n if (signal?.aborted) {\n writer.end();\n return;\n }\n\n // Wait for events or completion signal\n while (!completed && !signal?.aborted) {\n drainQueue();\n if (completed || signal?.aborted) break;\n await waitForSignal();\n }\n\n if (!signal?.aborted) {\n drainQueue();\n }\n } finally {\n signal?.removeEventListener('abort', onAbort);\n unsubscribe();\n }\n\n if (!signal?.aborted) {\n // Emit final data (Turn) if available before [DONE]\n if (finalData !== undefined) {\n writer.write(`data: ${JSON.stringify(finalData)}\\n\\n`);\n }\n writer.write('data: [DONE]\\n\\n');\n }\n writer.end();\n } catch (error) {\n if (!signal?.aborted) {\n const errorMsg = error instanceof Error ? error.message : String(error);\n writer.write(`data: ${JSON.stringify({ error: errorMsg })}\\n\\n`);\n }\n writer.end();\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/utils/id.ts","../src/types/messages.ts"],"sourcesContent":["/**\n * @fileoverview ID generation utilities for the Universal Provider Protocol.\n *\n * Provides functions for generating unique identifiers used throughout UPP,\n * including message IDs, tool call IDs, and other internal references.\n *\n * @module utils/id\n */\n\n/**\n * Generates a unique UUID v4 identifier.\n *\n * Uses the native `crypto.randomUUID()` when available for cryptographically\n * secure randomness. Falls back to a Math.random-based implementation for\n * environments without Web Crypto API support.\n *\n * @returns A UUID v4 string in the format `xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx`\n *\n * @example\n * ```typescript\n * const messageId = generateId();\n * // => \"f47ac10b-58cc-4372-a567-0e02b2c3d479\"\n * ```\n */\nexport function generateId(): string {\n if (typeof crypto !== 'undefined' && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0;\n const v = c === 'x' ? r : (r & 0x3) | 0x8;\n return v.toString(16);\n });\n}\n\n/**\n * Generates a short alphanumeric identifier.\n *\n * Creates a 12-character random string using alphanumeric characters (a-z, A-Z, 0-9).\n * Useful for tool call IDs and other cases where a full UUID is not required.\n *\n * @param prefix - Optional prefix to prepend to the generated ID\n * @returns A string containing the prefix followed by 12 random alphanumeric characters\n *\n * @example\n * ```typescript\n * const toolCallId = generateShortId('call_');\n * // => \"call_aB3xY9mK2pQr\"\n *\n * const simpleId = generateShortId();\n * // => \"Tz4wN8vL1sHj\"\n * ```\n */\nexport function generateShortId(prefix = ''): string {\n const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\n let result = prefix;\n for (let i = 0; i < 12; i++) {\n result += chars.charAt(Math.floor(Math.random() * chars.length));\n }\n return result;\n}\n","/**\n * @fileoverview Message types for conversation history.\n *\n * Defines the message classes used to represent conversation turns\n * between users and assistants, including support for multimodal\n * content and tool calls.\n *\n * @module types/messages\n */\n\nimport { generateId } from '../utils/id.ts';\nimport type {\n ContentBlock,\n TextBlock,\n ReasoningBlock,\n ImageBlock,\n DocumentBlock,\n AudioBlock,\n VideoBlock,\n UserContent,\n AssistantContent,\n} from './content.ts';\nimport type { ToolCall, ToolResult } from './tool.ts';\n\n/**\n * Message serialized to JSON format.\n * Picks common fields from Message, converts timestamp to string.\n */\nexport type MessageJSON = Pick<Message, 'id' | 'type' | 'metadata'> & {\n timestamp: string;\n content: ContentBlock[];\n toolCalls?: ToolCall[];\n results?: ToolResult[];\n};\n\n/**\n * Message role/type constants.\n *\n * Use these constants instead of raw strings for type-safe message handling:\n *\n * @example\n * ```typescript\n * import { MessageRole, isUserMessage } from 'upp';\n *\n * if (message.type === MessageRole.User) {\n * console.log('User said:', message.text);\n * } else if (message.type === MessageRole.Assistant) {\n * console.log('Assistant replied:', message.text);\n * }\n * ```\n */\nexport const MessageRole = {\n /** User message */\n User: 'user',\n /** Assistant/model response */\n Assistant: 'assistant',\n /** Tool execution result */\n ToolResult: 'tool_result',\n} as const;\n\n/**\n * Message type discriminator union.\n *\n * This type is derived from {@link MessageRole} constants. The name `MessageType`\n * is kept for backward compatibility; `MessageRole` works as both the const\n * object and this type.\n */\nexport type MessageType = (typeof MessageRole)[keyof typeof MessageRole];\n\n/**\n * Type alias for MessageType, allowing `MessageRole` to work as both const and type.\n */\nexport type MessageRole = MessageType;\n\n/**\n * Provider-namespaced metadata for messages.\n *\n * Each provider can attach its own metadata under its namespace,\n * preventing conflicts between different providers.\n *\n * @example\n * ```typescript\n * const metadata: MessageMetadata = {\n * openai: { model: 'gpt-4', finishReason: 'stop' },\n * anthropic: { model: 'claude-3', stopReason: 'end_turn' }\n * };\n * ```\n */\nexport interface MessageMetadata {\n [provider: string]: Record<string, unknown> | undefined;\n}\n\n/**\n * Options for constructing messages.\n */\nexport interface MessageOptions {\n /** Custom message ID (auto-generated if not provided) */\n id?: string;\n\n /** Message timestamp override (defaults to now) */\n timestamp?: Date;\n\n /** Provider-specific metadata */\n metadata?: MessageMetadata;\n}\n\n/**\n * Abstract base class for all message types.\n *\n * Provides common functionality for user, assistant, and tool result\n * messages, including content accessors and metadata handling.\n *\n * @example\n * ```typescript\n * // Access text content from any message\n * const text = message.text;\n *\n * // Access images\n * const images = message.images;\n * ```\n */\nexport abstract class Message {\n /** Unique message identifier */\n readonly id: string;\n\n /** Timestamp when the message was created */\n readonly timestamp: Date;\n\n /** Provider-specific metadata, namespaced by provider name */\n readonly metadata?: MessageMetadata;\n\n /** Message type discriminator (implemented by subclasses) */\n abstract readonly type: MessageType;\n\n /**\n * Returns the content blocks for this message.\n * Implemented by subclasses to provide type-specific content.\n */\n protected abstract getContent(): ContentBlock[];\n\n /**\n * Creates a new message instance.\n *\n * @param options - Optional message ID and metadata\n */\n constructor(options?: MessageOptions) {\n this.id = options?.id ?? generateId();\n this.timestamp = options?.timestamp ?? new Date();\n this.metadata = options?.metadata;\n }\n\n /**\n * Concatenated text content from all text blocks.\n * Blocks are joined with double newlines.\n */\n get text(): string {\n return this.getContent()\n .filter((block): block is TextBlock => block.type === 'text')\n .map((block) => block.text)\n .join('\\n\\n');\n }\n\n /**\n * All image content blocks in this message.\n */\n get images(): ImageBlock[] {\n return this.getContent().filter((block): block is ImageBlock => block.type === 'image');\n }\n\n /**\n * All document content blocks in this message.\n */\n get documents(): DocumentBlock[] {\n return this.getContent().filter((block): block is DocumentBlock => block.type === 'document');\n }\n\n /**\n * All audio content blocks in this message.\n */\n get audio(): AudioBlock[] {\n return this.getContent().filter((block): block is AudioBlock => block.type === 'audio');\n }\n\n /**\n * All video content blocks in this message.\n */\n get video(): VideoBlock[] {\n return this.getContent().filter((block): block is VideoBlock => block.type === 'video');\n }\n\n /**\n * All reasoning/thinking content blocks in this message.\n * Available when using extended thinking models.\n */\n get reasoning(): ReasoningBlock[] {\n return this.getContent().filter((block): block is ReasoningBlock => block.type === 'reasoning');\n }\n}\n\n/**\n * User input message.\n *\n * Represents a message from the user, which can contain text and/or\n * multimodal content like images, audio, or video.\n *\n * @example\n * ```typescript\n * // Simple text message\n * const msg = new UserMessage('Hello, world!');\n *\n * // Multimodal message\n * const msg = new UserMessage([\n * { type: 'text', text: 'What is in this image?' },\n * { type: 'image', source: { type: 'url', url: '...' }, mimeType: 'image/png' }\n * ]);\n * ```\n */\nexport class UserMessage extends Message {\n /** Message type discriminator */\n readonly type = 'user' as const;\n\n /** Content blocks in this message */\n readonly content: UserContent[];\n\n /**\n * Creates a new user message.\n *\n * @param content - String (converted to TextBlock) or array of content blocks\n * @param options - Optional message ID and metadata\n */\n constructor(content: string | UserContent[], options?: MessageOptions) {\n super(options);\n if (typeof content === 'string') {\n this.content = [{ type: 'text', text: content }];\n } else {\n this.content = content;\n }\n }\n\n protected getContent(): ContentBlock[] {\n return this.content;\n }\n}\n\n/**\n * Assistant response message.\n *\n * Represents a response from the AI assistant, which may contain\n * text, media content, and/or tool call requests.\n *\n * @example\n * ```typescript\n * // Simple text response\n * const msg = new AssistantMessage('Hello! How can I help?');\n *\n * // Response with tool calls\n * const msg = new AssistantMessage(\n * 'Let me check the weather...',\n * [{ toolCallId: 'call_1', toolName: 'get_weather', arguments: { location: 'NYC' } }]\n * );\n * ```\n */\nexport class AssistantMessage extends Message {\n /** Message type discriminator */\n readonly type = 'assistant' as const;\n\n /** Content blocks in this message */\n readonly content: AssistantContent[];\n\n /** Tool calls requested by the model (if any) */\n readonly toolCalls?: ToolCall[];\n\n /**\n * Creates a new assistant message.\n *\n * @param content - String (converted to TextBlock) or array of content blocks\n * @param toolCalls - Tool calls requested by the model\n * @param options - Optional message ID and metadata\n */\n constructor(\n content: string | AssistantContent[],\n toolCalls?: ToolCall[],\n options?: MessageOptions\n ) {\n super(options);\n if (typeof content === 'string') {\n this.content = [{ type: 'text', text: content }];\n } else {\n this.content = content;\n }\n this.toolCalls = toolCalls;\n }\n\n protected getContent(): ContentBlock[] {\n return this.content;\n }\n\n /**\n * Whether this message contains tool call requests.\n */\n get hasToolCalls(): boolean {\n return this.toolCalls !== undefined && this.toolCalls.length > 0;\n }\n}\n\n/**\n * Tool execution result message.\n *\n * Contains the results of executing one or more tool calls,\n * sent back to the model for further processing.\n *\n * @example\n * ```typescript\n * const msg = new ToolResultMessage([\n * { toolCallId: 'call_1', result: { temperature: 72, conditions: 'sunny' } },\n * { toolCallId: 'call_2', result: 'File not found', isError: true }\n * ]);\n * ```\n */\nexport class ToolResultMessage extends Message {\n /** Message type discriminator */\n readonly type = 'tool_result' as const;\n\n /** Results from tool executions */\n readonly results: ToolResult[];\n\n /**\n * Creates a new tool result message.\n *\n * @param results - Array of tool execution results\n * @param options - Optional message ID and metadata\n */\n constructor(results: ToolResult[], options?: MessageOptions) {\n super(options);\n this.results = results;\n }\n\n protected getContent(): ContentBlock[] {\n return this.results.map((result) => ({\n type: 'text' as const,\n text:\n typeof result.result === 'string'\n ? result.result\n : JSON.stringify(result.result),\n }));\n }\n}\n\n/**\n * Type guard for UserMessage.\n *\n * @param msg - The message to check\n * @returns True if the message is a UserMessage\n *\n * @example\n * ```typescript\n * if (isUserMessage(msg)) {\n * console.log('User said:', msg.text);\n * }\n * ```\n */\nexport function isUserMessage(msg: Message): msg is UserMessage {\n return msg.type === MessageRole.User;\n}\n\n/**\n * Type guard for AssistantMessage.\n *\n * @param msg - The message to check\n * @returns True if the message is an AssistantMessage\n *\n * @example\n * ```typescript\n * if (isAssistantMessage(msg)) {\n * console.log('Assistant said:', msg.text);\n * if (msg.hasToolCalls) {\n * console.log('Tool calls:', msg.toolCalls);\n * }\n * }\n * ```\n */\nexport function isAssistantMessage(msg: Message): msg is AssistantMessage {\n return msg.type === MessageRole.Assistant;\n}\n\n/**\n * Type guard for ToolResultMessage.\n *\n * @param msg - The message to check\n * @returns True if the message is a ToolResultMessage\n *\n * @example\n * ```typescript\n * if (isToolResultMessage(msg)) {\n * for (const result of msg.results) {\n * console.log(`Tool ${result.toolCallId}:`, result.result);\n * }\n * }\n * ```\n */\nexport function isToolResultMessage(msg: Message): msg is ToolResultMessage {\n return msg.type === MessageRole.ToolResult;\n}\n"],"mappings":"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","names":[]}
|
package/dist/chunk-Y5H7C5J4.js
DELETED
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ErrorCode
|
|
3
|
-
} from "./chunk-COS4ON4G.js";
|
|
4
|
-
|
|
5
|
-
// src/http/retry.ts
|
|
6
|
-
var ExponentialBackoff = class {
|
|
7
|
-
maxAttempts;
|
|
8
|
-
baseDelay;
|
|
9
|
-
maxDelay;
|
|
10
|
-
jitter;
|
|
11
|
-
/**
|
|
12
|
-
* Creates a new ExponentialBackoff instance.
|
|
13
|
-
*
|
|
14
|
-
* @param options - Configuration options
|
|
15
|
-
* @param options.maxAttempts - Maximum number of retry attempts (default: 3)
|
|
16
|
-
* @param options.baseDelay - Initial delay in milliseconds (default: 1000)
|
|
17
|
-
* @param options.maxDelay - Maximum delay cap in milliseconds (default: 30000)
|
|
18
|
-
* @param options.jitter - Whether to add random jitter to delays (default: true)
|
|
19
|
-
*/
|
|
20
|
-
constructor(options = {}) {
|
|
21
|
-
this.maxAttempts = options.maxAttempts ?? 3;
|
|
22
|
-
this.baseDelay = options.baseDelay ?? 1e3;
|
|
23
|
-
this.maxDelay = options.maxDelay ?? 3e4;
|
|
24
|
-
this.jitter = options.jitter ?? true;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Determines whether to retry and calculates the delay.
|
|
28
|
-
*
|
|
29
|
-
* @param error - The error that triggered the retry
|
|
30
|
-
* @param attempt - Current attempt number (1-indexed)
|
|
31
|
-
* @returns Delay in milliseconds before next retry, or null to stop retrying
|
|
32
|
-
*/
|
|
33
|
-
onRetry(error, attempt) {
|
|
34
|
-
if (attempt > this.maxAttempts) {
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
if (!this.isRetryable(error)) {
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
let delay = this.baseDelay * Math.pow(2, attempt - 1);
|
|
41
|
-
delay = Math.min(delay, this.maxDelay);
|
|
42
|
-
if (this.jitter) {
|
|
43
|
-
delay = delay * (0.5 + Math.random());
|
|
44
|
-
}
|
|
45
|
-
return Math.floor(delay);
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Checks if an error is eligible for retry.
|
|
49
|
-
*
|
|
50
|
-
* @param error - The error to evaluate
|
|
51
|
-
* @returns True if the error is transient and retryable
|
|
52
|
-
*/
|
|
53
|
-
isRetryable(error) {
|
|
54
|
-
return error.code === ErrorCode.RateLimited || error.code === ErrorCode.NetworkError || error.code === ErrorCode.Timeout || error.code === ErrorCode.ProviderError;
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
var LinearBackoff = class {
|
|
58
|
-
maxAttempts;
|
|
59
|
-
delay;
|
|
60
|
-
/**
|
|
61
|
-
* Creates a new LinearBackoff instance.
|
|
62
|
-
*
|
|
63
|
-
* @param options - Configuration options
|
|
64
|
-
* @param options.maxAttempts - Maximum number of retry attempts (default: 3)
|
|
65
|
-
* @param options.delay - Base delay multiplier in milliseconds (default: 1000)
|
|
66
|
-
*/
|
|
67
|
-
constructor(options = {}) {
|
|
68
|
-
this.maxAttempts = options.maxAttempts ?? 3;
|
|
69
|
-
this.delay = options.delay ?? 1e3;
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Determines whether to retry and calculates the linear delay.
|
|
73
|
-
*
|
|
74
|
-
* @param error - The error that triggered the retry
|
|
75
|
-
* @param attempt - Current attempt number (1-indexed)
|
|
76
|
-
* @returns Delay in milliseconds (delay * attempt), or null to stop retrying
|
|
77
|
-
*/
|
|
78
|
-
onRetry(error, attempt) {
|
|
79
|
-
if (attempt > this.maxAttempts) {
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
if (!this.isRetryable(error)) {
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
return this.delay * attempt;
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* Checks if an error is eligible for retry.
|
|
89
|
-
*
|
|
90
|
-
* @param error - The error to evaluate
|
|
91
|
-
* @returns True if the error is transient and retryable
|
|
92
|
-
*/
|
|
93
|
-
isRetryable(error) {
|
|
94
|
-
return error.code === ErrorCode.RateLimited || error.code === ErrorCode.NetworkError || error.code === ErrorCode.Timeout || error.code === ErrorCode.ProviderError;
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
var NoRetry = class {
|
|
98
|
-
/**
|
|
99
|
-
* Always returns null to indicate no retry should be attempted.
|
|
100
|
-
*
|
|
101
|
-
* @returns Always returns null
|
|
102
|
-
*/
|
|
103
|
-
onRetry(_error, _attempt) {
|
|
104
|
-
return null;
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
var TokenBucket = class {
|
|
108
|
-
tokens;
|
|
109
|
-
maxTokens;
|
|
110
|
-
refillRate;
|
|
111
|
-
lastRefill;
|
|
112
|
-
maxAttempts;
|
|
113
|
-
lock;
|
|
114
|
-
/**
|
|
115
|
-
* Creates a new TokenBucket instance.
|
|
116
|
-
*
|
|
117
|
-
* @param options - Configuration options
|
|
118
|
-
* @param options.maxTokens - Maximum bucket capacity (default: 10)
|
|
119
|
-
* @param options.refillRate - Tokens added per second (default: 1)
|
|
120
|
-
* @param options.maxAttempts - Maximum retry attempts on rate limit (default: 3)
|
|
121
|
-
*/
|
|
122
|
-
constructor(options = {}) {
|
|
123
|
-
this.maxTokens = options.maxTokens ?? 10;
|
|
124
|
-
this.refillRate = options.refillRate ?? 1;
|
|
125
|
-
this.maxAttempts = options.maxAttempts ?? 3;
|
|
126
|
-
this.tokens = this.maxTokens;
|
|
127
|
-
this.lastRefill = Date.now();
|
|
128
|
-
this.lock = Promise.resolve();
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* Called before each request to consume a token or calculate wait time.
|
|
132
|
-
*
|
|
133
|
-
* Refills the bucket based on elapsed time, then either:
|
|
134
|
-
* - Returns 0 if a token is available (consumed immediately)
|
|
135
|
-
* - Returns the wait time in milliseconds until the next token
|
|
136
|
-
*
|
|
137
|
-
* This method may allow tokens to go negative to reserve future capacity
|
|
138
|
-
* and avoid concurrent callers oversubscribing the same refill.
|
|
139
|
-
*
|
|
140
|
-
* @returns Delay in milliseconds before the request can proceed
|
|
141
|
-
*/
|
|
142
|
-
beforeRequest() {
|
|
143
|
-
return this.withLock(() => {
|
|
144
|
-
this.refill();
|
|
145
|
-
if (this.tokens >= 1) {
|
|
146
|
-
this.tokens -= 1;
|
|
147
|
-
return 0;
|
|
148
|
-
}
|
|
149
|
-
const deficit = 1 - this.tokens;
|
|
150
|
-
const msPerToken = 1e3 / this.refillRate;
|
|
151
|
-
this.tokens -= 1;
|
|
152
|
-
return Math.ceil(deficit * msPerToken);
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Handles retry logic for rate-limited requests.
|
|
157
|
-
*
|
|
158
|
-
* Only retries on RATE_LIMITED errors, waiting for bucket refill.
|
|
159
|
-
*
|
|
160
|
-
* @param error - The error that triggered the retry
|
|
161
|
-
* @param attempt - Current attempt number (1-indexed)
|
|
162
|
-
* @returns Delay in milliseconds (time for 2 tokens), or null to stop
|
|
163
|
-
*/
|
|
164
|
-
onRetry(error, attempt) {
|
|
165
|
-
if (attempt > this.maxAttempts) {
|
|
166
|
-
return null;
|
|
167
|
-
}
|
|
168
|
-
if (error.code !== ErrorCode.RateLimited) {
|
|
169
|
-
return null;
|
|
170
|
-
}
|
|
171
|
-
const msPerToken = 1e3 / this.refillRate;
|
|
172
|
-
return Math.ceil(msPerToken * 2);
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Resets the bucket to full capacity.
|
|
176
|
-
*
|
|
177
|
-
* Called automatically on successful requests to restore available tokens.
|
|
178
|
-
*/
|
|
179
|
-
reset() {
|
|
180
|
-
void this.withLock(() => {
|
|
181
|
-
this.tokens = this.maxTokens;
|
|
182
|
-
this.lastRefill = Date.now();
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
/**
|
|
186
|
-
* Refills the bucket based on elapsed time since last refill.
|
|
187
|
-
*/
|
|
188
|
-
refill() {
|
|
189
|
-
const now = Date.now();
|
|
190
|
-
const elapsed = (now - this.lastRefill) / 1e3;
|
|
191
|
-
const newTokens = elapsed * this.refillRate;
|
|
192
|
-
this.tokens = Math.min(this.maxTokens, this.tokens + newTokens);
|
|
193
|
-
this.lastRefill = now;
|
|
194
|
-
}
|
|
195
|
-
async withLock(fn) {
|
|
196
|
-
const next = this.lock.then(fn, fn);
|
|
197
|
-
this.lock = next.then(() => void 0, () => void 0);
|
|
198
|
-
return next;
|
|
199
|
-
}
|
|
200
|
-
};
|
|
201
|
-
var RetryAfterStrategy = class _RetryAfterStrategy {
|
|
202
|
-
maxAttempts;
|
|
203
|
-
fallbackDelay;
|
|
204
|
-
lastRetryAfter;
|
|
205
|
-
/**
|
|
206
|
-
* Creates a new RetryAfterStrategy instance.
|
|
207
|
-
*
|
|
208
|
-
* @param options - Configuration options
|
|
209
|
-
* @param options.maxAttempts - Maximum number of retry attempts (default: 3)
|
|
210
|
-
* @param options.fallbackDelay - Delay in ms when no Retry-After header (default: 5000)
|
|
211
|
-
*/
|
|
212
|
-
constructor(options = {}) {
|
|
213
|
-
this.maxAttempts = options.maxAttempts ?? 3;
|
|
214
|
-
this.fallbackDelay = options.fallbackDelay ?? 5e3;
|
|
215
|
-
}
|
|
216
|
-
/**
|
|
217
|
-
* Creates a request-scoped copy of this strategy.
|
|
218
|
-
*/
|
|
219
|
-
fork() {
|
|
220
|
-
return new _RetryAfterStrategy({
|
|
221
|
-
maxAttempts: this.maxAttempts,
|
|
222
|
-
fallbackDelay: this.fallbackDelay
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* Sets the retry delay from a Retry-After header value.
|
|
227
|
-
*
|
|
228
|
-
* Called by doFetch when a Retry-After header is present in the response.
|
|
229
|
-
* The value is used for the next onRetry call and then cleared.
|
|
230
|
-
*
|
|
231
|
-
* @param seconds - The Retry-After value in seconds
|
|
232
|
-
*/
|
|
233
|
-
setRetryAfter(seconds) {
|
|
234
|
-
this.lastRetryAfter = seconds * 1e3;
|
|
235
|
-
}
|
|
236
|
-
/**
|
|
237
|
-
* Determines retry delay using Retry-After header or fallback.
|
|
238
|
-
*
|
|
239
|
-
* @param error - The error that triggered the retry
|
|
240
|
-
* @param attempt - Current attempt number (1-indexed)
|
|
241
|
-
* @returns Delay from Retry-After header or fallback, null to stop
|
|
242
|
-
*/
|
|
243
|
-
onRetry(error, attempt) {
|
|
244
|
-
if (attempt > this.maxAttempts) {
|
|
245
|
-
return null;
|
|
246
|
-
}
|
|
247
|
-
if (error.code !== ErrorCode.RateLimited) {
|
|
248
|
-
return null;
|
|
249
|
-
}
|
|
250
|
-
const delay = this.lastRetryAfter ?? this.fallbackDelay;
|
|
251
|
-
this.lastRetryAfter = void 0;
|
|
252
|
-
return delay;
|
|
253
|
-
}
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
export {
|
|
257
|
-
ExponentialBackoff,
|
|
258
|
-
LinearBackoff,
|
|
259
|
-
NoRetry,
|
|
260
|
-
TokenBucket,
|
|
261
|
-
RetryAfterStrategy
|
|
262
|
-
};
|
|
263
|
-
//# sourceMappingURL=chunk-Y5H7C5J4.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/http/retry.ts"],"sourcesContent":["/**\n * Retry strategies for handling transient failures in HTTP requests.\n * @module http/retry\n */\n\nimport type { RetryStrategy } from '../types/provider.ts';\nimport { ErrorCode, type UPPError } from '../types/errors.ts';\n\n/**\n * Implements exponential backoff with optional jitter for retry delays.\n *\n * The delay between retries doubles with each attempt, helping to:\n * - Avoid overwhelming servers during outages\n * - Reduce thundering herd effects when many clients retry simultaneously\n * - Give transient issues time to resolve\n *\n * Delay formula: min(baseDelay * 2^(attempt-1), maxDelay)\n * With jitter: delay * random(0.5, 1.0)\n *\n * Only retries on transient errors: RATE_LIMITED, NETWORK_ERROR, TIMEOUT, PROVIDER_ERROR\n *\n * @implements {RetryStrategy}\n *\n * @example\n * ```typescript\n * // Default configuration (3 retries, 1s base, 30s max, jitter enabled)\n * const retry = new ExponentialBackoff();\n *\n * // Custom configuration\n * const customRetry = new ExponentialBackoff({\n * maxAttempts: 5, // Up to 5 retry attempts\n * baseDelay: 500, // Start with 500ms delay\n * maxDelay: 60000, // Cap at 60 seconds\n * jitter: false // Disable random jitter\n * });\n *\n * // Use with provider\n * const provider = createOpenAI({\n * retryStrategy: customRetry\n * });\n * ```\n */\nexport class ExponentialBackoff implements RetryStrategy {\n private maxAttempts: number;\n private baseDelay: number;\n private maxDelay: number;\n private jitter: boolean;\n\n /**\n * Creates a new ExponentialBackoff instance.\n *\n * @param options - Configuration options\n * @param options.maxAttempts - Maximum number of retry attempts (default: 3)\n * @param options.baseDelay - Initial delay in milliseconds (default: 1000)\n * @param options.maxDelay - Maximum delay cap in milliseconds (default: 30000)\n * @param options.jitter - Whether to add random jitter to delays (default: true)\n */\n constructor(options: {\n maxAttempts?: number;\n baseDelay?: number;\n maxDelay?: number;\n jitter?: boolean;\n } = {}) {\n this.maxAttempts = options.maxAttempts ?? 3;\n this.baseDelay = options.baseDelay ?? 1000;\n this.maxDelay = options.maxDelay ?? 30000;\n this.jitter = options.jitter ?? true;\n }\n\n /**\n * Determines whether to retry and calculates the delay.\n *\n * @param error - The error that triggered the retry\n * @param attempt - Current attempt number (1-indexed)\n * @returns Delay in milliseconds before next retry, or null to stop retrying\n */\n onRetry(error: UPPError, attempt: number): number | null {\n if (attempt > this.maxAttempts) {\n return null;\n }\n\n if (!this.isRetryable(error)) {\n return null;\n }\n\n let delay = this.baseDelay * Math.pow(2, attempt - 1);\n delay = Math.min(delay, this.maxDelay);\n\n if (this.jitter) {\n delay = delay * (0.5 + Math.random());\n }\n\n return Math.floor(delay);\n }\n\n /**\n * Checks if an error is eligible for retry.\n *\n * @param error - The error to evaluate\n * @returns True if the error is transient and retryable\n */\n private isRetryable(error: UPPError): boolean {\n return (\n error.code === ErrorCode.RateLimited ||\n error.code === ErrorCode.NetworkError ||\n error.code === ErrorCode.Timeout ||\n error.code === ErrorCode.ProviderError\n );\n }\n}\n\n/**\n * Implements linear backoff where delays increase proportionally with each attempt.\n *\n * Unlike exponential backoff, linear backoff increases delays at a constant rate:\n * - Attempt 1: delay * 1 (e.g., 1000ms)\n * - Attempt 2: delay * 2 (e.g., 2000ms)\n * - Attempt 3: delay * 3 (e.g., 3000ms)\n *\n * This strategy is simpler and more predictable than exponential backoff,\n * suitable for scenarios where gradual delay increase is preferred over\n * aggressive backoff.\n *\n * Only retries on transient errors: RATE_LIMITED, NETWORK_ERROR, TIMEOUT, PROVIDER_ERROR\n *\n * @implements {RetryStrategy}\n *\n * @example\n * ```typescript\n * // Default configuration (3 retries, 1s delay increment)\n * const retry = new LinearBackoff();\n *\n * // Custom configuration\n * const customRetry = new LinearBackoff({\n * maxAttempts: 4, // Up to 4 retry attempts\n * delay: 2000 // 2s, 4s, 6s, 8s delays\n * });\n *\n * // Use with provider\n * const provider = createAnthropic({\n * retryStrategy: customRetry\n * });\n * ```\n */\nexport class LinearBackoff implements RetryStrategy {\n private maxAttempts: number;\n private delay: number;\n\n /**\n * Creates a new LinearBackoff instance.\n *\n * @param options - Configuration options\n * @param options.maxAttempts - Maximum number of retry attempts (default: 3)\n * @param options.delay - Base delay multiplier in milliseconds (default: 1000)\n */\n constructor(options: {\n maxAttempts?: number;\n delay?: number;\n } = {}) {\n this.maxAttempts = options.maxAttempts ?? 3;\n this.delay = options.delay ?? 1000;\n }\n\n /**\n * Determines whether to retry and calculates the linear delay.\n *\n * @param error - The error that triggered the retry\n * @param attempt - Current attempt number (1-indexed)\n * @returns Delay in milliseconds (delay * attempt), or null to stop retrying\n */\n onRetry(error: UPPError, attempt: number): number | null {\n if (attempt > this.maxAttempts) {\n return null;\n }\n\n if (!this.isRetryable(error)) {\n return null;\n }\n\n return this.delay * attempt;\n }\n\n /**\n * Checks if an error is eligible for retry.\n *\n * @param error - The error to evaluate\n * @returns True if the error is transient and retryable\n */\n private isRetryable(error: UPPError): boolean {\n return (\n error.code === ErrorCode.RateLimited ||\n error.code === ErrorCode.NetworkError ||\n error.code === ErrorCode.Timeout ||\n error.code === ErrorCode.ProviderError\n );\n }\n}\n\n/**\n * Disables all retry behavior, failing immediately on any error.\n *\n * Use this strategy when:\n * - Retries are handled at a higher level in your application\n * - You want immediate failure feedback\n * - The operation is not idempotent\n * - Time sensitivity requires fast failure\n *\n * @implements {RetryStrategy}\n *\n * @example\n * ```typescript\n * // Disable retries for time-sensitive operations\n * const provider = createOpenAI({\n * retryStrategy: new NoRetry()\n * });\n * ```\n */\nexport class NoRetry implements RetryStrategy {\n /**\n * Always returns null to indicate no retry should be attempted.\n *\n * @returns Always returns null\n */\n onRetry(_error: UPPError, _attempt: number): null {\n return null;\n }\n}\n\n/**\n * Implements token bucket rate limiting with automatic refill.\n *\n * The token bucket algorithm provides smooth rate limiting by:\n * - Maintaining a bucket of tokens that replenish over time\n * - Consuming one token per request\n * - Delaying requests when the bucket is empty\n * - Allowing burst traffic up to the bucket capacity\n *\n * This is particularly useful for:\n * - Client-side rate limiting to avoid hitting API rate limits\n * - Smoothing request patterns to maintain consistent throughput\n * - Preventing accidental API abuse\n *\n * Unlike other retry strategies, TokenBucket implements {@link beforeRequest}\n * to proactively delay requests before they are made.\n *\n * @implements {RetryStrategy}\n *\n * @example\n * ```typescript\n * // Allow 10 requests burst, refill 1 token per second\n * const bucket = new TokenBucket({\n * maxTokens: 10, // Burst capacity\n * refillRate: 1, // Tokens per second\n * maxAttempts: 3 // Retry attempts on rate limit\n * });\n *\n * // Aggressive rate limiting: 5 req/s sustained\n * const strictBucket = new TokenBucket({\n * maxTokens: 5,\n * refillRate: 5\n * });\n *\n * // Use with provider\n * const provider = createOpenAI({\n * retryStrategy: bucket\n * });\n * ```\n */\nexport class TokenBucket implements RetryStrategy {\n private tokens: number;\n private maxTokens: number;\n private refillRate: number;\n private lastRefill: number;\n private maxAttempts: number;\n private lock: Promise<void>;\n\n /**\n * Creates a new TokenBucket instance.\n *\n * @param options - Configuration options\n * @param options.maxTokens - Maximum bucket capacity (default: 10)\n * @param options.refillRate - Tokens added per second (default: 1)\n * @param options.maxAttempts - Maximum retry attempts on rate limit (default: 3)\n */\n constructor(options: {\n maxTokens?: number;\n refillRate?: number;\n maxAttempts?: number;\n } = {}) {\n this.maxTokens = options.maxTokens ?? 10;\n this.refillRate = options.refillRate ?? 1;\n this.maxAttempts = options.maxAttempts ?? 3;\n this.tokens = this.maxTokens;\n this.lastRefill = Date.now();\n this.lock = Promise.resolve();\n }\n\n /**\n * Called before each request to consume a token or calculate wait time.\n *\n * Refills the bucket based on elapsed time, then either:\n * - Returns 0 if a token is available (consumed immediately)\n * - Returns the wait time in milliseconds until the next token\n *\n * This method may allow tokens to go negative to reserve future capacity\n * and avoid concurrent callers oversubscribing the same refill.\n *\n * @returns Delay in milliseconds before the request can proceed\n */\n beforeRequest(): Promise<number> {\n return this.withLock(() => {\n this.refill();\n\n if (this.tokens >= 1) {\n this.tokens -= 1;\n return 0;\n }\n\n const deficit = 1 - this.tokens;\n const msPerToken = 1000 / this.refillRate;\n this.tokens -= 1;\n return Math.ceil(deficit * msPerToken);\n });\n }\n\n /**\n * Handles retry logic for rate-limited requests.\n *\n * Only retries on RATE_LIMITED errors, waiting for bucket refill.\n *\n * @param error - The error that triggered the retry\n * @param attempt - Current attempt number (1-indexed)\n * @returns Delay in milliseconds (time for 2 tokens), or null to stop\n */\n onRetry(error: UPPError, attempt: number): number | null {\n if (attempt > this.maxAttempts) {\n return null;\n }\n\n if (error.code !== ErrorCode.RateLimited) {\n return null;\n }\n\n const msPerToken = 1000 / this.refillRate;\n return Math.ceil(msPerToken * 2);\n }\n\n /**\n * Resets the bucket to full capacity.\n *\n * Called automatically on successful requests to restore available tokens.\n */\n reset(): void {\n void this.withLock(() => {\n this.tokens = this.maxTokens;\n this.lastRefill = Date.now();\n });\n }\n\n /**\n * Refills the bucket based on elapsed time since last refill.\n */\n private refill(): void {\n const now = Date.now();\n const elapsed = (now - this.lastRefill) / 1000;\n const newTokens = elapsed * this.refillRate;\n\n this.tokens = Math.min(this.maxTokens, this.tokens + newTokens);\n this.lastRefill = now;\n }\n\n private async withLock<T>(fn: () => T | Promise<T>): Promise<T> {\n const next = this.lock.then(fn, fn);\n this.lock = next.then(() => undefined, () => undefined);\n return next;\n }\n}\n\n/**\n * Respects server-provided Retry-After headers for optimal retry timing.\n *\n * When servers return a 429 (Too Many Requests) response, they often include\n * a Retry-After header indicating when the client should retry. This strategy\n * uses that information for precise retry timing.\n *\n * Benefits over fixed backoff strategies:\n * - Follows server recommendations for optimal retry timing\n * - Avoids retrying too early and wasting requests\n * - Adapts to dynamic rate limit windows\n *\n * If no Retry-After header is provided, falls back to a configurable delay.\n * Only retries on RATE_LIMITED errors.\n *\n * @implements {RetryStrategy}\n *\n * @example\n * ```typescript\n * // Use server-recommended retry timing\n * const retryAfter = new RetryAfterStrategy({\n * maxAttempts: 5, // Retry up to 5 times\n * fallbackDelay: 10000 // 10s fallback if no header\n * });\n *\n * // The doFetch function automatically calls setRetryAfter\n * // when a Retry-After header is present in the response\n *\n * const provider = createOpenAI({\n * retryStrategy: retryAfter\n * });\n * ```\n */\nexport class RetryAfterStrategy implements RetryStrategy {\n private maxAttempts: number;\n private fallbackDelay: number;\n private lastRetryAfter?: number;\n\n /**\n * Creates a new RetryAfterStrategy instance.\n *\n * @param options - Configuration options\n * @param options.maxAttempts - Maximum number of retry attempts (default: 3)\n * @param options.fallbackDelay - Delay in ms when no Retry-After header (default: 5000)\n */\n constructor(options: {\n maxAttempts?: number;\n fallbackDelay?: number;\n } = {}) {\n this.maxAttempts = options.maxAttempts ?? 3;\n this.fallbackDelay = options.fallbackDelay ?? 5000;\n }\n\n /**\n * Creates a request-scoped copy of this strategy.\n */\n fork(): RetryAfterStrategy {\n return new RetryAfterStrategy({\n maxAttempts: this.maxAttempts,\n fallbackDelay: this.fallbackDelay,\n });\n }\n\n /**\n * Sets the retry delay from a Retry-After header value.\n *\n * Called by doFetch when a Retry-After header is present in the response.\n * The value is used for the next onRetry call and then cleared.\n *\n * @param seconds - The Retry-After value in seconds\n */\n setRetryAfter(seconds: number): void {\n this.lastRetryAfter = seconds * 1000;\n }\n\n /**\n * Determines retry delay using Retry-After header or fallback.\n *\n * @param error - The error that triggered the retry\n * @param attempt - Current attempt number (1-indexed)\n * @returns Delay from Retry-After header or fallback, null to stop\n */\n onRetry(error: UPPError, attempt: number): number | null {\n if (attempt > this.maxAttempts) {\n return null;\n }\n\n if (error.code !== ErrorCode.RateLimited) {\n return null;\n }\n\n const delay = this.lastRetryAfter ?? this.fallbackDelay;\n this.lastRetryAfter = undefined;\n return delay;\n }\n}\n"],"mappings":";;;;;AA0CO,IAAM,qBAAN,MAAkD;AAAA,EAC/C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWR,YAAY,UAKR,CAAC,GAAG;AACN,SAAK,cAAc,QAAQ,eAAe;AAC1C,SAAK,YAAY,QAAQ,aAAa;AACtC,SAAK,WAAW,QAAQ,YAAY;AACpC,SAAK,SAAS,QAAQ,UAAU;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,QAAQ,OAAiB,SAAgC;AACvD,QAAI,UAAU,KAAK,aAAa;AAC9B,aAAO;AAAA,IACT;AAEA,QAAI,CAAC,KAAK,YAAY,KAAK,GAAG;AAC5B,aAAO;AAAA,IACT;AAEA,QAAI,QAAQ,KAAK,YAAY,KAAK,IAAI,GAAG,UAAU,CAAC;AACpD,YAAQ,KAAK,IAAI,OAAO,KAAK,QAAQ;AAErC,QAAI,KAAK,QAAQ;AACf,cAAQ,SAAS,MAAM,KAAK,OAAO;AAAA,IACrC;AAEA,WAAO,KAAK,MAAM,KAAK;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQQ,YAAY,OAA0B;AAC5C,WACE,MAAM,SAAS,UAAU,eACzB,MAAM,SAAS,UAAU,gBACzB,MAAM,SAAS,UAAU,WACzB,MAAM,SAAS,UAAU;AAAA,EAE7B;AACF;AAmCO,IAAM,gBAAN,MAA6C;AAAA,EAC1C;AAAA,EACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASR,YAAY,UAGR,CAAC,GAAG;AACN,SAAK,cAAc,QAAQ,eAAe;AAC1C,SAAK,QAAQ,QAAQ,SAAS;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,QAAQ,OAAiB,SAAgC;AACvD,QAAI,UAAU,KAAK,aAAa;AAC9B,aAAO;AAAA,IACT;AAEA,QAAI,CAAC,KAAK,YAAY,KAAK,GAAG;AAC5B,aAAO;AAAA,IACT;AAEA,WAAO,KAAK,QAAQ;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQQ,YAAY,OAA0B;AAC5C,WACE,MAAM,SAAS,UAAU,eACzB,MAAM,SAAS,UAAU,gBACzB,MAAM,SAAS,UAAU,WACzB,MAAM,SAAS,UAAU;AAAA,EAE7B;AACF;AAqBO,IAAM,UAAN,MAAuC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAM5C,QAAQ,QAAkB,UAAwB;AAChD,WAAO;AAAA,EACT;AACF;AA0CO,IAAM,cAAN,MAA2C;AAAA,EACxC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUR,YAAY,UAIR,CAAC,GAAG;AACN,SAAK,YAAY,QAAQ,aAAa;AACtC,SAAK,aAAa,QAAQ,cAAc;AACxC,SAAK,cAAc,QAAQ,eAAe;AAC1C,SAAK,SAAS,KAAK;AACnB,SAAK,aAAa,KAAK,IAAI;AAC3B,SAAK,OAAO,QAAQ,QAAQ;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcA,gBAAiC;AAC/B,WAAO,KAAK,SAAS,MAAM;AACzB,WAAK,OAAO;AAEZ,UAAI,KAAK,UAAU,GAAG;AACpB,aAAK,UAAU;AACf,eAAO;AAAA,MACT;AAEA,YAAM,UAAU,IAAI,KAAK;AACzB,YAAM,aAAa,MAAO,KAAK;AAC/B,WAAK,UAAU;AACf,aAAO,KAAK,KAAK,UAAU,UAAU;AAAA,IACvC,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,QAAQ,OAAiB,SAAgC;AACvD,QAAI,UAAU,KAAK,aAAa;AAC9B,aAAO;AAAA,IACT;AAEA,QAAI,MAAM,SAAS,UAAU,aAAa;AACxC,aAAO;AAAA,IACT;AAEA,UAAM,aAAa,MAAO,KAAK;AAC/B,WAAO,KAAK,KAAK,aAAa,CAAC;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,QAAc;AACZ,SAAK,KAAK,SAAS,MAAM;AACvB,WAAK,SAAS,KAAK;AACnB,WAAK,aAAa,KAAK,IAAI;AAAA,IAC7B,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKQ,SAAe;AACrB,UAAM,MAAM,KAAK,IAAI;AACrB,UAAM,WAAW,MAAM,KAAK,cAAc;AAC1C,UAAM,YAAY,UAAU,KAAK;AAEjC,SAAK,SAAS,KAAK,IAAI,KAAK,WAAW,KAAK,SAAS,SAAS;AAC9D,SAAK,aAAa;AAAA,EACpB;AAAA,EAEA,MAAc,SAAY,IAAsC;AAC9D,UAAM,OAAO,KAAK,KAAK,KAAK,IAAI,EAAE;AAClC,SAAK,OAAO,KAAK,KAAK,MAAM,QAAW,MAAM,MAAS;AACtD,WAAO;AAAA,EACT;AACF;AAmCO,IAAM,qBAAN,MAAM,oBAA4C;AAAA,EAC/C;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASR,YAAY,UAGR,CAAC,GAAG;AACN,SAAK,cAAc,QAAQ,eAAe;AAC1C,SAAK,gBAAgB,QAAQ,iBAAiB;AAAA,EAChD;AAAA;AAAA;AAAA;AAAA,EAKA,OAA2B;AACzB,WAAO,IAAI,oBAAmB;AAAA,MAC5B,aAAa,KAAK;AAAA,MAClB,eAAe,KAAK;AAAA,IACtB,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,cAAc,SAAuB;AACnC,SAAK,iBAAiB,UAAU;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,QAAQ,OAAiB,SAAgC;AACvD,QAAI,UAAU,KAAK,aAAa;AAC9B,aAAO;AAAA,IACT;AAEA,QAAI,MAAM,SAAS,UAAU,aAAa;AACxC,aAAO;AAAA,IACT;AAEA,UAAM,QAAQ,KAAK,kBAAkB,KAAK;AAC1C,SAAK,iBAAiB;AACtB,WAAO;AAAA,EACT;AACF;","names":[]}
|