@ottocode/ai-sdk 0.1.4 → 0.1.6

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/payment.ts CHANGED
@@ -1,171 +1,234 @@
1
- import { Buffer } from 'node:buffer';
2
1
  import bs58 from 'bs58';
3
2
  import { createPaymentHeader } from 'x402/client';
4
3
  import { svm } from 'x402/shared';
5
4
  import type { PaymentRequirements } from 'x402/types';
6
5
  import type { WalletContext } from './auth.ts';
7
6
  import type {
8
- ExactPaymentRequirement,
9
- PaymentPayload,
10
- PaymentCallbacks,
7
+ ExactPaymentRequirement,
8
+ PaymentPayload,
9
+ PaymentCallbacks,
11
10
  } from './types.ts';
12
- import { buildWalletHeaders } from './auth.ts';
11
+ import {
12
+ address,
13
+ getTransactionEncoder,
14
+ getTransactionDecoder,
15
+ type Transaction,
16
+ type TransactionWithLifetime,
17
+ type TransactionWithinSizeLimit,
18
+ } from '@solana/kit';
13
19
 
14
20
  function simplifyPaymentError(errMsg: string): string {
15
- const lower = errMsg.toLowerCase();
16
- if (lower.includes('insufficient') || lower.includes('not enough') || lower.includes('balance'))
17
- return 'Insufficient USDC balance';
18
- if (lower.includes('simulation') || lower.includes('compute unit'))
19
- return 'Transaction simulation failed';
20
- if (lower.includes('blockhash') || lower.includes('expired'))
21
- return 'Transaction expired, please retry';
22
- if (lower.includes('timeout') || lower.includes('timed out'))
23
- return 'Transaction timed out';
24
- if (lower.includes('rejected') || lower.includes('cancelled'))
25
- return 'Transaction rejected';
26
- if (lower.includes('network') || lower.includes('connection'))
27
- return 'Network error';
28
- const short = (errMsg.split('.')[0] ?? errMsg).slice(0, 80);
29
- return short.length < errMsg.length ? `${short}...` : errMsg;
21
+ const lower = errMsg.toLowerCase();
22
+ if (
23
+ lower.includes('insufficient') ||
24
+ lower.includes('not enough') ||
25
+ lower.includes('balance')
26
+ )
27
+ return 'Insufficient USDC balance';
28
+ if (lower.includes('simulation') || lower.includes('compute unit'))
29
+ return 'Transaction simulation failed';
30
+ if (lower.includes('blockhash') || lower.includes('expired'))
31
+ return 'Transaction expired, please retry';
32
+ if (lower.includes('timeout') || lower.includes('timed out'))
33
+ return 'Transaction timed out';
34
+ if (lower.includes('rejected') || lower.includes('cancelled'))
35
+ return 'Transaction rejected';
36
+ if (lower.includes('network') || lower.includes('connection'))
37
+ return 'Network error';
38
+ const short = (errMsg.split('.')[0] ?? errMsg).slice(0, 80);
39
+ return short.length < errMsg.length ? `${short}...` : errMsg;
30
40
  }
31
41
 
