@ottocode/ai-sdk 0.1.0 → 0.1.2

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 ADDED
@@ -0,0 +1,346 @@
1
+ # @ottocode/ai-sdk
2
+
3
+ A drop-in SDK for accessing AI models (OpenAI, Anthropic, Google, Moonshot, MiniMax, Z.AI) through the [Setu](https://github.com/slashforge/setu) proxy with automatic x402 payments via Solana USDC.
4
+
5
+ All you need is a Solana wallet — the SDK handles authentication, payment negotiation, and provider routing automatically.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ bun add @ottocode/ai-sdk ai
11
+ # or
12
+ npm install @ottocode/ai-sdk ai
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ```ts
18
+ import { createSetu } from '@ottocode/ai-sdk';
19
+ import { generateText } from 'ai';
20
+
21
+ const setu = createSetu({
22
+ auth: { privateKey: process.env.SOLANA_PRIVATE_KEY! },
23
+ });
24
+
25
+ const { text } = await generateText({
26
+ model: setu.model('claude-sonnet-4-20250514'),
27
+ prompt: 'Hello!',
28
+ });
29
+
30
+ console.log(text);
31
+ ```
32
+
33
+ The SDK auto-resolves which provider to use based on the model name. It returns ai-sdk compatible model instances that work directly with `generateText()`, `streamText()`, etc.
34
+
35
+ ## Provider Auto-Resolution
36
+
37
+ Models are resolved to providers by prefix:
38
+
39
+ | Prefix | Provider | API Format |
40
+ |---|---|---|
41
+ | `claude-` | Anthropic | Messages |
42
+ | `gpt-`, `o1`, `o3`, `o4`, `codex-` | OpenAI | Responses |
43
+ | `gemini-` | Google | Native |
44
+ | `kimi-` | Moonshot | OpenAI Chat |
45
+ | `MiniMax-` | MiniMax | Messages |
46
+ | `z1-` | Z.AI | OpenAI Chat |
47
+
48
+ ```ts
49
+ setu.model('claude-sonnet-4-20250514'); // → anthropic
50
+ setu.model('gpt-4o'); // → openai
51
+ setu.model('gemini-2.5-pro'); // → google
52
+ setu.model('kimi-k2'); // → moonshot
53
+ ```
54
+
55
+ ## Explicit Provider
56
+
57
+ Override auto-resolution when needed:
58
+
59
+ ```ts
60
+ const model = setu.provider('openai').model('gpt-4o');
61
+ const model = setu.provider('anthropic', 'anthropic-messages').model('claude-sonnet-4-20250514');
62
+ ```
63
+
64
+ ## Configuration
65
+
66
+ ```ts
67
+ const setu = createSetu({
68
+ // Required: Solana wallet private key (base58)
69
+ auth: { privateKey: '...' },
70
+
71
+ // Optional: Setu API base URL (default: https://api.setu.ottocode.io)
72
+ baseURL: 'https://api.setu.ottocode.io',
73
+
74
+ // Optional: Solana RPC URL (default: https://api.mainnet-beta.solana.com)
75
+ rpcURL: 'https://api.mainnet-beta.solana.com',
76
+
77
+ // Optional: Payment callbacks
78
+ callbacks: { /* see Payment Callbacks */ },
79
+
80
+ // Optional: Cache configuration
81
+ cache: { /* see Caching */ },
82
+
83
+ // Optional: Payment options
84
+ payment: { /* see Payment Options */ },
85
+
86
+ // Optional: Custom model→provider mappings
87
+ modelMap: {
88
+ 'my-custom-model': 'openai',
89
+ },
90
+
91
+ // Optional: Register custom providers
92
+ providers: [
93
+ {
94
+ id: 'my-provider',
95
+ apiFormat: 'openai-chat',
96
+ modelPrefix: 'myp-',
97
+ },
98
+ ],
99
+ });
100
+ ```
101
+
102
+ ## Payment Callbacks
103
+
104
+ Monitor and control the payment lifecycle:
105
+
106
+ ```ts
107
+ const setu = createSetu({
108
+ auth: { privateKey: '...' },
109
+ callbacks: {
110
+ // Called when a 402 is received and payment is needed
111
+ onPaymentRequired: (amountUsd, currentBalance) => {
112
+ console.log(`Payment required: $${amountUsd}`);
113
+ },
114
+
115
+ // Called when the SDK is signing a transaction
116
+ onPaymentSigning: () => {
117
+ console.log('Signing payment...');
118
+ },
119
+
120
+ // Called after successful payment
121
+ onPaymentComplete: ({ amountUsd, newBalance, transactionId }) => {
122
+ console.log(`Paid $${amountUsd}, balance: $${newBalance}`);
123
+ },
124
+
125
+ // Called on payment failure
126
+ onPaymentError: (error) => {
127
+ console.error('Payment failed:', error);
128
+ },
129
+
130
+ // Called after each request with cost info (streaming & non-streaming)
131
+ onBalanceUpdate: ({ costUsd, balanceRemaining, inputTokens, outputTokens }) => {
132
+ console.log(`Cost: $${costUsd}, remaining: $${balanceRemaining}`);
133
+ },
134
+
135
+ // Optional: interactive approval before payment
136
+ onPaymentApproval: async ({ amountUsd, currentBalance }) => {
137
+ // return 'crypto' to pay, 'fiat' for fiat flow, 'cancel' to abort
138
+ return 'crypto';
139
+ },
140
+ },
141
+ });
142
+ ```
143
+
144
+ ## Payment Options
145
+
146
+ ```ts
147
+ const setu = createSetu({
148
+ auth: { privateKey: '...' },
149
+ payment: {
150
+ // 'auto' (default) — pay automatically
151
+ // 'approval' — call onPaymentApproval before each payment
152
+ topupApprovalMode: 'auto',
153
+
154
+ // Auto-pay without approval if wallet USDC balance >= threshold
155
+ autoPayThresholdUsd: 5.0,
156
+
157
+ // Max retries for a single API request (default: 3)
158
+ maxRequestAttempts: 3,
159
+
160
+ // Max total payment attempts per wallet (default: 20)
161
+ maxPaymentAttempts: 20,
162
+ },
163
+ });
164
+ ```
165
+
166
+ ## Caching
167
+
168
+ ### Anthropic Cache Control
169
+
170
+ By default, the SDK automatically injects `cache_control: { type: 'ephemeral' }` on the first system block and the last message for Anthropic models. This saves ~90% on cached token costs.
171
+
172
+ ```ts
173
+ // Default: auto caching (1 system + 1 message breakpoint)
174
+ createSetu({ auth });
175
+
176
+ // Disable completely
177
+ createSetu({ auth, cache: { anthropicCaching: false } });
178
+
179
+ // Manual: SDK won't inject cache_control — set it yourself in messages
180
+ createSetu({ auth, cache: { anthropicCaching: { strategy: 'manual' } } });
181
+
182
+ // Custom breakpoint count and placement
183
+ createSetu({
184
+ auth,
185
+ cache: {
186
+ anthropicCaching: {
187
+ systemBreakpoints: 2, // cache first 2 system blocks
188
+ systemPlacement: 'first', // 'first' | 'last' | 'all'
189
+ messageBreakpoints: 3, // cache last 3 messages
190
+ messagePlacement: 'last', // 'first' | 'last' | 'all'
191
+ },
192
+ },
193
+ });
194
+
195
+ // Full custom transform
196
+ createSetu({
197
+ auth,
198
+ cache: {
199
+ anthropicCaching: {
200
+ strategy: 'custom',
201
+ transform: (body) => {
202
+ // modify body however you want
203
+ return body;
204
+ },
205
+ },
206
+ },
207
+ });
208
+ ```
209
+
210
+ | Option | Default | Description |
211
+ |---|---|---|
212
+ | `strategy` | `'auto'` | `'auto'`, `'manual'`, `'custom'`, or `false` |
213
+ | `systemBreakpoints` | `1` | Number of system blocks to cache |
214
+ | `messageBreakpoints` | `1` | Number of messages to cache |
215
+ | `systemPlacement` | `'first'` | Which system blocks: `'first'`, `'last'`, `'all'` |
216
+ | `messagePlacement` | `'last'` | Which messages: `'first'`, `'last'`, `'all'` |
217
+ | `cacheType` | `'ephemeral'` | The `cache_control.type` value |
218
+
219
+ ### Setu Server-Side Caching
220
+
221
+ Provider-agnostic caching at the Setu proxy layer:
222
+
223
+ ```ts
224
+ createSetu({
225
+ auth,
226
+ cache: {
227
+ promptCacheKey: 'my-session-123',
228
+ promptCacheRetention: 'in_memory', // or '24h'
229
+ },
230
+ });
231
+ ```
232
+
233
+ ### OpenAI / Google
234
+
235
+ - **OpenAI**: Automatic server-side prefix caching — no configuration needed
236
+ - **Google**: Requires pre-uploaded `cachedContent` at the application level
237
+
238
+ ## Balance
239
+
240
+ ```ts
241
+ // Setu account balance
242
+ const balance = await setu.balance();
243
+ // { walletAddress, balance, totalSpent, totalTopups, requestCount }
244
+
245
+ // On-chain USDC balance
246
+ const wallet = await setu.walletBalance('mainnet');
247
+ // { walletAddress, usdcBalance, network }
248
+
249
+ // Wallet address
250
+ console.log(setu.walletAddress);
251
+ ```
252
+
253
+ ## Custom Providers
254
+
255
+ Register providers at init or runtime:
256
+
257
+ ```ts
258
+ // At init
259
+ const setu = createSetu({
260
+ auth,
261
+ providers: [
262
+ { id: 'my-provider', apiFormat: 'openai-chat', modelPrefix: 'myp-' },
263
+ ],
264
+ });
265
+
266
+ // At runtime
267
+ setu.registry.register({
268
+ id: 'another-provider',
269
+ apiFormat: 'anthropic-messages',
270
+ models: ['specific-model-id'],
271
+ });
272
+
273
+ // Map a specific model to a provider
274
+ setu.registry.mapModel('some-model', 'openai');
275
+ ```
276
+
277
+ ### API Formats
278
+
279
+ | Format | Description | Used by |
280
+ |---|---|---|
281
+ | `openai-responses` | OpenAI Responses API | OpenAI |
282
+ | `anthropic-messages` | Anthropic Messages API | Anthropic, MiniMax |
283
+ | `openai-chat` | OpenAI Chat Completions (compatible) | Moonshot, Z.AI |
284
+ | `google-native` | Google GenerativeAI native | Google |
285
+
286
+ ## Low-Level: Custom Fetch
287
+
288
+ Use the x402-aware fetch wrapper directly:
289
+
290
+ ```ts
291
+ const customFetch = setu.fetch();
292
+
293
+ const response = await customFetch('https://api.setu.ottocode.io/v1/messages', {
294
+ method: 'POST',
295
+ headers: { 'Content-Type': 'application/json' },
296
+ body: JSON.stringify({ model: 'claude-sonnet-4-20250514', messages: [...] }),
297
+ });
298
+ ```
299
+
300
+ ## Standalone Utilities
301
+
302
+ ```ts
303
+ import {
304
+ fetchBalance,
305
+ fetchWalletUsdcBalance,
306
+ getPublicKeyFromPrivate,
307
+ addAnthropicCacheControl,
308
+ createSetuFetch,
309
+ } from '@ottocode/ai-sdk';
310
+
311
+ // Get wallet address from private key
312
+ const address = getPublicKeyFromPrivate(privateKey);
313
+
314
+ // Fetch balance without creating a full Setu instance
315
+ const balance = await fetchBalance({ privateKey });
316
+
317
+ // Fetch on-chain USDC
318
+ const usdc = await fetchWalletUsdcBalance({ privateKey }, 'mainnet');
319
+
320
+ // Create a standalone x402-aware fetch
321
+ const setuFetch = createSetuFetch({
322
+ wallet: createWalletContext({ privateKey }),
323
+ baseURL: 'https://api.setu.ottocode.io',
324
+ });
325
+ ```
326
+
327
+ ## How It Works
328
+
329
+ 1. You call `setu.model('claude-sonnet-4-20250514')` — the SDK resolves this to Anthropic
330
+ 2. It creates an ai-sdk provider (`@ai-sdk/anthropic`) pointed at the Setu proxy
331
+ 3. A custom fetch wrapper intercepts all requests to:
332
+ - Inject wallet auth headers (address, nonce, signature)
333
+ - Inject Anthropic cache control (if enabled)
334
+ - Handle 402 responses by signing USDC payments via x402
335
+ - Sniff balance/cost info from SSE stream comments
336
+ 4. The Setu proxy verifies the wallet, checks balance, forwards to the real provider, tracks usage
337
+
338
+ ## Requirements
339
+
340
+ - Solana wallet with USDC (for payments)
341
+ - `ai` SDK v6+ as a peer dependency
342
+ - Node.js 18+ or Bun
343
+
344
+ ## License
345
+
346
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ottocode/ai-sdk",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts",
package/src/index.ts CHANGED
@@ -30,3 +30,6 @@ export type { CreateSetuFetchOptions } from './fetch.ts';
30
30
  export { fetchBalance, fetchWalletUsdcBalance } from './balance.ts';
31
31
  export { getPublicKeyFromPrivate } from './auth.ts';
32
32
  export { addAnthropicCacheControl } from './cache.ts';
33
+
34
+ export { generateWallet, importWallet, isValidPrivateKey } from './wallet.ts';
35
+ export type { WalletInfo } from './wallet.ts';
@@ -2,6 +2,8 @@ import { createOpenAI } from '@ai-sdk/openai';
2
2
  import { createAnthropic } from '@ai-sdk/anthropic';
3
3
  import { createGoogleGenerativeAI } from '@ai-sdk/google';
4
4
  import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
5
+ import { wrapLanguageModel } from 'ai';
6
+ import type { LanguageModelV3Middleware } from '@ai-sdk/provider';
5
7
  import type { ProviderApiFormat, ProviderId, FetchFunction } from '../types.ts';
6
8
 
7
9
  export function createModel(
@@ -10,9 +12,12 @@ export function createModel(
10
12
  providerId: ProviderId,
11
13
  baseURL: string,
12
14
  customFetch: FetchFunction,
15
+ middleware?: LanguageModelV3Middleware | LanguageModelV3Middleware[],
13
16
  ) {
14
17
  const fetchFn = customFetch as unknown as typeof globalThis.fetch;
15
18
 
19
+ let model;
20
+
16
21
  switch (apiFormat) {
17
22
  case 'anthropic-messages': {
18
23
  const provider = createAnthropic({
@@ -20,7 +25,8 @@ export function createModel(
20
25
  apiKey: 'setu-wallet-auth',
21
26
  fetch: fetchFn,
22
27
  });
23
- return provider(modelId);
28
+ model = provider(modelId);
29
+ break;
24
30
  }
25
31
 
26
32
  case 'google-native': {
@@ -29,7 +35,8 @@ export function createModel(
29
35
  apiKey: 'setu-wallet-auth',
30
36
  fetch: fetchFn,
31
37
  });
32
- return provider(modelId);
38
+ model = provider(modelId);
39
+ break;
33
40
  }
34
41
 
35
42
  case 'openai-chat': {
@@ -39,7 +46,8 @@ export function createModel(
39
46
  headers: { Authorization: 'Bearer setu-wallet-auth' },
40
47
  fetch: fetchFn,
41
48
  });
42
- return provider(modelId);
49
+ model = provider(modelId);
50
+ break;
43
51
  }
44
52
 
45
53
  case 'openai-responses':
@@ -49,7 +57,14 @@ export function createModel(
49
57
  apiKey: 'setu-wallet-auth',
50
58
  fetch: fetchFn,
51
59
  });
52
- return provider.responses(modelId);
60
+ model = provider.responses(modelId);
61
+ break;
53
62
  }
54
63
  }
