@altsafe/aidirector 1.0.1 → 1.1.0

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 CHANGED
@@ -1,27 +1,12 @@
1
- # @altsafe/aidirector
1
+ # @altsafe/aidirector - Client SDK
2
2
 
3
- Official TypeScript SDK for **AI Director** — the intelligent AI API gateway with automatic failover, response caching, and JSON extraction.
4
-
5
- [![npm version](https://img.shields.io/npm/v/@altsafe/aidirector.svg)](https://www.npmjs.com/package/@altsafe/aidirector)
6
- [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue.svg)](https://www.typescriptlang.org/)
7
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
-
9
- ## Features
10
-
11
- - 🔐 **HMAC Authentication** — Secure request signing (Node.js & browser)
12
- - ⚡ **10-minute Timeout** — Handles long-running AI requests
13
- - 🔄 **Automatic Retries** — Exponential backoff with configurable limits
14
- - 📦 **Structured Errors** — Type-safe error handling
15
- - 🌊 **Streaming Support** — Real-time response streaming
16
- - 🎯 **Full TypeScript** — Complete type definitions
3
+ Production-grade TypeScript SDK for the AI Director API gateway.
17
4
 
18
5
  ## Installation
19
6
 
20
7
  ```bash
21
8
  npm install @altsafe/aidirector
22
9
  # or
23
- bun add @altsafe/aidirector
24
- # or
25
10
  pnpm add @altsafe/aidirector
26
11
  ```
27
12
 
@@ -32,330 +17,125 @@ import { AIDirector } from '@altsafe/aidirector';
32
17
 
33
18
  const client = new AIDirector({
34
19
  secretKey: process.env.AIDIRECTOR_SECRET_KEY!,
35
- baseUrl: 'https://your-api.example.com',
20
+ baseUrl: 'https://your-instance.vercel.app',
36
21
  });
37
22
 
23
+ // Generate content
38
24
  const result = await client.generate({
39
- chainId: 'your-chain-id',
40
- prompt: 'Generate 5 user profiles with name and email',
41
- schema: { name: 'string', email: 'string' },
25
+ chainId: 'my-chain',
26
+ prompt: 'Generate 5 user profiles',
42
27
  });
43
28
 
44
29
  if (result.success) {
45
- console.log('Users:', result.data.valid);
46
- console.log('Model:', result.meta.modelUsed);
47
- console.log('Cached:', result.meta.cached);
30
+ console.log(result.data.valid);
48
31
  }
49
32
  ```
50
33
 
51
- ## Usage Examples
52
-
53
- ### Next.js Server Actions
54
-
55
- ```typescript
56
- 'use server';
57
-
58
- import { AIDirector } from '@altsafe/aidirector';
59
-
60
- const client = new AIDirector({
61
- secretKey: process.env.AIDIRECTOR_SECRET_KEY!,
62
- });
34
+ ## Features
63
35
 
64
- export async function generateContent(prompt: string) {
65
- return client.generate({
66
- chainId: process.env.DEFAULT_CHAIN_ID!,
67
- prompt,
68
- });
69
- }
70
- ```
36
+ - 🔐 **HMAC Authentication** - Secure request signing
37
+ - ⚡ **3-Step Architecture** - Token → Worker → Complete (minimizes costs)
38
+ - 📎 **File Attachments** - Upload and process documents
39
+ - 🔄 **Automatic Retries** - Exponential backoff on failures
40
+ - 💾 **Smart Caching** - Hybrid Redis caching with 7-day TTL
41
+ - 🎯 **TypeScript** - Full type safety
71
42
 
72
- ### API Routes
73
-
74
- ```typescript
75
- // app/api/generate/route.ts
76
- import { NextResponse } from 'next/server';
77
- import { AIDirector } from '@altsafe/aidirector';
78
-
79
- const client = new AIDirector({
80
- secretKey: process.env.AIDIRECTOR_SECRET_KEY!,
81
- });
82
-
83
- export async function POST(request: Request) {
84
- const { prompt, chainId } = await request.json();
85
- const result = await client.generate({ chainId, prompt });
86
- return NextResponse.json(result);
87
- }
88
- ```
89
-
90
- ### Streaming
43
+ ## Streaming (Recommended for Large Responses)
91
44
 
92
45
  ```typescript
93
46
  await client.generateStream(
94
47
  {
95
48
  chainId: 'my-chain',
96
- prompt: 'Write a long story...',
49
+ prompt: 'Generate 100 product descriptions',
97
50
  },
98
51
  {
99
- onChunk: (chunk) => process.stdout.write(chunk),
100
- onComplete: (result) => console.log('\nDone!', result.meta),
101
- onError: (error) => console.error('Error:', error),
52
+ onObject: (obj, index) => {
53
+ console.log(`Object ${index}:`, obj);
54
+ renderToUI(obj); // Render immediately!
55
+ },
56
+ onComplete: (result) => {
57
+ console.log(`Done! ${result.objectCount} objects`);
58
+ },
59
+ onError: (error) => {
60
+ console.error('Stream failed:', error);
61
+ },
102
62
  }
103
63
  );
104
64
  ```
105
65
 
106
- ### Error Handling
66
+ ## File Attachments
107
67
 
108
68
  ```typescript
109
- import {
110
- AIDirector,
111
- TimeoutError,
112
- RateLimitError,
113
- ValidationError,
114
- isAIDirectorError,
115
- isRetryableError,
116
- } from '@altsafe/aidirector';
69
+ import fs from 'fs';
117
70
 
118
- try {
119
- const result = await client.generate({ chainId, prompt });
120
- } catch (error) {
121
- if (error instanceof TimeoutError) {
122
- console.log('Request timed out after', error.timeoutMs, 'ms');
123
- } else if (error instanceof RateLimitError) {
124
- console.log('Rate limited, retry after', error.retryAfterMs, 'ms');
125
- } else if (error instanceof ValidationError) {
126
- console.log('Validation failed:', error.validationErrors);
127
- } else if (isAIDirectorError(error)) {
128
- console.log('AI Director error:', error.code, error.message);
129
-
130
- // Check if we should retry
131
- if (isRetryableError(error)) {
132
- console.log('This error is retryable');
133
- }
134
- }
135
- }
136
- ```
71
+ const fileBuffer = fs.readFileSync('report.pdf');
137
72
 
138
- ## API Reference
73
+ const result = await client.generate({
74
+ chainId: 'document-analysis',
75
+ prompt: 'Summarize this document',
76
+ files: [{
77
+ data: fileBuffer.toString('base64'),
78
+ filename: 'report.pdf',
79
+ mimeType: 'application/pdf',
80
+ }],
81
+ });
82
+ ```
139
83
 
140
- ### `new AIDirector(config)`
84
+ ## Configuration
141
85
 
142
86
  | Option | Type | Default | Description |
143
87
  |--------|------|---------|-------------|
144
- | `secretKey` | `string` | | **Required.** Your API secret key |
145
- | `baseUrl` | `string` | `localhost:3000` | API base URL |
146
- | `timeout` | `number` | `600000` | Request timeout (ms) |
88
+ | `secretKey` | `string` | **required** | Your API key (`aid_sk_...`) |
89
+ | `baseUrl` | `string` | `http://localhost:3000` | API base URL |
90
+ | `timeout` | `number` | `600000` | Request timeout (10 min) |
147
91
  | `maxRetries` | `number` | `3` | Max retry attempts |
148
- | `debug` | `boolean` | `false` | Enable console logging |
149
-
150
- ### `client.generate(options)`
92
+ | `debug` | `boolean` | `false` | Enable debug logging |
93
+ | `useOptimized` | `boolean` | `true` | Use 3-step Worker flow |
151
94
 
152
- Generate content using your fallback chain.
95
+ ## API Methods
153
96
 
154
- | Option | Type | Required | Description |
155
- |--------|------|----------|-------------|
156
- | `chainId` | `string` | Yes | Fallback chain ID |
157
- | `prompt` | `string` | Yes | Prompt to send |
158
- | `schema` | `object` | No | JSON schema for validation |
159
- | `timeout` | `number` | No | Override timeout for this request |
160
- | `options.temperature` | `number` | No | Randomness (0-2) |
161
- | `options.maxTokens` | `number` | No | Max output tokens |
162
- | `options.topP` | `number` | No | Nucleus sampling (0-1) |
163
- | `options.topK` | `number` | No | Top-K sampling |
164
- | `options.systemPrompt` | `string` | No | System prompt to prepend |
97
+ | Method | Description |
98
+ |--------|-------------|
99
+ | `generate(options)` | Generate content with fallback chain |
100
+ | `generateStream(options, callbacks)` | Stream JSON objects in real-time |
101
+ | `listModels()` | List available AI models |
102
+ | `listChains()` | List your fallback chains |
103
+ | `getUsageStats(options)` | Get usage statistics |
104
+ | `healthCheck()` | Check API health |
165
105
 
166
- **Returns:** `Promise<GenerateResult>`
167
-
168
- ```typescript
169
- interface GenerateResult {
170
- success: boolean;
171
- data: {
172
- valid: unknown[]; // Schema-compliant objects
173
- invalid: unknown[]; // Failed validation but parsed
174
- rawContent?: string; // Raw AI response
175
- };
176
- meta: {
177
- cached: boolean;
178
- modelUsed: string;
179
- tokensUsed: { input: number; output: number };
180
- latencyMs: number;
181
- attemptedModels: string[];
182
- finishReason?: string;
183
- };
184
- error?: {
185
- code: string;
186
- message: string;
187
- retryable?: boolean;
188
- };
189
- }
190
- ```
191
-
192
- ### `client.generateStream(options, callbacks)`
193
-
194
- Stream responses in real-time.
195
-
196
- ```typescript
197
- interface StreamCallbacks {
198
- onChunk?: (chunk: string) => void;
199
- onComplete?: (result: GenerateResult) => void;
200
- onError?: (error: Error) => void;
201
- onProgress?: (progress: StreamProgress) => void;
202
- }
203
- ```
204
-
205
- ### `client.listModels()`
206
-
207
- List all available AI models.
208
-
209
- ```typescript
210
- const models = await client.listModels();
211
- // Returns: ModelInfo[]
212
- ```
213
-
214
- ### `client.listChains()`
215
-
216
- Get your fallback chains (requires session auth).
217
-
218
- ```typescript
219
- const chains = await client.listChains();
220
- // Returns: ChainInfo[]
221
- ```
222
-
223
- ### `client.getUsage(options?)`
224
-
225
- Get usage statistics for your account.
226
-
227
- ```typescript
228
- const usage = await client.getUsage({
229
- startDate: new Date('2024-01-01'),
230
- endDate: new Date(),
231
- });
232
- // Returns: UsageStats
233
- ```
234
-
235
- ### `client.health()`
236
-
237
- Health check to verify API connection.
238
-
239
- ```typescript
240
- const { ok, latencyMs, version } = await client.health();
241
- ```
242
-
243
- ### `client.withConfig(overrides)`
244
-
245
- Create a new client with different configuration.
246
-
247
- ```typescript
248
- const debugClient = client.withConfig({ debug: true });
249
- const fastClient = client.withConfig({ timeout: 30000 });
250
- ```
251
-
252
- ## Error Classes
253
-
254
- | Error | Code | Retryable | Properties | Description |
255
- |-------|------|-----------|------------|-------------|
256
- | `ConfigurationError` | `CONFIGURATION_ERROR` | No | — | Invalid client setup |
257
- | `AuthenticationError` | `AUTH_ERROR` | No | `statusCode` | Invalid credentials |
258
- | `RateLimitError` | `RATE_LIMITED` | Yes | `retryAfterMs` | Too many requests |
259
- | `TimeoutError` | `TIMEOUT` | Yes | `timeoutMs` | Request timed out |
260
- | `NetworkError` | `NETWORK_ERROR` | Yes | `originalError` | Connection failed |
261
- | `ChainExecutionError` | `CHAIN_FAILED` | No | `attemptedModels` | All models failed |
262
- | `ValidationError` | `VALIDATION_ERROR` | No | `validationErrors` | Schema validation failed |
263
- | `ServerError` | `SERVER_ERROR` | Yes | `statusCode` | Internal server error |
264
-
265
- ### Error Helpers
266
-
267
- ```typescript
268
- import { isAIDirectorError, isRetryableError } from '@altsafe/aidirector';
269
-
270
- // Check if error is from AI Director
271
- if (isAIDirectorError(error)) {
272
- console.log(error.code, error.message);
273
- }
274
-
275
- // Check if error should be retried
276
- if (isRetryableError(error)) {
277
- // Implement retry logic
278
- }
279
- ```
280
-
281
- ## Advanced: HMAC Utilities
282
-
283
- For custom integrations, you can use the HMAC utilities directly:
106
+ ## Error Handling
284
107
 
285
108
  ```typescript
286
109
  import {
287
- generateSignature,
288
- getKeyPrefix,
289
- isValidSecretKey
110
+ RateLimitError,
111
+ TimeoutError,
112
+ AuthenticationError,
290
113
  } from '@altsafe/aidirector';
291
114
 
292
- // Validate a secret key format
293
- if (isValidSecretKey(key)) {
294
- console.log('Valid key');
115
+ try {
116
+ const result = await client.generate({ ... });
117
+ } catch (error) {
118
+ if (error instanceof RateLimitError) {
119
+ console.log(`Retry after ${error.retryAfterMs}ms`);
120
+ } else if (error instanceof TimeoutError) {
121
+ console.log('Request timed out');
122
+ } else if (error instanceof AuthenticationError) {
123
+ console.log('Invalid API key');
124
+ }
295
125
  }
296
-
297
- // Get key prefix for headers
298
- const prefix = getKeyPrefix(secretKey); // "aid_sk_xxxx"
299
-
300
- // Generate signature for custom requests
301
- const signature = await generateSignature(
302
- secretKey,
303
- 'POST',
304
- '/api/v1/generate',
305
- JSON.stringify(body),
306
- Date.now()
307
- );
308
126
  ```
309
127
 
310
- ## TypeScript Types
311
-
312
- All types are exported for your convenience:
128
+ ## Pricing
313
129
 
314
- ```typescript
315
- import type {
316
- // Configuration
317
- AIDirectorConfig,
318
-
319
- // Generation
320
- GenerateOptions,
321
- GenerateResult,
322
- GenerateData,
323
- GenerateMeta,
324
- GenerateError,
325
- GenerationParameters,
326
- TokenUsage,
327
-
328
- // Streaming
329
- StreamCallbacks,
330
- StreamProgress,
331
-
332
- // Chains & Models
333
- ChainInfo,
334
- ChainStep,
335
- ModelInfo,
336
-
337
- // Usage & Health
338
- UsageStats,
339
- HealthResult,
340
- } from '@altsafe/aidirector';
341
- ```
130
+ BYOK (Bring Your Own Key) - You pay for AI costs directly to providers.
342
131
 
343
- ## Security
344
-
345
- ⚠️ **Server-side only!** Never expose your secret key to browsers.
346
-
347
- - Next.js API routes / Server Actions
348
- - Express.js / Fastify
349
- - ✅ Angular Universal (SSR)
350
- - ✅ Edge Functions (Vercel, Cloudflare)
351
- - ❌ Client-side JavaScript
352
-
353
- Store your secret key in environment variables:
354
-
355
- ```bash
356
- AIDIRECTOR_SECRET_KEY=aid_sk_your_secret_key_here
357
- ```
132
+ | Tier | Price | Requests | Overage |
133
+ |------|-------|----------|---------|
134
+ | Free | $0 | 1K | Blocked |
135
+ | Starter | $9 | 25K | $0.50/1K |
136
+ | Pro | $29 | 100K | $0.40/1K |
137
+ | Scale | $79 | 500K | $0.30/1K |
358
138
 
359
139
  ## License
360
140
 
361
- MIT © altsafe
141
+ MIT
package/dist/index.d.mts CHANGED
@@ -58,11 +58,64 @@ interface GenerateOptions {
58
58
  * Override the client timeout for this request
59
59
  */
60
60
  timeout?: number;
61
+ /**
62
+ * Skip the cache for this request.
63
+ * Useful for prompts that should always generate fresh responses.
64
+ * @default false
65
+ */
66
+ noCache?: boolean;
67
+ /**
68
+ * File attachments to include with the request.
69
+ * The server automatically handles:
70
+ * - File type detection (magic bytes)
71
+ * - Model compatibility checking
72
+ * - Automatic conversion (e.g., DOCX → PDF)
73
+ *
74
+ * @example
75
+ * ```typescript
76
+ * await client.generate({
77
+ * chainId: 'my-chain',
78
+ * prompt: 'Summarize this document',
79
+ * files: [{
80
+ * data: documentBuffer.toString('base64'),
81
+ * filename: 'report.pdf',
82
+ * mimeType: 'application/pdf',
83
+ * }],
84
+ * });
85
+ * ```
86
+ */
87
+ files?: FileAttachment[];
88
+ /**
89
+ * Use the optimized 3-step generation flow.
90
+ * This minimizes Vercel compute costs by calling the Cloudflare Worker directly.
91
+ * Set to false to force legacy single-call mode.
92
+ * @default true
93
+ */
94
+ useOptimized?: boolean;
61
95
  /**
62
96
  * Generation parameters
63
97
  */
64
98
  options?: GenerationParameters;
65
99
  }
100
+ /**
101
+ * File attachment for generate requests.
102
+ * Pass files and the server handles detection/conversion automatically.
103
+ */
104
+ interface FileAttachment {
105
+ /**
106
+ * File content as base64-encoded string
107
+ */
108
+ data: string;
109
+ /**
110
+ * Original filename (optional, helps with type detection)
111
+ */
112
+ filename?: string;
113
+ /**
114
+ * Claimed MIME type (optional, will be verified by server)
115
+ * If omitted, server will auto-detect from magic bytes
116
+ */
117
+ mimeType?: string;
118
+ }
66
119
  /**
67
120
  * AI generation parameters
68
121
  */
@@ -195,13 +248,22 @@ interface GenerateError {
195
248
  */
196
249
  interface StreamCallbacks {
197
250
  /**
198
- * Called for each text chunk received
251
+ * Called for each text chunk received (legacy/backwards compatible)
199
252
  */
200
253
  onChunk?: (chunk: string) => void;
201
254
  /**
202
- * Called when streaming is complete
255
+ * Called for each VALID JSON object parsed from stream
256
+ * This is the preferred callback for JSON streaming - each object is complete and parseable
203
257
  */
204
- onComplete?: (result: GenerateResult) => void;
258
+ onObject?: (object: unknown, index: number) => void;
259
+ /**
260
+ * Called when a JSON array starts in the stream
261
+ */
262
+ onArrayStart?: () => void;
263
+ /**
264
+ * Called when streaming is complete with full result
265
+ */
266
+ onComplete?: (result: StreamCompleteResult) => void;
205
267
  /**
206
268
  * Called if an error occurs
207
269
  */
@@ -211,6 +273,30 @@ interface StreamCallbacks {
211
273
  */
212
274
  onProgress?: (progress: StreamProgress) => void;
213
275
  }
276
+ /**
277
+ * Result passed to onComplete callback during streaming
278
+ */
279
+ interface StreamCompleteResult {
280
+ /**
281
+ * All collected objects from the stream
282
+ */
283
+ objects: unknown[];
284
+ /**
285
+ * Total number of objects parsed
286
+ */
287
+ objectCount: number;
288
+ /**
289
+ * Token usage for the request
290
+ */
291
+ tokensUsed?: {
292
+ input: number;
293
+ output: number;
294
+ };
295
+ /**
296
+ * Whether the response was cached at stream end
297
+ */
298
+ cached?: boolean;
299
+ }
214
300
  /**
215
301
  * Streaming progress information
216
302
  */
@@ -408,6 +494,89 @@ interface HealthResult {
408
494
  */
409
495
  timestamp?: string;
410
496
  }
497
+ /**
498
+ * Detailed health check result with component status
499
+ */
500
+ interface DetailedHealthResult {
501
+ /**
502
+ * Overall status
503
+ */
504
+ status: 'healthy' | 'degraded' | 'unhealthy';
505
+ /**
506
+ * Server timestamp
507
+ */
508
+ timestamp: string;
509
+ /**
510
+ * Server uptime in seconds
511
+ */
512
+ uptime: number;
513
+ /**
514
+ * Component status
515
+ */
516
+ components: {
517
+ database: {
518
+ status: string;
519
+ latencyMs: number;
520
+ message?: string;
521
+ };
522
+ redis: {
523
+ status: string;
524
+ latencyMs: number;
525
+ message?: string;
526
+ };
527
+ gemini: {
528
+ status: string;
529
+ errorRate: number;
530
+ recentErrors: number;
531
+ };
532
+ openrouter: {
533
+ status: string;
534
+ errorRate: number;
535
+ recentErrors: number;
536
+ };
537
+ };
538
+ /**
539
+ * 24h metrics
540
+ */
541
+ metrics: {
542
+ last24h: {
543
+ totalRequests: number;
544
+ successRate: number;
545
+ cacheHitRate: number;
546
+ avgLatencyMs: number;
547
+ };
548
+ errorsByCode: Record<string, number>;
549
+ };
550
+ /**
551
+ * Cache status
552
+ */
553
+ cache: {
554
+ totalEntries: number;
555
+ redisAvailable: boolean;
556
+ };
557
+ }
558
+ /**
559
+ * Webhook configuration for async notifications
560
+ */
561
+ interface WebhookConfig {
562
+ /**
563
+ * Request ID to receive notifications for
564
+ */
565
+ requestId: string;
566
+ /**
567
+ * URL to call when request completes
568
+ */
569
+ url: string;
570
+ /**
571
+ * Optional secret for HMAC signing
572
+ */
573
+ secret?: string;
574
+ /**
575
+ * Number of retry attempts on failure
576
+ * @default 3
577
+ */
578
+ retryCount?: number;
579
+ }
411
580
 
412
581
  /**
413
582
  * AI Director Client SDK
@@ -467,6 +636,24 @@ declare class AIDirector {
467
636
  * ```
468
637
  */
469
638
  generate(options: GenerateOptions): Promise<GenerateResult>;
639
+ /**
640
+ * Optimized 3-step generation (minimizes Vercel compute costs)
641
+ *
642
+ * Step 1: Get token from Vercel (~50ms)
643
+ * Step 2: Call CF Worker directly (FREE)
644
+ * Step 3: Cache result in Vercel (~50ms)
645
+ *
646
+ * Total Vercel time: ~100ms vs 30-120s in legacy mode
647
+ */
648
+ private generateOptimized;
649
+ /**
650
+ * Cache completion from worker (Step 3 - async, non-blocking)
651
+ */
652
+ private cacheCompletionAsync;
653
+ /**
654
+ * Legacy single-call generation (higher Vercel compute cost)
655
+ */
656
+ private generateLegacy;
470
657
  /**
471
658
  * Generate content with streaming (for long responses)
472
659
  *
@@ -527,6 +714,67 @@ declare class AIDirector {
527
714
  * @returns New AIDirector instance
528
715
  */
529
716
  withConfig(overrides: Partial<AIDirectorConfig>): AIDirector;
717
+ /**
718
+ * Register a webhook for async notifications
719
+ *
720
+ * @param config - Webhook configuration
721
+ * @returns Registration result
722
+ */
723
+ registerWebhook(config: {
724
+ requestId: string;
725
+ url: string;
726
+ secret?: string;
727
+ retryCount?: number;
728
+ }): Promise<{
729
+ registered: boolean;
730
+ message: string;
731
+ }>;
732
+ /**
733
+ * Get detailed health information including component status
734
+ *
735
+ * @returns Detailed health status
736
+ */
737
+ healthDetailed(): Promise<{
738
+ status: 'healthy' | 'degraded' | 'unhealthy';
739
+ timestamp: string;
740
+ uptime: number;
741
+ components: Record<string, unknown>;
742
+ metrics: Record<string, unknown>;
743
+ }>;
744
+ /**
745
+ * Generate content for multiple prompts in a single request
746
+ *
747
+ * @param chainId - Chain to use for all items
748
+ * @param items - Array of prompts with IDs
749
+ * @param options - Batch options
750
+ * @returns Batch generation results
751
+ */
752
+ generateBatch(chainId: string, items: Array<{
753
+ id: string;
754
+ prompt: string;
755
+ systemPrompt?: string;
756
+ temperature?: number;
757
+ maxTokens?: number;
758
+ }>, options?: {
759
+ continueOnError?: boolean;
760
+ }): Promise<{
761
+ results: Array<{
762
+ id: string;
763
+ success: boolean;
764
+ data?: unknown[];
765
+ error?: string;
766
+ }>;
767
+ summary: {
768
+ total: number;
769
+ succeeded: number;
770
+ failed: number;
771
+ tokensUsed: {
772
+ input: number;
773
+ output: number;
774
+ total: number;
775
+ };
776
+ };
777
+ }>;
530
778
  /**
531
779
  * Make authenticated API request
532
780
  */
@@ -656,4 +904,4 @@ declare function getKeyPrefix(secretKey: string): string;
656
904
  */
657
905
  declare function isValidSecretKey(secretKey: string): boolean;
658
906
 
659
- export { AIDirector, type AIDirectorConfig, AIDirectorError, AuthenticationError, ChainExecutionError, type ChainInfo, type ChainStep, ConfigurationError, type GenerateData, type GenerateError, type GenerateMeta, type GenerateOptions, type GenerateResult, type GenerationParameters, type HealthResult, type ModelInfo, NetworkError, RateLimitError, ServerError, type StreamCallbacks, type StreamProgress, TimeoutError, type TokenUsage, type UsageStats, ValidationError, generateSignature, getKeyPrefix, isAIDirectorError, isRetryableError, isValidSecretKey };
907
+ export { AIDirector, type AIDirectorConfig, AIDirectorError, AuthenticationError, ChainExecutionError, type ChainInfo, type ChainStep, ConfigurationError, type DetailedHealthResult, type FileAttachment, type GenerateData, type GenerateError, type GenerateMeta, type GenerateOptions, type GenerateResult, type GenerationParameters, type HealthResult, type ModelInfo, NetworkError, RateLimitError, ServerError, type StreamCallbacks, type StreamCompleteResult, type StreamProgress, TimeoutError, type TokenUsage, type UsageStats, ValidationError, type WebhookConfig, generateSignature, getKeyPrefix, isAIDirectorError, isRetryableError, isValidSecretKey };
package/dist/index.d.ts CHANGED
@@ -58,11 +58,64 @@ interface GenerateOptions {
58
58
  * Override the client timeout for this request
59
59
  */
60
60
  timeout?: number;
61
+ /**
62
+ * Skip the cache for this request.
63
+ * Useful for prompts that should always generate fresh responses.
64
+ * @default false
65
+ */
66
+ noCache?: boolean;
67
+ /**
68
+ * File attachments to include with the request.
69
+ * The server automatically handles:
70
+ * - File type detection (magic bytes)
71
+ * - Model compatibility checking
72
+ * - Automatic conversion (e.g., DOCX → PDF)
73
+ *
74
+ * @example
75
+ * ```typescript
76
+ * await client.generate({
77
+ * chainId: 'my-chain',
78
+ * prompt: 'Summarize this document',
79
+ * files: [{
80
+ * data: documentBuffer.toString('base64'),
81
+ * filename: 'report.pdf',
82
+ * mimeType: 'application/pdf',
83
+ * }],
84
+ * });
85
+ * ```
86
+ */
87
+ files?: FileAttachment[];
88
+ /**
89
+ * Use the optimized 3-step generation flow.
90
+ * This minimizes Vercel compute costs by calling the Cloudflare Worker directly.
91
+ * Set to false to force legacy single-call mode.
92
+ * @default true
93
+ */
94
+ useOptimized?: boolean;
61
95
  /**
62
96
  * Generation parameters
63
97
  */
64
98
  options?: GenerationParameters;
65
99
  }
100
+ /**
101
+ * File attachment for generate requests.
102
+ * Pass files and the server handles detection/conversion automatically.
103
+ */
104
+ interface FileAttachment {
105
+ /**
106
+ * File content as base64-encoded string
107
+ */
108
+ data: string;
109
+ /**
110
+ * Original filename (optional, helps with type detection)
111
+ */
112
+ filename?: string;
113
+ /**
114
+ * Claimed MIME type (optional, will be verified by server)
115
+ * If omitted, server will auto-detect from magic bytes
116
+ */
117
+ mimeType?: string;
118
+ }
66
119
  /**
67
120
  * AI generation parameters
68
121
  */
@@ -195,13 +248,22 @@ interface GenerateError {
195
248
  */
196
249
  interface StreamCallbacks {
197
250
  /**
198
- * Called for each text chunk received
251
+ * Called for each text chunk received (legacy/backwards compatible)
199
252
  */
200
253
  onChunk?: (chunk: string) => void;
201
254
  /**
202
- * Called when streaming is complete
255
+ * Called for each VALID JSON object parsed from stream
256
+ * This is the preferred callback for JSON streaming - each object is complete and parseable
203
257
  */
204
- onComplete?: (result: GenerateResult) => void;
258
+ onObject?: (object: unknown, index: number) => void;
259
+ /**
260
+ * Called when a JSON array starts in the stream
261
+ */
262
+ onArrayStart?: () => void;
263
+ /**
264
+ * Called when streaming is complete with full result
265
+ */
266
+ onComplete?: (result: StreamCompleteResult) => void;
205
267
  /**
206
268
  * Called if an error occurs
207
269
  */
@@ -211,6 +273,30 @@ interface StreamCallbacks {
211
273
  */
212
274
  onProgress?: (progress: StreamProgress) => void;
213
275
  }
276
+ /**
277
+ * Result passed to onComplete callback during streaming
278
+ */
279
+ interface StreamCompleteResult {
280
+ /**
281
+ * All collected objects from the stream
282
+ */
283
+ objects: unknown[];
284
+ /**
285
+ * Total number of objects parsed
286
+ */
287
+ objectCount: number;
288
+ /**
289
+ * Token usage for the request
290
+ */
291
+ tokensUsed?: {
292
+ input: number;
293
+ output: number;
294
+ };
295
+ /**
296
+ * Whether the response was cached at stream end
297
+ */
298
+ cached?: boolean;
299
+ }
214
300
  /**
215
301
  * Streaming progress information
216
302
  */
@@ -408,6 +494,89 @@ interface HealthResult {
408
494
  */
409
495
  timestamp?: string;
410
496
  }
497
+ /**
498
+ * Detailed health check result with component status
499
+ */
500
+ interface DetailedHealthResult {
501
+ /**
502
+ * Overall status
503
+ */
504
+ status: 'healthy' | 'degraded' | 'unhealthy';
505
+ /**
506
+ * Server timestamp
507
+ */
508
+ timestamp: string;
509
+ /**
510
+ * Server uptime in seconds
511
+ */
512
+ uptime: number;
513
+ /**
514
+ * Component status
515
+ */
516
+ components: {
517
+ database: {
518
+ status: string;
519
+ latencyMs: number;
520
+ message?: string;
521
+ };
522
+ redis: {
523
+ status: string;
524
+ latencyMs: number;
525
+ message?: string;
526
+ };
527
+ gemini: {
528
+ status: string;
529
+ errorRate: number;
530
+ recentErrors: number;
531
+ };
532
+ openrouter: {
533
+ status: string;
534
+ errorRate: number;
535
+ recentErrors: number;
536
+ };
537
+ };
538
+ /**
539
+ * 24h metrics
540
+ */
541
+ metrics: {
542
+ last24h: {
543
+ totalRequests: number;
544
+ successRate: number;
545
+ cacheHitRate: number;
546
+ avgLatencyMs: number;
547
+ };
548
+ errorsByCode: Record<string, number>;
549
+ };
550
+ /**
551
+ * Cache status
552
+ */
553
+ cache: {
554
+ totalEntries: number;
555
+ redisAvailable: boolean;
556
+ };
557
+ }
558
+ /**
559
+ * Webhook configuration for async notifications
560
+ */
561
+ interface WebhookConfig {
562
+ /**
563
+ * Request ID to receive notifications for
564
+ */
565
+ requestId: string;
566
+ /**
567
+ * URL to call when request completes
568
+ */
569
+ url: string;
570
+ /**
571
+ * Optional secret for HMAC signing
572
+ */
573
+ secret?: string;
574
+ /**
575
+ * Number of retry attempts on failure
576
+ * @default 3
577
+ */
578
+ retryCount?: number;
579
+ }
411
580
 
412
581
  /**
413
582
  * AI Director Client SDK
@@ -467,6 +636,24 @@ declare class AIDirector {
467
636
  * ```
468
637
  */
469
638
  generate(options: GenerateOptions): Promise<GenerateResult>;
639
+ /**
640
+ * Optimized 3-step generation (minimizes Vercel compute costs)
641
+ *
642
+ * Step 1: Get token from Vercel (~50ms)
643
+ * Step 2: Call CF Worker directly (FREE)
644
+ * Step 3: Cache result in Vercel (~50ms)
645
+ *
646
+ * Total Vercel time: ~100ms vs 30-120s in legacy mode
647
+ */
648
+ private generateOptimized;
649
+ /**
650
+ * Cache completion from worker (Step 3 - async, non-blocking)
651
+ */
652
+ private cacheCompletionAsync;
653
+ /**
654
+ * Legacy single-call generation (higher Vercel compute cost)
655
+ */
656
+ private generateLegacy;
470
657
  /**
471
658
  * Generate content with streaming (for long responses)
472
659
  *
@@ -527,6 +714,67 @@ declare class AIDirector {
527
714
  * @returns New AIDirector instance
528
715
  */
529
716
  withConfig(overrides: Partial<AIDirectorConfig>): AIDirector;
717
+ /**
718
+ * Register a webhook for async notifications
719
+ *
720
+ * @param config - Webhook configuration
721
+ * @returns Registration result
722
+ */
723
+ registerWebhook(config: {
724
+ requestId: string;
725
+ url: string;
726
+ secret?: string;
727
+ retryCount?: number;
728
+ }): Promise<{
729
+ registered: boolean;
730
+ message: string;
731
+ }>;
732
+ /**
733
+ * Get detailed health information including component status
734
+ *
735
+ * @returns Detailed health status
736
+ */
737
+ healthDetailed(): Promise<{
738
+ status: 'healthy' | 'degraded' | 'unhealthy';
739
+ timestamp: string;
740
+ uptime: number;
741
+ components: Record<string, unknown>;
742
+ metrics: Record<string, unknown>;
743
+ }>;
744
+ /**
745
+ * Generate content for multiple prompts in a single request
746
+ *
747
+ * @param chainId - Chain to use for all items
748
+ * @param items - Array of prompts with IDs
749
+ * @param options - Batch options
750
+ * @returns Batch generation results
751
+ */
752
+ generateBatch(chainId: string, items: Array<{
753
+ id: string;
754
+ prompt: string;
755
+ systemPrompt?: string;
756
+ temperature?: number;
757
+ maxTokens?: number;
758
+ }>, options?: {
759
+ continueOnError?: boolean;
760
+ }): Promise<{
761
+ results: Array<{
762
+ id: string;
763
+ success: boolean;
764
+ data?: unknown[];
765
+ error?: string;
766
+ }>;
767
+ summary: {
768
+ total: number;
769
+ succeeded: number;
770
+ failed: number;
771
+ tokensUsed: {
772
+ input: number;
773
+ output: number;
774
+ total: number;
775
+ };
776
+ };
777
+ }>;
530
778
  /**
531
779
  * Make authenticated API request
532
780
  */
@@ -656,4 +904,4 @@ declare function getKeyPrefix(secretKey: string): string;
656
904
  */
657
905
  declare function isValidSecretKey(secretKey: string): boolean;
658
906
 
659
- export { AIDirector, type AIDirectorConfig, AIDirectorError, AuthenticationError, ChainExecutionError, type ChainInfo, type ChainStep, ConfigurationError, type GenerateData, type GenerateError, type GenerateMeta, type GenerateOptions, type GenerateResult, type GenerationParameters, type HealthResult, type ModelInfo, NetworkError, RateLimitError, ServerError, type StreamCallbacks, type StreamProgress, TimeoutError, type TokenUsage, type UsageStats, ValidationError, generateSignature, getKeyPrefix, isAIDirectorError, isRetryableError, isValidSecretKey };
907
+ export { AIDirector, type AIDirectorConfig, AIDirectorError, AuthenticationError, ChainExecutionError, type ChainInfo, type ChainStep, ConfigurationError, type DetailedHealthResult, type FileAttachment, type GenerateData, type GenerateError, type GenerateMeta, type GenerateOptions, type GenerateResult, type GenerationParameters, type HealthResult, type ModelInfo, NetworkError, RateLimitError, ServerError, type StreamCallbacks, type StreamCompleteResult, type StreamProgress, TimeoutError, type TokenUsage, type UsageStats, ValidationError, type WebhookConfig, generateSignature, getKeyPrefix, isAIDirectorError, isRetryableError, isValidSecretKey };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- 'use strict';var M=typeof process<"u"&&process.versions?.node;async function P(s){let{createHash:e}=await import('crypto');return e("sha256").update(s).digest("hex")}async function O(s,e,t,r,a){let{createHmac:n}=await import('crypto'),o=[e.toUpperCase(),t,a.toString(),r].join(`
2
- `),u=await P(s);return n("sha256",u).update(o).digest("hex")}async function K(s){let t=new TextEncoder().encode(s),r=await crypto.subtle.digest("SHA-256",t);return Array.from(new Uint8Array(r)).map(n=>n.toString(16).padStart(2,"0")).join("")}async function N(s,e,t,r,a){let n=new TextEncoder,o=[e.toUpperCase(),t,a.toString(),r].join(`
3
- `),u=await K(s),l=n.encode(u),i=await crypto.subtle.importKey("raw",l,{name:"HMAC",hash:"SHA-256"},false,["sign"]),d=await crypto.subtle.sign("HMAC",i,n.encode(o));return Array.from(new Uint8Array(d)).map(A=>A.toString(16).padStart(2,"0")).join("")}async function x(s,e,t,r,a){return M?O(s,e,t,r,a):N(s,e,t,r,a)}function S(s){return s.slice(0,12)}function R(s){return typeof s=="string"&&s.startsWith("aid_sk_")&&s.length>=20}var c=class extends Error{constructor(e,t,r){super(e),this.name="AIDirectorError",this.code=t,this.retryable=r?.retryable??false,this.statusCode=r?.statusCode,this.originalError=r?.cause;}},g=class extends c{constructor(e){super(e,"CONFIGURATION_ERROR",{retryable:false}),this.name="ConfigurationError";}},f=class extends c{constructor(e,t){super(e,"AUTH_ERROR",{retryable:false,statusCode:t}),this.name="AuthenticationError";}},p=class extends c{constructor(e,t=6e4){super(e,"RATE_LIMITED",{retryable:true,statusCode:429}),this.name="RateLimitError",this.retryAfterMs=t;}},y=class extends c{constructor(e){super(`Request timed out after ${e}ms`,"TIMEOUT",{retryable:true}),this.name="TimeoutError",this.timeoutMs=e;}},m=class extends c{constructor(e,t){super(e,"NETWORK_ERROR",{retryable:true,cause:t}),this.name="NetworkError";}},w=class extends c{constructor(e,t=[]){super(e,"CHAIN_FAILED",{retryable:false}),this.name="ChainExecutionError",this.attemptedModels=t;}},T=class extends c{constructor(e,t=[]){super(e,"VALIDATION_ERROR",{retryable:false}),this.name="ValidationError",this.validationErrors=t;}},h=class extends c{constructor(e,t=500){super(e,"SERVER_ERROR",{retryable:true,statusCode:t}),this.name="ServerError";}};function v(s){return s instanceof c}function _(s){return v(s)?s.retryable:false}var j="http://localhost:3000",G=6e5,L=3,k=[1e3,2e3,5e3],I=class s{constructor(e){if(!e.secretKey)throw new g("secretKey is required");if(!R(e.secretKey))throw new g("Invalid secret key format. Expected format: aid_sk_<key>");this.secretKey=e.secretKey,this.baseUrl=(e.baseUrl||j).replace(/\/$/,""),this.timeout=e.timeout??G,this.maxRetries=e.maxRetries??L,this.keyPrefix=S(e.secretKey),this.debug=e.debug??false,this.debug&&this.log("Initialized",{baseUrl:this.baseUrl,timeout:this.timeout});}async generate(e){let t=Date.now(),r="/api/v1/generate",a=JSON.stringify({chainId:e.chainId,prompt:e.prompt,schema:e.schema,options:{...e.options,timeout:e.timeout??this.timeout}}),n=null;for(let l=0;l<=this.maxRetries;l++)try{let i=await this.makeAuthenticatedRequest(r,"POST",a,e.timeout),d=Date.now()-t;return this.debug&&this.log("generate success",{latencyMs:d,attempt:l+1}),i.meta&&(i.meta.latencyMs=d),i}catch(i){if(n=i instanceof Error?i:new Error(String(i)),this.debug&&this.log(`generate attempt ${l+1} failed`,{error:n.message}),!this.isRetryable(i)||l>=this.maxRetries)break;let d=k[Math.min(l,k.length-1)];i instanceof p&&i.retryAfterMs>d?await this.sleep(i.retryAfterMs):await this.sleep(d);}let o=Date.now()-t,u=n instanceof y;return {success:false,data:{valid:[],invalid:[]},meta:{cached:false,modelUsed:"",tokensUsed:{input:0,output:0},latencyMs:o,attemptedModels:[]},error:{code:u?"TIMEOUT":"REQUEST_FAILED",message:n?.message||"Request failed after all retries",retryable:false}}}async generateStream(e,t){let r="/api/v1/generate/stream",a=JSON.stringify({chainId:e.chainId,prompt:e.prompt,schema:e.schema,options:e.options});try{let n=Date.now(),o=await x(this.secretKey,"POST",r,a,n),u=await this.fetchWithTimeout(`${this.baseUrl}${r}`,{method:"POST",headers:{"Content-Type":"application/json",Accept:"text/event-stream","x-aidirector-signature":o,"x-aidirector-timestamp":n.toString(),"x-aidirector-key":this.keyPrefix},body:a},e.timeout??this.timeout);if(!u.ok){let E=await u.json();throw this.parseError(u.status,E)}let l=u.body?.getReader();if(!l)throw new m("Response body is not readable");let i=new TextDecoder,d="";for(;;){let{done:E,value:A}=await l.read();if(E)break;d+=i.decode(A,{stream:!0});let D=d.split(`
4
- `);d=D.pop()||"";for(let C of D)if(C.startsWith("data: ")){let U=C.slice(6);if(U==="[DONE]")break;try{let b=JSON.parse(U);b.chunk&&t.onChunk?.(b.chunk),b.complete&&t.onComplete&&t.onComplete(b.result);}catch{}}}}catch(n){if(t.onError)t.onError(n instanceof Error?n:new Error(String(n)));else throw n}}async listModels(){let t=await(await this.fetchWithTimeout(`${this.baseUrl}/api/v1/models`,{method:"GET",headers:{Accept:"application/json"}})).json();if(!t.success)throw new h(t.error?.message||"Failed to list models");return t.data}async listChains(){let t=await(await this.fetchWithTimeout(`${this.baseUrl}/api/v1/chains`,{method:"GET",credentials:"include",headers:{Accept:"application/json"}})).json();if(!t.success)throw new h(t.error?.message||"Failed to list chains");return t.data}async getUsage(e){let t=new URLSearchParams;e?.startDate&&t.set("startDate",e.startDate.toISOString()),e?.endDate&&t.set("endDate",e.endDate.toISOString());let r=`${this.baseUrl}/api/v1/usage${t.toString()?"?"+t:""}`,n=await(await this.fetchWithTimeout(r,{method:"GET",credentials:"include",headers:{Accept:"application/json"}})).json();if(!n.success)throw new h(n.error?.message||"Failed to get usage");return n.data}async health(){let e=Date.now();try{let t=await this.fetchWithTimeout(`${this.baseUrl}/api/health`,{method:"GET"},5e3),r=Date.now()-e,a=await t.json().catch(()=>({}));return {ok:t.ok,latencyMs:r,version:a.version}}catch{return {ok:false,latencyMs:Date.now()-e}}}withConfig(e){return new s({secretKey:this.secretKey,baseUrl:this.baseUrl,timeout:this.timeout,maxRetries:this.maxRetries,debug:this.debug,...e})}async makeAuthenticatedRequest(e,t,r,a){let n=Date.now(),o=await x(this.secretKey,t,e,r,n),u=await this.fetchWithTimeout(`${this.baseUrl}${e}`,{method:t,headers:{"Content-Type":"application/json",Accept:"application/json","x-aidirector-signature":o,"x-aidirector-timestamp":n.toString(),"x-aidirector-key":this.keyPrefix},body:r},a??this.timeout),l=await u.json();if(!u.ok)throw this.parseError(u.status,l);return l}async fetchWithTimeout(e,t,r=this.timeout){let a=new AbortController,n=setTimeout(()=>a.abort(),r);try{return await fetch(e,{...t,signal:a.signal})}catch(o){throw o instanceof Error&&o.name==="AbortError"?new y(r):o instanceof Error?new m(o.message,o):new m(String(o))}finally{clearTimeout(n);}}parseError(e,t){let r=t?.error?.message||`HTTP ${e}`,a=t?.error?.code||"UNKNOWN";switch(e){case 401:return new f(r,e);case 429:let n=t?.retryAfterMs||6e4;return new p(r,n);case 500:case 502:case 503:case 504:return new h(r,e);default:return a==="CHAIN_FAILED"?new w(r,t?.error?.attemptedModels||[]):new c(r,a,{statusCode:e,retryable:e>=500})}}isRetryable(e){if(e instanceof c)return e.retryable;if(e instanceof Error){let t=e.message.toLowerCase();return t.includes("network")||t.includes("timeout")||e.name==="AbortError"}return false}sleep(e){return new Promise(t=>setTimeout(t,e))}log(e,t){let r="[AIDirector]";t?console.log(r,e,t):console.log(r,e);}};exports.AIDirector=I;exports.AIDirectorError=c;exports.AuthenticationError=f;exports.ChainExecutionError=w;exports.ConfigurationError=g;exports.NetworkError=m;exports.RateLimitError=p;exports.ServerError=h;exports.TimeoutError=y;exports.ValidationError=T;exports.generateSignature=x;exports.getKeyPrefix=S;exports.isAIDirectorError=v;exports.isRetryableError=_;exports.isValidSecretKey=R;
1
+ 'use strict';var M=typeof process<"u"&&process.versions?.node;async function j(o){let{createHash:e}=await import('crypto');return e("sha256").update(o).digest("hex")}async function K(o,e,t,r,a){let{createHmac:s}=await import('crypto'),c=[e.toUpperCase(),t,a.toString(),r].join(`
2
+ `),u=await j(o);return s("sha256",u).update(c).digest("hex")}async function N(o){let t=new TextEncoder().encode(o),r=await crypto.subtle.digest("SHA-256",t);return Array.from(new Uint8Array(r)).map(s=>s.toString(16).padStart(2,"0")).join("")}async function G(o,e,t,r,a){let s=new TextEncoder,c=[e.toUpperCase(),t,a.toString(),r].join(`
3
+ `),u=await N(o),n=s.encode(u),i=await crypto.subtle.importKey("raw",n,{name:"HMAC",hash:"SHA-256"},false,["sign"]),m=await crypto.subtle.sign("HMAC",i,s.encode(c));return Array.from(new Uint8Array(m)).map(h=>h.toString(16).padStart(2,"0")).join("")}async function A(o,e,t,r,a){return M?K(o,e,t,r,a):G(o,e,t,r,a)}function T(o){return o.slice(0,12)}function x(o){return typeof o=="string"&&o.startsWith("aid_sk_")&&o.length>=20}var d=class extends Error{constructor(e,t,r){super(e),this.name="AIDirectorError",this.code=t,this.retryable=r?.retryable??false,this.statusCode=r?.statusCode,this.originalError=r?.cause;}},b=class extends d{constructor(e){super(e,"CONFIGURATION_ERROR",{retryable:false}),this.name="ConfigurationError";}},E=class extends d{constructor(e,t){super(e,"AUTH_ERROR",{retryable:false,statusCode:t}),this.name="AuthenticationError";}},w=class extends d{constructor(e,t=6e4){super(e,"RATE_LIMITED",{retryable:true,statusCode:429}),this.name="RateLimitError",this.retryAfterMs=t;}},k=class extends d{constructor(e){super(`Request timed out after ${e}ms`,"TIMEOUT",{retryable:true}),this.name="TimeoutError",this.timeoutMs=e;}},y=class extends d{constructor(e,t){super(e,"NETWORK_ERROR",{retryable:true,cause:t}),this.name="NetworkError";}},g=class extends d{constructor(e,t=[]){super(e,"CHAIN_FAILED",{retryable:false}),this.name="ChainExecutionError",this.attemptedModels=t;}},U=class extends d{constructor(e,t=[]){super(e,"VALIDATION_ERROR",{retryable:false}),this.name="ValidationError",this.validationErrors=t;}},p=class extends d{constructor(e,t=500){super(e,"SERVER_ERROR",{retryable:true,statusCode:t}),this.name="ServerError";}};function v(o){return o instanceof d}function _(o){return v(o)?o.retryable:false}var L="http://localhost:3000",W=6e5,H=3,O=[1e3,2e3,5e3],I=class o{constructor(e){if(!e.secretKey)throw new b("secretKey is required");if(!x(e.secretKey))throw new b("Invalid secret key format. Expected format: aid_sk_<key>");this.secretKey=e.secretKey,this.baseUrl=(e.baseUrl||L).replace(/\/$/,""),this.timeout=e.timeout??W,this.maxRetries=e.maxRetries??H,this.keyPrefix=T(e.secretKey),this.debug=e.debug??false,this.debug&&this.log("Initialized",{baseUrl:this.baseUrl,timeout:this.timeout});}async generate(e){if(e.useOptimized!==false)try{return await this.generateOptimized(e)}catch(r){this.debug&&this.log("3-step generation failed, falling back to legacy",{error:r instanceof Error?r.message:String(r)});}return this.generateLegacy(e)}async generateOptimized(e){let t=Date.now(),r="/api/v1/generate/token",a=JSON.stringify({chainId:e.chainId,prompt:e.prompt,systemPrompt:e.options?.systemPrompt,temperature:e.options?.temperature,maxTokens:e.options?.maxTokens,topP:e.options?.topP,topK:e.options?.topK,files:e.files}),s=await this.makeAuthenticatedRequest(r,"POST",a);if(s.cached&&s.data)return this.debug&&this.log("generate cache hit (optimized)",{latencyMs:Date.now()-t}),{success:true,data:s.data,meta:{cached:true,modelUsed:s.meta?.modelUsed||"",tokensUsed:{input:0,output:0},latencyMs:Date.now()-t,attemptedModels:[]}};if(!s.token||!s.workerUrl)throw new g("Token generation failed - no token or worker URL returned",[]);let c=JSON.stringify({token:s.token,prompt:e.prompt,systemPrompt:e.options?.systemPrompt,timeout:e.timeout??this.timeout}),n=await(await this.fetchWithTimeout(s.workerUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:c},e.timeout??this.timeout)).json();if(!n.success||!n.content)return {success:false,data:{valid:[],invalid:[]},meta:{cached:false,modelUsed:n.modelId||"",tokensUsed:n.tokensUsed||{input:0,output:0},latencyMs:Date.now()-t,attemptedModels:[n.modelId||""]},error:{code:n.errorCode||"GENERATION_FAILED",message:n.errorMessage||"Generation failed",retryable:false}};let i=[],m=[];try{let h=JSON.parse(n.content);Array.isArray(h)?i=h:i=[h];}catch{i=[n.content];}n.completionToken&&this.cacheCompletionAsync({completionToken:n.completionToken,content:n.content,tokensUsed:n.tokensUsed||{input:0,output:0},finishReason:n.finishReason||"stop",chainId:e.chainId,modelUsed:n.modelId,prompt:e.prompt,systemPrompt:e.options?.systemPrompt,temperature:e.options?.temperature,maxTokens:e.options?.maxTokens,topP:e.options?.topP,topK:e.options?.topK}).catch(h=>{this.debug&&this.log("cacheCompletion failed (non-blocking)",{error:h});});let f=Date.now()-t;return this.debug&&this.log("generate success (optimized 3-step)",{latencyMs:f,modelUsed:n.modelId,tokensUsed:n.tokensUsed}),{success:true,data:{valid:i,invalid:m},meta:{cached:false,modelUsed:n.modelId,tokensUsed:n.tokensUsed||{input:0,output:0},latencyMs:f,attemptedModels:[n.modelId]}}}async cacheCompletionAsync(e){let t="/api/v1/generate/complete",r=JSON.stringify(e);await this.makeAuthenticatedRequest(t,"POST",r);}async generateLegacy(e){let t=Date.now(),r="/api/v1/generate",a=JSON.stringify({chainId:e.chainId,prompt:e.prompt,schema:e.schema,noCache:e.noCache,files:e.files,options:{...e.options,timeout:e.timeout??this.timeout}}),s=null;for(let n=0;n<=this.maxRetries;n++)try{let i=await this.makeAuthenticatedRequest(r,"POST",a,e.timeout),m=Date.now()-t;return this.debug&&this.log("generate success (legacy)",{latencyMs:m,attempt:n+1}),i.meta&&(i.meta.latencyMs=m),i}catch(i){if(s=i instanceof Error?i:new Error(String(i)),this.debug&&this.log(`generate attempt ${n+1} failed`,{error:s.message}),!this.isRetryable(i)||n>=this.maxRetries)break;let m=O[Math.min(n,O.length-1)];i instanceof w&&i.retryAfterMs>m?await this.sleep(i.retryAfterMs):await this.sleep(m);}let c=Date.now()-t,u=s instanceof k;return {success:false,data:{valid:[],invalid:[]},meta:{cached:false,modelUsed:"",tokensUsed:{input:0,output:0},latencyMs:c,attemptedModels:[]},error:{code:u?"TIMEOUT":"REQUEST_FAILED",message:s?.message||"Request failed after all retries",retryable:false}}}async generateStream(e,t){let r="/api/v1/generate/stream",a=JSON.stringify({chainId:e.chainId,prompt:e.prompt,schema:e.schema,options:e.options});try{let s=Date.now(),c=await A(this.secretKey,"POST",r,a,s),u=await this.fetchWithTimeout(`${this.baseUrl}${r}`,{method:"POST",headers:{"Content-Type":"application/json",Accept:"text/event-stream","x-aidirector-signature":c,"x-aidirector-timestamp":s.toString(),"x-aidirector-key":this.keyPrefix},body:a},e.timeout??this.timeout);if(!u.ok){let h=await u.json();throw this.parseError(u.status,h)}let n=u.body?.getReader();if(!n)throw new y("Response body is not readable");let i=new TextDecoder,m="",f=[];for(;;){let{done:h,value:D}=await n.read();if(h)break;m+=i.decode(D,{stream:!0});let P=m.split(`
4
+ `);m=P.pop()||"";let S="";for(let R of P){if(R.startsWith("event: ")){S=R.slice(7).trim();continue}if(R.startsWith("data: ")){let C=R.slice(6);if(C==="[DONE]")break;try{let l=JSON.parse(C);switch(S){case "start":this.debug&&this.log("Stream started",l);break;case "object":l.object!==void 0&&(f.push(l.object),t.onObject?.(l.object,l.index)),t.onChunk&&l.object&&t.onChunk(JSON.stringify(l.object));break;case "array_start":t.onArrayStart?.();break;case "complete":t.onComplete&&t.onComplete({objects:f,objectCount:l.objectCount,tokensUsed:l.tokensUsed,cached:l.cached});break;case "error":t.onError&&t.onError(new g(l.error||"Stream error",["STREAM_ERROR"]));break;default:l.chunk&&t.onChunk?.(l.chunk);}}catch{}S="";}}}}catch(s){if(t.onError)t.onError(s instanceof Error?s:new Error(String(s)));else throw s}}async listModels(){let t=await(await this.fetchWithTimeout(`${this.baseUrl}/api/v1/models`,{method:"GET",headers:{Accept:"application/json"}})).json();if(!t.success)throw new p(t.error?.message||"Failed to list models");return t.data}async listChains(){let t=await(await this.fetchWithTimeout(`${this.baseUrl}/api/v1/chains`,{method:"GET",credentials:"include",headers:{Accept:"application/json"}})).json();if(!t.success)throw new p(t.error?.message||"Failed to list chains");return t.data}async getUsage(e){let t=new URLSearchParams;e?.startDate&&t.set("startDate",e.startDate.toISOString()),e?.endDate&&t.set("endDate",e.endDate.toISOString());let r=`${this.baseUrl}/api/v1/usage${t.toString()?"?"+t:""}`,s=await(await this.fetchWithTimeout(r,{method:"GET",credentials:"include",headers:{Accept:"application/json"}})).json();if(!s.success)throw new p(s.error?.message||"Failed to get usage");return s.data}async health(){let e=Date.now();try{let t=await this.fetchWithTimeout(`${this.baseUrl}/api/health`,{method:"GET"},5e3),r=Date.now()-e,a=await t.json().catch(()=>({}));return {ok:t.ok,latencyMs:r,version:a.version}}catch{return {ok:false,latencyMs:Date.now()-e}}}withConfig(e){return new o({secretKey:this.secretKey,baseUrl:this.baseUrl,timeout:this.timeout,maxRetries:this.maxRetries,debug:this.debug,...e})}async registerWebhook(e){let t=await this.makeAuthenticatedRequest("/api/v1/webhooks","POST",JSON.stringify(e));if(!t.success||!t.data)throw new p(t.error?.message||"Failed to register webhook");return t.data}async healthDetailed(){return (await fetch(`${this.baseUrl}/api/v1/health/detailed`,{headers:{Accept:"application/json"}})).json()}async generateBatch(e,t,r={}){let a=await this.makeAuthenticatedRequest("/api/v1/generate/batch","POST",JSON.stringify({chainId:e,items:t,continueOnError:r.continueOnError??true}));if(!a.success||!a.data)throw new g(a.error?.message||"Batch generation failed",[]);return a.data}async makeAuthenticatedRequest(e,t,r,a){let s=Date.now(),c=await A(this.secretKey,t,e,r,s),u=await this.fetchWithTimeout(`${this.baseUrl}${e}`,{method:t,headers:{"Content-Type":"application/json",Accept:"application/json","x-aidirector-signature":c,"x-aidirector-timestamp":s.toString(),"x-aidirector-key":this.keyPrefix},body:r},a??this.timeout),n=await u.json();if(!u.ok)throw this.parseError(u.status,n);return n}async fetchWithTimeout(e,t,r=this.timeout){let a=new AbortController,s=setTimeout(()=>a.abort(),r);try{return await fetch(e,{...t,signal:a.signal})}catch(c){throw c instanceof Error&&c.name==="AbortError"?new k(r):c instanceof Error?new y(c.message,c):new y(String(c))}finally{clearTimeout(s);}}parseError(e,t){let r=t?.error?.message||`HTTP ${e}`,a=t?.error?.code||"UNKNOWN";switch(e){case 401:return new E(r,e);case 429:let s=t?.retryAfterMs||6e4;return new w(r,s);case 500:case 502:case 503:case 504:return new p(r,e);default:return a==="CHAIN_FAILED"?new g(r,t?.error?.attemptedModels||[]):new d(r,a,{statusCode:e,retryable:e>=500})}}isRetryable(e){if(e instanceof d)return e.retryable;if(e instanceof Error){let t=e.message.toLowerCase();return t.includes("network")||t.includes("timeout")||e.name==="AbortError"}return false}sleep(e){return new Promise(t=>setTimeout(t,e))}log(e,t){let r="[AIDirector]";t?console.log(r,e,t):console.log(r,e);}};exports.AIDirector=I;exports.AIDirectorError=d;exports.AuthenticationError=E;exports.ChainExecutionError=g;exports.ConfigurationError=b;exports.NetworkError=y;exports.RateLimitError=w;exports.ServerError=p;exports.TimeoutError=k;exports.ValidationError=U;exports.generateSignature=A;exports.getKeyPrefix=T;exports.isAIDirectorError=v;exports.isRetryableError=_;exports.isValidSecretKey=x;
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- var M=typeof process<"u"&&process.versions?.node;async function P(s){let{createHash:e}=await import('crypto');return e("sha256").update(s).digest("hex")}async function O(s,e,t,r,a){let{createHmac:n}=await import('crypto'),o=[e.toUpperCase(),t,a.toString(),r].join(`
2
- `),u=await P(s);return n("sha256",u).update(o).digest("hex")}async function K(s){let t=new TextEncoder().encode(s),r=await crypto.subtle.digest("SHA-256",t);return Array.from(new Uint8Array(r)).map(n=>n.toString(16).padStart(2,"0")).join("")}async function N(s,e,t,r,a){let n=new TextEncoder,o=[e.toUpperCase(),t,a.toString(),r].join(`
3
- `),u=await K(s),l=n.encode(u),i=await crypto.subtle.importKey("raw",l,{name:"HMAC",hash:"SHA-256"},false,["sign"]),d=await crypto.subtle.sign("HMAC",i,n.encode(o));return Array.from(new Uint8Array(d)).map(A=>A.toString(16).padStart(2,"0")).join("")}async function x(s,e,t,r,a){return M?O(s,e,t,r,a):N(s,e,t,r,a)}function S(s){return s.slice(0,12)}function R(s){return typeof s=="string"&&s.startsWith("aid_sk_")&&s.length>=20}var c=class extends Error{constructor(e,t,r){super(e),this.name="AIDirectorError",this.code=t,this.retryable=r?.retryable??false,this.statusCode=r?.statusCode,this.originalError=r?.cause;}},g=class extends c{constructor(e){super(e,"CONFIGURATION_ERROR",{retryable:false}),this.name="ConfigurationError";}},f=class extends c{constructor(e,t){super(e,"AUTH_ERROR",{retryable:false,statusCode:t}),this.name="AuthenticationError";}},p=class extends c{constructor(e,t=6e4){super(e,"RATE_LIMITED",{retryable:true,statusCode:429}),this.name="RateLimitError",this.retryAfterMs=t;}},y=class extends c{constructor(e){super(`Request timed out after ${e}ms`,"TIMEOUT",{retryable:true}),this.name="TimeoutError",this.timeoutMs=e;}},m=class extends c{constructor(e,t){super(e,"NETWORK_ERROR",{retryable:true,cause:t}),this.name="NetworkError";}},w=class extends c{constructor(e,t=[]){super(e,"CHAIN_FAILED",{retryable:false}),this.name="ChainExecutionError",this.attemptedModels=t;}},T=class extends c{constructor(e,t=[]){super(e,"VALIDATION_ERROR",{retryable:false}),this.name="ValidationError",this.validationErrors=t;}},h=class extends c{constructor(e,t=500){super(e,"SERVER_ERROR",{retryable:true,statusCode:t}),this.name="ServerError";}};function v(s){return s instanceof c}function _(s){return v(s)?s.retryable:false}var j="http://localhost:3000",G=6e5,L=3,k=[1e3,2e3,5e3],I=class s{constructor(e){if(!e.secretKey)throw new g("secretKey is required");if(!R(e.secretKey))throw new g("Invalid secret key format. Expected format: aid_sk_<key>");this.secretKey=e.secretKey,this.baseUrl=(e.baseUrl||j).replace(/\/$/,""),this.timeout=e.timeout??G,this.maxRetries=e.maxRetries??L,this.keyPrefix=S(e.secretKey),this.debug=e.debug??false,this.debug&&this.log("Initialized",{baseUrl:this.baseUrl,timeout:this.timeout});}async generate(e){let t=Date.now(),r="/api/v1/generate",a=JSON.stringify({chainId:e.chainId,prompt:e.prompt,schema:e.schema,options:{...e.options,timeout:e.timeout??this.timeout}}),n=null;for(let l=0;l<=this.maxRetries;l++)try{let i=await this.makeAuthenticatedRequest(r,"POST",a,e.timeout),d=Date.now()-t;return this.debug&&this.log("generate success",{latencyMs:d,attempt:l+1}),i.meta&&(i.meta.latencyMs=d),i}catch(i){if(n=i instanceof Error?i:new Error(String(i)),this.debug&&this.log(`generate attempt ${l+1} failed`,{error:n.message}),!this.isRetryable(i)||l>=this.maxRetries)break;let d=k[Math.min(l,k.length-1)];i instanceof p&&i.retryAfterMs>d?await this.sleep(i.retryAfterMs):await this.sleep(d);}let o=Date.now()-t,u=n instanceof y;return {success:false,data:{valid:[],invalid:[]},meta:{cached:false,modelUsed:"",tokensUsed:{input:0,output:0},latencyMs:o,attemptedModels:[]},error:{code:u?"TIMEOUT":"REQUEST_FAILED",message:n?.message||"Request failed after all retries",retryable:false}}}async generateStream(e,t){let r="/api/v1/generate/stream",a=JSON.stringify({chainId:e.chainId,prompt:e.prompt,schema:e.schema,options:e.options});try{let n=Date.now(),o=await x(this.secretKey,"POST",r,a,n),u=await this.fetchWithTimeout(`${this.baseUrl}${r}`,{method:"POST",headers:{"Content-Type":"application/json",Accept:"text/event-stream","x-aidirector-signature":o,"x-aidirector-timestamp":n.toString(),"x-aidirector-key":this.keyPrefix},body:a},e.timeout??this.timeout);if(!u.ok){let E=await u.json();throw this.parseError(u.status,E)}let l=u.body?.getReader();if(!l)throw new m("Response body is not readable");let i=new TextDecoder,d="";for(;;){let{done:E,value:A}=await l.read();if(E)break;d+=i.decode(A,{stream:!0});let D=d.split(`
4
- `);d=D.pop()||"";for(let C of D)if(C.startsWith("data: ")){let U=C.slice(6);if(U==="[DONE]")break;try{let b=JSON.parse(U);b.chunk&&t.onChunk?.(b.chunk),b.complete&&t.onComplete&&t.onComplete(b.result);}catch{}}}}catch(n){if(t.onError)t.onError(n instanceof Error?n:new Error(String(n)));else throw n}}async listModels(){let t=await(await this.fetchWithTimeout(`${this.baseUrl}/api/v1/models`,{method:"GET",headers:{Accept:"application/json"}})).json();if(!t.success)throw new h(t.error?.message||"Failed to list models");return t.data}async listChains(){let t=await(await this.fetchWithTimeout(`${this.baseUrl}/api/v1/chains`,{method:"GET",credentials:"include",headers:{Accept:"application/json"}})).json();if(!t.success)throw new h(t.error?.message||"Failed to list chains");return t.data}async getUsage(e){let t=new URLSearchParams;e?.startDate&&t.set("startDate",e.startDate.toISOString()),e?.endDate&&t.set("endDate",e.endDate.toISOString());let r=`${this.baseUrl}/api/v1/usage${t.toString()?"?"+t:""}`,n=await(await this.fetchWithTimeout(r,{method:"GET",credentials:"include",headers:{Accept:"application/json"}})).json();if(!n.success)throw new h(n.error?.message||"Failed to get usage");return n.data}async health(){let e=Date.now();try{let t=await this.fetchWithTimeout(`${this.baseUrl}/api/health`,{method:"GET"},5e3),r=Date.now()-e,a=await t.json().catch(()=>({}));return {ok:t.ok,latencyMs:r,version:a.version}}catch{return {ok:false,latencyMs:Date.now()-e}}}withConfig(e){return new s({secretKey:this.secretKey,baseUrl:this.baseUrl,timeout:this.timeout,maxRetries:this.maxRetries,debug:this.debug,...e})}async makeAuthenticatedRequest(e,t,r,a){let n=Date.now(),o=await x(this.secretKey,t,e,r,n),u=await this.fetchWithTimeout(`${this.baseUrl}${e}`,{method:t,headers:{"Content-Type":"application/json",Accept:"application/json","x-aidirector-signature":o,"x-aidirector-timestamp":n.toString(),"x-aidirector-key":this.keyPrefix},body:r},a??this.timeout),l=await u.json();if(!u.ok)throw this.parseError(u.status,l);return l}async fetchWithTimeout(e,t,r=this.timeout){let a=new AbortController,n=setTimeout(()=>a.abort(),r);try{return await fetch(e,{...t,signal:a.signal})}catch(o){throw o instanceof Error&&o.name==="AbortError"?new y(r):o instanceof Error?new m(o.message,o):new m(String(o))}finally{clearTimeout(n);}}parseError(e,t){let r=t?.error?.message||`HTTP ${e}`,a=t?.error?.code||"UNKNOWN";switch(e){case 401:return new f(r,e);case 429:let n=t?.retryAfterMs||6e4;return new p(r,n);case 500:case 502:case 503:case 504:return new h(r,e);default:return a==="CHAIN_FAILED"?new w(r,t?.error?.attemptedModels||[]):new c(r,a,{statusCode:e,retryable:e>=500})}}isRetryable(e){if(e instanceof c)return e.retryable;if(e instanceof Error){let t=e.message.toLowerCase();return t.includes("network")||t.includes("timeout")||e.name==="AbortError"}return false}sleep(e){return new Promise(t=>setTimeout(t,e))}log(e,t){let r="[AIDirector]";t?console.log(r,e,t):console.log(r,e);}};export{I as AIDirector,c as AIDirectorError,f as AuthenticationError,w as ChainExecutionError,g as ConfigurationError,m as NetworkError,p as RateLimitError,h as ServerError,y as TimeoutError,T as ValidationError,x as generateSignature,S as getKeyPrefix,v as isAIDirectorError,_ as isRetryableError,R as isValidSecretKey};
1
+ var M=typeof process<"u"&&process.versions?.node;async function j(o){let{createHash:e}=await import('crypto');return e("sha256").update(o).digest("hex")}async function K(o,e,t,r,a){let{createHmac:s}=await import('crypto'),c=[e.toUpperCase(),t,a.toString(),r].join(`
2
+ `),u=await j(o);return s("sha256",u).update(c).digest("hex")}async function N(o){let t=new TextEncoder().encode(o),r=await crypto.subtle.digest("SHA-256",t);return Array.from(new Uint8Array(r)).map(s=>s.toString(16).padStart(2,"0")).join("")}async function G(o,e,t,r,a){let s=new TextEncoder,c=[e.toUpperCase(),t,a.toString(),r].join(`
3
+ `),u=await N(o),n=s.encode(u),i=await crypto.subtle.importKey("raw",n,{name:"HMAC",hash:"SHA-256"},false,["sign"]),m=await crypto.subtle.sign("HMAC",i,s.encode(c));return Array.from(new Uint8Array(m)).map(h=>h.toString(16).padStart(2,"0")).join("")}async function A(o,e,t,r,a){return M?K(o,e,t,r,a):G(o,e,t,r,a)}function T(o){return o.slice(0,12)}function x(o){return typeof o=="string"&&o.startsWith("aid_sk_")&&o.length>=20}var d=class extends Error{constructor(e,t,r){super(e),this.name="AIDirectorError",this.code=t,this.retryable=r?.retryable??false,this.statusCode=r?.statusCode,this.originalError=r?.cause;}},b=class extends d{constructor(e){super(e,"CONFIGURATION_ERROR",{retryable:false}),this.name="ConfigurationError";}},E=class extends d{constructor(e,t){super(e,"AUTH_ERROR",{retryable:false,statusCode:t}),this.name="AuthenticationError";}},w=class extends d{constructor(e,t=6e4){super(e,"RATE_LIMITED",{retryable:true,statusCode:429}),this.name="RateLimitError",this.retryAfterMs=t;}},k=class extends d{constructor(e){super(`Request timed out after ${e}ms`,"TIMEOUT",{retryable:true}),this.name="TimeoutError",this.timeoutMs=e;}},y=class extends d{constructor(e,t){super(e,"NETWORK_ERROR",{retryable:true,cause:t}),this.name="NetworkError";}},g=class extends d{constructor(e,t=[]){super(e,"CHAIN_FAILED",{retryable:false}),this.name="ChainExecutionError",this.attemptedModels=t;}},U=class extends d{constructor(e,t=[]){super(e,"VALIDATION_ERROR",{retryable:false}),this.name="ValidationError",this.validationErrors=t;}},p=class extends d{constructor(e,t=500){super(e,"SERVER_ERROR",{retryable:true,statusCode:t}),this.name="ServerError";}};function v(o){return o instanceof d}function _(o){return v(o)?o.retryable:false}var L="http://localhost:3000",W=6e5,H=3,O=[1e3,2e3,5e3],I=class o{constructor(e){if(!e.secretKey)throw new b("secretKey is required");if(!x(e.secretKey))throw new b("Invalid secret key format. Expected format: aid_sk_<key>");this.secretKey=e.secretKey,this.baseUrl=(e.baseUrl||L).replace(/\/$/,""),this.timeout=e.timeout??W,this.maxRetries=e.maxRetries??H,this.keyPrefix=T(e.secretKey),this.debug=e.debug??false,this.debug&&this.log("Initialized",{baseUrl:this.baseUrl,timeout:this.timeout});}async generate(e){if(e.useOptimized!==false)try{return await this.generateOptimized(e)}catch(r){this.debug&&this.log("3-step generation failed, falling back to legacy",{error:r instanceof Error?r.message:String(r)});}return this.generateLegacy(e)}async generateOptimized(e){let t=Date.now(),r="/api/v1/generate/token",a=JSON.stringify({chainId:e.chainId,prompt:e.prompt,systemPrompt:e.options?.systemPrompt,temperature:e.options?.temperature,maxTokens:e.options?.maxTokens,topP:e.options?.topP,topK:e.options?.topK,files:e.files}),s=await this.makeAuthenticatedRequest(r,"POST",a);if(s.cached&&s.data)return this.debug&&this.log("generate cache hit (optimized)",{latencyMs:Date.now()-t}),{success:true,data:s.data,meta:{cached:true,modelUsed:s.meta?.modelUsed||"",tokensUsed:{input:0,output:0},latencyMs:Date.now()-t,attemptedModels:[]}};if(!s.token||!s.workerUrl)throw new g("Token generation failed - no token or worker URL returned",[]);let c=JSON.stringify({token:s.token,prompt:e.prompt,systemPrompt:e.options?.systemPrompt,timeout:e.timeout??this.timeout}),n=await(await this.fetchWithTimeout(s.workerUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:c},e.timeout??this.timeout)).json();if(!n.success||!n.content)return {success:false,data:{valid:[],invalid:[]},meta:{cached:false,modelUsed:n.modelId||"",tokensUsed:n.tokensUsed||{input:0,output:0},latencyMs:Date.now()-t,attemptedModels:[n.modelId||""]},error:{code:n.errorCode||"GENERATION_FAILED",message:n.errorMessage||"Generation failed",retryable:false}};let i=[],m=[];try{let h=JSON.parse(n.content);Array.isArray(h)?i=h:i=[h];}catch{i=[n.content];}n.completionToken&&this.cacheCompletionAsync({completionToken:n.completionToken,content:n.content,tokensUsed:n.tokensUsed||{input:0,output:0},finishReason:n.finishReason||"stop",chainId:e.chainId,modelUsed:n.modelId,prompt:e.prompt,systemPrompt:e.options?.systemPrompt,temperature:e.options?.temperature,maxTokens:e.options?.maxTokens,topP:e.options?.topP,topK:e.options?.topK}).catch(h=>{this.debug&&this.log("cacheCompletion failed (non-blocking)",{error:h});});let f=Date.now()-t;return this.debug&&this.log("generate success (optimized 3-step)",{latencyMs:f,modelUsed:n.modelId,tokensUsed:n.tokensUsed}),{success:true,data:{valid:i,invalid:m},meta:{cached:false,modelUsed:n.modelId,tokensUsed:n.tokensUsed||{input:0,output:0},latencyMs:f,attemptedModels:[n.modelId]}}}async cacheCompletionAsync(e){let t="/api/v1/generate/complete",r=JSON.stringify(e);await this.makeAuthenticatedRequest(t,"POST",r);}async generateLegacy(e){let t=Date.now(),r="/api/v1/generate",a=JSON.stringify({chainId:e.chainId,prompt:e.prompt,schema:e.schema,noCache:e.noCache,files:e.files,options:{...e.options,timeout:e.timeout??this.timeout}}),s=null;for(let n=0;n<=this.maxRetries;n++)try{let i=await this.makeAuthenticatedRequest(r,"POST",a,e.timeout),m=Date.now()-t;return this.debug&&this.log("generate success (legacy)",{latencyMs:m,attempt:n+1}),i.meta&&(i.meta.latencyMs=m),i}catch(i){if(s=i instanceof Error?i:new Error(String(i)),this.debug&&this.log(`generate attempt ${n+1} failed`,{error:s.message}),!this.isRetryable(i)||n>=this.maxRetries)break;let m=O[Math.min(n,O.length-1)];i instanceof w&&i.retryAfterMs>m?await this.sleep(i.retryAfterMs):await this.sleep(m);}let c=Date.now()-t,u=s instanceof k;return {success:false,data:{valid:[],invalid:[]},meta:{cached:false,modelUsed:"",tokensUsed:{input:0,output:0},latencyMs:c,attemptedModels:[]},error:{code:u?"TIMEOUT":"REQUEST_FAILED",message:s?.message||"Request failed after all retries",retryable:false}}}async generateStream(e,t){let r="/api/v1/generate/stream",a=JSON.stringify({chainId:e.chainId,prompt:e.prompt,schema:e.schema,options:e.options});try{let s=Date.now(),c=await A(this.secretKey,"POST",r,a,s),u=await this.fetchWithTimeout(`${this.baseUrl}${r}`,{method:"POST",headers:{"Content-Type":"application/json",Accept:"text/event-stream","x-aidirector-signature":c,"x-aidirector-timestamp":s.toString(),"x-aidirector-key":this.keyPrefix},body:a},e.timeout??this.timeout);if(!u.ok){let h=await u.json();throw this.parseError(u.status,h)}let n=u.body?.getReader();if(!n)throw new y("Response body is not readable");let i=new TextDecoder,m="",f=[];for(;;){let{done:h,value:D}=await n.read();if(h)break;m+=i.decode(D,{stream:!0});let P=m.split(`
4
+ `);m=P.pop()||"";let S="";for(let R of P){if(R.startsWith("event: ")){S=R.slice(7).trim();continue}if(R.startsWith("data: ")){let C=R.slice(6);if(C==="[DONE]")break;try{let l=JSON.parse(C);switch(S){case "start":this.debug&&this.log("Stream started",l);break;case "object":l.object!==void 0&&(f.push(l.object),t.onObject?.(l.object,l.index)),t.onChunk&&l.object&&t.onChunk(JSON.stringify(l.object));break;case "array_start":t.onArrayStart?.();break;case "complete":t.onComplete&&t.onComplete({objects:f,objectCount:l.objectCount,tokensUsed:l.tokensUsed,cached:l.cached});break;case "error":t.onError&&t.onError(new g(l.error||"Stream error",["STREAM_ERROR"]));break;default:l.chunk&&t.onChunk?.(l.chunk);}}catch{}S="";}}}}catch(s){if(t.onError)t.onError(s instanceof Error?s:new Error(String(s)));else throw s}}async listModels(){let t=await(await this.fetchWithTimeout(`${this.baseUrl}/api/v1/models`,{method:"GET",headers:{Accept:"application/json"}})).json();if(!t.success)throw new p(t.error?.message||"Failed to list models");return t.data}async listChains(){let t=await(await this.fetchWithTimeout(`${this.baseUrl}/api/v1/chains`,{method:"GET",credentials:"include",headers:{Accept:"application/json"}})).json();if(!t.success)throw new p(t.error?.message||"Failed to list chains");return t.data}async getUsage(e){let t=new URLSearchParams;e?.startDate&&t.set("startDate",e.startDate.toISOString()),e?.endDate&&t.set("endDate",e.endDate.toISOString());let r=`${this.baseUrl}/api/v1/usage${t.toString()?"?"+t:""}`,s=await(await this.fetchWithTimeout(r,{method:"GET",credentials:"include",headers:{Accept:"application/json"}})).json();if(!s.success)throw new p(s.error?.message||"Failed to get usage");return s.data}async health(){let e=Date.now();try{let t=await this.fetchWithTimeout(`${this.baseUrl}/api/health`,{method:"GET"},5e3),r=Date.now()-e,a=await t.json().catch(()=>({}));return {ok:t.ok,latencyMs:r,version:a.version}}catch{return {ok:false,latencyMs:Date.now()-e}}}withConfig(e){return new o({secretKey:this.secretKey,baseUrl:this.baseUrl,timeout:this.timeout,maxRetries:this.maxRetries,debug:this.debug,...e})}async registerWebhook(e){let t=await this.makeAuthenticatedRequest("/api/v1/webhooks","POST",JSON.stringify(e));if(!t.success||!t.data)throw new p(t.error?.message||"Failed to register webhook");return t.data}async healthDetailed(){return (await fetch(`${this.baseUrl}/api/v1/health/detailed`,{headers:{Accept:"application/json"}})).json()}async generateBatch(e,t,r={}){let a=await this.makeAuthenticatedRequest("/api/v1/generate/batch","POST",JSON.stringify({chainId:e,items:t,continueOnError:r.continueOnError??true}));if(!a.success||!a.data)throw new g(a.error?.message||"Batch generation failed",[]);return a.data}async makeAuthenticatedRequest(e,t,r,a){let s=Date.now(),c=await A(this.secretKey,t,e,r,s),u=await this.fetchWithTimeout(`${this.baseUrl}${e}`,{method:t,headers:{"Content-Type":"application/json",Accept:"application/json","x-aidirector-signature":c,"x-aidirector-timestamp":s.toString(),"x-aidirector-key":this.keyPrefix},body:r},a??this.timeout),n=await u.json();if(!u.ok)throw this.parseError(u.status,n);return n}async fetchWithTimeout(e,t,r=this.timeout){let a=new AbortController,s=setTimeout(()=>a.abort(),r);try{return await fetch(e,{...t,signal:a.signal})}catch(c){throw c instanceof Error&&c.name==="AbortError"?new k(r):c instanceof Error?new y(c.message,c):new y(String(c))}finally{clearTimeout(s);}}parseError(e,t){let r=t?.error?.message||`HTTP ${e}`,a=t?.error?.code||"UNKNOWN";switch(e){case 401:return new E(r,e);case 429:let s=t?.retryAfterMs||6e4;return new w(r,s);case 500:case 502:case 503:case 504:return new p(r,e);default:return a==="CHAIN_FAILED"?new g(r,t?.error?.attemptedModels||[]):new d(r,a,{statusCode:e,retryable:e>=500})}}isRetryable(e){if(e instanceof d)return e.retryable;if(e instanceof Error){let t=e.message.toLowerCase();return t.includes("network")||t.includes("timeout")||e.name==="AbortError"}return false}sleep(e){return new Promise(t=>setTimeout(t,e))}log(e,t){let r="[AIDirector]";t?console.log(r,e,t):console.log(r,e);}};export{I as AIDirector,d as AIDirectorError,E as AuthenticationError,g as ChainExecutionError,b as ConfigurationError,y as NetworkError,w as RateLimitError,p as ServerError,k as TimeoutError,U as ValidationError,A as generateSignature,T as getKeyPrefix,v as isAIDirectorError,_ as isRetryableError,x as isValidSecretKey};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@altsafe/aidirector",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Official TypeScript SDK for AI Director - Intelligent AI API Gateway with automatic failover, caching, and JSON extraction",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",