32
42
  export function pickPaymentRequirement(
33
- payload: unknown,
43
+ payload: unknown,
34
44
  ): ExactPaymentRequirement | null {
35
- const acceptsValue =
36
- typeof payload === 'object' && payload !== null
37
- ? (payload as { accepts?: unknown }).accepts
38
- : undefined;
39
- const accepts = Array.isArray(acceptsValue)
40
- ? (acceptsValue as ExactPaymentRequirement[])
41
- : [];
42
- return accepts.find((opt) => opt && opt.scheme === 'exact') ?? null;
45
+ const acceptsValue =
46
+ typeof payload === 'object' && payload !== null
47
+ ? (payload as { accepts?: unknown }).accepts
48
+ : undefined;
49
+ const accepts = Array.isArray(acceptsValue)
50
+ ? (acceptsValue as ExactPaymentRequirement[])
51
+ : [];
52
+ return accepts.find((opt) => opt && opt.scheme === 'exact') ?? null;
53
+ }
54
+
55
+ function wrapCallbackAsSigner(
56
+ walletAddress: string,
57
+ callback: (transaction: Uint8Array) => Promise<Uint8Array>,
58
+ ) {
59
+ const encoder = getTransactionEncoder();
60
+ const decoder = getTransactionDecoder();
61
+ return {
62
+ address: address(walletAddress),
63
+ modifyAndSignTransactions: async (
64
+ transactions: readonly (
65
+ | Transaction
66
+ | (Transaction & TransactionWithLifetime)
67
+ )[],
68
+ ): Promise<
69
+ readonly (Transaction &
70
+ TransactionWithinSizeLimit &
71
+ TransactionWithLifetime)[]
72
+ > => {
73
+ const results = [];
74
+ for (const tx of transactions) {
75
+ const bytes = new Uint8Array(encoder.encode(tx));
76
+ const signedBytes = await callback(bytes);
77
+ const signed = decoder.decode(signedBytes);
78
+ results.push(
79
+ signed as Transaction &
80
+ TransactionWithinSizeLimit &
81
+ TransactionWithLifetime,
82
+ );
83
+ }
84
+ return results;
85
+ },
86
+ };
87
+ }
88
+
89
+ async function resolvePaymentSigner(wallet: WalletContext) {
90
+ if (wallet.signTransaction) {
91
+ return wrapCallbackAsSigner(wallet.walletAddress, wallet.signTransaction);
92
+ }
93
+ if (wallet.keypair) {
94
+ const privateKeyBase58 = bs58.encode(wallet.keypair.secretKey);
95
+ return svm.createSignerFromBase58(privateKeyBase58);
96
+ }
97
+ throw new Error(
98
+ 'Setu: payments require either a privateKey or signer.signTransaction.',
99
+ );
43
100
  }
44
101
 
45
102
  export async function createPaymentPayload(
46
- requirement: ExactPaymentRequirement,
47
- wallet: WalletContext,
48
- rpcURL: string,
103
+ requirement: ExactPaymentRequirement,
104
+ wallet: WalletContext,
105
+ rpcURL: string,
49
106
  ): Promise<PaymentPayload> {
50
- const privateKeyBase58 = bs58.encode(wallet.keypair.secretKey);
51
- const signer = await svm.createSignerFromBase58(privateKeyBase58);
52
- const header = await createPaymentHeader(
53
- signer,
54
- 1,
55
- requirement as PaymentRequirements,
56
- { svmConfig: { rpcUrl: rpcURL } },
57
- );
58
- const decoded = JSON.parse(
59
- Buffer.from(header, 'base64').toString('utf-8'),
60
- ) as { payload: { transaction: string } };
107
+ const signer = await resolvePaymentSigner(wallet);
108
+ const header = await createPaymentHeader(
109
+ signer,
110
+ 1,
111
+ requirement as PaymentRequirements,
112
+ { svmConfig: { rpcUrl: rpcURL } },
113
+ );
114
+ const decoded = JSON.parse(atob(header)) as {
115
+ payload: { transaction: string };
116
+ };
61
117
 
62
- return {
63
- x402Version: 1,
64
- scheme: 'exact',
65
- network: requirement.network,
66
- payload: { transaction: decoded.payload.transaction },
67
- };
118
+ return {
119
+ x402Version: 1,
120
+ scheme: 'exact',
121
+ network: requirement.network,
122
+ payload: { transaction: decoded.payload.transaction },
123
+ };
68
124
  }
69
125
 
70
126
  interface PaymentResponse {
71
- amount_usd?: number | string;
72
- new_balance?: number | string;
73
- amount?: number;
74
- balance?: number;
75
- transaction?: string;
127
+ amount_usd?: number | string;
128
+ new_balance?: number | string;
129
+ amount?: number;
130
+ balance?: number;
131
+ transaction?: string;
76
132
  }
77
133
 