64
+
65
+ if (middleware) {
66
+ return wrapLanguageModel({ model, middleware });
67
+ }
68
+
69
+ return model;
55
70
  }
@@ -46,6 +46,11 @@ const BUILTIN_PROVIDERS: ProviderConfig[] = [
46
46
  apiFormat: 'anthropic-messages',
47
47
  modelPrefix: 'MiniMax-',
48
48
  },
49
+ {
50
+ id: 'zai',
51
+ apiFormat: 'openai-chat',
52
+ modelPrefix: 'glm-',
53
+ },
49
54
  {
50
55
  id: 'zai',
51
56
  apiFormat: 'openai-chat',
package/src/setu.ts CHANGED
@@ -55,6 +55,7 @@ export function createSetu(config: SetuConfig): SetuInstance {
55
55
  resolved.providerId,
56
56
  modelBaseURL,
57
57
  setuFetch,
58
+ config.middleware,
58
59
  );
59
60
  },
60
61
 
@@ -63,7 +64,7 @@ export function createSetu(config: SetuConfig): SetuInstance {
63
64
  model(modelId: string) {
64
65
  const resolved = registry.resolve(modelId);
65
66
  const format = apiFormat ?? resolved?.apiFormat ?? 'openai-chat';
66
- return createModel(modelId, format, providerId, modelBaseURL, setuFetch);
67
+ return createModel(modelId, format, providerId, modelBaseURL, setuFetch, config.middleware);
67
68
  },
68
69
  };
