@karixi/payload-ai 0.1.2 → 0.2.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
@@ -87,7 +87,113 @@ mcpPlugin({
87
87
  }),
88
88
  ```
89
89
 
90
- > **Note:** `getAITools` requires the same `provider` and `apiKeyEnvVar` as `aiPlugin` so it can create the AI provider when tools are invoked. `getAIPrompts()` and `getAIResources()` take no arguments.
90
+ ### Google Gemini
91
+
92
+ ```ts
93
+ aiPlugin({
94
+ provider: 'gemini',
95
+ apiKeyEnvVar: 'GEMINI_API_KEY',
96
+ // model: 'gemini-2.5-flash', // default — or 'gemini-2.5-pro' for best quality
97
+ features: { adminUI: true },
98
+ }),
99
+
100
+ mcpPlugin({
101
+ collections: { posts: { enabled: true } },
102
+ mcp: {
103
+ tools: getAITools({
104
+ provider: 'gemini',
105
+ apiKeyEnvVar: 'GEMINI_API_KEY',
106
+ }),
107
+ prompts: getAIPrompts(),
108
+ resources: getAIResources(),
109
+ },
110
+ }),
111
+ ```
112
+
113
+ Get an API key at [ai.google.dev](https://ai.google.dev).
114
+
115
+ ### Ollama (Local / Self-Hosted)
116
+
117
+ Run AI models locally with zero API costs. Works with [Ollama](https://ollama.com), [LocalAI](https://localai.io), [vLLM](https://vllm.ai), or [LM Studio](https://lmstudio.ai) — any server with an OpenAI-compatible endpoint.
118
+
119
+ ```ts
120
+ aiPlugin({
121
+ provider: 'ollama',
122
+ apiKeyEnvVar: 'OLLAMA_API_KEY', // set to any value, e.g. 'ollama' — not validated
123
+ baseUrl: 'http://localhost:11434', // default for Ollama
124
+ model: 'llama3.3:8b', // any model you've pulled
125
+ features: { adminUI: true },
126
+ }),
127
+
128
+ mcpPlugin({
129
+ collections: { posts: { enabled: true } },
130
+ mcp: {
131
+ tools: getAITools({
132
+ provider: 'ollama',
133
+ apiKeyEnvVar: 'OLLAMA_API_KEY',
134
+ baseUrl: 'http://localhost:11434',
135
+ model: 'llama3.3:8b',
136
+ }),
137
+ prompts: getAIPrompts(),
138
+ resources: getAIResources(),
139
+ },
140
+ }),
141
+ ```
142
+
143
+ #### Quick start with Ollama
144
+
145
+ ```bash
146
+ # Install and start Ollama
147
+ curl -fsSL https://ollama.com/install.sh | sh
148
+
149
+ # Pull a model (8B is good for JSON generation)
150
+ ollama pull llama3.3:8b
151
+
152
+ # Set a dummy API key env var
153
+ export OLLAMA_API_KEY=ollama
154
+ ```
155
+
156
+ #### Docker Compose (Ollama with GPU)
157
+
158
+ ```yaml
159
+ services:
160
+ ollama:
161
+ image: ollama/ollama:latest
162
+ ports:
163
+ - "11434:11434"
164
+ volumes:
165
+ - ollama_data:/root/.ollama
166
+ deploy:
167
+ resources:
168
+ reservations:
169
+ devices:
170
+ - driver: nvidia
171
+ count: all
172
+ capabilities: [gpu]
173
+ volumes:
174
+ ollama_data:
175
+ ```
176
+
177
+ #### Other compatible servers
178
+
179
+ | Server | Default URL | Notes |
180
+ |--------|------------|-------|
181
+ | [Ollama](https://ollama.com) | `http://localhost:11434` | Easiest setup, good model library |
182
+ | [LocalAI](https://localai.io) | `http://localhost:8080` | Multi-modal (LLM + image gen + TTS) |
183
+ | [vLLM](https://vllm.ai) | `http://localhost:8000` | Highest throughput, `guided_json` for 100% schema compliance |
184
+ | [LM Studio](https://lmstudio.ai) | `http://localhost:1234` | Desktop GUI, great for macOS/Apple Silicon |
185
+
186
+ #### Recommended models for JSON generation
187
+
188
+ | Model | Ollama Tag | VRAM | Best for |
189
+ |-------|-----------|------|----------|
190
+ | Qwen3 8B | `qwen3:8b` | 8 GB | Best 8B for structured output |
191
+ | Llama 3.3 8B | `llama3.3:8b` | 8 GB | General purpose, well-tested |
192
+ | Mistral Small 3 | `mistral-small3:22b` | 16 GB | Strong mid-range |
193
+ | Qwen3 32B | `qwen3:32b` | 20 GB | Near-cloud quality |
194
+ | Llama 3.3 70B | `llama3.3:70b` | 40 GB | Best open-source quality |
195
+
196
+ > **Note:** `getAITools` requires the same `provider`, `apiKeyEnvVar`, `baseUrl`, and `model` as `aiPlugin` so it can create the AI provider when tools are invoked. `getAIPrompts()` and `getAIResources()` take no arguments.
91
197
 
92
198
  ## Connecting Claude Code
93
199
 
@@ -165,10 +271,10 @@ These tools are registered inside the MCP plugin and callable by any MCP client:
165
271
 
166
272
  ### Dev Tools (`features.devTools: true`)
167
273
 
168
- Requires `@anthropic-ai/stagehand` (optional peer dependency):
274
+ Requires `@browserbasehq/stagehand` (optional peer dependency):
169
275
 
170
276
  ```bash
171
- bun add -d @anthropic-ai/stagehand
277
+ bun add -d @browserbasehq/stagehand
172
278
  ```
173
279
 
174
280
  Adds four additional MCP tools:
@@ -185,10 +291,12 @@ Adds four additional MCP tools:
185
291
  ```ts
186
292
  aiPlugin({
187
293
  // Required
188
- provider: 'anthropic' | 'openai',
294
+ provider: 'anthropic' | 'openai' | 'gemini' | 'ollama',
189
295
  apiKeyEnvVar: string, // env var name (not the key itself)
190
296
 
191
297
  // Optional
298
+ baseUrl: string, // provider endpoint (required for ollama, optional for gemini)
299
+ model: string, // model override (each provider has a sensible default)
192
300
  features: {
193
301
  adminUI: boolean, // default: false
194
302
  devTools: boolean, // default: false
@@ -0,0 +1,313 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-wcPFST8Q.mjs";
2
+ //#region src/core/providers/anthropic.ts
3
+ function isAnthropicResponse(value) {
4
+ return typeof value === "object" && value !== null && "content" in value && Array.isArray(value.content);
5
+ }
6
+ function createAnthropicProvider(configOrKey) {
7
+ const config = typeof configOrKey === "string" ? { apiKey: configOrKey } : configOrKey;
8
+ const apiKey = config.apiKey;
9
+ const model = config.model ?? "claude-sonnet-4-20250514";
10
+ const baseUrl = (config.baseUrl ?? "https://api.anthropic.com").replace(/\/+$/, "");
11
+ async function callAPI(messages) {
12
+ const response = await fetch(`${baseUrl}/v1/messages`, {
13
+ method: "POST",
14
+ headers: {
15
+ "x-api-key": apiKey,
16
+ "anthropic-version": "2023-06-01",
17
+ "content-type": "application/json"
18
+ },
19
+ body: JSON.stringify({
20
+ model,
21
+ max_tokens: 8192,
22
+ messages
23
+ })
24
+ });
25
+ if (!response.ok) {
26
+ const errorText = await response.text();
27
+ throw new Error(`Anthropic API error ${response.status}: ${errorText}`);
28
+ }
29
+ const data = await response.json();
30
+ if (!isAnthropicResponse(data)) throw new Error("Unexpected Anthropic API response shape");
31
+ return data;
32
+ }
33
+ return {
34
+ async generate(prompt, _outputSchema) {
35
+ const textBlock = (await callAPI([{
36
+ role: "user",
37
+ content: `${prompt}\n\nRespond with ONLY a valid JSON array. No markdown, no explanation, just the JSON array.`
38
+ }])).content.find((block) => block.type === "text");
39
+ if (!textBlock || !("text" in textBlock) || typeof textBlock.text !== "string") throw new Error("No text content in Anthropic response");
40
+ const jsonText = textBlock.text.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```\s*$/, "").trim();
41
+ const parsed = JSON.parse(jsonText);
42
+ if (!Array.isArray(parsed)) throw new Error("Anthropic response is not a JSON array");
43
+ return parsed;
44
+ },
45
+ async analyzeImage(imageBuffer) {
46
+ const base64 = imageBuffer.toString("base64");
47
+ let mediaType = "image/jpeg";
48
+ if (imageBuffer[0] === 137 && imageBuffer[1] === 80) mediaType = "image/png";
49
+ else if (imageBuffer[0] === 71 && imageBuffer[1] === 73) mediaType = "image/gif";
50
+ else if (imageBuffer[0] === 82 && imageBuffer[1] === 73) mediaType = "image/webp";
51
+ const textBlock = (await callAPI([{
52
+ role: "user",
53
+ content: [{
54
+ type: "image",
55
+ source: {
56
+ type: "base64",
57
+ media_type: mediaType,
58
+ data: base64
59
+ }
60
+ }, {
61
+ type: "text",
62
+ text: "Describe this image concisely for use as alt text. Focus on the main subject and important visual details. Respond with only the alt text description, no extra explanation."
63
+ }]
64
+ }])).content.find((block) => block.type === "text");
65
+ if (!textBlock || !("text" in textBlock) || typeof textBlock.text !== "string") throw new Error("No text content in Anthropic image analysis response");
66
+ return textBlock.text.trim();
67
+ }
68
+ };
69
+ }
70
+ //#endregion
71
+ //#region src/core/providers/gemini.ts
72
+ function isGeminiResponse(value) {
73
+ return typeof value === "object" && value !== null && "candidates" in value;
74
+ }
75
+ function detectMediaType$1(buffer) {
76
+ if (buffer[0] === 137 && buffer[1] === 80) return "image/png";
77
+ if (buffer[0] === 71 && buffer[1] === 73) return "image/gif";
78
+ if (buffer[0] === 82 && buffer[1] === 73) return "image/webp";
79
+ return "image/jpeg";
80
+ }
81
+ function extractText(response) {
82
+ if (!response.candidates || response.candidates.length === 0) {
83
+ const reason = response.promptFeedback?.blockReason ?? "unknown";
84
+ throw new Error(`Gemini request blocked: ${reason}`);
85
+ }
86
+ const candidate = response.candidates[0];
87
+ const text = candidate.content.parts.find((p) => "text" in p)?.text;
88
+ if (!text) throw new Error("No text content in Gemini response");
89
+ if (candidate.finishReason === "MAX_TOKENS") throw new Error("Gemini response truncated (MAX_TOKENS) — output may be incomplete");
90
+ return text.trim();
91
+ }
92
+ function createGeminiProvider(config) {
93
+ const model = config.model ?? "gemini-2.5-flash";
94
+ const baseUrl = config.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta";
95
+ async function callAPI(contents, systemInstruction, jsonMode) {
96
+ const body = { contents };
97
+ if (systemInstruction) body.system_instruction = { parts: [{ text: systemInstruction }] };
98
+ if (jsonMode) body.generationConfig = { responseMimeType: "application/json" };
99
+ const url = `${baseUrl}/models/${model}:generateContent`;
100
+ const response = await fetch(url, {
101
+ method: "POST",
102
+ headers: {
103
+ "x-goog-api-key": config.apiKey,
104
+ "content-type": "application/json"
105
+ },
106
+ body: JSON.stringify(body)
107
+ });
108
+ if (!response.ok) {
109
+ const errorText = await response.text();
110
+ throw new Error(`Gemini API error ${response.status}: ${errorText}`);
111
+ }
112
+ const data = await response.json();
113
+ if (!isGeminiResponse(data)) throw new Error("Unexpected Gemini API response shape");
114
+ return data;
115
+ }
116
+ return {
117
+ async generate(prompt, _outputSchema) {
118
+ const jsonText = extractText(await callAPI([{
119
+ role: "user",
120
+ parts: [{ text: `${prompt}\n\nRespond with ONLY a valid JSON array. No markdown, no explanation, just the JSON array.` }]
121
+ }], "You are a data generation assistant. Always respond with valid JSON arrays only.", true)).replace(/^```(?:json)?\s*/i, "").replace(/\s*```\s*$/, "").trim();
122
+ const parsed = JSON.parse(jsonText);
123
+ if (Array.isArray(parsed)) return parsed;
124
+ if (typeof parsed === "object" && parsed !== null && "items" in parsed) {
125
+ const items = parsed.items;
126
+ if (Array.isArray(items)) return items;
127
+ }
128
+ throw new Error("Gemini response is not a JSON array");
129
+ },
130
+ async analyzeImage(imageBuffer) {
131
+ const base64 = imageBuffer.toString("base64");
132
+ return extractText(await callAPI([{
133
+ role: "user",
134
+ parts: [{ inline_data: {
135
+ mime_type: detectMediaType$1(imageBuffer),
136
+ data: base64
137
+ } }, { text: "Describe this image concisely for use as alt text. Focus on the main subject and important visual details. Respond with only the alt text description, no extra explanation." }]
138
+ }]));
139
+ }
140
+ };
141
+ }
142
+ //#endregion
143
+ //#region src/core/providers/ollama.ts
144
+ function isChatResponse(value) {
145
+ return typeof value === "object" && value !== null && "choices" in value && Array.isArray(value.choices);
146
+ }
147
+ function detectMediaType(buffer) {
148
+ if (buffer[0] === 137 && buffer[1] === 80) return "image/png";
149
+ if (buffer[0] === 71 && buffer[1] === 73) return "image/gif";
150
+ if (buffer[0] === 82 && buffer[1] === 73) return "image/webp";
151
+ return "image/jpeg";
152
+ }
153
+ function createOllamaProvider(config) {
154
+ const baseUrl = (config.baseUrl ?? "http://localhost:11434").replace(/\/+$/, "");
155
+ const model = config.model ?? "llama3.3:8b";
156
+ const apiKey = config.apiKey ?? "ollama";
157
+ async function callAPI(messages, jsonMode) {
158
+ const body = {
159
+ model,
160
+ messages
161
+ };
162
+ if (jsonMode) body.response_format = { type: "json_object" };
163
+ const response = await fetch(`${baseUrl}/v1/chat/completions`, {
164
+ method: "POST",
165
+ headers: {
166
+ Authorization: `Bearer ${apiKey}`,
167
+ "content-type": "application/json"
168
+ },
169
+ body: JSON.stringify(body)
170
+ });
171
+ if (!response.ok) {
172
+ const errorText = await response.text();
173
+ throw new Error(`Ollama API error ${response.status}: ${errorText}`);
174
+ }
175
+ const data = await response.json();
176
+ if (!isChatResponse(data)) throw new Error("Unexpected API response shape from local LLM server");
177
+ return data;
178
+ }
179
+ return {
180
+ async generate(prompt, _outputSchema) {
181
+ const choice = (await callAPI([{
182
+ role: "system",
183
+ content: "You are a data generation assistant. Always respond with valid JSON only. When asked for an array, wrap it in {\"items\": [...]} so json_object mode is satisfied."
184
+ }, {
185
+ role: "user",
186
+ content: `${prompt}\n\nRespond with JSON object {"items": [...]} where items is the array of generated documents.`
187
+ }], true)).choices[0];
188
+ if (!choice || choice.message.content === null) throw new Error("No content in response from local LLM server");
189
+ const parsed = JSON.parse(choice.message.content);
190
+ if (typeof parsed === "object" && parsed !== null && "items" in parsed && Array.isArray(parsed.items)) return parsed.items;
191
+ if (Array.isArray(parsed)) return parsed;
192
+ throw new Error("Local LLM response is not a JSON array");
193
+ },
194
+ async analyzeImage(imageBuffer) {
195
+ const base64 = imageBuffer.toString("base64");
196
+ const choice = (await callAPI([{
197
+ role: "user",
198
+ content: [{
199
+ type: "image_url",
200
+ image_url: { url: `data:${detectMediaType(imageBuffer)};base64,${base64}` }
201
+ }, {
202
+ type: "text",
203
+ text: "Describe this image concisely for use as alt text. Focus on the main subject and important visual details. Respond with only the alt text description, no extra explanation."
204
+ }]
205
+ }], false)).choices[0];
206
+ if (!choice || choice.message.content === null) throw new Error("No content in image analysis response from local LLM server");
207
+ return choice.message.content.trim();
208
+ }
209
+ };
210
+ }
211
+ //#endregion
212
+ //#region src/core/providers/openai.ts
213
+ function isOpenAIResponse(value) {
214
+ return typeof value === "object" && value !== null && "choices" in value && Array.isArray(value.choices);
215
+ }
216
+ function createOpenAIProvider(configOrKey) {
217
+ const config = typeof configOrKey === "string" ? { apiKey: configOrKey } : configOrKey;
218
+ const apiKey = config.apiKey;
219
+ const model = config.model ?? "gpt-4o";
220
+ const baseUrl = (config.baseUrl ?? "https://api.openai.com").replace(/\/+$/, "");
221
+ async function callAPI(messages, jsonMode) {
222
+ const body = {
223
+ model,
224
+ messages
225
+ };
226
+ if (jsonMode) body.response_format = { type: "json_object" };
227
+ const response = await fetch(`${baseUrl}/v1/chat/completions`, {
228
+ method: "POST",
229
+ headers: {
230
+ Authorization: `Bearer ${apiKey}`,
231
+ "content-type": "application/json"
232
+ },
233
+ body: JSON.stringify(body)
234
+ });
235
+ if (!response.ok) {
236
+ const errorText = await response.text();
237
+ throw new Error(`OpenAI API error ${response.status}: ${errorText}`);
238
+ }
239
+ const data = await response.json();
240
+ if (!isOpenAIResponse(data)) throw new Error("Unexpected OpenAI API response shape");
241
+ return data;
242
+ }
243
+ return {
244
+ async generate(prompt, _outputSchema) {
245
+ const choice = (await callAPI([{
246
+ role: "system",
247
+ content: "You are a data generation assistant. Always respond with valid JSON only. When asked for an array, wrap it in {\"items\": [...]} so json_object mode is satisfied."
248
+ }, {
249
+ role: "user",
250
+ content: `${prompt}\n\nRespond with JSON object {"items": [...]} where items is the array of generated documents.`
251
+ }], true)).choices[0];
252
+ if (!choice || choice.message.content === null) throw new Error("No content in OpenAI response");
253
+ const parsed = JSON.parse(choice.message.content);
254
+ if (typeof parsed === "object" && parsed !== null && "items" in parsed && Array.isArray(parsed.items)) return parsed.items;
255
+ if (Array.isArray(parsed)) return parsed;
256
+ throw new Error("OpenAI response is not a JSON array");
257
+ },
258
+ async analyzeImage(imageBuffer) {
259
+ const base64 = imageBuffer.toString("base64");
260
+ let mediaType = "image/jpeg";
261
+ if (imageBuffer[0] === 137 && imageBuffer[1] === 80) mediaType = "image/png";
262
+ else if (imageBuffer[0] === 71 && imageBuffer[1] === 73) mediaType = "image/gif";
263
+ else if (imageBuffer[0] === 82 && imageBuffer[1] === 73) mediaType = "image/webp";
264
+ const choice = (await callAPI([{
265
+ role: "user",
266
+ content: [{
267
+ type: "image_url",
268
+ image_url: { url: `data:${mediaType};base64,${base64}` }
269
+ }, {
270
+ type: "text",
271
+ text: "Describe this image concisely for use as alt text. Focus on the main subject and important visual details. Respond with only the alt text description, no extra explanation."
272
+ }]
273
+ }], false)).choices[0];
274
+ if (!choice || choice.message.content === null) throw new Error("No content in OpenAI image analysis response");
275
+ return choice.message.content.trim();
276
+ }
277
+ };
278
+ }
279
+ //#endregion
280
+ //#region src/core/providers/base.ts
281
+ var base_exports = /* @__PURE__ */ __exportAll({ createProvider: () => createProvider });
282
+ function createProvider(config) {
283
+ switch (config.provider) {
284
+ case "anthropic": return createAnthropicProvider({
285
+ apiKey: config.apiKey,
286
+ model: config.model,
287
+ baseUrl: config.baseUrl
288
+ });
289
+ case "openai": return createOpenAIProvider({
290
+ apiKey: config.apiKey,
291
+ model: config.model,
292
+ baseUrl: config.baseUrl
293
+ });
294
+ case "gemini": return createGeminiProvider({
295
+ apiKey: config.apiKey,
296
+ model: config.model,
297
+ baseUrl: config.baseUrl
298
+ });
299
+ case "ollama": return createOllamaProvider({
300
+ baseUrl: config.baseUrl,
301
+ model: config.model,
302
+ apiKey: config.apiKey
303
+ });
304
+ default: {
305
+ const _exhaustive = config.provider;
306
+ throw new Error(`Unknown provider: ${String(_exhaustive)}`);
307
+ }
308
+ }
309
+ }
310
+ //#endregion
311
+ export { createProvider as n, base_exports as t };
312
+
313
+ //# sourceMappingURL=base-CWybHD_y.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base-CWybHD_y.mjs","names":["detectMediaType"],"sources":["../src/core/providers/anthropic.ts","../src/core/providers/gemini.ts","../src/core/providers/ollama.ts","../src/core/providers/openai.ts","../src/core/providers/base.ts"],"sourcesContent":["import type { AIProvider } from '../../types.js'\n\ntype AnthropicMessage = {\n role: 'user' | 'assistant'\n content: string | AnthropicContentBlock[]\n}\n\ntype AnthropicContentBlock =\n | { type: 'text'; text: string }\n | { type: 'image'; source: { type: 'base64'; media_type: string; data: string } }\n\ntype AnthropicResponse = {\n content: Array<{ type: string; text?: string }>\n usage?: { input_tokens: number; output_tokens: number }\n}\n\nfunction isAnthropicResponse(value: unknown): value is AnthropicResponse {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'content' in value &&\n Array.isArray((value as AnthropicResponse).content)\n )\n}\n\nexport type AnthropicProviderConfig = {\n apiKey: string\n model?: string\n baseUrl?: string\n}\n\nexport function createAnthropicProvider(configOrKey: AnthropicProviderConfig | string): AIProvider {\n const config: AnthropicProviderConfig =\n typeof configOrKey === 'string' ? { apiKey: configOrKey } : configOrKey\n const apiKey = config.apiKey\n const model = config.model ?? 'claude-sonnet-4-20250514'\n const baseUrl = (config.baseUrl ?? 'https://api.anthropic.com').replace(/\\/+$/, '')\n\n async function callAPI(messages: AnthropicMessage[]): Promise<AnthropicResponse> {\n const response = await fetch(`${baseUrl}/v1/messages`, {\n method: 'POST',\n headers: {\n 'x-api-key': apiKey,\n 'anthropic-version': '2023-06-01',\n 'content-type': 'application/json',\n },\n body: JSON.stringify({\n model,\n max_tokens: 8192,\n messages,\n }),\n })\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new Error(`Anthropic API error ${response.status}: ${errorText}`)\n }\n\n const data: unknown = await response.json()\n if (!isAnthropicResponse(data)) {\n throw new Error('Unexpected Anthropic API response shape')\n }\n return data\n }\n\n return {\n async generate(prompt: string, _outputSchema: Record<string, unknown>): Promise<unknown[]> {\n const messages: AnthropicMessage[] = [\n {\n role: 'user',\n content: `${prompt}\\n\\nRespond with ONLY a valid JSON array. No markdown, no explanation, just the JSON array.`,\n },\n ]\n\n const data = await callAPI(messages)\n const textBlock = data.content.find((block) => block.type === 'text')\n if (!textBlock || !('text' in textBlock) || typeof textBlock.text !== 'string') {\n throw new Error('No text content in Anthropic response')\n }\n\n const text = textBlock.text.trim()\n // Strip markdown code fences if present\n const jsonText = text\n .replace(/^```(?:json)?\\s*/i, '')\n .replace(/\\s*```\\s*$/, '')\n .trim()\n\n const parsed: unknown = JSON.parse(jsonText)\n if (!Array.isArray(parsed)) {\n throw new Error('Anthropic response is not a JSON array')\n }\n return parsed\n },\n\n async analyzeImage(imageBuffer: Buffer): Promise<string> {\n const base64 = imageBuffer.toString('base64')\n // Detect image type from buffer magic bytes\n let mediaType = 'image/jpeg'\n if (imageBuffer[0] === 0x89 && imageBuffer[1] === 0x50) mediaType = 'image/png'\n else if (imageBuffer[0] === 0x47 && imageBuffer[1] === 0x49) mediaType = 'image/gif'\n else if (imageBuffer[0] === 0x52 && imageBuffer[1] === 0x49) mediaType = 'image/webp'\n\n const messages: AnthropicMessage[] = [\n {\n role: 'user',\n content: [\n {\n type: 'image',\n source: { type: 'base64', media_type: mediaType, data: base64 },\n },\n {\n type: 'text',\n text: 'Describe this image concisely for use as alt text. Focus on the main subject and important visual details. Respond with only the alt text description, no extra explanation.',\n },\n ],\n },\n ]\n\n const data = await callAPI(messages)\n const textBlock = data.content.find((block) => block.type === 'text')\n if (!textBlock || !('text' in textBlock) || typeof textBlock.text !== 'string') {\n throw new Error('No text content in Anthropic image analysis response')\n }\n return textBlock.text.trim()\n },\n }\n}\n","import type { AIProvider } from '../../types.js'\n\ntype GeminiPart = { text: string } | { inline_data: { mime_type: string; data: string } }\n\ntype GeminiContent = {\n role: 'user' | 'model'\n parts: GeminiPart[]\n}\n\ntype GeminiResponse = {\n candidates?: Array<{\n content: { parts: Array<{ text?: string }>; role: string }\n finishReason: string\n }>\n usageMetadata?: {\n promptTokenCount: number\n candidatesTokenCount: number\n totalTokenCount: number\n }\n promptFeedback?: { blockReason?: string }\n}\n\nfunction isGeminiResponse(value: unknown): value is GeminiResponse {\n return typeof value === 'object' && value !== null && 'candidates' in value\n}\n\nfunction detectMediaType(buffer: Buffer): string {\n if (buffer[0] === 0x89 && buffer[1] === 0x50) return 'image/png'\n if (buffer[0] === 0x47 && buffer[1] === 0x49) return 'image/gif'\n if (buffer[0] === 0x52 && buffer[1] === 0x49) return 'image/webp'\n return 'image/jpeg'\n}\n\nfunction extractText(response: GeminiResponse): string {\n if (!response.candidates || response.candidates.length === 0) {\n const reason = response.promptFeedback?.blockReason ?? 'unknown'\n throw new Error(`Gemini request blocked: ${reason}`)\n }\n\n const candidate = response.candidates[0]\n const text = candidate.content.parts.find((p) => 'text' in p)?.text\n if (!text) {\n throw new Error('No text content in Gemini response')\n }\n\n if (candidate.finishReason === 'MAX_TOKENS') {\n throw new Error('Gemini response truncated (MAX_TOKENS) — output may be incomplete')\n }\n\n return text.trim()\n}\n\nexport type GeminiProviderConfig = {\n apiKey: string\n model?: string\n baseUrl?: string\n}\n\nexport function createGeminiProvider(config: GeminiProviderConfig): AIProvider {\n const model = config.model ?? 'gemini-2.5-flash'\n const baseUrl = config.baseUrl ?? 'https://generativelanguage.googleapis.com/v1beta'\n\n async function callAPI(\n contents: GeminiContent[],\n systemInstruction?: string,\n jsonMode?: boolean,\n ): Promise<GeminiResponse> {\n const body: Record<string, unknown> = { contents }\n\n if (systemInstruction) {\n body.system_instruction = { parts: [{ text: systemInstruction }] }\n }\n\n if (jsonMode) {\n body.generationConfig = {\n responseMimeType: 'application/json',\n }\n }\n\n const url = `${baseUrl}/models/${model}:generateContent`\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'x-goog-api-key': config.apiKey,\n 'content-type': 'application/json',\n },\n body: JSON.stringify(body),\n })\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new Error(`Gemini API error ${response.status}: ${errorText}`)\n }\n\n const data: unknown = await response.json()\n if (!isGeminiResponse(data)) {\n throw new Error('Unexpected Gemini API response shape')\n }\n return data\n }\n\n return {\n async generate(prompt: string, _outputSchema: Record<string, unknown>): Promise<unknown[]> {\n const contents: GeminiContent[] = [\n {\n role: 'user',\n parts: [\n {\n text: `${prompt}\\n\\nRespond with ONLY a valid JSON array. No markdown, no explanation, just the JSON array.`,\n },\n ],\n },\n ]\n\n const data = await callAPI(\n contents,\n 'You are a data generation assistant. Always respond with valid JSON arrays only.',\n true,\n )\n\n const text = extractText(data)\n const jsonText = text\n .replace(/^```(?:json)?\\s*/i, '')\n .replace(/\\s*```\\s*$/, '')\n .trim()\n\n const parsed: unknown = JSON.parse(jsonText)\n if (Array.isArray(parsed)) {\n return parsed\n }\n if (typeof parsed === 'object' && parsed !== null && 'items' in parsed) {\n const items = (parsed as { items: unknown }).items\n if (Array.isArray(items)) return items\n }\n throw new Error('Gemini response is not a JSON array')\n },\n\n async analyzeImage(imageBuffer: Buffer): Promise<string> {\n const base64 = imageBuffer.toString('base64')\n const mimeType = detectMediaType(imageBuffer)\n\n const contents: GeminiContent[] = [\n {\n role: 'user',\n parts: [\n { inline_data: { mime_type: mimeType, data: base64 } },\n {\n text: 'Describe this image concisely for use as alt text. Focus on the main subject and important visual details. Respond with only the alt text description, no extra explanation.',\n },\n ],\n },\n ]\n\n const data = await callAPI(contents)\n return extractText(data)\n },\n }\n}\n","import type { AIProvider } from '../../types.js'\n\n/**\n * OpenAI-compatible provider for local LLM servers.\n * Works with: Ollama, LocalAI, vLLM, LM Studio, and any\n * server exposing an OpenAI-compatible /v1/chat/completions endpoint.\n */\n\ntype ChatMessage = {\n role: 'user' | 'assistant' | 'system'\n content: string | ContentBlock[]\n}\n\ntype ContentBlock =\n | { type: 'text'; text: string }\n | { type: 'image_url'; image_url: { url: string } }\n\ntype ChatResponse = {\n choices: Array<{\n message: { role: string; content: string | null }\n }>\n}\n\nfunction isChatResponse(value: unknown): value is ChatResponse {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'choices' in value &&\n Array.isArray((value as ChatResponse).choices)\n )\n}\n\nfunction detectMediaType(buffer: Buffer): string {\n if (buffer[0] === 0x89 && buffer[1] === 0x50) return 'image/png'\n if (buffer[0] === 0x47 && buffer[1] === 0x49) return 'image/gif'\n if (buffer[0] === 0x52 && buffer[1] === 0x49) return 'image/webp'\n return 'image/jpeg'\n}\n\nexport type OllamaProviderConfig = {\n baseUrl?: string\n model?: string\n apiKey?: string\n}\n\nexport function createOllamaProvider(config: OllamaProviderConfig): AIProvider {\n const baseUrl = (config.baseUrl ?? 'http://localhost:11434').replace(/\\/+$/, '')\n const model = config.model ?? 'llama3.3:8b'\n const apiKey = config.apiKey ?? 'ollama'\n\n async function callAPI(messages: ChatMessage[], jsonMode: boolean): Promise<ChatResponse> {\n const body: Record<string, unknown> = { model, messages }\n if (jsonMode) {\n body.response_format = { type: 'json_object' }\n }\n\n const response = await fetch(`${baseUrl}/v1/chat/completions`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${apiKey}`,\n 'content-type': 'application/json',\n },\n body: JSON.stringify(body),\n })\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new Error(`Ollama API error ${response.status}: ${errorText}`)\n }\n\n const data: unknown = await response.json()\n if (!isChatResponse(data)) {\n throw new Error('Unexpected API response shape from local LLM server')\n }\n return data\n }\n\n return {\n async generate(prompt: string, _outputSchema: Record<string, unknown>): Promise<unknown[]> {\n const messages: ChatMessage[] = [\n {\n role: 'system',\n content:\n 'You are a data generation assistant. Always respond with valid JSON only. When asked for an array, wrap it in {\"items\": [...]} so json_object mode is satisfied.',\n },\n {\n role: 'user',\n content: `${prompt}\\n\\nRespond with JSON object {\"items\": [...]} where items is the array of generated documents.`,\n },\n ]\n\n const data = await callAPI(messages, true)\n const choice = data.choices[0]\n if (!choice || choice.message.content === null) {\n throw new Error('No content in response from local LLM server')\n }\n\n const parsed: unknown = JSON.parse(choice.message.content)\n if (\n typeof parsed === 'object' &&\n parsed !== null &&\n 'items' in parsed &&\n Array.isArray((parsed as { items: unknown }).items)\n ) {\n return (parsed as { items: unknown[] }).items\n }\n if (Array.isArray(parsed)) {\n return parsed\n }\n throw new Error('Local LLM response is not a JSON array')\n },\n\n async analyzeImage(imageBuffer: Buffer): Promise<string> {\n const base64 = imageBuffer.toString('base64')\n const mediaType = detectMediaType(imageBuffer)\n const dataUrl = `data:${mediaType};base64,${base64}`\n\n const messages: ChatMessage[] = [\n {\n role: 'user',\n content: [\n { type: 'image_url', image_url: { url: dataUrl } },\n {\n type: 'text',\n text: 'Describe this image concisely for use as alt text. Focus on the main subject and important visual details. Respond with only the alt text description, no extra explanation.',\n },\n ],\n },\n ]\n\n const data = await callAPI(messages, false)\n const choice = data.choices[0]\n if (!choice || choice.message.content === null) {\n throw new Error('No content in image analysis response from local LLM server')\n }\n return (choice.message.content as string).trim()\n },\n }\n}\n","import type { AIProvider } from '../../types.js'\n\ntype OpenAIMessage = {\n role: 'user' | 'assistant' | 'system'\n content: string | OpenAIContentBlock[]\n}\n\ntype OpenAIContentBlock =\n | { type: 'text'; text: string }\n | { type: 'image_url'; image_url: { url: string } }\n\ntype OpenAIResponse = {\n choices: Array<{\n message: { role: string; content: string | null }\n }>\n usage?: { prompt_tokens: number; completion_tokens: number; total_tokens: number }\n}\n\nfunction isOpenAIResponse(value: unknown): value is OpenAIResponse {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'choices' in value &&\n Array.isArray((value as OpenAIResponse).choices)\n )\n}\n\nexport type OpenAIProviderConfig = {\n apiKey: string\n model?: string\n baseUrl?: string\n}\n\nexport function createOpenAIProvider(configOrKey: OpenAIProviderConfig | string): AIProvider {\n const config: OpenAIProviderConfig =\n typeof configOrKey === 'string' ? { apiKey: configOrKey } : configOrKey\n const apiKey = config.apiKey\n const model = config.model ?? 'gpt-4o'\n const baseUrl = (config.baseUrl ?? 'https://api.openai.com').replace(/\\/+$/, '')\n\n async function callAPI(messages: OpenAIMessage[], jsonMode: boolean): Promise<OpenAIResponse> {\n const body: Record<string, unknown> = {\n model,\n messages,\n }\n if (jsonMode) {\n body.response_format = { type: 'json_object' }\n }\n\n const response = await fetch(`${baseUrl}/v1/chat/completions`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${apiKey}`,\n 'content-type': 'application/json',\n },\n body: JSON.stringify(body),\n })\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new Error(`OpenAI API error ${response.status}: ${errorText}`)\n }\n\n const data: unknown = await response.json()\n if (!isOpenAIResponse(data)) {\n throw new Error('Unexpected OpenAI API response shape')\n }\n return data\n }\n\n return {\n async generate(prompt: string, _outputSchema: Record<string, unknown>): Promise<unknown[]> {\n const messages: OpenAIMessage[] = [\n {\n role: 'system',\n content:\n 'You are a data generation assistant. Always respond with valid JSON only. When asked for an array, wrap it in {\"items\": [...]} so json_object mode is satisfied.',\n },\n {\n role: 'user',\n content: `${prompt}\\n\\nRespond with JSON object {\"items\": [...]} where items is the array of generated documents.`,\n },\n ]\n\n const data = await callAPI(messages, true)\n const choice = data.choices[0]\n if (!choice || choice.message.content === null) {\n throw new Error('No content in OpenAI response')\n }\n\n const parsed: unknown = JSON.parse(choice.message.content)\n if (\n typeof parsed === 'object' &&\n parsed !== null &&\n 'items' in parsed &&\n Array.isArray((parsed as { items: unknown }).items)\n ) {\n return (parsed as { items: unknown[] }).items\n }\n if (Array.isArray(parsed)) {\n return parsed\n }\n throw new Error('OpenAI response is not a JSON array')\n },\n\n async analyzeImage(imageBuffer: Buffer): Promise<string> {\n const base64 = imageBuffer.toString('base64')\n // Detect image type from buffer magic bytes\n let mediaType = 'image/jpeg'\n if (imageBuffer[0] === 0x89 && imageBuffer[1] === 0x50) mediaType = 'image/png'\n else if (imageBuffer[0] === 0x47 && imageBuffer[1] === 0x49) mediaType = 'image/gif'\n else if (imageBuffer[0] === 0x52 && imageBuffer[1] === 0x49) mediaType = 'image/webp'\n\n const dataUrl = `data:${mediaType};base64,${base64}`\n\n const messages: OpenAIMessage[] = [\n {\n role: 'user',\n content: [\n {\n type: 'image_url',\n image_url: { url: dataUrl },\n },\n {\n type: 'text',\n text: 'Describe this image concisely for use as alt text. Focus on the main subject and important visual details. Respond with only the alt text description, no extra explanation.',\n },\n ],\n },\n ]\n\n const data = await callAPI(messages, false)\n const choice = data.choices[0]\n if (!choice || choice.message.content === null) {\n throw new Error('No content in OpenAI image analysis response')\n }\n return choice.message.content.trim()\n },\n }\n}\n","import type { AIProvider } from '../../types.js'\nimport { createAnthropicProvider } from './anthropic.js'\nimport { createGeminiProvider } from './gemini.js'\nimport { createOllamaProvider } from './ollama.js'\nimport { createOpenAIProvider } from './openai.js'\n\nexport type { AIProvider }\n\nexport type ProviderConfig = {\n provider: 'anthropic' | 'openai' | 'gemini' | 'ollama'\n apiKey: string\n baseUrl?: string\n model?: string\n}\n\nexport function createProvider(config: ProviderConfig): AIProvider {\n switch (config.provider) {\n case 'anthropic':\n return createAnthropicProvider({\n apiKey: config.apiKey,\n model: config.model,\n baseUrl: config.baseUrl,\n })\n case 'openai':\n return createOpenAIProvider({\n apiKey: config.apiKey,\n model: config.model,\n baseUrl: config.baseUrl,\n })\n case 'gemini':\n return createGeminiProvider({\n apiKey: config.apiKey,\n model: config.model,\n baseUrl: config.baseUrl,\n })\n case 'ollama':\n return createOllamaProvider({\n baseUrl: config.baseUrl,\n model: config.model,\n apiKey: config.apiKey,\n })\n default: {\n const _exhaustive: never = config.provider\n throw new Error(`Unknown provider: ${String(_exhaustive)}`)\n }\n }\n}\n"],"mappings":"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"}