diff --git a/.changeset/two-bikes-kneel.md b/.changeset/two-bikes-kneel.md new file mode 100644 index 00000000..49440cf4 --- /dev/null +++ b/.changeset/two-bikes-kneel.md @@ -0,0 +1,9 @@ +--- +'@tanstack/ai-anthropic': minor +'@tanstack/ai-gemini': minor +'@tanstack/ai-ollama': minor +'@tanstack/ai-openai': minor +'@tanstack/ai': minor +--- + +Split up adapters for better tree shaking into separate functionalities diff --git a/README.md b/README.md index 77acf865..4b3d3750 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,9 @@ A powerful, type-safe AI SDK for building AI-powered applications. - Provider-agnostic adapters (OpenAI, Anthropic, Gemini, Ollama, etc.) +- **Tree-shakeable adapters** - Import only what you need for smaller bundles - **Multimodal content support** - Send images, audio, video, and documents +- **Image generation** - Generate images with OpenAI DALL-E/GPT-Image and Gemini Imagen - Chat completion, streaming, and agent loop strategies - Headless chat state management with adapters (SSE, HTTP stream, custom) - Isomorphic type-safe tools with server/client execution @@ -46,6 +48,30 @@ A powerful, type-safe AI SDK for building AI-powered applications. ### Read the docs → +## Tree-Shakeable Adapters + +Import only the functionality you need for smaller bundle sizes: + +```typescript +// Only chat functionality - no embedding or summarization code bundled +import { openaiText } from '@tanstack/ai-openai/adapters' +import { generate } from '@tanstack/ai' + +const textAdapter = openaiText() + +const result = generate({ + adapter: textAdapter, + model: 'gpt-4o', + messages: [{ role: 'user', content: [{ type: 'text', content: 'Hello!' }] }], +}) + +for await (const chunk of result) { + console.log(chunk) +} +``` + +Available adapters: `openaiText`, `openaiEmbed`, `openaiSummarize`, `anthropicText`, `geminiText`, `ollamaText`, and more. + ## Bonus: TanStack Start Integration TanStack AI works with **any** framework (Next.js, Express, Remix, etc.). diff --git a/docs/adapters/anthropic.md b/docs/adapters/anthropic.md index 422f68bf..5f255a49 100644 --- a/docs/adapters/anthropic.md +++ b/docs/adapters/anthropic.md @@ -1,9 +1,9 @@ --- title: Anthropic Adapter -slug: /adapters/anthropic +id: anthropic-adapter --- -The Anthropic adapter provides access to Claude models, including Claude 3.5 Sonnet, Claude 3 Opus, and more. +The Anthropic adapter provides access to Claude models, including Claude Sonnet 4.5, Claude Opus 4.5, and more. ## Installation @@ -14,63 +14,72 @@ npm install @tanstack/ai-anthropic ## Basic Usage ```typescript -import { chat } from "@tanstack/ai"; -import { anthropic } from "@tanstack/ai-anthropic"; +import { ai } from "@tanstack/ai"; +import { anthropicText } from "@tanstack/ai-anthropic"; -const adapter = anthropic(); +const adapter = anthropicText(); -const stream = chat({ +const stream = ai({ adapter, messages: [{ role: "user", content: "Hello!" }], - model: "claude-3-5-sonnet-20241022", + model: "claude-sonnet-4-5-20250929", }); ``` ## Basic Usage - Custom API Key ```typescript -import { chat } from "@tanstack/ai"; -import { createAnthropic } from "@tanstack/ai-anthropic"; +import { ai } from "@tanstack/ai"; +import { createAnthropicText } from "@tanstack/ai-anthropic"; -const adapter = createAnthropic(process.env.ANTHROPIC_API_KEY, { +const adapter = createAnthropicText(process.env.ANTHROPIC_API_KEY!, { // ... your config options - }); +}); -const stream = chat({ +const stream = ai({ adapter, messages: [{ role: "user", content: "Hello!" }], - model: "claude-3-5-sonnet-20241022", + model: "claude-sonnet-4-5-20250929", }); ``` ## Configuration ```typescript -import { anthropic, type AnthropicConfig } from "@tanstack/ai-anthropic"; +import { createAnthropicText, type AnthropicTextConfig } from "@tanstack/ai-anthropic"; -const config: AnthropicConfig = { - // ... your config options +const config: AnthropicTextConfig = { + baseURL: "https://api.anthropic.com", // Optional, for custom endpoints }; -const adapter = anthropic(config); +const adapter = createAnthropicText(process.env.ANTHROPIC_API_KEY!, config); ``` - + +## Available Models + +### Chat Models + +- `claude-sonnet-4-5-20250929` - Claude Sonnet 4.5 (balanced) +- `claude-opus-4-5-20251101` - Claude Opus 4.5 (most capable) +- `claude-haiku-4-0-20250514` - Claude Haiku 4.0 (fastest) +- `claude-3-5-sonnet-20241022` - Claude 3.5 Sonnet +- `claude-3-opus-20240229` - Claude 3 Opus ## Example: Chat Completion ```typescript -import { chat, toStreamResponse } from "@tanstack/ai"; -import { anthropic } from "@tanstack/ai-anthropic"; +import { ai, toStreamResponse } from "@tanstack/ai"; +import { anthropicText } from "@tanstack/ai-anthropic"; -const adapter = anthropic(); +const adapter = anthropicText(); export async function POST(request: Request) { const { messages } = await request.json(); - const stream = chat({ + const stream = ai({ adapter, messages, - model: "claude-3-5-sonnet-20241022", + model: "claude-sonnet-4-5-20250929", }); return toStreamResponse(stream); @@ -80,11 +89,11 @@ export async function POST(request: Request) { ## Example: With Tools ```typescript -import { chat, toolDefinition } from "@tanstack/ai"; -import { anthropic } from "@tanstack/ai-anthropic"; +import { ai, toolDefinition } from "@tanstack/ai"; +import { anthropicText } from "@tanstack/ai-anthropic"; import { z } from "zod"; -const adapter = anthropic(); +const adapter = anthropicText(); const searchDatabaseDef = toolDefinition({ name: "search_database", @@ -96,43 +105,39 @@ const searchDatabaseDef = toolDefinition({ const searchDatabase = searchDatabaseDef.server(async ({ query }) => { // Search database - return { results: [...] }; + return { results: [] }; }); -const stream = chat({ +const stream = ai({ adapter, messages, - model: "claude-3-5-sonnet-20241022", + model: "claude-sonnet-4-5-20250929", tools: [searchDatabase], }); ``` ## Provider Options -Anthropic supports provider-specific options: +Anthropic supports various provider-specific options: ```typescript -const stream = chat({ - adapter: anthropic(), +const stream = ai({ + adapter: anthropicText(), messages, - model: "claude-3-5-sonnet-20241022", + model: "claude-sonnet-4-5-20250929", providerOptions: { - thinking: { - type: "enabled", - budgetTokens: 1000, - }, - cacheControl: { - type: "ephemeral", - ttl: "5m", - }, - sendReasoning: true, + max_tokens: 4096, + temperature: 0.7, + top_p: 0.9, + top_k: 40, + stop_sequences: ["END"], }, }); ``` ### Thinking (Extended Thinking) -Enable extended thinking with a token budget. This allows Claude to show its reasoning process, which is streamed as `thinking` chunks and displayed as `ThinkingPart` in messages: +Enable extended thinking with a token budget. This allows Claude to show its reasoning process, which is streamed as `thinking` chunks: ```typescript providerOptions: { @@ -154,23 +159,51 @@ When thinking is enabled, the model's reasoning process is streamed separately f ### Prompt Caching -Cache prompts for better performance: +Cache prompts for better performance and reduced costs: ```typescript -messages: [ - { role: "user", content: [{ - type: "text", - content: "What is the capital of France?", - metadata: { - cache_control: { - type: "ephemeral", - ttl: "5m", - } - } - }]} -] +const stream = ai({ + adapter: anthropicText(), + messages: [ + { + role: "user", + content: [ + { + type: "text", + content: "What is the capital of France?", + metadata: { + cache_control: { + type: "ephemeral", + }, + }, + }, + ], + }, + ], + model: "claude-sonnet-4-5-20250929", +}); ``` +## Summarization + +Anthropic supports text summarization: + +```typescript +import { ai } from "@tanstack/ai"; +import { anthropicSummarize } from "@tanstack/ai-anthropic"; + +const adapter = anthropicSummarize(); + +const result = await ai({ + adapter, + model: "claude-sonnet-4-5-20250929", + text: "Your long text to summarize...", + maxLength: 100, + style: "concise", // "concise" | "bullet-points" | "paragraph" +}); + +console.log(result.summary); +``` ## Environment Variables @@ -182,15 +215,44 @@ ANTHROPIC_API_KEY=sk-ant-... ## API Reference -### `anthropic(config)` +### `anthropicText(config?)` + +Creates an Anthropic text/chat adapter using environment variables. + +**Returns:** An Anthropic text adapter instance. + +### `createAnthropicText(apiKey, config?)` + +Creates an Anthropic text/chat adapter with an explicit API key. + +**Parameters:** + +- `apiKey` - Your Anthropic API key +- `config.baseURL?` - Custom base URL (optional) + +**Returns:** An Anthropic text adapter instance. + +### `anthropicSummarize(config?)` + +Creates an Anthropic summarization adapter using environment variables. -Creates an Anthropic adapter instance. +**Returns:** An Anthropic summarize adapter instance. + +### `createAnthropicSummarize(apiKey, config?)` + +Creates an Anthropic summarization adapter with an explicit API key. **Parameters:** -- `config.apiKey` - Anthropic API key (required) +- `apiKey` - Your Anthropic API key +- `config.baseURL?` - Custom base URL (optional) + +**Returns:** An Anthropic summarize adapter instance. + +## Limitations -**Returns:** An Anthropic adapter instance. +- **Embeddings**: Anthropic does not support embeddings natively. Use OpenAI or Gemini for embedding needs. +- **Image Generation**: Anthropic does not support image generation. Use OpenAI or Gemini for image generation. ## Next Steps diff --git a/docs/adapters/gemini.md b/docs/adapters/gemini.md index 6dbb14cc..e8a938a6 100644 --- a/docs/adapters/gemini.md +++ b/docs/adapters/gemini.md @@ -3,7 +3,7 @@ title: Gemini Adapter id: gemini-adapter --- -The Google Gemini adapter provides access to Google's Gemini models, including Gemini Pro and Gemini Ultra. +The Google Gemini adapter provides access to Google's Gemini models, including text generation, embeddings, image generation with Imagen, and experimental text-to-speech. ## Installation @@ -14,68 +14,86 @@ npm install @tanstack/ai-gemini ## Basic Usage ```typescript -import { chat } from "@tanstack/ai"; -import { gemini } from "@tanstack/ai-gemini"; +import { ai } from "@tanstack/ai"; +import { geminiText } from "@tanstack/ai-gemini"; -const adapter = gemini(); +const adapter = geminiText(); -const stream = chat({ +const stream = ai({ adapter, messages: [{ role: "user", content: "Hello!" }], - model: "gemini-2.5-pro", + model: "gemini-2.0-flash-exp", }); ``` ## Basic Usage - Custom API Key ```typescript -import { chat } from "@tanstack/ai"; -import { createGemini } from "@tanstack/ai-gemini"; -const adapter = createGemini(process.env.GEMINI_API_KEY, { +import { ai } from "@tanstack/ai"; +import { createGeminiText } from "@tanstack/ai-gemini"; + +const adapter = createGeminiText(process.env.GEMINI_API_KEY!, { // ... your config options - }); -const stream = chat({ +}); + +const stream = ai({ adapter, messages: [{ role: "user", content: "Hello!" }], - model: "gemini-2.5-pro", + model: "gemini-2.0-flash-exp", }); ``` ## Configuration ```typescript -import { gemini, type GeminiConfig } from "@tanstack/ai-gemini"; +import { createGeminiText, type GeminiTextConfig } from "@tanstack/ai-gemini"; -const config: GeminiConfig = { - baseURL: "https://generativelanguage.googleapis.com/v1", // Optional +const config: GeminiTextConfig = { + baseURL: "https://generativelanguage.googleapis.com/v1beta", // Optional }; -const adapter = gemini(config); +const adapter = createGeminiText(process.env.GEMINI_API_KEY!, config); ``` ## Available Models ### Chat Models -- `gemini-2.5-pro` - Gemini Pro model -- `gemini-2.5-pro-vision` - Gemini Pro with vision capabilities -- `gemini-ultra` - Gemini Ultra model (when available) +- `gemini-2.0-flash-exp` - Gemini 2.0 Flash (fast, efficient) +- `gemini-2.0-flash-lite` - Gemini 2.0 Flash Lite (fastest) +- `gemini-2.5-pro` - Gemini 2.5 Pro (most capable) +- `gemini-2.5-flash` - Gemini 2.5 Flash +- `gemini-exp-1206` - Experimental Pro model + +### Embedding Models + +- `gemini-embedding-001` - Text embedding model +- `text-embedding-004` - Latest embedding model + +### Image Generation Models + +- `imagen-3.0-generate-002` - Imagen 3.0 +- `gemini-2.0-flash-preview-image-generation` - Gemini with image generation + +### Text-to-Speech Models (Experimental) + +- `gemini-2.5-flash-preview-tts` - Gemini TTS ## Example: Chat Completion ```typescript -import { chat, toStreamResponse } from "@tanstack/ai"; -import { gemini } from "@tanstack/ai-gemini"; +import { ai, toStreamResponse } from "@tanstack/ai"; +import { geminiText } from "@tanstack/ai-gemini"; -const adapter = gemini(); +const adapter = geminiText(); export async function POST(request: Request) { const { messages } = await request.json(); - const stream = chat({ + const stream = ai({ adapter, messages, - model: "gemini-2.5-pro", + model: "gemini-2.0-flash-exp", }); return toStreamResponse(stream); @@ -85,15 +103,15 @@ export async function POST(request: Request) { ## Example: With Tools ```typescript -import { chat, toolDefinition } from "@tanstack/ai"; -import { gemini } from "@tanstack/ai-gemini"; +import { ai, toolDefinition } from "@tanstack/ai"; +import { geminiText } from "@tanstack/ai-gemini"; import { z } from "zod"; -const adapter = gemini(); +const adapter = geminiText(); const getCalendarEventsDef = toolDefinition({ name: "get_calendar_events", - description: "Get calendar events", + description: "Get calendar events for a date", inputSchema: z.object({ date: z.string(), }), @@ -101,13 +119,13 @@ const getCalendarEventsDef = toolDefinition({ const getCalendarEvents = getCalendarEventsDef.server(async ({ date }) => { // Fetch calendar events - return { events: [...] }; + return { events: [] }; }); -const stream = chat({ +const stream = ai({ adapter, messages, - model: "gemini-2.5-pro", + model: "gemini-2.0-flash-exp", tools: [getCalendarEvents], }); ``` @@ -117,43 +135,247 @@ const stream = chat({ Gemini supports various provider-specific options: ```typescript -const stream = chat({ - adapter: gemini(), +const stream = ai({ + adapter: geminiText(), messages, - model: "gemini-2.5-pro", - providerOptions: { - maxOutputTokens: 1000, + model: "gemini-2.0-flash-exp", + providerOptions: { + maxOutputTokens: 2048, + temperature: 0.7, + topP: 0.9, topK: 40, + stopSequences: ["END"], + }, +}); +``` + +### Thinking + +Enable thinking for models that support it: + +```typescript +providerOptions: { + thinking: { + includeThoughts: true, + }, +} +``` + +### Structured Output + +Configure structured output format: + +```typescript +providerOptions: { + responseMimeType: "application/json", +} +``` + +## Embeddings + +Generate text embeddings for semantic search and similarity: + +```typescript +import { ai } from "@tanstack/ai"; +import { geminiEmbed } from "@tanstack/ai-gemini"; + +const adapter = geminiEmbed(); + +const result = await ai({ + adapter, + model: "gemini-embedding-001", + input: "The quick brown fox jumps over the lazy dog", +}); + +console.log(result.embeddings); +``` + +### Batch Embeddings + +```typescript +const result = await ai({ + adapter: geminiEmbed(), + model: "gemini-embedding-001", + input: [ + "First text to embed", + "Second text to embed", + "Third text to embed", + ], +}); +``` + +### Embedding Provider Options + +```typescript +const result = await ai({ + adapter: geminiEmbed(), + model: "gemini-embedding-001", + input: "...", + providerOptions: { + taskType: "RETRIEVAL_DOCUMENT", // or "RETRIEVAL_QUERY", "SEMANTIC_SIMILARITY", etc. }, }); ``` +## Summarization + +Summarize long text content: + +```typescript +import { ai } from "@tanstack/ai"; +import { geminiSummarize } from "@tanstack/ai-gemini"; + +const adapter = geminiSummarize(); + +const result = await ai({ + adapter, + model: "gemini-2.0-flash-exp", + text: "Your long text to summarize...", + maxLength: 100, + style: "concise", // "concise" | "bullet-points" | "paragraph" +}); + +console.log(result.summary); +``` + +## Image Generation + +Generate images with Imagen: + +```typescript +import { ai } from "@tanstack/ai"; +import { geminiImage } from "@tanstack/ai-gemini"; + +const adapter = geminiImage(); + +const result = await ai({ + adapter, + model: "imagen-3.0-generate-002", + prompt: "A futuristic cityscape at sunset", + numberOfImages: 1, +}); + +console.log(result.images); +``` + +### Image Provider Options + +```typescript +const result = await ai({ + adapter: geminiImage(), + model: "imagen-3.0-generate-002", + prompt: "...", + providerOptions: { + aspectRatio: "16:9", // "1:1" | "3:4" | "4:3" | "9:16" | "16:9" + personGeneration: "DONT_ALLOW", // Control person generation + safetyFilterLevel: "BLOCK_SOME", // Safety filtering + }, +}); +``` + +## Text-to-Speech (Experimental) + +> **Note:** Gemini TTS is experimental and may require the Live API for full functionality. + +Generate speech from text: + +```typescript +import { ai } from "@tanstack/ai"; +import { geminiTTS } from "@tanstack/ai-gemini"; + +const adapter = geminiTTS(); + +const result = await ai({ + adapter, + model: "gemini-2.5-flash-preview-tts", + text: "Hello from Gemini TTS!", +}); + +console.log(result.audio); // Base64 encoded audio +``` + ## Environment Variables Set your API key in environment variables: ```bash GEMINI_API_KEY=your-api-key-here +# or +GOOGLE_API_KEY=your-api-key-here ``` ## Getting an API Key -1. Go to [Google AI Studio](https://makersuite.google.com/app/apikey) +1. Go to [Google AI Studio](https://aistudio.google.com/apikey) 2. Create a new API key 3. Add it to your environment variables ## API Reference -### `gemini(config)` +### `geminiText(config?)` + +Creates a Gemini text/chat adapter using environment variables. -Creates a Gemini adapter instance. +**Returns:** A Gemini text adapter instance. + +### `createGeminiText(apiKey, config?)` + +Creates a Gemini text/chat adapter with an explicit API key. **Parameters:** -- `config.apiKey` - Gemini API key (required) +- `apiKey` - Your Gemini API key - `config.baseURL?` - Custom base URL (optional) -**Returns:** A Gemini adapter instance. +**Returns:** A Gemini text adapter instance. + +### `geminiEmbed(config?)` + +Creates a Gemini embedding adapter using environment variables. + +**Returns:** A Gemini embed adapter instance. + +### `createGeminiEmbed(apiKey, config?)` + +Creates a Gemini embedding adapter with an explicit API key. + +**Returns:** A Gemini embed adapter instance. + +### `geminiSummarize(config?)` + +Creates a Gemini summarization adapter using environment variables. + +**Returns:** A Gemini summarize adapter instance. + +### `createGeminiSummarize(apiKey, config?)` + +Creates a Gemini summarization adapter with an explicit API key. + +**Returns:** A Gemini summarize adapter instance. + +### `geminiImage(config?)` + +Creates a Gemini image generation adapter using environment variables. + +**Returns:** A Gemini image adapter instance. + +### `createGeminiImage(apiKey, config?)` + +Creates a Gemini image generation adapter with an explicit API key. + +**Returns:** A Gemini image adapter instance. + +### `geminiTTS(config?)` + +Creates a Gemini TTS adapter using environment variables. + +**Returns:** A Gemini TTS adapter instance. + +### `createGeminiTTS(apiKey, config?)` + +Creates a Gemini TTS adapter with an explicit API key. + +**Returns:** A Gemini TTS adapter instance. ## Next Steps diff --git a/docs/adapters/ollama.md b/docs/adapters/ollama.md index 9fbf65bd..fb9e20ab 100644 --- a/docs/adapters/ollama.md +++ b/docs/adapters/ollama.md @@ -3,7 +3,7 @@ title: Ollama Adapter id: ollama-adapter --- -The Ollama adapter provides access to local models running via Ollama, allowing you to run AI models on your own infrastructure. +The Ollama adapter provides access to local models running via Ollama, allowing you to run AI models on your own infrastructure with full privacy and no API costs. ## Installation @@ -14,14 +14,27 @@ npm install @tanstack/ai-ollama ## Basic Usage ```typescript -import { chat } from "@tanstack/ai"; -import { ollama } from "@tanstack/ai-ollama"; +import { ai } from "@tanstack/ai"; +import { ollamaText } from "@tanstack/ai-ollama"; -const adapter = ollama({ - baseURL: "http://localhost:11434", // Default Ollama URL +const adapter = ollamaText(); + +const stream = ai({ + adapter, + messages: [{ role: "user", content: "Hello!" }], + model: "llama3", }); +``` + +## Basic Usage - Custom Host -const stream = chat({ +```typescript +import { ai } from "@tanstack/ai"; +import { createOllamaText } from "@tanstack/ai-ollama"; + +const adapter = createOllamaText("http://your-server:11434"); + +const stream = ai({ adapter, messages: [{ role: "user", content: "Hello!" }], model: "llama3", @@ -31,41 +44,49 @@ const stream = chat({ ## Configuration ```typescript -import { ollama, type OllamaConfig } from "@tanstack/ai-ollama"; +import { createOllamaText } from "@tanstack/ai-ollama"; -const config: OllamaConfig = { - baseURL: "http://localhost:11434", // Ollama server URL - // No API key needed for local Ollama -}; +// Default localhost +const adapter = createOllamaText(); -const adapter = ollama(config); +// Custom host +const adapter = createOllamaText("http://your-server:11434"); ``` -## Available Models +## Available Models -To see available models, run: +To see available models on your Ollama instance: ```bash ollama list ``` +### Popular Models + +- `llama3` / `llama3.1` / `llama3.2` - Meta's Llama models +- `mistral` / `mistral:7b` - Mistral AI models +- `mixtral` - Mixtral MoE model +- `codellama` - Code-focused Llama +- `phi3` - Microsoft's Phi models +- `gemma` / `gemma2` - Google's Gemma models +- `qwen2` / `qwen2.5` - Alibaba's Qwen models +- `deepseek-coder` - DeepSeek coding model + ## Example: Chat Completion ```typescript -import { chat, toStreamResponse } from "@tanstack/ai"; -import { ollama } from "@tanstack/ai-ollama"; +import { ai, toStreamResponse } from "@tanstack/ai"; +import { ollamaText } from "@tanstack/ai-ollama"; -const adapter = ollama({ - baseURL: "http://localhost:11434", -}); +const adapter = ollamaText(); export async function POST(request: Request) { const { messages } = await request.json(); - const stream = chat({ + const stream = ai({ adapter, messages, - model: "llama3", // Use a model you have installed + model: "llama3", }); return toStreamResponse(stream); @@ -75,13 +96,11 @@ export async function POST(request: Request) { ## Example: With Tools ```typescript -import { chat, toolDefinition } from "@tanstack/ai"; -import { ollama } from "@tanstack/ai-ollama"; +import { ai, toolDefinition } from "@tanstack/ai"; +import { ollamaText } from "@tanstack/ai-ollama"; import { z } from "zod"; -const adapter = ollama({ - baseURL: "http://localhost:11434", -}); +const adapter = ollamaText(); const getLocalDataDef = toolDefinition({ name: "get_local_data", @@ -96,7 +115,7 @@ const getLocalData = getLocalDataDef.server(async ({ key }) => { return { data: "..." }; }); -const stream = chat({ +const stream = ai({ adapter, messages, model: "llama3", @@ -104,78 +123,235 @@ const stream = chat({ }); ``` -## Setting Up Ollama - -1. **Install Ollama:** - - ```bash - # macOS - brew install ollama - - # Linux - curl -fsSL https://ollama.com/install.sh | sh - - # Windows - # Download from https://ollama.com - ``` - -2. **Pull a model:** - - ```bash - ollama pull llama3 - ``` - -3. **Start Ollama server:** - ```bash - ollama serve - ``` +**Note:** Tool support varies by model. Models like `llama3`, `mistral`, and `qwen2` generally have good tool calling support. ## Provider Options Ollama supports various provider-specific options: ```typescript -const stream = chat({ - adapter: ollama({ baseURL: "http://localhost:11434" }), +const stream = ai({ + adapter: ollamaText(), messages, model: "llama3", providerOptions: { temperature: 0.7, - numPredict: 1000, - topP: 0.9, - topK: 40, + top_p: 0.9, + top_k: 40, + num_predict: 1000, // Max tokens to generate + repeat_penalty: 1.1, + num_ctx: 4096, // Context window size + num_gpu: -1, // GPU layers (-1 = auto) }, }); ``` -## Custom Ollama Server +### Advanced Options + +```typescript +providerOptions: { + // Sampling + temperature: 0.7, + top_p: 0.9, + top_k: 40, + min_p: 0.05, + typical_p: 1.0, + + // Generation + num_predict: 1000, + repeat_penalty: 1.1, + repeat_last_n: 64, + penalize_newline: false, + + // Performance + num_ctx: 4096, + num_batch: 512, + num_gpu: -1, + num_thread: 0, // 0 = auto + + // Memory + use_mmap: true, + use_mlock: false, + + // Mirostat sampling + mirostat: 0, // 0 = disabled, 1 = Mirostat, 2 = Mirostat 2.0 + mirostat_tau: 5.0, + mirostat_eta: 0.1, +} +``` + +## Embeddings -If you're running Ollama on a different host or port: +Generate text embeddings locally: ```typescript -const adapter = ollama({ - baseURL: "http://your-server:11434", +import { ai } from "@tanstack/ai"; +import { ollamaEmbed } from "@tanstack/ai-ollama"; + +const adapter = ollamaEmbed(); + +const result = await ai({ + adapter, + model: "nomic-embed-text", // or "mxbai-embed-large" + input: "The quick brown fox jumps over the lazy dog", }); + +console.log(result.embeddings); +``` + +### Embedding Models + +First, pull an embedding model: + +```bash +ollama pull nomic-embed-text +# or +ollama pull mxbai-embed-large +``` + +### Batch Embeddings + +```typescript +const result = await ai({ + adapter: ollamaEmbed(), + model: "nomic-embed-text", + input: [ + "First text to embed", + "Second text to embed", + "Third text to embed", + ], +}); +``` + +## Summarization + +Summarize long text content locally: + +```typescript +import { ai } from "@tanstack/ai"; +import { ollamaSummarize } from "@tanstack/ai-ollama"; + +const adapter = ollamaSummarize(); + +const result = await ai({ + adapter, + model: "llama3", + text: "Your long text to summarize...", + maxLength: 100, + style: "concise", // "concise" | "bullet-points" | "paragraph" +}); + +console.log(result.summary); +``` + +## Setting Up Ollama + +### 1. Install Ollama + +```bash +# macOS +brew install ollama + +# Linux +curl -fsSL https://ollama.com/install.sh | sh + +# Windows +# Download from https://ollama.com +``` + +### 2. Pull a Model + +```bash +ollama pull llama3 +``` + +### 3. Start Ollama Server + +```bash +ollama serve +``` + +The server runs on `http://localhost:11434` by default. + +## Running on a Remote Server + +```typescript +const adapter = createOllamaText("http://your-server:11434"); +``` + +To expose Ollama on a network interface: + +```bash +OLLAMA_HOST=0.0.0.0:11434 ollama serve +``` + +## Environment Variables + +Optionally set the host in environment variables: + +```bash +OLLAMA_HOST=http://localhost:11434 ``` ## API Reference -### `ollama(config)` +### `ollamaText(options?)` -Creates an Ollama adapter instance. +Creates an Ollama text/chat adapter. **Parameters:** -- `config.baseURL` - Ollama server URL (default: `http://localhost:11434`) +- `options.model?` - Default model (optional) + +**Returns:** An Ollama text adapter instance. + +### `createOllamaText(host?, options?)` + +Creates an Ollama text/chat adapter with a custom host. + +**Parameters:** + +- `host` - Ollama server URL (default: `http://localhost:11434`) +- `options.model?` - Default model (optional) + +**Returns:** An Ollama text adapter instance. + +### `ollamaEmbed(options?)` -**Returns:** An Ollama adapter instance. +Creates an Ollama embedding adapter. + +**Returns:** An Ollama embed adapter instance. + +### `createOllamaEmbed(host?, options?)` + +Creates an Ollama embedding adapter with a custom host. + +**Returns:** An Ollama embed adapter instance. + +### `ollamaSummarize(options?)` + +Creates an Ollama summarization adapter. + +**Returns:** An Ollama summarize adapter instance. + +### `createOllamaSummarize(host?, options?)` + +Creates an Ollama summarization adapter with a custom host. + +**Returns:** An Ollama summarize adapter instance. ## Benefits of Ollama - ✅ **Privacy** - Data stays on your infrastructure -- ✅ **Cost** - No API costs +- ✅ **Cost** - No API costs after hardware - ✅ **Customization** - Use any compatible model - ✅ **Offline** - Works without internet +- ✅ **Speed** - No network latency for local deployment + +## Limitations + +- **Image Generation**: Ollama does not support image generation. Use OpenAI or Gemini for image generation. +- **Performance**: Depends on your hardware (GPU recommended for larger models) ## Next Steps diff --git a/docs/adapters/openai.md b/docs/adapters/openai.md index 1d1392b0..7c4bf4d9 100644 --- a/docs/adapters/openai.md +++ b/docs/adapters/openai.md @@ -3,7 +3,7 @@ title: OpenAI Adapter id: openai-adapter --- -The OpenAI adapter provides access to OpenAI's GPT models, including GPT-4, GPT-3.5, and more. +The OpenAI adapter provides access to OpenAI's models, including GPT-4o, GPT-5, embeddings, image generation (DALL-E), text-to-speech (TTS), and audio transcription (Whisper). ## Installation @@ -14,12 +14,12 @@ npm install @tanstack/ai-openai ## Basic Usage ```typescript -import { chat } from "@tanstack/ai"; -import { openai } from "@tanstack/ai-openai"; +import { ai } from "@tanstack/ai"; +import { openaiText } from "@tanstack/ai-openai"; -const adapter = openai(); +const adapter = openaiText(); -const stream = chat({ +const stream = ai({ adapter, messages: [{ role: "user", content: "Hello!" }], model: "gpt-4o", @@ -29,12 +29,14 @@ const stream = chat({ ## Basic Usage - Custom API Key ```typescript -import { chat } from "@tanstack/ai"; -import { createOpenAI } from "@tanstack/ai-openai"; -const adapter = createOpenAI(process.env.OPENAI_API_KEY!, { +import { ai } from "@tanstack/ai"; +import { createOpenaiText } from "@tanstack/ai-openai"; + +const adapter = createOpenaiText(process.env.OPENAI_API_KEY!, { // ... your config options - }); -const stream = chat({ +}); + +const stream = ai({ adapter, messages: [{ role: "user", content: "Hello!" }], model: "gpt-4o", @@ -44,28 +46,61 @@ const stream = chat({ ## Configuration ```typescript -import { openai, type OpenAIConfig } from "@tanstack/ai-openai"; +import { createOpenaiText, type OpenAITextConfig } from "@tanstack/ai-openai"; -const config: OpenAIConfig = { +const config: OpenAITextConfig = { organization: "org-...", // Optional baseURL: "https://api.openai.com/v1", // Optional, for custom endpoints }; -const adapter = openai(config); +const adapter = createOpenaiText(process.env.OPENAI_API_KEY!, config); ``` - + +## Available Models + +### Chat Models + +- `gpt-4o` - GPT-4o (recommended) +- `gpt-4o-mini` - GPT-4o Mini (faster, cheaper) +- `gpt-5` - GPT-5 (with reasoning support) +- `o3` - O3 reasoning model +- `o3-mini` - O3 Mini + +### Embedding Models + +- `text-embedding-3-small` - Small embedding model +- `text-embedding-3-large` - Large embedding model +- `text-embedding-ada-002` - Legacy embedding model + +### Image Models + +- `gpt-image-1` - Latest image generation model +- `dall-e-3` - DALL-E 3 + +### Text-to-Speech Models + +- `tts-1` - Standard TTS (fast) +- `tts-1-hd` - High-definition TTS +- `gpt-4o-audio-preview` - GPT-4o with audio output + +### Transcription Models + +- `whisper-1` - Whisper large-v2 +- `gpt-4o-transcribe` - GPT-4o transcription +- `gpt-4o-mini-transcribe` - GPT-4o Mini transcription + ## Example: Chat Completion ```typescript -import { chat, toStreamResponse } from "@tanstack/ai"; -import { openai } from "@tanstack/ai-openai"; +import { ai, toStreamResponse } from "@tanstack/ai"; +import { openaiText } from "@tanstack/ai-openai"; -const adapter = openai(); +const adapter = openaiText(); export async function POST(request: Request) { const { messages } = await request.json(); - const stream = chat({ + const stream = ai({ adapter, messages, model: "gpt-4o", @@ -78,11 +113,11 @@ export async function POST(request: Request) { ## Example: With Tools ```typescript -import { chat, toolDefinition } from "@tanstack/ai"; -import { openai } from "@tanstack/ai-openai"; +import { ai, toolDefinition } from "@tanstack/ai"; +import { openaiText } from "@tanstack/ai-openai"; import { z } from "zod"; -const adapter = openai(); +const adapter = openaiText(); const getWeatherDef = toolDefinition({ name: "get_weather", @@ -97,7 +132,7 @@ const getWeather = getWeatherDef.server(async ({ location }) => { return { temperature: 72, conditions: "sunny" }; }); -const stream = chat({ +const stream = ai({ adapter, messages, model: "gpt-4o", @@ -110,23 +145,24 @@ const stream = chat({ OpenAI supports various provider-specific options: ```typescript -const stream = chat({ - adapter: openai(), +const stream = ai({ + adapter: openaiText(), messages, model: "gpt-4o", providerOptions: { temperature: 0.7, - maxTokens: 1000, - topP: 0.9, - frequencyPenalty: 0.5, - presencePenalty: 0.5, + max_tokens: 1000, + top_p: 0.9, + frequency_penalty: 0.5, + presence_penalty: 0.5, + stop: ["END"], }, }); ``` ### Reasoning -Enable reasoning for models that support it (e.g., GPT-5). This allows the model to show its reasoning process, which is streamed as `thinking` chunks: +Enable reasoning for models that support it (e.g., GPT-5, O3). This allows the model to show its reasoning process, which is streamed as `thinking` chunks: ```typescript providerOptions: { @@ -136,10 +172,190 @@ providerOptions: { }, } ``` - When reasoning is enabled, the model's reasoning process is streamed separately from the response text and appears as a collapsible thinking section in the UI. +## Embeddings + +Generate text embeddings for semantic search and similarity: + +```typescript +import { ai } from "@tanstack/ai"; +import { openaiEmbed } from "@tanstack/ai-openai"; + +const adapter = openaiEmbed(); + +const result = await ai({ + adapter, + model: "text-embedding-3-small", + input: "The quick brown fox jumps over the lazy dog", +}); + +console.log(result.embeddings); // Array of embedding vectors +``` + +### Batch Embeddings + +```typescript +const result = await ai({ + adapter: openaiEmbed(), + model: "text-embedding-3-small", + input: [ + "First text to embed", + "Second text to embed", + "Third text to embed", + ], +}); + +// result.embeddings contains an array of vectors +``` + +### Embedding Provider Options + +```typescript +const result = await ai({ + adapter: openaiEmbed(), + model: "text-embedding-3-small", + input: "...", + providerOptions: { + dimensions: 512, // Reduce dimensions for smaller storage + }, +}); +``` + +## Summarization + +Summarize long text content: + +```typescript +import { ai } from "@tanstack/ai"; +import { openaiSummarize } from "@tanstack/ai-openai"; + +const adapter = openaiSummarize(); + +const result = await ai({ + adapter, + model: "gpt-4o-mini", + text: "Your long text to summarize...", + maxLength: 100, + style: "concise", // "concise" | "bullet-points" | "paragraph" +}); + +console.log(result.summary); +``` + +## Image Generation + +Generate images with DALL-E: + +```typescript +import { ai } from "@tanstack/ai"; +import { openaiImage } from "@tanstack/ai-openai"; + +const adapter = openaiImage(); + +const result = await ai({ + adapter, + model: "gpt-image-1", + prompt: "A futuristic cityscape at sunset", + numberOfImages: 1, + size: "1024x1024", +}); + +console.log(result.images); +``` + +### Image Provider Options + +```typescript +const result = await ai({ + adapter: openaiImage(), + model: "gpt-image-1", + prompt: "...", + providerOptions: { + quality: "hd", // "standard" | "hd" + style: "natural", // "natural" | "vivid" + }, +}); +``` + +## Text-to-Speech + +Generate speech from text: + +```typescript +import { ai } from "@tanstack/ai"; +import { openaiTTS } from "@tanstack/ai-openai"; + +const adapter = openaiTTS(); + +const result = await ai({ + adapter, + model: "tts-1", + text: "Hello, welcome to TanStack AI!", + voice: "alloy", + format: "mp3", +}); + +// result.audio contains base64-encoded audio +console.log(result.format); // "mp3" +``` + +### TTS Voices + +Available voices: `alloy`, `echo`, `fable`, `onyx`, `nova`, `shimmer`, `ash`, `ballad`, `coral`, `sage`, `verse` + +### TTS Provider Options + +```typescript +const result = await ai({ + adapter: openaiTTS(), + model: "tts-1-hd", + text: "High quality speech", + providerOptions: { + speed: 1.0, // 0.25 to 4.0 + }, +}); +``` + +## Transcription + +Transcribe audio to text: + +```typescript +import { ai } from "@tanstack/ai"; +import { openaiTranscription } from "@tanstack/ai-openai"; + +const adapter = openaiTranscription(); + +const result = await ai({ + adapter, + model: "whisper-1", + audio: audioFile, // File object or base64 string + language: "en", +}); + +console.log(result.text); // Transcribed text +``` + +### Transcription Provider Options + +```typescript +const result = await ai({ + adapter: openaiTranscription(), + model: "whisper-1", + audio: audioFile, + providerOptions: { + response_format: "verbose_json", // Get timestamps + temperature: 0, + prompt: "Technical terms: API, SDK", + }, +}); + +// Access segments with timestamps +console.log(result.segments); +``` + ## Environment Variables Set your API key in environment variables: @@ -150,16 +366,83 @@ OPENAI_API_KEY=sk-... ## API Reference -### `openai(config)` +### `openaiText(config?)` -Creates an OpenAI adapter instance. +Creates an OpenAI text/chat adapter using environment variables. + +**Returns:** An OpenAI text adapter instance. + +### `createOpenaiText(apiKey, config?)` + +Creates an OpenAI text/chat adapter with an explicit API key. **Parameters:** - + +- `apiKey` - Your OpenAI API key - `config.organization?` - Organization ID (optional) - `config.baseURL?` - Custom base URL (optional) -**Returns:** An OpenAI adapter instance. +**Returns:** An OpenAI text adapter instance. + +### `openaiEmbed(config?)` + +Creates an OpenAI embedding adapter using environment variables. + +**Returns:** An OpenAI embed adapter instance. + +### `createOpenaiEmbed(apiKey, config?)` + +Creates an OpenAI embedding adapter with an explicit API key. + +**Returns:** An OpenAI embed adapter instance. + +### `openaiSummarize(config?)` + +Creates an OpenAI summarization adapter using environment variables. + +**Returns:** An OpenAI summarize adapter instance. + +### `createOpenaiSummarize(apiKey, config?)` + +Creates an OpenAI summarization adapter with an explicit API key. + +**Returns:** An OpenAI summarize adapter instance. + +### `openaiImage(config?)` + +Creates an OpenAI image generation adapter using environment variables. + +**Returns:** An OpenAI image adapter instance. + +### `createOpenaiImage(apiKey, config?)` + +Creates an OpenAI image generation adapter with an explicit API key. + +**Returns:** An OpenAI image adapter instance. + +### `openaiTTS(config?)` + +Creates an OpenAI TTS adapter using environment variables. + +**Returns:** An OpenAI TTS adapter instance. + +### `createOpenaiTTS(apiKey, config?)` + +Creates an OpenAI TTS adapter with an explicit API key. + +**Returns:** An OpenAI TTS adapter instance. + +### `openaiTranscription(config?)` + +Creates an OpenAI transcription adapter using environment variables. + +**Returns:** An OpenAI transcription adapter instance. + +### `createOpenaiTranscription(apiKey, config?)` + +Creates an OpenAI transcription adapter with an explicit API key. + +**Returns:** An OpenAI transcription adapter instance. ## Next Steps diff --git a/docs/api/ai.md b/docs/api/ai.md index 6f9240f4..f84f3db0 100644 --- a/docs/api/ai.md +++ b/docs/api/ai.md @@ -11,16 +11,16 @@ The core AI library for TanStack AI. npm install @tanstack/ai ``` -## `chat(options)` +## `ai(options)` Creates a streaming chat response. ```typescript -import { chat } from "@tanstack/ai"; -import { openai } from "@tanstack/ai-openai"; +import { ai } from "@tanstack/ai"; +import { openaiText } from "@tanstack/ai-openai"; -const stream = chat({ - adapter: openai(), +const stream = ai({ + adapter: openaiText(), messages: [{ role: "user", content: "Hello!" }], model: "gpt-4o", tools: [myTool], @@ -31,7 +31,7 @@ const stream = chat({ ### Parameters -- `adapter` - An AI adapter instance (e.g., `openai()`, `anthropic()`) +- `adapter` - An AI adapter instance (e.g., `openaiText()`, `anthropicText()`) - `messages` - Array of chat messages - `model` - Model identifier (type-safe based on adapter) - `tools?` - Array of tools for function calling @@ -49,11 +49,11 @@ An async iterable of `StreamChunk`. Creates a text summarization. ```typescript -import { summarize } from "@tanstack/ai"; -import { openai } from "@tanstack/ai-openai"; +import { ai } from "@tanstack/ai"; +import { openaiSummarize } from "@tanstack/ai-openai"; -const result = await summarize({ - adapter: openai(), +const result = await ai({ + adapter: openaiSummarize(), model: "gpt-4o", text: "Long text to summarize...", maxLength: 100, @@ -78,11 +78,11 @@ A `SummarizationResult` with the summary text. Creates embeddings for text input. ```typescript -import { embedding } from "@tanstack/ai"; -import { openai } from "@tanstack/ai-openai"; +import { ai } from "@tanstack/ai"; +import { openaiEmbed } from "@tanstack/ai-openai"; -const result = await embedding({ - adapter: openai(), +const result = await ai({ + adapter: openaiEmbed(), model: "text-embedding-3-small", input: "Text to embed", }); @@ -124,8 +124,8 @@ const myClientTool = myToolDef.client(async ({ param }) => { return { result: "..." }; }); -// Use directly in chat() (server-side, no execute) -chat({ +// Use directly in ai() (server-side, no execute) +ai({ tools: [myToolDef], // ... }); @@ -136,8 +136,8 @@ const myServerTool = myToolDef.server(async ({ param }) => { return { result: "..." }; }); -// Use directly in chat() (server-side, no execute) -chat({ +// Use directly in ai() (server-side, no execute) +ai({ tools: [myServerTool], // ... }); @@ -161,11 +161,11 @@ A `ToolDefinition` object with `.server()` and `.client()` methods for creating Converts a stream to a ReadableStream in Server-Sent Events format. ```typescript -import { toServerSentEventsStream, chat } from "@tanstack/ai"; -import { openai } from "@tanstack/ai-openai"; +import { ai, toServerSentEventsStream } from "@tanstack/ai"; +import { openaiText } from "@tanstack/ai-openai"; -const stream = chat({ - adapter: openai(), +const stream = ai({ + adapter: openaiText(), messages: [...], model: "gpt-4o", }); @@ -189,11 +189,11 @@ A `ReadableStream` in Server-Sent Events format. Each chunk is: Converts a stream to an HTTP Response with proper SSE headers. ```typescript -import { toStreamResponse, chat } from "@tanstack/ai"; -import { openai } from "@tanstack/ai-openai"; +import { ai, toStreamResponse } from "@tanstack/ai"; +import { openaiText } from "@tanstack/ai-openai"; -const stream = chat({ - adapter: openai(), +const stream = ai({ + adapter: openaiText(), messages: [...], model: "gpt-4o", }); @@ -214,11 +214,11 @@ A `Response` object suitable for HTTP endpoints with SSE headers (`Content-Type: Creates an agent loop strategy that limits iterations. ```typescript -import { maxIterations, chat } from "@tanstack/ai"; -import { openai } from "@tanstack/ai-openai"; +import { ai, maxIterations } from "@tanstack/ai"; +import { openaiText } from "@tanstack/ai-openai"; -const stream = chat({ - adapter: openai(), +const stream = ai({ + adapter: openaiText(), messages: [...], model: "gpt-4o", agentLoopStrategy: maxIterations(20), @@ -293,32 +293,94 @@ interface Tool { ## Usage Examples ```typescript -import { chat, summarize, embedding } from "@tanstack/ai"; -import { openai } from "@tanstack/ai-openai"; +import { ai } from "@tanstack/ai"; +import { + openaiText, + openaiSummarize, + openaiEmbed, + openaiImage, +} from "@tanstack/ai-openai"; + +// --- Streaming chat +const stream = ai({ + adapter: openaiText(), + messages: [{ role: "user", content: "Hello!" }], + model: "gpt-4o", +}); -const adapter = openai(); +// --- One-shot chat response +const response = await ai({ + adapter: openaiText(), + messages: [{ role: "user", content: "What's the capital of France?" }], + model: "gpt-4o", + oneShot: true, // Resolves with a single, complete response +}); -// Streaming chat -const stream = chat({ - adapter, - messages: [{ role: "user", content: "Hello!" }], +// --- Structured response +const parsed = await ai({ + adapter: openaiText(), + messages: [{ role: "user", content: "Summarize this text in JSON with keys 'summary' and 'keywords': ... " }], model: "gpt-4o", + parse: (content) => { + // Example: Expecting JSON output from model + try { + return JSON.parse(content); + } catch { + return { summary: "", keywords: [] }; + } + }, }); -// Summarization -const summary = await summarize({ - adapter, +// --- Structured response with tools +import { toolDefinition } from "@tanstack/ai"; +const weatherTool = toolDefinition({ + name: "getWeather", + description: "Get the current weather for a city", + parameters: { + city: { type: "string", description: "City name" }, + }, + async execute({ city }) { + // Implementation that fetches weather info + return { temperature: 72, condition: "Sunny" }; + }, +}); + +const toolResult = await ai({ + adapter: openaiText(), + model: "gpt-4o", + messages: [ + { role: "user", content: "What's the weather in Paris?" } + ], + tools: [weatherTool], + parse: (content, toolsOutput) => ({ + answer: content, + weather: toolsOutput.getWeather, + }), +}); + +// --- Summarization +const summary = await ai({ + adapter: openaiSummarize(), model: "gpt-4o", text: "Long text to summarize...", maxLength: 100, }); -// Embeddings -const embeddings = await embedding({ - adapter, +// --- Embeddings +const embeddings = await ai({ + adapter: openaiEmbed(), model: "text-embedding-3-small", input: "Text to embed", }); + +// --- Image generation +const image = await ai({ + adapter: openaiImage(), + model: "dall-e-3", + prompt: "A futuristic city skyline at sunset", + n: 1, // number of images + size: "1024x1024", +}); ``` ## Next Steps diff --git a/docs/config.json b/docs/config.json index d75a3b7b..d8a12298 100644 --- a/docs/config.json +++ b/docs/config.json @@ -69,6 +69,14 @@ { "label": "Per-Model Type Safety", "to": "guides/per-model-type-safety" + }, + { + "label": "Text-to-Speech", + "to": "guides/text-to-speech" + }, + { + "label": "Transcription", + "to": "guides/transcription" } ] }, @@ -163,12 +171,12 @@ "defaultCollapsed": true, "children": [ { - "label": "chat", - "to": "reference/functions/chat" + "label": "text", + "to": "reference/functions/text" }, { - "label": "chatOptions", - "to": "reference/functions/chatOptions" + "label": "textOptions", + "to": "reference/functions/textOptions" }, { "label": "combineStrategies", @@ -274,12 +282,12 @@ "to": "reference/interfaces/BaseStreamChunk" }, { - "label": "ChatCompletionChunk", - "to": "reference/interfaces/ChatCompletionChunk" + "label": "TextCompletionChunk", + "to": "reference/interfaces/TextCompletionChunk" }, { - "label": "ChatOptions", - "to": "reference/interfaces/ChatOptions" + "label": "TextOptions", + "to": "reference/interfaces/TextOptions" }, { "label": "ChunkRecording", @@ -457,12 +465,12 @@ "to": "reference/type-aliases/AnyClientTool" }, { - "label": "ChatStreamOptionsForModel", - "to": "reference/type-aliases/ChatStreamOptionsForModel" + "label": "TextStreamOptionsForModel", + "to": "reference/type-aliases/TextStreamOptionsForModel" }, { - "label": "ChatStreamOptionsUnion", - "to": "reference/type-aliases/ChatStreamOptionsUnion" + "label": "TextStreamOptionsUnion", + "to": "reference/type-aliases/TextStreamOptionsUnion" }, { "label": "ConstrainedContent", diff --git a/docs/getting-started/overview.md b/docs/getting-started/overview.md index 67395478..c432044e 100644 --- a/docs/getting-started/overview.md +++ b/docs/getting-started/overview.md @@ -24,7 +24,7 @@ The framework-agnostic core of TanStack AI provides the building blocks for crea - **Express** - Node.js server - **Remix Router v7** - Loaders and actions -TanStack AI lets you define a tool once and provide environment-specific implementations. Using `toolDefinition()` to declare the tool’s input/output types and the server behavior with `.server()` (or a client implementation with `.client()`). These isomorphic tools can be invoked from the AI runtime regardless of framework. +TanStack AI lets you define a tool once and provide environment-specific implementations. Using `toolDefinition()` to declare the tool's input/output types and the server behavior with `.server()` (or a client implementation with `.client()`). These isomorphic tools can be invoked from the AI runtime regardless of framework. ```typescript import { toolDefinition } from '@tanstack/ai' @@ -42,7 +42,7 @@ const getProducts = getProductsDef.server(async ({ query }) => { }) // Use in AI chat -chat({ tools: [getProducts] }) +ai({ tools: [getProducts] }) ``` ## Core Packages @@ -94,4 +94,4 @@ With the help of adapters, TanStack AI can connect to various LLM providers. Ava - [Quick Start Guide](./quick-start) - Get up and running in minutes - [Tools Guide](../guides/tools) - Learn about the isomorphic tool system -- [API Reference](../api/ai) - Explore the full API \ No newline at end of file +- [API Reference](../api/ai) - Explore the full API diff --git a/docs/getting-started/quick-start.md b/docs/getting-started/quick-start.md index ff0dcd8b..1b068121 100644 --- a/docs/getting-started/quick-start.md +++ b/docs/getting-started/quick-start.md @@ -22,8 +22,8 @@ First, create an API route that handles chat requests. Here's a simplified examp ```typescript // app/api/chat/route.ts (Next.js) // or src/routes/api/chat.ts (TanStack Start) -import { chat, toStreamResponse } from "@tanstack/ai"; -import { openai } from "@tanstack/ai-openai"; +import { ai, toStreamResponse } from "@tanstack/ai"; +import { openaiText } from "@tanstack/ai-openai"; export async function POST(request: Request) { // Check for API key @@ -43,8 +43,8 @@ export async function POST(request: Request) { try { // Create a streaming chat response - const stream = chat({ - adapter: openai(), + const stream = ai({ + adapter: openaiText(), messages, model: "gpt-4o", conversationId @@ -176,7 +176,7 @@ You now have a working chat application. The `useChat` hook handles: ## Using Tools -Since TanStack AI is framework-agnostic, you can define and use tools in any environment. Here’s a quick example of defining a tool and using it in a chat: +Since TanStack AI is framework-agnostic, you can define and use tools in any environment. Here's a quick example of defining a tool and using it in a chat: ```typescript import { toolDefinition } from '@tanstack/ai' @@ -190,7 +190,7 @@ const getProducts = getProductsDef.server(async ({ query }) => { return await db.products.search(query) }) -chat({ tools: [getProducts] }) +ai({ tools: [getProducts] }) ``` ## Next Steps diff --git a/docs/guides/agentic-cycle.md b/docs/guides/agentic-cycle.md index 2e68c1e7..8d15ddda 100644 --- a/docs/guides/agentic-cycle.md +++ b/docs/guides/agentic-cycle.md @@ -121,8 +121,8 @@ const getClothingAdvice = getClothingAdviceDef.server(async ({ temperature, cond export async function POST(request: Request) { const { messages } = await request.json(); - const stream = chat({ - adapter: openai(), + const stream = ai({ + adapter: openaiText(), messages, model: "gpt-4o", tools: [getWeather, getClothingAdvice], @@ -137,4 +137,4 @@ export async function POST(request: Request) { **Agentic Cycle**: 1. LLM calls `get_weather({city: "San Francisco"})` → Returns `{temp: 62, conditions: "cloudy"}` 2. LLM calls `get_clothing_advice({temperature: 62, conditions: "cloudy"})` → Returns `{recommendation: "Light jacket recommended"}` -3. LLM generates: "The weather in San Francisco is 62°F and cloudy. I recommend wearing a light jacket." \ No newline at end of file +3. LLM generates: "The weather in San Francisco is 62°F and cloudy. I recommend wearing a light jacket." diff --git a/docs/guides/client-tools.md b/docs/guides/client-tools.md index a7b19cb5..3011cbb0 100644 --- a/docs/guides/client-tools.md +++ b/docs/guides/client-tools.md @@ -93,15 +93,15 @@ To give the LLM access to client tools, pass the tool definitions (not implement ```typescript // api/chat/route.ts -import { chat, toServerSentEventsStream } from "@tanstack/ai"; -import { openai } from "@tanstack/ai-openai"; +import { ai, toServerSentEventsStream } from "@tanstack/ai"; +import { openaiText } from "@tanstack/ai-openai"; import { updateUIDef, saveToLocalStorageDef } from "@/tools/definitions"; export async function POST(request: Request) { const { messages } = await request.json(); - const stream = chat({ - adapter: openai(), + const stream = ai({ + adapter: openaiText(), messages, model: "gpt-4o", tools: [updateUIDef, saveToLocalStorageDef], // Pass definitions @@ -232,7 +232,7 @@ messages.forEach((message) => { ## Tool States Client tools go through a small set of observable lifecycle states you can surface in the UI to indicate progress: -- `awaiting-input` — the model intends to call the tool but arguments haven’t arrived yet. +- `awaiting-input` — the model intends to call the tool but arguments haven't arrived yet. - `input-streaming` — the model is streaming the tool arguments (partial input may be available). - `input-complete` — all arguments have been received and the tool is executing. - `completed` — the tool finished; part.output contains the result (or error details). @@ -297,17 +297,17 @@ const addToCartClient = addToCartDef.client((input) => { }); // Server: Pass definition for client execution -chat({ tools: [addToCartDef] }); // Client will execute +ai({ tools: [addToCartDef] }); // Client will execute // Or pass server implementation for server execution -chat({ tools: [addToCartServer] }); // Server will execute +ai({ tools: [addToCartServer] }); // Server will execute ``` ## Best Practices - **Keep client tools simple** - Since client tools run in the browser, avoid heavy computations or large dependencies that could bloat your bundle size. - **Handle errors gracefully** - Define clear error handling in your tool implementations and return meaningful error messages in your output schema. -- **Update UI reactively** - Use your framework’s state management (eg. React/Vue/Solid) to update the UI in response to tool executions. +- **Update UI reactively** - Use your framework's state management (eg. React/Vue/Solid) to update the UI in response to tool executions. - **Secure sensitive data** - Never store sensitive data (like API keys or personal info) in local storage or expose it via client tools. - **Provide feedback** - Use tool states to inform users about ongoing operations and results of client tool executions (loading spinners, success messages, error alerts). - **Type everything** - Leverage TypeScript and Zod schemas for full type safety from tool definitions to implementations to usage. diff --git a/docs/guides/image-generation.md b/docs/guides/image-generation.md new file mode 100644 index 00000000..27469144 --- /dev/null +++ b/docs/guides/image-generation.md @@ -0,0 +1,233 @@ +# Image Generation + +TanStack AI provides support for image generation through dedicated image adapters. This guide covers how to use the image generation functionality with OpenAI and Gemini providers. + +## Overview + +Image generation is handled by image adapters that follow the same tree-shakeable architecture as other adapters in TanStack AI. The image adapters support: + +- **OpenAI**: DALL-E 2, DALL-E 3, GPT-Image-1, and GPT-Image-1-Mini models +- **Gemini**: Imagen 3 and Imagen 4 models + +## Basic Usage + +### OpenAI Image Generation + +```typescript +import { ai } from '@tanstack/ai' +import { openaiImage } from '@tanstack/ai-openai' + +// Create an image adapter (uses OPENAI_API_KEY from environment) +const adapter = openaiImage() + +// Generate an image +const result = await ai({ + adapter, + model: 'dall-e-3', + prompt: 'A beautiful sunset over mountains', +}) + +console.log(result.images[0].url) // URL to the generated image +``` + +### Gemini Image Generation + +```typescript +import { ai } from '@tanstack/ai' +import { geminiImage } from '@tanstack/ai-gemini' + +// Create an image adapter (uses GOOGLE_API_KEY from environment) +const adapter = geminiImage() + +// Generate an image +const result = await ai({ + adapter, + model: 'imagen-3.0-generate-002', + prompt: 'A futuristic cityscape at night', +}) + +console.log(result.images[0].b64Json) // Base64 encoded image +``` + +## Options + +### Common Options + +All image adapters support these common options: + +| Option | Type | Description | +|--------|------|-------------| +| `prompt` | `string` | Text description of the image to generate (required) | +| `numberOfImages` | `number` | Number of images to generate | +| `size` | `string` | Size of the generated image in WIDTHxHEIGHT format | + +### Size Options + +#### OpenAI Models + +| Model | Supported Sizes | +|-------|----------------| +| `gpt-image-1` | `1024x1024`, `1536x1024`, `1024x1536`, `auto` | +| `gpt-image-1-mini` | `1024x1024`, `1536x1024`, `1024x1536`, `auto` | +| `dall-e-3` | `1024x1024`, `1792x1024`, `1024x1792` | +| `dall-e-2` | `256x256`, `512x512`, `1024x1024` | + +#### Gemini Models + +Gemini uses aspect ratios internally, but TanStack AI accepts WIDTHxHEIGHT format and converts them: + +| Size | Aspect Ratio | +|------|-------------| +| `1024x1024` | 1:1 | +| `1920x1080` | 16:9 | +| `1080x1920` | 9:16 | + +Alternatively, you can specify the aspect ratio directly in provider options: + +```typescript +const result = await ai({ + adapter, + model: 'imagen-4.0-generate-001', + prompt: 'A landscape photo', + providerOptions: { + aspectRatio: '16:9' + } +}) +``` + +## Provider Options + +### OpenAI Provider Options + +OpenAI models support model-specific provider options: + +#### GPT-Image-1 / GPT-Image-1-Mini + +```typescript +const result = await ai({ + adapter, + model: 'gpt-image-1', + prompt: 'A cat wearing a hat', + providerOptions: { + quality: 'high', // 'high' | 'medium' | 'low' | 'auto' + background: 'transparent', // 'transparent' | 'opaque' | 'auto' + outputFormat: 'png', // 'png' | 'jpeg' | 'webp' + moderation: 'low', // 'low' | 'auto' + } +}) +``` + +#### DALL-E 3 + +```typescript +const result = await ai({ + adapter, + model: 'dall-e-3', + prompt: 'A futuristic car', + providerOptions: { + quality: 'hd', // 'hd' | 'standard' + style: 'vivid', // 'vivid' | 'natural' + } +}) +``` + +### Gemini Provider Options + +```typescript +const result = await ai({ + adapter, + model: 'imagen-4.0-generate-001', + prompt: 'A beautiful garden', + providerOptions: { + aspectRatio: '16:9', + personGeneration: 'ALLOW_ADULT', // 'DONT_ALLOW' | 'ALLOW_ADULT' | 'ALLOW_ALL' + negativePrompt: 'blurry, low quality', + addWatermark: true, + outputMimeType: 'image/png', // 'image/png' | 'image/jpeg' | 'image/webp' + } +}) +``` + +## Response Format + +The image generation result includes: + +```typescript +interface ImageGenerationResult { + id: string // Unique identifier for this generation + model: string // The model used + images: GeneratedImage[] // Array of generated images + usage?: { + inputTokens: number + outputTokens: number + totalTokens: number + } +} + +interface GeneratedImage { + b64Json?: string // Base64 encoded image data + url?: string // URL to the image (OpenAI only) + revisedPrompt?: string // Revised prompt (OpenAI only) +} +``` + +## Model Availability + +### OpenAI Models + +| Model | Images per Request | +|-------|-------------------| +| `gpt-image-1` | 1-10 | +| `gpt-image-1-mini` | 1-10 | +| `dall-e-3` | 1 | +| `dall-e-2` | 1-10 | + +### Gemini Models + +| Model | Images per Request | +|-------|-------------------| +| `imagen-3.0-generate-002` | 1-4 | +| `imagen-4.0-generate-001` | 1-4 | +| `imagen-4.0-fast-generate-001` | 1-4 | +| `imagen-4.0-ultra-generate-001` | 1-4 | + +## Error Handling + +Image generation can fail for various reasons. The adapters validate inputs before making API calls: + +```typescript +try { + const result = await ai({ + adapter, + model: 'dall-e-3', + prompt: 'A cat', + size: '512x512', // Invalid size for DALL-E 3 + }) +} catch (error) { + console.error(error.message) + // "Size "512x512" is not supported by model "dall-e-3". + // Supported sizes: 1024x1024, 1792x1024, 1024x1792" +} +``` + +## Environment Variables + +The image adapters use the same environment variables as the text adapters: + +- **OpenAI**: `OPENAI_API_KEY` +- **Gemini**: `GOOGLE_API_KEY` or `GEMINI_API_KEY` + +## Explicit API Keys + +For production use or when you need explicit control: + +```typescript +import { createOpenaiImage } from '@tanstack/ai-openai' +import { createGeminiImage } from '@tanstack/ai-gemini' + +// OpenAI +const openaiAdapter = createOpenaiImage('your-openai-api-key') + +// Gemini +const geminiAdapter = createGeminiImage('your-google-api-key') +``` diff --git a/docs/guides/multimodal-content.md b/docs/guides/multimodal-content.md index 88c8d71a..e645a72b 100644 --- a/docs/guides/multimodal-content.md +++ b/docs/guides/multimodal-content.md @@ -53,13 +53,11 @@ const imageUrlPart: ImagePart = { Messages can have `content` as either a string or an array of `ContentPart`: ```typescript -import { chat } from '@tanstack/ai' -import { OpenAI } from '@tanstack/ai-openai' +import { ai } from '@tanstack/ai' +import { openaiText } from '@tanstack/ai-openai' -const openai = new OpenAI({ apiKey: 'your-key' }) - -const response = await chat({ - adapter: openai, +const response = await ai({ + adapter: openaiText(), model: 'gpt-4o', messages: [ { @@ -86,9 +84,9 @@ const response = await chat({ OpenAI supports images and audio in their vision and audio models: ```typescript -import { OpenAI } from '@tanstack/ai-openai' +import { openaiText } from '@tanstack/ai-openai' -const openai = new OpenAI({ apiKey: 'your-key' }) +const adapter = openaiText() // Image with detail level metadata const message = { @@ -113,9 +111,9 @@ const message = { Anthropic's Claude models support images and PDF documents: ```typescript -import { Anthropic } from '@tanstack/ai-anthropic' +import { anthropicText } from '@tanstack/ai-anthropic' -const anthropic = new Anthropic({ apiKey: 'your-key' }) +const adapter = anthropicText() // Image with media type const imageMessage = { @@ -152,9 +150,9 @@ const docMessage = { Google's Gemini models support a wide range of modalities: ```typescript -import { GeminiAdapter } from '@tanstack/ai-gemini' +import { geminiText } from '@tanstack/ai-gemini' -const gemini = new GeminiAdapter({ apiKey: 'your-key' }) +const adapter = geminiText() // Image with mimeType const message = { @@ -179,9 +177,9 @@ const message = { Ollama supports images in compatible models: ```typescript -import { OllamaAdapter } from '@tanstack/ai-ollama' +import { ollamaText } from '@tanstack/ai-ollama' -const ollama = new OllamaAdapter({ host: 'http://localhost:11434' }) +const adapter = ollamaText({ baseURL: 'http://localhost:11434' }) // Image as base64 const message = { @@ -278,19 +276,19 @@ import type { GeminiMediaMetadata } from '@tanstack/ai-gemini' When receiving messages from external sources (like `request.json()`), the data is typed as `any`, which can bypass TypeScript's type checking. Use `assertMessages` to restore type safety: ```typescript -import { assertMessages, chat } from '@tanstack/ai' -import { openai } from '@tanstack/ai-openai' +import { ai, assertMessages } from '@tanstack/ai' +import { openaiText } from '@tanstack/ai-openai' // In an API route handler const { messages: incomingMessages } = await request.json() -const adapter = openai() +const adapter = openaiText() // Assert incoming messages are compatible with gpt-4o (text + image only) const typedMessages = assertMessages({ adapter, model: 'gpt-4o' }, incomingMessages) // Now TypeScript will properly check any additional messages you add -const stream = chat({ +const stream = ai({ adapter, model: 'gpt-4o', messages: [ diff --git a/docs/guides/per-model-type-safety.md b/docs/guides/per-model-type-safety.md index ffa91256..9d259bbb 100644 --- a/docs/guides/per-model-type-safety.md +++ b/docs/guides/per-model-type-safety.md @@ -12,13 +12,13 @@ The AI SDK provides **model-specific type safety** for `providerOptions`. Each m ### ✅ Correct Usage ```typescript -import { chat } from "@tanstack/ai"; -import { openai } from "@tanstack/ai-openai"; +import { ai } from "@tanstack/ai"; +import { openaiText } from "@tanstack/ai-openai"; -const adapter = openai(); +const adapter = openaiText(); // ✅ gpt-5 supports structured outputs - `text` is allowed -const validCall = chat({ +const validCall = ai({ adapter, model: "gpt-5", messages: [], @@ -38,8 +38,8 @@ const validCall = chat({ ```typescript // ❌ gpt-4-turbo does NOT support structured outputs - `text` is rejected -const invalidCall = chat({ - adapter: openai(), +const invalidCall = ai({ + adapter: openaiText(), model: "gpt-4-turbo", messages: [], providerOptions: { diff --git a/docs/guides/server-tools.md b/docs/guides/server-tools.md index 0a8e43ea..703f26e5 100644 --- a/docs/guides/server-tools.md +++ b/docs/guides/server-tools.md @@ -137,18 +137,18 @@ const searchProducts = searchProductsDef.server(async ({ query, limit = 10 }) => ## Using Server Tools -Pass tools to the `chat` method: +Pass tools to the `ai` function: ```typescript -import { chat, toStreamResponse } from "@tanstack/ai"; -import { openai } from "@tanstack/ai-openai"; +import { ai, toStreamResponse } from "@tanstack/ai"; +import { openaiText } from "@tanstack/ai-openai"; import { getUserData, searchProducts } from "./tools"; export async function POST(request: Request) { const { messages } = await request.json(); - const stream = chat({ - adapter: openai(), + const stream = ai({ + adapter: openaiText(), messages, model: "gpt-4o", tools: [getUserData, searchProducts], @@ -202,12 +202,12 @@ export const searchProducts = searchProductsDef.server(async ({ query }) => { }); // api/chat/route.ts -import { chat } from "@tanstack/ai"; -import { openai } from "@tanstack/ai-openai"; +import { ai } from "@tanstack/ai"; +import { openaiText } from "@tanstack/ai-openai"; import { getUserData, searchProducts } from "@/tools/server"; -const stream = chat({ - adapter: openai(), +const stream = ai({ + adapter: openaiText(), messages, model: "gpt-4o", tools: [getUserData, searchProducts], diff --git a/docs/guides/streaming.md b/docs/guides/streaming.md index da2a806b..abab88e9 100644 --- a/docs/guides/streaming.md +++ b/docs/guides/streaming.md @@ -7,14 +7,14 @@ TanStack AI supports streaming responses for real-time chat experiences. Streami ## How Streaming Works -When you use `chat()`, it returns an async iterable stream of chunks: +When you use `ai()`, it returns an async iterable stream of chunks: ```typescript -import { chat } from "@tanstack/ai"; -import { openai } from "@tanstack/ai-openai"; +import { ai } from "@tanstack/ai"; +import { openaiText } from "@tanstack/ai-openai"; -const stream = chat({ - adapter: openai(), +const stream = ai({ + adapter: openaiText(), messages, model: "gpt-4o", }); @@ -30,14 +30,14 @@ for await (const chunk of stream) { Convert the stream to an HTTP response using `toStreamResponse`: ```typescript -import { chat, toStreamResponse } from "@tanstack/ai"; -import { openai } from "@tanstack/ai-openai"; +import { ai, toStreamResponse } from "@tanstack/ai"; +import { openaiText } from "@tanstack/ai-openai"; export async function POST(request: Request) { const { messages } = await request.json(); - const stream = chat({ - adapter: openai(), + const stream = ai({ + adapter: openaiText(), messages, model: "gpt-4o", }); diff --git a/docs/guides/text-to-speech.md b/docs/guides/text-to-speech.md new file mode 100644 index 00000000..5a14deaa --- /dev/null +++ b/docs/guides/text-to-speech.md @@ -0,0 +1,248 @@ +# Text-to-Speech (TTS) + +TanStack AI provides support for text-to-speech generation through dedicated TTS adapters. This guide covers how to convert text into spoken audio using OpenAI and Gemini providers. + +## Overview + +Text-to-speech (TTS) is handled by TTS adapters that follow the same tree-shakeable architecture as other adapters in TanStack AI. The TTS adapters support: + +- **OpenAI**: TTS-1, TTS-1-HD, and audio-capable GPT-4o models +- **Gemini**: Gemini 2.5 Flash TTS (experimental) + +## Basic Usage + +### OpenAI Text-to-Speech + +```typescript +import { ai } from '@tanstack/ai' +import { openaiTTS } from '@tanstack/ai-openai' + +// Create a TTS adapter (uses OPENAI_API_KEY from environment) +const adapter = openaiTTS() + +// Generate speech from text +const result = await ai({ + adapter, + model: 'tts-1', + text: 'Hello, welcome to TanStack AI!', + voice: 'alloy', +}) + +// result.audio contains base64-encoded audio data +console.log(result.format) // 'mp3' +console.log(result.contentType) // 'audio/mpeg' +``` + +### Gemini Text-to-Speech (Experimental) + +```typescript +import { ai } from '@tanstack/ai' +import { geminiTTS } from '@tanstack/ai-gemini' + +// Create a TTS adapter (uses GOOGLE_API_KEY from environment) +const adapter = geminiTTS() + +// Generate speech from text +const result = await ai({ + adapter, + model: 'gemini-2.5-flash-preview-tts', + text: 'Hello from Gemini TTS!', +}) + +console.log(result.audio) // Base64 encoded audio +``` + +## Options + +### Common Options + +All TTS adapters support these common options: + +| Option | Type | Description | +|--------|------|-------------| +| `text` | `string` | The text to convert to speech (required) | +| `voice` | `string` | The voice to use for generation | +| `format` | `string` | Output audio format (e.g., "mp3", "wav") | + +### OpenAI Voice Options + +OpenAI provides several distinct voices: + +| Voice | Description | +|-------|-------------| +| `alloy` | Neutral, balanced voice | +| `echo` | Warm, conversational voice | +| `fable` | Expressive, storytelling voice | +| `onyx` | Deep, authoritative voice | +| `nova` | Friendly, upbeat voice | +| `shimmer` | Clear, gentle voice | +| `ash` | Calm, measured voice | +| `ballad` | Melodic, flowing voice | +| `coral` | Bright, energetic voice | +| `sage` | Wise, thoughtful voice | +| `verse` | Poetic, rhythmic voice | + +### OpenAI Format Options + +| Format | Description | +|--------|-------------| +| `mp3` | MP3 audio (default) | +| `opus` | Opus audio (good for streaming) | +| `aac` | AAC audio | +| `flac` | FLAC audio (lossless) | +| `wav` | WAV audio (uncompressed) | +| `pcm` | Raw PCM audio | + +## Provider Options + +### OpenAI Provider Options + +```typescript +const result = await ai({ + adapter: openaiTTS(), + model: 'tts-1-hd', + text: 'High quality speech synthesis', + voice: 'nova', + format: 'mp3', + providerOptions: { + speed: 1.0, // 0.25 to 4.0 + }, +}) +``` + +| Option | Type | Description | +|--------|------|-------------| +| `speed` | `number` | Playback speed (0.25 to 4.0, default 1.0) | +| `instructions` | `string` | Voice style instructions (GPT-4o audio models only) | + +> **Note:** The `instructions` and `stream_format` options are only available with `gpt-4o-audio-preview` and `gpt-4o-mini-audio-preview` models, not with `tts-1` or `tts-1-hd`. + +## Response Format + +The TTS result includes: + +```typescript +interface TTSResult { + id: string // Unique identifier for this generation + model: string // The model used + audio: string // Base64-encoded audio data + format: string // Audio format (e.g., "mp3") + contentType: string // MIME type (e.g., "audio/mpeg") + duration?: number // Duration in seconds (if available) +} +``` + +## Playing Audio in the Browser + +```typescript +// Convert base64 to audio and play +function playAudio(result: TTSResult) { + const audioData = atob(result.audio) + const bytes = new Uint8Array(audioData.length) + for (let i = 0; i < audioData.length; i++) { + bytes[i] = audioData.charCodeAt(i) + } + + const blob = new Blob([bytes], { type: result.contentType }) + const url = URL.createObjectURL(blob) + + const audio = new Audio(url) + audio.play() + + // Clean up when done + audio.onended = () => URL.revokeObjectURL(url) +} +``` + +## Saving Audio to File (Node.js) + +```typescript +import { writeFile } from 'fs/promises' + +async function saveAudio(result: TTSResult, filename: string) { + const audioBuffer = Buffer.from(result.audio, 'base64') + await writeFile(filename, audioBuffer) + console.log(`Saved to ${filename}`) +} + +// Usage +const result = await ai({ + adapter: openaiTTS(), + model: 'tts-1', + text: 'Hello world!', +}) + +await saveAudio(result, 'output.mp3') +``` + +## Model Availability + +### OpenAI Models + +| Model | Quality | Speed | Use Case | +|-------|---------|-------|----------| +| `tts-1` | Standard | Fast | Real-time applications | +| `tts-1-hd` | High | Slower | Production audio | +| `gpt-4o-audio-preview` | Highest | Variable | Advanced voice control | +| `gpt-4o-mini-audio-preview` | High | Fast | Balanced quality/speed | + +### Gemini Models + +| Model | Status | Notes | +|-------|--------|-------| +| `gemini-2.5-flash-preview-tts` | Experimental | May require Live API for full features | + +## Error Handling + +```typescript +try { + const result = await ai({ + adapter: openaiTTS(), + model: 'tts-1', + text: 'Hello!', + }) +} catch (error) { + if (error.message.includes('exceeds maximum length')) { + console.error('Text is too long (max 4096 characters)') + } else if (error.message.includes('Speed must be between')) { + console.error('Invalid speed value') + } else { + console.error('TTS error:', error.message) + } +} +``` + +## Environment Variables + +The TTS adapters use the same environment variables as other adapters: + +- **OpenAI**: `OPENAI_API_KEY` +- **Gemini**: `GOOGLE_API_KEY` or `GEMINI_API_KEY` + +## Explicit API Keys + +For production use or when you need explicit control: + +```typescript +import { createOpenaiTTS } from '@tanstack/ai-openai' +import { createGeminiTTS } from '@tanstack/ai-gemini' + +// OpenAI +const openaiAdapter = createOpenaiTTS('your-openai-api-key') + +// Gemini +const geminiAdapter = createGeminiTTS('your-google-api-key') +``` + +## Best Practices + +1. **Text Length**: OpenAI TTS supports up to 4096 characters per request. For longer content, split into chunks. + +2. **Voice Selection**: Choose voices appropriate for your content—use `onyx` for authoritative content, `nova` for friendly interactions. + +3. **Format Selection**: Use `mp3` for general use, `opus` for streaming, `wav` for further processing. + +4. **Caching**: Cache generated audio to avoid regenerating the same content. + +5. **Error Handling**: Always handle errors gracefully, especially for user-facing applications. + diff --git a/docs/guides/tool-approval.md b/docs/guides/tool-approval.md index 0479a112..bc14e48d 100644 --- a/docs/guides/tool-approval.md +++ b/docs/guides/tool-approval.md @@ -56,15 +56,15 @@ const sendEmail = sendEmailDef.server(async ({ to, subject, body }) => { On the server, tools with `needsApproval: true` will pause execution and wait for approval: ```typescript -import { chat, toStreamResponse } from "@tanstack/ai"; -import { openai } from "@tanstack/ai-openai"; +import { ai, toStreamResponse } from "@tanstack/ai"; +import { openaiText } from "@tanstack/ai-openai"; import { sendEmail } from "./tools"; export async function POST(request: Request) { const { messages } = await request.json(); - const stream = chat({ - adapter: openai(), + const stream = ai({ + adapter: openaiText(), messages, model: "gpt-4o", tools: [sendEmail], diff --git a/docs/guides/tool-architecture.md b/docs/guides/tool-architecture.md index f691f510..de800320 100644 --- a/docs/guides/tool-architecture.md +++ b/docs/guides/tool-architecture.md @@ -68,16 +68,16 @@ sequenceDiagram **Server (API Route):** ```typescript -import { chat, toStreamResponse } from "@tanstack/ai"; -import { openai } from "@tanstack/ai-openai"; +import { ai, toStreamResponse } from "@tanstack/ai"; +import { openaiText } from "@tanstack/ai-openai"; import { getWeather, sendEmail } from "./tools"; export async function POST(request: Request) { const { messages } = await request.json(); // Create streaming chat with tools - const stream = chat({ - adapter: openai(), + const stream = ai({ + adapter: openaiText(), messages, model: "gpt-4o", tools: [getWeather, sendEmail], // Tool definitions passed here diff --git a/docs/guides/tools.md b/docs/guides/tools.md index 9f361696..46226832 100644 --- a/docs/guides/tools.md +++ b/docs/guides/tools.md @@ -173,8 +173,8 @@ const getWeatherServer = getWeatherDef.server(async (args) => { ### Server-Side ```typescript -import { chat, toStreamResponse } from "@tanstack/ai"; -import { openai } from "@tanstack/ai-openai"; +import { ai, toStreamResponse } from "@tanstack/ai"; +import { openaiText } from "@tanstack/ai-openai"; import { getWeatherDef } from "./tools"; export async function POST(request: Request) { @@ -186,8 +186,8 @@ export async function POST(request: Request) { return await response.json(); }); - const stream = chat({ - adapter: openai(), + const stream = ai({ + adapter: openaiText(), messages, model: "gpt-4o", tools: [getWeather], // Pass server tools @@ -223,16 +223,16 @@ const saveToStorage = saveToStorageDef.client((input) => { // Create typed tools array (no 'as const' needed!) const tools = clientTools(updateUI, saveToStorage); -const chatOptions = createChatClientOptions({ +const textOptions = createChatClientOptions({ connection: fetchServerSentEvents("/api/chat"), tools, }); // Infer message types for full type safety -type ChatMessages = InferChatMessages; +type ChatMessages = InferChatMessages; function ChatComponent() { - const { messages, sendMessage } = useChat(chatOptions); + const { messages, sendMessage } = useChat(textOptions); // messages is now fully typed with tool names and outputs! return ; @@ -279,8 +279,8 @@ const addToCartClient = addToCartDef.client((input) => { On the server, pass the definition (for client execution) or server implementation: ```typescript -chat({ - adapter: openai(), +ai({ + adapter: openaiText(), messages, tools: [addToCartDef], // Client will execute, or tools: [addToCartServer], // Server will execute diff --git a/docs/guides/transcription.md b/docs/guides/transcription.md new file mode 100644 index 00000000..ff55ae14 --- /dev/null +++ b/docs/guides/transcription.md @@ -0,0 +1,337 @@ +# Audio Transcription + +TanStack AI provides support for audio transcription (speech-to-text) through dedicated transcription adapters. This guide covers how to convert spoken audio into text using OpenAI's Whisper and GPT-4o transcription models. + +## Overview + +Audio transcription is handled by transcription adapters that follow the same tree-shakeable architecture as other adapters in TanStack AI. + +Currently supported: +- **OpenAI**: Whisper-1, GPT-4o-transcribe, GPT-4o-mini-transcribe + +## Basic Usage + +### OpenAI Transcription + +```typescript +import { ai } from '@tanstack/ai' +import { openaiTranscription } from '@tanstack/ai-openai' + +// Create a transcription adapter (uses OPENAI_API_KEY from environment) +const adapter = openaiTranscription() + +// Transcribe audio from a file +const audioFile = new File([audioBuffer], 'audio.mp3', { type: 'audio/mpeg' }) + +const result = await ai({ + adapter, + model: 'whisper-1', + audio: audioFile, + language: 'en', +}) + +console.log(result.text) // The transcribed text +``` + +### Using Base64 Audio + +```typescript +import { readFile } from 'fs/promises' + +// Read audio file as base64 +const audioBuffer = await readFile('recording.mp3') +const base64Audio = audioBuffer.toString('base64') + +const result = await ai({ + adapter: openaiTranscription(), + model: 'whisper-1', + audio: base64Audio, +}) + +console.log(result.text) +``` + +### Using Data URLs + +```typescript +const dataUrl = `data:audio/mpeg;base64,${base64AudioData}` + +const result = await ai({ + adapter: openaiTranscription(), + model: 'whisper-1', + audio: dataUrl, +}) +``` + +## Options + +### Common Options + +| Option | Type | Description | +|--------|------|-------------| +| `audio` | `File \| string` | Audio data (File object or base64 string) - required | +| `language` | `string` | Language code (e.g., "en", "es", "fr") | + +### Supported Languages + +Whisper supports many languages. Common codes include: + +| Code | Language | +|------|----------| +| `en` | English | +| `es` | Spanish | +| `fr` | French | +| `de` | German | +| `it` | Italian | +| `pt` | Portuguese | +| `ja` | Japanese | +| `ko` | Korean | +| `zh` | Chinese | +| `ru` | Russian | + +> **Tip:** Providing the correct language code improves accuracy and reduces latency. + +## Provider Options + +### OpenAI Provider Options + +```typescript +const result = await ai({ + adapter: openaiTranscription(), + model: 'whisper-1', + audio: audioFile, + providerOptions: { + response_format: 'verbose_json', // Get detailed output with timestamps + temperature: 0, // Lower = more deterministic + prompt: 'Technical terms: API, SDK, CLI', // Guide transcription + }, +}) +``` + +| Option | Type | Description | +|--------|------|-------------| +| `response_format` | `string` | Output format: "json", "text", "srt", "verbose_json", "vtt" | +| `temperature` | `number` | Sampling temperature (0 to 1) | +| `prompt` | `string` | Optional text to guide transcription style | +| `include` | `string[]` | Timestamp granularity: ["word"], ["segment"], or both | + +### Response Formats + +| Format | Description | +|--------|-------------| +| `json` | Simple JSON with text | +| `text` | Plain text only | +| `srt` | SubRip subtitle format | +| `verbose_json` | Detailed JSON with timestamps and segments | +| `vtt` | WebVTT subtitle format | + +## Response Format + +The transcription result includes: + +```typescript +interface TranscriptionResult { + id: string // Unique identifier + model: string // Model used + text: string // Full transcribed text + language?: string // Detected/specified language + duration?: number // Audio duration in seconds + segments?: Array<{ // Timestamped segments + start: number // Start time in seconds + end: number // End time in seconds + text: string // Segment text + words?: Array<{ // Word-level timestamps + word: string + start: number + end: number + confidence?: number + }> + }> +} +``` + +## Complete Example + +```typescript +import { ai } from '@tanstack/ai' +import { openaiTranscription } from '@tanstack/ai-openai' +import { readFile } from 'fs/promises' + +async function transcribeAudio(filepath: string) { + const adapter = openaiTranscription() + + // Read the audio file + const audioBuffer = await readFile(filepath) + const audioFile = new File( + [audioBuffer], + filepath.split('/').pop()!, + { type: 'audio/mpeg' } + ) + + // Transcribe with detailed output + const result = await ai({ + adapter, + model: 'whisper-1', + audio: audioFile, + language: 'en', + providerOptions: { + response_format: 'verbose_json', + include: ['segment', 'word'], + }, + }) + + console.log('Full text:', result.text) + console.log('Duration:', result.duration, 'seconds') + + // Print segments with timestamps + if (result.segments) { + for (const segment of result.segments) { + console.log(`[${segment.start.toFixed(2)}s - ${segment.end.toFixed(2)}s]: ${segment.text}`) + } + } + + return result +} + +// Usage +await transcribeAudio('./meeting-recording.mp3') +``` + +## Model Availability + +### OpenAI Models + +| Model | Description | Use Case | +|-------|-------------|----------| +| `whisper-1` | Whisper large-v2 | General transcription | +| `gpt-4o-transcribe` | GPT-4o-based transcription | Higher accuracy | +| `gpt-4o-transcribe-diarize` | With speaker diarization | Multi-speaker audio | +| `gpt-4o-mini-transcribe` | Faster, lighter model | Cost-effective | + +### Supported Audio Formats + +OpenAI supports these audio formats: + +- `mp3` - MPEG Audio Layer 3 +- `mp4` - MPEG-4 Audio +- `mpeg` - MPEG Audio +- `mpga` - MPEG Audio +- `m4a` - MPEG-4 Audio +- `wav` - Waveform Audio +- `webm` - WebM Audio +- `flac` - Free Lossless Audio Codec +- `ogg` - Ogg Vorbis + +> **Note:** Maximum file size is 25 MB. + +## Browser Usage + +### Recording and Transcribing + +```typescript +async function recordAndTranscribe() { + // Request microphone access + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) + const mediaRecorder = new MediaRecorder(stream) + const chunks: Blob[] = [] + + mediaRecorder.ondataavailable = (e) => chunks.push(e.data) + + mediaRecorder.onstop = async () => { + const audioBlob = new Blob(chunks, { type: 'audio/webm' }) + const audioFile = new File([audioBlob], 'recording.webm', { type: 'audio/webm' }) + + // Send to your API endpoint for transcription + const formData = new FormData() + formData.append('audio', audioFile) + + const response = await fetch('/api/transcribe', { + method: 'POST', + body: formData, + }) + + const result = await response.json() + console.log('Transcription:', result.text) + } + + // Start recording + mediaRecorder.start() + + // Stop after 10 seconds + setTimeout(() => mediaRecorder.stop(), 10000) +} +``` + +### Server API Endpoint + +```typescript +// api/transcribe.ts +import { ai } from '@tanstack/ai' +import { openaiTranscription } from '@tanstack/ai-openai' + +export async function POST(request: Request) { + const formData = await request.formData() + const audioFile = formData.get('audio') as File + + const adapter = openaiTranscription() + + const result = await ai({ + adapter, + model: 'whisper-1', + audio: audioFile, + }) + + return Response.json(result) +} +``` + +## Error Handling + +```typescript +try { + const result = await ai({ + adapter: openaiTranscription(), + model: 'whisper-1', + audio: audioFile, + }) +} catch (error) { + if (error.message.includes('Invalid file format')) { + console.error('Unsupported audio format') + } else if (error.message.includes('File too large')) { + console.error('Audio file exceeds 25 MB limit') + } else if (error.message.includes('Audio file is too short')) { + console.error('Audio must be at least 0.1 seconds') + } else { + console.error('Transcription error:', error.message) + } +} +``` + +## Environment Variables + +The transcription adapter uses: + +- `OPENAI_API_KEY`: Your OpenAI API key + +## Explicit API Keys + +```typescript +import { createOpenaiTranscription } from '@tanstack/ai-openai' + +const adapter = createOpenaiTranscription('your-openai-api-key') +``` + +## Best Practices + +1. **Audio Quality**: Better audio quality leads to more accurate transcriptions. Reduce background noise when possible. + +2. **Language Specification**: Always specify the language if known—this improves accuracy and speed. + +3. **File Size**: Keep audio files under 25 MB. For longer recordings, split into chunks. + +4. **Format Selection**: MP3 offers a good balance of quality and size. Use WAV or FLAC for highest quality. + +5. **Prompting**: Use the `prompt` option to provide context or expected vocabulary (e.g., technical terms, names). + +6. **Timestamps**: Request `verbose_json` format and enable `include: ['word', 'segment']` when you need timing information for captions or synchronization. + diff --git a/docs/guides/tree-shakeable-adapters.md b/docs/guides/tree-shakeable-adapters.md new file mode 100644 index 00000000..cda03b2f --- /dev/null +++ b/docs/guides/tree-shakeable-adapters.md @@ -0,0 +1,209 @@ +# Tree-Shakeable Adapters + +TanStack AI provides tree-shakeable adapters that allow you to import only the functionality you need, resulting in smaller bundle sizes. + +## Overview + +Instead of importing a monolithic adapter that includes chat, embedding, and summarization capabilities all at once, you can now import only the specific functionality you need: + +- **Text Adapters** - For chat and text generation +- **Embed Adapters** - For creating embeddings +- **Summarize Adapters** - For text summarization + +## Installation + +Each provider package (e.g., `@tanstack/ai-openai`, `@tanstack/ai-anthropic`) exports tree-shakeable adapters: + +```ts +// Import only what you need +import { openaiText } from '@tanstack/ai-openai' +import { openaiEmbed } from '@tanstack/ai-openai' +import { openaiSummarize } from '@tanstack/ai-openai' +``` + +## Available Adapters + +### OpenAI + +```ts +import { + openaiText, // Chat/text generation + openaiEmbed, // Embeddings + openaiSummarize, // Summarization + createOpenAIText, + createOpenAIEmbed, + createOpenAISummarize, +} from '@tanstack/ai-openai' +``` + +### Anthropic + +```ts +import { + anthropicText, // Chat/text generation + anthropicSummarize, // Summarization + createAnthropicText, + createAnthropicSummarize, +} from '@tanstack/ai-anthropic' +``` + +> Note: Anthropic does not support embeddings natively. + +### Gemini + +```ts +import { + geminiText, // Chat/text generation + geminiEmbed, // Embeddings + geminiSummarize, // Summarization + createGeminiText, + createGeminiEmbed, + createGeminiSummarize, +} from '@tanstack/ai-gemini' +``` + +### Ollama + +```ts +import { + ollamaText, // Chat/text generation + ollamaEmbed, // Embeddings + ollamaSummarize, // Summarization + createOllamaText, + createOllamaEmbed, + createOllamaSummarize, +} from '@tanstack/ai-ollama' +``` + +## Usage + +### Basic Usage + +Each adapter type has two ways to create instances: + +1. **Factory function** (recommended for quick setup): + +```ts +import { openaiText } from '@tanstack/ai-openai' + +const textAdapter = openaiText() + +``` + +2. **Class constructor** (for more control): + +```ts +import { createOpenAIText } from '@tanstack/ai-openai/adapters' + +const textAdapter = createOpenAIText({ + apiKey: 'your-api-key', + // additional configuration... +}) +``` + +### Using the `generate` Function + +The `generate` function provides a unified API that adapts based on the adapter type: + +```ts +import { generate } from '@tanstack/ai' +import { openaiText, openaiEmbed, openaiSummarize } from '@tanstack/ai-openai/adapters' + +// Chat generation - returns AsyncIterable +const chatResult = generate({ + adapter: openaiText(), + model: 'gpt-4o', + messages: [{ role: 'user', content: [{ type: 'text', content: 'Hello!' }] }], +}) + +for await (const chunk of chatResult) { + console.log(chunk) +} + +// Embeddings - returns Promise +const embedResult = await generate({ + adapter: openaiEmbed(), + model: 'text-embedding-3-small', + input: ['Hello, world!'], +}) + +console.log(embedResult.embeddings) + +// Summarization - returns Promise +const summarizeResult = await generate({ + adapter: openaiSummarize(), + model: 'gpt-4o-mini', + text: 'Long text to summarize...', +}) + +console.log(summarizeResult.summary) +``` + +### Type Safety + +Each adapter provides full type safety for its supported models and options: + +```ts +import { openaiText, type OpenAITextModel } from '@tanstack/ai-openai' + +const adapter = openaiText() + +// TypeScript knows the exact models supported +const model: OpenAITextModel = 'gpt-4o' // ✓ Valid +const model2: OpenAITextModel = 'invalid' // ✗ Type error +``` + +## Migration from Monolithic Adapters + +The legacy monolithic adapters are still available but deprecated: + +```ts +// Legacy (deprecated) +import { openai } from '@tanstack/ai-openai' + +// New tree-shakeable approach +import { openaiText, openaiEmbed } from '@tanstack/ai-openai/adapters' +``` + +## Bundle Size Benefits + +Using tree-shakeable adapters means: + +- Only the code you use is included in your bundle +- Unused adapter types are completely eliminated +- Smaller bundles lead to faster load times + +For example, if you only need chat functionality: + +```ts +// Only chat code is bundled +import { openaiText } from '@tanstack/ai-openai' +``` + +vs. + +```ts +// All functionality is bundled (chat, embed, summarize) +import { openai } from '@tanstack/ai-openai' +``` + +## Adapter Types + +Each adapter type implements a specific interface: + +- `ChatAdapter` - Provides `chatStream()` method for streaming chat responses +- `EmbeddingAdapter` - Provides `createEmbeddings()` method for vector embeddings +- `SummarizeAdapter` - Provides `summarize()` method for text summarization + +All adapters have a `kind` property that indicates their type: + +```ts +const textAdapter = openaiText() +console.log(textAdapter.kind) // 'chat' + +const embedAdapter = openaiEmbed() +console.log(embedAdapter.kind) // 'embedding' + +const summarizeAdapter = openaiSummarize() +console.log(summarizeAdapter.kind) // 'summarize' +``` diff --git a/docs/guides/video-generation.md b/docs/guides/video-generation.md new file mode 100644 index 00000000..54f61258 --- /dev/null +++ b/docs/guides/video-generation.md @@ -0,0 +1,331 @@ +# Video Generation (Experimental) + +> **⚠️ EXPERIMENTAL FEATURE WARNING** +> +> Video generation is an **experimental feature** that is subject to significant changes. Please read the caveats below carefully before using this feature. +> +> **Key Caveats:** +> - The API may change without notice in future versions +> - OpenAI's Sora API is in limited availability and may require organization verification +> - Video generation uses a jobs/polling architecture, which differs from other synchronous activities +> - Pricing, rate limits, and quotas may vary and are subject to change +> - Not all features described here may be available in your OpenAI account + +## Overview + +TanStack AI provides experimental support for video generation through dedicated video adapters. Unlike image generation, video generation is an **asynchronous operation** that uses a jobs/polling pattern: + +1. **Create a job** - Submit a prompt and receive a job ID +2. **Poll for status** - Check the job status until it's complete +3. **Retrieve the video** - Get the URL to download/view the generated video + +Currently supported: +- **OpenAI**: Sora-2 and Sora-2-Pro models (when available) + +## Basic Usage + +### Creating a Video Job + +```typescript +import { ai } from '@tanstack/ai' +import { openaiVideo } from '@tanstack/ai-openai' + +// Create a video adapter (uses OPENAI_API_KEY from environment) +const adapter = openaiVideo() + +// Start a video generation job +const { jobId, model } = await ai({ + adapter, + model: 'sora-2', + prompt: 'A golden retriever puppy playing in a field of sunflowers', +}) + +console.log('Job started:', jobId) +``` + +### Polling for Status + +```typescript +// Check the status of the job +const status = await ai({ + adapter, + model: 'sora-2', + jobId, + request: 'status', +}) + +console.log('Status:', status.status) // 'pending' | 'processing' | 'completed' | 'failed' +console.log('Progress:', status.progress) // 0-100 (if available) + +if (status.status === 'failed') { + console.error('Error:', status.error) +} +``` + +### Getting the Video URL + +```typescript +// Only call this after status is 'completed' +const { url, expiresAt } = await ai({ + adapter, + model: 'sora-2', + jobId, + request: 'url', +}) + +console.log('Video URL:', url) +console.log('Expires at:', expiresAt) +``` + +### Complete Example with Polling Loop + +```typescript +import { ai } from '@tanstack/ai' +import { openaiVideo } from '@tanstack/ai-openai' + +async function generateVideo(prompt: string) { + const adapter = openaiVideo() + + // 1. Create the job + const { jobId } = await ai({ + adapter, + model: 'sora-2', + prompt, + size: '1280x720', + duration: 8, // 4, 8, or 12 seconds + }) + + console.log('Job created:', jobId) + + // 2. Poll for completion + let status = 'pending' + while (status !== 'completed' && status !== 'failed') { + // Wait 5 seconds between polls + await new Promise((resolve) => setTimeout(resolve, 5000)) + + const result = await ai({ + adapter, + model: 'sora-2', + jobId, + request: 'status', + }) + + status = result.status + console.log(`Status: ${status}${result.progress ? ` (${result.progress}%)` : ''}`) + + if (result.status === 'failed') { + throw new Error(result.error || 'Video generation failed') + } + } + + // 3. Get the video URL + const { url } = await ai({ + adapter, + model: 'sora-2', + jobId, + request: 'url', + }) + + return url +} + +// Usage +const videoUrl = await generateVideo('A cat playing piano in a jazz bar') +console.log('Video ready:', videoUrl) +``` + +## Options + +### Job Creation Options + +| Option | Type | Description | +|--------|------|-------------| +| `prompt` | `string` | Text description of the video to generate (required) | +| `size` | `string` | Video resolution in WIDTHxHEIGHT format | +| `duration` | `number` | Video duration in seconds (maps to `seconds` parameter in API) | +| `providerOptions` | `object` | Provider-specific options | + +### Supported Sizes + +Based on [OpenAI API docs](https://platform.openai.com/docs/api-reference/videos/create): + +| Size | Description | +|------|-------------| +| `1280x720` | 720p landscape (16:9) - default | +| `720x1280` | 720p portrait (9:16) | +| `1792x1024` | Wide landscape | +| `1024x1792` | Tall portrait | + +### Supported Durations + +The API uses the `seconds` parameter. Allowed values: + +- `4` seconds +- `8` seconds (default) +- `12` seconds + +## Provider Options + +### OpenAI Provider Options + +Based on the [OpenAI Sora API](https://platform.openai.com/docs/api-reference/videos/create): + +```typescript +const { jobId } = await ai({ + adapter, + model: 'sora-2', + prompt: 'A beautiful sunset over the ocean', + size: '1280x720', // '1280x720', '720x1280', '1792x1024', '1024x1792' + duration: 8, // 4, 8, or 12 seconds + providerOptions: { + size: '1280x720', // Alternative way to specify size + seconds: 8, // Alternative way to specify duration + } +}) +``` + +## Response Types + +### VideoJobResult (from create) + +```typescript +interface VideoJobResult { + jobId: string // Unique job identifier for polling + model: string // Model used for generation +} +``` + +### VideoStatusResult (from status) + +```typescript +interface VideoStatusResult { + jobId: string + status: 'pending' | 'processing' | 'completed' | 'failed' + progress?: number // 0-100, if available + error?: string // Error message if failed +} +``` + +### VideoUrlResult (from url) + +```typescript +interface VideoUrlResult { + jobId: string + url: string // URL to download/stream the video + expiresAt?: Date // When the URL expires +} +``` + +## Model Variants + +| Model | Description | Use Case | +|-------|-------------|----------| +| `sora-2` | Faster generation, good quality | Rapid iteration, prototyping | +| `sora-2-pro` | Higher quality, slower | Production-quality output | + +## Error Handling + +Video generation can fail for various reasons. Always implement proper error handling: + +```typescript +try { + const { jobId } = await ai({ + adapter, + model: 'sora-2', + prompt: 'A scene', + }) + + // Poll for status... + const status = await ai({ + adapter, + model: 'sora-2', + jobId, + request: 'status', + }) + + if (status.status === 'failed') { + console.error('Generation failed:', status.error) + // Handle failure (e.g., retry, notify user) + } +} catch (error) { + if (error.message.includes('Video generation API is not available')) { + console.error('Sora API access may be required. Check your OpenAI account.') + } else if (error.message.includes('rate limit')) { + console.error('Rate limited. Please wait before trying again.') + } else { + console.error('Unexpected error:', error) + } +} +``` + +## Rate Limits and Quotas + +> **⚠️ Note:** Rate limits and quotas for video generation are subject to change and may vary by account tier. + +Typical considerations: +- Video generation is computationally expensive +- Concurrent job limits may apply +- Monthly generation quotas may exist +- Longer/higher-quality videos consume more quota + +Check the [OpenAI documentation](https://platform.openai.com/docs) for current limits. + +## Environment Variables + +The video adapter uses the same environment variable as other OpenAI adapters: + +- `OPENAI_API_KEY`: Your OpenAI API key + +## Explicit API Keys + +For production use or when you need explicit control: + +```typescript +import { createOpenaiVideo } from '@tanstack/ai-openai' + +const adapter = createOpenaiVideo('your-openai-api-key') +``` + +## Differences from Image Generation + +| Aspect | Image Generation | Video Generation | +|--------|-----------------|------------------| +| API Type | Synchronous | Jobs/Polling | +| Return Type | `ImageGenerationResult` | `VideoJobResult` → `VideoStatusResult` → `VideoUrlResult` | +| Wait Time | Seconds | Minutes | +| Multiple Outputs | `numberOfImages` option | Not supported | +| Options Field | `prompt`, `size`, `numberOfImages` | `prompt`, `size`, `duration` | + +## Known Limitations + +> **⚠️ These limitations are subject to change as the feature evolves.** + +1. **API Availability**: The Sora API may not be available in all OpenAI accounts +2. **Generation Time**: Video generation can take several minutes +3. **URL Expiration**: Generated video URLs may expire after a certain period +4. **No Real-time Progress**: Progress updates may be limited or delayed +5. **Audio Limitations**: Audio generation support may be limited +6. **Prompt Length**: Long prompts may be truncated + +## Best Practices + +1. **Implement Timeouts**: Set reasonable timeouts for the polling loop +2. **Handle Failures Gracefully**: Have fallback behavior for failed generations +3. **Cache URLs**: Store video URLs and check expiration before re-fetching +4. **User Feedback**: Show clear progress indicators during generation +5. **Validate Prompts**: Check prompt length and content before submission +6. **Monitor Usage**: Track generation usage to avoid hitting quotas + +## Future Considerations + +This feature is experimental. Future versions may include: + +- Additional video models and providers +- Streaming progress updates +- Video editing and manipulation +- Audio track generation +- Batch video generation +- Custom style/aesthetic controls + +Stay tuned to the [TanStack AI changelog](https://github.com/TanStack/ai/blob/main/CHANGELOG.md) for updates. + diff --git a/docs/protocol/http-stream-protocol.md b/docs/protocol/http-stream-protocol.md index 8330cd42..a474301d 100644 --- a/docs/protocol/http-stream-protocol.md +++ b/docs/protocol/http-stream-protocol.md @@ -173,15 +173,15 @@ Unlike SSE, HTTP streaming does not provide automatic reconnection: TanStack AI doesn't provide a built-in NDJSON formatter, but you can create one easily: ```typescript -import { chat } from '@tanstack/ai'; -import { openai } from '@tanstack/ai-openai'; +import { ai } from '@tanstack/ai'; +import { openaiText } from '@tanstack/ai-openai'; export async function POST(request: Request) { const { messages } = await request.json(); const encoder = new TextEncoder(); - const stream = chat({ - adapter: openai(), + const stream = ai({ + adapter: openaiText(), messages, model: 'gpt-4o', }); @@ -222,8 +222,8 @@ export async function POST(request: Request) { ```typescript import express from 'express'; -import { chat } from '@tanstack/ai'; -import { openai } from '@tanstack/ai-openai'; +import { ai } from '@tanstack/ai'; +import { openaiText } from '@tanstack/ai-openai'; const app = express(); app.use(express.json()); @@ -236,8 +236,8 @@ app.post('/api/chat', async (req, res) => { res.setHeader('Transfer-Encoding', 'chunked'); try { - const stream = chat({ - adapter: openai(), + const stream = ai({ + adapter: openaiText(), messages, model: 'gpt-4o', }); diff --git a/docs/protocol/sse-protocol.md b/docs/protocol/sse-protocol.md index 1f9f3d9e..b18713d1 100644 --- a/docs/protocol/sse-protocol.md +++ b/docs/protocol/sse-protocol.md @@ -167,14 +167,14 @@ SSE provides automatic reconnection: TanStack AI provides `toServerSentEventsStream()` and `toStreamResponse()` utilities: ```typescript -import { chat, toStreamResponse } from '@tanstack/ai'; -import { openai } from '@tanstack/ai-openai'; +import { ai, toStreamResponse } from '@tanstack/ai'; +import { openaiText } from '@tanstack/ai-openai'; export async function POST(request: Request) { const { messages } = await request.json(); - const stream = chat({ - adapter: openai(), + const stream = ai({ + adapter: openaiText(), messages, model: 'gpt-4o', }); @@ -224,7 +224,7 @@ export async function POST(request: Request) { const stream = new ReadableStream({ async start(controller) { try { - for await (const chunk of chat({ ... })) { + for await (const chunk of ai({ ... })) { const sseData = `data: ${JSON.stringify(chunk)}\n\n`; controller.enqueue(encoder.encode(sseData)); } diff --git a/docs/reference/classes/BaseAdapter.md b/docs/reference/classes/BaseAdapter.md index 1127e644..94e8007c 100644 --- a/docs/reference/classes/BaseAdapter.md +++ b/docs/reference/classes/BaseAdapter.md @@ -237,7 +237,7 @@ Defined in: [base-adapter.ts:74](https://github.com/TanStack/ai/blob/main/packag ##### options -[`ChatOptions`](../interfaces/ChatOptions.md) +[`TextOptions`](../interfaces/TextOptions.md) #### Returns diff --git a/docs/reference/functions/chat.md b/docs/reference/functions/text.md similarity index 94% rename from docs/reference/functions/chat.md rename to docs/reference/functions/text.md index 16934d76..ec320476 100644 --- a/docs/reference/functions/chat.md +++ b/docs/reference/functions/text.md @@ -29,7 +29,7 @@ Includes automatic tool execution loop ### options -[`ChatStreamOptionsForModel`](../type-aliases/ChatStreamOptionsForModel.md)\<`TAdapter`, `TModel`\> +[`TextStreamOptionsForModel`](../type-aliases/TextStreamOptionsForModel.md)\<`TAdapter`, `TModel`\> Chat options diff --git a/docs/reference/functions/chatOptions.md b/docs/reference/functions/textOptions.md similarity index 77% rename from docs/reference/functions/chatOptions.md rename to docs/reference/functions/textOptions.md index d776680b..a9c7d386 100644 --- a/docs/reference/functions/chatOptions.md +++ b/docs/reference/functions/textOptions.md @@ -1,12 +1,12 @@ --- -id: chatOptions -title: chatOptions +id: textOptions +title: textOptions --- -# Function: chatOptions() +# Function: textOptions() ```ts -function chatOptions(options): Omit, "model" | "providerOptions" | "messages" | "abortController"> & object; +function textOptions(options): Omit, "model" | "providerOptions" | "messages" | "abortController"> & object; ``` Defined in: [utilities/chat-options.ts:3](https://github.com/TanStack/ai/blob/main/packages/typescript/ai/src/utilities/chat-options.ts#L3) @@ -25,8 +25,8 @@ Defined in: [utilities/chat-options.ts:3](https://github.com/TanStack/ai/blob/ma ### options -`Omit`\<[`ChatStreamOptionsUnion`](../type-aliases/ChatStreamOptionsUnion.md)\<`TAdapter`\>, `"model"` \| `"providerOptions"` \| `"messages"` \| `"abortController"`\> & `object` +`Omit`\<[`TextStreamOptionsUnion`](../type-aliases/TextStreamOptionsUnion.md)\<`TAdapter`\>, `"model"` \| `"providerOptions"` \| `"messages"` \| `"abortController"`\> & `object` ## Returns -`Omit`\<[`ChatStreamOptionsUnion`](../type-aliases/ChatStreamOptionsUnion.md)\<`TAdapter`\>, `"model"` \| `"providerOptions"` \| `"messages"` \| `"abortController"`\> & `object` +`Omit`\<[`TextStreamOptionsUnion`](../type-aliases/TextStreamOptionsUnion.md)\<`TAdapter`\>, `"model"` \| `"providerOptions"` \| `"messages"` \| `"abortController"`\> & `object` diff --git a/docs/reference/index.md b/docs/reference/index.md index a3d506c0..b28dc1bc 100644 --- a/docs/reference/index.md +++ b/docs/reference/index.md @@ -25,8 +25,8 @@ title: "@tanstack/ai" - [ApprovalRequestedStreamChunk](interfaces/ApprovalRequestedStreamChunk.md) - [AudioPart](interfaces/AudioPart.md) - [BaseStreamChunk](interfaces/BaseStreamChunk.md) -- [ChatCompletionChunk](interfaces/ChatCompletionChunk.md) -- [ChatOptions](interfaces/ChatOptions.md) +- [TextCompletionChunk](interfaces/TextCompletionChunk.md) +- [TextOptions](interfaces/TextOptions.md) - [ChunkRecording](interfaces/ChunkRecording.md) - [ChunkStrategy](interfaces/ChunkStrategy.md) - [ClientTool](interfaces/ClientTool.md) @@ -73,8 +73,8 @@ title: "@tanstack/ai" - [AgentLoopStrategy](type-aliases/AgentLoopStrategy.md) - [AnyClientTool](type-aliases/AnyClientTool.md) -- [ChatStreamOptionsForModel](type-aliases/ChatStreamOptionsForModel.md) -- [ChatStreamOptionsUnion](type-aliases/ChatStreamOptionsUnion.md) +- [TextStreamOptionsForModel](type-aliases/TextStreamOptionsForModel.md) +- [TextStreamOptionsUnion](type-aliases/TextStreamOptionsUnion.md) - [ConstrainedContent](type-aliases/ConstrainedContent.md) - [ConstrainedModelMessage](type-aliases/ConstrainedModelMessage.md) - [ContentPart](type-aliases/ContentPart.md) @@ -101,8 +101,8 @@ title: "@tanstack/ai" ## Functions -- [chat](functions/chat.md) -- [chatOptions](functions/chatOptions.md) +- [chat](functions/text.md) +- [textOptions](functions/textOptions.md) - [combineStrategies](functions/combineStrategies.md) - [convertMessagesToModelMessages](functions/convertMessagesToModelMessages.md) - [convertZodToJsonSchema](functions/convertZodToJsonSchema.md) diff --git a/docs/reference/interfaces/AIAdapter.md b/docs/reference/interfaces/AIAdapter.md index 2ad47311..96bb97d4 100644 --- a/docs/reference/interfaces/AIAdapter.md +++ b/docs/reference/interfaces/AIAdapter.md @@ -133,7 +133,7 @@ Defined in: [types.ts:804](https://github.com/TanStack/ai/blob/main/packages/typ ##### options -[`ChatOptions`](ChatOptions.md)\<`string`, `TChatProviderOptions`\> +[`TextOptions`](TextOptions.md)\<`string`, `TChatProviderOptions`\> #### Returns diff --git a/docs/reference/interfaces/ChatCompletionChunk.md b/docs/reference/interfaces/TextCompletionChunk.md similarity index 93% rename from docs/reference/interfaces/ChatCompletionChunk.md rename to docs/reference/interfaces/TextCompletionChunk.md index 78235a12..fe2db802 100644 --- a/docs/reference/interfaces/ChatCompletionChunk.md +++ b/docs/reference/interfaces/TextCompletionChunk.md @@ -1,9 +1,9 @@ --- -id: ChatCompletionChunk -title: ChatCompletionChunk +id: TextCompletionChunk +title: TextCompletionChunk --- -# Interface: ChatCompletionChunk +# Interface: TextCompletionChunk Defined in: [types.ts:684](https://github.com/TanStack/ai/blob/main/packages/typescript/ai/src/types.ts#L684) diff --git a/docs/reference/interfaces/ChatOptions.md b/docs/reference/interfaces/TextOptions.md similarity index 97% rename from docs/reference/interfaces/ChatOptions.md rename to docs/reference/interfaces/TextOptions.md index 723a13d8..123ad406 100644 --- a/docs/reference/interfaces/ChatOptions.md +++ b/docs/reference/interfaces/TextOptions.md @@ -1,9 +1,9 @@ --- -id: ChatOptions -title: ChatOptions +id: TextOptions +title: TextOptions --- -# Interface: ChatOptions\ +# Interface: TextOptions\ Defined in: [types.ts:548](https://github.com/TanStack/ai/blob/main/packages/typescript/ai/src/types.ts#L548) diff --git a/docs/reference/type-aliases/ChatStreamOptionsForModel.md b/docs/reference/type-aliases/TextStreamOptionsForModel.md similarity index 65% rename from docs/reference/type-aliases/ChatStreamOptionsForModel.md rename to docs/reference/type-aliases/TextStreamOptionsForModel.md index 651be480..f0c05360 100644 --- a/docs/reference/type-aliases/ChatStreamOptionsForModel.md +++ b/docs/reference/type-aliases/TextStreamOptionsForModel.md @@ -1,18 +1,18 @@ --- -id: ChatStreamOptionsForModel -title: ChatStreamOptionsForModel +id: TextStreamOptionsForModel +title: TextStreamOptionsForModel --- -# Type Alias: ChatStreamOptionsForModel\ +# Type Alias: TextStreamOptionsForModel\ ```ts -type ChatStreamOptionsForModel = TAdapter extends AIAdapter ? Omit & object : never; +type TextStreamOptionsForModel = TAdapter extends AIAdapter ? Omit & object : never; ``` Defined in: [types.ts:883](https://github.com/TanStack/ai/blob/main/packages/typescript/ai/src/types.ts#L883) Chat options constrained by a specific model's capabilities. -Unlike ChatStreamOptionsUnion which creates a union over all models, +Unlike TextStreamOptionsUnion which creates a union over all models, this type takes a specific model and constrains messages accordingly. ## Type Parameters diff --git a/docs/reference/type-aliases/ChatStreamOptionsUnion.md b/docs/reference/type-aliases/TextStreamOptionsUnion.md similarity index 68% rename from docs/reference/type-aliases/ChatStreamOptionsUnion.md rename to docs/reference/type-aliases/TextStreamOptionsUnion.md index 02e3cb26..5db11467 100644 --- a/docs/reference/type-aliases/ChatStreamOptionsUnion.md +++ b/docs/reference/type-aliases/TextStreamOptionsUnion.md @@ -1,12 +1,12 @@ --- -id: ChatStreamOptionsUnion -title: ChatStreamOptionsUnion +id: TextStreamOptionsUnion +title: TextStreamOptionsUnion --- -# Type Alias: ChatStreamOptionsUnion\ +# Type Alias: TextStreamOptionsUnion\ ```ts -type ChatStreamOptionsUnion = TAdapter extends AIAdapter ? Models[number] extends infer TModel ? TModel extends string ? Omit & object : never : never : never; +type TextStreamOptionsUnion = TAdapter extends AIAdapter ? Models[number] extends infer TModel ? TModel extends string ? Omit & object : never : never : never; ``` Defined in: [types.ts:823](https://github.com/TanStack/ai/blob/main/packages/typescript/ai/src/types.ts#L823) diff --git a/examples/README.md b/examples/README.md index 460baa07..9abefa19 100644 --- a/examples/README.md +++ b/examples/README.md @@ -308,10 +308,10 @@ All examples use SSE for real-time streaming: **Backend (TypeScript):** ```typescript -import { chat, toStreamResponse } from '@tanstack/ai' +import { ai, toStreamResponse } from '@tanstack/ai' import { openai } from '@tanstack/ai-openai' -const stream = chat({ +const stream = ai({ adapter: openai(), model: 'gpt-4o', messages, @@ -360,7 +360,7 @@ const client = new ChatClient({ The TypeScript backend (`@tanstack/ai`) automatically handles tool execution: ```typescript -import { chat, toolDefinition } from '@tanstack/ai' +import { ai, toolDefinition } from '@tanstack/ai' import { z } from 'zod' // Step 1: Define the tool schema diff --git a/examples/ts-group-chat/chat-server/capnweb-rpc.ts b/examples/ts-group-chat/chat-server/capnweb-rpc.ts index ffa2bcd1..f9c067ba 100644 --- a/examples/ts-group-chat/chat-server/capnweb-rpc.ts +++ b/examples/ts-group-chat/chat-server/capnweb-rpc.ts @@ -1,14 +1,15 @@ // Cap'n Web RPC server implementation for chat import { RpcTarget } from 'capnweb' -import { WebSocket } from 'ws' import { ChatLogic } from './chat-logic.js' +import type { WebSocket } from 'ws' + // Local type definition to avoid importing from @tanstack/ai at module parse time interface ModelMessage { role: 'system' | 'user' | 'assistant' | 'tool' content?: string toolCallId?: string - toolCalls?: any[] + toolCalls?: Array } // Lazy-load claude service to avoid importing AI packages at module parse time @@ -57,7 +58,7 @@ export const activeServers = new Set() export const userMessageQueues = new Map>() // Global registry of client callbacks -export const clients = new Map() +export const clients = new Map) => void>() // Chat Server Implementation (one per connection) export class ChatServer extends RpcTarget { @@ -95,7 +96,7 @@ export class ChatServer extends RpcTarget { console.log(`📬 Exclude user: ${excludeUser || 'none'}`) let successCount = 0 - const successful: string[] = [] + const successful: Array = [] for (const username of clients.keys()) { if (excludeUser && username === excludeUser) { @@ -150,7 +151,10 @@ export class ChatServer extends RpcTarget { } // Client joins the chat - async joinChat(username: string, notificationCallback: Function) { + async joinChat( + username: string, + notificationCallback: (...args: Array) => void, + ) { console.log(`${username} is joining the chat`) this.currentUsername = username @@ -264,7 +268,7 @@ export class ChatServer extends RpcTarget { ) // Build conversation history for Claude - const conversationHistory: ModelMessage[] = globalChat + const conversationHistory: Array = globalChat .getMessages() .map((msg) => ({ role: 'user' as const, @@ -345,7 +349,7 @@ export class ChatServer extends RpcTarget { }) // Get conversation history from the current request - const conversationHistory: ModelMessage[] = globalChat + const conversationHistory: Array = globalChat .getMessages() .map((msg) => ({ role: 'user' as const, @@ -409,7 +413,7 @@ export class ChatServer extends RpcTarget { } // Stream Claude response (for future use if needed) - async *streamClaudeResponse(conversationHistory: ModelMessage[]) { + async *streamClaudeResponse(conversationHistory: Array) { const claudeService = await getClaudeService() yield* claudeService.streamResponse(conversationHistory) } diff --git a/examples/ts-group-chat/chat-server/chat-logic.ts b/examples/ts-group-chat/chat-server/chat-logic.ts index b6d7ac79..6133a55b 100644 --- a/examples/ts-group-chat/chat-server/chat-logic.ts +++ b/examples/ts-group-chat/chat-server/chat-logic.ts @@ -8,8 +8,8 @@ export interface ChatMessage { } export interface ChatState { - onlineUsers: string[] - messages: ChatMessage[] + onlineUsers: Array + messages: Array } // Core chat business logic class @@ -105,11 +105,11 @@ export class ChatLogic { } } - getMessages(): ChatMessage[] { + getMessages(): Array { return [...this.chatState.messages] } - getOnlineUsers(): string[] { + getOnlineUsers(): Array { return [...this.chatState.onlineUsers] } } diff --git a/examples/ts-group-chat/chat-server/claude-service.ts b/examples/ts-group-chat/chat-server/claude-service.ts index 0d2d6dbc..7853ccd0 100644 --- a/examples/ts-group-chat/chat-server/claude-service.ts +++ b/examples/ts-group-chat/chat-server/claude-service.ts @@ -1,6 +1,6 @@ // Claude AI service for handling queued AI responses -import { anthropic } from '@tanstack/ai-anthropic' -import { chat, toolDefinition } from '@tanstack/ai' +import { anthropicText } from '@tanstack/ai-anthropic' +import { ai, toolDefinition } from '@tanstack/ai' import type { JSONSchema, ModelMessage, StreamChunk } from '@tanstack/ai' // Define input schema for getWeather tool using JSONSchema @@ -92,7 +92,7 @@ export interface ClaudeQueueStatus { } export class ClaudeService { - private adapter = anthropic() // Uses ANTHROPIC_API_KEY from env + private adapter = anthropicText() // Uses ANTHROPIC_API_KEY from env private queue: Array = [] private currentRequest: ClaudeRequest | null = null private isProcessing = false @@ -149,7 +149,7 @@ export class ClaudeService { let chunkCount = 0 let accumulatedContent = '' - for await (const chunk of chat({ + for await (const chunk of ai({ adapter: this.adapter, systemPrompts: [systemMessage], messages: [...conversationHistory] as any, diff --git a/examples/ts-group-chat/package.json b/examples/ts-group-chat/package.json index 72e64073..90137ede 100644 --- a/examples/ts-group-chat/package.json +++ b/examples/ts-group-chat/package.json @@ -8,21 +8,21 @@ "test": "exit 0" }, "dependencies": { - "@tailwindcss/vite": "^4.1.17", + "@tailwindcss/vite": "^4.1.18", "@tanstack/ai": "workspace:*", "@tanstack/ai-anthropic": "workspace:*", "@tanstack/ai-client": "workspace:*", "@tanstack/ai-react": "workspace:*", "@tanstack/react-devtools": "^0.8.2", - "@tanstack/react-router": "^1.139.7", + "@tanstack/react-router": "^1.141.1", "@tanstack/react-router-devtools": "^1.139.7", "@tanstack/react-router-ssr-query": "^1.139.7", - "@tanstack/react-start": "^1.139.8", + "@tanstack/react-start": "^1.141.1", "@tanstack/router-plugin": "^1.139.7", "capnweb": "^0.1.0", - "react": "^19.2.0", - "react-dom": "^19.2.0", - "tailwindcss": "^4.1.17", + "react": "^19.2.3", + "react-dom": "^19.2.3", + "tailwindcss": "^4.1.18", "vite-tsconfig-paths": "^5.1.4", "ws": "^8.18.3" }, @@ -34,10 +34,10 @@ "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "@types/ws": "^8.18.1", - "@vitejs/plugin-react": "^5.1.1", + "@vitejs/plugin-react": "^5.1.2", "jsdom": "^27.2.0", "typescript": "5.9.3", - "vite": "^7.2.4", + "vite": "^7.2.7", "vitest": "^4.0.14", "web-vitals": "^5.1.0" } diff --git a/examples/ts-react-chat/package.json b/examples/ts-react-chat/package.json index 3c4ebfb4..f9a10c40 100644 --- a/examples/ts-react-chat/package.json +++ b/examples/ts-react-chat/package.json @@ -9,7 +9,7 @@ "test": "exit 0" }, "dependencies": { - "@tailwindcss/vite": "^4.1.17", + "@tailwindcss/vite": "^4.1.18", "@tanstack/ai": "workspace:*", "@tanstack/ai-anthropic": "workspace:*", "@tanstack/ai-client": "workspace:*", @@ -18,25 +18,25 @@ "@tanstack/ai-openai": "workspace:*", "@tanstack/ai-react": "workspace:*", "@tanstack/ai-react-ui": "workspace:*", - "@tanstack/nitro-v2-vite-plugin": "^1.139.0", + "@tanstack/nitro-v2-vite-plugin": "^1.141.0", "@tanstack/react-devtools": "^0.8.2", - "@tanstack/react-router": "^1.139.7", + "@tanstack/react-router": "^1.141.1", "@tanstack/react-router-devtools": "^1.139.7", "@tanstack/react-router-ssr-query": "^1.139.7", - "@tanstack/react-start": "^1.139.8", + "@tanstack/react-start": "^1.141.1", "@tanstack/react-store": "^0.8.0", "@tanstack/router-plugin": "^1.139.7", "@tanstack/store": "^0.8.0", "highlight.js": "^11.11.1", - "lucide-react": "^0.555.0", - "react": "^19.2.0", - "react-dom": "^19.2.0", + "lucide-react": "^0.561.0", + "react": "^19.2.3", + "react-dom": "^19.2.3", "react-markdown": "^10.1.0", "rehype-highlight": "^7.0.2", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", - "tailwindcss": "^4.1.17", + "tailwindcss": "^4.1.18", "vite-tsconfig-paths": "^5.1.4", "zod": "^4.1.13" }, @@ -48,10 +48,10 @@ "@types/node": "^24.10.1", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^5.1.1", + "@vitejs/plugin-react": "^5.1.2", "jsdom": "^27.2.0", "typescript": "5.9.3", - "vite": "^7.2.4", + "vite": "^7.2.7", "vitest": "^4.0.14", "web-vitals": "^5.1.0" } diff --git a/examples/ts-react-chat/src/routes/api.tanchat.ts b/examples/ts-react-chat/src/routes/api.tanchat.ts index 39cf0d5f..56c3303c 100644 --- a/examples/ts-react-chat/src/routes/api.tanchat.ts +++ b/examples/ts-react-chat/src/routes/api.tanchat.ts @@ -1,9 +1,9 @@ import { createFileRoute } from '@tanstack/react-router' -import { chat, maxIterations, toStreamResponse } from '@tanstack/ai' -import { openai } from '@tanstack/ai-openai' -import { ollama } from '@tanstack/ai-ollama' -import { anthropic } from '@tanstack/ai-anthropic' -import { gemini } from '@tanstack/ai-gemini' +import { ai, maxIterations, toStreamResponse } from '@tanstack/ai' +import { openaiText } from '@tanstack/ai-openai' +import { ollamaText } from '@tanstack/ai-ollama' +import { anthropicText } from '@tanstack/ai-anthropic' +import { geminiText } from '@tanstack/ai-gemini' import { addToCartToolDef, addToWishListToolDef, @@ -73,20 +73,20 @@ export const Route = createFileRoute('/api/tanchat')({ switch (provider) { case 'anthropic': - adapter = anthropic() - defaultModel = 'claude-sonnet-4-5-20250929' + adapter = anthropicText() + defaultModel = 'claude-sonnet-4-5' break case 'gemini': - adapter = gemini() + adapter = geminiText() defaultModel = 'gemini-2.0-flash-exp' break case 'ollama': - adapter = ollama() + adapter = ollamaText() defaultModel = 'mistral:7b' break case 'openai': default: - adapter = openai() + adapter = openaiText() defaultModel = 'gpt-4o' break } @@ -97,7 +97,7 @@ export const Route = createFileRoute('/api/tanchat')({ `[API Route] Using provider: ${provider}, model: ${selectedModel}`, ) - const stream = chat({ + const stream = ai({ adapter: adapter as any, model: selectedModel as any, tools: [ @@ -113,7 +113,7 @@ export const Route = createFileRoute('/api/tanchat')({ abortController, conversationId, }) - return toStreamResponse(stream, { abortController }) + return toStreamResponse(stream as any, { abortController }) } catch (error: any) { console.error('[API Route] Error in chat request:', { message: error?.message, diff --git a/examples/ts-solid-chat/package.json b/examples/ts-solid-chat/package.json index 87920223..5c16d816 100644 --- a/examples/ts-solid-chat/package.json +++ b/examples/ts-solid-chat/package.json @@ -9,7 +9,7 @@ "test": "exit 0" }, "dependencies": { - "@tailwindcss/vite": "^4.1.17", + "@tailwindcss/vite": "^4.1.18", "@tanstack/ai": "workspace:*", "@tanstack/ai-anthropic": "workspace:*", "@tanstack/ai-client": "workspace:*", @@ -19,7 +19,7 @@ "@tanstack/ai-openai": "workspace:*", "@tanstack/ai-solid": "workspace:*", "@tanstack/ai-solid-ui": "workspace:*", - "@tanstack/nitro-v2-vite-plugin": "^1.139.0", + "@tanstack/nitro-v2-vite-plugin": "^1.141.0", "@tanstack/router-plugin": "^1.139.7", "@tanstack/solid-ai-devtools": "workspace:*", "@tanstack/solid-devtools": "^0.7.15", @@ -33,7 +33,7 @@ "lucide-solid": "^0.554.0", "solid-js": "^1.9.10", "solid-markdown": "^2.1.0", - "tailwindcss": "^4.1.17", + "tailwindcss": "^4.1.18", "vite-tsconfig-paths": "^5.1.4", "zod": "^4.1.13" }, @@ -45,7 +45,7 @@ "@types/node": "^24.10.1", "jsdom": "^27.2.0", "typescript": "5.9.3", - "vite": "^7.2.4", + "vite": "^7.2.7", "vite-plugin-solid": "^2.11.10", "vitest": "^4.0.14", "web-vitals": "^5.1.0" diff --git a/examples/ts-solid-chat/src/routes/api.chat.ts b/examples/ts-solid-chat/src/routes/api.chat.ts index 8c96e7ae..a86d1d7d 100644 --- a/examples/ts-solid-chat/src/routes/api.chat.ts +++ b/examples/ts-solid-chat/src/routes/api.chat.ts @@ -1,6 +1,6 @@ import { createFileRoute } from '@tanstack/solid-router' -import { chat, maxIterations, toStreamResponse } from '@tanstack/ai' -import { anthropic } from '@tanstack/ai-anthropic' +import { ai, maxIterations, toStreamResponse } from '@tanstack/ai' +import { anthropicText } from '@tanstack/ai-anthropic' import { serverTools } from '@/lib/guitar-tools' const SYSTEM_PROMPT = `You are a helpful assistant for a guitar store. @@ -56,9 +56,9 @@ export const Route = createFileRoute('/api/chat')({ const { messages } = await request.json() try { // Use the stream abort signal for proper cancellation handling - const stream = chat({ - adapter: anthropic(), - model: 'claude-sonnet-4-5-20250929', + const stream = ai({ + adapter: anthropicText(), + model: 'claude-sonnet-4-5', tools: serverTools, systemPrompts: [SYSTEM_PROMPT], agentLoopStrategy: maxIterations(20), diff --git a/examples/ts-svelte-chat/package.json b/examples/ts-svelte-chat/package.json index 6c8e552d..7c3a40c7 100644 --- a/examples/ts-svelte-chat/package.json +++ b/examples/ts-svelte-chat/package.json @@ -29,13 +29,13 @@ "@sveltejs/adapter-auto": "^3.3.1", "@sveltejs/kit": "^2.15.10", "@sveltejs/vite-plugin-svelte": "^5.1.1", - "@tailwindcss/vite": "^4.1.17", + "@tailwindcss/vite": "^4.1.18", "@types/node": "^24.10.1", "svelte": "^5.20.0", "svelte-check": "^4.2.0", - "tailwindcss": "^4.1.17", + "tailwindcss": "^4.1.18", "tslib": "^2.8.1", "typescript": "5.9.3", - "vite": "^7.2.4" + "vite": "^7.2.7" } } diff --git a/examples/ts-svelte-chat/src/routes/api/chat/+server.ts b/examples/ts-svelte-chat/src/routes/api/chat/+server.ts index ee6a3195..9abcdbd4 100644 --- a/examples/ts-svelte-chat/src/routes/api/chat/+server.ts +++ b/examples/ts-svelte-chat/src/routes/api/chat/+server.ts @@ -1,11 +1,11 @@ -import { env } from '$env/dynamic/private' -import { chat, maxIterations, toStreamResponse } from '@tanstack/ai' -import { openai } from '@tanstack/ai-openai' -import { ollama } from '@tanstack/ai-ollama' -import { anthropic } from '@tanstack/ai-anthropic' -import { gemini } from '@tanstack/ai-gemini' +import { ai, maxIterations, toStreamResponse } from '@tanstack/ai' +import { openaiText } from '@tanstack/ai-openai' +import { ollamaText } from '@tanstack/ai-ollama' +import { anthropicText } from '@tanstack/ai-anthropic' +import { geminiText } from '@tanstack/ai-gemini' import type { RequestHandler } from './$types' +import { env } from '$env/dynamic/private' import { addToCartToolDef, @@ -81,20 +81,20 @@ export const POST: RequestHandler = async ({ request }) => { switch (provider) { case 'anthropic': - adapter = anthropic() - defaultModel = 'claude-sonnet-4-5-20250929' + adapter = anthropicText() + defaultModel = 'claude-sonnet-4-5' break case 'gemini': - adapter = gemini() + adapter = geminiText() defaultModel = 'gemini-2.0-flash-exp' break case 'ollama': - adapter = ollama() + adapter = ollamaText() defaultModel = 'mistral:7b' break case 'openai': default: - adapter = openai() + adapter = openaiText() defaultModel = 'gpt-4o' break } @@ -102,7 +102,7 @@ export const POST: RequestHandler = async ({ request }) => { // Determine model - use provided model or default based on provider const selectedModel = model || defaultModel - const stream = chat({ + const stream = ai({ adapter: adapter as any, model: selectedModel as any, tools: [ diff --git a/examples/ts-vue-chat/package.json b/examples/ts-vue-chat/package.json index d35140bb..1f58ba7f 100644 --- a/examples/ts-vue-chat/package.json +++ b/examples/ts-vue-chat/package.json @@ -24,17 +24,17 @@ "zod": "^4.1.13" }, "devDependencies": { - "@tailwindcss/vite": "^4.1.17", + "@tailwindcss/vite": "^4.1.18", "@types/node": "^24.10.1", "@vitejs/plugin-vue": "^5.2.3", "autoprefixer": "^10.4.21", "concurrently": "^9.1.2", "dotenv": "^17.2.3", "express": "^5.1.0", - "tailwindcss": "^4.1.17", + "tailwindcss": "^4.1.18", "tsx": "^4.20.6", "typescript": "5.9.3", - "vite": "^7.2.4", + "vite": "^7.2.7", "vue-tsc": "^2.2.10" } } diff --git a/examples/ts-vue-chat/vite.config.ts b/examples/ts-vue-chat/vite.config.ts index 7d3f3093..c8aab18c 100644 --- a/examples/ts-vue-chat/vite.config.ts +++ b/examples/ts-vue-chat/vite.config.ts @@ -2,11 +2,11 @@ import { fileURLToPath, URL } from 'node:url' import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import tailwindcss from '@tailwindcss/vite' -import { chat, maxIterations, toStreamResponse } from '@tanstack/ai' -import { openai } from '@tanstack/ai-openai' -import { anthropic } from '@tanstack/ai-anthropic' -import { gemini } from '@tanstack/ai-gemini' -import { ollama } from '@tanstack/ai-ollama' +import { ai, maxIterations, toStreamResponse } from '@tanstack/ai' +import { openaiText } from '@tanstack/ai-openai' +import { anthropicText } from '@tanstack/ai-anthropic' +import { geminiText } from '@tanstack/ai-gemini' +import { ollamaText } from '@tanstack/ai-ollama' import { toolDefinition } from '@tanstack/ai' import { z } from 'zod' import dotenv from 'dotenv' @@ -206,20 +206,20 @@ export default defineConfig({ switch (provider) { case 'anthropic': - adapter = anthropic() + adapter = anthropicText() defaultModel = 'claude-sonnet-4-5-20250929' break case 'gemini': - adapter = gemini() + adapter = geminiText() defaultModel = 'gemini-2.0-flash-exp' break case 'ollama': - adapter = ollama() + adapter = ollamaText() defaultModel = 'mistral:7b' break case 'openai': default: - adapter = openai() + adapter = openaiText() defaultModel = 'gpt-4o' break } @@ -231,7 +231,7 @@ export default defineConfig({ const abortController = new AbortController() - const stream = chat({ + const stream = ai({ adapter: adapter as any, model: selectedModel as any, tools: [ diff --git a/examples/vanilla-chat/package.json b/examples/vanilla-chat/package.json index 0f1fa49b..512b87b5 100644 --- a/examples/vanilla-chat/package.json +++ b/examples/vanilla-chat/package.json @@ -13,6 +13,6 @@ "@tanstack/ai-client": "workspace:*" }, "devDependencies": { - "vite": "^7.2.4" + "vite": "^7.2.7" } } diff --git a/package.json b/package.json index 2b7d740f..eb54e5b2 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "sherif": "^1.9.0", "tinyglobby": "^0.2.15", "typescript": "5.9.3", - "vite": "^7.2.4", + "vite": "^7.2.7", "vitest": "^4.0.14" } } diff --git a/packages/typescript/ai-anthropic/package.json b/packages/typescript/ai-anthropic/package.json index b3b500a7..6e02eb3a 100644 --- a/packages/typescript/ai-anthropic/package.json +++ b/packages/typescript/ai-anthropic/package.json @@ -44,10 +44,10 @@ "@tanstack/ai": "workspace:*" }, "devDependencies": { - "@vitest/coverage-v8": "4.0.14", - "zod": "^4.1.13" + "@vitest/coverage-v8": "4.0.14" }, "peerDependencies": { - "@tanstack/ai": "workspace:*" + "@tanstack/ai": "workspace:*", + "zod": "^4.0.0" } } diff --git a/packages/typescript/ai-anthropic/src/adapters/summarize.ts b/packages/typescript/ai-anthropic/src/adapters/summarize.ts new file mode 100644 index 00000000..89a6deee --- /dev/null +++ b/packages/typescript/ai-anthropic/src/adapters/summarize.ts @@ -0,0 +1,118 @@ +import { BaseSummarizeAdapter } from '@tanstack/ai/adapters' +import { ANTHROPIC_MODELS } from '../model-meta' +import { createAnthropicClient, getAnthropicApiKeyFromEnv } from '../utils' +import type { SummarizationOptions, SummarizationResult } from '@tanstack/ai' +import type { AnthropicClientConfig } from '../utils' + +/** + * Configuration for Anthropic summarize adapter + */ +export interface AnthropicSummarizeConfig extends AnthropicClientConfig {} + +/** + * Anthropic-specific provider options for summarization + */ +export interface AnthropicSummarizeProviderOptions { + /** Temperature for response generation (0-1) */ + temperature?: number + /** Maximum tokens in the response */ + maxTokens?: number +} + +/** + * Anthropic Summarize Adapter + * + * Tree-shakeable adapter for Anthropic summarization functionality. + * Import only what you need for smaller bundle sizes. + */ +export class AnthropicSummarizeAdapter extends BaseSummarizeAdapter< + typeof ANTHROPIC_MODELS, + AnthropicSummarizeProviderOptions +> { + readonly kind = 'summarize' as const + readonly name = 'anthropic' as const + readonly models = ANTHROPIC_MODELS + + private client: ReturnType + + constructor(config: AnthropicSummarizeConfig) { + super({}) + this.client = createAnthropicClient(config) + } + + async summarize(options: SummarizationOptions): Promise { + const systemPrompt = this.buildSummarizationPrompt(options) + + const response = await this.client.messages.create({ + model: options.model, + messages: [{ role: 'user', content: options.text }], + system: systemPrompt, + max_tokens: options.maxLength || 500, + temperature: 0.3, + stream: false, + }) + + const content = response.content + .map((c) => (c.type === 'text' ? c.text : '')) + .join('') + + return { + id: response.id, + model: response.model, + summary: content, + usage: { + promptTokens: response.usage.input_tokens, + completionTokens: response.usage.output_tokens, + totalTokens: response.usage.input_tokens + response.usage.output_tokens, + }, + } + } + + private buildSummarizationPrompt(options: SummarizationOptions): string { + let prompt = 'You are a professional summarizer. ' + + switch (options.style) { + case 'bullet-points': + prompt += 'Provide a summary in bullet point format. ' + break + case 'paragraph': + prompt += 'Provide a summary in paragraph format. ' + break + case 'concise': + prompt += 'Provide a very concise summary in 1-2 sentences. ' + break + default: + prompt += 'Provide a clear and concise summary. ' + } + + if (options.focus && options.focus.length > 0) { + prompt += `Focus on the following aspects: ${options.focus.join(', ')}. ` + } + + if (options.maxLength) { + prompt += `Keep the summary under ${options.maxLength} tokens. ` + } + + return prompt + } +} + +/** + * Creates an Anthropic summarize adapter with explicit API key + */ +export function createAnthropicSummarize( + apiKey: string, + config?: Omit, +): AnthropicSummarizeAdapter { + return new AnthropicSummarizeAdapter({ apiKey, ...config }) +} + +/** + * Creates an Anthropic summarize adapter with automatic API key detection + */ +export function anthropicSummarize( + config?: Omit, +): AnthropicSummarizeAdapter { + const apiKey = getAnthropicApiKeyFromEnv() + return createAnthropicSummarize(apiKey, config) +} diff --git a/packages/typescript/ai-anthropic/src/adapters/text.ts b/packages/typescript/ai-anthropic/src/adapters/text.ts new file mode 100644 index 00000000..92ed8219 --- /dev/null +++ b/packages/typescript/ai-anthropic/src/adapters/text.ts @@ -0,0 +1,620 @@ +import { BaseTextAdapter } from '@tanstack/ai/adapters' +import { ANTHROPIC_MODELS } from '../model-meta' +import { convertToolsToProviderFormat } from '../tools/tool-converter' +import { validateTextProviderOptions } from '../text/text-provider-options' +import { + convertZodToAnthropicSchema, + createAnthropicClient, + generateId, + getAnthropicApiKeyFromEnv, +} from '../utils' +import type { + StructuredOutputOptions, + StructuredOutputResult, +} from '@tanstack/ai/adapters' +import type { + Base64ImageSource, + Base64PDFSource, + DocumentBlockParam, + ImageBlockParam, + MessageParam, + TextBlockParam, + URLImageSource, + URLPDFSource, +} from '@anthropic-ai/sdk/resources/messages' +import type Anthropic_SDK from '@anthropic-ai/sdk' +import type { + ContentPart, + ModelMessage, + StreamChunk, + TextOptions, +} from '@tanstack/ai' +import type { + AnthropicChatModelProviderOptionsByName, + AnthropicModelInputModalitiesByName, +} from '../model-meta' +import type { + ExternalTextProviderOptions, + InternalTextProviderOptions, +} from '../text/text-provider-options' +import type { + AnthropicDocumentMetadata, + AnthropicImageMetadata, + AnthropicMessageMetadataByModality, + AnthropicTextMetadata, +} from '../message-types' +import type { AnthropicClientConfig } from '../utils' + +/** + * Configuration for Anthropic text adapter + */ +export interface AnthropicTextConfig extends AnthropicClientConfig {} + +/** + * Anthropic-specific provider options for text/chat + */ +export type AnthropicTextProviderOptions = ExternalTextProviderOptions + +type AnthropicContentBlocks = + Extract> extends Array + ? Array + : never +type AnthropicContentBlock = + AnthropicContentBlocks extends Array ? Block : never + +/** + * Anthropic Text (Chat) Adapter + * + * Tree-shakeable adapter for Anthropic chat/text completion functionality. + * Import only what you need for smaller bundle sizes. + */ +export class AnthropicTextAdapter extends BaseTextAdapter< + typeof ANTHROPIC_MODELS, + AnthropicTextProviderOptions, + AnthropicChatModelProviderOptionsByName, + AnthropicModelInputModalitiesByName, + AnthropicMessageMetadataByModality +> { + readonly kind = 'text' as const + readonly name = 'anthropic' as const + readonly models = ANTHROPIC_MODELS + + declare _modelProviderOptionsByName: AnthropicChatModelProviderOptionsByName + declare _modelInputModalitiesByName: AnthropicModelInputModalitiesByName + declare _messageMetadataByModality: AnthropicMessageMetadataByModality + + private client: Anthropic_SDK + + constructor(config: AnthropicTextConfig) { + super({}) + this.client = createAnthropicClient(config) + } + + async *chatStream( + options: TextOptions, + ): AsyncIterable { + try { + const requestParams = this.mapCommonOptionsToAnthropic(options) + + const stream = await this.client.beta.messages.create( + { ...requestParams, stream: true }, + { + signal: options.request?.signal, + headers: options.request?.headers, + }, + ) + + yield* this.processAnthropicStream(stream, options.model, () => + generateId(this.name), + ) + } catch (error: unknown) { + const err = error as Error & { status?: number; code?: string } + yield { + type: 'error', + id: generateId(this.name), + model: options.model, + timestamp: Date.now(), + error: { + message: err.message || 'Unknown error occurred', + code: err.code || String(err.status), + }, + } + } + } + + /** + * Generate structured output using Anthropic's tool-based approach. + * Anthropic doesn't have native structured output, so we use a tool with the schema + * and force the model to call it. + */ + async structuredOutput( + options: StructuredOutputOptions, + ): Promise> { + const { chatOptions, outputSchema } = options + + // Convert Zod schema to Anthropic-compatible JSON Schema + const jsonSchema = convertZodToAnthropicSchema(outputSchema) + + const requestParams = this.mapCommonOptionsToAnthropic(chatOptions) + + // Create a tool that will capture the structured output + // Ensure the schema has type: 'object' as required by Anthropic's SDK + const inputSchema = { + type: 'object' as const, + ...jsonSchema, + } + + const structuredOutputTool = { + name: 'structured_output', + description: + 'Use this tool to provide your response in the required structured format.', + input_schema: inputSchema, + } + + try { + // Make non-streaming request with tool_choice forced to our structured output tool + const response = await this.client.messages.create( + { + ...requestParams, + stream: false, + tools: [structuredOutputTool], + tool_choice: { type: 'tool', name: 'structured_output' }, + }, + { + signal: chatOptions.request?.signal, + headers: chatOptions.request?.headers, + }, + ) + + // Extract the tool use content from the response + let parsed: unknown = null + let rawText = '' + + for (const block of response.content) { + if (block.type === 'tool_use' && block.name === 'structured_output') { + parsed = block.input + rawText = JSON.stringify(block.input) + break + } + } + + if (parsed === null) { + // Fallback: try to extract text content and parse as JSON + rawText = response.content + .map((b) => { + if (b.type === 'text') { + return b.text + } + return '' + }) + .join('') + try { + parsed = JSON.parse(rawText) + } catch { + throw new Error( + `Failed to extract structured output from response. Content: ${rawText.slice(0, 200)}${rawText.length > 200 ? '...' : ''}`, + ) + } + } + + return { + data: parsed, + rawText, + } + } catch (error: unknown) { + const err = error as Error + throw new Error( + `Structured output generation failed: ${err.message || 'Unknown error occurred'}`, + ) + } + } + + private mapCommonOptionsToAnthropic( + options: TextOptions, + ) { + const providerOptions = options.providerOptions as + | InternalTextProviderOptions + | undefined + + const formattedMessages = this.formatMessages(options.messages) + const tools = options.tools + ? convertToolsToProviderFormat(options.tools) + : undefined + + const validProviderOptions: Partial = {} + if (providerOptions) { + const validKeys: Array = [ + 'container', + 'context_management', + 'mcp_servers', + 'service_tier', + 'stop_sequences', + 'system', + 'thinking', + 'tool_choice', + 'top_k', + ] + for (const key of validKeys) { + if (key in providerOptions) { + const value = providerOptions[key] + if (key === 'tool_choice' && typeof value === 'string') { + ;(validProviderOptions as Record)[key] = { + type: value, + } + } else { + ;(validProviderOptions as Record)[key] = value + } + } + } + } + + const thinkingBudget = + validProviderOptions.thinking?.type === 'enabled' + ? validProviderOptions.thinking.budget_tokens + : undefined + const defaultMaxTokens = options.options?.maxTokens || 1024 + const maxTokens = + thinkingBudget && thinkingBudget >= defaultMaxTokens + ? thinkingBudget + 1 + : defaultMaxTokens + + const requestParams: InternalTextProviderOptions = { + model: options.model, + max_tokens: maxTokens, + temperature: options.options?.temperature, + top_p: options.options?.topP, + messages: formattedMessages, + system: options.systemPrompts?.join('\n'), + tools: tools, + ...validProviderOptions, + } + validateTextProviderOptions(requestParams) + return requestParams + } + + private convertContentPartToAnthropic( + part: ContentPart, + ): TextBlockParam | ImageBlockParam | DocumentBlockParam { + switch (part.type) { + case 'text': { + const metadata = part.metadata as AnthropicTextMetadata | undefined + return { + type: 'text', + text: part.content, + ...metadata, + } + } + + case 'image': { + const metadata = part.metadata as AnthropicImageMetadata | undefined + const imageSource: Base64ImageSource | URLImageSource = + part.source.type === 'data' + ? { + type: 'base64', + data: part.source.value, + media_type: metadata?.mediaType ?? 'image/jpeg', + } + : { + type: 'url', + url: part.source.value, + } + const { mediaType: _mediaType, ...meta } = metadata || {} + return { + type: 'image', + source: imageSource, + ...meta, + } + } + case 'document': { + const metadata = part.metadata as AnthropicDocumentMetadata | undefined + const docSource: Base64PDFSource | URLPDFSource = + part.source.type === 'data' + ? { + type: 'base64', + data: part.source.value, + media_type: 'application/pdf', + } + : { + type: 'url', + url: part.source.value, + } + return { + type: 'document', + source: docSource, + ...metadata, + } + } + case 'audio': + case 'video': + throw new Error( + `Anthropic does not support ${part.type} content directly`, + ) + default: { + const _exhaustiveCheck: never = part + throw new Error( + `Unsupported content part type: ${(_exhaustiveCheck as ContentPart).type}`, + ) + } + } + } + + private formatMessages( + messages: Array, + ): InternalTextProviderOptions['messages'] { + const formattedMessages: InternalTextProviderOptions['messages'] = [] + + for (const message of messages) { + const role = message.role + + if (role === 'tool' && message.toolCallId) { + formattedMessages.push({ + role: 'user', + content: [ + { + type: 'tool_result', + tool_use_id: message.toolCallId, + content: + typeof message.content === 'string' ? message.content : '', + }, + ], + }) + continue + } + + if (role === 'assistant' && message.toolCalls?.length) { + const contentBlocks: AnthropicContentBlocks = [] + + if (message.content) { + const content = + typeof message.content === 'string' ? message.content : '' + const textBlock: AnthropicContentBlock = { + type: 'text', + text: content, + } + contentBlocks.push(textBlock) + } + + for (const toolCall of message.toolCalls) { + let parsedInput: unknown = {} + try { + parsedInput = toolCall.function.arguments + ? JSON.parse(toolCall.function.arguments) + : {} + } catch { + parsedInput = toolCall.function.arguments + } + + const toolUseBlock: AnthropicContentBlock = { + type: 'tool_use', + id: toolCall.id, + name: toolCall.function.name, + input: parsedInput, + } + contentBlocks.push(toolUseBlock) + } + + formattedMessages.push({ + role: 'assistant', + content: contentBlocks, + }) + + continue + } + + if (role === 'user' && Array.isArray(message.content)) { + const contentBlocks = message.content.map((part) => + this.convertContentPartToAnthropic(part), + ) + formattedMessages.push({ + role: 'user', + content: contentBlocks, + }) + continue + } + + formattedMessages.push({ + role: role === 'assistant' ? 'assistant' : 'user', + content: + typeof message.content === 'string' + ? message.content + : message.content + ? message.content.map((c) => + this.convertContentPartToAnthropic(c), + ) + : '', + }) + } + + return formattedMessages + } + + private async *processAnthropicStream( + stream: AsyncIterable, + model: string, + genId: () => string, + ): AsyncIterable { + let accumulatedContent = '' + let accumulatedThinking = '' + const timestamp = Date.now() + const toolCallsMap = new Map< + number, + { id: string; name: string; input: string } + >() + let currentToolIndex = -1 + + try { + for await (const event of stream) { + if (event.type === 'content_block_start') { + if (event.content_block.type === 'tool_use') { + currentToolIndex++ + toolCallsMap.set(currentToolIndex, { + id: event.content_block.id, + name: event.content_block.name, + input: '', + }) + } else if (event.content_block.type === 'thinking') { + accumulatedThinking = '' + } + } else if (event.type === 'content_block_delta') { + if (event.delta.type === 'text_delta') { + const delta = event.delta.text + accumulatedContent += delta + yield { + type: 'content', + id: genId(), + model: model, + timestamp, + delta, + content: accumulatedContent, + role: 'assistant', + } + } else if (event.delta.type === 'thinking_delta') { + const delta = event.delta.thinking + accumulatedThinking += delta + yield { + type: 'thinking', + id: genId(), + model: model, + timestamp, + delta, + content: accumulatedThinking, + } + } else if (event.delta.type === 'input_json_delta') { + const existing = toolCallsMap.get(currentToolIndex) + if (existing) { + existing.input += event.delta.partial_json + + yield { + type: 'tool_call', + id: genId(), + model: model, + timestamp, + toolCall: { + id: existing.id, + type: 'function', + function: { + name: existing.name, + arguments: event.delta.partial_json, + }, + }, + index: currentToolIndex, + } + } + } + } else if (event.type === 'content_block_stop') { + const existing = toolCallsMap.get(currentToolIndex) + if (existing && existing.input === '') { + yield { + type: 'tool_call', + id: genId(), + model: model, + timestamp, + toolCall: { + id: existing.id, + type: 'function', + function: { + name: existing.name, + arguments: '{}', + }, + }, + index: currentToolIndex, + } + } + } else if (event.type === 'message_stop') { + yield { + type: 'done', + id: genId(), + model: model, + timestamp, + finishReason: 'stop', + } + } else if (event.type === 'message_delta') { + if (event.delta.stop_reason) { + switch (event.delta.stop_reason) { + case 'tool_use': { + yield { + type: 'done', + id: genId(), + model: model, + timestamp, + finishReason: 'tool_calls', + usage: { + promptTokens: event.usage.input_tokens || 0, + completionTokens: event.usage.output_tokens || 0, + totalTokens: + (event.usage.input_tokens || 0) + + (event.usage.output_tokens || 0), + }, + } + break + } + case 'max_tokens': { + yield { + type: 'error', + id: genId(), + model: model, + timestamp, + error: { + message: + 'The response was cut off because the maximum token limit was reached.', + code: 'max_tokens', + }, + } + break + } + default: { + yield { + type: 'done', + id: genId(), + model: model, + timestamp, + finishReason: 'stop', + usage: { + promptTokens: event.usage.input_tokens || 0, + completionTokens: event.usage.output_tokens || 0, + totalTokens: + (event.usage.input_tokens || 0) + + (event.usage.output_tokens || 0), + }, + } + } + } + } + } + } + } catch (error: unknown) { + const err = error as Error & { status?: number; code?: string } + + yield { + type: 'error', + id: genId(), + model: model, + timestamp, + error: { + message: err.message || 'Unknown error occurred', + code: err.code || String(err.status), + }, + } + } + } +} + +/** + * Creates an Anthropic text adapter with explicit API key + */ +export function createAnthropicText( + apiKey: string, + config?: Omit, +): AnthropicTextAdapter { + return new AnthropicTextAdapter({ apiKey, ...config }) +} + +/** + * Creates an Anthropic text adapter with automatic API key detection + */ +export function anthropicText( + config?: Omit, +): AnthropicTextAdapter { + const apiKey = getAnthropicApiKeyFromEnv() + return createAnthropicText(apiKey, config) +} diff --git a/packages/typescript/ai-anthropic/src/anthropic-adapter.ts b/packages/typescript/ai-anthropic/src/anthropic-adapter.ts index edfcea79..b95acd90 100644 --- a/packages/typescript/ai-anthropic/src/anthropic-adapter.ts +++ b/packages/typescript/ai-anthropic/src/anthropic-adapter.ts @@ -10,7 +10,6 @@ import type { AnthropicTextMetadata, } from './message-types' import type { - ChatOptions, ContentPart, EmbeddingOptions, EmbeddingResult, @@ -18,6 +17,7 @@ import type { StreamChunk, SummarizationOptions, SummarizationResult, + TextOptions, } from '@tanstack/ai' import type { AnthropicChatModelProviderOptionsByName, @@ -81,7 +81,7 @@ export class Anthropic extends BaseAdapter< } async *chatStream( - options: ChatOptions, + options: TextOptions, ): AsyncIterable { try { // Map common options to Anthropic format using the centralized mapping function @@ -192,7 +192,7 @@ export class Anthropic extends BaseAdapter< * Handles translation of normalized options to Anthropic's API format */ private mapCommonOptionsToAnthropic( - options: ChatOptions, + options: TextOptions, ) { const providerOptions = options.providerOptions as | InternalTextProviderOptions diff --git a/packages/typescript/ai-anthropic/src/index.ts b/packages/typescript/ai-anthropic/src/index.ts index b4580b15..096bf95a 100644 --- a/packages/typescript/ai-anthropic/src/index.ts +++ b/packages/typescript/ai-anthropic/src/index.ts @@ -1,9 +1,46 @@ +// ============================================================================ +// New Tree-Shakeable Adapters (Recommended) +// ============================================================================ + +// Text (Chat) adapter - for chat/text completion +export { + AnthropicTextAdapter, + anthropicText, + createAnthropicText, + type AnthropicTextConfig, + type AnthropicTextProviderOptions, +} from './adapters/text' + +// Summarize adapter - for text summarization +export { + AnthropicSummarizeAdapter, + anthropicSummarize, + createAnthropicSummarize, + type AnthropicSummarizeConfig, + type AnthropicSummarizeProviderOptions, +} from './adapters/summarize' + +// Note: Anthropic does not support embeddings natively + +// ============================================================================ +// Legacy Exports (Deprecated - will be removed in future versions) +// ============================================================================ + +/** + * @deprecated Use `anthropicText()` or `anthropicSummarize()` instead. + * This monolithic adapter will be removed in a future version. + */ export { Anthropic, createAnthropic, anthropic, type AnthropicConfig, } from './anthropic-adapter' + +// ============================================================================ +// Type Exports +// ============================================================================ + export type { AnthropicChatModelProviderOptionsByName, AnthropicModelInputModalitiesByName, diff --git a/packages/typescript/ai-anthropic/src/tools/custom-tool.ts b/packages/typescript/ai-anthropic/src/tools/custom-tool.ts index 07ac10a3..05f96dd8 100644 --- a/packages/typescript/ai-anthropic/src/tools/custom-tool.ts +++ b/packages/typescript/ai-anthropic/src/tools/custom-tool.ts @@ -1,4 +1,4 @@ -import { convertZodToJsonSchema } from '@tanstack/ai' +import { convertZodToAnthropicSchema } from '../utils/schema-converter' import type { Tool } from '@tanstack/ai' import type { z } from 'zod' import type { CacheControl } from '../text/text-provider-options' @@ -29,13 +29,15 @@ export function convertCustomToolToAdapterFormat(tool: Tool): CustomTool { const metadata = (tool.metadata as { cacheControl?: CacheControl | null } | undefined) || {} - // Convert Zod schema to JSON Schema - const jsonSchema = convertZodToJsonSchema(tool.inputSchema) + // Convert Zod schema to Anthropic-compatible JSON Schema + const jsonSchema = tool.inputSchema + ? convertZodToAnthropicSchema(tool.inputSchema) + : { type: 'object', properties: {}, required: [] } const inputSchema = { type: 'object' as const, - properties: jsonSchema?.properties || null, - required: jsonSchema?.required || null, + properties: jsonSchema.properties || null, + required: jsonSchema.required || null, } return { diff --git a/packages/typescript/ai-anthropic/src/utils/client.ts b/packages/typescript/ai-anthropic/src/utils/client.ts new file mode 100644 index 00000000..dddc5caf --- /dev/null +++ b/packages/typescript/ai-anthropic/src/utils/client.ts @@ -0,0 +1,45 @@ +import Anthropic_SDK from '@anthropic-ai/sdk' + +export interface AnthropicClientConfig { + apiKey: string +} + +/** + * Creates an Anthropic SDK client instance + */ +export function createAnthropicClient( + config: AnthropicClientConfig, +): Anthropic_SDK { + return new Anthropic_SDK({ + apiKey: config.apiKey, + }) +} + +/** + * Gets Anthropic API key from environment variables + * @throws Error if ANTHROPIC_API_KEY is not found + */ +export function getAnthropicApiKeyFromEnv(): string { + const env = + typeof globalThis !== 'undefined' && (globalThis as any).window?.env + ? (globalThis as any).window.env + : typeof process !== 'undefined' + ? process.env + : undefined + const key = env?.ANTHROPIC_API_KEY + + if (!key) { + throw new Error( + 'ANTHROPIC_API_KEY is required. Please set it in your environment variables or use the factory function with an explicit API key.', + ) + } + + return key +} + +/** + * Generates a unique ID with a prefix + */ +export function generateId(prefix: string): string { + return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(7)}` +} diff --git a/packages/typescript/ai-anthropic/src/utils/index.ts b/packages/typescript/ai-anthropic/src/utils/index.ts new file mode 100644 index 00000000..b6e55f53 --- /dev/null +++ b/packages/typescript/ai-anthropic/src/utils/index.ts @@ -0,0 +1,7 @@ +export { + createAnthropicClient, + generateId, + getAnthropicApiKeyFromEnv, + type AnthropicClientConfig, +} from './client' +export { convertZodToAnthropicSchema } from './schema-converter' diff --git a/packages/typescript/ai-anthropic/src/utils/schema-converter.ts b/packages/typescript/ai-anthropic/src/utils/schema-converter.ts new file mode 100644 index 00000000..5e17ecfa --- /dev/null +++ b/packages/typescript/ai-anthropic/src/utils/schema-converter.ts @@ -0,0 +1,87 @@ +import { toJSONSchema } from 'zod' +import type { z } from 'zod' + +/** + * Check if a value is a Zod schema by looking for Zod-specific internals. + * Zod schemas have a `_zod` property that contains metadata. + */ +function isZodSchema(schema: unknown): schema is z.ZodType { + return ( + typeof schema === 'object' && + schema !== null && + '_zod' in schema && + typeof (schema as any)._zod === 'object' + ) +} + +/** + * Converts a Zod schema to JSON Schema format compatible with Anthropic's API. + * + * Anthropic accepts standard JSON Schema without special transformations. + * + * @param schema - Zod schema to convert + * @returns JSON Schema object compatible with Anthropic's structured output API + * + * @example + * ```typescript + * import { z } from 'zod'; + * + * const zodSchema = z.object({ + * location: z.string().describe('City name'), + * unit: z.enum(['celsius', 'fahrenheit']).optional() + * }); + * + * const jsonSchema = convertZodToAnthropicSchema(zodSchema); + * // Returns standard JSON Schema + * ``` + */ +export function convertZodToAnthropicSchema( + schema: z.ZodType, +): Record { + if (!isZodSchema(schema)) { + throw new Error('Expected a Zod schema') + } + + // Use Zod's built-in toJSONSchema + const jsonSchema = toJSONSchema(schema, { + target: 'openapi-3.0', + reused: 'ref', + }) + + // Remove $schema property as it's not needed for LLM providers + let result = jsonSchema + if (typeof result === 'object' && '$schema' in result) { + const { $schema, ...rest } = result + result = rest + } + + // Ensure object schemas always have type: "object" + if (typeof result === 'object') { + const isZodObject = + typeof schema === 'object' && + 'def' in schema && + schema.def.type === 'object' + + if (isZodObject && !result.type) { + result.type = 'object' + } + + if (Object.keys(result).length === 0) { + result.type = 'object' + } + + if ('properties' in result && !result.type) { + result.type = 'object' + } + + if (result.type === 'object' && !('properties' in result)) { + result.properties = {} + } + + if (result.type === 'object' && !('required' in result)) { + result.required = [] + } + } + + return result +} diff --git a/packages/typescript/ai-anthropic/tests/anthropic-adapter.test.ts b/packages/typescript/ai-anthropic/tests/anthropic-adapter.test.ts index 934a9204..5c9f4f4a 100644 --- a/packages/typescript/ai-anthropic/tests/anthropic-adapter.test.ts +++ b/packages/typescript/ai-anthropic/tests/anthropic-adapter.test.ts @@ -1,9 +1,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest' -import { chat, type Tool, type StreamChunk } from '@tanstack/ai' -import { - Anthropic, - type AnthropicProviderOptions, -} from '../src/anthropic-adapter' +import { ai, type Tool, type StreamChunk } from '@tanstack/ai' +import { AnthropicTextAdapter } from '../src/adapters/text' +import type { AnthropicProviderOptions } from '../src/anthropic-adapter' import { z } from 'zod' const mocks = vi.hoisted(() => { @@ -37,7 +35,7 @@ vi.mock('@anthropic-ai/sdk', () => { return { default: MockAnthropic } }) -const createAdapter = () => new Anthropic({ apiKey: 'test-key' }) +const createAdapter = () => new AnthropicTextAdapter({ apiKey: 'test-key' }) const toolArguments = JSON.stringify({ location: 'Berlin' }) @@ -107,7 +105,7 @@ describe('Anthropic adapter option mapping', () => { // Consume the stream to trigger the API call const chunks: StreamChunk[] = [] - for await (const chunk of chat({ + for await (const chunk of ai({ adapter, model: 'claude-3-7-sonnet-20250219', messages: [ diff --git a/packages/typescript/ai-client/package.json b/packages/typescript/ai-client/package.json index 43f17818..00c52dbb 100644 --- a/packages/typescript/ai-client/package.json +++ b/packages/typescript/ai-client/package.json @@ -47,7 +47,7 @@ }, "devDependencies": { "@vitest/coverage-v8": "4.0.14", - "vite": "^7.2.4", + "vite": "^7.2.7", "zod": "^4.1.13" } } diff --git a/packages/typescript/ai-client/src/chat-client.ts b/packages/typescript/ai-client/src/chat-client.ts index 3b9e1787..1d1ba091 100644 --- a/packages/typescript/ai-client/src/chat-client.ts +++ b/packages/typescript/ai-client/src/chat-client.ts @@ -26,6 +26,7 @@ export class ChatClient { private clientToolsRef: { current: Map } private currentStreamId: string | null = null private currentMessageId: string | null = null + private postStreamActions: Array<() => Promise> = [] private callbacksRef: { current: { @@ -323,6 +324,9 @@ export class ChatClient { } finally { this.abortController = null this.setIsLoading(false) + + // Drain any actions that were queued while the stream was in progress + await this.drainPostStreamActions() } } @@ -394,10 +398,13 @@ export class ChatClient { result.errorText, ) - // Check if we should auto-send - if (this.shouldAutoSend()) { - await this.continueFlow() + // If stream is in progress, queue continuation check for after it ends + if (this.isLoading) { + this.queuePostStreamAction(() => this.checkForContinuation()) + return } + + await this.checkForContinuation() } /** @@ -433,18 +440,39 @@ export class ChatClient { // Add response via processor this.processor.addToolApprovalResponse(response.id, response.approved) - // Check if we should auto-send - if (this.shouldAutoSend()) { - await this.continueFlow() + // If stream is in progress, queue continuation check for after it ends + if (this.isLoading) { + this.queuePostStreamAction(() => this.checkForContinuation()) + return } + + await this.checkForContinuation() } /** - * Continue the agent flow with current messages + * Queue an action to be executed after the current stream ends */ - private async continueFlow(): Promise { - if (this.isLoading) return - await this.streamResponse() + private queuePostStreamAction(action: () => Promise): void { + this.postStreamActions.push(action) + } + + /** + * Drain and execute all queued post-stream actions + */ + private async drainPostStreamActions(): Promise { + while (this.postStreamActions.length > 0) { + const action = this.postStreamActions.shift()! + await action() + } + } + + /** + * Check if we should continue the flow and do so if needed + */ + private async checkForContinuation(): Promise { + if (this.shouldAutoSend()) { + await this.streamResponse() + } } /** diff --git a/packages/typescript/ai-devtools/package.json b/packages/typescript/ai-devtools/package.json index 94734697..c27e8353 100644 --- a/packages/typescript/ai-devtools/package.json +++ b/packages/typescript/ai-devtools/package.json @@ -55,7 +55,7 @@ "devDependencies": { "@vitest/coverage-v8": "4.0.14", "jsdom": "^27.2.0", - "vite": "^7.2.4", + "vite": "^7.2.7", "vite-plugin-solid": "^2.11.10" } } diff --git a/packages/typescript/ai-devtools/src/store/ai-context.tsx b/packages/typescript/ai-devtools/src/store/ai-context.tsx index 42af6917..a5e683fb 100644 --- a/packages/typescript/ai-devtools/src/store/ai-context.tsx +++ b/packages/typescript/ai-devtools/src/store/ai-context.tsx @@ -1309,7 +1309,7 @@ export const AIProvider: ParentComponent = (props) => { // ============= Chat Events (for usage tracking) ============= cleanupFns.push( - aiEventClient.on('chat:started', (e) => { + aiEventClient.on('text:started', (e) => { const streamId = e.payload.streamId const model = e.payload.model const provider = e.payload.provider @@ -1350,7 +1350,7 @@ export const AIProvider: ParentComponent = (props) => { ) cleanupFns.push( - aiEventClient.on('chat:completed', (e) => { + aiEventClient.on('text:completed', (e) => { const { requestId, usage } = e.payload const conversationId = requestToConversation.get(requestId) @@ -1371,7 +1371,7 @@ export const AIProvider: ParentComponent = (props) => { ) cleanupFns.push( - aiEventClient.on('chat:iteration', (e) => { + aiEventClient.on('text:iteration', (e) => { const { requestId, iterationNumber } = e.payload const conversationId = requestToConversation.get(requestId) diff --git a/packages/typescript/ai-devtools/vite.config.ts b/packages/typescript/ai-devtools/vite.config.ts index 6fd73cc1..25e36f4a 100644 --- a/packages/typescript/ai-devtools/vite.config.ts +++ b/packages/typescript/ai-devtools/vite.config.ts @@ -4,7 +4,7 @@ import solid from 'vite-plugin-solid' import packageJson from './package.json' const config = defineConfig({ - plugins: [solid()], + plugins: [solid() as any], test: { name: packageJson.name, dir: './tests', diff --git a/packages/typescript/ai-gemini/package.json b/packages/typescript/ai-gemini/package.json index 7dc0491b..f6e0fedb 100644 --- a/packages/typescript/ai-gemini/package.json +++ b/packages/typescript/ai-gemini/package.json @@ -45,9 +45,10 @@ }, "devDependencies": { "@vitest/coverage-v8": "4.0.14", - "vite": "^7.2.4" + "vite": "^7.2.7" }, "peerDependencies": { - "@tanstack/ai": "workspace:*" + "@tanstack/ai": "workspace:*", + "zod": "^4.0.0" } } diff --git a/packages/typescript/ai-gemini/src/adapters/embed.ts b/packages/typescript/ai-gemini/src/adapters/embed.ts new file mode 100644 index 00000000..f90d9b98 --- /dev/null +++ b/packages/typescript/ai-gemini/src/adapters/embed.ts @@ -0,0 +1,118 @@ +import { createGeminiClient, getGeminiApiKeyFromEnv } from '../utils' + +import type { GoogleGenAI } from '@google/genai' +import type { EmbeddingAdapter } from '@tanstack/ai/adapters' +import type { EmbeddingOptions, EmbeddingResult } from '@tanstack/ai' + +/** + * Available Gemini embedding models + */ +export const GeminiEmbeddingModels = [ + 'text-embedding-004', + 'embedding-001', +] as const + +export type GeminiEmbeddingModel = (typeof GeminiEmbeddingModels)[number] + +/** + * Provider-specific options for Gemini embeddings + */ +export interface GeminiEmbedProviderOptions { + taskType?: + | 'RETRIEVAL_QUERY' + | 'RETRIEVAL_DOCUMENT' + | 'SEMANTIC_SIMILARITY' + | 'CLASSIFICATION' + | 'CLUSTERING' + title?: string + outputDimensionality?: number +} + +export interface GeminiEmbedAdapterOptions { + model?: GeminiEmbeddingModel +} + +/** + * Gemini Embedding Adapter + * A tree-shakeable embedding adapter for Google Gemini + */ +export class GeminiEmbedAdapter implements EmbeddingAdapter< + typeof GeminiEmbeddingModels, + GeminiEmbedProviderOptions +> { + readonly kind = 'embedding' as const + readonly name = 'gemini' as const + readonly models = GeminiEmbeddingModels + + /** Type-only property for provider options inference */ + declare _providerOptions?: GeminiEmbedProviderOptions + + private client: GoogleGenAI + private defaultModel: GeminiEmbeddingModel + + constructor( + apiKeyOrClient: string | GoogleGenAI, + options: GeminiEmbedAdapterOptions = {}, + ) { + this.client = + typeof apiKeyOrClient === 'string' + ? createGeminiClient({ apiKey: apiKeyOrClient }) + : apiKeyOrClient + this.defaultModel = options.model ?? 'text-embedding-004' + } + + async createEmbeddings(options: EmbeddingOptions): Promise { + const model = options.model || this.defaultModel + + // Ensure input is an array + const inputs = Array.isArray(options.input) + ? options.input + : [options.input] + + const embeddings: Array> = [] + + for (const input of inputs) { + const response = await this.client.models.embedContent({ + model, + contents: [{ role: 'user', parts: [{ text: input }] }], + config: { + outputDimensionality: options.dimensions, + }, + }) + + if (response.embeddings?.[0]?.values) { + embeddings.push(response.embeddings[0].values) + } + } + + return { + id: `embed-${Date.now()}`, + model, + embeddings, + usage: { + promptTokens: 0, + totalTokens: 0, + }, + } + } +} + +/** + * Creates a Gemini embedding adapter with explicit API key + */ +export function createGeminiEmbed( + apiKey: string, + options?: GeminiEmbedAdapterOptions, +): GeminiEmbedAdapter { + return new GeminiEmbedAdapter(apiKey, options) +} + +/** + * Creates a Gemini embedding adapter with API key from environment + */ +export function geminiEmbed( + options?: GeminiEmbedAdapterOptions, +): GeminiEmbedAdapter { + const apiKey = getGeminiApiKeyFromEnv() + return new GeminiEmbedAdapter(apiKey, options) +} diff --git a/packages/typescript/ai-gemini/src/adapters/image.ts b/packages/typescript/ai-gemini/src/adapters/image.ts new file mode 100644 index 00000000..f9212370 --- /dev/null +++ b/packages/typescript/ai-gemini/src/adapters/image.ts @@ -0,0 +1,176 @@ +import { BaseImageAdapter } from '@tanstack/ai/adapters' +import { GEMINI_IMAGE_MODELS } from '../model-meta' +import { + createGeminiClient, + generateId, + getGeminiApiKeyFromEnv, +} from '../utils' +import { + sizeToAspectRatio, + validateImageSize, + validateNumberOfImages, + validatePrompt, +} from '../image/image-provider-options' +import type { + GeminiImageModelProviderOptionsByName, + GeminiImageModelSizeByName, + GeminiImageProviderOptions, +} from '../image/image-provider-options' +import type { + GeneratedImage, + ImageGenerationOptions, + ImageGenerationResult, +} from '@tanstack/ai' +import type { + GenerateImagesConfig, + GenerateImagesResponse, + GoogleGenAI, +} from '@google/genai' +import type { GeminiClientConfig } from '../utils' + +/** + * Configuration for Gemini image adapter + */ +export interface GeminiImageConfig extends GeminiClientConfig {} + +/** + * Gemini Image Generation Adapter + * + * Tree-shakeable adapter for Gemini Imagen image generation functionality. + * Supports Imagen 3 and Imagen 4 models. + * + * Features: + * - Aspect ratio-based image sizing + * - Person generation controls + * - Safety filtering + * - Watermark options + */ +export class GeminiImageAdapter extends BaseImageAdapter< + typeof GEMINI_IMAGE_MODELS, + GeminiImageProviderOptions, + GeminiImageModelProviderOptionsByName, + GeminiImageModelSizeByName +> { + readonly kind = 'image' as const + readonly name = 'gemini' as const + readonly models = GEMINI_IMAGE_MODELS + + declare _modelProviderOptionsByName: GeminiImageModelProviderOptionsByName + declare _modelSizeByName: GeminiImageModelSizeByName + + private client: GoogleGenAI + + constructor(config: GeminiImageConfig) { + super({}) + this.client = createGeminiClient(config) + } + + async generateImages( + options: ImageGenerationOptions, + ): Promise { + const { model, prompt, numberOfImages, size } = options + + // Validate inputs + validatePrompt({ prompt, model }) + validateImageSize(model, size) + validateNumberOfImages(model, numberOfImages) + + // Build request config + const config = this.buildConfig(options) + + const response = await this.client.models.generateImages({ + model, + prompt, + config, + }) + + return this.transformResponse(model, response) + } + + private buildConfig( + options: ImageGenerationOptions, + ): GenerateImagesConfig { + const { size, numberOfImages, providerOptions } = options + + return { + numberOfImages: numberOfImages ?? 1, + // Map size to aspect ratio if provided (providerOptions.aspectRatio will override) + aspectRatio: size ? sizeToAspectRatio(size) : undefined, + ...providerOptions, + } + } + + private transformResponse( + model: string, + response: GenerateImagesResponse, + ): ImageGenerationResult { + const images: Array = (response.generatedImages ?? []).map( + (item) => ({ + b64Json: item.image?.imageBytes, + revisedPrompt: item.enhancedPrompt, + }), + ) + + return { + id: generateId(this.name), + model, + images, + usage: undefined, + } + } +} + +/** + * Creates a Gemini image adapter with explicit API key + * + * @param apiKey - Your Google API key + * @param config - Optional additional configuration + * @returns Configured Gemini image adapter instance + * + * @example + * ```typescript + * const adapter = createGeminiImage("your-api-key"); + * + * const result = await ai({ + * adapter, + * model: 'imagen-3.0-generate-002', + * prompt: 'A cute baby sea otter' + * }); + * ``` + */ +export function createGeminiImage( + apiKey: string, + config?: Omit, +): GeminiImageAdapter { + return new GeminiImageAdapter({ apiKey, ...config }) +} + +/** + * Creates a Gemini image adapter with automatic API key detection from environment variables. + * + * Looks for `GOOGLE_API_KEY` or `GEMINI_API_KEY` in: + * - `process.env` (Node.js) + * - `window.env` (Browser with injected env) + * + * @param config - Optional configuration (excluding apiKey which is auto-detected) + * @returns Configured Gemini image adapter instance + * @throws Error if GOOGLE_API_KEY or GEMINI_API_KEY is not found in environment + * + * @example + * ```typescript + * // Automatically uses GOOGLE_API_KEY from environment + * const adapter = geminiImage(); + * + * const result = await ai({ + * adapter, + * model: 'imagen-4.0-generate-001', + * prompt: 'A beautiful sunset over mountains' + * }); + * ``` + */ +export function geminiImage( + config?: Omit, +): GeminiImageAdapter { + const apiKey = getGeminiApiKeyFromEnv() + return createGeminiImage(apiKey, config) +} diff --git a/packages/typescript/ai-gemini/src/adapters/summarize.ts b/packages/typescript/ai-gemini/src/adapters/summarize.ts new file mode 100644 index 00000000..0e4e2b21 --- /dev/null +++ b/packages/typescript/ai-gemini/src/adapters/summarize.ts @@ -0,0 +1,150 @@ +import { + createGeminiClient, + generateId, + getGeminiApiKeyFromEnv, +} from '../utils' + +import type { GoogleGenAI } from '@google/genai' +import type { SummarizeAdapter } from '@tanstack/ai/adapters' +import type { SummarizationOptions, SummarizationResult } from '@tanstack/ai' + +/** + * Available Gemini models for summarization + */ +export const GeminiSummarizeModels = [ + 'gemini-2.0-flash', + 'gemini-1.5-flash', + 'gemini-1.5-pro', + 'gemini-2.0-flash-lite', +] as const + +export type GeminiSummarizeModel = (typeof GeminiSummarizeModels)[number] + +/** + * Provider-specific options for Gemini summarization + */ +export interface GeminiSummarizeProviderOptions { + /** Generation configuration */ + generationConfig?: { + temperature?: number + topP?: number + topK?: number + maxOutputTokens?: number + stopSequences?: Array + } + /** Safety settings */ + safetySettings?: Array<{ + category: string + threshold: string + }> +} + +export interface GeminiSummarizeAdapterOptions { + model?: GeminiSummarizeModel +} + +/** + * Gemini Summarize Adapter + * A tree-shakeable summarization adapter for Google Gemini + */ +export class GeminiSummarizeAdapter implements SummarizeAdapter< + typeof GeminiSummarizeModels, + GeminiSummarizeProviderOptions +> { + readonly kind = 'summarize' as const + readonly name = 'gemini' as const + readonly models = GeminiSummarizeModels + + /** Type-only property for provider options inference */ + declare _providerOptions?: GeminiSummarizeProviderOptions + + private client: GoogleGenAI + private defaultModel: GeminiSummarizeModel + + constructor( + apiKeyOrClient: string | GoogleGenAI, + options: GeminiSummarizeAdapterOptions = {}, + ) { + this.client = + typeof apiKeyOrClient === 'string' + ? createGeminiClient({ apiKey: apiKeyOrClient }) + : apiKeyOrClient + this.defaultModel = options.model ?? 'gemini-2.0-flash' + } + + async summarize(options: SummarizationOptions): Promise { + const model = options.model || this.defaultModel + + // Build the system prompt based on format + const formatInstructions = this.getFormatInstructions(options.style) + const lengthInstructions = options.maxLength + ? ` Keep the summary under ${options.maxLength} words.` + : '' + + const systemPrompt = `You are a helpful assistant that summarizes text. ${formatInstructions}${lengthInstructions}` + + const response = await this.client.models.generateContent({ + model, + contents: [ + { + role: 'user', + parts: [ + { text: `Please summarize the following:\n\n${options.text}` }, + ], + }, + ], + config: { + systemInstruction: systemPrompt, + }, + }) + + const summary = response.text ?? '' + const inputTokens = response.usageMetadata?.promptTokenCount ?? 0 + const outputTokens = response.usageMetadata?.candidatesTokenCount ?? 0 + + return { + id: generateId('sum'), + model, + summary, + usage: { + promptTokens: inputTokens, + completionTokens: outputTokens, + totalTokens: inputTokens + outputTokens, + }, + } + } + + private getFormatInstructions( + style?: 'paragraph' | 'bullet-points' | 'concise', + ): string { + switch (style) { + case 'bullet-points': + return 'Provide the summary as bullet points.' + case 'concise': + return 'Provide a very brief one or two sentence summary.' + case 'paragraph': + default: + return 'Provide the summary in paragraph form.' + } + } +} + +/** + * Creates a Gemini summarize adapter with explicit API key + */ +export function createGeminiSummarize( + apiKey: string, + options?: GeminiSummarizeAdapterOptions, +): GeminiSummarizeAdapter { + return new GeminiSummarizeAdapter(apiKey, options) +} + +/** + * Creates a Gemini summarize adapter with API key from environment + */ +export function geminiSummarize( + options?: GeminiSummarizeAdapterOptions, +): GeminiSummarizeAdapter { + const apiKey = getGeminiApiKeyFromEnv() + return new GeminiSummarizeAdapter(apiKey, options) +} diff --git a/packages/typescript/ai-gemini/src/adapters/text.ts b/packages/typescript/ai-gemini/src/adapters/text.ts new file mode 100644 index 00000000..3e82a179 --- /dev/null +++ b/packages/typescript/ai-gemini/src/adapters/text.ts @@ -0,0 +1,480 @@ +import { FinishReason } from '@google/genai' +import { BaseTextAdapter } from '@tanstack/ai/adapters' +import { GEMINI_MODELS } from '../model-meta' +import { convertToolsToProviderFormat } from '../tools/tool-converter' +import { + convertZodToGeminiSchema, + createGeminiClient, + generateId, + getGeminiApiKeyFromEnv, +} from '../utils' +import type { + StructuredOutputOptions, + StructuredOutputResult, +} from '@tanstack/ai/adapters' +import type { + GenerateContentParameters, + GenerateContentResponse, + GoogleGenAI, + Part, +} from '@google/genai' +import type { + ContentPart, + ModelMessage, + StreamChunk, + TextOptions, +} from '@tanstack/ai' +import type { + GeminiChatModelProviderOptionsByName, + GeminiModelInputModalitiesByName, +} from '../model-meta' +import type { ExternalTextProviderOptions } from '../text/text-provider-options' +import type { + GeminiAudioMetadata, + GeminiDocumentMetadata, + GeminiImageMetadata, + GeminiMessageMetadataByModality, + GeminiVideoMetadata, +} from '../message-types' +import type { GeminiClientConfig } from '../utils' + +/** + * Configuration for Gemini text adapter + */ +export interface GeminiTextConfig extends GeminiClientConfig {} + +/** + * Gemini-specific provider options for text/chat + */ +export type GeminiTextProviderOptions = ExternalTextProviderOptions + +/** + * Gemini Text (Chat) Adapter + * + * Tree-shakeable adapter for Gemini chat/text completion functionality. + * Import only what you need for smaller bundle sizes. + */ +export class GeminiTextAdapter extends BaseTextAdapter< + typeof GEMINI_MODELS, + GeminiTextProviderOptions, + GeminiChatModelProviderOptionsByName, + GeminiModelInputModalitiesByName, + GeminiMessageMetadataByModality +> { + readonly kind = 'text' as const + readonly name = 'gemini' as const + readonly models = GEMINI_MODELS + + declare _modelProviderOptionsByName: GeminiChatModelProviderOptionsByName + declare _modelInputModalitiesByName: GeminiModelInputModalitiesByName + declare _messageMetadataByModality: GeminiMessageMetadataByModality + + private client: GoogleGenAI + + constructor(config: GeminiTextConfig) { + super({}) + this.client = createGeminiClient(config) + } + + async *chatStream( + options: TextOptions, + ): AsyncIterable { + const mappedOptions = this.mapCommonOptionsToGemini(options) + + try { + const result = + await this.client.models.generateContentStream(mappedOptions) + + yield* this.processStreamChunks(result, options.model) + } catch (error) { + const timestamp = Date.now() + yield { + type: 'error', + id: generateId(this.name), + model: options.model, + timestamp, + error: { + message: + error instanceof Error + ? error.message + : 'An unknown error occurred during the chat stream.', + }, + } + } + } + + /** + * Generate structured output using Gemini's native JSON response format. + * Uses responseMimeType: 'application/json' and responseSchema for structured output. + * Converts the Zod schema to JSON Schema format compatible with Gemini's API. + */ + async structuredOutput( + options: StructuredOutputOptions, + ): Promise> { + const { chatOptions, outputSchema } = options + + // Convert Zod schema to Gemini-compatible JSON Schema + const jsonSchema = convertZodToGeminiSchema(outputSchema) + + const mappedOptions = this.mapCommonOptionsToGemini(chatOptions) + + try { + // Add structured output configuration + const result = await this.client.models.generateContent({ + ...mappedOptions, + config: { + ...mappedOptions.config, + responseMimeType: 'application/json', + responseSchema: jsonSchema, + }, + }) + + // Extract text content from the response + const rawText = this.extractTextFromResponse(result) + + // Parse the JSON response + let parsed: unknown + try { + parsed = JSON.parse(rawText) + } catch { + throw new Error( + `Failed to parse structured output as JSON. Content: ${rawText.slice(0, 200)}${rawText.length > 200 ? '...' : ''}`, + ) + } + + return { + data: parsed, + rawText, + } + } catch (error) { + throw new Error( + error instanceof Error + ? error.message + : 'An unknown error occurred during structured output generation.', + ) + } + } + + /** + * Extract text content from a non-streaming response + */ + private extractTextFromResponse(response: GenerateContentResponse): string { + let textContent = '' + + if (response.candidates?.[0]?.content?.parts) { + for (const part of response.candidates[0].content.parts) { + if (part.text) { + textContent += part.text + } + } + } + + return textContent + } + + private async *processStreamChunks( + result: AsyncGenerator, + model: string, + ): AsyncIterable { + const timestamp = Date.now() + let accumulatedContent = '' + const toolCallMap = new Map< + string, + { name: string; args: string; index: number } + >() + let nextToolIndex = 0 + + for await (const chunk of result) { + if (chunk.candidates?.[0]?.content?.parts) { + const parts = chunk.candidates[0].content.parts + + for (const part of parts) { + if (part.text) { + accumulatedContent += part.text + yield { + type: 'content', + id: generateId(this.name), + model, + timestamp, + delta: part.text, + content: accumulatedContent, + role: 'assistant', + } + } + + const functionCall = part.functionCall + if (functionCall) { + const toolCallId = + functionCall.name || `call_${Date.now()}_${nextToolIndex}` + const functionArgs = functionCall.args || {} + + let toolCallData = toolCallMap.get(toolCallId) + if (!toolCallData) { + toolCallData = { + name: functionCall.name || '', + args: + typeof functionArgs === 'string' + ? functionArgs + : JSON.stringify(functionArgs), + index: nextToolIndex++, + } + toolCallMap.set(toolCallId, toolCallData) + } else { + try { + const existingArgs = JSON.parse(toolCallData.args) + const newArgs = + typeof functionArgs === 'string' + ? JSON.parse(functionArgs) + : functionArgs + const mergedArgs = { ...existingArgs, ...newArgs } + toolCallData.args = JSON.stringify(mergedArgs) + } catch { + toolCallData.args = + typeof functionArgs === 'string' + ? functionArgs + : JSON.stringify(functionArgs) + } + } + + yield { + type: 'tool_call', + id: generateId(this.name), + model, + timestamp, + toolCall: { + id: toolCallId, + type: 'function', + function: { + name: toolCallData.name, + arguments: toolCallData.args, + }, + }, + index: toolCallData.index, + } + } + } + } else if (chunk.data) { + accumulatedContent += chunk.data + yield { + type: 'content', + id: generateId(this.name), + model, + timestamp, + delta: chunk.data, + content: accumulatedContent, + role: 'assistant', + } + } + + if (chunk.candidates?.[0]?.finishReason) { + const finishReason = chunk.candidates[0].finishReason + + if (finishReason === FinishReason.UNEXPECTED_TOOL_CALL) { + if (chunk.candidates[0].content?.parts) { + for (const part of chunk.candidates[0].content.parts) { + const functionCall = part.functionCall + if (functionCall) { + const toolCallId = + functionCall.name || `call_${Date.now()}_${nextToolIndex}` + const functionArgs = functionCall.args || {} + + toolCallMap.set(toolCallId, { + name: functionCall.name || '', + args: + typeof functionArgs === 'string' + ? functionArgs + : JSON.stringify(functionArgs), + index: nextToolIndex++, + }) + + yield { + type: 'tool_call', + id: generateId(this.name), + model, + timestamp, + toolCall: { + id: toolCallId, + type: 'function', + function: { + name: functionCall.name || '', + arguments: + typeof functionArgs === 'string' + ? functionArgs + : JSON.stringify(functionArgs), + }, + }, + index: nextToolIndex - 1, + } + } + } + } + } + if (finishReason === FinishReason.MAX_TOKENS) { + yield { + type: 'error', + id: generateId(this.name), + model, + timestamp, + error: { + message: + 'The response was cut off because the maximum token limit was reached.', + }, + } + } + + yield { + type: 'done', + id: generateId(this.name), + model, + timestamp, + finishReason: toolCallMap.size > 0 ? 'tool_calls' : 'stop', + usage: chunk.usageMetadata + ? { + promptTokens: chunk.usageMetadata.promptTokenCount ?? 0, + completionTokens: chunk.usageMetadata.thoughtsTokenCount ?? 0, + totalTokens: chunk.usageMetadata.totalTokenCount ?? 0, + } + : undefined, + } + } + } + } + + private convertContentPartToGemini(part: ContentPart): Part { + switch (part.type) { + case 'text': + return { text: part.content } + case 'image': + case 'audio': + case 'video': + case 'document': { + const metadata = part.metadata as + | GeminiDocumentMetadata + | GeminiImageMetadata + | GeminiVideoMetadata + | GeminiAudioMetadata + | undefined + if (part.source.type === 'data') { + return { + inlineData: { + data: part.source.value, + mimeType: metadata?.mimeType ?? 'image/jpeg', + }, + } + } else { + return { + fileData: { + fileUri: part.source.value, + mimeType: metadata?.mimeType ?? 'image/jpeg', + }, + } + } + } + default: { + const _exhaustiveCheck: never = part + throw new Error( + `Unsupported content part type: ${(_exhaustiveCheck as ContentPart).type}`, + ) + } + } + } + + private formatMessages( + messages: Array, + ): GenerateContentParameters['contents'] { + return messages.map((msg) => { + const role: 'user' | 'model' = msg.role === 'assistant' ? 'model' : 'user' + const parts: Array = [] + + if (Array.isArray(msg.content)) { + for (const contentPart of msg.content) { + parts.push(this.convertContentPartToGemini(contentPart)) + } + } else if (msg.content) { + parts.push({ text: msg.content }) + } + + if (msg.role === 'assistant' && msg.toolCalls?.length) { + for (const toolCall of msg.toolCalls) { + let parsedArgs: Record = {} + try { + parsedArgs = toolCall.function.arguments + ? (JSON.parse(toolCall.function.arguments) as Record< + string, + unknown + >) + : {} + } catch { + parsedArgs = toolCall.function.arguments as unknown as Record< + string, + unknown + > + } + + parts.push({ + functionCall: { + name: toolCall.function.name, + args: parsedArgs, + }, + }) + } + } + + if (msg.role === 'tool' && msg.toolCallId) { + parts.push({ + functionResponse: { + name: msg.toolCallId, + response: { + content: msg.content || '', + }, + }, + }) + } + + return { + role, + parts: parts.length > 0 ? parts : [{ text: '' }], + } + }) + } + + private mapCommonOptionsToGemini(options: TextOptions) { + const providerOpts = options.providerOptions + const requestOptions: GenerateContentParameters = { + model: options.model, + contents: this.formatMessages(options.messages), + config: { + ...providerOpts, + temperature: options.options?.temperature, + topP: options.options?.topP, + maxOutputTokens: options.options?.maxTokens, + systemInstruction: options.systemPrompts?.join('\n'), + ...((providerOpts as Record | undefined) + ?.generationConfig as Record | undefined), + tools: convertToolsToProviderFormat(options.tools), + }, + } + + return requestOptions + } +} + +/** + * Creates a Gemini text adapter with explicit API key + */ +export function createGeminiText( + apiKey: string, + config?: Omit, +): GeminiTextAdapter { + return new GeminiTextAdapter({ apiKey, ...config }) +} + +/** + * Creates a Gemini text adapter with automatic API key detection + */ +export function geminiText( + config?: Omit, +): GeminiTextAdapter { + const apiKey = getGeminiApiKeyFromEnv() + return createGeminiText(apiKey, config) +} diff --git a/packages/typescript/ai-gemini/src/adapters/tts.ts b/packages/typescript/ai-gemini/src/adapters/tts.ts new file mode 100644 index 00000000..1d72f8a9 --- /dev/null +++ b/packages/typescript/ai-gemini/src/adapters/tts.ts @@ -0,0 +1,192 @@ +import { BaseTTSAdapter } from '@tanstack/ai/adapters' +import { GEMINI_TTS_MODELS } from '../model-meta' +import { + createGeminiClient, + generateId, + getGeminiApiKeyFromEnv, +} from '../utils' +import type { TTSOptions, TTSResult } from '@tanstack/ai' +import type { GoogleGenAI } from '@google/genai' +import type { GeminiClientConfig } from '../utils' + +/** + * Provider-specific options for Gemini TTS + * + * @experimental Gemini TTS is an experimental feature and uses the Live API. + */ +export interface GeminiTTSProviderOptions { + /** + * Voice configuration for TTS. + * Note: Gemini TTS uses the Live API which has limited configuration options. + */ + voiceConfig?: { + prebuiltVoiceConfig?: { + voiceName?: string + } + } +} + +/** + * Configuration for Gemini TTS adapter + * + * @experimental Gemini TTS is an experimental feature. + */ +export interface GeminiTTSConfig extends GeminiClientConfig {} + +/** + * Gemini Text-to-Speech Adapter + * + * Tree-shakeable adapter for Gemini TTS functionality. + * + * **IMPORTANT**: Gemini TTS uses the Live API (WebSocket-based) which requires + * different handling than traditional REST APIs. This adapter provides a + * simplified interface but may have limitations. + * + * @experimental Gemini TTS is an experimental feature and may change. + * + * Models: + * - gemini-2.5-flash-preview-tts + */ +export class GeminiTTSAdapter extends BaseTTSAdapter< + typeof GEMINI_TTS_MODELS, + GeminiTTSProviderOptions +> { + readonly name = 'gemini' as const + readonly models = GEMINI_TTS_MODELS + + private client: GoogleGenAI + + constructor(config: GeminiTTSConfig) { + super(config) + this.client = createGeminiClient(config) + } + + /** + * Generate speech from text using Gemini's TTS model. + * + * Note: Gemini's TTS functionality uses the Live API, which is WebSocket-based. + * This implementation uses the multimodal generation endpoint with audio output + * configuration, which may have different capabilities than the full Live API. + * + * @experimental This implementation is experimental and may change. + */ + async generateSpeech( + options: TTSOptions, + ): Promise { + const { model, text, providerOptions } = options + + // Use Gemini's multimodal content generation with audio output + // Note: This requires the model to support audio output + const voiceConfig = providerOptions?.voiceConfig || { + prebuiltVoiceConfig: { + voiceName: 'Kore', // Default Gemini voice + }, + } + + const response = await this.client.models.generateContent({ + model, + contents: [ + { + role: 'user', + parts: [{ text: `Please speak the following text: ${text}` }], + }, + ], + config: { + // Configure for audio output + responseModalities: ['AUDIO'], + speechConfig: { + voiceConfig, + }, + }, + }) + + // Extract audio data from response + const candidate = response.candidates?.[0] + const parts = candidate?.content?.parts + + if (!parts || parts.length === 0) { + throw new Error('No audio output received from Gemini TTS') + } + + // Look for inline data (audio) + const audioPart = parts.find((part: any) => + part.inlineData?.mimeType?.startsWith('audio/'), + ) + + if (!audioPart || !('inlineData' in audioPart)) { + throw new Error('No audio data in Gemini TTS response') + } + + const inlineData = (audioPart as any).inlineData + const audioBase64 = inlineData.data + const mimeType = inlineData.mimeType || 'audio/wav' + const format = mimeType.split('/')[1] || 'wav' + + return { + id: generateId(this.name), + model, + audio: audioBase64, + format, + contentType: mimeType, + } + } +} + +/** + * Creates a Gemini TTS adapter with explicit API key + * + * @experimental Gemini TTS is an experimental feature and may change. + * + * @param apiKey - Your Google API key + * @param config - Optional additional configuration + * @returns Configured Gemini TTS adapter instance + * + * @example + * ```typescript + * const adapter = createGeminiTTS("your-api-key"); + * + * const result = await ai({ + * adapter, + * model: 'gemini-2.5-flash-preview-tts', + * text: 'Hello, world!' + * }); + * ``` + */ +export function createGeminiTTS( + apiKey: string, + config?: Omit, +): GeminiTTSAdapter { + return new GeminiTTSAdapter({ apiKey, ...config }) +} + +/** + * Creates a Gemini TTS adapter with automatic API key detection from environment variables. + * + * @experimental Gemini TTS is an experimental feature and may change. + * + * Looks for `GOOGLE_API_KEY` or `GEMINI_API_KEY` in: + * - `process.env` (Node.js) + * - `window.env` (Browser with injected env) + * + * @param config - Optional configuration (excluding apiKey which is auto-detected) + * @returns Configured Gemini TTS adapter instance + * @throws Error if GOOGLE_API_KEY or GEMINI_API_KEY is not found in environment + * + * @example + * ```typescript + * // Automatically uses GOOGLE_API_KEY from environment + * const adapter = geminiTTS(); + * + * const result = await ai({ + * adapter, + * model: 'gemini-2.5-flash-preview-tts', + * text: 'Welcome to TanStack AI!' + * }); + * ``` + */ +export function geminiTTS( + config?: Omit, +): GeminiTTSAdapter { + const apiKey = getGeminiApiKeyFromEnv() + return createGeminiTTS(apiKey, config) +} diff --git a/packages/typescript/ai-gemini/src/gemini-adapter.ts b/packages/typescript/ai-gemini/src/gemini-adapter.ts index 8a711b10..1eaf3a33 100644 --- a/packages/typescript/ai-gemini/src/gemini-adapter.ts +++ b/packages/typescript/ai-gemini/src/gemini-adapter.ts @@ -4,7 +4,6 @@ import { GEMINI_EMBEDDING_MODELS, GEMINI_MODELS } from './model-meta' import { convertToolsToProviderFormat } from './tools/tool-converter' import type { AIAdapterConfig, - ChatOptions, ContentPart, EmbeddingOptions, EmbeddingResult, @@ -12,6 +11,7 @@ import type { StreamChunk, SummarizationOptions, SummarizationResult, + TextOptions, } from '@tanstack/ai' import type { GeminiChatModelProviderOptionsByName, @@ -67,7 +67,7 @@ export class GeminiAdapter extends BaseAdapter< } async *chatStream( - options: ChatOptions, + options: TextOptions, ): AsyncIterable { // Map common options to Gemini format const mappedOptions = this.mapCommonOptionsToGemini(options) @@ -496,7 +496,7 @@ export class GeminiAdapter extends BaseAdapter< * Maps common options to Gemini-specific format * Handles translation of normalized options to Gemini's API format */ - private mapCommonOptionsToGemini(options: ChatOptions) { + private mapCommonOptionsToGemini(options: TextOptions) { const providerOpts = options.providerOptions const requestOptions: GenerateContentParameters = { model: options.model, diff --git a/packages/typescript/ai-gemini/src/image/image-provider-options.ts b/packages/typescript/ai-gemini/src/image/image-provider-options.ts new file mode 100644 index 00000000..2db3f4d2 --- /dev/null +++ b/packages/typescript/ai-gemini/src/image/image-provider-options.ts @@ -0,0 +1,239 @@ +import type { GeminiImageModels } from '../model-meta' +import type { + ImagePromptLanguage, + PersonGeneration, + SafetyFilterLevel, +} from '@google/genai' + +// Re-export SDK types so users can use them directly +export type { ImagePromptLanguage, PersonGeneration, SafetyFilterLevel } + +/** + * Gemini Imagen aspect ratio options + * Controls the aspect ratio of generated images + */ +export type GeminiAspectRatio = + | '1:1' + | '3:4' + | '4:3' + | '9:16' + | '16:9' + | '9:21' + | '21:9' + +/** + * Provider options for Gemini image generation + * These options match the @google/genai GenerateImagesConfig interface + * and can be spread directly into the API request. + */ +export interface GeminiImageProviderOptions { + /** + * The aspect ratio of generated images + * @default '1:1' + */ + aspectRatio?: GeminiAspectRatio + + /** + * Controls whether people can appear in generated images + * Use PersonGeneration enum values: DONT_ALLOW, ALLOW_ADULT, ALLOW_ALL + * @default 'ALLOW_ADULT' + */ + personGeneration?: PersonGeneration + + /** + * Safety filter level for content filtering + * Use SafetyFilterLevel enum values + */ + safetyFilterLevel?: SafetyFilterLevel + + /** + * Optional seed for reproducible image generation + * When the same seed is used with the same prompt and settings, + * you should get similar (though not identical) results + */ + seed?: number + + /** + * Whether to add a SynthID watermark to generated images + * SynthID helps identify AI-generated content + * @default true + */ + addWatermark?: boolean + + /** + * Language of the prompt + * Use ImagePromptLanguage enum values + */ + language?: ImagePromptLanguage + + /** + * Negative prompt - what to avoid in the generated image + * Not all models support negative prompts + */ + negativePrompt?: string + + /** + * Output MIME type for the generated image + * @default 'image/png' + */ + outputMimeType?: 'image/png' | 'image/jpeg' | 'image/webp' + + /** + * Compression quality for JPEG outputs (0-100) + * Higher values mean better quality but larger file sizes + * @default 75 + */ + outputCompressionQuality?: number + + /** + * Controls how much the model adheres to the text prompt + * Large values increase output and prompt alignment, + * but may compromise image quality + */ + guidanceScale?: number + + /** + * Whether to use the prompt rewriting logic + */ + enhancePrompt?: boolean + + /** + * Whether to report the safety scores of each generated image + * and the positive prompt in the response + */ + includeSafetyAttributes?: boolean + + /** + * Whether to include the Responsible AI filter reason + * if the image is filtered out of the response + */ + includeRaiReason?: boolean + + /** + * Cloud Storage URI used to store the generated images + */ + outputGcsUri?: string + + /** + * User specified labels to track billing usage + */ + labels?: Record +} + +/** + * Model-specific provider options mapping + * Currently all Imagen models use the same options structure + */ +export type GeminiImageModelProviderOptionsByName = { + [K in GeminiImageModels]: GeminiImageProviderOptions +} + +/** + * Supported size strings for Gemini Imagen models + * These map to aspect ratios internally + */ +export type GeminiImageSize = + | '1024x1024' + | '512x512' + | '1024x768' + | '1536x1024' + | '1792x1024' + | '1920x1080' + | '768x1024' + | '1024x1536' + | '1024x1792' + | '1080x1920' + +/** + * Model-specific size options mapping + * All Imagen models use the same size options + */ +export type GeminiImageModelSizeByName = { + [K in GeminiImageModels]: GeminiImageSize +} + +/** + * Valid sizes for Gemini Imagen models + * Gemini uses aspect ratios, but we map common WIDTHxHEIGHT formats to aspect ratios + * These are approximate mappings based on common image dimensions + */ +export const GEMINI_SIZE_TO_ASPECT_RATIO: Record = { + // Square + '1024x1024': '1:1', + '512x512': '1:1', + // Landscape + '1024x768': '4:3', + '1536x1024': '3:4', // Actually this is portrait, but matching common dimensions + '1792x1024': '16:9', + '1920x1080': '16:9', + // Portrait + '768x1024': '3:4', + '1024x1536': '4:3', // Inverted + '1024x1792': '9:16', + '1080x1920': '9:16', +} + +/** + * Maps a WIDTHxHEIGHT size string to a Gemini aspect ratio + * Returns undefined if the size cannot be mapped + */ +export function sizeToAspectRatio( + size: string | undefined, +): GeminiAspectRatio | undefined { + if (!size) return undefined + return GEMINI_SIZE_TO_ASPECT_RATIO[size] +} + +/** + * Validates that the provided size can be mapped to an aspect ratio + * Throws an error if the size is invalid + */ +export function validateImageSize( + model: string, + size: string | undefined, +): void { + if (!size) return + + const aspectRatio = sizeToAspectRatio(size) + if (!aspectRatio) { + const validSizes = Object.keys(GEMINI_SIZE_TO_ASPECT_RATIO) + throw new Error( + `Invalid size "${size}" for model "${model}". ` + + `Gemini Imagen uses aspect ratios. Valid sizes that map to aspect ratios: ${validSizes.join(', ')}. ` + + `Alternatively, use providerOptions.aspectRatio directly with values: 1:1, 3:4, 4:3, 9:16, 16:9, 9:21, 21:9`, + ) + } +} + +/** + * Validates the number of images requested + * Imagen models support 1-8 images per request (varies by model) + */ +export function validateNumberOfImages( + model: string, + numberOfImages: number | undefined, +): void { + if (numberOfImages === undefined) return + + // Most Imagen models support 1-4 images, some support up to 8 + const maxImages = 4 + if (numberOfImages < 1 || numberOfImages > maxImages) { + throw new Error( + `Invalid numberOfImages "${numberOfImages}" for model "${model}". ` + + `Must be between 1 and ${maxImages}.`, + ) + } +} + +/** + * Validates the prompt is not empty + */ +export function validatePrompt(options: { + prompt: string + model: string +}): void { + const { prompt, model } = options + if (!prompt || prompt.trim().length === 0) { + throw new Error(`Prompt cannot be empty for model "${model}".`) + } +} diff --git a/packages/typescript/ai-gemini/src/index.ts b/packages/typescript/ai-gemini/src/index.ts index 2cc667e6..3330af93 100644 --- a/packages/typescript/ai-gemini/src/index.ts +++ b/packages/typescript/ai-gemini/src/index.ts @@ -1,3 +1,84 @@ +// =========================== +// New tree-shakeable adapters +// =========================== + +// Text/Chat adapter +export { + GeminiTextAdapter, + createGeminiText, + geminiText, + type GeminiTextConfig, + type GeminiTextProviderOptions, +} from './adapters/text' + +// Embedding adapter +export { + GeminiEmbedAdapter, + GeminiEmbeddingModels, + createGeminiEmbed, + geminiEmbed, + type GeminiEmbedAdapterOptions, + type GeminiEmbeddingModel, + type GeminiEmbedProviderOptions, +} from './adapters/embed' + +// Summarize adapter +export { + GeminiSummarizeAdapter, + GeminiSummarizeModels, + createGeminiSummarize, + geminiSummarize, + type GeminiSummarizeAdapterOptions, + type GeminiSummarizeModel, + type GeminiSummarizeProviderOptions, +} from './adapters/summarize' + +// Image adapter +export { + GeminiImageAdapter, + createGeminiImage, + geminiImage, + type GeminiImageConfig, +} from './adapters/image' +export type { + GeminiImageProviderOptions, + GeminiImageModelProviderOptionsByName, + GeminiAspectRatio, + // Re-export SDK types for convenience + PersonGeneration, + SafetyFilterLevel, + ImagePromptLanguage, +} from './image/image-provider-options' + +// TTS adapter (experimental) +/** + * @experimental Gemini TTS is an experimental feature and may change. + */ +export { + GeminiTTSAdapter, + createGeminiTTS, + geminiTTS, + type GeminiTTSConfig, + type GeminiTTSProviderOptions, +} from './adapters/tts' + +// Re-export models from model-meta for convenience +export { GEMINI_MODELS as GeminiTextModels } from './model-meta' +export { GEMINI_IMAGE_MODELS as GeminiImageModels } from './model-meta' +export { GEMINI_TTS_MODELS as GeminiTTSModels } from './model-meta' +export type { GeminiModels as GeminiTextModel } from './model-meta' +export type { GeminiImageModels as GeminiImageModel } from './model-meta' + +// =========================== +// Legacy monolithic adapter (deprecated) +// =========================== + +/** + * @deprecated Use the new tree-shakeable adapters instead: + * - `geminiText()` / `createGeminiText()` for chat/text generation + * - `geminiEmbed()` / `createGeminiEmbed()` for embeddings + * - `geminiSummarize()` / `createGeminiSummarize()` for summarization + */ export { GeminiAdapter, createGemini, gemini } from './gemini-adapter' export type { GeminiAdapterConfig } from './gemini-adapter' export type { diff --git a/packages/typescript/ai-gemini/src/model-meta.ts b/packages/typescript/ai-gemini/src/model-meta.ts index 5048f96f..bae5cb5f 100644 --- a/packages/typescript/ai-gemini/src/model-meta.ts +++ b/packages/typescript/ai-gemini/src/model-meta.ts @@ -220,7 +220,7 @@ const GEMINI_2_5_FLASH_PREVIEW = { GeminiStructuredOutputOptions & GeminiThinkingOptions > -/* + const GEMINI_2_5_FLASH_IMAGE = { name: 'gemini-2.5-flash-image', max_input_tokens: 1_048_576, @@ -247,11 +247,11 @@ const GEMINI_2_5_FLASH_IMAGE = { }, } as const satisfies ModelMeta< GeminiToolConfigOptions & - GeminiSafetyOptions & - GeminiGenerationConfigOptions & - GeminiCachedContentOptions + GeminiSafetyOptions & + GeminiGenerationConfigOptions & + GeminiCachedContentOptions > - +/** const GEMINI_2_5_FLASH_LIVE = { name: 'gemini-2.5-flash-native-audio-preview-09-2025', max_input_tokens: 141_072, @@ -418,7 +418,7 @@ const GEMINI_2_FLASH = { GeminiCachedContentOptions & GeminiStructuredOutputOptions > -/* + const GEMINI_2_FLASH_IMAGE = { name: 'gemini-2.0-flash-preview-image-generation', max_input_tokens: 32_768, @@ -444,10 +444,10 @@ const GEMINI_2_FLASH_IMAGE = { }, } as const satisfies ModelMeta< GeminiToolConfigOptions & - GeminiSafetyOptions & - GeminiGenerationConfigOptions & - GeminiCachedContentOptions -> */ + GeminiSafetyOptions & + GeminiGenerationConfigOptions & + GeminiCachedContentOptions +> /* const GEMINI_2_FLASH_LIVE = { name: 'gemini-2.0-flash-live-001', @@ -514,7 +514,7 @@ const GEMINI_2_FLASH_LITE = { GeminiStructuredOutputOptions > -/* const IMAGEN_4_GENERATE = { +const IMAGEN_4_GENERATE = { name: 'imagen-4.0-generate-001', max_input_tokens: 480, max_output_tokens: 4, @@ -532,9 +532,9 @@ const GEMINI_2_FLASH_LITE = { }, } as const satisfies ModelMeta< GeminiToolConfigOptions & - GeminiSafetyOptions & - GeminiGenerationConfigOptions & - GeminiCachedContentOptions + GeminiSafetyOptions & + GeminiGenerationConfigOptions & + GeminiCachedContentOptions > const IMAGEN_4_GENERATE_ULTRA = { @@ -555,9 +555,9 @@ const IMAGEN_4_GENERATE_ULTRA = { }, } as const satisfies ModelMeta< GeminiToolConfigOptions & - GeminiSafetyOptions & - GeminiGenerationConfigOptions & - GeminiCachedContentOptions + GeminiSafetyOptions & + GeminiGenerationConfigOptions & + GeminiCachedContentOptions > const IMAGEN_4_GENERATE_FAST = { @@ -578,9 +578,9 @@ const IMAGEN_4_GENERATE_FAST = { }, } as const satisfies ModelMeta< GeminiToolConfigOptions & - GeminiSafetyOptions & - GeminiGenerationConfigOptions & - GeminiCachedContentOptions + GeminiSafetyOptions & + GeminiGenerationConfigOptions & + GeminiCachedContentOptions > const IMAGEN_3 = { @@ -600,11 +600,11 @@ const IMAGEN_3 = { }, } as const satisfies ModelMeta< GeminiToolConfigOptions & - GeminiSafetyOptions & - GeminiGenerationConfigOptions & - GeminiCachedContentOptions + GeminiSafetyOptions & + GeminiGenerationConfigOptions & + GeminiCachedContentOptions > - +/** const VEO_3_1_PREVIEW = { name: 'veo-3.1-generate-preview', max_input_tokens: 1024, @@ -779,17 +779,27 @@ export const GEMINI_MODELS = [ GEMINI_2_FLASH_LITE.name, ] as const -/* const GEMINI_IMAGE_MODELS = [ +export type GeminiModels = (typeof GEMINI_MODELS)[number] + +export type GeminiImageModels = (typeof GEMINI_IMAGE_MODELS)[number] + +export const GEMINI_IMAGE_MODELS = [ GEMINI_2_5_FLASH_IMAGE.name, GEMINI_2_FLASH_IMAGE.name, IMAGEN_3.name, IMAGEN_4_GENERATE.name, IMAGEN_4_GENERATE_FAST.name, IMAGEN_4_GENERATE_ULTRA.name, -] as const */ +] as const export const GEMINI_EMBEDDING_MODELS = [GEMINI_EMBEDDING.name] as const +/** + * Text-to-speech models + * @experimental Gemini TTS is an experimental feature and may change. + */ +export const GEMINI_TTS_MODELS = ['gemini-2.5-flash-preview-tts'] as const + /* const GEMINI_AUDIO_MODELS = [ GEMINI_2_5_PRO_TTS.name, GEMINI_2_5_FLASH_TTS.name, diff --git a/packages/typescript/ai-gemini/src/tools/tool-converter.ts b/packages/typescript/ai-gemini/src/tools/tool-converter.ts index f0a239ca..ccdd5edd 100644 --- a/packages/typescript/ai-gemini/src/tools/tool-converter.ts +++ b/packages/typescript/ai-gemini/src/tools/tool-converter.ts @@ -1,4 +1,4 @@ -import { convertZodToJsonSchema } from '@tanstack/ai' +import { convertZodToGeminiSchema } from '../utils/schema-converter' import { convertCodeExecutionToolToAdapterFormat } from './code-execution-tool' import { convertComputerUseToolToAdapterFormat } from './computer-use-tool' import { convertFileSearchToolToAdapterFormat } from './file-search-tool' @@ -76,8 +76,10 @@ export function convertToolsToProviderFormat( ) } - // Convert Zod schema to JSON Schema - const jsonSchema = convertZodToJsonSchema(tool.inputSchema) + // Convert Zod schema to Gemini-compatible JSON Schema + const jsonSchema = tool.inputSchema + ? convertZodToGeminiSchema(tool.inputSchema) + : { type: 'object', properties: {}, required: [] } functionDeclarations.push({ name: tool.name, diff --git a/packages/typescript/ai-gemini/src/utils/client.ts b/packages/typescript/ai-gemini/src/utils/client.ts new file mode 100644 index 00000000..21e0f2cd --- /dev/null +++ b/packages/typescript/ai-gemini/src/utils/client.ts @@ -0,0 +1,43 @@ +import { GoogleGenAI } from '@google/genai' + +export interface GeminiClientConfig { + apiKey: string +} + +/** + * Creates a Google Generative AI client instance + */ +export function createGeminiClient(config: GeminiClientConfig): GoogleGenAI { + return new GoogleGenAI({ + apiKey: config.apiKey, + }) +} + +/** + * Gets Google API key from environment variables + * @throws Error if GOOGLE_API_KEY or GEMINI_API_KEY is not found + */ +export function getGeminiApiKeyFromEnv(): string { + const env = + typeof globalThis !== 'undefined' && (globalThis as any).window?.env + ? (globalThis as any).window.env + : typeof process !== 'undefined' + ? process.env + : undefined + const key = env?.GOOGLE_API_KEY || env?.GEMINI_API_KEY + + if (!key) { + throw new Error( + 'GOOGLE_API_KEY or GEMINI_API_KEY is required. Please set it in your environment variables or use the factory function with an explicit API key.', + ) + } + + return key +} + +/** + * Generates a unique ID with a prefix + */ +export function generateId(prefix: string): string { + return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(7)}` +} diff --git a/packages/typescript/ai-gemini/src/utils/index.ts b/packages/typescript/ai-gemini/src/utils/index.ts new file mode 100644 index 00000000..a27fe1ef --- /dev/null +++ b/packages/typescript/ai-gemini/src/utils/index.ts @@ -0,0 +1,7 @@ +export { + createGeminiClient, + generateId, + getGeminiApiKeyFromEnv, + type GeminiClientConfig, +} from './client' +export { convertZodToGeminiSchema } from './schema-converter' diff --git a/packages/typescript/ai-gemini/src/utils/schema-converter.ts b/packages/typescript/ai-gemini/src/utils/schema-converter.ts new file mode 100644 index 00000000..a234b0b7 --- /dev/null +++ b/packages/typescript/ai-gemini/src/utils/schema-converter.ts @@ -0,0 +1,87 @@ +import { toJSONSchema } from 'zod' +import type { z } from 'zod' + +/** + * Check if a value is a Zod schema by looking for Zod-specific internals. + * Zod schemas have a `_zod` property that contains metadata. + */ +function isZodSchema(schema: unknown): schema is z.ZodType { + return ( + typeof schema === 'object' && + schema !== null && + '_zod' in schema && + typeof (schema as any)._zod === 'object' + ) +} + +/** + * Converts a Zod schema to JSON Schema format compatible with Gemini's API. + * + * Gemini accepts standard JSON Schema without special transformations. + * + * @param schema - Zod schema to convert + * @returns JSON Schema object compatible with Gemini's structured output API + * + * @example + * ```typescript + * import { z } from 'zod'; + * + * const zodSchema = z.object({ + * location: z.string().describe('City name'), + * unit: z.enum(['celsius', 'fahrenheit']).optional() + * }); + * + * const jsonSchema = convertZodToGeminiSchema(zodSchema); + * // Returns standard JSON Schema + * ``` + */ +export function convertZodToGeminiSchema( + schema: z.ZodType, +): Record { + if (!isZodSchema(schema)) { + throw new Error('Expected a Zod schema') + } + + // Use Zod's built-in toJSONSchema + const jsonSchema = toJSONSchema(schema, { + target: 'openapi-3.0', + reused: 'ref', + }) + + // Remove $schema property as it's not needed for LLM providers + let result = jsonSchema + if (typeof result === 'object' && '$schema' in result) { + const { $schema, ...rest } = result + result = rest + } + + // Ensure object schemas always have type: "object" + if (typeof result === 'object') { + const isZodObject = + typeof schema === 'object' && + 'def' in schema && + schema.def.type === 'object' + + if (isZodObject && !result.type) { + result.type = 'object' + } + + if (Object.keys(result).length === 0) { + result.type = 'object' + } + + if ('properties' in result && !result.type) { + result.type = 'object' + } + + if (result.type === 'object' && !('properties' in result)) { + result.properties = {} + } + + if (result.type === 'object' && !('required' in result)) { + result.required = [] + } + } + + return result +} diff --git a/packages/typescript/ai-gemini/tests/gemini-adapter.test.ts b/packages/typescript/ai-gemini/tests/gemini-adapter.test.ts index 6c7a08aa..2304a4f9 100644 --- a/packages/typescript/ai-gemini/tests/gemini-adapter.test.ts +++ b/packages/typescript/ai-gemini/tests/gemini-adapter.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach, vi } from 'vitest' -import { chat, summarize, embedding } from '@tanstack/ai' +import { ai } from '@tanstack/ai' import type { Tool, StreamChunk } from '@tanstack/ai' import { Type, @@ -7,10 +7,10 @@ import { type HarmCategory, type SafetySetting, } from '@google/genai' -import { - GeminiAdapter, - type GeminiProviderOptions, -} from '../src/gemini-adapter' +import { GeminiTextAdapter } from '../src/adapters/text' +import { GeminiSummarizeAdapter } from '../src/adapters/summarize' +import { GeminiEmbedAdapter } from '../src/adapters/embed' +import type { GeminiProviderOptions } from '../src/gemini-adapter' import type { Schema } from '@google/genai' const mocks = vi.hoisted(() => { @@ -54,7 +54,9 @@ vi.mock('@google/genai', async () => { } }) -const createAdapter = () => new GeminiAdapter({ apiKey: 'test-key' }) +const createTextAdapter = () => new GeminiTextAdapter({ apiKey: 'test-key' }) +const createSummarizeAdapter = () => new GeminiSummarizeAdapter('test-key') +const createEmbedAdapter = () => new GeminiEmbedAdapter('test-key') const weatherTool: Tool = { name: 'lookup_weather', @@ -95,10 +97,10 @@ describe('GeminiAdapter through AI', () => { mocks.generateContentStreamSpy.mockResolvedValue(createStream(streamChunks)) - const adapter = createAdapter() + const adapter = createTextAdapter() // Consume the stream to trigger the API call - for await (const _ of chat({ + for await (const _ of ai({ adapter, model: 'gemini-2.5-pro', messages: [{ role: 'user', content: 'How is the weather in Madrid?' }], @@ -207,10 +209,10 @@ describe('GeminiAdapter through AI', () => { cachedContent: 'cachedContents/weather-context', } as const - const adapter = createAdapter() + const adapter = createTextAdapter() // Consume the stream to trigger the API call - for await (const _ of chat({ + for await (const _ of ai({ adapter, model: 'gemini-2.5-pro', messages: [{ role: 'user', content: 'Provide structured response' }], @@ -309,9 +311,9 @@ describe('GeminiAdapter through AI', () => { mocks.generateContentStreamSpy.mockResolvedValue(createStream(streamChunks)) - const adapter = createAdapter() + const adapter = createTextAdapter() const received: StreamChunk[] = [] - for await (const chunk of chat({ + for await (const chunk of ai({ adapter, model: 'gemini-2.5-pro', messages: [{ role: 'user', content: 'Tell me a joke' }], @@ -350,19 +352,17 @@ describe('GeminiAdapter through AI', () => { it('uses summarize function with models API', async () => { const summaryText = 'Short and sweet.' mocks.generateContentSpy.mockResolvedValueOnce({ - candidates: [ - { - content: { - parts: [{ text: summaryText }], - }, - }, - ], + text: summaryText, + usageMetadata: { + promptTokenCount: 10, + candidatesTokenCount: 5, + }, }) - const adapter = createAdapter() - const result = await summarize({ + const adapter = createSummarizeAdapter() + const result = await ai({ adapter, - model: 'gemini-2.5-flash', + model: 'gemini-2.0-flash', text: 'A very long passage that needs to be shortened', maxLength: 123, style: 'paragraph', @@ -370,30 +370,30 @@ describe('GeminiAdapter through AI', () => { expect(mocks.generateContentSpy).toHaveBeenCalledTimes(1) const [payload] = mocks.generateContentSpy.mock.calls[0] - expect(payload.model).toBe('gemini-2.5-flash') - expect(payload.config).toMatchObject({ - temperature: 0.3, - maxOutputTokens: 123, - }) + expect(payload.model).toBe('gemini-2.0-flash') + expect(payload.config.systemInstruction).toContain('summarizes text') + expect(payload.config.systemInstruction).toContain('123 words') expect(result.summary).toBe(summaryText) }) it('creates embeddings via embedding function', async () => { - mocks.embedContentSpy.mockResolvedValueOnce({ - embeddings: [{ values: [0.1, 0.2] }, { values: [0.3, 0.4] }], - }) - - const adapter = createAdapter() - const result = await embedding({ + // The embed adapter calls embedContent once per input + mocks.embedContentSpy + .mockResolvedValueOnce({ + embeddings: [{ values: [0.1, 0.2] }], + }) + .mockResolvedValueOnce({ + embeddings: [{ values: [0.3, 0.4] }], + }) + + const adapter = createEmbedAdapter() + const result = await ai({ adapter, - model: 'gemini-embedding-001' as 'gemini-2.5-pro', // type workaround for embedding model + model: 'text-embedding-004', input: ['doc one', 'doc two'], }) - expect(mocks.embedContentSpy).toHaveBeenCalledTimes(1) - const [payload] = mocks.embedContentSpy.mock.calls[0] - expect(payload.model).toBe('gemini-embedding-001') - expect(payload.contents).toEqual(['doc one', 'doc two']) + expect(mocks.embedContentSpy).toHaveBeenCalledTimes(2) expect(result.embeddings).toEqual([ [0.1, 0.2], [0.3, 0.4], diff --git a/packages/typescript/ai-gemini/tests/image-adapter.test.ts b/packages/typescript/ai-gemini/tests/image-adapter.test.ts new file mode 100644 index 00000000..7ba2f456 --- /dev/null +++ b/packages/typescript/ai-gemini/tests/image-adapter.test.ts @@ -0,0 +1,195 @@ +import { describe, it, expect, vi } from 'vitest' +import { GeminiImageAdapter, createGeminiImage } from '../src/adapters/image' +import { + sizeToAspectRatio, + validateImageSize, + validateNumberOfImages, + validatePrompt, +} from '../src/image/image-provider-options' + +describe('Gemini Image Adapter', () => { + describe('createGeminiImage', () => { + it('creates an adapter with the provided API key', () => { + const adapter = createGeminiImage('test-api-key') + expect(adapter).toBeInstanceOf(GeminiImageAdapter) + expect(adapter.kind).toBe('image') + expect(adapter.name).toBe('gemini') + }) + + it('has the correct models', () => { + const adapter = createGeminiImage('test-api-key') + expect(adapter.models).toContain('imagen-3.0-generate-002') + expect(adapter.models).toContain('imagen-4.0-generate-001') + expect(adapter.models).toContain('imagen-4.0-fast-generate-001') + expect(adapter.models).toContain('imagen-4.0-ultra-generate-001') + }) + }) + + describe('sizeToAspectRatio', () => { + it('maps common sizes to aspect ratios', () => { + expect(sizeToAspectRatio('1024x1024')).toBe('1:1') + expect(sizeToAspectRatio('512x512')).toBe('1:1') + expect(sizeToAspectRatio('1920x1080')).toBe('16:9') + expect(sizeToAspectRatio('1080x1920')).toBe('9:16') + }) + + it('returns undefined for unknown sizes', () => { + expect(sizeToAspectRatio('999x999')).toBeUndefined() + expect(sizeToAspectRatio('invalid')).toBeUndefined() + }) + + it('returns undefined for undefined input', () => { + expect(sizeToAspectRatio(undefined)).toBeUndefined() + }) + }) + + describe('validateImageSize', () => { + it('accepts valid sizes that map to aspect ratios', () => { + expect(() => + validateImageSize('imagen-3.0-generate-002', '1024x1024'), + ).not.toThrow() + expect(() => + validateImageSize('imagen-4.0-generate-001', '1920x1080'), + ).not.toThrow() + }) + + it('rejects invalid sizes', () => { + expect(() => + validateImageSize('imagen-3.0-generate-002', '999x999'), + ).toThrow() + }) + + it('accepts undefined size', () => { + expect(() => + validateImageSize('imagen-3.0-generate-002', undefined), + ).not.toThrow() + }) + }) + + describe('validateNumberOfImages', () => { + it('accepts 1-4 images', () => { + expect(() => + validateNumberOfImages('imagen-3.0-generate-002', 1), + ).not.toThrow() + expect(() => + validateNumberOfImages('imagen-3.0-generate-002', 4), + ).not.toThrow() + }) + + it('rejects more than 4 images', () => { + expect(() => + validateNumberOfImages('imagen-3.0-generate-002', 5), + ).toThrow() + }) + + it('rejects 0 images', () => { + expect(() => + validateNumberOfImages('imagen-3.0-generate-002', 0), + ).toThrow() + }) + + it('accepts undefined', () => { + expect(() => + validateNumberOfImages('imagen-3.0-generate-002', undefined), + ).not.toThrow() + }) + }) + + describe('validatePrompt', () => { + it('rejects empty prompts', () => { + expect(() => + validatePrompt({ prompt: '', model: 'imagen-3.0-generate-002' }), + ).toThrow() + expect(() => + validatePrompt({ prompt: ' ', model: 'imagen-3.0-generate-002' }), + ).toThrow() + }) + + it('accepts non-empty prompts', () => { + expect(() => + validatePrompt({ prompt: 'A cat', model: 'imagen-3.0-generate-002' }), + ).not.toThrow() + }) + }) + + describe('generateImages', () => { + it('calls the Gemini models.generateImages API', async () => { + const mockResponse = { + generatedImages: [ + { + image: { + imageBytes: 'base64encodedimage', + }, + }, + ], + } + + const mockGenerateImages = vi.fn().mockResolvedValueOnce(mockResponse) + + const adapter = createGeminiImage('test-api-key') + // Replace the internal Gemini SDK client with our mock + ;( + adapter as unknown as { + client: { models: { generateImages: unknown } } + } + ).client = { + models: { + generateImages: mockGenerateImages, + }, + } + + const result = await adapter.generateImages({ + model: 'imagen-3.0-generate-002', + prompt: 'A cat wearing a hat', + numberOfImages: 1, + size: '1024x1024', + }) + + expect(mockGenerateImages).toHaveBeenCalledWith({ + model: 'imagen-3.0-generate-002', + prompt: 'A cat wearing a hat', + config: { + numberOfImages: 1, + aspectRatio: '1:1', + }, + }) + + expect(result.model).toBe('imagen-3.0-generate-002') + expect(result.images).toHaveLength(1) + expect(result.images[0].b64Json).toBe('base64encodedimage') + }) + + it('generates a unique ID for each response', async () => { + const mockResponse = { + generatedImages: [{ image: { imageBytes: 'base64' } }], + } + + const mockGenerateImages = vi.fn().mockResolvedValue(mockResponse) + + const adapter = createGeminiImage('test-api-key') + ;( + adapter as unknown as { + client: { models: { generateImages: unknown } } + } + ).client = { + models: { + generateImages: mockGenerateImages, + }, + } + + const result1 = await adapter.generateImages({ + model: 'imagen-3.0-generate-002', + prompt: 'Test prompt', + }) + + const result2 = await adapter.generateImages({ + model: 'imagen-3.0-generate-002', + prompt: 'Test prompt', + }) + + expect(result1.id).not.toBe(result2.id) + expect(result1.id).toMatch(/^gemini-/) + expect(result2.id).toMatch(/^gemini-/) + }) + }) +}) diff --git a/packages/typescript/ai-ollama/package.json b/packages/typescript/ai-ollama/package.json index 3d26a035..0baae378 100644 --- a/packages/typescript/ai-ollama/package.json +++ b/packages/typescript/ai-ollama/package.json @@ -42,14 +42,14 @@ ], "dependencies": { "@tanstack/ai": "workspace:*", - "ollama": "^0.6.3", - "zod": "^4.1.13" + "ollama": "^0.6.3" }, "devDependencies": { "@vitest/coverage-v8": "4.0.14", - "vite": "^7.2.4" + "vite": "^7.2.7" }, "peerDependencies": { - "@tanstack/ai": "workspace:*" + "@tanstack/ai": "workspace:*", + "zod": "^4.0.0" } } diff --git a/packages/typescript/ai-ollama/src/adapters/embed.ts b/packages/typescript/ai-ollama/src/adapters/embed.ts new file mode 100644 index 00000000..4d9c1003 --- /dev/null +++ b/packages/typescript/ai-ollama/src/adapters/embed.ts @@ -0,0 +1,129 @@ +import { + createOllamaClient, + estimateTokens, + getOllamaHostFromEnv, +} from '../utils' + +import type { Ollama } from 'ollama' +import type { EmbeddingAdapter } from '@tanstack/ai/adapters' +import type { EmbeddingOptions, EmbeddingResult } from '@tanstack/ai' + +/** + * Ollama embedding models + * Note: Ollama models are dynamically loaded, this is a common subset + */ +export const OllamaEmbeddingModels = [ + 'nomic-embed-text', + 'mxbai-embed-large', + 'all-minilm', + 'snowflake-arctic-embed', +] as const + +export type OllamaEmbeddingModel = + | (typeof OllamaEmbeddingModels)[number] + | (string & {}) + +/** + * Ollama-specific provider options for embeddings + */ +export interface OllamaEmbedProviderOptions { + /** Number of GPU layers to use */ + num_gpu?: number + /** Number of threads to use */ + num_thread?: number + /** Use memory-mapped model */ + use_mmap?: boolean + /** Use memory-locked model */ + use_mlock?: boolean +} + +export interface OllamaEmbedAdapterOptions { + model?: OllamaEmbeddingModel + host?: string +} + +/** + * Ollama Embedding Adapter + * A tree-shakeable embedding adapter for Ollama + */ +export class OllamaEmbedAdapter implements EmbeddingAdapter< + typeof OllamaEmbeddingModels, + OllamaEmbedProviderOptions +> { + readonly kind = 'embedding' as const + readonly name = 'ollama' as const + readonly models = OllamaEmbeddingModels + + /** Type-only property for provider options inference */ + declare _providerOptions?: OllamaEmbedProviderOptions + + private client: Ollama + private defaultModel: OllamaEmbeddingModel + + constructor( + hostOrClient?: string | Ollama, + options: OllamaEmbedAdapterOptions = {}, + ) { + if (typeof hostOrClient === 'string' || hostOrClient === undefined) { + this.client = createOllamaClient({ host: hostOrClient }) + } else { + this.client = hostOrClient + } + this.defaultModel = options.model ?? 'nomic-embed-text' + } + + async createEmbeddings(options: EmbeddingOptions): Promise { + const model = options.model || this.defaultModel + + // Ensure input is an array + const inputs = Array.isArray(options.input) + ? options.input + : [options.input] + + const embeddings: Array> = [] + + for (const input of inputs) { + const response = await this.client.embeddings({ + model, + prompt: input, + }) + + embeddings.push(response.embedding) + } + + const promptTokens = inputs.reduce( + (sum: number, input: string) => sum + estimateTokens(input), + 0, + ) + + return { + id: `embed-${Date.now()}`, + model, + embeddings, + usage: { + promptTokens, + totalTokens: promptTokens, + }, + } + } +} + +/** + * Creates an Ollama embedding adapter with explicit host + */ +export function createOllamaEmbed( + host?: string, + options?: OllamaEmbedAdapterOptions, +): OllamaEmbedAdapter { + return new OllamaEmbedAdapter(host, options) +} + +/** + * Creates an Ollama embedding adapter with host from environment + */ +export function ollamaEmbed( + options?: OllamaEmbedAdapterOptions, +): OllamaEmbedAdapter { + const host = getOllamaHostFromEnv() + return new OllamaEmbedAdapter(host, options) +} diff --git a/packages/typescript/ai-ollama/src/adapters/summarize.ts b/packages/typescript/ai-ollama/src/adapters/summarize.ts new file mode 100644 index 00000000..1291242c --- /dev/null +++ b/packages/typescript/ai-ollama/src/adapters/summarize.ts @@ -0,0 +1,167 @@ +import { + createOllamaClient, + estimateTokens, + generateId, + getOllamaHostFromEnv, +} from '../utils' + +import type { Ollama } from 'ollama' +import type { SummarizeAdapter } from '@tanstack/ai/adapters' +import type { SummarizationOptions, SummarizationResult } from '@tanstack/ai' + +/** + * Ollama models suitable for summarization + * Note: Ollama models are dynamically loaded, this is a common subset + */ +export const OllamaSummarizeModels = [ + 'llama2', + 'llama3', + 'llama3.1', + 'llama3.2', + 'mistral', + 'mixtral', + 'phi', + 'phi3', + 'qwen2', + 'qwen2.5', +] as const + +export type OllamaSummarizeModel = + | (typeof OllamaSummarizeModels)[number] + | (string & {}) + +/** + * Ollama-specific provider options for summarization + */ +export interface OllamaSummarizeProviderOptions { + /** Number of GPU layers to use */ + num_gpu?: number + /** Number of threads to use */ + num_thread?: number + /** Context window size */ + num_ctx?: number + /** Number of tokens to predict */ + num_predict?: number + /** Temperature for sampling */ + temperature?: number + /** Top-p sampling */ + top_p?: number + /** Top-k sampling */ + top_k?: number + /** Repeat penalty */ + repeat_penalty?: number +} + +export interface OllamaSummarizeAdapterOptions { + model?: OllamaSummarizeModel + host?: string +} + +/** + * Ollama Summarize Adapter + * A tree-shakeable summarization adapter for Ollama + */ +export class OllamaSummarizeAdapter implements SummarizeAdapter< + typeof OllamaSummarizeModels, + OllamaSummarizeProviderOptions +> { + readonly kind = 'summarize' as const + readonly name = 'ollama' as const + readonly models = OllamaSummarizeModels + + /** Type-only property for provider options inference */ + declare _providerOptions?: OllamaSummarizeProviderOptions + + private client: Ollama + private defaultModel: OllamaSummarizeModel + + constructor( + hostOrClient?: string | Ollama, + options: OllamaSummarizeAdapterOptions = {}, + ) { + if (typeof hostOrClient === 'string' || hostOrClient === undefined) { + this.client = createOllamaClient({ host: hostOrClient }) + } else { + this.client = hostOrClient + } + this.defaultModel = options.model ?? 'llama3' + } + + async summarize(options: SummarizationOptions): Promise { + const model = options.model || this.defaultModel + + const prompt = this.buildSummarizationPrompt(options) + + const response = await this.client.generate({ + model, + prompt, + options: { + temperature: 0.3, + num_predict: options.maxLength ?? 500, + }, + stream: false, + }) + + const promptTokens = estimateTokens(prompt) + const completionTokens = estimateTokens(response.response) + + return { + id: generateId('sum'), + model: response.model, + summary: response.response, + usage: { + promptTokens, + completionTokens, + totalTokens: promptTokens + completionTokens, + }, + } + } + + private buildSummarizationPrompt(options: SummarizationOptions): string { + let prompt = 'You are a professional summarizer. ' + + switch (options.style) { + case 'bullet-points': + prompt += 'Provide a summary in bullet point format. ' + break + case 'concise': + prompt += 'Provide a very brief one or two sentence summary. ' + break + case 'paragraph': + default: + prompt += 'Provide a clear and concise summary in paragraph format. ' + } + + if (options.maxLength) { + prompt += `Keep the summary under ${options.maxLength} words. ` + } + + if (options.focus && options.focus.length > 0) { + prompt += `Focus on: ${options.focus.join(', ')}. ` + } + + prompt += `\n\nText to summarize:\n${options.text}\n\nSummary:` + + return prompt + } +} + +/** + * Creates an Ollama summarize adapter with explicit host + */ +export function createOllamaSummarize( + host?: string, + options?: OllamaSummarizeAdapterOptions, +): OllamaSummarizeAdapter { + return new OllamaSummarizeAdapter(host, options) +} + +/** + * Creates an Ollama summarize adapter with host from environment + */ +export function ollamaSummarize( + options?: OllamaSummarizeAdapterOptions, +): OllamaSummarizeAdapter { + const host = getOllamaHostFromEnv() + return new OllamaSummarizeAdapter(host, options) +} diff --git a/packages/typescript/ai-ollama/src/adapters/text.ts b/packages/typescript/ai-ollama/src/adapters/text.ts new file mode 100644 index 00000000..b5b9b48d --- /dev/null +++ b/packages/typescript/ai-ollama/src/adapters/text.ts @@ -0,0 +1,406 @@ +import { BaseTextAdapter } from '@tanstack/ai/adapters' + +import { + convertZodToOllamaSchema, + createOllamaClient, + generateId, + getOllamaHostFromEnv, +} from '../utils' + +import type { + StructuredOutputOptions, + StructuredOutputResult, +} from '@tanstack/ai/adapters' +import type { + AbortableAsyncIterator, + ChatRequest, + ChatResponse, + Message, + Ollama, + Tool as OllamaTool, + ToolCall, +} from 'ollama' +import type { StreamChunk, TextOptions, Tool } from '@tanstack/ai' + +/** + * Ollama text models + * Note: Ollama models are dynamically loaded, this is a common subset + */ +export const OllamaTextModels = [ + 'llama2', + 'llama3', + 'llama3.1', + 'llama3.2', + 'codellama', + 'mistral', + 'mixtral', + 'phi', + 'phi3', + 'neural-chat', + 'starling-lm', + 'orca-mini', + 'vicuna', + 'nous-hermes', + 'qwen2', + 'qwen2.5', + 'gemma', + 'gemma2', + 'deepseek-coder', + 'command-r', +] as const + +export type OllamaTextModel = (typeof OllamaTextModels)[number] | (string & {}) + +/** + * Ollama-specific provider options + */ +export interface OllamaTextProviderOptions { + /** Number of tokens to keep from the prompt */ + num_keep?: number + /** Number of tokens from context to consider for next token prediction */ + top_k?: number + /** Minimum probability for nucleus sampling */ + min_p?: number + /** Tail-free sampling parameter */ + tfs_z?: number + /** Typical probability sampling parameter */ + typical_p?: number + /** Number of previous tokens to consider for repetition penalty */ + repeat_last_n?: number + /** Penalty for repeating tokens */ + repeat_penalty?: number + /** Enable Mirostat sampling (0=disabled, 1=Mirostat, 2=Mirostat 2.0) */ + mirostat?: number + /** Target entropy for Mirostat */ + mirostat_tau?: number + /** Learning rate for Mirostat */ + mirostat_eta?: number + /** Enable penalize_newline */ + penalize_newline?: boolean + /** Enable NUMA support */ + numa?: boolean + /** Context window size */ + num_ctx?: number + /** Batch size for prompt processing */ + num_batch?: number + /** Number of GQA groups (for some models) */ + num_gqa?: number + /** Number of GPU layers to use */ + num_gpu?: number + /** GPU to use for inference */ + main_gpu?: number + /** Use memory-mapped model */ + use_mmap?: boolean + /** Use memory-locked model */ + use_mlock?: boolean + /** Number of threads to use */ + num_thread?: number +} + +export interface OllamaTextAdapterOptions { + model?: OllamaTextModel + host?: string +} + +/** + * Ollama Text/Chat Adapter + * A tree-shakeable chat adapter for Ollama + */ +export class OllamaTextAdapter extends BaseTextAdapter< + typeof OllamaTextModels, + OllamaTextProviderOptions +> { + readonly kind = 'text' as const + readonly name = 'ollama' as const + readonly models = OllamaTextModels + + private client: Ollama + private defaultModel: OllamaTextModel + + constructor( + hostOrClient?: string | Ollama, + options: OllamaTextAdapterOptions = {}, + ) { + super({}) + if (typeof hostOrClient === 'string' || hostOrClient === undefined) { + this.client = createOllamaClient({ host: hostOrClient }) + } else { + this.client = hostOrClient + } + this.defaultModel = options.model ?? 'llama3' + } + + async *chatStream(options: TextOptions): AsyncIterable { + const mappedOptions = this.mapCommonOptionsToOllama(options) + const response = await this.client.chat({ + ...mappedOptions, + stream: true, + }) + yield* this.processOllamaStreamChunks(response) + } + + /** + * Generate structured output using Ollama's JSON format option. + * Uses format: 'json' with the schema to ensure structured output. + * Converts the Zod schema to JSON Schema format compatible with Ollama's API. + */ + async structuredOutput( + options: StructuredOutputOptions, + ): Promise> { + const { chatOptions, outputSchema } = options + + // Convert Zod schema to Ollama-compatible JSON Schema + const jsonSchema = convertZodToOllamaSchema(outputSchema) + + const mappedOptions = this.mapCommonOptionsToOllama(chatOptions) + + try { + // Make non-streaming request with JSON format + const response = await this.client.chat({ + ...mappedOptions, + stream: false, + format: jsonSchema, + }) + + const rawText = response.message.content + + // Parse the JSON response + let parsed: unknown + try { + parsed = JSON.parse(rawText) + } catch { + throw new Error( + `Failed to parse structured output as JSON. Content: ${rawText.slice(0, 200)}${rawText.length > 200 ? '...' : ''}`, + ) + } + + return { + data: parsed, + rawText, + } + } catch (error: unknown) { + const err = error as Error + throw new Error( + `Structured output generation failed: ${err.message || 'Unknown error occurred'}`, + ) + } + } + + private async *processOllamaStreamChunks( + stream: AbortableAsyncIterator, + ): AsyncIterable { + let accumulatedContent = '' + const timestamp = Date.now() + const responseId = generateId('msg') + let accumulatedReasoning = '' + let hasEmittedToolCalls = false + + for await (const chunk of stream) { + const handleToolCall = (toolCall: ToolCall): StreamChunk => { + const actualToolCall = toolCall as ToolCall & { + id: string + function: { index: number } + } + return { + type: 'tool_call', + id: responseId, + model: chunk.model, + timestamp, + toolCall: { + type: 'function', + id: actualToolCall.id, + function: { + name: actualToolCall.function.name || '', + arguments: + typeof actualToolCall.function.arguments === 'string' + ? actualToolCall.function.arguments + : JSON.stringify(actualToolCall.function.arguments), + }, + }, + index: actualToolCall.function.index, + } + } + + if (chunk.done) { + if (chunk.message.tool_calls && chunk.message.tool_calls.length > 0) { + for (const toolCall of chunk.message.tool_calls) { + yield handleToolCall(toolCall) + hasEmittedToolCalls = true + } + yield { + type: 'done', + id: responseId, + model: chunk.model, + timestamp, + finishReason: 'tool_calls', + } + continue + } + yield { + type: 'done', + id: responseId, + model: chunk.model, + timestamp, + finishReason: hasEmittedToolCalls ? 'tool_calls' : 'stop', + } + continue + } + + if (chunk.message.content) { + accumulatedContent += chunk.message.content + yield { + type: 'content', + id: responseId, + model: chunk.model, + timestamp, + delta: chunk.message.content, + content: accumulatedContent, + role: 'assistant', + } + } + + if (chunk.message.tool_calls && chunk.message.tool_calls.length > 0) { + for (const toolCall of chunk.message.tool_calls) { + yield handleToolCall(toolCall) + hasEmittedToolCalls = true + } + } + + if (chunk.message.thinking) { + accumulatedReasoning += chunk.message.thinking + yield { + type: 'thinking', + id: responseId, + model: chunk.model, + timestamp, + content: accumulatedReasoning, + delta: chunk.message.thinking, + } + } + } + } + + private convertToolsToOllamaFormat( + tools?: Array, + ): Array | undefined { + if (!tools || tools.length === 0) { + return undefined + } + + return tools.map((tool) => ({ + type: 'function', + function: { + name: tool.name, + description: tool.description, + parameters: tool.inputSchema + ? convertZodToOllamaSchema(tool.inputSchema) + : { type: 'object', properties: {}, required: [] }, + }, + })) + } + + private formatMessages(messages: TextOptions['messages']): Array { + return messages.map((msg) => { + let textContent = '' + const images: Array = [] + + if (Array.isArray(msg.content)) { + for (const part of msg.content) { + if (part.type === 'text') { + textContent += part.content + } else if (part.type === 'image') { + if (part.source.type === 'data') { + images.push(part.source.value) + } else { + images.push(part.source.value) + } + } + } + } else { + textContent = msg.content || '' + } + + const hasToolCallId = msg.role === 'tool' && msg.toolCallId + return { + role: hasToolCallId ? 'tool' : msg.role, + content: hasToolCallId + ? typeof msg.content === 'string' + ? msg.content + : JSON.stringify(msg.content) + : textContent, + ...(images.length > 0 ? { images } : {}), + ...(msg.role === 'assistant' && + msg.toolCalls && + msg.toolCalls.length > 0 + ? { + tool_calls: msg.toolCalls.map((toolCall) => { + let parsedArguments: Record = {} + if (typeof toolCall.function.arguments === 'string') { + try { + parsedArguments = JSON.parse( + toolCall.function.arguments, + ) as Record + } catch { + parsedArguments = {} + } + } else { + parsedArguments = toolCall.function + .arguments as unknown as Record + } + + return { + id: toolCall.id, + type: toolCall.type, + function: { + name: toolCall.function.name, + arguments: parsedArguments, + }, + } + }), + } + : {}), + } + }) + } + + private mapCommonOptionsToOllama(options: TextOptions): ChatRequest { + const model = options.model || this.defaultModel + const providerOptions = options.providerOptions as + | OllamaTextProviderOptions + | undefined + + const ollamaOptions = { + temperature: options.options?.temperature, + top_p: options.options?.topP, + num_predict: options.options?.maxTokens, + ...providerOptions, + } + + return { + model, + options: ollamaOptions, + messages: this.formatMessages(options.messages), + tools: this.convertToolsToOllamaFormat(options.tools), + } + } +} + +/** + * Creates an Ollama text adapter with explicit host + */ +export function createOllamaText( + host?: string, + options?: OllamaTextAdapterOptions, +): OllamaTextAdapter { + return new OllamaTextAdapter(host, options) +} + +/** + * Creates an Ollama text adapter with host from environment + */ +export function ollamaText( + options?: OllamaTextAdapterOptions, +): OllamaTextAdapter { + const host = getOllamaHostFromEnv() + return new OllamaTextAdapter(host, options) +} diff --git a/packages/typescript/ai-ollama/src/index.ts b/packages/typescript/ai-ollama/src/index.ts index e3f1428f..e4f0e779 100644 --- a/packages/typescript/ai-ollama/src/index.ts +++ b/packages/typescript/ai-ollama/src/index.ts @@ -1,3 +1,50 @@ +// =========================== +// New tree-shakeable adapters +// =========================== + +// Text/Chat adapter +export { + OllamaTextAdapter, + OllamaTextModels, + createOllamaText, + ollamaText, + type OllamaTextAdapterOptions, + type OllamaTextModel, + type OllamaTextProviderOptions, +} from './adapters/text' + +// Embedding adapter +export { + OllamaEmbedAdapter, + OllamaEmbeddingModels, + createOllamaEmbed, + ollamaEmbed, + type OllamaEmbedAdapterOptions, + type OllamaEmbeddingModel, + type OllamaEmbedProviderOptions, +} from './adapters/embed' + +// Summarize adapter +export { + OllamaSummarizeAdapter, + OllamaSummarizeModels, + createOllamaSummarize, + ollamaSummarize, + type OllamaSummarizeAdapterOptions, + type OllamaSummarizeModel, + type OllamaSummarizeProviderOptions, +} from './adapters/summarize' + +// =========================== +// Legacy monolithic adapter (deprecated) +// =========================== + +/** + * @deprecated Use the new tree-shakeable adapters instead: + * - `ollamaText()` / `createOllamaText()` for chat/text generation + * - `ollamaEmbed()` / `createOllamaEmbed()` for embeddings + * - `ollamaSummarize()` / `createOllamaSummarize()` for summarization + */ export { Ollama, createOllama, diff --git a/packages/typescript/ai-ollama/src/ollama-adapter.ts b/packages/typescript/ai-ollama/src/ollama-adapter.ts index fc6080c0..3a650106 100644 --- a/packages/typescript/ai-ollama/src/ollama-adapter.ts +++ b/packages/typescript/ai-ollama/src/ollama-adapter.ts @@ -1,5 +1,6 @@ import { Ollama as OllamaSDK } from 'ollama' -import { BaseAdapter, convertZodToJsonSchema } from '@tanstack/ai' +import { BaseAdapter } from '@tanstack/ai' +import { convertZodToOllamaSchema } from './utils/schema-converter' import type { AbortableAsyncIterator, ChatRequest, @@ -9,13 +10,13 @@ import type { ToolCall, } from 'ollama' import type { - ChatOptions, DefaultMessageMetadataByModality, EmbeddingOptions, EmbeddingResult, StreamChunk, SummarizationOptions, SummarizationResult, + TextOptions, Tool, } from '@tanstack/ai' @@ -163,7 +164,7 @@ export class Ollama extends BaseAdapter< }) } - async *chatStream(options: ChatOptions): AsyncIterable { + async *chatStream(options: TextOptions): AsyncIterable { // Use stream converter for now // Map common options to Ollama format const mappedOptions = this.mapCommonOptionsToOllama(options) @@ -373,7 +374,9 @@ export class Ollama extends BaseAdapter< function: { name: tool.name, description: tool.description, - parameters: convertZodToJsonSchema(tool.inputSchema), + parameters: tool.inputSchema + ? convertZodToOllamaSchema(tool.inputSchema) + : { type: 'object', properties: {}, required: [] }, }, })) } @@ -381,7 +384,7 @@ export class Ollama extends BaseAdapter< /** * Formats messages for Ollama, handling tool calls, tool results, and multimodal content */ - private formatMessages(messages: ChatOptions['messages']): Array { + private formatMessages(messages: TextOptions['messages']): Array { return messages.map((msg) => { let textContent = '' const images: Array = [] @@ -453,7 +456,7 @@ export class Ollama extends BaseAdapter< * Maps common options to Ollama-specific format * Handles translation of normalized options to Ollama's API format */ - private mapCommonOptionsToOllama(options: ChatOptions): ChatRequest { + private mapCommonOptionsToOllama(options: TextOptions): ChatRequest { const providerOptions = options.providerOptions as | OllamaProviderOptions | undefined diff --git a/packages/typescript/ai-ollama/src/utils/client.ts b/packages/typescript/ai-ollama/src/utils/client.ts new file mode 100644 index 00000000..dc5cd927 --- /dev/null +++ b/packages/typescript/ai-ollama/src/utils/client.ts @@ -0,0 +1,49 @@ +import { Ollama } from 'ollama' + +export interface OllamaClientConfig { + host?: string +} + +/** + * Creates an Ollama client instance + */ +export function createOllamaClient(config: OllamaClientConfig = {}): Ollama { + return new Ollama({ + host: config.host || 'http://localhost:11434', + }) +} + +/** + * Gets Ollama host from environment variables + * Falls back to default localhost + */ +export function getOllamaHostFromEnv(): string { + const env = + typeof globalThis !== 'undefined' && + (globalThis as Record).window + ? (( + (globalThis as Record).window as Record< + string, + unknown + > + ).env as Record | undefined) + : typeof process !== 'undefined' + ? process.env + : undefined + return env?.OLLAMA_HOST || 'http://localhost:11434' +} + +/** + * Generates a unique ID with a prefix + */ +export function generateId(prefix: string = 'msg'): string { + return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(7)}` +} + +/** + * Estimates token count for text (rough approximation) + */ +export function estimateTokens(text: string): number { + // Rough approximation: 1 token ≈ 4 characters + return Math.ceil(text.length / 4) +} diff --git a/packages/typescript/ai-ollama/src/utils/index.ts b/packages/typescript/ai-ollama/src/utils/index.ts new file mode 100644 index 00000000..3ba50049 --- /dev/null +++ b/packages/typescript/ai-ollama/src/utils/index.ts @@ -0,0 +1,8 @@ +export { + createOllamaClient, + estimateTokens, + generateId, + getOllamaHostFromEnv, + type OllamaClientConfig, +} from './client' +export { convertZodToOllamaSchema } from './schema-converter' diff --git a/packages/typescript/ai-ollama/src/utils/schema-converter.ts b/packages/typescript/ai-ollama/src/utils/schema-converter.ts new file mode 100644 index 00000000..bb47f9df --- /dev/null +++ b/packages/typescript/ai-ollama/src/utils/schema-converter.ts @@ -0,0 +1,87 @@ +import { toJSONSchema } from 'zod' +import type { z } from 'zod' + +/** + * Check if a value is a Zod schema by looking for Zod-specific internals. + * Zod schemas have a `_zod` property that contains metadata. + */ +function isZodSchema(schema: unknown): schema is z.ZodType { + return ( + typeof schema === 'object' && + schema !== null && + '_zod' in schema && + typeof (schema as any)._zod === 'object' + ) +} + +/** + * Converts a Zod schema to JSON Schema format compatible with Ollama's API. + * + * Ollama accepts standard JSON Schema without special transformations. + * + * @param schema - Zod schema to convert + * @returns JSON Schema object compatible with Ollama's structured output API + * + * @example + * ```typescript + * import { z } from 'zod'; + * + * const zodSchema = z.object({ + * location: z.string().describe('City name'), + * unit: z.enum(['celsius', 'fahrenheit']).optional() + * }); + * + * const jsonSchema = convertZodToOllamaSchema(zodSchema); + * // Returns standard JSON Schema + * ``` + */ +export function convertZodToOllamaSchema( + schema: z.ZodType, +): Record { + if (!isZodSchema(schema)) { + throw new Error('Expected a Zod schema') + } + + // Use Zod's built-in toJSONSchema + const jsonSchema = toJSONSchema(schema, { + target: 'openapi-3.0', + reused: 'ref', + }) + + // Remove $schema property as it's not needed for LLM providers + let result = jsonSchema + if (typeof result === 'object' && '$schema' in result) { + const { $schema, ...rest } = result + result = rest + } + + // Ensure object schemas always have type: "object" + if (typeof result === 'object') { + const isZodObject = + typeof schema === 'object' && + 'def' in schema && + schema.def.type === 'object' + + if (isZodObject && !result.type) { + result.type = 'object' + } + + if (Object.keys(result).length === 0) { + result.type = 'object' + } + + if ('properties' in result && !result.type) { + result.type = 'object' + } + + if (result.type === 'object' && !('properties' in result)) { + result.properties = {} + } + + if (result.type === 'object' && !('required' in result)) { + result.required = [] + } + } + + return result +} diff --git a/packages/typescript/ai-openai/package.json b/packages/typescript/ai-openai/package.json index 48a0d02d..f7a596eb 100644 --- a/packages/typescript/ai-openai/package.json +++ b/packages/typescript/ai-openai/package.json @@ -45,9 +45,10 @@ }, "devDependencies": { "@vitest/coverage-v8": "4.0.14", - "vite": "^7.2.4" + "vite": "^7.2.7" }, "peerDependencies": { - "@tanstack/ai": "workspace:*" + "@tanstack/ai": "workspace:*", + "zod": "^4.0.0" } } diff --git a/packages/typescript/ai-openai/src/adapters/embed.ts b/packages/typescript/ai-openai/src/adapters/embed.ts new file mode 100644 index 00000000..822de00b --- /dev/null +++ b/packages/typescript/ai-openai/src/adapters/embed.ts @@ -0,0 +1,116 @@ +import { BaseEmbeddingAdapter } from '@tanstack/ai/adapters' +import { OPENAI_EMBEDDING_MODELS } from '../model-meta' +import { + createOpenAIClient, + generateId, + getOpenAIApiKeyFromEnv, +} from '../utils' +import type { EmbeddingOptions, EmbeddingResult } from '@tanstack/ai' +import type OpenAI_SDK from 'openai' +import type { OpenAIClientConfig } from '../utils' + +/** + * Configuration for OpenAI embedding adapter + */ +export interface OpenAIEmbedConfig extends OpenAIClientConfig {} + +/** + * OpenAI-specific provider options for embeddings + * Based on OpenAI Embeddings API documentation + * @see https://platform.openai.com/docs/api-reference/embeddings/create + */ +export interface OpenAIEmbedProviderOptions { + /** Encoding format for embeddings: 'float' | 'base64' */ + encodingFormat?: 'float' | 'base64' + /** Unique identifier for end-user (for abuse monitoring) */ + user?: string +} + +/** + * OpenAI Embedding Adapter + * + * Tree-shakeable adapter for OpenAI embedding functionality. + * Import only what you need for smaller bundle sizes. + */ +export class OpenAIEmbedAdapter extends BaseEmbeddingAdapter< + typeof OPENAI_EMBEDDING_MODELS, + OpenAIEmbedProviderOptions +> { + readonly kind = 'embedding' as const + readonly name = 'openai' as const + readonly models = OPENAI_EMBEDDING_MODELS + + private client: OpenAI_SDK + + constructor(config: OpenAIEmbedConfig) { + super({}) + this.client = createOpenAIClient(config) + } + + async createEmbeddings(options: EmbeddingOptions): Promise { + const response = await this.client.embeddings.create({ + model: options.model || 'text-embedding-ada-002', + input: options.input, + dimensions: options.dimensions, + }) + + return { + id: generateId(this.name), + model: response.model, + embeddings: response.data.map((d) => d.embedding), + usage: { + promptTokens: response.usage.prompt_tokens, + totalTokens: response.usage.total_tokens, + }, + } + } +} + +/** + * Creates an OpenAI embedding adapter with explicit API key + * + * @param apiKey - Your OpenAI API key + * @param config - Optional additional configuration + * @returns Configured OpenAI embedding adapter instance + * + * @example + * ```typescript + * const adapter = createOpenaiEmbed("sk-..."); + * ``` + */ +export function createOpenaiEmbed( + apiKey: string, + config?: Omit, +): OpenAIEmbedAdapter { + return new OpenAIEmbedAdapter({ apiKey, ...config }) +} + +/** + * Creates an OpenAI embedding adapter with automatic API key detection from environment variables. + * + * Looks for `OPENAI_API_KEY` in: + * - `process.env` (Node.js) + * - `window.env` (Browser with injected env) + * + * @param config - Optional configuration (excluding apiKey which is auto-detected) + * @returns Configured OpenAI embedding adapter instance + * @throws Error if OPENAI_API_KEY is not found in environment + * + * @example + * ```typescript + * // Automatically uses OPENAI_API_KEY from environment + * const adapter = openaiEmbed(); + * + * await generate({ + * adapter, + * model: "text-embedding-3-small", + * input: "Hello, world!" + * }); + * ``` + */ +export function openaiEmbed( + config?: Omit, +): OpenAIEmbedAdapter { + const apiKey = getOpenAIApiKeyFromEnv() + return createOpenaiEmbed(apiKey, config) +} diff --git a/packages/typescript/ai-openai/src/adapters/image.ts b/packages/typescript/ai-openai/src/adapters/image.ts new file mode 100644 index 00000000..38039145 --- /dev/null +++ b/packages/typescript/ai-openai/src/adapters/image.ts @@ -0,0 +1,172 @@ +import { BaseImageAdapter } from '@tanstack/ai/adapters' +import { OPENAI_IMAGE_MODELS } from '../model-meta' +import { + createOpenAIClient, + generateId, + getOpenAIApiKeyFromEnv, +} from '../utils' +import { + validateImageSize, + validateNumberOfImages, + validatePrompt, +} from '../image/image-provider-options' +import type { + OpenAIImageModelProviderOptionsByName, + OpenAIImageModelSizeByName, + OpenAIImageProviderOptions, +} from '../image/image-provider-options' +import type { + GeneratedImage, + ImageGenerationOptions, + ImageGenerationResult, +} from '@tanstack/ai' +import type OpenAI_SDK from 'openai' +import type { OpenAIClientConfig } from '../utils' + +/** + * Configuration for OpenAI image adapter + */ +export interface OpenAIImageConfig extends OpenAIClientConfig {} + +/** + * OpenAI Image Generation Adapter + * + * Tree-shakeable adapter for OpenAI image generation functionality. + * Supports gpt-image-1, gpt-image-1-mini, dall-e-3, and dall-e-2 models. + * + * Features: + * - Model-specific type-safe provider options + * - Size validation per model + * - Number of images validation + */ +export class OpenAIImageAdapter extends BaseImageAdapter< + typeof OPENAI_IMAGE_MODELS, + OpenAIImageProviderOptions, + OpenAIImageModelProviderOptionsByName, + OpenAIImageModelSizeByName +> { + readonly kind = 'image' as const + readonly name = 'openai' as const + readonly models = OPENAI_IMAGE_MODELS + + private client: OpenAI_SDK + + constructor(config: OpenAIImageConfig) { + super({}) + this.client = createOpenAIClient(config) + } + + async generateImages( + options: ImageGenerationOptions, + ): Promise { + const { model, prompt, numberOfImages, size } = options + + // Validate inputs + validatePrompt({ prompt, model }) + validateImageSize(model, size) + validateNumberOfImages(model, numberOfImages) + + // Build request based on model type + const request = this.buildRequest(options) + + const response = await this.client.images.generate({ + ...request, + stream: false, + }) + + return this.transformResponse(model, response) + } + + private buildRequest( + options: ImageGenerationOptions, + ): OpenAI_SDK.Images.ImageGenerateParams { + const { model, prompt, numberOfImages, size, providerOptions } = options + + return { + model, + prompt, + n: numberOfImages ?? 1, + size: size as OpenAI_SDK.Images.ImageGenerateParams['size'], + ...providerOptions, + } + } + + private transformResponse( + model: string, + response: OpenAI_SDK.Images.ImagesResponse, + ): ImageGenerationResult { + const images: Array = (response.data ?? []).map((item) => ({ + b64Json: item.b64_json, + url: item.url, + revisedPrompt: item.revised_prompt, + })) + + return { + id: generateId(this.name), + model, + images, + usage: response.usage + ? { + inputTokens: response.usage.input_tokens, + outputTokens: response.usage.output_tokens, + totalTokens: response.usage.total_tokens, + } + : undefined, + } + } +} + +/** + * Creates an OpenAI image adapter with explicit API key + * + * @param apiKey - Your OpenAI API key + * @param config - Optional additional configuration + * @returns Configured OpenAI image adapter instance + * + * @example + * ```typescript + * const adapter = createOpenaiImage("sk-..."); + * + * const result = await ai({ + * adapter, + * model: 'gpt-image-1', + * prompt: 'A cute baby sea otter' + * }); + * ``` + */ +export function createOpenaiImage( + apiKey: string, + config?: Omit, +): OpenAIImageAdapter { + return new OpenAIImageAdapter({ apiKey, ...config }) +} + +/** + * Creates an OpenAI image adapter with automatic API key detection from environment variables. + * + * Looks for `OPENAI_API_KEY` in: + * - `process.env` (Node.js) + * - `window.env` (Browser with injected env) + * + * @param config - Optional configuration (excluding apiKey which is auto-detected) + * @returns Configured OpenAI image adapter instance + * @throws Error if OPENAI_API_KEY is not found in environment + * + * @example + * ```typescript + * // Automatically uses OPENAI_API_KEY from environment + * const adapter = openaiImage(); + * + * const result = await ai({ + * adapter, + * model: 'dall-e-3', + * prompt: 'A beautiful sunset over mountains' + * }); + * ``` + */ +export function openaiImage( + config?: Omit, +): OpenAIImageAdapter { + const apiKey = getOpenAIApiKeyFromEnv() + return createOpenaiImage(apiKey, config) +} diff --git a/packages/typescript/ai-openai/src/adapters/summarize.ts b/packages/typescript/ai-openai/src/adapters/summarize.ts new file mode 100644 index 00000000..34b4a9b0 --- /dev/null +++ b/packages/typescript/ai-openai/src/adapters/summarize.ts @@ -0,0 +1,145 @@ +import { BaseSummarizeAdapter } from '@tanstack/ai/adapters' +import { OPENAI_CHAT_MODELS } from '../model-meta' +import { createOpenAIClient, getOpenAIApiKeyFromEnv } from '../utils' +import type { SummarizationOptions, SummarizationResult } from '@tanstack/ai' +import type { OpenAIClientConfig } from '../utils' + +/** + * Configuration for OpenAI summarize adapter + */ +export interface OpenAISummarizeConfig extends OpenAIClientConfig {} + +/** + * OpenAI-specific provider options for summarization + */ +export interface OpenAISummarizeProviderOptions { + /** Temperature for response generation (0-2) */ + temperature?: number + /** Maximum tokens in the response */ + maxTokens?: number +} + +/** + * OpenAI Summarize Adapter + * + * Tree-shakeable adapter for OpenAI summarization functionality. + * Import only what you need for smaller bundle sizes. + */ +export class OpenAISummarizeAdapter extends BaseSummarizeAdapter< + typeof OPENAI_CHAT_MODELS, + OpenAISummarizeProviderOptions +> { + readonly kind = 'summarize' as const + readonly name = 'openai' as const + readonly models = OPENAI_CHAT_MODELS + + private client: ReturnType + + constructor(config: OpenAISummarizeConfig) { + super({}) + this.client = createOpenAIClient(config) + } + + async summarize(options: SummarizationOptions): Promise { + const systemPrompt = this.buildSummarizationPrompt(options) + + const response = await this.client.chat.completions.create({ + model: options.model || 'gpt-3.5-turbo', + messages: [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: options.text }, + ], + max_tokens: options.maxLength, + temperature: 0.3, + stream: false, + }) + + return { + id: response.id, + model: response.model, + summary: response.choices[0]?.message.content || '', + usage: { + promptTokens: response.usage?.prompt_tokens || 0, + completionTokens: response.usage?.completion_tokens || 0, + totalTokens: response.usage?.total_tokens || 0, + }, + } + } + + private buildSummarizationPrompt(options: SummarizationOptions): string { + let prompt = 'You are a professional summarizer. ' + + switch (options.style) { + case 'bullet-points': + prompt += 'Provide a summary in bullet point format. ' + break + case 'paragraph': + prompt += 'Provide a summary in paragraph format. ' + break + case 'concise': + prompt += 'Provide a very concise summary in 1-2 sentences. ' + break + default: + prompt += 'Provide a clear and concise summary. ' + } + + if (options.focus && options.focus.length > 0) { + prompt += `Focus on the following aspects: ${options.focus.join(', ')}. ` + } + + if (options.maxLength) { + prompt += `Keep the summary under ${options.maxLength} tokens. ` + } + + return prompt + } +} + +/** + * Creates an OpenAI summarize adapter with explicit API key + * + * @param apiKey - Your OpenAI API key + * @param config - Optional additional configuration + * @returns Configured OpenAI summarize adapter instance + * + * @example + * ```typescript + * const adapter = createOpenaiSummarize("sk-..."); + * ``` + */ +export function createOpenaiSummarize( + apiKey: string, + config?: Omit, +): OpenAISummarizeAdapter { + return new OpenAISummarizeAdapter({ apiKey, ...config }) +} + +/** + * Creates an OpenAI summarize adapter with automatic API key detection from environment variables. + * + * Looks for `OPENAI_API_KEY` in: + * - `process.env` (Node.js) + * - `window.env` (Browser with injected env) + * + * @param config - Optional configuration (excluding apiKey which is auto-detected) + * @returns Configured OpenAI summarize adapter instance + * @throws Error if OPENAI_API_KEY is not found in environment + * + * @example + * ```typescript + * // Automatically uses OPENAI_API_KEY from environment + * const adapter = openaiSummarize(); + * + * await generate({ + * adapter, + * model: "gpt-4", + * text: "Long article text..." + * }); + * ``` + */ +export function openaiSummarize( + config?: Omit, +): OpenAISummarizeAdapter { + const apiKey = getOpenAIApiKeyFromEnv() + return createOpenaiSummarize(apiKey, config) +} diff --git a/packages/typescript/ai-openai/src/adapters/text.ts b/packages/typescript/ai-openai/src/adapters/text.ts new file mode 100644 index 00000000..d1eed227 --- /dev/null +++ b/packages/typescript/ai-openai/src/adapters/text.ts @@ -0,0 +1,773 @@ +import { BaseTextAdapter } from '@tanstack/ai/adapters' +import { OPENAI_CHAT_MODELS } from '../model-meta' +import { validateTextProviderOptions } from '../text/text-provider-options' +import { convertToolsToProviderFormat } from '../tools' +import { + convertZodToOpenAISchema, + createOpenAIClient, + generateId, + getOpenAIApiKeyFromEnv, + transformNullsToUndefined, +} from '../utils' +import type { + StructuredOutputOptions, + StructuredOutputResult, +} from '@tanstack/ai/adapters' +import type OpenAI_SDK from 'openai' +import type { Responses } from 'openai/resources' +import type { + ContentPart, + ModelMessage, + StreamChunk, + TextOptions, +} from '@tanstack/ai' +import type { + OpenAIChatModelProviderOptionsByName, + OpenAIModelInputModalitiesByName, +} from '../model-meta' +import type { + ExternalTextProviderOptions, + InternalTextProviderOptions, +} from '../text/text-provider-options' +import type { + OpenAIAudioMetadata, + OpenAIImageMetadata, + OpenAIMessageMetadataByModality, +} from '../message-types' +import type { OpenAIClientConfig } from '../utils' + +/** + * Configuration for OpenAI text adapter + */ +export interface OpenAITextConfig extends OpenAIClientConfig {} + +/** + * Alias for TextProviderOptions + */ +export type OpenAITextProviderOptions = ExternalTextProviderOptions + +/** + * OpenAI Text (Chat) Adapter + * + * Tree-shakeable adapter for OpenAI chat/text completion functionality. + * Import only what you need for smaller bundle sizes. + */ +export class OpenAITextAdapter extends BaseTextAdapter< + typeof OPENAI_CHAT_MODELS, + OpenAITextProviderOptions, + OpenAIChatModelProviderOptionsByName, + OpenAIModelInputModalitiesByName, + OpenAIMessageMetadataByModality +> { + readonly kind = 'text' as const + readonly name = 'openai' as const + readonly models = OPENAI_CHAT_MODELS + + // Type-only properties for type inference + declare _modelProviderOptionsByName: OpenAIChatModelProviderOptionsByName + declare _modelInputModalitiesByName: OpenAIModelInputModalitiesByName + declare _messageMetadataByModality: OpenAIMessageMetadataByModality + + private client: OpenAI_SDK + + constructor(config: OpenAITextConfig) { + super({}) + this.client = createOpenAIClient(config) + } + + async *chatStream( + options: TextOptions, + ): AsyncIterable { + // Track tool call metadata by unique ID + // OpenAI streams tool calls with deltas - first chunk has ID/name, subsequent chunks only have args + // We assign our own indices as we encounter unique tool call IDs + const toolCallMetadata = new Map() + const requestArguments = this.mapTextOptionsToOpenAI(options) + + try { + const response = await this.client.responses.create( + { + ...requestArguments, + stream: true, + }, + { + headers: options.request?.headers, + signal: options.request?.signal, + }, + ) + + // Chat Completions API uses SSE format - iterate directly + yield* this.processOpenAIStreamChunks( + response, + toolCallMetadata, + options, + () => generateId(this.name), + ) + } catch (error: unknown) { + const err = error as Error + console.error('>>> chatStream: Fatal error during response creation <<<') + console.error('>>> Error message:', err.message) + console.error('>>> Error stack:', err.stack) + console.error('>>> Full error:', err) + throw error + } + } + + /** + * Generate structured output using OpenAI's native JSON Schema response format. + * Uses stream: false to get the complete response in one call. + * + * OpenAI has strict requirements for structured output: + * - All properties must be in the `required` array + * - Optional fields should have null added to their type union + * - additionalProperties must be false for all objects + * + * The schema conversion is handled by convertZodToOpenAISchema. + */ + async structuredOutput( + options: StructuredOutputOptions, + ): Promise> { + const { chatOptions, outputSchema } = options + const requestArguments = this.mapTextOptionsToOpenAI(chatOptions) + + // Convert Zod schema to OpenAI-compatible JSON Schema + const jsonSchema = convertZodToOpenAISchema(outputSchema) + + try { + const response = await this.client.responses.create( + { + ...requestArguments, + stream: false, + // Configure structured output via text.format + text: { + format: { + type: 'json_schema', + name: 'structured_output', + schema: jsonSchema, + strict: true, + }, + }, + }, + { + headers: chatOptions.request?.headers, + signal: chatOptions.request?.signal, + }, + ) + + // Extract text content from the response + const rawText = this.extractTextFromResponse(response) + + // Parse the JSON response + let parsed: unknown + try { + parsed = JSON.parse(rawText) + } catch { + throw new Error( + `Failed to parse structured output as JSON. Content: ${rawText.slice(0, 200)}${rawText.length > 200 ? '...' : ''}`, + ) + } + + // Transform null values to undefined to match original Zod schema expectations + // OpenAI returns null for optional fields we made nullable in the schema + const transformed = transformNullsToUndefined(parsed) + + return { + data: transformed, + rawText, + } + } catch (error: unknown) { + const err = error as Error + console.error('>>> structuredOutput: Error during response creation <<<') + console.error('>>> Error message:', err.message) + throw error + } + } + + /** + * Extract text content from a non-streaming response + */ + private extractTextFromResponse( + response: OpenAI_SDK.Responses.Response, + ): string { + let textContent = '' + + for (const item of response.output) { + if (item.type === 'message') { + for (const part of item.content) { + if (part.type === 'output_text') { + textContent += part.text + } + } + } + } + + return textContent + } + + private async *processOpenAIStreamChunks( + stream: AsyncIterable, + toolCallMetadata: Map, + options: TextOptions, + genId: () => string, + ): AsyncIterable { + let accumulatedContent = '' + let accumulatedReasoning = '' + const timestamp = Date.now() + let chunkCount = 0 + + // Track if we've been streaming deltas to avoid duplicating content from done events + let hasStreamedContentDeltas = false + let hasStreamedReasoningDeltas = false + + // Preserve response metadata across events + let responseId: string | null = null + let model: string = options.model + + const eventTypeCounts = new Map() + + try { + for await (const chunk of stream) { + chunkCount++ + const handleContentPart = ( + contentPart: + | OpenAI_SDK.Responses.ResponseOutputText + | OpenAI_SDK.Responses.ResponseOutputRefusal + | OpenAI_SDK.Responses.ResponseContentPartAddedEvent.ReasoningText, + ): StreamChunk => { + if (contentPart.type === 'output_text') { + accumulatedContent += contentPart.text + return { + type: 'content', + id: responseId || genId(), + model: model || options.model, + timestamp, + delta: contentPart.text, + content: accumulatedContent, + role: 'assistant', + } + } + + if (contentPart.type === 'reasoning_text') { + accumulatedReasoning += contentPart.text + return { + type: 'thinking', + id: responseId || genId(), + model: model || options.model, + timestamp, + delta: contentPart.text, + content: accumulatedReasoning, + } + } + return { + type: 'error', + id: responseId || genId(), + model: model || options.model, + timestamp, + error: { + message: contentPart.refusal, + }, + } + } + // handle general response events + if ( + chunk.type === 'response.created' || + chunk.type === 'response.incomplete' || + chunk.type === 'response.failed' + ) { + responseId = chunk.response.id + model = chunk.response.model + // Reset streaming flags for new response + hasStreamedContentDeltas = false + hasStreamedReasoningDeltas = false + accumulatedContent = '' + accumulatedReasoning = '' + if (chunk.response.error) { + yield { + type: 'error', + id: chunk.response.id, + model: chunk.response.model, + timestamp, + error: chunk.response.error, + } + } + if (chunk.response.incomplete_details) { + yield { + type: 'error', + id: chunk.response.id, + model: chunk.response.model, + timestamp, + error: { + message: chunk.response.incomplete_details.reason ?? '', + }, + } + } + } + // Handle output text deltas (token-by-token streaming) + // response.output_text.delta provides incremental text updates + if (chunk.type === 'response.output_text.delta' && chunk.delta) { + // Delta can be an array of strings or a single string + const textDelta = Array.isArray(chunk.delta) + ? chunk.delta.join('') + : typeof chunk.delta === 'string' + ? chunk.delta + : '' + + if (textDelta) { + accumulatedContent += textDelta + hasStreamedContentDeltas = true + yield { + type: 'content', + id: responseId || genId(), + model: model || options.model, + timestamp, + delta: textDelta, + content: accumulatedContent, + role: 'assistant', + } + } + } + + // Handle reasoning deltas (token-by-token thinking/reasoning streaming) + // response.reasoning_text.delta provides incremental reasoning updates + if (chunk.type === 'response.reasoning_text.delta' && chunk.delta) { + // Delta can be an array of strings or a single string + const reasoningDelta = Array.isArray(chunk.delta) + ? chunk.delta.join('') + : typeof chunk.delta === 'string' + ? chunk.delta + : '' + + if (reasoningDelta) { + accumulatedReasoning += reasoningDelta + hasStreamedReasoningDeltas = true + yield { + type: 'thinking', + id: responseId || genId(), + model: model || options.model, + timestamp, + delta: reasoningDelta, + content: accumulatedReasoning, + } + } + } + + // Handle reasoning summary deltas (when using reasoning.summary option) + // response.reasoning_summary_text.delta provides incremental summary updates + if ( + chunk.type === 'response.reasoning_summary_text.delta' && + chunk.delta + ) { + const summaryDelta = + typeof chunk.delta === 'string' ? chunk.delta : '' + + if (summaryDelta) { + accumulatedReasoning += summaryDelta + hasStreamedReasoningDeltas = true + yield { + type: 'thinking', + id: responseId || genId(), + model: model || options.model, + timestamp, + delta: summaryDelta, + content: accumulatedReasoning, + } + } + } + + // handle content_part added events for text, reasoning and refusals + if (chunk.type === 'response.content_part.added') { + const contentPart = chunk.part + yield handleContentPart(contentPart) + } + + if (chunk.type === 'response.content_part.done') { + const contentPart = chunk.part + + // Skip emitting chunks for content parts that we've already streamed via deltas + // The done event is just a completion marker, not new content + if (contentPart.type === 'output_text' && hasStreamedContentDeltas) { + // Content already accumulated from deltas, skip + continue + } + if ( + contentPart.type === 'reasoning_text' && + hasStreamedReasoningDeltas + ) { + // Reasoning already accumulated from deltas, skip + continue + } + + // Only emit if we haven't been streaming deltas (e.g., for non-streaming responses) + yield handleContentPart(contentPart) + } + + // handle output_item.added to capture function call metadata (name) + if (chunk.type === 'response.output_item.added') { + const item = chunk.item + if (item.type === 'function_call' && item.id) { + // Store the function name for later use + if (!toolCallMetadata.has(item.id)) { + toolCallMetadata.set(item.id, { + index: chunk.output_index, + name: item.name || '', + }) + } + } + } + + if (chunk.type === 'response.function_call_arguments.done') { + const { item_id, output_index } = chunk + + // Get the function name from metadata (captured in output_item.added) + const metadata = toolCallMetadata.get(item_id) + const name = metadata?.name || '' + + yield { + type: 'tool_call', + id: responseId || genId(), + model: model || options.model, + timestamp, + index: output_index, + toolCall: { + id: item_id, + type: 'function', + function: { + name, + arguments: chunk.arguments, + }, + }, + } + } + + if (chunk.type === 'response.completed') { + // Determine finish reason based on output + // If there are function_call items in the output, it's a tool_calls finish + const hasFunctionCalls = chunk.response.output.some( + (item: unknown) => + (item as { type: string }).type === 'function_call', + ) + + yield { + type: 'done', + id: responseId || genId(), + model: model || options.model, + timestamp, + usage: { + promptTokens: chunk.response.usage?.input_tokens || 0, + completionTokens: chunk.response.usage?.output_tokens || 0, + totalTokens: chunk.response.usage?.total_tokens || 0, + }, + finishReason: hasFunctionCalls ? 'tool_calls' : 'stop', + } + } + + if (chunk.type === 'error') { + yield { + type: 'error', + id: responseId || genId(), + model: model || options.model, + timestamp, + error: { + message: chunk.message, + code: chunk.code ?? undefined, + }, + } + } + } + } catch (error: unknown) { + const err = error as Error & { code?: string } + console.log( + '[OpenAI Adapter] Stream ended with error. Event type summary:', + { + totalChunks: chunkCount, + eventTypes: Object.fromEntries(eventTypeCounts), + error: err.message, + }, + ) + yield { + type: 'error', + id: genId(), + model: options.model, + timestamp, + error: { + message: err.message || 'Unknown error occurred', + code: err.code, + }, + } + } + } + + /** + * Maps common options to OpenAI-specific format + * Handles translation of normalized options to OpenAI's API format + */ + private mapTextOptionsToOpenAI(options: TextOptions) { + const providerOptions = options.providerOptions as + | Omit< + InternalTextProviderOptions, + | 'max_output_tokens' + | 'tools' + | 'metadata' + | 'temperature' + | 'input' + | 'top_p' + > + | undefined + const input = this.convertMessagesToInput(options.messages) + if (providerOptions) { + validateTextProviderOptions({ + ...providerOptions, + input, + model: options.model, + }) + } + + const tools = options.tools + ? convertToolsToProviderFormat(options.tools) + : undefined + + const requestParams: Omit< + OpenAI_SDK.Responses.ResponseCreateParams, + 'stream' + > = { + model: options.model, + temperature: options.options?.temperature, + max_output_tokens: options.options?.maxTokens, + top_p: options.options?.topP, + metadata: options.options?.metadata, + instructions: options.systemPrompts?.join('\n'), + ...providerOptions, + input, + tools, + } + + return requestParams + } + + private convertMessagesToInput( + messages: Array, + ): Responses.ResponseInput { + const result: Responses.ResponseInput = [] + + for (const message of messages) { + // Handle tool messages - convert to FunctionToolCallOutput + if (message.role === 'tool') { + result.push({ + type: 'function_call_output', + call_id: message.toolCallId || '', + output: + typeof message.content === 'string' + ? message.content + : JSON.stringify(message.content), + }) + continue + } + + // Handle assistant messages + if (message.role === 'assistant') { + // If the assistant message has tool calls, add them as FunctionToolCall objects + // OpenAI Responses API expects arguments as a string (JSON string) + if (message.toolCalls && message.toolCalls.length > 0) { + for (const toolCall of message.toolCalls) { + // Keep arguments as string for Responses API + // Our internal format stores arguments as a JSON string, which is what API expects + const argumentsString = + typeof toolCall.function.arguments === 'string' + ? toolCall.function.arguments + : JSON.stringify(toolCall.function.arguments) + + result.push({ + type: 'function_call', + call_id: toolCall.id, + name: toolCall.function.name, + arguments: argumentsString, + }) + } + } + + // Add the assistant's text message if there is content + if (message.content) { + // Assistant messages are typically text-only + const contentStr = this.extractTextContent(message.content) + if (contentStr) { + result.push({ + type: 'message', + role: 'assistant', + content: contentStr, + }) + } + } + + continue + } + + // Handle user messages (default case) - support multimodal content + const contentParts = this.normalizeContent(message.content) + const openAIContent: Array = [] + + for (const part of contentParts) { + openAIContent.push( + this.convertContentPartToOpenAI( + part as ContentPart< + OpenAIImageMetadata, + OpenAIAudioMetadata, + unknown, + unknown + >, + ), + ) + } + + // If no content parts, add empty text + if (openAIContent.length === 0) { + openAIContent.push({ type: 'input_text', text: '' }) + } + + result.push({ + type: 'message', + role: 'user', + content: openAIContent, + }) + } + + return result + } + + /** + * Converts a ContentPart to OpenAI input content item. + * Handles text, image, and audio content parts. + */ + private convertContentPartToOpenAI( + part: ContentPart< + OpenAIImageMetadata, + OpenAIAudioMetadata, + unknown, + unknown + >, + ): Responses.ResponseInputContent { + switch (part.type) { + case 'text': + return { + type: 'input_text', + text: part.content, + } + case 'image': { + const imageMetadata = part.metadata + if (part.source.type === 'url') { + return { + type: 'input_image', + image_url: part.source.value, + detail: imageMetadata?.detail || 'auto', + } + } + // For base64 data, construct a data URI + return { + type: 'input_image', + image_url: part.source.value, + detail: imageMetadata?.detail || 'auto', + } + } + case 'audio': { + if (part.source.type === 'url') { + // OpenAI may support audio URLs in the future + // For now, treat as data URI + return { + type: 'input_file', + file_url: part.source.value, + } + } + return { + type: 'input_file', + file_data: part.source.value, + } + } + + default: + throw new Error(`Unsupported content part type: ${part.type}`) + } + } + + /** + * Normalizes message content to an array of ContentPart. + * Handles backward compatibility with string content. + */ + private normalizeContent( + content: string | null | Array, + ): Array { + if (content === null) { + return [] + } + if (typeof content === 'string') { + return [{ type: 'text', content: content }] + } + return content + } + + /** + * Extracts text content from a content value that may be string, null, or ContentPart array. + */ + private extractTextContent( + content: string | null | Array, + ): string { + if (content === null) { + return '' + } + if (typeof content === 'string') { + return content + } + // It's an array of ContentPart + return content + .filter((p) => p.type === 'text') + .map((p) => p.content) + .join('') + } +} + +/** + * Creates an OpenAI text adapter with explicit API key + * + * @param apiKey - Your OpenAI API key + * @param config - Optional additional configuration + * @returns Configured OpenAI text adapter instance + * + * @example + * ```typescript + * const adapter = createOpenaiText("sk-..."); + * ``` + */ +export function createOpenaiText( + apiKey: string, + config?: Omit, +): OpenAITextAdapter { + return new OpenAITextAdapter({ apiKey, ...config }) +} + +/** + * Creates an OpenAI text adapter with automatic API key detection from environment variables. + * + * Looks for `OPENAI_API_KEY` in: + * - `process.env` (Node.js) + * - `window.env` (Browser with injected env) + * + * @param config - Optional configuration (excluding apiKey which is auto-detected) + * @returns Configured OpenAI text adapter instance + * @throws Error if OPENAI_API_KEY is not found in environment + * + * @example + * ```typescript + * // Automatically uses OPENAI_API_KEY from environment + * const adapter = openaiText(); + * + * await generate({ + * adapter, + * model: "gpt-4", + * messages: [{ role: "user", content: "Hello!" }] + * }); + * ``` + */ +export function openaiText( + config?: Omit, +): OpenAITextAdapter { + const apiKey = getOpenAIApiKeyFromEnv() + return createOpenaiText(apiKey, config) +} diff --git a/packages/typescript/ai-openai/src/adapters/transcription.ts b/packages/typescript/ai-openai/src/adapters/transcription.ts new file mode 100644 index 00000000..7bb754e6 --- /dev/null +++ b/packages/typescript/ai-openai/src/adapters/transcription.ts @@ -0,0 +1,239 @@ +import { BaseTranscriptionAdapter } from '@tanstack/ai/adapters' +import { OPENAI_TRANSCRIPTION_MODELS } from '../model-meta' +import { + createOpenAIClient, + generateId, + getOpenAIApiKeyFromEnv, +} from '../utils' +import type { OpenAITranscriptionProviderOptions } from '../audio/transcription-provider-options' +import type { + TranscriptionOptions, + TranscriptionResult, + TranscriptionSegment, +} from '@tanstack/ai' +import type OpenAI_SDK from 'openai' +import type { OpenAIClientConfig } from '../utils' + +/** + * Configuration for OpenAI Transcription adapter + */ +export interface OpenAITranscriptionConfig extends OpenAIClientConfig {} + +/** + * OpenAI Transcription (Speech-to-Text) Adapter + * + * Tree-shakeable adapter for OpenAI audio transcription functionality. + * Supports whisper-1, gpt-4o-transcribe, gpt-4o-mini-transcribe, and gpt-4o-transcribe-diarize models. + * + * Features: + * - Multiple transcription models with different capabilities + * - Language detection or specification + * - Multiple output formats: json, text, srt, verbose_json, vtt + * - Word and segment-level timestamps (with verbose_json) + * - Speaker diarization (with gpt-4o-transcribe-diarize) + */ +export class OpenAITranscriptionAdapter extends BaseTranscriptionAdapter< + typeof OPENAI_TRANSCRIPTION_MODELS, + OpenAITranscriptionProviderOptions +> { + readonly name = 'openai' as const + readonly models = OPENAI_TRANSCRIPTION_MODELS + + private client: OpenAI_SDK + + constructor(config: OpenAITranscriptionConfig) { + super(config) + this.client = createOpenAIClient(config) + } + + async transcribe( + options: TranscriptionOptions, + ): Promise { + const { model, audio, language, prompt, responseFormat, providerOptions } = + options + + // Convert audio input to File object + const file = this.prepareAudioFile(audio) + + // Build request + const request: OpenAI_SDK.Audio.TranscriptionCreateParams = { + model, + file, + language, + prompt, + response_format: this.mapResponseFormat(responseFormat), + ...providerOptions, + } + + // Call OpenAI API - use verbose_json to get timestamps when available + const useVerbose = + responseFormat === 'verbose_json' || + (!responseFormat && model !== 'whisper-1') + + if (useVerbose) { + request.response_format = 'verbose_json' + const response = (await this.client.audio.transcriptions.create( + request, + )) as OpenAI_SDK.Audio.Transcription & { + segments?: Array<{ + id: number + start: number + end: number + text: string + avg_logprob?: number + }> + words?: Array<{ + word: string + start: number + end: number + }> + duration?: number + language?: string + } + + return { + id: generateId(this.name), + model, + text: response.text, + language: response.language, + duration: response.duration, + segments: response.segments?.map( + (seg): TranscriptionSegment => ({ + id: seg.id, + start: seg.start, + end: seg.end, + text: seg.text, + confidence: seg.avg_logprob ? Math.exp(seg.avg_logprob) : undefined, + }), + ), + words: response.words?.map((w) => ({ + word: w.word, + start: w.start, + end: w.end, + })), + } + } else { + const response = await this.client.audio.transcriptions.create(request) + + return { + id: generateId(this.name), + model, + text: typeof response === 'string' ? response : response.text, + language, + } + } + } + + private prepareAudioFile(audio: string | File | Blob | ArrayBuffer): File { + // If already a File, return it + if (audio instanceof File) { + return audio + } + + // If Blob, convert to File + if (audio instanceof Blob) { + return new File([audio], 'audio.mp3', { + type: audio.type || 'audio/mpeg', + }) + } + + // If ArrayBuffer, convert to File + if (audio instanceof ArrayBuffer) { + return new File([audio], 'audio.mp3', { type: 'audio/mpeg' }) + } + + // If base64 string, decode and convert to File + if (typeof audio === 'string') { + // Check if it's a data URL + if (audio.startsWith('data:')) { + const parts = audio.split(',') + const header = parts[0] + const base64Data = parts[1] || '' + const mimeMatch = header?.match(/data:([^;]+)/) + const mimeType = mimeMatch?.[1] || 'audio/mpeg' + const binaryStr = atob(base64Data) + const bytes = new Uint8Array(binaryStr.length) + for (let i = 0; i < binaryStr.length; i++) { + bytes[i] = binaryStr.charCodeAt(i) + } + const extension = mimeType.split('/')[1] || 'mp3' + return new File([bytes], `audio.${extension}`, { type: mimeType }) + } + + // Assume raw base64 + const binaryStr = atob(audio) + const bytes = new Uint8Array(binaryStr.length) + for (let i = 0; i < binaryStr.length; i++) { + bytes[i] = binaryStr.charCodeAt(i) + } + return new File([bytes], 'audio.mp3', { type: 'audio/mpeg' }) + } + + throw new Error('Invalid audio input type') + } + + private mapResponseFormat( + format?: 'json' | 'text' | 'srt' | 'verbose_json' | 'vtt', + ): OpenAI_SDK.Audio.TranscriptionCreateParams['response_format'] { + if (!format) return 'json' + return format as OpenAI_SDK.Audio.TranscriptionCreateParams['response_format'] + } +} + +/** + * Creates an OpenAI Transcription adapter with explicit API key + * + * @param apiKey - Your OpenAI API key + * @param config - Optional additional configuration + * @returns Configured OpenAI Transcription adapter instance + * + * @example + * ```typescript + * const adapter = createOpenaiTranscription("sk-..."); + * + * const result = await ai({ + * adapter, + * model: 'whisper-1', + * audio: audioFile, + * language: 'en' + * }); + * ``` + */ +export function createOpenaiTranscription( + apiKey: string, + config?: Omit, +): OpenAITranscriptionAdapter { + return new OpenAITranscriptionAdapter({ apiKey, ...config }) +} + +/** + * Creates an OpenAI Transcription adapter with automatic API key detection from environment variables. + * + * Looks for `OPENAI_API_KEY` in: + * - `process.env` (Node.js) + * - `window.env` (Browser with injected env) + * + * @param config - Optional configuration (excluding apiKey which is auto-detected) + * @returns Configured OpenAI Transcription adapter instance + * @throws Error if OPENAI_API_KEY is not found in environment + * + * @example + * ```typescript + * // Automatically uses OPENAI_API_KEY from environment + * const adapter = openaiTranscription(); + * + * const result = await ai({ + * adapter, + * model: 'whisper-1', + * audio: audioFile + * }); + * + * console.log(result.text) + * ``` + */ +export function openaiTranscription( + config?: Omit, +): OpenAITranscriptionAdapter { + const apiKey = getOpenAIApiKeyFromEnv() + return createOpenaiTranscription(apiKey, config) +} diff --git a/packages/typescript/ai-openai/src/adapters/tts.ts b/packages/typescript/ai-openai/src/adapters/tts.ts new file mode 100644 index 00000000..1e2a0df4 --- /dev/null +++ b/packages/typescript/ai-openai/src/adapters/tts.ts @@ -0,0 +1,169 @@ +import { BaseTTSAdapter } from '@tanstack/ai/adapters' +import { OPENAI_TTS_MODELS } from '../model-meta' +import { + createOpenAIClient, + generateId, + getOpenAIApiKeyFromEnv, +} from '../utils' +import { + validateAudioInput, + validateInstructions, + validateSpeed, +} from '../audio/audio-provider-options' +import type { + OpenAITTSFormat, + OpenAITTSProviderOptions, + OpenAITTSVoice, +} from '../audio/tts-provider-options' +import type { TTSOptions, TTSResult } from '@tanstack/ai' +import type OpenAI_SDK from 'openai' +import type { OpenAIClientConfig } from '../utils' + +/** + * Configuration for OpenAI TTS adapter + */ +export interface OpenAITTSConfig extends OpenAIClientConfig {} + +/** + * OpenAI Text-to-Speech Adapter + * + * Tree-shakeable adapter for OpenAI TTS functionality. + * Supports tts-1, tts-1-hd, and gpt-4o-audio-preview models. + * + * Features: + * - Multiple voice options: alloy, ash, ballad, coral, echo, fable, onyx, nova, sage, shimmer, verse + * - Multiple output formats: mp3, opus, aac, flac, wav, pcm + * - Speed control (0.25 to 4.0) + */ +export class OpenAITTSAdapter extends BaseTTSAdapter< + typeof OPENAI_TTS_MODELS, + OpenAITTSProviderOptions +> { + readonly name = 'openai' as const + readonly models = OPENAI_TTS_MODELS + + private client: OpenAI_SDK + + constructor(config: OpenAITTSConfig) { + super(config) + this.client = createOpenAIClient(config) + } + + async generateSpeech( + options: TTSOptions, + ): Promise { + const { model, text, voice, format, speed, providerOptions } = options + + // Validate inputs using existing validators + const audioOptions = { + input: text, + model, + voice: voice as OpenAITTSVoice, + speed, + response_format: format as OpenAITTSFormat, + ...providerOptions, + } + + validateAudioInput(audioOptions) + validateSpeed(audioOptions) + validateInstructions(audioOptions) + + // Build request + const request: OpenAI_SDK.Audio.SpeechCreateParams = { + model, + input: text, + voice: voice || 'alloy', + response_format: format, + speed, + ...providerOptions, + } + + // Call OpenAI API + const response = await this.client.audio.speech.create(request) + + // Convert response to base64 + const arrayBuffer = await response.arrayBuffer() + const base64 = Buffer.from(arrayBuffer).toString('base64') + + const outputFormat = format || 'mp3' + const contentType = this.getContentType(outputFormat) + + return { + id: generateId(this.name), + model, + audio: base64, + format: outputFormat, + contentType, + } + } + + private getContentType(format: string): string { + const contentTypes: Record = { + mp3: 'audio/mpeg', + opus: 'audio/opus', + aac: 'audio/aac', + flac: 'audio/flac', + wav: 'audio/wav', + pcm: 'audio/pcm', + } + return contentTypes[format] || 'audio/mpeg' + } +} + +/** + * Creates an OpenAI TTS adapter with explicit API key + * + * @param apiKey - Your OpenAI API key + * @param config - Optional additional configuration + * @returns Configured OpenAI TTS adapter instance + * + * @example + * ```typescript + * const adapter = createOpenaiTTS("sk-..."); + * + * const result = await ai({ + * adapter, + * model: 'tts-1-hd', + * text: 'Hello, world!', + * voice: 'nova' + * }); + * ``` + */ +export function createOpenaiTTS( + apiKey: string, + config?: Omit, +): OpenAITTSAdapter { + return new OpenAITTSAdapter({ apiKey, ...config }) +} + +/** + * Creates an OpenAI TTS adapter with automatic API key detection from environment variables. + * + * Looks for `OPENAI_API_KEY` in: + * - `process.env` (Node.js) + * - `window.env` (Browser with injected env) + * + * @param config - Optional configuration (excluding apiKey which is auto-detected) + * @returns Configured OpenAI TTS adapter instance + * @throws Error if OPENAI_API_KEY is not found in environment + * + * @example + * ```typescript + * // Automatically uses OPENAI_API_KEY from environment + * const adapter = openaiTTS(); + * + * const result = await ai({ + * adapter, + * model: 'tts-1', + * text: 'Welcome to TanStack AI!', + * voice: 'alloy', + * format: 'mp3' + * }); + * ``` + */ +export function openaiTTS( + config?: Omit, +): OpenAITTSAdapter { + const apiKey = getOpenAIApiKeyFromEnv() + return createOpenaiTTS(apiKey, config) +} diff --git a/packages/typescript/ai-openai/src/adapters/video.ts b/packages/typescript/ai-openai/src/adapters/video.ts new file mode 100644 index 00000000..661e96d0 --- /dev/null +++ b/packages/typescript/ai-openai/src/adapters/video.ts @@ -0,0 +1,400 @@ +import { BaseVideoAdapter } from '@tanstack/ai/adapters' +import { OPENAI_VIDEO_MODELS } from '../model-meta' +import { createOpenAIClient, getOpenAIApiKeyFromEnv } from '../utils' +import { + toApiSeconds, + validateVideoSeconds, + validateVideoSize, +} from '../video/video-provider-options' +import type { + OpenAIVideoModelProviderOptionsByName, + OpenAIVideoProviderOptions, +} from '../video/video-provider-options' +import type { + VideoGenerationOptions, + VideoJobResult, + VideoStatusResult, + VideoUrlResult, +} from '@tanstack/ai' +import type OpenAI_SDK from 'openai' +import type { OpenAIClientConfig } from '../utils' + +/** + * Configuration for OpenAI video adapter. + * + * @experimental Video generation is an experimental feature and may change. + */ +export interface OpenAIVideoConfig extends OpenAIClientConfig {} + +/** + * OpenAI Video Generation Adapter + * + * Tree-shakeable adapter for OpenAI video generation functionality using Sora-2. + * Uses a jobs/polling architecture for async video generation. + * + * @experimental Video generation is an experimental feature and may change. + * + * Features: + * - Async job-based video generation + * - Status polling for job progress + * - URL retrieval for completed videos + * - Model-specific type-safe provider options + */ +export class OpenAIVideoAdapter extends BaseVideoAdapter< + typeof OPENAI_VIDEO_MODELS, + OpenAIVideoProviderOptions +> { + readonly name = 'openai' as const + readonly models = OPENAI_VIDEO_MODELS + + // Type-only properties for type inference + declare _modelProviderOptionsByName?: OpenAIVideoModelProviderOptionsByName + + private client: OpenAI_SDK + + constructor(config: OpenAIVideoConfig) { + super(config) + this.client = createOpenAIClient(config) + } + + /** + * Create a new video generation job. + * + * API: POST /v1/videos + * Docs: https://platform.openai.com/docs/api-reference/videos/create + * + * @experimental Video generation is an experimental feature and may change. + * + * @example + * ```ts + * const { jobId } = await adapter.createVideoJob({ + * model: 'sora-2', + * prompt: 'A cat chasing a dog in a sunny park', + * size: '1280x720', + * duration: 8 // seconds: 4, 8, or 12 + * }) + * ``` + */ + async createVideoJob( + options: VideoGenerationOptions, + ): Promise { + const { model, size, duration, providerOptions } = options + + // Validate inputs + validateVideoSize(model, size) + // Duration maps to 'seconds' in the API + const seconds = duration ?? providerOptions?.seconds + validateVideoSeconds(model, seconds) + + // Build request + const request = this.buildRequest(options) + + try { + // POST /v1/videos + // Cast to any because the videos API may not be in SDK types yet + const client = this.client as any + const response = await client.videos.create(request) + + return { + jobId: response.id, + model, + } + } catch (error: any) { + // Fallback for when the videos API is not available + if (error.message?.includes('videos') || error.code === 'invalid_api') { + throw new Error( + `Video generation API is not available. The Sora API may require special access. ` + + `Original error: ${error.message}`, + ) + } + throw error + } + } + + /** + * Get the current status of a video generation job. + * + * API: GET /v1/videos/{video_id} + * Docs: https://platform.openai.com/docs/api-reference/videos/get + * + * @experimental Video generation is an experimental feature and may change. + * + * @example + * ```ts + * const status = await adapter.getVideoStatus(jobId) + * if (status.status === 'completed') { + * console.log('Video is ready!') + * } else if (status.status === 'processing') { + * console.log(`Progress: ${status.progress}%`) + * } + * ``` + */ + async getVideoStatus(jobId: string): Promise { + try { + // GET /v1/videos/{video_id} + const client = this.client as any + const response = await client.videos.retrieve(jobId) + + return { + jobId, + status: this.mapStatus(response.status), + progress: response.progress, + error: response.error?.message, + } + } catch (error: any) { + if (error.status === 404) { + return { + jobId, + status: 'failed', + error: 'Job not found', + } + } + throw error + } + } + + /** + * Get the URL to download/view the generated video. + * + * API: GET /v1/videos/{video_id}/content + * Docs: https://platform.openai.com/docs/api-reference/videos/content + * + * @experimental Video generation is an experimental feature and may change. + * + * @example + * ```ts + * const { url, expiresAt } = await adapter.getVideoUrl(jobId) + * console.log('Video URL:', url) + * console.log('Expires at:', expiresAt) + * ``` + */ + async getVideoUrl(jobId: string): Promise { + try { + // GET /v1/videos/{video_id}/content + // The SDK may not have a .content() method, so we try multiple approaches + const client = this.client as any + + let response: any + + // Try different possible method names + if (typeof client.videos?.content === 'function') { + response = await client.videos.content(jobId) + } else if (typeof client.videos?.getContent === 'function') { + response = await client.videos.getContent(jobId) + } else if (typeof client.videos?.download === 'function') { + response = await client.videos.download(jobId) + } else { + // Fallback: check if retrieve returns the URL directly + const videoInfo = await client.videos.retrieve(jobId) + if (videoInfo.url) { + return { + jobId, + url: videoInfo.url, + expiresAt: videoInfo.expires_at + ? new Date(videoInfo.expires_at) + : undefined, + } + } + + // Last resort: The /content endpoint returns raw binary video data, not JSON. + // We need to construct a URL that the client can use to fetch the video. + // The URL needs to include auth, so we'll create a signed URL or return + // a proxy endpoint. + + // For now, return a URL that goes through our API to proxy the request + // since the raw endpoint requires auth headers that browsers can't send. + // The video element can't add Authorization headers, so we need a workaround. + + // Option 1: Return the direct URL (only works if OpenAI supports query param auth) + // Option 2: Return a blob URL after fetching (memory intensive) + // Option 3: Return a proxy URL through our server + + // Let's try fetching and returning a data URL for now + const baseUrl = this.config.baseUrl || 'https://api.openai.com/v1' + const apiKey = this.config.apiKey + + const contentResponse = await fetch( + `${baseUrl}/videos/${jobId}/content`, + { + method: 'GET', + headers: { + Authorization: `Bearer ${apiKey}`, + }, + }, + ) + + if (!contentResponse.ok) { + // Try to parse error as JSON, but it might be binary + const contentType = contentResponse.headers.get('content-type') + if (contentType?.includes('application/json')) { + const errorData = await contentResponse.json().catch(() => ({})) + throw new Error( + errorData.error?.message || + `Failed to get video content: ${contentResponse.status}`, + ) + } + throw new Error( + `Failed to get video content: ${contentResponse.status}`, + ) + } + + // The response is the raw video file - convert to base64 data URL + const videoBlob = await contentResponse.blob() + const buffer = await videoBlob.arrayBuffer() + const base64 = Buffer.from(buffer).toString('base64') + const mimeType = + contentResponse.headers.get('content-type') || 'video/mp4' + + return { + jobId, + url: `data:${mimeType};base64,${base64}`, + expiresAt: undefined, // Data URLs don't expire + } + } + + return { + jobId, + url: response.url, + expiresAt: response.expires_at + ? new Date(response.expires_at) + : undefined, + } + } catch (error: any) { + if (error.status === 404) { + throw new Error(`Video job not found: ${jobId}`) + } + if (error.status === 400) { + throw new Error( + `Video is not ready for download. Check status first. Job ID: ${jobId}`, + ) + } + throw error + } + } + + private buildRequest( + options: VideoGenerationOptions, + ): Record { + const { model, prompt, size, duration, providerOptions } = options + + const request: Record = { + model, + prompt, + } + + // Add size/resolution + // Supported: '1280x720', '720x1280', '1792x1024', '1024x1792' + if (size) { + request.size = size + } else if (providerOptions?.size) { + request.size = providerOptions.size + } + + // Add seconds (duration) + // Supported: '4', '8', or '12' - yes, the API wants strings + const seconds = duration ?? providerOptions?.seconds + if (seconds !== undefined) { + request.seconds = toApiSeconds(seconds) + } + + return request + } + + private mapStatus( + apiStatus: string, + ): 'pending' | 'processing' | 'completed' | 'failed' { + switch (apiStatus) { + case 'queued': + case 'pending': + return 'pending' + case 'processing': + case 'in_progress': + return 'processing' + case 'completed': + case 'succeeded': + return 'completed' + case 'failed': + case 'error': + case 'cancelled': + return 'failed' + default: + return 'processing' + } + } +} + +/** + * Creates an OpenAI video adapter with an explicit API key. + * + * @experimental Video generation is an experimental feature and may change. + * + * @param apiKey - Your OpenAI API key + * @param config - Optional additional configuration + * @returns Configured OpenAI video adapter instance + * + * @example + * ```typescript + * const adapter = createOpenaiVideo('your-api-key'); + * + * const { jobId } = await ai({ + * adapter, + * model: 'sora-2', + * prompt: 'A beautiful sunset over the ocean' + * }); + * ``` + */ +export function createOpenaiVideo( + apiKey: string, + config?: Omit, +): OpenAIVideoAdapter { + return new OpenAIVideoAdapter({ apiKey, ...config }) +} + +/** + * Creates an OpenAI video adapter with automatic API key detection from environment variables. + * + * Looks for `OPENAI_API_KEY` in: + * - `process.env` (Node.js) + * - `window.env` (Browser with injected env) + * + * @experimental Video generation is an experimental feature and may change. + * + * @param config - Optional configuration (excluding apiKey which is auto-detected) + * @returns Configured OpenAI video adapter instance + * @throws Error if OPENAI_API_KEY is not found in environment + * + * @example + * ```typescript + * // Automatically uses OPENAI_API_KEY from environment + * const adapter = openaiVideo(); + * + * // Create a video generation job + * const { jobId } = await ai({ + * adapter, + * model: 'sora-2', + * prompt: 'A cat playing piano' + * }); + * + * // Poll for status + * const status = await ai({ + * adapter, + * model: 'sora-2', + * jobId, + * request: 'status' + * }); + * + * // Get video URL when complete + * const { url } = await ai({ + * adapter, + * model: 'sora-2', + * jobId, + * request: 'url' + * }); + * ``` + */ +export function openaiVideo( + config?: Omit, +): OpenAIVideoAdapter { + const apiKey = getOpenAIApiKeyFromEnv() + return createOpenaiVideo(apiKey, config) +} diff --git a/packages/typescript/ai-openai/src/audio/transcription-provider-options.ts b/packages/typescript/ai-openai/src/audio/transcription-provider-options.ts new file mode 100644 index 00000000..4dfe5ffb --- /dev/null +++ b/packages/typescript/ai-openai/src/audio/transcription-provider-options.ts @@ -0,0 +1,18 @@ +/** + * Provider-specific options for OpenAI Transcription + */ +export interface OpenAITranscriptionProviderOptions { + /** + * The sampling temperature, between 0 and 1. + * Higher values like 0.8 will make the output more random, + * while lower values like 0.2 will make it more focused and deterministic. + */ + temperature?: number + + /** + * The timestamp granularities to populate for this transcription. + * response_format must be set to verbose_json to use timestamp granularities. + * Either or both of these options are supported: word, or segment. + */ + timestamp_granularities?: Array<'word' | 'segment'> +} diff --git a/packages/typescript/ai-openai/src/audio/tts-provider-options.ts b/packages/typescript/ai-openai/src/audio/tts-provider-options.ts new file mode 100644 index 00000000..4368caed --- /dev/null +++ b/packages/typescript/ai-openai/src/audio/tts-provider-options.ts @@ -0,0 +1,31 @@ +/** + * OpenAI TTS voice options + */ +export type OpenAITTSVoice = + | 'alloy' + | 'ash' + | 'ballad' + | 'coral' + | 'echo' + | 'fable' + | 'onyx' + | 'nova' + | 'sage' + | 'shimmer' + | 'verse' + +/** + * OpenAI TTS output format options + */ +export type OpenAITTSFormat = 'mp3' | 'opus' | 'aac' | 'flac' | 'wav' | 'pcm' + +/** + * Provider-specific options for OpenAI TTS + */ +export interface OpenAITTSProviderOptions { + /** + * Control the voice of your generated audio with additional instructions. + * Does not work with tts-1 or tts-1-hd. + */ + instructions?: string +} diff --git a/packages/typescript/ai-openai/src/image/image-provider-options.ts b/packages/typescript/ai-openai/src/image/image-provider-options.ts index 49e9fcc1..a16281cf 100644 --- a/packages/typescript/ai-openai/src/image/image-provider-options.ts +++ b/packages/typescript/ai-openai/src/image/image-provider-options.ts @@ -1,23 +1,269 @@ -interface ImageProviderOptions { +/** + * OpenAI Image Generation Provider Options + * + * These are provider-specific options for OpenAI image generation. + * Common options like prompt, numberOfImages, and size are handled + * in the base ImageGenerationOptions. + */ + +/** + * Quality options for gpt-image-1 and gpt-image-1-mini models + */ +export type GptImageQuality = 'high' | 'medium' | 'low' | 'auto' + +/** + * Quality options for dall-e-3 model + */ +export type DallE3Quality = 'hd' | 'standard' + +/** + * Quality options for dall-e-2 model (only standard is supported) + */ +export type DallE2Quality = 'standard' + +/** + * Style options for dall-e-3 model + */ +export type DallE3Style = 'vivid' | 'natural' + +/** + * Output format options for gpt-image-1 models + */ +export type GptImageOutputFormat = 'png' | 'jpeg' | 'webp' + +/** + * Response format options for dall-e models + */ +export type DallEResponseFormat = 'url' | 'b64_json' + +/** + * Background options for gpt-image-1 models + */ +export type GptImageBackground = 'transparent' | 'opaque' | 'auto' + +/** + * Moderation level for gpt-image-1 models + */ +export type GptImageModeration = 'low' | 'auto' + +/** + * Supported sizes for gpt-image-1 models + */ +export type GptImageSize = '1024x1024' | '1536x1024' | '1024x1536' | 'auto' + +/** + * Supported sizes for dall-e-3 model + */ +export type DallE3Size = '1024x1024' | '1792x1024' | '1024x1792' + +/** + * Supported sizes for dall-e-2 model + */ +export type DallE2Size = '256x256' | '512x512' | '1024x1024' + +/** + * Base provider options shared across all OpenAI image models + */ +export interface OpenAIImageBaseProviderOptions { /** - * A text prompt describing the desired image. The maximum length is 32000 characters for gpt-image-1, 1000 characters for dall-e-2 and 4000 characters for dall-e-3. + * A unique identifier representing your end-user. + * Can help OpenAI to monitor and detect abuse. */ - prompt: string + user?: string +} + +/** + * Provider options for gpt-image-1 model + * Field names match the OpenAI API for direct spreading + */ +export interface GptImage1ProviderOptions extends OpenAIImageBaseProviderOptions { /** - * Allows to set transparency for the background of the generated image(s). This parameter is only supported for gpt-image-1. Must be one of transparent, opaque or auto (default value). When auto is used, the model will automatically determine the best background for the image. + * The quality of the image. + * @default 'auto' + */ + quality?: GptImageQuality -If transparent, the output format needs to support transparency, so it should be set to either png (default value) or webp. + /** + * Background transparency setting. + * When 'transparent', output format must be 'png' or 'webp'. + * @default 'auto' */ - background?: 'transparent' | 'opaque' | 'auto' | null + background?: GptImageBackground + /** - * The image model to use for generation. + * Output image format. + * @default 'png' */ + output_format?: GptImageOutputFormat + + /** + * Compression level (0-100%) for webp/jpeg formats. + * @default 100 + */ + output_compression?: number + + /** + * Content moderation level. + * @default 'auto' + */ + moderation?: GptImageModeration + + /** + * Number of partial images to generate during streaming (0-3). + * Only used when stream: true. + * @default 0 + */ + partial_images?: number +} + +/** + * Provider options for gpt-image-1-mini model + * Same as gpt-image-1 + */ +export type GptImage1MiniProviderOptions = GptImage1ProviderOptions + +/** + * Provider options for dall-e-3 model + * Field names match the OpenAI API for direct spreading + */ +export interface DallE3ProviderOptions extends OpenAIImageBaseProviderOptions { + /** + * The quality of the image. + * @default 'standard' + */ + quality?: DallE3Quality + + /** + * The style of the generated images. + * 'vivid' causes the model to lean towards generating hyper-real and dramatic images. + * 'natural' causes the model to produce more natural, less hyper-real looking images. + * @default 'vivid' + */ + style?: DallE3Style + + /** + * The format in which generated images are returned. + * URLs are only valid for 60 minutes after generation. + * @default 'url' + */ + response_format?: DallEResponseFormat +} + +/** + * Provider options for dall-e-2 model + * Field names match the OpenAI API for direct spreading + */ +export interface DallE2ProviderOptions extends OpenAIImageBaseProviderOptions { + /** + * The quality of the image (only 'standard' is supported). + */ + quality?: DallE2Quality + + /** + * The format in which generated images are returned. + * URLs are only valid for 60 minutes after generation. + * @default 'url' + */ + response_format?: DallEResponseFormat +} + +/** + * Union of all OpenAI image provider options + */ +export type OpenAIImageProviderOptions = + | GptImage1ProviderOptions + | GptImage1MiniProviderOptions + | DallE3ProviderOptions + | DallE2ProviderOptions + +/** + * Type-only map from model name to its specific provider options. + * Used by the core AI types to narrow providerOptions based on the selected model. + */ +export type OpenAIImageModelProviderOptionsByName = { + 'gpt-image-1': GptImage1ProviderOptions + 'gpt-image-1-mini': GptImage1MiniProviderOptions + 'dall-e-3': DallE3ProviderOptions + 'dall-e-2': DallE2ProviderOptions +} + +/** + * Type-only map from model name to its supported sizes. + */ +export type OpenAIImageModelSizeByName = { + 'gpt-image-1': GptImageSize + 'gpt-image-1-mini': GptImageSize + 'dall-e-3': DallE3Size + 'dall-e-2': DallE2Size +} + +/** + * Internal options interface for validation + */ +interface ImageValidationOptions { + prompt: string model: string + background?: 'transparent' | 'opaque' | 'auto' | null +} + +/** + * Validates that the provided size is supported by the model. + * Throws a descriptive error if the size is not supported. + */ +export function validateImageSize( + model: string, + size: string | undefined, +): void { + if (!size || size === 'auto') return + + const validSizes: Record> = { + 'gpt-image-1': ['1024x1024', '1536x1024', '1024x1536', 'auto'], + 'gpt-image-1-mini': ['1024x1024', '1536x1024', '1024x1536', 'auto'], + 'dall-e-3': ['1024x1024', '1792x1024', '1024x1792'], + 'dall-e-2': ['256x256', '512x512', '1024x1024'], + } + + const modelSizes = validSizes[model] + if (!modelSizes) { + throw new Error(`Unknown image model: ${model}`) + } + + if (!modelSizes.includes(size)) { + throw new Error( + `Size "${size}" is not supported by model "${model}". ` + + `Supported sizes: ${modelSizes.join(', ')}`, + ) + } +} + +/** + * Validates that the number of images is within bounds for the model. + */ +export function validateNumberOfImages( + model: string, + numberOfImages: number | undefined, +): void { + if (numberOfImages === undefined) return + + // dall-e-3 only supports n=1 + if (model === 'dall-e-3' && numberOfImages !== 1) { + throw new Error( + `Model "dall-e-3" only supports generating 1 image at a time. ` + + `Requested: ${numberOfImages}`, + ) + } + + // Other models support 1-10 + if (numberOfImages < 1 || numberOfImages > 10) { + throw new Error( + `Number of images must be between 1 and 10. Requested: ${numberOfImages}`, + ) + } } -export const validateBackground = (options: ImageProviderOptions) => { +export const validateBackground = (options: ImageValidationOptions) => { if (options.background) { - const supportedModels = ['gpt-image-1'] + const supportedModels = ['gpt-image-1', 'gpt-image-1-mini'] if (!supportedModels.includes(options.model)) { throw new Error( `The model ${options.model} does not support background option.`, @@ -26,13 +272,16 @@ export const validateBackground = (options: ImageProviderOptions) => { } } -export const validatePrompt = (options: ImageProviderOptions) => { +export const validatePrompt = (options: ImageValidationOptions) => { if (options.prompt.length === 0) { throw new Error('Prompt cannot be empty.') } - if (options.model === 'gpt-image-1' && options.prompt.length > 32000) { + if ( + (options.model === 'gpt-image-1' || options.model === 'gpt-image-1-mini') && + options.prompt.length > 32000 + ) { throw new Error( - 'For gpt-image-1, prompt length must be less than or equal to 32000 characters.', + 'For gpt-image-1/gpt-image-1-mini, prompt length must be less than or equal to 32000 characters.', ) } if (options.model === 'dall-e-2' && options.prompt.length > 1000) { diff --git a/packages/typescript/ai-openai/src/index.ts b/packages/typescript/ai-openai/src/index.ts index df504301..2e0cf5d3 100644 --- a/packages/typescript/ai-openai/src/index.ts +++ b/packages/typescript/ai-openai/src/index.ts @@ -1,13 +1,114 @@ +// ============================================================================ +// New Tree-Shakeable Adapters (Recommended) +// ============================================================================ + +// Text (Chat) adapter - for chat/text completion +export { + OpenAITextAdapter, + createOpenaiText, + openaiText, + type OpenAITextConfig, + type OpenAITextProviderOptions, +} from './adapters/text' + +// Embedding adapter - for text embeddings +export { + OpenAIEmbedAdapter, + createOpenaiEmbed, + openaiEmbed, + type OpenAIEmbedConfig, + type OpenAIEmbedProviderOptions, +} from './adapters/embed' + +// Summarize adapter - for text summarization +export { + OpenAISummarizeAdapter, + createOpenaiSummarize, + openaiSummarize, + type OpenAISummarizeConfig, + type OpenAISummarizeProviderOptions, +} from './adapters/summarize' + +// Image adapter - for image generation +export { + OpenAIImageAdapter, + createOpenaiImage, + openaiImage, + type OpenAIImageConfig, +} from './adapters/image' +export type { + OpenAIImageProviderOptions, + OpenAIImageModelProviderOptionsByName, +} from './image/image-provider-options' + +// Video adapter - for video generation (experimental) +/** + * @experimental Video generation is an experimental feature and may change. + */ +export { + OpenAIVideoAdapter, + createOpenaiVideo, + openaiVideo, + type OpenAIVideoConfig, +} from './adapters/video' +export type { + OpenAIVideoProviderOptions, + OpenAIVideoModelProviderOptionsByName, + OpenAIVideoSize, + OpenAIVideoDuration, +} from './video/video-provider-options' + +// TTS adapter - for text-to-speech +export { + OpenAITTSAdapter, + createOpenaiTTS, + openaiTTS, + type OpenAITTSConfig, +} from './adapters/tts' +export type { + OpenAITTSProviderOptions, + OpenAITTSVoice, + OpenAITTSFormat, +} from './audio/tts-provider-options' + +// Transcription adapter - for speech-to-text +export { + OpenAITranscriptionAdapter, + createOpenaiTranscription, + openaiTranscription, + type OpenAITranscriptionConfig, +} from './adapters/transcription' +export type { OpenAITranscriptionProviderOptions } from './audio/transcription-provider-options' + +// ============================================================================ +// Legacy Exports (Deprecated - will be removed in future versions) +// ============================================================================ + +/** + * @deprecated Use `openaiText()`, `openaiEmbed()`, or `openaiSummarize()` instead. + * This monolithic adapter will be removed in a future version. + */ export { OpenAI, createOpenAI, openai, type OpenAIConfig, } from './openai-adapter' + +// ============================================================================ +// Type Exports +// ============================================================================ + export type { OpenAIChatModelProviderOptionsByName, OpenAIModelInputModalitiesByName, } from './model-meta' +export { + OPENAI_IMAGE_MODELS, + OPENAI_TTS_MODELS, + OPENAI_TRANSCRIPTION_MODELS, + OPENAI_VIDEO_MODELS, +} from './model-meta' export type { OpenAITextMetadata, OpenAIImageMetadata, diff --git a/packages/typescript/ai-openai/src/model-meta.ts b/packages/typescript/ai-openai/src/model-meta.ts index 024a6c1a..69ae9cba 100644 --- a/packages/typescript/ai-openai/src/model-meta.ts +++ b/packages/typescript/ai-openai/src/model-meta.ts @@ -300,7 +300,11 @@ const GPT5_CODEX = { OpenAIMetadataOptions > -/* const SORA2 = { +/** + * Sora-2 video generation model. + * @experimental Video generation is an experimental feature and may change. + */ +const SORA2 = { name: 'sora-2', pricing: { input: { @@ -321,6 +325,10 @@ const GPT5_CODEX = { OpenAIBaseOptions & OpenAIStreamingOptions & OpenAIMetadataOptions > +/** + * Sora-2-Pro video generation model (higher quality). + * @experimental Video generation is an experimental feature and may change. + */ const SORA2_PRO = { name: 'sora-2-pro', pricing: { @@ -386,7 +394,7 @@ const GPT_IMAGE_1_MINI = { }, } as const satisfies ModelMeta< OpenAIBaseOptions & OpenAIStreamingOptions & OpenAIMetadataOptions -> */ +> const O3_DEEP_RESEARCH = { name: 'o3-deep-research', @@ -1246,7 +1254,7 @@ const CODEX_MINI_LATEST = { OpenAIStreamingOptions & OpenAIMetadataOptions > -/* + const DALL_E_2 = { name: 'dall-e-2', pricing: { @@ -1287,7 +1295,7 @@ const DALL_E_3 = { }, } as const satisfies ModelMeta< OpenAIBaseOptions & OpenAIStreamingOptions & OpenAIMetadataOptions -> */ +> const GPT_3_5_TURBO = { name: 'gpt-3.5-turbo', @@ -1653,12 +1661,12 @@ export const OPENAI_CHAT_MODELS = [ ] as const // Image generation models (based on endpoints: "image-generation" or "image-edit") -/* const OPENAI_IMAGE_MODELS = [ +export const OPENAI_IMAGE_MODELS = [ GPT_IMAGE_1.name, GPT_IMAGE_1_MINI.name, DALL_E_3.name, DALL_E_2.name, -] as const */ +] as const // Embedding models (based on endpoints: "embedding") export const OPENAI_EMBEDDING_MODELS = [ @@ -1691,9 +1699,30 @@ export const OPENAI_EMBEDDING_MODELS = [ GPT_4O_MINI_TRANSCRIBE.name, ] as const -// Video generation models (based on endpoints: "video") -const OPENAI_VIDEO_MODELS = [SORA2.name, SORA2_PRO.name] as const +/** + * Video generation models (based on endpoints: "video") + * @experimental Video generation is an experimental feature and may change. + */ +export const OPENAI_VIDEO_MODELS = [SORA2.name, SORA2_PRO.name] as const + +/** + * Text-to-speech models (based on endpoints: "speech_generation") + */ +export const OPENAI_TTS_MODELS = [ + 'tts-1', + 'tts-1-hd', + 'gpt-4o-audio-preview', +] as const + +/** + * Transcription models (based on endpoints: "transcription") */ +export const OPENAI_TRANSCRIPTION_MODELS = [ + 'whisper-1', + 'gpt-4o-transcribe', + 'gpt-4o-mini-transcribe', + 'gpt-4o-transcribe-diarize', +] as const // const OPENAI_MODERATION_MODELS = [OMNI_MODERATION.name] as const // export type OpenAIChatModel = (typeof OPENAI_CHAT_MODELS)[number] diff --git a/packages/typescript/ai-openai/src/openai-adapter.ts b/packages/typescript/ai-openai/src/openai-adapter.ts index 676d5257..c281c012 100644 --- a/packages/typescript/ai-openai/src/openai-adapter.ts +++ b/packages/typescript/ai-openai/src/openai-adapter.ts @@ -5,7 +5,6 @@ import { validateTextProviderOptions } from './text/text-provider-options' import { convertToolsToProviderFormat } from './tools' import type { Responses } from 'openai/resources' import type { - ChatOptions, ContentPart, EmbeddingOptions, EmbeddingResult, @@ -13,6 +12,7 @@ import type { StreamChunk, SummarizationOptions, SummarizationResult, + TextOptions, } from '@tanstack/ai' import type { OpenAIChatModelProviderOptionsByName, @@ -88,13 +88,13 @@ export class OpenAI extends BaseAdapter< } async *chatStream( - options: ChatOptions, + options: TextOptions, ): AsyncIterable { // Track tool call metadata by unique ID // OpenAI streams tool calls with deltas - first chunk has ID/name, subsequent chunks only have args // We assign our own indices as we encounter unique tool call IDs const toolCallMetadata = new Map() - const requestArguments = this.mapChatOptionsToOpenAI(options) + const requestArguments = this.mapTextOptionsToOpenAI(options) try { const response = await this.client.responses.create( @@ -199,7 +199,7 @@ export class OpenAI extends BaseAdapter< private async *processOpenAIStreamChunks( stream: AsyncIterable, toolCallMetadata: Map, - options: ChatOptions, + options: TextOptions, generateId: () => string, ): AsyncIterable { let accumulatedContent = '' @@ -491,7 +491,7 @@ export class OpenAI extends BaseAdapter< * Maps common options to OpenAI-specific format * Handles translation of normalized options to OpenAI's API format */ - private mapChatOptionsToOpenAI(options: ChatOptions) { + private mapTextOptionsToOpenAI(options: TextOptions) { const providerOptions = options.providerOptions as | Omit< InternalTextProviderOptions, @@ -505,7 +505,11 @@ export class OpenAI extends BaseAdapter< | undefined const input = this.convertMessagesToInput(options.messages) if (providerOptions) { - validateTextProviderOptions({ ...providerOptions, input }) + validateTextProviderOptions({ + ...providerOptions, + input, + model: options.model, + }) } const tools = options.tools diff --git a/packages/typescript/ai-openai/src/tools/function-tool.ts b/packages/typescript/ai-openai/src/tools/function-tool.ts index 60737b4c..efebdb87 100644 --- a/packages/typescript/ai-openai/src/tools/function-tool.ts +++ b/packages/typescript/ai-openai/src/tools/function-tool.ts @@ -1,38 +1,35 @@ -import { convertZodToJsonSchema } from '@tanstack/ai' +import { convertZodToOpenAISchema } from '../utils/schema-converter' import type { Tool } from '@tanstack/ai' import type OpenAI from 'openai' export type FunctionTool = OpenAI.Responses.FunctionTool /** - * Converts a standard Tool to OpenAI FunctionTool format + * Converts a standard Tool to OpenAI FunctionTool format. + * + * Uses the OpenAI-specific schema converter which applies strict mode transformations: + * - All properties in required array + * - Optional fields made nullable + * - additionalProperties: false + * + * This enables strict mode for all tools automatically. */ export function convertFunctionToolToAdapterFormat(tool: Tool): FunctionTool { - // Convert Zod schema to JSON Schema + // Convert Zod schema to OpenAI-compatible JSON Schema (with strict mode transformations) const jsonSchema = tool.inputSchema - ? convertZodToJsonSchema(tool.inputSchema) - : undefined - - // Determine if we can use strict mode - // Strict mode requires all properties to be in the required array - const properties = jsonSchema?.properties || {} - const required = jsonSchema?.required || [] - const propertyNames = Object.keys(properties) - - // Only enable strict mode if all properties are required - // This ensures compatibility with tools that have optional parameters - const canUseStrict = - propertyNames.length > 0 && - propertyNames.every((prop: string) => required.includes(prop)) + ? convertZodToOpenAISchema(tool.inputSchema) + : { + type: 'object', + properties: {}, + required: [], + additionalProperties: false, + } return { type: 'function', name: tool.name, description: tool.description, - parameters: { - ...jsonSchema, - additionalProperties: false, - }, - strict: canUseStrict, + parameters: jsonSchema, + strict: true, // Always use strict mode since our schema converter handles the requirements } satisfies FunctionTool } diff --git a/packages/typescript/ai-openai/src/utils/client.ts b/packages/typescript/ai-openai/src/utils/client.ts new file mode 100644 index 00000000..828541e2 --- /dev/null +++ b/packages/typescript/ai-openai/src/utils/client.ts @@ -0,0 +1,47 @@ +import OpenAI_SDK from 'openai' + +export interface OpenAIClientConfig { + apiKey: string + organization?: string + baseURL?: string +} + +/** + * Creates an OpenAI SDK client instance + */ +export function createOpenAIClient(config: OpenAIClientConfig): OpenAI_SDK { + return new OpenAI_SDK({ + apiKey: config.apiKey, + organization: config.organization, + baseURL: config.baseURL, + }) +} + +/** + * Gets OpenAI API key from environment variables + * @throws Error if OPENAI_API_KEY is not found + */ +export function getOpenAIApiKeyFromEnv(): string { + const env = + typeof globalThis !== 'undefined' && (globalThis as any).window?.env + ? (globalThis as any).window.env + : typeof process !== 'undefined' + ? process.env + : undefined + const key = env?.OPENAI_API_KEY + + if (!key) { + throw new Error( + 'OPENAI_API_KEY is required. Please set it in your environment variables or use the factory function with an explicit API key.', + ) + } + + return key +} + +/** + * Generates a unique ID with a prefix + */ +export function generateId(prefix: string): string { + return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(7)}` +} diff --git a/packages/typescript/ai-openai/src/utils/index.ts b/packages/typescript/ai-openai/src/utils/index.ts new file mode 100644 index 00000000..20475201 --- /dev/null +++ b/packages/typescript/ai-openai/src/utils/index.ts @@ -0,0 +1,10 @@ +export { + createOpenAIClient, + getOpenAIApiKeyFromEnv, + generateId, + type OpenAIClientConfig, +} from './client' +export { + convertZodToOpenAISchema, + transformNullsToUndefined, +} from './schema-converter' diff --git a/packages/typescript/ai-openai/src/utils/schema-converter.ts b/packages/typescript/ai-openai/src/utils/schema-converter.ts new file mode 100644 index 00000000..c207e3db --- /dev/null +++ b/packages/typescript/ai-openai/src/utils/schema-converter.ts @@ -0,0 +1,213 @@ +import { toJSONSchema } from 'zod' +import type { z } from 'zod' + +/** + * Check if a value is a Zod schema by looking for Zod-specific internals. + * Zod schemas have a `_zod` property that contains metadata. + */ +function isZodSchema(schema: unknown): schema is z.ZodType { + return ( + typeof schema === 'object' && + schema !== null && + '_zod' in schema && + typeof (schema as any)._zod === 'object' + ) +} + +/** + * Recursively transform null values to undefined in an object. + * + * This is needed because OpenAI's structured output requires all fields to be + * in the `required` array, with optional fields made nullable (type: ["string", "null"]). + * When OpenAI returns null for optional fields, we need to convert them back to + * undefined to match the original Zod schema expectations. + * + * @param obj - Object to transform + * @returns Object with nulls converted to undefined + */ +export function transformNullsToUndefined(obj: T): T { + if (obj === null) { + return undefined as unknown as T + } + + if (Array.isArray(obj)) { + return obj.map((item) => transformNullsToUndefined(item)) as unknown as T + } + + if (typeof obj === 'object') { + const result: Record = {} + for (const [key, value] of Object.entries(obj as Record)) { + const transformed = transformNullsToUndefined(value) + // Only include the key if the value is not undefined + // This makes { notes: null } become {} (field absent) instead of { notes: undefined } + if (transformed !== undefined) { + result[key] = transformed + } + } + return result as T + } + + return obj +} + +/** + * Transform a JSON schema to be compatible with OpenAI's structured output requirements. + * OpenAI requires: + * - All properties must be in the `required` array + * - Optional fields should have null added to their type union + * - additionalProperties must be false for objects + * + * @param schema - JSON schema to transform + * @param originalRequired - Original required array (to know which fields were optional) + * @returns Transformed schema compatible with OpenAI structured output + */ +function makeOpenAIStructuredOutputCompatible( + schema: Record, + originalRequired: Array = [], +): Record { + const result = { ...schema } + + // Handle object types + if (result.type === 'object' && result.properties) { + const properties = { ...result.properties } + const allPropertyNames = Object.keys(properties) + + // Transform each property + for (const propName of allPropertyNames) { + const prop = properties[propName] + const wasOptional = !originalRequired.includes(propName) + + // Recursively transform nested objects/arrays + if (prop.type === 'object' && prop.properties) { + properties[propName] = makeOpenAIStructuredOutputCompatible( + prop, + prop.required || [], + ) + } else if (prop.type === 'array' && prop.items) { + properties[propName] = { + ...prop, + items: makeOpenAIStructuredOutputCompatible( + prop.items, + prop.items.required || [], + ), + } + } else if (wasOptional) { + // Make optional fields nullable by adding null to the type + if (prop.type && !Array.isArray(prop.type)) { + properties[propName] = { + ...prop, + type: [prop.type, 'null'], + } + } else if (Array.isArray(prop.type) && !prop.type.includes('null')) { + properties[propName] = { + ...prop, + type: [...prop.type, 'null'], + } + } + } + } + + result.properties = properties + // ALL properties must be required for OpenAI structured output + result.required = allPropertyNames + // additionalProperties must be false + result.additionalProperties = false + } + + // Handle array types with object items + if (result.type === 'array' && result.items) { + result.items = makeOpenAIStructuredOutputCompatible( + result.items, + result.items.required || [], + ) + } + + return result +} + +/** + * Converts a Zod schema to JSON Schema format compatible with OpenAI's structured output. + * + * OpenAI's structured output has strict requirements: + * - All properties must be in the `required` array + * - Optional fields should have null added to their type union + * - additionalProperties must be false for all objects + * + * @param schema - Zod schema to convert + * @returns JSON Schema object compatible with OpenAI's structured output API + * + * @example + * ```typescript + * import { z } from 'zod'; + * + * const zodSchema = z.object({ + * location: z.string().describe('City name'), + * unit: z.enum(['celsius', 'fahrenheit']).optional() + * }); + * + * const jsonSchema = convertZodToOpenAISchema(zodSchema); + * // Returns: + * // { + * // type: 'object', + * // properties: { + * // location: { type: 'string', description: 'City name' }, + * // unit: { type: ['string', 'null'], enum: ['celsius', 'fahrenheit'] } + * // }, + * // required: ['location', 'unit'], + * // additionalProperties: false + * // } + * ``` + */ +export function convertZodToOpenAISchema( + schema: z.ZodType, +): Record { + if (!isZodSchema(schema)) { + throw new Error('Expected a Zod schema') + } + + // Use Zod's built-in toJSONSchema + const jsonSchema = toJSONSchema(schema, { + target: 'openapi-3.0', + reused: 'ref', + }) + + // Remove $schema property as it's not needed for LLM providers + let result = jsonSchema + if (typeof result === 'object' && '$schema' in result) { + const { $schema, ...rest } = result + result = rest + } + + // Ensure object schemas always have type: "object" + if (typeof result === 'object') { + const isZodObject = + typeof schema === 'object' && + 'def' in schema && + schema.def.type === 'object' + + if (isZodObject && !result.type) { + result.type = 'object' + } + + if (Object.keys(result).length === 0) { + result.type = 'object' + } + + if ('properties' in result && !result.type) { + result.type = 'object' + } + + if (result.type === 'object' && !('properties' in result)) { + result.properties = {} + } + + if (result.type === 'object' && !('required' in result)) { + result.required = [] + } + + // Apply OpenAI-specific transformations for structured output + result = makeOpenAIStructuredOutputCompatible(result, result.required || []) + } + + return result +} diff --git a/packages/typescript/ai-openai/src/video/video-provider-options.ts b/packages/typescript/ai-openai/src/video/video-provider-options.ts new file mode 100644 index 00000000..b0355128 --- /dev/null +++ b/packages/typescript/ai-openai/src/video/video-provider-options.ts @@ -0,0 +1,123 @@ +/** + * OpenAI Video Generation Provider Options + * + * Based on https://platform.openai.com/docs/api-reference/videos/create + * + * @experimental Video generation is an experimental feature and may change. + */ + +/** + * Supported video sizes for OpenAI Sora video generation. + * Based on the official API documentation. + * + * @experimental Video generation is an experimental feature and may change. + */ +export type OpenAIVideoSize = + | '1280x720' // 720p landscape (16:9) + | '720x1280' // 720p portrait (9:16) + | '1792x1024' // Landscape wide + | '1024x1792' // Portrait tall + +/** + * Supported video durations (in seconds) for OpenAI Sora video generation. + * The API uses the `seconds` parameter with STRING values '4', '8', or '12'. + * Yes, really. They're strings. + * + * @experimental Video generation is an experimental feature and may change. + */ +export type OpenAIVideoSeconds = '4' | '8' | '12' + +/** + * Provider-specific options for OpenAI video generation. + * + * @experimental Video generation is an experimental feature and may change. + */ +export interface OpenAIVideoProviderOptions { + /** + * Video size in WIDTHxHEIGHT format. + * Supported: '1280x720', '720x1280', '1792x1024', '1024x1792' + */ + size?: OpenAIVideoSize + + /** + * Video duration in seconds. + * Supported values: 4, 8, or 12 seconds. + */ + seconds?: OpenAIVideoSeconds +} + +/** + * Model-specific provider options mapping. + * + * @experimental Video generation is an experimental feature and may change. + */ +export type OpenAIVideoModelProviderOptionsByName = { + 'sora-2': OpenAIVideoProviderOptions + 'sora-2-pro': OpenAIVideoProviderOptions +} + +/** + * Validate video size for a given model. + * + * @experimental Video generation is an experimental feature and may change. + */ +export function validateVideoSize( + model: string, + size?: string, +): asserts size is OpenAIVideoSize | undefined { + const validSizes: Array = [ + '1280x720', + '720x1280', + '1792x1024', + '1024x1792', + ] + + if (size && !validSizes.includes(size as OpenAIVideoSize)) { + throw new Error( + `Size "${size}" is not supported by model "${model}". Supported sizes: ${validSizes.join(', ')}`, + ) + } +} + +/** + * Validate video duration (seconds) for a given model. + * Accepts both string and number for convenience, but the API requires strings. + * + * @experimental Video generation is an experimental feature and may change. + */ +export function validateVideoSeconds( + model: string, + seconds?: number | string, +): asserts seconds is OpenAIVideoSeconds | number | undefined { + const validSeconds: Array = ['4', '8', '12'] + const validNumbers: Array = [4, 8, 12] + + if (seconds !== undefined) { + const isValid = + typeof seconds === 'string' + ? validSeconds.includes(seconds) + : validNumbers.includes(seconds) + + if (!isValid) { + throw new Error( + `Duration "${seconds}" is not supported by model "${model}". Supported durations: 4, 8, or 12 seconds`, + ) + } + } +} + +/** + * Convert duration to API format (string). + * The OpenAI Sora API inexplicably requires seconds as a string. + */ +export function toApiSeconds( + seconds: number | string | undefined, +): OpenAIVideoSeconds | undefined { + if (seconds === undefined) return undefined + return String(seconds) as OpenAIVideoSeconds +} + +/** + * @deprecated Use OpenAIVideoSeconds instead + */ +export type OpenAIVideoDuration = OpenAIVideoSeconds diff --git a/packages/typescript/ai-openai/tests/image-adapter.test.ts b/packages/typescript/ai-openai/tests/image-adapter.test.ts new file mode 100644 index 00000000..78ca7fb3 --- /dev/null +++ b/packages/typescript/ai-openai/tests/image-adapter.test.ts @@ -0,0 +1,220 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { OpenAIImageAdapter, createOpenaiImage } from '../src/adapters/image' +import { + validateImageSize, + validateNumberOfImages, + validatePrompt, +} from '../src/image/image-provider-options' + +describe('OpenAI Image Adapter', () => { + describe('createOpenaiImage', () => { + it('creates an adapter with the provided API key', () => { + const adapter = createOpenaiImage('test-api-key') + expect(adapter).toBeInstanceOf(OpenAIImageAdapter) + expect(adapter.kind).toBe('image') + expect(adapter.name).toBe('openai') + }) + + it('has the correct models', () => { + const adapter = createOpenaiImage('test-api-key') + expect(adapter.models).toContain('gpt-image-1') + expect(adapter.models).toContain('gpt-image-1-mini') + expect(adapter.models).toContain('dall-e-3') + expect(adapter.models).toContain('dall-e-2') + }) + }) + + describe('validateImageSize', () => { + describe('gpt-image-1', () => { + it('accepts valid sizes', () => { + expect(() => + validateImageSize('gpt-image-1', '1024x1024'), + ).not.toThrow() + expect(() => + validateImageSize('gpt-image-1', '1536x1024'), + ).not.toThrow() + expect(() => + validateImageSize('gpt-image-1', '1024x1536'), + ).not.toThrow() + expect(() => validateImageSize('gpt-image-1', 'auto')).not.toThrow() + }) + + it('rejects invalid sizes', () => { + expect(() => validateImageSize('gpt-image-1', '512x512')).toThrow() + expect(() => validateImageSize('gpt-image-1', '1792x1024')).toThrow() + }) + + it('accepts undefined size', () => { + expect(() => validateImageSize('gpt-image-1', undefined)).not.toThrow() + }) + }) + + describe('dall-e-3', () => { + it('accepts valid sizes', () => { + expect(() => validateImageSize('dall-e-3', '1024x1024')).not.toThrow() + expect(() => validateImageSize('dall-e-3', '1792x1024')).not.toThrow() + expect(() => validateImageSize('dall-e-3', '1024x1792')).not.toThrow() + }) + + it('rejects invalid sizes', () => { + expect(() => validateImageSize('dall-e-3', '512x512')).toThrow() + expect(() => validateImageSize('dall-e-3', '256x256')).toThrow() + }) + + it('accepts auto size (passes through)', () => { + // auto is treated as a pass-through and not validated + expect(() => validateImageSize('dall-e-3', 'auto')).not.toThrow() + }) + }) + + describe('dall-e-2', () => { + it('accepts valid sizes', () => { + expect(() => validateImageSize('dall-e-2', '256x256')).not.toThrow() + expect(() => validateImageSize('dall-e-2', '512x512')).not.toThrow() + expect(() => validateImageSize('dall-e-2', '1024x1024')).not.toThrow() + }) + + it('rejects invalid sizes', () => { + expect(() => validateImageSize('dall-e-2', '1792x1024')).toThrow() + expect(() => validateImageSize('dall-e-2', '1024x1792')).toThrow() + }) + }) + }) + + describe('validateNumberOfImages', () => { + describe('dall-e-3', () => { + it('only accepts 1 image', () => { + expect(() => validateNumberOfImages('dall-e-3', 1)).not.toThrow() + expect(() => validateNumberOfImages('dall-e-3', 2)).toThrow() + expect(() => + validateNumberOfImages('dall-e-3', undefined), + ).not.toThrow() + }) + }) + + describe('dall-e-2', () => { + it('accepts 1-10 images', () => { + expect(() => validateNumberOfImages('dall-e-2', 1)).not.toThrow() + expect(() => validateNumberOfImages('dall-e-2', 5)).not.toThrow() + expect(() => validateNumberOfImages('dall-e-2', 10)).not.toThrow() + expect(() => validateNumberOfImages('dall-e-2', 11)).toThrow() + expect(() => validateNumberOfImages('dall-e-2', 0)).toThrow() + }) + }) + + describe('gpt-image-1', () => { + it('accepts 1-10 images', () => { + expect(() => validateNumberOfImages('gpt-image-1', 1)).not.toThrow() + expect(() => validateNumberOfImages('gpt-image-1', 10)).not.toThrow() + expect(() => validateNumberOfImages('gpt-image-1', 11)).toThrow() + }) + }) + }) + + describe('validatePrompt', () => { + it('rejects empty prompts', () => { + expect(() => + validatePrompt({ prompt: '', model: 'gpt-image-1' }), + ).toThrow() + }) + + it('accepts whitespace-only prompts (does not trim)', () => { + // The validation checks length, not trimmed length + expect(() => + validatePrompt({ prompt: ' ', model: 'gpt-image-1' }), + ).not.toThrow() + }) + + it('accepts non-empty prompts', () => { + expect(() => + validatePrompt({ prompt: 'A cat', model: 'gpt-image-1' }), + ).not.toThrow() + }) + }) + + describe('generateImages', () => { + it('calls the OpenAI images.generate API', async () => { + const mockResponse = { + data: [ + { + b64_json: 'base64encodedimage', + revised_prompt: 'A beautiful cat', + }, + ], + usage: { + input_tokens: 10, + output_tokens: 100, + total_tokens: 110, + }, + } + + const mockGenerate = vi.fn().mockResolvedValueOnce(mockResponse) + + const adapter = createOpenaiImage('test-api-key') + // Replace the internal OpenAI SDK client with our mock + ;( + adapter as unknown as { client: { images: { generate: unknown } } } + ).client = { + images: { + generate: mockGenerate, + }, + } + + const result = await adapter.generateImages({ + model: 'gpt-image-1', + prompt: 'A cat wearing a hat', + numberOfImages: 1, + size: '1024x1024', + }) + + expect(mockGenerate).toHaveBeenCalledWith({ + model: 'gpt-image-1', + prompt: 'A cat wearing a hat', + n: 1, + size: '1024x1024', + stream: false, + }) + + expect(result.model).toBe('gpt-image-1') + expect(result.images).toHaveLength(1) + expect(result.images[0].b64Json).toBe('base64encodedimage') + expect(result.images[0].revisedPrompt).toBe('A beautiful cat') + expect(result.usage).toEqual({ + inputTokens: 10, + outputTokens: 100, + totalTokens: 110, + }) + }) + + it('generates a unique ID for each response', async () => { + const mockResponse = { + data: [{ b64_json: 'base64' }], + } + + const mockGenerate = vi.fn().mockResolvedValue(mockResponse) + + const adapter = createOpenaiImage('test-api-key') + ;( + adapter as unknown as { client: { images: { generate: unknown } } } + ).client = { + images: { + generate: mockGenerate, + }, + } + + const result1 = await adapter.generateImages({ + model: 'dall-e-3', + prompt: 'Test prompt', + }) + + const result2 = await adapter.generateImages({ + model: 'dall-e-3', + prompt: 'Test prompt', + }) + + expect(result1.id).not.toBe(result2.id) + expect(result1.id).toMatch(/^openai-/) + expect(result2.id).toMatch(/^openai-/) + }) + }) +}) diff --git a/packages/typescript/ai-openai/tests/openai-adapter.test.ts b/packages/typescript/ai-openai/tests/openai-adapter.test.ts index febed8db..dc5bfcef 100644 --- a/packages/typescript/ai-openai/tests/openai-adapter.test.ts +++ b/packages/typescript/ai-openai/tests/openai-adapter.test.ts @@ -1,8 +1,9 @@ import { describe, it, expect, beforeEach, vi } from 'vitest' -import { chat, type Tool, type StreamChunk } from '@tanstack/ai' -import { OpenAI, type OpenAIProviderOptions } from '../src/openai-adapter' +import { ai, type Tool, type StreamChunk } from '@tanstack/ai' +import { OpenAITextAdapter } from '../src/adapters/text' +import type { OpenAIProviderOptions } from '../src/openai-adapter' -const createAdapter = () => new OpenAI({ apiKey: 'test-key' }) +const createAdapter = () => new OpenAITextAdapter({ apiKey: 'test-key' }) const toolArguments = JSON.stringify({ location: 'Berlin' }) @@ -77,7 +78,7 @@ describe('OpenAI adapter option mapping', () => { } const chunks: StreamChunk[] = [] - for await (const chunk of chat({ + for await (const chunk of ai({ adapter, model: 'gpt-4o-mini', messages: [ diff --git a/packages/typescript/ai-react-ui/package.json b/packages/typescript/ai-react-ui/package.json index 2c10e419..5dd9b063 100644 --- a/packages/typescript/ai-react-ui/package.json +++ b/packages/typescript/ai-react-ui/package.json @@ -50,7 +50,7 @@ }, "devDependencies": { "@vitest/coverage-v8": "4.0.14", - "vite": "^7.2.4" + "vite": "^7.2.7" }, "files": [ "dist" diff --git a/packages/typescript/ai-react/package.json b/packages/typescript/ai-react/package.json index 89a3e8ae..04855ee1 100644 --- a/packages/typescript/ai-react/package.json +++ b/packages/typescript/ai-react/package.json @@ -49,7 +49,7 @@ "@types/react": "^19.2.7", "@vitest/coverage-v8": "4.0.14", "jsdom": "^27.2.0", - "vite": "^7.2.4", + "vite": "^7.2.7", "zod": "^4.1.13" }, "peerDependencies": { diff --git a/packages/typescript/ai-react/src/use-chat.ts b/packages/typescript/ai-react/src/use-chat.ts index 1064bb52..2f38c14c 100644 --- a/packages/typescript/ai-react/src/use-chat.ts +++ b/packages/typescript/ai-react/src/use-chat.ts @@ -80,14 +80,15 @@ export function useChat = any>( }, []) // Only run on mount - initialMessages are handled by ChatClient constructor // Cleanup on unmount: stop any in-flight requests + // Note: We only cleanup when client changes or component unmounts. + // DO NOT include isLoading in dependencies - that would cause the cleanup + // to run when isLoading changes, aborting continuation requests. useEffect(() => { return () => { - // Stop any active generation when component unmounts - if (isLoading) { - client.stop() - } + // Stop any active generation when component unmounts or client changes + client.stop() } - }, [client, isLoading]) + }, [client]) // Note: Callback options (onResponse, onChunk, onFinish, onError, onToolCall) // are captured at client creation time. Changes to these callbacks require diff --git a/packages/typescript/ai-solid-ui/package.json b/packages/typescript/ai-solid-ui/package.json index 8ade3b13..e1b310a1 100644 --- a/packages/typescript/ai-solid-ui/package.json +++ b/packages/typescript/ai-solid-ui/package.json @@ -52,7 +52,7 @@ }, "devDependencies": { "@vitest/coverage-v8": "4.0.14", - "vite": "^7.2.4" + "vite": "^7.2.7" }, "files": [ "src", diff --git a/packages/typescript/ai-solid/src/use-chat.ts b/packages/typescript/ai-solid/src/use-chat.ts index 8dd8ae1a..2a15fb37 100644 --- a/packages/typescript/ai-solid/src/use-chat.ts +++ b/packages/typescript/ai-solid/src/use-chat.ts @@ -63,14 +63,14 @@ export function useChat = any>( }) // Only run on mount - initialMessages are handled by ChatClient constructor // Cleanup on unmount: stop any in-flight requests + // Note: We use createEffect with a cleanup return to handle component unmount. + // The cleanup only runs on disposal (unmount), not on signal changes. createEffect(() => { return () => { // Stop any active generation when component unmounts - if (isLoading()) { - client().stop() - } + client().stop() } - }, [client, isLoading]) + }) // Note: Callback options (onResponse, onChunk, onFinish, onError, onToolCall) // are captured at client creation time. Changes to these callbacks require diff --git a/packages/typescript/ai-svelte/package.json b/packages/typescript/ai-svelte/package.json index f8292bfb..035631e6 100644 --- a/packages/typescript/ai-svelte/package.json +++ b/packages/typescript/ai-svelte/package.json @@ -56,7 +56,7 @@ "svelte": "^5.20.0", "svelte-check": "^4.2.0", "typescript": "5.9.3", - "vite": "^7.2.4", + "vite": "^7.2.7", "zod": "^4.1.13" }, "peerDependencies": { diff --git a/packages/typescript/ai-svelte/src/create-chat.svelte.ts b/packages/typescript/ai-svelte/src/create-chat.svelte.ts index 29d58301..c4081d27 100644 --- a/packages/typescript/ai-svelte/src/create-chat.svelte.ts +++ b/packages/typescript/ai-svelte/src/create-chat.svelte.ts @@ -68,6 +68,11 @@ export function createChat = any>( }, }) + // Note: Cleanup is handled by calling stop() directly when needed. + // Unlike React/Vue/Solid, Svelte 5 runes like $effect can only be used + // during component initialization, so we don't add automatic cleanup here. + // Users should call chat.stop() in their component's cleanup if needed. + // Define methods const sendMessage = async (content: string) => { await client.sendMessage(content) diff --git a/packages/typescript/ai-vue-ui/package.json b/packages/typescript/ai-vue-ui/package.json index 54835e7f..6c008d97 100644 --- a/packages/typescript/ai-vue-ui/package.json +++ b/packages/typescript/ai-vue-ui/package.json @@ -51,7 +51,7 @@ }, "devDependencies": { "@vitest/coverage-v8": "4.0.14", - "vite": "^7.2.4", + "vite": "^7.2.7", "vue-tsc": "^2.2.10" }, "files": [ diff --git a/packages/typescript/ai-vue/src/use-chat.ts b/packages/typescript/ai-vue/src/use-chat.ts index 946ad4a9..f190d0ee 100644 --- a/packages/typescript/ai-vue/src/use-chat.ts +++ b/packages/typescript/ai-vue/src/use-chat.ts @@ -39,10 +39,9 @@ export function useChat = any>( }) // Cleanup on unmount: stop any in-flight requests + // Note: client.stop() is safe to call even if nothing is in progress onScopeDispose(() => { - if (isLoading.value) { - client.stop() - } + client.stop() }) // Note: Callback options (onResponse, onChunk, onFinish, onError, onToolCall) diff --git a/packages/typescript/ai/package.json b/packages/typescript/ai/package.json index c395b371..8895e42f 100644 --- a/packages/typescript/ai/package.json +++ b/packages/typescript/ai/package.json @@ -17,6 +17,10 @@ "types": "./dist/esm/index.d.ts", "import": "./dist/esm/index.js" }, + "./adapters": { + "types": "./dist/esm/activities/index.d.ts", + "import": "./dist/esm/activities/index.js" + }, "./event-client": { "types": "./dist/esm/event-client.d.ts", "import": "./dist/esm/event-client.js" diff --git a/packages/typescript/ai/src/activities/embedding/adapter.ts b/packages/typescript/ai/src/activities/embedding/adapter.ts new file mode 100644 index 00000000..2118b206 --- /dev/null +++ b/packages/typescript/ai/src/activities/embedding/adapter.ts @@ -0,0 +1,69 @@ +import type { EmbeddingOptions, EmbeddingResult } from '../../types' + +/** + * Configuration for embedding adapter instances + */ +export interface EmbeddingAdapterConfig { + apiKey?: string + baseUrl?: string + timeout?: number + maxRetries?: number + headers?: Record +} + +/** + * Base interface for embedding adapters. + * Provides type-safe embedding generation functionality. + * + * Generic parameters: + * - TModels: Array of supported embedding model names + * - TProviderOptions: Provider-specific options for embedding endpoint + */ +export interface EmbeddingAdapter< + TModels extends ReadonlyArray = ReadonlyArray, + TProviderOptions extends object = Record, +> { + /** Discriminator for adapter kind - used by generate() to determine API shape */ + readonly kind: 'embedding' + /** Adapter name identifier */ + readonly name: string + /** Supported embedding models */ + readonly models: TModels + + // Type-only properties for type inference + /** @internal Type-only property for provider options inference */ + _providerOptions?: TProviderOptions + + /** + * Create embeddings for the given input + */ + createEmbeddings: (options: EmbeddingOptions) => Promise +} + +/** + * Abstract base class for embedding adapters. + * Extend this class to implement an embedding adapter for a specific provider. + */ +export abstract class BaseEmbeddingAdapter< + TModels extends ReadonlyArray = ReadonlyArray, + TProviderOptions extends object = Record, +> implements EmbeddingAdapter { + readonly kind = 'embedding' as const + abstract readonly name: string + abstract readonly models: TModels + + // Type-only property - never assigned at runtime + declare _providerOptions?: TProviderOptions + + protected config: EmbeddingAdapterConfig + + constructor(config: EmbeddingAdapterConfig = {}) { + this.config = config + } + + abstract createEmbeddings(options: EmbeddingOptions): Promise + + protected generateId(): string { + return `${this.name}-${Date.now()}-${Math.random().toString(36).substring(7)}` + } +} diff --git a/packages/typescript/ai/src/activities/embedding/index.ts b/packages/typescript/ai/src/activities/embedding/index.ts new file mode 100644 index 00000000..a1e77c45 --- /dev/null +++ b/packages/typescript/ai/src/activities/embedding/index.ts @@ -0,0 +1,161 @@ +/** + * Embedding Activity + * + * Generates vector embeddings from text input. + * This is a self-contained module with implementation, types, and JSDoc. + */ + +import { aiEventClient } from '../../event-client.js' +import type { EmbeddingAdapter } from './adapter' +import type { EmbeddingOptions, EmbeddingResult } from '../../types' + +// =========================== +// Activity Kind +// =========================== + +/** The adapter kind this activity handles */ +export const kind = 'embedding' as const + +// =========================== +// Type Extraction Helpers +// =========================== + +/** Extract model types from an EmbeddingAdapter */ +export type EmbeddingModels = + TAdapter extends EmbeddingAdapter ? M[number] : string + +/** Extract provider options from an EmbeddingAdapter */ +export type EmbeddingProviderOptions = + TAdapter extends EmbeddingAdapter ? P : object + +// =========================== +// Activity Options Type +// =========================== + +/** + * Options for the embedding activity. + * + * @template TAdapter - The embedding adapter type + * @template TModel - The model name type (inferred from adapter) + */ +export interface EmbeddingActivityOptions< + TAdapter extends EmbeddingAdapter, object>, + TModel extends EmbeddingModels, +> { + /** The embedding adapter to use */ + adapter: TAdapter & { kind: typeof kind } + /** The model name (autocompletes based on adapter) */ + model: TModel + /** Text input to embed (single string or array of strings) */ + input: string | Array + /** Optional: Number of dimensions for the embedding vector */ + dimensions?: number + /** Provider-specific options */ + providerOptions?: EmbeddingProviderOptions +} + +// =========================== +// Activity Result Type +// =========================== + +/** Result type for the embedding activity */ +export type EmbeddingActivityResult = Promise + +// =========================== +// Helper Functions +// =========================== + +function createId(prefix: string): string { + return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}` +} + +// =========================== +// Activity Implementation +// =========================== + +/** + * Embedding activity - generates vector embeddings from text. + * + * Embeddings are numerical representations of text that capture semantic meaning. + * They can be used for similarity search, clustering, classification, and more. + * + * @example Generate embeddings for a single text + * ```ts + * import { ai } from '@tanstack/ai' + * import { openaiEmbed } from '@tanstack/ai-openai' + * + * const result = await ai({ + * adapter: openaiEmbed(), + * model: 'text-embedding-3-small', + * input: 'Hello, world!' + * }) + * + * console.log(result.embeddings[0]) // Array of numbers + * ``` + * + * @example Generate embeddings for multiple texts + * ```ts + * const result = await ai({ + * adapter: openaiEmbed(), + * model: 'text-embedding-3-small', + * input: ['Hello', 'World', 'How are you?'] + * }) + * + * // result.embeddings is an array of embedding vectors + * result.embeddings.forEach((embedding, i) => { + * console.log(`Text ${i}: ${embedding.length} dimensions`) + * }) + * ``` + * + * @example Specify embedding dimensions + * ```ts + * const result = await ai({ + * adapter: openaiEmbed(), + * model: 'text-embedding-3-small', + * input: 'Hello, world!', + * dimensions: 256 // Reduce to 256 dimensions + * }) + * ``` + */ +export async function embeddingActivity< + TAdapter extends EmbeddingAdapter, object>, + TModel extends EmbeddingModels, +>( + options: EmbeddingActivityOptions, +): EmbeddingActivityResult { + const { adapter, model, input, dimensions } = options + const requestId = createId('embedding') + const inputCount = Array.isArray(input) ? input.length : 1 + const startTime = Date.now() + + aiEventClient.emit('embedding:started', { + requestId, + model: model as string, + inputCount, + timestamp: startTime, + }) + + const embeddingOptions: EmbeddingOptions = { + model: model as string, + input, + dimensions, + } + + const result = await adapter.createEmbeddings(embeddingOptions) + + const duration = Date.now() - startTime + + aiEventClient.emit('embedding:completed', { + requestId, + model: model as string, + inputCount, + duration, + timestamp: Date.now(), + }) + + return result +} + +// Re-export adapter types +export type { EmbeddingAdapter, EmbeddingAdapterConfig } from './adapter' +export { BaseEmbeddingAdapter } from './adapter' diff --git a/packages/typescript/ai/src/activities/image/adapter.ts b/packages/typescript/ai/src/activities/image/adapter.ts new file mode 100644 index 00000000..d4e91dae --- /dev/null +++ b/packages/typescript/ai/src/activities/image/adapter.ts @@ -0,0 +1,91 @@ +import type { ImageGenerationOptions, ImageGenerationResult } from '../../types' + +/** + * Configuration for image adapter instances + */ +export interface ImageAdapterConfig { + apiKey?: string + baseUrl?: string + timeout?: number + maxRetries?: number + headers?: Record +} + +/** + * Base interface for image generation adapters. + * Provides type-safe image generation functionality with support for + * model-specific provider options. + * + * Generic parameters: + * - TModels: Array of supported image model names + * - TProviderOptions: Base provider-specific options for image generation + * - TModelProviderOptionsByName: Map from model name to its specific provider options + * - TModelSizeByName: Map from model name to its supported sizes + */ +export interface ImageAdapter< + TModels extends ReadonlyArray = ReadonlyArray, + TProviderOptions extends object = Record, + TModelProviderOptionsByName extends Record = Record, + TModelSizeByName extends Record = Record, +> { + /** Discriminator for adapter kind - used by generate() to determine API shape */ + readonly kind: 'image' + /** Adapter name identifier */ + readonly name: string + /** Supported image generation models */ + readonly models: TModels + + // Type-only properties for type inference + /** @internal Type-only property for provider options inference */ + _providerOptions?: TProviderOptions + /** @internal Type-only map from model name to its specific provider options */ + _modelProviderOptionsByName?: TModelProviderOptionsByName + /** @internal Type-only map from model name to its supported sizes */ + _modelSizeByName?: TModelSizeByName + + /** + * Generate images from a prompt + */ + generateImages: ( + options: ImageGenerationOptions, + ) => Promise +} + +/** + * Abstract base class for image generation adapters. + * Extend this class to implement an image adapter for a specific provider. + */ +export abstract class BaseImageAdapter< + TModels extends ReadonlyArray = ReadonlyArray, + TProviderOptions extends object = Record, + TModelProviderOptionsByName extends Record = Record, + TModelSizeByName extends Record = Record, +> implements ImageAdapter< + TModels, + TProviderOptions, + TModelProviderOptionsByName, + TModelSizeByName +> { + readonly kind = 'image' as const + abstract readonly name: string + abstract readonly models: TModels + + // Type-only properties - never assigned at runtime + declare _providerOptions?: TProviderOptions + declare _modelProviderOptionsByName?: TModelProviderOptionsByName + declare _modelSizeByName?: TModelSizeByName + + protected config: ImageAdapterConfig + + constructor(config: ImageAdapterConfig = {}) { + this.config = config + } + + abstract generateImages( + options: ImageGenerationOptions, + ): Promise + + protected generateId(): string { + return `${this.name}-${Date.now()}-${Math.random().toString(36).substring(7)}` + } +} diff --git a/packages/typescript/ai/src/activities/image/index.ts b/packages/typescript/ai/src/activities/image/index.ts new file mode 100644 index 00000000..6248dc20 --- /dev/null +++ b/packages/typescript/ai/src/activities/image/index.ts @@ -0,0 +1,155 @@ +/** + * Image Activity + * + * Generates images from text prompts. + * This is a self-contained module with implementation, types, and JSDoc. + */ + +import type { ImageAdapter } from './adapter' +import type { ImageGenerationResult } from '../../types' + +// =========================== +// Activity Kind +// =========================== + +/** The adapter kind this activity handles */ +export const kind = 'image' as const + +// =========================== +// Type Extraction Helpers +// =========================== + +/** Extract model types from an ImageAdapter */ +export type ImageModels = + TAdapter extends ImageAdapter ? M[number] : string + +/** + * Extract model-specific provider options from an ImageAdapter. + * If the model has specific options defined in ModelProviderOptions (and not just via index signature), + * use those; otherwise fall back to base provider options. + */ +export type ImageProviderOptionsForModel = + TAdapter extends ImageAdapter + ? string extends keyof ModelOptions + ? // ModelOptions is Record or has index signature - use BaseOptions + BaseOptions + : // ModelOptions has explicit keys - check if TModel is one of them + TModel extends keyof ModelOptions + ? ModelOptions[TModel] + : BaseOptions + : object + +/** + * Extract model-specific size options from an ImageAdapter. + * If the model has specific sizes defined, use those; otherwise fall back to string. + */ +export type ImageSizeForModel = + TAdapter extends ImageAdapter + ? string extends keyof SizeByName + ? // SizeByName has index signature - fall back to string + string + : // SizeByName has explicit keys - check if TModel is one of them + TModel extends keyof SizeByName + ? SizeByName[TModel] + : string + : string + +// =========================== +// Activity Options Type +// =========================== + +/** + * Options for the image activity. + * + * @template TAdapter - The image adapter type + * @template TModel - The model name type (inferred from adapter) + */ +export interface ImageActivityOptions< + TAdapter extends ImageAdapter, object, any, any>, + TModel extends ImageModels, +> { + /** The image adapter to use */ + adapter: TAdapter & { kind: typeof kind } + /** The model name (autocompletes based on adapter) */ + model: TModel + /** Text description of the desired image(s) */ + prompt: string + /** Number of images to generate (default: 1) */ + numberOfImages?: number + /** Image size in WIDTHxHEIGHT format (e.g., "1024x1024") */ + size?: ImageSizeForModel + /** Provider-specific options for image generation */ + providerOptions?: ImageProviderOptionsForModel +} + +// =========================== +// Activity Result Type +// =========================== + +/** Result type for the image activity */ +export type ImageActivityResult = Promise + +// =========================== +// Activity Implementation +// =========================== + +/** + * Image activity - generates images from text prompts. + * + * Uses AI image generation models to create images based on natural language descriptions. + * + * @example Generate a single image + * ```ts + * import { ai } from '@tanstack/ai' + * import { openaiImage } from '@tanstack/ai-openai' + * + * const result = await ai({ + * adapter: openaiImage(), + * model: 'dall-e-3', + * prompt: 'A serene mountain landscape at sunset' + * }) + * + * console.log(result.images[0].url) + * ``` + * + * @example Generate multiple images + * ```ts + * const result = await ai({ + * adapter: openaiImage(), + * model: 'dall-e-2', + * prompt: 'A cute robot mascot', + * numberOfImages: 4, + * size: '512x512' + * }) + * + * result.images.forEach((image, i) => { + * console.log(`Image ${i + 1}: ${image.url}`) + * }) + * ``` + * + * @example With provider-specific options + * ```ts + * const result = await ai({ + * adapter: openaiImage(), + * model: 'dall-e-3', + * prompt: 'A professional headshot photo', + * size: '1024x1024', + * providerOptions: { + * quality: 'hd', + * style: 'natural' + * } + * }) + * ``` + */ +export async function imageActivity< + TAdapter extends ImageAdapter, object, any, any>, + TModel extends ImageModels, +>(options: ImageActivityOptions): ImageActivityResult { + const { adapter, ...rest } = options + + return adapter.generateImages(rest) +} + +// Re-export adapter types +export type { ImageAdapter, ImageAdapterConfig } from './adapter' +export { BaseImageAdapter } from './adapter' diff --git a/packages/typescript/ai/src/activities/index.ts b/packages/typescript/ai/src/activities/index.ts new file mode 100644 index 00000000..66152106 --- /dev/null +++ b/packages/typescript/ai/src/activities/index.ts @@ -0,0 +1,817 @@ +/** + * Activities Index + * + * Central hub for all AI activities. This module exports: + * - All activity implementations and their types + * - All adapter interfaces and base classes + * - The activity routing map and unified type definitions + * + * To add a new activity: + * 1. Create a new directory under activities/ with index.ts and adapter.ts + * 2. Export the activity and adapter from this file + * 3. Add it to the activityMap + */ + +// Import the activity functions and kinds for the map +import { textActivity, kind as textKindValue } from './text/index' +import { + embeddingActivity, + kind as embeddingKindValue, +} from './embedding/index' +import { + summarizeActivity, + kind as summarizeKindValue, +} from './summarize/index' +import { imageActivity, kind as imageKindValue } from './image/index' +import { videoActivity, kind as videoKindValue } from './video/index' +import { ttsActivity, kind as ttsKindValue } from './tts/index' +import { + transcriptionActivity, + kind as transcriptionKindValue, +} from './transcription/index' + +// Import model types for use in local type definitions +import type { + InputModalitiesForModel, + MessageMetadataForAdapter, + TextModels, + TextProviderOptionsForModel, + // eslint-disable-next-line import/no-duplicates +} from './text/index' +import type { + EmbeddingActivityOptions, + EmbeddingActivityResult, + EmbeddingModels, + EmbeddingProviderOptions, +} from './embedding/index' +import type { + SummarizeActivityOptions, + SummarizeActivityResult, + SummarizeModels, + SummarizeProviderOptions, +} from './summarize/index' +import type { + ImageActivityOptions, + ImageActivityResult, + ImageModels, + ImageProviderOptionsForModel, + ImageSizeForModel, +} from './image/index' +import type { + VideoActivityOptions, + VideoActivityResult, + VideoCreateOptions, + VideoModels, + VideoProviderOptions, + VideoStatusOptions, + VideoUrlOptions, +} from './video/index' +import type { + TTSActivityOptions, + TTSActivityResult, + TTSModels, + TTSProviderOptions, +} from './tts/index' +import type { + TranscriptionActivityOptions, + TranscriptionActivityResult, + TranscriptionModels, + TranscriptionProviderOptions, +} from './transcription/index' + +// Import adapter types for type definitions +import type { TextAdapter } from './text/adapter' +import type { EmbeddingAdapter } from './embedding/adapter' +import type { SummarizeAdapter } from './summarize/adapter' +import type { ImageAdapter } from './image/adapter' +import type { VideoAdapter } from './video/adapter' +import type { TTSAdapter } from './tts/adapter' +import type { TranscriptionAdapter } from './transcription/adapter' +// eslint-disable-next-line import/no-duplicates +import type { TextActivityOptions, TextActivityResult } from './text/index' + +import type { z } from 'zod' + +import type { + ConstrainedModelMessage, + EmbeddingResult, + ImageGenerationResult, + StreamChunk, + SummarizationResult, + TTSResult, + TextOptions, + TranscriptionResult, + VideoJobResult, + VideoStatusResult, + VideoUrlResult, +} from '../types' + +// =========================== +// Text Activity +// =========================== + +export { + kind as textKind, + textActivity, + textOptions, + type TextActivityOptions, + type TextActivityResult, + type CommonOptions, + type TextModels, + type TextProviderOptionsForModel, + type InputModalitiesForModel, + type MessageMetadataForAdapter, +} from './text/index' + +export { + BaseTextAdapter, + type TextAdapter, + type TextAdapterConfig, + type StructuredOutputOptions, + type StructuredOutputResult, +} from './text/adapter' + +// =========================== +// Embedding Activity +// =========================== + +export { + kind as embeddingKind, + embeddingActivity, + type EmbeddingActivityOptions, + type EmbeddingActivityResult, + type EmbeddingModels, + type EmbeddingProviderOptions, +} from './embedding/index' + +export { + BaseEmbeddingAdapter, + type EmbeddingAdapter, + type EmbeddingAdapterConfig, +} from './embedding/adapter' + +// =========================== +// Summarize Activity +// =========================== + +export { + kind as summarizeKind, + summarizeActivity, + type SummarizeActivityOptions, + type SummarizeActivityResult, + type SummarizeModels, + type SummarizeProviderOptions, +} from './summarize/index' + +export { + BaseSummarizeAdapter, + type SummarizeAdapter, + type SummarizeAdapterConfig, +} from './summarize/adapter' + +// =========================== +// Image Activity +// =========================== + +export { + kind as imageKind, + imageActivity, + type ImageActivityOptions, + type ImageActivityResult, + type ImageModels, + type ImageProviderOptionsForModel, + type ImageSizeForModel, +} from './image/index' + +export { + BaseImageAdapter, + type ImageAdapter, + type ImageAdapterConfig, +} from './image/adapter' + +// =========================== +// Video Activity (Experimental) +// =========================== + +export { + kind as videoKind, + videoActivity, + type VideoActivityOptions, + type VideoActivityResult, + type VideoModels, + type VideoProviderOptions, + type VideoCreateOptions, + type VideoStatusOptions, + type VideoUrlOptions, +} from './video/index' + +export { + BaseVideoAdapter, + type VideoAdapter, + type VideoAdapterConfig, +} from './video/adapter' + +// =========================== +// TTS Activity +// =========================== + +export { + kind as ttsKind, + ttsActivity, + type TTSActivityOptions, + type TTSActivityResult, + type TTSModels, + type TTSProviderOptions, +} from './tts/index' + +export { + BaseTTSAdapter, + type TTSAdapter, + type TTSAdapterConfig, +} from './tts/adapter' + +// =========================== +// Transcription Activity +// =========================== + +export { + kind as transcriptionKind, + transcriptionActivity, + type TranscriptionActivityOptions, + type TranscriptionActivityResult, + type TranscriptionModels, + type TranscriptionProviderOptions, +} from './transcription/index' + +export { + BaseTranscriptionAdapter, + type TranscriptionAdapter, + type TranscriptionAdapterConfig, +} from './transcription/adapter' + +// =========================== +// Activity Handler Type +// =========================== + +/** Type for activity handler functions */ +type ActivityHandler = (options: any) => any + +// =========================== +// Activity Map +// =========================== + +/** + * Map of adapter kind to activity handler function. + * This allows for pluggable activities without modifying the ai function. + */ +export const activityMap = new Map([ + [textKindValue, textActivity], + [embeddingKindValue, embeddingActivity], + [summarizeKindValue, summarizeActivity], + [imageKindValue, imageActivity], + [videoKindValue, videoActivity], + [ttsKindValue, ttsActivity], + [transcriptionKindValue, transcriptionActivity], +]) + +// =========================== +// Adapter Union Types +// =========================== + +/** Union of all adapter types that can be passed to ai() */ +export type AIAdapter = + | TextAdapter, object, any, any, any> + | EmbeddingAdapter, object> + | SummarizeAdapter, object> + | ImageAdapter, object, any, any> + | VideoAdapter, object> + | TTSAdapter, object> + | TranscriptionAdapter, object> + +/** Alias for backwards compatibility */ +export type GenerateAdapter = AIAdapter + +/** Union of all adapters (legacy name) */ +export type AnyAdapter = + | TextAdapter + | EmbeddingAdapter + | SummarizeAdapter + | ImageAdapter + | VideoAdapter + | TTSAdapter + | TranscriptionAdapter + +/** Union type of all adapter kinds */ +export type AdapterKind = + | 'text' + | 'embedding' + | 'summarize' + | 'image' + | 'video' + | 'tts' + | 'transcription' + +// =========================== +// Unified Options Type +// =========================== + +/** Union of all adapter types with their kind discriminator */ +export type AnyAIAdapter = + | (TextAdapter, object, any, any, any> & { + kind: 'text' + }) + | (EmbeddingAdapter, object> & { kind: 'embedding' }) + | (SummarizeAdapter, object> & { kind: 'summarize' }) + | (ImageAdapter, object, any, any> & { kind: 'image' }) + | (VideoAdapter, object> & { kind: 'video' }) + | (TTSAdapter, object> & { kind: 'tts' }) + | (TranscriptionAdapter, object> & { + kind: 'transcription' + }) + +/** Infer the correct options type based on adapter kind */ +export type AIOptionsFor< + TAdapter extends AnyAIAdapter, + TModel extends string, + TSchema extends z.ZodType | undefined = undefined, + TStream extends boolean | undefined = undefined, + TRequest extends 'create' | 'status' | 'url' = 'create', +> = TAdapter extends { kind: 'text' } + ? TAdapter extends TextAdapter, object, any, any, any> + ? TextActivityOptions< + TAdapter, + TModel & TextModels, + TSchema, + TStream extends boolean ? TStream : true + > + : never + : TAdapter extends { kind: 'embedding' } + ? TAdapter extends EmbeddingAdapter, object> + ? EmbeddingActivityOptions> + : never + : TAdapter extends { kind: 'summarize' } + ? TAdapter extends SummarizeAdapter, object> + ? SummarizeActivityOptions< + TAdapter, + TModel & SummarizeModels, + TStream extends boolean ? TStream : false + > + : never + : TAdapter extends { kind: 'image' } + ? TAdapter extends ImageAdapter, object, any, any> + ? ImageActivityOptions> + : never + : TAdapter extends { kind: 'video' } + ? TAdapter extends VideoAdapter, object> + ? VideoActivityOptions< + TAdapter, + TModel & VideoModels, + TRequest + > + : never + : TAdapter extends { kind: 'tts' } + ? TAdapter extends TTSAdapter, object> + ? TTSActivityOptions> + : never + : TAdapter extends { kind: 'transcription' } + ? TAdapter extends TranscriptionAdapter< + ReadonlyArray, + object + > + ? TranscriptionActivityOptions< + TAdapter, + TModel & TranscriptionModels + > + : never + : never + +// =========================== +// Unified Result Type +// =========================== + +/** Infer the return type based on adapter kind, schema, and stream */ +export type AIResultFor< + TAdapter extends AnyAIAdapter, + TSchema extends z.ZodType | undefined = undefined, + TStream extends boolean | undefined = undefined, + TRequest extends 'create' | 'status' | 'url' = 'create', +> = TAdapter extends { kind: 'text' } + ? TextActivityResult + : TAdapter extends { kind: 'embedding' } + ? EmbeddingActivityResult + : TAdapter extends { kind: 'summarize' } + ? SummarizeActivityResult + : TAdapter extends { kind: 'image' } + ? ImageActivityResult + : TAdapter extends { kind: 'video' } + ? VideoActivityResult + : TAdapter extends { kind: 'tts' } + ? TTSActivityResult + : TAdapter extends { kind: 'transcription' } + ? TranscriptionActivityResult + : never + +// =========================== +// Unified Options Type (Legacy) +// =========================== + +/** Unified options type for those who need it */ +export type GenerateOptions< + TAdapter extends AIAdapter, + TModel extends string, + TSchema extends z.ZodType | undefined = undefined, + TStream extends boolean = true, +> = + TAdapter extends TextAdapter, object, any, any, any> + ? TextActivityOptions< + TAdapter, + TModel & TextModels, + TSchema, + TStream + > + : TAdapter extends EmbeddingAdapter, object> + ? EmbeddingActivityOptions> + : TAdapter extends SummarizeAdapter, object> + ? SummarizeActivityOptions< + TAdapter, + TModel & SummarizeModels, + TStream + > + : TAdapter extends ImageAdapter, object, any, any> + ? ImageActivityOptions> + : never + +// =========================== +// Legacy Type Aliases +// =========================== + +/** @deprecated Use TextActivityOptions */ +export type GenerateTextOptions< + TAdapter extends TextAdapter, object, any, any, any>, + TModel extends TextModels, + TSchema extends z.ZodType | undefined = undefined, + TStream extends boolean = true, +> = TextActivityOptions + +/** @deprecated Use EmbeddingActivityOptions */ +export type GenerateEmbeddingOptions< + TAdapter extends EmbeddingAdapter, object>, + TModel extends EmbeddingModels, +> = EmbeddingActivityOptions + +/** @deprecated Use SummarizeActivityOptions */ +export type GenerateSummarizeOptions< + TAdapter extends SummarizeAdapter, object>, + TModel extends SummarizeModels, + TStream extends boolean = false, +> = SummarizeActivityOptions + +/** @deprecated Use ImageActivityOptions */ +export type GenerateImageOptions< + TAdapter extends ImageAdapter, object, any, any>, + TModel extends ImageModels, +> = ImageActivityOptions + +// =========================== +// Implementation Types for ai() +// =========================== + +/** + * Union type for all possible ai() options (used in implementation signature) + */ +export type AIOptionsUnion = + | TextActivityOptions< + TextAdapter, object, any, any, any>, + string, + z.ZodType | undefined, + boolean + > + | EmbeddingActivityOptions< + EmbeddingAdapter, object>, + string + > + | SummarizeActivityOptions< + SummarizeAdapter, object>, + string, + boolean + > + | ImageActivityOptions< + ImageAdapter, object, any, any>, + string + > + | VideoCreateOptions, object>, string> + | VideoStatusOptions, object>, string> + | VideoUrlOptions, object>, string> + | TTSActivityOptions, object>, string> + | TranscriptionActivityOptions< + TranscriptionAdapter, object>, + string + > + +/** + * Union type for all possible ai() return types (used in implementation signature) + */ +export type AIResultUnion = + | AsyncIterable + | Promise + | Promise + | Promise + | Promise + | Promise + | Promise + | Promise + | Promise + | Promise + | Promise + +// =========================== +// Explicit AI Option Types +// =========================== +// These types provide clear autocomplete and required field enforcement +// for the ai() function. They are slightly different from ActivityOptions +// as they include constraints like ConstrainedModelMessage for text. + +/** + * Explicit embedding options - provides clear autocomplete and required field enforcement + */ +export type AIEmbeddingOptions< + TAdapter extends EmbeddingAdapter, object>, + TModel extends EmbeddingModels, +> = { + /** The embedding adapter to use */ + adapter: TAdapter & { kind: 'embedding' } + /** The model name (autocompletes based on adapter) */ + model: TModel + /** Text input to embed (single string or array of strings) - REQUIRED */ + input: string | Array + /** Optional: Number of dimensions for the embedding vector */ + dimensions?: number + /** Provider-specific options */ + providerOptions?: EmbeddingProviderOptions +} + +/** + * Explicit summarize options - provides clear autocomplete and required field enforcement + */ +export type AISummarizeOptions< + TAdapter extends SummarizeAdapter, object>, + TModel extends SummarizeModels, + TStream extends boolean = false, +> = { + /** The summarize adapter to use */ + adapter: TAdapter & { kind: 'summarize' } + /** The model name (autocompletes based on adapter) */ + model: TModel + /** The text to summarize - REQUIRED */ + text: string + /** Maximum length of the summary (in words or characters, provider-dependent) */ + maxLength?: number + /** Style of summary to generate */ + style?: 'bullet-points' | 'paragraph' | 'concise' + /** Topics or aspects to focus on in the summary */ + focus?: Array + /** Whether to stream the response */ + stream?: TStream + /** Provider-specific options */ + providerOptions?: SummarizeProviderOptions +} + +/** + * Explicit image options - provides clear autocomplete and required field enforcement + */ +export type AIImageOptions< + TAdapter extends ImageAdapter, object, any, any>, + TModel extends ImageModels, +> = { + /** The image adapter to use */ + adapter: TAdapter & { kind: 'image' } + /** The model name (autocompletes based on adapter) */ + model: TModel + /** The prompt describing the image to generate - REQUIRED */ + prompt: string + /** Number of images to generate (default: 1) */ + numberOfImages?: number + /** Image size in WIDTHxHEIGHT format (e.g., "1024x1024") - autocompletes based on model */ + size?: ImageSizeForModel + /** Provider-specific options */ + providerOptions?: ImageProviderOptionsForModel +} + +/** + * Explicit video options for creating a job - provides clear autocomplete and required field enforcement. + * + * @experimental Video generation is an experimental feature and may change. + */ +export type AIVideoCreateOptions< + TAdapter extends VideoAdapter, object>, + TModel extends VideoModels, +> = { + /** The video adapter to use */ + adapter: TAdapter & { kind: 'video' } + /** The model name (autocompletes based on adapter) */ + model: TModel + /** Request type - create a new job */ + request?: 'create' + /** Text description of the desired video - REQUIRED */ + prompt: string + /** Video size in WIDTHxHEIGHT format (e.g., "1280x720") */ + size?: string + /** Video duration in seconds */ + duration?: number + /** Provider-specific options */ + providerOptions?: VideoProviderOptions +} + +/** + * Explicit video options for checking status. + * + * @experimental Video generation is an experimental feature and may change. + */ +export type AIVideoStatusOptions< + TAdapter extends VideoAdapter, object>, + TModel extends VideoModels, +> = { + /** The video adapter to use */ + adapter: TAdapter & { kind: 'video' } + /** The model name (autocompletes based on adapter) */ + model: TModel + /** Request type - get status */ + request: 'status' + /** Job ID to check status for - REQUIRED */ + jobId: string +} + +/** + * Explicit video options for getting the video URL. + * + * @experimental Video generation is an experimental feature and may change. + */ +export type AIVideoUrlOptions< + TAdapter extends VideoAdapter, object>, + TModel extends VideoModels, +> = { + /** The video adapter to use */ + adapter: TAdapter & { kind: 'video' } + /** The model name (autocompletes based on adapter) */ + model: TModel + /** Request type - get URL */ + request: 'url' + /** Job ID to get URL for - REQUIRED */ + jobId: string +} + +/** + * Union of all video options types. + * + * @experimental Video generation is an experimental feature and may change. + */ +export type AIVideoOptions< + TAdapter extends VideoAdapter, object>, + TModel extends VideoModels, +> = + | AIVideoCreateOptions + | AIVideoStatusOptions + | AIVideoUrlOptions + +/** + * Explicit TTS options - provides clear autocomplete and required field enforcement. + */ +export type AITTSOptions< + TAdapter extends TTSAdapter, object>, + TModel extends TTSModels, +> = { + /** The TTS adapter to use */ + adapter: TAdapter & { kind: 'tts' } + /** The model name (autocompletes based on adapter) */ + model: TModel + /** The text to convert to speech - REQUIRED */ + text: string + /** The voice to use for generation */ + voice?: string + /** The output audio format */ + format?: 'mp3' | 'opus' | 'aac' | 'flac' | 'wav' | 'pcm' + /** The speed of the generated audio (0.25 to 4.0) */ + speed?: number + /** Provider-specific options */ + providerOptions?: TTSProviderOptions +} + +/** + * Explicit transcription options - provides clear autocomplete and required field enforcement. + */ +export type AITranscriptionOptions< + TAdapter extends TranscriptionAdapter, object>, + TModel extends TranscriptionModels, +> = { + /** The transcription adapter to use */ + adapter: TAdapter & { kind: 'transcription' } + /** The model name (autocompletes based on adapter) */ + model: TModel + /** The audio data to transcribe - REQUIRED */ + audio: string | File | Blob | ArrayBuffer + /** The language of the audio in ISO-639-1 format (e.g., 'en') */ + language?: string + /** An optional prompt to guide the transcription */ + prompt?: string + /** The format of the transcription output */ + responseFormat?: 'json' | 'text' | 'srt' | 'verbose_json' | 'vtt' + /** Provider-specific options */ + providerOptions?: TranscriptionProviderOptions +} + +/** + * Explicit text options - provides clear autocomplete and required field enforcement. + * Uses NoInfer on providerOptions to prevent inference widening. + * Uses ConstrainedModelMessage to constrain content types by model's supported input modalities. + */ +export type AITextOptions< + TAdapter extends TextAdapter, object, any, any, any>, + TModel extends TextModels, + TSchema extends z.ZodType | undefined, + TStream extends boolean, +> = { + /** The text adapter to use */ + adapter: TAdapter & { kind: 'text' } + /** The model name (autocompletes based on adapter) */ + model: TModel + /** Conversation messages - content types are constrained by the model's supported input modalities */ + messages: Array< + ConstrainedModelMessage< + InputModalitiesForModel, + MessageMetadataForAdapter['image'], + MessageMetadataForAdapter['audio'], + MessageMetadataForAdapter['video'], + MessageMetadataForAdapter['document'], + MessageMetadataForAdapter['text'] + > + > + /** System prompts to prepend to the conversation */ + systemPrompts?: TextOptions['systemPrompts'] + /** Tools for function calling (auto-executed when called) */ + tools?: TextOptions['tools'] + /** Additional options like temperature, maxTokens, etc. */ + options?: TextOptions['options'] + /** Provider-specific options (narrowed by model) */ + providerOptions?: NoInfer> + /** AbortController for cancellation */ + abortController?: TextOptions['abortController'] + /** Strategy for controlling the agent loop */ + agentLoopStrategy?: TextOptions['agentLoopStrategy'] + /** Unique conversation identifier for tracking */ + conversationId?: TextOptions['conversationId'] + /** Optional Zod schema for structured output */ + outputSchema?: TSchema + /** Whether to stream the text result (default: true) */ + stream?: TStream +} + +// =========================== +// Re-exported Type Aliases for ai.ts compatibility +// =========================== + +/** @deprecated Use TextActivityOptions */ +export type TextGenerateOptions< + TAdapter extends TextAdapter, object, any, any, any>, + TModel extends TextModels, + TSchema extends z.ZodType | undefined = undefined, + TStream extends boolean = true, +> = TextActivityOptions + +/** @deprecated Use EmbeddingActivityOptions */ +export type EmbeddingGenerateOptions< + TAdapter extends EmbeddingAdapter, object>, + TModel extends EmbeddingModels, +> = EmbeddingActivityOptions + +/** @deprecated Use SummarizeActivityOptions */ +export type SummarizeGenerateOptions< + TAdapter extends SummarizeAdapter, object>, + TModel extends SummarizeModels, + TStream extends boolean = false, +> = SummarizeActivityOptions + +/** @deprecated Use ImageActivityOptions */ +export type ImageGenerateOptions< + TAdapter extends ImageAdapter, object, any, any>, + TModel extends ImageModels, +> = ImageActivityOptions + +/** + * @deprecated Use VideoActivityOptions + * @experimental Video generation is an experimental feature and may change. + */ +export type VideoGenerateOptions< + TAdapter extends VideoAdapter, object>, + TModel extends VideoModels, + TRequest extends 'create' | 'status' | 'url' = 'create', +> = VideoActivityOptions + +/** + * @deprecated Use VideoActivityOptions + * @experimental Video generation is an experimental feature and may change. + */ +export type GenerateVideoOptions< + TAdapter extends VideoAdapter, object>, + TModel extends VideoModels, + TRequest extends 'create' | 'status' | 'url' = 'create', +> = VideoActivityOptions diff --git a/packages/typescript/ai/src/activities/summarize/adapter.ts b/packages/typescript/ai/src/activities/summarize/adapter.ts new file mode 100644 index 00000000..229199a5 --- /dev/null +++ b/packages/typescript/ai/src/activities/summarize/adapter.ts @@ -0,0 +1,71 @@ +import type { SummarizationOptions, SummarizationResult } from '../../types' + +/** + * Configuration for summarize adapter instances + */ +export interface SummarizeAdapterConfig { + apiKey?: string + baseUrl?: string + timeout?: number + maxRetries?: number + headers?: Record +} + +/** + * Base interface for summarize adapters. + * Provides type-safe summarization functionality. + * + * Generic parameters: + * - TModels: Array of supported model names for summarization + * - TProviderOptions: Provider-specific options for summarization endpoint + */ +export interface SummarizeAdapter< + TModels extends ReadonlyArray = ReadonlyArray, + TProviderOptions extends object = Record, +> { + /** Discriminator for adapter kind - used by generate() to determine API shape */ + readonly kind: 'summarize' + /** Adapter name identifier */ + readonly name: string + /** Supported models for summarization */ + readonly models: TModels + + // Type-only properties for type inference + /** @internal Type-only property for provider options inference */ + _providerOptions?: TProviderOptions + + /** + * Summarize the given text + */ + summarize: (options: SummarizationOptions) => Promise +} + +/** + * Abstract base class for summarize adapters. + * Extend this class to implement a summarize adapter for a specific provider. + */ +export abstract class BaseSummarizeAdapter< + TModels extends ReadonlyArray = ReadonlyArray, + TProviderOptions extends object = Record, +> implements SummarizeAdapter { + readonly kind = 'summarize' as const + abstract readonly name: string + abstract readonly models: TModels + + // Type-only property - never assigned at runtime + declare _providerOptions?: TProviderOptions + + protected config: SummarizeAdapterConfig + + constructor(config: SummarizeAdapterConfig = {}) { + this.config = config + } + + abstract summarize( + options: SummarizationOptions, + ): Promise + + protected generateId(): string { + return `${this.name}-${Date.now()}-${Math.random().toString(36).substring(7)}` + } +} diff --git a/packages/typescript/ai/src/activities/summarize/index.ts b/packages/typescript/ai/src/activities/summarize/index.ts new file mode 100644 index 00000000..8f8aceb6 --- /dev/null +++ b/packages/typescript/ai/src/activities/summarize/index.ts @@ -0,0 +1,277 @@ +/** + * Summarize Activity + * + * Generates summaries from text input. + * This is a self-contained module with implementation, types, and JSDoc. + */ + +import { aiEventClient } from '../../event-client.js' +import type { SummarizeAdapter } from './adapter' +import type { + StreamChunk, + SummarizationOptions, + SummarizationResult, +} from '../../types' + +// =========================== +// Activity Kind +// =========================== + +/** The adapter kind this activity handles */ +export const kind = 'summarize' as const + +// =========================== +// Type Extraction Helpers +// =========================== + +/** Extract model types from a SummarizeAdapter */ +export type SummarizeModels = + TAdapter extends SummarizeAdapter ? M[number] : string + +/** Extract provider options from a SummarizeAdapter */ +export type SummarizeProviderOptions = + TAdapter extends SummarizeAdapter ? P : object + +// =========================== +// Activity Options Type +// =========================== + +/** + * Options for the summarize activity. + * + * @template TAdapter - The summarize adapter type + * @template TModel - The model name type (inferred from adapter) + * @template TStream - Whether to stream the output + */ +export interface SummarizeActivityOptions< + TAdapter extends SummarizeAdapter, object>, + TModel extends SummarizeModels, + TStream extends boolean = false, +> { + /** The summarize adapter to use */ + adapter: TAdapter & { kind: typeof kind } + /** The model name (autocompletes based on adapter) */ + model: TModel + /** The text to summarize */ + text: string + /** Maximum length of the summary (in words or characters, provider-dependent) */ + maxLength?: number + /** Style of summary to generate */ + style?: 'bullet-points' | 'paragraph' | 'concise' + /** Topics or aspects to focus on in the summary */ + focus?: Array + /** Provider-specific options */ + providerOptions?: SummarizeProviderOptions + /** + * Whether to stream the summarization result. + * When true, returns an AsyncIterable for streaming output. + * When false or not provided, returns a Promise. + * + * @default false + */ + stream?: TStream +} + +// =========================== +// Activity Result Type +// =========================== + +/** + * Result type for the summarize activity. + * - If stream is true: AsyncIterable + * - Otherwise: Promise + */ +export type SummarizeActivityResult = + TStream extends true + ? AsyncIterable + : Promise + +// =========================== +// Helper Functions +// =========================== + +function createId(prefix: string): string { + return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}` +} + +// =========================== +// Activity Implementation +// =========================== + +/** + * Summarize activity - generates summaries from text. + * + * Supports both streaming and non-streaming modes. + * + * @example Basic summarization + * ```ts + * import { ai } from '@tanstack/ai' + * import { openaiSummarize } from '@tanstack/ai-openai' + * + * const result = await ai({ + * adapter: openaiSummarize(), + * model: 'gpt-4o-mini', + * text: 'Long article text here...' + * }) + * + * console.log(result.summary) + * ``` + * + * @example Summarization with style + * ```ts + * const result = await ai({ + * adapter: openaiSummarize(), + * model: 'gpt-4o-mini', + * text: 'Long article text here...', + * style: 'bullet-points', + * maxLength: 100 + * }) + * ``` + * + * @example Focused summarization + * ```ts + * const result = await ai({ + * adapter: openaiSummarize(), + * model: 'gpt-4o-mini', + * text: 'Long technical document...', + * focus: ['key findings', 'methodology'] + * }) + * ``` + * + * @example Streaming summarization + * ```ts + * for await (const chunk of ai({ + * adapter: openaiSummarize(), + * model: 'gpt-4o-mini', + * text: 'Long article text here...', + * stream: true + * })) { + * if (chunk.type === 'content') { + * process.stdout.write(chunk.delta) + * } + * } + * ``` + */ +export function summarizeActivity< + TAdapter extends SummarizeAdapter, object>, + TModel extends SummarizeModels, + TStream extends boolean = false, +>( + options: SummarizeActivityOptions, +): SummarizeActivityResult { + const { stream } = options + + if (stream) { + return runStreamingSummarize( + options as unknown as SummarizeActivityOptions< + SummarizeAdapter, object>, + string, + true + >, + ) as SummarizeActivityResult + } + + return runSummarize( + options as unknown as SummarizeActivityOptions< + SummarizeAdapter, object>, + string, + false + >, + ) as SummarizeActivityResult +} + +/** + * Run non-streaming summarization + */ +async function runSummarize( + options: SummarizeActivityOptions< + SummarizeAdapter, object>, + string, + false + >, +): Promise { + const { adapter, model, text, maxLength, style, focus } = options + const requestId = createId('summarize') + const inputLength = text.length + const startTime = Date.now() + + aiEventClient.emit('summarize:started', { + requestId, + model, + inputLength, + timestamp: startTime, + }) + + const summarizeOptions: SummarizationOptions = { + model, + text, + maxLength, + style, + focus, + } + + const result = await adapter.summarize(summarizeOptions) + + const duration = Date.now() - startTime + const outputLength = result.summary.length + + aiEventClient.emit('summarize:completed', { + requestId, + model, + inputLength, + outputLength, + duration, + timestamp: Date.now(), + }) + + return result +} + +/** + * Run streaming summarization + * Wraps the non-streaming summarize into a streaming interface. + */ +async function* runStreamingSummarize( + options: SummarizeActivityOptions< + SummarizeAdapter, object>, + string, + true + >, +): AsyncIterable { + const { adapter, model, text, maxLength, style, focus } = options + + const summarizeOptions: SummarizationOptions = { + model, + text, + maxLength, + style, + focus, + } + + const result = await adapter.summarize(summarizeOptions) + + // Yield content chunk with the summary + yield { + type: 'content', + id: result.id, + model: result.model, + timestamp: Date.now(), + delta: result.summary, + content: result.summary, + role: 'assistant', + } + + // Yield done chunk + yield { + type: 'done', + id: result.id, + model: result.model, + timestamp: Date.now(), + finishReason: 'stop', + usage: result.usage, + } +} + +// Re-export adapter types +export type { SummarizeAdapter, SummarizeAdapterConfig } from './adapter' +export { BaseSummarizeAdapter } from './adapter' diff --git a/packages/typescript/ai/src/activities/text/adapter.ts b/packages/typescript/ai/src/activities/text/adapter.ts new file mode 100644 index 00000000..5a74aa08 --- /dev/null +++ b/packages/typescript/ai/src/activities/text/adapter.ts @@ -0,0 +1,163 @@ +import type { z } from 'zod' +import type { + DefaultMessageMetadataByModality, + Modality, + StreamChunk, + TextOptions, +} from '../../types' + +/** + * Configuration for adapter instances + */ +export interface TextAdapterConfig { + apiKey?: string + baseUrl?: string + timeout?: number + maxRetries?: number + headers?: Record +} + +/** + * Options for structured output generation + */ +export interface StructuredOutputOptions { + /** Text options for the request */ + chatOptions: TextOptions + /** Zod schema for the structured output - adapters convert this to their provider's format */ + outputSchema: z.ZodType +} + +/** + * Result from structured output generation + */ +export interface StructuredOutputResult { + /** The parsed data conforming to the schema */ + data: T + /** The raw text response from the model before parsing */ + rawText: string +} + +/** + * Base interface for text adapters. + * Provides type-safe chat/text completion functionality. + * + * Generic parameters: + * - TModels: Array of supported model names + * - TProviderOptions: Provider-specific options for text endpoint + * - TModelProviderOptionsByName: Map from model name to its specific provider options + * - TModelInputModalitiesByName: Map from model name to its supported input modalities + * - TMessageMetadataByModality: Map from modality type to adapter-specific metadata types + */ +export interface TextAdapter< + TModels extends ReadonlyArray = ReadonlyArray, + TProviderOptions extends object = Record, + TModelProviderOptionsByName extends Record = Record< + string, + unknown + >, + TModelInputModalitiesByName extends Record> = + Record>, + TMessageMetadataByModality extends { + text: unknown + image: unknown + audio: unknown + video: unknown + document: unknown + } = DefaultMessageMetadataByModality, +> { + /** Discriminator for adapter kind - used by generate() to determine API shape */ + readonly kind: 'text' + /** Adapter name identifier */ + readonly name: string + /** Supported chat models */ + readonly models: TModels + + // Type-only properties for type inference + /** @internal Type-only property for provider options inference */ + _providerOptions?: TProviderOptions + /** @internal Type-only map from model name to its specific provider options */ + _modelProviderOptionsByName?: TModelProviderOptionsByName + /** @internal Type-only map from model name to its supported input modalities */ + _modelInputModalitiesByName?: TModelInputModalitiesByName + /** @internal Type-only map for message metadata types */ + _messageMetadataByModality?: TMessageMetadataByModality + + /** + * Stream text completions from the model + */ + chatStream: ( + options: TextOptions, + ) => AsyncIterable + + /** + * Generate structured output using the provider's native structured output API. + * This method uses stream: false and sends the JSON schema to the provider + * to ensure the response conforms to the expected structure. + * + * @param options - Structured output options containing chat options and JSON schema + * @returns Promise with the raw data (validation is done in the ai function) + */ + structuredOutput: ( + options: StructuredOutputOptions, + ) => Promise> +} + +/** + * Abstract base class for text adapters. + * Extend this class to implement a text adapter for a specific provider. + */ +export abstract class BaseTextAdapter< + TModels extends ReadonlyArray = ReadonlyArray, + TProviderOptions extends object = Record, + TModelProviderOptionsByName extends Record = Record< + string, + unknown + >, + TModelInputModalitiesByName extends Record> = + Record>, + TMessageMetadataByModality extends { + text: unknown + image: unknown + audio: unknown + video: unknown + document: unknown + } = DefaultMessageMetadataByModality, +> implements TextAdapter< + TModels, + TProviderOptions, + TModelProviderOptionsByName, + TModelInputModalitiesByName, + TMessageMetadataByModality +> { + readonly kind = 'text' as const + abstract readonly name: string + abstract readonly models: TModels + + // Type-only properties - never assigned at runtime + declare _providerOptions?: TProviderOptions + declare _modelProviderOptionsByName?: TModelProviderOptionsByName + declare _modelInputModalitiesByName?: TModelInputModalitiesByName + declare _messageMetadataByModality?: TMessageMetadataByModality + + protected config: TextAdapterConfig + + constructor(config: TextAdapterConfig = {}) { + this.config = config + } + + abstract chatStream( + options: TextOptions, + ): AsyncIterable + + /** + * Generate structured output using the provider's native structured output API. + * Concrete implementations should override this to use provider-specific structured output. + */ + abstract structuredOutput( + options: StructuredOutputOptions, + ): Promise> + + protected generateId(): string { + return `${this.name}-${Date.now()}-${Math.random().toString(36).substring(7)}` + } +} diff --git a/packages/typescript/ai/src/utilities/agent-loop-strategies.ts b/packages/typescript/ai/src/activities/text/agent-loop-strategies.ts similarity index 97% rename from packages/typescript/ai/src/utilities/agent-loop-strategies.ts rename to packages/typescript/ai/src/activities/text/agent-loop-strategies.ts index 2f45fab5..8f133078 100644 --- a/packages/typescript/ai/src/utilities/agent-loop-strategies.ts +++ b/packages/typescript/ai/src/activities/text/agent-loop-strategies.ts @@ -1,4 +1,4 @@ -import type { AgentLoopStrategy } from '../types' +import type { AgentLoopStrategy } from '../../types' /** * Creates a strategy that continues for a maximum number of iterations diff --git a/packages/typescript/ai/src/core/chat.ts b/packages/typescript/ai/src/activities/text/index.ts similarity index 51% rename from packages/typescript/ai/src/core/chat.ts rename to packages/typescript/ai/src/activities/text/index.ts index aebaf65a..e76bab15 100644 --- a/packages/typescript/ai/src/core/chat.ts +++ b/packages/typescript/ai/src/activities/text/index.ts @@ -1,26 +1,260 @@ -import { aiEventClient } from '../event-client.js' -import { ToolCallManager, executeToolCalls } from '../tools/tool-calls' -import { maxIterations as maxIterationsStrategy } from '../utilities/agent-loop-strategies' +/** + * Text Activity + * + * Handles agentic text generation, one-shot text generation, and agentic structured output. + * This is a self-contained module with implementation, types, and JSDoc. + */ + +import { aiEventClient } from '../../event-client.js' +import { streamToText } from '../../stream-to-response.js' +import { ToolCallManager, executeToolCalls } from './tools/tool-calls' +// Schema conversion is now done at the adapter level +// Each adapter imports and uses convertZodToJsonSchema with provider-specific options +import { maxIterations as maxIterationsStrategy } from './agent-loop-strategies' import type { ApprovalRequest, ClientToolRequest, ToolResult, -} from '../tools/tool-calls' +} from './tools/tool-calls' +import type { z } from 'zod' +import type { TextAdapter } from './adapter' import type { AIAdapter, AgentLoopStrategy, - ChatOptions, - ChatStreamOptionsForModel, + DefaultMessageMetadataByModality, DoneStreamChunk, + Modality, ModelMessage, StreamChunk, + TextOptions, + TextStreamOptionsUnion, Tool, ToolCall, -} from '../types' +} from '../../types' + +// =========================== +// Activity Kind +// =========================== + +/** The adapter kind this activity handles */ +export const kind = 'text' as const + +// =========================== +// Common Options +// =========================== + +/** + * Common options shared across different AI provider implementations. + * These options represent the standard parameters that work across OpenAI, Anthropic, and Gemini. + */ +export interface CommonOptions { + /** + * Controls the randomness of the output. + * Higher values (e.g., 0.8) make output more random, lower values (e.g., 0.2) make it more focused and deterministic. + * Range: [0.0, 2.0] + * + * Note: Generally recommended to use either temperature or topP, but not both. + * + * Provider usage: + * - OpenAI: `temperature` (number) - in text.top_p field + * - Anthropic: `temperature` (number) - ranges from 0.0 to 1.0, default 1.0 + * - Gemini: `generationConfig.temperature` (number) - ranges from 0.0 to 2.0 + */ + temperature?: number + + /** + * Nucleus sampling parameter. An alternative to temperature sampling. + * The model considers the results of tokens with topP probability mass. + * For example, 0.1 means only tokens comprising the top 10% probability mass are considered. + * + * Note: Generally recommended to use either temperature or topP, but not both. + * + * Provider usage: + * - OpenAI: `text.top_p` (number) + * - Anthropic: `top_p` (number | null) + * - Gemini: `generationConfig.topP` (number) + */ + topP?: number + + /** + * The maximum number of tokens to generate in the response. + * + * Provider usage: + * - OpenAI: `max_output_tokens` (number) - includes visible output and reasoning tokens + * - Anthropic: `max_tokens` (number, required) - range x >= 1 + * - Gemini: `generationConfig.maxOutputTokens` (number) + */ + maxTokens?: number + + /** + * Additional metadata to attach to the request. + * Can be used for tracking, debugging, or passing custom information. + * Structure and constraints vary by provider. + * + * Provider usage: + * - OpenAI: `metadata` (Record) - max 16 key-value pairs, keys max 64 chars, values max 512 chars + * - Anthropic: `metadata` (Record) - includes optional user_id (max 256 chars) + * - Gemini: Not directly available in TextProviderOptions + */ + metadata?: Record +} + +// =========================== +// Type Extraction Helpers +// =========================== + +/** Extract model types from a TextAdapter */ +export type TextModels = + TAdapter extends TextAdapter ? M[number] : string + +/** + * Extract model-specific provider options from a TextAdapter. + * If the model has specific options defined in ModelOptions (and not just via index signature), + * use those; otherwise fall back to base provider options. + */ +export type TextProviderOptionsForModel = + TAdapter extends TextAdapter< + any, + infer BaseOptions, + infer ModelOptions, + any, + any + > + ? string extends keyof ModelOptions + ? // ModelOptions is Record or has index signature - use BaseOptions + BaseOptions + : // ModelOptions has explicit keys - check if TModel is one of them + TModel extends keyof ModelOptions + ? ModelOptions[TModel] + : BaseOptions + : object + +/** + * Extract input modalities for a specific model from a TextAdapter. + * Returns the modalities array if the model is defined in the map, otherwise all modalities. + */ +export type InputModalitiesForModel = + TAdapter extends TextAdapter + ? TModel extends keyof ModalitiesByName + ? ModalitiesByName[TModel] + : ReadonlyArray + : ReadonlyArray + +/** + * Extract message metadata types by modality from a TextAdapter. + * Returns the adapter's metadata map or defaults if not defined. + */ +export type MessageMetadataForAdapter = + TAdapter extends TextAdapter + ? MetadataByModality + : DefaultMessageMetadataByModality + +// =========================== +// Activity Options Type +// =========================== + +/** + * Options for the text activity. + * + * @template TAdapter - The text adapter type + * @template TModel - The model name type (inferred from adapter) + * @template TSchema - Optional Zod schema for structured output + * @template TStream - Whether to stream the output (default: true) + */ +export interface TextActivityOptions< + TAdapter extends TextAdapter, object, any, any, any>, + TModel extends TextModels, + TSchema extends z.ZodType | undefined = undefined, + TStream extends boolean = true, +> { + /** The text adapter to use */ + adapter: TAdapter & { kind: typeof kind } + /** The model name (autocompletes based on adapter) */ + model: TModel + /** Conversation messages */ + messages: Array + /** System prompts to prepend to the conversation */ + systemPrompts?: TextOptions['systemPrompts'] + /** Tools for function calling (auto-executed when called) */ + tools?: TextOptions['tools'] + /** Additional options like temperature, maxTokens, etc. */ + options?: TextOptions['options'] + /** Provider-specific options */ + providerOptions?: TextProviderOptionsForModel + /** AbortController for cancellation */ + abortController?: TextOptions['abortController'] + /** Strategy for controlling the agent loop */ + agentLoopStrategy?: TextOptions['agentLoopStrategy'] + /** Unique conversation identifier for tracking */ + conversationId?: TextOptions['conversationId'] + /** + * Optional Zod schema for structured output. + * When provided, the activity will: + * 1. Run the full agentic loop (executing tools as needed) + * 2. Once complete, return a Promise with the parsed output matching the schema + * + * @example + * ```ts + * const result = await ai({ + * adapter: openaiText(), + * model: 'gpt-4o', + * messages: [{ role: 'user', content: 'Generate a person' }], + * outputSchema: z.object({ name: z.string(), age: z.number() }) + * }) + * // result is { name: string, age: number } + * ``` + */ + outputSchema?: TSchema + /** + * Whether to stream the text result. + * When true (default), returns an AsyncIterable for streaming output. + * When false, returns a Promise with the collected text content. + * + * Note: If outputSchema is provided, this option is ignored and the result + * is always a Promise>. + * + * @default true + * + * @example Non-streaming text + * ```ts + * const text = await ai({ + * adapter: openaiText(), + * model: 'gpt-4o', + * messages: [{ role: 'user', content: 'Hello!' }], + * stream: false + * }) + * // text is a string with the full response + * ``` + */ + stream?: TStream +} + +// =========================== +// Activity Result Type +// =========================== -interface ChatEngineConfig< - TAdapter extends AIAdapter, - TParams extends ChatOptions = ChatOptions, +/** + * Result type for the text activity. + * - If outputSchema is provided: Promise> + * - If stream is false: Promise + * - Otherwise (stream is true, default): AsyncIterable + */ +export type TextActivityResult< + TSchema extends z.ZodType | undefined, + TStream extends boolean = true, +> = TSchema extends z.ZodType + ? Promise> + : TStream extends false + ? Promise + : AsyncIterable + +// =========================== +// ChatEngine Implementation +// =========================== + +interface TextEngineConfig< + TAdapter extends TextAdapter, + TParams extends TextOptions = TextOptions, > { adapter: TAdapter systemPrompts?: Array @@ -28,11 +262,11 @@ interface ChatEngineConfig< } type ToolPhaseResult = 'continue' | 'stop' | 'wait' -type CyclePhase = 'processChat' | 'executeToolCalls' +type CyclePhase = 'processText' | 'executeToolCalls' -class ChatEngine< - TAdapter extends AIAdapter, - TParams extends ChatOptions = ChatOptions, +class TextEngine< + TAdapter extends TextAdapter, + TParams extends TextOptions = TextOptions, > { private readonly adapter: TAdapter private readonly params: TParams @@ -57,9 +291,9 @@ class ChatEngine< private shouldEmitStreamEnd = true private earlyTermination = false private toolPhase: ToolPhaseResult = 'continue' - private cyclePhase: CyclePhase = 'processChat' + private cyclePhase: CyclePhase = 'processText' - constructor(config: ChatEngineConfig) { + constructor(config: TextEngineConfig) { this.adapter = config.adapter this.params = config.params this.systemPrompts = config.params.systemPrompts || [] @@ -77,8 +311,18 @@ class ChatEngine< this.effectiveSignal = config.params.abortController?.signal } - async *chat(): AsyncGenerator { - this.beforeChat() + /** Get the accumulated content after the chat loop completes */ + getAccumulatedContent(): string { + return this.accumulatedContent + } + + /** Get the final messages array after the chat loop completes */ + getMessages(): Array { + return this.messages + } + + async *run(): AsyncGenerator { + this.beforeRun() try { const pendingPhase = yield* this.checkForPendingToolCalls() @@ -93,7 +337,7 @@ class ChatEngine< this.beginCycle() - if (this.cyclePhase === 'processChat') { + if (this.cyclePhase === 'processText') { yield* this.streamModelResponse() } else { yield* this.processToolCalls() @@ -102,16 +346,16 @@ class ChatEngine< this.endCycle() } while (this.shouldContinue()) } finally { - this.afterChat() + this.afterRun() } } - private beforeChat(): void { + private beforeRun(): void { this.streamStartTime = Date.now() const { model, tools, options, providerOptions, conversationId } = this.params - aiEventClient.emit('chat:started', { + aiEventClient.emit('text:started', { requestId: this.requestId, streamId: this.streamId, model: model, @@ -134,15 +378,15 @@ class ChatEngine< }) } - private afterChat(): void { + private afterRun(): void { if (!this.shouldEmitStreamEnd) { return } const now = Date.now() - // Emit chat:completed with final state - aiEventClient.emit('chat:completed', { + // Emit text:completed with final state + aiEventClient.emit('text:completed', { requestId: this.requestId, streamId: this.streamId, model: this.params.model, @@ -163,18 +407,18 @@ class ChatEngine< } private beginCycle(): void { - if (this.cyclePhase === 'processChat') { + if (this.cyclePhase === 'processText') { this.beginIteration() } } private endCycle(): void { - if (this.cyclePhase === 'processChat') { + if (this.cyclePhase === 'processText') { this.cyclePhase = 'executeToolCalls' return } - this.cyclePhase = 'processChat' + this.cyclePhase = 'processText' this.iterationCount++ } @@ -365,7 +609,7 @@ class ChatEngine< const doneChunk = this.createSyntheticDoneChunk() - aiEventClient.emit('chat:iteration', { + aiEventClient.emit('text:iteration', { requestId: this.requestId, streamId: this.streamId, iterationNumber: this.iterationCount + 1, @@ -431,7 +675,7 @@ class ChatEngine< return } - aiEventClient.emit('chat:iteration', { + aiEventClient.emit('text:iteration', { requestId: this.requestId, streamId: this.streamId, iterationNumber: this.iterationCount + 1, @@ -710,54 +954,136 @@ class ChatEngine< } } +// =========================== +// Activity Implementation +// =========================== + /** - * Standalone chat streaming function with type inference from adapter - * Returns an async iterable of StreamChunks for streaming responses - * Includes automatic tool execution loop + * Text activity - handles agentic text generation, one-shot text generation, and agentic structured output. * - * @param options Chat options - * @param options.adapter - AI adapter instance to use - * @param options.model - Model name (autocompletes based on adapter) - * @param options.messages - Conversation messages - * @param options.tools - Optional tools for function calling (auto-executed) - * @param options.agentLoopStrategy - Optional strategy for controlling tool execution loop + * This activity supports four modes: + * 1. **Streaming agentic text**: Stream responses with automatic tool execution + * 2. **Streaming one-shot text**: Simple streaming request/response without tools + * 3. **Non-streaming text**: Returns collected text as a string (stream: false) + * 4. **Agentic structured output**: Run tools, then return structured data * - * @example - * ```typescript - * const stream = chat({ - * adapter: openai(), - * model: 'gpt-4o', - * messages: [{ role: 'user', content: 'Hello!' }], - * tools: [weatherTool], // Optional: auto-executed when called - * }); + * @example Full agentic text (streaming with tools) + * ```ts + * import { ai } from '@tanstack/ai' + * import { openaiText } from '@tanstack/ai-openai' * - * for await (const chunk of stream) { + * for await (const chunk of ai({ + * adapter: openaiText(), + * model: 'gpt-4o', + * messages: [{ role: 'user', content: 'What is the weather?' }], + * tools: [weatherTool] + * })) { * if (chunk.type === 'content') { - * console.log(chunk.delta); + * console.log(chunk.delta) * } * } * ``` + * + * @example One-shot text (streaming without tools) + * ```ts + * for await (const chunk of ai({ + * adapter: openaiText(), + * model: 'gpt-4o', + * messages: [{ role: 'user', content: 'Hello!' }] + * })) { + * console.log(chunk) + * } + * ``` + * + * @example Non-streaming text (stream: false) + * ```ts + * const text = await ai({ + * adapter: openaiText(), + * model: 'gpt-4o', + * messages: [{ role: 'user', content: 'Hello!' }], + * stream: false + * }) + * // text is a string with the full response + * ``` + * + * @example Agentic structured output (tools + structured response) + * ```ts + * import { z } from 'zod' + * + * const result = await ai({ + * adapter: openaiText(), + * model: 'gpt-4o', + * messages: [{ role: 'user', content: 'Research and summarize the topic' }], + * tools: [researchTool, analyzeTool], + * outputSchema: z.object({ + * summary: z.string(), + * keyPoints: z.array(z.string()) + * }) + * }) + * // result is { summary: string, keyPoints: string[] } + * ``` */ -export async function* chat< - TAdapter extends AIAdapter, - const TModel extends TAdapter extends AIAdapter< - infer Models, - any, - any, - any, - any, - any - > - ? Models[number] - : string, +export function textActivity< + TAdapter extends TextAdapter, object, any, any, any>, + TModel extends TextModels, + TSchema extends z.ZodType | undefined = undefined, + TStream extends boolean = true, >( - options: ChatStreamOptionsForModel, + options: TextActivityOptions, +): TextActivityResult { + const { outputSchema, stream } = options + + // If outputSchema is provided, run agentic structured output + if (outputSchema) { + return runAgenticStructuredOutput( + options as unknown as TextActivityOptions< + TextAdapter, object, any, any, any>, + string, + z.ZodType, + boolean + >, + ) as TextActivityResult + } + + // If stream is explicitly false, run non-streaming text + if (stream === false) { + return runNonStreamingText( + options as unknown as TextActivityOptions< + TextAdapter, object, any, any, any>, + string, + undefined, + false + >, + ) as TextActivityResult + } + + // Otherwise, run streaming text (default) + return runStreamingText( + options as unknown as TextActivityOptions< + TextAdapter, object, any, any, any>, + string, + undefined, + true + >, + ) as TextActivityResult +} + +/** + * Run streaming text (agentic or one-shot depending on tools) + */ +async function* runStreamingText( + options: TextActivityOptions< + TextAdapter, object, any, any, any>, + string, + undefined, + true + >, ): AsyncIterable { - const { adapter, ...chatOptions } = options + const { adapter, ...textOptions } = options - const engine = new ChatEngine({ + const engine = new TextEngine({ adapter, - params: chatOptions as ChatOptions< + params: textOptions as TextOptions< string, Record, undefined, @@ -765,7 +1091,162 @@ export async function* chat< >, }) - for await (const chunk of engine.chat()) { + for await (const chunk of engine.run()) { yield chunk } } + +/** + * Run non-streaming text - collects all content and returns as a string. + * Runs the full agentic loop (if tools are provided) but returns collected text. + */ +function runNonStreamingText( + options: TextActivityOptions< + TextAdapter, object, any, any, any>, + string, + undefined, + false + >, +): Promise { + // Run the streaming text and collect all text using streamToText + const stream = runStreamingText( + options as unknown as TextActivityOptions< + TextAdapter, object, any, any, any>, + string, + undefined, + true + >, + ) + + return streamToText(stream) +} + +/** + * Run agentic structured output: + * 1. Execute the full agentic loop (with tools) + * 2. Once complete, call adapter.structuredOutput with the conversation context + * 3. Validate and return the structured result + */ +async function runAgenticStructuredOutput( + options: TextActivityOptions< + TextAdapter, object, any, any, any>, + string, + TSchema, + boolean + >, +): Promise> { + const { adapter, outputSchema, ...textOptions } = options + + if (!outputSchema) { + throw new Error('outputSchema is required for structured output') + } + + // Create the engine and run the agentic loop + const engine = new TextEngine({ + adapter, + params: textOptions as TextOptions< + string, + Record, + undefined, + Record + >, + }) + + // Consume the stream to run the agentic loop + for await (const _chunk of engine.run()) { + // Just consume the stream to execute the agentic loop + } + + // Get the final messages from the engine (includes tool results) + const finalMessages = engine.getMessages() + + // Build text options for structured output, excluding tools since + // the agentic loop is complete and we only need the final response + const { + tools: _tools, + agentLoopStrategy: _als, + ...structuredTextOptions + } = textOptions + + // Call the adapter's structured output method with the conversation context + // Each adapter is responsible for converting the Zod schema to its provider's format + const result = await adapter.structuredOutput({ + chatOptions: { + ...structuredTextOptions, + messages: finalMessages, + }, + outputSchema, + }) + + // Validate the result against the Zod schema + const validationResult = outputSchema.safeParse(result.data) + if (!validationResult.success) { + throw new Error( + `Structured output validation failed: ${validationResult.error.message}`, + ) + } + + return validationResult.data +} + +// =========================== +// Text Options Helper +// =========================== + +/** + * Type-safe helper to create text options with model-specific provider options. + * + * @example + * ```ts + * import { textOptions, ai } from '@tanstack/ai' + * import { openaiText } from '@tanstack/ai-openai' + * + * const opts = textOptions({ + * adapter: openaiText(), + * model: 'gpt-4o', + * options: { temperature: 0.7 } + * }) + * ``` + */ +export function textOptions< + TAdapter extends AIAdapter, + const TModel extends TAdapter extends AIAdapter< + infer Models, + any, + any, + any, + any + > + ? Models[number] + : string, +>( + options: Omit< + TextStreamOptionsUnion, + 'providerOptions' | 'model' | 'messages' | 'abortController' + > & { + adapter: TAdapter + model: TModel + providerOptions?: TAdapter extends AIAdapter< + any, + any, + any, + any, + infer ModelProviderOptions + > + ? TModel extends keyof ModelProviderOptions + ? ModelProviderOptions[TModel] + : never + : never + }, +): typeof options { + return options +} + +// Re-export adapter types +export type { + TextAdapter, + TextAdapterConfig, + StructuredOutputOptions, + StructuredOutputResult, +} from './adapter' +export { BaseTextAdapter } from './adapter' diff --git a/packages/typescript/ai/src/message-converters.ts b/packages/typescript/ai/src/activities/text/messages.ts similarity index 80% rename from packages/typescript/ai/src/message-converters.ts rename to packages/typescript/ai/src/activities/text/messages.ts index 90845fad..375b7241 100644 --- a/packages/typescript/ai/src/message-converters.ts +++ b/packages/typescript/ai/src/activities/text/messages.ts @@ -1,18 +1,76 @@ -/** - * Message Converters - * - * Functions for converting between UIMessage and ModelMessage formats. - */ - import type { + AIAdapter, + ConstrainedModelMessage, ContentPart, MessagePart, + Modality, ModelMessage, TextPart, ToolCallPart, ToolResultPart, UIMessage, -} from './types' +} from '../../types' + +/** + * Type-safe helper to create a messages array constrained by a model's supported modalities. + * + * This function provides compile-time checking that your messages only contain + * content types supported by the specified model. It's particularly useful when + * combining typed messages with untyped data (like from request.json()). + * + * @example + * ```typescript + * import { messages, chat } from '@tanstack/ai' + * import { openai } from '@tanstack/ai-openai' + * + * const adapter = openai() + * + * // This will error at compile time because gpt-4o only supports text+image + * const msgs = messages({ adapter, model: 'gpt-4o' }, [ + * { + * role: 'user', + * content: [ + * { type: 'video', source: { type: 'url', value: '...' } } // Error! + * ] + * } + * ]) + * ``` + */ +export function messages< + TAdapter extends AIAdapter, + const TModel extends TAdapter extends AIAdapter< + infer Models, + any, + any, + any, + any, + any + > + ? Models[number] + : string, +>( + _options: { adapter: TAdapter; model: TModel }, + msgs: TAdapter extends AIAdapter< + any, + any, + any, + any, + any, + infer ModelInputModalities + > + ? TModel extends keyof ModelInputModalities + ? ModelInputModalities[TModel] extends ReadonlyArray + ? Array> + : Array + : Array + : Array, +): typeof msgs { + return msgs +} + +// =========================== +// Message Converters +// =========================== /** * Helper to extract text content from string or ContentPart array @@ -65,11 +123,11 @@ export function convertMessagesToModelMessages( export function uiMessageToModelMessages( uiMessage: UIMessage, ): Array { - const messages: Array = [] + const messageList: Array = [] // Skip system messages - they're handled via systemPrompts, not ModelMessages if (uiMessage.role === 'system') { - return messages + return messageList } // Separate parts by type @@ -112,14 +170,14 @@ export function uiMessageToModelMessages( // Create the main message if (uiMessage.role !== 'assistant' || content || !toolCalls) { - messages.push({ + messageList.push({ role: uiMessage.role, content, ...(toolCalls && toolCalls.length > 0 && { toolCalls }), }) } else if (toolCalls.length > 0) { // Assistant message with only tool calls - messages.push({ + messageList.push({ role: 'assistant', content, toolCalls, @@ -132,7 +190,7 @@ export function uiMessageToModelMessages( toolResultPart.state === 'complete' || toolResultPart.state === 'error' ) { - messages.push({ + messageList.push({ role: 'tool', content: toolResultPart.content, toolCallId: toolResultPart.toolCallId, @@ -140,7 +198,7 @@ export function uiMessageToModelMessages( } } - return messages + return messageList } /** diff --git a/packages/typescript/ai/src/stream/index.ts b/packages/typescript/ai/src/activities/text/stream/index.ts similarity index 100% rename from packages/typescript/ai/src/stream/index.ts rename to packages/typescript/ai/src/activities/text/stream/index.ts diff --git a/packages/typescript/ai/src/stream/json-parser.ts b/packages/typescript/ai/src/activities/text/stream/json-parser.ts similarity index 100% rename from packages/typescript/ai/src/stream/json-parser.ts rename to packages/typescript/ai/src/activities/text/stream/json-parser.ts diff --git a/packages/typescript/ai/src/stream/message-updaters.ts b/packages/typescript/ai/src/activities/text/stream/message-updaters.ts similarity index 99% rename from packages/typescript/ai/src/stream/message-updaters.ts rename to packages/typescript/ai/src/activities/text/stream/message-updaters.ts index ab1d0b0b..5deb39f8 100644 --- a/packages/typescript/ai/src/stream/message-updaters.ts +++ b/packages/typescript/ai/src/activities/text/stream/message-updaters.ts @@ -10,7 +10,7 @@ import type { ToolCallPart, ToolResultPart, UIMessage, -} from '../types' +} from '../../../types' import type { ToolCallState, ToolResultState } from './types' /** diff --git a/packages/typescript/ai/src/stream/processor.ts b/packages/typescript/ai/src/activities/text/stream/processor.ts similarity index 99% rename from packages/typescript/ai/src/stream/processor.ts rename to packages/typescript/ai/src/activities/text/stream/processor.ts index d8441a33..3c827eab 100644 --- a/packages/typescript/ai/src/stream/processor.ts +++ b/packages/typescript/ai/src/activities/text/stream/processor.ts @@ -13,10 +13,7 @@ * - Recording/replay for testing * - Event-driven architecture for UI updates */ -import { - generateMessageId, - uiMessageToModelMessages, -} from '../message-converters' +import { generateMessageId, uiMessageToModelMessages } from '../messages.js' import { defaultJSONParser } from './json-parser' import { updateTextPart, @@ -43,7 +40,7 @@ import type { ToolCall, ToolCallPart, UIMessage, -} from '../types' +} from '../../../types' /** * Events emitted by the StreamProcessor diff --git a/packages/typescript/ai/src/stream/strategies.ts b/packages/typescript/ai/src/activities/text/stream/strategies.ts similarity index 100% rename from packages/typescript/ai/src/stream/strategies.ts rename to packages/typescript/ai/src/activities/text/stream/strategies.ts diff --git a/packages/typescript/ai/src/stream/types.ts b/packages/typescript/ai/src/activities/text/stream/types.ts similarity index 97% rename from packages/typescript/ai/src/stream/types.ts rename to packages/typescript/ai/src/activities/text/stream/types.ts index a1d3a52b..60f3e767 100644 --- a/packages/typescript/ai/src/stream/types.ts +++ b/packages/typescript/ai/src/activities/text/stream/types.ts @@ -5,7 +5,7 @@ * The canonical chunk format is StreamChunk from @tanstack/ai types. */ -import type { StreamChunk, ToolCall } from '../types' +import type { StreamChunk, ToolCall } from '../../../types' /** * Tool call states - track the lifecycle of a tool call diff --git a/packages/typescript/ai/src/tools/tool-calls.ts b/packages/typescript/ai/src/activities/text/tools/tool-calls.ts similarity index 99% rename from packages/typescript/ai/src/tools/tool-calls.ts rename to packages/typescript/ai/src/activities/text/tools/tool-calls.ts index 48287245..333b1b55 100644 --- a/packages/typescript/ai/src/tools/tool-calls.ts +++ b/packages/typescript/ai/src/activities/text/tools/tool-calls.ts @@ -5,7 +5,7 @@ import type { Tool, ToolCall, ToolResultStreamChunk, -} from '../types' +} from '../../../types' import type { z } from 'zod' /** diff --git a/packages/typescript/ai/src/tools/tool-definition.ts b/packages/typescript/ai/src/activities/text/tools/tool-definition.ts similarity index 98% rename from packages/typescript/ai/src/tools/tool-definition.ts rename to packages/typescript/ai/src/activities/text/tools/tool-definition.ts index 7b6fb6bc..5384e9a6 100644 --- a/packages/typescript/ai/src/tools/tool-definition.ts +++ b/packages/typescript/ai/src/activities/text/tools/tool-definition.ts @@ -1,5 +1,10 @@ import type { z } from 'zod' -import type { InferSchemaType, JSONSchema, SchemaInput, Tool } from '../types' +import type { + InferSchemaType, + JSONSchema, + SchemaInput, + Tool, +} from '../../../types' /** * Marker type for server-side tools diff --git a/packages/typescript/ai/src/activities/text/tools/zod-converter.ts b/packages/typescript/ai/src/activities/text/tools/zod-converter.ts new file mode 100644 index 00000000..a512198d --- /dev/null +++ b/packages/typescript/ai/src/activities/text/tools/zod-converter.ts @@ -0,0 +1,235 @@ +import { toJSONSchema } from 'zod' +import type { z } from 'zod' +import type { SchemaInput } from '../../../types' + +/** + * Check if a value is a Zod schema by looking for Zod-specific internals. + * Zod schemas have a `_zod` property that contains metadata. + */ +function isZodSchema(schema: unknown): schema is z.ZodType { + return ( + typeof schema === 'object' && + schema !== null && + '_zod' in schema && + typeof (schema as any)._zod === 'object' + ) +} + +/** + * Transform a JSON schema to be compatible with OpenAI's structured output requirements. + * OpenAI requires: + * - All properties must be in the `required` array + * - Optional fields should have null added to their type union + * - additionalProperties must be false for objects + * + * @param schema - JSON schema to transform + * @param originalRequired - Original required array (to know which fields were optional) + * @returns Transformed schema compatible with OpenAI structured output + */ +function makeStructuredOutputCompatible( + schema: Record, + originalRequired: Array = [], +): Record { + const result = { ...schema } + + // Handle object types + if (result.type === 'object' && result.properties) { + const properties = { ...result.properties } + const allPropertyNames = Object.keys(properties) + + // Transform each property + for (const propName of allPropertyNames) { + const prop = properties[propName] + const wasOptional = !originalRequired.includes(propName) + + // Recursively transform nested objects/arrays + if (prop.type === 'object' && prop.properties) { + properties[propName] = makeStructuredOutputCompatible( + prop, + prop.required || [], + ) + } else if (prop.type === 'array' && prop.items) { + properties[propName] = { + ...prop, + items: makeStructuredOutputCompatible( + prop.items, + prop.items.required || [], + ), + } + } else if (wasOptional) { + // Make optional fields nullable by adding null to the type + if (prop.type && !Array.isArray(prop.type)) { + properties[propName] = { + ...prop, + type: [prop.type, 'null'], + } + } else if (Array.isArray(prop.type) && !prop.type.includes('null')) { + properties[propName] = { + ...prop, + type: [...prop.type, 'null'], + } + } + } + } + + result.properties = properties + // ALL properties must be required for OpenAI structured output + result.required = allPropertyNames + // additionalProperties must be false + result.additionalProperties = false + } + + // Handle array types with object items + if (result.type === 'array' && result.items) { + result.items = makeStructuredOutputCompatible( + result.items, + result.items.required || [], + ) + } + + return result +} + +/** + * Options for schema conversion + */ +export interface ConvertSchemaOptions { + /** + * When true, transforms the schema to be compatible with OpenAI's structured output requirements: + * - All properties are added to the `required` array + * - Optional fields get null added to their type union + * - additionalProperties is set to false for all objects + * + * @default false + */ + forStructuredOutput?: boolean +} + +/** + * Converts a schema (Zod or JSONSchema) to JSON Schema format compatible with LLM providers. + * If the input is already a JSONSchema object, it is returned as-is. + * If the input is a Zod schema, it is converted to JSON Schema. + * + * @param schema - Zod schema or JSONSchema object to convert + * @param options - Conversion options + * @returns JSON Schema object that can be sent to LLM providers + * + * @example + * ```typescript + * import { z } from 'zod'; + * + * // Using Zod schema + * const zodSchema = z.object({ + * location: z.string().describe('City name'), + * unit: z.enum(['celsius', 'fahrenheit']).optional() + * }); + * + * const jsonSchema = convertZodToJsonSchema(zodSchema); + * // Returns: + * // { + * // type: 'object', + * // properties: { + * // location: { type: 'string', description: 'City name' }, + * // unit: { type: 'string', enum: ['celsius', 'fahrenheit'] } + * // }, + * // required: ['location'] + * // } + * + * // For OpenAI structured output (all fields required, optional fields nullable) + * const structuredSchema = convertZodToJsonSchema(zodSchema, { forStructuredOutput: true }); + * // Returns: + * // { + * // type: 'object', + * // properties: { + * // location: { type: 'string', description: 'City name' }, + * // unit: { type: ['string', 'null'], enum: ['celsius', 'fahrenheit'] } + * // }, + * // required: ['location', 'unit'], + * // additionalProperties: false + * // } + * + * // Using JSONSchema directly (passes through unchanged) + * const rawSchema = { + * type: 'object', + * properties: { location: { type: 'string' } }, + * required: ['location'] + * }; + * const result = convertZodToJsonSchema(rawSchema); + * // Returns the same object + * ``` + */ +export function convertZodToJsonSchema( + schema: SchemaInput | undefined, + options: ConvertSchemaOptions = {}, +): Record | undefined { + if (!schema) return undefined + + const { forStructuredOutput = false } = options + + // If it's not a Zod schema, assume it's already a JSONSchema and pass through + if (!isZodSchema(schema)) { + // Still apply structured output transformation if requested + if (forStructuredOutput && typeof schema === 'object') { + return makeStructuredOutputCompatible( + schema, + (schema as any).required || [], + ) + } + return schema + } + + // Use Alcyone Labs fork which is compatible with Zod v4 + const jsonSchema = toJSONSchema(schema, { + target: 'openapi-3.0', + reused: 'ref', + }) + + // Remove $schema property as it's not needed for LLM providers + let result = jsonSchema + if (typeof result === 'object' && '$schema' in result) { + const { $schema, ...rest } = result + result = rest + } + + // Ensure object schemas always have type: "object" + // This fixes cases where zod-to-json-schema doesn't set type for empty objects + if (typeof result === 'object') { + // Check if the input schema is a ZodObject by inspecting its internal structure + const isZodObject = + typeof schema === 'object' && + 'def' in schema && + schema.def.type === 'object' + + // If we know it's a ZodObject but result doesn't have type, set it + if (isZodObject && !result.type) { + result.type = 'object' + } + + // If result is completely empty (no keys), it's likely an empty object schema + if (Object.keys(result).length === 0) { + result.type = 'object' + } + + // If it has properties (even empty), it should be an object type + if ('properties' in result && !result.type) { + result.type = 'object' + } + + // Ensure properties exists for object types (even if empty) + if (result.type === 'object' && !('properties' in result)) { + result.properties = {} + } + + // Ensure required exists for object types (even if empty array) + if (result.type === 'object' && !('required' in result)) { + result.required = [] + } + + // Apply structured output transformation if requested + if (forStructuredOutput) { + result = makeStructuredOutputCompatible(result, result.required || []) + } + } + + return result +} diff --git a/packages/typescript/ai/src/activities/transcription/adapter.ts b/packages/typescript/ai/src/activities/transcription/adapter.ts new file mode 100644 index 00000000..f81c2560 --- /dev/null +++ b/packages/typescript/ai/src/activities/transcription/adapter.ts @@ -0,0 +1,74 @@ +import type { TranscriptionOptions, TranscriptionResult } from '../../types' + +/** + * Configuration for transcription adapter instances + */ +export interface TranscriptionAdapterConfig { + apiKey?: string + baseUrl?: string + timeout?: number + maxRetries?: number + headers?: Record +} + +/** + * Base interface for audio transcription adapters. + * Provides type-safe transcription functionality with support for + * model-specific provider options. + * + * Generic parameters: + * - TModels: Array of supported transcription model names + * - TProviderOptions: Base provider-specific options for transcription + */ +export interface TranscriptionAdapter< + TModels extends ReadonlyArray = ReadonlyArray, + TProviderOptions extends object = Record, +> { + /** Discriminator for adapter kind - used by ai() to determine API shape */ + readonly kind: 'transcription' + /** Adapter name identifier */ + readonly name: string + /** Supported transcription models */ + readonly models: TModels + + // Type-only properties for type inference + /** @internal Type-only property for provider options inference */ + _providerOptions?: TProviderOptions + + /** + * Transcribe audio to text + */ + transcribe: ( + options: TranscriptionOptions, + ) => Promise +} + +/** + * Abstract base class for audio transcription adapters. + * Extend this class to implement a transcription adapter for a specific provider. + */ +export abstract class BaseTranscriptionAdapter< + TModels extends ReadonlyArray = ReadonlyArray, + TProviderOptions extends object = Record, +> implements TranscriptionAdapter { + readonly kind = 'transcription' as const + abstract readonly name: string + abstract readonly models: TModels + + // Type-only properties - never assigned at runtime + declare _providerOptions?: TProviderOptions + + protected config: TranscriptionAdapterConfig + + constructor(config: TranscriptionAdapterConfig = {}) { + this.config = config + } + + abstract transcribe( + options: TranscriptionOptions, + ): Promise + + protected generateId(): string { + return `${this.name}-${Date.now()}-${Math.random().toString(36).substring(7)}` + } +} diff --git a/packages/typescript/ai/src/activities/transcription/index.ts b/packages/typescript/ai/src/activities/transcription/index.ts new file mode 100644 index 00000000..2a8a1c4d --- /dev/null +++ b/packages/typescript/ai/src/activities/transcription/index.ts @@ -0,0 +1,125 @@ +/** + * Transcription Activity + * + * Transcribes audio to text using speech-to-text models. + * This is a self-contained module with implementation, types, and JSDoc. + */ + +import type { TranscriptionAdapter } from './adapter' +import type { TranscriptionResult } from '../../types' + +// =========================== +// Activity Kind +// =========================== + +/** The adapter kind this activity handles */ +export const kind = 'transcription' as const + +// =========================== +// Type Extraction Helpers +// =========================== + +/** Extract model types from a TranscriptionAdapter */ +export type TranscriptionModels = + TAdapter extends TranscriptionAdapter ? M[number] : string + +/** + * Extract provider options from a TranscriptionAdapter. + */ +export type TranscriptionProviderOptions = + TAdapter extends TranscriptionAdapter + ? TProviderOptions + : object + +// =========================== +// Activity Options Type +// =========================== + +/** + * Options for the transcription activity. + * + * @template TAdapter - The transcription adapter type + * @template TModel - The model name type (inferred from adapter) + */ +export interface TranscriptionActivityOptions< + TAdapter extends TranscriptionAdapter, object>, + TModel extends TranscriptionModels, +> { + /** The transcription adapter to use */ + adapter: TAdapter & { kind: typeof kind } + /** The model name (autocompletes based on adapter) */ + model: TModel + /** The audio data to transcribe - can be base64 string, File, Blob, or Buffer */ + audio: string | File | Blob | ArrayBuffer + /** The language of the audio in ISO-639-1 format (e.g., 'en') */ + language?: string + /** An optional prompt to guide the transcription */ + prompt?: string + /** The format of the transcription output */ + responseFormat?: 'json' | 'text' | 'srt' | 'verbose_json' | 'vtt' + /** Provider-specific options for transcription */ + providerOptions?: TranscriptionProviderOptions +} + +// =========================== +// Activity Result Type +// =========================== + +/** Result type for the transcription activity */ +export type TranscriptionActivityResult = Promise + +// =========================== +// Activity Implementation +// =========================== + +/** + * Transcription activity - converts audio to text. + * + * Uses AI speech-to-text models to transcribe audio content. + * + * @example Transcribe an audio file + * ```ts + * import { ai } from '@tanstack/ai' + * import { openaiTranscription } from '@tanstack/ai-openai' + * + * const result = await ai({ + * adapter: openaiTranscription(), + * model: 'whisper-1', + * audio: audioFile, // File, Blob, or base64 string + * language: 'en' + * }) + * + * console.log(result.text) + * ``` + * + * @example With verbose output for timestamps + * ```ts + * const result = await ai({ + * adapter: openaiTranscription(), + * model: 'whisper-1', + * audio: audioFile, + * responseFormat: 'verbose_json' + * }) + * + * result.segments?.forEach(segment => { + * console.log(`[${segment.start}s - ${segment.end}s]: ${segment.text}`) + * }) + * ``` + */ +export async function transcriptionActivity< + TAdapter extends TranscriptionAdapter, object>, + TModel extends TranscriptionModels, +>( + options: TranscriptionActivityOptions, +): TranscriptionActivityResult { + const { adapter, ...rest } = options + + return adapter.transcribe(rest) +} + +// Re-export adapter types +export type { + TranscriptionAdapter, + TranscriptionAdapterConfig, +} from './adapter' +export { BaseTranscriptionAdapter } from './adapter' diff --git a/packages/typescript/ai/src/activities/tts/adapter.ts b/packages/typescript/ai/src/activities/tts/adapter.ts new file mode 100644 index 00000000..17c7ab74 --- /dev/null +++ b/packages/typescript/ai/src/activities/tts/adapter.ts @@ -0,0 +1,72 @@ +import type { TTSOptions, TTSResult } from '../../types' + +/** + * Configuration for TTS adapter instances + */ +export interface TTSAdapterConfig { + apiKey?: string + baseUrl?: string + timeout?: number + maxRetries?: number + headers?: Record +} + +/** + * Base interface for text-to-speech adapters. + * Provides type-safe TTS functionality with support for + * model-specific provider options. + * + * Generic parameters: + * - TModels: Array of supported TTS model names + * - TProviderOptions: Base provider-specific options for TTS generation + */ +export interface TTSAdapter< + TModels extends ReadonlyArray = ReadonlyArray, + TProviderOptions extends object = Record, +> { + /** Discriminator for adapter kind - used by ai() to determine API shape */ + readonly kind: 'tts' + /** Adapter name identifier */ + readonly name: string + /** Supported TTS models */ + readonly models: TModels + + // Type-only properties for type inference + /** @internal Type-only property for provider options inference */ + _providerOptions?: TProviderOptions + + /** + * Generate speech from text + */ + generateSpeech: (options: TTSOptions) => Promise +} + +/** + * Abstract base class for text-to-speech adapters. + * Extend this class to implement a TTS adapter for a specific provider. + */ +export abstract class BaseTTSAdapter< + TModels extends ReadonlyArray = ReadonlyArray, + TProviderOptions extends object = Record, +> implements TTSAdapter { + readonly kind = 'tts' as const + abstract readonly name: string + abstract readonly models: TModels + + // Type-only properties - never assigned at runtime + declare _providerOptions?: TProviderOptions + + protected config: TTSAdapterConfig + + constructor(config: TTSAdapterConfig = {}) { + this.config = config + } + + abstract generateSpeech( + options: TTSOptions, + ): Promise + + protected generateId(): string { + return `${this.name}-${Date.now()}-${Math.random().toString(36).substring(7)}` + } +} diff --git a/packages/typescript/ai/src/activities/tts/index.ts b/packages/typescript/ai/src/activities/tts/index.ts new file mode 100644 index 00000000..eaf72d74 --- /dev/null +++ b/packages/typescript/ai/src/activities/tts/index.ts @@ -0,0 +1,118 @@ +/** + * TTS Activity + * + * Generates speech audio from text using text-to-speech models. + * This is a self-contained module with implementation, types, and JSDoc. + */ + +import type { TTSAdapter } from './adapter' +import type { TTSResult } from '../../types' + +// =========================== +// Activity Kind +// =========================== + +/** The adapter kind this activity handles */ +export const kind = 'tts' as const + +// =========================== +// Type Extraction Helpers +// =========================== + +/** Extract model types from a TTSAdapter */ +export type TTSModels = + TAdapter extends TTSAdapter ? M[number] : string + +/** + * Extract provider options from a TTSAdapter. + */ +export type TTSProviderOptions = + TAdapter extends TTSAdapter + ? TProviderOptions + : object + +// =========================== +// Activity Options Type +// =========================== + +/** + * Options for the TTS activity. + * + * @template TAdapter - The TTS adapter type + * @template TModel - The model name type (inferred from adapter) + */ +export interface TTSActivityOptions< + TAdapter extends TTSAdapter, object>, + TModel extends TTSModels, +> { + /** The TTS adapter to use */ + adapter: TAdapter & { kind: typeof kind } + /** The model name (autocompletes based on adapter) */ + model: TModel + /** The text to convert to speech */ + text: string + /** The voice to use for generation */ + voice?: string + /** The output audio format */ + format?: 'mp3' | 'opus' | 'aac' | 'flac' | 'wav' | 'pcm' + /** The speed of the generated audio (0.25 to 4.0) */ + speed?: number + /** Provider-specific options for TTS generation */ + providerOptions?: TTSProviderOptions +} + +// =========================== +// Activity Result Type +// =========================== + +/** Result type for the TTS activity */ +export type TTSActivityResult = Promise + +// =========================== +// Activity Implementation +// =========================== + +/** + * TTS activity - generates speech from text. + * + * Uses AI text-to-speech models to create audio from natural language text. + * + * @example Generate speech from text + * ```ts + * import { ai } from '@tanstack/ai' + * import { openaiTTS } from '@tanstack/ai-openai' + * + * const result = await ai({ + * adapter: openaiTTS(), + * model: 'tts-1-hd', + * text: 'Hello, welcome to TanStack AI!', + * voice: 'nova' + * }) + * + * console.log(result.audio) // base64-encoded audio + * ``` + * + * @example With format and speed options + * ```ts + * const result = await ai({ + * adapter: openaiTTS(), + * model: 'tts-1', + * text: 'This is slower speech.', + * voice: 'alloy', + * format: 'wav', + * speed: 0.8 + * }) + * ``` + */ +export async function ttsActivity< + TAdapter extends TTSAdapter, object>, + TModel extends TTSModels, +>(options: TTSActivityOptions): TTSActivityResult { + const { adapter, ...rest } = options + + return adapter.generateSpeech(rest) +} + +// Re-export adapter types +export type { TTSAdapter, TTSAdapterConfig } from './adapter' +export { BaseTTSAdapter } from './adapter' diff --git a/packages/typescript/ai/src/activities/video/adapter.ts b/packages/typescript/ai/src/activities/video/adapter.ts new file mode 100644 index 00000000..f5a55d70 --- /dev/null +++ b/packages/typescript/ai/src/activities/video/adapter.ts @@ -0,0 +1,101 @@ +import type { + VideoGenerationOptions, + VideoJobResult, + VideoStatusResult, + VideoUrlResult, +} from '../../types' + +/** + * Configuration for video adapter instances + * + * @experimental Video generation is an experimental feature and may change. + */ +export interface VideoAdapterConfig { + apiKey?: string + baseUrl?: string + timeout?: number + maxRetries?: number + headers?: Record +} + +/** + * Base interface for video generation adapters. + * Provides type-safe video generation functionality with support for + * job-based async operations (create, poll status, get URL). + * + * @experimental Video generation is an experimental feature and may change. + * + * Generic parameters: + * - TModels: Array of supported video model names + * - TProviderOptions: Base provider-specific options for video generation + */ +export interface VideoAdapter< + TModels extends ReadonlyArray = ReadonlyArray, + TProviderOptions extends object = Record, +> { + /** Discriminator for adapter kind - used by ai() to determine API shape */ + readonly kind: 'video' + /** Adapter name identifier */ + readonly name: string + /** Supported video generation models */ + readonly models: TModels + + // Type-only properties for type inference + /** @internal Type-only property for provider options inference */ + _providerOptions?: TProviderOptions + + /** + * Create a new video generation job. + * Returns a job ID that can be used to poll for status and retrieve the video. + */ + createVideoJob: ( + options: VideoGenerationOptions, + ) => Promise + + /** + * Get the current status of a video generation job. + */ + getVideoStatus: (jobId: string) => Promise + + /** + * Get the URL to download/view the generated video. + * Should only be called after status is 'completed'. + */ + getVideoUrl: (jobId: string) => Promise +} + +/** + * Abstract base class for video generation adapters. + * Extend this class to implement a video adapter for a specific provider. + * + * @experimental Video generation is an experimental feature and may change. + */ +export abstract class BaseVideoAdapter< + TModels extends ReadonlyArray = ReadonlyArray, + TProviderOptions extends object = Record, +> implements VideoAdapter { + readonly kind = 'video' as const + abstract readonly name: string + abstract readonly models: TModels + + // Type-only properties - never assigned at runtime + declare _providerOptions?: TProviderOptions + + protected config: VideoAdapterConfig + + constructor(config: VideoAdapterConfig = {}) { + this.config = config + } + + abstract createVideoJob( + options: VideoGenerationOptions, + ): Promise + + abstract getVideoStatus(jobId: string): Promise + + abstract getVideoUrl(jobId: string): Promise + + protected generateId(): string { + return `${this.name}-${Date.now()}-${Math.random().toString(36).substring(7)}` + } +} diff --git a/packages/typescript/ai/src/activities/video/index.ts b/packages/typescript/ai/src/activities/video/index.ts new file mode 100644 index 00000000..3ed876ec --- /dev/null +++ b/packages/typescript/ai/src/activities/video/index.ts @@ -0,0 +1,230 @@ +/** + * Video Activity (Experimental) + * + * Generates videos from text prompts using a jobs/polling architecture. + * This is a self-contained module with implementation, types, and JSDoc. + * + * @experimental Video generation is an experimental feature and may change. + */ + +import type { VideoAdapter } from './adapter' +import type { + VideoJobResult, + VideoStatusResult, + VideoUrlResult, +} from '../../types' + +// =========================== +// Activity Kind +// =========================== + +/** The adapter kind this activity handles */ +export const kind = 'video' as const + +// =========================== +// Type Extraction Helpers +// =========================== + +/** Extract model types from a VideoAdapter */ +export type VideoModels = + TAdapter extends VideoAdapter ? M[number] : string + +/** + * Extract provider options from a VideoAdapter. + */ +export type VideoProviderOptions = + TAdapter extends VideoAdapter + ? TProviderOptions + : object + +// =========================== +// Activity Options Types +// =========================== + +/** + * Base options shared by all video activity operations. + */ +interface VideoActivityBaseOptions< + TAdapter extends VideoAdapter, object>, + TModel extends VideoModels, +> { + /** The video adapter to use */ + adapter: TAdapter & { kind: typeof kind } + /** The model name (autocompletes based on adapter) */ + model: TModel +} + +/** + * Options for creating a new video generation job. + * + * @experimental Video generation is an experimental feature and may change. + */ +export interface VideoCreateOptions< + TAdapter extends VideoAdapter, object>, + TModel extends VideoModels, +> extends VideoActivityBaseOptions { + /** Request type - create a new job (default if not specified) */ + request?: 'create' + /** Text description of the desired video */ + prompt: string + /** Video size in WIDTHxHEIGHT format (e.g., "1280x720") */ + size?: string + /** Video duration in seconds */ + duration?: number + /** Provider-specific options for video generation */ + providerOptions?: VideoProviderOptions +} + +/** + * Options for polling the status of a video generation job. + * + * @experimental Video generation is an experimental feature and may change. + */ +export interface VideoStatusOptions< + TAdapter extends VideoAdapter, object>, + TModel extends VideoModels, +> extends VideoActivityBaseOptions { + /** Request type - get job status */ + request: 'status' + /** The job ID to check status for */ + jobId: string +} + +/** + * Options for getting the URL of a completed video. + * + * @experimental Video generation is an experimental feature and may change. + */ +export interface VideoUrlOptions< + TAdapter extends VideoAdapter, object>, + TModel extends VideoModels, +> extends VideoActivityBaseOptions { + /** Request type - get video URL */ + request: 'url' + /** The job ID to get URL for */ + jobId: string +} + +/** + * Union type for all video activity options. + * Discriminated by the `request` field. + * + * @experimental Video generation is an experimental feature and may change. + */ +export type VideoActivityOptions< + TAdapter extends VideoAdapter, object>, + TModel extends VideoModels, + TRequest extends 'create' | 'status' | 'url' = 'create', +> = TRequest extends 'status' + ? VideoStatusOptions + : TRequest extends 'url' + ? VideoUrlOptions + : VideoCreateOptions + +// =========================== +// Activity Result Types +// =========================== + +/** + * Result type for the video activity, based on request type. + * + * @experimental Video generation is an experimental feature and may change. + */ +export type VideoActivityResult< + TRequest extends 'create' | 'status' | 'url' = 'create', +> = TRequest extends 'status' + ? Promise + : TRequest extends 'url' + ? Promise + : Promise + +// =========================== +// Activity Implementation +// =========================== + +/** + * Video activity - generates videos from text prompts using a jobs/polling pattern. + * + * Uses AI video generation models to create videos based on natural language descriptions. + * Unlike image generation, video generation is asynchronous and requires polling for completion. + * + * @experimental Video generation is an experimental feature and may change. + * + * @example Create a video generation job + * ```ts + * import { ai } from '@tanstack/ai' + * import { openaiVideo } from '@tanstack/ai-openai' + * + * // Start a video generation job + * const { jobId } = await ai({ + * adapter: openaiVideo(), + * model: 'sora-2', + * prompt: 'A cat chasing a dog in a sunny park' + * }) + * + * console.log('Job started:', jobId) + * ``` + * + * @example Poll for job status + * ```ts + * // Check status of the job + * const status = await ai({ + * adapter: openaiVideo(), + * model: 'sora-2', + * jobId, + * request: 'status' + * }) + * + * console.log('Status:', status.status, 'Progress:', status.progress) + * ``` + * + * @example Get the video URL when complete + * ```ts + * // Get the video URL (after status is 'completed') + * const { url } = await ai({ + * adapter: openaiVideo(), + * model: 'sora-2', + * jobId, + * request: 'url' + * }) + * + * console.log('Video URL:', url) + * ``` + */ +export async function videoActivity< + TAdapter extends VideoAdapter, object>, + TModel extends VideoModels, +>( + options: + | VideoCreateOptions + | VideoStatusOptions + | VideoUrlOptions, +): Promise { + const { adapter, request = 'create' } = options + + switch (request) { + case 'status': { + const statusOptions = options as VideoStatusOptions + return adapter.getVideoStatus(statusOptions.jobId) + } + case 'url': { + const urlOptions = options as VideoUrlOptions + return adapter.getVideoUrl(urlOptions.jobId) + } + case 'create': + default: { + const createOptions = options as VideoCreateOptions + return adapter.createVideoJob({ + model: createOptions.model, + prompt: createOptions.prompt, + size: createOptions.size, + duration: createOptions.duration, + providerOptions: createOptions.providerOptions, + }) + } + } +} + +// Re-export adapter types +export type { VideoAdapter, VideoAdapterConfig } from './adapter' +export { BaseVideoAdapter } from './adapter' diff --git a/packages/typescript/ai/src/ai.ts b/packages/typescript/ai/src/ai.ts new file mode 100644 index 00000000..079b14e6 --- /dev/null +++ b/packages/typescript/ai/src/ai.ts @@ -0,0 +1,339 @@ +/** + * @module ai + * + * Unified ai function that infers its entire API from the adapter's kind. + * Uses conditional types to ensure proper type checking based on adapter kind and options. + */ + +import { activityMap } from './activities' +import type { + AIEmbeddingOptions, + AIImageOptions, + AIOptionsUnion, + AIResultUnion, + AISummarizeOptions, + AITextOptions, + AIVideoCreateOptions, + AIVideoStatusOptions, + AIVideoUrlOptions, + AnyAIAdapter, + EmbeddingModels, + ImageModels, + SummarizeModels, + TextModels, + VideoModels, +} from './activities' +import type { TextAdapter } from './activities/text/adapter' +import type { EmbeddingAdapter } from './activities/embedding/adapter' +import type { SummarizeAdapter } from './activities/summarize/adapter' +import type { ImageAdapter } from './activities/image/adapter' +import type { VideoAdapter } from './activities/video/adapter' +import type { z } from 'zod' +import type { + EmbeddingResult, + ImageGenerationResult, + StreamChunk, + SummarizationResult, + VideoJobResult, + VideoStatusResult, + VideoUrlResult, +} from './types' + +// =========================== +// Adapter Union Type +// =========================== + +/** Union of all adapter types that can be passed to ai() */ +export type GenerateAdapter = + | TextAdapter, object, any, any, any> + | EmbeddingAdapter, object> + | SummarizeAdapter, object> + | ImageAdapter, object, any, any> + | VideoAdapter, object> + +/** Alias for backwards compatibility */ +export type AnyAdapter = GenerateAdapter + +// =========================== +// Local Type Aliases +// =========================== + +// Alias imported types to internal names for consistency in this file +type ExtractTextModels = TextModels +type ExtractEmbeddingModels = EmbeddingModels +type ExtractSummarizeModels = SummarizeModels +type ExtractImageModels = ImageModels +type ExtractVideoModels = VideoModels + +// =========================== +// Options/Return Type Mapping +// =========================== + +type AIOptionsFor< + TAdapter extends AnyAIAdapter, + TModel extends string, + TSchema extends z.ZodType | undefined = undefined, + TTextStream extends boolean = true, + TSummarizeStream extends boolean = false, + TVideoRequest extends 'create' | 'status' | 'url' = 'create', +> = TAdapter extends { kind: 'text' } + ? AITextOptions< + Extract< + TAdapter, + TextAdapter, object, any, any, any> + >, + TModel & ExtractTextModels, + TSchema, + TTextStream + > + : TAdapter extends { kind: 'embedding' } + ? AIEmbeddingOptions< + Extract, object>>, + TModel & ExtractEmbeddingModels + > + : TAdapter extends { kind: 'summarize' } + ? AISummarizeOptions< + Extract, object>>, + TModel & ExtractSummarizeModels, + TSummarizeStream + > + : TAdapter extends { kind: 'image' } + ? AIImageOptions< + Extract< + TAdapter, + ImageAdapter, object, any, any> + >, + TModel & ExtractImageModels + > + : TAdapter extends { kind: 'video' } + ? TVideoRequest extends 'status' + ? AIVideoStatusOptions< + Extract, object>>, + TModel & ExtractVideoModels + > + : TVideoRequest extends 'url' + ? AIVideoUrlOptions< + Extract< + TAdapter, + VideoAdapter, object> + >, + TModel & ExtractVideoModels + > + : AIVideoCreateOptions< + Extract< + TAdapter, + VideoAdapter, object> + >, + TModel & ExtractVideoModels + > + : never + +type AIReturnFor< + TAdapter extends AnyAIAdapter, + TSchema extends z.ZodType | undefined = undefined, + TTextStream extends boolean = true, + TSummarizeStream extends boolean = false, + TVideoRequest extends 'create' | 'status' | 'url' = 'create', +> = TAdapter extends { kind: 'text' } + ? TSchema extends z.ZodType + ? Promise> + : TTextStream extends false + ? Promise + : AsyncIterable + : TAdapter extends { kind: 'embedding' } + ? Promise + : TAdapter extends { kind: 'summarize' } + ? TSummarizeStream extends true + ? AsyncIterable + : Promise + : TAdapter extends { kind: 'image' } + ? Promise + : TAdapter extends { kind: 'video' } + ? TVideoRequest extends 'status' + ? Promise + : TVideoRequest extends 'url' + ? Promise + : Promise + : never + +// =========================== +// AI Function +// =========================== + +/** + * Unified AI function - routes to the appropriate activity based on adapter kind. + * + * This is the main entry point for all AI operations. The adapter's `kind` property + * determines which activity is executed: + * - `'text'` → Text activity (streaming, tools, structured output) + * - `'embedding'` → Embedding activity (vector generation) + * - `'summarize'` → Summarize activity (text summarization) + * - `'image'` → Image activity (image generation) + * - `'video'` → Video activity (video generation via jobs/polling) [experimental] + * + * @example Chat generation (streaming) + * ```ts + * import { ai } from '@tanstack/ai' + * import { openaiText } from '@tanstack/ai-openai' + * + * for await (const chunk of ai({ + * adapter: openaiText(), + * model: 'gpt-4o', + * messages: [{ role: 'user', content: 'Hello!' }] + * })) { + * console.log(chunk) + * } + * ``` + * + * @example Chat with tools (agentic) + * ```ts + * for await (const chunk of ai({ + * adapter: openaiText(), + * model: 'gpt-4o', + * messages: [{ role: 'user', content: 'What is the weather?' }], + * tools: [weatherTool] + * })) { + * console.log(chunk) + * } + * ``` + * + * @example Structured output (with or without tools) + * ```ts + * import { z } from 'zod' + * + * const result = await ai({ + * adapter: openaiText(), + * model: 'gpt-4o', + * messages: [{ role: 'user', content: 'Generate a person' }], + * outputSchema: z.object({ name: z.string(), age: z.number() }) + * }) + * // result is { name: string, age: number } + * ``` + * + * @example Embedding generation + * ```ts + * import { openaiEmbed } from '@tanstack/ai-openai' + * + * const result = await ai({ + * adapter: openaiEmbed(), + * model: 'text-embedding-3-small', + * input: 'Hello, world!' + * }) + * ``` + * + * @example Summarization + * ```ts + * import { openaiSummarize } from '@tanstack/ai-openai' + * + * const result = await ai({ + * adapter: openaiSummarize(), + * model: 'gpt-4o-mini', + * text: 'Long text to summarize...' + * }) + * ``` + * + * @example Image generation + * ```ts + * import { openaiImage } from '@tanstack/ai-openai' + * + * const result = await ai({ + * adapter: openaiImage(), + * model: 'dall-e-3', + * prompt: 'A serene mountain landscape' + * }) + * ``` + * + * @example Video generation (experimental) + * ```ts + * import { openaiVideo } from '@tanstack/ai-openai' + * + * // Create a video job + * const { jobId } = await ai({ + * adapter: openaiVideo(), + * model: 'sora-2', + * prompt: 'A cat chasing a dog' + * }) + * + * // Poll for status + * const status = await ai({ + * adapter: openaiVideo(), + * model: 'sora-2', + * jobId, + * request: 'status' + * }) + * + * // Get video URL when complete + * const { url } = await ai({ + * adapter: openaiVideo(), + * model: 'sora-2', + * jobId, + * request: 'url' + * }) + * ``` + */ +export function ai< + TAdapter extends AnyAIAdapter, + const TModel extends string, + TSchema extends z.ZodType | undefined = undefined, + TTextStream extends boolean = true, + TSummarizeStream extends boolean = false, + TVideoRequest extends 'create' | 'status' | 'url' = 'create', +>( + options: AIOptionsFor< + TAdapter, + TModel, + TSchema, + TTextStream, + TSummarizeStream, + TVideoRequest + >, +): AIReturnFor + +// Implementation +export function ai(options: AIOptionsUnion): AIResultUnion { + const { adapter } = options + + const handler = activityMap.get(adapter.kind) + if (!handler) { + throw new Error(`Unknown adapter kind: ${adapter.kind}`) + } + + return handler(options) +} + +// =========================== +// Re-exported Types +// =========================== + +// Re-export adapter types +export type { TextAdapter } from './activities/text/adapter' +export type { EmbeddingAdapter } from './activities/embedding/adapter' +export type { SummarizeAdapter } from './activities/summarize/adapter' +export type { ImageAdapter } from './activities/image/adapter' +export type { VideoAdapter } from './activities/video/adapter' + +// Re-export type helpers +export type { + TextModels, + EmbeddingModels, + SummarizeModels, + ImageModels, + VideoModels, +} from './activities' + +// Re-export activity option types and legacy aliases used by the package entrypoint +export type { + AIAdapter, + AnyAIAdapter, + GenerateOptions, + TextGenerateOptions, + EmbeddingGenerateOptions, + SummarizeGenerateOptions, + ImageGenerateOptions, + VideoGenerateOptions, + GenerateTextOptions, + GenerateEmbeddingOptions, + GenerateSummarizeOptions, + GenerateImageOptions, + GenerateVideoOptions, +} from './activities' diff --git a/packages/typescript/ai/src/base-adapter.ts b/packages/typescript/ai/src/base-adapter.ts index f533becc..f417ab5d 100644 --- a/packages/typescript/ai/src/base-adapter.ts +++ b/packages/typescript/ai/src/base-adapter.ts @@ -1,7 +1,6 @@ import type { AIAdapter, AIAdapterConfig, - ChatOptions, DefaultMessageMetadataByModality, EmbeddingOptions, EmbeddingResult, @@ -9,6 +8,7 @@ import type { StreamChunk, SummarizationOptions, SummarizationResult, + TextOptions, } from './types' /** @@ -67,7 +67,7 @@ export abstract class BaseAdapter< this.config = config } - abstract chatStream(options: ChatOptions): AsyncIterable + abstract chatStream(options: TextOptions): AsyncIterable abstract summarize( options: SummarizationOptions, diff --git a/packages/typescript/ai/src/core/chat-common-options.ts b/packages/typescript/ai/src/core/chat-common-options.ts deleted file mode 100644 index c457335e..00000000 --- a/packages/typescript/ai/src/core/chat-common-options.ts +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Common options shared across different AI provider implementations. - * These options represent the standard parameters that work across OpenAI, Anthropic, and Gemini. - */ -export interface CommonOptions { - /** - * Controls the randomness of the output. - * Higher values (e.g., 0.8) make output more random, lower values (e.g., 0.2) make it more focused and deterministic. - * Range: [0.0, 2.0] - * - * Note: Generally recommended to use either temperature or topP, but not both. - * - * Provider usage: - * - OpenAI: `temperature` (number) - in text.top_p field - * - Anthropic: `temperature` (number) - ranges from 0.0 to 1.0, default 1.0 - * - Gemini: `generationConfig.temperature` (number) - ranges from 0.0 to 2.0 - */ - temperature?: number - - /** - * Nucleus sampling parameter. An alternative to temperature sampling. - * The model considers the results of tokens with topP probability mass. - * For example, 0.1 means only tokens comprising the top 10% probability mass are considered. - * - * Note: Generally recommended to use either temperature or topP, but not both. - * - * Provider usage: - * - OpenAI: `text.top_p` (number) - * - Anthropic: `top_p` (number | null) - * - Gemini: `generationConfig.topP` (number) - */ - topP?: number - - /** - * The maximum number of tokens to generate in the response. - * - * Provider usage: - * - OpenAI: `max_output_tokens` (number) - includes visible output and reasoning tokens - * - Anthropic: `max_tokens` (number, required) - range x >= 1 - * - Gemini: `generationConfig.maxOutputTokens` (number) - */ - maxTokens?: number - - /** - * Additional metadata to attach to the request. - * Can be used for tracking, debugging, or passing custom information. - * Structure and constraints vary by provider. - * - * Provider usage: - * - OpenAI: `metadata` (Record) - max 16 key-value pairs, keys max 64 chars, values max 512 chars - * - Anthropic: `metadata` (Record) - includes optional user_id (max 256 chars) - * - Gemini: Not directly available in TextProviderOptions - */ - metadata?: Record -} diff --git a/packages/typescript/ai/src/core/embedding.ts b/packages/typescript/ai/src/core/embedding.ts deleted file mode 100644 index e91226a1..00000000 --- a/packages/typescript/ai/src/core/embedding.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { aiEventClient } from '../event-client.js' -import type { - AIAdapter, - EmbeddingOptions, - EmbeddingResult, - ExtractModelsFromAdapter, -} from '../types' - -function createId(prefix: string): string { - return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}` -} - -/** - * Standalone embedding function with type inference from adapter - */ -export async function embedding< - TAdapter extends AIAdapter, ->( - options: Omit & { - adapter: TAdapter - model: ExtractModelsFromAdapter - }, -): Promise { - const { adapter, model, ...restOptions } = options - const requestId = createId('embedding') - const inputCount = Array.isArray(restOptions.input) - ? restOptions.input.length - : 1 - const startTime = Date.now() - - aiEventClient.emit('embedding:started', { - requestId, - model: model as string, - inputCount, - timestamp: startTime, - }) - - const result = await adapter.createEmbeddings({ - ...restOptions, - model: model as string, - }) - - const duration = Date.now() - startTime - - aiEventClient.emit('embedding:completed', { - requestId, - model: model as string, - inputCount, - duration, - timestamp: Date.now(), - }) - - return result -} diff --git a/packages/typescript/ai/src/core/summarize.ts b/packages/typescript/ai/src/core/summarize.ts deleted file mode 100644 index ed21b5c0..00000000 --- a/packages/typescript/ai/src/core/summarize.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { aiEventClient } from '../event-client.js' -import type { - AIAdapter, - ExtractModelsFromAdapter, - SummarizationOptions, - SummarizationResult, -} from '../types' - -function createId(prefix: string): string { - return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}` -} - -/** - * Standalone summarize function with type inference from adapter - */ -export async function summarize< - TAdapter extends AIAdapter, ->( - options: Omit & { - adapter: TAdapter - model: ExtractModelsFromAdapter - text: string - }, -): Promise { - const { adapter, model, text, ...restOptions } = options - const requestId = createId('summarize') - const inputLength = text.length - const startTime = Date.now() - - aiEventClient.emit('summarize:started', { - requestId, - model: model as string, - inputLength, - timestamp: startTime, - }) - - const result = await adapter.summarize({ - ...restOptions, - text, - model: model as string, - }) - - const duration = Date.now() - startTime - const outputLength = result.summary.length - - aiEventClient.emit('summarize:completed', { - requestId, - model: model as string, - inputLength, - outputLength, - duration, - timestamp: Date.now(), - }) - - return result -} diff --git a/packages/typescript/ai/src/event-client.ts b/packages/typescript/ai/src/event-client.ts index 86bbdd0a..dfcd4f30 100644 --- a/packages/typescript/ai/src/event-client.ts +++ b/packages/typescript/ai/src/event-client.ts @@ -110,7 +110,7 @@ export interface AIDevtoolsEventMap { duration: number timestamp: number } - 'tanstack-ai-devtools:chat:started': { + 'tanstack-ai-devtools:text:started': { requestId: string streamId: string provider: string @@ -124,7 +124,7 @@ export interface AIDevtoolsEventMap { options?: Record providerOptions?: Record } - 'tanstack-ai-devtools:chat:completed': { + 'tanstack-ai-devtools:text:completed': { requestId: string streamId: string model: string @@ -138,7 +138,7 @@ export interface AIDevtoolsEventMap { } timestamp: number } - 'tanstack-ai-devtools:chat:iteration': { + 'tanstack-ai-devtools:text:iteration': { requestId: string streamId: string iterationNumber: number @@ -192,7 +192,7 @@ export interface AIDevtoolsEventMap { timestamp: number } - // Chat Client events - from @tanstack/ai-client package + // Text Client events - from @tanstack/ai-client package 'tanstack-ai-devtools:client:created': { clientId: string initialMessageCount: number diff --git a/packages/typescript/ai/src/index.ts b/packages/typescript/ai/src/index.ts index b91778c4..51d1b253 100644 --- a/packages/typescript/ai/src/index.ts +++ b/packages/typescript/ai/src/index.ts @@ -1,6 +1,21 @@ -export { chat } from './core/chat' -export { summarize } from './core/summarize' -export { embedding } from './core/embedding' +// Main AI function - the one export to rule them all +export { + ai, + type AIAdapter, + type AnyAdapter, + type GenerateAdapter, + type GenerateOptions, + type TextGenerateOptions, + type EmbeddingGenerateOptions, + type SummarizeGenerateOptions, + type ImageGenerateOptions, + type GenerateTextOptions, + type GenerateEmbeddingOptions, + type GenerateSummarizeOptions, + type GenerateImageOptions, +} from './ai' + +// Tool definition export { toolDefinition, type ToolDefinition, @@ -12,33 +27,48 @@ export { type InferToolName, type InferToolInput, type InferToolOutput, -} from './tools/tool-definition' -export { convertZodToJsonSchema } from './tools/zod-converter' +} from './activities/text/tools/tool-definition' +export { convertZodToJsonSchema } from './activities/text/tools/zod-converter' + +// Stream utilities export { + streamToText, toServerSentEventsStream, toStreamResponse, -} from './utilities/stream-to-response' +} from './stream-to-response' + +// Base adapter export { BaseAdapter } from './base-adapter' -export { ToolCallManager } from './tools/tool-calls' + +// Tool call management +export { ToolCallManager } from './activities/text/tools/tool-calls' + +// Agent loop strategies export { maxIterations, untilFinishReason, combineStrategies, -} from './utilities/agent-loop-strategies' +} from './activities/text/agent-loop-strategies' + +// All types export * from './types' -export { chatOptions } from './utilities/chat-options' -export { messages } from './utilities/messages' + +// Utility builders +export { textOptions } from './activities/text/index' +export { messages } from './activities/text/messages' + +// Event client export { aiEventClient } from './event-client' // Message converters export { convertMessagesToModelMessages, + generateMessageId, uiMessageToModelMessages, modelMessageToUIMessage, modelMessagesToUIMessages, normalizeToUIMessage, - generateMessageId, -} from './message-converters' +} from './activities/text/messages' // Stream processing (unified for server and client) export { @@ -52,7 +82,7 @@ export { PartialJSONParser, defaultJSONParser, parsePartialJSON, -} from './stream' +} from './activities/text/stream' export type { ChunkStrategy, ChunkRecording, @@ -65,4 +95,4 @@ export type { ToolCallState, ToolResultState, JSONParser, -} from './stream' +} from './activities/text/stream' diff --git a/packages/typescript/ai/src/utilities/stream-to-response.ts b/packages/typescript/ai/src/stream-to-response.ts similarity index 72% rename from packages/typescript/ai/src/utilities/stream-to-response.ts rename to packages/typescript/ai/src/stream-to-response.ts index 19069204..df8a4c97 100644 --- a/packages/typescript/ai/src/utilities/stream-to-response.ts +++ b/packages/typescript/ai/src/stream-to-response.ts @@ -1,4 +1,38 @@ -import type { StreamChunk } from '../types' +import type { StreamChunk } from './types' + +/** + * Collect all text content from a StreamChunk async iterable and return as a string. + * + * This function consumes the entire stream, accumulating content from 'content' type chunks, + * and returns the final concatenated text. + * + * @param stream - AsyncIterable of StreamChunks from ai() + * @returns Promise - The accumulated text content + * + * @example + * ```typescript + * const stream = ai({ + * adapter: openaiText(), + * model: 'gpt-4o', + * messages: [{ role: 'user', content: 'Hello!' }] + * }); + * const text = await streamToText(stream); + * console.log(text); // "Hello! How can I help you today?" + * ``` + */ +export async function streamToText( + stream: AsyncIterable, +): Promise { + let accumulatedContent = '' + + for await (const chunk of stream) { + if (chunk.type === 'content' && chunk.delta) { + accumulatedContent += chunk.delta + } + } + + return accumulatedContent +} /** * Convert a StreamChunk async iterable to a ReadableStream in Server-Sent Events format @@ -8,13 +42,13 @@ import type { StreamChunk } from '../types' * - Each chunk is followed by "\n\n" * - Stream ends with "data: [DONE]\n\n" * - * @param stream - AsyncIterable of StreamChunks from chat() + * @param stream - AsyncIterable of StreamChunks from ai() * @param abortController - Optional AbortController to abort when stream is cancelled * @returns ReadableStream in Server-Sent Events format * * @example * ```typescript - * const stream = chat({ adapter: openai(), model: "gpt-4o", messages: [...] }); + * const stream = ai({ adapter: openaiText(), model: "gpt-4o", messages: [...] }); * const readableStream = toServerSentEventsStream(stream); * // Use with Response, or any API that accepts ReadableStream * ``` @@ -79,7 +113,7 @@ export function toServerSentEventsStream( * Create a streaming HTTP response from a StreamChunk async iterable * Includes proper headers for Server-Sent Events * - * @param stream - AsyncIterable of StreamChunks from chat() + * @param stream - AsyncIterable of StreamChunks from ai() * @param init - Optional Response initialization options * @param abortController - Optional AbortController to abort when client disconnects * @returns Response object with SSE headers and streaming body @@ -89,8 +123,8 @@ export function toServerSentEventsStream( * export async function POST(request: Request) { * const { messages } = await request.json(); * const abortController = new AbortController(); - * const stream = chat({ - * adapter: openai(), + * const stream = ai({ + * adapter: openaiText(), * model: "gpt-4o", * messages, * options: { abortSignal: abortController.signal } diff --git a/packages/typescript/ai/src/tools/zod-converter.ts b/packages/typescript/ai/src/tools/zod-converter.ts deleted file mode 100644 index 9edb45bb..00000000 --- a/packages/typescript/ai/src/tools/zod-converter.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { toJSONSchema } from 'zod' -import type { z } from 'zod' -import type { SchemaInput } from '../types' - -/** - * Check if a value is a Zod schema by looking for Zod-specific internals. - * Zod schemas have a `_zod` property that contains metadata. - */ -function isZodSchema(schema: unknown): schema is z.ZodType { - return ( - typeof schema === 'object' && - schema !== null && - '_zod' in schema && - typeof (schema as any)._zod === 'object' - ) -} - -/** - * Converts a schema (Zod or JSONSchema) to JSON Schema format compatible with LLM providers. - * If the input is already a JSONSchema object, it is returned as-is. - * If the input is a Zod schema, it is converted to JSON Schema. - * - * @param schema - Zod schema or JSONSchema object to convert - * @returns JSON Schema object that can be sent to LLM providers - * - * @example - * ```typescript - * import { z } from 'zod'; - * - * // Using Zod schema - * const zodSchema = z.object({ - * location: z.string().describe('City name'), - * unit: z.enum(['celsius', 'fahrenheit']).optional() - * }); - * - * const jsonSchema = convertZodToJsonSchema(zodSchema); - * // Returns: - * // { - * // type: 'object', - * // properties: { - * // location: { type: 'string', description: 'City name' }, - * // unit: { type: 'string', enum: ['celsius', 'fahrenheit'] } - * // }, - * // required: ['location'] - * // } - * - * // Using JSONSchema directly (passes through unchanged) - * const rawSchema = { - * type: 'object', - * properties: { location: { type: 'string' } }, - * required: ['location'] - * }; - * const result = convertZodToJsonSchema(rawSchema); - * // Returns the same object - * ``` - */ -export function convertZodToJsonSchema( - schema: SchemaInput | undefined, -): Record | undefined { - if (!schema) return undefined - - // If it's not a Zod schema, assume it's already a JSONSchema and pass through - if (!isZodSchema(schema)) { - return schema - } - - // Use Alcyone Labs fork which is compatible with Zod v4 - const jsonSchema = toJSONSchema(schema, { - target: 'openapi-3.0', - reused: 'ref', - }) - - // Remove $schema property as it's not needed for LLM providers - let result = jsonSchema - if (typeof result === 'object' && '$schema' in result) { - const { $schema, ...rest } = result - result = rest - } - - // Ensure object schemas always have type: "object" - // This fixes cases where zod-to-json-schema doesn't set type for empty objects - if (typeof result === 'object') { - // Check if the input schema is a ZodObject by inspecting its internal structure - const isZodObject = - typeof schema === 'object' && - 'def' in schema && - schema.def.type === 'object' - - // If we know it's a ZodObject but result doesn't have type, set it - if (isZodObject && !result.type) { - result.type = 'object' - } - - // If result is completely empty (no keys), it's likely an empty object schema - if (Object.keys(result).length === 0) { - result.type = 'object' - } - - // If it has properties (even empty), it should be an object type - if ('properties' in result && !result.type) { - result.type = 'object' - } - - // Ensure properties exists for object types (even if empty) - if (result.type === 'object' && !('properties' in result)) { - result.properties = {} - } - - // Ensure required exists for object types (even if empty array) - if (result.type === 'object' && !('required' in result)) { - result.required = [] - } - } - - return result -} diff --git a/packages/typescript/ai/src/types.ts b/packages/typescript/ai/src/types.ts index 748526c2..6763547a 100644 --- a/packages/typescript/ai/src/types.ts +++ b/packages/typescript/ai/src/types.ts @@ -1,6 +1,15 @@ -import type { CommonOptions } from './core/chat-common-options' +import type { CommonOptions } from './activities/text/index' import type { z } from 'zod' -import type { ToolCallState, ToolResultState } from './stream/types' +import type { + ToolCallState, + ToolResultState, +} from './activities/text/stream/types' +import type { + AnyAdapter, + EmbeddingAdapter, + SummarizeAdapter, + TextAdapter, +} from './activities' /** * JSON Schema type for defining tool input/output schemas as raw JSON Schema objects. @@ -545,7 +554,7 @@ export type AgentLoopStrategy = (state: AgentLoopState) => boolean /** * Options passed into the SDK and further piped to the AI provider. */ -export interface ChatOptions< +export interface TextOptions< TModel extends string = string, TProviderOptionsSuperset extends Record = Record, TOutput extends ResponseFormat | undefined = undefined, @@ -560,6 +569,13 @@ export interface ChatOptions< providerOptions?: TProviderOptionsForModel request?: Request | RequestInit output?: TOutput + /** + * Zod schema for structured output. + * When provided, the adapter should use the provider's native structured output API + * to ensure the response conforms to this schema. + * The schema will be converted to JSON Schema format before being sent to the provider. + */ + outputSchema?: z.ZodType /** * Conversation ID for correlating client and server-side devtools events. * When provided, server-side events will be linked to the client conversation in devtools. @@ -679,9 +695,9 @@ export type StreamChunk = | ToolInputAvailableStreamChunk | ThinkingStreamChunk -// Simple streaming format for basic chat completions -// Converted to StreamChunk format by convertChatCompletionStream() -export interface ChatCompletionChunk { +// Simple streaming format for basic text completions +// Converted to StreamChunk format by convertTextCompletionStream() +export interface TextCompletionChunk { id: string model: string content: string @@ -729,6 +745,242 @@ export interface EmbeddingResult { } } +// ============================================================================ +// Image Generation Types +// ============================================================================ + +/** + * Options for image generation. + * These are the common options supported across providers. + */ +export interface ImageGenerationOptions< + TProviderOptions extends object = object, +> { + /** The model to use for image generation */ + model: string + /** Text description of the desired image(s) */ + prompt: string + /** Number of images to generate (default: 1) */ + numberOfImages?: number + /** Image size in WIDTHxHEIGHT format (e.g., "1024x1024") */ + size?: string + /** Provider-specific options for image generation */ + providerOptions?: TProviderOptions +} + +/** + * A single generated image + */ +export interface GeneratedImage { + /** Base64-encoded image data */ + b64Json?: string + /** URL to the generated image (may be temporary) */ + url?: string + /** Revised prompt used by the model (if applicable) */ + revisedPrompt?: string +} + +/** + * Result of image generation + */ +export interface ImageGenerationResult { + /** Unique identifier for the generation */ + id: string + /** Model used for generation */ + model: string + /** Array of generated images */ + images: Array + /** Token usage information (if available) */ + usage?: { + inputTokens?: number + outputTokens?: number + totalTokens?: number + } +} + +// ============================================================================ +// Video Generation Types (Experimental) +// ============================================================================ + +/** + * Options for video generation. + * These are the common options supported across providers. + * + * @experimental Video generation is an experimental feature and may change. + */ +export interface VideoGenerationOptions< + TProviderOptions extends object = object, +> { + /** The model to use for video generation */ + model: string + /** Text description of the desired video */ + prompt: string + /** Video size in WIDTHxHEIGHT format (e.g., "1280x720") */ + size?: string + /** Video duration in seconds */ + duration?: number + /** Provider-specific options for video generation */ + providerOptions?: TProviderOptions +} + +/** + * Result of creating a video generation job. + * + * @experimental Video generation is an experimental feature and may change. + */ +export interface VideoJobResult { + /** Unique job identifier for polling status */ + jobId: string + /** Model used for generation */ + model: string +} + +/** + * Status of a video generation job. + * + * @experimental Video generation is an experimental feature and may change. + */ +export interface VideoStatusResult { + /** Job identifier */ + jobId: string + /** Current status of the job */ + status: 'pending' | 'processing' | 'completed' | 'failed' + /** Progress percentage (0-100), if available */ + progress?: number + /** Error message if status is 'failed' */ + error?: string +} + +/** + * Result containing the URL to a generated video. + * + * @experimental Video generation is an experimental feature and may change. + */ +export interface VideoUrlResult { + /** Job identifier */ + jobId: string + /** URL to the generated video */ + url: string + /** When the URL expires, if applicable */ + expiresAt?: Date +} + +// ============================================================================ +// Text-to-Speech (TTS) Types +// ============================================================================ + +/** + * Options for text-to-speech generation. + * These are the common options supported across providers. + */ +export interface TTSOptions { + /** The model to use for TTS generation */ + model: string + /** The text to convert to speech */ + text: string + /** The voice to use for generation */ + voice?: string + /** The output audio format */ + format?: 'mp3' | 'opus' | 'aac' | 'flac' | 'wav' | 'pcm' + /** The speed of the generated audio (0.25 to 4.0) */ + speed?: number + /** Provider-specific options for TTS generation */ + providerOptions?: TProviderOptions +} + +/** + * Result of text-to-speech generation. + */ +export interface TTSResult { + /** Unique identifier for the generation */ + id: string + /** Model used for generation */ + model: string + /** Base64-encoded audio data */ + audio: string + /** Audio format of the generated audio */ + format: string + /** Duration of the audio in seconds, if available */ + duration?: number + /** Content type of the audio (e.g., 'audio/mp3') */ + contentType?: string +} + +// ============================================================================ +// Transcription (Speech-to-Text) Types +// ============================================================================ + +/** + * Options for audio transcription. + * These are the common options supported across providers. + */ +export interface TranscriptionOptions< + TProviderOptions extends object = object, +> { + /** The model to use for transcription */ + model: string + /** The audio data to transcribe - can be base64 string, File, Blob, or Buffer */ + audio: string | File | Blob | ArrayBuffer + /** The language of the audio in ISO-639-1 format (e.g., 'en') */ + language?: string + /** An optional prompt to guide the transcription */ + prompt?: string + /** The format of the transcription output */ + responseFormat?: 'json' | 'text' | 'srt' | 'verbose_json' | 'vtt' + /** Provider-specific options for transcription */ + providerOptions?: TProviderOptions +} + +/** + * A single segment of transcribed audio with timing information. + */ +export interface TranscriptionSegment { + /** Unique identifier for the segment */ + id: number + /** Start time of the segment in seconds */ + start: number + /** End time of the segment in seconds */ + end: number + /** Transcribed text for this segment */ + text: string + /** Confidence score (0-1), if available */ + confidence?: number + /** Speaker identifier, if diarization is enabled */ + speaker?: string +} + +/** + * A single word with timing information. + */ +export interface TranscriptionWord { + /** The transcribed word */ + word: string + /** Start time in seconds */ + start: number + /** End time in seconds */ + end: number +} + +/** + * Result of audio transcription. + */ +export interface TranscriptionResult { + /** Unique identifier for the transcription */ + id: string + /** Model used for transcription */ + model: string + /** The full transcribed text */ + text: string + /** Language detected or specified */ + language?: string + /** Duration of the audio in seconds */ + duration?: number + /** Detailed segments with timing, if available */ + segments?: Array + /** Word-level timestamps, if available */ + words?: Array +} + /** * Default metadata type for adapters that don't define custom metadata. * Uses unknown for all modalities. @@ -800,7 +1052,7 @@ export interface AIAdapter< // Structured streaming with JSON chunks (supports tool calls and rich content) chatStream: ( - options: ChatOptions, + options: TextOptions, ) => AsyncIterable // Summarization @@ -818,7 +1070,7 @@ export interface AIAdapterConfig { headers?: Record } -export type ChatStreamOptionsUnion< +export type TextStreamOptionsUnion< TAdapter extends AIAdapter, > = TAdapter extends AIAdapter< @@ -833,7 +1085,7 @@ export type ChatStreamOptionsUnion< ? Models[number] extends infer TModel ? TModel extends string ? Omit< - ChatOptions, + TextOptions, 'model' | 'providerOptions' | 'responseFormat' | 'messages' > & { adapter: TAdapter @@ -874,11 +1126,11 @@ export type ChatStreamOptionsUnion< : never /** - * Chat options constrained by a specific model's capabilities. - * Unlike ChatStreamOptionsUnion which creates a union over all models, + * Text options constrained by a specific model's capabilities. + * Unlike TextStreamOptionsUnion which creates a union over all models, * this type takes a specific model and constrains messages accordingly. */ -export type ChatStreamOptionsForModel< +export type TextStreamOptionsForModel< TAdapter extends AIAdapter, TModel extends string, > = @@ -892,7 +1144,7 @@ export type ChatStreamOptionsForModel< infer MessageMetadata > ? Omit< - ChatOptions, + TextOptions, 'model' | 'providerOptions' | 'responseFormat' | 'messages' > & { adapter: TAdapter @@ -953,3 +1205,82 @@ export type ExtractModalitiesForModel< ? ModelInputModalities[TModel] : ReadonlyArray : ReadonlyArray + +// ============================================================================ +// New Adapter Types (Tree-Shakeable Architecture) +// ============================================================================ + +/** + * Extract models from any of the new adapter types + */ +export type ExtractModelsFromTextAdapter = + T extends TextAdapter ? M[number] : never + +export type ExtractModelsFromEmbeddingAdapter = + T extends EmbeddingAdapter ? M[number] : never + +export type ExtractModelsFromSummarizeAdapter = + T extends SummarizeAdapter ? M[number] : never + +/** + * Extract models from any adapter type (unified) + */ +export type ExtractModelsFromAnyAdapter = + T extends TextAdapter + ? M[number] + : T extends EmbeddingAdapter + ? M[number] + : T extends SummarizeAdapter + ? M[number] + : never + +/** + * Text options for the new TextAdapter type + */ +export type TextOptionsForTextAdapter< + TAdapter extends TextAdapter, + TModel extends string, +> = + TAdapter extends TextAdapter< + any, + any, + infer ModelProviderOptions, + infer ModelInputModalities, + infer MessageMetadata + > + ? Omit< + TextOptions, + 'model' | 'providerOptions' | 'responseFormat' | 'messages' + > & { + adapter: TAdapter + model: TModel + providerOptions?: TModel extends keyof ModelProviderOptions + ? ModelProviderOptions[TModel] + : never + messages: TModel extends keyof ModelInputModalities + ? ModelInputModalities[TModel] extends ReadonlyArray + ? MessageMetadata extends { + text: infer TTextMeta + image: infer TImageMeta + audio: infer TAudioMeta + video: infer TVideoMeta + document: infer TDocumentMeta + } + ? Array< + ConstrainedModelMessage< + ModelInputModalities[TModel], + TImageMeta, + TAudioMeta, + TVideoMeta, + TDocumentMeta, + TTextMeta + > + > + : Array> + : Array + : Array + } + : never + +// Re-export adapter types from adapters module +export type { TextAdapter, EmbeddingAdapter, SummarizeAdapter, AnyAdapter } diff --git a/packages/typescript/ai/src/utilities/chat-options.ts b/packages/typescript/ai/src/utilities/chat-options.ts deleted file mode 100644 index 39d3045c..00000000 --- a/packages/typescript/ai/src/utilities/chat-options.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { AIAdapter, ChatStreamOptionsUnion } from '../types' - -export function chatOptions< - TAdapter extends AIAdapter, - const TModel extends TAdapter extends AIAdapter< - infer Models, - any, - any, - any, - any - > - ? Models[number] - : string, ->( - options: Omit< - ChatStreamOptionsUnion, - 'providerOptions' | 'model' | 'messages' | 'abortController' - > & { - adapter: TAdapter - model: TModel - providerOptions?: TAdapter extends AIAdapter< - any, - any, - any, - any, - infer ModelProviderOptions - > - ? TModel extends keyof ModelProviderOptions - ? ModelProviderOptions[TModel] - : never - : never - }, -): typeof options { - return options -} diff --git a/packages/typescript/ai/src/utilities/messages.ts b/packages/typescript/ai/src/utilities/messages.ts deleted file mode 100644 index 7ad909ea..00000000 --- a/packages/typescript/ai/src/utilities/messages.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { - AIAdapter, - ConstrainedModelMessage, - Modality, - ModelMessage, -} from '../types' - -/** - * Type-safe helper to create a messages array constrained by a model's supported modalities. - * - * This function provides compile-time checking that your messages only contain - * content types supported by the specified model. It's particularly useful when - * combining typed messages with untyped data (like from request.json()). - * - * @example - * ```typescript - * import { messages, chat } from '@tanstack/ai' - * import { openai } from '@tanstack/ai-openai' - * - * const adapter = openai() - * - * // This will error at compile time because gpt-4o only supports text+image - * const msgs = messages({ adapter, model: 'gpt-4o' }, [ - * { - * role: 'user', - * content: [ - * { type: 'video', source: { type: 'url', value: '...' } } // Error! - * ] - * } - * ]) - * ``` - */ -export function messages< - TAdapter extends AIAdapter, - const TModel extends TAdapter extends AIAdapter< - infer Models, - any, - any, - any, - any, - any - > - ? Models[number] - : string, ->( - _options: { adapter: TAdapter; model: TModel }, - msgs: TAdapter extends AIAdapter< - any, - any, - any, - any, - any, - infer ModelInputModalities - > - ? TModel extends keyof ModelInputModalities - ? ModelInputModalities[TModel] extends ReadonlyArray - ? Array> - : Array - : Array - : Array, -): typeof msgs { - return msgs -} diff --git a/packages/typescript/ai/tests/agent-loop-strategies.test.ts b/packages/typescript/ai/tests/agent-loop-strategies.test.ts index b41ef393..62303620 100644 --- a/packages/typescript/ai/tests/agent-loop-strategies.test.ts +++ b/packages/typescript/ai/tests/agent-loop-strategies.test.ts @@ -3,7 +3,7 @@ import { maxIterations, untilFinishReason, combineStrategies, -} from '../src/utilities/agent-loop-strategies' +} from '../src/activities/text/agent-loop-strategies' import type { AgentLoopState } from '../src/types' describe('Agent Loop Strategies', () => { diff --git a/packages/typescript/ai/tests/ai-abort.test.ts b/packages/typescript/ai/tests/ai-abort.test.ts index 78c3cad5..1a7a96f7 100644 --- a/packages/typescript/ai/tests/ai-abort.test.ts +++ b/packages/typescript/ai/tests/ai-abort.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect } from 'vitest' import { z } from 'zod' -import { chat } from '../src/core/chat' -import type { ChatOptions, StreamChunk } from '../src/types' +import { textActivity } from '../src/activities/text' +import type { TextOptions, StreamChunk } from '../src/types' import { BaseAdapter } from '../src/base-adapter' // Mock adapter that tracks abort signal usage @@ -15,15 +15,16 @@ class MockAdapter extends BaseAdapter< public receivedAbortSignals: (AbortSignal | undefined)[] = [] public chatStreamCallCount = 0 + readonly kind = 'text' as const name = 'mock' models = ['test-model'] as const - private getAbortSignal(options: ChatOptions): AbortSignal | undefined { + private getAbortSignal(options: TextOptions): AbortSignal | undefined { const signal = (options.request as RequestInit | undefined)?.signal return signal ?? undefined } - async *chatStream(options: ChatOptions): AsyncIterable { + async *chatStream(options: TextOptions): AsyncIterable { this.chatStreamCallCount++ const abortSignal = this.getAbortSignal(options) this.receivedAbortSignals.push(abortSignal) @@ -63,6 +64,10 @@ class MockAdapter extends BaseAdapter< } } + async structuredOutput(_options: any): Promise { + return { data: {}, rawText: '{}' } + } + async summarize(_options: any): Promise { return { summary: 'test' } } @@ -72,14 +77,14 @@ class MockAdapter extends BaseAdapter< } } -describe('chat() - Abort Signal Handling', () => { +describe('textActivity() - Abort Signal Handling', () => { it('should propagate abortSignal to adapter.chatStream()', async () => { const mockAdapter = new MockAdapter() const abortController = new AbortController() const abortSignal = abortController.signal - const stream = chat({ + const stream = textActivity({ adapter: mockAdapter, model: 'test-model', messages: [{ role: 'user', content: 'Hello' }], @@ -100,7 +105,7 @@ describe('chat() - Abort Signal Handling', () => { const abortController = new AbortController() - const stream = chat({ + const stream = textActivity({ adapter: mockAdapter, model: 'test-model', messages: [{ role: 'user', content: 'Hello' }], @@ -132,7 +137,7 @@ describe('chat() - Abort Signal Handling', () => { // Abort before starting abortController.abort() - const stream = chat({ + const stream = textActivity({ adapter: mockAdapter, model: 'test-model', messages: [{ role: 'user', content: 'Hello' }], @@ -154,7 +159,7 @@ describe('chat() - Abort Signal Handling', () => { // Create adapter that yields tool_calls class ToolCallAdapter extends MockAdapter { - async *chatStream(_options: ChatOptions): AsyncIterable { + async *chatStream(_options: TextOptions): AsyncIterable { yield { type: 'tool_call', id: 'test-id', @@ -182,7 +187,7 @@ describe('chat() - Abort Signal Handling', () => { const toolAdapter = new ToolCallAdapter() - const stream = chat({ + const stream = textActivity({ adapter: toolAdapter, model: 'test-model', messages: [{ role: 'user', content: 'Hello' }], @@ -216,7 +221,7 @@ describe('chat() - Abort Signal Handling', () => { it('should handle undefined abortSignal gracefully', async () => { const mockAdapter = new MockAdapter() - const stream = chat({ + const stream = textActivity({ adapter: mockAdapter, model: 'test-model', messages: [{ role: 'user', content: 'Hello' }], diff --git a/packages/typescript/ai/tests/ai-chat.test.ts b/packages/typescript/ai/tests/ai-text.test.ts similarity index 94% rename from packages/typescript/ai/tests/ai-chat.test.ts rename to packages/typescript/ai/tests/ai-text.test.ts index 2ec8a650..5b2a015a 100644 --- a/packages/typescript/ai/tests/ai-chat.test.ts +++ b/packages/typescript/ai/tests/ai-text.test.ts @@ -1,11 +1,11 @@ /* eslint-disable @typescript-eslint/require-await */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { z } from 'zod' -import { chat } from '../src/core/chat' +import { textActivity } from '../src/activities/text' import { BaseAdapter } from '../src/base-adapter' import { aiEventClient } from '../src/event-client.js' -import { maxIterations } from '../src/utilities/agent-loop-strategies' -import type { ChatOptions, ModelMessage, StreamChunk, Tool } from '../src/types' +import { maxIterations } from '../src/activities/text/agent-loop-strategies' +import type { TextOptions, ModelMessage, StreamChunk, Tool } from '../src/types' // Mock event client to track events const eventListeners = new Map) => void>>() @@ -43,16 +43,17 @@ class MockAdapter extends BaseAdapter< model: string messages: Array tools?: Array - request?: ChatOptions['request'] + request?: TextOptions['request'] systemPrompts?: Array providerOptions?: any }> = [] + readonly kind = 'text' as const name = 'mock' models = ['test-model'] as const // Helper method for consistent tracking when subclasses override chatStream - protected trackStreamCall(options: ChatOptions): void { + protected trackStreamCall(options: TextOptions): void { this.chatStreamCallCount++ this.chatStreamCalls.push({ model: options.model, @@ -65,7 +66,7 @@ class MockAdapter extends BaseAdapter< } // Default implementation - will be overridden in tests - async *chatStream(options: ChatOptions): AsyncIterable { + async *chatStream(options: TextOptions): AsyncIterable { this.trackStreamCall(options) yield { type: 'content', @@ -85,6 +86,10 @@ class MockAdapter extends BaseAdapter< } } + async structuredOutput(_options: any): Promise { + return { data: {}, rawText: '{}' } + } + async summarize(_options: any): Promise { return { summary: 'test' } } @@ -103,18 +108,18 @@ async function collectChunks(stream: AsyncIterable): Promise> { return chunks } -describe('chat() - Comprehensive Logic Path Coverage', () => { +describe('textActivity() - Comprehensive Logic Path Coverage', () => { describe('Initialization & Setup', () => { it('should generate unique request and stream IDs', async () => { const adapter = new MockAdapter() - const stream1 = chat({ + const stream1 = textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Hello' }], }) - const stream2 = chat({ + const stream2 = textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Hi' }], @@ -125,11 +130,11 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { collectChunks(stream2), ]) - const event1 = capturedEvents.find((e) => e.type === 'chat:started') + const event1 = capturedEvents.find((e) => e.type === 'text:started') const event2 = capturedEvents .slice() .reverse() - .find((e) => e.type === 'chat:started') + .find((e) => e.type === 'text:started') expect(event1).toBeDefined() expect(event2).toBeDefined() @@ -142,7 +147,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const adapter = new MockAdapter() await collectChunks( - chat({ + textActivity({ adapter, model: 'test-model', messages: [ @@ -159,7 +164,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { }), ) - const event = capturedEvents.find((e) => e.type === 'chat:started') + const event = capturedEvents.find((e) => e.type === 'text:started') expect(event).toBeDefined() expect(event?.data.model).toBe('test-model') expect(event?.data.messageCount).toBe(2) @@ -171,7 +176,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const adapter = new MockAdapter() await collectChunks( - chat({ + textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Hello' }], @@ -188,7 +193,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const adapter = new MockAdapter() await collectChunks( - chat({ + textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Hello' }], @@ -211,7 +216,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const adapter = new MockAdapter() await collectChunks( - chat({ + textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Hello' }], @@ -233,7 +238,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const adapter = new MockAdapter() await collectChunks( - chat({ + textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Hello' }], @@ -251,7 +256,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { it('should stream simple content without tools', async () => { const adapter = new MockAdapter() - const stream = chat({ + const stream = textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Hello' }], @@ -265,7 +270,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { expect(chunks[1]?.type).toBe('done') // Check events - expect(capturedEvents.some((e) => e.type === 'chat:started')).toBe(true) + expect(capturedEvents.some((e) => e.type === 'text:started')).toBe(true) expect(capturedEvents.some((e) => e.type === 'stream:started')).toBe(true) expect( capturedEvents.some((e) => e.type === 'stream:chunk:content'), @@ -278,7 +283,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { it('should accumulate content across multiple chunks', async () => { class ContentAdapter extends MockAdapter { - async *chatStream(options: ChatOptions): AsyncIterable { + async *chatStream(options: TextOptions): AsyncIterable { this.trackStreamCall(options) yield { type: 'content', @@ -319,7 +324,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const adapter = new ContentAdapter() - const stream = chat({ + const stream = textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Say hello' }], @@ -342,7 +347,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { it('should handle empty content chunks', async () => { class EmptyContentAdapter extends MockAdapter { - async *chatStream(options: ChatOptions): AsyncIterable { + async *chatStream(options: TextOptions): AsyncIterable { this.trackStreamCall(options) yield { type: 'content', @@ -366,7 +371,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const adapter = new EmptyContentAdapter() const chunks = await collectChunks( - chat({ + textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Test' }], @@ -393,7 +398,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { class ToolAdapter extends MockAdapter { iteration = 0 - async *chatStream(options: ChatOptions): AsyncIterable { + async *chatStream(options: TextOptions): AsyncIterable { this.trackStreamCall(options) if (this.iteration === 0) { @@ -444,7 +449,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const adapter = new ToolAdapter() const chunks = await collectChunks( - chat({ + textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Weather?' }], @@ -462,7 +467,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { expect( capturedEvents.some((e) => e.type === 'stream:chunk:tool-call'), ).toBe(true) - expect(capturedEvents.some((e) => e.type === 'chat:iteration')).toBe(true) + expect(capturedEvents.some((e) => e.type === 'text:iteration')).toBe(true) expect(capturedEvents.some((e) => e.type === 'tool:call-completed')).toBe( true, ) @@ -483,7 +488,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { class StreamingToolAdapter extends MockAdapter { iteration = 0 - async *chatStream(options: ChatOptions): AsyncIterable { + async *chatStream(options: TextOptions): AsyncIterable { this.trackStreamCall(options) if (this.iteration === 0) { @@ -550,7 +555,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const adapter = new StreamingToolAdapter() const chunks = await collectChunks( - chat({ + textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Calculate' }], @@ -581,7 +586,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { class MultipleToolsAdapter extends MockAdapter { iteration = 0 - async *chatStream(options: ChatOptions): AsyncIterable { + async *chatStream(options: TextOptions): AsyncIterable { this.trackStreamCall(options) if (this.iteration === 0) { @@ -641,7 +646,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const adapter = new MultipleToolsAdapter() const chunks = await collectChunks( - chat({ + textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Use both tools' }], @@ -657,7 +662,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { // Check iteration event const iterationEvents = capturedEvents.filter( - (e) => e.type === 'chat:iteration', + (e) => e.type === 'text:iteration', ) expect(iterationEvents.length).toBeGreaterThan(0) expect(iterationEvents[0]?.data.toolCallCount).toBe(2) @@ -673,7 +678,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { class ContentWithToolsAdapter extends MockAdapter { iteration = 0 - async *chatStream(options: ChatOptions): AsyncIterable { + async *chatStream(options: TextOptions): AsyncIterable { this.trackStreamCall(options) if (this.iteration === 0) { @@ -738,7 +743,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const adapter = new ContentWithToolsAdapter() await collectChunks( - chat({ + textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Test' }], @@ -759,7 +764,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { class NoContentToolsAdapter extends MockAdapter { iteration = 0 - async *chatStream(options: ChatOptions): AsyncIterable { + async *chatStream(options: TextOptions): AsyncIterable { this.trackStreamCall(options) if (this.iteration === 0) { @@ -815,7 +820,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const adapter = new NoContentToolsAdapter() await collectChunks( - chat({ + textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Test' }], @@ -837,7 +842,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { } class IncompleteToolAdapter extends MockAdapter { - async *chatStream(options: ChatOptions): AsyncIterable { + async *chatStream(options: TextOptions): AsyncIterable { this.trackStreamCall(options) // Incomplete tool call (empty name) yield { @@ -868,7 +873,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const adapter = new IncompleteToolAdapter() await collectChunks( - chat({ + textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Test' }], @@ -895,7 +900,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { class ToolResultAdapter extends MockAdapter { iteration = 0 - async *chatStream(options: ChatOptions): AsyncIterable { + async *chatStream(options: TextOptions): AsyncIterable { this.trackStreamCall(options) if (this.iteration === 0) { this.iteration++ @@ -942,7 +947,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const adapter = new ToolResultAdapter() const chunks = await collectChunks( - chat({ + textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Test' }], @@ -974,7 +979,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { class MessageHistoryAdapter extends MockAdapter { iteration = 0 - async *chatStream(options: ChatOptions): AsyncIterable { + async *chatStream(options: TextOptions): AsyncIterable { this.trackStreamCall(options) if (this.iteration === 0) { @@ -1028,7 +1033,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const adapter = new MessageHistoryAdapter() await collectChunks( - chat({ + textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Test' }], @@ -1049,7 +1054,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { class ErrorToolAdapter extends MockAdapter { iteration = 0 - async *chatStream(options: ChatOptions): AsyncIterable { + async *chatStream(options: TextOptions): AsyncIterable { this.trackStreamCall(options) if (this.iteration === 0) { this.iteration++ @@ -1096,7 +1101,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const adapter = new ErrorToolAdapter() const chunks = await collectChunks( - chat({ + textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Call error tool' }], @@ -1114,7 +1119,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { it('should handle unknown tool calls', async () => { class UnknownToolAdapter extends MockAdapter { - async *chatStream(options: ChatOptions): AsyncIterable { + async *chatStream(options: TextOptions): AsyncIterable { this.trackStreamCall(options) yield { type: 'tool_call', @@ -1141,7 +1146,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const adapter = new UnknownToolAdapter() const chunks = await collectChunks( - chat({ + textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Test' }], @@ -1180,7 +1185,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { } class ApprovalAdapter extends MockAdapter { - async *chatStream(options: ChatOptions): AsyncIterable { + async *chatStream(options: TextOptions): AsyncIterable { this.trackStreamCall(options) yield { type: 'tool_call', @@ -1210,7 +1215,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const adapter = new ApprovalAdapter() const chunks = await collectChunks( - chat({ + textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Delete file' }], @@ -1249,7 +1254,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { } class ClientToolAdapter extends MockAdapter { - async *chatStream(options: ChatOptions): AsyncIterable { + async *chatStream(options: TextOptions): AsyncIterable { this.trackStreamCall(options) yield { type: 'tool_call', @@ -1276,7 +1281,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const adapter = new ClientToolAdapter() const chunks = await collectChunks( - chat({ + textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Use client tool' }], @@ -1323,7 +1328,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { } class MixedToolsAdapter extends MockAdapter { - async *chatStream(options: ChatOptions): AsyncIterable { + async *chatStream(options: TextOptions): AsyncIterable { this.trackStreamCall(options) yield { type: 'tool_call', @@ -1374,7 +1379,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const adapter = new MixedToolsAdapter() const chunks = await collectChunks( - chat({ + textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Use all tools' }], @@ -1417,7 +1422,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { } class PendingToolAdapter extends MockAdapter { - async *chatStream(options: ChatOptions): AsyncIterable { + async *chatStream(options: TextOptions): AsyncIterable { this.trackStreamCall(options) const toolMessage = options.messages.find( @@ -1480,7 +1485,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { } as any, ] - const stream = chat({ + const stream = textActivity({ adapter, model: 'test-model', messages, @@ -1505,7 +1510,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { class LoopAdapter extends MockAdapter { iteration = 0 - async *chatStream(options: ChatOptions): AsyncIterable { + async *chatStream(options: TextOptions): AsyncIterable { this.trackStreamCall(options) if (this.iteration < 3) { this.iteration++ @@ -1552,7 +1557,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const adapter = new LoopAdapter() await collectChunks( - chat({ + textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Loop' }], @@ -1575,7 +1580,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { class InfiniteLoopAdapter extends MockAdapter { iteration = 0 - async *chatStream(options: ChatOptions): AsyncIterable { + async *chatStream(options: TextOptions): AsyncIterable { this.trackStreamCall(options) yield { type: 'tool_call', @@ -1604,7 +1609,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { // Consume stream - should stop after 5 iterations (default) const chunks: Array = [] - for await (const chunk of chat({ + for await (const chunk of textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Loop' }], @@ -1629,7 +1634,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { } class StopAdapter extends MockAdapter { - async *chatStream(options: ChatOptions): AsyncIterable { + async *chatStream(options: TextOptions): AsyncIterable { this.trackStreamCall(options) yield { type: 'content', @@ -1653,7 +1658,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const adapter = new StopAdapter() await collectChunks( - chat({ + textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Hello' }], @@ -1667,7 +1672,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { it('should exit loop when no tools provided', async () => { class NoToolsAdapter extends MockAdapter { - async *chatStream(options: ChatOptions): AsyncIterable { + async *chatStream(options: TextOptions): AsyncIterable { this.trackStreamCall(options) yield { type: 'tool_call', @@ -1694,7 +1699,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const adapter = new NoToolsAdapter() await collectChunks( - chat({ + textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Test' }], @@ -1715,7 +1720,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { } class NoToolCallsAdapter extends MockAdapter { - async *chatStream(options: ChatOptions): AsyncIterable { + async *chatStream(options: TextOptions): AsyncIterable { this.trackStreamCall(options) // Tool call with empty name (invalid) yield { @@ -1743,7 +1748,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const adapter = new NoToolCallsAdapter() await collectChunks( - chat({ + textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Test' }], @@ -1765,7 +1770,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { abortController.abort() // Abort before starting const chunks = await collectChunks( - chat({ + textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Hello' }], @@ -1780,7 +1785,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { it('should check abort signal during streaming', async () => { class StreamingAdapter extends MockAdapter { - async *chatStream(options: ChatOptions): AsyncIterable { + async *chatStream(options: TextOptions): AsyncIterable { this.trackStreamCall(options) yield { type: 'content', @@ -1814,7 +1819,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const adapter = new StreamingAdapter() const abortController = new AbortController() - const stream = chat({ + const stream = textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Hello' }], @@ -1845,7 +1850,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { } class ToolCallAdapter extends MockAdapter { - async *chatStream(options: ChatOptions): AsyncIterable { + async *chatStream(options: TextOptions): AsyncIterable { this.trackStreamCall(options) yield { type: 'tool_call', @@ -1872,7 +1877,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const adapter = new ToolCallAdapter() const abortController = new AbortController() - const stream = chat({ + const stream = textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Test' }], @@ -1896,7 +1901,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { describe('Error Handling Paths', () => { it('should stop on error chunk and return early', async () => { class ErrorAdapter extends MockAdapter { - async *chatStream(options: ChatOptions): AsyncIterable { + async *chatStream(options: TextOptions): AsyncIterable { this.trackStreamCall(options) yield { type: 'content', @@ -1931,7 +1936,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const adapter = new ErrorAdapter() const chunks = await collectChunks( - chat({ + textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Hello' }], @@ -1960,7 +1965,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { describe('Finish Reason Paths', () => { it("should handle finish reason 'stop'", async () => { class StopFinishAdapter extends MockAdapter { - async *chatStream(options: ChatOptions): AsyncIterable { + async *chatStream(options: TextOptions): AsyncIterable { this.trackStreamCall(options) yield { type: 'content', @@ -1984,7 +1989,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const adapter = new StopFinishAdapter() const chunks = await collectChunks( - chat({ + textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Test' }], @@ -1997,7 +2002,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { it("should handle finish reason 'length'", async () => { class LengthAdapter extends MockAdapter { - async *chatStream(options: ChatOptions): AsyncIterable { + async *chatStream(options: TextOptions): AsyncIterable { this.trackStreamCall(options) yield { type: 'content', @@ -2021,7 +2026,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const adapter = new LengthAdapter() const chunks = await collectChunks( - chat({ + textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Test' }], @@ -2034,7 +2039,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { it('should handle finish reason null', async () => { class NullFinishAdapter extends MockAdapter { - async *chatStream(options: ChatOptions): AsyncIterable { + async *chatStream(options: TextOptions): AsyncIterable { this.trackStreamCall(options) yield { type: 'content', @@ -2058,7 +2063,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const adapter = new NullFinishAdapter() const chunks = await collectChunks( - chat({ + textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Test' }], @@ -2076,7 +2081,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const adapter = new MockAdapter() await collectChunks( - chat({ + textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Hello' }], @@ -2086,14 +2091,14 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const eventTypes = capturedEvents.map((e) => e.type) // Check event order and presence - expect(eventTypes.includes('chat:started')).toBe(true) + expect(eventTypes.includes('text:started')).toBe(true) expect(eventTypes.includes('stream:started')).toBe(true) expect(eventTypes.includes('stream:chunk:content')).toBe(true) expect(eventTypes.includes('stream:chunk:done')).toBe(true) expect(eventTypes.includes('stream:ended')).toBe(true) // chat:started should come before stream:started - const chatStartedIndex = eventTypes.indexOf('chat:started') + const chatStartedIndex = eventTypes.indexOf('text:started') const streamStartedIndex = eventTypes.indexOf('stream:started') expect(chatStartedIndex).toBeLessThan(streamStartedIndex) @@ -2112,7 +2117,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { class ToolAdapter extends MockAdapter { iteration = 0 - async *chatStream(options: ChatOptions): AsyncIterable { + async *chatStream(options: TextOptions): AsyncIterable { this.trackStreamCall(options) if (this.iteration === 0) { this.iteration++ @@ -2159,7 +2164,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const adapter = new ToolAdapter() await collectChunks( - chat({ + textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Test' }], @@ -2169,7 +2174,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { // Should emit chat:iteration event const iterationEvents = capturedEvents.filter( - (e) => e.type === 'chat:iteration', + (e) => e.type === 'text:iteration', ) expect(iterationEvents.length).toBeGreaterThan(0) expect(iterationEvents[0]?.data.iterationNumber).toBe(1) @@ -2179,7 +2184,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const adapter = new MockAdapter() await collectChunks( - chat({ + textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Hello' }], @@ -2202,7 +2207,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { class MultiIterationAdapter extends MockAdapter { iteration = 0 - async *chatStream(options: ChatOptions): AsyncIterable { + async *chatStream(options: TextOptions): AsyncIterable { this.trackStreamCall(options) if (this.iteration === 0) { this.iteration++ @@ -2258,7 +2263,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const adapter = new MultiIterationAdapter() await collectChunks( - chat({ + textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Test' }], @@ -2278,7 +2283,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const adapter = new MockAdapter() const chunks = await collectChunks( - chat({ + textActivity({ adapter, model: 'test-model', messages: [], @@ -2293,7 +2298,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const adapter = new MockAdapter() const chunks = await collectChunks( - chat({ + textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Hello' }], @@ -2313,7 +2318,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { } class MissingIdAdapter extends MockAdapter { - async *chatStream(options: ChatOptions): AsyncIterable { + async *chatStream(options: TextOptions): AsyncIterable { this.trackStreamCall(options) yield { type: 'tool_call', @@ -2340,7 +2345,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const adapter = new MissingIdAdapter() await collectChunks( - chat({ + textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Test' }], @@ -2361,7 +2366,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { } class InvalidJsonAdapter extends MockAdapter { - async *chatStream(options: ChatOptions): AsyncIterable { + async *chatStream(options: TextOptions): AsyncIterable { this.trackStreamCall(options) yield { type: 'tool_call', @@ -2391,7 +2396,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { // This will cause an unhandled error, but we can test that it throws await expect( collectChunks( - chat({ + textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Test' }], @@ -2405,7 +2410,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { describe('Tool Result Chunk Events from Adapter', () => { it('should emit stream:chunk:tool-result event when adapter sends tool_result chunk', async () => { class ToolResultChunkAdapter extends MockAdapter { - async *chatStream(options: ChatOptions): AsyncIterable { + async *chatStream(options: TextOptions): AsyncIterable { this.trackStreamCall(options) yield { type: 'content', @@ -2438,7 +2443,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const adapter = new ToolResultChunkAdapter() await collectChunks( - chat({ + textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Continue' }], @@ -2473,7 +2478,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { class ApprovalResponseAdapter extends MockAdapter { iteration = 0 - async *chatStream(options: ChatOptions): AsyncIterable { + async *chatStream(options: TextOptions): AsyncIterable { this.trackStreamCall(options) // Check if messages have approval response in parts @@ -2546,7 +2551,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const adapter = new ApprovalResponseAdapter() // First call - should request approval - const stream1 = chat({ + const stream1 = textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Delete file' }], @@ -2591,7 +2596,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { } as any, ] - const stream2 = chat({ + const stream2 = textActivity({ adapter, model: 'test-model', messages: messagesWithApproval, @@ -2616,7 +2621,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { class ClientOutputAdapter extends MockAdapter { iteration = 0 - async *chatStream(options: ChatOptions): AsyncIterable { + async *chatStream(options: TextOptions): AsyncIterable { this.trackStreamCall(options) if (this.iteration === 0) { @@ -2669,7 +2674,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const adapter = new ClientOutputAdapter() // First call - should request client execution - const stream1 = chat({ + const stream1 = textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'Use client tool' }], @@ -2709,7 +2714,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { } as any, ] - const stream2 = chat({ + const stream2 = textActivity({ adapter, model: 'test-model', messages: messagesWithOutput, @@ -2744,7 +2749,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { class MixedPartsAdapter extends MockAdapter { iteration = 0 - async *chatStream(options: ChatOptions): AsyncIterable { + async *chatStream(options: TextOptions): AsyncIterable { this.trackStreamCall(options) if (this.iteration === 0) { this.iteration++ @@ -2845,7 +2850,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { } as any, ] - const stream = chat({ + const stream = textActivity({ adapter, model: 'test-model', messages: messagesWithBoth, @@ -2877,7 +2882,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { class TemperatureToolAdapter extends MockAdapter { iteration = 0 - async *chatStream(options: ChatOptions): AsyncIterable { + async *chatStream(options: TextOptions): AsyncIterable { this.trackStreamCall(options) const baseId = `test-${Date.now()}-${Math.random() .toString(36) @@ -2965,7 +2970,7 @@ describe('chat() - Comprehensive Logic Path Coverage', () => { const adapter = new TemperatureToolAdapter() - const stream = chat({ + const stream = textActivity({ adapter, model: 'test-model', messages: [{ role: 'user', content: 'what is the temperature?' }], diff --git a/packages/typescript/ai/tests/generate-types.test-d.ts b/packages/typescript/ai/tests/generate-types.test-d.ts new file mode 100644 index 00000000..3b0dff9f --- /dev/null +++ b/packages/typescript/ai/tests/generate-types.test-d.ts @@ -0,0 +1,1693 @@ +/** + * Type tests for the ai function + * These tests verify that TypeScript correctly infers types and provides autocomplete + */ + +import { describe, expectTypeOf, it } from 'vitest' +import { + BaseEmbeddingAdapter, + BaseImageAdapter, + BaseSummarizeAdapter, + BaseTextAdapter, +} from '../src/activities' +import { ai } from '../src/ai' +import type { + StructuredOutputOptions, + StructuredOutputResult, +} from '../src/activities' +import type { + EmbeddingOptions, + EmbeddingResult, + ImageGenerationOptions, + ImageGenerationResult, + StreamChunk, + SummarizationOptions, + SummarizationResult, + TextOptions, +} from '../src/types' + +// Define test models +const TEST_CHAT_MODELS = ['gpt-4o', 'gpt-4o-mini', 'gpt-3.5-turbo'] as const +const TEST_EMBED_MODELS = [ + 'text-embedding-3-large', + 'text-embedding-3-small', +] as const +const TEST_SUMMARIZE_MODELS = ['summarize-v1', 'summarize-v2'] as const + +// Define strict provider options for testing (without index signatures) +interface TestTextProviderOptions { + temperature?: number + maxTokens?: number +} + +interface TestEmbedProviderOptions { + encodingFormat?: 'float' | 'base64' +} + +interface TestSummarizeProviderOptions { + style?: 'bullet-points' | 'paragraph' +} + +// Mock adapters for type testing +class TestTextAdapter extends BaseTextAdapter< + typeof TEST_CHAT_MODELS, + TestTextProviderOptions +> { + readonly kind = 'text' as const + readonly name = 'test' as const + readonly models = TEST_CHAT_MODELS + + constructor() { + super({}) + } + + async *chatStream(_options: TextOptions): AsyncIterable { + // Mock implementation + } + + structuredOutput( + _options: StructuredOutputOptions, + ): Promise> { + return Promise.resolve({ + data: {}, + rawText: '{}', + }) + } +} + +const TEST_CHAT_MODELS_WITH_MAP = ['model-a', 'model-b'] as const + +interface TestBaseProviderOptions { + baseOnly?: boolean +} + +type TestModelProviderOptionsByName = { + 'model-a': TestBaseProviderOptions & { + foo?: number + } + 'model-b': TestBaseProviderOptions & { + bar?: string + } +} + +type TestModelInputModalitiesByName = { + 'model-a': readonly ['text'] + 'model-b': readonly ['text'] +} + +class TestTextAdapterWithModelOptions extends BaseTextAdapter< + typeof TEST_CHAT_MODELS_WITH_MAP, + TestBaseProviderOptions, + TestModelProviderOptionsByName, + TestModelInputModalitiesByName +> { + readonly kind = 'text' as const + readonly name = 'test-with-map' as const + readonly models = TEST_CHAT_MODELS_WITH_MAP + + _modelProviderOptionsByName!: TestModelProviderOptionsByName + _modelInputModalitiesByName!: TestModelInputModalitiesByName + + constructor() { + super({}) + } + + async *chatStream(_options: TextOptions): AsyncIterable { + // Mock implementation + } + + structuredOutput( + _options: StructuredOutputOptions, + ): Promise> { + return Promise.resolve({ + data: {}, + rawText: '{}', + }) + } +} + +class TestEmbedAdapter extends BaseEmbeddingAdapter< + typeof TEST_EMBED_MODELS, + TestEmbedProviderOptions +> { + readonly kind = 'embedding' as const + readonly name = 'test' as const + readonly models = TEST_EMBED_MODELS + + constructor() { + super({}) + } + + createEmbeddings(_options: EmbeddingOptions): Promise { + return Promise.resolve({ + id: 'test', + model: 'text-embedding-3-small', + embeddings: [[0.1, 0.2]], + usage: { promptTokens: 1, totalTokens: 1 }, + }) + } +} + +class TestSummarizeAdapter extends BaseSummarizeAdapter< + typeof TEST_SUMMARIZE_MODELS, + TestSummarizeProviderOptions +> { + readonly kind = 'summarize' as const + readonly name = 'test' as const + readonly models = TEST_SUMMARIZE_MODELS + + constructor() { + super({}) + } + + summarize(_options: SummarizationOptions): Promise { + return Promise.resolve({ + id: 'test', + model: 'summarize-v1', + summary: 'Test summary', + usage: { promptTokens: 10, completionTokens: 5, totalTokens: 15 }, + }) + } +} + +describe('ai() type inference', () => { + it('should infer text adapter return type as AsyncIterable', () => { + const textAdapter = new TestTextAdapter() + const result = ai({ + adapter: textAdapter, + model: 'gpt-4o', + messages: [{ role: 'user', content: 'Hello' }], + }) + + expectTypeOf(result).toMatchTypeOf>() + }) + + it('should infer embedding adapter return type as Promise', () => { + const embedAdapter = new TestEmbedAdapter() + const result = ai({ + adapter: embedAdapter, + model: 'text-embedding-3-small', + input: 'Hello', + }) + + expectTypeOf(result).toMatchTypeOf>() + }) + + it('should infer summarize adapter return type as Promise', () => { + const summarizeAdapter = new TestSummarizeAdapter() + const result = ai({ + adapter: summarizeAdapter, + model: 'summarize-v1', + text: 'Long text to summarize', + }) + + expectTypeOf(result).toMatchTypeOf>() + }) + + it('should enforce valid model for text adapter', () => { + const textAdapter = new TestTextAdapter() + + // This should work - valid model + ai({ + adapter: textAdapter, + model: 'gpt-4o', + messages: [{ role: 'user', content: 'Hello' }], + }) + + // invalid model should error + ai({ + adapter: textAdapter, + // @ts-expect-error - invalid model + model: 'invalid-model', + messages: [{ role: 'user', content: 'Hello' }], + }) + }) + + it('should enforce valid model for embedding adapter', () => { + const embedAdapter = new TestEmbedAdapter() + + // This should work - valid model + ai({ + adapter: embedAdapter, + model: 'text-embedding-3-small', + input: 'Hello', + }) + + // invalid model should error + ai({ + adapter: embedAdapter, + // @ts-expect-error - invalid model + model: 'invalid-embedding-model', + input: 'Hello', + }) + }) + + it('should enforce valid model for summarize adapter', () => { + const summarizeAdapter = new TestSummarizeAdapter() + + // This should work - valid model + ai({ + adapter: summarizeAdapter, + model: 'summarize-v1', + text: 'Text to summarize', + }) + + // invalid model should error + ai({ + adapter: summarizeAdapter, + // @ts-expect-error - invalid model + model: 'invalid-summarize-model', + text: 'Text to summarize', + }) + }) + + it('should enforce strict providerOptions for text adapter', () => { + const textAdapter = new TestTextAdapter() + + // This should work - valid provider options + ai({ + adapter: textAdapter, + model: 'gpt-4o', + messages: [{ role: 'user', content: 'Hello' }], + providerOptions: { + temperature: 0.7, + maxTokens: 100, + }, + }) + + // invalid property should error + ai({ + adapter: textAdapter, + model: 'gpt-4o', + messages: [{ role: 'user', content: 'Hello' }], + providerOptions: { + temperature: 0.7, + // @ts-expect-error - invalid property + invalidProperty: 'should-error', + }, + }) + }) + + it('should enforce strict providerOptions for embedding adapter', () => { + const embedAdapter = new TestEmbedAdapter() + + // This should work - valid provider options + ai({ + adapter: embedAdapter, + model: 'text-embedding-3-small', + input: 'Hello', + providerOptions: { + encodingFormat: 'float', + }, + }) + + // temperature is not valid for embedding adapter + ai({ + adapter: embedAdapter, + model: 'text-embedding-3-small', + input: 'Hello', + providerOptions: { + // @ts-expect-error - temperature is not valid for embedding adapter + temperature: 0.7, + }, + }) + }) + + it('should enforce strict providerOptions for summarize adapter', () => { + const summarizeAdapter = new TestSummarizeAdapter() + + // This should work - valid provider options + ai({ + adapter: summarizeAdapter, + model: 'summarize-v1', + text: 'Text to summarize', + providerOptions: { + style: 'bullet-points', + }, + }) + + // invalid property should error + ai({ + adapter: summarizeAdapter, + model: 'summarize-v1', + text: 'Text to summarize', + providerOptions: { + // @ts-expect-error - invalid property + invalidOption: 'should-error', + }, + }) + }) + + it('should not allow chat-specific options for embedding adapter', () => { + const embedAdapter = new TestEmbedAdapter() + + ai({ + adapter: embedAdapter, + model: 'text-embedding-3-small', + input: 'Hello', + // @ts-expect-error - messages is not valid for embedding adapter + messages: [{ role: 'user', content: 'Hello' }], + }) + }) + + it('should not allow chat-specific options for summarize adapter', () => { + const summarizeAdapter = new TestSummarizeAdapter() + + ai({ + adapter: summarizeAdapter, + model: 'summarize-v1', + text: 'Text to summarize', + // @ts-expect-error - messages is not valid for summarize adapter + messages: [{ role: 'user', content: 'Hello' }], + }) + }) + + it('should not allow embedding-specific options for text adapter', () => { + const textAdapter = new TestTextAdapter() + + ai({ + adapter: textAdapter, + model: 'gpt-4o', + messages: [{ role: 'user', content: 'Hello' }], + // @ts-expect-error - input is not valid for chat adapter + input: 'Hello', + }) + }) + + it('should not allow summarize-specific options for text adapter', () => { + const textAdapter = new TestTextAdapter() + + ai({ + adapter: textAdapter, + model: 'gpt-4o', + messages: [{ role: 'user', content: 'Hello' }], + // @ts-expect-error - text is not valid for chat adapter + text: 'Text to summarize', + }) + }) + + it('should narrow providerOptions based on model (per-model map)', () => { + const adapter = new TestTextAdapterWithModelOptions() + + // model-a should accept both baseOnly and foo + ai({ + adapter, + model: 'model-a', + messages: [{ role: 'user', content: 'Hello' }], + providerOptions: { + baseOnly: true, + foo: 123, + }, + }) + + // model-a should NOT accept bar (it's model-b specific) + ai({ + adapter, + model: 'model-a', + messages: [{ role: 'user', content: 'Hello' }], + providerOptions: { + // @ts-expect-error - bar is not supported for model-a + bar: 'nope', + }, + }) + + // model-b should accept both baseOnly and bar + ai({ + adapter, + model: 'model-b', + messages: [{ role: 'user', content: 'Hello' }], + providerOptions: { + baseOnly: true, + bar: 'ok', + }, + }) + + // model-b should NOT accept foo (it's model-a specific) + ai({ + adapter, + model: 'model-b', + messages: [{ role: 'user', content: 'Hello' }], + providerOptions: { + // @ts-expect-error - foo is not supported for model-b + foo: 123, + }, + }) + }) +}) + +describe('ai() with outputSchema', () => { + // Import zod for schema tests + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + const { z } = require('zod') as typeof import('zod') + + it('should return Promise when outputSchema is provided', () => { + const textAdapter = new TestTextAdapter() + + const PersonSchema = z.object({ + name: z.string(), + age: z.number(), + }) + + const result = ai({ + adapter: textAdapter, + model: 'gpt-4o', + messages: [{ role: 'user', content: 'Generate a person' }], + outputSchema: PersonSchema, + }) + + // Return type should be Promise<{ name: string; age: number }> + expectTypeOf(result).toMatchTypeOf>() + }) + + it('should return AsyncIterable when outputSchema is not provided', () => { + const textAdapter = new TestTextAdapter() + + const result = ai({ + adapter: textAdapter, + model: 'gpt-4o', + messages: [{ role: 'user', content: 'Hello' }], + }) + + // Return type should be AsyncIterable + expectTypeOf(result).toMatchTypeOf>() + }) + + it('should infer complex nested schema types', () => { + const textAdapter = new TestTextAdapter() + + const AddressSchema = z.object({ + street: z.string(), + city: z.string(), + country: z.string(), + }) + + const PersonWithAddressSchema = z.object({ + name: z.string(), + addresses: z.array(AddressSchema), + }) + + const result = ai({ + adapter: textAdapter, + model: 'gpt-4o', + messages: [{ role: 'user', content: 'Generate a person with addresses' }], + outputSchema: PersonWithAddressSchema, + }) + + // Return type should be Promise with the correct nested structure + expectTypeOf(result).toMatchTypeOf< + Promise<{ + name: string + addresses: Array<{ street: string; city: string; country: string }> + }> + >() + }) + + it('should not allow outputSchema for embedding adapter', () => { + const embedAdapter = new TestEmbedAdapter() + + const PersonSchema = z.object({ + name: z.string(), + }) + + ai({ + adapter: embedAdapter, + model: 'text-embedding-3-small', + input: 'Hello', + // @ts-expect-error - outputSchema is not valid for embedding adapter + outputSchema: PersonSchema, + }) + }) + + it('should not allow outputSchema for summarize adapter', () => { + const summarizeAdapter = new TestSummarizeAdapter() + + const PersonSchema = z.object({ + name: z.string(), + }) + + ai({ + adapter: summarizeAdapter, + model: 'summarize-v1', + text: 'Text to summarize', + // @ts-expect-error - outputSchema is not valid for summarize adapter + outputSchema: PersonSchema, + }) + }) + + it('should infer schema with optional fields', () => { + const textAdapter = new TestTextAdapter() + + const PersonSchema = z.object({ + name: z.string(), + age: z.number().optional(), + email: z.string().nullable(), + }) + + const result = ai({ + adapter: textAdapter, + model: 'gpt-4o', + messages: [{ role: 'user', content: 'Generate a person' }], + outputSchema: PersonSchema, + }) + + expectTypeOf(result).toMatchTypeOf< + Promise<{ + name: string + age?: number + email: string | null + }> + >() + }) + + it('should work with union types in schema', () => { + const textAdapter = new TestTextAdapter() + + const ResponseSchema = z.discriminatedUnion('type', [ + z.object({ type: z.literal('success'), data: z.string() }), + z.object({ type: z.literal('error'), message: z.string() }), + ]) + + const result = ai({ + adapter: textAdapter, + model: 'gpt-4o', + messages: [{ role: 'user', content: 'Generate a response' }], + outputSchema: ResponseSchema, + }) + + expectTypeOf(result).toMatchTypeOf< + Promise< + { type: 'success'; data: string } | { type: 'error'; message: string } + > + >() + }) +}) + +describe('ai() with summarize streaming', () => { + it('should return Promise when stream is not provided', () => { + const summarizeAdapter = new TestSummarizeAdapter() + const result = ai({ + adapter: summarizeAdapter, + model: 'summarize-v1', + text: 'Long text to summarize', + }) + + expectTypeOf(result).toMatchTypeOf>() + }) + + it('should return Promise when stream is false', () => { + const summarizeAdapter = new TestSummarizeAdapter() + const result = ai({ + adapter: summarizeAdapter, + model: 'summarize-v1', + text: 'Long text to summarize', + stream: false, + }) + + expectTypeOf(result).toMatchTypeOf>() + }) + + it('should return AsyncIterable when stream is true', () => { + const summarizeAdapter = new TestSummarizeAdapter() + const result = ai({ + adapter: summarizeAdapter, + model: 'summarize-v1', + text: 'Long text to summarize', + stream: true, + }) + + expectTypeOf(result).toMatchTypeOf>() + }) + + it('should allow stream option for text adapter', () => { + const textAdapter = new TestTextAdapter() + + // stream: true is valid (explicit streaming, the default) + ai({ + adapter: textAdapter, + model: 'gpt-4o', + messages: [{ role: 'user', content: 'Hello' }], + stream: true, + }) + + // stream: false is valid (non-streaming mode) + ai({ + adapter: textAdapter, + model: 'gpt-4o', + messages: [{ role: 'user', content: 'Hello' }], + stream: false, + }) + }) + + it('should not allow stream option for embedding adapter', () => { + const embedAdapter = new TestEmbedAdapter() + + ai({ + adapter: embedAdapter, + model: 'text-embedding-3-small', + input: 'Hello', + // @ts-expect-error - stream is not valid for embedding adapter + stream: true, + }) + }) +}) + +// =========================== +// Image Adapter Test Setup +// =========================== + +const TEST_IMAGE_MODELS = ['image-model-1', 'image-model-2'] as const + +interface TestImageProviderOptions { + quality?: 'standard' | 'hd' +} + +type TestImageModelProviderOptionsByName = { + 'image-model-1': TestImageProviderOptions & { style?: 'vivid' | 'natural' } + 'image-model-2': TestImageProviderOptions & { + background?: 'transparent' | 'opaque' + } +} + +type TestImageModelSizeByName = { + 'image-model-1': '256x256' | '512x512' | '1024x1024' + 'image-model-2': '1024x1024' | '1792x1024' | '1024x1792' +} + +class TestImageAdapter extends BaseImageAdapter< + typeof TEST_IMAGE_MODELS, + TestImageProviderOptions, + TestImageModelProviderOptionsByName, + TestImageModelSizeByName +> { + readonly kind = 'image' as const + readonly name = 'test-image' as const + readonly models = TEST_IMAGE_MODELS + + _modelProviderOptionsByName!: TestImageModelProviderOptionsByName + _modelSizeByName!: TestImageModelSizeByName + + constructor() { + super({}) + } + + generateImages( + _options: ImageGenerationOptions, + ): Promise { + return Promise.resolve({ + id: 'test', + model: 'image-model-1', + images: [{ url: 'https://example.com/image.png' }], + }) + } +} + +// =========================== +// Text Adapter with Different Input Modalities Per Model +// =========================== + +const TEST_MULTIMODAL_MODELS = [ + 'text-only-model', + 'text-image-model', + 'multimodal-model', +] as const + +interface TestMultimodalProviderOptions { + temperature?: number +} + +// Define different input modalities per model +type TestMultimodalInputModalitiesByName = { + 'text-only-model': readonly ['text'] + 'text-image-model': readonly ['text', 'image'] + 'multimodal-model': readonly ['text', 'image', 'audio', 'document'] +} + +// Custom metadata types for testing +interface TestImageMetadata { + altText?: string +} + +interface TestMessageMetadataByModality { + text: unknown + image: TestImageMetadata + audio: unknown + video: unknown + document: unknown +} + +class TestMultimodalAdapter extends BaseTextAdapter< + typeof TEST_MULTIMODAL_MODELS, + TestMultimodalProviderOptions, + Record, + TestMultimodalInputModalitiesByName, + TestMessageMetadataByModality +> { + readonly kind = 'text' as const + readonly name = 'test-multimodal' as const + readonly models = TEST_MULTIMODAL_MODELS + + declare _modelInputModalitiesByName: TestMultimodalInputModalitiesByName + declare _messageMetadataByModality: TestMessageMetadataByModality + + constructor() { + super({}) + } + + async *chatStream(_options: TextOptions): AsyncIterable { + // Mock implementation + } + + structuredOutput( + _options: StructuredOutputOptions, + ): Promise> { + return Promise.resolve({ + data: {}, + rawText: '{}', + }) + } +} + +// =========================== +// Text Adapter Type Tests +// =========================== + +describe('ai() text adapter type safety', () => { + it('should return type that conforms to outputSchema type', () => { + const textAdapter = new TestTextAdapter() + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + const { z } = require('zod') as typeof import('zod') + + const PersonSchema = z.object({ + name: z.string(), + age: z.number(), + }) + + const result = ai({ + adapter: textAdapter, + model: 'gpt-4o', + messages: [{ role: 'user', content: 'Generate a person' }], + outputSchema: PersonSchema, + }) + + // Return type should match the schema + expectTypeOf(result).toExtend>() + // Should NOT match a different type + expectTypeOf(result).not.toExtend>() + }) + + it('should error on invalid provider options', () => { + const textAdapter = new TestTextAdapter() + + ai({ + adapter: textAdapter, + model: 'gpt-4o', + messages: [{ role: 'user', content: 'Hello' }], + providerOptions: { + // @ts-expect-error - unknownOption is not valid for text adapter + unknownOption: 'invalid', + }, + }) + }) + + it('should error on non-existing props', () => { + const textAdapter = new TestTextAdapter() + + ai({ + adapter: textAdapter, + model: 'gpt-4o', + messages: [{ role: 'user', content: 'Hello' }], + // @ts-expect-error - nonExistingProp is not a valid option + nonExistingProp: 'should-error', + }) + }) + + it('should reject embedding-specific properties on text adapter', () => { + const textAdapter = new TestTextAdapter() + + ai({ + adapter: textAdapter, + model: 'gpt-4o', + messages: [{ role: 'user', content: 'Hello' }], + // @ts-expect-error - input is an embedding-specific property + input: 'not allowed on text adapter', + }) + + ai({ + adapter: textAdapter, + model: 'gpt-4o', + messages: [{ role: 'user', content: 'Hello' }], + // @ts-expect-error - dimensions is an embedding-specific property + dimensions: 1024, + }) + }) + + it('should reject summarize-specific properties on text adapter', () => { + const textAdapter = new TestTextAdapter() + + ai({ + adapter: textAdapter, + model: 'gpt-4o', + messages: [{ role: 'user', content: 'Hello' }], + // @ts-expect-error - text is a summarize-specific property + text: 'not allowed on text adapter', + }) + + ai({ + adapter: textAdapter, + model: 'gpt-4o', + messages: [{ role: 'user', content: 'Hello' }], + // @ts-expect-error - maxLength is a summarize-specific property + maxLength: 500, + }) + }) + + it('should reject image-specific properties on text adapter', () => { + const textAdapter = new TestTextAdapter() + + ai({ + adapter: textAdapter, + model: 'gpt-4o', + messages: [{ role: 'user', content: 'Hello' }], + // @ts-expect-error - prompt is an image-specific property + prompt: 'not allowed on text adapter', + }) + + ai({ + adapter: textAdapter, + model: 'gpt-4o', + messages: [{ role: 'user', content: 'Hello' }], + // @ts-expect-error - size is an image-specific property + size: '1024x1024', + }) + }) + + it('should reject providerOptions from other adapters on text adapter', () => { + const textAdapter = new TestTextAdapter() + + ai({ + adapter: textAdapter, + model: 'gpt-4o', + messages: [{ role: 'user', content: 'Hello' }], + providerOptions: { + // @ts-expect-error - encodingFormat is an embedding providerOption + encodingFormat: 'float', + }, + }) + + ai({ + adapter: textAdapter, + model: 'gpt-4o', + messages: [{ role: 'user', content: 'Hello' }], + providerOptions: { + // @ts-expect-error - quality is an image providerOption + quality: 'hd', + }, + }) + }) + + it('should change providerOptions type based on model selected', () => { + const adapter = new TestTextAdapterWithModelOptions() + + // model-a should accept foo (and baseOnly which is shared) + ai({ + adapter, + model: 'model-a', + messages: [{ role: 'user', content: 'Hello' }], + providerOptions: { + baseOnly: true, + foo: 42, + }, + }) + + // model-a should NOT accept bar (model-b specific) + ai({ + adapter, + model: 'model-a', + messages: [{ role: 'user', content: 'Hello' }], + providerOptions: { + baseOnly: true, // shared property - OK + // @ts-expect-error - bar is not valid for model-a + bar: 'invalid-for-model-a', + }, + }) + + // model-b should accept bar (and baseOnly which is shared) + ai({ + adapter, + model: 'model-b', + messages: [{ role: 'user', content: 'Hello' }], + providerOptions: { + baseOnly: true, + bar: 'valid-for-model-b', + }, + }) + + // model-b should NOT accept foo (model-a specific) + ai({ + adapter, + model: 'model-b', + messages: [{ role: 'user', content: 'Hello' }], + providerOptions: { + baseOnly: true, // shared property - OK + // @ts-expect-error - foo is not valid for model-b + foo: 42, + }, + }) + }) +}) + +// =========================== +// Text Adapter Input Modality Constraint Tests +// =========================== + +describe('ai() text adapter input modality constraints', () => { + it('should allow text content on text-only model', () => { + const adapter = new TestMultimodalAdapter() + + // Text content should work for text-only-model + ai({ + adapter, + model: 'text-only-model', + messages: [{ role: 'user', content: 'Hello, how are you?' }], + }) + + // String content should also work + ai({ + adapter, + model: 'text-only-model', + messages: [{ role: 'user', content: 'Hello' }], + }) + }) + + it('should reject image content on text-only model', () => { + const adapter = new TestMultimodalAdapter() + + ai({ + adapter, + model: 'text-only-model', + messages: [ + { + role: 'user', + content: [ + { + // @ts-expect-error - image content not allowed on text-only model + type: 'image', + source: { type: 'url', value: 'https://example.com/image.png' }, + }, + ], + }, + ], + }) + }) + + it('should reject document content on text-only model', () => { + const adapter = new TestMultimodalAdapter() + + ai({ + adapter, + model: 'text-only-model', + messages: [ + { + role: 'user', + content: [ + { + // @ts-expect-error - document content not allowed on text-only model + type: 'document', + source: { type: 'url', value: 'https://example.com/doc.pdf' }, + }, + ], + }, + ], + }) + }) + + it('should reject audio content on text-only model', () => { + const adapter = new TestMultimodalAdapter() + + ai({ + adapter, + model: 'text-only-model', + messages: [ + { + role: 'user', + content: [ + { + // @ts-expect-error - audio content not allowed on text-only model + type: 'audio', + source: { type: 'url', value: 'https://example.com/audio.mp3' }, + }, + ], + }, + ], + }) + }) + + it('should allow text and image content on text-image model', () => { + const adapter = new TestMultimodalAdapter() + + // Text content should work + ai({ + adapter, + model: 'text-image-model', + messages: [{ role: 'user', content: 'Hello' }], + }) + + // Image content should work with proper metadata type + ai({ + adapter, + model: 'text-image-model', + messages: [ + { + role: 'user', + content: [ + { type: 'text', content: 'What is in this image?' }, + { + type: 'image', + source: { type: 'url', value: 'https://example.com/image.png' }, + metadata: { altText: 'A photo' }, + }, + ], + }, + ], + }) + }) + + it('should reject document content on text-image model', () => { + const adapter = new TestMultimodalAdapter() + + ai({ + adapter, + model: 'text-image-model', + messages: [ + { + role: 'user', + content: [ + { + // @ts-expect-error - document content not allowed on text-image model + type: 'document', + source: { type: 'url', value: 'https://example.com/doc.pdf' }, + }, + ], + }, + ], + }) + }) + + it('should reject audio content on text-image model', () => { + const adapter = new TestMultimodalAdapter() + + ai({ + adapter, + model: 'text-image-model', + messages: [ + { + role: 'user', + content: [ + { + // @ts-expect-error - audio content not allowed on text-image model + type: 'audio', + source: { type: 'url', value: 'https://example.com/audio.mp3' }, + }, + ], + }, + ], + }) + }) + + it('should allow all supported modalities on multimodal model', () => { + const adapter = new TestMultimodalAdapter() + + // All supported content types should work on multimodal-model + ai({ + adapter, + model: 'multimodal-model', + messages: [ + { + role: 'user', + content: [ + { type: 'text', content: 'Analyze these files' }, + { + type: 'image', + source: { type: 'url', value: 'https://example.com/image.png' }, + }, + { + type: 'audio', + source: { type: 'url', value: 'https://example.com/audio.mp3' }, + }, + { + type: 'document', + source: { type: 'url', value: 'https://example.com/doc.pdf' }, + }, + ], + }, + ], + }) + }) + + it('should reject video content on multimodal model that does not support video', () => { + const adapter = new TestMultimodalAdapter() + + ai({ + adapter, + model: 'multimodal-model', + messages: [ + { + role: 'user', + content: [ + { + // @ts-expect-error - video content not allowed (multimodal-model only supports text, image, audio, document) + type: 'video', + source: { type: 'url', value: 'https://example.com/video.mp4' }, + }, + ], + }, + ], + }) + }) + + it('should enforce adapter-specific metadata types on content parts', () => { + const adapter = new TestMultimodalAdapter() + + // Valid metadata for image (TestImageMetadata has altText) + ai({ + adapter, + model: 'text-image-model', + messages: [ + { + role: 'user', + content: [ + { + type: 'image', + source: { type: 'url', value: 'https://example.com/image.png' }, + metadata: { altText: 'Description' }, + }, + ], + }, + ], + }) + }) +}) + +// =========================== +// Image Adapter Type Tests +// =========================== + +describe('ai() image adapter type safety', () => { + it('should have size determined by the model', () => { + const imageAdapter = new TestImageAdapter() + + // image-model-1 supports 256x256, 512x512, 1024x1024 + ai({ + adapter: imageAdapter, + model: 'image-model-1', + prompt: 'A beautiful sunset', + size: '512x512', // valid for image-model-1 + }) + + // image-model-2 supports 1024x1024, 1792x1024, 1024x1792 + ai({ + adapter: imageAdapter, + model: 'image-model-2', + prompt: 'A beautiful sunset', + size: '1792x1024', // valid for image-model-2 + }) + }) + + it('should return ImageGenerationResult type', () => { + const imageAdapter = new TestImageAdapter() + + const result = ai({ + adapter: imageAdapter, + model: 'image-model-1', + prompt: 'A beautiful sunset', + }) + + expectTypeOf(result).toExtend>() + }) + + it('should error on invalid size', () => { + const imageAdapter = new TestImageAdapter() + + ai({ + adapter: imageAdapter, + model: 'image-model-1', + prompt: 'A beautiful sunset', + // @ts-expect-error - 2048x2048 is not a valid size for image-model-1 + size: '2048x2048', + }) + }) + + it('should error when size valid for one model is used with another', () => { + const imageAdapter = new TestImageAdapter() + + // 1792x1024 is valid for image-model-2 but NOT for image-model-1 + ai({ + adapter: imageAdapter, + model: 'image-model-1', + prompt: 'A beautiful sunset', + // @ts-expect-error - 1792x1024 is not valid for image-model-1 (only image-model-2) + size: '1792x1024', + }) + + // 256x256 is valid for image-model-1 but NOT for image-model-2 + ai({ + adapter: imageAdapter, + model: 'image-model-2', + prompt: 'A beautiful sunset', + // @ts-expect-error - 256x256 is not valid for image-model-2 (only image-model-1) + size: '256x256', + }) + }) + + it('should have model-specific provider options for image adapter', () => { + const imageAdapter = new TestImageAdapter() + + // image-model-1 supports style option + ai({ + adapter: imageAdapter, + model: 'image-model-1', + prompt: 'A beautiful sunset', + providerOptions: { + quality: 'hd', // shared + style: 'vivid', // model-1 specific + }, + }) + + // image-model-1 should NOT accept background (model-2 specific) + ai({ + adapter: imageAdapter, + model: 'image-model-1', + prompt: 'A beautiful sunset', + providerOptions: { + // @ts-expect-error - background is not valid for image-model-1 + background: 'transparent', + }, + }) + + // image-model-2 supports background option + ai({ + adapter: imageAdapter, + model: 'image-model-2', + prompt: 'A beautiful sunset', + providerOptions: { + quality: 'hd', // shared + background: 'transparent', // model-2 specific + }, + }) + + // image-model-2 should NOT accept style (model-1 specific) + ai({ + adapter: imageAdapter, + model: 'image-model-2', + prompt: 'A beautiful sunset', + providerOptions: { + // @ts-expect-error - style is not valid for image-model-2 + style: 'vivid', + }, + }) + }) + + it('should reject text-specific properties on image adapter', () => { + const imageAdapter = new TestImageAdapter() + + ai({ + adapter: imageAdapter, + model: 'image-model-1', + prompt: 'A beautiful sunset', + // @ts-expect-error - messages is a text-specific property + messages: [{ role: 'user', content: 'Hello' }], + }) + + ai({ + adapter: imageAdapter, + model: 'image-model-1', + prompt: 'A beautiful sunset', + // @ts-expect-error - tools is a text-specific property + tools: [], + }) + + ai({ + adapter: imageAdapter, + model: 'image-model-1', + prompt: 'A beautiful sunset', + // @ts-expect-error - systemPrompts is a text-specific property + systemPrompts: ['You are helpful'], + }) + }) + + it('should reject embedding-specific properties on image adapter', () => { + const imageAdapter = new TestImageAdapter() + + ai({ + adapter: imageAdapter, + model: 'image-model-1', + prompt: 'A beautiful sunset', + // @ts-expect-error - input is an embedding-specific property + input: 'not allowed on image adapter', + }) + + ai({ + adapter: imageAdapter, + model: 'image-model-1', + prompt: 'A beautiful sunset', + // @ts-expect-error - dimensions is an embedding-specific property + dimensions: 1024, + }) + }) + + it('should reject summarize-specific properties on image adapter', () => { + const imageAdapter = new TestImageAdapter() + + ai({ + adapter: imageAdapter, + model: 'image-model-1', + prompt: 'A beautiful sunset', + // @ts-expect-error - text is a summarize-specific property + text: 'not allowed on image adapter', + }) + + ai({ + adapter: imageAdapter, + model: 'image-model-1', + prompt: 'A beautiful sunset', + // @ts-expect-error - maxLength is a summarize-specific property + maxLength: 500, + }) + + ai({ + adapter: imageAdapter, + model: 'image-model-1', + prompt: 'A beautiful sunset', + // @ts-expect-error - style (summarize) is a summarize-specific property + style: 'bullet-points', + }) + }) + + it('should reject providerOptions from other adapters on image adapter', () => { + const imageAdapter = new TestImageAdapter() + + ai({ + adapter: imageAdapter, + model: 'image-model-1', + prompt: 'A beautiful sunset', + providerOptions: { + // @ts-expect-error - temperature is a text providerOption + temperature: 0.7, + }, + }) + + ai({ + adapter: imageAdapter, + model: 'image-model-1', + prompt: 'A beautiful sunset', + providerOptions: { + // @ts-expect-error - maxTokens is a text providerOption + maxTokens: 100, + }, + }) + + ai({ + adapter: imageAdapter, + model: 'image-model-1', + prompt: 'A beautiful sunset', + providerOptions: { + // @ts-expect-error - encodingFormat is an embedding providerOption + encodingFormat: 'float', + }, + }) + }) +}) + +// =========================== +// Embedding Adapter Type Tests +// =========================== + +describe('ai() embedding adapter type safety', () => { + it('should reject text-specific properties on embedding adapter', () => { + const embedAdapter = new TestEmbedAdapter() + + ai({ + adapter: embedAdapter, + model: 'text-embedding-3-small', + input: 'Hello', + // @ts-expect-error - messages is a text-specific property + messages: [{ role: 'user', content: 'Hello' }], + }) + + ai({ + adapter: embedAdapter, + model: 'text-embedding-3-small', + input: 'Hello', + // @ts-expect-error - tools is a text-specific property + tools: [], + }) + + ai({ + adapter: embedAdapter, + model: 'text-embedding-3-small', + input: 'Hello', + // @ts-expect-error - systemPrompts is a text-specific property + systemPrompts: ['You are helpful'], + }) + + ai({ + adapter: embedAdapter, + model: 'text-embedding-3-small', + input: 'Hello', + // @ts-expect-error - outputSchema is a text-specific property + outputSchema: {}, + }) + }) + + it('should reject summarize-specific properties on embedding adapter', () => { + const embedAdapter = new TestEmbedAdapter() + + ai({ + adapter: embedAdapter, + model: 'text-embedding-3-small', + input: 'Hello', + // @ts-expect-error - text is a summarize-specific property + text: 'not allowed on embedding adapter', + }) + + ai({ + adapter: embedAdapter, + model: 'text-embedding-3-small', + input: 'Hello', + // @ts-expect-error - maxLength is a summarize-specific property + maxLength: 500, + }) + + ai({ + adapter: embedAdapter, + model: 'text-embedding-3-small', + input: 'Hello', + // @ts-expect-error - style is a summarize-specific property + style: 'bullet-points', + }) + + ai({ + adapter: embedAdapter, + model: 'text-embedding-3-small', + input: 'Hello', + // @ts-expect-error - focus is a summarize-specific property + focus: 'key points', + }) + }) + + it('should reject image-specific properties on embedding adapter', () => { + const embedAdapter = new TestEmbedAdapter() + + ai({ + adapter: embedAdapter, + model: 'text-embedding-3-small', + input: 'Hello', + // @ts-expect-error - prompt is an image-specific property + prompt: 'not allowed on embedding adapter', + }) + + ai({ + adapter: embedAdapter, + model: 'text-embedding-3-small', + input: 'Hello', + // @ts-expect-error - size is an image-specific property + size: '1024x1024', + }) + + ai({ + adapter: embedAdapter, + model: 'text-embedding-3-small', + input: 'Hello', + // @ts-expect-error - n is an image-specific property + n: 4, + }) + }) + + it('should reject providerOptions from other adapters on embedding adapter', () => { + const embedAdapter = new TestEmbedAdapter() + + ai({ + adapter: embedAdapter, + model: 'text-embedding-3-small', + input: 'Hello', + providerOptions: { + // @ts-expect-error - temperature is a text providerOption + temperature: 0.7, + }, + }) + + ai({ + adapter: embedAdapter, + model: 'text-embedding-3-small', + input: 'Hello', + providerOptions: { + // @ts-expect-error - maxTokens is a text providerOption + maxTokens: 100, + }, + }) + + ai({ + adapter: embedAdapter, + model: 'text-embedding-3-small', + input: 'Hello', + providerOptions: { + // @ts-expect-error - quality is an image providerOption + quality: 'hd', + }, + }) + }) +}) + +// =========================== +// Summarize Adapter Type Tests +// =========================== + +describe('ai() summarize adapter type safety', () => { + it('should reject text-specific properties on summarize adapter', () => { + const summarizeAdapter = new TestSummarizeAdapter() + + ai({ + adapter: summarizeAdapter, + model: 'summarize-v1', + text: 'Long text to summarize', + // @ts-expect-error - messages is a text-specific property + messages: [{ role: 'user', content: 'Hello' }], + }) + + ai({ + adapter: summarizeAdapter, + model: 'summarize-v1', + text: 'Long text to summarize', + // @ts-expect-error - tools is a text-specific property + tools: [], + }) + + ai({ + adapter: summarizeAdapter, + model: 'summarize-v1', + text: 'Long text to summarize', + // @ts-expect-error - systemPrompts is a text-specific property + systemPrompts: ['You are helpful'], + }) + + ai({ + adapter: summarizeAdapter, + model: 'summarize-v1', + text: 'Long text to summarize', + // @ts-expect-error - outputSchema is a text-specific property + outputSchema: {}, + }) + }) + + it('should reject embedding-specific properties on summarize adapter', () => { + const summarizeAdapter = new TestSummarizeAdapter() + + ai({ + adapter: summarizeAdapter, + model: 'summarize-v1', + text: 'Long text to summarize', + // @ts-expect-error - input is an embedding-specific property + input: 'not allowed on summarize adapter', + }) + + ai({ + adapter: summarizeAdapter, + model: 'summarize-v1', + text: 'Long text to summarize', + // @ts-expect-error - dimensions is an embedding-specific property + dimensions: 1024, + }) + }) + + it('should reject image-specific properties on summarize adapter', () => { + const summarizeAdapter = new TestSummarizeAdapter() + + ai({ + adapter: summarizeAdapter, + model: 'summarize-v1', + text: 'Long text to summarize', + // @ts-expect-error - prompt is an image-specific property + prompt: 'not allowed on summarize adapter', + }) + + ai({ + adapter: summarizeAdapter, + model: 'summarize-v1', + text: 'Long text to summarize', + // @ts-expect-error - size is an image-specific property + size: '1024x1024', + }) + + ai({ + adapter: summarizeAdapter, + model: 'summarize-v1', + text: 'Long text to summarize', + // @ts-expect-error - n is an image-specific property + n: 4, + }) + }) + + it('should reject providerOptions from other adapters on summarize adapter', () => { + const summarizeAdapter = new TestSummarizeAdapter() + + ai({ + adapter: summarizeAdapter, + model: 'summarize-v1', + text: 'Long text to summarize', + providerOptions: { + // @ts-expect-error - temperature is a text providerOption + temperature: 0.7, + }, + }) + + ai({ + adapter: summarizeAdapter, + model: 'summarize-v1', + text: 'Long text to summarize', + providerOptions: { + // @ts-expect-error - maxTokens is a text providerOption + maxTokens: 100, + }, + }) + + ai({ + adapter: summarizeAdapter, + model: 'summarize-v1', + text: 'Long text to summarize', + providerOptions: { + // @ts-expect-error - encodingFormat is an embedding providerOption + encodingFormat: 'float', + }, + }) + + ai({ + adapter: summarizeAdapter, + model: 'summarize-v1', + text: 'Long text to summarize', + providerOptions: { + // @ts-expect-error - quality is an image providerOption + quality: 'hd', + }, + }) + }) +}) diff --git a/packages/typescript/ai/tests/generate.test.ts b/packages/typescript/ai/tests/generate.test.ts new file mode 100644 index 00000000..26771155 --- /dev/null +++ b/packages/typescript/ai/tests/generate.test.ts @@ -0,0 +1,297 @@ +import { describe, expect, it, vi } from 'vitest' +import { ai } from '../src/ai' +import { + BaseTextAdapter, + BaseEmbeddingAdapter, + BaseSummarizeAdapter, +} from '../src/activities' +import type { + StructuredOutputOptions, + StructuredOutputResult, +} from '../src/activities' +import type { + TextOptions, + EmbeddingOptions, + EmbeddingResult, + ModelMessage, + StreamChunk, + SummarizationOptions, + SummarizationResult, +} from '../src' + +// Mock adapters for testing + +const MOCK_MODELS = ['model-a', 'model-b'] as const + +class MockTextAdapter extends BaseTextAdapter { + readonly kind = 'text' as const + readonly name = 'mock' as const + readonly models = MOCK_MODELS + + private mockChunks: Array + + constructor(mockChunks: Array = []) { + super({}) + this.mockChunks = mockChunks + } + + async *chatStream(_options: TextOptions): AsyncIterable { + for (const chunk of this.mockChunks) { + yield chunk + } + } + + structuredOutput( + _options: StructuredOutputOptions, + ): Promise> { + return Promise.resolve({ + data: {}, + rawText: '{}', + }) + } +} + +class MockEmbeddingAdapter extends BaseEmbeddingAdapter< + typeof MOCK_MODELS, + Record +> { + readonly kind = 'embedding' as const + readonly name = 'mock' as const + readonly models = MOCK_MODELS + + private mockResult: EmbeddingResult + + constructor(mockResult?: EmbeddingResult) { + super({}) + this.mockResult = mockResult ?? { + id: 'test-id', + model: 'model-a', + embeddings: [[0.1, 0.2, 0.3]], + usage: { promptTokens: 10, totalTokens: 10 }, + } + } + + createEmbeddings(_options: EmbeddingOptions): Promise { + return Promise.resolve(this.mockResult) + } +} + +class MockSummarizeAdapter extends BaseSummarizeAdapter< + typeof MOCK_MODELS, + Record +> { + readonly kind = 'summarize' as const + readonly name = 'mock' as const + readonly models = MOCK_MODELS + + private mockResult: SummarizationResult + + constructor(mockResult?: SummarizationResult) { + super({}) + this.mockResult = mockResult ?? { + id: 'test-id', + model: 'model-a', + summary: 'This is a summary.', + usage: { promptTokens: 100, completionTokens: 20, totalTokens: 120 }, + } + } + + summarize(_options: SummarizationOptions): Promise { + return Promise.resolve(this.mockResult) + } +} + +describe('generate function', () => { + describe('with chat adapter', () => { + it('should return an async iterable of StreamChunks', async () => { + const mockChunks: Array = [ + { + type: 'content', + id: '1', + model: 'model-a', + delta: 'Hello', + content: 'Hello', + timestamp: Date.now(), + }, + { + type: 'content', + id: '2', + model: 'model-a', + delta: ' world', + content: 'Hello world', + timestamp: Date.now(), + }, + { + type: 'done', + id: '3', + model: 'model-a', + timestamp: Date.now(), + finishReason: 'stop', + }, + ] + + const adapter = new MockTextAdapter(mockChunks) + const messages: Array = [ + { role: 'user', content: [{ type: 'text', content: 'Hi' }] }, + ] + + const result = ai({ + adapter, + model: 'model-a', + messages, + }) + + // Result should be an async iterable + expect(result).toBeDefined() + expect(typeof result[Symbol.asyncIterator]).toBe('function') + + // Collect all chunks + const collected: Array = [] + for await (const chunk of result) { + collected.push(chunk) + } + + expect(collected).toHaveLength(3) + expect(collected[0]?.type).toBe('content') + expect(collected[2]?.type).toBe('done') + }) + + it('should pass options to the text adapter', async () => { + const adapter = new MockTextAdapter([]) + const chatStreamSpy = vi.spyOn(adapter, 'chatStream') + + const messages: Array = [ + { role: 'user', content: [{ type: 'text', content: 'Test message' }] }, + ] + + // Consume the iterable to trigger the method + const result = ai({ + adapter, + model: 'model-a', + messages, + systemPrompts: ['Be helpful'], + options: { temperature: 0.7 }, + }) + for await (const _ of result) { + // Consume + } + + expect(chatStreamSpy).toHaveBeenCalled() + }) + }) + + describe('with embedding adapter', () => { + it('should return an EmbeddingResult', async () => { + const expectedResult: EmbeddingResult = { + id: 'embed-123', + model: 'model-a', + embeddings: [[0.5, 0.6, 0.7]], + usage: { promptTokens: 15, totalTokens: 15 }, + } + + const adapter = new MockEmbeddingAdapter(expectedResult) + + const result = await ai({ + adapter, + model: 'model-a', + input: ['Test text'], + }) + + expect(result).toEqual(expectedResult) + }) + + it('should pass options to the embedding adapter', async () => { + const adapter = new MockEmbeddingAdapter() + const createEmbeddingsSpy = vi.spyOn(adapter, 'createEmbeddings') + + await ai({ + adapter, + model: 'model-a', + input: ['Hello', 'World'], + }) + + expect(createEmbeddingsSpy).toHaveBeenCalled() + }) + }) + + describe('with summarize adapter', () => { + it('should return a SummarizationResult', async () => { + const expectedResult: SummarizationResult = { + id: 'sum-456', + model: 'model-b', + summary: 'A concise summary of the text.', + usage: { promptTokens: 200, completionTokens: 30, totalTokens: 230 }, + } + + const adapter = new MockSummarizeAdapter(expectedResult) + + const result = await ai({ + adapter, + model: 'model-b', + text: 'Long text to summarize...', + }) + + expect(result).toEqual(expectedResult) + }) + + it('should pass options to the summarize adapter', async () => { + const adapter = new MockSummarizeAdapter() + const summarizeSpy = vi.spyOn(adapter, 'summarize') + + await ai({ + adapter, + model: 'model-a', + text: 'Some text to summarize', + style: 'bullet-points', + maxLength: 100, + }) + + expect(summarizeSpy).toHaveBeenCalled() + }) + }) + + describe('type safety', () => { + it('should have proper return type inference for text adapter', () => { + const adapter = new MockTextAdapter([]) + const messages: Array = [] + + // TypeScript should infer AsyncIterable + const result = ai({ + adapter, + model: 'model-a', + messages, + }) + + // This ensures the type is AsyncIterable, not Promise + expect(typeof result[Symbol.asyncIterator]).toBe('function') + }) + + it('should have proper return type inference for embedding adapter', () => { + const adapter = new MockEmbeddingAdapter() + + // TypeScript should infer Promise + const result = ai({ + adapter, + model: 'model-a', + input: ['test'], + }) + + // This ensures the type is Promise + expect(result).toBeInstanceOf(Promise) + }) + + it('should have proper return type inference for summarize adapter', () => { + const adapter = new MockSummarizeAdapter() + + // TypeScript should infer Promise + const result = ai({ + adapter, + model: 'model-a', + text: 'test', + }) + + // This ensures the type is Promise + expect(result).toBeInstanceOf(Promise) + }) + }) +}) diff --git a/packages/typescript/ai/tests/message-updaters.test.ts b/packages/typescript/ai/tests/message-updaters.test.ts index b984ba6b..327d2462 100644 --- a/packages/typescript/ai/tests/message-updaters.test.ts +++ b/packages/typescript/ai/tests/message-updaters.test.ts @@ -8,7 +8,7 @@ import { updateToolCallState, updateToolCallWithOutput, updateToolResultPart, -} from '../src/stream/message-updaters' +} from '../src/activities/text/stream/message-updaters' import type { ToolCallPart, UIMessage } from '../src/types' // Helper to create a test message diff --git a/packages/typescript/ai/tests/strategies.test.ts b/packages/typescript/ai/tests/strategies.test.ts index 9b257903..5bfadc00 100644 --- a/packages/typescript/ai/tests/strategies.test.ts +++ b/packages/typescript/ai/tests/strategies.test.ts @@ -5,7 +5,7 @@ import { BatchStrategy, WordBoundaryStrategy, CompositeStrategy, -} from '../src/stream/strategies' +} from '../src/activities/text/stream/strategies' describe('Chunk Strategies', () => { describe('ImmediateStrategy', () => { diff --git a/packages/typescript/ai/tests/stream-processor-edge-cases.test.ts b/packages/typescript/ai/tests/stream-processor-edge-cases.test.ts index 77390fb2..5bc22379 100644 --- a/packages/typescript/ai/tests/stream-processor-edge-cases.test.ts +++ b/packages/typescript/ai/tests/stream-processor-edge-cases.test.ts @@ -1,9 +1,9 @@ import { describe, expect, it, vi } from 'vitest' -import { StreamProcessor } from '../src/stream' +import { StreamProcessor } from '../src/activities/text/stream' import type { StreamProcessorEvents, StreamProcessorHandlers, -} from '../src/stream' +} from '../src/activities/text/stream' describe('StreamProcessor Edge Cases and Real-World Scenarios', () => { describe('Content Chunk Delta/Content Fallback Logic', () => { diff --git a/packages/typescript/ai/tests/stream-processor-replay.test.ts b/packages/typescript/ai/tests/stream-processor-replay.test.ts index 9087180e..3bbaf183 100644 --- a/packages/typescript/ai/tests/stream-processor-replay.test.ts +++ b/packages/typescript/ai/tests/stream-processor-replay.test.ts @@ -1,8 +1,8 @@ import { describe, it, expect } from 'vitest' import { readFile } from 'fs/promises' import { join } from 'path' -import { StreamProcessor } from '../src/stream' -import type { ChunkRecording } from '../src/stream/types' +import { StreamProcessor } from '../src/activities/text/stream' +import type { ChunkRecording } from '../src/activities/text/stream/types' async function loadFixture(name: string): Promise { const fixturePath = join(__dirname, 'fixtures', `${name}.json`) diff --git a/packages/typescript/ai/tests/stream-processor.test.ts b/packages/typescript/ai/tests/stream-processor.test.ts index 1f92239d..3369eb7a 100644 --- a/packages/typescript/ai/tests/stream-processor.test.ts +++ b/packages/typescript/ai/tests/stream-processor.test.ts @@ -3,8 +3,8 @@ import { ImmediateStrategy, PunctuationStrategy, StreamProcessor, -} from '../src/stream' -import type { StreamProcessorHandlers } from '../src/stream' +} from '../src/activities/text/stream' +import type { StreamProcessorHandlers } from '../src/activities/text/stream' import type { StreamChunk, UIMessage } from '../src/types' // Mock stream generator helper diff --git a/packages/typescript/ai/tests/stream-to-response.test.ts b/packages/typescript/ai/tests/stream-to-response.test.ts index 38bc3853..dd4f6e4e 100644 --- a/packages/typescript/ai/tests/stream-to-response.test.ts +++ b/packages/typescript/ai/tests/stream-to-response.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect, vi } from 'vitest' import { toServerSentEventsStream, toStreamResponse, -} from '../src/utilities/stream-to-response' +} from '../src/stream-to-response' import type { StreamChunk } from '../src/types' // Helper to create mock async iterable diff --git a/packages/typescript/ai/tests/tool-call-manager.test.ts b/packages/typescript/ai/tests/tool-call-manager.test.ts index 9d74205c..98100500 100644 --- a/packages/typescript/ai/tests/tool-call-manager.test.ts +++ b/packages/typescript/ai/tests/tool-call-manager.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest' import { z } from 'zod' -import { ToolCallManager } from '../src/tools/tool-calls' +import { ToolCallManager } from '../src/activities/text/tools/tool-calls' import type { DoneStreamChunk, Tool } from '../src/types' describe('ToolCallManager', () => { diff --git a/packages/typescript/ai/tests/tool-definition.test.ts b/packages/typescript/ai/tests/tool-definition.test.ts index 0cb3a91c..681ebf91 100644 --- a/packages/typescript/ai/tests/tool-definition.test.ts +++ b/packages/typescript/ai/tests/tool-definition.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, vi } from 'vitest' import { z } from 'zod' -import { toolDefinition } from '../src/tools/tool-definition' +import { toolDefinition } from '../src/activities/text/tools/tool-definition' describe('toolDefinition', () => { it('should create a tool definition with basic properties', () => { diff --git a/packages/typescript/ai/tests/zod-converter.test.ts b/packages/typescript/ai/tests/zod-converter.test.ts index adcd35ba..d5dc4d7f 100644 --- a/packages/typescript/ai/tests/zod-converter.test.ts +++ b/packages/typescript/ai/tests/zod-converter.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { z } from 'zod' -import { convertZodToJsonSchema } from '../src/tools/zod-converter' +import { convertZodToJsonSchema } from '../src/activities/text/tools/zod-converter' import type { JSONSchema } from '../src/types' describe('convertZodToJsonSchema', () => { diff --git a/packages/typescript/ai/vite.config.ts b/packages/typescript/ai/vite.config.ts index c6c899af..12f112dc 100644 --- a/packages/typescript/ai/vite.config.ts +++ b/packages/typescript/ai/vite.config.ts @@ -29,7 +29,11 @@ const config = defineConfig({ export default mergeConfig( config, tanstackViteConfig({ - entry: ['./src/index.ts', './src/event-client.ts'], + entry: [ + './src/index.ts', + './src/event-client.ts', + './src/activities/index.ts', + ], srcDir: './src', cjs: false, }), diff --git a/packages/typescript/react-ai-devtools/package.json b/packages/typescript/react-ai-devtools/package.json index 74f64f03..d2b08550 100644 --- a/packages/typescript/react-ai-devtools/package.json +++ b/packages/typescript/react-ai-devtools/package.json @@ -53,7 +53,7 @@ }, "devDependencies": { "@vitest/coverage-v8": "4.0.14", - "vite": "^7.2.4" + "vite": "^7.2.7" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", diff --git a/packages/typescript/smoke-tests/adapters/.env.example b/packages/typescript/smoke-tests/adapters/.env.example index 02ee6697..f9bbe386 100644 --- a/packages/typescript/smoke-tests/adapters/.env.example +++ b/packages/typescript/smoke-tests/adapters/.env.example @@ -2,14 +2,30 @@ ANTHROPIC_API_KEY=your_anthropic_api_key_here OPENAI_API_KEY=your_openai_api_key_here GEMINI_API_KEY=your_gemini_api_key_here +XAI_API_KEY=your_grok_api_key_here # Ollama models (optional, defaults shown) OLLAMA_MODEL=mistral:7b OLLAMA_SUMMARY_MODEL=mistral:7b OLLAMA_EMBEDDING_MODEL=nomic-embed-text -# Optional overrides for embeddings/summarization on hosted providers +# Optional overrides for OpenAI +OPENAI_MODEL=gpt-4o-mini OPENAI_EMBEDDING_MODEL=text-embedding-3-small OPENAI_SUMMARY_MODEL=gpt-4o-mini -GEMINI_EMBEDDING_MODEL=embedding-001 -GEMINI_SUMMARY_MODEL=gemini-2.5-flash +OPENAI_IMAGE_MODEL=gpt-image-1 + +# Optional overrides for Anthropic +ANTHROPIC_MODEL=claude-3-5-haiku-20241022 +ANTHROPIC_SUMMARY_MODEL=claude-3-5-haiku-20241022 + +# Optional overrides for Gemini +GEMINI_MODEL=gemini-2.0-flash-lite +GEMINI_EMBEDDING_MODEL=gemini-embedding-001 +GEMINI_SUMMARY_MODEL=gemini-2.0-flash-lite +GEMINI_IMAGE_MODEL=imagen-3.0-generate-002 + +# Optional overrides for Grok/xAI +GROK_MODEL=grok-3 +GROK_SUMMARY_MODEL=grok-3 +GROK_IMAGE_MODEL=grok-2-image-1212 diff --git a/packages/typescript/smoke-tests/adapters/fixtures/test-audio.mp3 b/packages/typescript/smoke-tests/adapters/fixtures/test-audio.mp3 new file mode 100644 index 00000000..08466969 Binary files /dev/null and b/packages/typescript/smoke-tests/adapters/fixtures/test-audio.mp3 differ diff --git a/packages/typescript/smoke-tests/adapters/package.json b/packages/typescript/smoke-tests/adapters/package.json index 376bd61a..07d11da6 100644 --- a/packages/typescript/smoke-tests/adapters/package.json +++ b/packages/typescript/smoke-tests/adapters/package.json @@ -7,7 +7,7 @@ "license": "MIT", "type": "module", "scripts": { - "start": "tsx src/index.ts", + "start": "tsx src/cli.ts", "typecheck": "tsc --noEmit" }, "dependencies": { @@ -15,7 +15,8 @@ "@tanstack/ai-anthropic": "workspace:*", "@tanstack/ai-gemini": "workspace:*", "@tanstack/ai-ollama": "workspace:*", - "@tanstack/ai-openai": "workspace:*" + "@tanstack/ai-openai": "workspace:*", + "commander": "^13.1.0" }, "devDependencies": { "@alcyone-labs/zod-to-json-schema": "^4.0.10", diff --git a/packages/typescript/smoke-tests/adapters/src/adapters/index.ts b/packages/typescript/smoke-tests/adapters/src/adapters/index.ts new file mode 100644 index 00000000..c239d326 --- /dev/null +++ b/packages/typescript/smoke-tests/adapters/src/adapters/index.ts @@ -0,0 +1,223 @@ +import { + createAnthropicSummarize, + createAnthropicText, +} from '@tanstack/ai-anthropic' +import { + createGeminiEmbed, + createGeminiImage, + createGeminiSummarize, + createGeminiText, + createGeminiTTS, +} from '@tanstack/ai-gemini' +import { + createOllamaEmbed, + createOllamaSummarize, + createOllamaText, +} from '@tanstack/ai-ollama' +import { + createOpenaiEmbed, + createOpenaiImage, + createOpenaiSummarize, + createOpenaiText, + createOpenaiTTS, + createOpenaiTranscription, +} from '@tanstack/ai-openai' + +/** + * Adapter set containing all adapters for a provider + */ +export interface AdapterSet { + /** Text/Chat adapter for conversational AI */ + textAdapter: any + /** Summarize adapter for text summarization */ + summarizeAdapter?: any + /** Embedding adapter for vector embeddings */ + embeddingAdapter?: any + /** Image adapter for image generation */ + imageAdapter?: any + /** TTS adapter for text-to-speech */ + ttsAdapter?: any + /** Transcription adapter for speech-to-text */ + transcriptionAdapter?: any + /** Model to use for chat */ + chatModel: string + /** Model to use for summarization */ + summarizeModel: string + /** Model to use for embeddings */ + embeddingModel: string + /** Model to use for image generation */ + imageModel?: string + /** Model to use for TTS */ + ttsModel?: string + /** Model to use for transcription */ + transcriptionModel?: string +} + +/** + * Definition for an adapter provider + */ +export interface AdapterDefinition { + /** Unique identifier (lowercase) */ + id: string + /** Human-readable name */ + name: string + /** Environment variable key for API key (null if not required) */ + envKey: string | null + /** Factory function to create adapters (returns null if env key is missing) */ + create: () => AdapterSet | null +} + +// Model defaults from environment or sensible defaults +const ANTHROPIC_MODEL = + process.env.ANTHROPIC_MODEL || 'claude-3-5-haiku-20241022' +const ANTHROPIC_SUMMARY_MODEL = + process.env.ANTHROPIC_SUMMARY_MODEL || ANTHROPIC_MODEL +const ANTHROPIC_EMBEDDING_MODEL = + process.env.ANTHROPIC_EMBEDDING_MODEL || ANTHROPIC_MODEL + +const OPENAI_MODEL = process.env.OPENAI_MODEL || 'gpt-4o-mini' +const OPENAI_SUMMARY_MODEL = process.env.OPENAI_SUMMARY_MODEL || OPENAI_MODEL +const OPENAI_EMBEDDING_MODEL = + process.env.OPENAI_EMBEDDING_MODEL || 'text-embedding-3-small' +const OPENAI_IMAGE_MODEL = process.env.OPENAI_IMAGE_MODEL || 'gpt-image-1' +const OPENAI_TTS_MODEL = process.env.OPENAI_TTS_MODEL || 'tts-1' +const OPENAI_TRANSCRIPTION_MODEL = + process.env.OPENAI_TRANSCRIPTION_MODEL || 'whisper-1' + +const GEMINI_MODEL = process.env.GEMINI_MODEL || 'gemini-2.0-flash-lite' +const GEMINI_SUMMARY_MODEL = process.env.GEMINI_SUMMARY_MODEL || GEMINI_MODEL +const GEMINI_EMBEDDING_MODEL = + process.env.GEMINI_EMBEDDING_MODEL || 'gemini-embedding-001' +const GEMINI_IMAGE_MODEL = + process.env.GEMINI_IMAGE_MODEL || 'imagen-3.0-generate-002' +const GEMINI_TTS_MODEL = + process.env.GEMINI_TTS_MODEL || 'gemini-2.5-flash-preview-tts' + +const OLLAMA_MODEL = process.env.OLLAMA_MODEL || 'mistral:7b' +const OLLAMA_SUMMARY_MODEL = process.env.OLLAMA_SUMMARY_MODEL || OLLAMA_MODEL +const OLLAMA_EMBEDDING_MODEL = + process.env.OLLAMA_EMBEDDING_MODEL || 'nomic-embed-text' + +/** + * Create Anthropic adapters + */ +function createAnthropicAdapters(): AdapterSet | null { + const apiKey = process.env.ANTHROPIC_API_KEY + if (!apiKey) return null + + return { + textAdapter: createAnthropicText(apiKey), + summarizeAdapter: createAnthropicSummarize(apiKey), + // Anthropic does not support embeddings or image generation natively + embeddingAdapter: undefined, + imageAdapter: undefined, + chatModel: ANTHROPIC_MODEL, + summarizeModel: ANTHROPIC_SUMMARY_MODEL, + embeddingModel: ANTHROPIC_EMBEDDING_MODEL, + } +} + +/** + * Create OpenAI adapters + */ +function createOpenAIAdapters(): AdapterSet | null { + const apiKey = process.env.OPENAI_API_KEY + if (!apiKey) return null + + return { + textAdapter: createOpenaiText(apiKey), + summarizeAdapter: createOpenaiSummarize(apiKey), + embeddingAdapter: createOpenaiEmbed(apiKey), + imageAdapter: createOpenaiImage(apiKey), + ttsAdapter: createOpenaiTTS(apiKey), + transcriptionAdapter: createOpenaiTranscription(apiKey), + chatModel: OPENAI_MODEL, + summarizeModel: OPENAI_SUMMARY_MODEL, + embeddingModel: OPENAI_EMBEDDING_MODEL, + imageModel: OPENAI_IMAGE_MODEL, + ttsModel: OPENAI_TTS_MODEL, + transcriptionModel: OPENAI_TRANSCRIPTION_MODEL, + } +} + +/** + * Create Gemini adapters + */ +function createGeminiAdapters(): AdapterSet | null { + const apiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY + if (!apiKey) return null + + return { + textAdapter: createGeminiText(apiKey), + summarizeAdapter: createGeminiSummarize(apiKey), + embeddingAdapter: createGeminiEmbed(apiKey), + imageAdapter: createGeminiImage(apiKey), + ttsAdapter: createGeminiTTS(apiKey), + chatModel: GEMINI_MODEL, + summarizeModel: GEMINI_SUMMARY_MODEL, + embeddingModel: GEMINI_EMBEDDING_MODEL, + imageModel: GEMINI_IMAGE_MODEL, + ttsModel: GEMINI_TTS_MODEL, + } +} + +/** + * Create Ollama adapters (no API key required) + */ +function createOllamaAdapters(): AdapterSet | null { + return { + textAdapter: createOllamaText(), + summarizeAdapter: createOllamaSummarize(), + embeddingAdapter: createOllamaEmbed(), + // Ollama does not support image generation + imageAdapter: undefined, + chatModel: OLLAMA_MODEL, + summarizeModel: OLLAMA_SUMMARY_MODEL, + embeddingModel: OLLAMA_EMBEDDING_MODEL, + } +} + +/** + * Registry of all available adapters + */ +export const ADAPTERS: Array = [ + { + id: 'openai', + name: 'OpenAI', + envKey: 'OPENAI_API_KEY', + create: createOpenAIAdapters, + }, + { + id: 'anthropic', + name: 'Anthropic', + envKey: 'ANTHROPIC_API_KEY', + create: createAnthropicAdapters, + }, + { + id: 'gemini', + name: 'Gemini', + envKey: 'GEMINI_API_KEY', + create: createGeminiAdapters, + }, + + { + id: 'ollama', + name: 'Ollama', + envKey: null, + create: createOllamaAdapters, + }, +] + +/** + * Get adapter definition by ID + */ +export function getAdapter(id: string): AdapterDefinition | undefined { + return ADAPTERS.find((a) => a.id.toLowerCase() === id.toLowerCase()) +} + +/** + * Get all adapter IDs + */ +export function getAdapterIds(): Array { + return ADAPTERS.map((a) => a.id) +} diff --git a/packages/typescript/smoke-tests/adapters/src/cli.ts b/packages/typescript/smoke-tests/adapters/src/cli.ts new file mode 100644 index 00000000..b723e5a6 --- /dev/null +++ b/packages/typescript/smoke-tests/adapters/src/cli.ts @@ -0,0 +1,533 @@ +#!/usr/bin/env node + +import { config } from 'dotenv' +import { Command } from 'commander' +import { ADAPTERS, getAdapter } from './adapters' +import type { AdapterDefinition, AdapterSet } from './adapters' +import { TESTS, getTest, getDefaultTests } from './tests' +import type { TestDefinition, AdapterCapability } from './tests' +import type { AdapterContext, TestOutcome } from './harness' + +// Load .env.local first (higher priority), then .env +config({ path: '.env.local' }) +config({ path: '.env' }) + +interface AdapterResult { + adapter: string + tests: Record +} + +interface TestTask { + adapterDef: AdapterDefinition + adapterSet: AdapterSet + test: TestDefinition + ctx: AdapterContext +} + +/** + * Get the display width of a string, accounting for emojis + */ +function displayWidth(str: string): number { + // Emojis and some special characters take 2 columns + // This regex matches most common emojis + const emojiRegex = + /[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|✅|❌|⚠️/gu + const emojiCount = (str.match(emojiRegex) || []).length + // Each emoji takes ~2 display columns but counts as 1-2 in length + // We need to add extra padding for emojis + return str.length + emojiCount +} + +/** + * Pad a string to a display width, accounting for emojis + */ +function padEnd(str: string, width: number): string { + const currentWidth = displayWidth(str) + const padding = Math.max(0, width - currentWidth) + return str + ' '.repeat(padding) +} + +/** + * List available adapters and/or tests + */ +function listCommand(options: { adapters?: boolean; tests?: boolean }) { + const showAll = !options.adapters && !options.tests + + if (showAll || options.adapters) { + console.log('\n📦 Available Adapters:\n') + console.log(' ID Name Env Key Status') + console.log(' ---------- ---------- ------------------- ------') + for (const adapter of ADAPTERS) { + const envValue = adapter.envKey ? process.env[adapter.envKey] : null + const status = + adapter.envKey === null + ? '✅ Ready' + : envValue + ? '✅ Ready' + : '⚠️ Missing env' + + console.log( + ` ${adapter.id.padEnd(10)} ${adapter.name.padEnd(10)} ${(adapter.envKey || 'none').padEnd(19)} ${status}`, + ) + } + } + + if (showAll || options.tests) { + console.log('\n🧪 Available Tests:\n') + console.log(' ID Name Requires Description') + console.log(' --- -------------------- ---------- -----------') + for (const test of TESTS) { + const requires = test.requires.join(', ') + const skipNote = test.skipByDefault ? ' (skip by default)' : '' + console.log( + ` ${test.id} ${test.name.padEnd(20)} ${requires.padEnd(10)} ${test.description}${skipNote}`, + ) + } + } + + console.log('') +} + +/** + * Check if adapter has the required capability + */ +function hasCapability( + adapterSet: AdapterSet, + capability: AdapterCapability, +): boolean { + switch (capability) { + case 'text': + return !!adapterSet.textAdapter + case 'summarize': + return !!adapterSet.summarizeAdapter + case 'embedding': + return !!adapterSet.embeddingAdapter + case 'image': + return !!adapterSet.imageAdapter + case 'tts': + return !!adapterSet.ttsAdapter + case 'transcription': + return !!adapterSet.transcriptionAdapter + default: + return false + } +} + +/** + * Format the results grid with proper emoji alignment + */ +function formatGrid(results: AdapterResult[], testsRun: TestDefinition[]) { + const headers = ['Adapter', ...testsRun.map((t) => t.id)] + + // Build rows with result indicators + const rows = results.map((result) => [ + result.adapter, + ...testsRun.map((test) => { + const outcome = result.tests[test.id] + if (!outcome) return '—' + if (outcome.ignored) return '—' + return outcome.passed ? '✅' : '❌' + }), + ]) + + // Calculate column widths based on display width + const colWidths = headers.map((header, index) => { + const headerWidth = displayWidth(header) + const maxCellWidth = Math.max( + ...rows.map((row) => displayWidth(row[index] || '')), + ) + return Math.max(headerWidth, maxCellWidth) + }) + + const separator = colWidths.map((w) => '-'.repeat(w)).join('-+-') + const formatRow = (row: string[]) => + row.map((cell, idx) => padEnd(cell, colWidths[idx]!)).join(' | ') + + console.log(formatRow(headers)) + console.log(separator) + rows.forEach((row) => console.log(formatRow(row))) +} + +/** + * Clear the current line and move cursor to beginning + */ +function clearLine() { + process.stdout.write('\r\x1b[K') +} + +/** + * Update progress display + */ +function updateProgress( + completed: number, + total: number, + running: string[], + failed: number, +) { + clearLine() + const runningStr = + running.length > 0 ? ` | Running: ${running.join(', ')}` : '' + const failedStr = failed > 0 ? ` | ❌ ${failed} failed` : '' + process.stdout.write( + `⏳ Progress: ${completed}/${total} completed${failedStr}${runningStr}`, + ) +} + +/** + * Run tests sequentially (original behavior) + */ +async function runSequential( + adaptersToRun: AdapterDefinition[], + testsToRun: TestDefinition[], +): Promise { + const results: AdapterResult[] = [] + + for (const adapterDef of adaptersToRun) { + const adapterSet = adapterDef.create() + + if (!adapterSet) { + console.log( + `⚠️ Skipping ${adapterDef.name}: ${adapterDef.envKey} not set`, + ) + continue + } + + console.log(`\n${adapterDef.name}`) + + const adapterResult: AdapterResult = { + adapter: adapterDef.name, + tests: {}, + } + + const ctx: AdapterContext = { + adapterName: adapterDef.name, + textAdapter: adapterSet.textAdapter, + summarizeAdapter: adapterSet.summarizeAdapter, + embeddingAdapter: adapterSet.embeddingAdapter, + imageAdapter: adapterSet.imageAdapter, + ttsAdapter: adapterSet.ttsAdapter, + transcriptionAdapter: adapterSet.transcriptionAdapter, + model: adapterSet.chatModel, + summarizeModel: adapterSet.summarizeModel, + embeddingModel: adapterSet.embeddingModel, + imageModel: adapterSet.imageModel, + ttsModel: adapterSet.ttsModel, + transcriptionModel: adapterSet.transcriptionModel, + } + + for (const test of testsToRun) { + const missingCapabilities = test.requires.filter( + (cap) => !hasCapability(adapterSet, cap), + ) + + if (missingCapabilities.length > 0) { + console.log( + `[${adapterDef.name}] — ${test.id}: Ignored (missing: ${missingCapabilities.join(', ')})`, + ) + adapterResult.tests[test.id] = { passed: true, ignored: true } + continue + } + + adapterResult.tests[test.id] = await test.run(ctx) + } + + results.push(adapterResult) + } + + return results +} + +/** + * Run tests in parallel with progress display + */ +async function runParallel( + adaptersToRun: AdapterDefinition[], + testsToRun: TestDefinition[], + concurrency: number, +): Promise { + // Build task queue + const tasks: TestTask[] = [] + const resultsMap = new Map() + const skippedAdapters: string[] = [] + + for (const adapterDef of adaptersToRun) { + const adapterSet = adapterDef.create() + + if (!adapterSet) { + skippedAdapters.push(`${adapterDef.name} (${adapterDef.envKey} not set)`) + continue + } + + // Initialize result for this adapter + const adapterResult: AdapterResult = { + adapter: adapterDef.name, + tests: {}, + } + resultsMap.set(adapterDef.id, adapterResult) + + const ctx: AdapterContext = { + adapterName: adapterDef.name, + textAdapter: adapterSet.textAdapter, + summarizeAdapter: adapterSet.summarizeAdapter, + embeddingAdapter: adapterSet.embeddingAdapter, + imageAdapter: adapterSet.imageAdapter, + ttsAdapter: adapterSet.ttsAdapter, + transcriptionAdapter: adapterSet.transcriptionAdapter, + model: adapterSet.chatModel, + summarizeModel: adapterSet.summarizeModel, + embeddingModel: adapterSet.embeddingModel, + imageModel: adapterSet.imageModel, + ttsModel: adapterSet.ttsModel, + transcriptionModel: adapterSet.transcriptionModel, + } + + for (const test of testsToRun) { + const missingCapabilities = test.requires.filter( + (cap) => !hasCapability(adapterSet, cap), + ) + + if (missingCapabilities.length > 0) { + // Mark as ignored immediately + adapterResult.tests[test.id] = { passed: true, ignored: true } + continue + } + + tasks.push({ adapterDef, adapterSet, test, ctx }) + } + } + + // Show skipped adapters + if (skippedAdapters.length > 0) { + console.log(`⚠️ Skipping: ${skippedAdapters.join(', ')}`) + } + + const total = tasks.length + let completed = 0 + let failed = 0 + const running = new Set() + const failedTests: Array<{ name: string; error: string }> = [] + + // Show initial progress + console.log( + `\n🔄 Running ${total} tests with ${concurrency} parallel workers\n`, + ) + updateProgress(completed, total, Array.from(running), failed) + + // Suppress console.log during parallel execution + const originalLog = console.log + console.log = () => {} + + // Process tasks with limited concurrency + const taskQueue = [...tasks] + + async function runTask(task: TestTask): Promise { + const taskName = `${task.adapterDef.name}/${task.test.id}` + running.add(taskName) + updateProgress(completed, total, Array.from(running), failed) + + try { + const outcome = await task.test.run(task.ctx) + const adapterResult = resultsMap.get(task.adapterDef.id)! + adapterResult.tests[task.test.id] = outcome + + if (!outcome.passed && !outcome.ignored) { + failed++ + failedTests.push({ + name: taskName, + error: outcome.error || 'Unknown error', + }) + } + } catch (error: any) { + const adapterResult = resultsMap.get(task.adapterDef.id)! + const errorMsg = error?.message || String(error) + adapterResult.tests[task.test.id] = { passed: false, error: errorMsg } + failed++ + failedTests.push({ name: taskName, error: errorMsg }) + } + + running.delete(taskName) + completed++ + updateProgress(completed, total, Array.from(running), failed) + } + + // Run with concurrency limit + const workers: Promise[] = [] + + async function worker() { + while (taskQueue.length > 0) { + const task = taskQueue.shift() + if (task) { + await runTask(task) + } + } + } + + // Start workers + for (let i = 0; i < Math.min(concurrency, tasks.length); i++) { + workers.push(worker()) + } + + // Wait for all workers to complete + await Promise.all(workers) + + // Restore console.log + console.log = originalLog + + // Clear progress line and show completion + clearLine() + console.log(`✅ Completed ${total} tests (${failed} failed)\n`) + + // Show failed tests summary + if (failedTests.length > 0) { + console.log('Failed tests:') + for (const ft of failedTests) { + console.log(` ❌ ${ft.name}: ${ft.error}`) + } + console.log('') + } + + // Return results in adapter order + return adaptersToRun + .filter((a) => resultsMap.has(a.id)) + .map((a) => resultsMap.get(a.id)!) +} + +/** + * Run tests with optional filtering + */ +async function runCommand(options: { + adapters?: string + tests?: string + parallel?: string +}) { + // Parse adapter filter + const adapterFilter = options.adapters + ? options.adapters.split(',').map((a) => a.trim().toLowerCase()) + : null + + // Parse test filter + const testFilter = options.tests + ? options.tests.split(',').map((t) => t.trim().toUpperCase()) + : null + + // Parse parallel option (default to 5) + const parallel = options.parallel ? parseInt(options.parallel, 10) : 5 + + // Determine which adapters to run + const adaptersToRun = adapterFilter + ? ADAPTERS.filter((a) => adapterFilter.includes(a.id.toLowerCase())) + : ADAPTERS + + // Validate adapter filter + if (adapterFilter) { + for (const id of adapterFilter) { + if (!getAdapter(id)) { + console.error(`❌ Unknown adapter: "${id}"`) + console.error( + ` Valid adapters: ${ADAPTERS.map((a) => a.id).join(', ')}`, + ) + process.exit(1) + } + } + } + + // Determine which tests to run + let testsToRun: TestDefinition[] + if (testFilter) { + testsToRun = [] + for (const id of testFilter) { + const test = getTest(id) + if (!test) { + console.error(`❌ Unknown test: "${id}"`) + console.error(` Valid tests: ${TESTS.map((t) => t.id).join(', ')}`) + process.exit(1) + } + testsToRun.push(test) + } + } else { + testsToRun = getDefaultTests() + } + + console.log('🚀 Starting TanStack AI adapter tests') + console.log(` Adapters: ${adaptersToRun.map((a) => a.name).join(', ')}`) + console.log(` Tests: ${testsToRun.map((t) => t.id).join(', ')}`) + console.log(` Parallel: ${parallel}`) + + // Run tests + let results: AdapterResult[] + if (parallel > 1) { + results = await runParallel(adaptersToRun, testsToRun, parallel) + } else { + results = await runSequential(adaptersToRun, testsToRun) + } + + console.log('\n') + + if (results.length === 0) { + console.log('⚠️ No tests were run.') + if (adapterFilter) { + console.log( + ' The specified adapters may not be configured or available.', + ) + } + process.exit(1) + } + + // Print results grid + formatGrid(results, testsToRun) + + // Check for failures + const allPassed = results.every((result) => + testsToRun.every((test) => { + const outcome = result.tests[test.id] + return !outcome || outcome.ignored || outcome.passed + }), + ) + + console.log('\n' + '='.repeat(60)) + if (allPassed) { + console.log('✅ All tests passed!') + process.exit(0) + } else { + console.log('❌ Some tests failed') + process.exit(1) + } +} + +// Set up CLI +const program = new Command() + .name('tanstack-ai-tests') + .description('TanStack AI adapter smoke tests') + .version('1.0.0') + +program + .command('list') + .description('List available adapters and tests') + .option('--adapters', 'List adapters only') + .option('--tests', 'List tests only') + .action(listCommand) + +program + .command('run') + .description('Run tests') + .option( + '--adapters ', + 'Comma-separated list of adapters (e.g., openai,gemini)', + ) + .option( + '--tests ', + 'Comma-separated list of test acronyms (e.g., CST,OST,STR)', + ) + .option( + '--parallel ', + 'Number of tests to run in parallel (default: 5, use 1 for sequential)', + '5', + ) + .action(runCommand) + +// Default command is 'run' for backward compatibility +program.action(() => { + runCommand({}) +}) + +program.parse() diff --git a/packages/typescript/smoke-tests/adapters/src/harness.ts b/packages/typescript/smoke-tests/adapters/src/harness.ts index 25d01ee6..026b7393 100644 --- a/packages/typescript/smoke-tests/adapters/src/harness.ts +++ b/packages/typescript/smoke-tests/adapters/src/harness.ts @@ -1,10 +1,19 @@ import { mkdir, writeFile } from 'node:fs/promises' import { join } from 'node:path' -import { chat } from '@tanstack/ai' +import { ai } from '@tanstack/ai' import type { Tool } from '@tanstack/ai' const OUTPUT_DIR = join(process.cwd(), 'output') +/** + * Result of a test run + */ +export interface TestOutcome { + passed: boolean + error?: string + ignored?: boolean +} + interface ToolCallCapture { id: string name: string @@ -38,10 +47,30 @@ interface StreamCapture { export interface AdapterContext { adapterName: string - adapter: any + /** Text/Chat adapter for conversational AI */ + textAdapter: any + /** Summarize adapter for text summarization */ + summarizeAdapter?: any + /** Embedding adapter for vector embeddings */ + embeddingAdapter?: any + /** Image adapter for image generation */ + imageAdapter?: any + /** TTS adapter for text-to-speech */ + ttsAdapter?: any + /** Transcription adapter for speech-to-text */ + transcriptionAdapter?: any + /** Model for chat/text */ model: string + /** Model for summarization */ summarizeModel?: string + /** Model for embeddings */ embeddingModel?: string + /** Model for image generation */ + imageModel?: string + /** Model for TTS */ + ttsModel?: string + /** Model for transcription */ + transcriptionModel?: string } interface DebugEnvelope { @@ -122,7 +151,7 @@ export async function captureStream(opts: { adapterName: string testName: string phase: string - adapter: any + textAdapter: any model: string messages: Array tools?: Array @@ -132,15 +161,15 @@ export async function captureStream(opts: { adapterName: _adapterName, testName: _testName, phase, - adapter, + textAdapter, model, messages, tools, agentLoopStrategy, } = opts - const stream = chat({ - adapter, + const stream = ai({ + adapter: textAdapter, model, messages, tools, @@ -289,7 +318,7 @@ export async function runTestCase(opts: { const { adapterContext, testName, - description, + description: _description, messages, tools, agentLoopStrategy, @@ -308,7 +337,7 @@ export async function runTestCase(opts: { adapterName: adapterContext.adapterName, testName, phase: 'main', - adapter: adapterContext.adapter, + textAdapter: adapterContext.textAdapter, model: adapterContext.model, messages, tools, diff --git a/packages/typescript/smoke-tests/adapters/src/index.ts b/packages/typescript/smoke-tests/adapters/src/index.ts index 43e19f41..1e31c942 100644 --- a/packages/typescript/smoke-tests/adapters/src/index.ts +++ b/packages/typescript/smoke-tests/adapters/src/index.ts @@ -1,686 +1,37 @@ -import { config } from 'dotenv' -import { - chat, - embedding, - summarize, - toolDefinition, - maxIterations, - type Tool, -} from '@tanstack/ai' -import { z } from 'zod' -import { createAnthropic } from '@tanstack/ai-anthropic' -import { createGemini } from '@tanstack/ai-gemini' -import { ollama } from '@tanstack/ai-ollama' -import { createOpenAI } from '@tanstack/ai-openai' -import { - AdapterContext, - buildApprovalMessages, +/** + * TanStack AI Adapter Smoke Tests + * + * This module provides programmatic access to the test suite. + * For CLI usage, run: pnpm start [command] [options] + * + * @example + * ```bash + * # List available adapters and tests + * pnpm start list + * + * # Run all tests on all adapters + * pnpm start run + * + * # Run specific tests on specific adapters + * pnpm start run --adapters openai,gemini --tests CST,OST,STR + * ``` + */ + +// Re-export adapters +export { ADAPTERS, getAdapter, getAdapterIds } from './adapters' +export type { AdapterDefinition, AdapterSet } from './adapters' + +// Re-export tests +export { TESTS, getTest, getTestIds, getDefaultTests } from './tests' +export type { TestDefinition, AdapterCapability } from './tests' + +// Re-export harness utilities +export { + runTestCase, captureStream, + writeDebugFile, createDebugEnvelope, - runTestCase, summarizeRun, - writeDebugFile, + buildApprovalMessages, } from './harness' - -// Load .env.local first (higher priority), then .env -config({ path: '.env.local' }) -config({ path: '.env' }) - -const ANTHROPIC_MODEL = - process.env.ANTHROPIC_MODEL || 'claude-3-5-haiku-20241022' -const ANTHROPIC_SUMMARY_MODEL = - process.env.ANTHROPIC_SUMMARY_MODEL || ANTHROPIC_MODEL -const ANTHROPIC_EMBEDDING_MODEL = - process.env.ANTHROPIC_EMBEDDING_MODEL || ANTHROPIC_MODEL - -const OPENAI_MODEL = process.env.OPENAI_MODEL || 'gpt-4o-mini' -const OPENAI_SUMMARY_MODEL = process.env.OPENAI_SUMMARY_MODEL || OPENAI_MODEL -const OPENAI_EMBEDDING_MODEL = - process.env.OPENAI_EMBEDDING_MODEL || 'text-embedding-3-small' - -const GEMINI_MODEL = process.env.GEMINI_MODEL || 'gemini-2.0-flash-lite' -const GEMINI_SUMMARY_MODEL = process.env.GEMINI_SUMMARY_MODEL || GEMINI_MODEL -const GEMINI_EMBEDDING_MODEL = - process.env.GEMINI_EMBEDDING_MODEL || 'gemini-embedding-001' - -// Using llama3.2:3b for better stability with approval flows -// granite4:3b was flaky with approval-required tools -// Can override via OLLAMA_MODEL env var if needed -const OLLAMA_MODEL = process.env.OLLAMA_MODEL || 'mistral:7b' -const OLLAMA_SUMMARY_MODEL = process.env.OLLAMA_SUMMARY_MODEL || OLLAMA_MODEL -const OLLAMA_EMBEDDING_MODEL = - process.env.OLLAMA_EMBEDDING_MODEL || 'nomic-embed-text' - -type TestOutcome = { passed: boolean; error?: string; ignored?: boolean } - -interface AdapterResult { - adapter: string - model: string - summarizeModel: string - embeddingModel: string - tests: Record -} - -interface AdapterConfig { - name: string - chatModel: string - summarizeModel: string - embeddingModel: string - adapter: any -} - -interface TestDefinition { - id: string - label: string - run: (ctx: AdapterContext) => Promise -} - -async function testCapitalOfFrance( - adapterContext: AdapterContext, -): Promise { - return runTestCase({ - adapterContext, - testName: 'test1-chat-stream', - description: 'chat stream returns Paris for capital of France', - messages: [ - { role: 'user' as const, content: 'what is the capital of france' }, - ], - validate: (run) => { - const hasParis = run.fullResponse.toLowerCase().includes('paris') - return { - passed: hasParis, - error: hasParis ? undefined : "Response does not contain 'Paris'", - meta: { hasParis }, - } - }, - }) -} - -async function testTemperatureTool( - adapterContext: AdapterContext, -): Promise { - let toolExecuteCalled = false - let toolExecuteCallCount = 0 - const toolExecuteCalls: Array<{ - timestamp: string - arguments: any - result?: string - error?: string - }> = [] - - const expectedLocation = 'San Francisco' - - const temperatureTool = toolDefinition({ - name: 'get_temperature', - description: - 'Get the current temperature in degrees for a specific location', - inputSchema: z.object({ - location: z - .string() - .describe('The city or location to get the temperature for'), - }), - }).server(async (args) => { - toolExecuteCalled = true - toolExecuteCallCount++ - const callInfo: any = { - timestamp: new Date().toISOString(), - arguments: args, - } - try { - // Verify location was passed correctly - if (!args || typeof args !== 'object') { - throw new Error('Arguments must be an object') - } - if (!args.location || typeof args.location !== 'string') { - throw new Error('Location argument is missing or invalid') - } - - const result = '70' - callInfo.result = result - toolExecuteCalls.push(callInfo) - return result - } catch (error: any) { - callInfo.error = error.message - toolExecuteCalls.push(callInfo) - throw error - } - }) - - return runTestCase({ - adapterContext, - testName: 'test2-temperature-tool', - description: - 'tool call with location parameter returns a temperature value', - messages: [ - { - role: 'user' as const, - content: `use the get_temperature tool to get the temperature for ${expectedLocation} and report the answer as a number`, - }, - ], - tools: [temperatureTool], - agentLoopStrategy: maxIterations(20), - validate: (run) => { - const responseLower = run.fullResponse.toLowerCase() - const hasSeventy = - responseLower.includes('70') || responseLower.includes('seventy') - const toolCallFound = run.toolCalls.length > 0 - const toolResultFound = run.toolResults.length > 0 - - // Check that location was passed correctly - const locationPassedCorrectly = toolExecuteCalls.some( - (call) => - call.arguments && - call.arguments.location && - typeof call.arguments.location === 'string' && - call.arguments.location.length > 0, - ) - - // Check if the location matches what was requested (case-insensitive) - const locationMatches = toolExecuteCalls.some( - (call) => - call.arguments && - call.arguments.location && - call.arguments.location - .toLowerCase() - .includes(expectedLocation.toLowerCase()), - ) - - const issues: string[] = [] - if (!toolCallFound) issues.push('no tool call') - if (!toolResultFound) issues.push('no tool result') - if (!hasSeventy) issues.push("no '70' or 'seventy' in response") - if (!locationPassedCorrectly) - issues.push('location argument not passed or invalid') - if (!locationMatches) { - issues.push( - `location argument '${ - toolExecuteCalls[0]?.arguments?.location || 'missing' - }' does not match expected '${expectedLocation}'`, - ) - } - - return { - passed: - toolCallFound && - toolResultFound && - hasSeventy && - locationPassedCorrectly && - locationMatches, - error: issues.length ? issues.join(', ') : undefined, - meta: { - hasSeventy, - toolCallFound, - toolResultFound, - toolExecuteCalled, - toolExecuteCallCount, - toolExecuteCalls, - locationPassedCorrectly, - locationMatches, - expectedLocation, - actualLocation: toolExecuteCalls[0]?.arguments?.location, - }, - } - }, - }) -} - -async function testApprovalToolFlow( - adapterContext: AdapterContext, -): Promise { - const testName = 'test3-approval-tool-flow' - - let toolExecuteCalled = false - let toolExecuteCallCount = 0 - const toolExecuteCalls: Array<{ - timestamp: string - arguments: any - result?: string - error?: string - }> = [] - - const addToCartTool: Tool = toolDefinition({ - name: 'addToCart', - description: 'Add an item to the shopping cart', - inputSchema: z.object({ - item: z.string().describe('The name of the item to add to the cart'), - }), - needsApproval: true, - }).server(async (args) => { - toolExecuteCalled = true - toolExecuteCallCount++ - const callInfo: any = { - timestamp: new Date().toISOString(), - arguments: args, - } - try { - const result = JSON.stringify({ success: true, item: args.item }) - callInfo.result = result - toolExecuteCalls.push(callInfo) - return result - } catch (error: any) { - callInfo.error = error.message - toolExecuteCalls.push(callInfo) - throw error - } - }) - - const messages = [ - { - role: 'user' as const, - content: 'add a hammer to the cart', - }, - ] - - const debugData = createDebugEnvelope( - adapterContext.adapterName, - testName, - adapterContext.model, - messages, - [addToCartTool], - ) - - const requestRun = await captureStream({ - adapterName: adapterContext.adapterName, - testName, - phase: 'request', - adapter: adapterContext.adapter, - model: adapterContext.model, - messages, - tools: [addToCartTool], - agentLoopStrategy: maxIterations(20), - }) - - const approval = requestRun.approvalRequests[0] - const toolCall = requestRun.toolCalls[0] - - if (!approval || !toolCall) { - const error = `No approval request found. toolCalls: ${requestRun.toolCalls.length}, approvals: ${requestRun.approvalRequests.length}` - debugData.summary = { - request: summarizeRun(requestRun), - toolExecuteCalled, - toolExecuteCallCount, - toolExecuteCalls, - } - debugData.chunks = requestRun.chunks - debugData.result = { passed: false, error } - await writeDebugFile(adapterContext.adapterName, testName, debugData) - console.log(`[${adapterContext.adapterName}] ❌ ${testName}: ${error}`) - return { passed: false, error } - } - - const approvalMessages = buildApprovalMessages(messages, requestRun, approval) - - const approvedRun = await captureStream({ - adapterName: adapterContext.adapterName, - testName, - phase: 'approved', - adapter: adapterContext.adapter, - model: adapterContext.model, - messages: approvalMessages, - tools: [addToCartTool], - agentLoopStrategy: maxIterations(20), - }) - - const fullResponse = requestRun.fullResponse + ' ' + approvedRun.fullResponse - const hasHammerInResponse = fullResponse.toLowerCase().includes('hammer') - const passed = - requestRun.toolCalls.length > 0 && - requestRun.approvalRequests.length > 0 && - toolExecuteCalled && - toolExecuteCallCount === 1 && - hasHammerInResponse - - debugData.chunks = [...requestRun.chunks, ...approvedRun.chunks] - debugData.finalMessages = approvedRun.reconstructedMessages - debugData.summary = { - request: summarizeRun(requestRun), - approved: summarizeRun(approvedRun), - hasHammerInResponse, - toolExecuteCalled, - toolExecuteCallCount, - toolExecuteCalls, - } - debugData.result = { - passed, - error: passed - ? undefined - : `toolCallFound: ${ - requestRun.toolCalls.length > 0 - }, approvalRequestFound: ${ - requestRun.approvalRequests.length > 0 - }, toolExecuteCalled: ${toolExecuteCalled}, toolExecuteCallCount: ${toolExecuteCallCount}, hasHammerInResponse: ${hasHammerInResponse}`, - } - - await writeDebugFile(adapterContext.adapterName, testName, debugData) - console.log( - `[${adapterContext.adapterName}] ${passed ? '✅' : '❌'} ${testName}${ - passed ? '' : `: ${debugData.result.error}` - }`, - ) - - return { passed, error: debugData.result.error } -} - -async function testSummarize( - adapterContext: AdapterContext, -): Promise { - const testName = 'test5-summarize' - const adapterName = adapterContext.adapterName - const model = adapterContext.summarizeModel || adapterContext.model - const text = - 'Paris is the capital and most populous city of France, known for landmarks like the Eiffel Tower and the Louvre. It is a major center for art, fashion, gastronomy, and culture.' - - const debugData: Record = { - adapter: adapterName, - test: testName, - model, - timestamp: new Date().toISOString(), - input: { text, maxLength: 80, style: 'concise' as const }, - } - - try { - const result = await summarize({ - adapter: adapterContext.adapter, - model, - text, - maxLength: 80, - style: 'concise', - }) - - const summary = result.summary || '' - const summaryLower = summary.toLowerCase() - const passed = summary.length > 0 && summaryLower.includes('paris') - - debugData.summary = { - summary, - usage: result.usage, - summaryLength: summary.length, - } - debugData.result = { - passed, - error: passed ? undefined : "Summary missing 'Paris'", - } - - await writeDebugFile(adapterName, testName, debugData) - - console.log( - `[${adapterName}] ${passed ? '✅' : '❌'} ${testName}${ - passed ? '' : `: ${debugData.result.error}` - }`, - ) - - return { passed, error: debugData.result.error } - } catch (error: any) { - const message = error?.message || String(error) - debugData.summary = { error: message } - debugData.result = { passed: false, error: message } - await writeDebugFile(adapterName, testName, debugData) - console.log(`[${adapterName}] ❌ ${testName}: ${message}`) - return { passed: false, error: message } - } -} - -async function testEmbedding( - adapterContext: AdapterContext, -): Promise { - const testName = 'test6-embedding' - const adapterName = adapterContext.adapterName - - // Anthropic embedding is not supported, mark as ignored - if (adapterName === 'Anthropic') { - console.log(`[${adapterName}] ⋯ ${testName}: Ignored (not supported)`) - return { passed: true, ignored: true } - } - - const model = adapterContext.embeddingModel || adapterContext.model - const inputs = [ - 'The Eiffel Tower is located in Paris.', - 'The Colosseum is located in Rome.', - ] - - const debugData: Record = { - adapter: adapterName, - test: testName, - model, - timestamp: new Date().toISOString(), - input: { inputs }, - } - - try { - const result = await embedding({ - adapter: adapterContext.adapter, - model, - input: inputs, - }) - - const embeddings = result.embeddings || [] - const lengths = embeddings.map((e) => e?.length || 0) - const vectorsAreNumeric = embeddings.every( - (vec) => Array.isArray(vec) && vec.every((n) => typeof n === 'number'), - ) - const passed = - embeddings.length === inputs.length && - vectorsAreNumeric && - lengths.every((len) => len > 0) - - debugData.summary = { - embeddingLengths: lengths, - firstEmbeddingPreview: embeddings[0]?.slice(0, 8), - usage: result.usage, - } - debugData.result = { - passed, - error: passed ? undefined : 'Embeddings missing, empty, or invalid', - } - - await writeDebugFile(adapterName, testName, debugData) - - console.log( - `[${adapterName}] ${passed ? '✅' : '❌'} ${testName}${ - passed ? '' : `: ${debugData.result.error}` - }`, - ) - - return { passed, error: debugData.result.error } - } catch (error: any) { - const message = error?.message || String(error) - debugData.summary = { error: message } - debugData.result = { passed: false, error: message } - await writeDebugFile(adapterName, testName, debugData) - console.log(`[${adapterName}] ❌ ${testName}: ${message}`) - return { passed: false, error: message } - } -} - -const TEST_DEFINITIONS: TestDefinition[] = [ - { id: 'chat-stream', label: 'chat (stream)', run: testCapitalOfFrance }, - { id: 'tools', label: 'tools', run: testTemperatureTool }, - { id: 'approval', label: 'approval', run: testApprovalToolFlow }, - { id: 'summarize', label: 'summarize', run: testSummarize }, - { id: 'embedding', label: 'embedding', run: testEmbedding }, -] - -function shouldTestAdapter(adapterName: string, filter?: string): boolean { - if (!filter) return true - return adapterName.toLowerCase() === filter.toLowerCase() -} - -function formatGrid(results: AdapterResult[]) { - const headers = ['Adapter', ...TEST_DEFINITIONS.map((t) => t.label)] - const rows = results.map((result) => [ - `${result.adapter} (chat: ${result.model})`, - ...TEST_DEFINITIONS.map((test) => { - const outcome = result.tests[test.id] - if (!outcome) return '—' - if (outcome.ignored) return '⋯' - return outcome.passed ? '✅' : '❌' - }), - ]) - - const colWidths = headers.map((header, index) => - Math.max( - header.length, - ...rows.map((row) => (row[index] ? row[index].length : 0)), - ), - ) - - const separator = colWidths.map((w) => '-'.repeat(w)).join('-+-') - const formatRow = (row: string[]) => - row.map((cell, idx) => cell.padEnd(colWidths[idx])).join(' | ') - - console.log(formatRow(headers)) - console.log(separator) - rows.forEach((row) => console.log(formatRow(row))) -} - -async function runTests(filterAdapter?: string) { - if (filterAdapter) { - console.log(`🚀 Starting adapter tests for: ${filterAdapter}`) - } else { - console.log('🚀 Starting adapter tests for all adapters') - } - - const results: AdapterResult[] = [] - - const runAdapterSuite = async (config: AdapterConfig) => { - const ctx: AdapterContext = { - adapterName: config.name, - adapter: config.adapter, - model: config.chatModel, - summarizeModel: config.summarizeModel, - embeddingModel: config.embeddingModel, - } - - const adapterResult: AdapterResult = { - adapter: config.name, - model: config.chatModel, - summarizeModel: config.summarizeModel, - embeddingModel: config.embeddingModel, - tests: {}, - } - - console.log( - `\n${config.name} (chat: ${config.chatModel}, summarize: ${config.summarizeModel}, embedding: ${config.embeddingModel})`, - ) - - for (const test of TEST_DEFINITIONS) { - adapterResult.tests[test.id] = await test.run(ctx) - } - - results.push(adapterResult) - } - - // Anthropic - if (shouldTestAdapter('Anthropic', filterAdapter)) { - const anthropicApiKey = process.env.ANTHROPIC_API_KEY - if (anthropicApiKey) { - await runAdapterSuite({ - name: 'Anthropic', - chatModel: ANTHROPIC_MODEL, - summarizeModel: ANTHROPIC_SUMMARY_MODEL, - embeddingModel: ANTHROPIC_EMBEDDING_MODEL, - adapter: createAnthropic(anthropicApiKey), - }) - } else { - console.log('⚠️ Skipping Anthropic tests: ANTHROPIC_API_KEY not set') - } - } - - // OpenAI - if (shouldTestAdapter('OpenAI', filterAdapter)) { - const openaiApiKey = process.env.OPENAI_API_KEY - if (openaiApiKey) { - await runAdapterSuite({ - name: 'OpenAI', - chatModel: OPENAI_MODEL, - summarizeModel: OPENAI_SUMMARY_MODEL, - embeddingModel: OPENAI_EMBEDDING_MODEL, - adapter: createOpenAI(openaiApiKey), - }) - } else { - console.log('⚠️ Skipping OpenAI tests: OPENAI_API_KEY not set') - } - } - - // Gemini - if (shouldTestAdapter('Gemini', filterAdapter)) { - const geminiApiKey = - process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY - if (geminiApiKey) { - await runAdapterSuite({ - name: 'Gemini', - chatModel: GEMINI_MODEL, - summarizeModel: GEMINI_SUMMARY_MODEL, - embeddingModel: GEMINI_EMBEDDING_MODEL, - adapter: createGemini(geminiApiKey), - }) - } else { - console.log( - '⚠️ Skipping Gemini tests: GEMINI_API_KEY or GOOGLE_API_KEY not set', - ) - } - } - - // Ollama - if (shouldTestAdapter('Ollama', filterAdapter)) { - await runAdapterSuite({ - name: 'Ollama', - chatModel: OLLAMA_MODEL, - summarizeModel: OLLAMA_SUMMARY_MODEL, - embeddingModel: OLLAMA_EMBEDDING_MODEL, - adapter: ollama(), - }) - } - - console.log('\n') - - if (results.length === 0) { - console.log('\n⚠️ No tests were run.') - if (filterAdapter) { - console.log( - ` The adapter "${filterAdapter}" may not be configured or available.`, - ) - } - process.exit(1) - } - - formatGrid(results) - - const allPassed = results.every((result) => - TEST_DEFINITIONS.every((test) => { - const outcome = result.tests[test.id] - // Ignored tests don't count as failures - return !outcome || outcome.ignored || outcome.passed - }), - ) - - console.log('\n' + '='.repeat(60)) - if (allPassed) { - console.log('✅ All tests passed!') - process.exit(0) - } else { - console.log('❌ Some tests failed') - process.exit(1) - } -} - -// Get adapter name from command line arguments (e.g., "pnpm start ollama") -const filterAdapter = process.argv[2] - -// Validate adapter name if provided -if (filterAdapter) { - const validAdapters = ['anthropic', 'openai', 'gemini', 'ollama'] - const normalizedFilter = filterAdapter.toLowerCase() - if (!validAdapters.includes(normalizedFilter)) { - console.error( - `❌ Invalid adapter name: "${filterAdapter}"\n` + - `Valid adapters: ${validAdapters.join(', ')}`, - ) - process.exit(1) - } -} - -runTests(filterAdapter).catch((error) => { - console.error('Fatal error:', error) - process.exit(1) -}) +export type { AdapterContext, TestOutcome } from './harness' diff --git a/packages/typescript/smoke-tests/adapters/src/tests/ags-agentic-structured.ts b/packages/typescript/smoke-tests/adapters/src/tests/ags-agentic-structured.ts new file mode 100644 index 00000000..d78e762e --- /dev/null +++ b/packages/typescript/smoke-tests/adapters/src/tests/ags-agentic-structured.ts @@ -0,0 +1,128 @@ +import { ai, maxIterations, toolDefinition } from '@tanstack/ai' +import { z } from 'zod' +import { writeDebugFile } from '../harness' +import type { AdapterContext, TestOutcome } from '../harness' + +// Schema for product recommendation with price +const RecommendationSchema = z.object({ + productName: z.string().describe('Name of the recommended product'), + currentPrice: z.number().describe('Current price in dollars'), + reason: z.string().describe('Why this product is recommended'), +}) + +type Recommendation = z.infer + +/** + * AGS: Agentic Structured Output Test + * + * Tests structured output that requires tool calls to gather + * information before producing the final structured response. + */ +export async function runAGS( + adapterContext: AdapterContext, +): Promise { + const testName = 'ags-agentic-structured' + const adapterName = adapterContext.adapterName + + let toolCalled = false + let priceReturned: number | null = null + + // Tool that provides price information + const getPriceTool = toolDefinition({ + name: 'get_product_price', + description: 'Get the current price of a product', + inputSchema: z.object({ + productName: z.string().describe('Name of the product to look up'), + }), + }).server(async (args) => { + toolCalled = true + // Return a mock price based on product name + const price = args.productName.toLowerCase().includes('laptop') + ? 999.99 + : 49.99 + priceReturned = price + return JSON.stringify({ productName: args.productName, price }) + }) + + const debugData: Record = { + adapter: adapterName, + test: testName, + model: adapterContext.model, + timestamp: new Date().toISOString(), + } + + try { + const result = (await ai({ + adapter: adapterContext.textAdapter, + model: adapterContext.model, + messages: [ + { + role: 'user' as const, + content: + 'I need a laptop recommendation. First use the get_product_price tool to look up the price of "Gaming Laptop Pro", then give me a structured recommendation.', + }, + ], + tools: [getPriceTool], + agentLoopStrategy: maxIterations(10), + outputSchema: RecommendationSchema, + })) as Recommendation + + // Validate the result + const hasProductName = + typeof result.productName === 'string' && result.productName.length > 0 + const hasPrice = + typeof result.currentPrice === 'number' && result.currentPrice > 0 + const hasReason = + typeof result.reason === 'string' && result.reason.length > 0 + + // Verify the tool was called and price matches + const priceMatches = + priceReturned !== null && + Math.abs(result.currentPrice - priceReturned) < 0.01 + + const passed = + toolCalled && hasProductName && hasPrice && hasReason && priceMatches + + const issues: string[] = [] + if (!toolCalled) issues.push('tool was not called') + if (!hasProductName) issues.push('missing or invalid productName') + if (!hasPrice) issues.push('missing or invalid currentPrice') + if (!hasReason) issues.push('missing or invalid reason') + if (!priceMatches) { + issues.push( + `price mismatch: expected ${priceReturned}, got ${result.currentPrice}`, + ) + } + + debugData.summary = { + result, + toolCalled, + priceReturned, + hasProductName, + hasPrice, + hasReason, + priceMatches, + } + debugData.result = { + passed, + error: issues.length ? issues.join(', ') : undefined, + } + + await writeDebugFile(adapterName, testName, debugData) + + console.log( + `[${adapterName}] ${passed ? '✅' : '❌'} ${testName}${ + passed ? '' : `: ${debugData.result.error}` + }`, + ) + + return { passed, error: debugData.result.error } + } catch (error: any) { + const message = error?.message || String(error) + debugData.summary = { error: message, toolCalled, priceReturned } + debugData.result = { passed: false, error: message } + await writeDebugFile(adapterName, testName, debugData) + console.log(`[${adapterName}] ❌ ${testName}: ${message}`) + return { passed: false, error: message } + } +} diff --git a/packages/typescript/smoke-tests/adapters/src/tests/apr-approval-flow.ts b/packages/typescript/smoke-tests/adapters/src/tests/apr-approval-flow.ts new file mode 100644 index 00000000..f4d3e9a0 --- /dev/null +++ b/packages/typescript/smoke-tests/adapters/src/tests/apr-approval-flow.ts @@ -0,0 +1,154 @@ +import { maxIterations, toolDefinition } from '@tanstack/ai' +import { z } from 'zod' +import type { Tool } from '@tanstack/ai' +import { + buildApprovalMessages, + captureStream, + createDebugEnvelope, + summarizeRun, + writeDebugFile, +} from '../harness' +import type { AdapterContext, TestOutcome } from '../harness' + +/** + * APR: Approval Flow Test + * + * Tests the tool approval flow by using a tool that requires + * user approval before execution. + */ +export async function runAPR( + adapterContext: AdapterContext, +): Promise { + const testName = 'apr-approval-flow' + + let toolExecuteCalled = false + let toolExecuteCallCount = 0 + const toolExecuteCalls: Array<{ + timestamp: string + arguments: any + result?: string + error?: string + }> = [] + + const addToCartTool: Tool = toolDefinition({ + name: 'addToCart', + description: 'Add an item to the shopping cart', + inputSchema: z.object({ + item: z.string().describe('The name of the item to add to the cart'), + }), + needsApproval: true, + }).server(async (args) => { + toolExecuteCalled = true + toolExecuteCallCount++ + const callInfo: any = { + timestamp: new Date().toISOString(), + arguments: args, + } + try { + const result = JSON.stringify({ success: true, item: args.item }) + callInfo.result = result + toolExecuteCalls.push(callInfo) + return result + } catch (error: any) { + callInfo.error = error.message + toolExecuteCalls.push(callInfo) + throw error + } + }) + + const messages = [ + { + role: 'user' as const, + content: 'add a hammer to the cart', + }, + ] + + const debugData = createDebugEnvelope( + adapterContext.adapterName, + testName, + adapterContext.model, + messages, + [addToCartTool], + ) + + const requestRun = await captureStream({ + adapterName: adapterContext.adapterName, + testName, + phase: 'request', + textAdapter: adapterContext.textAdapter, + model: adapterContext.model, + messages, + tools: [addToCartTool], + agentLoopStrategy: maxIterations(20), + }) + + const approval = requestRun.approvalRequests[0] + const toolCall = requestRun.toolCalls[0] + + if (!approval || !toolCall) { + const error = `No approval request found. toolCalls: ${requestRun.toolCalls.length}, approvals: ${requestRun.approvalRequests.length}` + debugData.summary = { + request: summarizeRun(requestRun), + toolExecuteCalled, + toolExecuteCallCount, + toolExecuteCalls, + } + debugData.chunks = requestRun.chunks + debugData.result = { passed: false, error } + await writeDebugFile(adapterContext.adapterName, testName, debugData) + console.log(`[${adapterContext.adapterName}] ❌ ${testName}: ${error}`) + return { passed: false, error } + } + + const approvalMessages = buildApprovalMessages(messages, requestRun, approval) + + const approvedRun = await captureStream({ + adapterName: adapterContext.adapterName, + testName, + phase: 'approved', + textAdapter: adapterContext.textAdapter, + model: adapterContext.model, + messages: approvalMessages, + tools: [addToCartTool], + agentLoopStrategy: maxIterations(20), + }) + + const fullResponse = requestRun.fullResponse + ' ' + approvedRun.fullResponse + const hasHammerInResponse = fullResponse.toLowerCase().includes('hammer') + const passed = + requestRun.toolCalls.length > 0 && + requestRun.approvalRequests.length > 0 && + toolExecuteCalled && + toolExecuteCallCount === 1 && + hasHammerInResponse + + debugData.chunks = [...requestRun.chunks, ...approvedRun.chunks] + debugData.finalMessages = approvedRun.reconstructedMessages + debugData.summary = { + request: summarizeRun(requestRun), + approved: summarizeRun(approvedRun), + hasHammerInResponse, + toolExecuteCalled, + toolExecuteCallCount, + toolExecuteCalls, + } + debugData.result = { + passed, + error: passed + ? undefined + : `toolCallFound: ${ + requestRun.toolCalls.length > 0 + }, approvalRequestFound: ${ + requestRun.approvalRequests.length > 0 + }, toolExecuteCalled: ${toolExecuteCalled}, toolExecuteCallCount: ${toolExecuteCallCount}, hasHammerInResponse: ${hasHammerInResponse}`, + } + + await writeDebugFile(adapterContext.adapterName, testName, debugData) + console.log( + `[${adapterContext.adapterName}] ${passed ? '✅' : '❌'} ${testName}${ + passed ? '' : `: ${debugData.result.error}` + }`, + ) + + return { passed, error: debugData.result.error } +} diff --git a/packages/typescript/smoke-tests/adapters/src/tests/cst-chat-stream.ts b/packages/typescript/smoke-tests/adapters/src/tests/cst-chat-stream.ts new file mode 100644 index 00000000..ce5385e9 --- /dev/null +++ b/packages/typescript/smoke-tests/adapters/src/tests/cst-chat-stream.ts @@ -0,0 +1,29 @@ +import { runTestCase } from '../harness' +import type { AdapterContext, TestOutcome } from '../harness' + +/** + * CST: Chat Stream Test + * + * Tests basic streaming chat completion by asking a simple question + * and verifying the response contains the expected answer. + */ +export async function runCST( + adapterContext: AdapterContext, +): Promise { + return runTestCase({ + adapterContext, + testName: 'cst-chat-stream', + description: 'chat stream returns Paris for capital of France', + messages: [ + { role: 'user' as const, content: 'what is the capital of france' }, + ], + validate: (run) => { + const hasParis = run.fullResponse.toLowerCase().includes('paris') + return { + passed: hasParis, + error: hasParis ? undefined : "Response does not contain 'Paris'", + meta: { hasParis }, + } + }, + }) +} diff --git a/packages/typescript/smoke-tests/adapters/src/tests/emb-embedding.ts b/packages/typescript/smoke-tests/adapters/src/tests/emb-embedding.ts new file mode 100644 index 00000000..c6d5bc04 --- /dev/null +++ b/packages/typescript/smoke-tests/adapters/src/tests/emb-embedding.ts @@ -0,0 +1,84 @@ +import { ai } from '@tanstack/ai' +import { writeDebugFile } from '../harness' +import type { AdapterContext, TestOutcome } from '../harness' + +/** + * EMB: Embedding Test + * + * Tests vector embedding generation by providing text inputs + * and verifying we get valid numeric vectors. + */ +export async function runEMB( + adapterContext: AdapterContext, +): Promise { + const testName = 'emb-embedding' + const adapterName = adapterContext.adapterName + + // Skip if no embedding adapter is available + if (!adapterContext.embeddingAdapter) { + console.log( + `[${adapterName}] — ${testName}: Ignored (no embedding adapter)`, + ) + return { passed: true, ignored: true } + } + + const model = adapterContext.embeddingModel || adapterContext.model + const inputs = [ + 'The Eiffel Tower is located in Paris.', + 'The Colosseum is located in Rome.', + ] + + const debugData: Record = { + adapter: adapterName, + test: testName, + model, + timestamp: new Date().toISOString(), + input: { inputs }, + } + + try { + const result = await ai({ + adapter: adapterContext.embeddingAdapter, + model, + input: inputs, + }) + + const embeddings: Array> = result.embeddings || [] + const lengths = embeddings.map((e: Array) => e?.length || 0) + const vectorsAreNumeric = embeddings.every( + (vec: Array) => + Array.isArray(vec) && vec.every((n: number) => typeof n === 'number'), + ) + const passed = + embeddings.length === inputs.length && + vectorsAreNumeric && + lengths.every((len: number) => len > 0) + + debugData.summary = { + embeddingLengths: lengths, + firstEmbeddingPreview: embeddings[0]?.slice(0, 8), + usage: result.usage, + } + debugData.result = { + passed, + error: passed ? undefined : 'Embeddings missing, empty, or invalid', + } + + await writeDebugFile(adapterName, testName, debugData) + + console.log( + `[${adapterName}] ${passed ? '✅' : '❌'} ${testName}${ + passed ? '' : `: ${debugData.result.error}` + }`, + ) + + return { passed, error: debugData.result.error } + } catch (error: any) { + const message = error?.message || String(error) + debugData.summary = { error: message } + debugData.result = { passed: false, error: message } + await writeDebugFile(adapterName, testName, debugData) + console.log(`[${adapterName}] ❌ ${testName}: ${message}`) + return { passed: false, error: message } + } +} diff --git a/packages/typescript/smoke-tests/adapters/src/tests/img-image-generation.ts b/packages/typescript/smoke-tests/adapters/src/tests/img-image-generation.ts new file mode 100644 index 00000000..8d197b09 --- /dev/null +++ b/packages/typescript/smoke-tests/adapters/src/tests/img-image-generation.ts @@ -0,0 +1,92 @@ +import { ai } from '@tanstack/ai' +import { writeDebugFile } from '../harness' +import type { AdapterContext, TestOutcome } from '../harness' + +/** + * IMG: Image Generation Test + * + * Tests image generation by providing a text prompt and + * verifying we get valid image data back. + * + * NOTE: This test is skipped by default to avoid generating + * images on every run. Use --tests img to run explicitly. + */ +export async function runIMG( + adapterContext: AdapterContext, +): Promise { + const testName = 'img-image-generation' + const adapterName = adapterContext.adapterName + + // Skip if no image adapter is available + if (!adapterContext.imageAdapter) { + console.log(`[${adapterName}] — ${testName}: Ignored (no image adapter)`) + return { passed: true, ignored: true } + } + + const model = adapterContext.imageModel || 'dall-e-3' + const prompt = 'A simple red circle on a white background' + + const debugData: Record = { + adapter: adapterName, + test: testName, + model, + timestamp: new Date().toISOString(), + input: { prompt }, + } + + try { + const result = await ai({ + adapter: adapterContext.imageAdapter, + model, + prompt, + n: 1, + size: '1024x1024', + }) + + // Check that we got valid image data + const images = result.images || [] + const hasImages = images.length > 0 + const hasValidImage = images.some( + (img: any) => + (img.url && typeof img.url === 'string' && img.url.length > 0) || + (img.b64Json && + typeof img.b64Json === 'string' && + img.b64Json.length > 0), + ) + + const passed = hasImages && hasValidImage + + debugData.summary = { + imageCount: images.length, + hasUrl: images[0]?.url ? true : false, + hasB64: images[0]?.b64Json ? true : false, + // Don't log the actual image data, just metadata + firstImageKeys: images[0] ? Object.keys(images[0]) : [], + } + debugData.result = { + passed, + error: passed + ? undefined + : hasImages + ? 'Image data missing url or b64Json' + : 'No images returned', + } + + await writeDebugFile(adapterName, testName, debugData) + + console.log( + `[${adapterName}] ${passed ? '✅' : '❌'} ${testName}${ + passed ? '' : `: ${debugData.result.error}` + }`, + ) + + return { passed, error: debugData.result.error } + } catch (error: any) { + const message = error?.message || String(error) + debugData.summary = { error: message } + debugData.result = { passed: false, error: message } + await writeDebugFile(adapterName, testName, debugData) + console.log(`[${adapterName}] ❌ ${testName}: ${message}`) + return { passed: false, error: message } + } +} diff --git a/packages/typescript/smoke-tests/adapters/src/tests/index.ts b/packages/typescript/smoke-tests/adapters/src/tests/index.ts new file mode 100644 index 00000000..7e9dbca6 --- /dev/null +++ b/packages/typescript/smoke-tests/adapters/src/tests/index.ts @@ -0,0 +1,150 @@ +import type { AdapterContext, TestOutcome } from '../harness' + +// Import all test runners +import { runCST } from './cst-chat-stream' +import { runOST } from './ost-one-shot-text' +import { runTLS } from './tls-tool-server' +import { runAPR } from './apr-approval-flow' +import { runSTR } from './str-structured-output' +import { runAGS } from './ags-agentic-structured' +import { runSUM } from './sum-summarize' +import { runEMB } from './emb-embedding' +import { runIMG } from './img-image-generation' +import { runTTS } from './tts-text-to-speech' +import { runTRN } from './trn-transcription' + +/** + * Adapter capability types + */ +export type AdapterCapability = + | 'text' + | 'summarize' + | 'embedding' + | 'image' + | 'tts' + | 'transcription' + +/** + * Definition for a test + */ +export interface TestDefinition { + /** 3-letter acronym identifier (uppercase) */ + id: string + /** Human-readable name */ + name: string + /** Brief description of what the test does */ + description: string + /** Function to run the test */ + run: (ctx: AdapterContext) => Promise + /** Required adapter capabilities (defaults to ['text']) */ + requires: AdapterCapability[] + /** If true, test is skipped unless explicitly requested */ + skipByDefault?: boolean +} + +/** + * Registry of all available tests + */ +export const TESTS: TestDefinition[] = [ + { + id: 'CST', + name: 'Chat Stream', + description: 'Streaming chat completion with basic prompt', + run: runCST, + requires: ['text'], + }, + { + id: 'OST', + name: 'One-Shot Text', + description: 'Non-streaming text completion (stream: false)', + run: runOST, + requires: ['text'], + }, + { + id: 'TLS', + name: 'Tool Server', + description: 'Tool execution with server-side handler', + run: runTLS, + requires: ['text'], + }, + { + id: 'APR', + name: 'Approval Flow', + description: 'Tool execution requiring user approval', + run: runAPR, + requires: ['text'], + }, + { + id: 'STR', + name: 'Structured Output', + description: 'Generate structured output with Zod schema', + run: runSTR, + requires: ['text'], + }, + { + id: 'AGS', + name: 'Agentic Structured', + description: 'Structured output with tool calls in agentic flow', + run: runAGS, + requires: ['text'], + }, + { + id: 'SUM', + name: 'Summarize', + description: 'Text summarization', + run: runSUM, + requires: ['summarize'], + }, + { + id: 'EMB', + name: 'Embedding', + description: 'Vector embeddings generation', + run: runEMB, + requires: ['embedding'], + }, + { + id: 'IMG', + name: 'Image Generation', + description: 'Generate images from text prompt', + run: runIMG, + requires: ['image'], + skipByDefault: true, // Skip unless explicitly requested + }, + { + id: 'TTS', + name: 'Text-to-Speech', + description: 'Generate speech audio from text', + run: runTTS, + requires: ['tts'], + skipByDefault: true, // Skip unless explicitly requested + }, + { + id: 'TRN', + name: 'Transcription', + description: 'Transcribe audio to text', + run: runTRN, + requires: ['transcription'], + skipByDefault: true, // Skip unless explicitly requested + }, +] + +/** + * Get test definition by ID (case-insensitive) + */ +export function getTest(id: string): TestDefinition | undefined { + return TESTS.find((t) => t.id.toLowerCase() === id.toLowerCase()) +} + +/** + * Get all test IDs + */ +export function getTestIds(): string[] { + return TESTS.map((t) => t.id) +} + +/** + * Get tests that run by default (excluding skipByDefault tests) + */ +export function getDefaultTests(): TestDefinition[] { + return TESTS.filter((t) => !t.skipByDefault) +} diff --git a/packages/typescript/smoke-tests/adapters/src/tests/ost-one-shot-text.ts b/packages/typescript/smoke-tests/adapters/src/tests/ost-one-shot-text.ts new file mode 100644 index 00000000..f9185529 --- /dev/null +++ b/packages/typescript/smoke-tests/adapters/src/tests/ost-one-shot-text.ts @@ -0,0 +1,69 @@ +import { ai } from '@tanstack/ai' +import { writeDebugFile } from '../harness' +import type { AdapterContext, TestOutcome } from '../harness' + +/** + * OST: One-Shot Text Test + * + * Tests non-streaming text completion by setting stream: false + * and verifying we get a complete string response. + */ +export async function runOST( + adapterContext: AdapterContext, +): Promise { + const testName = 'ost-one-shot-text' + const adapterName = adapterContext.adapterName + + const debugData: Record = { + adapter: adapterName, + test: testName, + model: adapterContext.model, + timestamp: new Date().toISOString(), + } + + try { + const result = await ai({ + adapter: adapterContext.textAdapter, + model: adapterContext.model, + stream: false, + messages: [ + { + role: 'user' as const, + content: 'What is 2 + 2? Reply with just the number.', + }, + ], + }) + + // Result should be a string when stream: false + const response = typeof result === 'string' ? result : String(result) + const hasFour = + response.includes('4') || response.toLowerCase().includes('four') + + debugData.summary = { + response, + responseType: typeof result, + hasFour, + } + debugData.result = { + passed: hasFour, + error: hasFour ? undefined : "Response does not contain '4' or 'four'", + } + + await writeDebugFile(adapterName, testName, debugData) + + console.log( + `[${adapterName}] ${hasFour ? '✅' : '❌'} ${testName}${ + hasFour ? '' : `: ${debugData.result.error}` + }`, + ) + + return { passed: hasFour, error: debugData.result.error } + } catch (error: any) { + const message = error?.message || String(error) + debugData.summary = { error: message } + debugData.result = { passed: false, error: message } + await writeDebugFile(adapterName, testName, debugData) + console.log(`[${adapterName}] ❌ ${testName}: ${message}`) + return { passed: false, error: message } + } +} diff --git a/packages/typescript/smoke-tests/adapters/src/tests/str-structured-output.ts b/packages/typescript/smoke-tests/adapters/src/tests/str-structured-output.ts new file mode 100644 index 00000000..98e031ed --- /dev/null +++ b/packages/typescript/smoke-tests/adapters/src/tests/str-structured-output.ts @@ -0,0 +1,112 @@ +import { ai } from '@tanstack/ai' +import { z } from 'zod' +import { writeDebugFile } from '../harness' +import type { AdapterContext, TestOutcome } from '../harness' + +// Schema for structured recipe output +const RecipeSchema = z.object({ + name: z.string().describe('The name of the recipe'), + prepTime: z.string().describe('Preparation time (e.g., "15 minutes")'), + servings: z.number().describe('Number of servings'), + ingredients: z + .array( + z.object({ + item: z.string().describe('Ingredient name'), + amount: z.string().describe('Amount needed (e.g., "2 cups")'), + }), + ) + .describe('List of ingredients'), + instructions: z + .array(z.string()) + .describe('Step-by-step cooking instructions'), +}) + +type Recipe = z.infer + +/** + * STR: Structured Output Test + * + * Tests structured output generation using a Zod schema. + * Verifies the response conforms to the expected structure. + */ +export async function runSTR( + adapterContext: AdapterContext, +): Promise { + const testName = 'str-structured-output' + const adapterName = adapterContext.adapterName + + const debugData: Record = { + adapter: adapterName, + test: testName, + model: adapterContext.model, + timestamp: new Date().toISOString(), + } + + try { + const result = (await ai({ + adapter: adapterContext.textAdapter, + model: adapterContext.model, + messages: [ + { + role: 'user' as const, + content: + 'Generate a simple recipe for scrambled eggs. Include the name, prep time, servings, ingredients with amounts, and step-by-step instructions.', + }, + ], + outputSchema: RecipeSchema, + })) as Recipe + + // Validate the structure + const hasName = typeof result.name === 'string' && result.name.length > 0 + const hasPrepTime = + typeof result.prepTime === 'string' && result.prepTime.length > 0 + const hasServings = + typeof result.servings === 'number' && result.servings > 0 + const hasIngredients = + Array.isArray(result.ingredients) && result.ingredients.length > 0 + const hasInstructions = + Array.isArray(result.instructions) && result.instructions.length > 0 + + const passed = + hasName && hasPrepTime && hasServings && hasIngredients && hasInstructions + + const issues: string[] = [] + if (!hasName) issues.push('missing or invalid name') + if (!hasPrepTime) issues.push('missing or invalid prepTime') + if (!hasServings) issues.push('missing or invalid servings') + if (!hasIngredients) issues.push('missing or empty ingredients') + if (!hasInstructions) issues.push('missing or empty instructions') + + debugData.summary = { + result, + hasName, + hasPrepTime, + hasServings, + hasIngredients, + hasInstructions, + ingredientCount: result.ingredients?.length, + instructionCount: result.instructions?.length, + } + debugData.result = { + passed, + error: issues.length ? issues.join(', ') : undefined, + } + + await writeDebugFile(adapterName, testName, debugData) + + console.log( + `[${adapterName}] ${passed ? '✅' : '❌'} ${testName}${ + passed ? '' : `: ${debugData.result.error}` + }`, + ) + + return { passed, error: debugData.result.error } + } catch (error: any) { + const message = error?.message || String(error) + debugData.summary = { error: message } + debugData.result = { passed: false, error: message } + await writeDebugFile(adapterName, testName, debugData) + console.log(`[${adapterName}] ❌ ${testName}: ${message}`) + return { passed: false, error: message } + } +} diff --git a/packages/typescript/smoke-tests/adapters/src/tests/sum-summarize.ts b/packages/typescript/smoke-tests/adapters/src/tests/sum-summarize.ts new file mode 100644 index 00000000..8dc63df4 --- /dev/null +++ b/packages/typescript/smoke-tests/adapters/src/tests/sum-summarize.ts @@ -0,0 +1,77 @@ +import { ai } from '@tanstack/ai' +import { writeDebugFile } from '../harness' +import type { AdapterContext, TestOutcome } from '../harness' + +/** + * SUM: Summarize Test + * + * Tests text summarization by providing a paragraph and + * verifying the summary contains key information. + */ +export async function runSUM( + adapterContext: AdapterContext, +): Promise { + const testName = 'sum-summarize' + const adapterName = adapterContext.adapterName + + // Skip if no summarize adapter is available + if (!adapterContext.summarizeAdapter) { + console.log( + `[${adapterName}] — ${testName}: Ignored (no summarize adapter)`, + ) + return { passed: true, ignored: true } + } + + const model = adapterContext.summarizeModel || adapterContext.model + const text = + 'Paris is the capital and most populous city of France, known for landmarks like the Eiffel Tower and the Louvre. It is a major center for art, fashion, gastronomy, and culture.' + + const debugData: Record = { + adapter: adapterName, + test: testName, + model, + timestamp: new Date().toISOString(), + input: { text, maxLength: 80, style: 'concise' as const }, + } + + try { + const result = await ai({ + adapter: adapterContext.summarizeAdapter, + model, + text, + maxLength: 80, + style: 'concise', + }) + + const summary = result.summary || '' + const summaryLower = summary.toLowerCase() + const passed = summary.length > 0 && summaryLower.includes('paris') + + debugData.summary = { + summary, + usage: result.usage, + summaryLength: summary.length, + } + debugData.result = { + passed, + error: passed ? undefined : "Summary missing 'Paris'", + } + + await writeDebugFile(adapterName, testName, debugData) + + console.log( + `[${adapterName}] ${passed ? '✅' : '❌'} ${testName}${ + passed ? '' : `: ${debugData.result.error}` + }`, + ) + + return { passed, error: debugData.result.error } + } catch (error: any) { + const message = error?.message || String(error) + debugData.summary = { error: message } + debugData.result = { passed: false, error: message } + await writeDebugFile(adapterName, testName, debugData) + console.log(`[${adapterName}] ❌ ${testName}: ${message}`) + return { passed: false, error: message } + } +} diff --git a/packages/typescript/smoke-tests/adapters/src/tests/tls-tool-server.ts b/packages/typescript/smoke-tests/adapters/src/tests/tls-tool-server.ts new file mode 100644 index 00000000..9000971b --- /dev/null +++ b/packages/typescript/smoke-tests/adapters/src/tests/tls-tool-server.ts @@ -0,0 +1,138 @@ +import { maxIterations, toolDefinition } from '@tanstack/ai' +import { z } from 'zod' +import { runTestCase } from '../harness' +import type { AdapterContext, TestOutcome } from '../harness' + +/** + * TLS: Tool Server Test + * + * Tests tool execution with a server-side handler by requesting + * temperature for a location and verifying the tool was called correctly. + */ +export async function runTLS( + adapterContext: AdapterContext, +): Promise { + let toolExecuteCalled = false + let toolExecuteCallCount = 0 + const toolExecuteCalls: Array<{ + timestamp: string + arguments: any + result?: string + error?: string + }> = [] + + const expectedLocation = 'San Francisco' + + const temperatureTool = toolDefinition({ + name: 'get_temperature', + description: + 'Get the current temperature in degrees for a specific location', + inputSchema: z.object({ + location: z + .string() + .describe('The city or location to get the temperature for'), + }), + }).server(async (args) => { + toolExecuteCalled = true + toolExecuteCallCount++ + const callInfo: any = { + timestamp: new Date().toISOString(), + arguments: args, + } + try { + // Verify location was passed correctly + if (typeof args !== 'object') { + throw new Error('Arguments must be an object') + } + if (!args.location || typeof args.location !== 'string') { + throw new Error('Location argument is missing or invalid') + } + + const result = '70' + callInfo.result = result + toolExecuteCalls.push(callInfo) + return result + } catch (error: any) { + callInfo.error = error.message + toolExecuteCalls.push(callInfo) + throw error + } + }) + + return runTestCase({ + adapterContext, + testName: 'tls-tool-server', + description: + 'tool call with location parameter returns a temperature value', + messages: [ + { + role: 'user' as const, + content: `use the get_temperature tool to get the temperature for ${expectedLocation} and report the answer as a number`, + }, + ], + tools: [temperatureTool], + agentLoopStrategy: maxIterations(20), + validate: (run) => { + const responseLower = run.fullResponse.toLowerCase() + const hasSeventy = + responseLower.includes('70') || responseLower.includes('seventy') + const toolCallFound = run.toolCalls.length > 0 + const toolResultFound = run.toolResults.length > 0 + + // Check that location was passed correctly + const locationPassedCorrectly = toolExecuteCalls.some( + (call) => + call.arguments && + call.arguments.location && + typeof call.arguments.location === 'string' && + call.arguments.location.length > 0, + ) + + // Check if the location matches what was requested (case-insensitive) + const locationMatches = toolExecuteCalls.some( + (call) => + call.arguments && + call.arguments.location && + call.arguments.location + .toLowerCase() + .includes(expectedLocation.toLowerCase()), + ) + + const issues: string[] = [] + if (!toolCallFound) issues.push('no tool call') + if (!toolResultFound) issues.push('no tool result') + if (!hasSeventy) issues.push("no '70' or 'seventy' in response") + if (!locationPassedCorrectly) + issues.push('location argument not passed or invalid') + if (!locationMatches) { + issues.push( + `location argument '${ + toolExecuteCalls[0]?.arguments?.location || 'missing' + }' does not match expected '${expectedLocation}'`, + ) + } + + return { + passed: + toolCallFound && + toolResultFound && + hasSeventy && + locationPassedCorrectly && + locationMatches, + error: issues.length ? issues.join(', ') : undefined, + meta: { + hasSeventy, + toolCallFound, + toolResultFound, + toolExecuteCalled, + toolExecuteCallCount, + toolExecuteCalls, + locationPassedCorrectly, + locationMatches, + expectedLocation, + actualLocation: toolExecuteCalls[0]?.arguments?.location, + }, + } + }, + }) +} diff --git a/packages/typescript/smoke-tests/adapters/src/tests/trn-transcription.ts b/packages/typescript/smoke-tests/adapters/src/tests/trn-transcription.ts new file mode 100644 index 00000000..ef15ded9 --- /dev/null +++ b/packages/typescript/smoke-tests/adapters/src/tests/trn-transcription.ts @@ -0,0 +1,115 @@ +import { readFile } from 'node:fs/promises' +import { join } from 'node:path' +import { ai } from '@tanstack/ai' +import { writeDebugFile } from '../harness' +import type { AdapterContext, TestOutcome } from '../harness' + +/** + * TRN: Audio Transcription Test + * + * Tests audio transcription by providing an audio file and + * verifying we get valid transcription text back. + * + * NOTE: This test is skipped by default to avoid transcription + * costs on every run. Use --tests trn to run explicitly. + * + * Requires a test audio file at: fixtures/test-audio.mp3 + */ +export async function runTRN( + adapterContext: AdapterContext, +): Promise { + const testName = 'trn-transcription' + const adapterName = adapterContext.adapterName + + // Skip if no transcription adapter is available + if (!adapterContext.transcriptionAdapter) { + console.log( + `[${adapterName}] — ${testName}: Ignored (no transcription adapter)`, + ) + return { passed: true, ignored: true } + } + + const model = adapterContext.transcriptionModel || 'whisper-1' + + const debugData: Record = { + adapter: adapterName, + test: testName, + model, + timestamp: new Date().toISOString(), + } + + try { + // Try to load test audio file + const testAudioPath = join(process.cwd(), 'fixtures', 'test-audio.mp3') + let audioData: string + + try { + const audioBuffer = await readFile(testAudioPath) + audioData = audioBuffer.toString('base64') + debugData.input = { + audioFile: testAudioPath, + audioSize: audioBuffer.length, + } + } catch (fileError) { + // No test audio file available - skip test + console.log( + `[${adapterName}] — ${testName}: Ignored (no test audio file at fixtures/test-audio.mp3)`, + ) + return { passed: true, ignored: true } + } + + const result = await ai({ + adapter: adapterContext.transcriptionAdapter, + model, + audio: audioData, + language: 'en', + }) + + // Check that we got valid transcription data + const hasText = + result.text && typeof result.text === 'string' && result.text.length > 0 + const hasId = result.id && typeof result.id === 'string' + const hasModel = result.model && typeof result.model === 'string' + + const passed = hasText && hasId && hasModel + + debugData.summary = { + hasText, + hasId, + hasModel, + textLength: result.text?.length || 0, + textPreview: result.text?.substring(0, 100) || '', + language: result.language, + duration: result.duration, + segmentCount: result.segments?.length || 0, + wordCount: result.words?.length || 0, + } + debugData.result = { + passed, + error: passed + ? undefined + : !hasText + ? 'Transcription text missing' + : !hasId + ? 'ID missing' + : 'Model missing', + } + + await writeDebugFile(adapterName, testName, debugData) + + console.log( + `[${adapterName}] ${passed ? '✅' : '❌'} ${testName}${ + passed ? '' : `: ${debugData.result.error}` + }`, + ) + + return { passed, error: debugData.result.error } + } catch (error: any) { + const message = error?.message || String(error) + debugData.summary = { error: message } + debugData.result = { passed: false, error: message } + await writeDebugFile(adapterName, testName, debugData) + console.log(`[${adapterName}] ❌ ${testName}: ${message}`) + return { passed: false, error: message } + } +} diff --git a/packages/typescript/smoke-tests/adapters/src/tests/tts-text-to-speech.ts b/packages/typescript/smoke-tests/adapters/src/tests/tts-text-to-speech.ts new file mode 100644 index 00000000..853e3e61 --- /dev/null +++ b/packages/typescript/smoke-tests/adapters/src/tests/tts-text-to-speech.ts @@ -0,0 +1,94 @@ +import { ai } from '@tanstack/ai' +import { writeDebugFile } from '../harness' +import type { AdapterContext, TestOutcome } from '../harness' + +/** + * TTS: Text-to-Speech Test + * + * Tests text-to-speech generation by providing text and + * verifying we get valid audio data back. + * + * NOTE: This test is skipped by default to avoid generating + * audio on every run. Use --tests tts to run explicitly. + */ +export async function runTTS( + adapterContext: AdapterContext, +): Promise { + const testName = 'tts-text-to-speech' + const adapterName = adapterContext.adapterName + + // Skip if no TTS adapter is available + if (!adapterContext.ttsAdapter) { + console.log(`[${adapterName}] — ${testName}: Ignored (no TTS adapter)`) + return { passed: true, ignored: true } + } + + const model = adapterContext.ttsModel || 'tts-1' + const text = 'Hello, this is a test of text to speech synthesis.' + + const debugData: Record = { + adapter: adapterName, + test: testName, + model, + timestamp: new Date().toISOString(), + input: { text }, + } + + try { + const result = await ai({ + adapter: adapterContext.ttsAdapter, + model, + text, + voice: 'alloy', + format: 'mp3', + }) + + // Check that we got valid audio data + const hasAudio = + result.audio && + typeof result.audio === 'string' && + result.audio.length > 0 + const hasFormat = result.format && typeof result.format === 'string' + const hasId = result.id && typeof result.id === 'string' + + const passed = hasAudio && hasFormat && hasId + + debugData.summary = { + hasAudio, + hasFormat, + hasId, + format: result.format, + audioLength: result.audio?.length || 0, + // Don't log the actual audio data, just metadata + contentType: result.contentType, + duration: result.duration, + } + debugData.result = { + passed, + error: passed + ? undefined + : !hasAudio + ? 'Audio data missing' + : !hasFormat + ? 'Format missing' + : 'ID missing', + } + + await writeDebugFile(adapterName, testName, debugData) + + console.log( + `[${adapterName}] ${passed ? '✅' : '❌'} ${testName}${ + passed ? '' : `: ${debugData.result.error}` + }`, + ) + + return { passed, error: debugData.result.error } + } catch (error: any) { + const message = error?.message || String(error) + debugData.summary = { error: message } + debugData.result = { passed: false, error: message } + await writeDebugFile(adapterName, testName, debugData) + console.log(`[${adapterName}] ❌ ${testName}: ${message}`) + return { passed: false, error: message } + } +} diff --git a/packages/typescript/smoke-tests/e2e/package.json b/packages/typescript/smoke-tests/e2e/package.json index 4725e59e..7628bfcb 100644 --- a/packages/typescript/smoke-tests/e2e/package.json +++ b/packages/typescript/smoke-tests/e2e/package.json @@ -13,18 +13,18 @@ "postinstall": "npx playwright install --with-deps chromium" }, "dependencies": { - "@tailwindcss/vite": "^4.1.17", + "@tailwindcss/vite": "^4.1.18", "@tanstack/ai": "workspace:*", "@tanstack/ai-client": "workspace:*", "@tanstack/ai-openai": "workspace:*", "@tanstack/ai-react": "workspace:*", - "@tanstack/nitro-v2-vite-plugin": "^1.139.0", - "@tanstack/react-router": "^1.139.7", - "@tanstack/react-start": "^1.139.8", + "@tanstack/nitro-v2-vite-plugin": "^1.141.0", + "@tanstack/react-router": "^1.141.1", + "@tanstack/react-start": "^1.141.1", "@tanstack/router-plugin": "^1.139.7", - "react": "^19.2.0", - "react-dom": "^19.2.0", - "tailwindcss": "^4.1.17", + "react": "^19.2.3", + "react-dom": "^19.2.3", + "tailwindcss": "^4.1.18", "vite-tsconfig-paths": "^5.1.4" }, "devDependencies": { @@ -32,8 +32,8 @@ "@types/node": "^24.10.1", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^5.1.1", + "@vitejs/plugin-react": "^5.1.2", "typescript": "5.9.3", - "vite": "^7.2.4" + "vite": "^7.2.7" } } diff --git a/packages/typescript/smoke-tests/e2e/src/routes/api.tanchat.ts b/packages/typescript/smoke-tests/e2e/src/routes/api.tanchat.ts index a7ea69a7..6bd2928a 100644 --- a/packages/typescript/smoke-tests/e2e/src/routes/api.tanchat.ts +++ b/packages/typescript/smoke-tests/e2e/src/routes/api.tanchat.ts @@ -1,6 +1,6 @@ import { createFileRoute } from '@tanstack/react-router' -import { chat, toStreamResponse, maxIterations } from '@tanstack/ai' -import { openai } from '@tanstack/ai-openai' +import { ai, maxIterations, toStreamResponse } from '@tanstack/ai' +import { openaiText } from '@tanstack/ai-openai' export const Route = createFileRoute('/api/tanchat')({ server: { @@ -16,8 +16,8 @@ export const Route = createFileRoute('/api/tanchat')({ const { messages } = await request.json() try { - const stream = chat({ - adapter: openai(), + const stream = ai({ + adapter: openaiText(), model: 'gpt-4o-mini', systemPrompts: [ 'You are a helpful assistant. Provide clear and concise answers.', diff --git a/packages/typescript/solid-ai-devtools/package.json b/packages/typescript/solid-ai-devtools/package.json index 05f45279..e6dca24b 100644 --- a/packages/typescript/solid-ai-devtools/package.json +++ b/packages/typescript/solid-ai-devtools/package.json @@ -52,7 +52,7 @@ }, "devDependencies": { "@vitest/coverage-v8": "4.0.14", - "vite": "^7.2.4", + "vite": "^7.2.7", "vite-plugin-solid": "^2.11.10" }, "peerDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 214dfb20..7ff74579 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,7 +13,7 @@ importers: devDependencies: '@changesets/cli': specifier: ^2.29.8 - version: 2.29.8(@types/node@24.10.1) + version: 2.29.8(@types/node@24.10.3) '@faker-js/faker': specifier: ^10.1.0 version: 10.1.0 @@ -22,28 +22,28 @@ importers: version: 1.2.0 '@tanstack/eslint-config': specifier: 0.3.3 - version: 0.3.3(@typescript-eslint/utils@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + version: 0.3.3(@typescript-eslint/utils@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@tanstack/typedoc-config': specifier: 0.3.1 version: 0.3.1(typescript@5.9.3) '@tanstack/vite-config': specifier: 0.4.1 - version: 0.4.1(@types/node@24.10.1)(rollup@4.53.3)(typescript@5.9.3)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 0.4.1(@types/node@24.10.3)(rollup@4.53.3)(typescript@5.9.3)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@types/node': specifier: ^24.10.1 - version: 24.10.1 + version: 24.10.3 eslint: specifier: ^9.39.1 version: 9.39.1(jiti@2.6.1) eslint-plugin-unused-imports: specifier: ^4.3.0 - version: 4.3.0(@typescript-eslint/eslint-plugin@8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)) + version: 4.3.0(@typescript-eslint/eslint-plugin@8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)) happy-dom: specifier: ^20.0.10 - version: 20.0.10 + version: 20.0.11 knip: specifier: ^5.70.2 - version: 5.70.2(@types/node@24.10.1)(typescript@5.9.3) + version: 5.73.4(@types/node@24.10.3)(typescript@5.9.3) markdown-link-extractor: specifier: ^4.0.3 version: 4.0.3 @@ -58,10 +58,10 @@ importers: version: 3.7.4 prettier-plugin-svelte: specifier: ^3.4.0 - version: 3.4.0(prettier@3.7.4)(svelte@5.44.1) + version: 3.4.0(prettier@3.7.4)(svelte@5.45.10) publint: specifier: ^0.3.15 - version: 0.3.15 + version: 0.3.16 sherif: specifier: ^1.9.0 version: 1.9.0 @@ -72,11 +72,11 @@ importers: specifier: 5.9.3 version: 5.9.3 vite: - specifier: ^7.2.4 - version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + specifier: ^7.2.7 + version: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vitest: specifier: ^4.0.14 - version: 4.0.14(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + version: 4.0.15(@types/node@24.10.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) examples/php-slim: devDependencies: @@ -93,8 +93,8 @@ importers: examples/ts-group-chat: dependencies: '@tailwindcss/vite': - specifier: ^4.1.17 - version: 4.1.17(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + specifier: ^4.1.18 + version: 4.1.18(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@tanstack/ai': specifier: workspace:* version: link:../../packages/typescript/ai @@ -109,53 +109,53 @@ importers: version: link:../../packages/typescript/ai-react '@tanstack/react-devtools': specifier: ^0.8.2 - version: 0.8.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(csstype@3.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(solid-js@1.9.10) + version: 0.8.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@1.9.10) '@tanstack/react-router': - specifier: ^1.139.7 - version: 1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + specifier: ^1.141.1 + version: 1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-router-devtools': specifier: ^1.139.7 - version: 1.139.7(@tanstack/react-router@1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.139.10)(@types/node@24.10.1)(csstype@3.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(solid-js@1.9.10)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + version: 1.141.1(@tanstack/react-router@1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.141.1)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@1.9.10) '@tanstack/react-router-ssr-query': specifier: ^1.139.7 - version: 1.139.7(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.5(react@19.2.0))(@tanstack/react-router@1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.139.10)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 1.141.1(@tanstack/query-core@5.90.12)(@tanstack/react-query@5.90.12(react@19.2.3))(@tanstack/react-router@1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.141.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-start': - specifier: ^1.139.8 - version: 1.139.8(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + specifier: ^1.141.1 + version: 1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@tanstack/router-plugin': specifier: ^1.139.7 - version: 1.139.10(@tanstack/react-router@1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 1.141.1(@tanstack/react-router@1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) capnweb: specifier: ^0.1.0 version: 0.1.0 react: - specifier: ^19.2.0 - version: 19.2.0 + specifier: ^19.2.3 + version: 19.2.3 react-dom: - specifier: ^19.2.0 - version: 19.2.0(react@19.2.0) + specifier: ^19.2.3 + version: 19.2.3(react@19.2.3) tailwindcss: - specifier: ^4.1.17 - version: 4.1.17 + specifier: ^4.1.18 + version: 4.1.18 vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.9.3)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 5.1.4(typescript@5.9.3)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) ws: specifier: ^8.18.3 version: 8.18.3 devDependencies: '@tanstack/devtools-vite': specifier: ^0.3.11 - version: 0.3.11(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 0.3.12(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@testing-library/dom': specifier: ^10.4.1 version: 10.4.1 '@testing-library/react': specifier: ^16.3.0 - version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@types/node': specifier: ^24.10.1 - version: 24.10.1 + version: 24.10.3 '@types/react': specifier: ^19.2.7 version: 19.2.7 @@ -166,20 +166,20 @@ importers: specifier: ^8.18.1 version: 8.18.1 '@vitejs/plugin-react': - specifier: ^5.1.1 - version: 5.1.1(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + specifier: ^5.1.2 + version: 5.1.2(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) jsdom: specifier: ^27.2.0 - version: 27.2.0(postcss@8.5.6) + version: 27.3.0(postcss@8.5.6) typescript: specifier: 5.9.3 version: 5.9.3 vite: - specifier: ^7.2.4 - version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + specifier: ^7.2.7 + version: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vitest: specifier: ^4.0.14 - version: 4.0.14(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + version: 4.0.15(@types/node@24.10.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) web-vitals: specifier: ^5.1.0 version: 5.1.0 @@ -187,8 +187,8 @@ importers: examples/ts-react-chat: dependencies: '@tailwindcss/vite': - specifier: ^4.1.17 - version: 4.1.17(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + specifier: ^4.1.18 + version: 4.1.18(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@tanstack/ai': specifier: workspace:* version: link:../../packages/typescript/ai @@ -214,29 +214,29 @@ importers: specifier: workspace:* version: link:../../packages/typescript/ai-react-ui '@tanstack/nitro-v2-vite-plugin': - specifier: ^1.139.0 - version: 1.139.0(rolldown@1.0.0-beta.53)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + specifier: ^1.141.0 + version: 1.141.0(rolldown@1.0.0-beta.53)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@tanstack/react-devtools': specifier: ^0.8.2 - version: 0.8.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(csstype@3.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(solid-js@1.9.10) + version: 0.8.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@1.9.10) '@tanstack/react-router': - specifier: ^1.139.7 - version: 1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + specifier: ^1.141.1 + version: 1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-router-devtools': specifier: ^1.139.7 - version: 1.139.7(@tanstack/react-router@1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.139.10)(@types/node@24.10.1)(csstype@3.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(solid-js@1.9.10)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + version: 1.141.1(@tanstack/react-router@1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.141.1)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@1.9.10) '@tanstack/react-router-ssr-query': specifier: ^1.139.7 - version: 1.139.7(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.5(react@19.2.0))(@tanstack/react-router@1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.139.10)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 1.141.1(@tanstack/query-core@5.90.12)(@tanstack/react-query@5.90.12(react@19.2.3))(@tanstack/react-router@1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.141.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-start': - specifier: ^1.139.8 - version: 1.139.8(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + specifier: ^1.141.1 + version: 1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@tanstack/react-store': specifier: ^0.8.0 - version: 0.8.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 0.8.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/router-plugin': specifier: ^1.139.7 - version: 1.139.10(@tanstack/react-router@1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 1.141.1(@tanstack/react-router@1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@tanstack/store': specifier: ^0.8.0 version: 0.8.0 @@ -244,17 +244,17 @@ importers: specifier: ^11.11.1 version: 11.11.1 lucide-react: - specifier: ^0.555.0 - version: 0.555.0(react@19.2.0) + specifier: ^0.561.0 + version: 0.561.0(react@19.2.3) react: - specifier: ^19.2.0 - version: 19.2.0 + specifier: ^19.2.3 + version: 19.2.3 react-dom: - specifier: ^19.2.0 - version: 19.2.0(react@19.2.0) + specifier: ^19.2.3 + version: 19.2.3(react@19.2.3) react-markdown: specifier: ^10.1.0 - version: 10.1.0(@types/react@19.2.7)(react@19.2.0) + version: 10.1.0(@types/react@19.2.7)(react@19.2.3) rehype-highlight: specifier: ^7.0.2 version: 7.0.2 @@ -268,18 +268,18 @@ importers: specifier: ^4.0.1 version: 4.0.1 tailwindcss: - specifier: ^4.1.17 - version: 4.1.17 + specifier: ^4.1.18 + version: 4.1.18 vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.9.3)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 5.1.4(typescript@5.9.3)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) zod: specifier: ^4.1.13 version: 4.1.13 devDependencies: '@tanstack/devtools-vite': specifier: ^0.3.11 - version: 0.3.11(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 0.3.12(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@tanstack/react-ai-devtools': specifier: workspace:* version: link:../../packages/typescript/react-ai-devtools @@ -288,10 +288,10 @@ importers: version: 10.4.1 '@testing-library/react': specifier: ^16.3.0 - version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@types/node': specifier: ^24.10.1 - version: 24.10.1 + version: 24.10.3 '@types/react': specifier: ^19.2.7 version: 19.2.7 @@ -299,20 +299,20 @@ importers: specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.7) '@vitejs/plugin-react': - specifier: ^5.1.1 - version: 5.1.1(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + specifier: ^5.1.2 + version: 5.1.2(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) jsdom: specifier: ^27.2.0 - version: 27.2.0(postcss@8.5.6) + version: 27.3.0(postcss@8.5.6) typescript: specifier: 5.9.3 version: 5.9.3 vite: - specifier: ^7.2.4 - version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + specifier: ^7.2.7 + version: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vitest: specifier: ^4.0.14 - version: 4.0.14(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + version: 4.0.15(@types/node@24.10.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) web-vitals: specifier: ^5.1.0 version: 5.1.0 @@ -320,8 +320,8 @@ importers: examples/ts-solid-chat: dependencies: '@tailwindcss/vite': - specifier: ^4.1.17 - version: 4.1.17(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + specifier: ^4.1.18 + version: 4.1.18(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@tanstack/ai': specifier: workspace:* version: link:../../packages/typescript/ai @@ -350,29 +350,29 @@ importers: specifier: workspace:* version: link:../../packages/typescript/ai-solid-ui '@tanstack/nitro-v2-vite-plugin': - specifier: ^1.139.0 - version: 1.139.0(rolldown@1.0.0-beta.53)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + specifier: ^1.141.0 + version: 1.141.0(rolldown@1.0.0-beta.53)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@tanstack/router-plugin': specifier: ^1.139.7 - version: 1.139.10(@tanstack/react-router@1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 1.141.1(@tanstack/react-router@1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@tanstack/solid-ai-devtools': specifier: workspace:* version: link:../../packages/typescript/solid-ai-devtools '@tanstack/solid-devtools': specifier: ^0.7.15 - version: 0.7.15(csstype@3.2.3)(solid-js@1.9.10) + version: 0.7.17(csstype@3.2.3)(solid-js@1.9.10) '@tanstack/solid-router': specifier: ^1.139.10 - version: 1.139.10(solid-js@1.9.10) + version: 1.141.1(solid-js@1.9.10) '@tanstack/solid-router-devtools': specifier: ^1.139.10 - version: 1.139.10(@tanstack/router-core@1.139.10)(@tanstack/solid-router@1.139.10(solid-js@1.9.10))(@types/node@24.10.1)(csstype@3.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(solid-js@1.9.10)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + version: 1.141.1(@tanstack/router-core@1.141.1)(@tanstack/solid-router@1.141.1(solid-js@1.9.10))(csstype@3.2.3)(solid-js@1.9.10) '@tanstack/solid-router-ssr-query': specifier: ^1.139.10 - version: 1.139.10(@tanstack/query-core@5.90.11)(@tanstack/router-core@1.139.10)(@tanstack/solid-query@5.90.14(solid-js@1.9.10))(@tanstack/solid-router@1.139.10(solid-js@1.9.10))(eslint@9.39.1(jiti@2.6.1))(solid-js@1.9.10)(typescript@5.9.3) + version: 1.141.1(@tanstack/query-core@5.90.12)(@tanstack/router-core@1.141.1)(@tanstack/solid-query@5.90.15(solid-js@1.9.10))(@tanstack/solid-router@1.141.1(solid-js@1.9.10))(eslint@9.39.1(jiti@2.6.1))(solid-js@1.9.10)(typescript@5.9.3) '@tanstack/solid-start': specifier: ^1.139.10 - version: 1.139.10(solid-js@1.9.10)(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 1.141.1(@tanstack/react-router@1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(solid-js@1.9.10)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@tanstack/solid-store': specifier: ^0.8.0 version: 0.8.0(solid-js@1.9.10) @@ -390,13 +390,13 @@ importers: version: 1.9.10 solid-markdown: specifier: ^2.1.0 - version: 2.1.0(solid-js@1.9.10) + version: 2.1.1(solid-js@1.9.10) tailwindcss: - specifier: ^4.1.17 - version: 4.1.17 + specifier: ^4.1.18 + version: 4.1.18 vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.9.3)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 5.1.4(typescript@5.9.3)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) zod: specifier: ^4.1.13 version: 4.1.13 @@ -409,28 +409,28 @@ importers: version: 0.4.0 '@tanstack/devtools-vite': specifier: ^0.3.11 - version: 0.3.11(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 0.3.12(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@testing-library/dom': specifier: ^10.4.1 version: 10.4.1 '@types/node': specifier: ^24.10.1 - version: 24.10.1 + version: 24.10.3 jsdom: specifier: ^27.2.0 - version: 27.2.0(postcss@8.5.6) + version: 27.3.0(postcss@8.5.6) typescript: specifier: 5.9.3 version: 5.9.3 vite: - specifier: ^7.2.4 - version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + specifier: ^7.2.7 + version: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vite-plugin-solid: specifier: ^2.11.10 - version: 2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) vitest: specifier: ^4.0.14 - version: 4.0.14(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + version: 4.0.15(@types/node@24.10.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) web-vitals: specifier: ^5.1.0 version: 5.1.0 @@ -463,7 +463,7 @@ importers: version: 11.11.1 lucide-svelte: specifier: ^0.468.0 - version: 0.468.0(svelte@5.44.1) + version: 0.468.0(svelte@5.45.10) marked: specifier: ^15.0.6 version: 15.0.12 @@ -476,28 +476,28 @@ importers: devDependencies: '@sveltejs/adapter-auto': specifier: ^3.3.1 - version: 3.3.1(@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))) + version: 3.3.1(@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.45.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.45.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))) '@sveltejs/kit': specifier: ^2.15.10 - version: 2.49.2(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 2.49.2(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.45.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.45.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@sveltejs/vite-plugin-svelte': specifier: ^5.1.1 - version: 5.1.1(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 5.1.1(svelte@5.45.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@tailwindcss/vite': - specifier: ^4.1.17 - version: 4.1.17(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + specifier: ^4.1.18 + version: 4.1.18(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@types/node': specifier: ^24.10.1 - version: 24.10.1 + version: 24.10.3 svelte: specifier: ^5.20.0 - version: 5.44.1 + version: 5.45.10 svelte-check: specifier: ^4.2.0 - version: 4.3.4(picomatch@4.0.3)(svelte@5.44.1)(typescript@5.9.3) + version: 4.3.4(picomatch@4.0.3)(svelte@5.45.10)(typescript@5.9.3) tailwindcss: - specifier: ^4.1.17 - version: 4.1.17 + specifier: ^4.1.18 + version: 4.1.18 tslib: specifier: ^2.8.1 version: 2.8.1 @@ -505,8 +505,8 @@ importers: specifier: 5.9.3 version: 5.9.3 vite: - specifier: ^7.2.4 - version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + specifier: ^7.2.7 + version: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) examples/ts-vue-chat: dependencies: @@ -542,20 +542,20 @@ importers: version: 3.5.25(typescript@5.9.3) vue-router: specifier: ^4.5.0 - version: 4.6.3(vue@3.5.25(typescript@5.9.3)) + version: 4.6.4(vue@3.5.25(typescript@5.9.3)) zod: specifier: ^4.1.13 version: 4.1.13 devDependencies: '@tailwindcss/vite': - specifier: ^4.1.17 - version: 4.1.17(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + specifier: ^4.1.18 + version: 4.1.18(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@types/node': specifier: ^24.10.1 - version: 24.10.1 + version: 24.10.3 '@vitejs/plugin-vue': specifier: ^5.2.3 - version: 5.2.4(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.3)) + version: 5.2.4(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3)) autoprefixer: specifier: ^10.4.21 version: 10.4.22(postcss@8.5.6) @@ -569,17 +569,17 @@ importers: specifier: ^5.1.0 version: 5.2.1 tailwindcss: - specifier: ^4.1.17 - version: 4.1.17 + specifier: ^4.1.18 + version: 4.1.18 tsx: specifier: ^4.20.6 - version: 4.20.6 + version: 4.21.0 typescript: specifier: 5.9.3 version: 5.9.3 vite: - specifier: ^7.2.4 - version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + specifier: ^7.2.7 + version: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vue-tsc: specifier: ^2.2.10 version: 2.2.12(typescript@5.9.3) @@ -591,8 +591,8 @@ importers: version: link:../../packages/typescript/ai-client devDependencies: vite: - specifier: ^7.2.4 - version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + specifier: ^7.2.7 + version: 7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) packages/typescript/ai: dependencies: @@ -608,7 +608,7 @@ importers: devDependencies: '@vitest/coverage-v8': specifier: 4.0.14 - version: 4.0.14(vitest@4.0.14(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.14(vitest@4.0.15(@types/node@25.0.1)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) zod: specifier: ^4.1.13 version: 4.1.13 @@ -617,17 +617,17 @@ importers: dependencies: '@anthropic-ai/sdk': specifier: ^0.71.0 - version: 0.71.0(zod@4.1.13) + version: 0.71.2(zod@4.1.13) '@tanstack/ai': specifier: workspace:* version: link:../ai + zod: + specifier: ^4.0.0 + version: 4.1.13 devDependencies: '@vitest/coverage-v8': specifier: 4.0.14 - version: 4.0.14(vitest@4.0.14(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) - zod: - specifier: ^4.1.13 - version: 4.1.13 + version: 4.0.14(vitest@4.0.15(@types/node@25.0.1)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) packages/typescript/ai-client: dependencies: @@ -637,10 +637,10 @@ importers: devDependencies: '@vitest/coverage-v8': specifier: 4.0.14 - version: 4.0.14(vitest@4.0.14(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.14(vitest@4.0.15(@types/node@25.0.1)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) vite: - specifier: ^7.2.4 - version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + specifier: ^7.2.7 + version: 7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) zod: specifier: ^4.1.13 version: 4.1.13 @@ -655,7 +655,7 @@ importers: version: 0.4.4(csstype@3.2.3)(solid-js@1.9.10) '@tanstack/devtools-utils': specifier: ^0.0.8 - version: 0.0.8(@types/react@19.2.7)(csstype@3.2.3)(react@19.2.0)(solid-js@1.9.10)(vue@3.5.25(typescript@5.9.3)) + version: 0.0.8(@types/react@19.2.7)(csstype@3.2.3)(react@19.2.3)(solid-js@1.9.10)(vue@3.5.25(typescript@5.9.3)) goober: specifier: ^2.1.18 version: 2.1.18(csstype@3.2.3) @@ -665,32 +665,35 @@ importers: devDependencies: '@vitest/coverage-v8': specifier: 4.0.14 - version: 4.0.14(vitest@4.0.14(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.14(vitest@4.0.15(@types/node@25.0.1)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) jsdom: specifier: ^27.2.0 - version: 27.2.0(postcss@8.5.6) + version: 27.3.0(postcss@8.5.6) vite: - specifier: ^7.2.4 - version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + specifier: ^7.2.7 + version: 7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vite-plugin-solid: specifier: ^2.11.10 - version: 2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) packages/typescript/ai-gemini: dependencies: '@google/genai': specifier: ^1.30.0 - version: 1.30.0 + version: 1.33.0 '@tanstack/ai': specifier: workspace:* version: link:../ai + zod: + specifier: ^4.0.0 + version: 4.1.13 devDependencies: '@vitest/coverage-v8': specifier: 4.0.14 - version: 4.0.14(vitest@4.0.14(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.14(vitest@4.0.15(@types/node@25.0.1)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) vite: - specifier: ^7.2.4 - version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + specifier: ^7.2.7 + version: 7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) packages/typescript/ai-ollama: dependencies: @@ -701,15 +704,15 @@ importers: specifier: ^0.6.3 version: 0.6.3 zod: - specifier: ^4.1.13 + specifier: ^4.0.0 version: 4.1.13 devDependencies: '@vitest/coverage-v8': specifier: 4.0.14 - version: 4.0.14(vitest@4.0.14(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.14(vitest@4.0.15(@types/node@25.0.1)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) vite: - specifier: ^7.2.4 - version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + specifier: ^7.2.7 + version: 7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) packages/typescript/ai-openai: dependencies: @@ -718,14 +721,17 @@ importers: version: link:../ai openai: specifier: ^6.9.1 - version: 6.9.1(ws@8.18.3)(zod@4.1.13) + version: 6.10.0(ws@8.18.3)(zod@4.1.13) + zod: + specifier: ^4.0.0 + version: 4.1.13 devDependencies: '@vitest/coverage-v8': specifier: 4.0.14 - version: 4.0.14(vitest@4.0.14(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.14(vitest@4.0.15(@types/node@25.0.1)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) vite: - specifier: ^7.2.4 - version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + specifier: ^7.2.7 + version: 7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) packages/typescript/ai-react: dependencies: @@ -737,23 +743,23 @@ importers: version: link:../ai-client react: specifier: '>=18.0.0' - version: 19.2.0 + version: 19.2.3 devDependencies: '@testing-library/react': specifier: ^16.3.0 - version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@types/react': specifier: ^19.2.7 version: 19.2.7 '@vitest/coverage-v8': specifier: 4.0.14 - version: 4.0.14(vitest@4.0.14(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.14(vitest@4.0.15(@types/node@25.0.1)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) jsdom: specifier: ^27.2.0 - version: 27.2.0(postcss@8.5.6) + version: 27.3.0(postcss@8.5.6) vite: - specifier: ^7.2.4 - version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + specifier: ^7.2.7 + version: 7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) zod: specifier: ^4.1.13 version: 4.1.13 @@ -771,13 +777,13 @@ importers: version: 19.2.7 react: specifier: ^18.0.0 || ^19.0.0 - version: 19.2.0 + version: 19.2.3 react-dom: specifier: ^18.0.0 || ^19.0.0 - version: 19.2.0(react@19.2.0) + version: 19.2.3(react@19.2.3) react-markdown: specifier: ^10.1.0 - version: 10.1.0(@types/react@19.2.7)(react@19.2.0) + version: 10.1.0(@types/react@19.2.7)(react@19.2.3) rehype-highlight: specifier: ^7.0.2 version: 7.0.2 @@ -793,10 +799,10 @@ importers: devDependencies: '@vitest/coverage-v8': specifier: 4.0.14 - version: 4.0.14(vitest@4.0.14(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.14(vitest@4.0.15(@types/node@25.0.1)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) vite: - specifier: ^7.2.4 - version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + specifier: ^7.2.7 + version: 7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) packages/typescript/ai-solid: dependencies: @@ -815,25 +821,25 @@ importers: version: 0.8.10(solid-js@1.9.10) '@types/node': specifier: ^24.10.1 - version: 24.10.1 + version: 24.10.3 '@vitest/coverage-v8': specifier: 4.0.14 - version: 4.0.14(vitest@4.0.14(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.14(vitest@4.0.15(@types/node@24.10.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) jsdom: specifier: ^27.2.0 - version: 27.2.0(postcss@8.5.6) + version: 27.3.0(postcss@8.5.6) solid-js: specifier: ^1.9.10 version: 1.9.10 tsdown: specifier: ^0.17.0-beta.6 - version: 0.17.0-beta.6(oxc-resolver@11.14.0)(publint@0.3.15)(typescript@5.9.3) + version: 0.17.3(oxc-resolver@11.15.0)(publint@0.3.16)(typescript@5.9.3) typescript: specifier: 5.9.3 version: 5.9.3 vitest: specifier: ^4.0.14 - version: 4.0.14(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + version: 4.0.15(@types/node@24.10.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) packages/typescript/ai-solid-ui: dependencies: @@ -860,14 +866,14 @@ importers: version: 1.9.10 solid-markdown: specifier: ^2.1.0 - version: 2.1.0(solid-js@1.9.10) + version: 2.1.1(solid-js@1.9.10) devDependencies: '@vitest/coverage-v8': specifier: 4.0.14 - version: 4.0.14(vitest@4.0.14(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.14(vitest@4.0.15(@types/node@25.0.1)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) vite: - specifier: ^7.2.4 - version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + specifier: ^7.2.7 + version: 7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) packages/typescript/ai-svelte: dependencies: @@ -879,32 +885,32 @@ importers: version: link:../ai-client svelte: specifier: ^5.20.0 - version: 5.44.1 + version: 5.45.10 devDependencies: '@sveltejs/package': specifier: ^2.3.10 - version: 2.5.7(svelte@5.44.1)(typescript@5.9.3) + version: 2.5.7(svelte@5.45.10)(typescript@5.9.3) '@sveltejs/vite-plugin-svelte': specifier: ^5.1.1 - version: 5.1.1(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 5.1.1(svelte@5.45.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@types/node': specifier: ^24.10.1 - version: 24.10.1 + version: 24.10.3 '@vitest/coverage-v8': specifier: 4.0.14 - version: 4.0.14(vitest@4.0.14(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.14(vitest@4.0.15(@types/node@24.10.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) jsdom: specifier: ^27.2.0 - version: 27.2.0(postcss@8.5.6) + version: 27.3.0(postcss@8.5.6) svelte-check: specifier: ^4.2.0 - version: 4.3.4(picomatch@4.0.3)(svelte@5.44.1)(typescript@5.9.3) + version: 4.3.4(picomatch@4.0.3)(svelte@5.45.10)(typescript@5.9.3) typescript: specifier: 5.9.3 version: 5.9.3 vite: - specifier: ^7.2.4 - version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + specifier: ^7.2.7 + version: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) zod: specifier: ^4.1.13 version: 4.1.13 @@ -923,25 +929,25 @@ importers: devDependencies: '@types/node': specifier: ^24.10.1 - version: 24.10.1 + version: 24.10.3 '@vitest/coverage-v8': specifier: 4.0.14 - version: 4.0.14(vitest@4.0.14(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.14(vitest@4.0.15(@types/node@24.10.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@vue/test-utils': specifier: ^2.4.6 version: 2.4.6 jsdom: specifier: ^27.2.0 - version: 27.2.0(postcss@8.5.6) + version: 27.3.0(postcss@8.5.6) tsdown: specifier: ^0.17.0-beta.6 - version: 0.17.0-beta.6(oxc-resolver@11.14.0)(publint@0.3.15)(typescript@5.9.3) + version: 0.17.3(oxc-resolver@11.15.0)(publint@0.3.16)(typescript@5.9.3) typescript: specifier: 5.9.3 version: 5.9.3 vitest: specifier: ^4.0.14 - version: 4.0.14(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + version: 4.0.15(@types/node@24.10.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vue: specifier: ^3.5.25 version: 3.5.25(typescript@5.9.3) @@ -959,7 +965,7 @@ importers: version: link:../ai-vue '@vitejs/plugin-vue': specifier: ^6.0.2 - version: 6.0.2(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.3)) + version: 6.0.3(vite@7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3)) rehype-highlight: specifier: ^7.0.2 version: 7.0.2 @@ -978,10 +984,10 @@ importers: devDependencies: '@vitest/coverage-v8': specifier: 4.0.14 - version: 4.0.14(vitest@4.0.14(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.14(vitest@4.0.15(@types/node@25.0.1)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) vite: - specifier: ^7.2.4 - version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + specifier: ^7.2.7 + version: 7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vue-tsc: specifier: ^2.2.10 version: 2.2.12(typescript@5.9.3) @@ -993,20 +999,20 @@ importers: version: link:../ai-devtools '@tanstack/devtools-utils': specifier: ^0.0.8 - version: 0.0.8(@types/react@19.2.7)(csstype@3.2.3)(react@19.2.0)(solid-js@1.9.10)(vue@3.5.25(typescript@5.9.3)) + version: 0.0.8(@types/react@19.2.7)(csstype@3.2.3)(react@19.2.3)(solid-js@1.9.10)(vue@3.5.25(typescript@5.9.3)) '@types/react': specifier: ^17.0.0 || ^18.0.0 || ^19.0.0 version: 19.2.7 react: specifier: ^17.0.0 || ^18.0.0 || ^19.0.0 - version: 19.2.0 + version: 19.2.3 devDependencies: '@vitest/coverage-v8': specifier: 4.0.14 - version: 4.0.14(vitest@4.0.14(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.14(vitest@4.0.15(@types/node@25.0.1)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) vite: - specifier: ^7.2.4 - version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + specifier: ^7.2.7 + version: 7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) packages/typescript/smoke-tests: {} @@ -1027,19 +1033,22 @@ importers: '@tanstack/ai-openai': specifier: workspace:* version: link:../../ai-openai + commander: + specifier: ^13.1.0 + version: 13.1.0 devDependencies: '@alcyone-labs/zod-to-json-schema': specifier: ^4.0.10 version: 4.0.10(zod@4.1.13) '@types/node': specifier: ^24.10.1 - version: 24.10.1 + version: 24.10.3 dotenv: specifier: ^17.2.3 version: 17.2.3 tsx: specifier: ^4.20.6 - version: 4.20.6 + version: 4.21.0 typescript: specifier: 5.9.3 version: 5.9.3 @@ -1050,8 +1059,8 @@ importers: packages/typescript/smoke-tests/e2e: dependencies: '@tailwindcss/vite': - specifier: ^4.1.17 - version: 4.1.17(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + specifier: ^4.1.18 + version: 4.1.18(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@tanstack/ai': specifier: workspace:* version: link:../../ai @@ -1065,36 +1074,36 @@ importers: specifier: workspace:* version: link:../../ai-react '@tanstack/nitro-v2-vite-plugin': - specifier: ^1.139.0 - version: 1.139.0(rolldown@1.0.0-beta.53)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + specifier: ^1.141.0 + version: 1.141.0(rolldown@1.0.0-beta.53)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@tanstack/react-router': - specifier: ^1.139.7 - version: 1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + specifier: ^1.141.1 + version: 1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-start': - specifier: ^1.139.8 - version: 1.139.8(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + specifier: ^1.141.1 + version: 1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@tanstack/router-plugin': specifier: ^1.139.7 - version: 1.139.10(@tanstack/react-router@1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 1.141.1(@tanstack/react-router@1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) react: - specifier: ^19.2.0 - version: 19.2.0 + specifier: ^19.2.3 + version: 19.2.3 react-dom: - specifier: ^19.2.0 - version: 19.2.0(react@19.2.0) + specifier: ^19.2.3 + version: 19.2.3(react@19.2.3) tailwindcss: - specifier: ^4.1.17 - version: 4.1.17 + specifier: ^4.1.18 + version: 4.1.18 vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.9.3)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 5.1.4(typescript@5.9.3)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) devDependencies: '@playwright/test': specifier: ^1.57.0 version: 1.57.0 '@types/node': specifier: ^24.10.1 - version: 24.10.1 + version: 24.10.3 '@types/react': specifier: ^19.2.7 version: 19.2.7 @@ -1102,14 +1111,14 @@ importers: specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.7) '@vitejs/plugin-react': - specifier: ^5.1.1 - version: 5.1.1(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + specifier: ^5.1.2 + version: 5.1.2(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) typescript: specifier: 5.9.3 version: 5.9.3 vite: - specifier: ^7.2.4 - version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + specifier: ^7.2.7 + version: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) packages/typescript/solid-ai-devtools: dependencies: @@ -1118,36 +1127,130 @@ importers: version: link:../ai-devtools '@tanstack/devtools-utils': specifier: ^0.0.8 - version: 0.0.8(@types/react@19.2.7)(csstype@3.2.3)(react@19.2.0)(solid-js@1.9.10)(vue@3.5.25(typescript@5.9.3)) + version: 0.0.8(@types/react@19.2.7)(csstype@3.2.3)(react@19.2.3)(solid-js@1.9.10)(vue@3.5.25(typescript@5.9.3)) solid-js: specifier: '>=1.9.7' version: 1.9.10 devDependencies: '@vitest/coverage-v8': specifier: 4.0.14 - version: 4.0.14(vitest@4.0.14(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.14(vitest@4.0.15(@types/node@25.0.1)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) vite: - specifier: ^7.2.4 - version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + specifier: ^7.2.7 + version: 7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vite-plugin-solid: specifier: ^2.11.10 - version: 2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) -packages: + testing/panel: + dependencies: + '@tailwindcss/vite': + specifier: ^4.1.18 + version: 4.1.18(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@tanstack/ai': + specifier: workspace:* + version: link:../../packages/typescript/ai + '@tanstack/ai-anthropic': + specifier: workspace:* + version: link:../../packages/typescript/ai-anthropic + '@tanstack/ai-client': + specifier: workspace:* + version: link:../../packages/typescript/ai-client + '@tanstack/ai-gemini': + specifier: workspace:* + version: link:../../packages/typescript/ai-gemini + '@tanstack/ai-ollama': + specifier: workspace:* + version: link:../../packages/typescript/ai-ollama + '@tanstack/ai-openai': + specifier: workspace:* + version: link:../../packages/typescript/ai-openai + '@tanstack/ai-react': + specifier: workspace:* + version: link:../../packages/typescript/ai-react + '@tanstack/ai-react-ui': + specifier: workspace:* + version: link:../../packages/typescript/ai-react-ui + '@tanstack/nitro-v2-vite-plugin': + specifier: ^1.141.0 + version: 1.141.0(rolldown@1.0.0-beta.53)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@tanstack/react-router': + specifier: ^1.141.1 + version: 1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/react-start': + specifier: ^1.141.1 + version: 1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@tanstack/start': + specifier: ^1.120.20 + version: 1.120.20(@types/node@24.10.3)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown@1.0.0-beta.53)(terser@5.44.1)(tsx@4.21.0)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2) + highlight.js: + specifier: ^11.11.1 + version: 11.11.1 + lucide-react: + specifier: ^0.561.0 + version: 0.561.0(react@19.2.3) + react: + specifier: ^19.2.3 + version: 19.2.3 + react-dom: + specifier: ^19.2.3 + version: 19.2.3(react@19.2.3) + react-markdown: + specifier: ^10.1.0 + version: 10.1.0(@types/react@19.2.7)(react@19.2.3) + rehype-highlight: + specifier: ^7.0.2 + version: 7.0.2 + rehype-raw: + specifier: ^7.0.0 + version: 7.0.0 + rehype-sanitize: + specifier: ^6.0.0 + version: 6.0.0 + remark-gfm: + specifier: ^4.0.1 + version: 4.0.1 + tailwindcss: + specifier: ^4.1.18 + version: 4.1.18 + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.9.3)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + zod: + specifier: ^4.1.13 + version: 4.1.13 + devDependencies: + '@types/node': + specifier: ^24.10.1 + version: 24.10.3 + '@types/react': + specifier: ^19.2.7 + version: 19.2.7 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.7) + '@vitejs/plugin-react': + specifier: ^5.1.2 + version: 5.1.2(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + typescript: + specifier: 5.9.3 + version: 5.9.3 + vite: + specifier: ^7.2.7 + version: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - '@acemir/cssom@0.9.24': - resolution: {integrity: sha512-5YjgMmAiT2rjJZU7XK1SNI7iqTy92DpaYVgG6x63FxkJ11UpYfLndHJATtinWJClAXiOlW9XWaUyAQf8pMrQPg==} +packages: - '@adobe/css-tools@4.4.4': - resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} + '@acemir/cssom@0.9.29': + resolution: {integrity: sha512-G90x0VW+9nW4dFajtjCoT+NM0scAfH9Mb08IcjgFHYbfiL/lU04dTF9JuVOi3/OH+DJCQdcIseSXkdCB9Ky6JA==} '@alcyone-labs/zod-to-json-schema@4.0.10': resolution: {integrity: sha512-TFsSpAPToqmqmT85SGHXuxoCwEeK9zUDvn512O9aBVvWRhSuy+VvAXZkifzsdllD3ncF0ZjUrf4MpBwIEixdWQ==} peerDependencies: zod: ^4.0.5 - '@anthropic-ai/sdk@0.71.0': - resolution: {integrity: sha512-go1XeWXmpxuiTkosSXpb8tokLk2ZLkIRcXpbWVwJM6gH5OBtHOVsfPfGuqI1oW7RRt4qc59EmYbrXRZ0Ng06Jw==} + '@anthropic-ai/sdk@0.71.2': + resolution: {integrity: sha512-TGNDEUuEstk/DKu0/TflXAEt+p+p/WhTlFzEnoosvbaDU2LTjm42igSdlL0VijrKpWejtOKxX0b8A7uc+XiSAQ==} hasBin: true peerDependencies: zod: ^3.25.0 || ^4.0.0 @@ -1155,11 +1258,11 @@ packages: zod: optional: true - '@asamuzakjp/css-color@4.0.5': - resolution: {integrity: sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==} + '@asamuzakjp/css-color@4.1.0': + resolution: {integrity: sha512-9xiBAtLn4aNsa4mDnpovJvBn72tNEIACyvlqaNJ+ADemR+yeMJWnBudOi2qGDviJa7SwcDOU/TRh5dnET7qk0w==} - '@asamuzakjp/dom-selector@6.7.4': - resolution: {integrity: sha512-buQDjkm+wDPXd6c13534URWZqbz0RP5PAhXZ+LIoa5LgwInT9HVJvGIJivg75vi8I13CxDGdTnz+aY5YUJlIAA==} + '@asamuzakjp/dom-selector@6.7.6': + resolution: {integrity: sha512-hBaJER6A9MpdG3WgdlOolHmbOYvSk46y7IQN/1+iqiCuUu6iWdQrs9DGKF8ocqsEqWujWf/V7b7vaDgiUmIvUg==} '@asamuzakjp/nwsapi@2.3.9': resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} @@ -1379,8 +1482,8 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} - '@cloudflare/kv-asset-handler@0.4.0': - resolution: {integrity: sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==} + '@cloudflare/kv-asset-handler@0.4.1': + resolution: {integrity: sha512-Nu8ahitGFFJztxUml9oD/DLb7Z28C8cd8F46IVQ7y5Btz575pvMY8AqZsXkX7Gds29eCKdMgIHjIvzskHgPSFg==} engines: {node: '>=18.0.0'} '@crazydos/vue-markdown@1.1.4': @@ -1423,6 +1526,12 @@ packages: resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} + '@deno/shim-deno-test@0.5.0': + resolution: {integrity: sha512-4nMhecpGlPi0cSzT67L+Tm+GOJqvuk8gqHBziqcUQOarnuIax1z96/gJHCSIz2Z0zhxE6Rzwb3IZXPtFh51j+w==} + + '@deno/shim-deno@0.19.2': + resolution: {integrity: sha512-q3VTHl44ad8T2Tw2SpeAvghdGOjlnLPDNO2cpOxwMrBE/PVas6geWpbpIgrM+czOCH0yejp0yi8OaTuB+NU40Q==} + '@emnapi/core@1.7.1': resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} @@ -1432,162 +1541,456 @@ packages: '@emnapi/wasi-threads@1.1.0': resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + '@esbuild/aix-ppc64@0.20.2': + resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.25.12': resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.27.1': + resolution: {integrity: sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.20.2': + resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.25.12': resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.27.1': + resolution: {integrity: sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.20.2': + resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.25.12': resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-arm@0.27.1': + resolution: {integrity: sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.20.2': + resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.25.12': resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/android-x64@0.27.1': + resolution: {integrity: sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.20.2': + resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.25.12': resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.27.1': + resolution: {integrity: sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.20.2': + resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.25.12': resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.27.1': + resolution: {integrity: sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.20.2': + resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.25.12': resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.27.1': + resolution: {integrity: sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.20.2': + resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.25.12': resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.27.1': + resolution: {integrity: sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.20.2': + resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.25.12': resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.27.1': + resolution: {integrity: sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.20.2': + resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.25.12': resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.27.1': + resolution: {integrity: sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.20.2': + resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.25.12': resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.27.1': + resolution: {integrity: sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.20.2': + resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.25.12': resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.27.1': + resolution: {integrity: sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.20.2': + resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.25.12': resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.27.1': + resolution: {integrity: sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.20.2': + resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.25.12': resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.27.1': + resolution: {integrity: sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.20.2': + resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.25.12': resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.27.1': + resolution: {integrity: sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.20.2': + resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.25.12': resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.27.1': + resolution: {integrity: sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.20.2': + resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.25.12': resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} engines: {node: '>=18'} cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.27.1': + resolution: {integrity: sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.25.12': resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.27.1': + resolution: {integrity: sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.20.2': + resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.25.12': resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.27.1': + resolution: {integrity: sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.25.12': resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.12': - resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + '@esbuild/openbsd-arm64@0.27.1': + resolution: {integrity: sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==} engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.20.2': + resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==} + engines: {node: '>=12'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.25.12': + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.1': + resolution: {integrity: sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.27.1': + resolution: {integrity: sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.20.2': + resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.25.12': resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.27.1': + resolution: {integrity: sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.20.2': + resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.25.12': resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.27.1': + resolution: {integrity: sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.20.2': + resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.25.12': resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.27.1': + resolution: {integrity: sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.20.2': + resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.25.12': resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} engines: {node: '>=18'} cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.27.1': + resolution: {integrity: sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.9.0': resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1610,8 +2013,8 @@ packages: resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/eslintrc@3.3.1': - resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + '@eslint/eslintrc@3.3.3': + resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/js@9.39.1': @@ -1630,14 +2033,14 @@ packages: resolution: {integrity: sha512-C3mrr3b5dRVlKPJdfrAXS8+dq+rq8Qm5SNRazca0JKgw1HQERFmrVb0towvMmw5uu8hHKNiQasMaR/tydf3Zsg==} engines: {node: ^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0, npm: '>=10'} - '@gerrit0/mini-shiki@3.15.0': - resolution: {integrity: sha512-L5IHdZIDa4bG4yJaOzfasOH/o22MCesY0mx+n6VATbaiCtMeR59pdRqYk4bEiQkIHfxsHPNgdi7VJlZb2FhdMQ==} + '@gerrit0/mini-shiki@3.19.0': + resolution: {integrity: sha512-ZSlWfLvr8Nl0T4iA3FF/8VH8HivYF82xQts2DY0tJxZd4wtXJ8AA0nmdW9lmO4hlrh3f9xNwEPtOgqETPqKwDA==} - '@google/genai@1.30.0': - resolution: {integrity: sha512-3MRcgczBFbUat1wIlZoLJ0vCCfXgm7Qxjh59cZi2X08RgWLtm9hKOspzp7TOg1TV2e26/MLxR2GR5yD5GmBV2w==} + '@google/genai@1.33.0': + resolution: {integrity: sha512-ThUjFZ1N0DU88peFjnQkb8K198EWaW2RmmnDShFQ+O+xkIH9itjpRe358x3L/b4X/A7dimkvq63oz49Vbh7Cog==} engines: {node: '>=20.0.0'} peerDependencies: - '@modelcontextprotocol/sdk': ^1.20.1 + '@modelcontextprotocol/sdk': ^1.24.0 peerDependenciesMeta: '@modelcontextprotocol/sdk': optional: true @@ -1670,6 +2073,14 @@ packages: '@ioredis/commands@1.4.0': resolution: {integrity: sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==} + '@isaacs/balanced-match@4.0.1': + resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} + engines: {node: 20 || >=22} + + '@isaacs/brace-expansion@5.0.0': + resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} + engines: {node: 20 || >=22} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -1715,8 +2126,8 @@ packages: '@manypkg/get-packages@1.1.3': resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} - '@mapbox/node-pre-gyp@2.0.0': - resolution: {integrity: sha512-llMXd39jtP0HpQLVI37Bf1m2ADlEb35GYSh1SDSLsBhR+5iCxiNGlT31yqbNtVHygHAtMy6dWFERpU2JgufhPg==} + '@mapbox/node-pre-gyp@2.0.3': + resolution: {integrity: sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg==} engines: {node: '>=18'} hasBin: true @@ -1810,14 +2221,26 @@ packages: '@one-ini/wasm@0.1.1': resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + '@oozcitak/dom@1.15.10': + resolution: {integrity: sha512-0JT29/LaxVgRcGKvHmSrUTEvZ8BXvZhGl2LASRUgHqDTC1M5g1pLmVv56IYNyt3bG2CUjDkc67wnyZC14pbQrQ==} + engines: {node: '>=8.0'} + '@oozcitak/dom@2.0.2': resolution: {integrity: sha512-GjpKhkSYC3Mj4+lfwEyI1dqnsKTgwGy48ytZEhm4A/xnH/8z9M3ZVXKr/YGQi3uCLs1AEBS+x5T2JPiueEDW8w==} engines: {node: '>=20.0'} + '@oozcitak/infra@1.0.8': + resolution: {integrity: sha512-JRAUc9VR6IGHOL7OGF+yrvs0LO8SlqGnPAMqyzOuFZPSZSXI7Xf2O9+awQPSMXgIWGtgUf/dA6Hs6X6ySEaWTg==} + engines: {node: '>=6.0'} + '@oozcitak/infra@2.0.2': resolution: {integrity: sha512-2g+E7hoE2dgCz/APPOEK5s3rMhJvNxSMBrP+U+j1OWsIbtSpWxxlUjq1lU8RIsFJNYv7NMlnVsCuHcUzJW+8vA==} engines: {node: '>=20.0'} + '@oozcitak/url@1.0.4': + resolution: {integrity: sha512-kDcD8y+y3FCSOvnBI6HJgl00viO/nGbQoCINmQ0h98OhnGITrWR3bOGfwYCthgcrV8AnTJz8MzslTQbC3SOAmw==} + engines: {node: '>=8.0'} + '@oozcitak/url@3.0.0': resolution: {integrity: sha512-ZKfET8Ak1wsLAiLWNfFkZc/BraDccuTJKR6svTYc7sVjbR+Iu0vtXdiDMY4o6jaFl5TW2TlS7jbLl4VovtAJWQ==} engines: {node: '>=20.0'} @@ -1826,105 +2249,110 @@ packages: resolution: {integrity: sha512-hAX0pT/73190NLqBPPWSdBVGtbY6VOhWYK3qqHqtXQ1gK7kS2yz4+ivsN07hpJ6I3aeMtKP6J6npsEKOAzuTLA==} engines: {node: '>=20.0'} - '@oxc-project/runtime@0.101.0': - resolution: {integrity: sha512-t3qpfVZIqSiLQ5Kqt/MC4Ge/WCOGrrcagAdzTcDaggupjiGxUx4nJF2v6wUCXWSzWHn5Ns7XLv13fCJEwCOERQ==} - engines: {node: ^20.19.0 || >=22.12.0} + '@oozcitak/util@8.3.8': + resolution: {integrity: sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ==} + engines: {node: '>=8.0'} '@oxc-project/types@0.101.0': resolution: {integrity: sha512-nuFhqlUzJX+gVIPPfuE6xurd4lST3mdcWOhyK/rZO0B9XWMKm79SuszIQEnSMmmDhq1DC8WWVYGVd+6F93o1gQ==} - '@oxc-resolver/binding-android-arm-eabi@11.14.0': - resolution: {integrity: sha512-jB47iZ/thvhE+USCLv+XY3IknBbkKr/p7OBsQDTHode/GPw+OHRlit3NQ1bjt1Mj8V2CS7iHdSDYobZ1/0gagQ==} + '@oxc-resolver/binding-android-arm-eabi@11.15.0': + resolution: {integrity: sha512-Q+lWuFfq7whNelNJIP1dhXaVz4zO9Tu77GcQHyxDWh3MaCoO2Bisphgzmsh4ZoUe2zIchQh6OvQL99GlWHg9Tw==} cpu: [arm] os: [android] - '@oxc-resolver/binding-android-arm64@11.14.0': - resolution: {integrity: sha512-XFJ9t7d/Cz+dWLyqtTy3Xrekz+qqN4hmOU2iOUgr7u71OQsPUHIIeS9/wKanEK0l413gPwapIkyc5x9ltlOtyw==} + '@oxc-resolver/binding-android-arm64@11.15.0': + resolution: {integrity: sha512-vbdBttesHR0W1oJaxgWVTboyMUuu+VnPsHXJ6jrXf4czELzB6GIg5DrmlyhAmFBhjwov+yJH/DfTnHS+2sDgOw==} cpu: [arm64] os: [android] - '@oxc-resolver/binding-darwin-arm64@11.14.0': - resolution: {integrity: sha512-gwehBS9smA1mzK8frDsmUCHz+6baJVwkKF6qViHhoqA3kRKvIZ3k6WNP4JmF19JhOiGxRcoPa8gZRfzNgXwP2A==} + '@oxc-resolver/binding-darwin-arm64@11.15.0': + resolution: {integrity: sha512-R67lsOe1UzNjqVBCwCZX1rlItTsj/cVtBw4Uy19CvTicqEWvwaTn8t34zLD75LQwDDPCY3C8n7NbD+LIdw+ZoA==} cpu: [arm64] os: [darwin] - '@oxc-resolver/binding-darwin-x64@11.14.0': - resolution: {integrity: sha512-5wwJvfuoahKiAqqAsMLOI28rqdh3P2K7HkjIWUXNMWAZq6ErX0L5rwJzu6T32+Zxw3k18C7R9IS4wDq/3Ar+6w==} + '@oxc-resolver/binding-darwin-x64@11.15.0': + resolution: {integrity: sha512-77mya5F8WV0EtCxI0MlVZcqkYlaQpfNwl/tZlfg4jRsoLpFbaTeWv75hFm6TE84WULVlJtSgvf7DhoWBxp9+ZQ==} cpu: [x64] os: [darwin] - '@oxc-resolver/binding-freebsd-x64@11.14.0': - resolution: {integrity: sha512-MWTt+LOQNcQ6fa+Uu5VikkihLi1PSIrQqqp0QD44k2AORasNWl0jRGBTcMSBIgNe82qEQWYvlGzvOEEOBp01Og==} + '@oxc-resolver/binding-freebsd-x64@11.15.0': + resolution: {integrity: sha512-X1Sz7m5PC+6D3KWIDXMUtux+0Imj6HfHGdBStSvgdI60OravzI1t83eyn6eN0LPTrynuPrUgjk7tOnOsBzSWHw==} cpu: [x64] os: [freebsd] - '@oxc-resolver/binding-linux-arm-gnueabihf@11.14.0': - resolution: {integrity: sha512-b6/IBqYrS3o0XiLVBsnex/wK8pTTK+hbGfAMOHVU6p7DBpwPPLgC/tav4IXoOIUCssTFz7aWh/xtUok0swn8VQ==} + '@oxc-resolver/binding-linux-arm-gnueabihf@11.15.0': + resolution: {integrity: sha512-L1x/wCaIRre+18I4cH/lTqSAymlV0k4HqfSYNNuI9oeL28Ks86lI6O5VfYL6sxxWYgjuWB98gNGo7tq7d4GarQ==} cpu: [arm] os: [linux] - '@oxc-resolver/binding-linux-arm-musleabihf@11.14.0': - resolution: {integrity: sha512-o2Qh5+y5YoqVK6YfzkalHdpmQ5bkbGGxuLg1pZLQ1Ift0x+Vix7DaFEpdCl5Z9xvYXogd/TwOlL0TPl4+MTFLA==} + '@oxc-resolver/binding-linux-arm-musleabihf@11.15.0': + resolution: {integrity: sha512-abGXd/zMGa0tH8nKlAXdOnRy4G7jZmkU0J85kMKWns161bxIgGn/j7zxqh3DKEW98wAzzU9GofZMJ0P5YCVPVw==} cpu: [arm] os: [linux] - '@oxc-resolver/binding-linux-arm64-gnu@11.14.0': - resolution: {integrity: sha512-lk8mCSg0Tg4sEG73RiPjb7keGcEPwqQnBHX3Z+BR2SWe+qNHpoHcyFMNafzSvEC18vlxC04AUSoa6kJl/C5zig==} + '@oxc-resolver/binding-linux-arm64-gnu@11.15.0': + resolution: {integrity: sha512-SVjjjtMW66Mza76PBGJLqB0KKyFTBnxmtDXLJPbL6ZPGSctcXVmujz7/WAc0rb9m2oV0cHQTtVjnq6orQnI/jg==} cpu: [arm64] os: [linux] - '@oxc-resolver/binding-linux-arm64-musl@11.14.0': - resolution: {integrity: sha512-KykeIVhCM7pn93ABa0fNe8vk4XvnbfZMELne2s6P9tdJH9KMBsCFBi7a2BmSdUtTqWCAJokAcm46lpczU52Xaw==} + '@oxc-resolver/binding-linux-arm64-musl@11.15.0': + resolution: {integrity: sha512-JDv2/AycPF2qgzEiDeMJCcSzKNDm3KxNg0KKWipoKEMDFqfM7LxNwwSVyAOGmrYlE4l3dg290hOMsr9xG7jv9g==} cpu: [arm64] os: [linux] - '@oxc-resolver/binding-linux-ppc64-gnu@11.14.0': - resolution: {integrity: sha512-QqPPWAcZU/jHAuam4f3zV8OdEkYRPD2XR0peVet3hoMMgsihR3Lhe7J/bLclmod297FG0+OgBYQVMh2nTN6oWA==} + '@oxc-resolver/binding-linux-ppc64-gnu@11.15.0': + resolution: {integrity: sha512-zbu9FhvBLW4KJxo7ElFvZWbSt4vP685Qc/Gyk/Ns3g2gR9qh2qWXouH8PWySy+Ko/qJ42+HJCLg+ZNcxikERfg==} cpu: [ppc64] os: [linux] - '@oxc-resolver/binding-linux-riscv64-gnu@11.14.0': - resolution: {integrity: sha512-DunWA+wafeG3hj1NADUD3c+DRvmyVNqF5LSHVUWA2bzswqmuEZXl3VYBSzxfD0j+UnRTFYLxf27AMptoMsepYg==} + '@oxc-resolver/binding-linux-riscv64-gnu@11.15.0': + resolution: {integrity: sha512-Kfleehe6B09C2qCnyIU01xLFqFXCHI4ylzkicfX/89j+gNHh9xyNdpEvit88Kq6i5tTGdavVnM6DQfOE2qNtlg==} cpu: [riscv64] os: [linux] - '@oxc-resolver/binding-linux-riscv64-musl@11.14.0': - resolution: {integrity: sha512-4SRvwKTTk2k67EQr9Ny4NGf/BhlwggCI1CXwBbA9IV4oP38DH8b+NAPxDY0ySGRsWbPkG92FYOqM4AWzG4GSgA==} + '@oxc-resolver/binding-linux-riscv64-musl@11.15.0': + resolution: {integrity: sha512-J7LPiEt27Tpm8P+qURDwNc8q45+n+mWgyys4/V6r5A8v5gDentHRGUx3iVk5NxdKhgoGulrzQocPTZVosq25Eg==} cpu: [riscv64] os: [linux] - '@oxc-resolver/binding-linux-s390x-gnu@11.14.0': - resolution: {integrity: sha512-hZKvkbsurj4JOom//R1Ab2MlC4cGeVm5zzMt4IsS3XySQeYjyMJ5TDZ3J5rQ8bVj3xi4FpJU2yFZ72GApsHQ6A==} + '@oxc-resolver/binding-linux-s390x-gnu@11.15.0': + resolution: {integrity: sha512-+8/d2tAScPjVJNyqa7GPGnqleTB/XW9dZJQ2D/oIM3wpH3TG+DaFEXBbk4QFJ9K9AUGBhvQvWU2mQyhK/yYn3Q==} cpu: [s390x] os: [linux] - '@oxc-resolver/binding-linux-x64-gnu@11.14.0': - resolution: {integrity: sha512-hABxQXFXJurivw+0amFdeEcK67cF1BGBIN1+sSHzq3TRv4RoG8n5q2JE04Le2n2Kpt6xg4Y5+lcv+rb2mCJLgQ==} + '@oxc-resolver/binding-linux-x64-gnu@11.15.0': + resolution: {integrity: sha512-xtvSzH7Nr5MCZI2FKImmOdTl9kzuQ51RPyLh451tvD2qnkg3BaqI9Ox78bTk57YJhlXPuxWSOL5aZhKAc9J6qg==} cpu: [x64] os: [linux] - '@oxc-resolver/binding-linux-x64-musl@11.14.0': - resolution: {integrity: sha512-Ln73wUB5migZRvC7obAAdqVwvFvk7AUs2JLt4g9QHr8FnqivlsjpUC9Nf2ssrybdjyQzEMjttUxPZz6aKPSAHw==} + '@oxc-resolver/binding-linux-x64-musl@11.15.0': + resolution: {integrity: sha512-14YL1zuXj06+/tqsuUZuzL0T425WA/I4nSVN1kBXeC5WHxem6lQ+2HGvG+crjeJEqHgZUT62YIgj88W+8E7eyg==} cpu: [x64] os: [linux] - '@oxc-resolver/binding-wasm32-wasi@11.14.0': - resolution: {integrity: sha512-z+NbELmCOKNtWOqEB5qDfHXOSWB3kGQIIehq6nHtZwHLzdVO2oBq6De/ayhY3ygriC1XhgaIzzniY7jgrNl4Kw==} + '@oxc-resolver/binding-openharmony-arm64@11.15.0': + resolution: {integrity: sha512-/7Qli+1Wk93coxnrQaU8ySlICYN8HsgyIrzqjgIkQEpI//9eUeaeIHZptNl2fMvBGeXa7k2QgLbRNaBRgpnvMw==} + cpu: [arm64] + os: [openharmony] + + '@oxc-resolver/binding-wasm32-wasi@11.15.0': + resolution: {integrity: sha512-q5rn2eIMQLuc/AVGR2rQKb2EVlgreATGG8xXg8f4XbbYCVgpxaq+dgMbiPStyNywW1MH8VU2T09UEm30UtOQvg==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@oxc-resolver/binding-win32-arm64-msvc@11.14.0': - resolution: {integrity: sha512-Ft0+qd7HSO61qCTLJ4LCdBGZkpKyDj1rG0OVSZL1DxWQoh97m7vEHd7zAvUtw8EcWjOMBQuX4mfRap/x2MOCpQ==} + '@oxc-resolver/binding-win32-arm64-msvc@11.15.0': + resolution: {integrity: sha512-yCAh2RWjU/8wWTxQDgGPgzV9QBv0/Ojb5ej1c/58iOjyTuy/J1ZQtYi2SpULjKmwIxLJdTiCHpMilauWimE31w==} cpu: [arm64] os: [win32] - '@oxc-resolver/binding-win32-ia32-msvc@11.14.0': - resolution: {integrity: sha512-o54jYNSfGdPxHSvXEhZg8FOV3K99mJ1f7hb1alRFb+Yec1GQXNrJXxZPIxNMYeFT13kwAWB7zuQ0HZLnDHFxfw==} + '@oxc-resolver/binding-win32-ia32-msvc@11.15.0': + resolution: {integrity: sha512-lmXKb6lvA6M6QIbtYfgjd+AryJqExZVSY2bfECC18OPu7Lv1mHFF171Mai5l9hG3r4IhHPPIwT10EHoilSCYeA==} cpu: [ia32] os: [win32] - '@oxc-resolver/binding-win32-x64-msvc@11.14.0': - resolution: {integrity: sha512-j97icaORyM6A7GjgmUzfn7V+KGzVvctRA+eAlJb0c2OQNaETFxl6BXZdnGBDb+6oA0Y4Sr/wnekd1kQ0aVyKGg==} + '@oxc-resolver/binding-win32-x64-msvc@11.15.0': + resolution: {integrity: sha512-HZsfne0s/tGOcJK9ZdTGxsNU2P/dH0Shf0jqrPvsC6wX0Wk+6AyhSpHFLQCnLOuFQiHHU0ePfM8iYsoJb5hHpQ==} cpu: [x64] os: [win32] @@ -1988,6 +2416,12 @@ packages: cpu: [x64] os: [linux] + '@parcel/watcher-wasm@2.3.0': + resolution: {integrity: sha512-ejBAX8H0ZGsD8lSICDNyMbSEtPMWgDL0WFCt/0z7hyf5v8Imz4rAM8xY379mBsECkq/Wdqa5WEDLqtjZ+6NxfA==} + engines: {node: '>= 10.0.0'} + bundledDependencies: + - napi-wasm + '@parcel/watcher-wasm@2.5.1': resolution: {integrity: sha512-RJxlQQLkaMMIuWRozy+z2vEqbaQlCuaCgVZIUCzQLYggY22LZbP5Y1+ia+FD724Ids9e+XIyOLXLrLgQSHIthw==} engines: {node: '>= 10.0.0'} @@ -2028,21 +2462,21 @@ packages: '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} - '@poppinss/colors@4.1.5': - resolution: {integrity: sha512-FvdDqtcRCtz6hThExcFOgW0cWX+xwSMWcRuQe5ZEb2m7cVQOAVZOIMt+/v9RxGiD9/OY16qJBXK4CVKWAPalBw==} + '@poppinss/colors@4.1.6': + resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} - '@poppinss/dumper@0.6.4': - resolution: {integrity: sha512-iG0TIdqv8xJ3Lt9O8DrPRxw1MRLjNpoqiSGU03P/wNLP/s0ra0udPJ1J2Tx5M0J3H/cVyEgpbn8xUKRY9j59kQ==} + '@poppinss/dumper@0.6.5': + resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==} - '@poppinss/exception@1.2.2': - resolution: {integrity: sha512-m7bpKCD4QMlFCjA/nKTs23fuvoVFoA83brRKmObCUNmi/9tVu8Ve3w4YQAnJu4q3Tjf5fr685HYIC/IA2zHRSg==} + '@poppinss/exception@1.2.3': + resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} '@publint/pack@0.1.2': resolution: {integrity: sha512-S+9ANAvUmjutrshV4jZjaiG8XQyuJIZ8a4utWmN/vW1sgQ9IfBnPndwkmQYw53QmouOIytT874u65HEmu6H5jw==} engines: {node: '>=18'} - '@quansync/fs@0.1.5': - resolution: {integrity: sha512-lNS9hL2aS2NZgNW7BBj+6EBl4rOf8l+tQ0eRY6JWCI8jI2kc53gSoqbjojU0OnAWhzoXiOjFyGsHcDGePB3lhA==} + '@quansync/fs@1.0.0': + resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} '@rolldown/binding-android-arm64@1.0.0-beta.53': resolution: {integrity: sha512-Ok9V8o7o6YfSdTTYA/uHH30r3YtOxLD6G3wih/U9DO0ucBBFq8WPt/DslU53OgfteLRHITZny9N/qCUxMf9kjQ==} @@ -2121,15 +2555,12 @@ packages: cpu: [x64] os: [win32] + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + '@rolldown/pluginutils@1.0.0-beta.40': resolution: {integrity: sha512-s3GeJKSQOwBlzdUrj4ISjJj5SfSh+aqn0wjOar4Bx95iV1ETI7F6S/5hLcfAxZ9kXDcyrAkxPlqmd1ZITttf+w==} - '@rolldown/pluginutils@1.0.0-beta.47': - resolution: {integrity: sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==} - - '@rolldown/pluginutils@1.0.0-beta.50': - resolution: {integrity: sha512-5e76wQiQVeL1ICOZVUg4LSOVYg9jyhGCin+icYozhsUzM+fHE7kddi1bdiE0jwVqTfkjba3jUFbEkoC9WkdvyA==} - '@rolldown/pluginutils@1.0.0-beta.53': resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==} @@ -2337,17 +2768,17 @@ packages: '@rushstack/ts-command-line@4.22.6': resolution: {integrity: sha512-QSRqHT/IfoC5nk9zn6+fgyqOPXHME0BfchII9EUPR19pocsNp/xSbeBCbD3PIR2Lg+Q5qk7OFqk1VhWPMdKHJg==} - '@shikijs/engine-oniguruma@3.15.0': - resolution: {integrity: sha512-HnqFsV11skAHvOArMZdLBZZApRSYS4LSztk2K3016Y9VCyZISnlYUYsL2hzlS7tPqKHvNqmI5JSUJZprXloMvA==} + '@shikijs/engine-oniguruma@3.20.0': + resolution: {integrity: sha512-Yx3gy7xLzM0ZOjqoxciHjA7dAt5tyzJE3L4uQoM83agahy+PlW244XJSrmJRSBvGYELDhYXPacD4R/cauV5bzQ==} - '@shikijs/langs@3.15.0': - resolution: {integrity: sha512-WpRvEFvkVvO65uKYW4Rzxs+IG0gToyM8SARQMtGGsH4GDMNZrr60qdggXrFOsdfOVssG/QQGEl3FnJ3EZ+8w8A==} + '@shikijs/langs@3.20.0': + resolution: {integrity: sha512-le+bssCxcSHrygCWuOrYJHvjus6zhQ2K7q/0mgjiffRbkhM4o1EWu2m+29l0yEsHDbWaWPNnDUTRVVBvBBeKaA==} - '@shikijs/themes@3.15.0': - resolution: {integrity: sha512-8ow2zWb1IDvCKjYb0KiLNrK4offFdkfNVPXb1OZykpLCzRU6j+efkY+Y7VQjNlNFXonSw+4AOdGYtmqykDbRiQ==} + '@shikijs/themes@3.20.0': + resolution: {integrity: sha512-U1NSU7Sl26Q7ErRvJUouArxfM2euWqq1xaSrbqMu2iqa+tSp0D1Yah8216sDYbdDHw4C8b75UpE65eWorm2erQ==} - '@shikijs/types@3.15.0': - resolution: {integrity: sha512-BnP+y/EQnhihgHy4oIAN+6FFtmfTekwOLsQbRw9hOKwqgNy8Bdsjq8B05oAt/ZgvIWWFrshV71ytOrlPfYjIJw==} + '@shikijs/types@3.20.0': + resolution: {integrity: sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw==} '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} @@ -2448,20 +2879,20 @@ packages: '@solidjs/router': optional: true - '@speed-highlight/core@1.2.8': - resolution: {integrity: sha512-IGytNtnUnPIobIbOq5Y6LIlqiHNX+vnToQIS7lj6L5819C+rA8TXRDkkG8vePsiBOGcoW9R6i+dp2YBUKdB09Q==} + '@speed-highlight/core@1.2.12': + resolution: {integrity: sha512-uilwrK0Ygyri5dToHYdZSjcvpS2ZwX0w5aSt3GCEN9hrjxWCoeV4Z2DTXuxjwbntaLQIEEAlCeNQss5SoHvAEA==} '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} - '@stylistic/eslint-plugin@5.5.0': - resolution: {integrity: sha512-IeZF+8H0ns6prg4VrkhgL+yrvDXWDH2cKchrbh80ejG9dQgZWp10epHMbgRuQvgchLII/lfh6Xn3lu6+6L86Hw==} + '@stylistic/eslint-plugin@5.6.1': + resolution: {integrity: sha512-JCs+MqoXfXrRPGbGmho/zGS/jMcn3ieKl/A8YImqib76C8kjgZwq5uUFzc30lJkMvcchuRn6/v8IApLxli3Jyw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: '>=9.0.0' - '@sveltejs/acorn-typescript@1.0.7': - resolution: {integrity: sha512-znp1A/Y1Jj4l/Zy7PX5DZKBE0ZNY+5QBngiE21NJkfSTyzzC5iKNWOtwFXKtIrn7MXEFBck4jD95iBNkGjK92Q==} + '@sveltejs/acorn-typescript@1.0.8': + resolution: {integrity: sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA==} peerDependencies: acorn: ^8.9.0 @@ -2509,65 +2940,65 @@ packages: resolution: {integrity: sha512-08eKiDAjj4zLug1taXSIJ0kGL5cawjVCyJkBb6EWSg5fEPX6L+Wtr0CH2If4j5KYylz85iaZiFlUItvgJvll5g==} engines: {node: ^14.13.1 || ^16.0.0 || >=18} - '@tailwindcss/node@4.1.17': - resolution: {integrity: sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==} + '@tailwindcss/node@4.1.18': + resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} - '@tailwindcss/oxide-android-arm64@4.1.17': - resolution: {integrity: sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==} + '@tailwindcss/oxide-android-arm64@4.1.18': + resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==} engines: {node: '>= 10'} cpu: [arm64] os: [android] - '@tailwindcss/oxide-darwin-arm64@4.1.17': - resolution: {integrity: sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==} + '@tailwindcss/oxide-darwin-arm64@4.1.18': + resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@tailwindcss/oxide-darwin-x64@4.1.17': - resolution: {integrity: sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==} + '@tailwindcss/oxide-darwin-x64@4.1.18': + resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@tailwindcss/oxide-freebsd-x64@4.1.17': - resolution: {integrity: sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==} + '@tailwindcss/oxide-freebsd-x64@4.1.18': + resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==} engines: {node: '>= 10'} cpu: [x64] os: [freebsd] - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17': - resolution: {integrity: sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==} + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': + resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@tailwindcss/oxide-linux-arm64-gnu@4.1.17': - resolution: {integrity: sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==} + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': + resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tailwindcss/oxide-linux-arm64-musl@4.1.17': - resolution: {integrity: sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==} + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': + resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tailwindcss/oxide-linux-x64-gnu@4.1.17': - resolution: {integrity: sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==} + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': + resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tailwindcss/oxide-linux-x64-musl@4.1.17': - resolution: {integrity: sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==} + '@tailwindcss/oxide-linux-x64-musl@4.1.18': + resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tailwindcss/oxide-wasm32-wasi@4.1.17': - resolution: {integrity: sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==} + '@tailwindcss/oxide-wasm32-wasi@4.1.18': + resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} engines: {node: '>=14.0.0'} cpu: [wasm32] bundledDependencies: @@ -2578,39 +3009,35 @@ packages: - '@emnapi/wasi-threads' - tslib - '@tailwindcss/oxide-win32-arm64-msvc@4.1.17': - resolution: {integrity: sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==} + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': + resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@tailwindcss/oxide-win32-x64-msvc@4.1.17': - resolution: {integrity: sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==} + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': + resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@tailwindcss/oxide@4.1.17': - resolution: {integrity: sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==} + '@tailwindcss/oxide@4.1.18': + resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==} engines: {node: '>= 10'} - '@tailwindcss/vite@4.1.17': - resolution: {integrity: sha512-4+9w8ZHOiGnpcGI6z1TVVfWaX/koK7fKeSYF3qlYg2xpBtbteP2ddBxiarL+HVgfSJGeK5RIxRQmKm4rTJJAwA==} + '@tailwindcss/vite@4.1.18': + resolution: {integrity: sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==} peerDependencies: vite: ^5.2.0 || ^6 || ^7 - '@tanstack/devtools-client@0.0.4': - resolution: {integrity: sha512-LefnH9KE9uRDEWifc3QDcooskA8ikfs41bybDTgpYQpyTUspZnaEdUdya9Hry0KYxZ8nos0S3nNbsP79KHqr6Q==} + '@tanstack/devtools-client@0.0.5': + resolution: {integrity: sha512-hsNDE3iu4frt9cC2ppn1mNRnLKo2uc1/1hXAyY9z4UYb+o40M2clFAhiFoo4HngjfGJDV3x18KVVIq7W4Un+zA==} engines: {node: '>=18'} '@tanstack/devtools-event-bus@0.3.3': resolution: {integrity: sha512-lWl88uLAz7ZhwNdLH6A3tBOSEuBCrvnY9Fzr5JPdzJRFdM5ZFdyNWz1Bf5l/F3GU57VodrN0KCFi9OA26H5Kpg==} engines: {node: '>=18'} - '@tanstack/devtools-event-client@0.3.5': - resolution: {integrity: sha512-RL1f5ZlfZMpghrCIdzl6mLOFLTuhqmPNblZgBaeKfdtk5rfbjykurv+VfYydOFXj0vxVIoA2d/zT7xfD7Ph8fw==} - engines: {node: '>=18'} - '@tanstack/devtools-event-client@0.4.0': resolution: {integrity: sha512-RPfGuk2bDZgcu9bAJodvO2lnZeHuz4/71HjZ0bGb/SPg8+lyTA+RLSKQvo7fSmPSi8/vcH3aKQ8EM9ywf1olaw==} engines: {node: '>=18'} @@ -2639,20 +3066,26 @@ packages: vue: optional: true - '@tanstack/devtools-vite@0.3.11': - resolution: {integrity: sha512-t5jaWJNgkXOQTxuNrwkz71cN86zPZnLJY2Rz0IaMDgjb0ib1EKHeRgdqHMR/2YL96yhCHHDCHroBQXsw5Da4dg==} + '@tanstack/devtools-vite@0.3.12': + resolution: {integrity: sha512-fGJgu4xUhKmGk+a+/aHD8l5HKVk6+ObA+6D3YC3xCXbai/YmaGhztqcZf1tKUqjZyYyQLHsjqmKzvJgVpQP1jw==} engines: {node: '>=18'} peerDependencies: vite: ^6.0.0 || ^7.0.0 - '@tanstack/devtools@0.8.2': - resolution: {integrity: sha512-ltVS+MpOrA37CiVunSOCcXctaDnQOJV6FPE5Y2uLq3m8b0spmHwp0edp1PRd2CMG4LnGIlRf7lYdhHa2p9CHNA==} + '@tanstack/devtools@0.9.1': + resolution: {integrity: sha512-fW/1ewT+g0LgJGraS/Irwle3uRgM1VDwfhi/NP3aGHhGyCDAWJI0+Id9FBzjChQ5BuEU5qD5fdegcFqTZShXHw==} engines: {node: '>=18'} peerDependencies: solid-js: '>=1.9.7' - '@tanstack/directive-functions-plugin@1.139.0': - resolution: {integrity: sha512-qLGxldnWa0pp/siZEFEYDU+eB/j40bd1V3IuTzP0sFnrYi11Ldx1yVkOruDKUbO1WM0o+OlPhp22Q1h+LMdDMA==} + '@tanstack/directive-functions-plugin@1.131.2': + resolution: {integrity: sha512-5Pz6aVPS0BW+0bLvMzWsoajfjI6ZeWqkbVBaQfIbSTm4DOBO05JuQ/pb7W7m3GbCb5TK1a/SKDhuTX6Ag5I7UQ==} + engines: {node: '>=12'} + peerDependencies: + vite: '>=6.0.0' + + '@tanstack/directive-functions-plugin@1.141.0': + resolution: {integrity: sha512-Ca8ylyh2c100Kn9nFUA4Gao95eISBGLbff+4unJ6MF+t+/FR3awIsIC5gBxeEVu+nv6HPaY9ZeD0/Ehh4OsXpQ==} engines: {node: '>=12'} peerDependencies: vite: '>=6.0.0 || >=7.0.0' @@ -2661,24 +3094,25 @@ packages: resolution: {integrity: sha512-8VFyAaIFV9onJcfc5yVj5WWl6DmN3W4m+t0Mb+nZrQmqHy+kDndw5O5Xv2BHVWRRPTqnhlJYh6wHWGh0R81ZzQ==} engines: {node: '>=18'} - '@tanstack/history@1.139.0': - resolution: {integrity: sha512-l6wcxwDBeh/7Dhles23U1O8lp9kNJmAb2yNjekR6olZwCRNAVA8TCXlVCrueELyFlYZqvQkh0ofxnzG62A1Kkg==} + '@tanstack/history@1.131.2': + resolution: {integrity: sha512-cs1WKawpXIe+vSTeiZUuSBy8JFjEuDgdMKZFRLKwQysKo8y2q6Q1HvS74Yw+m5IhOW1nTZooa6rlgdfXcgFAaw==} + engines: {node: '>=12'} + + '@tanstack/history@1.141.0': + resolution: {integrity: sha512-LS54XNyxyTs5m/pl1lkwlg7uZM3lvsv2FIIV1rsJgnfwVCnI+n4ZGZ2CcjNT13BPu/3hPP+iHmliBSscJxW5FQ==} engines: {node: '>=12'} - '@tanstack/nitro-v2-vite-plugin@1.139.0': - resolution: {integrity: sha512-TedrzuMjtHA08x47wCaU6KTrrat8gyzn8a4HUswTwjAa0sMpz4vbOOUDDZyYaJpmjKKbTjgTs6iRr4MONneKTQ==} + '@tanstack/nitro-v2-vite-plugin@1.141.0': + resolution: {integrity: sha512-OW0U7ftm4unRKhL9AZoTtdYIT3UnKfY8UQ25QNzI2uUPeVOyE9/yINFEgPGzS6gn4jQmJ3N2QvFZNvEjrbD0iA==} engines: {node: '>=22.12'} peerDependencies: vite: '>=7.0.0' - '@tanstack/query-core@5.90.11': - resolution: {integrity: sha512-f9z/nXhCgWDF4lHqgIE30jxLe4sYv15QodfdPDKYAk7nAEjNcndy4dHz3ezhdUaR23BpWa4I2EH4/DZ0//Uf8A==} + '@tanstack/query-core@5.90.12': + resolution: {integrity: sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg==} - '@tanstack/query-core@5.90.5': - resolution: {integrity: sha512-wLamYp7FaDq6ZnNehypKI5fNvxHPfTYylE0m/ZpuuzJfJqhR5Pxg9gvGBHZx4n7J+V5Rg5mZxHHTlv25Zt5u+w==} - - '@tanstack/react-devtools@0.8.2': - resolution: {integrity: sha512-D1oG2QivpAmiT4iq7PxbsajmoYmtnhwg9gEK7q9mDiVcnyPjwnhg1ujDvKIzP+ZaRTkQzpJYwtTmS9DzYp8Akg==} + '@tanstack/react-devtools@0.8.4': + resolution: {integrity: sha512-fq7GrpHIRdIBa5HmNN+CmC7CopY3tfKE4AXoszNSLk4yHKjC8iEjx7rh4r4RO5iUxHnmoCKX+NPXDgfsIKtaog==} engines: {node: '>=18'} peerDependencies: '@types/react': '>=16.8' @@ -2686,25 +3120,25 @@ packages: react: '>=16.8' react-dom: '>=16.8' - '@tanstack/react-query@5.90.5': - resolution: {integrity: sha512-pN+8UWpxZkEJ/Rnnj2v2Sxpx1WFlaa9L6a4UO89p6tTQbeo+m0MS8oYDjbggrR8QcTyjKoYWKS3xJQGr3ExT8Q==} + '@tanstack/react-query@5.90.12': + resolution: {integrity: sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg==} peerDependencies: react: ^18 || ^19 - '@tanstack/react-router-devtools@1.139.7': - resolution: {integrity: sha512-ySuFWfR5mHtbs/le5SUb56OxCWTZskwynPp6E9qnyDgB4vX6P7OJDqdgv7rqiorYNjFmAaywraaVZGQ8WuB4+g==} + '@tanstack/react-router-devtools@1.141.1': + resolution: {integrity: sha512-+XCn9cXSe1fZAD9jRrezEYE0ojn9U+Y0lRTRFdR8n51wx0UzJ6xe/Pewtw0rp03h/zmBR0pX+HRNU9NJDneWGA==} engines: {node: '>=12'} peerDependencies: - '@tanstack/react-router': ^1.139.7 - '@tanstack/router-core': ^1.139.7 + '@tanstack/react-router': ^1.141.1 + '@tanstack/router-core': ^1.141.1 react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' peerDependenciesMeta: '@tanstack/router-core': optional: true - '@tanstack/react-router-ssr-query@1.139.7': - resolution: {integrity: sha512-S38TJsBrA7NRxFzdCTb7uDIls1JEtRJAuSLWQrlVigmFEHY2yk2/r45aSKu0cc/ucxRRZ+0JxUo4lFInoFYt1w==} + '@tanstack/react-router-ssr-query@1.141.1': + resolution: {integrity: sha512-80KEKpHx9OZwXcdCOZDyftqxWMP8QHGU6LQgyIsuunPrf76Wf1riyBnZrPOAOJPcjM3PDhTHLNIfmRBnAtItUw==} engines: {node: '>=12'} peerDependencies: '@tanstack/query-core': '>=5.90.0' @@ -2713,29 +3147,40 @@ packages: react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' - '@tanstack/react-router@1.139.7': - resolution: {integrity: sha512-5vhwIAwoxWl7oeIZRNgk5wh9TCkaAinK9qbfdKuKzwGtMHqnv1bRrfKwam3/MaMwHCmvnNfnFj0RYfnBA/ilEg==} + '@tanstack/react-router@1.141.1': + resolution: {integrity: sha512-pLQ6ZFCh5s86ewZIAu2wQc2svf+DqttD7CFd1NPSxdEU20KvMMj9RPQdDHj/pbf2VvB3i1nEDxjli/Z0PBvnCQ==} engines: {node: '>=12'} peerDependencies: react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' - '@tanstack/react-start-client@1.139.7': - resolution: {integrity: sha512-082eg9SvYdg4+kZFO6fhiwazoWOa8TUWLIi2Um3OLcnlBJzAf3cwsYE+Ub4siPucRX4DxzSDrY5TgH+uMYKtBQ==} + '@tanstack/react-start-client@1.141.1': + resolution: {integrity: sha512-DkScmgoed8DbLxivWI/LbyGUsPcuOuWI0WGcNrPdFJLn2SqJjGSXhNcYZAAKlwY2TtCp7L+YnZoeOi/EDgrdSg==} engines: {node: '>=22.12.0'} peerDependencies: react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' - '@tanstack/react-start-server@1.139.8': - resolution: {integrity: sha512-7lmu6a2PDpxd1J438FmV/lxc5vRRvy34dV9NYRNvOj6fxcGfagxix1qi6NKtgmiSQQ83DNfrckHno0wlOJJLOg==} + '@tanstack/react-start-plugin@1.131.50': + resolution: {integrity: sha512-ys+sGvnnE8BUNjGsngg+MGn3F5lV4okL5CWEKFzjBSjQsrTN7apGfmqvBP3O6PkRPHpXZ8X3Z5QsFvSc0CaDRQ==} + engines: {node: '>=12'} + peerDependencies: + '@vitejs/plugin-react': '>=4.3.4' + vite: '>=6.0.0' + + '@tanstack/react-start-router-manifest@1.120.19': + resolution: {integrity: sha512-z+4YL6shTtsHjk32yaIemQwgkx6FcqwPBYfeNt7Co2eOpWrvsoo/Fe9869/oIY2sPyhiWDs1rDb3e0qnAy8Cag==} + engines: {node: '>=12'} + + '@tanstack/react-start-server@1.141.1': + resolution: {integrity: sha512-GsZ7De5CCDkKcrimvwJiYJHMFGw7LQRu8IuLvqyzYgQvJPcbkfY5UIQd2IZfgRNFegzb2aYzkAXwv5lxfdu1VQ==} engines: {node: '>=22.12.0'} peerDependencies: react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' - '@tanstack/react-start@1.139.8': - resolution: {integrity: sha512-vNSd1w+NCDAmTzkiPC6klnwVZBH8EjXg+c5sf7+PPUYXMZMb7kYCRiH8xKjCBRQkubgQeA8bnVsbRWqC21hQHw==} + '@tanstack/react-start@1.141.1': + resolution: {integrity: sha512-03iELlg9T9ZN9rKAM1BTCCIBptLbaoZYCZXe0xGf4ZLs3Md+EhmJZibtKluclVQcnjzeiE0T17j1A/YxvVwTZg==} engines: {node: '>=22.12.0'} peerDependencies: react: '>=18.0.0 || >=19.0.0' @@ -2748,52 +3193,41 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tanstack/router-core@1.139.10': - resolution: {integrity: sha512-gougqlYumNOn98d2ZhyoRJTNT8RvFip97z6T2T3/JTPrErwOsKaIA2FwlkfLJmJY1JQtUuF38IREJdfQrTJiqg==} - engines: {node: '>=12'} - - '@tanstack/router-core@1.139.7': - resolution: {integrity: sha512-mqgsJi4/B2Jo6PXRUs1AsWA+06nqiqVZe1aXioA3vR6PesNeKUSXWfmIoYF6wOx3osiV0BnwB1JCBrInCOQSWA==} + '@tanstack/router-core@1.131.50': + resolution: {integrity: sha512-eojd4JZ5ziUhGEmXZ4CaVX5mQdiTMiz56Sp8ZQ6r7deb55Q+5G4JQDkeuXpI7HMAvzr+4qlsFeLaDRXXjXyOqQ==} engines: {node: '>=12'} - '@tanstack/router-devtools-core@1.139.10': - resolution: {integrity: sha512-rAUAhTvwivA49dkYR4bRUPRxqShO9dTD1+r3tZsnt23XlpmGtFvxBw8FYY2C9BvqaRLu+2RxACJDaXETVfm3OA==} + '@tanstack/router-core@1.141.1': + resolution: {integrity: sha512-fR1GGpp6v3dVKu4KIAjEh+Sd0qGLQd/wvCOVHeopSY6aFidXKCzwrS5cBOBqoPPWTKmn6CdW1a0CzFr5Furdog==} engines: {node: '>=12'} - peerDependencies: - '@tanstack/router-core': ^1.139.10 - csstype: ^3.0.10 - solid-js: '>=1.9.5' - peerDependenciesMeta: - csstype: - optional: true - '@tanstack/router-devtools-core@1.139.7': - resolution: {integrity: sha512-Tx6+rCyjthlH7KS9Jz6YdT2KQ6rZQ66F+XJOj7Rel8zGAvyqx8USzcqTRvC+QjaU1jIJq+mNPWpMdKkkxPSOVA==} + '@tanstack/router-devtools-core@1.141.1': + resolution: {integrity: sha512-wD9yRvOk6FI+thiNBplhkGutPIPBlXvWu9ttU/obdFY5oXQj9WYgNS+IO9BEe8Pz5rNEu8zE/oLn4RUGIVdtnw==} engines: {node: '>=12'} peerDependencies: - '@tanstack/router-core': ^1.139.7 + '@tanstack/router-core': ^1.141.1 csstype: ^3.0.10 solid-js: '>=1.9.5' peerDependenciesMeta: csstype: optional: true - '@tanstack/router-generator@1.139.10': - resolution: {integrity: sha512-Uo0xmz6w1Ayv1AMyWLsT0ngXmjB8yAKv5khOaci/ZxAZNyvz3t84jqI7XXlG9fwtDRdTF4G/qBmXlPEmPk6Wfg==} + '@tanstack/router-generator@1.131.50': + resolution: {integrity: sha512-zlMBw5l88GIg3v+378JsfDYq3ejEaJmD3P1R+m0yEPxh0N//Id1FjKNSS7yJbejlK2WGVm9DUG46iBdTDMQM+Q==} engines: {node: '>=12'} - '@tanstack/router-generator@1.139.7': - resolution: {integrity: sha512-xnmF1poNH/dHtwFxecCcRsaLRIXVnXRZiWYUpvtyaPv4pQYayCrFQCg2ygDbCV0/8H7ctMBJh5MIL7GgPR7+xw==} + '@tanstack/router-generator@1.141.1': + resolution: {integrity: sha512-21RbVAoIDn7s/n/PKMN6U60d5hCeVADrBH/uN6B/poMT4MVYtJXqISVzkc2RAboVRw6eRdYFeF+YlwA3nF6y3Q==} engines: {node: '>=12'} - '@tanstack/router-plugin@1.139.10': - resolution: {integrity: sha512-0c9wzBKuz2U1jO+oAszT6VRaQDWPLfCJuPeXX7MCisM0nV2LVaxdb/y9YaWSKJ7zlQ7pwFkh37KYqcJhPXug/A==} + '@tanstack/router-plugin@1.131.50': + resolution: {integrity: sha512-gdEBPGzx7llQNRnaqfPJ1iaPS3oqB8SlvKRG5l7Fxp4q4yINgkeowFYSKEhPOc9bjoNhGrIHOlvPTPXEzAQXzQ==} engines: {node: '>=12'} peerDependencies: '@rsbuild/core': '>=1.0.2' - '@tanstack/react-router': ^1.139.10 - vite: '>=5.0.0 || >=6.0.0 || >=7.0.0' - vite-plugin-solid: ^2.11.10 + '@tanstack/react-router': ^1.131.50 + vite: '>=5.0.0 || >=6.0.0' + vite-plugin-solid: ^2.11.2 webpack: '>=5.92.0' peerDependenciesMeta: '@rsbuild/core': @@ -2807,12 +3241,12 @@ packages: webpack: optional: true - '@tanstack/router-plugin@1.139.7': - resolution: {integrity: sha512-sgB8nOoVKr0A2lw5p7kQ3MtEA03d1t+Qvqyy+f/QkHy5pGk8Yohg64TEX+2e98plfM3j5vAOu/JhAyoLLrp1Jw==} + '@tanstack/router-plugin@1.141.1': + resolution: {integrity: sha512-SoNXm8QK8cqX1Q4y1AfLU0tZLOt4mB4wkdpBI/Mi3ZEezF8tIrmaenyJ3987cjT9jedTy3VDBab1wN0g8MoOXg==} engines: {node: '>=12'} peerDependencies: '@rsbuild/core': '>=1.0.2' - '@tanstack/react-router': ^1.139.7 + '@tanstack/react-router': ^1.141.1 vite: '>=5.0.0 || >=6.0.0 || >=7.0.0' vite-plugin-solid: ^2.11.10 webpack: '>=5.92.0' @@ -2828,52 +3262,53 @@ packages: webpack: optional: true - '@tanstack/router-ssr-query-core@1.139.10': - resolution: {integrity: sha512-cTNjPzDOVY8nbSCqgqLcPJ2dpMWrgo5VDFDal5c6xb38rU84mTzocXOX8a/9fjWMn4wzxv5p1S8HPN6RXfKD4w==} + '@tanstack/router-ssr-query-core@1.141.1': + resolution: {integrity: sha512-bkRXUhktifxBewnnphH59E0sGcsUI1NmNqxzCAmXIb93xYgafhjUGGYwfK6FqFBOmCB5isr32exGO3+UMHJr/A==} engines: {node: '>=12'} peerDependencies: '@tanstack/query-core': '>=5.90.0' '@tanstack/router-core': '>=1.127.0' - '@tanstack/router-ssr-query-core@1.139.7': - resolution: {integrity: sha512-Ei4P2g/7xNO99OgvBOAAeVLI6VnqXYcSTI1Q6b1NYBzsb4aIo8Ne38cgVnanDlnIRrUJjIDQdZTAtu0AdANiyg==} + '@tanstack/router-utils@1.131.2': + resolution: {integrity: sha512-sr3x0d2sx9YIJoVth0QnfEcAcl+39sQYaNQxThtHmRpyeFYNyM2TTH+Ud3TNEnI3bbzmLYEUD+7YqB987GzhDA==} + engines: {node: '>=12'} + + '@tanstack/router-utils@1.141.0': + resolution: {integrity: sha512-/eFGKCiix1SvjxwgzrmH4pHjMiMxc+GA4nIbgEkG2RdAJqyxLcRhd7RPLG0/LZaJ7d0ad3jrtRqsHLv2152Vbw==} engines: {node: '>=12'} - peerDependencies: - '@tanstack/query-core': '>=5.90.0' - '@tanstack/router-core': '>=1.127.0' - '@tanstack/router-utils@1.139.0': - resolution: {integrity: sha512-jT7D6NimWqoFSkid4vCno8gvTyfL1+NHpgm3es0B2UNhKKRV3LngOGilm1m6v8Qvk/gy6Fh/tvB+s+hBl6GhOg==} + '@tanstack/server-functions-plugin@1.131.2': + resolution: {integrity: sha512-hWsaSgEZAVyzHg8+IcJWCEtfI9ZSlNELErfLiGHG9XCHEXMegFWsrESsKHlASzJqef9RsuOLDl+1IMPIskwdDw==} engines: {node: '>=12'} - '@tanstack/server-functions-plugin@1.139.0': - resolution: {integrity: sha512-IpNFiCoy2YU6gY/4lCKIVlFyU67ltlcUMGcdnrevqOgq20AbMyeLbbBVo9tAA3TkHK9F+9Hd7DqGXsup2pmBLg==} + '@tanstack/server-functions-plugin@1.141.0': + resolution: {integrity: sha512-WtqK9f3rGhocjPzmYgfBTF4nt8oLka6EuT0Hc8MheU59bS5WliuKJTU3nvPPNEQUCMFv3J4fFa3vE2b6tC8BUQ==} engines: {node: '>=12'} - '@tanstack/solid-devtools@0.7.15': - resolution: {integrity: sha512-c9QqNwBiWSXTytodgiwu93jkV7VhaktyckbvX8TytqdWxkTSUiqTgbyQ+RjXvG1M3IDTwymXj2YkRhr0krb4Ug==} + '@tanstack/solid-devtools@0.7.17': + resolution: {integrity: sha512-S8nXefYEKDWVIo7q87pNGSz1zD2rrPlE0yka9PB6uK+uRXxTK7dHviWGzvBTi0k5qtEsLMPPaAo2smdxjbO7MQ==} engines: {node: '>=18'} peerDependencies: solid-js: '>=1.9.7' - '@tanstack/solid-query@5.90.14': - resolution: {integrity: sha512-e/TP+92mOFoQtInffcYKvAExgbQoBSDrarKnGwnindQBItp0ne1VdIg88K2U8rNwwM/wdj15V5azpXIkZkNQDw==} + '@tanstack/solid-query@5.90.15': + resolution: {integrity: sha512-5bCMbGJHMGSTK4sA3OKs3p8w21+k4tp+KwVin8IwkMnmzVfGzEkuZnDM8WexO2fMqfr7nBpCGxZHSiUVcniCRQ==} peerDependencies: solid-js: ^1.6.0 - '@tanstack/solid-router-devtools@1.139.10': - resolution: {integrity: sha512-ZrGkenyRs4tjxj39ihOE14W2Uj6+19wF58V+neBE0goR75ueeKSwgVqvz4GtIE6p6EhwaoebS4lOuFTtwSSQcg==} + '@tanstack/solid-router-devtools@1.141.1': + resolution: {integrity: sha512-ezm8V1UR47kY/xquIobascexHkbWOcH1YXc8BRvYDKV33NEdeUhMVUqkZmwuojlRYXhF7LEfj2aSwZr0uBfH8A==} engines: {node: '>=12'} peerDependencies: - '@tanstack/router-core': ^1.139.10 - '@tanstack/solid-router': ^1.139.10 + '@tanstack/router-core': ^1.141.1 + '@tanstack/solid-router': ^1.141.1 solid-js: ^1.9.10 peerDependenciesMeta: '@tanstack/router-core': optional: true - '@tanstack/solid-router-ssr-query@1.139.10': - resolution: {integrity: sha512-NXB8m1ndNhzc2fd0INUeDHqSlUDGhuBjvJoBf0tYjtwne3CHXJVADNN6vCxCgjOYmS+dXrqlK0kcOY7/4TsclQ==} + '@tanstack/solid-router-ssr-query@1.141.1': + resolution: {integrity: sha512-jM2RKblxWbrMV2nmZ+L+Y2HvfBZ448tZHXYBP/NrQa2YAHVXCZfVE5ojjB5vyDZHc7ueFAF8+yIvMGnIQQE99g==} engines: {node: '>=12'} peerDependencies: '@tanstack/query-core': '>=5.90.0' @@ -2881,26 +3316,26 @@ packages: '@tanstack/solid-router': '>=1.127.0' solid-js: ^1.9.10 - '@tanstack/solid-router@1.139.10': - resolution: {integrity: sha512-zEq1cM92ORD21OOvmhX9cbCR1PDqP5H7u2s3WxOea0cwxQCgGGePrcD/wxKLnSrhQlx3XXCZPgCFTXxXHbsdCg==} + '@tanstack/solid-router@1.141.1': + resolution: {integrity: sha512-xcWkYdwvTXzQwLd4gI0efq7dUzXlcTyent/UDrGf1K5dDBqlFSAj9c11vNwP0+YvCaPt4U1r8czyMD1Qr3aNGA==} engines: {node: '>=12'} peerDependencies: solid-js: ^1.9.10 - '@tanstack/solid-start-client@1.139.10': - resolution: {integrity: sha512-zfygCE+lXNixaMFVaiPYQwTzmilfmgPhyHgU6zUvSg9Tb9odTvQDYx2/RMHkyaoZY6oAtJx5GGsI+INe1wxCCA==} + '@tanstack/solid-start-client@1.141.1': + resolution: {integrity: sha512-NGc70Jj9DoWFieFX9iQG70fmuTvX7nfTUG5vlboxBzvTuPrIdyeddrQVnBX/kXUdFzLGzez7QlxqyOrm3zouuQ==} engines: {node: '>=22.12.0'} peerDependencies: solid-js: '>=1.0.0' - '@tanstack/solid-start-server@1.139.10': - resolution: {integrity: sha512-Jlb27HgEKmA2vS7ikC6lsPFCCQY/EQiIR41TUrp466GTNVETQuMZ3G5L3hc+7f8L+Kwt4lsfFUgRNp4Ars8EqQ==} + '@tanstack/solid-start-server@1.141.1': + resolution: {integrity: sha512-vIuw74+6cgBmh1zAqc27dKHd5rU2K4V6nHt+u7vBKUUu/9QIF7PoQD0fj2dWnrjnz7EJKb1W7Gr9rAtOdglgGw==} engines: {node: '>=22.12.0'} peerDependencies: solid-js: ^1.0.0 - '@tanstack/solid-start@1.139.10': - resolution: {integrity: sha512-ZgCW+tSrt6nTJSzb5UvLQf5VWxhWy0Y/EeOrxe46UZ+1If96wyBKB8PoxtQDb31SuxM7rI9gx1ZYseXQd8Jbuw==} + '@tanstack/solid-start@1.141.1': + resolution: {integrity: sha512-b7B7UUPn3GANvnvTusiryTr+CUEs+lMMFRRiDiBZXx+Yb1cqtp/UYilEDHdqnx2xUoAhR0FBd4kRcieCV69DVg==} engines: {node: '>=22.12.0'} peerDependencies: solid-js: '>=1.0.0' @@ -2911,42 +3346,81 @@ packages: peerDependencies: solid-js: ^1.6.0 - '@tanstack/start-client-core@1.139.10': - resolution: {integrity: sha512-eF6z4Ag/nmhfduGHRm2DCwowUUweFTyIJqQ6Vo0fbDyi23eKRNcf4uzewR9tE8EHlFqIV987pgMVbWaaZQqmjQ==} - engines: {node: '>=22.12.0'} + '@tanstack/start-api-routes@1.120.19': + resolution: {integrity: sha512-zvMI9Rfwsm3CCLTLqdvUfteDRMdPKTOO05O3L8vp49BrYYsLrT0OplhounzdRMgGMnKd4qCXUC9Pj4UOUOodTw==} + engines: {node: '>=12'} - '@tanstack/start-client-core@1.139.7': - resolution: {integrity: sha512-omG032CeYUWlwQt6s7VFqhc9dGHKWNJ0C5PoIckL+G/HcV+0/RxYkiKzx/HTTzWt+K+LpsBDFFNnrTUUyTE5sw==} - engines: {node: '>=22.12.0'} + '@tanstack/start-client-core@1.131.50': + resolution: {integrity: sha512-8fbwYca1NAu/5WyGvO3e341/FPpsiqdPrrzkoc0cXQimMN1DligoRjvHgP13q3n5w1tFMSqChGzXfOVJP9ndSw==} + engines: {node: '>=12'} - '@tanstack/start-plugin-core@1.139.10': - resolution: {integrity: sha512-NzvaBFmODpqLqkN/1iaD7ikF3TDXDvXh4IkhhyVzI4lY5YAxErdQ61wxOnieSxHrFpyz/P/iJI8ipsFO65gj2g==} + '@tanstack/start-client-core@1.141.1': + resolution: {integrity: sha512-Rk/b0ekX7p0ZBKOg9WM5c632YPqu7GlvZSYnAjNi1GDp1/sET6g2Trp+GAjs1s8kakp2pMQ4sZUG/11grCMfJw==} engines: {node: '>=22.12.0'} + + '@tanstack/start-config@1.120.20': + resolution: {integrity: sha512-oH/mfTSHV8Qbil74tWicPLW6+kKmT3esXCnDzvrkhi3+N8ZuVUDr01Qpil0Wxf9lLPfM5L6VX03nF4hSU8vljg==} + engines: {node: '>=12'} peerDependencies: - vite: '>=7.0.0' + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + vite: ^6.0.0 + + '@tanstack/start-plugin-core@1.131.50': + resolution: {integrity: sha512-eFvMA0chqLtHbq+8ojp1fXN7AQjhmeoOpQaZaU1d51wb7ugetrn0k3OuHblxtE/O0L4HEC9s4X5zmFJt0vLh0w==} + engines: {node: '>=12'} + peerDependencies: + vite: '>=6.0.0' - '@tanstack/start-plugin-core@1.139.8': - resolution: {integrity: sha512-u1+rof/1vNHzFVR0yPWWSVwzbCtvndQsfjBR104xSTLCLB0oGvFvkCU0xLLyKtxhqsrYZFrqudg5B8aVH2plOg==} + '@tanstack/start-plugin-core@1.141.1': + resolution: {integrity: sha512-jXfgKeM4XX7aoP7WF/cJZH52N0ewIYRdP028ItXaSwUMcDyBO+PPTNbSEqTu3zbzLsmTOOyuJEpIrVA0/lmbjA==} engines: {node: '>=22.12.0'} peerDependencies: vite: '>=7.0.0' - '@tanstack/start-server-core@1.139.10': - resolution: {integrity: sha512-7xMuHFVyqGf++87CrGrhlHE4AO1ltbhf0Cyrj5oe2c0i5c9YYRW95lo+av72IN+xgysyC/yBm8/RyP8UEaTSEA==} - engines: {node: '>=22.12.0'} + '@tanstack/start-server-core@1.131.50': + resolution: {integrity: sha512-3SWwwhW2GKMhPSaqWRal6Jj1Y9ObfdWEXKFQid1LBuk5xk/Es4bmW68o++MbVgs/GxUxyeZ3TRVqb0c7RG1sog==} + engines: {node: '>=12'} - '@tanstack/start-server-core@1.139.8': - resolution: {integrity: sha512-jKC83uMS2kgCHoqlHmxh9hAK1pN9Wd8l+Lhkibwp9PKKMW4Z1bxy5xCx6sr3TD2yJEOP25SRhYMrtAKmrLmYGA==} + '@tanstack/start-server-core@1.141.1': + resolution: {integrity: sha512-Qk/lZ/+iGUyNYeAAuj89bLR6GXLD/9BIpAR2CUwlS+xXGL0kQmOFcb1UvccWZ2QwtW+csxJW4NeQOeMuqsfyhA==} engines: {node: '>=22.12.0'} - '@tanstack/start-storage-context@1.139.10': - resolution: {integrity: sha512-nBAH4QAIdVhxfrbiEU8wuIQfozfPPhb989yAhLgntE8lkHhLLAmyHPGt6oWEWbVNZ1eka5QDm2J0d7Z7K7b2Og==} - engines: {node: '>=22.12.0'} + '@tanstack/start-server-functions-client@1.131.50': + resolution: {integrity: sha512-4aM17fFdVAFH6uLPswKJxzrhhIjcCwKqzfTcgY3OnhUKnaZBTQwJA+nUHQCI6IWvEvrcrNVtFTtv13TkDk3YMw==} + engines: {node: '>=12'} + + '@tanstack/start-server-functions-fetcher@1.131.50': + resolution: {integrity: sha512-yeZekr84BkyLaNaZ4llKbDBb+CJPVESP881iJijP++SuRmvetivUs75KiV9VFIf7MhdefICmRcCdff/KbK5QnQ==} + engines: {node: '>=12'} + + '@tanstack/start-server-functions-handler@1.120.19': + resolution: {integrity: sha512-Ow8HkNieoqHumD3QK4YUDIhzBtFX9mMEDrxFYtbVBgxP1C9Rm/YDuwnUNP49q1tTOZ22Bs4wSDjBXvu+OgSSfA==} + engines: {node: '>=12'} + + '@tanstack/start-server-functions-server@1.131.2': + resolution: {integrity: sha512-u67d6XspczlC/dYki/Id28oWsTjkZMJhDqO4E23U3rHs8eYgxvMBHKqdeqWgOyC+QWT9k6ze1pJmbv+rmc3wOQ==} + engines: {node: '>=12'} - '@tanstack/start-storage-context@1.139.7': - resolution: {integrity: sha512-l2utb0CXLE+wfj1wlAUPHohiq7n5nOBMl3pflhl3JzCXt+6D9AAogkfrysyxOAvx3KnLh3oG+qwf1KHWIDB9HA==} + '@tanstack/start-server-functions-ssr@1.120.19': + resolution: {integrity: sha512-D4HGvJXWvVUssgkLDtdSJTFfWuT+nVv9GauPfVQTtMUUy+NbExNkFWKvF+XvCS81lBqnCKL7VrWqZMXiod0gTA==} + engines: {node: '>=12'} + + '@tanstack/start-storage-context@1.131.50': + resolution: {integrity: sha512-qbVFdx/B5URJXzWjguaiCcQhJw2NL8qFGtSzLSGilxQnvtJdM+V9VBMizKIxhm9oiYnfqGsVfyMOBD7q9f8Y1Q==} + engines: {node: '>=12'} + + '@tanstack/start-storage-context@1.141.1': + resolution: {integrity: sha512-UPOQd4qsytgmc+pHeeS3oIZQazhyGAmEaCS/IrZI42TzpuVh2ZbLVssKEoDziheNP1dH5KT2lsL1bU9asAw7tA==} engines: {node: '>=22.12.0'} + '@tanstack/start@1.120.20': + resolution: {integrity: sha512-fQO+O/5xJpli5KlV6pwDz6DtpbqO/0atdVSyVnkemzk0Mej9azm4HXtw+cKkIPtsSplWs4B1EbMtgGMb9ADhSA==} + engines: {node: '>=12'} + + '@tanstack/store@0.7.7': + resolution: {integrity: sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ==} + '@tanstack/store@0.8.0': resolution: {integrity: sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ==} @@ -2954,8 +3428,12 @@ packages: resolution: {integrity: sha512-frgA1vjzxbdU5/xn/Z/UqyOd1yuegEfAnx9QNbcX+1XQ3TCzD+x89cMZH9iyxdTC1Tasx2gq7DCNCvX962X9WA==} engines: {node: '>=18'} - '@tanstack/virtual-file-routes@1.139.0': - resolution: {integrity: sha512-9PImF1d1tovTUIpjFVa0W7Fwj/MHif7BaaczgJJfbv3sDt1Gh+oW9W9uCw9M3ndEJynnp5ZD/TTs0RGubH5ssg==} + '@tanstack/virtual-file-routes@1.131.2': + resolution: {integrity: sha512-VEEOxc4mvyu67O+Bl0APtYjwcNRcL9it9B4HKbNgcBTIOEalhk+ufBl4kiqc8WP1sx1+NAaiS+3CcJBhrqaSRg==} + engines: {node: '>=12'} + + '@tanstack/virtual-file-routes@1.141.0': + resolution: {integrity: sha512-CJrWtr6L9TVzEImm9S7dQINx+xJcYP/aDkIi6gnaWtIgbZs1pnzsE0yJc2noqXZ+yAOqLx3TBGpBEs9tS0P9/A==} engines: {node: '>=12'} '@tanstack/vite-config@0.4.1': @@ -2966,10 +3444,6 @@ packages: resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} - '@testing-library/jest-dom@6.9.1': - resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} - engines: {node: '>=14', npm: '>=6', yarn: '>=1'} - '@testing-library/react@16.3.0': resolution: {integrity: sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==} engines: {node: '>=18'} @@ -2997,6 +3471,9 @@ packages: '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + '@types/babel__code-frame@7.0.6': + resolution: {integrity: sha512-Anitqkl3+KrzcW2k77lRlg/GfLZLWXBuNgbEcIOU6M92yw42vsd3xV/Z/yAHEj8m+KUjL6bWOVOFqX8PFPJ4LA==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -3009,6 +3486,9 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/braces@3.0.5': + resolution: {integrity: sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -3036,17 +3516,23 @@ packages: '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + '@types/micromatch@4.0.10': + resolution: {integrity: sha512-5jOhFDElqr4DKTrTEbnW8DZ4Hz5LRUEmyrGpCMrD/NphYv3nUnaF08xmSLx1rGGnyEs/kFnhiw6dCgcDqMr5PQ==} + '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} - '@types/node@20.19.25': - resolution: {integrity: sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==} + '@types/node@20.19.26': + resolution: {integrity: sha512-0l6cjgF0XnihUpndDhk+nyD3exio3iKaYROSgvh/qSevPXax3L8p5DBRFjbvalnwatGgHEQn2R88y2fA3g4irg==} + + '@types/node@24.10.3': + resolution: {integrity: sha512-gqkrWUsS8hcm0r44yn7/xZeV1ERva/nLgrLxFRUGb7aoNMIJfZJ3AC261zDQuOAKC7MiXai1WCpYc48jAHoShQ==} - '@types/node@24.10.1': - resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} + '@types/node@25.0.1': + resolution: {integrity: sha512-czWPzKIAXucn9PtsttxmumiQ9N0ok9FrBwgRWrwmVLlp86BrMExzvXRLFYRJ+Ex3g6yqj+KuaxfX1JTgV2lpfg==} '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} @@ -3071,100 +3557,63 @@ packages: '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - '@typescript-eslint/eslint-plugin@8.46.3': - resolution: {integrity: sha512-sbaQ27XBUopBkRiuY/P9sWGOWUW4rl8fDoHIUmLpZd8uldsTyB4/Zg6bWTegPoTLnKj9Hqgn3QD6cjPNB32Odw==} + '@typescript-eslint/eslint-plugin@8.49.0': + resolution: {integrity: sha512-JXij0vzIaTtCwu6SxTh8qBc66kmf1xs7pI4UOiMDFVct6q86G0Zs7KRcEoJgY3Cav3x5Tq0MF5jwgpgLqgKG3A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.46.3 + '@typescript-eslint/parser': ^8.49.0 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.46.3': - resolution: {integrity: sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==} + '@typescript-eslint/parser@8.49.0': + resolution: {integrity: sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.46.3': - resolution: {integrity: sha512-Fz8yFXsp2wDFeUElO88S9n4w1I4CWDTXDqDr9gYvZgUpwXQqmZBr9+NTTql5R3J7+hrJZPdpiWaB9VNhAKYLuQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/project-service@8.48.0': - resolution: {integrity: sha512-Ne4CTZyRh1BecBf84siv42wv5vQvVmgtk8AuiEffKTUo3DrBaGYZueJSxxBZ8fjk/N3DrgChH4TOdIOwOwiqqw==} + '@typescript-eslint/project-service@8.49.0': + resolution: {integrity: sha512-/wJN0/DKkmRUMXjZUXYZpD1NEQzQAAn9QWfGwo+Ai8gnzqH7tvqS7oNVdTjKqOcPyVIdZdyCMoqN66Ia789e7g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.46.3': - resolution: {integrity: sha512-FCi7Y1zgrmxp3DfWfr+3m9ansUUFoy8dkEdeQSgA9gbm8DaHYvZCdkFRQrtKiedFf3Ha6VmoqoAaP68+i+22kg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/scope-manager@8.48.0': - resolution: {integrity: sha512-uGSSsbrtJrLduti0Q1Q9+BF1/iFKaxGoQwjWOIVNJv0o6omrdyR8ct37m4xIl5Zzpkp69Kkmvom7QFTtue89YQ==} + '@typescript-eslint/scope-manager@8.49.0': + resolution: {integrity: sha512-npgS3zi+/30KSOkXNs0LQXtsg9ekZ8OISAOLGWA/ZOEn0ZH74Ginfl7foziV8DT+D98WfQ5Kopwqb/PZOaIJGg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.46.3': - resolution: {integrity: sha512-GLupljMniHNIROP0zE7nCcybptolcH8QZfXOpCfhQDAdwJ/ZTlcaBOYebSOZotpti/3HrHSw7D3PZm75gYFsOA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/tsconfig-utils@8.48.0': - resolution: {integrity: sha512-WNebjBdFdyu10sR1M4OXTt2OkMd5KWIL+LLfeH9KhgP+jzfDV/LI3eXzwJ1s9+Yc0Kzo2fQCdY/OpdusCMmh6w==} + '@typescript-eslint/tsconfig-utils@8.49.0': + resolution: {integrity: sha512-8prixNi1/6nawsRYxet4YOhnbW+W9FK/bQPxsGB1D3ZrDzbJ5FXw5XmzxZv82X3B+ZccuSxo/X8q9nQ+mFecWA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.46.3': - resolution: {integrity: sha512-ZPCADbr+qfz3aiTTYNNkCbUt+cjNwI/5McyANNrFBpVxPt7GqpEYz5ZfdwuFyGUnJ9FdDXbGODUu6iRCI6XRXw==} + '@typescript-eslint/type-utils@8.49.0': + resolution: {integrity: sha512-KTExJfQ+svY8I10P4HdxKzWsvtVnsuCifU5MvXrRwoP2KOlNZ9ADNEWWsQTJgMxLzS5VLQKDjkCT/YzgsnqmZg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.46.3': - resolution: {integrity: sha512-G7Ok9WN/ggW7e/tOf8TQYMaxgID3Iujn231hfi0Pc7ZheztIJVpO44ekY00b7akqc6nZcvregk0Jpah3kep6hA==} + '@typescript-eslint/types@8.49.0': + resolution: {integrity: sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/types@8.48.0': - resolution: {integrity: sha512-cQMcGQQH7kwKoVswD1xdOytxQR60MWKM1di26xSUtxehaDs/32Zpqsu5WJlXTtTTqyAVK8R7hvsUnIXRS+bjvA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/typescript-estree@8.46.3': - resolution: {integrity: sha512-f/NvtRjOm80BtNM5OQtlaBdM5BRFUv7gf381j9wygDNL+qOYSNOgtQ/DCndiYi80iIOv76QqaTmp4fa9hwI0OA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/typescript-estree@8.48.0': - resolution: {integrity: sha512-ljHab1CSO4rGrQIAyizUS6UGHHCiAYhbfcIZ1zVJr5nMryxlXMVWS3duFPSKvSUbFPwkXMFk1k0EMIjub4sRRQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/utils@8.46.3': - resolution: {integrity: sha512-VXw7qmdkucEx9WkmR3ld/u6VhRyKeiF1uxWwCy/iuNfokjJ7VhsgLSOTjsol8BunSw190zABzpwdNsze2Kpo4g==} + '@typescript-eslint/typescript-estree@8.49.0': + resolution: {integrity: sha512-jrLdRuAbPfPIdYNppHJ/D0wN+wwNfJ32YTAm10eJVsFmrVpXQnDWBn8niCSMlWjvml8jsce5E/O+86IQtTbJWA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.48.0': - resolution: {integrity: sha512-yTJO1XuGxCsSfIVt1+1UrLHtue8xz16V8apzPYI06W0HbEbEWHxHXgZaAgavIkoh+GeV6hKKd5jm0sS6OYxWXQ==} + '@typescript-eslint/utils@8.49.0': + resolution: {integrity: sha512-N3W7rJw7Rw+z1tRsHZbK395TWSYvufBXumYtEGzypgMUthlg0/hmCImeA8hgO2d2G4pd7ftpxxul2J8OdtdaFA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.46.3': - resolution: {integrity: sha512-uk574k8IU0rOF/AjniX8qbLSGURJVUCeM5e4MIMKBFFi8weeiLrG1fyQejyLXQpRZbU/1BuQasleV/RfHC3hHg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/visitor-keys@8.48.0': - resolution: {integrity: sha512-T0XJMaRPOH3+LBbAfzR2jalckP1MSG/L9eUtY0DEzUyVaXJ/t6zN0nR7co5kz0Jko/nkSYCBRkz1djvjajVTTg==} + '@typescript-eslint/visitor-keys@8.49.0': + resolution: {integrity: sha512-LlKaciDe3GmZFphXIc79THF/YYBugZ7FS1pO581E/edlVVNbZKDy93evqmrfQ9/Y4uN0vVhX4iuchq26mK/iiA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@ungap/structured-clone@1.3.0': @@ -3265,13 +3714,23 @@ packages: cpu: [x64] os: [win32] - '@vercel/nft@0.30.3': - resolution: {integrity: sha512-UEq+eF0ocEf9WQCV1gktxKhha36KDs7jln5qii6UpPf5clMqDc0p3E7d9l2Smx0i9Pm1qpq4S4lLfNl97bbv6w==} + '@vercel/nft@0.30.4': + resolution: {integrity: sha512-wE6eAGSXScra60N2l6jWvNtVK0m+sh873CpfZW4KI2v8EHuUQp+mSEi4T+IcdPCSEDgCdAS/7bizbhQlkjzrSA==} engines: {node: '>=18'} hasBin: true - '@vitejs/plugin-react@5.1.1': - resolution: {integrity: sha512-WQfkSw0QbQ5aJ2CHYw23ZGkqnRwqKHD/KYsMeTkZzPT4Jcf0DcBxBtwMJxnu6E7oxw5+JC6ZAiePgh28uJ1HBA==} + '@vinxi/listhen@1.5.6': + resolution: {integrity: sha512-WSN1z931BtasZJlgPp704zJFnQFRg7yzSjkm3MzAWQYe4uXFXlFr1hc5Ac2zae5/HDOz5x1/zDM5Cb54vTCnWw==} + hasBin: true + + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + '@vitejs/plugin-react@5.1.2': + resolution: {integrity: sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 @@ -3283,11 +3742,11 @@ packages: vite: ^5.0.0 || ^6.0.0 vue: ^3.2.25 - '@vitejs/plugin-vue@6.0.2': - resolution: {integrity: sha512-iHmwV3QcVGGvSC1BG5bZ4z6iwa1SOpAPWmnjOErd4Ske+lZua5K9TtAVdx0gMBClJ28DViCbSmZitjWZsWO3LA==} + '@vitejs/plugin-vue@6.0.3': + resolution: {integrity: sha512-TlGPkLFLVOY3T7fZrwdvKpjprR3s4fxRln0ORDo1VQ7HHyxJwTlrjKU3kpVWTlaAjIEuCTokmjkZnr8Tpc925w==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: - vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 vue: ^3.2.25 '@vitest/coverage-v8@4.0.14': @@ -3299,11 +3758,11 @@ packages: '@vitest/browser': optional: true - '@vitest/expect@4.0.14': - resolution: {integrity: sha512-RHk63V3zvRiYOWAV0rGEBRO820ce17hz7cI2kDmEdfQsBjT2luEKB5tCOc91u1oSQoUOZkSv3ZyzkdkSLD7lKw==} + '@vitest/expect@4.0.15': + resolution: {integrity: sha512-Gfyva9/GxPAWXIWjyGDli9O+waHDC0Q0jaLdFP1qPAUUfo1FEXPXUfUkp3eZA0sSq340vPycSyOlYUeM15Ft1w==} - '@vitest/mocker@4.0.14': - resolution: {integrity: sha512-RzS5NujlCzeRPF1MK7MXLiEFpkIXeMdQ+rN3Kk3tDI9j0mtbr7Nmuq67tpkOJQpgyClbOltCXMjLZicJHsH5Cg==} + '@vitest/mocker@4.0.15': + resolution: {integrity: sha512-CZ28GLfOEIFkvCFngN8Sfx5h+Se0zN+h4B7yOsPVCcgtiO7t5jt9xQh2E1UkFep+eb9fjyMfuC5gBypwb07fvQ==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0-0 @@ -3316,45 +3775,45 @@ packages: '@vitest/pretty-format@4.0.14': resolution: {integrity: sha512-SOYPgujB6TITcJxgd3wmsLl+wZv+fy3av2PpiPpsWPZ6J1ySUYfScfpIt2Yv56ShJXR2MOA6q2KjKHN4EpdyRQ==} - '@vitest/runner@4.0.14': - resolution: {integrity: sha512-BsAIk3FAqxICqREbX8SetIteT8PiaUL/tgJjmhxJhCsigmzzH8xeadtp7LRnTpCVzvf0ib9BgAfKJHuhNllKLw==} + '@vitest/pretty-format@4.0.15': + resolution: {integrity: sha512-SWdqR8vEv83WtZcrfLNqlqeQXlQLh2iilO1Wk1gv4eiHKjEzvgHb2OVc3mIPyhZE6F+CtfYjNlDJwP5MN6Km7A==} + + '@vitest/runner@4.0.15': + resolution: {integrity: sha512-+A+yMY8dGixUhHmNdPUxOh0la6uVzun86vAbuMT3hIDxMrAOmn5ILBHm8ajrqHE0t8R9T1dGnde1A5DTnmi3qw==} - '@vitest/snapshot@4.0.14': - resolution: {integrity: sha512-aQVBfT1PMzDSA16Y3Fp45a0q8nKexx6N5Amw3MX55BeTeZpoC08fGqEZqVmPcqN0ueZsuUQ9rriPMhZ3Mu19Ag==} + '@vitest/snapshot@4.0.15': + resolution: {integrity: sha512-A7Ob8EdFZJIBjLjeO0DZF4lqR6U7Ydi5/5LIZ0xcI+23lYlsYJAfGn8PrIWTYdZQRNnSRlzhg0zyGu37mVdy5g==} - '@vitest/spy@4.0.14': - resolution: {integrity: sha512-JmAZT1UtZooO0tpY3GRyiC/8W7dCs05UOq9rfsUUgEZEdq+DuHLmWhPsrTt0TiW7WYeL/hXpaE07AZ2RCk44hg==} + '@vitest/spy@4.0.15': + resolution: {integrity: sha512-+EIjOJmnY6mIfdXtE/bnozKEvTC4Uczg19yeZ2vtCz5Yyb0QQ31QWVQ8hswJ3Ysx/K2EqaNsVanjr//2+P3FHw==} '@vitest/utils@4.0.14': resolution: {integrity: sha512-hLqXZKAWNg8pI+SQXyXxWCTOpA3MvsqcbVeNgSi8x/CSN2wi26dSzn1wrOhmCmFjEvN9p8/kLFRHa6PI8jHazw==} + '@vitest/utils@4.0.15': + resolution: {integrity: sha512-HXjPW2w5dxhTD0dLwtYHDnelK3j8sR8cWIaLxr22evTyY6q8pRCjZSmhRWVjBaOVXChQd6AwMzi9pucorXCPZA==} + '@volar/language-core@2.4.15': resolution: {integrity: sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==} - '@volar/language-core@2.4.23': - resolution: {integrity: sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==} + '@volar/language-core@2.4.27': + resolution: {integrity: sha512-DjmjBWZ4tJKxfNC1F6HyYERNHPYS7L7OPFyCrestykNdUZMFYzI9WTyvwPcaNaHlrEUwESHYsfEw3isInncZxQ==} '@volar/source-map@2.4.15': resolution: {integrity: sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==} - '@volar/source-map@2.4.23': - resolution: {integrity: sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==} + '@volar/source-map@2.4.27': + resolution: {integrity: sha512-ynlcBReMgOZj2i6po+qVswtDUeeBRCTgDurjMGShbm8WYZgJ0PA4RmtebBJ0BCYol1qPv3GQF6jK7C9qoVc7lg==} '@volar/typescript@2.4.15': resolution: {integrity: sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==} - '@volar/typescript@2.4.23': - resolution: {integrity: sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==} - - '@vue/compiler-core@3.5.24': - resolution: {integrity: sha512-eDl5H57AOpNakGNAkFDH+y7kTqrQpJkZFXhWZQGyx/5Wh7B1uQYvcWkvZi11BDhscPgj8N7XV3oRwiPnx1Vrig==} + '@volar/typescript@2.4.27': + resolution: {integrity: sha512-eWaYCcl/uAPInSK2Lze6IqVWaBu/itVqR5InXcHXFyles4zO++Mglt3oxdgj75BDcv1Knr9Y93nowS8U3wqhxg==} '@vue/compiler-core@3.5.25': resolution: {integrity: sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw==} - '@vue/compiler-dom@3.5.24': - resolution: {integrity: sha512-1QHGAvs53gXkWdd3ZMGYuvQFXHW4ksKWPG8HP8/2BscrbZ0brw183q2oNWjMrSWImYLHxHrx1ItBQr50I/q2zw==} - '@vue/compiler-dom@3.5.25': resolution: {integrity: sha512-4We0OAcMZsKgYoGlMjzYvaoErltdFI2/25wqanuTu+S4gismOTRTBPi4IASOjxWdzIwrYSjnqONfKvuqkXzE2Q==} @@ -3400,9 +3859,6 @@ packages: peerDependencies: vue: 3.5.25 - '@vue/shared@3.5.24': - resolution: {integrity: sha512-9cwHL2EsJBdi8NY22pngYYWzkTDhld6fAD6jlaeloNGciNSJL6bLpbxVgXl96X00Jtc6YWQv96YA/0sxex/k1A==} - '@vue/shared@3.5.25': resolution: {integrity: sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==} @@ -3479,6 +3935,9 @@ packages: alien-signals@1.0.13: resolution: {integrity: sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==} + ansi-align@3.0.1: + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -3605,8 +4064,8 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - bare-events@2.8.1: - resolution: {integrity: sha512-oxSAxTS1hRfnyit2CL5QpAOS5ixfBjj6ex3yTNvXyY/kE719jQ/IjuESJBK2w5v4wwQRAHGseVJXx9QBYOtFGQ==} + bare-events@2.8.2: + resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==} peerDependencies: bare-abort-controller: '*' peerDependenciesMeta: @@ -3616,8 +4075,8 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.8.22: - resolution: {integrity: sha512-/tk9kky/d8T8CTXIQYASLyhAxR5VwL3zct1oAoVTaOUHwrmsGnfbRwNdEq+vOl2BN8i3PcDdP0o4Q+jjKQoFbQ==} + baseline-browser-mapping@2.9.7: + resolution: {integrity: sha512-k9xFKplee6KIio3IDbwj+uaCLpqzOwakOgmqzPezM0sFJlFKcg30vk2wOiAJtkTSfx0SSQDSe8q+mWA/fSH5Zg==} hasBin: true better-path-resolve@1.0.0: @@ -3650,6 +4109,10 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + boxen@7.1.1: + resolution: {integrity: sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==} + engines: {node: '>=14.16'} + brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -3660,8 +4123,8 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.27.0: - resolution: {integrity: sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==} + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -3685,10 +4148,10 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} - c12@3.3.1: - resolution: {integrity: sha512-LcWQ01LT9tkoUINHgpIOv3mMs+Abv7oVCrtpMRi1PaapVEpWoMga5WuT7/DqFTu7URP9ftbOmimNw1KNIGh9DQ==} + c12@3.3.2: + resolution: {integrity: sha512-QkikB2X5voO1okL3QsES0N690Sn/K9WokXqUsDQsWy5SnYb+psYQFGA10iy1bZHj3fjISKsI67Q90gruvWWM3A==} peerDependencies: - magicast: ^0.3.5 + magicast: '*' peerDependenciesMeta: magicast: optional: true @@ -3709,8 +4172,12 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - caniuse-lite@1.0.30001759: - resolution: {integrity: sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==} + camelcase@7.0.1: + resolution: {integrity: sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==} + engines: {node: '>=14.16'} + + caniuse-lite@1.0.30001760: + resolution: {integrity: sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==} capnweb@0.1.0: resolution: {integrity: sha512-+pygKx1JFTZTRdd1hHgaBRg5BwULEDZq8ZoHXkYP2GXNV3lrjXLj5qzlGz+SgBCJjWUmNBtlh7JPWdr0wIbY8w==} @@ -3775,6 +4242,10 @@ packages: citty@0.1.6: resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + cli-boxes@3.0.0: + resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} + engines: {node: '>=10'} + cli-cursor@3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} @@ -3821,6 +4292,10 @@ packages: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} + commander@13.1.0: + resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} + engines: {node: '>=18'} + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -3894,10 +4369,6 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} - cookie@1.0.2: - resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} - engines: {node: '>=18'} - core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -3932,11 +4403,8 @@ packages: resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} engines: {node: '>= 6'} - css.escape@1.5.1: - resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} - - cssstyle@5.3.3: - resolution: {integrity: sha512-OytmFH+13/QXONJcC75QNdMtKpceNk3u8ThBjyyYjkEcy/ekBwR1mMAuNvi3gdBPW3N5TlCzQ0WZw8H0lN/bDw==} + cssstyle@5.3.4: + resolution: {integrity: sha512-KyOS/kJMEq5O9GdPnaf82noigg5X5DYn0kZPJTaAsCUaBizp6Xa1y9D4Qoqf/JazEXWuruErHgVXwjN5391ZJw==} engines: {node: '>=20'} csstype@3.2.3: @@ -3953,6 +4421,9 @@ packages: dataloader@1.4.0: resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==} + dax-sh@0.39.2: + resolution: {integrity: sha512-gpuGEkBQM+5y6p4cWaw9+ePy5TNon+fdwFVtTI8leU3UhwhsBfPewRxMXGuQNC+M2b/MDGMlfgpqynkcd0C3FQ==} + db0@0.3.4: resolution: {integrity: sha512-RiXXi4WaNzPTHEOu8UPQKMooIbqOEyqA1t7Z6MsdxSCeb8iUC9ko3LcmsLmeUt2SM5bctfArZKkRQggKZz7JNw==} peerDependencies: @@ -3979,6 +4450,14 @@ packages: de-indent@1.0.2: resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -4033,6 +4512,10 @@ packages: destr@2.0.5: resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} @@ -4046,8 +4529,8 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - devalue@5.5.0: - resolution: {integrity: sha512-69sM5yrHfFLJt0AZ9QqZXGCPfJ7fQjvpln3Rq5+PS03LD32Ost1Q9N+eEnaQwGRIriKkMImXD56ocjQmfjbV3w==} + devalue@5.6.1: + resolution: {integrity: sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A==} devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -4063,9 +4546,6 @@ packages: dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} - dom-accessibility-api@0.6.3: - resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} - dom-serializer@2.0.0: resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} @@ -4129,8 +4609,8 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.244: - resolution: {integrity: sha512-OszpBN7xZX4vWMPJwB9illkN/znA8M36GQqQxi6MNy9axWxhOfJyZZJtSLQCpEFLHP2xK33BiWx9aIuIEXVCcw==} + electron-to-chromium@1.5.267: + resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -4142,6 +4622,10 @@ packages: resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} engines: {node: '>=14'} + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} @@ -4152,8 +4636,8 @@ packages: end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} - enhanced-resolve@5.18.3: - resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} + enhanced-resolve@5.18.4: + resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} engines: {node: '>=10.13.0'} enquirer@2.3.6: @@ -4194,11 +4678,21 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} + esbuild@0.20.2: + resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==} + engines: {node: '>=12'} + hasBin: true + esbuild@0.25.12: resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} hasBin: true + esbuild@0.27.1: + resolution: {integrity: sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -4312,8 +4806,8 @@ packages: resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} engines: {node: '>=0.10'} - esrap@2.2.0: - resolution: {integrity: sha512-WBmtxe7R9C5mvL4n2le8nMUe4mD5V9oiK2vJpQ9I3y20ENPUomPcphBXE8D1x/Bm84oN1V+lOfgXxtqmxTp3Xg==} + esrap@2.2.1: + resolution: {integrity: sha512-GiYWG34AN/4CUyaWAgunGt0Rxvr1PTMlGC0vvEov/uOQYWne2bpN03Um+k8jT+q3op33mKouP2zeJ6OlM+qeUg==} esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} @@ -4344,6 +4838,9 @@ packages: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + events-universal@1.0.1: resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} @@ -4355,16 +4852,16 @@ packages: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} - expect-type@1.2.2: - resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} express@5.2.1: resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} - exsolve@1.0.7: - resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} + exsolve@1.0.8: + resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -4461,8 +4958,8 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} - form-data@4.0.4: - resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} formatly@0.3.0: @@ -4481,6 +4978,10 @@ packages: fraction.js@5.3.4: resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + fresh@2.0.0: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} @@ -4558,8 +5059,8 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - glob@10.4.5: - resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} hasBin: true globals@14.0.0: @@ -4605,9 +5106,6 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - gtoken@8.0.0: resolution: {integrity: sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==} engines: {node: '>=18'} @@ -4616,6 +5114,9 @@ packages: resolution: {integrity: sha512-O1Ld7Dr+nqPnmGpdhzLmMTQ4vAsD+rHwMm1NLUmoUFFymBOMKxCCrtDxqdBRYXdeEPEi3SyoR4TizJLQrnKBNA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + h3@1.13.0: + resolution: {integrity: sha512-vFEAu/yf8UMUcB4s43OaDaigcqpQd14yanmOsn+NcRX3/guSKncyE2rOYhq8RIchgJrPSs/QiIddnTTR1ddiAg==} + h3@1.15.4: resolution: {integrity: sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==} @@ -4628,8 +5129,8 @@ packages: crossws: optional: true - happy-dom@20.0.10: - resolution: {integrity: sha512-6umCCHcjQrhP5oXhrHQQvLB0bwb1UzHAHdsXy+FjtKoYjUhmNZsQL8NivwM1vDvNEChJabVrUYxUnp/ZdYmy2g==} + happy-dom@20.0.11: + resolution: {integrity: sha512-QsCdAUHAmiDeKeaNojb1OHOPF7NjcWPBR7obdu3NwH2a/oyQaLg5d0aaCy/9My6CdPChYF07dvz5chaXBGaD4g==} engines: {node: '>=20.0.0'} has-flag@4.0.0: @@ -4666,8 +5167,8 @@ packages: hast-util-to-jsx-runtime@2.3.6: resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} - hast-util-to-parse5@8.0.0: - resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==} + hast-util-to-parse5@8.0.1: + resolution: {integrity: sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==} hast-util-to-text@4.0.2: resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==} @@ -4727,6 +5228,10 @@ packages: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} + http-proxy@1.18.1: + resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} + engines: {node: '>=8.0.0'} + http-shutdown@1.2.2: resolution: {integrity: sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} @@ -4738,8 +5243,8 @@ packages: httpxy@0.1.7: resolution: {integrity: sha512-pXNx8gnANKAndgga5ahefxc++tJvNL87CXoRwxn1cJE2ZkWEojF3tNfQIEhZX/vfpt+wzeAzpUI4qkediX1MLQ==} - human-id@4.1.2: - resolution: {integrity: sha512-v/J+4Z/1eIJovEBdlV5TYj1IR+ZiohcYGRY+qN/oC9dAfKzVT023N/Bgw37hrKCoVRBvk3bqyzpr2PP5YeTMSg==} + human-id@4.1.3: + resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==} hasBin: true human-signals@5.0.0: @@ -4750,8 +5255,8 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} - iconv-lite@0.7.0: - resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} + iconv-lite@0.7.1: + resolution: {integrity: sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==} engines: {node: '>=0.10.0'} ieee754@1.2.1: @@ -4776,18 +5281,14 @@ packages: import-meta-resolve@4.2.0: resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} - import-without-cache@0.2.2: - resolution: {integrity: sha512-4TTuRrZ0jBULXzac3EoX9ZviOs8Wn9iAbNhJEyLhTpAGF9eNmYSruaMMN/Tec/yqaO7H6yS2kALfQDJ5FxfatA==} + import-without-cache@0.2.3: + resolution: {integrity: sha512-roCvX171VqJ7+7pQt1kSRfwaJvFAC2zhThJWXal1rN8EqzPS3iapkAoNpHh4lM8Na1BDen+n9rVfo73RN+Y87g==} engines: {node: '>=20.19.0'} imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} - indent-string@4.0.0: - resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} - engines: {node: '>=8'} - inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -4797,8 +5298,8 @@ packages: inline-style-parser@0.1.1: resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==} - inline-style-parser@0.2.4: - resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} ioredis@5.8.2: resolution: {integrity: sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==} @@ -4928,13 +5429,17 @@ packages: isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - isbot@5.1.31: - resolution: {integrity: sha512-DPgQshehErHAqSCKDb3rNW03pa2wS/v5evvUqtxt6TTnHRqAG8FdzcSSJs9656pK6Y+NT7K9R4acEYXLHYfpUQ==} + isbot@5.1.32: + resolution: {integrity: sha512-VNfjM73zz2IBZmdShMfAUg10prm6t7HFUQmNAEOAVS4YH92ZrZcvkMcGX6cIgBJAzWDzPent/EeAtYEHNPNPBQ==} engines: {node: '>=18'} isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isexe@3.1.1: + resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} + engines: {node: '>=16'} + istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -4958,6 +5463,10 @@ packages: resolution: {integrity: sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + hasBin: true + jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true @@ -4984,12 +5493,16 @@ packages: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} hasBin: true + js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + hasBin: true + js-yaml@4.1.1: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true - jsdom@27.2.0: - resolution: {integrity: sha512-454TI39PeRDW1LgpyLPyURtB4Zx1tklSr6+OFOipsxGUH1WMTvk6C65JQdrj455+DP2uJ1+veBEHTGFKWVLFoA==} + jsdom@27.3.0: + resolution: {integrity: sha512-GtldT42B8+jefDUC4yUKAvsaOrH7PDHmZxZXNgF2xMmymjUbRYJvpAybZAKEmXDGTM0mCsz8duOa4vTm5AY2Kg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} peerDependencies: canvas: ^3.0.0 @@ -5035,8 +5548,8 @@ packages: jwa@2.0.1: resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} - jws@4.0.0: - resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==} + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} kebab-case@1.0.2: resolution: {integrity: sha512-7n6wXq4gNgBELfDCpzKc+mRrZFs7D+wgfF5WRFLNAr4DA/qtr9Js8uOAVAfHhuLMfAcQ0pRKqbpjx+TcJVdE1Q==} @@ -5052,16 +5565,16 @@ packages: resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} engines: {node: '>= 8'} - knip@5.70.2: - resolution: {integrity: sha512-LI7DbeVnk7h9+FAet5KzzHNdDwJyqDa2+cn4uQfZYTfpuVjEqtGmYD9r5b9JEuOs4eVkf/7sskNhWXxELm3C/Q==} + knip@5.73.4: + resolution: {integrity: sha512-q0DDgqsRMa4z2IMEPEblns0igitG8Fu7exkvEgQx1QMLKEqSvcvKP9fMk+C1Ehy+Ux6oayl6zfAEGt6DvFtidw==} engines: {node: '>=18.18.0'} hasBin: true peerDependencies: '@types/node': '>=18' typescript: '>=5.0.4 <7' - knitwork@1.2.0: - resolution: {integrity: sha512-xYSH7AvuQ6nXkq42x0v5S8/Iry+cfulBz/DJQzhIyESdLD7425jXsPy4vn5cCXU+HhRN2kVw51Vd1K6/By4BQg==} + knitwork@1.3.0: + resolution: {integrity: sha512-4LqMNoONzR43B1W0ek0fhXMsDNW/zxa1NdFAVMY+k28pgZLovR4G3PB5MrpTxCy1QaZCqNoiaKPr5w5qZHfSNw==} known-css-properties@0.30.0: resolution: {integrity: sha512-VSWXYUnsPu9+WYKkfmJyLKtIvaRJi1kXUqVmBACORXZQxT5oZDsoZ2vQP+bQFDnWtpI/4eq3MLoRMjI2fnLzTQ==} @@ -5208,8 +5721,8 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.2.2: - resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==} + lru-cache@11.2.4: + resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} engines: {node: 20 || >=22} lru-cache@5.1.1: @@ -5219,8 +5732,8 @@ packages: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} - lucide-react@0.555.0: - resolution: {integrity: sha512-D8FvHUGbxWBRQM90NZeIyhAvkFfsh3u9ekrMvJ30Z6gnpBHS6HC6ldLg7tL45hwiIz/u66eKDtdA23gwwGsAHA==} + lucide-react@0.561.0: + resolution: {integrity: sha512-Y59gMY38tl4/i0qewcqohPdEbieBy7SovpBL9IFebhc2mDd8x4PZSOsiFRkpPcOq6bj1r/mjH/Rk73gSlIJP2A==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -5316,8 +5829,8 @@ packages: mdast-util-phrasing@4.1.0: resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} - mdast-util-to-hast@13.2.0: - resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} mdast-util-to-markdown@2.1.2: resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} @@ -5450,9 +5963,14 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} - mime-types@3.0.1: - resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} - engines: {node: '>= 0.6'} + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true mime@3.0.0: resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} @@ -5472,9 +5990,9 @@ packages: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} - min-indent@1.0.1: - resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} - engines: {node: '>=4'} + minimatch@10.1.1: + resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} + engines: {node: 20 || >=22} minimatch@3.0.8: resolution: {integrity: sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==} @@ -5520,6 +6038,9 @@ packages: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -5577,8 +6098,8 @@ packages: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - node-forge@1.3.1: - resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} + node-forge@1.3.3: + resolution: {integrity: sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==} engines: {node: '>= 6.13.0'} node-gyp-build@4.8.4: @@ -5588,8 +6109,8 @@ packages: node-machine-id@1.1.12: resolution: {integrity: sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==} - node-mock-http@1.0.3: - resolution: {integrity: sha512-jN8dK25fsfnMrVsEhluUTPkBFY+6ybu7jSB1n+ri/vOGjJxU8J9CZhpSGkHXSkFjtUhbmoncG/YG9ta5Ludqog==} + node-mock-http@1.0.4: + resolution: {integrity: sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==} node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} @@ -5647,8 +6168,11 @@ packages: obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} - ofetch@1.5.0: - resolution: {integrity: sha512-A7llJ7eZyziA5xq9//3ZurA8OhFqtS99K5/V1sLBJ5j137CM/OAjlbA/TEJXBuOWwOfLqih+oH5U3ran4za1FQ==} + ofetch@1.5.1: + resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} + + ohash@1.1.6: + resolution: {integrity: sha512-TBu7PtV8YkAZn0tSxobKY2n2aAQva936lhRrj6957aDaCf9IEtqsKbgMzXE/F/sjqYOwmrukeORHNLe5glk7Cg==} ohash@2.0.11: resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} @@ -5675,8 +6199,8 @@ packages: resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} engines: {node: '>=12'} - openai@6.9.1: - resolution: {integrity: sha512-vQ5Rlt0ZgB3/BNmTa7bIijYFhz3YBceAA3Z4JuoMSBftBF9YqFHIEhZakSs+O/Ad7EaoEimZvHxD5ylRjN11Lg==} + openai@6.10.0: + resolution: {integrity: sha512-ITxOGo7rO3XRMiKA5l7tQ43iNNu+iXGFAcf2t+aWVzzqRaS0i7m1K2BhxNdaveB+5eENhO0VY1FkiZzhBk4v3A==} hasBin: true peerDependencies: ws: ^8.18.0 @@ -5698,8 +6222,8 @@ packages: outdent@0.5.0: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} - oxc-resolver@11.14.0: - resolution: {integrity: sha512-i4wNrqhOd+4YdHJfHglHtFiqqSxXuzFA+RUqmmWN1aMD3r1HqUSrIhw17tSO4jwKfhLs9uw1wzFPmvMsWacStg==} + oxc-resolver@11.15.0: + resolution: {integrity: sha512-Hk2J8QMYwmIO9XTCUiOH00+Xk2/+aBxRUnhrSlANDyCnLYc32R1WSIq1sU2yEdlqd53FfMpPEpnBYIKQMzliJw==} p-filter@2.1.0: resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} @@ -5735,8 +6259,8 @@ packages: package-manager-detector@0.2.11: resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} - package-manager-detector@1.5.0: - resolution: {integrity: sha512-uBj69dVlYe/+wxj8JOpr97XfsxH/eumMt6HqjNTmJDf/6NO9s+0uxeOneIz3AsPt2m6y9PqzDzd3ATcU17MNfw==} + package-manager-detector@1.6.0: + resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} @@ -5786,6 +6310,9 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + path-to-regexp@8.3.0: resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} @@ -5904,8 +6431,8 @@ packages: proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - publint@0.3.15: - resolution: {integrity: sha512-xPbRAPW+vqdiaKy5sVVY0uFAu3LaviaPO3pZ9FaRx59l9+U/RKR1OEbLhkug87cwiVKxPXyB4txsv5cad67u+A==} + publint@0.3.16: + resolution: {integrity: sha512-MFqyfRLAExPVZdTQFwkAQELzA8idyXzROVOytg6nEJ/GEypXBUmMGrVaID8cTuzRS1U5L8yTOdOJtMXgFUJAeA==} engines: {node: '>=18'} hasBin: true @@ -5924,6 +6451,9 @@ packages: quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + quansync@1.0.0: + resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -5944,10 +6474,10 @@ packages: rc9@2.1.2: resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} - react-dom@19.2.0: - resolution: {integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==} + react-dom@19.2.3: + resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==} peerDependencies: - react: ^19.2.0 + react: ^19.2.3 react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} @@ -5961,12 +6491,16 @@ packages: '@types/react': '>=18' react: '>=18' + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + react-refresh@0.18.0: resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} engines: {node: '>=0.10.0'} - react@19.2.0: - resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==} + react@19.2.3: + resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} engines: {node: '>=0.10.0'} read-yaml-file@1.1.0: @@ -6003,10 +6537,6 @@ packages: resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} engines: {node: '>= 4'} - redent@3.0.0: - resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} - engines: {node: '>=8'} - redis-errors@1.2.0: resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} engines: {node: '>=4'} @@ -6044,6 +6574,9 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -6076,8 +6609,8 @@ packages: resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==} hasBin: true - rolldown-plugin-dts@0.18.2: - resolution: {integrity: sha512-jRz3SHwr69F/IGEDMHtWjwVjgZwo3PZEadmMt4uA/e3rbIytoLJhvktSKlIAy/4QeWhVL9XeuCJBC66wvBQRwg==} + rolldown-plugin-dts@0.18.3: + resolution: {integrity: sha512-rd1LZ0Awwfyn89UndUF/HoFF4oH9a5j+2ZeuKSJYM80vmeN/p0gslYMnHTQHBEXPhUlvAlqGA3tVgXB/1qFNDg==} engines: {node: '>=20.19.0'} peerDependencies: '@ts-macro/tsc': ^0.3.6 @@ -6123,8 +6656,8 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - rou3@0.7.10: - resolution: {integrity: sha512-aoFj6f7MJZ5muJ+Of79nrhs9N3oLGqi2VEMe94Zbkjb6Wupha46EuoYgpWSOZlXww3bbd8ojgXTAA2mzimX5Ww==} + rou3@0.7.11: + resolution: {integrity: sha512-ELguG3ENDw5NKNmWHO3OGEjcgdxkCNvnMR22gKHEgRXuwiriap5RIYdummOaOiqUNcC5yU5txGCHWNm7KlHuAA==} router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} @@ -6173,6 +6706,10 @@ packages: engines: {node: '>=10'} hasBin: true + send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} + send@1.2.0: resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} engines: {node: '>= 18'} @@ -6203,6 +6740,10 @@ packages: serve-placeholder@2.0.2: resolution: {integrity: sha512-/TMG8SboeiQbZJWRlfTCqMs2DD3SZgWp0kDQePz9yUuCnDfDh/92gf7/PxGhzXTKBIPASIHxFcZndoNbp6QOLQ==} + serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} + serve-static@2.2.0: resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} engines: {node: '>= 18'} @@ -6317,8 +6858,8 @@ packages: solid-js@1.9.10: resolution: {integrity: sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew==} - solid-markdown@2.1.0: - resolution: {integrity: sha512-SCGs9QXb9qIFhxu4OdUIyJKJhp/+7jgfwDA993gZHF4iG8McXnJtnAcPGqPYWkynxa7U3DxrMsIWxC2xKP/WYg==} + solid-markdown@2.1.1: + resolution: {integrity: sha512-ly/5XbvCaFW5i6ds5GDU7+X4tRUfqa8Z7YUs+Ge18KMYXoFJGSlfJU91aW+tjz7bMSfxQrKcMLwB5z2lgErWIA==} engines: {node: '>=18', pnpm: '>=9.15.9'} peerDependencies: solid-js: ^1.6.0 @@ -6418,10 +6959,6 @@ packages: resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} engines: {node: '>=12'} - strip-indent@3.0.0: - resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} - engines: {node: '>=8'} - strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -6433,14 +6970,14 @@ packages: strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} - style-to-js@1.1.18: - resolution: {integrity: sha512-JFPn62D4kJaPTnhFUI244MThx+FEGbi+9dw1b9yBBQ+1CZpV7QAT8kUtJ7b7EUNdHajjF/0x8fT+16oLJoojLg==} + style-to-js@1.1.21: + resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} style-to-object@0.3.0: resolution: {integrity: sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==} - style-to-object@1.0.11: - resolution: {integrity: sha512-5A560JmXr7wDyGLK12Nq/EYS38VkGlglVzkis1JEdbGWSnbQIEhZzTJhzURXN5/8WwwFCs/f/VVcmkTppbXLow==} + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} supports-color@10.2.2: resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} @@ -6472,8 +7009,8 @@ packages: svelte: ^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0 typescript: ^4.9.4 || ^5.0.0 - svelte@5.44.1: - resolution: {integrity: sha512-8VnkRXpa6tJ9IqiwKvzZBNnBy9tZg0N63duDz0EJqiozsmBEAZfHiZzWWWAneIN+cAWkK1JkafW1xIbC4YrdBA==} + svelte@5.45.10: + resolution: {integrity: sha512-GiWXq6akkEN3zVDMQ1BVlRolmks5JkEdzD/67mvXOz6drRfuddT5JwsGZjMGSnsTRv/PjAXX8fqBcOr2g2qc/Q==} engines: {node: '>=18'} symbol-tree@3.2.4: @@ -6487,8 +7024,8 @@ packages: resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} engines: {node: '>=20'} - tailwindcss@4.1.17: - resolution: {integrity: sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==} + tailwindcss@4.1.18: + resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} tapable@2.3.0: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} @@ -6509,8 +7046,8 @@ packages: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} - terser@5.44.0: - resolution: {integrity: sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==} + terser@5.44.1: + resolution: {integrity: sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==} engines: {node: '>=10'} hasBin: true @@ -6526,9 +7063,6 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinyexec@0.3.2: - resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - tinyexec@1.0.2: resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} engines: {node: '>=18'} @@ -6541,11 +7075,11 @@ packages: resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} engines: {node: '>=14.0.0'} - tldts-core@7.0.16: - resolution: {integrity: sha512-XHhPmHxphLi+LGbH0G/O7dmUH9V65OY20R7vH8gETHsp5AZCjBk9l8sqmRKLaGOxnETU7XNSDUPtewAy/K6jbA==} + tldts-core@7.0.19: + resolution: {integrity: sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==} - tldts@7.0.16: - resolution: {integrity: sha512-5bdPHSwbKTeHmXrgecID4Ljff8rQjv7g8zKQPkCozRo2HWWni+p310FSn5ImI+9kWw9kK4lzOB5q/a6iv0IJsw==} + tldts@7.0.19: + resolution: {integrity: sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==} hasBin: true tmp@0.2.5: @@ -6613,13 +7147,13 @@ packages: resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} engines: {node: '>=6'} - tsdown@0.17.0-beta.6: - resolution: {integrity: sha512-Hbuwzo4hg5759mnzzxZdCNT3hbqTwmXegCoB+60U57usOjVyqGx7rzngnKX/8u1VxXsmFipmBLHt+T4AaSMUbw==} + tsdown@0.17.3: + resolution: {integrity: sha512-bgLgTog+oyadDTr9SZ57jZtb+A4aglCjo3xgJrkCDxbzcQl2l2iDDr4b06XHSQHwyDNIhYFDgPRhuu1wL3pNsw==} engines: {node: '>=20.19.0'} hasBin: true peerDependencies: '@arethetypeswrong/core': ^0.18.1 - '@vitejs/devtools': ^0.0.0-alpha.18 + '@vitejs/devtools': ^0.0.0-alpha.19 publint: ^0.3.0 typescript: ^5.0.0 unplugin-lightningcss: ^0.4.0 @@ -6641,8 +7175,8 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsx@4.20.6: - resolution: {integrity: sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==} + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} engines: {node: '>=18.0.0'} hasBin: true @@ -6650,8 +7184,12 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - type-fest@5.1.0: - resolution: {integrity: sha512-wQ531tuWvB6oK+pchHIu5lHe5f5wpSCqB8Kf4dWQRbOYc9HTge7JL0G4Qd44bh6QuJCccIzL3bugb8GI0MwHrg==} + type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + + type-fest@5.3.1: + resolution: {integrity: sha512-VCn+LMHbd4t6sF3wfU/+HKT63C9OoyrSIf4b+vtWHpt2U7/4InZG467YDNMFMR70DdHjAdpPWmw2lzRdg0Xqqg==} engines: {node: '>=20'} type-is@2.0.1: @@ -6676,8 +7214,8 @@ packages: peerDependencies: typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x - typescript-eslint@8.46.3: - resolution: {integrity: sha512-bAfgMavTuGo+8n6/QQDVQz4tZ4f7Soqg53RbrlZQEoAltYop/XR4RAts/I0BrO3TTClTSTFJ0wYbla+P8cEWJA==} + typescript-eslint@8.49.0: + resolution: {integrity: sha512-zRSVH1WXD0uXczCXw+nsdjGPUdx4dfrs5VQoHnUWmv1U3oNlAKv4FUNdLDhVUg+gYn+a5hUESqch//Rv5wVhrg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -6702,8 +7240,8 @@ packages: ultrahtml@1.6.0: resolution: {integrity: sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==} - unconfig-core@7.4.1: - resolution: {integrity: sha512-Bp/bPZjV2Vl/fofoA2OYLSnw1Z0MOhCX7zHnVCYrazpfZvseBbGhwcNQMxsg185Mqh7VZQqK3C8hFG/Dyng+yA==} + unconfig-core@7.4.2: + resolution: {integrity: sha512-VgPCvLWugINbXvMQDf8Jh0mlbvNjNC6eSUziHsBCMpxR05OPrNrvDnyatdMjRgcHaaNsCqz+wjNXxNw1kRLHUg==} uncrypto@0.1.3: resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} @@ -6711,6 +7249,9 @@ packages: unctx@2.4.1: resolution: {integrity: sha512-AbaYw0Nm4mK4qjhns67C+kgxR2YWiwlDBPzxrN8h8C6VtAdCgditAY5Dezu3IJy4XVqAnbrXt9oQJvsn3fyozg==} + undici-types@5.28.4: + resolution: {integrity: sha512-3OeMF5Lyowe8VW0skf5qaIE7Or3yS9LS7fvMUI0gg4YxpIBVg0L8BxCmROw2CcYhSkpR68Epz7CGc8MPj94Uww==} + undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -6721,6 +7262,9 @@ packages: resolution: {integrity: sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==} engines: {node: '>=20.18.1'} + unenv@1.10.0: + resolution: {integrity: sha512-wY5bskBQFL9n3Eca5XnhH6KbUo/tfvkwm9OpcdCvLaeA7piBNbavbOKJySEwQ1V0RH6HvNlSAFRTpvTqgKRQXQ==} + unenv@2.0.0-rc.24: resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} @@ -6774,15 +7318,15 @@ packages: resolution: {integrity: sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==} engines: {node: '>=20.19.0'} - unplugin@2.3.10: - resolution: {integrity: sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw==} + unplugin@2.3.11: + resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} engines: {node: '>=18.12.0'} unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} - unrun@0.2.16: - resolution: {integrity: sha512-DBkjUpQv9AQs1464XWnWQ97RuxPCu+CImvQMPmqFeHoL2Bi6C1BGPacMuXVw4VMIfQewNJZWUxPt5envG90oUA==} + unrun@0.2.19: + resolution: {integrity: sha512-DbwbJ9BvPEb3BeZnIpP9S5tGLO/JIgPQ3JrpMRFIfZMZfMG19f26OlLbC2ml8RRdrI2ZA7z2t+at5tsIHbh6Qw==} engines: {node: '>=20.19.0'} hasBin: true peerDependencies: @@ -6791,8 +7335,8 @@ packages: synckit: optional: true - unstorage@1.17.2: - resolution: {integrity: sha512-cKEsD6iBWJgOMJ6vW1ID/SYuqNf8oN4yqRk8OYqaVQ3nnkJXOT1PSpaMh2QfzLs78UN5kSNRD2c/mgjT8tX7+w==} + unstorage@1.17.3: + resolution: {integrity: sha512-i+JYyy0DoKmQ3FximTHbGadmIYb8JEpq7lxUjnjeB702bCPum0vzo6oy5Mfu0lpqISw7hCyMW2yj4nWC8bqJ3Q==} peerDependencies: '@azure/app-configuration': ^1.8.0 '@azure/cosmos': ^4.2.0 @@ -6864,8 +7408,8 @@ packages: unwasm@0.3.11: resolution: {integrity: sha512-Vhp5gb1tusSQw5of/g3Q697srYgMXvwMgXMjcG4ZNga02fDX9coxJ9fAb0Ci38hM2Hv/U1FXRPGgjP2BYqhNoQ==} - update-browserslist-db@1.1.4: - resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} + update-browserslist-db@1.2.2: + resolution: {integrity: sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -6897,6 +7441,10 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vinxi@0.5.3: + resolution: {integrity: sha512-4sL2SMrRzdzClapP44oXdGjCE1oq7/DagsbjY5A09EibmoIO4LP8ScRVdh03lfXxKRk7nCWK7n7dqKvm+fp/9w==} + hasBin: true + vite-plugin-dts@4.2.3: resolution: {integrity: sha512-O5NalzHANQRwVw1xj8KQun3Bv8OSDAlNJXrnqoAz10BOuW8FVvY5g4ygj+DlJZL5mtSPuMu9vd3OfrdW5d4k6w==} engines: {node: ^14.18.0 || >=16.0.0} @@ -6930,19 +7478,19 @@ packages: vite: optional: true - vite@7.2.4: - resolution: {integrity: sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==} - engines: {node: ^20.19.0 || >=22.12.0} + vite@6.4.1: + resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 jiti: '>=1.21.0' - less: ^4.0.0 + less: '*' lightningcss: ^1.21.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' terser: ^5.16.0 tsx: ^4.8.1 yaml: ^2.4.2 @@ -6970,26 +7518,66 @@ packages: yaml: optional: true - vitefu@1.1.1: - resolution: {integrity: sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==} + vite@7.2.7: + resolution: {integrity: sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true peerDependencies: - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0 - peerDependenciesMeta: - vite: - optional: true - - vitest@4.0.14: - resolution: {integrity: sha512-d9B2J9Cm9dN9+6nxMnnNJKJCtcyKfnHj15N6YNJfaFHRLua/d3sRKU9RuKmO9mB0XdFtUizlxfz/VPbd3OxGhw==} + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitefu@1.1.1: + resolution: {integrity: sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0 + peerDependenciesMeta: + vite: + optional: true + + vitest@4.0.15: + resolution: {integrity: sha512-n1RxDp8UJm6N0IbJLQo+yzLZ2sQCDyl1o0LeugbPWf8+8Fttp29GghsQBjYJVmWq3gBFfe9Hs1spR44vovn2wA==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.0.14 - '@vitest/browser-preview': 4.0.14 - '@vitest/browser-webdriverio': 4.0.14 - '@vitest/ui': 4.0.14 + '@vitest/browser-playwright': 4.0.15 + '@vitest/browser-preview': 4.0.15 + '@vitest/browser-webdriverio': 4.0.15 + '@vitest/ui': 4.0.15 happy-dom: '*' jsdom: '*' peerDependenciesMeta: @@ -7024,8 +7612,8 @@ packages: peerDependencies: eslint: ^8.57.0 || ^9.0.0 - vue-router@4.6.3: - resolution: {integrity: sha512-ARBedLm9YlbvQomnmq91Os7ck6efydTSpRP3nuOKCvgJOHNrhRoJDSKtee8kcL1Vf7nz6U+PMBL+hTvR3bTVQg==} + vue-router@4.6.4: + resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==} peerDependencies: vue: ^3.5.0 @@ -7101,11 +7689,20 @@ packages: engines: {node: '>= 8'} hasBin: true + which@4.0.0: + resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} + engines: {node: ^16.13.0 || >=18.0.0} + hasBin: true + why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} hasBin: true + widest-line@4.0.1: + resolution: {integrity: sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==} + engines: {node: '>=12'} + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -7137,8 +7734,12 @@ packages: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} - xmlbuilder2@4.0.1: - resolution: {integrity: sha512-vXeky0YRVjhx5pseJDQLk0F6u7gyA8++ceVOS88r4dWu4lWdY/ZjbL45QrN+g0GzZLg1D5AkzThpikZa98SC/g==} + xmlbuilder2@3.1.1: + resolution: {integrity: sha512-WCSfbfZnQDdLQLiMdGUQpMxxckeQ4oZNMNhLVkcekTu7xhD4tuUDyAPoY8CwXvBYE6LwBHd6QW2WZXlOWr1vCw==} + engines: {node: '>=12.0'} + + xmlbuilder2@4.0.3: + resolution: {integrity: sha512-bx8Q1STctnNaaDymWnkfQLKofs0mGNN7rLLapJlGuV3VlvegD7Ls4ggMjE3aUSWItCCzU0PEv45lI87iSigiCA==} engines: {node: '>=20.0'} xmlchars@2.2.0: @@ -7158,8 +7759,8 @@ packages: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} - yaml@2.8.1: - resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + yaml@2.8.2: + resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} engines: {node: '>= 14.6'} hasBin: true @@ -7178,8 +7779,8 @@ packages: youch-core@0.3.3: resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} - youch@4.1.0-beta.11: - resolution: {integrity: sha512-sQi6PERyO/mT8w564ojOVeAlYTtVQmC2GaktQAf+IdI75/GKIggosBuvyVXvEV+FATAT6RbLdIjFoiIId4ozoQ==} + youch@4.1.0-beta.13: + resolution: {integrity: sha512-3+AG1Xvt+R7M7PSDudhbfbwiyveW6B8PLBIwTyEC598biEYIjHhC89i6DBEvR0EZUjGY3uGSnC429HpIa2Z09g==} zimmerframe@1.1.4: resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} @@ -7199,36 +7800,33 @@ packages: snapshots: - '@acemir/cssom@0.9.24': {} - - '@adobe/css-tools@4.4.4': - optional: true + '@acemir/cssom@0.9.29': {} '@alcyone-labs/zod-to-json-schema@4.0.10(zod@4.1.13)': dependencies: zod: 4.1.13 - '@anthropic-ai/sdk@0.71.0(zod@4.1.13)': + '@anthropic-ai/sdk@0.71.2(zod@4.1.13)': dependencies: json-schema-to-ts: 3.1.1 optionalDependencies: zod: 4.1.13 - '@asamuzakjp/css-color@4.0.5': + '@asamuzakjp/css-color@4.1.0': dependencies: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - lru-cache: 11.2.2 + lru-cache: 11.2.4 - '@asamuzakjp/dom-selector@6.7.4': + '@asamuzakjp/dom-selector@6.7.6': dependencies: '@asamuzakjp/nwsapi': 2.3.9 bidi-js: 1.0.3 css-tree: 3.1.0 is-potential-custom-element-name: 1.0.1 - lru-cache: 11.2.2 + lru-cache: 11.2.4 '@asamuzakjp/nwsapi@2.3.9': {} @@ -7282,7 +7880,7 @@ snapshots: dependencies: '@babel/compat-data': 7.28.5 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.27.0 + browserslist: 4.28.1 lru-cache: 5.1.1 semver: 6.3.1 @@ -7471,7 +8069,7 @@ snapshots: dependencies: '@changesets/types': 6.1.0 - '@changesets/cli@2.29.8(@types/node@24.10.1)': + '@changesets/cli@2.29.8(@types/node@24.10.3)': dependencies: '@changesets/apply-release-plan': 7.0.14 '@changesets/assemble-release-plan': 6.0.9 @@ -7487,7 +8085,7 @@ snapshots: '@changesets/should-skip-package': 0.1.2 '@changesets/types': 6.1.0 '@changesets/write': 0.4.0 - '@inquirer/external-editor': 1.0.3(@types/node@24.10.1) + '@inquirer/external-editor': 1.0.3(@types/node@24.10.3) '@manypkg/get-packages': 1.1.3 ansi-colors: 4.1.3 ci-info: 3.9.0 @@ -7590,10 +8188,10 @@ snapshots: dependencies: '@changesets/types': 6.1.0 fs-extra: 7.0.1 - human-id: 4.1.2 + human-id: 4.1.3 prettier: 2.8.8 - '@cloudflare/kv-asset-handler@0.4.0': + '@cloudflare/kv-asset-handler@0.4.1': dependencies: mime: 3.0.0 @@ -7633,6 +8231,13 @@ snapshots: '@csstools/css-tokenizer@3.0.4': {} + '@deno/shim-deno-test@0.5.0': {} + + '@deno/shim-deno@0.19.2': + dependencies: + '@deno/shim-deno-test': 0.5.0 + which: 4.0.0 + '@emnapi/core@1.7.1': dependencies: '@emnapi/wasi-threads': 1.1.0 @@ -7646,84 +8251,231 @@ snapshots: dependencies: tslib: 2.8.1 + '@esbuild/aix-ppc64@0.20.2': + optional: true + '@esbuild/aix-ppc64@0.25.12': optional: true + '@esbuild/aix-ppc64@0.27.1': + optional: true + + '@esbuild/android-arm64@0.20.2': + optional: true + '@esbuild/android-arm64@0.25.12': optional: true + '@esbuild/android-arm64@0.27.1': + optional: true + + '@esbuild/android-arm@0.20.2': + optional: true + '@esbuild/android-arm@0.25.12': optional: true + '@esbuild/android-arm@0.27.1': + optional: true + + '@esbuild/android-x64@0.20.2': + optional: true + '@esbuild/android-x64@0.25.12': optional: true + '@esbuild/android-x64@0.27.1': + optional: true + + '@esbuild/darwin-arm64@0.20.2': + optional: true + '@esbuild/darwin-arm64@0.25.12': optional: true + '@esbuild/darwin-arm64@0.27.1': + optional: true + + '@esbuild/darwin-x64@0.20.2': + optional: true + '@esbuild/darwin-x64@0.25.12': optional: true + '@esbuild/darwin-x64@0.27.1': + optional: true + + '@esbuild/freebsd-arm64@0.20.2': + optional: true + '@esbuild/freebsd-arm64@0.25.12': optional: true + '@esbuild/freebsd-arm64@0.27.1': + optional: true + + '@esbuild/freebsd-x64@0.20.2': + optional: true + '@esbuild/freebsd-x64@0.25.12': optional: true + '@esbuild/freebsd-x64@0.27.1': + optional: true + + '@esbuild/linux-arm64@0.20.2': + optional: true + '@esbuild/linux-arm64@0.25.12': optional: true + '@esbuild/linux-arm64@0.27.1': + optional: true + + '@esbuild/linux-arm@0.20.2': + optional: true + '@esbuild/linux-arm@0.25.12': optional: true + '@esbuild/linux-arm@0.27.1': + optional: true + + '@esbuild/linux-ia32@0.20.2': + optional: true + '@esbuild/linux-ia32@0.25.12': optional: true + '@esbuild/linux-ia32@0.27.1': + optional: true + + '@esbuild/linux-loong64@0.20.2': + optional: true + '@esbuild/linux-loong64@0.25.12': optional: true + '@esbuild/linux-loong64@0.27.1': + optional: true + + '@esbuild/linux-mips64el@0.20.2': + optional: true + '@esbuild/linux-mips64el@0.25.12': optional: true + '@esbuild/linux-mips64el@0.27.1': + optional: true + + '@esbuild/linux-ppc64@0.20.2': + optional: true + '@esbuild/linux-ppc64@0.25.12': optional: true + '@esbuild/linux-ppc64@0.27.1': + optional: true + + '@esbuild/linux-riscv64@0.20.2': + optional: true + '@esbuild/linux-riscv64@0.25.12': optional: true + '@esbuild/linux-riscv64@0.27.1': + optional: true + + '@esbuild/linux-s390x@0.20.2': + optional: true + '@esbuild/linux-s390x@0.25.12': optional: true + '@esbuild/linux-s390x@0.27.1': + optional: true + + '@esbuild/linux-x64@0.20.2': + optional: true + '@esbuild/linux-x64@0.25.12': optional: true + '@esbuild/linux-x64@0.27.1': + optional: true + '@esbuild/netbsd-arm64@0.25.12': optional: true + '@esbuild/netbsd-arm64@0.27.1': + optional: true + + '@esbuild/netbsd-x64@0.20.2': + optional: true + '@esbuild/netbsd-x64@0.25.12': optional: true + '@esbuild/netbsd-x64@0.27.1': + optional: true + '@esbuild/openbsd-arm64@0.25.12': optional: true + '@esbuild/openbsd-arm64@0.27.1': + optional: true + + '@esbuild/openbsd-x64@0.20.2': + optional: true + '@esbuild/openbsd-x64@0.25.12': optional: true + '@esbuild/openbsd-x64@0.27.1': + optional: true + '@esbuild/openharmony-arm64@0.25.12': optional: true + '@esbuild/openharmony-arm64@0.27.1': + optional: true + + '@esbuild/sunos-x64@0.20.2': + optional: true + '@esbuild/sunos-x64@0.25.12': optional: true + '@esbuild/sunos-x64@0.27.1': + optional: true + + '@esbuild/win32-arm64@0.20.2': + optional: true + '@esbuild/win32-arm64@0.25.12': optional: true + '@esbuild/win32-arm64@0.27.1': + optional: true + + '@esbuild/win32-ia32@0.20.2': + optional: true + '@esbuild/win32-ia32@0.25.12': optional: true + '@esbuild/win32-ia32@0.27.1': + optional: true + + '@esbuild/win32-x64@0.20.2': + optional: true + '@esbuild/win32-x64@0.25.12': optional: true + '@esbuild/win32-x64@0.27.1': + optional: true + '@eslint-community/eslint-utils@4.9.0(eslint@9.39.1(jiti@2.6.1))': dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -7747,7 +8499,7 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.1': + '@eslint/eslintrc@3.3.3': dependencies: ajv: 6.12.6 debug: 4.4.3 @@ -7772,15 +8524,15 @@ snapshots: '@faker-js/faker@10.1.0': {} - '@gerrit0/mini-shiki@3.15.0': + '@gerrit0/mini-shiki@3.19.0': dependencies: - '@shikijs/engine-oniguruma': 3.15.0 - '@shikijs/langs': 3.15.0 - '@shikijs/themes': 3.15.0 - '@shikijs/types': 3.15.0 + '@shikijs/engine-oniguruma': 3.20.0 + '@shikijs/langs': 3.20.0 + '@shikijs/themes': 3.20.0 + '@shikijs/types': 3.20.0 '@shikijs/vscode-textmate': 10.0.2 - '@google/genai@1.30.0': + '@google/genai@1.33.0': dependencies: google-auth-library: 10.5.0 ws: 8.18.3 @@ -7800,15 +8552,21 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} - '@inquirer/external-editor@1.0.3(@types/node@24.10.1)': + '@inquirer/external-editor@1.0.3(@types/node@24.10.3)': dependencies: chardet: 2.1.1 - iconv-lite: 0.7.0 + iconv-lite: 0.7.1 optionalDependencies: - '@types/node': 24.10.1 + '@types/node': 24.10.3 '@ioredis/commands@1.4.0': {} + '@isaacs/balanced-match@4.0.1': {} + + '@isaacs/brace-expansion@5.0.0': + dependencies: + '@isaacs/balanced-match': 4.0.1 + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -7870,7 +8628,7 @@ snapshots: globby: 11.1.0 read-yaml-file: 1.1.0 - '@mapbox/node-pre-gyp@2.0.0': + '@mapbox/node-pre-gyp@2.0.3': dependencies: consola: 3.4.2 detect-libc: 2.1.2 @@ -7883,23 +8641,23 @@ snapshots: - encoding - supports-color - '@microsoft/api-extractor-model@7.29.6(@types/node@24.10.1)': + '@microsoft/api-extractor-model@7.29.6(@types/node@24.10.3)': dependencies: '@microsoft/tsdoc': 0.15.1 '@microsoft/tsdoc-config': 0.17.1 - '@rushstack/node-core-library': 5.7.0(@types/node@24.10.1) + '@rushstack/node-core-library': 5.7.0(@types/node@24.10.3) transitivePeerDependencies: - '@types/node' - '@microsoft/api-extractor@7.47.7(@types/node@24.10.1)': + '@microsoft/api-extractor@7.47.7(@types/node@24.10.3)': dependencies: - '@microsoft/api-extractor-model': 7.29.6(@types/node@24.10.1) + '@microsoft/api-extractor-model': 7.29.6(@types/node@24.10.3) '@microsoft/tsdoc': 0.15.1 '@microsoft/tsdoc-config': 0.17.1 - '@rushstack/node-core-library': 5.7.0(@types/node@24.10.1) + '@rushstack/node-core-library': 5.7.0(@types/node@24.10.3) '@rushstack/rig-package': 0.5.3 - '@rushstack/terminal': 0.14.0(@types/node@24.10.1) - '@rushstack/ts-command-line': 4.22.6(@types/node@24.10.1) + '@rushstack/terminal': 0.14.0(@types/node@24.10.3) + '@rushstack/ts-command-line': 4.22.6(@types/node@24.10.3) lodash: 4.17.21 minimatch: 3.0.8 resolve: 1.22.11 @@ -7984,16 +8742,31 @@ snapshots: '@one-ini/wasm@0.1.1': {} + '@oozcitak/dom@1.15.10': + dependencies: + '@oozcitak/infra': 1.0.8 + '@oozcitak/url': 1.0.4 + '@oozcitak/util': 8.3.8 + '@oozcitak/dom@2.0.2': dependencies: '@oozcitak/infra': 2.0.2 '@oozcitak/url': 3.0.0 '@oozcitak/util': 10.0.0 + '@oozcitak/infra@1.0.8': + dependencies: + '@oozcitak/util': 8.3.8 + '@oozcitak/infra@2.0.2': dependencies: '@oozcitak/util': 10.0.0 + '@oozcitak/url@1.0.4': + dependencies: + '@oozcitak/infra': 1.0.8 + '@oozcitak/util': 8.3.8 + '@oozcitak/url@3.0.0': dependencies: '@oozcitak/infra': 2.0.2 @@ -8001,67 +8774,70 @@ snapshots: '@oozcitak/util@10.0.0': {} - '@oxc-project/runtime@0.101.0': {} + '@oozcitak/util@8.3.8': {} '@oxc-project/types@0.101.0': {} - '@oxc-resolver/binding-android-arm-eabi@11.14.0': + '@oxc-resolver/binding-android-arm-eabi@11.15.0': optional: true - '@oxc-resolver/binding-android-arm64@11.14.0': + '@oxc-resolver/binding-android-arm64@11.15.0': optional: true - '@oxc-resolver/binding-darwin-arm64@11.14.0': + '@oxc-resolver/binding-darwin-arm64@11.15.0': optional: true - '@oxc-resolver/binding-darwin-x64@11.14.0': + '@oxc-resolver/binding-darwin-x64@11.15.0': optional: true - '@oxc-resolver/binding-freebsd-x64@11.14.0': + '@oxc-resolver/binding-freebsd-x64@11.15.0': optional: true - '@oxc-resolver/binding-linux-arm-gnueabihf@11.14.0': + '@oxc-resolver/binding-linux-arm-gnueabihf@11.15.0': optional: true - '@oxc-resolver/binding-linux-arm-musleabihf@11.14.0': + '@oxc-resolver/binding-linux-arm-musleabihf@11.15.0': optional: true - '@oxc-resolver/binding-linux-arm64-gnu@11.14.0': + '@oxc-resolver/binding-linux-arm64-gnu@11.15.0': optional: true - '@oxc-resolver/binding-linux-arm64-musl@11.14.0': + '@oxc-resolver/binding-linux-arm64-musl@11.15.0': optional: true - '@oxc-resolver/binding-linux-ppc64-gnu@11.14.0': + '@oxc-resolver/binding-linux-ppc64-gnu@11.15.0': optional: true - '@oxc-resolver/binding-linux-riscv64-gnu@11.14.0': + '@oxc-resolver/binding-linux-riscv64-gnu@11.15.0': optional: true - '@oxc-resolver/binding-linux-riscv64-musl@11.14.0': + '@oxc-resolver/binding-linux-riscv64-musl@11.15.0': optional: true - '@oxc-resolver/binding-linux-s390x-gnu@11.14.0': + '@oxc-resolver/binding-linux-s390x-gnu@11.15.0': optional: true - '@oxc-resolver/binding-linux-x64-gnu@11.14.0': + '@oxc-resolver/binding-linux-x64-gnu@11.15.0': optional: true - '@oxc-resolver/binding-linux-x64-musl@11.14.0': + '@oxc-resolver/binding-linux-x64-musl@11.15.0': optional: true - '@oxc-resolver/binding-wasm32-wasi@11.14.0': + '@oxc-resolver/binding-openharmony-arm64@11.15.0': + optional: true + + '@oxc-resolver/binding-wasm32-wasi@11.15.0': dependencies: '@napi-rs/wasm-runtime': 1.1.0 optional: true - '@oxc-resolver/binding-win32-arm64-msvc@11.14.0': + '@oxc-resolver/binding-win32-arm64-msvc@11.15.0': optional: true - '@oxc-resolver/binding-win32-ia32-msvc@11.14.0': + '@oxc-resolver/binding-win32-ia32-msvc@11.15.0': optional: true - '@oxc-resolver/binding-win32-x64-msvc@11.14.0': + '@oxc-resolver/binding-win32-x64-msvc@11.15.0': optional: true '@parcel/watcher-android-arm64@2.5.1': @@ -8094,6 +8870,11 @@ snapshots: '@parcel/watcher-linux-x64-musl@2.5.1': optional: true + '@parcel/watcher-wasm@2.3.0': + dependencies: + is-glob: 4.0.3 + micromatch: 4.0.8 + '@parcel/watcher-wasm@2.5.1': dependencies: is-glob: 4.0.3 @@ -8138,23 +8919,23 @@ snapshots: '@polka/url@1.0.0-next.29': {} - '@poppinss/colors@4.1.5': + '@poppinss/colors@4.1.6': dependencies: kleur: 4.1.5 - '@poppinss/dumper@0.6.4': + '@poppinss/dumper@0.6.5': dependencies: - '@poppinss/colors': 4.1.5 + '@poppinss/colors': 4.1.6 '@sindresorhus/is': 7.1.1 supports-color: 10.2.2 - '@poppinss/exception@1.2.2': {} + '@poppinss/exception@1.2.3': {} '@publint/pack@0.1.2': {} - '@quansync/fs@0.1.5': + '@quansync/fs@1.0.0': dependencies: - quansync: 0.2.11 + quansync: 1.0.0 '@rolldown/binding-android-arm64@1.0.0-beta.53': optional: true @@ -8197,11 +8978,9 @@ snapshots: '@rolldown/binding-win32-x64-msvc@1.0.0-beta.53': optional: true - '@rolldown/pluginutils@1.0.0-beta.40': {} - - '@rolldown/pluginutils@1.0.0-beta.47': {} + '@rolldown/pluginutils@1.0.0-beta.27': {} - '@rolldown/pluginutils@1.0.0-beta.50': {} + '@rolldown/pluginutils@1.0.0-beta.40': {} '@rolldown/pluginutils@1.0.0-beta.53': {} @@ -8256,7 +9035,7 @@ snapshots: dependencies: serialize-javascript: 6.0.2 smob: 1.5.0 - terser: 5.44.0 + terser: 5.44.1 optionalDependencies: rollup: 4.53.3 @@ -8334,7 +9113,7 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.53.3': optional: true - '@rushstack/node-core-library@5.7.0(@types/node@24.10.1)': + '@rushstack/node-core-library@5.7.0(@types/node@24.10.3)': dependencies: ajv: 8.13.0 ajv-draft-04: 1.0.0(ajv@8.13.0) @@ -8345,43 +9124,43 @@ snapshots: resolve: 1.22.11 semver: 7.5.4 optionalDependencies: - '@types/node': 24.10.1 + '@types/node': 24.10.3 '@rushstack/rig-package@0.5.3': dependencies: resolve: 1.22.11 strip-json-comments: 3.1.1 - '@rushstack/terminal@0.14.0(@types/node@24.10.1)': + '@rushstack/terminal@0.14.0(@types/node@24.10.3)': dependencies: - '@rushstack/node-core-library': 5.7.0(@types/node@24.10.1) + '@rushstack/node-core-library': 5.7.0(@types/node@24.10.3) supports-color: 8.1.1 optionalDependencies: - '@types/node': 24.10.1 + '@types/node': 24.10.3 - '@rushstack/ts-command-line@4.22.6(@types/node@24.10.1)': + '@rushstack/ts-command-line@4.22.6(@types/node@24.10.3)': dependencies: - '@rushstack/terminal': 0.14.0(@types/node@24.10.1) + '@rushstack/terminal': 0.14.0(@types/node@24.10.3) '@types/argparse': 1.0.38 argparse: 1.0.10 string-argv: 0.3.2 transitivePeerDependencies: - '@types/node' - '@shikijs/engine-oniguruma@3.15.0': + '@shikijs/engine-oniguruma@3.20.0': dependencies: - '@shikijs/types': 3.15.0 + '@shikijs/types': 3.20.0 '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/langs@3.15.0': + '@shikijs/langs@3.20.0': dependencies: - '@shikijs/types': 3.15.0 + '@shikijs/types': 3.20.0 - '@shikijs/themes@3.15.0': + '@shikijs/themes@3.20.0': dependencies: - '@shikijs/types': 3.15.0 + '@shikijs/types': 3.20.0 - '@shikijs/types@3.15.0': + '@shikijs/types@3.20.0': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 @@ -8502,38 +9281,38 @@ snapshots: '@testing-library/dom': 10.4.1 solid-js: 1.9.10 - '@speed-highlight/core@1.2.8': {} + '@speed-highlight/core@1.2.12': {} '@standard-schema/spec@1.0.0': {} - '@stylistic/eslint-plugin@5.5.0(eslint@9.39.1(jiti@2.6.1))': + '@stylistic/eslint-plugin@5.6.1(eslint@9.39.1(jiti@2.6.1))': dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) - '@typescript-eslint/types': 8.48.0 + '@typescript-eslint/types': 8.49.0 eslint: 9.39.1(jiti@2.6.1) eslint-visitor-keys: 4.2.1 espree: 10.4.0 estraverse: 5.3.0 picomatch: 4.0.3 - '@sveltejs/acorn-typescript@1.0.7(acorn@8.15.0)': + '@sveltejs/acorn-typescript@1.0.8(acorn@8.15.0)': dependencies: acorn: 8.15.0 - '@sveltejs/adapter-auto@3.3.1(@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))': + '@sveltejs/adapter-auto@3.3.1(@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.45.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.45.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))': dependencies: - '@sveltejs/kit': 2.49.2(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + '@sveltejs/kit': 2.49.2(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.45.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.45.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) import-meta-resolve: 4.2.0 - '@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + '@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.45.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.45.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@standard-schema/spec': 1.0.0 - '@sveltejs/acorn-typescript': 1.0.7(acorn@8.15.0) - '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + '@sveltejs/acorn-typescript': 1.0.8(acorn@8.15.0) + '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.45.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@types/cookie': 0.6.0 acorn: 8.15.0 cookie: 0.6.0 - devalue: 5.5.0 + devalue: 5.6.1 esm-env: 1.2.2 kleur: 4.1.5 magic-string: 0.30.21 @@ -8541,39 +9320,39 @@ snapshots: sade: 1.8.1 set-cookie-parser: 2.7.2 sirv: 3.0.2 - svelte: 5.44.1 - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + svelte: 5.45.10 + vite: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - '@sveltejs/package@2.5.7(svelte@5.44.1)(typescript@5.9.3)': + '@sveltejs/package@2.5.7(svelte@5.45.10)(typescript@5.9.3)': dependencies: chokidar: 5.0.0 kleur: 4.1.5 sade: 1.8.1 semver: 7.7.3 - svelte: 5.44.1 - svelte2tsx: 0.7.45(svelte@5.44.1)(typescript@5.9.3) + svelte: 5.45.10 + svelte2tsx: 0.7.45(svelte@5.45.10)(typescript@5.9.3) transitivePeerDependencies: - typescript - '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.45.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.45.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.45.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) debug: 4.4.3 - svelte: 5.44.1 - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + svelte: 5.45.10 + vite: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + '@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.45.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.45.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.45.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) debug: 4.4.3 deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.21 - svelte: 5.44.1 - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) - vitefu: 1.1.1(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + svelte: 5.45.10 + vite: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vitefu: 1.1.1(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) transitivePeerDependencies: - supports-color @@ -8584,77 +9363,77 @@ snapshots: transitivePeerDependencies: - encoding - '@tailwindcss/node@4.1.17': + '@tailwindcss/node@4.1.18': dependencies: '@jridgewell/remapping': 2.3.5 - enhanced-resolve: 5.18.3 + enhanced-resolve: 5.18.4 jiti: 2.6.1 lightningcss: 1.30.2 magic-string: 0.30.21 source-map-js: 1.2.1 - tailwindcss: 4.1.17 + tailwindcss: 4.1.18 - '@tailwindcss/oxide-android-arm64@4.1.17': + '@tailwindcss/oxide-android-arm64@4.1.18': optional: true - '@tailwindcss/oxide-darwin-arm64@4.1.17': + '@tailwindcss/oxide-darwin-arm64@4.1.18': optional: true - '@tailwindcss/oxide-darwin-x64@4.1.17': + '@tailwindcss/oxide-darwin-x64@4.1.18': optional: true - '@tailwindcss/oxide-freebsd-x64@4.1.17': + '@tailwindcss/oxide-freebsd-x64@4.1.18': optional: true - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17': + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': optional: true - '@tailwindcss/oxide-linux-arm64-gnu@4.1.17': + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': optional: true - '@tailwindcss/oxide-linux-arm64-musl@4.1.17': + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': optional: true - '@tailwindcss/oxide-linux-x64-gnu@4.1.17': + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': optional: true - '@tailwindcss/oxide-linux-x64-musl@4.1.17': + '@tailwindcss/oxide-linux-x64-musl@4.1.18': optional: true - '@tailwindcss/oxide-wasm32-wasi@4.1.17': + '@tailwindcss/oxide-wasm32-wasi@4.1.18': optional: true - '@tailwindcss/oxide-win32-arm64-msvc@4.1.17': + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': optional: true - '@tailwindcss/oxide-win32-x64-msvc@4.1.17': + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': optional: true - '@tailwindcss/oxide@4.1.17': + '@tailwindcss/oxide@4.1.18': optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.1.17 - '@tailwindcss/oxide-darwin-arm64': 4.1.17 - '@tailwindcss/oxide-darwin-x64': 4.1.17 - '@tailwindcss/oxide-freebsd-x64': 4.1.17 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.17 - '@tailwindcss/oxide-linux-arm64-gnu': 4.1.17 - '@tailwindcss/oxide-linux-arm64-musl': 4.1.17 - '@tailwindcss/oxide-linux-x64-gnu': 4.1.17 - '@tailwindcss/oxide-linux-x64-musl': 4.1.17 - '@tailwindcss/oxide-wasm32-wasi': 4.1.17 - '@tailwindcss/oxide-win32-arm64-msvc': 4.1.17 - '@tailwindcss/oxide-win32-x64-msvc': 4.1.17 - - '@tailwindcss/vite@4.1.17(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': - dependencies: - '@tailwindcss/node': 4.1.17 - '@tailwindcss/oxide': 4.1.17 - tailwindcss: 4.1.17 - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) - - '@tanstack/devtools-client@0.0.4': - dependencies: - '@tanstack/devtools-event-client': 0.3.5 + '@tailwindcss/oxide-android-arm64': 4.1.18 + '@tailwindcss/oxide-darwin-arm64': 4.1.18 + '@tailwindcss/oxide-darwin-x64': 4.1.18 + '@tailwindcss/oxide-freebsd-x64': 4.1.18 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.18 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.18 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.18 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.18 + '@tailwindcss/oxide-linux-x64-musl': 4.1.18 + '@tailwindcss/oxide-wasm32-wasi': 4.1.18 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 + + '@tailwindcss/vite@4.1.18(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@tailwindcss/node': 4.1.18 + '@tailwindcss/oxide': 4.1.18 + tailwindcss: 4.1.18 + vite: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + + '@tanstack/devtools-client@0.0.5': + dependencies: + '@tanstack/devtools-event-client': 0.4.0 '@tanstack/devtools-event-bus@0.3.3': dependencies: @@ -8663,8 +9442,6 @@ snapshots: - bufferutil - utf-8-validate - '@tanstack/devtools-event-client@0.3.5': {} - '@tanstack/devtools-event-client@0.4.0': {} '@tanstack/devtools-ui@0.4.4(csstype@3.2.3)(solid-js@1.9.10)': @@ -8675,41 +9452,41 @@ snapshots: transitivePeerDependencies: - csstype - '@tanstack/devtools-utils@0.0.8(@types/react@19.2.7)(csstype@3.2.3)(react@19.2.0)(solid-js@1.9.10)(vue@3.5.25(typescript@5.9.3))': + '@tanstack/devtools-utils@0.0.8(@types/react@19.2.7)(csstype@3.2.3)(react@19.2.3)(solid-js@1.9.10)(vue@3.5.25(typescript@5.9.3))': dependencies: '@tanstack/devtools-ui': 0.4.4(csstype@3.2.3)(solid-js@1.9.10) optionalDependencies: '@types/react': 19.2.7 - react: 19.2.0 + react: 19.2.3 solid-js: 1.9.10 vue: 3.5.25(typescript@5.9.3) transitivePeerDependencies: - csstype - '@tanstack/devtools-vite@0.3.11(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + '@tanstack/devtools-vite@0.3.12(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@babel/core': 7.28.5 '@babel/generator': 7.28.5 '@babel/parser': 7.28.5 '@babel/traverse': 7.28.5 '@babel/types': 7.28.5 - '@tanstack/devtools-client': 0.0.4 + '@tanstack/devtools-client': 0.0.5 '@tanstack/devtools-event-bus': 0.3.3 chalk: 5.6.2 launch-editor: 2.12.0 picomatch: 4.0.3 - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - '@tanstack/devtools@0.8.2(csstype@3.2.3)(solid-js@1.9.10)': + '@tanstack/devtools@0.9.1(csstype@3.2.3)(solid-js@1.9.10)': dependencies: '@solid-primitives/event-listener': 2.4.3(solid-js@1.9.10) '@solid-primitives/keyboard': 1.3.3(solid-js@1.9.10) '@solid-primitives/resize-observer': 2.1.3(solid-js@1.9.10) - '@tanstack/devtools-client': 0.0.4 + '@tanstack/devtools-client': 0.0.5 '@tanstack/devtools-event-bus': 0.3.3 '@tanstack/devtools-ui': 0.4.4(csstype@3.2.3)(solid-js@1.9.10) clsx: 2.1.1 @@ -8720,28 +9497,41 @@ snapshots: - csstype - utf-8-validate - '@tanstack/directive-functions-plugin@1.139.0(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + '@tanstack/directive-functions-plugin@1.131.2(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/core': 7.28.5 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + '@tanstack/router-utils': 1.131.2 + babel-dead-code-elimination: 1.0.10 + tiny-invariant: 1.3.3 + vite: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + transitivePeerDependencies: + - supports-color + + '@tanstack/directive-functions-plugin@1.141.0(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@babel/code-frame': 7.27.1 '@babel/core': 7.28.5 '@babel/traverse': 7.28.5 '@babel/types': 7.28.5 - '@tanstack/router-utils': 1.139.0 + '@tanstack/router-utils': 1.141.0 babel-dead-code-elimination: 1.0.10 pathe: 2.0.3 tiny-invariant: 1.3.3 - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color - '@tanstack/eslint-config@0.3.3(@typescript-eslint/utils@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@tanstack/eslint-config@0.3.3(@typescript-eslint/utils@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint/js': 9.39.1 - '@stylistic/eslint-plugin': 5.5.0(eslint@9.39.1(jiti@2.6.1)) - eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)) + '@stylistic/eslint-plugin': 5.6.1(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-n: 17.23.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) globals: 16.5.0 - typescript-eslint: 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + typescript-eslint: 8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) vue-eslint-parser: 10.2.0(eslint@9.39.1(jiti@2.6.1)) transitivePeerDependencies: - '@typescript-eslint/utils' @@ -8750,13 +9540,15 @@ snapshots: - supports-color - typescript - '@tanstack/history@1.139.0': {} + '@tanstack/history@1.131.2': {} + + '@tanstack/history@1.141.0': {} - '@tanstack/nitro-v2-vite-plugin@1.139.0(rolldown@1.0.0-beta.53)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + '@tanstack/nitro-v2-vite-plugin@1.141.0(rolldown@1.0.0-beta.53)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: nitropack: 2.12.9(rolldown@1.0.0-beta.53) pathe: 2.0.3 - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -8788,216 +9580,258 @@ snapshots: - uploadthing - xml2js - '@tanstack/query-core@5.90.11': {} + '@tanstack/query-core@5.90.12': {} - '@tanstack/query-core@5.90.5': {} - - '@tanstack/react-devtools@0.8.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(csstype@3.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(solid-js@1.9.10)': + '@tanstack/react-devtools@0.8.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@1.9.10)': dependencies: - '@tanstack/devtools': 0.8.2(csstype@3.2.3)(solid-js@1.9.10) + '@tanstack/devtools': 0.9.1(csstype@3.2.3)(solid-js@1.9.10) '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) transitivePeerDependencies: - bufferutil - csstype - solid-js - utf-8-validate - '@tanstack/react-query@5.90.5(react@19.2.0)': + '@tanstack/react-query@5.90.12(react@19.2.3)': dependencies: - '@tanstack/query-core': 5.90.5 - react: 19.2.0 + '@tanstack/query-core': 5.90.12 + react: 19.2.3 - '@tanstack/react-router-devtools@1.139.7(@tanstack/react-router@1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.139.10)(@types/node@24.10.1)(csstype@3.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(solid-js@1.9.10)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)': + '@tanstack/react-router-devtools@1.141.1(@tanstack/react-router@1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.141.1)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@1.9.10)': dependencies: - '@tanstack/react-router': 1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@tanstack/router-devtools-core': 1.139.7(@tanstack/router-core@1.139.10)(@types/node@24.10.1)(csstype@3.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(solid-js@1.9.10)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + '@tanstack/react-router': 1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/router-devtools-core': 1.141.1(@tanstack/router-core@1.141.1)(csstype@3.2.3)(solid-js@1.9.10) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@tanstack/router-core': 1.139.10 + '@tanstack/router-core': 1.141.1 transitivePeerDependencies: - - '@types/node' - csstype - - jiti - - less - - lightningcss - - sass - - sass-embedded - solid-js - - stylus - - sugarss - - terser - - tsx - - yaml - '@tanstack/react-router-ssr-query@1.139.7(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.5(react@19.2.0))(@tanstack/react-router@1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.139.10)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@tanstack/react-router-ssr-query@1.141.1(@tanstack/query-core@5.90.12)(@tanstack/react-query@5.90.12(react@19.2.3))(@tanstack/react-router@1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.141.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@tanstack/query-core': 5.90.11 - '@tanstack/react-query': 5.90.5(react@19.2.0) - '@tanstack/react-router': 1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@tanstack/router-ssr-query-core': 1.139.7(@tanstack/query-core@5.90.11)(@tanstack/router-core@1.139.10) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@tanstack/query-core': 5.90.12 + '@tanstack/react-query': 5.90.12(react@19.2.3) + '@tanstack/react-router': 1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/router-ssr-query-core': 1.141.1(@tanstack/query-core@5.90.12)(@tanstack/router-core@1.141.1) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) transitivePeerDependencies: - '@tanstack/router-core' - '@tanstack/react-router@1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@tanstack/react-router@1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@tanstack/history': 1.139.0 - '@tanstack/react-store': 0.8.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@tanstack/router-core': 1.139.7 - isbot: 5.1.31 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@tanstack/history': 1.141.0 + '@tanstack/react-store': 0.8.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/router-core': 1.141.1 + isbot: 5.1.32 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@tanstack/react-start-client@1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@tanstack/react-start-client@1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@tanstack/react-router': 1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@tanstack/router-core': 1.139.7 - '@tanstack/start-client-core': 1.139.7 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@tanstack/react-router': 1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/router-core': 1.141.1 + '@tanstack/start-client-core': 1.141.1 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@tanstack/react-start-server@1.139.8(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@tanstack/history': 1.139.0 - '@tanstack/react-router': 1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@tanstack/router-core': 1.139.7 - '@tanstack/start-client-core': 1.139.7 - '@tanstack/start-server-core': 1.139.8 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - transitivePeerDependencies: - - crossws - - '@tanstack/react-start@1.139.8(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + '@tanstack/react-start-plugin@1.131.50(@tanstack/react-router@1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@vitejs/plugin-react@4.7.0(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(rolldown@1.0.0-beta.53)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@tanstack/react-router': 1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@tanstack/react-start-client': 1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@tanstack/react-start-server': 1.139.8(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@tanstack/router-utils': 1.139.0 - '@tanstack/start-client-core': 1.139.7 - '@tanstack/start-plugin-core': 1.139.8(@tanstack/react-router@1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) - '@tanstack/start-server-core': 1.139.8 + '@tanstack/start-plugin-core': 1.131.50(@tanstack/react-router@1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(rolldown@1.0.0-beta.53)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@vitejs/plugin-react': 4.7.0(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) pathe: 2.0.3 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + zod: 3.25.76 transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@electric-sql/pglite' + - '@libsql/client' + - '@netlify/blobs' + - '@planetscale/database' - '@rsbuild/core' - - crossws + - '@tanstack/react-router' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bare-abort-controller + - better-sqlite3 + - drizzle-orm + - encoding + - idb-keyval + - mysql2 + - react-native-b4a + - rolldown + - sqlite3 - supports-color + - uploadthing - vite-plugin-solid - webpack + - xml2js - '@tanstack/react-store@0.8.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@tanstack/store': 0.8.0 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - use-sync-external-store: 1.6.0(react@19.2.0) - - '@tanstack/router-core@1.139.10': - dependencies: - '@tanstack/history': 1.139.0 - '@tanstack/store': 0.8.0 - cookie-es: 2.0.0 - seroval: 1.4.0 - seroval-plugins: 1.4.0(seroval@1.4.0) - tiny-invariant: 1.3.3 - tiny-warning: 1.0.3 - - '@tanstack/router-core@1.139.7': - dependencies: - '@tanstack/history': 1.139.0 - '@tanstack/store': 0.8.0 - cookie-es: 2.0.0 - seroval: 1.4.0 - seroval-plugins: 1.4.0(seroval@1.4.0) - tiny-invariant: 1.3.3 - tiny-warning: 1.0.3 - - '@tanstack/router-devtools-core@1.139.10(@tanstack/router-core@1.139.10)(@types/node@24.10.1)(csstype@3.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(solid-js@1.9.10)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)': + '@tanstack/react-start-router-manifest@1.120.19(@types/node@24.10.3)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rolldown@1.0.0-beta.53)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)': dependencies: - '@tanstack/router-core': 1.139.10 - clsx: 2.1.1 - goober: 2.1.18(csstype@3.2.3) - solid-js: 1.9.10 + '@tanstack/router-core': 1.141.1 tiny-invariant: 1.3.3 - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) - optionalDependencies: - csstype: 3.2.3 + vinxi: 0.5.3(@types/node@24.10.3)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rolldown@1.0.0-beta.53)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - - '@types/node' - - jiti - - less + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@electric-sql/pglite' + - '@libsql/client' + - '@netlify/blobs' + - '@planetscale/database' + - '@types/node' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bare-abort-controller + - better-sqlite3 + - db0 + - debug + - drizzle-orm + - encoding + - idb-keyval + - ioredis + - jiti + - less - lightningcss + - mysql2 + - react-native-b4a + - rolldown - sass - sass-embedded + - sqlite3 - stylus - sugarss + - supports-color - terser - tsx + - uploadthing + - xml2js - yaml - '@tanstack/router-devtools-core@1.139.7(@tanstack/router-core@1.139.10)(@types/node@24.10.1)(csstype@3.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(solid-js@1.9.10)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)': + '@tanstack/react-start-server@1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@tanstack/history': 1.141.0 + '@tanstack/react-router': 1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/router-core': 1.141.1 + '@tanstack/start-client-core': 1.141.1 + '@tanstack/start-server-core': 1.141.1 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + transitivePeerDependencies: + - crossws + + '@tanstack/react-start@1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@tanstack/react-router': 1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/react-start-client': 1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/react-start-server': 1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/router-utils': 1.141.0 + '@tanstack/start-client-core': 1.141.1 + '@tanstack/start-plugin-core': 1.141.1(@tanstack/react-router@1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@tanstack/start-server-core': 1.141.1 + pathe: 2.0.3 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + vite: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + transitivePeerDependencies: + - '@rsbuild/core' + - crossws + - supports-color + - vite-plugin-solid + - webpack + + '@tanstack/react-store@0.8.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@tanstack/store': 0.8.0 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + use-sync-external-store: 1.6.0(react@19.2.3) + + '@tanstack/router-core@1.131.50': + dependencies: + '@tanstack/history': 1.131.2 + '@tanstack/store': 0.7.7 + cookie-es: 1.2.2 + seroval: 1.4.0 + seroval-plugins: 1.4.0(seroval@1.4.0) + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + + '@tanstack/router-core@1.141.1': + dependencies: + '@tanstack/history': 1.141.0 + '@tanstack/store': 0.8.0 + cookie-es: 2.0.0 + seroval: 1.4.0 + seroval-plugins: 1.4.0(seroval@1.4.0) + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + + '@tanstack/router-devtools-core@1.141.1(@tanstack/router-core@1.141.1)(csstype@3.2.3)(solid-js@1.9.10)': dependencies: - '@tanstack/router-core': 1.139.10 + '@tanstack/router-core': 1.141.1 clsx: 2.1.1 goober: 2.1.18(csstype@3.2.3) solid-js: 1.9.10 tiny-invariant: 1.3.3 - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) optionalDependencies: csstype: 3.2.3 - transitivePeerDependencies: - - '@types/node' - - jiti - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - terser - - tsx - - yaml - '@tanstack/router-generator@1.139.10': + '@tanstack/router-generator@1.131.50': dependencies: - '@tanstack/router-core': 1.139.10 - '@tanstack/router-utils': 1.139.0 - '@tanstack/virtual-file-routes': 1.139.0 + '@tanstack/router-core': 1.131.50 + '@tanstack/router-utils': 1.131.2 + '@tanstack/virtual-file-routes': 1.131.2 prettier: 3.7.4 recast: 0.23.11 source-map: 0.7.6 - tsx: 4.20.6 + tsx: 4.21.0 zod: 3.25.76 transitivePeerDependencies: - supports-color - '@tanstack/router-generator@1.139.7': + '@tanstack/router-generator@1.141.1': dependencies: - '@tanstack/router-core': 1.139.7 - '@tanstack/router-utils': 1.139.0 - '@tanstack/virtual-file-routes': 1.139.0 + '@tanstack/router-core': 1.141.1 + '@tanstack/router-utils': 1.141.0 + '@tanstack/virtual-file-routes': 1.141.0 prettier: 3.7.4 recast: 0.23.11 source-map: 0.7.6 - tsx: 4.20.6 + tsx: 4.21.0 zod: 3.25.76 transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.139.10(@tanstack/react-router@1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + '@tanstack/router-plugin@1.131.50(@tanstack/react-router@1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@babel/core': 7.28.5 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5) @@ -9005,22 +9839,22 @@ snapshots: '@babel/template': 7.27.2 '@babel/traverse': 7.28.5 '@babel/types': 7.28.5 - '@tanstack/router-core': 1.139.10 - '@tanstack/router-generator': 1.139.10 - '@tanstack/router-utils': 1.139.0 - '@tanstack/virtual-file-routes': 1.139.0 + '@tanstack/router-core': 1.131.50 + '@tanstack/router-generator': 1.131.50 + '@tanstack/router-utils': 1.131.2 + '@tanstack/virtual-file-routes': 1.131.2 babel-dead-code-elimination: 1.0.10 chokidar: 3.6.0 - unplugin: 2.3.10 + unplugin: 2.3.11 zod: 3.25.76 optionalDependencies: - '@tanstack/react-router': 1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) - vite-plugin-solid: 2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + '@tanstack/react-router': 1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + vite: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite-plugin-solid: 2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.139.7(@tanstack/react-router@1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + '@tanstack/router-plugin@1.141.1(@tanstack/react-router@1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@babel/core': 7.28.5 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5) @@ -9028,32 +9862,38 @@ snapshots: '@babel/template': 7.27.2 '@babel/traverse': 7.28.5 '@babel/types': 7.28.5 - '@tanstack/router-core': 1.139.7 - '@tanstack/router-generator': 1.139.7 - '@tanstack/router-utils': 1.139.0 - '@tanstack/virtual-file-routes': 1.139.0 + '@tanstack/router-core': 1.141.1 + '@tanstack/router-generator': 1.141.1 + '@tanstack/router-utils': 1.141.0 + '@tanstack/virtual-file-routes': 1.141.0 babel-dead-code-elimination: 1.0.10 chokidar: 3.6.0 - unplugin: 2.3.10 + unplugin: 2.3.11 zod: 3.25.76 optionalDependencies: - '@tanstack/react-router': 1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) - vite-plugin-solid: 2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + '@tanstack/react-router': 1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + vite: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite-plugin-solid: 2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) transitivePeerDependencies: - supports-color - '@tanstack/router-ssr-query-core@1.139.10(@tanstack/query-core@5.90.11)(@tanstack/router-core@1.139.10)': + '@tanstack/router-ssr-query-core@1.141.1(@tanstack/query-core@5.90.12)(@tanstack/router-core@1.141.1)': dependencies: - '@tanstack/query-core': 5.90.11 - '@tanstack/router-core': 1.139.10 + '@tanstack/query-core': 5.90.12 + '@tanstack/router-core': 1.141.1 - '@tanstack/router-ssr-query-core@1.139.7(@tanstack/query-core@5.90.11)(@tanstack/router-core@1.139.10)': + '@tanstack/router-utils@1.131.2': dependencies: - '@tanstack/query-core': 5.90.11 - '@tanstack/router-core': 1.139.10 + '@babel/core': 7.28.5 + '@babel/generator': 7.28.5 + '@babel/parser': 7.28.5 + '@babel/preset-typescript': 7.28.5(@babel/core@7.28.5) + ansis: 4.2.0 + diff: 8.0.2 + transitivePeerDependencies: + - supports-color - '@tanstack/router-utils@1.139.0': + '@tanstack/router-utils@1.141.0': dependencies: '@babel/core': 7.28.5 '@babel/generator': 7.28.5 @@ -9066,7 +9906,23 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/server-functions-plugin@1.139.0(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + '@tanstack/server-functions-plugin@1.131.2(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/core': 7.28.5 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.5) + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + '@tanstack/directive-functions-plugin': 1.131.2(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + babel-dead-code-elimination: 1.0.10 + tiny-invariant: 1.3.3 + transitivePeerDependencies: + - supports-color + - vite + + '@tanstack/server-functions-plugin@1.141.0(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@babel/code-frame': 7.27.1 '@babel/core': 7.28.5 @@ -9075,55 +9931,43 @@ snapshots: '@babel/template': 7.27.2 '@babel/traverse': 7.28.5 '@babel/types': 7.28.5 - '@tanstack/directive-functions-plugin': 1.139.0(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + '@tanstack/directive-functions-plugin': 1.141.0(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) babel-dead-code-elimination: 1.0.10 tiny-invariant: 1.3.3 transitivePeerDependencies: - supports-color - vite - '@tanstack/solid-devtools@0.7.15(csstype@3.2.3)(solid-js@1.9.10)': + '@tanstack/solid-devtools@0.7.17(csstype@3.2.3)(solid-js@1.9.10)': dependencies: - '@tanstack/devtools': 0.8.2(csstype@3.2.3)(solid-js@1.9.10) + '@tanstack/devtools': 0.9.1(csstype@3.2.3)(solid-js@1.9.10) solid-js: 1.9.10 transitivePeerDependencies: - bufferutil - csstype - utf-8-validate - '@tanstack/solid-query@5.90.14(solid-js@1.9.10)': + '@tanstack/solid-query@5.90.15(solid-js@1.9.10)': dependencies: - '@tanstack/query-core': 5.90.11 + '@tanstack/query-core': 5.90.12 solid-js: 1.9.10 - '@tanstack/solid-router-devtools@1.139.10(@tanstack/router-core@1.139.10)(@tanstack/solid-router@1.139.10(solid-js@1.9.10))(@types/node@24.10.1)(csstype@3.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(solid-js@1.9.10)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)': + '@tanstack/solid-router-devtools@1.141.1(@tanstack/router-core@1.141.1)(@tanstack/solid-router@1.141.1(solid-js@1.9.10))(csstype@3.2.3)(solid-js@1.9.10)': dependencies: - '@tanstack/router-devtools-core': 1.139.10(@tanstack/router-core@1.139.10)(@types/node@24.10.1)(csstype@3.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(solid-js@1.9.10)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) - '@tanstack/solid-router': 1.139.10(solid-js@1.9.10) + '@tanstack/router-devtools-core': 1.141.1(@tanstack/router-core@1.141.1)(csstype@3.2.3)(solid-js@1.9.10) + '@tanstack/solid-router': 1.141.1(solid-js@1.9.10) solid-js: 1.9.10 - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) optionalDependencies: - '@tanstack/router-core': 1.139.10 + '@tanstack/router-core': 1.141.1 transitivePeerDependencies: - - '@types/node' - csstype - - jiti - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - terser - - tsx - - yaml - '@tanstack/solid-router-ssr-query@1.139.10(@tanstack/query-core@5.90.11)(@tanstack/router-core@1.139.10)(@tanstack/solid-query@5.90.14(solid-js@1.9.10))(@tanstack/solid-router@1.139.10(solid-js@1.9.10))(eslint@9.39.1(jiti@2.6.1))(solid-js@1.9.10)(typescript@5.9.3)': + '@tanstack/solid-router-ssr-query@1.141.1(@tanstack/query-core@5.90.12)(@tanstack/router-core@1.141.1)(@tanstack/solid-query@5.90.15(solid-js@1.9.10))(@tanstack/solid-router@1.141.1(solid-js@1.9.10))(eslint@9.39.1(jiti@2.6.1))(solid-js@1.9.10)(typescript@5.9.3)': dependencies: - '@tanstack/query-core': 5.90.11 - '@tanstack/router-ssr-query-core': 1.139.10(@tanstack/query-core@5.90.11)(@tanstack/router-core@1.139.10) - '@tanstack/solid-query': 5.90.14(solid-js@1.9.10) - '@tanstack/solid-router': 1.139.10(solid-js@1.9.10) + '@tanstack/query-core': 5.90.12 + '@tanstack/router-ssr-query-core': 1.141.1(@tanstack/query-core@5.90.12)(@tanstack/router-core@1.141.1) + '@tanstack/solid-query': 5.90.15(solid-js@1.9.10) + '@tanstack/solid-router': 1.141.1(solid-js@1.9.10) eslint-plugin-solid: 0.14.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) solid-js: 1.9.10 transitivePeerDependencies: @@ -9132,51 +9976,51 @@ snapshots: - supports-color - typescript - '@tanstack/solid-router@1.139.10(solid-js@1.9.10)': + '@tanstack/solid-router@1.141.1(solid-js@1.9.10)': dependencies: '@solid-devtools/logger': 0.9.11(solid-js@1.9.10) '@solid-primitives/refs': 1.1.2(solid-js@1.9.10) '@solidjs/meta': 0.29.4(solid-js@1.9.10) - '@tanstack/history': 1.139.0 - '@tanstack/router-core': 1.139.10 + '@tanstack/history': 1.141.0 + '@tanstack/router-core': 1.141.1 '@tanstack/solid-store': 0.8.0(solid-js@1.9.10) - isbot: 5.1.31 + isbot: 5.1.32 solid-js: 1.9.10 tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@tanstack/solid-start-client@1.139.10(solid-js@1.9.10)': + '@tanstack/solid-start-client@1.141.1(solid-js@1.9.10)': dependencies: - '@tanstack/router-core': 1.139.10 - '@tanstack/solid-router': 1.139.10(solid-js@1.9.10) - '@tanstack/start-client-core': 1.139.10 + '@tanstack/router-core': 1.141.1 + '@tanstack/solid-router': 1.141.1(solid-js@1.9.10) + '@tanstack/start-client-core': 1.141.1 solid-js: 1.9.10 tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@tanstack/solid-start-server@1.139.10(solid-js@1.9.10)': + '@tanstack/solid-start-server@1.141.1(solid-js@1.9.10)': dependencies: '@solidjs/meta': 0.29.4(solid-js@1.9.10) - '@tanstack/history': 1.139.0 - '@tanstack/router-core': 1.139.10 - '@tanstack/solid-router': 1.139.10(solid-js@1.9.10) - '@tanstack/start-client-core': 1.139.10 - '@tanstack/start-server-core': 1.139.10 + '@tanstack/history': 1.141.0 + '@tanstack/router-core': 1.141.1 + '@tanstack/solid-router': 1.141.1(solid-js@1.9.10) + '@tanstack/start-client-core': 1.141.1 + '@tanstack/start-server-core': 1.141.1 solid-js: 1.9.10 transitivePeerDependencies: - crossws - '@tanstack/solid-start@1.139.10(solid-js@1.9.10)(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + '@tanstack/solid-start@1.141.1(@tanstack/react-router@1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(solid-js@1.9.10)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@tanstack/solid-router': 1.139.10(solid-js@1.9.10) - '@tanstack/solid-start-client': 1.139.10(solid-js@1.9.10) - '@tanstack/solid-start-server': 1.139.10(solid-js@1.9.10) - '@tanstack/start-client-core': 1.139.10 - '@tanstack/start-plugin-core': 1.139.10(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) - '@tanstack/start-server-core': 1.139.10 + '@tanstack/solid-router': 1.141.1(solid-js@1.9.10) + '@tanstack/solid-start-client': 1.141.1(solid-js@1.9.10) + '@tanstack/solid-start-server': 1.141.1(solid-js@1.9.10) + '@tanstack/start-client-core': 1.141.1 + '@tanstack/start-plugin-core': 1.141.1(@tanstack/react-router@1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@tanstack/start-server-core': 1.141.1 pathe: 2.0.3 solid-js: 1.9.10 - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - '@rsbuild/core' - '@tanstack/react-router' @@ -9190,77 +10034,220 @@ snapshots: '@tanstack/store': 0.8.0 solid-js: 1.9.10 - '@tanstack/start-client-core@1.139.10': + '@tanstack/start-api-routes@1.120.19(@types/node@24.10.3)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rolldown@1.0.0-beta.53)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)': dependencies: - '@tanstack/router-core': 1.139.10 - '@tanstack/start-storage-context': 1.139.10 - seroval: 1.4.0 + '@tanstack/router-core': 1.141.1 + '@tanstack/start-server-core': 1.141.1 + vinxi: 0.5.3(@types/node@24.10.3)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rolldown@1.0.0-beta.53)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@electric-sql/pglite' + - '@libsql/client' + - '@netlify/blobs' + - '@planetscale/database' + - '@types/node' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bare-abort-controller + - better-sqlite3 + - crossws + - db0 + - debug + - drizzle-orm + - encoding + - idb-keyval + - ioredis + - jiti + - less + - lightningcss + - mysql2 + - react-native-b4a + - rolldown + - sass + - sass-embedded + - sqlite3 + - stylus + - sugarss + - supports-color + - terser + - tsx + - uploadthing + - xml2js + - yaml + + '@tanstack/start-client-core@1.131.50': + dependencies: + '@tanstack/router-core': 1.131.50 + '@tanstack/start-storage-context': 1.131.50 + cookie-es: 1.2.2 tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@tanstack/start-client-core@1.139.7': + '@tanstack/start-client-core@1.141.1': dependencies: - '@tanstack/router-core': 1.139.7 - '@tanstack/start-storage-context': 1.139.7 + '@tanstack/router-core': 1.141.1 + '@tanstack/start-storage-context': 1.141.1 seroval: 1.4.0 tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@tanstack/start-plugin-core@1.139.10(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + '@tanstack/start-config@1.120.20(@types/node@24.10.3)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown@1.0.0-beta.53)(terser@5.44.1)(tsx@4.21.0)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2)': + dependencies: + '@tanstack/react-router': 1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/react-start-plugin': 1.131.50(@tanstack/react-router@1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@vitejs/plugin-react@4.7.0(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(rolldown@1.0.0-beta.53)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@tanstack/router-generator': 1.141.1 + '@tanstack/router-plugin': 1.141.1(@tanstack/react-router@1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@tanstack/server-functions-plugin': 1.141.0(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@tanstack/start-server-functions-handler': 1.120.19 + '@vitejs/plugin-react': 4.7.0(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + import-meta-resolve: 4.2.0 + nitropack: 2.12.9(rolldown@1.0.0-beta.53) + ofetch: 1.5.1 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + vinxi: 0.5.3(@types/node@24.10.3)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rolldown@1.0.0-beta.53)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + zod: 3.25.76 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@electric-sql/pglite' + - '@libsql/client' + - '@netlify/blobs' + - '@planetscale/database' + - '@rsbuild/core' + - '@types/node' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bare-abort-controller + - better-sqlite3 + - crossws + - db0 + - debug + - drizzle-orm + - encoding + - idb-keyval + - ioredis + - jiti + - less + - lightningcss + - mysql2 + - react-native-b4a + - rolldown + - sass + - sass-embedded + - sqlite3 + - stylus + - sugarss + - supports-color + - terser + - tsx + - uploadthing + - vite-plugin-solid + - webpack + - xml2js + - yaml + + '@tanstack/start-plugin-core@1.131.50(@tanstack/react-router@1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(rolldown@1.0.0-beta.53)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@babel/code-frame': 7.26.2 '@babel/core': 7.28.5 '@babel/types': 7.28.5 - '@rolldown/pluginutils': 1.0.0-beta.40 - '@tanstack/router-core': 1.139.10 - '@tanstack/router-generator': 1.139.10 - '@tanstack/router-plugin': 1.139.10(@tanstack/react-router@1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) - '@tanstack/router-utils': 1.139.0 - '@tanstack/server-functions-plugin': 1.139.0(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) - '@tanstack/start-client-core': 1.139.10 - '@tanstack/start-server-core': 1.139.10 + '@tanstack/router-core': 1.131.50 + '@tanstack/router-generator': 1.131.50 + '@tanstack/router-plugin': 1.131.50(@tanstack/react-router@1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@tanstack/router-utils': 1.131.2 + '@tanstack/server-functions-plugin': 1.131.2(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@tanstack/start-server-core': 1.131.50 + '@types/babel__code-frame': 7.0.6 + '@types/babel__core': 7.20.5 babel-dead-code-elimination: 1.0.10 cheerio: 1.1.2 - exsolve: 1.0.7 + h3: 1.13.0 + nitropack: 2.12.9(rolldown@1.0.0-beta.53) pathe: 2.0.3 - srvx: 0.8.16 - tinyglobby: 0.2.15 ufo: 1.6.1 - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) - vitefu: 1.1.1(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) - xmlbuilder2: 4.0.1 + vite: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vitefu: 1.1.1(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + xmlbuilder2: 3.1.1 zod: 3.25.76 transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@electric-sql/pglite' + - '@libsql/client' + - '@netlify/blobs' + - '@planetscale/database' - '@rsbuild/core' - '@tanstack/react-router' - - crossws + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bare-abort-controller + - better-sqlite3 + - drizzle-orm + - encoding + - idb-keyval + - mysql2 + - react-native-b4a + - rolldown + - sqlite3 - supports-color + - uploadthing - vite-plugin-solid - webpack + - xml2js - '@tanstack/start-plugin-core@1.139.8(@tanstack/react-router@1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + '@tanstack/start-plugin-core@1.141.1(@tanstack/react-router@1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@babel/code-frame': 7.26.2 '@babel/core': 7.28.5 '@babel/types': 7.28.5 '@rolldown/pluginutils': 1.0.0-beta.40 - '@tanstack/router-core': 1.139.7 - '@tanstack/router-generator': 1.139.7 - '@tanstack/router-plugin': 1.139.7(@tanstack/react-router@1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) - '@tanstack/router-utils': 1.139.0 - '@tanstack/server-functions-plugin': 1.139.0(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) - '@tanstack/start-client-core': 1.139.7 - '@tanstack/start-server-core': 1.139.8 + '@tanstack/router-core': 1.141.1 + '@tanstack/router-generator': 1.141.1 + '@tanstack/router-plugin': 1.141.1(@tanstack/react-router@1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@tanstack/router-utils': 1.141.0 + '@tanstack/server-functions-plugin': 1.141.0(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@tanstack/start-client-core': 1.141.1 + '@tanstack/start-server-core': 1.141.1 babel-dead-code-elimination: 1.0.10 cheerio: 1.1.2 - exsolve: 1.0.7 + exsolve: 1.0.8 pathe: 2.0.3 srvx: 0.8.16 tinyglobby: 0.2.15 ufo: 1.6.1 - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) - vitefu: 1.1.1(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) - xmlbuilder2: 4.0.1 + vite: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vitefu: 1.1.1(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + xmlbuilder2: 4.0.3 zod: 3.25.76 transitivePeerDependencies: - '@rsbuild/core' @@ -9270,37 +10257,144 @@ snapshots: - vite-plugin-solid - webpack - '@tanstack/start-server-core@1.139.10': + '@tanstack/start-server-core@1.131.50': + dependencies: + '@tanstack/history': 1.131.2 + '@tanstack/router-core': 1.131.50 + '@tanstack/start-client-core': 1.131.50 + '@tanstack/start-storage-context': 1.131.50 + h3: 1.13.0 + isbot: 5.1.32 + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + unctx: 2.4.1 + + '@tanstack/start-server-core@1.141.1': dependencies: - '@tanstack/history': 1.139.0 - '@tanstack/router-core': 1.139.10 - '@tanstack/start-client-core': 1.139.10 - '@tanstack/start-storage-context': 1.139.10 + '@tanstack/history': 1.141.0 + '@tanstack/router-core': 1.141.1 + '@tanstack/start-client-core': 1.141.1 + '@tanstack/start-storage-context': 1.141.1 h3-v2: h3@2.0.0-beta.5 seroval: 1.4.0 tiny-invariant: 1.3.3 transitivePeerDependencies: - crossws - '@tanstack/start-server-core@1.139.8': + '@tanstack/start-server-functions-client@1.131.50(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@tanstack/history': 1.139.0 - '@tanstack/router-core': 1.139.7 - '@tanstack/start-client-core': 1.139.7 - '@tanstack/start-storage-context': 1.139.7 - h3-v2: h3@2.0.0-beta.5 - seroval: 1.4.0 + '@tanstack/server-functions-plugin': 1.131.2(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@tanstack/start-server-functions-fetcher': 1.131.50 + transitivePeerDependencies: + - supports-color + - vite + + '@tanstack/start-server-functions-fetcher@1.131.50': + dependencies: + '@tanstack/router-core': 1.131.50 + '@tanstack/start-client-core': 1.131.50 + + '@tanstack/start-server-functions-handler@1.120.19': + dependencies: + '@tanstack/router-core': 1.141.1 + '@tanstack/start-client-core': 1.141.1 + '@tanstack/start-server-core': 1.141.1 + tiny-invariant: 1.3.3 + transitivePeerDependencies: + - crossws + + '@tanstack/start-server-functions-server@1.131.2(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@tanstack/server-functions-plugin': 1.131.2(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + tiny-invariant: 1.3.3 + transitivePeerDependencies: + - supports-color + - vite + + '@tanstack/start-server-functions-ssr@1.120.19(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@tanstack/server-functions-plugin': 1.141.0(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@tanstack/start-client-core': 1.141.1 + '@tanstack/start-server-core': 1.141.1 + '@tanstack/start-server-functions-fetcher': 1.131.50 tiny-invariant: 1.3.3 transitivePeerDependencies: - crossws + - supports-color + - vite - '@tanstack/start-storage-context@1.139.10': + '@tanstack/start-storage-context@1.131.50': dependencies: - '@tanstack/router-core': 1.139.10 + '@tanstack/router-core': 1.131.50 - '@tanstack/start-storage-context@1.139.7': + '@tanstack/start-storage-context@1.141.1': dependencies: - '@tanstack/router-core': 1.139.7 + '@tanstack/router-core': 1.141.1 + + '@tanstack/start@1.120.20(@types/node@24.10.3)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown@1.0.0-beta.53)(terser@5.44.1)(tsx@4.21.0)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2)': + dependencies: + '@tanstack/react-start-client': 1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/react-start-router-manifest': 1.120.19(@types/node@24.10.3)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rolldown@1.0.0-beta.53)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + '@tanstack/react-start-server': 1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/start-api-routes': 1.120.19(@types/node@24.10.3)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rolldown@1.0.0-beta.53)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + '@tanstack/start-config': 1.120.20(@types/node@24.10.3)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown@1.0.0-beta.53)(terser@5.44.1)(tsx@4.21.0)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2) + '@tanstack/start-server-functions-client': 1.131.50(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@tanstack/start-server-functions-handler': 1.120.19 + '@tanstack/start-server-functions-server': 1.131.2(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@tanstack/start-server-functions-ssr': 1.120.19(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@electric-sql/pglite' + - '@libsql/client' + - '@netlify/blobs' + - '@planetscale/database' + - '@rsbuild/core' + - '@types/node' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bare-abort-controller + - better-sqlite3 + - crossws + - db0 + - debug + - drizzle-orm + - encoding + - idb-keyval + - ioredis + - jiti + - less + - lightningcss + - mysql2 + - react + - react-dom + - react-native-b4a + - rolldown + - sass + - sass-embedded + - sqlite3 + - stylus + - sugarss + - supports-color + - terser + - tsx + - uploadthing + - vite + - vite-plugin-solid + - webpack + - xml2js + - yaml + + '@tanstack/store@0.7.7': {} '@tanstack/store@0.8.0': {} @@ -9312,14 +10406,16 @@ snapshots: transitivePeerDependencies: - typescript - '@tanstack/virtual-file-routes@1.139.0': {} + '@tanstack/virtual-file-routes@1.131.2': {} - '@tanstack/vite-config@0.4.1(@types/node@24.10.1)(rollup@4.53.3)(typescript@5.9.3)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + '@tanstack/virtual-file-routes@1.141.0': {} + + '@tanstack/vite-config@0.4.1(@types/node@24.10.3)(rollup@4.53.3)(typescript@5.9.3)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: rollup-plugin-preserve-directives: 0.4.0(rollup@4.53.3) - vite-plugin-dts: 4.2.3(@types/node@24.10.1)(rollup@4.53.3)(typescript@5.9.3)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) - vite-plugin-externalize-deps: 0.10.0(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) - vite-tsconfig-paths: 5.1.4(typescript@5.9.3)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + vite-plugin-dts: 4.2.3(@types/node@24.10.3)(rollup@4.53.3)(typescript@5.9.3)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + vite-plugin-externalize-deps: 0.10.0(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + vite-tsconfig-paths: 5.1.4(typescript@5.9.3)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) transitivePeerDependencies: - '@types/node' - rollup @@ -9338,22 +10434,12 @@ snapshots: picocolors: 1.1.1 pretty-format: 27.5.1 - '@testing-library/jest-dom@6.9.1': - dependencies: - '@adobe/css-tools': 4.4.4 - aria-query: 5.3.2 - css.escape: 1.5.1 - dom-accessibility-api: 0.6.3 - picocolors: 1.1.1 - redent: 3.0.0 - optional: true - - '@testing-library/react@16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@testing-library/react@16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@babel/runtime': 7.28.4 '@testing-library/dom': 10.4.1 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) optionalDependencies: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) @@ -9371,6 +10457,8 @@ snapshots: '@types/aria-query@5.0.4': {} + '@types/babel__code-frame@7.0.6': {} + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.28.5 @@ -9392,6 +10480,8 @@ snapshots: dependencies: '@babel/types': 7.28.5 + '@types/braces@3.0.5': {} + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -9421,18 +10511,27 @@ snapshots: dependencies: '@types/unist': 3.0.3 + '@types/micromatch@4.0.10': + dependencies: + '@types/braces': 3.0.5 + '@types/ms@2.1.0': {} '@types/node@12.20.55': {} - '@types/node@20.19.25': + '@types/node@20.19.26': dependencies: undici-types: 6.21.0 - '@types/node@24.10.1': + '@types/node@24.10.3': dependencies: undici-types: 7.16.0 + '@types/node@25.0.1': + dependencies: + undici-types: 7.16.0 + optional: true + '@types/react-dom@19.2.3(@types/react@19.2.7)': dependencies: '@types/react': 19.2.7 @@ -9451,18 +10550,17 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 24.10.1 + '@types/node': 24.10.3 - '@typescript-eslint/eslint-plugin@8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.46.3 - '@typescript-eslint/type-utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.46.3 + '@typescript-eslint/parser': 8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.49.0 + '@typescript-eslint/type-utils': 8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.49.0 eslint: 9.39.1(jiti@2.6.1) - graphemer: 1.4.0 ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.1.0(typescript@5.9.3) @@ -9470,59 +10568,41 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.46.3 - '@typescript-eslint/types': 8.46.3 - '@typescript-eslint/typescript-estree': 8.46.3(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.46.3 + '@typescript-eslint/scope-manager': 8.49.0 + '@typescript-eslint/types': 8.49.0 + '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.49.0 debug: 4.4.3 eslint: 9.39.1(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.46.3(typescript@5.9.3)': + '@typescript-eslint/project-service@8.49.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.48.0(typescript@5.9.3) - '@typescript-eslint/types': 8.48.0 + '@typescript-eslint/tsconfig-utils': 8.49.0(typescript@5.9.3) + '@typescript-eslint/types': 8.49.0 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.48.0(typescript@5.9.3)': + '@typescript-eslint/scope-manager@8.49.0': dependencies: - '@typescript-eslint/tsconfig-utils': 8.48.0(typescript@5.9.3) - '@typescript-eslint/types': 8.48.0 - debug: 4.4.3 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color + '@typescript-eslint/types': 8.49.0 + '@typescript-eslint/visitor-keys': 8.49.0 - '@typescript-eslint/scope-manager@8.46.3': - dependencies: - '@typescript-eslint/types': 8.46.3 - '@typescript-eslint/visitor-keys': 8.46.3 - - '@typescript-eslint/scope-manager@8.48.0': - dependencies: - '@typescript-eslint/types': 8.48.0 - '@typescript-eslint/visitor-keys': 8.48.0 - - '@typescript-eslint/tsconfig-utils@8.46.3(typescript@5.9.3)': - dependencies: - typescript: 5.9.3 - - '@typescript-eslint/tsconfig-utils@8.48.0(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.49.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.46.3 - '@typescript-eslint/typescript-estree': 8.46.3(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.49.0 + '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) debug: 4.4.3 eslint: 9.39.1(jiti@2.6.1) ts-api-utils: 2.1.0(typescript@5.9.3) @@ -9530,32 +10610,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.46.3': {} - - '@typescript-eslint/types@8.48.0': {} + '@typescript-eslint/types@8.49.0': {} - '@typescript-eslint/typescript-estree@8.46.3(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.49.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.46.3(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.46.3(typescript@5.9.3) - '@typescript-eslint/types': 8.46.3 - '@typescript-eslint/visitor-keys': 8.46.3 - debug: 4.4.3 - fast-glob: 3.3.3 - is-glob: 4.0.3 - minimatch: 9.0.5 - semver: 7.7.3 - ts-api-utils: 2.1.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/typescript-estree@8.48.0(typescript@5.9.3)': - dependencies: - '@typescript-eslint/project-service': 8.48.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.48.0(typescript@5.9.3) - '@typescript-eslint/types': 8.48.0 - '@typescript-eslint/visitor-keys': 8.48.0 + '@typescript-eslint/project-service': 8.49.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.49.0(typescript@5.9.3) + '@typescript-eslint/types': 8.49.0 + '@typescript-eslint/visitor-keys': 8.49.0 debug: 4.4.3 minimatch: 9.0.5 semver: 7.7.3 @@ -9565,36 +10627,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': - dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.46.3 - '@typescript-eslint/types': 8.46.3 - '@typescript-eslint/typescript-estree': 8.46.3(typescript@5.9.3) - eslint: 9.39.1(jiti@2.6.1) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/utils@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.48.0 - '@typescript-eslint/types': 8.48.0 - '@typescript-eslint/typescript-estree': 8.48.0(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.49.0 + '@typescript-eslint/types': 8.49.0 + '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.46.3': - dependencies: - '@typescript-eslint/types': 8.46.3 - eslint-visitor-keys: 4.2.1 - - '@typescript-eslint/visitor-keys@8.48.0': + '@typescript-eslint/visitor-keys@8.49.0': dependencies: - '@typescript-eslint/types': 8.48.0 + '@typescript-eslint/types': 8.49.0 eslint-visitor-keys: 4.2.1 '@ungap/structured-clone@1.3.0': {} @@ -9658,16 +10704,16 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vercel/nft@0.30.3(rollup@4.53.3)': + '@vercel/nft@0.30.4(rollup@4.53.3)': dependencies: - '@mapbox/node-pre-gyp': 2.0.0 + '@mapbox/node-pre-gyp': 2.0.3 '@rollup/pluginutils': 5.3.0(rollup@4.53.3) acorn: 8.15.0 acorn-import-attributes: 1.9.5(acorn@8.15.0) async-sema: 3.1.1 bindings: 1.5.0 estree-walker: 2.0.2 - glob: 10.4.5 + glob: 10.5.0 graceful-fs: 4.2.11 node-gyp-build: 4.8.4 picomatch: 4.0.3 @@ -9677,30 +10723,79 @@ snapshots: - rollup - supports-color - '@vitejs/plugin-react@5.1.1(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + '@vinxi/listhen@1.5.6': + dependencies: + '@parcel/watcher': 2.5.1 + '@parcel/watcher-wasm': 2.3.0 + citty: 0.1.6 + clipboardy: 4.0.0 + consola: 3.4.2 + defu: 6.1.4 + get-port-please: 3.2.0 + h3: 1.13.0 + http-shutdown: 1.2.2 + jiti: 1.21.7 + mlly: 1.8.0 + node-forge: 1.3.3 + pathe: 1.1.2 + std-env: 3.10.0 + ufo: 1.6.1 + untun: 0.1.3 + uqr: 0.1.2 + + '@vitejs/plugin-react@4.7.0(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@babel/core': 7.28.5 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.5) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + transitivePeerDependencies: + - supports-color + + '@vitejs/plugin-react@5.1.2(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@babel/core': 7.28.5 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.5) - '@rolldown/pluginutils': 1.0.0-beta.47 + '@rolldown/pluginutils': 1.0.0-beta.53 '@types/babel__core': 7.20.5 react-refresh: 0.18.0 - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color - '@vitejs/plugin-vue@5.2.4(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.3))': + '@vitejs/plugin-vue@5.2.4(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))': dependencies: - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vue: 3.5.25(typescript@5.9.3) - '@vitejs/plugin-vue@6.0.2(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.3))': + '@vitejs/plugin-vue@6.0.3(vite@7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))': dependencies: - '@rolldown/pluginutils': 1.0.0-beta.50 - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + '@rolldown/pluginutils': 1.0.0-beta.53 + vite: 7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vue: 3.5.25(typescript@5.9.3) - '@vitest/coverage-v8@4.0.14(vitest@4.0.14(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + '@vitest/coverage-v8@4.0.14(vitest@4.0.15(@types/node@24.10.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@bcoe/v8-coverage': 1.0.2 + '@vitest/utils': 4.0.14 + ast-v8-to-istanbul: 0.3.8 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + magicast: 0.5.1 + obug: 2.1.1 + std-env: 3.10.0 + tinyrainbow: 3.0.3 + vitest: 4.0.15(@types/node@24.10.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + transitivePeerDependencies: + - supports-color + + '@vitest/coverage-v8@4.0.14(vitest@4.0.15(@types/node@25.0.1)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.0.14 @@ -9713,60 +10808,69 @@ snapshots: obug: 2.1.1 std-env: 3.10.0 tinyrainbow: 3.0.3 - vitest: 4.0.14(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vitest: 4.0.15(@types/node@25.0.1)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color - '@vitest/expect@4.0.14': + '@vitest/expect@4.0.15': dependencies: '@standard-schema/spec': 1.0.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.0.14 - '@vitest/utils': 4.0.14 + '@vitest/spy': 4.0.15 + '@vitest/utils': 4.0.15 chai: 6.2.1 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.14(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + '@vitest/mocker@4.0.15(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@vitest/spy': 4.0.14 + '@vitest/spy': 4.0.15 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) '@vitest/pretty-format@4.0.14': dependencies: tinyrainbow: 3.0.3 - '@vitest/runner@4.0.14': + '@vitest/pretty-format@4.0.15': dependencies: - '@vitest/utils': 4.0.14 + tinyrainbow: 3.0.3 + + '@vitest/runner@4.0.15': + dependencies: + '@vitest/utils': 4.0.15 pathe: 2.0.3 - '@vitest/snapshot@4.0.14': + '@vitest/snapshot@4.0.15': dependencies: - '@vitest/pretty-format': 4.0.14 + '@vitest/pretty-format': 4.0.15 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.0.14': {} + '@vitest/spy@4.0.15': {} '@vitest/utils@4.0.14': dependencies: '@vitest/pretty-format': 4.0.14 tinyrainbow: 3.0.3 + '@vitest/utils@4.0.15': + dependencies: + '@vitest/pretty-format': 4.0.15 + tinyrainbow: 3.0.3 + '@volar/language-core@2.4.15': dependencies: '@volar/source-map': 2.4.15 - '@volar/language-core@2.4.23': + '@volar/language-core@2.4.27': dependencies: - '@volar/source-map': 2.4.23 + '@volar/source-map': 2.4.27 '@volar/source-map@2.4.15': {} - '@volar/source-map@2.4.23': {} + '@volar/source-map@2.4.27': {} '@volar/typescript@2.4.15': dependencies: @@ -9774,20 +10878,12 @@ snapshots: path-browserify: 1.0.1 vscode-uri: 3.1.0 - '@volar/typescript@2.4.23': + '@volar/typescript@2.4.27': dependencies: - '@volar/language-core': 2.4.23 + '@volar/language-core': 2.4.27 path-browserify: 1.0.1 vscode-uri: 3.1.0 - '@vue/compiler-core@3.5.24': - dependencies: - '@babel/parser': 7.28.5 - '@vue/shared': 3.5.24 - entities: 4.5.0 - estree-walker: 2.0.2 - source-map-js: 1.2.1 - '@vue/compiler-core@3.5.25': dependencies: '@babel/parser': 7.28.5 @@ -9796,11 +10892,6 @@ snapshots: estree-walker: 2.0.2 source-map-js: 1.2.1 - '@vue/compiler-dom@3.5.24': - dependencies: - '@vue/compiler-core': 3.5.24 - '@vue/shared': 3.5.24 - '@vue/compiler-dom@3.5.25': dependencies: '@vue/compiler-core': 3.5.25 @@ -9832,10 +10923,10 @@ snapshots: '@vue/language-core@2.1.6(typescript@5.9.3)': dependencies: - '@volar/language-core': 2.4.23 - '@vue/compiler-dom': 3.5.24 + '@volar/language-core': 2.4.27 + '@vue/compiler-dom': 3.5.25 '@vue/compiler-vue2': 2.7.16 - '@vue/shared': 3.5.24 + '@vue/shared': 3.5.25 computeds: 0.0.1 minimatch: 9.0.5 muggle-string: 0.4.1 @@ -9878,8 +10969,6 @@ snapshots: '@vue/shared': 3.5.25 vue: 3.5.25(typescript@5.9.3) - '@vue/shared@3.5.24': {} - '@vue/shared@3.5.25': {} '@vue/test-utils@2.4.6': @@ -9891,7 +10980,7 @@ snapshots: '@yarnpkg/parsers@3.0.2': dependencies: - js-yaml: 3.14.1 + js-yaml: 3.14.2 tslib: 2.8.1 '@zkochan/js-yaml@0.0.7': @@ -9906,7 +10995,7 @@ snapshots: accepts@2.0.0: dependencies: - mime-types: 3.0.1 + mime-types: 3.0.2 negotiator: 1.0.0 acorn-import-attributes@1.9.5(acorn@8.15.0): @@ -9952,6 +11041,10 @@ snapshots: alien-signals@1.0.13: {} + ansi-align@3.0.1: + dependencies: + string-width: 4.2.3 + ansi-colors@4.1.3: {} ansi-regex@5.0.1: {} @@ -9975,7 +11068,7 @@ snapshots: archiver-utils@5.0.2: dependencies: - glob: 10.4.5 + glob: 10.5.0 graceful-fs: 4.2.11 is-stream: 2.0.1 lazystream: 1.0.1 @@ -10035,8 +11128,8 @@ snapshots: autoprefixer@10.4.22(postcss@8.5.6): dependencies: - browserslist: 4.27.0 - caniuse-lite: 1.0.30001759 + browserslist: 4.28.1 + caniuse-lite: 1.0.30001760 fraction.js: 5.3.4 normalize-range: 0.1.2 picocolors: 1.1.1 @@ -10046,7 +11139,7 @@ snapshots: axios@1.13.2: dependencies: follow-redirects: 1.15.11 - form-data: 4.0.4 + form-data: 4.0.5 proxy-from-env: 1.1.0 transitivePeerDependencies: - debug @@ -10084,11 +11177,11 @@ snapshots: balanced-match@1.0.2: {} - bare-events@2.8.1: {} + bare-events@2.8.2: {} base64-js@1.5.1: {} - baseline-browser-mapping@2.8.22: {} + baseline-browser-mapping@2.9.7: {} better-path-resolve@1.0.0: dependencies: @@ -10119,8 +11212,8 @@ snapshots: bytes: 3.1.2 content-type: 1.0.5 debug: 4.4.3 - http-errors: 2.0.0 - iconv-lite: 0.7.0 + http-errors: 2.0.1 + iconv-lite: 0.7.1 on-finished: 2.4.1 qs: 6.14.0 raw-body: 3.0.2 @@ -10130,6 +11223,17 @@ snapshots: boolbase@1.0.0: {} + boxen@7.1.1: + dependencies: + ansi-align: 3.0.1 + camelcase: 7.0.1 + chalk: 5.6.2 + cli-boxes: 3.0.0 + string-width: 5.1.2 + type-fest: 2.19.0 + widest-line: 4.0.1 + wrap-ansi: 8.1.0 + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -10143,13 +11247,13 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.27.0: + browserslist@4.28.1: dependencies: - baseline-browser-mapping: 2.8.22 - caniuse-lite: 1.0.30001759 - electron-to-chromium: 1.5.244 + baseline-browser-mapping: 2.9.7 + caniuse-lite: 1.0.30001760 + electron-to-chromium: 1.5.267 node-releases: 2.0.27 - update-browserslist-db: 1.1.4(browserslist@4.27.0) + update-browserslist-db: 1.2.2(browserslist@4.28.1) buffer-crc32@1.0.0: {} @@ -10169,13 +11273,13 @@ snapshots: bytes@3.1.2: {} - c12@3.3.1(magicast@0.5.1): + c12@3.3.2(magicast@0.5.1): dependencies: chokidar: 4.0.3 confbox: 0.2.2 defu: 6.1.4 dotenv: 17.2.3 - exsolve: 1.0.7 + exsolve: 1.0.8 giget: 2.0.0 jiti: 2.6.1 ohash: 2.0.11 @@ -10200,7 +11304,9 @@ snapshots: callsites@3.1.0: {} - caniuse-lite@1.0.30001759: {} + camelcase@7.0.1: {} + + caniuse-lite@1.0.30001760: {} capnweb@0.1.0: {} @@ -10276,6 +11382,8 @@ snapshots: dependencies: consola: 3.4.2 + cli-boxes@3.0.0: {} + cli-cursor@3.1.0: dependencies: restore-cursor: 3.1.0 @@ -10314,6 +11422,8 @@ snapshots: commander@10.0.1: {} + commander@13.1.0: {} + commander@2.20.3: {} comment-parser@1.4.1: {} @@ -10372,8 +11482,6 @@ snapshots: cookie@0.7.2: {} - cookie@1.0.2: {} - core-util-is@1.0.3: {} crc-32@1.2.2: {} @@ -10410,12 +11518,9 @@ snapshots: css-what@6.2.2: {} - css.escape@1.5.1: - optional: true - - cssstyle@5.3.3(postcss@8.5.6): + cssstyle@5.3.4(postcss@8.5.6): dependencies: - '@asamuzakjp/css-color': 4.0.5 + '@asamuzakjp/css-color': 4.1.0 '@csstools/css-syntax-patches-for-csstree': 1.0.14(postcss@8.5.6) css-tree: 3.1.0 transitivePeerDependencies: @@ -10432,10 +11537,19 @@ snapshots: dataloader@1.4.0: {} + dax-sh@0.39.2: + dependencies: + '@deno/shim-deno': 0.19.2 + undici-types: 5.28.4 + db0@0.3.4: {} de-indent@1.0.2: {} + debug@2.6.9: + dependencies: + ms: 2.0.0 + debug@4.4.3: dependencies: ms: 2.1.3 @@ -10470,13 +11584,15 @@ snapshots: destr@2.0.5: {} + destroy@1.2.0: {} + detect-indent@6.1.0: {} detect-libc@1.0.3: {} detect-libc@2.1.2: {} - devalue@5.5.0: {} + devalue@5.6.1: {} devlop@1.1.0: dependencies: @@ -10490,9 +11606,6 @@ snapshots: dom-accessibility-api@0.5.16: {} - dom-accessibility-api@0.6.3: - optional: true - dom-serializer@2.0.0: dependencies: domelementtype: 2.3.0 @@ -10513,11 +11626,11 @@ snapshots: dot-prop@10.1.0: dependencies: - type-fest: 5.1.0 + type-fest: 5.3.1 dotenv-expand@11.0.7: dependencies: - dotenv: 16.6.1 + dotenv: 16.4.7 dotenv@16.4.7: {} @@ -10525,9 +11638,9 @@ snapshots: dotenv@17.2.3: {} - dts-resolver@2.1.3(oxc-resolver@11.14.0): + dts-resolver@2.1.3(oxc-resolver@11.15.0): optionalDependencies: - oxc-resolver: 11.14.0 + oxc-resolver: 11.15.0 dunder-proto@1.0.1: dependencies: @@ -10552,7 +11665,7 @@ snapshots: ee-first@1.1.1: {} - electron-to-chromium@1.5.244: {} + electron-to-chromium@1.5.267: {} emoji-regex@8.0.0: {} @@ -10560,6 +11673,8 @@ snapshots: empathic@2.0.0: {} + encodeurl@1.0.2: {} + encodeurl@2.0.0: {} encoding-sniffer@0.2.1: @@ -10571,7 +11686,7 @@ snapshots: dependencies: once: 1.4.0 - enhanced-resolve@5.18.3: + enhanced-resolve@5.18.4: dependencies: graceful-fs: 4.2.11 tapable: 2.3.0 @@ -10608,6 +11723,32 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.2 + esbuild@0.20.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.20.2 + '@esbuild/android-arm': 0.20.2 + '@esbuild/android-arm64': 0.20.2 + '@esbuild/android-x64': 0.20.2 + '@esbuild/darwin-arm64': 0.20.2 + '@esbuild/darwin-x64': 0.20.2 + '@esbuild/freebsd-arm64': 0.20.2 + '@esbuild/freebsd-x64': 0.20.2 + '@esbuild/linux-arm': 0.20.2 + '@esbuild/linux-arm64': 0.20.2 + '@esbuild/linux-ia32': 0.20.2 + '@esbuild/linux-loong64': 0.20.2 + '@esbuild/linux-mips64el': 0.20.2 + '@esbuild/linux-ppc64': 0.20.2 + '@esbuild/linux-riscv64': 0.20.2 + '@esbuild/linux-s390x': 0.20.2 + '@esbuild/linux-x64': 0.20.2 + '@esbuild/netbsd-x64': 0.20.2 + '@esbuild/openbsd-x64': 0.20.2 + '@esbuild/sunos-x64': 0.20.2 + '@esbuild/win32-arm64': 0.20.2 + '@esbuild/win32-ia32': 0.20.2 + '@esbuild/win32-x64': 0.20.2 + esbuild@0.25.12: optionalDependencies: '@esbuild/aix-ppc64': 0.25.12 @@ -10637,6 +11778,35 @@ snapshots: '@esbuild/win32-ia32': 0.25.12 '@esbuild/win32-x64': 0.25.12 + esbuild@0.27.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.1 + '@esbuild/android-arm': 0.27.1 + '@esbuild/android-arm64': 0.27.1 + '@esbuild/android-x64': 0.27.1 + '@esbuild/darwin-arm64': 0.27.1 + '@esbuild/darwin-x64': 0.27.1 + '@esbuild/freebsd-arm64': 0.27.1 + '@esbuild/freebsd-x64': 0.27.1 + '@esbuild/linux-arm': 0.27.1 + '@esbuild/linux-arm64': 0.27.1 + '@esbuild/linux-ia32': 0.27.1 + '@esbuild/linux-loong64': 0.27.1 + '@esbuild/linux-mips64el': 0.27.1 + '@esbuild/linux-ppc64': 0.27.1 + '@esbuild/linux-riscv64': 0.27.1 + '@esbuild/linux-s390x': 0.27.1 + '@esbuild/linux-x64': 0.27.1 + '@esbuild/netbsd-arm64': 0.27.1 + '@esbuild/netbsd-x64': 0.27.1 + '@esbuild/openbsd-arm64': 0.27.1 + '@esbuild/openbsd-x64': 0.27.1 + '@esbuild/openharmony-arm64': 0.27.1 + '@esbuild/sunos-x64': 0.27.1 + '@esbuild/win32-arm64': 0.27.1 + '@esbuild/win32-ia32': 0.27.1 + '@esbuild/win32-x64': 0.27.1 + escalade@3.2.0: {} escape-html@1.0.3: {} @@ -10666,27 +11836,27 @@ snapshots: eslint: 9.39.1(jiti@2.6.1) eslint-compat-utils: 0.5.1(eslint@9.39.1(jiti@2.6.1)) - eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)): dependencies: - '@typescript-eslint/types': 8.48.0 + '@typescript-eslint/types': 8.49.0 comment-parser: 1.4.1 debug: 4.4.3 eslint: 9.39.1(jiti@2.6.1) eslint-import-context: 0.1.9(unrs-resolver@1.11.1) is-glob: 4.0.3 - minimatch: 9.0.5 + minimatch: 10.1.1 semver: 7.7.3 stable-hash-x: 0.2.0 unrs-resolver: 1.11.1 optionalDependencies: - '@typescript-eslint/utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) transitivePeerDependencies: - supports-color eslint-plugin-n@17.23.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) - enhanced-resolve: 5.18.3 + enhanced-resolve: 5.18.4 eslint: 9.39.1(jiti@2.6.1) eslint-plugin-es-x: 7.8.0(eslint@9.39.1(jiti@2.6.1)) get-tsconfig: 4.13.0 @@ -10700,22 +11870,22 @@ snapshots: eslint-plugin-solid@0.14.5(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) estraverse: 5.3.0 is-html: 2.0.0 kebab-case: 1.0.2 known-css-properties: 0.30.0 - style-to-object: 1.0.11 + style-to-object: 1.0.14 typescript: 5.9.3 transitivePeerDependencies: - supports-color - eslint-plugin-unused-imports@4.3.0(@typescript-eslint/eslint-plugin@8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-unused-imports@4.3.0(@typescript-eslint/eslint-plugin@8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint-scope@8.4.0: dependencies: @@ -10733,7 +11903,7 @@ snapshots: '@eslint/config-array': 0.21.1 '@eslint/config-helpers': 0.4.2 '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.1 + '@eslint/eslintrc': 3.3.3 '@eslint/js': 9.39.1 '@eslint/plugin-kit': 0.4.1 '@humanfs/node': 0.16.7 @@ -10781,7 +11951,7 @@ snapshots: dependencies: estraverse: 5.3.0 - esrap@2.2.0: + esrap@2.2.1: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -10805,9 +11975,11 @@ snapshots: event-target-shim@5.0.1: {} + eventemitter3@4.0.7: {} + events-universal@1.0.1: dependencies: - bare-events: 2.8.1 + bare-events: 2.8.2 transitivePeerDependencies: - bare-abort-controller @@ -10825,7 +11997,7 @@ snapshots: signal-exit: 4.1.0 strip-final-newline: 3.0.0 - expect-type@1.2.2: {} + expect-type@1.3.0: {} express@5.2.1: dependencies: @@ -10842,9 +12014,9 @@ snapshots: etag: 1.8.1 finalhandler: 2.1.1 fresh: 2.0.0 - http-errors: 2.0.0 + http-errors: 2.0.1 merge-descriptors: 2.0.0 - mime-types: 3.0.1 + mime-types: 3.0.2 on-finished: 2.4.1 once: 1.4.0 parseurl: 1.3.3 @@ -10860,7 +12032,7 @@ snapshots: transitivePeerDependencies: - supports-color - exsolve@1.0.7: {} + exsolve@1.0.8: {} extend@3.0.2: {} @@ -10952,7 +12124,7 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - form-data@4.0.4: + form-data@4.0.5: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 @@ -10972,11 +12144,13 @@ snapshots: fraction.js@5.3.4: {} + fresh@0.5.2: {} + fresh@2.0.0: {} front-matter@4.0.2: dependencies: - js-yaml: 3.14.1 + js-yaml: 3.14.2 fs-constants@1.0.0: {} @@ -11064,7 +12238,7 @@ snapshots: dependencies: is-glob: 4.0.3 - glob@10.4.5: + glob@10.5.0: dependencies: foreground-child: 3.3.1 jackspeak: 3.4.3 @@ -11111,7 +12285,7 @@ snapshots: gcp-metadata: 8.1.2 google-logging-utils: 1.1.3 gtoken: 8.0.0 - jws: 4.0.0 + jws: 4.0.1 transitivePeerDependencies: - supports-color @@ -11121,12 +12295,10 @@ snapshots: graceful-fs@4.2.11: {} - graphemer@1.4.0: {} - gtoken@8.0.0: dependencies: gaxios: 7.1.3 - jws: 4.0.0 + jws: 4.0.1 transitivePeerDependencies: - supports-color @@ -11134,6 +12306,19 @@ snapshots: dependencies: duplexer: 0.1.2 + h3@1.13.0: + dependencies: + cookie-es: 1.2.2 + crossws: 0.3.5 + defu: 6.1.4 + destr: 2.0.5 + iron-webcrypto: 1.2.1 + ohash: 1.1.6 + radix3: 1.1.2 + ufo: 1.6.1 + uncrypto: 0.1.3 + unenv: 1.10.0 + h3@1.15.4: dependencies: cookie-es: 1.2.2 @@ -11141,7 +12326,7 @@ snapshots: defu: 6.1.4 destr: 2.0.5 iron-webcrypto: 1.2.1 - node-mock-http: 1.0.3 + node-mock-http: 1.0.4 radix3: 1.1.2 ufo: 1.6.1 uncrypto: 0.1.3 @@ -11150,12 +12335,12 @@ snapshots: dependencies: cookie-es: 2.0.0 fetchdts: 0.1.7 - rou3: 0.7.10 + rou3: 0.7.11 srvx: 0.8.16 - happy-dom@20.0.10: + happy-dom@20.0.11: dependencies: - '@types/node': 20.19.25 + '@types/node': 20.19.26 '@types/whatwg-mimetype': 3.0.2 whatwg-mimetype: 3.0.0 @@ -11196,9 +12381,9 @@ snapshots: '@types/unist': 3.0.3 '@ungap/structured-clone': 1.3.0 hast-util-from-parse5: 8.0.3 - hast-util-to-parse5: 8.0.0 + hast-util-to-parse5: 8.0.1 html-void-elements: 3.0.0 - mdast-util-to-hast: 13.2.0 + mdast-util-to-hast: 13.2.1 parse5: 7.3.0 unist-util-position: 5.0.0 unist-util-visit: 5.0.0 @@ -11226,18 +12411,18 @@ snapshots: mdast-util-mdxjs-esm: 2.0.1 property-information: 7.1.0 space-separated-tokens: 2.0.2 - style-to-js: 1.1.18 + style-to-js: 1.1.21 unist-util-position: 5.0.0 vfile-message: 4.0.3 transitivePeerDependencies: - supports-color - hast-util-to-parse5@8.0.0: + hast-util-to-parse5@8.0.1: dependencies: '@types/hast': 3.0.4 comma-separated-tokens: 2.0.3 devlop: 1.1.0 - property-information: 6.5.0 + property-information: 7.1.0 space-separated-tokens: 2.0.2 web-namespaces: 2.0.1 zwitch: 2.0.4 @@ -11315,6 +12500,14 @@ snapshots: transitivePeerDependencies: - supports-color + http-proxy@1.18.1: + dependencies: + eventemitter3: 4.0.7 + follow-redirects: 1.15.11 + requires-port: 1.0.0 + transitivePeerDependencies: + - debug + http-shutdown@1.2.2: {} https-proxy-agent@7.0.6: @@ -11326,7 +12519,7 @@ snapshots: httpxy@0.1.7: {} - human-id@4.1.2: {} + human-id@4.1.3: {} human-signals@5.0.0: {} @@ -11334,7 +12527,7 @@ snapshots: dependencies: safer-buffer: 2.1.2 - iconv-lite@0.7.0: + iconv-lite@0.7.1: dependencies: safer-buffer: 2.1.2 @@ -11353,20 +12546,17 @@ snapshots: import-meta-resolve@4.2.0: {} - import-without-cache@0.2.2: {} + import-without-cache@0.2.3: {} imurmurhash@0.1.4: {} - indent-string@4.0.0: - optional: true - inherits@2.0.4: {} ini@1.3.8: {} inline-style-parser@0.1.1: {} - inline-style-parser@0.2.4: {} + inline-style-parser@0.2.7: {} ioredis@5.8.2: dependencies: @@ -11473,10 +12663,12 @@ snapshots: isarray@1.0.0: {} - isbot@5.1.31: {} + isbot@5.1.32: {} isexe@2.0.0: {} + isexe@3.1.1: {} + istanbul-lib-coverage@3.2.2: {} istanbul-lib-report@3.0.1: @@ -11511,6 +12703,8 @@ snapshots: chalk: 4.1.2 pretty-format: 30.2.0 + jiti@1.21.7: {} + jiti@2.6.1: {} jju@1.4.0: {} @@ -11519,7 +12713,7 @@ snapshots: dependencies: config-chain: 1.1.13 editorconfig: 1.0.4 - glob: 10.4.5 + glob: 10.5.0 js-cookie: 3.0.5 nopt: 7.2.1 @@ -11534,15 +12728,20 @@ snapshots: argparse: 1.0.10 esprima: 4.0.1 + js-yaml@3.14.2: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + js-yaml@4.1.1: dependencies: argparse: 2.0.1 - jsdom@27.2.0(postcss@8.5.6): + jsdom@27.3.0(postcss@8.5.6): dependencies: - '@acemir/cssom': 0.9.24 - '@asamuzakjp/dom-selector': 6.7.4 - cssstyle: 5.3.3(postcss@8.5.6) + '@acemir/cssom': 0.9.29 + '@asamuzakjp/dom-selector': 6.7.6 + cssstyle: 5.3.4(postcss@8.5.6) data-urls: 6.0.0 decimal.js: 10.6.0 html-encoding-sniffer: 4.0.0 @@ -11599,7 +12798,7 @@ snapshots: ecdsa-sig-formatter: 1.0.11 safe-buffer: 5.2.1 - jws@4.0.0: + jws@4.0.1: dependencies: jwa: 2.0.1 safe-buffer: 5.2.1 @@ -11614,16 +12813,16 @@ snapshots: klona@2.0.6: {} - knip@5.70.2(@types/node@24.10.1)(typescript@5.9.3): + knip@5.73.4(@types/node@24.10.3)(typescript@5.9.3): dependencies: '@nodelib/fs.walk': 1.2.8 - '@types/node': 24.10.1 + '@types/node': 24.10.3 fast-glob: 3.3.3 formatly: 0.3.0 jiti: 2.6.1 js-yaml: 4.1.1 minimist: 1.2.8 - oxc-resolver: 11.14.0 + oxc-resolver: 11.15.0 picocolors: 1.1.1 picomatch: 4.0.3 smol-toml: 1.5.2 @@ -11631,7 +12830,7 @@ snapshots: typescript: 5.9.3 zod: 4.1.13 - knitwork@1.2.0: {} + knitwork@1.3.0: {} known-css-properties@0.30.0: {} @@ -11720,7 +12919,7 @@ snapshots: http-shutdown: 1.2.2 jiti: 2.6.1 mlly: 1.8.0 - node-forge: 1.3.1 + node-forge: 1.3.3 pathe: 1.1.2 std-env: 3.10.0 ufo: 1.6.1 @@ -11773,7 +12972,7 @@ snapshots: lru-cache@10.4.3: {} - lru-cache@11.2.2: {} + lru-cache@11.2.4: {} lru-cache@5.1.1: dependencies: @@ -11783,17 +12982,17 @@ snapshots: dependencies: yallist: 4.0.0 - lucide-react@0.555.0(react@19.2.0): + lucide-react@0.561.0(react@19.2.3): dependencies: - react: 19.2.0 + react: 19.2.3 lucide-solid@0.554.0(solid-js@1.9.10): dependencies: solid-js: 1.9.10 - lucide-svelte@0.468.0(svelte@5.44.1): + lucide-svelte@0.468.0(svelte@5.45.10): dependencies: - svelte: 5.44.1 + svelte: 5.45.10 lunr@2.3.9: {} @@ -11964,7 +13163,7 @@ snapshots: '@types/mdast': 4.0.4 unist-util-is: 6.0.1 - mdast-util-to-hast@13.2.0: + mdast-util-to-hast@13.2.1: dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 @@ -12212,10 +13411,12 @@ snapshots: dependencies: mime-db: 1.52.0 - mime-types@3.0.1: + mime-types@3.0.2: dependencies: mime-db: 1.54.0 + mime@1.6.0: {} + mime@3.0.0: {} mime@4.1.0: {} @@ -12224,8 +13425,9 @@ snapshots: mimic-fn@4.0.0: {} - min-indent@1.0.1: - optional: true + minimatch@10.1.1: + dependencies: + '@isaacs/brace-expansion': 5.0.0 minimatch@3.0.8: dependencies: @@ -12270,6 +13472,8 @@ snapshots: mrmime@2.0.1: {} + ms@2.0.0: {} + ms@2.1.3: {} muggle-string@0.4.1: {} @@ -12284,7 +13488,7 @@ snapshots: nitropack@2.12.9(rolldown@1.0.0-beta.53): dependencies: - '@cloudflare/kv-asset-handler': 0.4.0 + '@cloudflare/kv-asset-handler': 0.4.1 '@rollup/plugin-alias': 5.1.1(rollup@4.53.3) '@rollup/plugin-commonjs': 28.0.9(rollup@4.53.3) '@rollup/plugin-inject': 5.0.5(rollup@4.53.3) @@ -12292,9 +13496,9 @@ snapshots: '@rollup/plugin-node-resolve': 16.0.3(rollup@4.53.3) '@rollup/plugin-replace': 6.0.3(rollup@4.53.3) '@rollup/plugin-terser': 0.4.4(rollup@4.53.3) - '@vercel/nft': 0.30.3(rollup@4.53.3) + '@vercel/nft': 0.30.4(rollup@4.53.3) archiver: 7.0.1 - c12: 3.3.1(magicast@0.5.1) + c12: 3.3.2(magicast@0.5.1) chokidar: 4.0.3 citty: 0.1.6 compatx: 0.2.0 @@ -12310,7 +13514,7 @@ snapshots: esbuild: 0.25.12 escape-string-regexp: 5.0.0 etag: 1.8.1 - exsolve: 1.0.7 + exsolve: 1.0.8 globby: 15.0.0 gzip-size: 7.0.0 h3: 1.15.4 @@ -12319,15 +13523,15 @@ snapshots: ioredis: 5.8.2 jiti: 2.6.1 klona: 2.0.6 - knitwork: 1.2.0 + knitwork: 1.3.0 listhen: 1.9.0 magic-string: 0.30.21 magicast: 0.5.1 mime: 4.1.0 mlly: 1.8.0 node-fetch-native: 1.6.7 - node-mock-http: 1.0.3 - ofetch: 1.5.0 + node-mock-http: 1.0.4 + ofetch: 1.5.1 ohash: 2.0.11 pathe: 2.0.3 perfect-debounce: 2.0.0 @@ -12349,10 +13553,10 @@ snapshots: unenv: 2.0.0-rc.24 unimport: 5.5.0 unplugin-utils: 0.3.1 - unstorage: 1.17.2(db0@0.3.4)(ioredis@5.8.2) + unstorage: 1.17.3(db0@0.3.4)(ioredis@5.8.2) untyped: 2.0.0 unwasm: 0.3.11 - youch: 4.1.0-beta.11 + youch: 4.1.0-beta.13 youch-core: 0.3.3 transitivePeerDependencies: - '@azure/app-configuration' @@ -12400,13 +13604,13 @@ snapshots: fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 - node-forge@1.3.1: {} + node-forge@1.3.3: {} node-gyp-build@4.8.4: {} node-machine-id@1.1.12: {} - node-mock-http@1.0.3: {} + node-mock-http@1.0.4: {} node-releases@2.0.27: {} @@ -12468,7 +13672,7 @@ snapshots: tree-kill: 1.2.2 tsconfig-paths: 4.2.0 tslib: 2.8.1 - yaml: 2.8.1 + yaml: 2.8.2 yargs: 17.7.2 yargs-parser: 21.1.1 optionalDependencies: @@ -12497,12 +13701,14 @@ snapshots: obug@2.1.1: {} - ofetch@1.5.0: + ofetch@1.5.1: dependencies: destr: 2.0.5 node-fetch-native: 1.6.7 ufo: 1.6.1 + ohash@1.1.6: {} + ohash@2.0.11: {} ollama@0.6.3: @@ -12531,7 +13737,7 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 - openai@6.9.1(ws@8.18.3)(zod@4.1.13): + openai@6.10.0(ws@8.18.3)(zod@4.1.13): optionalDependencies: ws: 8.18.3 zod: 4.1.13 @@ -12558,27 +13764,28 @@ snapshots: outdent@0.5.0: {} - oxc-resolver@11.14.0: + oxc-resolver@11.15.0: optionalDependencies: - '@oxc-resolver/binding-android-arm-eabi': 11.14.0 - '@oxc-resolver/binding-android-arm64': 11.14.0 - '@oxc-resolver/binding-darwin-arm64': 11.14.0 - '@oxc-resolver/binding-darwin-x64': 11.14.0 - '@oxc-resolver/binding-freebsd-x64': 11.14.0 - '@oxc-resolver/binding-linux-arm-gnueabihf': 11.14.0 - '@oxc-resolver/binding-linux-arm-musleabihf': 11.14.0 - '@oxc-resolver/binding-linux-arm64-gnu': 11.14.0 - '@oxc-resolver/binding-linux-arm64-musl': 11.14.0 - '@oxc-resolver/binding-linux-ppc64-gnu': 11.14.0 - '@oxc-resolver/binding-linux-riscv64-gnu': 11.14.0 - '@oxc-resolver/binding-linux-riscv64-musl': 11.14.0 - '@oxc-resolver/binding-linux-s390x-gnu': 11.14.0 - '@oxc-resolver/binding-linux-x64-gnu': 11.14.0 - '@oxc-resolver/binding-linux-x64-musl': 11.14.0 - '@oxc-resolver/binding-wasm32-wasi': 11.14.0 - '@oxc-resolver/binding-win32-arm64-msvc': 11.14.0 - '@oxc-resolver/binding-win32-ia32-msvc': 11.14.0 - '@oxc-resolver/binding-win32-x64-msvc': 11.14.0 + '@oxc-resolver/binding-android-arm-eabi': 11.15.0 + '@oxc-resolver/binding-android-arm64': 11.15.0 + '@oxc-resolver/binding-darwin-arm64': 11.15.0 + '@oxc-resolver/binding-darwin-x64': 11.15.0 + '@oxc-resolver/binding-freebsd-x64': 11.15.0 + '@oxc-resolver/binding-linux-arm-gnueabihf': 11.15.0 + '@oxc-resolver/binding-linux-arm-musleabihf': 11.15.0 + '@oxc-resolver/binding-linux-arm64-gnu': 11.15.0 + '@oxc-resolver/binding-linux-arm64-musl': 11.15.0 + '@oxc-resolver/binding-linux-ppc64-gnu': 11.15.0 + '@oxc-resolver/binding-linux-riscv64-gnu': 11.15.0 + '@oxc-resolver/binding-linux-riscv64-musl': 11.15.0 + '@oxc-resolver/binding-linux-s390x-gnu': 11.15.0 + '@oxc-resolver/binding-linux-x64-gnu': 11.15.0 + '@oxc-resolver/binding-linux-x64-musl': 11.15.0 + '@oxc-resolver/binding-openharmony-arm64': 11.15.0 + '@oxc-resolver/binding-wasm32-wasi': 11.15.0 + '@oxc-resolver/binding-win32-arm64-msvc': 11.15.0 + '@oxc-resolver/binding-win32-ia32-msvc': 11.15.0 + '@oxc-resolver/binding-win32-x64-msvc': 11.15.0 p-filter@2.1.0: dependencies: @@ -12610,7 +13817,7 @@ snapshots: dependencies: quansync: 0.2.11 - package-manager-detector@1.5.0: {} + package-manager-detector@1.6.0: {} parent-module@1.0.1: dependencies: @@ -12662,6 +13869,8 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 + path-to-regexp@6.3.0: {} + path-to-regexp@8.3.0: {} path-type@4.0.0: {} @@ -12691,7 +13900,7 @@ snapshots: pkg-types@2.3.0: dependencies: confbox: 0.2.2 - exsolve: 1.0.7 + exsolve: 1.0.8 pathe: 2.0.3 playwright-core@1.57.0: {} @@ -12714,10 +13923,10 @@ snapshots: premove@4.0.0: {} - prettier-plugin-svelte@3.4.0(prettier@3.7.4)(svelte@5.44.1): + prettier-plugin-svelte@3.4.0(prettier@3.7.4)(svelte@5.45.10): dependencies: prettier: 3.7.4 - svelte: 5.44.1 + svelte: 5.45.10 prettier@2.8.8: {} @@ -12754,10 +13963,10 @@ snapshots: proxy-from-env@1.1.0: {} - publint@0.3.15: + publint@0.3.16: dependencies: '@publint/pack': 0.1.2 - package-manager-detector: 1.5.0 + package-manager-detector: 1.6.0 picocolors: 1.1.1 sade: 1.8.1 @@ -12771,6 +13980,8 @@ snapshots: quansync@0.2.11: {} + quansync@1.0.0: {} + queue-microtask@1.2.3: {} radix3@1.1.2: {} @@ -12785,7 +13996,7 @@ snapshots: dependencies: bytes: 3.1.2 http-errors: 2.0.1 - iconv-lite: 0.7.0 + iconv-lite: 0.7.1 unpipe: 1.0.0 rc9@2.1.2: @@ -12793,16 +14004,16 @@ snapshots: defu: 6.1.4 destr: 2.0.5 - react-dom@19.2.0(react@19.2.0): + react-dom@19.2.3(react@19.2.3): dependencies: - react: 19.2.0 + react: 19.2.3 scheduler: 0.27.0 react-is@17.0.2: {} react-is@18.3.1: {} - react-markdown@10.1.0(@types/react@19.2.7)(react@19.2.0): + react-markdown@10.1.0(@types/react@19.2.7)(react@19.2.3): dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 @@ -12810,8 +14021,8 @@ snapshots: devlop: 1.1.0 hast-util-to-jsx-runtime: 2.3.6 html-url-attributes: 3.0.1 - mdast-util-to-hast: 13.2.0 - react: 19.2.0 + mdast-util-to-hast: 13.2.1 + react: 19.2.3 remark-parse: 11.0.0 remark-rehype: 11.1.2 unified: 11.0.5 @@ -12820,14 +14031,16 @@ snapshots: transitivePeerDependencies: - supports-color + react-refresh@0.17.0: {} + react-refresh@0.18.0: {} - react@19.2.0: {} + react@19.2.3: {} read-yaml-file@1.1.0: dependencies: graceful-fs: 4.2.11 - js-yaml: 3.14.1 + js-yaml: 3.14.2 pify: 4.0.1 strip-bom: 3.0.0 @@ -12875,12 +14088,6 @@ snapshots: tiny-invariant: 1.3.3 tslib: 2.8.1 - redent@3.0.0: - dependencies: - indent-string: 4.0.0 - strip-indent: 3.0.0 - optional: true - redis-errors@1.2.0: {} redis-parser@3.0.0: @@ -12930,7 +14137,7 @@ snapshots: dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 - mdast-util-to-hast: 13.2.0 + mdast-util-to-hast: 13.2.1 unified: 11.0.5 vfile: 6.0.3 @@ -12944,6 +14151,8 @@ snapshots: require-from-string@2.0.2: {} + requires-port@1.0.0: {} + resolve-from@4.0.0: {} resolve-from@5.0.0: {} @@ -12967,16 +14176,16 @@ snapshots: rimraf@5.0.10: dependencies: - glob: 10.4.5 + glob: 10.5.0 - rolldown-plugin-dts@0.18.2(oxc-resolver@11.14.0)(rolldown@1.0.0-beta.53)(typescript@5.9.3): + rolldown-plugin-dts@0.18.3(oxc-resolver@11.15.0)(rolldown@1.0.0-beta.53)(typescript@5.9.3): dependencies: '@babel/generator': 7.28.5 '@babel/parser': 7.28.5 '@babel/types': 7.28.5 ast-kit: 2.2.0 birpc: 3.0.0 - dts-resolver: 2.1.3(oxc-resolver@11.14.0) + dts-resolver: 2.1.3(oxc-resolver@11.15.0) get-tsconfig: 4.13.0 magic-string: 0.30.21 obug: 2.1.1 @@ -13049,7 +14258,7 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.53.3 fsevents: 2.3.3 - rou3@0.7.10: {} + rou3@0.7.11: {} router@2.2.0: dependencies: @@ -13095,6 +14304,24 @@ snapshots: semver@7.7.3: {} + send@0.19.0: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + send@1.2.0: dependencies: debug: 4.4.3 @@ -13102,8 +14329,8 @@ snapshots: escape-html: 1.0.3 etag: 1.8.1 fresh: 2.0.0 - http-errors: 2.0.0 - mime-types: 3.0.1 + http-errors: 2.0.1 + mime-types: 3.0.2 ms: 2.1.3 on-finished: 2.4.1 range-parser: 1.2.1 @@ -13131,6 +14358,15 @@ snapshots: dependencies: defu: 6.1.4 + serve-static@1.16.2: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.0 + transitivePeerDependencies: + - supports-color + serve-static@2.2.0: dependencies: encodeurl: 2.0.0 @@ -13241,7 +14477,7 @@ snapshots: seroval: 1.3.2 seroval-plugins: 1.3.3(seroval@1.3.2) - solid-markdown@2.1.0(solid-js@1.9.10): + solid-markdown@2.1.1(solid-js@1.9.10): dependencies: comma-separated-tokens: 2.0.3 property-information: 6.5.0 @@ -13347,11 +14583,6 @@ snapshots: strip-final-newline@3.0.0: {} - strip-indent@3.0.0: - dependencies: - min-indent: 1.0.1 - optional: true - strip-json-comments@3.1.1: {} strip-json-comments@5.0.3: {} @@ -13360,17 +14591,17 @@ snapshots: dependencies: js-tokens: 9.0.1 - style-to-js@1.1.18: + style-to-js@1.1.21: dependencies: - style-to-object: 1.0.11 + style-to-object: 1.0.14 style-to-object@0.3.0: dependencies: inline-style-parser: 0.1.1 - style-to-object@1.0.11: + style-to-object@1.0.14: dependencies: - inline-style-parser: 0.2.4 + inline-style-parser: 0.2.7 supports-color@10.2.2: {} @@ -13384,38 +14615,38 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svelte-check@4.3.4(picomatch@4.0.3)(svelte@5.44.1)(typescript@5.9.3): + svelte-check@4.3.4(picomatch@4.0.3)(svelte@5.45.10)(typescript@5.9.3): dependencies: '@jridgewell/trace-mapping': 0.3.31 chokidar: 4.0.3 fdir: 6.5.0(picomatch@4.0.3) picocolors: 1.1.1 sade: 1.8.1 - svelte: 5.44.1 + svelte: 5.45.10 typescript: 5.9.3 transitivePeerDependencies: - picomatch - svelte2tsx@0.7.45(svelte@5.44.1)(typescript@5.9.3): + svelte2tsx@0.7.45(svelte@5.45.10)(typescript@5.9.3): dependencies: dedent-js: 1.0.1 scule: 1.3.0 - svelte: 5.44.1 + svelte: 5.45.10 typescript: 5.9.3 - svelte@5.44.1: + svelte@5.45.10: dependencies: '@jridgewell/remapping': 2.3.5 '@jridgewell/sourcemap-codec': 1.5.5 - '@sveltejs/acorn-typescript': 1.0.7(acorn@8.15.0) + '@sveltejs/acorn-typescript': 1.0.8(acorn@8.15.0) '@types/estree': 1.0.8 acorn: 8.15.0 aria-query: 5.3.2 axobject-query: 4.1.0 clsx: 2.1.1 - devalue: 5.5.0 + devalue: 5.6.1 esm-env: 1.2.2 - esrap: 2.2.0 + esrap: 2.2.1 is-reference: 3.0.3 locate-character: 3.0.0 magic-string: 0.30.21 @@ -13427,7 +14658,7 @@ snapshots: tagged-tag@1.0.0: {} - tailwindcss@4.1.17: {} + tailwindcss@4.1.18: {} tapable@2.3.0: {} @@ -13458,7 +14689,7 @@ snapshots: term-size@2.2.1: {} - terser@5.44.0: + terser@5.44.1: dependencies: '@jridgewell/source-map': 0.3.11 acorn: 8.15.0 @@ -13477,8 +14708,6 @@ snapshots: tinybench@2.9.0: {} - tinyexec@0.3.2: {} - tinyexec@1.0.2: {} tinyglobby@0.2.15: @@ -13488,11 +14717,11 @@ snapshots: tinyrainbow@3.0.3: {} - tldts-core@7.0.16: {} + tldts-core@7.0.19: {} - tldts@7.0.16: + tldts@7.0.19: dependencies: - tldts-core: 7.0.16 + tldts-core: 7.0.19 tmp@0.2.5: {} @@ -13506,7 +14735,7 @@ snapshots: tough-cookie@6.0.0: dependencies: - tldts: 7.0.16 + tldts: 7.0.19 tr46@0.0.3: {} @@ -13541,24 +14770,25 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 - tsdown@0.17.0-beta.6(oxc-resolver@11.14.0)(publint@0.3.15)(typescript@5.9.3): + tsdown@0.17.3(oxc-resolver@11.15.0)(publint@0.3.16)(typescript@5.9.3): dependencies: ansis: 4.2.0 cac: 6.7.14 + defu: 6.1.4 empathic: 2.0.0 hookable: 5.5.3 - import-without-cache: 0.2.2 + import-without-cache: 0.2.3 obug: 2.1.1 rolldown: 1.0.0-beta.53 - rolldown-plugin-dts: 0.18.2(oxc-resolver@11.14.0)(rolldown@1.0.0-beta.53)(typescript@5.9.3) + rolldown-plugin-dts: 0.18.3(oxc-resolver@11.15.0)(rolldown@1.0.0-beta.53)(typescript@5.9.3) semver: 7.7.3 tinyexec: 1.0.2 tinyglobby: 0.2.15 tree-kill: 1.2.2 - unconfig-core: 7.4.1 - unrun: 0.2.16 + unconfig-core: 7.4.2 + unrun: 0.2.19 optionalDependencies: - publint: 0.3.15 + publint: 0.3.16 typescript: 5.9.3 transitivePeerDependencies: - '@ts-macro/tsc' @@ -13569,9 +14799,9 @@ snapshots: tslib@2.8.1: {} - tsx@4.20.6: + tsx@4.21.0: dependencies: - esbuild: 0.25.12 + esbuild: 0.27.1 get-tsconfig: 4.13.0 optionalDependencies: fsevents: 2.3.3 @@ -13580,7 +14810,9 @@ snapshots: dependencies: prelude-ls: 1.2.1 - type-fest@5.1.0: + type-fest@2.19.0: {} + + type-fest@5.3.1: dependencies: tagged-tag: 1.0.0 @@ -13588,12 +14820,12 @@ snapshots: dependencies: content-type: 1.0.5 media-typer: 1.1.0 - mime-types: 3.0.1 + mime-types: 3.0.2 typedoc-plugin-frontmatter@1.3.0(typedoc-plugin-markdown@4.9.0(typedoc@0.28.14(typescript@5.9.3))): dependencies: typedoc-plugin-markdown: 4.9.0(typedoc@0.28.14(typescript@5.9.3)) - yaml: 2.8.1 + yaml: 2.8.2 typedoc-plugin-markdown@4.9.0(typedoc@0.28.14(typescript@5.9.3)): dependencies: @@ -13601,19 +14833,19 @@ snapshots: typedoc@0.28.14(typescript@5.9.3): dependencies: - '@gerrit0/mini-shiki': 3.15.0 + '@gerrit0/mini-shiki': 3.19.0 lunr: 2.3.9 markdown-it: 14.1.0 minimatch: 9.0.5 typescript: 5.9.3 - yaml: 2.8.1 + yaml: 2.8.2 - typescript-eslint@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): + typescript-eslint@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.46.3(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: @@ -13629,10 +14861,10 @@ snapshots: ultrahtml@1.6.0: {} - unconfig-core@7.4.1: + unconfig-core@7.4.2: dependencies: - '@quansync/fs': 0.1.5 - quansync: 0.2.11 + '@quansync/fs': 1.0.0 + quansync: 1.0.0 uncrypto@0.1.3: {} @@ -13641,7 +14873,9 @@ snapshots: acorn: 8.15.0 estree-walker: 3.0.3 magic-string: 0.30.21 - unplugin: 2.3.10 + unplugin: 2.3.11 + + undici-types@5.28.4: {} undici-types@6.21.0: {} @@ -13649,6 +14883,14 @@ snapshots: undici@7.16.0: {} + unenv@1.10.0: + dependencies: + consola: 3.4.2 + defu: 6.1.4 + mime: 3.0.0 + node-fetch-native: 1.6.7 + pathe: 1.1.2 + unenv@2.0.0-rc.24: dependencies: pathe: 2.0.3 @@ -13679,7 +14921,7 @@ snapshots: scule: 1.3.0 strip-literal: 3.1.0 tinyglobby: 0.2.15 - unplugin: 2.3.10 + unplugin: 2.3.11 unplugin-utils: 0.3.1 unist-util-find-after@5.0.0: @@ -13734,7 +14976,7 @@ snapshots: pathe: 2.0.3 picomatch: 4.0.3 - unplugin@2.3.10: + unplugin@2.3.11: dependencies: '@jridgewell/remapping': 2.3.5 acorn: 8.15.0 @@ -13765,12 +15007,11 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 - unrun@0.2.16: + unrun@0.2.19: dependencies: - '@oxc-project/runtime': 0.101.0 rolldown: 1.0.0-beta.53 - unstorage@1.17.2(db0@0.3.4)(ioredis@5.8.2): + unstorage@1.17.3(db0@0.3.4)(ioredis@5.8.2): dependencies: anymatch: 3.1.3 chokidar: 4.0.3 @@ -13778,7 +15019,7 @@ snapshots: h3: 1.15.4 lru-cache: 10.4.3 node-fetch-native: 1.6.7 - ofetch: 1.5.0 + ofetch: 1.5.1 ufo: 1.6.1 optionalDependencies: db0: 0.3.4 @@ -13795,21 +15036,21 @@ snapshots: citty: 0.1.6 defu: 6.1.4 jiti: 2.6.1 - knitwork: 1.2.0 + knitwork: 1.3.0 scule: 1.3.0 unwasm@0.3.11: dependencies: - knitwork: 1.2.0 + knitwork: 1.3.0 magic-string: 0.30.21 mlly: 1.8.0 pathe: 2.0.3 pkg-types: 2.3.0 - unplugin: 2.3.10 + unplugin: 2.3.11 - update-browserslist-db@1.1.4(browserslist@4.27.0): + update-browserslist-db@1.2.2(browserslist@4.28.1): dependencies: - browserslist: 4.27.0 + browserslist: 4.28.1 escalade: 3.2.0 picocolors: 1.1.1 @@ -13819,9 +15060,9 @@ snapshots: dependencies: punycode: 2.3.1 - use-sync-external-store@1.6.0(react@19.2.0): + use-sync-external-store@1.6.0(react@19.2.3): dependencies: - react: 19.2.0 + react: 19.2.3 util-deprecate@1.0.2: {} @@ -13842,11 +15083,92 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-plugin-dts@4.2.3(@types/node@24.10.1)(rollup@4.53.3)(typescript@5.9.3)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): + vinxi@0.5.3(@types/node@24.10.3)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rolldown@1.0.0-beta.53)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): + dependencies: + '@babel/core': 7.28.5 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.5) + '@types/micromatch': 4.0.10 + '@vinxi/listhen': 1.5.6 + boxen: 7.1.1 + chokidar: 3.6.0 + citty: 0.1.6 + consola: 3.4.2 + crossws: 0.3.5 + dax-sh: 0.39.2 + defu: 6.1.4 + es-module-lexer: 1.7.0 + esbuild: 0.20.2 + fast-glob: 3.3.3 + get-port-please: 3.2.0 + h3: 1.13.0 + hookable: 5.5.3 + http-proxy: 1.18.1 + micromatch: 4.0.8 + nitropack: 2.12.9(rolldown@1.0.0-beta.53) + node-fetch-native: 1.6.7 + path-to-regexp: 6.3.0 + pathe: 1.1.2 + radix3: 1.1.2 + resolve: 1.22.11 + serve-placeholder: 2.0.2 + serve-static: 1.16.2 + ufo: 1.6.1 + unctx: 2.4.1 + unenv: 1.10.0 + unstorage: 1.17.3(db0@0.3.4)(ioredis@5.8.2) + vite: 6.4.1(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + zod: 3.25.76 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@electric-sql/pglite' + - '@libsql/client' + - '@netlify/blobs' + - '@planetscale/database' + - '@types/node' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bare-abort-controller + - better-sqlite3 + - db0 + - debug + - drizzle-orm + - encoding + - idb-keyval + - ioredis + - jiti + - less + - lightningcss + - mysql2 + - react-native-b4a + - rolldown + - sass + - sass-embedded + - sqlite3 + - stylus + - sugarss + - supports-color + - terser + - tsx + - uploadthing + - xml2js + - yaml + + vite-plugin-dts@4.2.3(@types/node@24.10.3)(rollup@4.53.3)(typescript@5.9.3)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)): dependencies: - '@microsoft/api-extractor': 7.47.7(@types/node@24.10.1) + '@microsoft/api-extractor': 7.47.7(@types/node@24.10.3) '@rollup/pluginutils': 5.3.0(rollup@4.53.3) - '@volar/typescript': 2.4.23 + '@volar/typescript': 2.4.27 '@vue/language-core': 2.1.6(typescript@5.9.3) compare-versions: 6.1.1 debug: 4.4.3 @@ -13855,17 +15177,17 @@ snapshots: magic-string: 0.30.21 typescript: 5.9.3 optionalDependencies: - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - '@types/node' - rollup - supports-color - vite-plugin-externalize-deps@0.10.0(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): + vite-plugin-externalize-deps@0.10.0(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)): dependencies: - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): + vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)): dependencies: '@babel/core': 7.28.5 '@types/babel__core': 7.20.5 @@ -13873,25 +15195,36 @@ snapshots: merge-anything: 5.1.7 solid-js: 1.9.10 solid-refresh: 0.6.3(solid-js@1.9.10) - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) - vitefu: 1.1.1(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) - optionalDependencies: - '@testing-library/jest-dom': 6.9.1 + vite: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vitefu: 1.1.1(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + transitivePeerDependencies: + - supports-color + + vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)): + dependencies: + '@babel/core': 7.28.5 + '@types/babel__core': 7.20.5 + babel-preset-solid: 1.9.10(@babel/core@7.28.5)(solid-js@1.9.10) + merge-anything: 5.1.7 + solid-js: 1.9.10 + solid-refresh: 0.6.3(solid-js@1.9.10) + vite: 7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vitefu: 1.1.1(vite@7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) transitivePeerDependencies: - supports-color - vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): + vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)): dependencies: debug: 4.4.3 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.3) optionalDependencies: - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color - typescript - vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): + vite@6.4.1(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -13900,44 +15233,121 @@ snapshots: rollup: 4.53.3 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 24.10.1 + '@types/node': 24.10.3 fsevents: 2.3.3 jiti: 2.6.1 lightningcss: 1.30.2 - terser: 5.44.0 - tsx: 4.20.6 - yaml: 2.8.1 + terser: 5.44.1 + tsx: 4.21.0 + yaml: 2.8.2 - vitefu@1.1.1(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): + vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.53.3 + tinyglobby: 0.2.15 optionalDependencies: - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + '@types/node': 24.10.3 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.2 + terser: 5.44.1 + tsx: 4.21.0 + yaml: 2.8.2 - vitest@4.0.14(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): + vite@7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: - '@vitest/expect': 4.0.14 - '@vitest/mocker': 4.0.14(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) - '@vitest/pretty-format': 4.0.14 - '@vitest/runner': 4.0.14 - '@vitest/snapshot': 4.0.14 - '@vitest/spy': 4.0.14 - '@vitest/utils': 4.0.14 + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.53.3 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 25.0.1 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.2 + terser: 5.44.1 + tsx: 4.21.0 + yaml: 2.8.2 + + vitefu@1.1.1(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)): + optionalDependencies: + vite: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + + vitefu@1.1.1(vite@7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)): + optionalDependencies: + vite: 7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + + vitest@4.0.15(@types/node@24.10.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): + dependencies: + '@vitest/expect': 4.0.15 + '@vitest/mocker': 4.0.15(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/pretty-format': 4.0.15 + '@vitest/runner': 4.0.15 + '@vitest/snapshot': 4.0.15 + '@vitest/spy': 4.0.15 + '@vitest/utils': 4.0.15 + es-module-lexer: 1.7.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.0.3 + vite: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.10.3 + happy-dom: 20.0.11 + jsdom: 27.3.0(postcss@8.5.6) + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + + vitest@4.0.15(@types/node@25.0.1)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): + dependencies: + '@vitest/expect': 4.0.15 + '@vitest/mocker': 4.0.15(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/pretty-format': 4.0.15 + '@vitest/runner': 4.0.15 + '@vitest/snapshot': 4.0.15 + '@vitest/spy': 4.0.15 + '@vitest/utils': 4.0.15 es-module-lexer: 1.7.0 - expect-type: 1.2.2 + expect-type: 1.3.0 magic-string: 0.30.21 obug: 2.1.1 pathe: 2.0.3 picomatch: 4.0.3 std-env: 3.10.0 tinybench: 2.9.0 - tinyexec: 0.3.2 + tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 24.10.1 - happy-dom: 20.0.10 - jsdom: 27.2.0(postcss@8.5.6) + '@types/node': 25.0.1 + happy-dom: 20.0.11 + jsdom: 27.3.0(postcss@8.5.6) transitivePeerDependencies: - jiti - less @@ -13967,7 +15377,7 @@ snapshots: transitivePeerDependencies: - supports-color - vue-router@4.6.3(vue@3.5.25(typescript@5.9.3)): + vue-router@4.6.4(vue@3.5.25(typescript@5.9.3)): dependencies: '@vue/devtools-api': 6.6.4 vue: 3.5.25(typescript@5.9.3) @@ -14034,11 +15444,19 @@ snapshots: dependencies: isexe: 2.0.0 + which@4.0.0: + dependencies: + isexe: 3.1.1 + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 stackback: 0.0.2 + widest-line@4.0.1: + dependencies: + string-width: 5.1.2 + word-wrap@1.2.5: {} wrap-ansi@7.0.0: @@ -14059,7 +15477,14 @@ snapshots: xml-name-validator@5.0.0: {} - xmlbuilder2@4.0.1: + xmlbuilder2@3.1.1: + dependencies: + '@oozcitak/dom': 1.15.10 + '@oozcitak/infra': 1.0.8 + '@oozcitak/util': 8.3.8 + js-yaml: 3.14.1 + + xmlbuilder2@4.0.3: dependencies: '@oozcitak/dom': 2.0.2 '@oozcitak/infra': 2.0.2 @@ -14076,7 +15501,7 @@ snapshots: yallist@5.0.0: {} - yaml@2.8.1: {} + yaml@2.8.2: {} yargs-parser@21.1.1: {} @@ -14094,15 +15519,15 @@ snapshots: youch-core@0.3.3: dependencies: - '@poppinss/exception': 1.2.2 + '@poppinss/exception': 1.2.3 error-stack-parser-es: 1.0.5 - youch@4.1.0-beta.11: + youch@4.1.0-beta.13: dependencies: - '@poppinss/colors': 4.1.5 - '@poppinss/dumper': 0.6.4 - '@speed-highlight/core': 1.2.8 - cookie: 1.0.2 + '@poppinss/colors': 4.1.6 + '@poppinss/dumper': 0.6.5 + '@speed-highlight/core': 1.2.12 + cookie-es: 2.0.0 youch-core: 0.3.3 zimmerframe@1.1.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9ffeb0fe..47ed9ebd 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -6,3 +6,4 @@ packages: - 'packages/typescript/*' - 'packages/typescript/smoke-tests/*' - 'examples/*' + - 'testing/*' diff --git a/testing/panel/package.json b/testing/panel/package.json index bb38ee26..a9ae79dc 100644 --- a/testing/panel/package.json +++ b/testing/panel/package.json @@ -8,37 +8,38 @@ "preview": "vite preview" }, "dependencies": { - "@tailwindcss/vite": "^4.1.17", + "@tailwindcss/vite": "^4.1.18", "@tanstack/ai": "workspace:*", "@tanstack/ai-anthropic": "workspace:*", "@tanstack/ai-client": "workspace:*", "@tanstack/ai-gemini": "workspace:*", + "@tanstack/ai-ollama": "workspace:*", "@tanstack/ai-openai": "workspace:*", "@tanstack/ai-react": "workspace:*", "@tanstack/ai-react-ui": "workspace:*", - "@tanstack/nitro-v2-vite-plugin": "^1.139.7", - "@tanstack/react-router": "^1.139.7", - "@tanstack/react-start": "^1.139.7", - "@tanstack/start": "^1.139.7", - "highlight.js": "^11.11.4", - "lucide-react": "^0.555.0", - "react": "^19.2.0", - "react-dom": "^19.2.0", - "react-markdown": "^10.0.0", - "rehype-highlight": "^7.0.1", + "@tanstack/nitro-v2-vite-plugin": "^1.141.0", + "@tanstack/react-router": "^1.141.1", + "@tanstack/react-start": "^1.141.1", + "@tanstack/start": "^1.120.20", + "highlight.js": "^11.11.1", + "lucide-react": "^0.561.0", + "react": "^19.2.3", + "react-dom": "^19.2.3", + "react-markdown": "^10.1.0", + "rehype-highlight": "^7.0.2", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", - "remark-gfm": "^4.0.0", - "tailwindcss": "^4.1.17", + "remark-gfm": "^4.0.1", + "tailwindcss": "^4.1.18", "vite-tsconfig-paths": "^5.1.4", - "zod": "^3.25.0" + "zod": "^4.1.13" }, "devDependencies": { "@types/node": "^24.10.1", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^5.1.1", + "@vitejs/plugin-react": "^5.1.2", "typescript": "5.9.3", - "vite": "^7.2.4" + "vite": "^7.2.7" } } diff --git a/testing/panel/src/components/Header.tsx b/testing/panel/src/components/Header.tsx index 2b5e1276..c682ca70 100644 --- a/testing/panel/src/components/Header.tsx +++ b/testing/panel/src/components/Header.tsx @@ -1,7 +1,19 @@ import { Link } from '@tanstack/react-router' import { useState } from 'react' -import { Home, Menu, X, FlaskConical } from 'lucide-react' +import { + ChefHat, + FileText, + FlaskConical, + Home, + ImageIcon, + Menu, + Mic, + Package, + Video, + Volume2, + X, +} from 'lucide-react' export default function Header() { const [isOpen, setIsOpen] = useState(false) @@ -69,6 +81,112 @@ export default function Header() { Stream Debugger + + setIsOpen(false)} + className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2" + activeProps={{ + className: + 'flex items-center gap-3 p-3 rounded-lg bg-green-600 hover:bg-green-700 transition-colors mb-2', + }} + > + +
+ Add-on Manager + + Multi-Tool + +
+ + +
+

+ Activities +

+ + setIsOpen(false)} + className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2" + activeProps={{ + className: + 'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2', + }} + > + + Summarize + + + setIsOpen(false)} + className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2" + activeProps={{ + className: + 'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2', + }} + > + + Image Generation + + + setIsOpen(false)} + className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2" + activeProps={{ + className: + 'flex items-center gap-3 p-3 rounded-lg bg-purple-600 hover:bg-purple-700 transition-colors mb-2', + }} + > +