@ottocode/ai-sdk 0.1.4 → 0.1.5

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/src/fetch.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  import type { WalletContext } from './auth.ts';
2
- import type { PaymentCallbacks, PaymentOptions, CacheOptions, BalanceUpdate, AnthropicCacheConfig } from './types.ts';
3
- import { buildWalletHeaders } from './auth.ts';
2
+ import type {
3
+ PaymentCallbacks,
4
+ PaymentOptions,
5
+ CacheOptions,
6
+ BalanceUpdate,
7
+ } from './types.ts';
4
8
  import { pickPaymentRequirement, handlePayment } from './payment.ts';
5
9
  import { addAnthropicCacheControl } from './cache.ts';
6
10
 
@@ -9,280 +13,297 @@ const DEFAULT_MAX_ATTEMPTS = 3;
9
13
  const DEFAULT_MAX_PAYMENT_ATTEMPTS = 20;
10
14
 
11
15
  interface PaymentQueueEntry {
12
- promise: Promise<void>;
13
- resolve: () => void;
16
+ promise: Promise<void>;
17
+ resolve: () => void;
14
18
  }
15
19
 
16
20
  const paymentQueues = new Map<string, PaymentQueueEntry>();
17
21
  const globalPaymentAttempts = new Map<string, number>();
18
22
 
19
23
  async function acquirePaymentLock(walletAddress: string): Promise<() => void> {
20
- const existing = paymentQueues.get(walletAddress);
21
-
22
- let resolveFunc: () => void = () => {};
23
- const newPromise = new Promise<void>((resolve) => {
24
- resolveFunc = resolve;
25
- });
26
-
27
- const entry: PaymentQueueEntry = { promise: newPromise, resolve: resolveFunc };
28
- paymentQueues.set(walletAddress, entry);
29
-
30
- if (existing) {
31
- await existing.promise;
32
- }
33
-
34
- return () => {
35
- if (paymentQueues.get(walletAddress) === entry) {
36
- paymentQueues.delete(walletAddress);
37
- }
38
- resolveFunc();
39
- };
24
+ const existing = paymentQueues.get(walletAddress);
25
+
26
+ let resolveFunc: () => void = () => {};
27
+ const newPromise = new Promise<void>((resolve) => {
28
+ resolveFunc = resolve;
29
+ });
30
+
31
+ const entry: PaymentQueueEntry = {
32
+ promise: newPromise,
33
+ resolve: resolveFunc,
34
+ };
35
+ paymentQueues.set(walletAddress, entry);
36
+
37
+ if (existing) {
38
+ await existing.promise;
39
+ }
40
+
41
+ return () => {
42
+ if (paymentQueues.get(walletAddress) === entry) {
43
+ paymentQueues.delete(walletAddress);
44
+ }
45
+ resolveFunc();
46
+ };
40
47
  }
41
48
 
42
49
  function tryParseSetuComment(
43
- line: string,
44
- onBalanceUpdate: (update: BalanceUpdate) => void,
50
+ line: string,
51
+ onBalanceUpdate: (update: BalanceUpdate) => void,
45
52
  ) {
46
- const trimmed = line.replace(/\r$/, '');
47
- if (!trimmed.startsWith(': setu ')) return;
48
- try {
49
- const data = JSON.parse(trimmed.slice(7));
50
- onBalanceUpdate({
51
- costUsd: parseFloat(data.cost_usd ?? '0'),
52
- balanceRemaining: parseFloat(data.balance_remaining ?? '0'),
53
- inputTokens: data.input_tokens ? Number(data.input_tokens) : undefined,
54
- outputTokens: data.output_tokens ? Number(data.output_tokens) : undefined,
55
- });
56
- } catch {}
53
+ const trimmed = line.replace(/\r$/, '');
54
+ if (!trimmed.startsWith(': setu ')) return;
55
+ try {
56
+ const data = JSON.parse(trimmed.slice(7));
57
+ onBalanceUpdate({
58
+ costUsd: parseFloat(data.cost_usd ?? '0'),
59
+ balanceRemaining: parseFloat(data.balance_remaining ?? '0'),
60
+ inputTokens: data.input_tokens ? Number(data.input_tokens) : undefined,
61
+ outputTokens: data.output_tokens ? Number(data.output_tokens) : undefined,
62
+ });
63
+ } catch {}
57
64
  }
