@ottocode/ai-sdk 0.1.3 → 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/payment.ts CHANGED
@@ -1,171 +1,211 @@
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,
10
+ LegacySigner,
11
11
  } from './types.ts';
12
- import { buildWalletHeaders } from './auth.ts';
13
12
 
14
13
  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;
14
+ const lower = errMsg.toLowerCase();
15
+ if (
16
+ lower.includes('insufficient') ||
17
+ lower.includes('not enough') ||
18
+ lower.includes('balance')
19
+ )
20
+ return 'Insufficient USDC balance';
21
+ if (lower.includes('simulation') || lower.includes('compute unit'))
22
+ return 'Transaction simulation failed';
23
+ if (lower.includes('blockhash') || lower.includes('expired'))
24
+ return 'Transaction expired, please retry';
25
+ if (lower.includes('timeout') || lower.includes('timed out'))
26
+ return 'Transaction timed out';
27
+ if (lower.includes('rejected') || lower.includes('cancelled'))
28
+ return 'Transaction rejected';
29
+ if (lower.includes('network') || lower.includes('connection'))
30
+ return 'Network error';
31
+ const short = (errMsg.split('.')[0] ?? errMsg).slice(0, 80);
32
+ return short.length < errMsg.length ? `${short}...` : errMsg;
30
33
  }
31
34
 
32
35
  export function pickPaymentRequirement(
33
- payload: unknown,
36
+ payload: unknown,
34
37
  ): 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;
38
+ const acceptsValue =
39
+ typeof payload === 'object' && payload !== null
40
+ ? (payload as { accepts?: unknown }).accepts
41
+ : undefined;
42
+ const accepts = Array.isArray(acceptsValue)
43
+ ? (acceptsValue as ExactPaymentRequirement[])
44
+ : [];
45
+ return accepts.find((opt) => opt && opt.scheme === 'exact') ?? null;
46
+ }
47
+
48
+ function isLegacySigner(signer: unknown): signer is LegacySigner {
49
+ return (
50
+ typeof signer === 'object' &&
51
+ signer !== null &&
52
+ 'secretKey' in signer &&
53
+ signer.secretKey instanceof Uint8Array &&
54
+ 'publicKey' in signer &&
55
+ typeof (signer as LegacySigner).publicKey?.toBase58 === 'function'
56
+ );
57
+ }
58
+
59
+ async function resolvePaymentSigner(wallet: WalletContext) {
60
+ if (wallet.transactionSigner) {
61
+ if (isLegacySigner(wallet.transactionSigner)) {
62
+ const privateKeyBase58 = bs58.encode(wallet.transactionSigner.secretKey);
63
+ return svm.createSignerFromBase58(privateKeyBase58);
64
+ }
65
+ return wallet.transactionSigner as Exclude<
66
+ typeof wallet.transactionSigner,
67
+ LegacySigner
68
+ >;
69
+ }
70
+ if (wallet.keypair) {
71
+ const privateKeyBase58 = bs58.encode(wallet.keypair.secretKey);
72
+ return svm.createSignerFromBase58(privateKeyBase58);
73
+ }
74
+ throw new Error(
75
+ 'Setu: payments require either a privateKey or signer.signTransaction.',
76
+ );
43
77
  }
44
78
 
45
79
  export async function createPaymentPayload(
46
- requirement: ExactPaymentRequirement,
47
- wallet: WalletContext,
48
- rpcURL: string,
80
+ requirement: ExactPaymentRequirement,
81
+ wallet: WalletContext,
82
+ rpcURL: string,
49
83
  ): 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 } };
84
+ const signer = await resolvePaymentSigner(wallet);
85
+ const header = await createPaymentHeader(
86
+ signer,
87
+ 1,
88
+ requirement as PaymentRequirements,
89
+ { svmConfig: { rpcUrl: rpcURL } },
90
+ );
91
+ const decoded = JSON.parse(atob(header)) as {
92
+ payload: { transaction: string };
93
+ };
61
94
 
62
- return {
63
- x402Version: 1,
64
- scheme: 'exact',
65
- network: requirement.network,
66
- payload: { transaction: decoded.payload.transaction },
67
- };
95
+ return {
96
+ x402Version: 1,
97
+ scheme: 'exact',
98
+ network: requirement.network,
99
+ payload: { transaction: decoded.payload.transaction },
100
+ };
68
101
  }
69
102
 
70
103
  interface PaymentResponse {
71
- amount_usd?: number | string;
72
- new_balance?: number | string;
73
- amount?: number;
74
- balance?: number;
75
- transaction?: string;
104
+ amount_usd?: number | string;
105
+ new_balance?: number | string;
106
+ amount?: number;
107
+ balance?: number;
108
+ transaction?: string;
76
109
  }
