@providerprotocol/ai 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +84 -0
- package/dist/anthropic/index.d.ts +41 -0
- package/dist/anthropic/index.js +500 -0
- package/dist/anthropic/index.js.map +1 -0
- package/dist/chunk-CUCRF5W6.js +136 -0
- package/dist/chunk-CUCRF5W6.js.map +1 -0
- package/dist/chunk-FTFX2VET.js +424 -0
- package/dist/chunk-FTFX2VET.js.map +1 -0
- package/dist/chunk-QUUX4G7U.js +117 -0
- package/dist/chunk-QUUX4G7U.js.map +1 -0
- package/dist/chunk-Y6Q7JCNP.js +39 -0
- package/dist/chunk-Y6Q7JCNP.js.map +1 -0
- package/dist/google/index.d.ts +69 -0
- package/dist/google/index.js +517 -0
- package/dist/google/index.js.map +1 -0
- package/dist/http/index.d.ts +61 -0
- package/dist/http/index.js +43 -0
- package/dist/http/index.js.map +1 -0
- package/dist/index.d.ts +792 -0
- package/dist/index.js +898 -0
- package/dist/index.js.map +1 -0
- package/dist/openai/index.d.ts +204 -0
- package/dist/openai/index.js +1340 -0
- package/dist/openai/index.js.map +1 -0
- package/dist/provider-CUJWjgNl.d.ts +192 -0
- package/dist/retry-I2661_rv.d.ts +118 -0
- package/package.json +88 -0
- package/src/anthropic/index.ts +3 -0
- package/src/core/image.ts +188 -0
- package/src/core/llm.ts +619 -0
- package/src/core/provider.ts +92 -0
- package/src/google/index.ts +3 -0
- package/src/http/errors.ts +112 -0
- package/src/http/fetch.ts +210 -0
- package/src/http/index.ts +31 -0
- package/src/http/keys.ts +136 -0
- package/src/http/retry.ts +205 -0
- package/src/http/sse.ts +136 -0
- package/src/index.ts +32 -0
- package/src/openai/index.ts +9 -0
- package/src/providers/anthropic/index.ts +17 -0
- package/src/providers/anthropic/llm.ts +196 -0
- package/src/providers/anthropic/transform.ts +452 -0
- package/src/providers/anthropic/types.ts +213 -0
- package/src/providers/google/index.ts +17 -0
- package/src/providers/google/llm.ts +203 -0
- package/src/providers/google/transform.ts +487 -0
- package/src/providers/google/types.ts +214 -0
- package/src/providers/openai/index.ts +151 -0
- package/src/providers/openai/llm.completions.ts +201 -0
- package/src/providers/openai/llm.responses.ts +211 -0
- package/src/providers/openai/transform.completions.ts +628 -0
- package/src/providers/openai/transform.responses.ts +718 -0
- package/src/providers/openai/types.ts +711 -0
- package/src/types/content.ts +133 -0
- package/src/types/errors.ts +85 -0
- package/src/types/index.ts +105 -0
- package/src/types/llm.ts +211 -0
- package/src/types/messages.ts +182 -0
- package/src/types/provider.ts +195 -0
- package/src/types/schema.ts +58 -0
- package/src/types/stream.ts +146 -0
- package/src/types/thread.ts +226 -0
- package/src/types/tool.ts +88 -0
- package/src/types/turn.ts +118 -0
- package/src/utils/id.ts +28 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { UPPError, type ErrorCode, type Modality } from '../types/errors.ts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Map HTTP status codes to UPP error codes
|
|
5
|
+
*/
|
|
6
|
+
export function statusToErrorCode(status: number): ErrorCode {
|
|
7
|
+
switch (status) {
|
|
8
|
+
case 400:
|
|
9
|
+
return 'INVALID_REQUEST';
|
|
10
|
+
case 401:
|
|
11
|
+
case 403:
|
|
12
|
+
return 'AUTHENTICATION_FAILED';
|
|
13
|
+
case 404:
|
|
14
|
+
return 'MODEL_NOT_FOUND';
|
|
15
|
+
case 408:
|
|
16
|
+
return 'TIMEOUT';
|
|
17
|
+
case 413:
|
|
18
|
+
return 'CONTEXT_LENGTH_EXCEEDED';
|
|
19
|
+
case 429:
|
|
20
|
+
return 'RATE_LIMITED';
|
|
21
|
+
case 500:
|
|
22
|
+
case 502:
|
|
23
|
+
case 503:
|
|
24
|
+
case 504:
|
|
25
|
+
return 'PROVIDER_ERROR';
|
|
26
|
+
default:
|
|
27
|
+
return 'PROVIDER_ERROR';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Normalize HTTP error responses to UPPError
|
|
33
|
+
* Maps HTTP status codes to appropriate ErrorCode values
|
|
34
|
+
* Extracts error message from response body when available
|
|
35
|
+
*/
|
|
36
|
+
export async function normalizeHttpError(
|
|
37
|
+
response: Response,
|
|
38
|
+
provider: string,
|
|
39
|
+
modality: Modality
|
|
40
|
+
): Promise<UPPError> {
|
|
41
|
+
const code = statusToErrorCode(response.status);
|
|
42
|
+
let message = `HTTP ${response.status}: ${response.statusText}`;
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const body = await response.text();
|
|
46
|
+
if (body) {
|
|
47
|
+
try {
|
|
48
|
+
const json = JSON.parse(body);
|
|
49
|
+
// Common error message locations across providers
|
|
50
|
+
const extractedMessage =
|
|
51
|
+
json.error?.message ||
|
|
52
|
+
json.message ||
|
|
53
|
+
json.error?.error?.message ||
|
|
54
|
+
json.detail;
|
|
55
|
+
|
|
56
|
+
if (extractedMessage) {
|
|
57
|
+
message = extractedMessage;
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// Body is not JSON, use raw text if short
|
|
61
|
+
if (body.length < 200) {
|
|
62
|
+
message = body;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
// Failed to read body, use default message
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return new UPPError(message, code, provider, modality, response.status);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Create a network error
|
|
75
|
+
*/
|
|
76
|
+
export function networkError(
|
|
77
|
+
error: Error,
|
|
78
|
+
provider: string,
|
|
79
|
+
modality: Modality
|
|
80
|
+
): UPPError {
|
|
81
|
+
return new UPPError(
|
|
82
|
+
`Network error: ${error.message}`,
|
|
83
|
+
'NETWORK_ERROR',
|
|
84
|
+
provider,
|
|
85
|
+
modality,
|
|
86
|
+
undefined,
|
|
87
|
+
error
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Create a timeout error
|
|
93
|
+
*/
|
|
94
|
+
export function timeoutError(
|
|
95
|
+
timeout: number,
|
|
96
|
+
provider: string,
|
|
97
|
+
modality: Modality
|
|
98
|
+
): UPPError {
|
|
99
|
+
return new UPPError(
|
|
100
|
+
`Request timed out after ${timeout}ms`,
|
|
101
|
+
'TIMEOUT',
|
|
102
|
+
provider,
|
|
103
|
+
modality
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Create a cancelled error
|
|
109
|
+
*/
|
|
110
|
+
export function cancelledError(provider: string, modality: Modality): UPPError {
|
|
111
|
+
return new UPPError('Request was cancelled', 'CANCELLED', provider, modality);
|
|
112
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import type { ProviderConfig } from '../types/provider.ts';
|
|
2
|
+
import type { Modality } from '../types/errors.ts';
|
|
3
|
+
import { UPPError } from '../types/errors.ts';
|
|
4
|
+
import {
|
|
5
|
+
normalizeHttpError,
|
|
6
|
+
networkError,
|
|
7
|
+
timeoutError,
|
|
8
|
+
cancelledError,
|
|
9
|
+
} from './errors.ts';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Default timeout in milliseconds
|
|
13
|
+
*/
|
|
14
|
+
const DEFAULT_TIMEOUT = 120000; // 2 minutes
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Execute fetch with retry, timeout, and error normalization
|
|
18
|
+
*
|
|
19
|
+
* @param url - Request URL
|
|
20
|
+
* @param init - Fetch init options
|
|
21
|
+
* @param config - Provider config
|
|
22
|
+
* @param provider - Provider name for error messages
|
|
23
|
+
* @param modality - Modality for error messages
|
|
24
|
+
*/
|
|
25
|
+
export async function doFetch(
|
|
26
|
+
url: string,
|
|
27
|
+
init: RequestInit,
|
|
28
|
+
config: ProviderConfig,
|
|
29
|
+
provider: string,
|
|
30
|
+
modality: Modality
|
|
31
|
+
): Promise<Response> {
|
|
32
|
+
const fetchFn = config.fetch ?? fetch;
|
|
33
|
+
const timeout = config.timeout ?? DEFAULT_TIMEOUT;
|
|
34
|
+
const strategy = config.retryStrategy;
|
|
35
|
+
|
|
36
|
+
// Pre-request delay (e.g., token bucket)
|
|
37
|
+
if (strategy?.beforeRequest) {
|
|
38
|
+
const delay = await strategy.beforeRequest();
|
|
39
|
+
if (delay > 0) {
|
|
40
|
+
await sleep(delay);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let lastError: UPPError | undefined;
|
|
45
|
+
let attempt = 0;
|
|
46
|
+
|
|
47
|
+
while (true) {
|
|
48
|
+
attempt++;
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const response = await fetchWithTimeout(
|
|
52
|
+
fetchFn,
|
|
53
|
+
url,
|
|
54
|
+
init,
|
|
55
|
+
timeout,
|
|
56
|
+
provider,
|
|
57
|
+
modality
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// Check for HTTP errors
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
const error = await normalizeHttpError(response, provider, modality);
|
|
63
|
+
|
|
64
|
+
// Check for Retry-After header
|
|
65
|
+
const retryAfter = response.headers.get('Retry-After');
|
|
66
|
+
if (retryAfter && strategy) {
|
|
67
|
+
const seconds = parseInt(retryAfter, 10);
|
|
68
|
+
if (!isNaN(seconds) && 'setRetryAfter' in strategy) {
|
|
69
|
+
(strategy as { setRetryAfter: (s: number) => void }).setRetryAfter(
|
|
70
|
+
seconds
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Try to retry
|
|
76
|
+
if (strategy) {
|
|
77
|
+
const delay = await strategy.onRetry(error, attempt);
|
|
78
|
+
if (delay !== null) {
|
|
79
|
+
await sleep(delay);
|
|
80
|
+
lastError = error;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Success - reset strategy state
|
|
89
|
+
strategy?.reset?.();
|
|
90
|
+
|
|
91
|
+
return response;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
// Already a UPPError, handle retry
|
|
94
|
+
if (error instanceof UPPError) {
|
|
95
|
+
if (strategy) {
|
|
96
|
+
const delay = await strategy.onRetry(error, attempt);
|
|
97
|
+
if (delay !== null) {
|
|
98
|
+
await sleep(delay);
|
|
99
|
+
lastError = error;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Network error
|
|
107
|
+
const uppError = networkError(error as Error, provider, modality);
|
|
108
|
+
|
|
109
|
+
if (strategy) {
|
|
110
|
+
const delay = await strategy.onRetry(uppError, attempt);
|
|
111
|
+
if (delay !== null) {
|
|
112
|
+
await sleep(delay);
|
|
113
|
+
lastError = uppError;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
throw uppError;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Fetch with timeout
|
|
125
|
+
*/
|
|
126
|
+
async function fetchWithTimeout(
|
|
127
|
+
fetchFn: typeof fetch,
|
|
128
|
+
url: string,
|
|
129
|
+
init: RequestInit,
|
|
130
|
+
timeout: number,
|
|
131
|
+
provider: string,
|
|
132
|
+
modality: Modality
|
|
133
|
+
): Promise<Response> {
|
|
134
|
+
const controller = new AbortController();
|
|
135
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
136
|
+
|
|
137
|
+
// Merge abort signals if one was provided
|
|
138
|
+
const existingSignal = init.signal;
|
|
139
|
+
if (existingSignal) {
|
|
140
|
+
existingSignal.addEventListener('abort', () => controller.abort());
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const response = await fetchFn(url, {
|
|
145
|
+
...init,
|
|
146
|
+
signal: controller.signal,
|
|
147
|
+
});
|
|
148
|
+
return response;
|
|
149
|
+
} catch (error) {
|
|
150
|
+
if ((error as Error).name === 'AbortError') {
|
|
151
|
+
// Check if it was the user's signal or our timeout
|
|
152
|
+
if (existingSignal?.aborted) {
|
|
153
|
+
throw cancelledError(provider, modality);
|
|
154
|
+
}
|
|
155
|
+
throw timeoutError(timeout, provider, modality);
|
|
156
|
+
}
|
|
157
|
+
throw error;
|
|
158
|
+
} finally {
|
|
159
|
+
clearTimeout(timeoutId);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Sleep for a given number of milliseconds
|
|
165
|
+
*/
|
|
166
|
+
function sleep(ms: number): Promise<void> {
|
|
167
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Streaming fetch - returns response without checking ok status
|
|
172
|
+
* Used when we need to read the stream for SSE
|
|
173
|
+
*/
|
|
174
|
+
export async function doStreamFetch(
|
|
175
|
+
url: string,
|
|
176
|
+
init: RequestInit,
|
|
177
|
+
config: ProviderConfig,
|
|
178
|
+
provider: string,
|
|
179
|
+
modality: Modality
|
|
180
|
+
): Promise<Response> {
|
|
181
|
+
const fetchFn = config.fetch ?? fetch;
|
|
182
|
+
const timeout = config.timeout ?? DEFAULT_TIMEOUT;
|
|
183
|
+
const strategy = config.retryStrategy;
|
|
184
|
+
|
|
185
|
+
// Pre-request delay
|
|
186
|
+
if (strategy?.beforeRequest) {
|
|
187
|
+
const delay = await strategy.beforeRequest();
|
|
188
|
+
if (delay > 0) {
|
|
189
|
+
await sleep(delay);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// For streaming, we don't retry - the consumer handles errors
|
|
194
|
+
try {
|
|
195
|
+
const response = await fetchWithTimeout(
|
|
196
|
+
fetchFn,
|
|
197
|
+
url,
|
|
198
|
+
init,
|
|
199
|
+
timeout,
|
|
200
|
+
provider,
|
|
201
|
+
modality
|
|
202
|
+
);
|
|
203
|
+
return response;
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (error instanceof UPPError) {
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
throw networkError(error as Error, provider, modality);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Key management
|
|
2
|
+
export {
|
|
3
|
+
resolveApiKey,
|
|
4
|
+
RoundRobinKeys,
|
|
5
|
+
WeightedKeys,
|
|
6
|
+
DynamicKey,
|
|
7
|
+
} from './keys.ts';
|
|
8
|
+
|
|
9
|
+
// Retry strategies
|
|
10
|
+
export {
|
|
11
|
+
ExponentialBackoff,
|
|
12
|
+
LinearBackoff,
|
|
13
|
+
NoRetry,
|
|
14
|
+
TokenBucket,
|
|
15
|
+
RetryAfterStrategy,
|
|
16
|
+
} from './retry.ts';
|
|
17
|
+
|
|
18
|
+
// HTTP fetch
|
|
19
|
+
export { doFetch, doStreamFetch } from './fetch.ts';
|
|
20
|
+
|
|
21
|
+
// SSE parsing
|
|
22
|
+
export { parseSSEStream, parseSimpleTextStream } from './sse.ts';
|
|
23
|
+
|
|
24
|
+
// Error utilities
|
|
25
|
+
export {
|
|
26
|
+
normalizeHttpError,
|
|
27
|
+
networkError,
|
|
28
|
+
timeoutError,
|
|
29
|
+
cancelledError,
|
|
30
|
+
statusToErrorCode,
|
|
31
|
+
} from './errors.ts';
|
package/src/http/keys.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { ProviderConfig, KeyStrategy } from '../types/provider.ts';
|
|
2
|
+
import { UPPError, type Modality } from '../types/errors.ts';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Round-robin through a list of API keys
|
|
6
|
+
*/
|
|
7
|
+
export class RoundRobinKeys implements KeyStrategy {
|
|
8
|
+
private keys: string[];
|
|
9
|
+
private index = 0;
|
|
10
|
+
|
|
11
|
+
constructor(keys: string[]) {
|
|
12
|
+
if (keys.length === 0) {
|
|
13
|
+
throw new Error('RoundRobinKeys requires at least one key');
|
|
14
|
+
}
|
|
15
|
+
this.keys = keys;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
getKey(): string {
|
|
19
|
+
const key = this.keys[this.index]!;
|
|
20
|
+
this.index = (this.index + 1) % this.keys.length;
|
|
21
|
+
return key;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Weighted random selection of API keys
|
|
27
|
+
*/
|
|
28
|
+
export class WeightedKeys implements KeyStrategy {
|
|
29
|
+
private entries: Array<{ key: string; weight: number }>;
|
|
30
|
+
private totalWeight: number;
|
|
31
|
+
|
|
32
|
+
constructor(keys: Array<{ key: string; weight: number }>) {
|
|
33
|
+
if (keys.length === 0) {
|
|
34
|
+
throw new Error('WeightedKeys requires at least one key');
|
|
35
|
+
}
|
|
36
|
+
this.entries = keys;
|
|
37
|
+
this.totalWeight = keys.reduce((sum, k) => sum + k.weight, 0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
getKey(): string {
|
|
41
|
+
const random = Math.random() * this.totalWeight;
|
|
42
|
+
let cumulative = 0;
|
|
43
|
+
|
|
44
|
+
for (const entry of this.entries) {
|
|
45
|
+
cumulative += entry.weight;
|
|
46
|
+
if (random <= cumulative) {
|
|
47
|
+
return entry.key;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Fallback to last key (shouldn't happen)
|
|
52
|
+
return this.entries[this.entries.length - 1]!.key;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Dynamic key selection based on custom logic
|
|
58
|
+
*/
|
|
59
|
+
export class DynamicKey implements KeyStrategy {
|
|
60
|
+
private selector: () => string | Promise<string>;
|
|
61
|
+
|
|
62
|
+
constructor(selector: () => string | Promise<string>) {
|
|
63
|
+
this.selector = selector;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async getKey(): Promise<string> {
|
|
67
|
+
return this.selector();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check if a value is a KeyStrategy
|
|
73
|
+
*/
|
|
74
|
+
function isKeyStrategy(value: unknown): value is KeyStrategy {
|
|
75
|
+
return (
|
|
76
|
+
typeof value === 'object' &&
|
|
77
|
+
value !== null &&
|
|
78
|
+
'getKey' in value &&
|
|
79
|
+
typeof (value as KeyStrategy).getKey === 'function'
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Resolve API key from ProviderConfig
|
|
85
|
+
* Falls back to environment variable if provided and config.apiKey is not set
|
|
86
|
+
* Throws UPPError with AUTHENTICATION_FAILED if no key is available
|
|
87
|
+
*
|
|
88
|
+
* @param config - Provider configuration
|
|
89
|
+
* @param envVar - Environment variable name to check as fallback
|
|
90
|
+
* @param provider - Provider name for error messages
|
|
91
|
+
* @param modality - Modality for error messages
|
|
92
|
+
*/
|
|
93
|
+
export async function resolveApiKey(
|
|
94
|
+
config: ProviderConfig,
|
|
95
|
+
envVar?: string,
|
|
96
|
+
provider = 'unknown',
|
|
97
|
+
modality: Modality = 'llm'
|
|
98
|
+
): Promise<string> {
|
|
99
|
+
const { apiKey } = config;
|
|
100
|
+
|
|
101
|
+
// If apiKey is provided in config
|
|
102
|
+
if (apiKey !== undefined) {
|
|
103
|
+
// String
|
|
104
|
+
if (typeof apiKey === 'string') {
|
|
105
|
+
return apiKey;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Function
|
|
109
|
+
if (typeof apiKey === 'function') {
|
|
110
|
+
return apiKey();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// KeyStrategy
|
|
114
|
+
if (isKeyStrategy(apiKey)) {
|
|
115
|
+
return apiKey.getKey();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Try environment variable
|
|
120
|
+
if (envVar) {
|
|
121
|
+
const envValue = process.env[envVar];
|
|
122
|
+
if (envValue) {
|
|
123
|
+
return envValue;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// No key found
|
|
128
|
+
throw new UPPError(
|
|
129
|
+
envVar
|
|
130
|
+
? `API key not found. Set ${envVar} environment variable or provide apiKey in config.`
|
|
131
|
+
: 'API key not found. Provide apiKey in config.',
|
|
132
|
+
'AUTHENTICATION_FAILED',
|
|
133
|
+
provider,
|
|
134
|
+
modality
|
|
135
|
+
);
|
|
136
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import type { RetryStrategy } from '../types/provider.ts';
|
|
2
|
+
import type { UPPError } from '../types/errors.ts';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Exponential backoff retry strategy
|
|
6
|
+
*/
|
|
7
|
+
export class ExponentialBackoff implements RetryStrategy {
|
|
8
|
+
private maxAttempts: number;
|
|
9
|
+
private baseDelay: number;
|
|
10
|
+
private maxDelay: number;
|
|
11
|
+
private jitter: boolean;
|
|
12
|
+
|
|
13
|
+
constructor(options: {
|
|
14
|
+
maxAttempts?: number;
|
|
15
|
+
baseDelay?: number;
|
|
16
|
+
maxDelay?: number;
|
|
17
|
+
jitter?: boolean;
|
|
18
|
+
} = {}) {
|
|
19
|
+
this.maxAttempts = options.maxAttempts ?? 3;
|
|
20
|
+
this.baseDelay = options.baseDelay ?? 1000;
|
|
21
|
+
this.maxDelay = options.maxDelay ?? 30000;
|
|
22
|
+
this.jitter = options.jitter ?? true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
onRetry(error: UPPError, attempt: number): number | null {
|
|
26
|
+
if (attempt > this.maxAttempts) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Only retry on retryable errors
|
|
31
|
+
if (!this.isRetryable(error)) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Calculate delay with exponential backoff
|
|
36
|
+
let delay = this.baseDelay * Math.pow(2, attempt - 1);
|
|
37
|
+
delay = Math.min(delay, this.maxDelay);
|
|
38
|
+
|
|
39
|
+
// Add jitter
|
|
40
|
+
if (this.jitter) {
|
|
41
|
+
delay = delay * (0.5 + Math.random());
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return Math.floor(delay);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private isRetryable(error: UPPError): boolean {
|
|
48
|
+
return (
|
|
49
|
+
error.code === 'RATE_LIMITED' ||
|
|
50
|
+
error.code === 'NETWORK_ERROR' ||
|
|
51
|
+
error.code === 'TIMEOUT' ||
|
|
52
|
+
error.code === 'PROVIDER_ERROR'
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Linear backoff retry strategy
|
|
59
|
+
*/
|
|
60
|
+
export class LinearBackoff implements RetryStrategy {
|
|
61
|
+
private maxAttempts: number;
|
|
62
|
+
private delay: number;
|
|
63
|
+
|
|
64
|
+
constructor(options: {
|
|
65
|
+
maxAttempts?: number;
|
|
66
|
+
delay?: number;
|
|
67
|
+
} = {}) {
|
|
68
|
+
this.maxAttempts = options.maxAttempts ?? 3;
|
|
69
|
+
this.delay = options.delay ?? 1000;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
onRetry(error: UPPError, attempt: number): number | null {
|
|
73
|
+
if (attempt > this.maxAttempts) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Only retry on retryable errors
|
|
78
|
+
if (!this.isRetryable(error)) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return this.delay * attempt;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private isRetryable(error: UPPError): boolean {
|
|
86
|
+
return (
|
|
87
|
+
error.code === 'RATE_LIMITED' ||
|
|
88
|
+
error.code === 'NETWORK_ERROR' ||
|
|
89
|
+
error.code === 'TIMEOUT' ||
|
|
90
|
+
error.code === 'PROVIDER_ERROR'
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* No retry strategy - fail immediately
|
|
97
|
+
*/
|
|
98
|
+
export class NoRetry implements RetryStrategy {
|
|
99
|
+
onRetry(): null {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Token bucket rate limiter with retry
|
|
106
|
+
*/
|
|
107
|
+
export class TokenBucket implements RetryStrategy {
|
|
108
|
+
private tokens: number;
|
|
109
|
+
private maxTokens: number;
|
|
110
|
+
private refillRate: number; // tokens per second
|
|
111
|
+
private lastRefill: number;
|
|
112
|
+
private maxAttempts: number;
|
|
113
|
+
|
|
114
|
+
constructor(options: {
|
|
115
|
+
maxTokens?: number;
|
|
116
|
+
refillRate?: number;
|
|
117
|
+
maxAttempts?: number;
|
|
118
|
+
} = {}) {
|
|
119
|
+
this.maxTokens = options.maxTokens ?? 10;
|
|
120
|
+
this.refillRate = options.refillRate ?? 1;
|
|
121
|
+
this.maxAttempts = options.maxAttempts ?? 3;
|
|
122
|
+
this.tokens = this.maxTokens;
|
|
123
|
+
this.lastRefill = Date.now();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
beforeRequest(): number {
|
|
127
|
+
this.refill();
|
|
128
|
+
|
|
129
|
+
if (this.tokens >= 1) {
|
|
130
|
+
this.tokens -= 1;
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Calculate time until next token
|
|
135
|
+
const msPerToken = 1000 / this.refillRate;
|
|
136
|
+
return Math.ceil(msPerToken);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
onRetry(error: UPPError, attempt: number): number | null {
|
|
140
|
+
if (attempt > this.maxAttempts) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (error.code !== 'RATE_LIMITED') {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Wait for token bucket to refill
|
|
149
|
+
const msPerToken = 1000 / this.refillRate;
|
|
150
|
+
return Math.ceil(msPerToken * 2); // Wait for 2 tokens
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
reset(): void {
|
|
154
|
+
this.tokens = this.maxTokens;
|
|
155
|
+
this.lastRefill = Date.now();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private refill(): void {
|
|
159
|
+
const now = Date.now();
|
|
160
|
+
const elapsed = (now - this.lastRefill) / 1000;
|
|
161
|
+
const newTokens = elapsed * this.refillRate;
|
|
162
|
+
|
|
163
|
+
this.tokens = Math.min(this.maxTokens, this.tokens + newTokens);
|
|
164
|
+
this.lastRefill = now;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Retry strategy that respects Retry-After headers
|
|
170
|
+
*/
|
|
171
|
+
export class RetryAfterStrategy implements RetryStrategy {
|
|
172
|
+
private maxAttempts: number;
|
|
173
|
+
private fallbackDelay: number;
|
|
174
|
+
private lastRetryAfter?: number;
|
|
175
|
+
|
|
176
|
+
constructor(options: {
|
|
177
|
+
maxAttempts?: number;
|
|
178
|
+
fallbackDelay?: number;
|
|
179
|
+
} = {}) {
|
|
180
|
+
this.maxAttempts = options.maxAttempts ?? 3;
|
|
181
|
+
this.fallbackDelay = options.fallbackDelay ?? 5000;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Set the Retry-After value from response headers
|
|
186
|
+
* Call this before onRetry when you have a Retry-After header
|
|
187
|
+
*/
|
|
188
|
+
setRetryAfter(seconds: number): void {
|
|
189
|
+
this.lastRetryAfter = seconds * 1000;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
onRetry(error: UPPError, attempt: number): number | null {
|
|
193
|
+
if (attempt > this.maxAttempts) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (error.code !== 'RATE_LIMITED') {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const delay = this.lastRetryAfter ?? this.fallbackDelay;
|
|
202
|
+
this.lastRetryAfter = undefined;
|
|
203
|
+
return delay;
|
|
204
|
+
}
|
|
205
|
+
}
|