58
65
 
59
66
  function wrapResponseWithBalanceSniffing(
60
- response: Response,
61
- callbacks: PaymentCallbacks,
67
+ response: Response,
68
+ callbacks: PaymentCallbacks,
62
69
  ): Response {
63
- if (!callbacks.onBalanceUpdate) return response;
64
-
65
- const balanceHeader = response.headers.get('x-balance-remaining');
66
- const costHeader = response.headers.get('x-cost-usd');
67
- if (balanceHeader && costHeader) {
68
- callbacks.onBalanceUpdate({
69
- costUsd: parseFloat(costHeader),
70
- balanceRemaining: parseFloat(balanceHeader),
71
- });
72
- return response;
73
- }
74
-
75
- if (!response.body) return response;
76
-
77
- const onBalanceUpdate = callbacks.onBalanceUpdate;
78
- let partial = '';
79
- const decoder = new TextDecoder();
80
- const transform = new TransformStream<Uint8Array, Uint8Array>({
81
- transform(chunk, controller) {
82
- controller.enqueue(chunk);
83
- partial += decoder.decode(chunk, { stream: true });
84
- let nlIndex = partial.indexOf('\n');
85
- while (nlIndex !== -1) {
86
- const line = partial.slice(0, nlIndex);
87
- partial = partial.slice(nlIndex + 1);
88
- tryParseSetuComment(line, onBalanceUpdate);
89
- nlIndex = partial.indexOf('\n');
90
- }
91
- },
92
- flush() {
93
- if (partial.trim()) {
94
- tryParseSetuComment(partial, onBalanceUpdate);
95
- }
96
- },
97
- });
98
-
99
- return new Response(response.body.pipeThrough(transform), {
100
- status: response.status,
101
- statusText: response.statusText,
102
- headers: response.headers,
103
- });
70
+ if (!callbacks.onBalanceUpdate) return response;
71
+
72
+ const balanceHeader = response.headers.get('x-balance-remaining');
73
+ const costHeader = response.headers.get('x-cost-usd');
74
+ if (balanceHeader && costHeader) {
75
+ callbacks.onBalanceUpdate({
76
+ costUsd: parseFloat(costHeader),
77
+ balanceRemaining: parseFloat(balanceHeader),
78
+ });
79
+ return response;
80
+ }
81
+
82
+ if (!response.body) return response;
83
+
84
+ const onBalanceUpdate = callbacks.onBalanceUpdate;
85
+ let partial = '';
86
+ const decoder = new TextDecoder();
87
+ const transform = new TransformStream<Uint8Array, Uint8Array>({
88
+ transform(chunk, controller) {
89
+ controller.enqueue(chunk);
90
+ partial += decoder.decode(chunk, { stream: true });
91
+ let nlIndex = partial.indexOf('\n');
92
+ while (nlIndex !== -1) {
93
+ const line = partial.slice(0, nlIndex);
94
+ partial = partial.slice(nlIndex + 1);
95
+ tryParseSetuComment(line, onBalanceUpdate);
96
+ nlIndex = partial.indexOf('\n');
97
+ }
98
+ },
99
+ flush() {
100
+ if (partial.trim()) {
101
+ tryParseSetuComment(partial, onBalanceUpdate);
102
+ }
103
+ },
104
+ });
105
+
106
+ return new Response(response.body.pipeThrough(transform), {
107
+ status: response.status,
108
+ statusText: response.statusText,
109
+ headers: response.headers,
110
+ });
104
111
  }
105
112
 