78
134
  export async function processSinglePayment(args: {
79
- requirement: ExactPaymentRequirement;
80
- wallet: WalletContext;
81
- rpcURL: string;
82
- baseURL: string;
83
- baseFetch: typeof fetch;
84
- callbacks: PaymentCallbacks;
135
+ requirement: ExactPaymentRequirement;
136
+ wallet: WalletContext;
137
+ rpcURL: string;
138
+ baseURL: string;
139
+ baseFetch: typeof fetch;
140
+ callbacks: PaymentCallbacks;
85
141
  }): Promise<{ attempts: number; balance?: number | string }> {
86
- args.callbacks.onPaymentSigning?.();
142
+ args.callbacks.onPaymentSigning?.();
87
143
 
88
- let paymentPayload: PaymentPayload;
89
- try {
90
- paymentPayload = await createPaymentPayload(
91
- args.requirement,
92
- args.wallet,
93
- args.rpcURL,
94
- );
95
- } catch (err) {
96
- const errMsg = err instanceof Error ? err.message : String(err);
97
- const userMsg = `Payment failed: ${simplifyPaymentError(errMsg)}`;
98
- args.callbacks.onPaymentError?.(userMsg);
99
- throw new Error(`Setu: ${userMsg}`);
100
- }
144
+ let paymentPayload: PaymentPayload;
145
+ try {
146
+ paymentPayload = await createPaymentPayload(
147
+ args.requirement,
148
+ args.wallet,
149
+ args.rpcURL,
150
+ );
151
+ } catch (err) {
152
+ const errMsg = err instanceof Error ? err.message : String(err);
153
+ const userMsg = `Payment failed: ${simplifyPaymentError(errMsg)}`;
154
+ args.callbacks.onPaymentError?.(userMsg);
155
+ throw new Error(`Setu: ${userMsg}`);
156
+ }
101
157
 
102
- const walletHeaders = buildWalletHeaders(args.wallet);
103
- const response = await args.baseFetch(`${args.baseURL}/v1/topup`, {
104
- method: 'POST',
105
- headers: { 'Content-Type': 'application/json', ...walletHeaders },
106
- body: JSON.stringify({
107
- paymentPayload,
108
- paymentRequirement: args.requirement,
109
- }),
110
- });
158
+ const walletHeaders = await args.wallet.buildHeaders();
159
+ const response = await args.baseFetch(`${args.baseURL}/v1/topup`, {
160
+ method: 'POST',
161
+ headers: { 'Content-Type': 'application/json', ...walletHeaders },
162
+ body: JSON.stringify({
163
+ paymentPayload,
164
+ paymentRequirement: args.requirement,
165
+ }),
166
+ });
111
167
 
112
- const rawBody = await response.text().catch(() => '');
113
- if (!response.ok) {
114
- if (response.status === 400 && rawBody.toLowerCase().includes('already processed')) {
115
- return { attempts: 1 };
116
- }
117
- args.callbacks.onPaymentError?.(`Topup failed: ${response.status}`);
118
- throw new Error(`Setu topup failed (${response.status}): ${rawBody}`);
119
- }
168
+ const rawBody = await response.text().catch(() => '');
169
+ if (!response.ok) {
170
+ if (
171
+ response.status === 400 &&
172
+ rawBody.toLowerCase().includes('already processed')
173
+ ) {
174
+ return { attempts: 1 };
175
+ }
176
+ args.callbacks.onPaymentError?.(`Topup failed: ${response.status}`);
177
+ throw new Error(`Setu topup failed (${response.status}): ${rawBody}`);
178
+ }
120
179
 
121
- let parsed: PaymentResponse | undefined;
122
- try {
123
- parsed = rawBody ? (JSON.parse(rawBody) as PaymentResponse) : undefined;
124
- } catch {
125
- parsed = undefined;
126
- }
180
+ let parsed: PaymentResponse | undefined;
181
+ try {
182
+ parsed = rawBody ? (JSON.parse(rawBody) as PaymentResponse) : undefined;
183
+ } catch {
184
+ parsed = undefined;
185
+ }
127
186
 
128
- if (parsed) {
129
- const amountUsd =
130
- typeof parsed.amount_usd === 'string'
131
- ? parseFloat(parsed.amount_usd)
132
- : (parsed.amount_usd ?? parsed.amount ?? 0);
133
- const newBalance =
134
- typeof parsed.new_balance === 'string'
135
- ? parseFloat(parsed.new_balance)
136
- : (parsed.new_balance ?? parsed.balance ?? 0);
137
- args.callbacks.onPaymentComplete?.({
138
- amountUsd,
139
- newBalance,
140
- transactionId: parsed.transaction,
141
- });
142
- return { attempts: 1, balance: newBalance };
143
- }
144
- return { attempts: 1 };
187
+ if (parsed) {
188
+ const amountUsd =
189
+ typeof parsed.amount_usd === 'string'
190
+ ? parseFloat(parsed.amount_usd)
191
+ : (parsed.amount_usd ?? parsed.amount ?? 0);
192
+ const newBalance =
193
+ typeof parsed.new_balance === 'string'
194
+ ? parseFloat(parsed.new_balance)
195
+ : (parsed.new_balance ?? parsed.balance ?? 0);
196
+ args.callbacks.onPaymentComplete?.({
197
+ amountUsd,
198
+ newBalance,
199
+ transactionId: parsed.transaction,
200
+ });
201
+ return { attempts: 1, balance: newBalance };
202
+ }
203
+ return { attempts: 1 };
145
204
  }
