From 409696fa3c7ffd8c8d2c7110bfaab4f49de0ed1c Mon Sep 17 00:00:00 2001 From: Keoma Wright Date: Thu, 5 Feb 2026 22:45:56 +0200 Subject: [PATCH 1/4] fix netlify deploy output and uploads (#2107) Co-authored-by: embire2 --- app/components/deploy/GitHubDeploy.client.tsx | 7 +- app/components/deploy/GitLabDeploy.client.tsx | 7 +- .../deploy/NetlifyDeploy.client.tsx | 9 ++- app/components/deploy/VercelDeploy.client.tsx | 9 ++- app/components/deploy/deployUtils.ts | 15 ++++ app/lib/runtime/action-runner.ts | 23 +++++- app/routes/api.netlify-deploy.ts | 75 ++++++++++++++----- 7 files changed, 114 insertions(+), 31 deletions(-) create mode 100644 app/components/deploy/deployUtils.ts diff --git a/app/components/deploy/GitHubDeploy.client.tsx b/app/components/deploy/GitHubDeploy.client.tsx index d49f4278de..313d901cc6 100644 --- a/app/components/deploy/GitHubDeploy.client.tsx +++ b/app/components/deploy/GitHubDeploy.client.tsx @@ -7,6 +7,7 @@ import { useState } from 'react'; import type { ActionCallbackData } from '~/lib/runtime/message-parser'; import { chatId } from '~/lib/persistence/useChatHistory'; import { getLocalStorage } from '~/lib/persistence/localStorage'; +import { formatBuildFailureOutput } from './deployUtils'; export function useGitHubDeploy() { const [isDeploying, setIsDeploying] = useState(false); @@ -65,10 +66,12 @@ export function useGitHubDeploy() { // Then run it await artifact.runner.runAction(actionData); - if (!artifact.runner.buildOutput) { + const buildOutput = artifact.runner.buildOutput; + + if (!buildOutput || buildOutput.exitCode !== 0) { // Notify that build failed deployArtifact.runner.handleDeployAction('building', 'failed', { - error: 'Build failed. Check the terminal for details.', + error: formatBuildFailureOutput(buildOutput?.output), source: 'github', }); throw new Error('Build failed'); diff --git a/app/components/deploy/GitLabDeploy.client.tsx b/app/components/deploy/GitLabDeploy.client.tsx index 1173bac8a6..92bd274d79 100644 --- a/app/components/deploy/GitLabDeploy.client.tsx +++ b/app/components/deploy/GitLabDeploy.client.tsx @@ -7,6 +7,7 @@ import { useState } from 'react'; import type { ActionCallbackData } from '~/lib/runtime/message-parser'; import { chatId } from '~/lib/persistence/useChatHistory'; import { getLocalStorage } from '~/lib/persistence/localStorage'; +import { formatBuildFailureOutput } from './deployUtils'; export function useGitLabDeploy() { const [isDeploying, setIsDeploying] = useState(false); @@ -65,10 +66,12 @@ export function useGitLabDeploy() { // Then run it await artifact.runner.runAction(actionData); - if (!artifact.runner.buildOutput) { + const buildOutput = artifact.runner.buildOutput; + + if (!buildOutput || buildOutput.exitCode !== 0) { // Notify that build failed deployArtifact.runner.handleDeployAction('building', 'failed', { - error: 'Build failed. Check the terminal for details.', + error: formatBuildFailureOutput(buildOutput?.output), source: 'gitlab', }); throw new Error('Build failed'); diff --git a/app/components/deploy/NetlifyDeploy.client.tsx b/app/components/deploy/NetlifyDeploy.client.tsx index 327efba3a9..2c0a71326f 100644 --- a/app/components/deploy/NetlifyDeploy.client.tsx +++ b/app/components/deploy/NetlifyDeploy.client.tsx @@ -7,6 +7,7 @@ import { path } from '~/utils/path'; import { useState } from 'react'; import type { ActionCallbackData } from '~/lib/runtime/message-parser'; import { chatId } from '~/lib/persistence/useChatHistory'; +import { formatBuildFailureOutput } from './deployUtils'; export function useNetlifyDeploy() { const [isDeploying, setIsDeploying] = useState(false); @@ -65,10 +66,12 @@ export function useNetlifyDeploy() { // Then run it await artifact.runner.runAction(actionData); - if (!artifact.runner.buildOutput) { + const buildOutput = artifact.runner.buildOutput; + + if (!buildOutput || buildOutput.exitCode !== 0) { // Notify that build failed deployArtifact.runner.handleDeployAction('building', 'failed', { - error: 'Build failed. Check the terminal for details.', + error: formatBuildFailureOutput(buildOutput?.output), source: 'netlify', }); throw new Error('Build failed'); @@ -81,7 +84,7 @@ export function useNetlifyDeploy() { const container = await webcontainer; // Remove /home/project from buildPath if it exists - const buildPath = artifact.runner.buildOutput.path.replace('/home/project', ''); + const buildPath = buildOutput.path.replace('/home/project', ''); console.log('Original buildPath', buildPath); diff --git a/app/components/deploy/VercelDeploy.client.tsx b/app/components/deploy/VercelDeploy.client.tsx index 46d2d415b4..b98f1429ba 100644 --- a/app/components/deploy/VercelDeploy.client.tsx +++ b/app/components/deploy/VercelDeploy.client.tsx @@ -7,6 +7,7 @@ import { path } from '~/utils/path'; import { useState } from 'react'; import type { ActionCallbackData } from '~/lib/runtime/message-parser'; import { chatId } from '~/lib/persistence/useChatHistory'; +import { formatBuildFailureOutput } from './deployUtils'; export function useVercelDeploy() { const [isDeploying, setIsDeploying] = useState(false); @@ -64,10 +65,12 @@ export function useVercelDeploy() { // Then run it await artifact.runner.runAction(actionData); - if (!artifact.runner.buildOutput) { + const buildOutput = artifact.runner.buildOutput; + + if (!buildOutput || buildOutput.exitCode !== 0) { // Notify that build failed deployArtifact.runner.handleDeployAction('building', 'failed', { - error: 'Build failed. Check the terminal for details.', + error: formatBuildFailureOutput(buildOutput?.output), source: 'vercel', }); throw new Error('Build failed'); @@ -80,7 +83,7 @@ export function useVercelDeploy() { const container = await webcontainer; // Remove /home/project from buildPath if it exists - const buildPath = artifact.runner.buildOutput.path.replace('/home/project', ''); + const buildPath = buildOutput.path.replace('/home/project', ''); // Check if the build path exists let finalBuildPath = buildPath; diff --git a/app/components/deploy/deployUtils.ts b/app/components/deploy/deployUtils.ts new file mode 100644 index 0000000000..e8019c2886 --- /dev/null +++ b/app/components/deploy/deployUtils.ts @@ -0,0 +1,15 @@ +const MAX_BUILD_OUTPUT_CHARS = 4000; + +export function formatBuildFailureOutput(output?: string) { + const trimmed = output?.trim(); + + if (!trimmed) { + return 'Build failed with no output captured.'; + } + + if (trimmed.length <= MAX_BUILD_OUTPUT_CHARS) { + return trimmed; + } + + return `Build output (truncated):\n${trimmed.slice(-MAX_BUILD_OUTPUT_CHARS)}`; +} diff --git a/app/lib/runtime/action-runner.ts b/app/lib/runtime/action-runner.ts index b14d3a89b0..64f5ee6d1b 100644 --- a/app/lib/runtime/action-runner.ts +++ b/app/lib/runtime/action-runner.ts @@ -395,7 +395,7 @@ export class ActionRunner { const buildProcess = await webcontainer.spawn('npm', ['run', 'build']); let output = ''; - buildProcess.output.pipeTo( + const outputPromise = buildProcess.output.pipeTo( new WritableStream({ write(data) { output += data; @@ -404,8 +404,21 @@ export class ActionRunner { ); const exitCode = await buildProcess.exit; + await outputPromise.catch(() => { + // Ignore output piping errors; we still have whatever was captured + }); + + let buildDir = ''; if (exitCode !== 0) { + const buildResult = { + path: buildDir, + exitCode, + output, + }; + + this.buildOutput = buildResult; + // Trigger build failed alert this.onDeployAlert?.({ type: 'error', @@ -435,8 +448,6 @@ export class ActionRunner { // Check for common build directories const commonBuildDirs = ['dist', 'build', 'out', 'output', '.next', 'public']; - let buildDir = ''; - // Try to find the first existing build directory for (const dir of commonBuildDirs) { const dirPath = nodePath.join(webcontainer.workdir, dir); @@ -455,11 +466,15 @@ export class ActionRunner { buildDir = nodePath.join(webcontainer.workdir, 'dist'); } - return { + const buildResult = { path: buildDir, exitCode, output, }; + + this.buildOutput = buildResult; + + return buildResult; } async handleSupabaseAction(action: SupabaseAction) { const { operation, content, filePath } = action; diff --git a/app/routes/api.netlify-deploy.ts b/app/routes/api.netlify-deploy.ts index 48543e97cc..40dd65eb34 100644 --- a/app/routes/api.netlify-deploy.ts +++ b/app/routes/api.netlify-deploy.ts @@ -8,6 +8,23 @@ interface DeployRequestBody { chatId: string; } +async function readNetlifyError(response: Response) { + try { + const contentType = response.headers.get('content-type') || ''; + + if (contentType.includes('application/json')) { + const data = (await response.json()) as { message?: string; error?: string } | undefined; + return data?.message || data?.error || JSON.stringify(data); + } + + const text = await response.text(); + + return text; + } catch { + return undefined; + } +} + export async function action({ request }: ActionFunctionArgs) { try { const { siteId, files, token, chatId } = (await request.json()) as DeployRequestBody & { token: string }; @@ -35,7 +52,11 @@ export async function action({ request }: ActionFunctionArgs) { }); if (!createSiteResponse.ok) { - return json({ error: 'Failed to create site' }, { status: 400 }); + const errorDetail = await readNetlifyError(createSiteResponse); + return json( + { error: `Failed to create site${errorDetail ? `: ${errorDetail}` : ''}` }, + { status: createSiteResponse.status }, + ); } const newSite = (await createSiteResponse.json()) as any; @@ -84,7 +105,11 @@ export async function action({ request }: ActionFunctionArgs) { }); if (!createSiteResponse.ok) { - return json({ error: 'Failed to create site' }, { status: 400 }); + const errorDetail = await readNetlifyError(createSiteResponse); + return json( + { error: `Failed to create site${errorDetail ? `: ${errorDetail}` : ''}` }, + { status: createSiteResponse.status }, + ); } const newSite = (await createSiteResponse.json()) as any; @@ -121,18 +146,22 @@ export async function action({ request }: ActionFunctionArgs) { skip_processing: false, draft: false, // Change this to false for production deployments function_schedules: [], - required: Object.keys(fileDigests), // Add this line framework: null, }), }); if (!deployResponse.ok) { - return json({ error: 'Failed to create deployment' }, { status: 400 }); + const errorDetail = await readNetlifyError(deployResponse); + return json( + { error: `Failed to create deployment${errorDetail ? `: ${errorDetail}` : ''}` }, + { status: deployResponse.status }, + ); } const deploy = (await deployResponse.json()) as any; let retryCount = 0; const maxRetries = 60; + let filesUploaded = false; // Poll until deploy is ready for file uploads while (retryCount < maxRetries) { @@ -142,12 +171,24 @@ export async function action({ request }: ActionFunctionArgs) { }, }); + if (!statusResponse.ok) { + const errorDetail = await readNetlifyError(statusResponse); + return json( + { error: `Failed to check deployment status${errorDetail ? `: ${errorDetail}` : ''}` }, + { status: statusResponse.status }, + ); + } + const status = (await statusResponse.json()) as any; - if (status.state === 'prepared' || status.state === 'uploaded') { + if (!filesUploaded && (status.state === 'prepared' || status.state === 'uploaded')) { // Upload all files regardless of required array for (const [filePath, content] of Object.entries(files)) { const normalizedPath = filePath.startsWith('/') ? filePath : '/' + filePath; + const encodedPath = normalizedPath + .split('/') + .map((segment) => encodeURIComponent(segment)) + .join('/'); let uploadSuccess = false; let uploadRetries = 0; @@ -155,7 +196,7 @@ export async function action({ request }: ActionFunctionArgs) { while (!uploadSuccess && uploadRetries < 3) { try { const uploadResponse = await fetch( - `https://api.netlify.com/api/v1/deploys/${deploy.id}/files${normalizedPath}`, + `https://api.netlify.com/api/v1/deploys/${deploy.id}/files${encodedPath}`, { method: 'PUT', headers: { @@ -184,21 +225,21 @@ export async function action({ request }: ActionFunctionArgs) { return json({ error: `Failed to upload file ${filePath}` }, { status: 500 }); } } + + filesUploaded = true; } if (status.state === 'ready') { // Only return after files are uploaded - if (Object.keys(files).length === 0 || status.summary?.status === 'ready') { - return json({ - success: true, - deploy: { - id: status.id, - state: status.state, - url: status.ssl_url || status.url, - }, - site: siteInfo, - }); - } + return json({ + success: true, + deploy: { + id: status.id, + state: status.state, + url: status.ssl_url || status.url, + }, + site: siteInfo, + }); } if (status.state === 'error') { From 4e343f1a829a875cccd81299075fa04011d2d1be Mon Sep 17 00:00:00 2001 From: Gerome Elassaad Date: Fri, 6 Feb 2026 07:55:09 +1100 Subject: [PATCH 2/4] fix: remove 'use client' directives incompatible with Vite (#2033) * Remove 'use client' directive from Collapsible.tsx removed incompatible 'use client' * Remove 'use client' directive from ScrollArea.tsx incompatible 'use client' removed * Remove 'use client' directive from Badge.tsx incompatible 'use client' removed --- app/components/ui/Badge.tsx | 2 -- app/components/ui/Collapsible.tsx | 2 -- app/components/ui/ScrollArea.tsx | 2 -- 3 files changed, 6 deletions(-) diff --git a/app/components/ui/Badge.tsx b/app/components/ui/Badge.tsx index 14729e6b07..720f9c35a3 100644 --- a/app/components/ui/Badge.tsx +++ b/app/components/ui/Badge.tsx @@ -1,5 +1,3 @@ -'use client'; - import * as React from 'react'; import { cva, type VariantProps } from 'class-variance-authority'; import { classNames } from '~/utils/classNames'; diff --git a/app/components/ui/Collapsible.tsx b/app/components/ui/Collapsible.tsx index 61ddbbb6d5..279f6a5894 100644 --- a/app/components/ui/Collapsible.tsx +++ b/app/components/ui/Collapsible.tsx @@ -1,5 +1,3 @@ -'use client'; - import * as CollapsiblePrimitive from '@radix-ui/react-collapsible'; const Collapsible = CollapsiblePrimitive.Root; diff --git a/app/components/ui/ScrollArea.tsx b/app/components/ui/ScrollArea.tsx index 38176a28d0..e04fe28ffb 100644 --- a/app/components/ui/ScrollArea.tsx +++ b/app/components/ui/ScrollArea.tsx @@ -1,5 +1,3 @@ -'use client'; - import * as React from 'react'; import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'; import { classNames } from '~/utils/classNames'; From b7ef2247b880f25b868b809086bd0a29369d0f2b Mon Sep 17 00:00:00 2001 From: Stijnus <72551117+Stijnus@users.noreply.github.com> Date: Sat, 7 Feb 2026 15:26:46 +0100 Subject: [PATCH 3/4] feat: add Cerebras and Fireworks AI LLM providers (#2113) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: improve local model provider robustness and UX - Extract shared Docker URL rewriting and env conversion into BaseProvider to eliminate 4x duplicated code across Ollama and LMStudio - Add error handling and 5s timeouts to all model-listing fetches so one unreachable provider doesn't block the entire model list - Fix Ollama using createOllama() instead of mutating provider internals - Fix LLMManager singleton ignoring env updates on subsequent requests - Narrow cache key to only include provider-relevant env vars instead of the entire server environment - Fix 'as any' casts in LMStudio and OpenAILike by using shared convertEnvToRecord helper - Replace console.log/error with structured logger in OpenAILike - Fix typo: filteredStaticModesl -> filteredStaticModels in manager - Add connection status indicator (green/red dot) for local providers in the ModelSelector dropdown - Show helpful "is X running?" message when local provider has no models Co-Authored-By: Claude Opus 4.6 * feat: add Cerebras LLM provider - Add Cerebras provider with 8 models (Llama, GPT OSS, Qwen, ZAI GLM) - Integrate @ai-sdk/cerebras@0.2.16 for compatibility - Add CEREBRAS_API_KEY to environment configuration - Register provider in LLMManager registry Models included: - llama3.1-8b, llama-3.3-70b - gpt-oss-120b (reasoning) - qwen-3-32b, qwen-3-235b variants - zai-glm-4.6, zai-glm-4.7 (reasoning) Co-Authored-By: Claude Sonnet 4.5 * feat: add Fireworks AI LLM provider - Add Fireworks provider with 6 popular models - Integrate @ai-sdk/fireworks@0.2.16 for compatibility - Add FIREWORKS_API_KEY to environment configuration - Register provider in LLMManager registry Models included: - Llama 3.1 variants (405B, 70B, 8B Instruct) - DeepSeek R1 (reasoning model) - Qwen 2.5 72B Instruct - FireFunction V2 Co-Authored-By: Claude Sonnet 4.5 * feat: add coding-specific models to existing providers Enhanced providers with state-of-the-art coding models: **DeepSeek Provider:** + DeepSeek V3.2 (integrates thinking + tool-use) + DeepSeek V3.2-Speciale (high-compute variant, beats GPT-5) **Fireworks Provider:** + Qwen3-Coder 480B (262K context, best for coding) + Qwen3-Coder 30B (fast coding specialist) **Cerebras Provider:** + Qwen3-Coder 480B (2000 tokens/sec!) - Removed deprecated models (qwen-3-32b, llama-3.3-70b) Total new models: 4 Total coding models across all providers: 12+ Performance highlights: - Qwen3-Coder: State-of-the-art coding performance - DeepSeek V3.2: Integrates thinking directly into tool-use - ZAI GLM 4.6: 73.8% SWE-bench score - Ultra-fast inference: 2000 tok/s on Cerebras Co-Authored-By: Claude Sonnet 4.5 * feat: add dynamic model discovery to providers Implemented getDynamicModels() for automatic model discovery: **DeepSeek Provider:** - Fetches models from https://api.deepseek.com/models - Automatically discovers new models as DeepSeek adds them - Filters out static models to avoid duplicates **Cerebras Provider:** - Fetches models from https://api.cerebras.ai/v1/models - Auto-discovers new Cerebras models - Keeps UI up-to-date with latest offerings **Fireworks Provider:** - Fetches from https://api.fireworks.ai/v1/accounts/fireworks/models - Includes context_length from API response - Discovers new Qwen-Coder and other models automatically **Moonshot Provider:** - Fetches from https://api.moonshot.ai/v1/models - OpenAI-compatible endpoint - Auto-discovers new Kimi models Benefits: - ✅ No manual updates needed when providers add new models - ✅ Users always have access to latest models - ✅ Graceful fallback to static models if API fails - ✅ 5-second timeout prevents hanging - ✅ Caching system built into BaseProvider Technical details: - Uses BaseProvider's built-in caching system - Cache invalidates when API keys change - Failed API calls fallback to static models - All endpoints have 5-second timeout protection Co-Authored-By: Claude Sonnet 4.5 * feat: add Z.AI provider with GLM models and JWT authentication Merged changes from PR #2069 to add Z.AI provider: - Added GLM-4.6 (200K), GLM-4.5 (128K), and GLM-4.5 Flash models - Implemented secure JWT token generation with HMAC-SHA256 signing - Added dynamic model discovery from Z.AI API - Included proper error handling and token validation - GLM-4.6 achieves 73.8% on SWE-bench coding benchmarks Co-Authored-By: Claude Sonnet 4.5 --------- Co-authored-by: Claude Opus 4.6 --- .env.example | 12 + app/components/chat/ModelSelector.tsx | 85 ++- app/lib/modules/llm/base-provider.ts | 61 +- app/lib/modules/llm/manager.ts | 9 +- app/lib/modules/llm/providers/cerebras.ts | 137 +++++ app/lib/modules/llm/providers/deepseek.ts | 66 +++ app/lib/modules/llm/providers/fireworks.ts | 147 +++++ app/lib/modules/llm/providers/lmstudio.ts | 86 +-- app/lib/modules/llm/providers/moonshot.ts | 51 ++ app/lib/modules/llm/providers/ollama.ts | 119 ++-- app/lib/modules/llm/providers/openai-like.ts | 22 +- app/lib/modules/llm/providers/z-ai.ts | 193 +++++++ app/lib/modules/llm/registry.ts | 6 + package.json | 2 + pnpm-lock.yaml | 536 +++++++++++------- ....timestamp-1770328346417-a90f095482a09.mjs | 110 ++++ 16 files changed, 1303 insertions(+), 339 deletions(-) create mode 100644 app/lib/modules/llm/providers/cerebras.ts create mode 100644 app/lib/modules/llm/providers/fireworks.ts create mode 100644 app/lib/modules/llm/providers/z-ai.ts create mode 100644 vite.config.ts.timestamp-1770328346417-a90f095482a09.mjs diff --git a/.env.example b/.env.example index 9bec51ec64..b724838845 100644 --- a/.env.example +++ b/.env.example @@ -12,6 +12,14 @@ # Get your API key from: https://console.anthropic.com/ ANTHROPIC_API_KEY=your_anthropic_api_key_here +# Cerebras (High-performance inference) +# Get your API key from: https://cloud.cerebras.ai/settings +CEREBRAS_API_KEY=your_cerebras_api_key_here + +# Fireworks AI (Fast inference with FireAttention engine) +# Get your API key from: https://fireworks.ai/api-keys +FIREWORKS_API_KEY=your_fireworks_api_key_here + # OpenAI GPT models # Get your API key from: https://platform.openai.com/api-keys OPENAI_API_KEY=your_openai_api_key_here @@ -59,6 +67,10 @@ XAI_API_KEY=your_xai_api_key_here # Get your API key from: https://platform.moonshot.ai/console/api-keys MOONSHOT_API_KEY=your_moonshot_api_key_here +# Z.AI (GLM models with JWT authentication) +# Get your API key from: https://open.bigmodel.cn/usercenter/apikeys +ZAI_API_KEY=your_zai_api_key_here + # Hugging Face # Get your API key from: https://huggingface.co/settings/tokens HuggingFace_API_KEY=your_huggingface_api_key_here diff --git a/app/components/chat/ModelSelector.tsx b/app/components/chat/ModelSelector.tsx index 2ccb9a5277..c69331fcf4 100644 --- a/app/components/chat/ModelSelector.tsx +++ b/app/components/chat/ModelSelector.tsx @@ -3,6 +3,7 @@ import { useEffect, useState, useRef, useMemo, useCallback } from 'react'; import type { KeyboardEvent } from 'react'; import type { ModelInfo } from '~/lib/modules/llm/types'; import { classNames } from '~/utils/classNames'; +import { LOCAL_PROVIDERS } from '~/lib/stores/settings'; // Fuzzy search utilities const levenshteinDistance = (str1: string, str2: string): number => { @@ -130,6 +131,32 @@ export const ModelSelector = ({ const providerDropdownRef = useRef(null); const [showFreeModelsOnly, setShowFreeModelsOnly] = useState(false); + type ConnectionStatus = 'unknown' | 'connected' | 'disconnected'; + + const [localProviderStatus, setLocalProviderStatus] = useState>({}); + + // Check connectivity of local providers when provider list changes + useEffect(() => { + const checkLocalProviders = async () => { + const statuses: Record = {}; + + for (const p of providerList) { + if (!LOCAL_PROVIDERS.includes(p.name)) { + continue; + } + + // If the provider has models loaded, it's connected + const hasModels = modelList.some((m) => m.provider === p.name); + + statuses[p.name] = hasModels ? 'connected' : 'disconnected'; + } + + setLocalProviderStatus(statuses); + }; + + checkLocalProviders(); + }, [providerList, modelList]); + // Debounce search queries useEffect(() => { const timer = setTimeout(() => { @@ -440,7 +467,28 @@ export const ModelSelector = ({ tabIndex={0} >
-
{provider?.name || 'Select provider'}
+
+ {provider?.name && LOCAL_PROVIDERS.includes(provider.name) && ( + + )} + {provider?.name || 'Select provider'} +
-
+
+ {LOCAL_PROVIDERS.includes(providerOption.name) && ( + + )} + +
)) )} @@ -717,8 +779,17 @@ export const ModelSelector = ({ ? `No models match "${debouncedModelSearchQuery}"${showFreeModelsOnly ? ' (free only)' : ''}` : showFreeModelsOnly ? 'No free models available' - : 'No models available'} + : provider?.name && LOCAL_PROVIDERS.includes(provider.name) + ? `No models found — is ${provider.name} running?` + : 'No models available'}
+ {!debouncedModelSearchQuery && provider?.name && LOCAL_PROVIDERS.includes(provider.name) && ( +
+ Make sure {provider.name} is running and has at least one model loaded. + {provider.name === 'Ollama' && ' Try: ollama pull llama3.2'} + {provider.name === 'LMStudio' && ' Load a model in LM Studio first.'} +
+ )} {debouncedModelSearchQuery && (
Try searching for model names, context sizes (e.g., "128k", "1M"), or capabilities diff --git a/app/lib/modules/llm/base-provider.ts b/app/lib/modules/llm/base-provider.ts index 9cb23403d7..ada2f3a4d2 100644 --- a/app/lib/modules/llm/base-provider.ts +++ b/app/lib/modules/llm/base-provider.ts @@ -4,6 +4,9 @@ import type { IProviderSetting } from '~/types/model'; import { createOpenAI } from '@ai-sdk/openai'; import { LLMManager } from './manager'; +/** Default timeout for model listing API calls (5 seconds) */ +const MODEL_FETCH_TIMEOUT = 5_000; + export abstract class BaseProvider implements ProviderInfo { abstract name: string; abstract staticModels: ModelInfo[]; @@ -17,6 +20,48 @@ export abstract class BaseProvider implements ProviderInfo { labelForGetApiKey?: string; icon?: string; + /** + * Convert Cloudflare Env bindings to a plain Record. + * Useful because provider methods expect Record but + * Cloudflare Workers pass an Env interface. + */ + protected convertEnvToRecord(env?: Env): Record { + if (!env) { + return {}; + } + + return Object.entries(env).reduce( + (acc, [key, value]) => { + acc[key] = String(value); + + return acc; + }, + {} as Record, + ); + } + + /** + * Rewrite localhost / 127.0.0.1 URLs to host.docker.internal when + * running inside Docker. Only applies on the server side. + */ + protected resolveDockerUrl(baseUrl: string, serverEnv?: Record): string { + const isDocker = process?.env?.RUNNING_IN_DOCKER === 'true' || serverEnv?.RUNNING_IN_DOCKER === 'true'; + + if (!isDocker) { + return baseUrl; + } + + return baseUrl.replace('localhost', 'host.docker.internal').replace('127.0.0.1', 'host.docker.internal'); + } + + /** + * Create an AbortSignal that times out after the given milliseconds. + * Used to prevent model-listing fetches from hanging indefinitely. + */ + protected createTimeoutSignal(ms: number = MODEL_FETCH_TIMEOUT): AbortSignal { + return AbortSignal.timeout(ms); + } + getProviderBaseUrlAndKey(options: { apiKeys?: Record; providerSettings?: IProviderSetting; @@ -59,7 +104,6 @@ export abstract class BaseProvider implements ProviderInfo { serverEnv?: Record; }): ModelInfo[] | null { if (!this.cachedDynamicModels) { - // console.log('no dynamic models',this.name); return null; } @@ -67,8 +111,8 @@ export abstract class BaseProvider implements ProviderInfo { const generatedCacheKey = this.getDynamicModelsCacheKey(options); if (cacheKey !== generatedCacheKey) { - // console.log('cache key mismatch',this.name,cacheKey,generatedCacheKey); this.cachedDynamicModels = undefined; + return null; } @@ -79,10 +123,20 @@ export abstract class BaseProvider implements ProviderInfo { providerSettings?: Record; serverEnv?: Record; }) { + // Only include provider-relevant env keys, not the entire server environment + const relevantEnvKeys = [this.config.baseUrlKey, this.config.apiTokenKey].filter(Boolean) as string[]; + const relevantEnv: Record = {}; + + for (const key of relevantEnvKeys) { + if (options.serverEnv?.[key]) { + relevantEnv[key] = options.serverEnv[key]; + } + } + return JSON.stringify({ apiKeys: options.apiKeys?.[this.name], providerSettings: options.providerSettings?.[this.name], - serverEnv: options.serverEnv, + serverEnv: relevantEnv, }); } storeDynamicModels( @@ -95,7 +149,6 @@ export abstract class BaseProvider implements ProviderInfo { ) { const cacheId = this.getDynamicModelsCacheKey(options); - // console.log('caching dynamic models',this.name,cacheId); this.cachedDynamicModels = { cacheId, models, diff --git a/app/lib/modules/llm/manager.ts b/app/lib/modules/llm/manager.ts index aec9190593..687c399ccf 100644 --- a/app/lib/modules/llm/manager.ts +++ b/app/lib/modules/llm/manager.ts @@ -9,7 +9,7 @@ export class LLMManager { private static _instance: LLMManager; private _providers: Map = new Map(); private _modelList: ModelInfo[] = []; - private readonly _env: any = {}; + private _env: Record = {}; private constructor(_env: Record) { this._registerProvidersFromDirectory(); @@ -19,6 +19,9 @@ export class LLMManager { static getInstance(env: Record = {}): LLMManager { if (!LLMManager._instance) { LLMManager._instance = new LLMManager(env); + } else if (Object.keys(env).length > 0) { + // Update env on subsequent calls so Cloudflare Workers get fresh bindings + LLMManager._instance._env = env; } return LLMManager._instance; @@ -121,10 +124,10 @@ export class LLMManager { const staticModels = Array.from(this._providers.values()).flatMap((p) => p.staticModels || []); const dynamicModelsFlat = dynamicModels.flat(); const dynamicModelKeys = dynamicModelsFlat.map((d) => `${d.name}-${d.provider}`); - const filteredStaticModesl = staticModels.filter((m) => !dynamicModelKeys.includes(`${m.name}-${m.provider}`)); + const filteredStaticModels = staticModels.filter((m) => !dynamicModelKeys.includes(`${m.name}-${m.provider}`)); // Combine static and dynamic models - const modelList = [...dynamicModelsFlat, ...filteredStaticModesl]; + const modelList = [...dynamicModelsFlat, ...filteredStaticModels]; modelList.sort((a, b) => a.name.localeCompare(b.name)); this._modelList = modelList; diff --git a/app/lib/modules/llm/providers/cerebras.ts b/app/lib/modules/llm/providers/cerebras.ts new file mode 100644 index 0000000000..3ac15d0eab --- /dev/null +++ b/app/lib/modules/llm/providers/cerebras.ts @@ -0,0 +1,137 @@ +import { BaseProvider } from '~/lib/modules/llm/base-provider'; +import type { ModelInfo } from '~/lib/modules/llm/types'; +import type { IProviderSetting } from '~/types/model'; +import type { LanguageModelV1 } from 'ai'; +import { createCerebras } from '@ai-sdk/cerebras'; + +export default class CerebrasProvider extends BaseProvider { + name = 'Cerebras'; + getApiKeyLink = 'https://cloud.cerebras.ai/settings'; + + config = { + apiTokenKey: 'CEREBRAS_API_KEY', + }; + + staticModels: ModelInfo[] = [ + { + name: 'qwen3-coder-480b', + label: 'Qwen3-Coder 480B (2000 tok/s, Best for Coding)', + provider: 'Cerebras', + maxTokenAllowed: 262000, + }, + { + name: 'llama3.1-8b', + label: 'Llama 3.1 8B', + provider: 'Cerebras', + maxTokenAllowed: 8000, + }, + { + name: 'gpt-oss-120b', + label: 'GPT OSS 120B (Reasoning)', + provider: 'Cerebras', + maxTokenAllowed: 8000, + }, + { + name: 'qwen-3-235b-a22b-instruct-2507', + label: 'Qwen 3 235B A22B Instruct', + provider: 'Cerebras', + maxTokenAllowed: 8000, + }, + { + name: 'qwen-3-235b-a22b-thinking-2507', + label: 'Qwen 3 235B A22B Thinking', + provider: 'Cerebras', + maxTokenAllowed: 8000, + }, + { + name: 'zai-glm-4.6', + label: 'ZAI GLM 4.6 (Coding: 73.8% SWE-bench)', + provider: 'Cerebras', + maxTokenAllowed: 8000, + }, + { + name: 'zai-glm-4.7', + label: 'ZAI GLM 4.7 (Reasoning)', + provider: 'Cerebras', + maxTokenAllowed: 8000, + }, + ]; + + async getDynamicModels( + apiKeys?: Record, + settings?: IProviderSetting, + serverEnv?: Record, + ): Promise { + const { apiKey } = this.getProviderBaseUrlAndKey({ + apiKeys, + providerSettings: settings, + serverEnv: serverEnv as any, + defaultBaseUrlKey: '', + defaultApiTokenKey: 'CEREBRAS_API_KEY', + }); + + if (!apiKey) { + return []; + } + + try { + const response = await fetch('https://api.cerebras.ai/v1/models', { + headers: { + Authorization: `Bearer ${apiKey}`, + }, + signal: this.createTimeoutSignal(5000), + }); + + if (!response.ok) { + console.error(`Cerebras API error: ${response.statusText}`); + return []; + } + + const data = (await response.json()) as any; + const staticModelIds = this.staticModels.map((m) => m.name); + + // Filter out models we already have in staticModels + const dynamicModels = + data.data + ?.filter((model: any) => !staticModelIds.includes(model.id)) + .map((m: any) => ({ + name: m.id, + label: `${m.id} (Dynamic)`, + provider: this.name, + maxTokenAllowed: 32000, // Default, Cerebras typically has good context + })) || []; + + return dynamicModels; + } catch (error) { + console.error(`Failed to fetch Cerebras models:`, error); + return []; + } + } + + getModelInstance(options: { + model: string; + serverEnv: Env; + apiKeys?: Record; + providerSettings?: Record; + }): LanguageModelV1 { + const { model, serverEnv, apiKeys, providerSettings } = options; + + const { apiKey } = this.getProviderBaseUrlAndKey({ + apiKeys, + providerSettings: providerSettings?.[this.name], + serverEnv: serverEnv as any, + defaultBaseUrlKey: '', + defaultApiTokenKey: 'CEREBRAS_API_KEY', + }); + + if (!apiKey) { + throw new Error(`Missing API key for ${this.name} provider`); + } + + const cerebras = createCerebras({ + apiKey, + }); + + return cerebras(model); + } +} diff --git a/app/lib/modules/llm/providers/deepseek.ts b/app/lib/modules/llm/providers/deepseek.ts index 7c9042d7cd..2acc4fda44 100644 --- a/app/lib/modules/llm/providers/deepseek.ts +++ b/app/lib/modules/llm/providers/deepseek.ts @@ -34,8 +34,74 @@ export default class DeepseekProvider extends BaseProvider { maxTokenAllowed: 8000, maxCompletionTokens: 8192, }, + { + name: 'deepseek-v3.2', + label: 'DeepSeek V3.2 (Coding + Tool Use)', + provider: 'Deepseek', + maxTokenAllowed: 64000, + maxCompletionTokens: 8192, + }, + { + name: 'deepseek-v3.2-speciale', + label: 'DeepSeek V3.2 Speciale (High-Compute)', + provider: 'Deepseek', + maxTokenAllowed: 64000, + maxCompletionTokens: 8192, + }, ]; + async getDynamicModels( + apiKeys?: Record, + settings?: IProviderSetting, + serverEnv?: Record, + ): Promise { + const { apiKey } = this.getProviderBaseUrlAndKey({ + apiKeys, + providerSettings: settings, + serverEnv: serverEnv as any, + defaultBaseUrlKey: '', + defaultApiTokenKey: 'DEEPSEEK_API_KEY', + }); + + if (!apiKey) { + return []; + } + + try { + const response = await fetch('https://api.deepseek.com/models', { + headers: { + Authorization: `Bearer ${apiKey}`, + }, + signal: this.createTimeoutSignal(5000), + }); + + if (!response.ok) { + console.error(`DeepSeek API error: ${response.statusText}`); + return []; + } + + const data = (await response.json()) as any; + const staticModelIds = this.staticModels.map((m) => m.name); + + // Filter out models we already have in staticModels + const dynamicModels = + data.data + ?.filter((model: any) => !staticModelIds.includes(model.id)) + .map((m: any) => ({ + name: m.id, + label: `${m.id} (Dynamic)`, + provider: this.name, + maxTokenAllowed: 64000, // Default, adjust per model if available + maxCompletionTokens: 8192, + })) || []; + + return dynamicModels; + } catch (error) { + console.error(`Failed to fetch DeepSeek models:`, error); + return []; + } + } + getModelInstance(options: { model: string; serverEnv: Env; diff --git a/app/lib/modules/llm/providers/fireworks.ts b/app/lib/modules/llm/providers/fireworks.ts new file mode 100644 index 0000000000..7dfa8bdf80 --- /dev/null +++ b/app/lib/modules/llm/providers/fireworks.ts @@ -0,0 +1,147 @@ +import { BaseProvider } from '~/lib/modules/llm/base-provider'; +import type { ModelInfo } from '~/lib/modules/llm/types'; +import type { IProviderSetting } from '~/types/model'; +import type { LanguageModelV1 } from 'ai'; +import { createFireworks } from '@ai-sdk/fireworks'; + +export default class FireworksProvider extends BaseProvider { + name = 'Fireworks'; + getApiKeyLink = 'https://fireworks.ai/api-keys'; + + config = { + apiTokenKey: 'FIREWORKS_API_KEY', + }; + + staticModels: ModelInfo[] = [ + { + name: 'accounts/fireworks/models/qwen3-coder-480b-a35b-instruct', + label: 'Qwen3-Coder 480B (Best for Coding)', + provider: 'Fireworks', + maxTokenAllowed: 262000, + }, + { + name: 'accounts/fireworks/models/qwen3-coder-30b-a3b-instruct', + label: 'Qwen3-Coder 30B (Fast Coding)', + provider: 'Fireworks', + maxTokenAllowed: 262000, + }, + { + name: 'accounts/fireworks/models/llama-v3p1-405b-instruct', + label: 'Llama 3.1 405B Instruct', + provider: 'Fireworks', + maxTokenAllowed: 128000, + }, + { + name: 'accounts/fireworks/models/llama-v3p1-70b-instruct', + label: 'Llama 3.1 70B Instruct', + provider: 'Fireworks', + maxTokenAllowed: 128000, + }, + { + name: 'accounts/fireworks/models/llama-v3p1-8b-instruct', + label: 'Llama 3.1 8B Instruct', + provider: 'Fireworks', + maxTokenAllowed: 128000, + }, + { + name: 'accounts/fireworks/models/deepseek-r1', + label: 'DeepSeek R1 (Reasoning)', + provider: 'Fireworks', + maxTokenAllowed: 64000, + }, + { + name: 'accounts/fireworks/models/qwen2p5-72b-instruct', + label: 'Qwen 2.5 72B Instruct', + provider: 'Fireworks', + maxTokenAllowed: 128000, + }, + { + name: 'accounts/fireworks/models/firefunction-v2', + label: 'FireFunction V2', + provider: 'Fireworks', + maxTokenAllowed: 8000, + }, + ]; + + async getDynamicModels( + apiKeys?: Record, + settings?: IProviderSetting, + serverEnv?: Record, + ): Promise { + const { apiKey } = this.getProviderBaseUrlAndKey({ + apiKeys, + providerSettings: settings, + serverEnv: serverEnv as any, + defaultBaseUrlKey: '', + defaultApiTokenKey: 'FIREWORKS_API_KEY', + }); + + if (!apiKey) { + return []; + } + + try { + // Try the accounts/fireworks/models endpoint which lists public models + const response = await fetch('https://api.fireworks.ai/v1/accounts/fireworks/models?page_size=100', { + headers: { + Authorization: `Bearer ${apiKey}`, + }, + signal: this.createTimeoutSignal(5000), + }); + + if (!response.ok) { + console.error(`Fireworks API error: ${response.statusText}`); + return []; + } + + const data = (await response.json()) as any; + const staticModelIds = this.staticModels.map((m) => m.name); + + // Filter out models we already have in staticModels + const dynamicModels = + data.data + ?.filter((model: any) => { + const modelPath = `accounts/fireworks/models/${model.id}`; + return !staticModelIds.includes(modelPath) && !staticModelIds.includes(model.id); + }) + .map((m: any) => ({ + name: `accounts/fireworks/models/${m.id}`, + label: `${m.id} (Dynamic)`, + provider: this.name, + maxTokenAllowed: m.context_length || 128000, + })) || []; + + return dynamicModels; + } catch (error) { + console.error(`Failed to fetch Fireworks models:`, error); + return []; + } + } + + getModelInstance(options: { + model: string; + serverEnv: Env; + apiKeys?: Record; + providerSettings?: Record; + }): LanguageModelV1 { + const { model, serverEnv, apiKeys, providerSettings } = options; + + const { apiKey } = this.getProviderBaseUrlAndKey({ + apiKeys, + providerSettings: providerSettings?.[this.name], + serverEnv: serverEnv as any, + defaultBaseUrlKey: '', + defaultApiTokenKey: 'FIREWORKS_API_KEY', + }); + + if (!apiKey) { + throw new Error(`Missing API key for ${this.name} provider`); + } + + const fireworks = createFireworks({ + apiKey, + }); + + return fireworks(model); + } +} diff --git a/app/lib/modules/llm/providers/lmstudio.ts b/app/lib/modules/llm/providers/lmstudio.ts index fe5b27cd4a..e7f6793eb9 100644 --- a/app/lib/modules/llm/providers/lmstudio.ts +++ b/app/lib/modules/llm/providers/lmstudio.ts @@ -18,11 +18,11 @@ export default class LMStudioProvider extends BaseProvider { staticModels: ModelInfo[] = []; - async getDynamicModels( + private _resolveBaseUrl( apiKeys?: Record, settings?: IProviderSetting, - serverEnv: Record = {}, - ): Promise { + serverEnv?: Record, + ): string { let { baseUrl } = this.getProviderBaseUrlAndKey({ apiKeys, providerSettings: settings, @@ -35,27 +35,54 @@ export default class LMStudioProvider extends BaseProvider { throw new Error('No baseUrl found for LMStudio provider'); } - if (typeof window === 'undefined') { - /* - * Running in Server - * Backend: Check if we're running in Docker - */ - const isDocker = process?.env?.RUNNING_IN_DOCKER === 'true' || serverEnv?.RUNNING_IN_DOCKER === 'true'; + baseUrl = this.resolveDockerUrl(baseUrl, serverEnv); - baseUrl = isDocker ? baseUrl.replace('localhost', 'host.docker.internal') : baseUrl; - baseUrl = isDocker ? baseUrl.replace('127.0.0.1', 'host.docker.internal') : baseUrl; - } + return baseUrl; + } + + async getDynamicModels( + apiKeys?: Record, + settings?: IProviderSetting, + serverEnv: Record = {}, + ): Promise { + const baseUrl = this._resolveBaseUrl(apiKeys, settings, serverEnv); + + try { + const response = await fetch(`${baseUrl}/v1/models`, { + signal: this.createTimeoutSignal(), + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const data = (await response.json()) as { data: Array<{ id: string }> }; + + return data.data.map((model) => ({ + name: model.id, + label: model.id, + provider: this.name, + maxTokenAllowed: 8000, + })); + } catch (error) { + if (error instanceof DOMException && error.name === 'TimeoutError') { + logger.warn('LMStudio model fetch timed out — is LM Studio running?'); + + return []; + } - const response = await fetch(`${baseUrl}/v1/models`); - const data = (await response.json()) as { data: Array<{ id: string }> }; + if (error instanceof TypeError && error.message.includes('fetch')) { + logger.warn(`LMStudio not reachable at ${baseUrl} — is LM Studio running?`); - return data.data.map((model) => ({ - name: model.id, - label: model.id, - provider: this.name, - maxTokenAllowed: 8000, - })); + return []; + } + + logger.error('Error fetching LMStudio models:', error); + + return []; + } } + getModelInstance: (options: { model: string; serverEnv?: Env; @@ -63,24 +90,9 @@ export default class LMStudioProvider extends BaseProvider { providerSettings?: Record; }) => LanguageModelV1 = (options) => { const { apiKeys, providerSettings, serverEnv, model } = options; - let { baseUrl } = this.getProviderBaseUrlAndKey({ - apiKeys, - providerSettings: providerSettings?.[this.name], - serverEnv: serverEnv as any, - defaultBaseUrlKey: 'LMSTUDIO_API_BASE_URL', - defaultApiTokenKey: '', - }); + const envRecord = this.convertEnvToRecord(serverEnv); - if (!baseUrl) { - throw new Error('No baseUrl found for LMStudio provider'); - } - - const isDocker = process?.env?.RUNNING_IN_DOCKER === 'true' || serverEnv?.RUNNING_IN_DOCKER === 'true'; - - if (typeof window === 'undefined') { - baseUrl = isDocker ? baseUrl.replace('localhost', 'host.docker.internal') : baseUrl; - baseUrl = isDocker ? baseUrl.replace('127.0.0.1', 'host.docker.internal') : baseUrl; - } + const baseUrl = this._resolveBaseUrl(apiKeys, providerSettings?.[this.name], envRecord); logger.debug('LMStudio Base Url used: ', baseUrl); diff --git a/app/lib/modules/llm/providers/moonshot.ts b/app/lib/modules/llm/providers/moonshot.ts index a59f80e225..2417d43e0b 100644 --- a/app/lib/modules/llm/providers/moonshot.ts +++ b/app/lib/modules/llm/providers/moonshot.ts @@ -41,6 +41,57 @@ export default class MoonshotProvider extends BaseProvider { { name: 'kimi-thinking-preview', label: 'Kimi Thinking', provider: 'Moonshot', maxTokenAllowed: 128000 }, ]; + async getDynamicModels( + apiKeys?: Record, + settings?: IProviderSetting, + serverEnv?: Record, + ): Promise { + const { apiKey } = this.getProviderBaseUrlAndKey({ + apiKeys, + providerSettings: settings, + serverEnv: serverEnv as any, + defaultBaseUrlKey: '', + defaultApiTokenKey: 'MOONSHOT_API_KEY', + }); + + if (!apiKey) { + return []; + } + + try { + const response = await fetch('https://api.moonshot.ai/v1/models', { + headers: { + Authorization: `Bearer ${apiKey}`, + }, + signal: this.createTimeoutSignal(5000), + }); + + if (!response.ok) { + console.error(`Moonshot API error: ${response.statusText}`); + return []; + } + + const data = (await response.json()) as any; + const staticModelIds = this.staticModels.map((m) => m.name); + + // Filter out models we already have in staticModels + const dynamicModels = + data.data + ?.filter((model: any) => !staticModelIds.includes(model.id)) + .map((m: any) => ({ + name: m.id, + label: `${m.id} (Dynamic)`, + provider: this.name, + maxTokenAllowed: 128000, // Kimi models typically have large context + })) || []; + + return dynamicModels; + } catch (error) { + console.error(`Failed to fetch Moonshot models:`, error); + return []; + } + } + getModelInstance(options: { model: string; serverEnv: Env; diff --git a/app/lib/modules/llm/providers/ollama.ts b/app/lib/modules/llm/providers/ollama.ts index e50ecae561..3d7f53b8ee 100644 --- a/app/lib/modules/llm/providers/ollama.ts +++ b/app/lib/modules/llm/providers/ollama.ts @@ -2,7 +2,7 @@ import { BaseProvider } from '~/lib/modules/llm/base-provider'; import type { ModelInfo } from '~/lib/modules/llm/types'; import type { IProviderSetting } from '~/types/model'; import type { LanguageModelV1 } from 'ai'; -import { ollama } from 'ollama-ai-provider'; +import { createOllama } from 'ollama-ai-provider'; import { logger } from '~/utils/logger'; interface OllamaModelDetails { @@ -39,31 +39,17 @@ export default class OllamaProvider extends BaseProvider { staticModels: ModelInfo[] = []; - private _convertEnvToRecord(env?: Env): Record { - if (!env) { - return {}; - } - - // Convert Env to a plain object with string values - return Object.entries(env).reduce( - (acc, [key, value]) => { - acc[key] = String(value); - return acc; - }, - {} as Record, - ); - } - getDefaultNumCtx(serverEnv?: Env): number { - const envRecord = this._convertEnvToRecord(serverEnv); + const envRecord = this.convertEnvToRecord(serverEnv); + return envRecord.DEFAULT_NUM_CTX ? parseInt(envRecord.DEFAULT_NUM_CTX, 10) : 32768; } - async getDynamicModels( + private _resolveBaseUrl( apiKeys?: Record, settings?: IProviderSetting, - serverEnv: Record = {}, - ): Promise { + serverEnv?: Record, + ): string { let { baseUrl } = this.getProviderBaseUrlAndKey({ apiKeys, providerSettings: settings, @@ -73,31 +59,55 @@ export default class OllamaProvider extends BaseProvider { }); if (!baseUrl) { - throw new Error('No baseUrl found for OLLAMA provider'); + throw new Error('No baseUrl found for Ollama provider'); } - if (typeof window === 'undefined') { - /* - * Running in Server - * Backend: Check if we're running in Docker - */ - const isDocker = process?.env?.RUNNING_IN_DOCKER === 'true' || serverEnv?.RUNNING_IN_DOCKER === 'true'; + baseUrl = this.resolveDockerUrl(baseUrl, serverEnv); - baseUrl = isDocker ? baseUrl.replace('localhost', 'host.docker.internal') : baseUrl; - baseUrl = isDocker ? baseUrl.replace('127.0.0.1', 'host.docker.internal') : baseUrl; - } + return baseUrl; + } + + async getDynamicModels( + apiKeys?: Record, + settings?: IProviderSetting, + serverEnv: Record = {}, + ): Promise { + const baseUrl = this._resolveBaseUrl(apiKeys, settings, serverEnv); + + try { + const response = await fetch(`${baseUrl}/api/tags`, { + signal: this.createTimeoutSignal(), + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const data = (await response.json()) as OllamaApiResponse; - const response = await fetch(`${baseUrl}/api/tags`); - const data = (await response.json()) as OllamaApiResponse; + return data.models.map((model: OllamaModel) => ({ + name: model.name, + label: `${model.name} (${model.details.parameter_size})`, + provider: this.name, + maxTokenAllowed: 8000, + })); + } catch (error) { + if (error instanceof DOMException && error.name === 'TimeoutError') { + logger.warn('Ollama model fetch timed out — is Ollama running?'); - // console.log({ ollamamodels: data.models }); + return []; + } - return data.models.map((model: OllamaModel) => ({ - name: model.name, - label: `${model.name} (${model.details.parameter_size})`, - provider: this.name, - maxTokenAllowed: 8000, - })); + if (error instanceof TypeError && error.message.includes('fetch')) { + logger.warn(`Ollama not reachable at ${baseUrl} — is Ollama running?`); + + return []; + } + + logger.error('Error fetching Ollama models:', error); + + return []; + } } getModelInstance: (options: { @@ -107,33 +117,18 @@ export default class OllamaProvider extends BaseProvider { providerSettings?: Record; }) => LanguageModelV1 = (options) => { const { apiKeys, providerSettings, serverEnv, model } = options; - const envRecord = this._convertEnvToRecord(serverEnv); - - let { baseUrl } = this.getProviderBaseUrlAndKey({ - apiKeys, - providerSettings: providerSettings?.[this.name], - serverEnv: envRecord, - defaultBaseUrlKey: 'OLLAMA_API_BASE_URL', - defaultApiTokenKey: '', - }); + const envRecord = this.convertEnvToRecord(serverEnv); - // Backend: Check if we're running in Docker - if (!baseUrl) { - throw new Error('No baseUrl found for OLLAMA provider'); - } - - const isDocker = process?.env?.RUNNING_IN_DOCKER === 'true' || envRecord.RUNNING_IN_DOCKER === 'true'; - baseUrl = isDocker ? baseUrl.replace('localhost', 'host.docker.internal') : baseUrl; - baseUrl = isDocker ? baseUrl.replace('127.0.0.1', 'host.docker.internal') : baseUrl; + const baseUrl = this._resolveBaseUrl(apiKeys, providerSettings?.[this.name], envRecord); logger.debug('Ollama Base Url used: ', baseUrl); - const ollamaInstance = ollama(model, { - numCtx: this.getDefaultNumCtx(serverEnv), - }) as LanguageModelV1 & { config: any }; - - ollamaInstance.config.baseURL = `${baseUrl}/api`; + const ollamaProvider = createOllama({ + baseURL: `${baseUrl}/api`, + }); - return ollamaInstance; + return ollamaProvider(model, { + numCtx: this.getDefaultNumCtx(serverEnv), + }); }; } diff --git a/app/lib/modules/llm/providers/openai-like.ts b/app/lib/modules/llm/providers/openai-like.ts index 713da07bc0..85dc42d68b 100644 --- a/app/lib/modules/llm/providers/openai-like.ts +++ b/app/lib/modules/llm/providers/openai-like.ts @@ -2,6 +2,11 @@ import { BaseProvider, getOpenAILikeModel } from '~/lib/modules/llm/base-provide import type { ModelInfo } from '~/lib/modules/llm/types'; import type { IProviderSetting } from '~/types/model'; import type { LanguageModelV1 } from 'ai'; +import { logger } from '~/utils/logger'; + +interface OpenAIModelsResponse { + data: Array<{ id: string }>; +} export default class OpenAILikeProvider extends BaseProvider { name = 'OpenAILike'; @@ -37,29 +42,31 @@ export default class OpenAILikeProvider extends BaseProvider { headers: { Authorization: `Bearer ${apiKey}`, }, + signal: this.createTimeoutSignal(), }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } - const res = (await response.json()) as any; + const res = (await response.json()) as OpenAIModelsResponse; - return res.data.map((model: any) => ({ + return res.data.map((model) => ({ name: model.id, label: model.id, provider: this.name, maxTokenAllowed: 8000, })); } catch (error) { - console.log(`${this.name}: Not allowed to GET /models endpoint for provider`, error); + logger.info(`${this.name}: Could not fetch /models endpoint, checking fallback env`, error); // Fallback to OPENAI_LIKE_API_MODELS if available // eslint-disable-next-line dot-notation const modelsEnv = serverEnv['OPENAI_LIKE_API_MODELS'] || settings?.OPENAI_LIKE_API_MODELS; if (modelsEnv) { - console.log(`${this.name}: OPENAI_LIKE_API_MODELS=${modelsEnv}`); + logger.info(`${this.name}: Using OPENAI_LIKE_API_MODELS fallback`); + return this._parseModelsFromEnv(modelsEnv); } @@ -107,11 +114,11 @@ export default class OpenAILikeProvider extends BaseProvider { }); } - console.log(`${this.name}: Parsed Models:`, models); + logger.info(`${this.name}: Parsed ${models.length} models from env`); return models; } catch (error) { - console.error(`${this.name}: Error parsing OPENAI_LIKE_API_MODELS:`, error); + logger.error(`${this.name}: Error parsing OPENAI_LIKE_API_MODELS:`, error); return []; } } @@ -149,11 +156,12 @@ export default class OpenAILikeProvider extends BaseProvider { providerSettings?: Record; }): LanguageModelV1 { const { model, serverEnv, apiKeys, providerSettings } = options; + const envRecord = this.convertEnvToRecord(serverEnv); const { baseUrl, apiKey } = this.getProviderBaseUrlAndKey({ apiKeys, providerSettings: providerSettings?.[this.name], - serverEnv: serverEnv as any, + serverEnv: envRecord, defaultBaseUrlKey: 'OPENAI_LIKE_API_BASE_URL', defaultApiTokenKey: 'OPENAI_LIKE_API_KEY', }); diff --git a/app/lib/modules/llm/providers/z-ai.ts b/app/lib/modules/llm/providers/z-ai.ts new file mode 100644 index 0000000000..d400082015 --- /dev/null +++ b/app/lib/modules/llm/providers/z-ai.ts @@ -0,0 +1,193 @@ +import { BaseProvider } from '~/lib/modules/llm/base-provider'; +import type { IProviderSetting } from '~/types/model'; +import type { LanguageModelV1 } from 'ai'; +import type { ModelInfo } from '~/lib/modules/llm/types'; +import { createOpenAI } from '@ai-sdk/openai'; +import crypto from 'node:crypto'; + +export default class ZaiProvider extends BaseProvider { + name = 'Z.ai'; + getApiKeyLink = 'https://open.bigmodel.cn/usercenter/apikeys'; + + config = { + baseUrlKey: 'ZAI_BASE_URL', + apiTokenKey: 'ZAI_API_KEY', + baseUrl: 'https://api.z.ai/api/coding/paas/v4', //Dedicated endpoint for Coding Plan + }; + + staticModels: ModelInfo[] = [ + { + name: 'glm-4.6', + label: 'GLM-4.6 (200K)', + provider: 'Z.ai', + maxTokenAllowed: 200000, + maxCompletionTokens: 65536, + }, + { + name: 'glm-4.5', + label: 'GLM-4.5 (128K)', + provider: 'Z.ai', + maxTokenAllowed: 128000, + maxCompletionTokens: 65536, + }, + { + name: 'glm-4.5-flash', + label: 'GLM-4.5 Flash (128K)', + provider: 'Z.ai', + maxTokenAllowed: 128000, + maxCompletionTokens: 65536, + }, + ]; + + async getDynamicModels( + apiKeys?: Record, + settings?: IProviderSetting, + serverEnv?: Record, + ): Promise { + const { baseUrl, apiKey } = this.getProviderBaseUrlAndKey({ + apiKeys, + providerSettings: settings, + serverEnv: serverEnv as any, + defaultBaseUrlKey: 'ZAI_BASE_URL', + defaultApiTokenKey: 'ZAI_API_KEY', + }); + + if (!apiKey) { + throw new Error(`Missing Api Key configuration for ${this.name} provider`); + } + + const token = this._generateToken(apiKey); + + if (!this._isValidToken(token)) { + throw new Error(`Invalid API key format for ${this.name} provider`); + } + + try { + const response = await fetch(`${baseUrl}/models`, { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error(`Failed to fetch models: ${response.status} ${response.statusText}`); + } + + const res = (await response.json()) as any; + const staticModelIds = this.staticModels.map((m) => m.name); + + // Filter out static models and only include GLM models + const data = + res.data?.filter( + (model: any) => + model.object === 'model' && model.id?.startsWith('glm-') && !staticModelIds.includes(model.id), + ) || []; + + return data.map((m: any) => { + let contextWindow = 128000; + let maxCompletionTokens = 65536; + + if (m.id?.includes('glm-4.6')) { + contextWindow = 200000; + maxCompletionTokens = 65536; + } else if (m.id?.includes('glm-4.5')) { + contextWindow = 128000; + maxCompletionTokens = 65536; + } else if (m.id?.includes('glm-4')) { + contextWindow = 128000; + maxCompletionTokens = 8192; + } else if (m.id?.includes('glm-3')) { + contextWindow = 32000; + maxCompletionTokens = 4096; + } + + return { + name: m.id, + label: `${m.id} (${Math.floor(contextWindow / 1000)}k context)`, + provider: this.name, + maxTokenAllowed: contextWindow, + maxCompletionTokens, + }; + }); + } catch (error) { + console.error(`Failed to fetch dynamic models for ${this.name}:`, error); + return []; + } + } + + private _generateToken(apiKey: string): string { + try { + const [id, secret] = apiKey.split('.'); + + if (!id || !secret) { + throw new Error(`Invalid API key format for ${this.name}. Expected: id.secret`); + } + + const now = Date.now(); + const payload = { + apiKey: id, + exp: now + 3600 * 1000, + timestamp: now, + }; + + const header = { alg: 'HS256', sign_type: 'SIGN' }; + + const base64Url = (obj: any) => + Buffer.from(JSON.stringify(obj)).toString('base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); + const signature = crypto + .createHmac('sha256', secret) + .update(base64Url(header) + '.' + base64Url(payload)) + .digest('base64') + .replace(/=/g, '') + .replace(/\+/g, '-') + .replace(/\//g, '_'); + + return `${base64Url(header)}.${base64Url(payload)}.${signature}`; + } catch (error) { + console.error(`Failed to generate JWT token for ${this.name}:`, error); + throw new Error(`Failed to generate JWT token: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + } + + /** + * Validates JWT token format + */ + private _isValidToken(token: string): boolean { + try { + const parts = token.split('.'); + return parts.length === 3 && parts.every((part) => part.length > 0); + } catch { + return false; + } + } + + getModelInstance(options: { + model: string; + serverEnv: Env; + apiKeys?: Record; + providerSettings?: Record; + }): LanguageModelV1 { + const { model, serverEnv, apiKeys, providerSettings } = options; + + const { baseUrl, apiKey } = this.getProviderBaseUrlAndKey({ + apiKeys, + providerSettings: providerSettings?.[this.name], + serverEnv: serverEnv as any, + defaultBaseUrlKey: 'ZAI_BASE_URL', + defaultApiTokenKey: 'ZAI_API_KEY', + }); + + if (!apiKey) { + throw new Error(`Missing API key for ${this.name} provider`); + } + + const token = this._generateToken(apiKey); + const zaiClient = createOpenAI({ + baseURL: baseUrl, + apiKey: token, + }); + + return zaiClient(model); + } +} diff --git a/app/lib/modules/llm/registry.ts b/app/lib/modules/llm/registry.ts index a28e4f9f3a..01bbe81140 100644 --- a/app/lib/modules/llm/registry.ts +++ b/app/lib/modules/llm/registry.ts @@ -1,6 +1,8 @@ import AnthropicProvider from './providers/anthropic'; +import CerebrasProvider from './providers/cerebras'; import CohereProvider from './providers/cohere'; import DeepseekProvider from './providers/deepseek'; +import FireworksProvider from './providers/fireworks'; import GoogleProvider from './providers/google'; import GroqProvider from './providers/groq'; import HuggingFaceProvider from './providers/huggingface'; @@ -17,11 +19,14 @@ import HyperbolicProvider from './providers/hyperbolic'; import AmazonBedrockProvider from './providers/amazon-bedrock'; import GithubProvider from './providers/github'; import MoonshotProvider from './providers/moonshot'; +import ZaiProvider from './providers/z-ai'; export { AnthropicProvider, + CerebrasProvider, CohereProvider, DeepseekProvider, + FireworksProvider, GoogleProvider, GroqProvider, HuggingFaceProvider, @@ -38,4 +43,5 @@ export { LMStudioProvider, AmazonBedrockProvider, GithubProvider, + ZaiProvider, }; diff --git a/package.json b/package.json index b02ba021ec..5c8c2a977f 100644 --- a/package.json +++ b/package.json @@ -48,8 +48,10 @@ "dependencies": { "@ai-sdk/amazon-bedrock": "1.0.6", "@ai-sdk/anthropic": "0.0.39", + "@ai-sdk/cerebras": "^0.2.16", "@ai-sdk/cohere": "1.0.3", "@ai-sdk/deepseek": "0.1.3", + "@ai-sdk/fireworks": "^0.2.16", "@ai-sdk/google": "0.0.52", "@ai-sdk/mistral": "0.0.43", "@ai-sdk/openai": "1.1.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e4061c9079..b397e9814a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,12 +17,18 @@ importers: '@ai-sdk/anthropic': specifier: 0.0.39 version: 0.0.39(zod@3.25.76) + '@ai-sdk/cerebras': + specifier: ^0.2.16 + version: 0.2.16(zod@3.25.76) '@ai-sdk/cohere': specifier: 1.0.3 version: 1.0.3(zod@3.25.76) '@ai-sdk/deepseek': specifier: 0.1.3 version: 0.1.3(zod@3.25.76) + '@ai-sdk/fireworks': + specifier: ^0.2.16 + version: 0.2.16(zod@3.25.76) '@ai-sdk/google': specifier: 0.0.52 version: 0.0.52(zod@3.25.76) @@ -504,6 +510,12 @@ packages: peerDependencies: zod: ^3.0.0 + '@ai-sdk/cerebras@0.2.16': + resolution: {integrity: sha512-FbT3gFYADXwyjQlpluWxl5fRnkJvGMHX5ahLZZ7qqpDQHH86ZO6X9j9Gk6vcMCwNPpI7+miiK79q1e5wzVHBSQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.0.0 + '@ai-sdk/cohere@1.0.3': resolution: {integrity: sha512-SDjPinUcGzTNiSMN+9zs1fuAcP8rU1/+CmDWAGu7eMhwVGDurgiOqscC0Oqs/aLsodLt/sFeOvyqj86DAknpbg==} engines: {node: '>=18'} @@ -516,6 +528,12 @@ packages: peerDependencies: zod: ^3.0.0 + '@ai-sdk/fireworks@0.2.16': + resolution: {integrity: sha512-YHUqW9QHMNjEg5vF0cmhnlAQJCMljWVWNAAFmKCPX31Dj4JaoCjOrIInrNEFerFRaO64hEffhlhuC1EmuO2Lyg==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.0.0 + '@ai-sdk/google@0.0.52': resolution: {integrity: sha512-bfsA/1Ae0SQ6NfLwWKs5SU4MBwlzJjVhK6bTVBicYFjUxg9liK/W76P1Tq/qK9OlrODACz3i1STOIWsFPpIOuQ==} engines: {node: '>=18'} @@ -534,6 +552,12 @@ packages: peerDependencies: zod: ^3.0.0 + '@ai-sdk/openai-compatible@0.2.16': + resolution: {integrity: sha512-LkvfcM8slJedRyJa/MiMiaOzcMjV1zNDwzTHEGz7aAsgsQV0maLfmJRi/nuSwf5jmp0EouC+JXXDUj2l94HgQw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.0.0 + '@ai-sdk/openai@1.1.2': resolution: {integrity: sha512-9rfcwjl4g1/Bdr2SmgFQr+aw81r62MvIKE7QDHMC4ulFd/Hej2oClROSMpDFZHXzs7RGeb32VkRyCHUWWgN3RQ==} engines: {node: '>=18'} @@ -793,6 +817,10 @@ packages: resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + '@babel/compat-data@7.28.0': resolution: {integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==} engines: {node: '>=6.9.0'} @@ -863,6 +891,10 @@ packages: resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.27.1': resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} @@ -928,6 +960,10 @@ packages: resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==} engines: {node: '>=6.9.0'} + '@babel/runtime@7.28.6': + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + engines: {node: '>=6.9.0'} + '@babel/template@7.27.2': resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} @@ -1148,14 +1184,14 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.25.4': - resolution: {integrity: sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==} + '@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.25.8': - resolution: {integrity: sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==} + '@esbuild/aix-ppc64@0.25.4': + resolution: {integrity: sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] @@ -1178,14 +1214,14 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.25.4': - resolution: {integrity: sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==} + '@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.25.8': - resolution: {integrity: sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==} + '@esbuild/android-arm64@0.25.4': + resolution: {integrity: sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==} engines: {node: '>=18'} cpu: [arm64] os: [android] @@ -1208,14 +1244,14 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-arm@0.25.4': - resolution: {integrity: sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==} + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-arm@0.25.8': - resolution: {integrity: sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==} + '@esbuild/android-arm@0.25.4': + resolution: {integrity: sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==} engines: {node: '>=18'} cpu: [arm] os: [android] @@ -1238,14 +1274,14 @@ packages: cpu: [x64] os: [android] - '@esbuild/android-x64@0.25.4': - resolution: {integrity: sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==} + '@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.25.8': - resolution: {integrity: sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==} + '@esbuild/android-x64@0.25.4': + resolution: {integrity: sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==} engines: {node: '>=18'} cpu: [x64] os: [android] @@ -1268,14 +1304,14 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.25.4': - resolution: {integrity: sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==} + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.25.8': - resolution: {integrity: sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==} + '@esbuild/darwin-arm64@0.25.4': + resolution: {integrity: sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] @@ -1298,14 +1334,14 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.25.4': - resolution: {integrity: sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==} + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.25.8': - resolution: {integrity: sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==} + '@esbuild/darwin-x64@0.25.4': + resolution: {integrity: sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==} engines: {node: '>=18'} cpu: [x64] os: [darwin] @@ -1328,14 +1364,14 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.25.4': - resolution: {integrity: sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==} + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.25.8': - resolution: {integrity: sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==} + '@esbuild/freebsd-arm64@0.25.4': + resolution: {integrity: sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] @@ -1358,14 +1394,14 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.4': - resolution: {integrity: sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==} + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.8': - resolution: {integrity: sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==} + '@esbuild/freebsd-x64@0.25.4': + resolution: {integrity: sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] @@ -1388,14 +1424,14 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.25.4': - resolution: {integrity: sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==} + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.25.8': - resolution: {integrity: sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==} + '@esbuild/linux-arm64@0.25.4': + resolution: {integrity: sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] @@ -1418,14 +1454,14 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.25.4': - resolution: {integrity: sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==} + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.25.8': - resolution: {integrity: sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==} + '@esbuild/linux-arm@0.25.4': + resolution: {integrity: sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] @@ -1448,14 +1484,14 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.25.4': - resolution: {integrity: sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==} + '@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.25.8': - resolution: {integrity: sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==} + '@esbuild/linux-ia32@0.25.4': + resolution: {integrity: sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==} engines: {node: '>=18'} cpu: [ia32] os: [linux] @@ -1478,14 +1514,14 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.25.4': - resolution: {integrity: sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==} + '@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.25.8': - resolution: {integrity: sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==} + '@esbuild/linux-loong64@0.25.4': + resolution: {integrity: sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==} engines: {node: '>=18'} cpu: [loong64] os: [linux] @@ -1508,14 +1544,14 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.25.4': - resolution: {integrity: sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==} + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.25.8': - resolution: {integrity: sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==} + '@esbuild/linux-mips64el@0.25.4': + resolution: {integrity: sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] @@ -1538,14 +1574,14 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.25.4': - resolution: {integrity: sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==} + '@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.25.8': - resolution: {integrity: sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==} + '@esbuild/linux-ppc64@0.25.4': + resolution: {integrity: sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] @@ -1568,14 +1604,14 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.25.4': - resolution: {integrity: sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==} + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.25.8': - resolution: {integrity: sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==} + '@esbuild/linux-riscv64@0.25.4': + resolution: {integrity: sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] @@ -1598,14 +1634,14 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.25.4': - resolution: {integrity: sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==} + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.25.8': - resolution: {integrity: sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==} + '@esbuild/linux-s390x@0.25.4': + resolution: {integrity: sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==} engines: {node: '>=18'} cpu: [s390x] os: [linux] @@ -1628,26 +1664,26 @@ packages: cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.25.4': - resolution: {integrity: sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==} + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.25.8': - resolution: {integrity: sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==} + '@esbuild/linux-x64@0.25.4': + resolution: {integrity: sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.4': - resolution: {integrity: sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==} + '@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.25.8': - resolution: {integrity: sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==} + '@esbuild/netbsd-arm64@0.25.4': + resolution: {integrity: sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] @@ -1670,14 +1706,14 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.4': - resolution: {integrity: sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==} + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.8': - resolution: {integrity: sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==} + '@esbuild/netbsd-x64@0.25.4': + resolution: {integrity: sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] @@ -1688,14 +1724,14 @@ packages: cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-arm64@0.25.4': - resolution: {integrity: sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==} + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-arm64@0.25.8': - resolution: {integrity: sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==} + '@esbuild/openbsd-arm64@0.25.4': + resolution: {integrity: sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] @@ -1718,20 +1754,20 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.4': - resolution: {integrity: sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==} + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.8': - resolution: {integrity: sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==} + '@esbuild/openbsd-x64@0.25.4': + resolution: {integrity: sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.25.8': - resolution: {integrity: sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==} + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] @@ -1754,14 +1790,14 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.25.4': - resolution: {integrity: sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==} + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.25.8': - resolution: {integrity: sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==} + '@esbuild/sunos-x64@0.25.4': + resolution: {integrity: sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==} engines: {node: '>=18'} cpu: [x64] os: [sunos] @@ -1784,14 +1820,14 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.25.4': - resolution: {integrity: sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==} + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.25.8': - resolution: {integrity: sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==} + '@esbuild/win32-arm64@0.25.4': + resolution: {integrity: sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==} engines: {node: '>=18'} cpu: [arm64] os: [win32] @@ -1814,14 +1850,14 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.25.4': - resolution: {integrity: sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==} + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.25.8': - resolution: {integrity: sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==} + '@esbuild/win32-ia32@0.25.4': + resolution: {integrity: sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==} engines: {node: '>=18'} cpu: [ia32] os: [win32] @@ -1844,14 +1880,14 @@ packages: cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.25.4': - resolution: {integrity: sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==} + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} engines: {node: '>=18'} cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.25.8': - resolution: {integrity: sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==} + '@esbuild/win32-x64@0.25.4': + resolution: {integrity: sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -4353,6 +4389,15 @@ packages: supports-color: optional: true + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decimal.js@10.6.0: resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} @@ -4637,13 +4682,13 @@ packages: engines: {node: '>=18'} hasBin: true - esbuild@0.25.4: - resolution: {integrity: sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==} + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} hasBin: true - esbuild@0.25.8: - resolution: {integrity: sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==} + esbuild@0.25.4: + resolution: {integrity: sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==} engines: {node: '>=18'} hasBin: true @@ -4834,8 +4879,8 @@ packages: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} - exponential-backoff@3.1.2: - resolution: {integrity: sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==} + exponential-backoff@3.1.3: + resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} express-rate-limit@7.5.1: resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} @@ -4998,6 +5043,10 @@ packages: resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} engines: {node: '>=14.14'} + fs-extra@11.3.3: + resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==} + engines: {node: '>=14.14'} + fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -5089,7 +5138,7 @@ packages: glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} engines: {node: '>=12'} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me glob@9.3.5: resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} @@ -5339,8 +5388,8 @@ packages: inline-style-parser@0.2.4: resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} - ip-address@9.0.5: - resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} + ip-address@10.1.0: + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} engines: {node: '>= 12'} ipaddr.js@1.9.1: @@ -5524,9 +5573,6 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true - jsbn@1.1.0: - resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} - jsdom@26.1.0: resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} engines: {node: '>=18'} @@ -5586,6 +5632,9 @@ packages: jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + jspdf@2.5.2: resolution: {integrity: sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==} @@ -5651,6 +5700,9 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + log-symbols@4.1.0: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} @@ -7353,8 +7405,8 @@ packages: resolution: {integrity: sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==} engines: {node: '>= 10'} - socks@2.8.6: - resolution: {integrity: sha512-pe4Y2yzru68lXCb38aAqRf5gvN8YdjP1lok5o0J7BOHljkyCGKVz7H3vpVIXKD27rj2giOJ7DwVyk/GWrPHDWA==} + socks@2.8.7: + resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} source-map-js@1.2.1: @@ -8303,6 +8355,13 @@ snapshots: '@ai-sdk/provider-utils': 1.0.9(zod@3.25.76) zod: 3.25.76 + '@ai-sdk/cerebras@0.2.16(zod@3.25.76)': + dependencies: + '@ai-sdk/openai-compatible': 0.2.16(zod@3.25.76) + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) + zod: 3.25.76 + '@ai-sdk/cohere@1.0.3(zod@3.25.76)': dependencies: '@ai-sdk/provider': 1.0.1 @@ -8316,6 +8375,13 @@ snapshots: '@ai-sdk/provider-utils': 2.1.2(zod@3.25.76) zod: 3.25.76 + '@ai-sdk/fireworks@0.2.16(zod@3.25.76)': + dependencies: + '@ai-sdk/openai-compatible': 0.2.16(zod@3.25.76) + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) + zod: 3.25.76 + '@ai-sdk/google@0.0.52(zod@3.25.76)': dependencies: '@ai-sdk/provider': 0.0.24 @@ -8335,6 +8401,12 @@ snapshots: '@ai-sdk/provider-utils': 2.1.2(zod@3.25.76) zod: 3.25.76 + '@ai-sdk/openai-compatible@0.2.16(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) + zod: 3.25.76 + '@ai-sdk/openai@1.1.2(zod@3.25.76)': dependencies: '@ai-sdk/provider': 1.0.6 @@ -8876,6 +8948,12 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + '@babel/compat-data@7.28.0': {} '@babel/core@7.28.0': @@ -8982,6 +9060,8 @@ snapshots: '@babel/helper-validator-identifier@7.27.1': {} + '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-option@7.27.1': {} '@babel/helpers@7.27.6': @@ -9050,6 +9130,8 @@ snapshots: '@babel/runtime@7.27.6': {} + '@babel/runtime@7.28.6': {} + '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 @@ -9319,7 +9401,7 @@ snapshots: '@electron/node-gyp@https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2': dependencies: env-paths: 2.2.1 - exponential-backoff: 3.1.2 + exponential-backoff: 3.1.3 glob: 8.1.0 graceful-fs: 4.2.11 make-fetch-happen: 10.2.1 @@ -9386,8 +9468,8 @@ snapshots: '@electron/windows-sign@1.2.2': dependencies: cross-dirname: 0.1.0 - debug: 4.4.1 - fs-extra: 11.3.0 + debug: 4.4.3 + fs-extra: 11.3.3 minimist: 1.2.8 postject: 1.0.0-alpha.6 transitivePeerDependencies: @@ -9407,10 +9489,10 @@ snapshots: '@esbuild/aix-ppc64@0.23.1': optional: true - '@esbuild/aix-ppc64@0.25.4': + '@esbuild/aix-ppc64@0.25.12': optional: true - '@esbuild/aix-ppc64@0.25.8': + '@esbuild/aix-ppc64@0.25.4': optional: true '@esbuild/android-arm64@0.17.6': @@ -9422,10 +9504,10 @@ snapshots: '@esbuild/android-arm64@0.23.1': optional: true - '@esbuild/android-arm64@0.25.4': + '@esbuild/android-arm64@0.25.12': optional: true - '@esbuild/android-arm64@0.25.8': + '@esbuild/android-arm64@0.25.4': optional: true '@esbuild/android-arm@0.17.6': @@ -9437,10 +9519,10 @@ snapshots: '@esbuild/android-arm@0.23.1': optional: true - '@esbuild/android-arm@0.25.4': + '@esbuild/android-arm@0.25.12': optional: true - '@esbuild/android-arm@0.25.8': + '@esbuild/android-arm@0.25.4': optional: true '@esbuild/android-x64@0.17.6': @@ -9452,10 +9534,10 @@ snapshots: '@esbuild/android-x64@0.23.1': optional: true - '@esbuild/android-x64@0.25.4': + '@esbuild/android-x64@0.25.12': optional: true - '@esbuild/android-x64@0.25.8': + '@esbuild/android-x64@0.25.4': optional: true '@esbuild/darwin-arm64@0.17.6': @@ -9467,10 +9549,10 @@ snapshots: '@esbuild/darwin-arm64@0.23.1': optional: true - '@esbuild/darwin-arm64@0.25.4': + '@esbuild/darwin-arm64@0.25.12': optional: true - '@esbuild/darwin-arm64@0.25.8': + '@esbuild/darwin-arm64@0.25.4': optional: true '@esbuild/darwin-x64@0.17.6': @@ -9482,10 +9564,10 @@ snapshots: '@esbuild/darwin-x64@0.23.1': optional: true - '@esbuild/darwin-x64@0.25.4': + '@esbuild/darwin-x64@0.25.12': optional: true - '@esbuild/darwin-x64@0.25.8': + '@esbuild/darwin-x64@0.25.4': optional: true '@esbuild/freebsd-arm64@0.17.6': @@ -9497,10 +9579,10 @@ snapshots: '@esbuild/freebsd-arm64@0.23.1': optional: true - '@esbuild/freebsd-arm64@0.25.4': + '@esbuild/freebsd-arm64@0.25.12': optional: true - '@esbuild/freebsd-arm64@0.25.8': + '@esbuild/freebsd-arm64@0.25.4': optional: true '@esbuild/freebsd-x64@0.17.6': @@ -9512,10 +9594,10 @@ snapshots: '@esbuild/freebsd-x64@0.23.1': optional: true - '@esbuild/freebsd-x64@0.25.4': + '@esbuild/freebsd-x64@0.25.12': optional: true - '@esbuild/freebsd-x64@0.25.8': + '@esbuild/freebsd-x64@0.25.4': optional: true '@esbuild/linux-arm64@0.17.6': @@ -9527,10 +9609,10 @@ snapshots: '@esbuild/linux-arm64@0.23.1': optional: true - '@esbuild/linux-arm64@0.25.4': + '@esbuild/linux-arm64@0.25.12': optional: true - '@esbuild/linux-arm64@0.25.8': + '@esbuild/linux-arm64@0.25.4': optional: true '@esbuild/linux-arm@0.17.6': @@ -9542,10 +9624,10 @@ snapshots: '@esbuild/linux-arm@0.23.1': optional: true - '@esbuild/linux-arm@0.25.4': + '@esbuild/linux-arm@0.25.12': optional: true - '@esbuild/linux-arm@0.25.8': + '@esbuild/linux-arm@0.25.4': optional: true '@esbuild/linux-ia32@0.17.6': @@ -9557,10 +9639,10 @@ snapshots: '@esbuild/linux-ia32@0.23.1': optional: true - '@esbuild/linux-ia32@0.25.4': + '@esbuild/linux-ia32@0.25.12': optional: true - '@esbuild/linux-ia32@0.25.8': + '@esbuild/linux-ia32@0.25.4': optional: true '@esbuild/linux-loong64@0.17.6': @@ -9572,10 +9654,10 @@ snapshots: '@esbuild/linux-loong64@0.23.1': optional: true - '@esbuild/linux-loong64@0.25.4': + '@esbuild/linux-loong64@0.25.12': optional: true - '@esbuild/linux-loong64@0.25.8': + '@esbuild/linux-loong64@0.25.4': optional: true '@esbuild/linux-mips64el@0.17.6': @@ -9587,10 +9669,10 @@ snapshots: '@esbuild/linux-mips64el@0.23.1': optional: true - '@esbuild/linux-mips64el@0.25.4': + '@esbuild/linux-mips64el@0.25.12': optional: true - '@esbuild/linux-mips64el@0.25.8': + '@esbuild/linux-mips64el@0.25.4': optional: true '@esbuild/linux-ppc64@0.17.6': @@ -9602,10 +9684,10 @@ snapshots: '@esbuild/linux-ppc64@0.23.1': optional: true - '@esbuild/linux-ppc64@0.25.4': + '@esbuild/linux-ppc64@0.25.12': optional: true - '@esbuild/linux-ppc64@0.25.8': + '@esbuild/linux-ppc64@0.25.4': optional: true '@esbuild/linux-riscv64@0.17.6': @@ -9617,10 +9699,10 @@ snapshots: '@esbuild/linux-riscv64@0.23.1': optional: true - '@esbuild/linux-riscv64@0.25.4': + '@esbuild/linux-riscv64@0.25.12': optional: true - '@esbuild/linux-riscv64@0.25.8': + '@esbuild/linux-riscv64@0.25.4': optional: true '@esbuild/linux-s390x@0.17.6': @@ -9632,10 +9714,10 @@ snapshots: '@esbuild/linux-s390x@0.23.1': optional: true - '@esbuild/linux-s390x@0.25.4': + '@esbuild/linux-s390x@0.25.12': optional: true - '@esbuild/linux-s390x@0.25.8': + '@esbuild/linux-s390x@0.25.4': optional: true '@esbuild/linux-x64@0.17.6': @@ -9647,16 +9729,16 @@ snapshots: '@esbuild/linux-x64@0.23.1': optional: true - '@esbuild/linux-x64@0.25.4': + '@esbuild/linux-x64@0.25.12': optional: true - '@esbuild/linux-x64@0.25.8': + '@esbuild/linux-x64@0.25.4': optional: true - '@esbuild/netbsd-arm64@0.25.4': + '@esbuild/netbsd-arm64@0.25.12': optional: true - '@esbuild/netbsd-arm64@0.25.8': + '@esbuild/netbsd-arm64@0.25.4': optional: true '@esbuild/netbsd-x64@0.17.6': @@ -9668,19 +9750,19 @@ snapshots: '@esbuild/netbsd-x64@0.23.1': optional: true - '@esbuild/netbsd-x64@0.25.4': + '@esbuild/netbsd-x64@0.25.12': optional: true - '@esbuild/netbsd-x64@0.25.8': + '@esbuild/netbsd-x64@0.25.4': optional: true '@esbuild/openbsd-arm64@0.23.1': optional: true - '@esbuild/openbsd-arm64@0.25.4': + '@esbuild/openbsd-arm64@0.25.12': optional: true - '@esbuild/openbsd-arm64@0.25.8': + '@esbuild/openbsd-arm64@0.25.4': optional: true '@esbuild/openbsd-x64@0.17.6': @@ -9692,13 +9774,13 @@ snapshots: '@esbuild/openbsd-x64@0.23.1': optional: true - '@esbuild/openbsd-x64@0.25.4': + '@esbuild/openbsd-x64@0.25.12': optional: true - '@esbuild/openbsd-x64@0.25.8': + '@esbuild/openbsd-x64@0.25.4': optional: true - '@esbuild/openharmony-arm64@0.25.8': + '@esbuild/openharmony-arm64@0.25.12': optional: true '@esbuild/sunos-x64@0.17.6': @@ -9710,10 +9792,10 @@ snapshots: '@esbuild/sunos-x64@0.23.1': optional: true - '@esbuild/sunos-x64@0.25.4': + '@esbuild/sunos-x64@0.25.12': optional: true - '@esbuild/sunos-x64@0.25.8': + '@esbuild/sunos-x64@0.25.4': optional: true '@esbuild/win32-arm64@0.17.6': @@ -9725,10 +9807,10 @@ snapshots: '@esbuild/win32-arm64@0.23.1': optional: true - '@esbuild/win32-arm64@0.25.4': + '@esbuild/win32-arm64@0.25.12': optional: true - '@esbuild/win32-arm64@0.25.8': + '@esbuild/win32-arm64@0.25.4': optional: true '@esbuild/win32-ia32@0.17.6': @@ -9740,10 +9822,10 @@ snapshots: '@esbuild/win32-ia32@0.23.1': optional: true - '@esbuild/win32-ia32@0.25.4': + '@esbuild/win32-ia32@0.25.12': optional: true - '@esbuild/win32-ia32@0.25.8': + '@esbuild/win32-ia32@0.25.4': optional: true '@esbuild/win32-x64@0.17.6': @@ -9755,10 +9837,10 @@ snapshots: '@esbuild/win32-x64@0.23.1': optional: true - '@esbuild/win32-x64@0.25.4': + '@esbuild/win32-x64@0.25.12': optional: true - '@esbuild/win32-x64@0.25.8': + '@esbuild/win32-x64@0.25.4': optional: true '@eslint-community/eslint-utils@4.7.0(eslint@9.31.0(jiti@1.21.7))': @@ -11393,8 +11475,8 @@ snapshots: '@testing-library/dom@10.4.0': dependencies: - '@babel/code-frame': 7.27.1 - '@babel/runtime': 7.27.6 + '@babel/code-frame': 7.29.0 + '@babel/runtime': 7.28.6 '@types/aria-query': 5.0.4 aria-query: 5.3.0 chalk: 4.1.2 @@ -12006,7 +12088,7 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -12802,6 +12884,10 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.4.3: + dependencies: + ms: 2.1.3 + decimal.js@10.6.0: {} decode-named-character-reference@1.2.0: @@ -13029,9 +13115,9 @@ snapshots: electron-winstaller@5.4.0: dependencies: '@electron/asar': 3.4.1 - debug: 4.4.1 + debug: 4.4.3 fs-extra: 7.0.1 - lodash: 4.17.21 + lodash: 4.17.23 temp: 0.9.4 optionalDependencies: '@electron/windows-sign': 1.2.2 @@ -13190,6 +13276,35 @@ snapshots: '@esbuild/win32-ia32': 0.23.1 '@esbuild/win32-x64': 0.23.1 + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + esbuild@0.25.4: optionalDependencies: '@esbuild/aix-ppc64': 0.25.4 @@ -13218,35 +13333,6 @@ snapshots: '@esbuild/win32-ia32': 0.25.4 '@esbuild/win32-x64': 0.25.4 - esbuild@0.25.8: - optionalDependencies: - '@esbuild/aix-ppc64': 0.25.8 - '@esbuild/android-arm': 0.25.8 - '@esbuild/android-arm64': 0.25.8 - '@esbuild/android-x64': 0.25.8 - '@esbuild/darwin-arm64': 0.25.8 - '@esbuild/darwin-x64': 0.25.8 - '@esbuild/freebsd-arm64': 0.25.8 - '@esbuild/freebsd-x64': 0.25.8 - '@esbuild/linux-arm': 0.25.8 - '@esbuild/linux-arm64': 0.25.8 - '@esbuild/linux-ia32': 0.25.8 - '@esbuild/linux-loong64': 0.25.8 - '@esbuild/linux-mips64el': 0.25.8 - '@esbuild/linux-ppc64': 0.25.8 - '@esbuild/linux-riscv64': 0.25.8 - '@esbuild/linux-s390x': 0.25.8 - '@esbuild/linux-x64': 0.25.8 - '@esbuild/netbsd-arm64': 0.25.8 - '@esbuild/netbsd-x64': 0.25.8 - '@esbuild/openbsd-arm64': 0.25.8 - '@esbuild/openbsd-x64': 0.25.8 - '@esbuild/openharmony-arm64': 0.25.8 - '@esbuild/sunos-x64': 0.25.8 - '@esbuild/win32-arm64': 0.25.8 - '@esbuild/win32-ia32': 0.25.8 - '@esbuild/win32-x64': 0.25.8 - escalade@3.2.0: {} escape-html@1.0.3: {} @@ -13460,7 +13546,7 @@ snapshots: expect-type@1.2.2: {} - exponential-backoff@3.1.2: {} + exponential-backoff@3.1.3: {} express-rate-limit@7.5.1(express@5.1.0): dependencies: @@ -13695,6 +13781,13 @@ snapshots: jsonfile: 6.1.0 universalify: 2.0.1 + fs-extra@11.3.3: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + optional: true + fs-extra@7.0.1: dependencies: graceful-fs: 4.2.11 @@ -14058,7 +14151,7 @@ snapshots: dependencies: '@tootallnate/once': 2.0.0 agent-base: 6.0.2 - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -14079,7 +14172,7 @@ snapshots: https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -14162,10 +14255,7 @@ snapshots: inline-style-parser@0.2.4: {} - ip-address@9.0.5: - dependencies: - jsbn: 1.1.0 - sprintf-js: 1.1.3 + ip-address@10.1.0: {} ipaddr.js@1.9.1: {} @@ -14324,8 +14414,6 @@ snapshots: dependencies: argparse: 2.0.1 - jsbn@1.1.0: {} - jsdom@26.1.0: dependencies: cssstyle: 4.6.0 @@ -14397,6 +14485,13 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + optional: true + jspdf@2.5.2: dependencies: '@babel/runtime': 7.27.6 @@ -14463,6 +14558,8 @@ snapshots: lodash@4.17.21: {} + lodash@4.17.23: {} + log-symbols@4.1.0: dependencies: chalk: 4.1.2 @@ -16680,14 +16777,14 @@ snapshots: socks-proxy-agent@7.0.0: dependencies: agent-base: 6.0.2 - debug: 4.4.1 - socks: 2.8.6 + debug: 4.4.3 + socks: 2.8.7 transitivePeerDependencies: - supports-color - socks@2.8.6: + socks@2.8.7: dependencies: - ip-address: 9.0.5 + ip-address: 10.1.0 smart-buffer: 4.2.0 source-map-js@1.2.1: {} @@ -16721,7 +16818,8 @@ snapshots: spdx-license-ids@3.0.21: {} - sprintf-js@1.1.3: {} + sprintf-js@1.1.3: + optional: true ssri@10.0.6: dependencies: @@ -17006,7 +17104,7 @@ snapshots: tsx@4.20.3: dependencies: - esbuild: 0.25.8 + esbuild: 0.25.12 get-tsconfig: 4.10.1 optionalDependencies: fsevents: 2.3.3 diff --git a/vite.config.ts.timestamp-1770328346417-a90f095482a09.mjs b/vite.config.ts.timestamp-1770328346417-a90f095482a09.mjs new file mode 100644 index 0000000000..b22de5a82e --- /dev/null +++ b/vite.config.ts.timestamp-1770328346417-a90f095482a09.mjs @@ -0,0 +1,110 @@ +// vite.config.ts +import { cloudflareDevProxyVitePlugin as remixCloudflareDevProxy, vitePlugin as remixVitePlugin } from "file:///Users/stijnus/Documents/GitHub/bolt.diy/node_modules/.pnpm/@remix-run+dev@2.16.8_@remix-run+react@2.16.8_react-dom@18.3.1_react@18.3.1__react@18.3.1_typ_uozynh2jdinnz6binv6akofmd4/node_modules/@remix-run/dev/dist/index.js"; +import UnoCSS from "file:///Users/stijnus/Documents/GitHub/bolt.diy/node_modules/.pnpm/unocss@0.61.9_postcss@8.5.6_rollup@4.45.1_vite@5.4.19_@types+node@24.1.0_sass-embedded@1.89.2_/node_modules/unocss/dist/vite.mjs"; +import { defineConfig } from "file:///Users/stijnus/Documents/GitHub/bolt.diy/node_modules/.pnpm/vite@5.4.19_@types+node@24.1.0_sass-embedded@1.89.2/node_modules/vite/dist/node/index.js"; +import { nodePolyfills } from "file:///Users/stijnus/Documents/GitHub/bolt.diy/node_modules/.pnpm/vite-plugin-node-polyfills@0.22.0_rollup@4.45.1_vite@5.4.19_@types+node@24.1.0_sass-embedded@1.89.2_/node_modules/vite-plugin-node-polyfills/dist/index.js"; +import { optimizeCssModules } from "file:///Users/stijnus/Documents/GitHub/bolt.diy/node_modules/.pnpm/vite-plugin-optimize-css-modules@1.2.0_vite@5.4.19_@types+node@24.1.0_sass-embedded@1.89.2_/node_modules/vite-plugin-optimize-css-modules/dist/index.mjs"; +import tsconfigPaths from "file:///Users/stijnus/Documents/GitHub/bolt.diy/node_modules/.pnpm/vite-tsconfig-paths@4.3.2_typescript@5.8.3_vite@5.4.19_@types+node@24.1.0_sass-embedded@1.89.2_/node_modules/vite-tsconfig-paths/dist/index.mjs"; +import * as dotenv from "file:///Users/stijnus/Documents/GitHub/bolt.diy/node_modules/.pnpm/dotenv@16.6.1/node_modules/dotenv/lib/main.js"; +dotenv.config({ path: ".env.local" }); +dotenv.config({ path: ".env" }); +dotenv.config(); +var vite_config_default = defineConfig((config2) => { + return { + define: { + "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV) + }, + build: { + target: "esnext" + }, + plugins: [ + nodePolyfills({ + include: ["buffer", "process", "util", "stream"], + globals: { + Buffer: true, + process: true, + global: true + }, + protocolImports: true, + exclude: ["child_process", "fs", "path"] + }), + { + name: "buffer-polyfill", + transform(code, id) { + if (id.includes("env.mjs")) { + return { + code: `import { Buffer } from 'buffer'; +${code}`, + map: null + }; + } + return null; + } + }, + config2.mode !== "test" && remixCloudflareDevProxy(), + remixVitePlugin({ + future: { + v3_fetcherPersist: true, + v3_relativeSplatPath: true, + v3_throwAbortReason: true, + v3_lazyRouteDiscovery: true + } + }), + UnoCSS(), + tsconfigPaths(), + chrome129IssuePlugin(), + config2.mode === "production" && optimizeCssModules({ apply: "build" }) + ], + envPrefix: [ + "VITE_", + "OPENAI_LIKE_API_BASE_URL", + "OPENAI_LIKE_API_MODELS", + "OLLAMA_API_BASE_URL", + "LMSTUDIO_API_BASE_URL", + "TOGETHER_API_BASE_URL" + ], + css: { + preprocessorOptions: { + scss: { + api: "modern-compiler" + } + } + }, + test: { + exclude: [ + "**/node_modules/**", + "**/dist/**", + "**/cypress/**", + "**/.{idea,git,cache,output,temp}/**", + "**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*", + "**/tests/preview/**" + // Exclude preview tests that require Playwright + ] + } + }; +}); +function chrome129IssuePlugin() { + return { + name: "chrome129IssuePlugin", + configureServer(server) { + server.middlewares.use((req, res, next) => { + const raw = req.headers["user-agent"]?.match(/Chrom(e|ium)\/([0-9]+)\./); + if (raw) { + const version = parseInt(raw[2], 10); + if (version === 129) { + res.setHeader("content-type", "text/html"); + res.end( + '

Please use Chrome Canary for testing.

Chrome 129 has an issue with JavaScript modules & Vite local development, see for more information.

Note: This only impacts local development. `pnpm run build` and `pnpm run start` will work fine in this browser.

' + ); + return; + } + } + next(); + }); + } + }; +} +export { + vite_config_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvc3Rpam51cy9Eb2N1bWVudHMvR2l0SHViL2JvbHQuZGl5XCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvc3Rpam51cy9Eb2N1bWVudHMvR2l0SHViL2JvbHQuZGl5L3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9zdGlqbnVzL0RvY3VtZW50cy9HaXRIdWIvYm9sdC5kaXkvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBjbG91ZGZsYXJlRGV2UHJveHlWaXRlUGx1Z2luIGFzIHJlbWl4Q2xvdWRmbGFyZURldlByb3h5LCB2aXRlUGx1Z2luIGFzIHJlbWl4Vml0ZVBsdWdpbiB9IGZyb20gJ0ByZW1peC1ydW4vZGV2JztcbmltcG9ydCBVbm9DU1MgZnJvbSAndW5vY3NzL3ZpdGUnO1xuaW1wb3J0IHsgZGVmaW5lQ29uZmlnLCB0eXBlIFZpdGVEZXZTZXJ2ZXIgfSBmcm9tICd2aXRlJztcbmltcG9ydCB7IG5vZGVQb2x5ZmlsbHMgfSBmcm9tICd2aXRlLXBsdWdpbi1ub2RlLXBvbHlmaWxscyc7XG5pbXBvcnQgeyBvcHRpbWl6ZUNzc01vZHVsZXMgfSBmcm9tICd2aXRlLXBsdWdpbi1vcHRpbWl6ZS1jc3MtbW9kdWxlcyc7XG5pbXBvcnQgdHNjb25maWdQYXRocyBmcm9tICd2aXRlLXRzY29uZmlnLXBhdGhzJztcbmltcG9ydCAqIGFzIGRvdGVudiBmcm9tICdkb3RlbnYnO1xuXG4vLyBMb2FkIGVudmlyb25tZW50IHZhcmlhYmxlcyBmcm9tIG11bHRpcGxlIGZpbGVzXG5kb3RlbnYuY29uZmlnKHsgcGF0aDogJy5lbnYubG9jYWwnIH0pO1xuZG90ZW52LmNvbmZpZyh7IHBhdGg6ICcuZW52JyB9KTtcbmRvdGVudi5jb25maWcoKTtcblxuZXhwb3J0IGRlZmF1bHQgZGVmaW5lQ29uZmlnKChjb25maWcpID0+IHtcbiAgcmV0dXJuIHtcbiAgICBkZWZpbmU6IHtcbiAgICAgICdwcm9jZXNzLmVudi5OT0RFX0VOVic6IEpTT04uc3RyaW5naWZ5KHByb2Nlc3MuZW52Lk5PREVfRU5WKSxcbiAgICB9LFxuICAgIGJ1aWxkOiB7XG4gICAgICB0YXJnZXQ6ICdlc25leHQnLFxuICAgIH0sXG4gICAgcGx1Z2luczogW1xuICAgICAgbm9kZVBvbHlmaWxscyh7XG4gICAgICAgIGluY2x1ZGU6IFsnYnVmZmVyJywgJ3Byb2Nlc3MnLCAndXRpbCcsICdzdHJlYW0nXSxcbiAgICAgICAgZ2xvYmFsczoge1xuICAgICAgICAgIEJ1ZmZlcjogdHJ1ZSxcbiAgICAgICAgICBwcm9jZXNzOiB0cnVlLFxuICAgICAgICAgIGdsb2JhbDogdHJ1ZSxcbiAgICAgICAgfSxcbiAgICAgICAgcHJvdG9jb2xJbXBvcnRzOiB0cnVlLFxuICAgICAgICBleGNsdWRlOiBbJ2NoaWxkX3Byb2Nlc3MnLCAnZnMnLCAncGF0aCddLFxuICAgICAgfSksXG4gICAgICB7XG4gICAgICAgIG5hbWU6ICdidWZmZXItcG9seWZpbGwnLFxuICAgICAgICB0cmFuc2Zvcm0oY29kZSwgaWQpIHtcbiAgICAgICAgICBpZiAoaWQuaW5jbHVkZXMoJ2Vudi5tanMnKSkge1xuICAgICAgICAgICAgcmV0dXJuIHtcbiAgICAgICAgICAgICAgY29kZTogYGltcG9ydCB7IEJ1ZmZlciB9IGZyb20gJ2J1ZmZlcic7XFxuJHtjb2RlfWAsXG4gICAgICAgICAgICAgIG1hcDogbnVsbCxcbiAgICAgICAgICAgIH07XG4gICAgICAgICAgfVxuXG4gICAgICAgICAgcmV0dXJuIG51bGw7XG4gICAgICAgIH0sXG4gICAgICB9LFxuICAgICAgY29uZmlnLm1vZGUgIT09ICd0ZXN0JyAmJiByZW1peENsb3VkZmxhcmVEZXZQcm94eSgpLFxuICAgICAgcmVtaXhWaXRlUGx1Z2luKHtcbiAgICAgICAgZnV0dXJlOiB7XG4gICAgICAgICAgdjNfZmV0Y2hlclBlcnNpc3Q6IHRydWUsXG4gICAgICAgICAgdjNfcmVsYXRpdmVTcGxhdFBhdGg6IHRydWUsXG4gICAgICAgICAgdjNfdGhyb3dBYm9ydFJlYXNvbjogdHJ1ZSxcbiAgICAgICAgICB2M19sYXp5Um91dGVEaXNjb3Zlcnk6IHRydWUsXG4gICAgICAgIH0sXG4gICAgICB9KSxcbiAgICAgIFVub0NTUygpLFxuICAgICAgdHNjb25maWdQYXRocygpLFxuICAgICAgY2hyb21lMTI5SXNzdWVQbHVnaW4oKSxcbiAgICAgIGNvbmZpZy5tb2RlID09PSAncHJvZHVjdGlvbicgJiYgb3B0aW1pemVDc3NNb2R1bGVzKHsgYXBwbHk6ICdidWlsZCcgfSksXG4gICAgXSxcbiAgICBlbnZQcmVmaXg6IFtcbiAgICAgICdWSVRFXycsXG4gICAgICAnT1BFTkFJX0xJS0VfQVBJX0JBU0VfVVJMJyxcbiAgICAgICdPUEVOQUlfTElLRV9BUElfTU9ERUxTJyxcbiAgICAgICdPTExBTUFfQVBJX0JBU0VfVVJMJyxcbiAgICAgICdMTVNUVURJT19BUElfQkFTRV9VUkwnLFxuICAgICAgJ1RPR0VUSEVSX0FQSV9CQVNFX1VSTCcsXG4gICAgXSxcbiAgICBjc3M6IHtcbiAgICAgIHByZXByb2Nlc3Nvck9wdGlvbnM6IHtcbiAgICAgICAgc2Nzczoge1xuICAgICAgICAgIGFwaTogJ21vZGVybi1jb21waWxlcicsXG4gICAgICAgIH0sXG4gICAgICB9LFxuICAgIH0sXG4gICAgdGVzdDoge1xuICAgICAgZXhjbHVkZTogW1xuICAgICAgICAnKiovbm9kZV9tb2R1bGVzLyoqJyxcbiAgICAgICAgJyoqL2Rpc3QvKionLFxuICAgICAgICAnKiovY3lwcmVzcy8qKicsXG4gICAgICAgICcqKi8ue2lkZWEsZ2l0LGNhY2hlLG91dHB1dCx0ZW1wfS8qKicsXG4gICAgICAgICcqKi97a2FybWEscm9sbHVwLHdlYnBhY2ssdml0ZSx2aXRlc3QsamVzdCxhdmEsYmFiZWwsbnljLGN5cHJlc3MsdHN1cCxidWlsZH0uY29uZmlnLionLFxuICAgICAgICAnKiovdGVzdHMvcHJldmlldy8qKicsIC8vIEV4Y2x1ZGUgcHJldmlldyB0ZXN0cyB0aGF0IHJlcXVpcmUgUGxheXdyaWdodFxuICAgICAgXSxcbiAgICB9LFxuICB9O1xufSk7XG5cbmZ1bmN0aW9uIGNocm9tZTEyOUlzc3VlUGx1Z2luKCkge1xuICByZXR1cm4ge1xuICAgIG5hbWU6ICdjaHJvbWUxMjlJc3N1ZVBsdWdpbicsXG4gICAgY29uZmlndXJlU2VydmVyKHNlcnZlcjogVml0ZURldlNlcnZlcikge1xuICAgICAgc2VydmVyLm1pZGRsZXdhcmVzLnVzZSgocmVxLCByZXMsIG5leHQpID0+IHtcbiAgICAgICAgY29uc3QgcmF3ID0gcmVxLmhlYWRlcnNbJ3VzZXItYWdlbnQnXT8ubWF0Y2goL0Nocm9tKGV8aXVtKVxcLyhbMC05XSspXFwuLyk7XG5cbiAgICAgICAgaWYgKHJhdykge1xuICAgICAgICAgIGNvbnN0IHZlcnNpb24gPSBwYXJzZUludChyYXdbMl0sIDEwKTtcblxuICAgICAgICAgIGlmICh2ZXJzaW9uID09PSAxMjkpIHtcbiAgICAgICAgICAgIHJlcy5zZXRIZWFkZXIoJ2NvbnRlbnQtdHlwZScsICd0ZXh0L2h0bWwnKTtcbiAgICAgICAgICAgIHJlcy5lbmQoXG4gICAgICAgICAgICAgICc8Ym9keT48aDE+UGxlYXNlIHVzZSBDaHJvbWUgQ2FuYXJ5IGZvciB0ZXN0aW5nLjwvaDE+PHA+Q2hyb21lIDEyOSBoYXMgYW4gaXNzdWUgd2l0aCBKYXZhU2NyaXB0IG1vZHVsZXMgJiBWaXRlIGxvY2FsIGRldmVsb3BtZW50LCBzZWUgPGEgaHJlZj1cImh0dHBzOi8vZ2l0aHViLmNvbS9zdGFja2JsaXR6L2JvbHQubmV3L2lzc3Vlcy84NiNpc3N1ZWNvbW1lbnQtMjM5NTUxOTI1OFwiPmZvciBtb3JlIGluZm9ybWF0aW9uLjwvYT48L3A+PHA+PGI+Tm90ZTo8L2I+IFRoaXMgb25seSBpbXBhY3RzIDx1PmxvY2FsIGRldmVsb3BtZW50PC91Pi4gYHBucG0gcnVuIGJ1aWxkYCBhbmQgYHBucG0gcnVuIHN0YXJ0YCB3aWxsIHdvcmsgZmluZSBpbiB0aGlzIGJyb3dzZXIuPC9wPjwvYm9keT4nLFxuICAgICAgICAgICAgKTtcblxuICAgICAgICAgICAgcmV0dXJuO1xuICAgICAgICAgIH1cbiAgICAgICAgfVxuXG4gICAgICAgIG5leHQoKTtcbiAgICAgIH0pO1xuICAgIH0sXG4gIH07XG59Il0sCiAgIm1hcHBpbmdzIjogIjtBQUEwUyxTQUFTLGdDQUFnQyx5QkFBeUIsY0FBYyx1QkFBdUI7QUFDalosT0FBTyxZQUFZO0FBQ25CLFNBQVMsb0JBQXdDO0FBQ2pELFNBQVMscUJBQXFCO0FBQzlCLFNBQVMsMEJBQTBCO0FBQ25DLE9BQU8sbUJBQW1CO0FBQzFCLFlBQVksWUFBWTtBQUdqQixjQUFPLEVBQUUsTUFBTSxhQUFhLENBQUM7QUFDN0IsY0FBTyxFQUFFLE1BQU0sT0FBTyxDQUFDO0FBQ3ZCLGNBQU87QUFFZCxJQUFPLHNCQUFRLGFBQWEsQ0FBQ0EsWUFBVztBQUN0QyxTQUFPO0FBQUEsSUFDTCxRQUFRO0FBQUEsTUFDTix3QkFBd0IsS0FBSyxVQUFVLFFBQVEsSUFBSSxRQUFRO0FBQUEsSUFDN0Q7QUFBQSxJQUNBLE9BQU87QUFBQSxNQUNMLFFBQVE7QUFBQSxJQUNWO0FBQUEsSUFDQSxTQUFTO0FBQUEsTUFDUCxjQUFjO0FBQUEsUUFDWixTQUFTLENBQUMsVUFBVSxXQUFXLFFBQVEsUUFBUTtBQUFBLFFBQy9DLFNBQVM7QUFBQSxVQUNQLFFBQVE7QUFBQSxVQUNSLFNBQVM7QUFBQSxVQUNULFFBQVE7QUFBQSxRQUNWO0FBQUEsUUFDQSxpQkFBaUI7QUFBQSxRQUNqQixTQUFTLENBQUMsaUJBQWlCLE1BQU0sTUFBTTtBQUFBLE1BQ3pDLENBQUM7QUFBQSxNQUNEO0FBQUEsUUFDRSxNQUFNO0FBQUEsUUFDTixVQUFVLE1BQU0sSUFBSTtBQUNsQixjQUFJLEdBQUcsU0FBUyxTQUFTLEdBQUc7QUFDMUIsbUJBQU87QUFBQSxjQUNMLE1BQU07QUFBQSxFQUFxQyxJQUFJO0FBQUEsY0FDL0MsS0FBSztBQUFBLFlBQ1A7QUFBQSxVQUNGO0FBRUEsaUJBQU87QUFBQSxRQUNUO0FBQUEsTUFDRjtBQUFBLE1BQ0FBLFFBQU8sU0FBUyxVQUFVLHdCQUF3QjtBQUFBLE1BQ2xELGdCQUFnQjtBQUFBLFFBQ2QsUUFBUTtBQUFBLFVBQ04sbUJBQW1CO0FBQUEsVUFDbkIsc0JBQXNCO0FBQUEsVUFDdEIscUJBQXFCO0FBQUEsVUFDckIsdUJBQXVCO0FBQUEsUUFDekI7QUFBQSxNQUNGLENBQUM7QUFBQSxNQUNELE9BQU87QUFBQSxNQUNQLGNBQWM7QUFBQSxNQUNkLHFCQUFxQjtBQUFBLE1BQ3JCQSxRQUFPLFNBQVMsZ0JBQWdCLG1CQUFtQixFQUFFLE9BQU8sUUFBUSxDQUFDO0FBQUEsSUFDdkU7QUFBQSxJQUNBLFdBQVc7QUFBQSxNQUNUO0FBQUEsTUFDQTtBQUFBLE1BQ0E7QUFBQSxNQUNBO0FBQUEsTUFDQTtBQUFBLE1BQ0E7QUFBQSxJQUNGO0FBQUEsSUFDQSxLQUFLO0FBQUEsTUFDSCxxQkFBcUI7QUFBQSxRQUNuQixNQUFNO0FBQUEsVUFDSixLQUFLO0FBQUEsUUFDUDtBQUFBLE1BQ0Y7QUFBQSxJQUNGO0FBQUEsSUFDQSxNQUFNO0FBQUEsTUFDSixTQUFTO0FBQUEsUUFDUDtBQUFBLFFBQ0E7QUFBQSxRQUNBO0FBQUEsUUFDQTtBQUFBLFFBQ0E7QUFBQSxRQUNBO0FBQUE7QUFBQSxNQUNGO0FBQUEsSUFDRjtBQUFBLEVBQ0Y7QUFDRixDQUFDO0FBRUQsU0FBUyx1QkFBdUI7QUFDOUIsU0FBTztBQUFBLElBQ0wsTUFBTTtBQUFBLElBQ04sZ0JBQWdCLFFBQXVCO0FBQ3JDLGFBQU8sWUFBWSxJQUFJLENBQUMsS0FBSyxLQUFLLFNBQVM7QUFDekMsY0FBTSxNQUFNLElBQUksUUFBUSxZQUFZLEdBQUcsTUFBTSwwQkFBMEI7QUFFdkUsWUFBSSxLQUFLO0FBQ1AsZ0JBQU0sVUFBVSxTQUFTLElBQUksQ0FBQyxHQUFHLEVBQUU7QUFFbkMsY0FBSSxZQUFZLEtBQUs7QUFDbkIsZ0JBQUksVUFBVSxnQkFBZ0IsV0FBVztBQUN6QyxnQkFBSTtBQUFBLGNBQ0Y7QUFBQSxZQUNGO0FBRUE7QUFBQSxVQUNGO0FBQUEsUUFDRjtBQUVBLGFBQUs7QUFBQSxNQUNQLENBQUM7QUFBQSxJQUNIO0FBQUEsRUFDRjtBQUNGOyIsCiAgIm5hbWVzIjogWyJjb25maWciXQp9Cg== From 2e254ac19a696394030601bc602f54945b12bfc4 Mon Sep 17 00:00:00 2001 From: Stijnus <72551117+Stijnus@users.noreply.github.com> Date: Thu, 5 Feb 2026 22:55:17 +0100 Subject: [PATCH 4/4] feat: add web URL content fetcher for chat context Add ability to fetch and inject web page content into chat as context. Includes SSRF protection (blocks private IPs, localhost), content extraction (strips scripts/styles/nav), and a clean popover UI. Reimplements the concept from PR #1703 without the issues (duplicated ChatBox, dual API routes, SSRF vulnerability, window.prompt UX). Co-Authored-By: Claude Opus 4.6 --- app/components/chat/BaseChat.tsx | 3 + app/components/chat/Chat.client.tsx | 15 +++ app/components/chat/ChatBox.tsx | 3 + app/components/chat/WebSearch.client.tsx | 163 +++++++++++++++++++++++ app/routes/api.web-search.ts | 104 +++++++++++++++ app/utils/url.ts | 42 ++++++ 6 files changed, 330 insertions(+) create mode 100644 app/components/chat/WebSearch.client.tsx create mode 100644 app/routes/api.web-search.ts create mode 100644 app/utils/url.ts diff --git a/app/components/chat/BaseChat.tsx b/app/components/chat/BaseChat.tsx index 7daffb6dd9..934a3d5545 100644 --- a/app/components/chat/BaseChat.tsx +++ b/app/components/chat/BaseChat.tsx @@ -81,6 +81,7 @@ interface BaseChatProps { selectedElement?: ElementInfo | null; setSelectedElement?: (element: ElementInfo | null) => void; addToolResult?: ({ toolCallId, result }: { toolCallId: string; result: any }) => void; + onWebSearchResult?: (result: string) => void; } export const BaseChat = React.forwardRef( @@ -130,6 +131,7 @@ export const BaseChat = React.forwardRef( addToolResult = () => { throw new Error('addToolResult not implemented'); }, + onWebSearchResult, }, ref, ) => { @@ -465,6 +467,7 @@ export const BaseChat = React.forwardRef( setDesignScheme={setDesignScheme} selectedElement={selectedElement} setSelectedElement={setSelectedElement} + onWebSearchResult={onWebSearchResult} />
diff --git a/app/components/chat/Chat.client.tsx b/app/components/chat/Chat.client.tsx index c4706e1764..ccddaf51d6 100644 --- a/app/components/chat/Chat.client.tsx +++ b/app/components/chat/Chat.client.tsx @@ -594,6 +594,20 @@ export const ChatImpl = memo( Cookies.set('selectedProvider', newProvider.name, { expires: 30 }); }; + const handleWebSearchResult = useCallback( + (result: string) => { + const currentInput = input || ''; + const newInput = currentInput.length > 0 ? `${result}\n\n${currentInput}` : result; + + // Update the input via the same mechanism as handleInputChange + const syntheticEvent = { + target: { value: newInput }, + } as React.ChangeEvent; + handleInputChange(syntheticEvent); + }, + [input, handleInputChange], + ); + return ( ); }, diff --git a/app/components/chat/ChatBox.tsx b/app/components/chat/ChatBox.tsx index 4cd9a149a2..209a24c949 100644 --- a/app/components/chat/ChatBox.tsx +++ b/app/components/chat/ChatBox.tsx @@ -19,6 +19,7 @@ import { ColorSchemeDialog } from '~/components/ui/ColorSchemeDialog'; import type { DesignScheme } from '~/types/design-scheme'; import type { ElementInfo } from '~/components/workbench/Inspector'; import { McpTools } from './MCPTools'; +import { WebSearch } from './WebSearch.client'; interface ChatBoxProps { isModelSettingsCollapsed: boolean; @@ -55,6 +56,7 @@ interface ChatBoxProps { handleStop?: (() => void) | undefined; enhancingPrompt?: boolean | undefined; enhancePrompt?: (() => void) | undefined; + onWebSearchResult?: (result: string) => void; chatMode?: 'discuss' | 'build'; setChatMode?: (mode: 'discuss' | 'build') => void; designScheme?: DesignScheme; @@ -265,6 +267,7 @@ export const ChatBox: React.FC = (props) => { props.handleFileUpload()}>
+ props.onWebSearchResult?.(result)} disabled={props.isStreaming} /> void; + disabled?: boolean; +} + +interface WebSearchData { + title: string; + description: string; + content: string; + sourceUrl: string; +} + +interface WebSearchResponse { + success: boolean; + data?: WebSearchData; + error?: string; +} + +function formatSearchResult(data: WebSearchData): string { + const parts: string[] = [`[Web content from ${data.sourceUrl}]`]; + + if (data.title) { + parts.push(`Title: ${data.title}`); + } + + if (data.description) { + parts.push(`Description: ${data.description}`); + } + + parts.push('', data.content); + + return parts.join('\n'); +} + +export function WebSearch({ onSearchResult, disabled = false }: WebSearchProps) { + const [isOpen, setIsOpen] = useState(false); + const [isSearching, setIsSearching] = useState(false); + const [url, setUrl] = useState(''); + const inputRef = useRef(null); + const containerRef = useRef(null); + + useEffect(() => { + if (isOpen) { + inputRef.current?.focus(); + } + }, [isOpen]); + + useEffect(() => { + if (!isOpen) { + return undefined; + } + + const handleClickOutside = (event: MouseEvent) => { + if (containerRef.current && !containerRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + + return () => document.removeEventListener('mousedown', handleClickOutside); + }, [isOpen]); + + const handleFetch = async () => { + const trimmedUrl = url.trim(); + + if (!trimmedUrl) { + return; + } + + setIsSearching(true); + + try { + const response = await fetch('/api/web-search', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url: trimmedUrl }), + }); + + const result = (await response.json()) as WebSearchResponse; + + if (!response.ok || !result.success || !result.data) { + throw new Error(result.error || 'Failed to fetch URL content'); + } + + onSearchResult(formatSearchResult(result.data)); + toast.success('URL content fetched'); + setUrl(''); + setIsOpen(false); + } catch (error) { + toast.error(error instanceof Error ? error.message : 'Failed to fetch URL'); + } finally { + setIsSearching(false); + } + }; + + return ( +
+ setIsOpen(!isOpen)} + className="transition-all" + > + {isSearching ? ( +
+ ) : ( +
+ )} + + {isOpen && ( +
+ setUrl(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter' && !isSearching) { + handleFetch(); + } + + if (e.key === 'Escape') { + setIsOpen(false); + } + }} + placeholder="https://example.com" + disabled={isSearching} + className={classNames( + 'w-[300px] px-3 py-1.5 text-sm rounded-md', + 'border border-bolt-elements-borderColor', + 'bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary', + 'placeholder-bolt-elements-textTertiary', + 'focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus', + )} + /> + +
+ )} +
+ ); +} diff --git a/app/routes/api.web-search.ts b/app/routes/api.web-search.ts new file mode 100644 index 0000000000..96354f7594 --- /dev/null +++ b/app/routes/api.web-search.ts @@ -0,0 +1,104 @@ +import { json } from '@remix-run/cloudflare'; +import type { ActionFunctionArgs } from '@remix-run/cloudflare'; +import { isAllowedUrl } from '~/utils/url'; + +const MAX_CONTENT_LENGTH = 8000; + +const FETCH_HEADERS = { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'Accept-Language': 'en-US,en;q=0.5', +}; + +function extractTitle(html: string): string { + const match = html.match(/]*>([^<]+)<\/title>/i); + return match ? match[1].trim() : ''; +} + +function extractMetaDescription(html: string): string { + const match = html.match(/]*name=["']description["'][^>]*content=["']([^"']*)["'][^>]*>/i); + + if (match) { + return match[1].trim(); + } + + // Try reverse attribute order + const altMatch = html.match(/]*content=["']([^"']*)["'][^>]*name=["']description["'][^>]*>/i); + + return altMatch ? altMatch[1].trim() : ''; +} + +function extractTextContent(html: string): string { + return html + .replace(/)<[^<]*)*<\/script>/gi, ' ') + .replace(/)<[^<]*)*<\/style>/gi, ' ') + .replace(/)<[^<]*)*<\/nav>/gi, ' ') + .replace(/)<[^<]*)*<\/header>/gi, ' ') + .replace(/)<[^<]*)*<\/footer>/gi, ' ') + .replace(/<[^>]+>/g, ' ') + .replace(/ /g, ' ') + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/\s+/g, ' ') + .trim(); +} + +export async function action({ request }: ActionFunctionArgs) { + if (request.method !== 'POST') { + return json({ error: 'Method not allowed' }, { status: 405 }); + } + + try { + const { url } = (await request.json()) as { url?: string }; + + if (!url || typeof url !== 'string') { + return json({ error: 'URL is required' }, { status: 400 }); + } + + if (!isAllowedUrl(url)) { + return json({ error: 'URL is not allowed. Only public HTTP/HTTPS URLs are accepted.' }, { status: 400 }); + } + + const response = await fetch(url, { + headers: FETCH_HEADERS, + signal: AbortSignal.timeout(10_000), + }); + + if (!response.ok) { + return json({ error: `Failed to fetch URL: ${response.status} ${response.statusText}` }, { status: 502 }); + } + + const contentType = response.headers.get('content-type') || ''; + + if (!contentType.includes('text/html') && !contentType.includes('text/plain')) { + return json({ error: 'URL must point to an HTML or text page' }, { status: 400 }); + } + + const html = await response.text(); + const title = extractTitle(html); + const description = extractMetaDescription(html); + const content = extractTextContent(html); + + return json({ + success: true, + data: { + title, + description, + content: content.length > MAX_CONTENT_LENGTH ? content.slice(0, MAX_CONTENT_LENGTH) + '...' : content, + sourceUrl: url, + }, + }); + } catch (error) { + if (error instanceof DOMException && error.name === 'TimeoutError') { + return json({ error: 'Request timed out after 10 seconds' }, { status: 504 }); + } + + console.error('Web search error:', error); + + return json({ error: error instanceof Error ? error.message : 'Failed to fetch URL' }, { status: 500 }); + } +} diff --git a/app/utils/url.ts b/app/utils/url.ts new file mode 100644 index 0000000000..ad6cbd0564 --- /dev/null +++ b/app/utils/url.ts @@ -0,0 +1,42 @@ +/** + * URL validation utilities with SSRF protection. + */ + +const PRIVATE_IP_PATTERNS = [ + /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, // Loopback + /^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, // Class A private + /^172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}$/, // Class B private + /^192\.168\.\d{1,3}\.\d{1,3}$/, // Class C private + /^169\.254\.\d{1,3}\.\d{1,3}$/, // Link-local + /^0\.0\.0\.0$/, // Unspecified +]; + +const BLOCKED_HOSTNAMES = new Set(['localhost', '[::1]', '0.0.0.0']); + +export function isValidUrl(input: string): boolean { + try { + const url = new URL(input); + return url.protocol === 'http:' || url.protocol === 'https:'; + } catch { + return false; + } +} + +export function isAllowedUrl(input: string): boolean { + if (!isValidUrl(input)) { + return false; + } + + const url = new URL(input); + const hostname = url.hostname.toLowerCase(); + + if (BLOCKED_HOSTNAMES.has(hostname)) { + return false; + } + + if (PRIVATE_IP_PATTERNS.some((pattern) => pattern.test(hostname))) { + return false; + } + + return true; +}