106
113
  async function getWalletUsdcBalance(
107
- walletAddress: string,
108
- rpcUrl: string,
114
+ walletAddress: string,
115
+ rpcUrl: string,
109
116
  ): Promise<number> {
110
- const USDC_MINT_MAINNET = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
111
- const USDC_MINT_DEVNET = '4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU';
112
-
113
- try {
114
- const usdcMint = rpcUrl.includes('devnet') ? USDC_MINT_DEVNET : USDC_MINT_MAINNET;
115
- const response = await fetch(rpcUrl, {
116
- method: 'POST',
117
- headers: { 'Content-Type': 'application/json' },
118
- body: JSON.stringify({
119
- jsonrpc: '2.0',
120
- id: 1,
121
- method: 'getTokenAccountsByOwner',
122
- params: [walletAddress, { mint: usdcMint }, { encoding: 'jsonParsed' }],
123
- }),
124
- });
125
- if (!response.ok) return 0;
126
- const data = (await response.json()) as {
127
- result?: {
128
- value?: Array<{
129
- account: {
130
- data: { parsed: { info: { tokenAmount: { uiAmount: number } } } };
131
- };
132
- }>;
133
- };
134
- };
135
- let total = 0;
136
- for (const acct of data.result?.value ?? []) {
137
- total += acct.account.data.parsed.info.tokenAmount.uiAmount ?? 0;
138
- }
139
- return total;
140
- } catch {
141
- return 0;
142
- }
117
+ const USDC_MINT_MAINNET = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
118
+ const USDC_MINT_DEVNET = '4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU';
119
+
120
+ try {
121
+ const usdcMint = rpcUrl.includes('devnet')
122
+ ? USDC_MINT_DEVNET
123
+ : USDC_MINT_MAINNET;
124
+ const response = await fetch(rpcUrl, {
125
+ method: 'POST',
126
+ headers: { 'Content-Type': 'application/json' },
127
+ body: JSON.stringify({
128
+ jsonrpc: '2.0',
129
+ id: 1,
130
+ method: 'getTokenAccountsByOwner',
131
+ params: [walletAddress, { mint: usdcMint }, { encoding: 'jsonParsed' }],
132
+ }),
133
+ });
134
+ if (!response.ok) return 0;
135
+ const data = (await response.json()) as {
136
+ result?: {
137
+ value?: Array<{
138
+ account: {
139
+ data: { parsed: { info: { tokenAmount: { uiAmount: number } } } };
140
+ };
141
+ }>;
142
+ };
143
+ };
144
+ let total = 0;
145
+ for (const acct of data.result?.value ?? []) {
146
+ total += acct.account.data.parsed.info.tokenAmount.uiAmount ?? 0;
147
+ }
148
+ return total;
149
+ } catch {
150
+ return 0;
151
+ }
143
152
  }
144
153
 
145
154
  export interface CreateSetuFetchOptions {
146
- wallet: WalletContext;
147
- baseURL: string;
148
- rpcURL?: string;
149
- callbacks?: PaymentCallbacks;
150
- cache?: CacheOptions;
151
- payment?: PaymentOptions;
155
+ wallet: WalletContext;
156
+ baseURL: string;
157
+ rpcURL?: string;
158
+ callbacks?: PaymentCallbacks;
159
+ cache?: CacheOptions;
160
+ payment?: PaymentOptions;
152
161
  }
153
162
 