69
70
  },
package/src/types.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { LanguageModelV3Middleware } from '@ai-sdk/provider';
2
+
1
3
  export type ProviderId =
2
4
  | 'openai'
3
5
  | 'anthropic'
@@ -81,6 +83,7 @@ export interface SetuConfig {
81
83
  callbacks?: PaymentCallbacks;
82
84
  cache?: CacheOptions;
83
85
  payment?: PaymentOptions;
86
+ middleware?: LanguageModelV3Middleware | LanguageModelV3Middleware[];
84
87
  }
85
88
 
86
89
  export interface ExactPaymentRequirement {
package/src/wallet.ts ADDED
@@ -0,0 +1,34 @@
1
+ import { Keypair } from '@solana/web3.js';
2
+ import bs58 from 'bs58';
3
+
4
+ export interface WalletInfo {
5
+ publicKey: string;
6
+ privateKey: string;
7
+ }
8
+
9
+ export function generateWallet(): WalletInfo {
10
+ const keypair = Keypair.generate();
11
+ return {
12
+ privateKey: bs58.encode(keypair.secretKey),
13
+ publicKey: keypair.publicKey.toBase58(),
14
+ };
15
+ }
16
+
17
+ export function importWallet(privateKey: string): WalletInfo {
18
+ const privateKeyBytes = bs58.decode(privateKey);
19
+ const keypair = Keypair.fromSecretKey(privateKeyBytes);
20
+ return {
21
+ privateKey,
22
+ publicKey: keypair.publicKey.toBase58(),
23
+ };
24
+ }
25
+
26
+ export function isValidPrivateKey(privateKey: string): boolean {
27
+ try {
28
+ const bytes = bs58.decode(privateKey);
29
+ Keypair.fromSecretKey(bytes);
30
+ return true;
31
+ } catch {
32
+ return false;
33
+ }
34
+ }