77
110
 
78
111
  export async function processSinglePayment(args: {
79
- requirement: ExactPaymentRequirement;
80
- wallet: WalletContext;
81
- rpcURL: string;
82
- baseURL: string;
83
- baseFetch: typeof fetch;
84
- callbacks: PaymentCallbacks;
112
+ requirement: ExactPaymentRequirement;
113
+ wallet: WalletContext;
114
+ rpcURL: string;
115
+ baseURL: string;
116
+ baseFetch: typeof fetch;
117
+ callbacks: PaymentCallbacks;
85
118
  }): Promise<{ attempts: number; balance?: number | string }> {
86
- args.callbacks.onPaymentSigning?.();
119
+ args.callbacks.onPaymentSigning?.();
87
120
 
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
- }
121
+ let paymentPayload: PaymentPayload;
122
+ try {
123
+ paymentPayload = await createPaymentPayload(
124
+ args.requirement,
125
+ args.wallet,
126
+ args.rpcURL,
127
+ );
128
+ } catch (err) {
129
+ const errMsg = err instanceof Error ? err.message : String(err);
130
+ const userMsg = `Payment failed: ${simplifyPaymentError(errMsg)}`;
131
+ args.callbacks.onPaymentError?.(userMsg);
132
+ throw new Error(`Setu: ${userMsg}`);
133
+ }
101
134
 
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
- });
135
+ const walletHeaders = await args.wallet.buildHeaders();
136
+ const response = await args.baseFetch(`${args.baseURL}/v1/topup`, {
137
+ method: 'POST',
138
+ headers: { 'Content-Type': 'application/json', ...walletHeaders },
139
+ body: JSON.stringify({
140
+ paymentPayload,
141
+ paymentRequirement: args.requirement,
142
+ }),
143
+ });
111
144
 
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
- }
145
+ const rawBody = await response.text().catch(() => '');
146
+ if (!response.ok) {
147
+ if (
148
+ response.status === 400 &&
149
+ rawBody.toLowerCase().includes('already processed')
150
+ ) {
151
+ return { attempts: 1 };
152
+ }
153
+ args.callbacks.onPaymentError?.(`Topup failed: ${response.status}`);
154
+ throw new Error(`Setu topup failed (${response.status}): ${rawBody}`);
155
+ }
120
156
 
121
- let parsed: PaymentResponse | undefined;
122
- try {
123
- parsed = rawBody ? (JSON.parse(rawBody) as PaymentResponse) : undefined;
124
- } catch {
125
- parsed = undefined;
126
- }
157
+ let parsed: PaymentResponse | undefined;
158
+ try {
159
+ parsed = rawBody ? (JSON.parse(rawBody) as PaymentResponse) : undefined;
160
+ } catch {
161
+ parsed = undefined;
162
+ }
127
163
 
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 };
164
+ if (parsed) {
165
+ const amountUsd =
166
+ typeof parsed.amount_usd === 'string'
167
+ ? parseFloat(parsed.amount_usd)
168
+ : (parsed.amount_usd ?? parsed.amount ?? 0);
169
+ const newBalance =
170
+ typeof parsed.new_balance === 'string'
171
+ ? parseFloat(parsed.new_balance)
172
+ : (parsed.new_balance ?? parsed.balance ?? 0);
173
+ args.callbacks.onPaymentComplete?.({
174
+ amountUsd,
175
+ newBalance,
176
+ transactionId: parsed.transaction,
177
+ });
178
+ return { attempts: 1, balance: newBalance };
179
+ }
180
+ return { attempts: 1 };
145
181
  }
146
182
 
147
183
  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;
184
+ requirement: ExactPaymentRequirement;
185
+ wallet: WalletContext;
186
+ rpcURL: string;
187
+ baseURL: string;
188
+ baseFetch: typeof fetch;
189
+ maxAttempts: number;
190
+ callbacks: PaymentCallbacks;
155
191
  }): 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.`);
192
+ let attempts = 0;
193
+ while (attempts < args.maxAttempts) {
194
+ const result = await processSinglePayment(args);
195
+ attempts += result.attempts;
196
+ const balanceValue =
197
+ typeof result.balance === 'number'
198
+ ? result.balance
199
+ : result.balance != null
200
+ ? Number(result.balance)
201
+ : undefined;
202
+ if (
203
+ balanceValue == null ||
204
+ Number.isNaN(balanceValue) ||
205
+ balanceValue >= 0
206
+ ) {
207
+ return { attemptsUsed: attempts };
208
+ }
209
+ }
210
+ throw new Error(`Setu: payment failed after ${attempts} additional top-ups.`);
171
211
  }
@@ -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
  }