154
163
  export function createSetuFetch(options: CreateSetuFetchOptions) {
155
- const {
156
- wallet,
157
- baseURL,
158
- rpcURL = DEFAULT_RPC_URL,
159
- callbacks = {},
160
- cache,
161
- payment,
162
- } = options;
163
-
164
- const maxAttempts = payment?.maxRequestAttempts ?? DEFAULT_MAX_ATTEMPTS;
165
- const maxPaymentAttempts = payment?.maxPaymentAttempts ?? DEFAULT_MAX_PAYMENT_ATTEMPTS;
166
- const topupApprovalMode = payment?.topupApprovalMode ?? 'auto';
167
- const autoPayThresholdUsd = payment?.autoPayThresholdUsd ?? 0;
168
- const baseFetch = globalThis.fetch.bind(globalThis);
169
-
170
- return async (
171
- input: Parameters<typeof fetch>[0],
172
- init?: Parameters<typeof fetch>[1],
173
- ) => {
174
- let attempt = 0;
175
-
176
- while (attempt < maxAttempts) {
177
- attempt++;
178
- let body = init?.body;
179
- if (body && typeof body === 'string') {
180
- try {
181
- const parsed = JSON.parse(body);
182
- if (cache?.promptCacheKey) parsed.prompt_cache_key = cache.promptCacheKey;
183
- if (cache?.promptCacheRetention) parsed.prompt_cache_retention = cache.promptCacheRetention;
184
- const cacheConfig = cache?.anthropicCaching;
185
- if (cacheConfig !== false) {
186
- const anthropicConfig = typeof cacheConfig === 'object' ? cacheConfig : undefined;
187
- addAnthropicCacheControl(parsed, anthropicConfig);
188
- }
189
- body = JSON.stringify(parsed);
190
- } catch {}
191
- }
192
-
193
- const headers = new Headers(init?.headers);
194
- const walletHeaders = buildWalletHeaders(wallet);
195
- headers.set('x-wallet-address', walletHeaders['x-wallet-address']!);
196
- headers.set('x-wallet-nonce', walletHeaders['x-wallet-nonce']!);
197
- headers.set('x-wallet-signature', walletHeaders['x-wallet-signature']!);
198
-
199
- const response = await baseFetch(input, { ...init, body, headers });
200
-
201
- if (response.status !== 402) {
202
- return wrapResponseWithBalanceSniffing(response, callbacks);
203
- }
204
-
205
- const payload = await response.json().catch(() => ({}));
206
- const requirement = pickPaymentRequirement(payload);
207
- if (!requirement) {
208
- callbacks.onPaymentError?.('Unsupported payment requirement');
209
- throw new Error('Setu: unsupported payment requirement');
210
- }
211
- if (attempt >= maxAttempts) {
212
- callbacks.onPaymentError?.('Payment failed after multiple attempts');
213
- throw new Error('Setu: payment failed after multiple attempts');
214
- }
215
-
216
- const currentAttempts = globalPaymentAttempts.get(wallet.walletAddress) ?? 0;
217
- const remainingPayments = maxPaymentAttempts - currentAttempts;
218
- if (remainingPayments <= 0) {
219
- callbacks.onPaymentError?.('Maximum payment attempts exceeded');
220
- throw new Error('Setu: payment failed after maximum payment attempts.');
221
- }
222
-
223
- const releaseLock = await acquirePaymentLock(wallet.walletAddress);
224
-
225
- try {
226
- const amountUsd = parseInt(requirement.maxAmountRequired, 10) / 1_000_000;
227
- let walletUsdcBalance = 0;
228
- if (autoPayThresholdUsd > 0) {
229
- walletUsdcBalance = await getWalletUsdcBalance(wallet.walletAddress, rpcURL);
230
- }
231
-
232
- const canAutoPay = autoPayThresholdUsd > 0 && walletUsdcBalance >= autoPayThresholdUsd;
233
-
234
- const requestApproval = async () => {
235
- if (!callbacks.onPaymentApproval) return;
236
- const approval = await callbacks.onPaymentApproval({
237
- amountUsd,
238
- currentBalance: walletUsdcBalance,
239
- });
240
- if (approval === 'cancel') {
241
- callbacks.onPaymentError?.('Payment cancelled by user');
242
- throw new Error('Setu: payment cancelled by user');
243
- }
244
- if (approval === 'fiat') {
245
- const err = new Error('Setu: fiat payment selected') as Error & { code: string };
246
- err.code = 'SETU_FIAT_SELECTED';
247
- throw err;
248
- }
249
- };
250
-
251
- if (!canAutoPay && topupApprovalMode === 'approval') {
252
- await requestApproval();
253
- }
254
-
255
- callbacks.onPaymentRequired?.(amountUsd, walletUsdcBalance);
256
-
257
- const doPayment = async () => {
258
- const outcome = await handlePayment({
259
- requirement,
260
- wallet,
261
- rpcURL,
262
- baseURL,
263
- baseFetch,
264
- maxAttempts: remainingPayments,
265
- callbacks,
266
- });
267
- const newTotal = currentAttempts + outcome.attemptsUsed;
268
- globalPaymentAttempts.set(wallet.walletAddress, newTotal);
269
- };
270
-
271
- if (canAutoPay) {
272
- try {
273
- await doPayment();
274
- } catch {
275
- await requestApproval();
276
- await doPayment();
277
- }
278
- } else {
279
- await doPayment();
280
- }
281
- } finally {
282
- releaseLock();
283
- }
284
- }
285
-
286
- throw new Error('Setu: max attempts exceeded');
287
- };
164
+ const {
165
+ wallet,
166
+ baseURL,
167
+ rpcURL = DEFAULT_RPC_URL,
168
+ callbacks = {},
169
+ cache,
170
+ payment,
171
+ } = options;
172
+
173
+ const maxAttempts = payment?.maxRequestAttempts ?? DEFAULT_MAX_ATTEMPTS;
174
+ const maxPaymentAttempts =
175
+ payment?.maxPaymentAttempts ?? DEFAULT_MAX_PAYMENT_ATTEMPTS;
176
+ const topupApprovalMode = payment?.topupApprovalMode ?? 'auto';
177
+ const autoPayThresholdUsd = payment?.autoPayThresholdUsd ?? 0;
178
+ const baseFetch = globalThis.fetch.bind(globalThis);
179
+
180
+ return async (
181
+ input: Parameters<typeof fetch>[0],
182
+ init?: Parameters<typeof fetch>[1],
183
+ ) => {
184
+ let attempt = 0;
185
+
186
+ while (attempt < maxAttempts) {
187
+ attempt++;
188
+ let body = init?.body;
189
+ if (body && typeof body === 'string') {
190
+ try {
191
+ const parsed = JSON.parse(body);
192
+ if (cache?.promptCacheKey)
193
+ parsed.prompt_cache_key = cache.promptCacheKey;
194
+ if (cache?.promptCacheRetention)
195
+ parsed.prompt_cache_retention = cache.promptCacheRetention;
196
+ const cacheConfig = cache?.anthropicCaching;
197
+ if (cacheConfig !== false) {
198
+ const anthropicConfig =
199
+ typeof cacheConfig === 'object' ? cacheConfig : undefined;
200
+ addAnthropicCacheControl(parsed, anthropicConfig);
201
+ }
202
+ body = JSON.stringify(parsed);
203
+ } catch {}
204
+ }
205
+
206
+ const headers = new Headers(init?.headers);
207
+ const walletHeaders = await wallet.buildHeaders();
208
+ headers.set('x-wallet-address', walletHeaders['x-wallet-address']);
209
+ headers.set('x-wallet-nonce', walletHeaders['x-wallet-nonce']);
210
+ headers.set('x-wallet-signature', walletHeaders['x-wallet-signature']);
211
+
212
+ const response = await baseFetch(input, { ...init, body, headers });
213
+
214
+ if (response.status !== 402) {
215
+ return wrapResponseWithBalanceSniffing(response, callbacks);
216
+ }
217
+
218
+ const payload = await response.json().catch(() => ({}));
219
+ const requirement = pickPaymentRequirement(payload);
220
+ if (!requirement) {
221
+ callbacks.onPaymentError?.('Unsupported payment requirement');
222
+ throw new Error('Setu: unsupported payment requirement');
223
+ }
224
+ if (attempt >= maxAttempts) {
225
+ callbacks.onPaymentError?.('Payment failed after multiple attempts');
226
+ throw new Error('Setu: payment failed after multiple attempts');
227
+ }
228
+
229
+ const currentAttempts =
230
+ globalPaymentAttempts.get(wallet.walletAddress) ?? 0;
231
+ const remainingPayments = maxPaymentAttempts - currentAttempts;
232
+ if (remainingPayments <= 0) {
233
+ callbacks.onPaymentError?.('Maximum payment attempts exceeded');
234
+ throw new Error('Setu: payment failed after maximum payment attempts.');
235
+ }
236
+
237
+ const releaseLock = await acquirePaymentLock(wallet.walletAddress);
238
+
239
+ try {
240
+ const amountUsd =
241
+ parseInt(requirement.maxAmountRequired, 10) / 1_000_000;
242
+ let walletUsdcBalance = 0;
243
+ if (autoPayThresholdUsd > 0) {
244
+ walletUsdcBalance = await getWalletUsdcBalance(
245
+ wallet.walletAddress,
246
+ rpcURL,
247
+ );
248
+ }
249
+
250
+ const canAutoPay =
251
+ autoPayThresholdUsd > 0 && walletUsdcBalance >= autoPayThresholdUsd;
252
+
253
+ const requestApproval = async () => {
254
+ if (!callbacks.onPaymentApproval) return;
255
+ const approval = await callbacks.onPaymentApproval({
256
+ amountUsd,
257
+ currentBalance: walletUsdcBalance,
258
+ });
259
+ if (approval === 'cancel') {
260
+ callbacks.onPaymentError?.('Payment cancelled by user');
261
+ throw new Error('Setu: payment cancelled by user');
262
+ }
263
+ if (approval === 'fiat') {
264
+ const err = new Error('Setu: fiat payment selected') as Error & {
265
+ code: string;
266
+ };
267
+ err.code = 'SETU_FIAT_SELECTED';
268
+ throw err;
269
+ }
270
+ };
271
+
272
+ if (!canAutoPay && topupApprovalMode === 'approval') {
273
+ await requestApproval();
274
+ }
275
+
276
+ callbacks.onPaymentRequired?.(amountUsd, walletUsdcBalance);
277
+
278
+ const doPayment = async () => {
279
+ const outcome = await handlePayment({
280
+ requirement,
281
+ wallet,
282
+ rpcURL,
283
+ baseURL,
284
+ baseFetch,
285
+ maxAttempts: remainingPayments,
286
+ callbacks,
287
+ });
288
+ const newTotal = currentAttempts + outcome.attemptsUsed;
289
+ globalPaymentAttempts.set(wallet.walletAddress, newTotal);
290
+ };
291
+
292
+ if (canAutoPay) {
293
+ try {
294
+ await doPayment();
295
+ } catch {
296
+ await requestApproval();
297
+ await doPayment();
298
+ }
299
+ } else {
300
+ await doPayment();
301
+ }
302
+ } finally {
303
+ releaseLock();
304
+ }
305
+ }
306
+
307
+ throw new Error('Setu: max attempts exceeded');
308
+ };
288
309
  }
