@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ottocode/ai-sdk",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts",
@@ -24,6 +24,6 @@
24
24
  "ai": ">=6.0.0"
25
25
  },
26
26
  "devDependencies": {
27
- "typescript": "^5.0.0"
27
+ "typescript": "~5.9.3"
28
28
  }
29
29
  }
package/src/auth.ts CHANGED
@@ -4,41 +4,76 @@ import nacl from 'tweetnacl';
4
4
  import type { SetuAuth } from './types.ts';
5
5
 
6
6
  export interface WalletContext {
7
- keypair: Keypair;
8
- walletAddress: string;
9
- privateKeyBytes: Uint8Array;
7
+ walletAddress: string;
8
+ buildHeaders: () => Promise<Record<string, string>> | Record<string, string>;
9
+ keypair?: Keypair;
10
+ privateKeyBytes?: Uint8Array;
11
+ signTransaction?: (transaction: Uint8Array) => Promise<Uint8Array>;
10
12
  }
11
13
 
12
- export function createWalletContext(auth: Required<SetuAuth>): WalletContext {
13
- const privateKeyBytes = bs58.decode(auth.privateKey!);
14
- const keypair = Keypair.fromSecretKey(privateKeyBytes);
15
- const walletAddress = keypair.publicKey.toBase58();
16
- return { keypair, walletAddress, privateKeyBytes };
14
+ export function createWalletContext(auth: SetuAuth): WalletContext {
15
+ if (auth.signer) {
16
+ const {
17
+ walletAddress,
18
+ signNonce: customSignNonce,
19
+ signTransaction,
20
+ } = auth.signer;
21
+ return {
22
+ walletAddress,
23
+ signTransaction,
24
+ buildHeaders: async () => {
25
+ const nonce = Date.now().toString();
26
+ const signature = await customSignNonce(nonce);
27
+ return {
28
+ 'x-wallet-address': walletAddress,
29
+ 'x-wallet-nonce': nonce,
30
+ 'x-wallet-signature': signature,
31
+ };
32
+ },
33
+ };
34
+ }
35
+
36
+ if (!auth.privateKey) {
37
+ throw new Error('Setu: either privateKey or signer is required.');
38
+ }
39
+
40
+ const privateKeyBytes = bs58.decode(auth.privateKey);
41
+ const keypair = Keypair.fromSecretKey(privateKeyBytes);
42
+ const walletAddress = keypair.publicKey.toBase58();
43
+ return {
44
+ keypair,
45
+ walletAddress,
46
+ privateKeyBytes,
47
+ buildHeaders: () => buildWalletHeaders(walletAddress, privateKeyBytes),
48
+ };
17
49
  }
18
50
 
19
51
  export function signNonce(nonce: string, secretKey: Uint8Array): string {
20
- const data = new TextEncoder().encode(nonce);
21
- const signature = nacl.sign.detached(data, secretKey);
22
- return bs58.encode(signature);
52
+ const data = new TextEncoder().encode(nonce);
53
+ const signature = nacl.sign.detached(data, secretKey);
54
+ return bs58.encode(signature);
23
55
  }
24
56
 
25
- export function buildWalletHeaders(ctx: WalletContext): Record<string, string> {
26
- const nonce = Date.now().toString();
27
- const signature = signNonce(nonce, ctx.privateKeyBytes);
28
- return {
29
- 'x-wallet-address': ctx.walletAddress,
30
- 'x-wallet-nonce': nonce,
31
- 'x-wallet-signature': signature,
32
- };
57
+ export function buildWalletHeaders(
58
+ walletAddress: string,
59
+ privateKeyBytes: Uint8Array,
60
+ ): Record<string, string> {
61
+ const nonce = Date.now().toString();
62
+ const signature = signNonce(nonce, privateKeyBytes);
63
+ return {
64
+ 'x-wallet-address': walletAddress,
65
+ 'x-wallet-nonce': nonce,
66
+ 'x-wallet-signature': signature,
67
+ };
33
68
  }
34
69
 
35
70
  export function getPublicKeyFromPrivate(privateKey?: string): string | null {
36
- if (!privateKey) return null;
37
- try {
38
- const privateKeyBytes = bs58.decode(privateKey);
39
- const keypair = Keypair.fromSecretKey(privateKeyBytes);
40
- return keypair.publicKey.toBase58();
41
- } catch {
42
- return null;
43
- }
71
+ if (!privateKey) return null;
72
+ try {
73
+ const privateKeyBytes = bs58.decode(privateKey);
74
+ const keypair = Keypair.fromSecretKey(privateKeyBytes);
75
+ return keypair.publicKey.toBase58();
76
+ } catch {
77
+ return null;
78
+ }
44
79
  }
package/src/balance.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import bs58 from 'bs58';
2
2
  import { Keypair } from '@solana/web3.js';
3
3
  import type { SetuAuth, BalanceResponse, WalletUsdcBalance } from './types.ts';
4
- import { signNonce } from './auth.ts';
4
+ import type { WalletContext } from './auth.ts';
5
+ import { createWalletContext } from './auth.ts';
5
6
 
6
7
  const DEFAULT_BASE_URL = 'https://api.setu.ottocode.io';
7
8
  const DEFAULT_RPC_URL = 'https://api.mainnet-beta.solana.com';
@@ -9,101 +10,114 @@ const USDC_MINT_MAINNET = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
9
10
  const USDC_MINT_DEVNET = '4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU';
10
11
 
11
12
  function trimTrailingSlash(url: string) {
12
- return url.endsWith('/') ? url.slice(0, -1) : url;
13
+ return url.endsWith('/') ? url.slice(0, -1) : url;
14
+ }
15
+
16
+ function isWalletContext(input: unknown): input is WalletContext {
17
+ return (
18
+ typeof input === 'object' &&
19
+ input !== null &&
20
+ 'buildHeaders' in input &&
21
+ typeof (input as WalletContext).buildHeaders === 'function'
22
+ );
13
23
  }
14
24
 
15
25
  export async function fetchBalance(
16
- auth: Required<SetuAuth>,
17
- baseURL?: string,
26
+ walletOrAuth: WalletContext | SetuAuth,
27
+ baseURL?: string,
18
28
  ): Promise<BalanceResponse | null> {
19
- try {
20
- const privateKeyBytes = bs58.decode(auth.privateKey!);
21
- const keypair = Keypair.fromSecretKey(privateKeyBytes);
22
- const walletAddress = keypair.publicKey.toBase58();
23
- const url = trimTrailingSlash(baseURL ?? DEFAULT_BASE_URL);
24
-
25
- const nonce = Date.now().toString();
26
- const signature = signNonce(nonce, privateKeyBytes);
27
-
28
- const response = await fetch(`${url}/v1/balance`, {
29
- headers: {
30
- 'x-wallet-address': walletAddress,
31
- 'x-wallet-nonce': nonce,
32
- 'x-wallet-signature': signature,
33
- },
34
- });
35
-
36
- if (!response.ok) return null;
37
-
38
- const data = (await response.json()) as {
39
- wallet_address: string;
40
- balance_usd: number;
41
- total_spent: number;
42
- total_topups: number;
43
- request_count: number;
44
- created_at?: string;
45
- last_request?: string;
46
- };
47
-
48
- return {
49
- walletAddress: data.wallet_address,
50
- balance: data.balance_usd,
51
- totalSpent: data.total_spent,
52
- totalTopups: data.total_topups,
53
- requestCount: data.request_count,
54
- createdAt: data.created_at,
55
- lastRequest: data.last_request,
56
- };
57
- } catch {
58
- return null;
59
- }
29
+ try {
30
+ const wallet = isWalletContext(walletOrAuth)
31
+ ? walletOrAuth
32
+ : createWalletContext(walletOrAuth);
33
+ const url = trimTrailingSlash(baseURL ?? DEFAULT_BASE_URL);
34
+ const headers = await wallet.buildHeaders();
35
+
36
+ const response = await fetch(`${url}/v1/balance`, { headers });
37
+
38
+ if (!response.ok) return null;
39
+
40
+ const data = (await response.json()) as {
41
+ wallet_address: string;
42
+ balance_usd: number;
43
+ total_spent: number;
44
+ total_topups: number;
45
+ request_count: number;
46
+ created_at?: string;
47
+ last_request?: string;
48
+ };
49
+
50
+ return {
51
+ walletAddress: data.wallet_address,
52
+ balance: data.balance_usd,
53
+ totalSpent: data.total_spent,
54
+ totalTopups: data.total_topups,
55
+ requestCount: data.request_count,
56
+ createdAt: data.created_at,
57
+ lastRequest: data.last_request,
58
+ };
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
63
+
64
+ type WalletUsdcBalanceInput =
65
+ | Required<Pick<SetuAuth, 'privateKey'>>
66
+ | { walletAddress: string };
67
+
68
+ function resolveWalletAddress(input: WalletUsdcBalanceInput): string {
69
+ if ('walletAddress' in input) return input.walletAddress;
70
+ const privateKeyBytes = bs58.decode(input.privateKey);
71
+ const keypair = Keypair.fromSecretKey(privateKeyBytes);
72
+ return keypair.publicKey.toBase58();
60
73
  }
61
74
 
62
75
  export async function fetchWalletUsdcBalance(
63
- auth: Required<SetuAuth>,
64
- network: 'mainnet' | 'devnet' = 'mainnet',
76
+ input: WalletUsdcBalanceInput,
77
+ network: 'mainnet' | 'devnet' = 'mainnet',
65
78
  ): Promise<WalletUsdcBalance | null> {
66
- try {
67
- const privateKeyBytes = bs58.decode(auth.privateKey!);
68
- const keypair = Keypair.fromSecretKey(privateKeyBytes);
69
- const walletAddress = keypair.publicKey.toBase58();
70
- const rpcUrl = network === 'devnet' ? 'https://api.devnet.solana.com' : DEFAULT_RPC_URL;
71
- const usdcMint = network === 'devnet' ? USDC_MINT_DEVNET : USDC_MINT_MAINNET;
72
-
73
- const response = await fetch(rpcUrl, {
74
- method: 'POST',
75
- headers: { 'Content-Type': 'application/json' },
76
- body: JSON.stringify({
77
- jsonrpc: '2.0',
78
- id: 1,
79
- method: 'getTokenAccountsByOwner',
80
- params: [walletAddress, { mint: usdcMint }, { encoding: 'jsonParsed' }],
81
- }),
82
- });
83
-
84
- if (!response.ok) return null;
85
-
86
- const data = (await response.json()) as {
87
- result?: {
88
- value?: Array<{
89
- account: {
90
- data: {
91
- parsed: {
92
- info: { tokenAmount: { uiAmount: number } };
93
- };
94
- };
95
- };
96
- }>;
97
- };
98
- };
99
-
100
- let totalUsdcBalance = 0;
101
- for (const account of data.result?.value ?? []) {
102
- totalUsdcBalance += account.account.data.parsed.info.tokenAmount.uiAmount ?? 0;
103
- }
104
-
105
- return { walletAddress, usdcBalance: totalUsdcBalance, network };
106
- } catch {
107
- return null;
108
- }
79
+ try {
80
+ const walletAddress = resolveWalletAddress(input);
81
+ const rpcUrl =
82
+ network === 'devnet' ? 'https://api.devnet.solana.com' : DEFAULT_RPC_URL;
83
+ const usdcMint =
84
+ network === 'devnet' ? USDC_MINT_DEVNET : USDC_MINT_MAINNET;
85
+
86
+ const response = await fetch(rpcUrl, {
87
+ method: 'POST',
88
+ headers: { 'Content-Type': 'application/json' },
89
+ body: JSON.stringify({
90
+ jsonrpc: '2.0',
91
+ id: 1,
92
+ method: 'getTokenAccountsByOwner',
93
+ params: [walletAddress, { mint: usdcMint }, { encoding: 'jsonParsed' }],
94
+ }),
95
+ });
96
+
97
+ if (!response.ok) return null;
98
+
99
+ const data = (await response.json()) as {
100
+ result?: {
101
+ value?: Array<{
102
+ account: {
103
+ data: {
104
+ parsed: {
105
+ info: { tokenAmount: { uiAmount: number } };
106
+ };
107
+ };
108
+ };
109
+ }>;
110
+ };
111
+ };
112
+
113
+ let totalUsdcBalance = 0;
114
+ for (const account of data.result?.value ?? []) {
115
+ totalUsdcBalance +=
116
+ account.account.data.parsed.info.tokenAmount.uiAmount ?? 0;
117
+ }
118
+
119
+ return { walletAddress, usdcBalance: totalUsdcBalance, network };
120
+ } catch {
121
+ return null;
122
+ }
109
123
  }
package/src/cache.ts CHANGED
@@ -1,159 +1,181 @@
1
1
  import type { AnthropicCacheConfig, AnthropicCachePlacement } from './types.ts';
2
2
 
3
3
  interface MessageBlock {
4
- type: string;
5
- cache_control?: { type: string };
6
- [key: string]: unknown;
4
+ type: string;
5
+ cache_control?: { type: string };
6
+ [key: string]: unknown;
7
7
  }
8
8
 
9
9
  interface Message {
10
- role: string;
11
- content: unknown;
12
- [key: string]: unknown;
10
+ role: string;
11
+ content: unknown;
12
+ [key: string]: unknown;
13
13
  }
14
14
 
15
15
  interface ParsedBody {
16
- system?: MessageBlock[];
17
- messages?: Message[];
18
- [key: string]: unknown;
16
+ system?: MessageBlock[];
17
+ messages?: Message[];
18
+ [key: string]: unknown;
19
19
  }
20
20
 
21
21
  const DEFAULT_CONFIG: Required<Omit<AnthropicCacheConfig, 'transform'>> = {
22
- strategy: 'auto',
23
- systemBreakpoints: 1,
24
- messageBreakpoints: 1,
25
- systemPlacement: 'first',
26
- messagePlacement: 'last',
27
- cacheType: 'ephemeral',
22
+ strategy: 'auto',
23
+ systemBreakpoints: 1,
24
+ messageBreakpoints: 1,
25
+ systemPlacement: 'first',
26
+ messagePlacement: 'last',
27
+ cacheType: 'ephemeral',
28
28
  };
29
29
 
30
- function resolveConfig(config?: AnthropicCacheConfig): Required<Omit<AnthropicCacheConfig, 'transform'>> & { transform?: AnthropicCacheConfig['transform'] } {
31
- if (!config) return { ...DEFAULT_CONFIG };
32
- return {
33
- strategy: config.strategy ?? DEFAULT_CONFIG.strategy,
34
- systemBreakpoints: config.systemBreakpoints ?? DEFAULT_CONFIG.systemBreakpoints,
35
- messageBreakpoints: config.messageBreakpoints ?? DEFAULT_CONFIG.messageBreakpoints,
36
- systemPlacement: config.systemPlacement ?? DEFAULT_CONFIG.systemPlacement,
37
- messagePlacement: config.messagePlacement ?? DEFAULT_CONFIG.messagePlacement,
38
- cacheType: config.cacheType ?? DEFAULT_CONFIG.cacheType,
39
- transform: config.transform,
40
- };
30
+ function resolveConfig(config?: AnthropicCacheConfig): Required<
31
+ Omit<AnthropicCacheConfig, 'transform'>
32
+ > & {
33
+ transform?: AnthropicCacheConfig['transform'];
34
+ } {
35
+ if (!config) return { ...DEFAULT_CONFIG };
36
+ return {
37
+ strategy: config.strategy ?? DEFAULT_CONFIG.strategy,
38
+ systemBreakpoints:
39
+ config.systemBreakpoints ?? DEFAULT_CONFIG.systemBreakpoints,
40
+ messageBreakpoints:
41
+ config.messageBreakpoints ?? DEFAULT_CONFIG.messageBreakpoints,
42
+ systemPlacement: config.systemPlacement ?? DEFAULT_CONFIG.systemPlacement,
43
+ messagePlacement:
44
+ config.messagePlacement ?? DEFAULT_CONFIG.messagePlacement,
45
+ cacheType: config.cacheType ?? DEFAULT_CONFIG.cacheType,
46
+ transform: config.transform,
47
+ };
41
48
  }
42
49
 
43
50
  function shouldCacheAtIndex(
44
- index: number,
45
- total: number,
46
- maxBreakpoints: number,
47
- placement: AnthropicCachePlacement,
51
+ index: number,
52
+ total: number,
53
+ maxBreakpoints: number,
54
+ placement: AnthropicCachePlacement,
48
55
  ): boolean {
49
- if (maxBreakpoints <= 0) return false;
50
-
51
- switch (placement) {
52
- case 'first': {
53
- return index < maxBreakpoints;
54
- }
55
- case 'last': {
56
- const startFrom = total - maxBreakpoints;
57
- return index >= startFrom && index < total;
58
- }
59
- case 'all': {
60
- return true;
61
- }
62
- }
56
+ if (maxBreakpoints <= 0) return false;
57
+
58
+ switch (placement) {
59
+ case 'first': {
60
+ return index < maxBreakpoints;
61
+ }
62
+ case 'last': {
63
+ const startFrom = total - maxBreakpoints;
64
+ return index >= startFrom && index < total;
65
+ }
66
+ case 'all': {
67
+ return true;
68
+ }
69
+ }
63
70
  }
64
71
 
65
72
  function injectCacheOnBlocks(
66
- blocks: MessageBlock[],
67
- maxBreakpoints: number,
68
- placement: AnthropicCachePlacement,
69
- cacheType: string,
73
+ blocks: MessageBlock[],
74
+ maxBreakpoints: number,
75
+ placement: AnthropicCachePlacement,
76
+ cacheType: string,
70
77
  ): { blocks: MessageBlock[]; used: number } {
71
- let used = 0;
72
- const eligible = blocks.map((_, i) => shouldCacheAtIndex(i, blocks.length, maxBreakpoints, placement));
73
- const result = blocks.map((block, i) => {
74
- if (block.cache_control) return block;
75
- if (used < maxBreakpoints && eligible[i]) {
76
- used++;
77
- return { ...block, cache_control: { type: cacheType } };
78
- }
79
- return block;
80
- });
81
- return { blocks: result, used };
78
+ let used = 0;
79
+ const eligible = blocks.map((_, i) =>
80
+ shouldCacheAtIndex(i, blocks.length, maxBreakpoints, placement),
81
+ );
82
+ const result = blocks.map((block, i) => {
83
+ if (block.cache_control) return block;
84
+ if (used < maxBreakpoints && eligible[i]) {
85
+ used++;
86
+ return { ...block, cache_control: { type: cacheType } };
87
+ }
88
+ return block;
89
+ });
90
+ return { blocks: result, used };
82
91
  }
83
92
 
84
93
  export function addAnthropicCacheControl(
85
- parsed: ParsedBody,
86
- config?: AnthropicCacheConfig,
94
+ parsed: ParsedBody,
95
+ config?: AnthropicCacheConfig,
87
96
  ): ParsedBody {
88
- const resolved = resolveConfig(config);
89
-
90
- if (resolved.strategy === false) return parsed;
91
-
92
- if (resolved.strategy === 'manual') return parsed;
93
-
94
- if (resolved.strategy === 'custom' && resolved.transform) {
95
- return resolved.transform(parsed as Record<string, unknown>) as ParsedBody;
96
- }
97
-
98
- let systemUsed = 0;
99
-
100
- if (parsed.system && Array.isArray(parsed.system)) {
101
- const result = injectCacheOnBlocks(
102
- parsed.system,
103
- resolved.systemBreakpoints,
104
- resolved.systemPlacement,
105
- resolved.cacheType,
106
- );
107
- parsed.system = result.blocks;
108
- systemUsed = result.used;
109
- }
110
-
111
- if (parsed.messages && Array.isArray(parsed.messages)) {
112
- let messageUsed = 0;
113
- const messageCount = parsed.messages.length;
114
-
115
- const eligibleIndices: number[] = [];
116
- for (let i = 0; i < messageCount; i++) {
117
- if (shouldCacheAtIndex(i, messageCount, resolved.messageBreakpoints, resolved.messagePlacement)) {
118
- eligibleIndices.push(i);
119
- }
120
- }
121
-
122
- parsed.messages = parsed.messages.map((msg, msgIndex) => {
123
- if (messageUsed >= resolved.messageBreakpoints) return msg;
124
- if (!eligibleIndices.includes(msgIndex)) return msg;
125
-
126
- if (Array.isArray(msg.content)) {
127
- const blocks = msg.content as MessageBlock[];
128
- const lastIdx = blocks.length - 1;
129
- const content = blocks.map((block, blockIndex) => {
130
- if (block.cache_control) return block;
131
- if (blockIndex === lastIdx && messageUsed < resolved.messageBreakpoints) {
132
- messageUsed++;
133
- return { ...block, cache_control: { type: resolved.cacheType } };
134
- }
135
- return block;
136
- });
137
- return { ...msg, content };
138
- }
139
-
140
- if (typeof msg.content === 'string' && messageUsed < resolved.messageBreakpoints) {
141
- messageUsed++;
142
- return {
143
- ...msg,
144
- content: [
145
- {
146
- type: 'text',
147
- text: msg.content,
148
- cache_control: { type: resolved.cacheType },
149
- },
150
- ],
151
- };
152
- }
153
-
154
- return msg;
155
- });
156
- }
157
-
158
- return parsed;
97
+ const resolved = resolveConfig(config);
98
+
99
+ if (resolved.strategy === false) return parsed;
100
+
101
+ if (resolved.strategy === 'manual') return parsed;
102
+
103
+ if (resolved.strategy === 'custom' && resolved.transform) {
104
+ return resolved.transform(parsed as Record<string, unknown>) as ParsedBody;
105
+ }
106
+
107
+ let _systemUsed = 0;
108
+
109
+ if (parsed.system && Array.isArray(parsed.system)) {
110
+ const result = injectCacheOnBlocks(
111
+ parsed.system,
112
+ resolved.systemBreakpoints,
113
+ resolved.systemPlacement,
114
+ resolved.cacheType,
115
+ );
116
+ parsed.system = result.blocks;
117
+ _systemUsed = result.used;
118
+ }
119
+
120
+ if (parsed.messages && Array.isArray(parsed.messages)) {
121
+ let messageUsed = 0;
122
+ const messageCount = parsed.messages.length;
123
+
124
+ const eligibleIndices: number[] = [];
125
+ for (let i = 0; i < messageCount; i++) {
126
+ if (
127
+ shouldCacheAtIndex(
128
+ i,
129
+ messageCount,
130
+ resolved.messageBreakpoints,
131
+ resolved.messagePlacement,
132
+ )
133
+ ) {
134
+ eligibleIndices.push(i);
135
+ }
136
+ }
137
+
138
+ parsed.messages = parsed.messages.map((msg, msgIndex) => {
139
+ if (messageUsed >= resolved.messageBreakpoints) return msg;
140
+ if (!eligibleIndices.includes(msgIndex)) return msg;
141
+
142
+ if (Array.isArray(msg.content)) {
143
+ const blocks = msg.content as MessageBlock[];
144
+ const lastIdx = blocks.length - 1;
145
+ const content = blocks.map((block, blockIndex) => {
146
+ if (block.cache_control) return block;
147
+ if (
148
+ blockIndex === lastIdx &&
149
+ messageUsed < resolved.messageBreakpoints
150
+ ) {
151
+ messageUsed++;
152
+ return { ...block, cache_control: { type: resolved.cacheType } };
153
+ }
154
+ return block;
155
+ });
156
+ return { ...msg, content };
157
+ }
158
+
159
+ if (
160
+ typeof msg.content === 'string' &&
161
+ messageUsed < resolved.messageBreakpoints
162
+ ) {
163
+ messageUsed++;
164
+ return {
165
+ ...msg,
166
+ content: [
167
+ {
168
+ type: 'text',
169
+ text: msg.content,
170
+ cache_control: { type: resolved.cacheType },
171
+ },
172
+ ],
173
+ };
174
+ }
175
+
176
+ return msg;
177
+ });
178
+ }
179
+
180
+ return parsed;
159
181
  }