146
205
 
147
206
  export async function handlePayment(args: {
148
- requirement: ExactPaymentRequirement;
149
- wallet: WalletContext;
150
- rpcURL: string;
151
- baseURL: string;
152
- baseFetch: typeof fetch;
153
- maxAttempts: number;
154
- callbacks: PaymentCallbacks;
207
+ requirement: ExactPaymentRequirement;
208
+ wallet: WalletContext;
209
+ rpcURL: string;
210
+ baseURL: string;
211
+ baseFetch: typeof fetch;
212
+ maxAttempts: number;
213
+ callbacks: PaymentCallbacks;
155
214
  }): Promise<{ attemptsUsed: number }> {
156
- let attempts = 0;
157
- while (attempts < args.maxAttempts) {
158
- const result = await processSinglePayment(args);
159
- attempts += result.attempts;
160
- const balanceValue =
161
- typeof result.balance === 'number'
162
- ? result.balance
163
- : result.balance != null
164
- ? Number(result.balance)
165
- : undefined;
166
- if (balanceValue == null || Number.isNaN(balanceValue) || balanceValue >= 0) {
167
- return { attemptsUsed: attempts };
168
- }
169
- }
170
- throw new Error(`Setu: payment failed after ${attempts} additional top-ups.`);
215
+ let attempts = 0;
216
+ while (attempts < args.maxAttempts) {
217
+ const result = await processSinglePayment(args);
218
+ attempts += result.attempts;
219
+ const balanceValue =
220
+ typeof result.balance === 'number'
221
+ ? result.balance
222
+ : result.balance != null
223
+ ? Number(result.balance)
224
+ : undefined;
225
+ if (
226
+ balanceValue == null ||
227
+ Number.isNaN(balanceValue) ||
228
+ balanceValue >= 0
229
+ ) {
230
+ return { attemptsUsed: attempts };
231
+ }
232
+ }
233
+ throw new Error(`Setu: payment failed after ${attempts} additional top-ups.`);
171
234
  }
@@ -7,64 +7,64 @@ import type { LanguageModelV3Middleware } from '@ai-sdk/provider';
7
7
  import type { ProviderApiFormat, ProviderId, FetchFunction } from '../types.ts';
8
8
 