package/src/index.ts CHANGED
@@ -2,23 +2,25 @@ export { createSetu } from './setu.ts';
2
2
  export type { SetuInstance, SetuProvider } from './setu.ts';
3
3
 
4
4
  export type {
5
- SetuConfig,
6
- SetuAuth,
7
- ProviderId,
8
- ProviderApiFormat,
9
- ProviderConfig,
10
- PaymentCallbacks,
11
- PaymentOptions,
12
- CacheOptions,
13
- BalanceUpdate,
14
- BalanceResponse,
15
- WalletUsdcBalance,
16
- ExactPaymentRequirement,
17
- PaymentPayload,
18
- FetchFunction,
19
- AnthropicCacheStrategy,
20
- AnthropicCachePlacement,
21
- AnthropicCacheConfig,
5
+ SetuConfig,
6
+ SetuAuth,
7
+ ExternalSigner,
8
+ LegacySigner,
9
+ ProviderId,
10
+ ProviderApiFormat,
11
+ ProviderConfig,
12
+ PaymentCallbacks,
13
+ PaymentOptions,
14
+ CacheOptions,
15
+ BalanceUpdate,
16
+ BalanceResponse,
17
+ WalletUsdcBalance,
18
+ ExactPaymentRequirement,
19
+ PaymentPayload,
20
+ FetchFunction,
21
+ AnthropicCacheStrategy,
22
+ AnthropicCachePlacement,
23
+ AnthropicCacheConfig,
22
24
  } from './types.ts';
23
25
 
24
26
  export { ProviderRegistry } from './providers/registry.ts';