ai-site-pilot 0.2.3 → 0.4.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
@@ -5,36 +5,17 @@
5
5
 
6
6
  AI chat widget that can **control and navigate your website**. Unlike typical chatbots that just answer questions, Site Pilot can take actions—scroll to sections, open modals, filter content, and more.
7
7
 
8
+ Works with any AI model (Gemini, GPT-4, Claude, Llama) via [OpenRouter](https://openrouter.ai).
9
+
8
10
  ## Features
9
11
 
10
12
  - 🎯 **Tool System** - Define custom actions the AI can take on your site
11
- - 🌊 **Streaming** - Real-time streaming responses with SSE
13
+ - 🌊 **Streaming** - Real-time streaming responses
14
+ - 🤖 **Any Model** - GPT-4, Claude, Gemini, Llama - just change one string
12
15
  - 🎤 **Speech** - Voice input and text-to-speech output
13
16
  - 🎨 **Themeable** - CSS variables for easy customization
14
17
  - 📱 **Responsive** - Works on all screen sizes
15
- - **Vercel AI SDK** - Works with any LLM provider
16
-
17
- ## How It Works
18
-
19
- The package provides the chat UI and streaming infrastructure. **You teach the AI about your specific site** through:
20
-
21
- 1. **System Prompt** - Tell the AI what sections, data, and features exist on your site
22
- 2. **Tool Definitions** - Define what actions the AI can take (filter, navigate, open modals)
23
- 3. **Client Handlers** - Write the code that actually executes those actions
24
-
25
- ```
26
- User: "Show me your mobile apps"
27
-
28
- AI understands request
29
-
30
- AI calls filter_by_category("Mobile") tool
31
-
32
- Your handler receives the call
33
-
34
- Your code filters the UI
35
- ```
36
-
37
- The AI doesn't automatically know your site structure—you teach it via the system prompt. See the [complete example](#teaching-the-ai-your-site) below.
18
+ - 🆓 **Free Tier** - Gemini 2.0 Flash is free on OpenRouter
38
19
 
39
20
  ## Installation
40
21
 
@@ -44,13 +25,16 @@ npm install ai-site-pilot
44
25
 
45
26
  ## Quick Start
46
27
 
47
- ### 1. Create the API Route
28
+ ### 1. Get an OpenRouter API Key
29
+
30
+ Sign up at [openrouter.ai](https://openrouter.ai) and get your API key.
31
+
32
+ ### 2. Create the API Route
48
33
 
49
34
  ```typescript
50
35
  // app/api/chat/route.ts
51
- import { createChatHandler } from 'ai-site-pilot/api';
36
+ import { createHandler } from 'ai-site-pilot/api';
52
37
  import { defineTool } from 'ai-site-pilot/tools';
53
- import { google } from '@ai-sdk/google';
54
38
 
55
39
  const navigateTool = defineTool({
56
40
  name: 'navigate',
@@ -68,15 +52,15 @@ const navigateTool = defineTool({
68
52
  },
69
53
  });
70
54
 
71
- export const POST = createChatHandler({
72
- model: google('gemini-2.0-flash'),
55
+ export const POST = createHandler({
56
+ model: 'google/gemini-2.0-flash-exp:free', // Free! Or use any model
73
57
  systemPrompt: `You are a helpful assistant for our website.
74
58
  You can navigate users to different sections using the navigate tool.`,
75
59
  tools: [navigateTool],
76
60
  });
77
61
  ```
78
62
 
79
- ### 2. Add the Component
63
+ ### 3. Add the Component
80
64
 
81
65
  ```tsx
82
66
  // app/layout.tsx or components/ChatWidget.tsx
@@ -105,72 +89,71 @@ export function ChatWidget() {
105
89
  }
106
90
  ```
107
91
 
108
- ## API Reference
92
+ ### 4. Add Environment Variable
109
93
 
110
- ### `<SitePilot />`
94
+ ```bash
95
+ # .env.local
96
+ OPENROUTER_API_KEY=sk-or-...
97
+ ```
111
98
 
112
- Main chat widget component.
99
+ That's it!
113
100
 
114
- | Prop | Type | Default | Description |
115
- |------|------|---------|-------------|
116
- | `apiEndpoint` | `string` | required | API endpoint for chat |
117
- | `theme` | `SitePilotTheme` | `{}` | Theme configuration |
118
- | `suggestions` | `Suggestion[]` | `[]` | Suggestion prompts |
119
- | `features` | `SitePilotFeatures` | `{}` | Feature toggles |
120
- | `onToolCall` | `(name, args) => void` | - | Tool call handler |
121
- | `defaultOpen` | `boolean` | `false` | Initial open state |
122
- | `placeholder` | `string` | `'Type a message...'` | Input placeholder |
123
- | `welcomeMessage` | `string` | `'Hi! I'm here to help...'` | Welcome message |
101
+ ## Available Models
124
102
 
125
- #### Theme Options
103
+ Change the `model` string to use any model:
126
104
 
127
- ```typescript
128
- interface SitePilotTheme {
129
- position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
130
- borderRadius?: number;
131
-
132
- // Colors - all accept CSS color values (hex, rgb, hsl)
133
- accentColor?: string; // Primary accent '#f59e0b'
134
- accentColorDark?: string; // Gradient end '#d97706'
135
- backgroundColor?: string; // Panel background '#0F0720'
136
- textColor?: string; // Primary text '#ffffff'
137
- textMutedColor?: string; // Secondary text '#a1a1aa'
138
- borderColor?: string; // Border 'rgba(255,255,255,0.1)'
139
- userMessageBg?: string; // User message bubble
140
- assistantMessageBg?: string; // Assistant message bubble
141
- }
142
- ```
105
+ | Model | ID | Notes |
106
+ |-------|-----|-------|
107
+ | Gemini 2.0 Flash | `google/gemini-2.0-flash-exp:free` | **Free!** |
108
+ | GPT-4o | `openai/gpt-4o` | Best overall |
109
+ | Claude 3.5 Sonnet | `anthropic/claude-3.5-sonnet` | Best for coding |
110
+ | Llama 3.1 70B | `meta-llama/llama-3.1-70b-instruct` | Open source |
143
111
 
144
- #### Feature Toggles
112
+ See all models at [openrouter.ai/models](https://openrouter.ai/models)
145
113
 
146
- ```typescript
147
- interface SitePilotFeatures {
148
- speech?: boolean; // Voice input (default: true)
149
- tts?: boolean; // Text-to-speech (default: true)
150
- fullscreen?: boolean; // Fullscreen mode (default: true)
151
- suggestions?: boolean; // Show suggestions (default: true)
152
- }
153
- ```
114
+ ## API Reference
154
115
 
155
- ### `createChatHandler()`
116
+ ### `createHandler()`
156
117
 
157
- Factory for creating Next.js API route handlers.
118
+ Creates a Next.js API route handler.
158
119
 
159
120
  ```typescript
160
- import { createChatHandler } from 'ai-site-pilot/api';
121
+ import { createHandler } from 'ai-site-pilot/api';
161
122
 
162
- export const POST = createChatHandler({
163
- model: google('gemini-2.0-flash'), // Any Vercel AI SDK model
123
+ export const POST = createHandler({
124
+ // Required
164
125
  systemPrompt: 'You are a helpful assistant...',
126
+
127
+ // Optional
128
+ apiKey: process.env.OPENROUTER_API_KEY, // Uses env var by default
129
+ model: 'google/gemini-2.0-flash-exp:free', // Default
165
130
  tools: [myTool1, myTool2],
166
131
  temperature: 0.7,
167
- maxTokens: 1000,
132
+ siteUrl: 'https://mysite.com', // Shown in OpenRouter dashboard
133
+ siteName: 'My Site',
168
134
  });
169
135
  ```
170
136
 
137
+ ### `<SitePilot />`
138
+
139
+ Main chat widget component.
140
+
141
+ ```tsx
142
+ <SitePilot
143
+ apiEndpoint="/api/chat"
144
+ suggestions={[{ text: 'Help me', icon: '❓' }]}
145
+ onToolCall={(name, args) => { /* handle tool calls */ }}
146
+ theme={{ accentColor: '#f59e0b' }}
147
+ features={{ speech: true, tts: true }}
148
+ welcomeMessage="Hi! How can I help?"
149
+ placeholder="Type a message..."
150
+ defaultOpen={false}
151
+ />
152
+ ```
153
+
171
154
  ### `defineTool()`
172
155
 
173
- Helper for defining tools with type safety.
156
+ Helper for defining tools.
174
157
 
175
158
  ```typescript
176
159
  import { defineTool } from 'ai-site-pilot/tools';
@@ -186,143 +169,64 @@ const searchTool = defineTool({
186
169
  },
187
170
  required: ['query'],
188
171
  },
189
- handler: async ({ query, category }) => {
190
- // Client-side handler (optional)
191
- const results = await searchProducts(query, category);
192
- displayResults(results);
193
- },
194
172
  });
195
173
  ```
196
174
 
197
- ### `useChat()`
175
+ ## Custom API Implementation
198
176
 
199
- Hook for custom chat implementations.
177
+ If you need to use a different AI provider, implement this SSE format:
200
178
 
201
- ```typescript
202
- import { useChat } from 'ai-site-pilot/hooks';
203
-
204
- function MyCustomChat() {
205
- const {
206
- messages,
207
- input,
208
- setInput,
209
- isLoading,
210
- sendMessage,
211
- clearMessages,
212
- } = useChat({
213
- apiEndpoint: '/api/chat',
214
- onToolCall: (name, args) => {
215
- // Handle tool calls
216
- },
217
- });
218
-
219
- return (
220
- // Your custom UI
221
- );
222
- }
223
179
  ```
224
-
225
- ## Styling
226
-
227
- ### CSS Variables
228
-
229
- Override these variables to customize the appearance:
230
-
231
- ```css
232
- .pilot-container {
233
- --pilot-accent-h: 38; /* Hue */
234
- --pilot-accent-s: 92%; /* Saturation */
235
- --pilot-accent-l: 50%; /* Lightness */
236
- --pilot-bg: #0F0720; /* Background */
237
- --pilot-text: #ffffff; /* Text color */
238
- --pilot-text-muted: #a1a1aa; /* Muted text */
239
- --pilot-border: rgba(255, 255, 255, 0.1);
240
- --pilot-radius: 24px;
241
- }
180
+ data: {"type":"text","content":"Hello, "}
181
+ data: {"type":"text","content":"how can I help?"}
182
+ data: {"type":"tool","name":"navigate","args":{"section":"products"}}
183
+ data: {"type":"done"}
242
184
  ```
243
185
 
244
- ### Tailwind Integration
186
+ Use the built-in SSE helpers:
245
187
 
246
- If using Tailwind, you can extend your config:
247
-
248
- ```javascript
249
- // tailwind.config.js
250
- module.exports = {
251
- theme: {
252
- extend: {
253
- colors: {
254
- pilot: {
255
- accent: 'hsl(var(--pilot-accent-h), var(--pilot-accent-s), var(--pilot-accent-l))',
256
- },
257
- },
258
- },
259
- },
260
- };
261
- ```
188
+ ```typescript
189
+ import { createSSEEncoder, getSSEHeaders } from 'ai-site-pilot/api';
262
190
 
263
- ## Use Cases
191
+ export async function POST(req: Request) {
192
+ const sse = createSSEEncoder();
264
193
 
265
- ### E-commerce
194
+ const stream = new ReadableStream({
195
+ async start(controller) {
196
+ controller.enqueue(sse.encodeText('Hello!'));
197
+ controller.enqueue(sse.encodeTool('navigate', { section: 'products' }));
198
+ controller.enqueue(sse.encodeDone());
199
+ controller.close();
200
+ },
201
+ });
266
202
 
267
- ```typescript
268
- const tools = [
269
- defineTool({
270
- name: 'search_products',
271
- description: 'Search product catalog',
272
- parameters: { /* ... */ },
273
- }),
274
- defineTool({
275
- name: 'add_to_cart',
276
- description: 'Add item to shopping cart',
277
- parameters: { /* ... */ },
278
- }),
279
- defineTool({
280
- name: 'show_category',
281
- description: 'Filter products by category',
282
- parameters: { /* ... */ },
283
- }),
284
- ];
203
+ return new Response(stream, { headers: getSSEHeaders() });
204
+ }
285
205
  ```
286
206
 
287
- ### Documentation Sites
207
+ ## Handling Tool-Only Responses
208
+
209
+ When the AI calls tools without text, customize the fallback:
288
210
 
289
211
  ```typescript
290
- const tools = [
291
- defineTool({
292
- name: 'search_docs',
293
- description: 'Search documentation',
294
- parameters: { /* ... */ },
295
- }),
296
- defineTool({
297
- name: 'navigate_to_page',
298
- description: 'Go to a documentation page',
299
- parameters: { /* ... */ },
300
- }),
301
- ];
302
- ```
212
+ import { SitePilot, createFallbackMessageGenerator } from 'ai-site-pilot';
303
213
 
304
- ### Portfolio Sites
214
+ const generateFallback = createFallbackMessageGenerator({
215
+ navigate: (args) => `Scrolled to **${args.section}**.`,
216
+ filter: (args) => `Showing **${args.category}** items.`,
217
+ });
305
218
 
306
- ```typescript
307
- const tools = [
308
- defineTool({
309
- name: 'open_project',
310
- description: 'Open project details modal',
311
- parameters: { /* ... */ },
312
- }),
313
- defineTool({
314
- name: 'filter_by_category',
315
- description: 'Filter projects by category',
316
- parameters: { /* ... */ },
317
- }),
318
- ];
219
+ <SitePilot
220
+ apiEndpoint="/api/chat"
221
+ generateFallbackMessage={generateFallback}
222
+ />
319
223
  ```
320
224
 
321
225
  ## Requirements
322
226
 
323
227
  - React 18+ or React 19
324
228
  - Next.js 13+ (for API routes)
325
- - A Vercel AI SDK compatible model
229
+ - OpenRouter API key (free at [openrouter.ai](https://openrouter.ai))
326
230
 
327
231
  ## License
328
232
 
@@ -1,56 +1,50 @@
1
- import { LanguageModel } from 'ai';
2
1
  import { T as ToolDefinition } from '../types--7jDyUM6.mjs';
3
2
  import { S as StreamEvent } from '../types-K00dDlBC.mjs';
4
3
 
5
4
  /**
6
- * Factory for creating Next.js API route handlers
5
+ * Universal chat handler using OpenRouter
6
+ * Works with any model: Gemini, GPT-4, Claude, Llama, etc.
7
+ * No SDK required - just standard fetch.
7
8
  */
8
9
 
9
- interface ChatHandlerConfig {
10
- /** The AI model to use (from Vercel AI SDK) */
11
- model: LanguageModel;
10
+ interface HandlerConfig {
11
+ /** OpenRouter API key (or set OPENROUTER_API_KEY env var) */
12
+ apiKey?: string;
13
+ /** Model to use (e.g., 'google/gemini-2.0-flash-exp:free', 'openai/gpt-4o', 'anthropic/claude-3.5-sonnet') */
14
+ model?: string;
12
15
  /** System prompt for the AI */
13
16
  systemPrompt: string;
14
17
  /** Tool definitions for the AI */
15
18
  tools?: ToolDefinition[];
16
19
  /** Temperature for response generation (0-1) */
17
20
  temperature?: number;
18
- /** Maximum tokens in response */
19
- maxTokens?: number;
21
+ /** Your site URL (shown in OpenRouter dashboard) */
22
+ siteUrl?: string;
23
+ /** Your app name (shown in OpenRouter dashboard) */
24
+ siteName?: string;
20
25
  }
21
26
  /**
22
- * Create a Next.js API route handler for chat
27
+ * Create a Next.js API route handler using OpenRouter
23
28
  *
24
- * Works with any Vercel AI SDK compatible model including:
25
- * - Google Gemini (@ai-sdk/google)
26
- * - OpenAI (@ai-sdk/openai)
27
- * - Anthropic (@ai-sdk/anthropic)
28
- * - And more...
29
+ * Works with any model - just change the model string:
30
+ * - 'google/gemini-2.0-flash-exp:free' (free!)
31
+ * - 'openai/gpt-4o'
32
+ * - 'anthropic/claude-3.5-sonnet'
33
+ * - 'meta-llama/llama-3.1-70b-instruct'
29
34
  *
30
35
  * @example
31
36
  * ```ts
32
37
  * // app/api/chat/route.ts
33
- * import { createChatHandler } from 'ai-site-pilot/api';
34
- * import { google } from '@ai-sdk/google';
38
+ * import { createHandler } from 'ai-site-pilot/api';
35
39
  *
36
- * export const POST = createChatHandler({
37
- * model: google('gemini-2.0-flash'),
40
+ * export const POST = createHandler({
41
+ * model: 'google/gemini-2.0-flash-exp:free',
38
42
  * systemPrompt: 'You are a helpful assistant...',
39
43
  * tools: myTools,
40
44
  * });
41
45
  * ```
42
- *
43
- * @example Using OpenAI
44
- * ```ts
45
- * import { openai } from '@ai-sdk/openai';
46
- *
47
- * export const POST = createChatHandler({
48
- * model: openai('gpt-4o'),
49
- * systemPrompt: 'You are a helpful assistant...',
50
- * });
51
- * ```
52
46
  */
53
- declare function createChatHandler(config: ChatHandlerConfig): (req: Request) => Promise<Response>;
47
+ declare function createHandler(config: HandlerConfig): (req: Request) => Promise<Response>;
54
48
 
55
49
  /**
56
50
  * SSE streaming utilities
@@ -75,4 +69,4 @@ declare function getSSEHeaders(): HeadersInit;
75
69
  */
76
70
  declare function parseSSEStream(reader: ReadableStreamDefaultReader<Uint8Array>): AsyncGenerator<StreamEvent>;
77
71
 
78
- export { type ChatHandlerConfig, createChatHandler, createSSEEncoder, getSSEHeaders, parseSSEStream };
72
+ export { type HandlerConfig, createHandler, createSSEEncoder, getSSEHeaders, parseSSEStream };
@@ -1,56 +1,50 @@
1
- import { LanguageModel } from 'ai';
2
1
  import { T as ToolDefinition } from '../types--7jDyUM6.js';
3
2
  import { S as StreamEvent } from '../types-K00dDlBC.js';
4
3
 
5
4
  /**
6
- * Factory for creating Next.js API route handlers
5
+ * Universal chat handler using OpenRouter
6
+ * Works with any model: Gemini, GPT-4, Claude, Llama, etc.
7
+ * No SDK required - just standard fetch.
7
8
  */
8
9
 
9
- interface ChatHandlerConfig {
10
- /** The AI model to use (from Vercel AI SDK) */
11
- model: LanguageModel;
10
+ interface HandlerConfig {
11
+ /** OpenRouter API key (or set OPENROUTER_API_KEY env var) */
12
+ apiKey?: string;
13
+ /** Model to use (e.g., 'google/gemini-2.0-flash-exp:free', 'openai/gpt-4o', 'anthropic/claude-3.5-sonnet') */
14
+ model?: string;
12
15
  /** System prompt for the AI */
13
16
  systemPrompt: string;
14
17
  /** Tool definitions for the AI */
15
18
  tools?: ToolDefinition[];
16
19
  /** Temperature for response generation (0-1) */
17
20
  temperature?: number;
18
- /** Maximum tokens in response */
19
- maxTokens?: number;
21
+ /** Your site URL (shown in OpenRouter dashboard) */
22
+ siteUrl?: string;
23
+ /** Your app name (shown in OpenRouter dashboard) */
24
+ siteName?: string;
20
25
  }
21
26
  /**
22
- * Create a Next.js API route handler for chat
27
+ * Create a Next.js API route handler using OpenRouter
23
28
  *
24
- * Works with any Vercel AI SDK compatible model including:
25
- * - Google Gemini (@ai-sdk/google)
26
- * - OpenAI (@ai-sdk/openai)
27
- * - Anthropic (@ai-sdk/anthropic)
28
- * - And more...
29
+ * Works with any model - just change the model string:
30
+ * - 'google/gemini-2.0-flash-exp:free' (free!)
31
+ * - 'openai/gpt-4o'
32
+ * - 'anthropic/claude-3.5-sonnet'
33
+ * - 'meta-llama/llama-3.1-70b-instruct'
29
34
  *
30
35
  * @example
31
36
  * ```ts
32
37
  * // app/api/chat/route.ts
33
- * import { createChatHandler } from 'ai-site-pilot/api';
34
- * import { google } from '@ai-sdk/google';
38
+ * import { createHandler } from 'ai-site-pilot/api';
35
39
  *
36
- * export const POST = createChatHandler({
37
- * model: google('gemini-2.0-flash'),
40
+ * export const POST = createHandler({
41
+ * model: 'google/gemini-2.0-flash-exp:free',
38
42
  * systemPrompt: 'You are a helpful assistant...',
39
43
  * tools: myTools,
40
44
  * });
41
45
  * ```
42
- *
43
- * @example Using OpenAI
44
- * ```ts
45
- * import { openai } from '@ai-sdk/openai';
46
- *
47
- * export const POST = createChatHandler({
48
- * model: openai('gpt-4o'),
49
- * systemPrompt: 'You are a helpful assistant...',
50
- * });
51
- * ```
52
46
  */
53
- declare function createChatHandler(config: ChatHandlerConfig): (req: Request) => Promise<Response>;
47
+ declare function createHandler(config: HandlerConfig): (req: Request) => Promise<Response>;
54
48
 
55
49
  /**
56
50
  * SSE streaming utilities
@@ -75,4 +69,4 @@ declare function getSSEHeaders(): HeadersInit;
75
69
  */
76
70
  declare function parseSSEStream(reader: ReadableStreamDefaultReader<Uint8Array>): AsyncGenerator<StreamEvent>;
77
71
 
78
- export { type ChatHandlerConfig, createChatHandler, createSSEEncoder, getSSEHeaders, parseSSEStream };
72
+ export { type HandlerConfig, createHandler, createSSEEncoder, getSSEHeaders, parseSSEStream };
package/dist/api/index.js CHANGED
@@ -1,9 +1,5 @@
1
1
  'use strict';
2
2
 
3
- var ai = require('ai');
4
-
5
- // src/api/createChatHandler.ts
6
-
7
3
  // src/api/streaming.ts
8
4
  function createSSEEncoder() {
9
5
  const encoder = new TextEncoder();
@@ -63,54 +59,125 @@ async function* parseSSEStream(reader) {
63
59
  }
64
60
  }
65
61
 
66
- // src/api/createChatHandler.ts
67
- function convertToolsToAISDK(tools) {
68
- const result = {};
69
- for (const tool of tools) {
70
- result[tool.name] = {
62
+ // src/api/createHandler.ts
63
+ function convertTools(tools) {
64
+ return tools.map((tool) => ({
65
+ type: "function",
66
+ function: {
67
+ name: tool.name,
71
68
  description: tool.description,
72
69
  parameters: {
73
70
  type: "object",
74
- properties: tool.parameters.properties,
71
+ properties: Object.fromEntries(
72
+ Object.entries(tool.parameters.properties).map(([key, value]) => [
73
+ key,
74
+ {
75
+ type: value.type,
76
+ description: value.description,
77
+ ...value.enum && { enum: value.enum }
78
+ }
79
+ ])
80
+ ),
75
81
  required: tool.parameters.required
76
82
  }
77
- };
78
- }
79
- return result;
83
+ }
84
+ }));
80
85
  }
81
- function createChatHandler(config) {
82
- const { model, systemPrompt, tools = [], temperature = 0.7, maxTokens } = config;
86
+ function createHandler(config) {
87
+ const {
88
+ apiKey = process.env.OPENROUTER_API_KEY,
89
+ model = "google/gemini-2.0-flash-exp:free",
90
+ systemPrompt,
91
+ tools = [],
92
+ temperature = 0.7,
93
+ siteUrl,
94
+ siteName
95
+ } = config;
83
96
  return async function POST(req) {
97
+ if (!apiKey) {
98
+ return new Response(
99
+ JSON.stringify({ error: "OpenRouter API key not configured. Get one at https://openrouter.ai" }),
100
+ { status: 500, headers: { "Content-Type": "application/json" } }
101
+ );
102
+ }
84
103
  try {
85
104
  const body = await req.json();
86
105
  const { messages } = body;
87
- const coreMessages = messages.map((m) => ({
88
- role: m.role,
89
- content: m.content
90
- }));
106
+ const openRouterMessages = [
107
+ { role: "system", content: systemPrompt },
108
+ ...messages.map((m) => ({
109
+ role: m.role === "assistant" ? "assistant" : "user",
110
+ content: m.content
111
+ }))
112
+ ];
91
113
  const sse = createSSEEncoder();
92
114
  const stream = new ReadableStream({
93
115
  async start(controller) {
94
116
  try {
95
- const result = ai.streamText({
96
- model,
97
- system: systemPrompt,
98
- messages: coreMessages,
99
- temperature,
100
- maxTokens,
101
- tools: tools.length > 0 ? convertToolsToAISDK(tools) : void 0
117
+ const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
118
+ method: "POST",
119
+ headers: {
120
+ "Authorization": `Bearer ${apiKey}`,
121
+ "Content-Type": "application/json",
122
+ "HTTP-Referer": siteUrl || "",
123
+ "X-Title": siteName || ""
124
+ },
125
+ body: JSON.stringify({
126
+ model,
127
+ messages: openRouterMessages,
128
+ temperature,
129
+ stream: true,
130
+ ...tools.length > 0 && { tools: convertTools(tools) }
131
+ })
102
132
  });
103
- for await (const chunk of (await result).textStream) {
104
- if (chunk) {
105
- controller.enqueue(sse.encodeText(chunk));
106
- }
133
+ if (!response.ok) {
134
+ const error = await response.text();
135
+ console.error("OpenRouter error:", error);
136
+ controller.enqueue(sse.encodeError("Failed to get AI response"));
137
+ controller.enqueue(sse.encodeDone());
138
+ controller.close();
139
+ return;
140
+ }
141
+ const reader = response.body?.getReader();
142
+ if (!reader) {
143
+ controller.enqueue(sse.encodeError("No response stream"));
144
+ controller.enqueue(sse.encodeDone());
145
+ controller.close();
146
+ return;
107
147
  }
108
- const finalResult = await result;
109
- const toolCalls = await finalResult.toolCalls || [];
110
- for (const toolCall of toolCalls) {
111
- controller.enqueue(
112
- sse.encodeTool(toolCall.toolName, toolCall.args)
113
- );
148
+ const decoder = new TextDecoder();
149
+ let buffer = "";
150
+ while (true) {
151
+ const { done, value } = await reader.read();
152
+ if (done) break;
153
+ buffer += decoder.decode(value, { stream: true });
154
+ const lines = buffer.split("\n");
155
+ buffer = lines.pop() || "";
156
+ for (const line of lines) {
157
+ if (line.startsWith("data: ")) {
158
+ const data = line.slice(6);
159
+ if (data === "[DONE]") continue;
160
+ try {
161
+ const parsed = JSON.parse(data);
162
+ const delta = parsed.choices?.[0]?.delta;
163
+ if (delta?.content) {
164
+ controller.enqueue(sse.encodeText(delta.content));
165
+ }
166
+ if (delta?.tool_calls) {
167
+ for (const toolCall of delta.tool_calls) {
168
+ if (toolCall.function?.name && toolCall.function?.arguments) {
169
+ try {
170
+ const args = JSON.parse(toolCall.function.arguments);
171
+ controller.enqueue(sse.encodeTool(toolCall.function.name, args));
172
+ } catch {
173
+ }
174
+ }
175
+ }
176
+ }
177
+ } catch {
178
+ }
179
+ }
180
+ }
114
181
  }
115
182
  controller.enqueue(sse.encodeDone());
116
183
  controller.close();
@@ -121,20 +188,18 @@ function createChatHandler(config) {
121
188
  }
122
189
  }
123
190
  });
124
- return new Response(stream, {
125
- headers: getSSEHeaders()
126
- });
191
+ return new Response(stream, { headers: getSSEHeaders() });
127
192
  } catch (error) {
128
- console.error("Chat API error:", error);
129
- return new Response(JSON.stringify({ error: "Internal server error" }), {
130
- status: 500,
131
- headers: { "Content-Type": "application/json" }
132
- });
193
+ console.error("Handler error:", error);
194
+ return new Response(
195
+ JSON.stringify({ error: "Internal server error" }),
196
+ { status: 500, headers: { "Content-Type": "application/json" } }
197
+ );
133
198
  }
134
199
  };
135
200
  }
136
201
 
137
- exports.createChatHandler = createChatHandler;
202
+ exports.createHandler = createHandler;
138
203
  exports.createSSEEncoder = createSSEEncoder;
139
204
  exports.getSSEHeaders = getSSEHeaders;
140
205
  exports.parseSSEStream = parseSSEStream;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/api/streaming.ts","../../src/api/createChatHandler.ts"],"names":["streamText"],"mappings":";;;;;;;AASO,SAAS,gBAAA,GAAmB;AACjC,EAAA,MAAM,OAAA,GAAU,IAAI,WAAA,EAAY;AAEhC,EAAA,OAAO;AAAA,IACL,OAAO,KAAA,EAAgC;AACrC,MAAA,OAAO,QAAQ,MAAA,CAAO,CAAA,MAAA,EAAS,IAAA,CAAK,SAAA,CAAU,KAAK,CAAC;;AAAA,CAAM,CAAA;AAAA,IAC5D,CAAA;AAAA,IAEA,WAAW,OAAA,EAA6B;AACtC,MAAA,OAAO,OAAA,CAAQ,MAAA,CAAO,CAAA,MAAA,EAAS,IAAA,CAAK,SAAA,CAAU,EAAE,IAAA,EAAM,MAAA,EAAQ,OAAA,EAAS,CAAC;;AAAA,CAAM,CAAA;AAAA,IAChF,CAAA;AAAA,IAEA,UAAA,CAAW,MAAc,IAAA,EAA2C;AAClE,MAAA,OAAO,OAAA,CAAQ,MAAA,CAAO,CAAA,MAAA,EAAS,IAAA,CAAK,SAAA,CAAU,EAAE,IAAA,EAAM,MAAA,EAAQ,IAAA,EAAM,IAAA,EAAM,CAAC;;AAAA,CAAM,CAAA;AAAA,IACnF,CAAA;AAAA,IAEA,UAAA,GAAyB;AACvB,MAAA,OAAO,OAAA,CAAQ,OAAO,CAAA,MAAA,EAAS,IAAA,CAAK,UAAU,EAAE,IAAA,EAAM,MAAA,EAAQ,CAAC;;AAAA,CAAM,CAAA;AAAA,IACvE,CAAA;AAAA,IAEA,YAAY,OAAA,EAA6B;AACvC,MAAA,OAAO,OAAA,CAAQ,MAAA,CAAO,CAAA,MAAA,EAAS,IAAA,CAAK,SAAA,CAAU,EAAE,IAAA,EAAM,OAAA,EAAS,OAAA,EAAS,CAAC;;AAAA,CAAM,CAAA;AAAA,IACjF;AAAA,GACF;AACF;AAKO,SAAS,aAAA,GAA6B;AAC3C,EAAA,OAAO;AAAA,IACL,cAAA,EAAgB,mBAAA;AAAA,IAChB,eAAA,EAAiB,UAAA;AAAA,IACjB,YAAA,EAAc;AAAA,GAChB;AACF;AAKA,gBAAuB,eACrB,MAAA,EAC6B;AAC7B,EAAA,MAAM,OAAA,GAAU,IAAI,WAAA,EAAY;AAChC,EAAA,IAAI,MAAA,GAAS,EAAA;AAEb,EAAA,OAAO,IAAA,EAAM;AACX,IAAA,MAAM,EAAE,IAAA,EAAM,KAAA,EAAM,GAAI,MAAM,OAAO,IAAA,EAAK;AAC1C,IAAA,IAAI,IAAA,EAAM;AAEV,IAAA,MAAA,IAAU,QAAQ,MAAA,CAAO,KAAA,EAAO,EAAE,MAAA,EAAQ,MAAM,CAAA;AAChD,IAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,KAAA,CAAM,IAAI,CAAA;AAC/B,IAAA,MAAA,GAAS,KAAA,CAAM,KAAI,IAAK,EAAA;AAExB,IAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,MAAA,IAAI,IAAA,CAAK,UAAA,CAAW,QAAQ,CAAA,EAAG;AAC7B,QAAA,IAAI;AACF,UAAA,MAAM,OAAO,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,KAAA,CAAM,CAAC,CAAC,CAAA;AACrC,UAAA,MAAM,IAAA;AAAA,QACR,CAAA,CAAA,MAAQ;AAAA,QAER;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;;;AC9CA,SAAS,oBAAoB,KAAA,EAAyB;AACpD,EAAA,MAAM,SAAuE,EAAC;AAE9E,EAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,IAAA,MAAA,CAAO,IAAA,CAAK,IAAI,CAAA,GAAI;AAAA,MAClB,aAAa,IAAA,CAAK,WAAA;AAAA,MAClB,UAAA,EAAY;AAAA,QACV,IAAA,EAAM,QAAA;AAAA,QACN,UAAA,EAAY,KAAK,UAAA,CAAW,UAAA;AAAA,QAC5B,QAAA,EAAU,KAAK,UAAA,CAAW;AAAA;AAC5B,KACF;AAAA,EACF;AAEA,EAAA,OAAO,MAAA;AACT;AAkCO,SAAS,kBAAkB,MAAA,EAA2B;AAC3D,EAAA,MAAM,EAAE,OAAO,YAAA,EAAc,KAAA,GAAQ,EAAC,EAAG,WAAA,GAAc,GAAA,EAAK,SAAA,EAAU,GAAI,MAAA;AAE1E,EAAA,OAAO,eAAe,KAAK,GAAA,EAAiC;AAC1D,IAAA,IAAI;AACF,MAAA,MAAM,IAAA,GAAQ,MAAM,GAAA,CAAI,IAAA,EAAK;AAC7B,MAAA,MAAM,EAAE,UAAS,GAAI,IAAA;AAGrB,MAAA,MAAM,YAAA,GAA8B,QAAA,CAAS,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,QACvD,MAAM,CAAA,CAAE,IAAA;AAAA,QACR,SAAS,CAAA,CAAE;AAAA,OACb,CAAE,CAAA;AAGF,MAAA,MAAM,MAAM,gBAAA,EAAiB;AAG7B,MAAA,MAAM,MAAA,GAAS,IAAI,cAAA,CAAe;AAAA,QAChC,MAAM,MAAM,UAAA,EAAY;AACtB,UAAA,IAAI;AACF,YAAA,MAAM,SAASA,aAAA,CAAW;AAAA,cACxB,KAAA;AAAA,cACA,MAAA,EAAQ,YAAA;AAAA,cACR,QAAA,EAAU,YAAA;AAAA,cACV,WAAA;AAAA,cACA,SAAA;AAAA,cACA,OAAO,KAAA,CAAM,MAAA,GAAS,CAAA,GAAI,mBAAA,CAAoB,KAAK,CAAA,GAAI,KAAA;AAAA,aACxD,CAAA;AAGD,YAAA,WAAA,MAAiB,KAAA,IAAA,CAAU,MAAM,MAAA,EAAQ,UAAA,EAAY;AACnD,cAAA,IAAI,KAAA,EAAO;AACT,gBAAA,UAAA,CAAW,OAAA,CAAQ,GAAA,CAAI,UAAA,CAAW,KAAK,CAAC,CAAA;AAAA,cAC1C;AAAA,YACF;AAGA,YAAA,MAAM,cAAc,MAAM,MAAA;AAC1B,YAAA,MAAM,SAAA,GAAa,MAAM,WAAA,CAAY,SAAA,IAAc,EAAC;AAEpD,YAAA,KAAA,MAAW,YAAY,SAAA,EAAW;AAChC,cAAA,UAAA,CAAW,OAAA;AAAA,gBACT,GAAA,CAAI,UAAA,CAAW,QAAA,CAAS,QAAA,EAAU,SAAS,IAA+B;AAAA,eAC5E;AAAA,YACF;AAEA,YAAA,UAAA,CAAW,OAAA,CAAQ,GAAA,CAAI,UAAA,EAAY,CAAA;AACnC,YAAA,UAAA,CAAW,KAAA,EAAM;AAAA,UACnB,SAAS,KAAA,EAAO;AACd,YAAA,OAAA,CAAQ,KAAA,CAAM,oBAAoB,KAAK,CAAA;AACvC,YAAA,UAAA,CAAW,OAAA,CAAQ,GAAA,CAAI,WAAA,CAAY,oCAAoC,CAAC,CAAA;AACxE,YAAA,UAAA,CAAW,KAAA,EAAM;AAAA,UACnB;AAAA,QACF;AAAA,OACD,CAAA;AAED,MAAA,OAAO,IAAI,SAAS,MAAA,EAAQ;AAAA,QAC1B,SAAS,aAAA;AAAc,OACxB,CAAA;AAAA,IACH,SAAS,KAAA,EAAO;AACd,MAAA,OAAA,CAAQ,KAAA,CAAM,mBAAmB,KAAK,CAAA;AACtC,MAAA,OAAO,IAAI,SAAS,IAAA,CAAK,SAAA,CAAU,EAAE,KAAA,EAAO,uBAAA,EAAyB,CAAA,EAAG;AAAA,QACtE,MAAA,EAAQ,GAAA;AAAA,QACR,OAAA,EAAS,EAAE,cAAA,EAAgB,kBAAA;AAAmB,OAC/C,CAAA;AAAA,IACH;AAAA,EACF,CAAA;AACF","file":"index.js","sourcesContent":["/**\n * SSE streaming utilities\n */\n\nimport type { StreamEvent } from '../types';\n\n/**\n * Create an SSE encoder for streaming responses\n */\nexport function createSSEEncoder() {\n const encoder = new TextEncoder();\n\n return {\n encode(event: StreamEvent): Uint8Array {\n return encoder.encode(`data: ${JSON.stringify(event)}\\n\\n`);\n },\n\n encodeText(content: string): Uint8Array {\n return encoder.encode(`data: ${JSON.stringify({ type: 'text', content })}\\n\\n`);\n },\n\n encodeTool(name: string, args: Record<string, unknown>): Uint8Array {\n return encoder.encode(`data: ${JSON.stringify({ type: 'tool', name, args })}\\n\\n`);\n },\n\n encodeDone(): Uint8Array {\n return encoder.encode(`data: ${JSON.stringify({ type: 'done' })}\\n\\n`);\n },\n\n encodeError(message: string): Uint8Array {\n return encoder.encode(`data: ${JSON.stringify({ type: 'error', message })}\\n\\n`);\n },\n };\n}\n\n/**\n * Create SSE response headers\n */\nexport function getSSEHeaders(): HeadersInit {\n return {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n };\n}\n\n/**\n * Parse SSE events from a ReadableStream\n */\nexport async function* parseSSEStream(\n reader: ReadableStreamDefaultReader<Uint8Array>\n): AsyncGenerator<StreamEvent> {\n const decoder = new TextDecoder();\n let buffer = '';\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() || '';\n\n for (const line of lines) {\n if (line.startsWith('data: ')) {\n try {\n const data = JSON.parse(line.slice(6)) as StreamEvent;\n yield data;\n } catch {\n // Skip malformed JSON\n }\n }\n }\n }\n}\n","/**\n * Factory for creating Next.js API route handlers\n */\n\nimport { streamText, type CoreMessage, type LanguageModel } from 'ai';\nimport type { ToolDefinition } from '../tools/types';\nimport { createSSEEncoder, getSSEHeaders } from './streaming';\n\nexport interface ChatHandlerConfig {\n /** The AI model to use (from Vercel AI SDK) */\n model: LanguageModel;\n /** System prompt for the AI */\n systemPrompt: string;\n /** Tool definitions for the AI */\n tools?: ToolDefinition[];\n /** Temperature for response generation (0-1) */\n temperature?: number;\n /** Maximum tokens in response */\n maxTokens?: number;\n}\n\ninterface RequestBody {\n messages: Array<{ role: 'user' | 'assistant'; content: string }>;\n}\n\n/**\n * Convert tool definitions to Vercel AI SDK format\n */\nfunction convertToolsToAISDK(tools: ToolDefinition[]) {\n const result: Record<string, { description: string; parameters: unknown }> = {};\n\n for (const tool of tools) {\n result[tool.name] = {\n description: tool.description,\n parameters: {\n type: 'object',\n properties: tool.parameters.properties,\n required: tool.parameters.required,\n },\n };\n }\n\n return result;\n}\n\n/**\n * Create a Next.js API route handler for chat\n *\n * Works with any Vercel AI SDK compatible model including:\n * - Google Gemini (@ai-sdk/google)\n * - OpenAI (@ai-sdk/openai)\n * - Anthropic (@ai-sdk/anthropic)\n * - And more...\n *\n * @example\n * ```ts\n * // app/api/chat/route.ts\n * import { createChatHandler } from 'ai-site-pilot/api';\n * import { google } from '@ai-sdk/google';\n *\n * export const POST = createChatHandler({\n * model: google('gemini-2.0-flash'),\n * systemPrompt: 'You are a helpful assistant...',\n * tools: myTools,\n * });\n * ```\n *\n * @example Using OpenAI\n * ```ts\n * import { openai } from '@ai-sdk/openai';\n *\n * export const POST = createChatHandler({\n * model: openai('gpt-4o'),\n * systemPrompt: 'You are a helpful assistant...',\n * });\n * ```\n */\nexport function createChatHandler(config: ChatHandlerConfig) {\n const { model, systemPrompt, tools = [], temperature = 0.7, maxTokens } = config;\n\n return async function POST(req: Request): Promise<Response> {\n try {\n const body = (await req.json()) as RequestBody;\n const { messages } = body;\n\n // Convert messages to CoreMessage format\n const coreMessages: CoreMessage[] = messages.map((m) => ({\n role: m.role,\n content: m.content,\n }));\n\n // Create the SSE encoder\n const sse = createSSEEncoder();\n\n // Create a readable stream for SSE\n const stream = new ReadableStream({\n async start(controller) {\n try {\n const result = streamText({\n model,\n system: systemPrompt,\n messages: coreMessages,\n temperature,\n maxTokens,\n tools: tools.length > 0 ? convertToolsToAISDK(tools) : undefined,\n });\n\n // Stream text chunks\n for await (const chunk of (await result).textStream) {\n if (chunk) {\n controller.enqueue(sse.encodeText(chunk));\n }\n }\n\n // Get tool calls from the result\n const finalResult = await result;\n const toolCalls = (await finalResult.toolCalls) || [];\n\n for (const toolCall of toolCalls) {\n controller.enqueue(\n sse.encodeTool(toolCall.toolName, toolCall.args as Record<string, unknown>)\n );\n }\n\n controller.enqueue(sse.encodeDone());\n controller.close();\n } catch (error) {\n console.error('Streaming error:', error);\n controller.enqueue(sse.encodeError('An error occurred during streaming'));\n controller.close();\n }\n },\n });\n\n return new Response(stream, {\n headers: getSSEHeaders(),\n });\n } catch (error) {\n console.error('Chat API error:', error);\n return new Response(JSON.stringify({ error: 'Internal server error' }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n });\n }\n };\n}\n"]}
1
+ {"version":3,"sources":["../../src/api/streaming.ts","../../src/api/createHandler.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * SSE streaming utilities\n */\n\nimport type { StreamEvent } from '../types';\n\n/**\n * Create an SSE encoder for streaming responses\n */\nexport function createSSEEncoder() {\n const encoder = new TextEncoder();\n\n return {\n encode(event: StreamEvent): Uint8Array {\n return encoder.encode(`data: ${JSON.stringify(event)}\\n\\n`);\n },\n\n encodeText(content: string): Uint8Array {\n return encoder.encode(`data: ${JSON.stringify({ type: 'text', content })}\\n\\n`);\n },\n\n encodeTool(name: string, args: Record<string, unknown>): Uint8Array {\n return encoder.encode(`data: ${JSON.stringify({ type: 'tool', name, args })}\\n\\n`);\n },\n\n encodeDone(): Uint8Array {\n return encoder.encode(`data: ${JSON.stringify({ type: 'done' })}\\n\\n`);\n },\n\n encodeError(message: string): Uint8Array {\n return encoder.encode(`data: ${JSON.stringify({ type: 'error', message })}\\n\\n`);\n },\n };\n}\n\n/**\n * Create SSE response headers\n */\nexport function getSSEHeaders(): HeadersInit {\n return {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n };\n}\n\n/**\n * Parse SSE events from a ReadableStream\n */\nexport async function* parseSSEStream(\n reader: ReadableStreamDefaultReader<Uint8Array>\n): AsyncGenerator<StreamEvent> {\n const decoder = new TextDecoder();\n let buffer = '';\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() || '';\n\n for (const line of lines) {\n if (line.startsWith('data: ')) {\n try {\n const data = JSON.parse(line.slice(6)) as StreamEvent;\n yield data;\n } catch {\n // Skip malformed JSON\n }\n }\n }\n }\n}\n","/**\n * Universal chat handler using OpenRouter\n * Works with any model: Gemini, GPT-4, Claude, Llama, etc.\n * No SDK required - just standard fetch.\n */\n\nimport type { ToolDefinition } from '../tools/types';\nimport { createSSEEncoder, getSSEHeaders } from './streaming';\n\nexport interface HandlerConfig {\n /** OpenRouter API key (or set OPENROUTER_API_KEY env var) */\n apiKey?: string;\n /** Model to use (e.g., 'google/gemini-2.0-flash-exp:free', 'openai/gpt-4o', 'anthropic/claude-3.5-sonnet') */\n model?: string;\n /** System prompt for the AI */\n systemPrompt: string;\n /** Tool definitions for the AI */\n tools?: ToolDefinition[];\n /** Temperature for response generation (0-1) */\n temperature?: number;\n /** Your site URL (shown in OpenRouter dashboard) */\n siteUrl?: string;\n /** Your app name (shown in OpenRouter dashboard) */\n siteName?: string;\n}\n\ninterface RequestBody {\n messages: Array<{ role: 'user' | 'assistant'; content: string }>;\n}\n\ninterface OpenRouterMessage {\n role: 'system' | 'user' | 'assistant';\n content: string;\n}\n\ninterface OpenRouterTool {\n type: 'function';\n function: {\n name: string;\n description: string;\n parameters: {\n type: 'object';\n properties: Record<string, unknown>;\n required: string[];\n };\n };\n}\n\n/**\n * Convert tool definitions to OpenAI/OpenRouter format\n */\nfunction convertTools(tools: ToolDefinition[]): OpenRouterTool[] {\n return tools.map(tool => ({\n type: 'function' as const,\n function: {\n name: tool.name,\n description: tool.description,\n parameters: {\n type: 'object' as const,\n properties: Object.fromEntries(\n Object.entries(tool.parameters.properties).map(([key, value]) => [\n key,\n {\n type: value.type,\n description: value.description,\n ...(value.enum && { enum: value.enum }),\n },\n ])\n ),\n required: tool.parameters.required,\n },\n },\n }));\n}\n\n/**\n * Create a Next.js API route handler using OpenRouter\n *\n * Works with any model - just change the model string:\n * - 'google/gemini-2.0-flash-exp:free' (free!)\n * - 'openai/gpt-4o'\n * - 'anthropic/claude-3.5-sonnet'\n * - 'meta-llama/llama-3.1-70b-instruct'\n *\n * @example\n * ```ts\n * // app/api/chat/route.ts\n * import { createHandler } from 'ai-site-pilot/api';\n *\n * export const POST = createHandler({\n * model: 'google/gemini-2.0-flash-exp:free',\n * systemPrompt: 'You are a helpful assistant...',\n * tools: myTools,\n * });\n * ```\n */\nexport function createHandler(config: HandlerConfig) {\n const {\n apiKey = process.env.OPENROUTER_API_KEY,\n model = 'google/gemini-2.0-flash-exp:free',\n systemPrompt,\n tools = [],\n temperature = 0.7,\n siteUrl,\n siteName,\n } = config;\n\n return async function POST(req: Request): Promise<Response> {\n if (!apiKey) {\n return new Response(\n JSON.stringify({ error: 'OpenRouter API key not configured. Get one at https://openrouter.ai' }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n );\n }\n\n try {\n const body = (await req.json()) as RequestBody;\n const { messages } = body;\n\n // Build messages array with system prompt\n const openRouterMessages: OpenRouterMessage[] = [\n { role: 'system', content: systemPrompt },\n ...messages.map((m) => ({\n role: m.role === 'assistant' ? 'assistant' as const : 'user' as const,\n content: m.content,\n })),\n ];\n\n const sse = createSSEEncoder();\n\n const stream = new ReadableStream({\n async start(controller) {\n try {\n const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${apiKey}`,\n 'Content-Type': 'application/json',\n 'HTTP-Referer': siteUrl || '',\n 'X-Title': siteName || '',\n },\n body: JSON.stringify({\n model,\n messages: openRouterMessages,\n temperature,\n stream: true,\n ...(tools.length > 0 && { tools: convertTools(tools) }),\n }),\n });\n\n if (!response.ok) {\n const error = await response.text();\n console.error('OpenRouter error:', error);\n controller.enqueue(sse.encodeError('Failed to get AI response'));\n controller.enqueue(sse.encodeDone());\n controller.close();\n return;\n }\n\n const reader = response.body?.getReader();\n if (!reader) {\n controller.enqueue(sse.encodeError('No response stream'));\n controller.enqueue(sse.encodeDone());\n controller.close();\n return;\n }\n\n const decoder = new TextDecoder();\n let buffer = '';\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() || '';\n\n for (const line of lines) {\n if (line.startsWith('data: ')) {\n const data = line.slice(6);\n if (data === '[DONE]') continue;\n\n try {\n const parsed = JSON.parse(data);\n const delta = parsed.choices?.[0]?.delta;\n\n // Handle text content\n if (delta?.content) {\n controller.enqueue(sse.encodeText(delta.content));\n }\n\n // Handle tool calls\n if (delta?.tool_calls) {\n for (const toolCall of delta.tool_calls) {\n if (toolCall.function?.name && toolCall.function?.arguments) {\n try {\n const args = JSON.parse(toolCall.function.arguments);\n controller.enqueue(sse.encodeTool(toolCall.function.name, args));\n } catch {\n // Arguments might be streamed in chunks, skip incomplete\n }\n }\n }\n }\n } catch {\n // Skip malformed JSON\n }\n }\n }\n }\n\n controller.enqueue(sse.encodeDone());\n controller.close();\n } catch (error) {\n console.error('Streaming error:', error);\n controller.enqueue(sse.encodeError('An error occurred during streaming'));\n controller.close();\n }\n },\n });\n\n return new Response(stream, { headers: getSSEHeaders() });\n } catch (error) {\n console.error('Handler error:', error);\n return new Response(\n JSON.stringify({ error: 'Internal server error' }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n );\n }\n };\n}\n"]}
@@ -1,7 +1,3 @@
1
- import { streamText } from 'ai';
2
-
3
- // src/api/createChatHandler.ts
4
-
5
1
  // src/api/streaming.ts
6
2
  function createSSEEncoder() {
7
3
  const encoder = new TextEncoder();
@@ -61,54 +57,125 @@ async function* parseSSEStream(reader) {
61
57
  }
62
58
  }
63
59
 
64
- // src/api/createChatHandler.ts
65
- function convertToolsToAISDK(tools) {
66
- const result = {};
67
- for (const tool of tools) {
68
- result[tool.name] = {
60
+ // src/api/createHandler.ts
61
+ function convertTools(tools) {
62
+ return tools.map((tool) => ({
63
+ type: "function",
64
+ function: {
65
+ name: tool.name,
69
66
  description: tool.description,
70
67
  parameters: {
71
68
  type: "object",
72
- properties: tool.parameters.properties,
69
+ properties: Object.fromEntries(
70
+ Object.entries(tool.parameters.properties).map(([key, value]) => [
71
+ key,
72
+ {
73
+ type: value.type,
74
+ description: value.description,
75
+ ...value.enum && { enum: value.enum }
76
+ }
77
+ ])
78
+ ),
73
79
  required: tool.parameters.required
74
80
  }
75
- };
76
- }
77
- return result;
81
+ }
82
+ }));
78
83
  }
79
- function createChatHandler(config) {
80
- const { model, systemPrompt, tools = [], temperature = 0.7, maxTokens } = config;
84
+ function createHandler(config) {
85
+ const {
86
+ apiKey = process.env.OPENROUTER_API_KEY,
87
+ model = "google/gemini-2.0-flash-exp:free",
88
+ systemPrompt,
89
+ tools = [],
90
+ temperature = 0.7,
91
+ siteUrl,
92
+ siteName
93
+ } = config;
81
94
  return async function POST(req) {
95
+ if (!apiKey) {
96
+ return new Response(
97
+ JSON.stringify({ error: "OpenRouter API key not configured. Get one at https://openrouter.ai" }),
98
+ { status: 500, headers: { "Content-Type": "application/json" } }
99
+ );
100
+ }
82
101
  try {
83
102
  const body = await req.json();
84
103
  const { messages } = body;
85
- const coreMessages = messages.map((m) => ({
86
- role: m.role,
87
- content: m.content
88
- }));
104
+ const openRouterMessages = [
105
+ { role: "system", content: systemPrompt },
106
+ ...messages.map((m) => ({
107
+ role: m.role === "assistant" ? "assistant" : "user",
108
+ content: m.content
109
+ }))
110
+ ];
89
111
  const sse = createSSEEncoder();
90
112
  const stream = new ReadableStream({
91
113
  async start(controller) {
92
114
  try {
93
- const result = streamText({
94
- model,
95
- system: systemPrompt,
96
- messages: coreMessages,
97
- temperature,
98
- maxTokens,
99
- tools: tools.length > 0 ? convertToolsToAISDK(tools) : void 0
115
+ const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
116
+ method: "POST",
117
+ headers: {
118
+ "Authorization": `Bearer ${apiKey}`,
119
+ "Content-Type": "application/json",
120
+ "HTTP-Referer": siteUrl || "",
121
+ "X-Title": siteName || ""
122
+ },
123
+ body: JSON.stringify({
124
+ model,
125
+ messages: openRouterMessages,
126
+ temperature,
127
+ stream: true,
128
+ ...tools.length > 0 && { tools: convertTools(tools) }
129
+ })
100
130
  });
101
- for await (const chunk of (await result).textStream) {
102
- if (chunk) {
103
- controller.enqueue(sse.encodeText(chunk));
104
- }
131
+ if (!response.ok) {
132
+ const error = await response.text();
133
+ console.error("OpenRouter error:", error);
134
+ controller.enqueue(sse.encodeError("Failed to get AI response"));
135
+ controller.enqueue(sse.encodeDone());
136
+ controller.close();
137
+ return;
138
+ }
139
+ const reader = response.body?.getReader();
140
+ if (!reader) {
141
+ controller.enqueue(sse.encodeError("No response stream"));
142
+ controller.enqueue(sse.encodeDone());
143
+ controller.close();
144
+ return;
105
145
  }
106
- const finalResult = await result;
107
- const toolCalls = await finalResult.toolCalls || [];
108
- for (const toolCall of toolCalls) {
109
- controller.enqueue(
110
- sse.encodeTool(toolCall.toolName, toolCall.args)
111
- );
146
+ const decoder = new TextDecoder();
147
+ let buffer = "";
148
+ while (true) {
149
+ const { done, value } = await reader.read();
150
+ if (done) break;
151
+ buffer += decoder.decode(value, { stream: true });
152
+ const lines = buffer.split("\n");
153
+ buffer = lines.pop() || "";
154
+ for (const line of lines) {
155
+ if (line.startsWith("data: ")) {
156
+ const data = line.slice(6);
157
+ if (data === "[DONE]") continue;
158
+ try {
159
+ const parsed = JSON.parse(data);
160
+ const delta = parsed.choices?.[0]?.delta;
161
+ if (delta?.content) {
162
+ controller.enqueue(sse.encodeText(delta.content));
163
+ }
164
+ if (delta?.tool_calls) {
165
+ for (const toolCall of delta.tool_calls) {
166
+ if (toolCall.function?.name && toolCall.function?.arguments) {
167
+ try {
168
+ const args = JSON.parse(toolCall.function.arguments);
169
+ controller.enqueue(sse.encodeTool(toolCall.function.name, args));
170
+ } catch {
171
+ }
172
+ }
173
+ }
174
+ }
175
+ } catch {
176
+ }
177
+ }
178
+ }
112
179
  }
113
180
  controller.enqueue(sse.encodeDone());
114
181
  controller.close();
@@ -119,19 +186,17 @@ function createChatHandler(config) {
119
186
  }
120
187
  }
121
188
  });
122
- return new Response(stream, {
123
- headers: getSSEHeaders()
124
- });
189
+ return new Response(stream, { headers: getSSEHeaders() });
125
190
  } catch (error) {
126
- console.error("Chat API error:", error);
127
- return new Response(JSON.stringify({ error: "Internal server error" }), {
128
- status: 500,
129
- headers: { "Content-Type": "application/json" }
130
- });
191
+ console.error("Handler error:", error);
192
+ return new Response(
193
+ JSON.stringify({ error: "Internal server error" }),
194
+ { status: 500, headers: { "Content-Type": "application/json" } }
195
+ );
131
196
  }
132
197
  };
133
198
  }
134
199
 
135
- export { createChatHandler, createSSEEncoder, getSSEHeaders, parseSSEStream };
200
+ export { createHandler, createSSEEncoder, getSSEHeaders, parseSSEStream };
136
201
  //# sourceMappingURL=index.mjs.map
137
202
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/api/streaming.ts","../../src/api/createChatHandler.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * SSE streaming utilities\n */\n\nimport type { StreamEvent } from '../types';\n\n/**\n * Create an SSE encoder for streaming responses\n */\nexport function createSSEEncoder() {\n const encoder = new TextEncoder();\n\n return {\n encode(event: StreamEvent): Uint8Array {\n return encoder.encode(`data: ${JSON.stringify(event)}\\n\\n`);\n },\n\n encodeText(content: string): Uint8Array {\n return encoder.encode(`data: ${JSON.stringify({ type: 'text', content })}\\n\\n`);\n },\n\n encodeTool(name: string, args: Record<string, unknown>): Uint8Array {\n return encoder.encode(`data: ${JSON.stringify({ type: 'tool', name, args })}\\n\\n`);\n },\n\n encodeDone(): Uint8Array {\n return encoder.encode(`data: ${JSON.stringify({ type: 'done' })}\\n\\n`);\n },\n\n encodeError(message: string): Uint8Array {\n return encoder.encode(`data: ${JSON.stringify({ type: 'error', message })}\\n\\n`);\n },\n };\n}\n\n/**\n * Create SSE response headers\n */\nexport function getSSEHeaders(): HeadersInit {\n return {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n };\n}\n\n/**\n * Parse SSE events from a ReadableStream\n */\nexport async function* parseSSEStream(\n reader: ReadableStreamDefaultReader<Uint8Array>\n): AsyncGenerator<StreamEvent> {\n const decoder = new TextDecoder();\n let buffer = '';\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() || '';\n\n for (const line of lines) {\n if (line.startsWith('data: ')) {\n try {\n const data = JSON.parse(line.slice(6)) as StreamEvent;\n yield data;\n } catch {\n // Skip malformed JSON\n }\n }\n }\n }\n}\n","/**\n * Factory for creating Next.js API route handlers\n */\n\nimport { streamText, type CoreMessage, type LanguageModel } from 'ai';\nimport type { ToolDefinition } from '../tools/types';\nimport { createSSEEncoder, getSSEHeaders } from './streaming';\n\nexport interface ChatHandlerConfig {\n /** The AI model to use (from Vercel AI SDK) */\n model: LanguageModel;\n /** System prompt for the AI */\n systemPrompt: string;\n /** Tool definitions for the AI */\n tools?: ToolDefinition[];\n /** Temperature for response generation (0-1) */\n temperature?: number;\n /** Maximum tokens in response */\n maxTokens?: number;\n}\n\ninterface RequestBody {\n messages: Array<{ role: 'user' | 'assistant'; content: string }>;\n}\n\n/**\n * Convert tool definitions to Vercel AI SDK format\n */\nfunction convertToolsToAISDK(tools: ToolDefinition[]) {\n const result: Record<string, { description: string; parameters: unknown }> = {};\n\n for (const tool of tools) {\n result[tool.name] = {\n description: tool.description,\n parameters: {\n type: 'object',\n properties: tool.parameters.properties,\n required: tool.parameters.required,\n },\n };\n }\n\n return result;\n}\n\n/**\n * Create a Next.js API route handler for chat\n *\n * Works with any Vercel AI SDK compatible model including:\n * - Google Gemini (@ai-sdk/google)\n * - OpenAI (@ai-sdk/openai)\n * - Anthropic (@ai-sdk/anthropic)\n * - And more...\n *\n * @example\n * ```ts\n * // app/api/chat/route.ts\n * import { createChatHandler } from 'ai-site-pilot/api';\n * import { google } from '@ai-sdk/google';\n *\n * export const POST = createChatHandler({\n * model: google('gemini-2.0-flash'),\n * systemPrompt: 'You are a helpful assistant...',\n * tools: myTools,\n * });\n * ```\n *\n * @example Using OpenAI\n * ```ts\n * import { openai } from '@ai-sdk/openai';\n *\n * export const POST = createChatHandler({\n * model: openai('gpt-4o'),\n * systemPrompt: 'You are a helpful assistant...',\n * });\n * ```\n */\nexport function createChatHandler(config: ChatHandlerConfig) {\n const { model, systemPrompt, tools = [], temperature = 0.7, maxTokens } = config;\n\n return async function POST(req: Request): Promise<Response> {\n try {\n const body = (await req.json()) as RequestBody;\n const { messages } = body;\n\n // Convert messages to CoreMessage format\n const coreMessages: CoreMessage[] = messages.map((m) => ({\n role: m.role,\n content: m.content,\n }));\n\n // Create the SSE encoder\n const sse = createSSEEncoder();\n\n // Create a readable stream for SSE\n const stream = new ReadableStream({\n async start(controller) {\n try {\n const result = streamText({\n model,\n system: systemPrompt,\n messages: coreMessages,\n temperature,\n maxTokens,\n tools: tools.length > 0 ? convertToolsToAISDK(tools) : undefined,\n });\n\n // Stream text chunks\n for await (const chunk of (await result).textStream) {\n if (chunk) {\n controller.enqueue(sse.encodeText(chunk));\n }\n }\n\n // Get tool calls from the result\n const finalResult = await result;\n const toolCalls = (await finalResult.toolCalls) || [];\n\n for (const toolCall of toolCalls) {\n controller.enqueue(\n sse.encodeTool(toolCall.toolName, toolCall.args as Record<string, unknown>)\n );\n }\n\n controller.enqueue(sse.encodeDone());\n controller.close();\n } catch (error) {\n console.error('Streaming error:', error);\n controller.enqueue(sse.encodeError('An error occurred during streaming'));\n controller.close();\n }\n },\n });\n\n return new Response(stream, {\n headers: getSSEHeaders(),\n });\n } catch (error) {\n console.error('Chat API error:', error);\n return new Response(JSON.stringify({ error: 'Internal server error' }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n });\n }\n };\n}\n"]}
1
+ {"version":3,"sources":["../../src/api/streaming.ts","../../src/api/createHandler.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * SSE streaming utilities\n */\n\nimport type { StreamEvent } from '../types';\n\n/**\n * Create an SSE encoder for streaming responses\n */\nexport function createSSEEncoder() {\n const encoder = new TextEncoder();\n\n return {\n encode(event: StreamEvent): Uint8Array {\n return encoder.encode(`data: ${JSON.stringify(event)}\\n\\n`);\n },\n\n encodeText(content: string): Uint8Array {\n return encoder.encode(`data: ${JSON.stringify({ type: 'text', content })}\\n\\n`);\n },\n\n encodeTool(name: string, args: Record<string, unknown>): Uint8Array {\n return encoder.encode(`data: ${JSON.stringify({ type: 'tool', name, args })}\\n\\n`);\n },\n\n encodeDone(): Uint8Array {\n return encoder.encode(`data: ${JSON.stringify({ type: 'done' })}\\n\\n`);\n },\n\n encodeError(message: string): Uint8Array {\n return encoder.encode(`data: ${JSON.stringify({ type: 'error', message })}\\n\\n`);\n },\n };\n}\n\n/**\n * Create SSE response headers\n */\nexport function getSSEHeaders(): HeadersInit {\n return {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n };\n}\n\n/**\n * Parse SSE events from a ReadableStream\n */\nexport async function* parseSSEStream(\n reader: ReadableStreamDefaultReader<Uint8Array>\n): AsyncGenerator<StreamEvent> {\n const decoder = new TextDecoder();\n let buffer = '';\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() || '';\n\n for (const line of lines) {\n if (line.startsWith('data: ')) {\n try {\n const data = JSON.parse(line.slice(6)) as StreamEvent;\n yield data;\n } catch {\n // Skip malformed JSON\n }\n }\n }\n }\n}\n","/**\n * Universal chat handler using OpenRouter\n * Works with any model: Gemini, GPT-4, Claude, Llama, etc.\n * No SDK required - just standard fetch.\n */\n\nimport type { ToolDefinition } from '../tools/types';\nimport { createSSEEncoder, getSSEHeaders } from './streaming';\n\nexport interface HandlerConfig {\n /** OpenRouter API key (or set OPENROUTER_API_KEY env var) */\n apiKey?: string;\n /** Model to use (e.g., 'google/gemini-2.0-flash-exp:free', 'openai/gpt-4o', 'anthropic/claude-3.5-sonnet') */\n model?: string;\n /** System prompt for the AI */\n systemPrompt: string;\n /** Tool definitions for the AI */\n tools?: ToolDefinition[];\n /** Temperature for response generation (0-1) */\n temperature?: number;\n /** Your site URL (shown in OpenRouter dashboard) */\n siteUrl?: string;\n /** Your app name (shown in OpenRouter dashboard) */\n siteName?: string;\n}\n\ninterface RequestBody {\n messages: Array<{ role: 'user' | 'assistant'; content: string }>;\n}\n\ninterface OpenRouterMessage {\n role: 'system' | 'user' | 'assistant';\n content: string;\n}\n\ninterface OpenRouterTool {\n type: 'function';\n function: {\n name: string;\n description: string;\n parameters: {\n type: 'object';\n properties: Record<string, unknown>;\n required: string[];\n };\n };\n}\n\n/**\n * Convert tool definitions to OpenAI/OpenRouter format\n */\nfunction convertTools(tools: ToolDefinition[]): OpenRouterTool[] {\n return tools.map(tool => ({\n type: 'function' as const,\n function: {\n name: tool.name,\n description: tool.description,\n parameters: {\n type: 'object' as const,\n properties: Object.fromEntries(\n Object.entries(tool.parameters.properties).map(([key, value]) => [\n key,\n {\n type: value.type,\n description: value.description,\n ...(value.enum && { enum: value.enum }),\n },\n ])\n ),\n required: tool.parameters.required,\n },\n },\n }));\n}\n\n/**\n * Create a Next.js API route handler using OpenRouter\n *\n * Works with any model - just change the model string:\n * - 'google/gemini-2.0-flash-exp:free' (free!)\n * - 'openai/gpt-4o'\n * - 'anthropic/claude-3.5-sonnet'\n * - 'meta-llama/llama-3.1-70b-instruct'\n *\n * @example\n * ```ts\n * // app/api/chat/route.ts\n * import { createHandler } from 'ai-site-pilot/api';\n *\n * export const POST = createHandler({\n * model: 'google/gemini-2.0-flash-exp:free',\n * systemPrompt: 'You are a helpful assistant...',\n * tools: myTools,\n * });\n * ```\n */\nexport function createHandler(config: HandlerConfig) {\n const {\n apiKey = process.env.OPENROUTER_API_KEY,\n model = 'google/gemini-2.0-flash-exp:free',\n systemPrompt,\n tools = [],\n temperature = 0.7,\n siteUrl,\n siteName,\n } = config;\n\n return async function POST(req: Request): Promise<Response> {\n if (!apiKey) {\n return new Response(\n JSON.stringify({ error: 'OpenRouter API key not configured. Get one at https://openrouter.ai' }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n );\n }\n\n try {\n const body = (await req.json()) as RequestBody;\n const { messages } = body;\n\n // Build messages array with system prompt\n const openRouterMessages: OpenRouterMessage[] = [\n { role: 'system', content: systemPrompt },\n ...messages.map((m) => ({\n role: m.role === 'assistant' ? 'assistant' as const : 'user' as const,\n content: m.content,\n })),\n ];\n\n const sse = createSSEEncoder();\n\n const stream = new ReadableStream({\n async start(controller) {\n try {\n const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${apiKey}`,\n 'Content-Type': 'application/json',\n 'HTTP-Referer': siteUrl || '',\n 'X-Title': siteName || '',\n },\n body: JSON.stringify({\n model,\n messages: openRouterMessages,\n temperature,\n stream: true,\n ...(tools.length > 0 && { tools: convertTools(tools) }),\n }),\n });\n\n if (!response.ok) {\n const error = await response.text();\n console.error('OpenRouter error:', error);\n controller.enqueue(sse.encodeError('Failed to get AI response'));\n controller.enqueue(sse.encodeDone());\n controller.close();\n return;\n }\n\n const reader = response.body?.getReader();\n if (!reader) {\n controller.enqueue(sse.encodeError('No response stream'));\n controller.enqueue(sse.encodeDone());\n controller.close();\n return;\n }\n\n const decoder = new TextDecoder();\n let buffer = '';\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() || '';\n\n for (const line of lines) {\n if (line.startsWith('data: ')) {\n const data = line.slice(6);\n if (data === '[DONE]') continue;\n\n try {\n const parsed = JSON.parse(data);\n const delta = parsed.choices?.[0]?.delta;\n\n // Handle text content\n if (delta?.content) {\n controller.enqueue(sse.encodeText(delta.content));\n }\n\n // Handle tool calls\n if (delta?.tool_calls) {\n for (const toolCall of delta.tool_calls) {\n if (toolCall.function?.name && toolCall.function?.arguments) {\n try {\n const args = JSON.parse(toolCall.function.arguments);\n controller.enqueue(sse.encodeTool(toolCall.function.name, args));\n } catch {\n // Arguments might be streamed in chunks, skip incomplete\n }\n }\n }\n }\n } catch {\n // Skip malformed JSON\n }\n }\n }\n }\n\n controller.enqueue(sse.encodeDone());\n controller.close();\n } catch (error) {\n console.error('Streaming error:', error);\n controller.enqueue(sse.encodeError('An error occurred during streaming'));\n controller.close();\n }\n },\n });\n\n return new Response(stream, { headers: getSSEHeaders() });\n } catch (error) {\n console.error('Handler error:', error);\n return new Response(\n JSON.stringify({ error: 'Internal server error' }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n );\n }\n };\n}\n"]}
package/dist/styles.css CHANGED
@@ -13,6 +13,12 @@
13
13
  */
14
14
 
15
15
  .pilot-container {
16
+ /* Container should not interfere with fixed children */
17
+ position: relative;
18
+ z-index: 199;
19
+ pointer-events: none;
20
+
21
+ /* CSS custom properties for theming */
16
22
  --pilot-bg: #0F0720;
17
23
  --pilot-bg-95: rgba(15, 7, 32, 0.95);
18
24
  --pilot-text: #ffffff;
@@ -31,6 +37,11 @@
31
37
  --pilot-accent-glow-strong: hsla(var(--pilot-accent-h), var(--pilot-accent-s), var(--pilot-accent-l), 0.3);
32
38
  }
33
39
 
40
+ /* Fixed children need pointer events enabled */
41
+ .pilot-container > * {
42
+ pointer-events: auto;
43
+ }
44
+
34
45
  /* Panel */
35
46
  .pilot-panel {
36
47
  background: var(--pilot-bg-95);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ai-site-pilot",
3
- "version": "0.2.3",
4
- "description": "AI chat widget that can control and navigate your website. Full-stack solution with streaming, tool system, and polished UI.",
3
+ "version": "0.4.0",
4
+ "description": "AI chat widget that can control and navigate your website. Works with any AI model via OpenRouter.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
7
7
  "types": "./dist/index.d.ts",
@@ -44,10 +44,10 @@
44
44
  "chat",
45
45
  "chatbot",
46
46
  "site-pilot",
47
+ "openrouter",
47
48
  "navigation",
48
49
  "react",
49
50
  "nextjs",
50
- "vercel-ai-sdk",
51
51
  "streaming",
52
52
  "tools",
53
53
  "function-calling"
@@ -71,7 +71,6 @@
71
71
  "react-dom": "^18.0.0 || ^19.0.0"
72
72
  },
73
73
  "dependencies": {
74
- "ai": "^4.0.0",
75
74
  "framer-motion": "^11.0.0",
76
75
  "lucide-react": "^0.400.0",
77
76
  "react-markdown": "^9.0.0"