9
9
  export function createModel(
10
- modelId: string,
11
- apiFormat: ProviderApiFormat,
12
- providerId: ProviderId,
13
- baseURL: string,
14
- customFetch: FetchFunction,
15
- middleware?: LanguageModelV3Middleware | LanguageModelV3Middleware[],
10
+ modelId: string,
11
+ apiFormat: ProviderApiFormat,
12
+ providerId: ProviderId,
13
+ baseURL: string,
14
+ customFetch: FetchFunction,
15
+ middleware?: LanguageModelV3Middleware | LanguageModelV3Middleware[],
16
16
  ) {
17
- const fetchFn = customFetch as unknown as typeof globalThis.fetch;
17
+ const fetchFn = customFetch as unknown as typeof globalThis.fetch;
18
18
 
19
- let model;
19
+ let model:
20
+ | ReturnType<ReturnType<typeof createOpenAI>['responses']>
21
+ | ReturnType<ReturnType<typeof createAnthropic>>;
20
22
 
21
- switch (apiFormat) {
22
- case 'anthropic-messages': {
23
- const provider = createAnthropic({
24
- baseURL,
25
- apiKey: 'setu-wallet-auth',
26
- fetch: fetchFn,
27
- });
28
- model = provider(modelId);
29
- break;
30
- }
23
+ switch (apiFormat) {
24
+ case 'anthropic-messages': {
25
+ const provider = createAnthropic({
26
+ baseURL,
27
+ apiKey: 'setu-wallet-auth',
28
+ fetch: fetchFn,
29
+ });
30
+ model = provider(modelId);
31
+ break;
32
+ }
31
33
 
32
- case 'google-native': {
33
- const provider = createGoogleGenerativeAI({
34
- baseURL,
35
- apiKey: 'setu-wallet-auth',
36
- fetch: fetchFn,
37
- });
38
- model = provider(modelId);
39
- break;
40
- }
34
+ case 'google-native': {
35
+ const provider = createGoogleGenerativeAI({
36
+ baseURL,
37
+ apiKey: 'setu-wallet-auth',
38
+ fetch: fetchFn,
39
+ });
40
+ model = provider(modelId);
41
+ break;
42
+ }
41
43
 
42
- case 'openai-chat': {
43
- const provider = createOpenAICompatible({
44
- name: `setu-${providerId}`,
45
- baseURL,
46
- headers: { Authorization: 'Bearer setu-wallet-auth' },
47
- fetch: fetchFn,
48
- });
49
- model = provider(modelId);
50
- break;
51
- }
44
+ case 'openai-chat': {
45
+ const provider = createOpenAICompatible({
46
+ name: `setu-${providerId}`,
47
+ baseURL,
48
+ headers: { Authorization: 'Bearer setu-wallet-auth' },
49
+ fetch: fetchFn,
50
+ });
51
+ model = provider(modelId);
52
+ break;
53
+ }
54
+ default: {
55
+ const provider = createOpenAI({
56
+ baseURL,
57
+ apiKey: 'setu-wallet-auth',
58
+ fetch: fetchFn,
59
+ });
60
+ model = provider.responses(modelId);
61
+ break;
62
+ }
63
+ }
52
64
 
53
- case 'openai-responses':
54
- default: {
55
- const provider = createOpenAI({
56
- baseURL,
57
- apiKey: 'setu-wallet-auth',
58
- fetch: fetchFn,
59
- });
60
- model = provider.responses(modelId);
61
- break;
62
- }
63
- }
65
+ if (middleware) {
66
+ return wrapLanguageModel({ model, middleware });
67
+ }
64
68
 
65
- if (middleware) {
66
- return wrapLanguageModel({ model, middleware });
67
- }
68
-
69
- return model;
69
+ return model;
70
70
  }
@@ -1,105 +1,70 @@
1
- import type { ProviderId, ProviderConfig, ProviderApiFormat } from '../types.ts';
1
+ import type {
2
+ ProviderId,
3
+ ProviderConfig,
4
+ ProviderApiFormat,
5
+ } from '../types.ts';
6
+ import { setuCatalog } from '../catalog.ts';
2
7
 
3
- const BUILTIN_PROVIDERS: ProviderConfig[] = [
4
- {
5
- id: 'openai',
6
- apiFormat: 'openai-responses',
7
- modelPrefix: 'gpt-',
8
- },
9
- {
10
- id: 'openai',
11
- apiFormat: 'openai-responses',
12
- modelPrefix: 'o1',
13
- },
14
- {
15
- id: 'openai',
16
- apiFormat: 'openai-responses',
17
- modelPrefix: 'o3',
18
- },
19
- {
20
- id: 'openai',
21
- apiFormat: 'openai-responses',
22
- modelPrefix: 'o4',
23
- },
24
- {
25
- id: 'openai',
26
- apiFormat: 'openai-responses',
27
- modelPrefix: 'codex-',
28
- },
29
- {
30
- id: 'anthropic',
31
- apiFormat: 'anthropic-messages',
32
- modelPrefix: 'claude-',
33
- },
34
- {
35
- id: 'google',
36
- apiFormat: 'google-native',
37
- modelPrefix: 'gemini-',
38
- },
39
- {
40
- id: 'moonshot',
41
- apiFormat: 'openai-chat',
42
- modelPrefix: 'kimi-',
43
- },
44
- {
45
- id: 'minimax',
46
- apiFormat: 'anthropic-messages',
47
- modelPrefix: 'MiniMax-',
48
- },
49
- {
50
- id: 'zai',
51
- apiFormat: 'openai-chat',
52
- modelPrefix: 'glm-',
53
- },
54
- {
55
- id: 'zai',
56
- apiFormat: 'openai-chat',
57
- modelPrefix: 'z1-',
58
- },
59
- ];
8
+ const OWNER_API_FORMAT: Record<string, ProviderApiFormat> = {
9
+ openai: 'openai-responses',
10
+ anthropic: 'anthropic-messages',
11
+ google: 'google-native',
12
+ minimax: 'anthropic-messages',
13
+ moonshot: 'openai-chat',
14
+ zai: 'openai-chat',
15
+ };
60
16
 
61
17
  export class ProviderRegistry {
62
- private configs: ProviderConfig[];
63
- private modelMap: Record<string, ProviderId>;
18
+ private configs: ProviderConfig[];
19
+ private modelMap: Record<string, ProviderId>;
64
20
 
65
- constructor(
66
- customProviders?: ProviderConfig[],
67
- modelMap?: Record<string, ProviderId>,
68
- ) {
69
- this.configs = [...BUILTIN_PROVIDERS, ...(customProviders ?? [])];
70
- this.modelMap = modelMap ?? {};
71
- }
21
+ constructor(
22
+ customProviders?: ProviderConfig[],
23
+ modelMap?: Record<string, ProviderId>,
24
+ ) {
25
+ this.configs = [...(customProviders ?? [])];
26
+ this.modelMap = modelMap ?? {};
27
+ }
72
28
 
73
- resolve(modelId: string): { providerId: ProviderId; apiFormat: ProviderApiFormat } | null {
74
- if (this.modelMap[modelId]) {
75
- const providerId = this.modelMap[modelId];
76
- const config = this.configs.find((c) => c.id === providerId);
77
- return {
78
- providerId,
79
- apiFormat: config?.apiFormat ?? 'openai-chat',
80
- };
81
- }
29
+ resolve(
30
+ modelId: string,
31
+ ): { providerId: ProviderId; apiFormat: ProviderApiFormat } | null {
32
+ if (this.modelMap[modelId]) {
33
+ const providerId = this.modelMap[modelId];
34
+ const config = this.configs.find((c) => c.id === providerId);
35
+ return {
36
+ providerId,
37
+ apiFormat: config?.apiFormat ?? 'openai-chat',
38
+ };
39
+ }
82
40
 
83
- for (const config of this.configs) {
84
- if (config.models?.includes(modelId)) {
85
- return { providerId: config.id, apiFormat: config.apiFormat };
86
- }
87
- }
41
+ for (const config of this.configs) {
42
+ if (config.models?.includes(modelId)) {
43
+ return { providerId: config.id, apiFormat: config.apiFormat };
44
+ }
45
+ }
88
46
 
89
- for (const config of this.configs) {
90
- if (config.modelPrefix && modelId.startsWith(config.modelPrefix)) {
91
- return { providerId: config.id, apiFormat: config.apiFormat };
92
- }
93
- }
47
+ for (const config of this.configs) {
48
+ if (config.modelPrefix && modelId.startsWith(config.modelPrefix)) {
49
+ return { providerId: config.id, apiFormat: config.apiFormat };
50
+ }
51
+ }
94
52
 
95
- return null;
96
- }
53
+ const entry = setuCatalog.models.find((m) => m.id === modelId);
54
+ if (entry) {
55
+ const providerId = entry.owned_by as ProviderId;
56
+ const apiFormat = OWNER_API_FORMAT[providerId] ?? 'openai-chat';
57
+ return { providerId, apiFormat };
58
+ }
97
59
 
98
- register(config: ProviderConfig): void {
99
- this.configs.push(config);
100
- }
60
+ return null;
61
+ }
101
62
 
102
- mapModel(modelId: string, providerId: ProviderId): void {
103
- this.modelMap[modelId] = providerId;
104
- }
63
+ register(config: ProviderConfig): void {
64
+ this.configs.push(config);
65
+ }
66
+
67
+ mapModel(modelId: string, providerId: ProviderId): void {
68
+ this.modelMap[modelId] = providerId;
69
+ }
105
70
  }