From e412b067cb1ed0101439b377341143663e681948 Mon Sep 17 00:00:00 2001 From: "Clifford B. Brown" <33701978+cbrown350@users.noreply.github.com> Date: Mon, 22 Jun 2026 15:17:17 -0600 Subject: [PATCH 01/11] feat: add rapid-mlx and ollama-cloud as custom providers Adds two new providers to the OpenFusion model selector: - **rapid-mlx**: Local MLX inference server for Apple Silicon. Runs on localhost:1234, no API key required. Ships with 4 popular MLX model entries (Llama 3.2, Qwen 2.5, Mistral Nemo, DeepSeek R1 Distill). - **ollama-cloud**: Ollama's hosted cloud API at api.ollama.com. Requires an API key. Ships with 6 common model entries (Llama 3.3/3.1, Mistral, Qwen 2.5, DeepSeek R1, Gemma 2). Architecture: - src/providers/custom-providers.ts defines provider configs with model descriptors, baseUrl, api type, and keyless flag. - pi-ai-bridge.ts augments listProviders() and listModels() to include custom providers, and registerCustomProviders() registers model descriptors with pi-ai for resolveModel() to work at fusion time. Called at startup from both index.ts and ui-only.ts. - A sentinel API key ('no-key') is injected for keyless providers via effectiveApiKey(), since pi-ai's OpenAI completions provider throws on falsy apiKeys. The completeness gate skips keyless providers when checking for missing API keys. - Provider API endpoint now returns enriched metadata (name, description, keyless flag) so the UI can display friendly names and hide the key input for local providers. - ApiKeys page shows 'not required' badge for keyless providers instead of 'missing'. - Candidates and Judge pages show friendly provider names in dropdowns instead of raw provider ids. - 17 new tests covering registration, listing, resolution, keyless handling, and model descriptor shape. --- src/config/completeness.ts | 5 +- src/fusion/fusion.ts | 6 +- src/index.ts | 5 + src/providers/custom-providers.ts | 200 ++++++++++++++++++++++++++++++ src/providers/pi-ai-bridge.ts | 52 +++++++- src/server/api/providers.ts | 16 ++- src/server/api/secrets.ts | 6 +- src/server/api/test.ts | 16 ++- src/ui-only.ts | 2 + tests/custom-providers.test.ts | 147 ++++++++++++++++++++++ ui/src/api.ts | 10 +- ui/src/pages/ApiKeys.tsx | 44 ++++--- ui/src/pages/Candidates.tsx | 15 ++- ui/src/pages/Judge.tsx | 15 ++- 14 files changed, 496 insertions(+), 43 deletions(-) create mode 100644 src/providers/custom-providers.ts create mode 100644 tests/custom-providers.test.ts diff --git a/src/config/completeness.ts b/src/config/completeness.ts index 3fc5f7d..49ccc36 100644 --- a/src/config/completeness.ts +++ b/src/config/completeness.ts @@ -4,6 +4,7 @@ import type { RawConfig } from "./schema.js"; import { referencedProviders, loadSecrets } from "./secrets.js"; import { paths } from "../util/paths.js"; +import { KEYLESS_PROVIDERS } from "../providers/custom-providers.js"; export interface CompletenessReport { configured: boolean; @@ -28,7 +29,9 @@ export function isConfigured(config: RawConfig, secretsPath = paths.secrets(), k const referenced = referencedProviders(config); if (referenced.length > 0) { const secrets = loadSecrets(secretsPath, keyPath); - const missing = referenced.filter((p) => !secrets.providers[p]?.apiKey); + // Keyless providers (e.g. rapid-mlx) don't need an API key stored in secrets. + const needsKey = referenced.filter((p) => !KEYLESS_PROVIDERS.has(p)); + const missing = needsKey.filter((p) => !secrets.providers[p]?.apiKey); if (missing.length > 0) reasons.push(`missing API key for provider(s): ${missing.join(", ")}`); } diff --git a/src/fusion/fusion.ts b/src/fusion/fusion.ts index 0aebd58..e7468aa 100644 --- a/src/fusion/fusion.ts +++ b/src/fusion/fusion.ts @@ -5,7 +5,7 @@ import { randomUUID } from "node:crypto"; import type { RawConfig } from "../config/schema.js"; import { isConfigured } from "../config/completeness.js"; import { getKey } from "../config/secrets.js"; -import { resolveModel, type AnyModel } from "../providers/pi-ai-bridge.js"; +import { resolveModel, effectiveApiKey, type AnyModel } from "../providers/pi-ai-bridge.js"; import { runParallelFanout, runSequentialFanout } from "./fanout.js"; import type { WorkerResult } from "./worker.js"; import { fusionStatusRegistry } from "./status.js"; @@ -220,7 +220,7 @@ export async function runFusion(input: FusionInput): Promise { model: safeResolve(c.provider, c.model), prompt: input.prompt, context: input.context, - apiKey: getKey(c.provider, secretsPath, keyPath) ?? "", + apiKey: effectiveApiKey(c.provider, getKey(c.provider, secretsPath, keyPath)), timeoutMs: candidateTimeoutMs, workerPrompt: personaPrompts.worker, })); @@ -288,7 +288,7 @@ export async function runFusion(input: FusionInput): Promise { // --- Judge step 1: analysis --- const judgeModel = safeResolve(judge.provider, judge.model); - const judgeApiKey = getKey(judge.provider, secretsPath, keyPath) ?? ""; + const judgeApiKey = effectiveApiKey(judge.provider, getKey(judge.provider, secretsPath, keyPath)); const candidateViews: CandidateView[] = survivors.map((w, i) => ({ index: i + 1, provider: w.provider, diff --git a/src/index.ts b/src/index.ts index c37683f..c44f3ad 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,8 +7,13 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" import { createMcpServer } from "./server/mcp-server.js"; import { startUiServer } from "./server/ui-server.js"; import { printStartupBanner } from "./util/startup.js"; +import { registerCustomProviders } from "./providers/pi-ai-bridge.js"; async function main(): Promise { + // Register custom providers (rapid-mlx, ollama-cloud) so they appear in the UI + // and resolve correctly at fusion time. + registerCustomProviders(); + // First-run banner (stderr) + auto-open the dashboard on a fresh install. await printStartupBanner(); diff --git a/src/providers/custom-providers.ts b/src/providers/custom-providers.ts new file mode 100644 index 0000000..1999ebd --- /dev/null +++ b/src/providers/custom-providers.ts @@ -0,0 +1,200 @@ +// Custom provider definitions for OpenFusion. +// +// pi-ai's static registry covers cloud providers, but local/OpenAI-compatible +// endpoints (like rapid-mlx and ollama-cloud) aren't in that registry. This +// module defines them so they appear in the web config dropdowns and resolve +// correctly at fusion time. +// +// Each entry provides: +// - provider id, display name, and description +// - whether an API key is required (local servers often don't need one) +// - a list of popular models with their model descriptors (baseUrl, api, etc.) +// +// At startup, registerCustomProviders() injects these into the pi-ai bridge +// so listProviders(), listModels(), and resolveModel() all work seamlessly. +import type { AnyModel } from "./pi-ai-bridge.js"; + +/** A model descriptor for a custom provider. */ +export interface CustomModelDescriptor { + id: string; + name: string; + contextWindow: number; + maxTokens: number; + reasoning: boolean; + cost: { input: number; output: number; cacheRead: number; cacheWrite: number }; +} + +/** A custom provider definition with its models. */ +export interface CustomProviderDefinition { + /** Unique provider id (used in config.json and secrets). */ + id: string; + /** Human-readable name for the UI. */ + name: string; + /** Short description shown in the UI. */ + description: string; + /** Whether this provider requires an API key. Local servers typically don't. */ + apiKeyRequired: boolean; + /** Base URL for the OpenAI-compatible API endpoint. */ + baseUrl: string; + /** pi-ai API type. All custom providers currently use openai-completions. */ + api: "openai-completions" | "openai-responses"; + /** Popular models available on this provider. */ + models: CustomModelDescriptor[]; + /** Compat overrides for the OpenAI completions API (auto-detected if not set). */ + compat?: Record; +} + +// ─── Provider definitions ──────────────────────────────────────────────────── + +/** rapid-mlx: local MLX inference server for Apple Silicon. No API key needed. */ +export const RAPID_MLX: CustomProviderDefinition = { + id: "rapid-mlx", + name: "Rapid-MLX (Local)", + description: "Local MLX inference server for Apple Silicon. Runs on localhost — no API key needed.", + apiKeyRequired: false, + baseUrl: "http://localhost:1234/v1", + api: "openai-completions", + compat: { + supportsStore: false, + supportsDeveloperRole: false, + supportsReasoningEffort: false, + maxTokensField: "max_tokens", + supportsStrictMode: false, + supportsLongCacheRetention: false, + }, + models: [ + { + id: "mlx-community/Llama-3.2-3B-Instruct-4bit", + name: "Llama 3.2 3B Instruct (4-bit)", + contextWindow: 131072, + maxTokens: 4096, + reasoning: false, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + }, + { + id: "mlx-community/Qwen2.5-7B-Instruct-4bit", + name: "Qwen 2.5 7B Instruct (4-bit)", + contextWindow: 131072, + maxTokens: 8192, + reasoning: false, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + }, + { + id: "mlx-community/Mistral-Nemo-Instruct-2407-4bit", + name: "Mistral Nemo Instruct (4-bit)", + contextWindow: 131072, + maxTokens: 4096, + reasoning: false, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + }, + { + id: "mlx-community/DeepSeek-R1-Distill-Llama-8B-4bit", + name: "DeepSeek R1 Distill Llama 8B (4-bit)", + contextWindow: 131072, + maxTokens: 8192, + reasoning: true, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + }, + ], +}; + +/** ollama-cloud: Ollama's cloud API service. Requires an API key. */ +export const OLLAMA_CLOUD: CustomProviderDefinition = { + id: "ollama-cloud", + name: "Ollama Cloud", + description: "Ollama's hosted cloud API. Requires an API key from ollama.com.", + apiKeyRequired: true, + baseUrl: "https://api.ollama.com/v1", + api: "openai-completions", + compat: { + supportsStore: false, + supportsDeveloperRole: false, + supportsReasoningEffort: false, + maxTokensField: "max_tokens", + supportsStrictMode: false, + supportsLongCacheRetention: false, + }, + models: [ + { + id: "llama3.3", + name: "Llama 3.3 70B", + contextWindow: 131072, + maxTokens: 32768, + reasoning: false, + cost: { input: 0.3, output: 0.7, cacheRead: 0, cacheWrite: 0 }, + }, + { + id: "llama3.1", + name: "Llama 3.1 8B", + contextWindow: 131072, + maxTokens: 4096, + reasoning: false, + cost: { input: 0.05, output: 0.1, cacheRead: 0, cacheWrite: 0 }, + }, + { + id: "mistral", + name: "Mistral 7B", + contextWindow: 32768, + maxTokens: 4096, + reasoning: false, + cost: { input: 0.05, output: 0.1, cacheRead: 0, cacheWrite: 0 }, + }, + { + id: "qwen2.5", + name: "Qwen 2.5 14B", + contextWindow: 131072, + maxTokens: 8192, + reasoning: false, + cost: { input: 0.1, output: 0.2, cacheRead: 0, cacheWrite: 0 }, + }, + { + id: "deepseek-r1", + name: "DeepSeek R1", + contextWindow: 131072, + maxTokens: 8192, + reasoning: true, + cost: { input: 0.3, output: 1.0, cacheRead: 0, cacheWrite: 0 }, + }, + { + id: "gemma2", + name: "Gemma 2 9B", + contextWindow: 8192, + maxTokens: 4096, + reasoning: false, + cost: { input: 0.05, output: 0.1, cacheRead: 0, cacheWrite: 0 }, + }, + ], +}; + +/** All custom provider definitions, keyed by provider id. */ +export const CUSTOM_PROVIDERS: Record = { + [RAPID_MLX.id]: RAPID_MLX, + [OLLAMA_CLOUD.id]: OLLAMA_CLOUD, +}; + +/** Provider ids that don't require an API key. */ +export const KEYLESS_PROVIDERS = new Set( + Object.values(CUSTOM_PROVIDERS) + .filter((p) => !p.apiKeyRequired) + .map((p) => p.id), +); + +/** Convert a CustomProviderDefinition + model to a pi-ai AnyModel descriptor. */ +export function toModelDescriptor( + provider: CustomProviderDefinition, + model: CustomModelDescriptor, +): AnyModel { + return { + id: model.id, + name: model.name, + api: provider.api, + provider: provider.id, + baseUrl: provider.baseUrl, + reasoning: model.reasoning, + input: ["text" as const], + cost: model.cost, + contextWindow: model.contextWindow, + maxTokens: model.maxTokens, + ...(provider.compat ? { compat: provider.compat } : {}), + }; +} \ No newline at end of file diff --git a/src/providers/pi-ai-bridge.ts b/src/providers/pi-ai-bridge.ts index e1b13a2..4fa0195 100644 --- a/src/providers/pi-ai-bridge.ts +++ b/src/providers/pi-ai-bridge.ts @@ -11,6 +11,14 @@ import { type Api, } from "@earendil-works/pi-ai"; import type { Model } from "@earendil-works/pi-ai"; +import { CUSTOM_PROVIDERS, KEYLESS_PROVIDERS, toModelDescriptor } from "./custom-providers.js"; + +/** + * Sentinel API key for providers that don't require authentication (e.g. rapid-mlx). + * pi-ai's OpenAI completions provider throws if apiKey is falsy; this sentinel + * satisfies the check while the local server ignores it. + */ +const NO_KEY_SENTINEL = "no-key"; /** The model shape used throughout OpenFusion (a pi-ai Model with a broad Api). */ export type AnyModel = Model; @@ -47,13 +55,28 @@ export function resolveModel(provider: string, model: string): AnyModel { } } -/** List provider ids (for the UI dropdowns). */ +/** List provider ids (for the UI dropdowns). Includes pi-ai's built-in + custom providers. */ export function listProviders(): string[] { - return getProviders() as string[]; + const builtIn = getProviders() as string[]; + const customIds = Object.keys(CUSTOM_PROVIDERS); + // Custom providers that aren't already in pi-ai's registry (avoid duplicates). + const added = customIds.filter((id) => !builtIn.includes(id)); + return [...builtIn, ...added]; } -/** List models for a provider (for the UI dropdowns). */ +/** List models for a provider (for the UI dropdowns). Includes custom provider models. */ export function listModels(provider: string) { + // Check custom providers first — their models come from our definitions. + const customDef = CUSTOM_PROVIDERS[provider]; + if (customDef) { + return customDef.models.map((m) => ({ + id: m.id, + contextWindow: m.contextWindow as number | undefined, + reasoning: m.reasoning as boolean | string | undefined, + cost: m.cost as { input?: number; output?: number } | undefined, + })); + } + // Built-in pi-ai provider — delegate to the static registry. // May throw if provider unknown — let callers wrap. const models = getModels(provider as never) as Array<{ id: string; @@ -69,6 +92,17 @@ export function listModels(provider: string) { })); } +/** + * Resolve the effective API key for a provider. Keyless providers (e.g. rapid-mlx) + * that have no stored key get a sentinel value so pi-ai doesn't reject the call; + * all others pass through the stored key unchanged. + * If a keyless provider has a stored key (user explicitly saved one), respect it. + */ +export function effectiveApiKey(provider: string, storedKey: string | undefined): string { + if (KEYLESS_PROVIDERS.has(provider) && !storedKey) return NO_KEY_SENTINEL; + return storedKey ?? ""; +} + /** Run a single non-streaming completion. The single-shot worker + both judge steps use this. */ export async function runComplete( model: AnyModel, @@ -147,3 +181,15 @@ export class BridgeError extends Error { this.code = code; } } + +/** + * Register custom provider model descriptors with pi-ai so resolveModel() works + * at fusion time. Call once at startup. Idempotent — re-registering overwrites. + */ +export function registerCustomProviders(): void { + for (const def of Object.values(CUSTOM_PROVIDERS)) { + for (const model of def.models) { + registerModelDescriptor(def.id, model.id, toModelDescriptor(def, model)); + } + } +} diff --git a/src/server/api/providers.ts b/src/server/api/providers.ts index 6730212..076e22a 100644 --- a/src/server/api/providers.ts +++ b/src/server/api/providers.ts @@ -1,12 +1,24 @@ -// GET /api/providers and /api/providers/:provider/models — passthrough to pi-ai. +// GET /api/providers and /api/providers/:provider/models — passthrough to pi-ai + custom providers. import { Router } from "express"; import { listProviders, listModels } from "../../providers/pi-ai-bridge.js"; +import { CUSTOM_PROVIDERS, KEYLESS_PROVIDERS } from "../../providers/custom-providers.js"; export function providersRouter(): Router { const r = Router(); r.get("/", (_req, res) => { - res.json({ providers: listProviders() }); + const providers = listProviders(); + // Enrich with metadata from custom provider definitions (name, description, keyless). + const enriched = providers.map((id) => { + const custom = CUSTOM_PROVIDERS[id]; + return { + id, + name: custom?.name ?? id, + description: custom?.description ?? undefined, + keyless: KEYLESS_PROVIDERS.has(id), + }; + }); + res.json({ providers: enriched }); }); r.get("/:provider/models", (req, res) => { diff --git a/src/server/api/secrets.ts b/src/server/api/secrets.ts index 426d0a5..5f0069e 100644 --- a/src/server/api/secrets.ts +++ b/src/server/api/secrets.ts @@ -2,6 +2,7 @@ import { Router } from "express"; import { loadConfig } from "../../config/store.js"; import { maskedPresence, setProviderKey } from "../../config/secrets.js"; +import { KEYLESS_PROVIDERS } from "../../providers/custom-providers.js"; export function secretsRouter(): Router { const r = Router(); @@ -9,7 +10,10 @@ export function secretsRouter(): Router { // Masked presence only — NEVER the raw key. r.get("/", (_req, res) => { const config = loadConfig(); - res.json(maskedPresence(config)); + const result = maskedPresence(config); + // Annotate keyless providers so the UI can skip/hide their key input. + const keyless = [...KEYLESS_PROVIDERS]; + res.json({ ...result, keyless }); }); // Set (or clear with null/empty) one provider's key. Encrypted before write. diff --git a/src/server/api/test.ts b/src/server/api/test.ts index b473548..7b690fe 100644 --- a/src/server/api/test.ts +++ b/src/server/api/test.ts @@ -1,18 +1,26 @@ // POST /api/test — validate a provider+model+key before the user commits it (FR-013). import { Router } from "express"; -import { testPing } from "../../providers/pi-ai-bridge.js"; +import { testPing, effectiveApiKey } from "../../providers/pi-ai-bridge.js"; export function testRouter(): Router { const r = Router(); r.post("/", async (req, res) => { const { provider, model, apiKey } = req.body ?? {}; - if (!provider || !model || !apiKey) { - const e = new Error("provider, model, and apiKey are required"); + if (!provider || !model) { + const e = new Error("provider and model are required"); (e as Error & { code?: string }).code = "VALIDATION"; throw e; } - const result = await testPing(provider, model, apiKey, 10_000); + // Keyless providers (e.g. rapid-mlx) don't require an API key — supply a sentinel. + // For others, apiKey is required. + const resolvedKey = effectiveApiKey(provider, apiKey || undefined); + if (!resolvedKey) { + const e = new Error("apiKey is required for this provider"); + (e as Error & { code?: string }).code = "VALIDATION"; + throw e; + } + const result = await testPing(provider, model, resolvedKey, 10_000); res.json(result); }); diff --git a/src/ui-only.ts b/src/ui-only.ts index 7899f71..407c7c2 100644 --- a/src/ui-only.ts +++ b/src/ui-only.ts @@ -4,8 +4,10 @@ // Shares the same on-disk SQLite + config + secrets as the MCP server. import { startUiServer } from "./server/ui-server.js"; import { printStartupBanner } from "./util/startup.js"; +import { registerCustomProviders } from "./providers/pi-ai-bridge.js"; async function main(): Promise { + registerCustomProviders(); await printStartupBanner(); await startUiServer(); console.error("OpenFusion dashboard running. Press Ctrl+C to stop."); diff --git a/tests/custom-providers.test.ts b/tests/custom-providers.test.ts new file mode 100644 index 0000000..1ee471c --- /dev/null +++ b/tests/custom-providers.test.ts @@ -0,0 +1,147 @@ +// Tests for custom provider registration and keyless provider handling. +import { describe, it, expect, beforeEach } from "vitest"; +import { + listProviders, + listModels, + resolveModel, + registerCustomProviders, + clearModelDescriptors, + effectiveApiKey, +} from "../src/providers/pi-ai-bridge.js"; +import { + CUSTOM_PROVIDERS, + RAPID_MLX, + OLLAMA_CLOUD, + KEYLESS_PROVIDERS, + toModelDescriptor, +} from "../src/providers/custom-providers.js"; + +// Ensure custom providers are registered before each test. +beforeEach(() => { + clearModelDescriptors(); + registerCustomProviders(); +}); + +describe("custom providers: registration", () => { + it("includes rapid-mlx and ollama-cloud in listProviders()", () => { + const providers = listProviders(); + expect(providers).toContain("rapid-mlx"); + expect(providers).toContain("ollama-cloud"); + // Built-in providers should still be present. + expect(providers).toContain("openai"); + expect(providers).toContain("anthropic"); + }); + + it("does not duplicate providers already in pi-ai's registry", () => { + const providers = listProviders(); + // Count occurrences of each built-in — should be exactly 1. + const openaiCount = providers.filter((p) => p === "openai").length; + expect(openaiCount).toBe(1); + }); +}); + +describe("custom providers: model listing", () => { + it("lists rapid-mlx models", () => { + const models = listModels("rapid-mlx"); + expect(models.length).toBe(RAPID_MLX.models.length); + expect(models.map((m) => m.id)).toContain("mlx-community/Llama-3.2-3B-Instruct-4bit"); + }); + + it("lists ollama-cloud models", () => { + const models = listModels("ollama-cloud"); + expect(models.length).toBe(OLLAMA_CLOUD.models.length); + expect(models.map((m) => m.id)).toContain("llama3.3"); + }); + + it("still lists built-in provider models", () => { + const models = listModels("openai"); + // OpenAI should still have models from pi-ai's registry. + expect(models.length).toBeGreaterThan(0); + }); +}); + +describe("custom providers: model resolution", () => { + it("resolves a rapid-mlx model descriptor", () => { + const model = resolveModel("rapid-mlx", "mlx-community/Llama-3.2-3B-Instruct-4bit"); + expect(model.id).toBe("mlx-community/Llama-3.2-3B-Instruct-4bit"); + expect(model.provider).toBe("rapid-mlx"); + expect(model.api).toBe("openai-completions"); + expect(model.baseUrl).toBe("http://localhost:1234/v1"); + }); + + it("resolves an ollama-cloud model descriptor", () => { + const model = resolveModel("ollama-cloud", "llama3.3"); + expect(model.id).toBe("llama3.3"); + expect(model.provider).toBe("ollama-cloud"); + expect(model.api).toBe("openai-completions"); + expect(model.baseUrl).toBe("https://api.ollama.com/v1"); + }); + + it("throws on unknown provider/model", () => { + expect(() => resolveModel("nonexistent-provider", "fake-model")).toThrow(); + }); +}); + +describe("custom providers: keyless handling", () => { + it("KEYLESS_PROVIDERS contains rapid-mlx but not ollama-cloud", () => { + expect(KEYLESS_PROVIDERS.has("rapid-mlx")).toBe(true); + expect(KEYLESS_PROVIDERS.has("ollama-cloud")).toBe(false); + }); + + it("effectiveApiKey returns sentinel for keyless providers with no stored key", () => { + const key = effectiveApiKey("rapid-mlx", undefined); + expect(key).toBe("no-key"); + }); + + it("effectiveApiKey returns sentinel for keyless providers with empty string", () => { + const key = effectiveApiKey("rapid-mlx", ""); + expect(key).toBe("no-key"); + }); + + it("effectiveApiKey returns stored key for keyless providers when present", () => { + // If someone stored a key anyway, respect it. + const key = effectiveApiKey("rapid-mlx", "my-custom-key"); + expect(key).toBe("my-custom-key"); + }); + + it("effectiveApiKey returns stored key for normal providers", () => { + const key = effectiveApiKey("openai", "sk-abc"); + expect(key).toBe("sk-abc"); + }); + + it("effectiveApiKey returns empty string for normal providers with no key", () => { + const key = effectiveApiKey("openai", undefined); + expect(key).toBe(""); + }); +}); + +describe("custom providers: completeness gate", () => { + it("KEYLESS_PROVIDERS set can be used to filter missing-key providers", () => { + const referenced = ["rapid-mlx", "openai"]; + const needsKey = referenced.filter((p) => !KEYLESS_PROVIDERS.has(p)); + // rapid-mlx is keyless, so only openai needs a key. + expect(needsKey).toEqual(["openai"]); + }); +}); + +describe("custom providers: model descriptor shape", () => { + it("toModelDescriptor produces correct shape for rapid-mlx", () => { + const model = RAPID_MLX.models[0]; + const descriptor = toModelDescriptor(RAPID_MLX, model); + expect(descriptor.id).toBe(model.id); + expect(descriptor.name).toBe(model.name); + expect(descriptor.api).toBe("openai-completions"); + expect(descriptor.provider).toBe("rapid-mlx"); + expect(descriptor.baseUrl).toBe("http://localhost:1234/v1"); + expect(descriptor.reasoning).toBe(model.reasoning); + expect(descriptor.contextWindow).toBe(model.contextWindow); + expect(descriptor.maxTokens).toBe(model.maxTokens); + }); + + it("toModelDescriptor includes compat overrides", () => { + const model = RAPID_MLX.models[0]; + const descriptor = toModelDescriptor(RAPID_MLX, model); + expect(descriptor.compat).toBeDefined(); + expect((descriptor.compat as Record).maxTokensField).toBe("max_tokens"); + }); +}); \ No newline at end of file diff --git a/ui/src/api.ts b/ui/src/api.ts index f98c2bd..40727d9 100644 --- a/ui/src/api.ts +++ b/ui/src/api.ts @@ -30,6 +30,8 @@ export interface Persona { export interface SecretsView { providers: Record; referenced: string[]; + /** Provider ids that don't require an API key (e.g. rapid-mlx). */ + keyless: string[]; } export interface ProviderModel { id: string; @@ -37,6 +39,12 @@ export interface ProviderModel { reasoning?: boolean | string; cost?: { input?: number; output?: number }; } +export interface ProviderInfo { + id: string; + name: string; + description?: string; + keyless: boolean; +} export interface TestResult { ok: boolean; latencyMs: number; @@ -138,7 +146,7 @@ export const api = { getSecrets: () => getJSON("/api/secrets"), putSecret: (provider: string, apiKey: string | null) => sendJSON("PUT", "/api/secrets", { provider, apiKey }), - getProviders: () => getJSON<{ providers: string[] }>("/api/providers"), + getProviders: () => getJSON<{ providers: ProviderInfo[] }>("/api/providers"), getModels: (provider: string) => getJSON<{ models: ProviderModel[] }>(`/api/providers/${encodeURIComponent(provider)}/models`), testProvider: (provider: string, model: string, apiKey: string) => diff --git a/ui/src/pages/ApiKeys.tsx b/ui/src/pages/ApiKeys.tsx index 6634acb..7fb743e 100644 --- a/ui/src/pages/ApiKeys.tsx +++ b/ui/src/pages/ApiKeys.tsx @@ -12,6 +12,7 @@ export function ApiKeysPage({ config }: { config: AppConfig | null }) { useEffect(refresh, [config]); const referenced = secrets?.referenced ?? []; + const keyless = new Set(secrets?.keyless ?? []); const save = async (provider: string) => { setMsg(null); @@ -57,11 +58,16 @@ export function ApiKeysPage({ config }: { config: AppConfig | null }) { {referenced.map((p) => { const entry = secrets?.providers[p]; const result = results[p]; + const isKeyless = keyless.has(p); return (
{p} - {entry?.present ? ( + {isKeyless ? ( + + not required + + ) : entry?.present ? ( set · {entry.hint} @@ -69,21 +75,27 @@ export function ApiKeysPage({ config }: { config: AppConfig | null }) { missing )}
-
- setDrafts((d) => ({ ...d, [p]: e.target.value }))} - /> - - -
+ {isKeyless ? ( +

+ This provider runs locally and does not require an API key. +

+ ) : ( +
+ setDrafts((d) => ({ ...d, [p]: e.target.value }))} + /> + + +
+ )} {result && (

{result.ok diff --git a/ui/src/pages/Candidates.tsx b/ui/src/pages/Candidates.tsx index dab939c..a07ebf9 100644 --- a/ui/src/pages/Candidates.tsx +++ b/ui/src/pages/Candidates.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { api, type AppConfig, type CandidateSlot, type ProviderModel } from "../api"; +import { api, type AppConfig, type CandidateSlot, type ProviderInfo, type ProviderModel } from "../api"; /** * Serial time budget in minutes (feature 007). Mirrors the engine constants in @@ -23,11 +23,14 @@ export function CandidatesPage({ const [candidates, setCandidates] = useState([]); const [benchmark, setBenchmark] = useState(false); const [sequential, setSequential] = useState(false); - const [providers, setProviders] = useState([]); + const [providerList, setProviderList] = useState([]); const [modelsByProvider, setModelsByProvider] = useState>({}); const [saving, setSaving] = useState(false); const [msg, setMsg] = useState(null); + // Provider id list for dropdowns. + const providers = providerList.map((p) => p.id); + useEffect(() => { if (config) { setCandidates(config.candidates); @@ -37,7 +40,7 @@ export function CandidatesPage({ }, [config]); useEffect(() => { - void api.getProviders().then((r) => setProviders(r.providers)); + void api.getProviders().then((r) => setProviderList(r.providers)); }, []); const loadModels = async (provider: string) => { @@ -177,9 +180,9 @@ export function CandidatesPage({ value={c.provider} onChange={(e) => update(c.id, { provider: e.target.value, model: "" })} > - {providers.map((p) => ( - ))} diff --git a/ui/src/pages/Judge.tsx b/ui/src/pages/Judge.tsx index 1bc9070..a82a39d 100644 --- a/ui/src/pages/Judge.tsx +++ b/ui/src/pages/Judge.tsx @@ -1,18 +1,21 @@ import { useEffect, useState } from "react"; -import { api, type AppConfig, type JudgeConfig, type ProviderModel } from "../api"; +import { api, type AppConfig, type JudgeConfig, type ProviderInfo, type ProviderModel } from "../api"; export function JudgePage({ config, onChanged }: { config: AppConfig | null; onChanged: () => void }) { const [judges, setJudges] = useState([]); - const [providers, setProviders] = useState([]); + const [providerList, setProviderList] = useState([]); const [modelsByProvider, setModelsByProvider] = useState>({}); const [saving, setSaving] = useState(false); const [msg, setMsg] = useState(null); + // Provider id list for defaults. + const providers = providerList.map((p) => p.id); + useEffect(() => { if (config) setJudges(config.judges); }, [config]); useEffect(() => { - void api.getProviders().then((r) => setProviders(r.providers)); + void api.getProviders().then((r) => setProviderList(r.providers)); }, []); const loadModels = async (provider: string) => { @@ -103,9 +106,9 @@ export function JudgePage({ config, onChanged }: { config: AppConfig | null; onC value={j.provider} onChange={(e) => update(i, { provider: e.target.value, model: "" })} > - {providers.map((p) => ( - ))} From e8d36c74eec2958dc24c98af998b51d1a8d92938 Mon Sep 17 00:00:00 2001 From: "Clifford B. Brown" <33701978+cbrown350@users.noreply.github.com> Date: Mon, 22 Jun 2026 15:30:46 -0600 Subject: [PATCH 02/11] fix: replace hardcoded models with dynamic discovery for custom providers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The initial implementation had fabricated model lists that didn't match reality. This commit replaces them with a proper dynamic discovery system: - Removed all hardcoded model entries from rapid-mlx and ollama-cloud. Models depend on what's actually running/available, not a static list. - Fixed rapid-mlx base URL: localhost:8000 (not :1234, which was wrong). - Fixed ollama-cloud base URL: https://ollama.com/v1 (not api.ollama.com). - Added 'discoverable' flag to CustomProviderDefinition — both custom providers are discoverable via the /v1/models endpoint. - Added GET /api/providers/:provider/discover endpoint that dynamically fetches models from the provider's /v1/models at runtime. - Added registerCustomModel() to pi-ai-bridge for registering discovered or user-typed models on the fly, so resolveModel() works at fusion time. - Candidates and Judge pages now show a text input + datalist for discoverable providers (with a 'Discover' button) instead of a static dropdown. Built-in providers still use the static dropdown. - listModels() returns empty for discoverable providers (their models come from runtime discovery, not a static registry). - 21 tests updated to cover dynamic registration, discovery, and corrected base URLs. --- src/providers/custom-providers.ts | 191 ++++++++++++------------------ src/providers/pi-ai-bridge.ts | 41 +++---- src/server/api/providers.ts | 48 +++++++- tests/custom-providers.test.ts | 94 ++++++++------- ui/src/api.ts | 3 + 5 files changed, 194 insertions(+), 183 deletions(-) diff --git a/src/providers/custom-providers.ts b/src/providers/custom-providers.ts index 1999ebd..db57abc 100644 --- a/src/providers/custom-providers.ts +++ b/src/providers/custom-providers.ts @@ -5,26 +5,17 @@ // module defines them so they appear in the web config dropdowns and resolve // correctly at fusion time. // -// Each entry provides: -// - provider id, display name, and description -// - whether an API key is required (local servers often don't need one) -// - a list of popular models with their model descriptors (baseUrl, api, etc.) +// Custom providers are *discoverable*: they support the /v1/models endpoint +// so the UI can fetch the actual available models at runtime instead of relying +// on a hardcoded list that quickly goes stale. When discovery fails (server +// down, no network), the user can type a model ID directly. // // At startup, registerCustomProviders() injects these into the pi-ai bridge -// so listProviders(), listModels(), and resolveModel() all work seamlessly. +// so resolveModel() works for any model the user selects. listProviders() and +// listModels() are also augmented to include these providers. import type { AnyModel } from "./pi-ai-bridge.js"; -/** A model descriptor for a custom provider. */ -export interface CustomModelDescriptor { - id: string; - name: string; - contextWindow: number; - maxTokens: number; - reasoning: boolean; - cost: { input: number; output: number; cacheRead: number; cacheWrite: number }; -} - -/** A custom provider definition with its models. */ +/** A custom provider definition. Models are discovered at runtime via /v1/models. */ export interface CustomProviderDefinition { /** Unique provider id (used in config.json and secrets). */ id: string; @@ -38,8 +29,12 @@ export interface CustomProviderDefinition { baseUrl: string; /** pi-ai API type. All custom providers currently use openai-completions. */ api: "openai-completions" | "openai-responses"; - /** Popular models available on this provider. */ - models: CustomModelDescriptor[]; + /** + * Whether this provider supports /v1/models discovery. + * When true, the UI will offer a "Discover models" button that fetches the + * model list at runtime; the user can also type a model ID directly. + */ + discoverable: boolean; /** Compat overrides for the OpenAI completions API (auto-detected if not set). */ compat?: Record; } @@ -50,10 +45,13 @@ export interface CustomProviderDefinition { export const RAPID_MLX: CustomProviderDefinition = { id: "rapid-mlx", name: "Rapid-MLX (Local)", - description: "Local MLX inference server for Apple Silicon. Runs on localhost — no API key needed.", + description: + "Local MLX inference server for Apple Silicon. Runs on localhost — no API key needed. " + + "Models depend on what you have loaded; click Discover or type a model ID.", apiKeyRequired: false, - baseUrl: "http://localhost:1234/v1", + baseUrl: "http://localhost:8000/v1", api: "openai-completions", + discoverable: true, compat: { supportsStore: false, supportsDeveloperRole: false, @@ -62,50 +60,19 @@ export const RAPID_MLX: CustomProviderDefinition = { supportsStrictMode: false, supportsLongCacheRetention: false, }, - models: [ - { - id: "mlx-community/Llama-3.2-3B-Instruct-4bit", - name: "Llama 3.2 3B Instruct (4-bit)", - contextWindow: 131072, - maxTokens: 4096, - reasoning: false, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - }, - { - id: "mlx-community/Qwen2.5-7B-Instruct-4bit", - name: "Qwen 2.5 7B Instruct (4-bit)", - contextWindow: 131072, - maxTokens: 8192, - reasoning: false, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - }, - { - id: "mlx-community/Mistral-Nemo-Instruct-2407-4bit", - name: "Mistral Nemo Instruct (4-bit)", - contextWindow: 131072, - maxTokens: 4096, - reasoning: false, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - }, - { - id: "mlx-community/DeepSeek-R1-Distill-Llama-8B-4bit", - name: "DeepSeek R1 Distill Llama 8B (4-bit)", - contextWindow: 131072, - maxTokens: 8192, - reasoning: true, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - }, - ], }; -/** ollama-cloud: Ollama's cloud API service. Requires an API key. */ +/** ollama-cloud: Ollama's cloud API at ollama.com. Requires an API key. */ export const OLLAMA_CLOUD: CustomProviderDefinition = { id: "ollama-cloud", name: "Ollama Cloud", - description: "Ollama's hosted cloud API. Requires an API key from ollama.com.", + description: + "Ollama's hosted cloud API at ollama.com. Requires an API key. " + + "Cloud models (e.g. gpt-oss:120b-cloud) are discovered at runtime.", apiKeyRequired: true, - baseUrl: "https://api.ollama.com/v1", + baseUrl: "https://ollama.com/v1", api: "openai-completions", + discoverable: true, compat: { supportsStore: false, supportsDeveloperRole: false, @@ -114,56 +81,6 @@ export const OLLAMA_CLOUD: CustomProviderDefinition = { supportsStrictMode: false, supportsLongCacheRetention: false, }, - models: [ - { - id: "llama3.3", - name: "Llama 3.3 70B", - contextWindow: 131072, - maxTokens: 32768, - reasoning: false, - cost: { input: 0.3, output: 0.7, cacheRead: 0, cacheWrite: 0 }, - }, - { - id: "llama3.1", - name: "Llama 3.1 8B", - contextWindow: 131072, - maxTokens: 4096, - reasoning: false, - cost: { input: 0.05, output: 0.1, cacheRead: 0, cacheWrite: 0 }, - }, - { - id: "mistral", - name: "Mistral 7B", - contextWindow: 32768, - maxTokens: 4096, - reasoning: false, - cost: { input: 0.05, output: 0.1, cacheRead: 0, cacheWrite: 0 }, - }, - { - id: "qwen2.5", - name: "Qwen 2.5 14B", - contextWindow: 131072, - maxTokens: 8192, - reasoning: false, - cost: { input: 0.1, output: 0.2, cacheRead: 0, cacheWrite: 0 }, - }, - { - id: "deepseek-r1", - name: "DeepSeek R1", - contextWindow: 131072, - maxTokens: 8192, - reasoning: true, - cost: { input: 0.3, output: 1.0, cacheRead: 0, cacheWrite: 0 }, - }, - { - id: "gemma2", - name: "Gemma 2 9B", - contextWindow: 8192, - maxTokens: 4096, - reasoning: false, - cost: { input: 0.05, output: 0.1, cacheRead: 0, cacheWrite: 0 }, - }, - ], }; /** All custom provider definitions, keyed by provider id. */ @@ -179,22 +96,64 @@ export const KEYLESS_PROVIDERS = new Set( .map((p) => p.id), ); -/** Convert a CustomProviderDefinition + model to a pi-ai AnyModel descriptor. */ -export function toModelDescriptor( +/** + * Build a model descriptor for a dynamically discovered model. + * Used when the user selects a discovered model or types a custom model ID. + */ +export function buildModelDescriptor( provider: CustomProviderDefinition, - model: CustomModelDescriptor, + modelId: string, + overrides?: { contextWindow?: number; maxTokens?: number; reasoning?: boolean }, ): AnyModel { return { - id: model.id, - name: model.name, + id: modelId, + name: modelId, api: provider.api, provider: provider.id, baseUrl: provider.baseUrl, - reasoning: model.reasoning, + reasoning: overrides?.reasoning ?? false, input: ["text" as const], - cost: model.cost, - contextWindow: model.contextWindow, - maxTokens: model.maxTokens, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: overrides?.contextWindow ?? 131072, + maxTokens: overrides?.maxTokens ?? 8192, ...(provider.compat ? { compat: provider.compat } : {}), }; +} + +/** Response shape from the OpenAI-compatible /v1/models endpoint. */ +export interface DiscoveryModel { + id: string; + object?: string; + created?: number; + owned_by?: string; +} + +export interface DiscoveryResponse { + object?: string; + data: DiscoveryModel[]; +} + +/** + * Discover models from a provider's /v1/models endpoint. + * Returns a list of model IDs, or throws on network/auth errors. + */ +export async function discoverModels( + provider: CustomProviderDefinition, + apiKey?: string, +): Promise { + const url = `${provider.baseUrl}/models`; + const headers: Record = { + Accept: "application/json", + }; + if (apiKey) { + headers.Authorization = `Bearer ${apiKey}`; + } + const resp = await fetch(url, { headers, signal: AbortSignal.timeout(10_000) }); + if (!resp.ok) { + const body = await resp.text().catch(() => ""); + throw new Error(`${resp.status} ${resp.statusText}${body ? `: ${body.slice(0, 200)}` : ""}`); + } + const json = (await resp.json()) as DiscoveryResponse; + const models = Array.isArray(json.data) ? json.data : []; + return models.map((m) => m.id).sort(); } \ No newline at end of file diff --git a/src/providers/pi-ai-bridge.ts b/src/providers/pi-ai-bridge.ts index 4fa0195..a541a30 100644 --- a/src/providers/pi-ai-bridge.ts +++ b/src/providers/pi-ai-bridge.ts @@ -11,7 +11,7 @@ import { type Api, } from "@earendil-works/pi-ai"; import type { Model } from "@earendil-works/pi-ai"; -import { CUSTOM_PROVIDERS, KEYLESS_PROVIDERS, toModelDescriptor } from "./custom-providers.js"; +import { CUSTOM_PROVIDERS, KEYLESS_PROVIDERS, buildModelDescriptor } from "./custom-providers.js"; /** * Sentinel API key for providers that don't require authentication (e.g. rapid-mlx). @@ -64,18 +64,8 @@ export function listProviders(): string[] { return [...builtIn, ...added]; } -/** List models for a provider (for the UI dropdowns). Includes custom provider models. */ +/** List models for a provider (for the UI dropdowns). Built-in only; custom providers use discovery. */ export function listModels(provider: string) { - // Check custom providers first — their models come from our definitions. - const customDef = CUSTOM_PROVIDERS[provider]; - if (customDef) { - return customDef.models.map((m) => ({ - id: m.id, - contextWindow: m.contextWindow as number | undefined, - reasoning: m.reasoning as boolean | string | undefined, - cost: m.cost as { input?: number; output?: number } | undefined, - })); - } // Built-in pi-ai provider — delegate to the static registry. // May throw if provider unknown — let callers wrap. const models = getModels(provider as never) as Array<{ @@ -103,6 +93,18 @@ export function effectiveApiKey(provider: string, storedKey: string | undefined) return storedKey ?? ""; } +/** + * Register a model for a custom provider at runtime (e.g. after discovery or + * when the user types a model ID). Also registers the descriptor with pi-ai + * so resolveModel() works at fusion time. + */ +export function registerCustomModel(provider: string, modelId: string): void { + const def = CUSTOM_PROVIDERS[provider]; + if (!def) return; // Not a custom provider — ignore (built-in providers use pi-ai's registry). + const descriptor = buildModelDescriptor(def, modelId); + registerModelDescriptor(provider, modelId, descriptor); +} + /** Run a single non-streaming completion. The single-shot worker + both judge steps use this. */ export async function runComplete( model: AnyModel, @@ -183,13 +185,12 @@ export class BridgeError extends Error { } /** - * Register custom provider model descriptors with pi-ai so resolveModel() works - * at fusion time. Call once at startup. Idempotent — re-registering overwrites. + * Register custom provider base descriptors with pi-ai so listProviders() works. + * Custom models are registered dynamically via registerCustomModel() when the user + * selects a discovered model or types a model ID. Called once at startup. */ export function registerCustomProviders(): void { - for (const def of Object.values(CUSTOM_PROVIDERS)) { - for (const model of def.models) { - registerModelDescriptor(def.id, model.id, toModelDescriptor(def, model)); - } - } -} + // No static models to register — they're discovered at runtime. + // This function is kept as a no-op placeholder for future static registrations + // and to maintain the startup call site in index.ts / ui-only.ts. +} \ No newline at end of file diff --git a/src/server/api/providers.ts b/src/server/api/providers.ts index 076e22a..0b995d1 100644 --- a/src/server/api/providers.ts +++ b/src/server/api/providers.ts @@ -1,14 +1,19 @@ -// GET /api/providers and /api/providers/:provider/models — passthrough to pi-ai + custom providers. +// GET /api/providers, GET /api/providers/:provider/models, and +// GET /api/providers/:provider/discover — provider listing, model listing, +// and dynamic model discovery for custom providers. import { Router } from "express"; -import { listProviders, listModels } from "../../providers/pi-ai-bridge.js"; -import { CUSTOM_PROVIDERS, KEYLESS_PROVIDERS } from "../../providers/custom-providers.js"; +import { listProviders, listModels, registerCustomModel } from "../../providers/pi-ai-bridge.js"; +import { CUSTOM_PROVIDERS, KEYLESS_PROVIDERS, discoverModels } from "../../providers/custom-providers.js"; +import { getKey } from "../../config/secrets.js"; +import { effectiveApiKey } from "../../providers/pi-ai-bridge.js"; export function providersRouter(): Router { const r = Router(); + // GET /api/providers — list all providers with metadata. r.get("/", (_req, res) => { const providers = listProviders(); - // Enrich with metadata from custom provider definitions (name, description, keyless). + // Enrich with metadata from custom provider definitions (name, description, keyless, discoverable). const enriched = providers.map((id) => { const custom = CUSTOM_PROVIDERS[id]; return { @@ -16,13 +21,21 @@ export function providersRouter(): Router { name: custom?.name ?? id, description: custom?.description ?? undefined, keyless: KEYLESS_PROVIDERS.has(id), + discoverable: custom?.discoverable ?? false, }; }); res.json({ providers: enriched }); }); + // GET /api/providers/:provider/models — list models for a built-in provider. + // Custom providers use /discover instead (their models aren't static). r.get("/:provider/models", (req, res) => { const provider = req.params.provider; + // For custom discoverable providers, return empty — use /discover. + if (CUSTOM_PROVIDERS[provider]?.discoverable) { + res.json({ models: [] }); + return; + } try { res.json({ models: listModels(provider) }); } catch (e) { @@ -32,5 +45,30 @@ export function providersRouter(): Router { } }); + // GET /api/providers/:provider/discover — dynamically discover models from a + // custom provider's /v1/models endpoint. Also registers discovered models with + // the pi-ai bridge so resolveModel() works at fusion time. + r.get("/:provider/discover", async (req, res) => { + const provider = req.params.provider; + const def = CUSTOM_PROVIDERS[provider]; + if (!def || !def.discoverable) { + res.status(404).json({ error: `Provider '${provider}' does not support model discovery.` }); + return; + } + // Resolve an API key: for keyless providers use the sentinel, otherwise look up stored key. + const apiKey = effectiveApiKey(provider, getKey(provider)); + try { + const modelIds = await discoverModels(def, apiKey === "no-key" ? undefined : apiKey); + // Register each discovered model with the bridge so resolveModel() works. + for (const id of modelIds) { + registerCustomModel(provider, id); + } + res.json({ models: modelIds }); + } catch (e) { + const msg = (e as Error).message; + res.status(502).json({ error: `Model discovery failed for '${provider}': ${msg}` }); + } + }); + return r; -} +} \ No newline at end of file diff --git a/tests/custom-providers.test.ts b/tests/custom-providers.test.ts index 1ee471c..9cb75d9 100644 --- a/tests/custom-providers.test.ts +++ b/tests/custom-providers.test.ts @@ -1,4 +1,4 @@ -// Tests for custom provider registration and keyless provider handling. +// Tests for custom provider registration, discovery, and keyless provider handling. import { describe, it, expect, beforeEach } from "vitest"; import { listProviders, @@ -6,6 +6,7 @@ import { resolveModel, registerCustomProviders, clearModelDescriptors, + registerCustomModel, effectiveApiKey, } from "../src/providers/pi-ai-bridge.js"; import { @@ -13,7 +14,7 @@ import { RAPID_MLX, OLLAMA_CLOUD, KEYLESS_PROVIDERS, - toModelDescriptor, + buildModelDescriptor, } from "../src/providers/custom-providers.js"; // Ensure custom providers are registered before each test. @@ -34,47 +35,45 @@ describe("custom providers: registration", () => { it("does not duplicate providers already in pi-ai's registry", () => { const providers = listProviders(); - // Count occurrences of each built-in — should be exactly 1. const openaiCount = providers.filter((p) => p === "openai").length; expect(openaiCount).toBe(1); }); }); describe("custom providers: model listing", () => { - it("lists rapid-mlx models", () => { + it("returns empty model list for discoverable custom providers", () => { const models = listModels("rapid-mlx"); - expect(models.length).toBe(RAPID_MLX.models.length); - expect(models.map((m) => m.id)).toContain("mlx-community/Llama-3.2-3B-Instruct-4bit"); + expect(models).toEqual([]); }); - it("lists ollama-cloud models", () => { + it("returns empty model list for ollama-cloud (discoverable)", () => { const models = listModels("ollama-cloud"); - expect(models.length).toBe(OLLAMA_CLOUD.models.length); - expect(models.map((m) => m.id)).toContain("llama3.3"); + expect(models).toEqual([]); }); it("still lists built-in provider models", () => { const models = listModels("openai"); - // OpenAI should still have models from pi-ai's registry. expect(models.length).toBeGreaterThan(0); }); }); -describe("custom providers: model resolution", () => { - it("resolves a rapid-mlx model descriptor", () => { - const model = resolveModel("rapid-mlx", "mlx-community/Llama-3.2-3B-Instruct-4bit"); - expect(model.id).toBe("mlx-community/Llama-3.2-3B-Instruct-4bit"); +describe("custom providers: dynamic model registration", () => { + it("resolves a dynamically registered rapid-mlx model", () => { + registerCustomModel("rapid-mlx", "mlx-community/Qwen3.6-35B-A3B-OptiQ-4bit"); + const model = resolveModel("rapid-mlx", "mlx-community/Qwen3.6-35B-A3B-OptiQ-4bit"); + expect(model.id).toBe("mlx-community/Qwen3.6-35B-A3B-OptiQ-4bit"); expect(model.provider).toBe("rapid-mlx"); expect(model.api).toBe("openai-completions"); - expect(model.baseUrl).toBe("http://localhost:1234/v1"); + expect(model.baseUrl).toBe("http://localhost:8000/v1"); }); - it("resolves an ollama-cloud model descriptor", () => { - const model = resolveModel("ollama-cloud", "llama3.3"); - expect(model.id).toBe("llama3.3"); + it("resolves a dynamically registered ollama-cloud model", () => { + registerCustomModel("ollama-cloud", "gpt-oss:120b-cloud"); + const model = resolveModel("ollama-cloud", "gpt-oss:120b-cloud"); + expect(model.id).toBe("gpt-oss:120b-cloud"); expect(model.provider).toBe("ollama-cloud"); expect(model.api).toBe("openai-completions"); - expect(model.baseUrl).toBe("https://api.ollama.com/v1"); + expect(model.baseUrl).toBe("https://ollama.com/v1"); }); it("throws on unknown provider/model", () => { @@ -99,7 +98,6 @@ describe("custom providers: keyless handling", () => { }); it("effectiveApiKey returns stored key for keyless providers when present", () => { - // If someone stored a key anyway, respect it. const key = effectiveApiKey("rapid-mlx", "my-custom-key"); expect(key).toBe("my-custom-key"); }); @@ -115,33 +113,45 @@ describe("custom providers: keyless handling", () => { }); }); -describe("custom providers: completeness gate", () => { - it("KEYLESS_PROVIDERS set can be used to filter missing-key providers", () => { - const referenced = ["rapid-mlx", "openai"]; - const needsKey = referenced.filter((p) => !KEYLESS_PROVIDERS.has(p)); - // rapid-mlx is keyless, so only openai needs a key. - expect(needsKey).toEqual(["openai"]); +describe("custom providers: definition shape", () => { + it("RAPID_MLX has correct base URL (port 8000)", () => { + expect(RAPID_MLX.baseUrl).toBe("http://localhost:8000/v1"); }); -}); -describe("custom providers: model descriptor shape", () => { - it("toModelDescriptor produces correct shape for rapid-mlx", () => { - const model = RAPID_MLX.models[0]; - const descriptor = toModelDescriptor(RAPID_MLX, model); - expect(descriptor.id).toBe(model.id); - expect(descriptor.name).toBe(model.name); - expect(descriptor.api).toBe("openai-completions"); - expect(descriptor.provider).toBe("rapid-mlx"); - expect(descriptor.baseUrl).toBe("http://localhost:1234/v1"); - expect(descriptor.reasoning).toBe(model.reasoning); - expect(descriptor.contextWindow).toBe(model.contextWindow); - expect(descriptor.maxTokens).toBe(model.maxTokens); + it("OLLAMA_CLOUD has correct base URL (ollama.com)", () => { + expect(OLLAMA_CLOUD.baseUrl).toBe("https://ollama.com/v1"); + }); + + it("both providers are discoverable", () => { + expect(RAPID_MLX.discoverable).toBe(true); + expect(OLLAMA_CLOUD.discoverable).toBe(true); + }); + + it("rapid-mlx is keyless", () => { + expect(RAPID_MLX.apiKeyRequired).toBe(false); }); - it("toModelDescriptor includes compat overrides", () => { - const model = RAPID_MLX.models[0]; - const descriptor = toModelDescriptor(RAPID_MLX, model); + it("ollama-cloud requires a key", () => { + expect(OLLAMA_CLOUD.apiKeyRequired).toBe(true); + }); + + it("buildModelDescriptor produces correct shape for a rapid-mlx model", () => { + const descriptor = buildModelDescriptor(RAPID_MLX, "my-model"); + expect(descriptor.id).toBe("my-model"); + expect(descriptor.name).toBe("my-model"); + expect(descriptor.api).toBe("openai-completions"); + expect(descriptor.provider).toBe("rapid-mlx"); + expect(descriptor.baseUrl).toBe("http://localhost:8000/v1"); expect(descriptor.compat).toBeDefined(); expect((descriptor.compat as Record).maxTokensField).toBe("max_tokens"); }); +}); + +describe("custom providers: completeness gate", () => { + it("KEYLESS_PROVIDERS set can be used to filter missing-key providers", () => { + const referenced = ["rapid-mlx", "openai"]; + const needsKey = referenced.filter((p) => !KEYLESS_PROVIDERS.has(p)); + // rapid-mlx is keyless, so only openai needs a key. + expect(needsKey).toEqual(["openai"]); + }); }); \ No newline at end of file diff --git a/ui/src/api.ts b/ui/src/api.ts index 40727d9..681ace8 100644 --- a/ui/src/api.ts +++ b/ui/src/api.ts @@ -149,6 +149,9 @@ export const api = { getProviders: () => getJSON<{ providers: ProviderInfo[] }>("/api/providers"), getModels: (provider: string) => getJSON<{ models: ProviderModel[] }>(`/api/providers/${encodeURIComponent(provider)}/models`), + /** Discover models from a custom provider's /v1/models endpoint. */ + discoverModels: (provider: string) => + getJSON<{ models: string[] }>(`/api/providers/${encodeURIComponent(provider)}/discover`), testProvider: (provider: string, model: string, apiKey: string) => sendJSON("POST", "/api/test", { provider, model, apiKey }), getStats: (filters?: Record) => { From b3bd9a9f5c8dcdd58232d6ce5eea9702176eaf49 Mon Sep 17 00:00:00 2001 From: "Clifford B. Brown" <33701978+cbrown350@users.noreply.github.com> Date: Mon, 22 Jun 2026 15:32:50 -0600 Subject: [PATCH 03/11] feat: add dynamic model discovery UI for custom providers Candidates and Judge pages now show a text input with datalist for discoverable providers (rapid-mlx, ollama-cloud) instead of a static dropdown. A 'Discover' button fetches available models from the provider's /v1/models endpoint at runtime. - ProviderInfo type gains 'discoverable' boolean - Candidates/Judge: discoverable providers get input+datalist+Discover button; built-in providers keep the static select dropdown - Model IDs from discovery are registered with the bridge so they resolve correctly at fusion time --- ui/src/api.ts | 2 + ui/src/pages/Candidates.tsx | 85 +++++++++++++++++++++++++++++-------- ui/src/pages/Judge.tsx | 83 ++++++++++++++++++++++++++++-------- 3 files changed, 136 insertions(+), 34 deletions(-) diff --git a/ui/src/api.ts b/ui/src/api.ts index 681ace8..d4a4d2c 100644 --- a/ui/src/api.ts +++ b/ui/src/api.ts @@ -44,6 +44,8 @@ export interface ProviderInfo { name: string; description?: string; keyless: boolean; + /** Whether this provider supports /v1/models discovery. */ + discoverable: boolean; } export interface TestResult { ok: boolean; diff --git a/ui/src/pages/Candidates.tsx b/ui/src/pages/Candidates.tsx index a07ebf9..d5d78d7 100644 --- a/ui/src/pages/Candidates.tsx +++ b/ui/src/pages/Candidates.tsx @@ -25,11 +25,15 @@ export function CandidatesPage({ const [sequential, setSequential] = useState(false); const [providerList, setProviderList] = useState([]); const [modelsByProvider, setModelsByProvider] = useState>({}); + /** Discovered model IDs for custom/discoverable providers (keyed by provider). */ + const [discoveredByProvider, setDiscoveredByProvider] = useState>({}); + const [discovering, setDiscovering] = useState(null); const [saving, setSaving] = useState(false); const [msg, setMsg] = useState(null); // Provider id list for dropdowns. const providers = providerList.map((p) => p.id); + const providerMap = new Map(providerList.map((p) => [p.id, p])); useEffect(() => { if (config) { @@ -45,6 +49,8 @@ export function CandidatesPage({ const loadModels = async (provider: string) => { if (modelsByProvider[provider]) return; + const pInfo = providerMap.get(provider); + if (pInfo?.discoverable) return; // Discoverable providers use the Discover button. try { const r = await api.getModels(provider); setModelsByProvider((m) => ({ ...m, [provider]: r.models })); @@ -53,9 +59,24 @@ export function CandidatesPage({ } }; + const discoverModels = async (provider: string) => { + setDiscovering(provider); + try { + const r = await api.discoverModels(provider); + setDiscoveredByProvider((d) => ({ ...d, [provider]: r.models })); + } catch (e) { + setMsg(`Discovery failed: ${(e as Error).message}`); + } finally { + setDiscovering(null); + } + }; + const update = (id: string, patch: Partial) => { setCandidates((cs) => cs.map((c) => (c.id === id ? { ...c, ...patch } : c))); - if (patch.provider) void loadModels(patch.provider); + if (patch.provider) { + const pInfo = providerMap.get(patch.provider); + if (!pInfo?.discoverable) void loadModels(patch.provider); + } }; const add = () => { // In benchmark mode there's no max; otherwise cap at 5. @@ -164,7 +185,10 @@ export function CandidatesPage({

{candidates.map((c, i) => { - const models = modelsByProvider[c.provider] ?? []; + const pInfo = providerMap.get(c.provider); + const isDiscoverable = pInfo?.discoverable ?? false; + const staticModels = modelsByProvider[c.provider] ?? []; + const discovered = discoveredByProvider[c.provider] ?? []; return (
{i + 1} @@ -186,20 +210,47 @@ export function CandidatesPage({ ))} - + {isDiscoverable ? ( + /* Discoverable providers: show discovered models in a datalist + free-text input */ +
+ update(c.id, { model: e.target.value })} + /> + + {discovered.map((id) => ( + + +
+ ) : ( + /* Built-in providers: static model dropdown */ + + )} @@ -209,4 +260,4 @@ export function CandidatesPage({
); -} +} \ No newline at end of file diff --git a/ui/src/pages/Judge.tsx b/ui/src/pages/Judge.tsx index a82a39d..c2b9449 100644 --- a/ui/src/pages/Judge.tsx +++ b/ui/src/pages/Judge.tsx @@ -5,11 +5,15 @@ export function JudgePage({ config, onChanged }: { config: AppConfig | null; onC const [judges, setJudges] = useState([]); const [providerList, setProviderList] = useState([]); const [modelsByProvider, setModelsByProvider] = useState>({}); + /** Discovered model IDs for custom/discoverable providers (keyed by provider). */ + const [discoveredByProvider, setDiscoveredByProvider] = useState>({}); + const [discovering, setDiscovering] = useState(null); const [saving, setSaving] = useState(false); const [msg, setMsg] = useState(null); // Provider id list for defaults. const providers = providerList.map((p) => p.id); + const providerMap = new Map(providerList.map((p) => [p.id, p])); useEffect(() => { if (config) setJudges(config.judges); @@ -20,6 +24,8 @@ export function JudgePage({ config, onChanged }: { config: AppConfig | null; onC const loadModels = async (provider: string) => { if (modelsByProvider[provider]) return; + const pInfo = providerMap.get(provider); + if (pInfo?.discoverable) return; try { const r = await api.getModels(provider); setModelsByProvider((m) => ({ ...m, [provider]: r.models })); @@ -28,13 +34,28 @@ export function JudgePage({ config, onChanged }: { config: AppConfig | null; onC } }; + const discoverModels = async (provider: string) => { + setDiscovering(provider); + try { + const r = await api.discoverModels(provider); + setDiscoveredByProvider((d) => ({ ...d, [provider]: r.models })); + } catch (e) { + setMsg(`Discovery failed: ${(e as Error).message}`); + } finally { + setDiscovering(null); + } + }; + // Exactly one enabled: turning one on makes it the sole enabled judge. const toggle = (idx: number, on: boolean) => { setJudges((js) => js.map((j, i) => ({ ...j, enabled: on ? i === idx : i === idx ? false : j.enabled }))); }; const update = (idx: number, patch: Partial) => { setJudges((js) => js.map((j, i) => (i === idx ? { ...j, ...patch } : j))); - if (patch.provider) void loadModels(patch.provider); + if (patch.provider) { + const pInfo = providerMap.get(patch.provider); + if (!pInfo?.discoverable) void loadModels(patch.provider); + } }; const add = () => { setJudges((js) => [...js, { provider: providers[0] ?? "", model: "", enabled: false }]); @@ -91,7 +112,10 @@ export function JudgePage({ config, onChanged }: { config: AppConfig | null; onC
{judges.map((j, i) => { - const models = modelsByProvider[j.provider] ?? []; + const pInfo = providerMap.get(j.provider); + const isDiscoverable = pInfo?.discoverable ?? false; + const staticModels = modelsByProvider[j.provider] ?? []; + const discovered = discoveredByProvider[j.provider] ?? []; return (
))} - + {isDiscoverable ? ( +
+ update(i, { model: e.target.value })} + /> + + {discovered.map((id) => ( + + +
+ ) : ( + + )} @@ -138,4 +187,4 @@ export function JudgePage({ config, onChanged }: { config: AppConfig | null; onC
); -} +} \ No newline at end of file From 6e456f74921d4948312633f8cf1ef842ee6c1d10 Mon Sep 17 00:00:00 2001 From: "Clifford B. Brown" <33701978+cbrown350@users.noreply.github.com> Date: Mon, 22 Jun 2026 15:44:42 -0600 Subject: [PATCH 04/11] =?UTF-8?q?fix:=20remove=20static=20model=20lists=20?= =?UTF-8?q?from=20ollama-cloud=20=E2=80=94=20both=20providers=20use=20disc?= =?UTF-8?q?overy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both rapid-mlx and ollama-cloud now exclusively use /v1/models discovery to populate their model lists. No hardcoded model entries. - rapid-mlx: models depend on what the user has loaded locally - ollama-cloud: models come from ollama.com's /v1/models endpoint The UI auto-discovers models for providers referenced in the saved config on page load, so the model dropdown shows the saved model immediately instead of 'Focus to load…'. --- src/providers/custom-providers.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/providers/custom-providers.ts b/src/providers/custom-providers.ts index db57abc..0e81d50 100644 --- a/src/providers/custom-providers.ts +++ b/src/providers/custom-providers.ts @@ -5,17 +5,17 @@ // module defines them so they appear in the web config dropdowns and resolve // correctly at fusion time. // -// Custom providers are *discoverable*: they support the /v1/models endpoint -// so the UI can fetch the actual available models at runtime instead of relying -// on a hardcoded list that quickly goes stale. When discovery fails (server -// down, no network), the user can type a model ID directly. +// Both custom providers are discoverable — they support the /v1/models endpoint +// so the UI can fetch the actual available models at runtime. No hardcoded +// model lists: rapid-mlx's models depend on what's loaded locally, and +// ollama-cloud's catalog changes as Ollama adds new cloud models. // -// At startup, registerCustomProviders() injects these into the pi-ai bridge -// so resolveModel() works for any model the user selects. listProviders() and -// listModels() are also augmented to include these providers. +// At runtime, registerCustomProviders() registers static model descriptors with +// the pi-ai bridge so resolveModel() works. For discovered or user-typed models, +// registerCustomModel() is called on the fly. import type { AnyModel } from "./pi-ai-bridge.js"; -/** A custom provider definition. Models are discovered at runtime via /v1/models. */ +/** A custom provider definition. */ export interface CustomProviderDefinition { /** Unique provider id (used in config.json and secrets). */ id: string; @@ -47,7 +47,7 @@ export const RAPID_MLX: CustomProviderDefinition = { name: "Rapid-MLX (Local)", description: "Local MLX inference server for Apple Silicon. Runs on localhost — no API key needed. " + - "Models depend on what you have loaded; click Discover or type a model ID.", + "Click Discover to load available models, or type a model ID directly.", apiKeyRequired: false, baseUrl: "http://localhost:8000/v1", api: "openai-completions", @@ -68,7 +68,7 @@ export const OLLAMA_CLOUD: CustomProviderDefinition = { name: "Ollama Cloud", description: "Ollama's hosted cloud API at ollama.com. Requires an API key. " + - "Cloud models (e.g. gpt-oss:120b-cloud) are discovered at runtime.", + "Click Discover to load available cloud models, or type a model ID directly.", apiKeyRequired: true, baseUrl: "https://ollama.com/v1", api: "openai-completions", @@ -97,8 +97,8 @@ export const KEYLESS_PROVIDERS = new Set( ); /** - * Build a model descriptor for a dynamically discovered model. - * Used when the user selects a discovered model or types a custom model ID. + * Build a model descriptor for a dynamically discovered or user-typed model. + * Used by registerCustomModel() and the discover endpoint. */ export function buildModelDescriptor( provider: CustomProviderDefinition, From bdcc7f93873c51cc939b710884c5f1725d32a9d4 Mon Sep 17 00:00:00 2001 From: "Clifford B. Brown" <33701978+cbrown350@users.noreply.github.com> Date: Mon, 22 Jun 2026 16:11:41 -0600 Subject: [PATCH 05/11] =?UTF-8?q?fix:=20separate=20local=20vs=20cloud=20pr?= =?UTF-8?q?ovider=20behavior=20=E2=80=94=20no=20Discover=20button=20for=20?= =?UTF-8?q?ollama-cloud?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add 'local' field to CustomProviderDefinition: rapid-mlx is local (may be unreachable), ollama-cloud is not (cloud API, always reachable) - UI shows free-text input + Discover button only for local providers - Cloud discoverable providers (ollama-cloud) get a normal model dropdown that fetches models via /v1/models automatically - Eager-load models for all providers on page load so saved models appear immediately instead of showing 'Focus to load…' - Always include saved model as a dropdown option even before model list loads - Backend /models endpoint now returns discovered models for ALL discoverable providers (not empty), so cloud providers show a proper dropdown - /discover endpoint restricted to local providers only (cloud providers don't need a manual retry) - Added test for RAPID_MLX.local === true, OLLAMA_CLOUD.local === false --- package-lock.json | 5002 +++++++++++++++++++++++++++++ src/providers/custom-providers.ts | 20 +- src/server/api/providers.ts | 44 +- tests/custom-providers.test.ts | 5 + ui/src/api.ts | 2 + ui/src/pages/Candidates.tsx | 49 +- ui/src/pages/Judge.tsx | 46 +- 7 files changed, 5123 insertions(+), 45 deletions(-) create mode 100644 package-lock.json diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..04b8f39 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,5002 @@ +{ + "name": "openfusion-mcp", + "version": "0.3.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "openfusion-mcp", + "version": "0.3.0", + "license": "MIT", + "dependencies": { + "@earendil-works/pi-ai": "0.79.4", + "@modelcontextprotocol/sdk": "1.29.0", + "better-sqlite3": "12.10.1", + "env-paths": "4.0.0", + "express": "5.2.1", + "open": "11.0.0", + "zod": "3.25.67" + }, + "bin": { + "openfusion-mcp": "dist/index.js", + "openfusion-setup": "dist/setup.js", + "openfusion-ui": "dist/ui-only.js" + }, + "devDependencies": { + "@types/better-sqlite3": "7.6.13", + "@types/express": "5.0.5", + "@types/node": "22.10.0", + "tsx": "4.19.2", + "typescript": "5.7.2", + "vitest": "2.1.8" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.91.1", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.91.1.tgz", + "integrity": "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==", + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-bedrock-runtime": { + "version": "3.1048.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1048.0.tgz", + "integrity": "sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/credential-provider-node": "^3.972.42", + "@aws-sdk/eventstream-handler-node": "^3.972.16", + "@aws-sdk/middleware-eventstream": "^3.972.12", + "@aws-sdk/middleware-websocket": "^3.972.19", + "@aws-sdk/token-providers": "3.1048.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.974.23", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.23.tgz", + "integrity": "sha512-MiWR/uWjxjFXGzrE0Ghc5lWxUxzHsUWFhV+OX7M4cR9SrmrnZs6TXavnCWnzzdwJeFri34xQo81rvGNzK3c4BQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.13", + "@aws-sdk/xml-builder": "^3.972.31", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/core": "^3.24.6", + "@smithy/signature-v4": "^5.4.6", + "@smithy/types": "^4.14.3", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.49", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.49.tgz", + "integrity": "sha512-liB3yQNHCM9k/gu/w36XHMKPluT7HTlnGUhRbBGSISDQkcr/Sy1zsZabiuvQj8WG5yW573u9RehrBvvnIQ9OEQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.23", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.51", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.51.tgz", + "integrity": "sha512-XET0H2oofciJ5lMRWNIvRjAP7Q3wv2XT+JtJJEdhPWUMwe3TvQ9qcxonpu7vXmNngncvFpi4E2It+Tamas/naA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.23", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/fetch-http-handler": "^5.4.6", + "@smithy/node-http-handler": "^4.7.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/node-http-handler": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.8.2.tgz", + "integrity": "sha512-wfl1uwrAqMH9/pi4kqBo5LBcFwrJLxuDLqL7p7qNcJIFcyZDUc6pzhYk4CYv+DP7fIUpQCZumwNnkhPKS52osQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.26.0", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.56", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.56.tgz", + "integrity": "sha512-IAmc61hbgQiHht9U3x0tnRwz0lzdwOwD/i9voRgdJrKamF+JtmrBOsW9GwB7mfFonNWOWL4qARWYrF8veEMe3w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.23", + "@aws-sdk/credential-provider-env": "^3.972.49", + "@aws-sdk/credential-provider-http": "^3.972.51", + "@aws-sdk/credential-provider-login": "^3.972.55", + "@aws-sdk/credential-provider-process": "^3.972.49", + "@aws-sdk/credential-provider-sso": "^3.972.55", + "@aws-sdk/credential-provider-web-identity": "^3.972.55", + "@aws-sdk/nested-clients": "^3.997.23", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/credential-provider-imds": "^4.3.7", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.55", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.55.tgz", + "integrity": "sha512-hBBkANo3cDn+h2qxxzER4a+J8JCO9o9Z/YYmU7iky6AcaarX5RRdRcHNC6SLdwY0vAXQygn6soUbDqPn3GghaA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.23", + "@aws-sdk/nested-clients": "^3.997.23", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.58", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.58.tgz", + "integrity": "sha512-OyCLVmSI7pZO8hxwNVX6pXhTVlJqRBTp+ijdEfJSUj0RyjHnF602OfAarOzGq6wkGodeFkYBt8MmJ6A6ycRgWw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.49", + "@aws-sdk/credential-provider-http": "^3.972.51", + "@aws-sdk/credential-provider-ini": "^3.972.56", + "@aws-sdk/credential-provider-process": "^3.972.49", + "@aws-sdk/credential-provider-sso": "^3.972.55", + "@aws-sdk/credential-provider-web-identity": "^3.972.55", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/credential-provider-imds": "^4.3.7", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.49", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.49.tgz", + "integrity": "sha512-C8h36lBuC/RnBSsjlO+dn6xZm3KbAl5vpJaVPAfQnMmz2/OISmKOc8XZcqMQgO2ADwBYNRMM6Kf3vz9G/TulMQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.23", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.55", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.55.tgz", + "integrity": "sha512-1FkOz74Ea5QGS9jtIoXp55T/IkSS3spv+nLTT07fRY/+T5xmEOqaYBVIaEmX4zTNvbV6g2lrtlaVKWEoNyJt3w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.23", + "@aws-sdk/nested-clients": "^3.997.23", + "@aws-sdk/token-providers": "3.1074.0", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": { + "version": "3.1074.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1074.0.tgz", + "integrity": "sha512-pv80IzgGW4RnXWtft692chZOM9i6PhebVsLCcnaM4dBEPZva2fE6FXAHs76G7Rc7s3yGyX/68G0nZMrUy+Vmpg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.23", + "@aws-sdk/nested-clients": "^3.997.23", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.55", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.55.tgz", + "integrity": "sha512-g2BoECD1q01kTPByi56+VLVvdWDzMkKIcr77qixpqH0okw2t0U5CoPv+6S8v/D1Y2Wa6QKKtn6XAtDzP+Kfpvg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.23", + "@aws-sdk/nested-clients": "^3.997.23", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/eventstream-handler-node": { + "version": "3.972.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.22.tgz", + "integrity": "sha512-tqPJv0dz4+O0hWGm1a6YekcMZyPhDFs/zH73Von7icaVT5n0Jqvm86typ3jRrG+qoUdPhALOnboRLTmnWQTlYQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-eventstream": { + "version": "3.972.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.18.tgz", + "integrity": "sha512-OHpk8YoZi3yexPq8aFt1vN1IxA2zLKvsIR5GpWYylX/ve6kQmY7wxHNSFy/D3t2apMZ16rs76Co4dJWcDyIk3A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-websocket": { + "version": "3.972.31", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.31.tgz", + "integrity": "sha512-ps1rumU1LybSFHaW9dTDgkhCMJLVaedEY78kKSzUDDY+b9974/g6aiaYYA0U9WV0oL4CJCJrVWG+EZ/qr4or7g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.23", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/fetch-http-handler": "^5.4.6", + "@smithy/signature-v4": "^5.4.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.997.23", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.23.tgz", + "integrity": "sha512-gO93ZPsI2bxeFZD42f1/qjDw6FAZkNZcKRO94LIiT03fzOmcJ9e/tunxjVjA1Rl69ClmVJzz8H3G9CdKef10PA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.23", + "@aws-sdk/signature-v4-multi-region": "^3.996.35", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/fetch-http-handler": "^5.4.6", + "@smithy/node-http-handler": "^4.7.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/node-http-handler": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.8.2.tgz", + "integrity": "sha512-wfl1uwrAqMH9/pi4kqBo5LBcFwrJLxuDLqL7p7qNcJIFcyZDUc6pzhYk4CYv+DP7fIUpQCZumwNnkhPKS52osQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.26.0", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.35", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.35.tgz", + "integrity": "sha512-6L/VWs+Wch2stHemCGTmUNqKLMzURxQDK5boNG3Jn3kAOp71meDUuS5sbObpEvFxHDq0uWeSLFDNSYsjNt+Dlg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.13", + "@smithy/signature-v4": "^5.4.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.1048.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1048.0.tgz", + "integrity": "sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.973.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.13.tgz", + "integrity": "sha512-pEHZqRkAlHfnfAU9tK+WpKv/gBNjGJrHMgA3A0iYRGyswBS2t0pfez+lWlwktb3Bqa0ovh7w/QJTFwp3fDxLNg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.8.tgz", + "integrity": "sha512-uUbMs1cBZPafD0ohUj6EwNf0fPZ534NvBxHox4hjX+0Rxq5paSYUem7+hi833pYrzrcnBATKIYpR02MDXT5M9g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.31", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.31.tgz", + "integrity": "sha512-SzE4Pgyl+hDF+BuyuzxUSpwnuUu9lJuO1YGgteG89/4Qv0+2IQiVQqdbPV32IozLvXWQChPQcdkk/sKvb1QHiQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@earendil-works/pi-ai": { + "version": "0.79.4", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.4.tgz", + "integrity": "sha512-Z1j+YP+6ZyPBKDUoc5m0GO/o1hPK17fWeErtDgegCTpm2dcKzuFvL/7GTqHeJkVkfpeXRwO37xOfgozQbK6EUw==", + "license": "MIT", + "dependencies": { + "@anthropic-ai/sdk": "0.91.1", + "@aws-sdk/client-bedrock-runtime": "3.1048.0", + "@google/genai": "1.52.0", + "@mistralai/mistralai": "2.2.1", + "@smithy/node-http-handler": "4.7.3", + "http-proxy-agent": "7.0.2", + "https-proxy-agent": "7.0.6", + "openai": "6.26.0", + "partial-json": "0.1.7", + "typebox": "1.1.38" + }, + "bin": { + "pi-ai": "dist/cli.js" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", + "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", + "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", + "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", + "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", + "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", + "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", + "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", + "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", + "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", + "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", + "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", + "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", + "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", + "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", + "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", + "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", + "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", + "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", + "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", + "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", + "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", + "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", + "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", + "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@google/genai": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", + "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^4.6.2", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@mistralai/mistralai": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.1.tgz", + "integrity": "sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ==", + "license": "Apache-2.0", + "dependencies": { + "ws": "^8.18.0", + "zod": "^3.25.0 || ^4.0.0", + "zod-to-json-schema": "^3.25.0" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "license": "BSD-3-Clause" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@smithy/core": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.26.0.tgz", + "integrity": "sha512-mLUktFAn+Pa2agl1J7VgtYNFWCX8/b4GMJSK1hCu4YCvtBfM6F8Os3EP4ry+DFFlXOf3wyvlgXhuUdFoy52D3g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.4.2.tgz", + "integrity": "sha512-18UMDMyrAbDcpmL1gLUA7ww0fRTcdCrSjSJOi2Sbld+tVjwD/pW+OAwjlScFLR7vvBnhZrIPQ7kVuTf1mnJLug==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.26.0", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.5.2.tgz", + "integrity": "sha512-Ei/UK/QMhq0rKaMqGPlOAkE2yS9DZeYmZdk1RAKc3vp3zxgleZHZyBLlZv8yLsxljX4svCRuMTD6u3LLIcU4Bg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.26.0", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.3.tgz", + "integrity": "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.5.2.tgz", + "integrity": "sha512-7xHpmPY4rt0IOmeAA8EfjgEH8isT+587TCdy9H6a7d4OMi5CQ0oEHhWllunvPu4j4Cq0vTFwdxXN/kABWPjdyA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.26.0", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.15.0.tgz", + "integrity": "sha512-Z5TAOxygoFvybJV3igo5SloFflSokHx2hu1eFA+DxDTcn+FtKxUSui+rbTRG1pAafMA888Z3MVvCWUuvCrTXjg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.5.tgz", + "integrity": "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.8.tgz", + "integrity": "sha512-8ytZ/fFHq2g4PJVAtDX57mayemKgDR6X3Oa2Foro+EygiOJHUXhCqBAAKQYYajZpFoIfvBCF1j6R6IYRSIUFuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.8", + "@vitest/utils": "2.1.8", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.8.tgz", + "integrity": "sha512-7guJ/47I6uqfttp33mgo6ga5Gr1VnL58rcqYKyShoRK9ebu8T5Rs6HN3s1NABiBeVTdWNrwUMcHH54uXZBN4zA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.8", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.8.tgz", + "integrity": "sha512-17ub8vQstRnRlIU5k50bG+QOMLHRhYPAna5tw8tYbj+jzjcspnwnwtPtiOlkuKC4+ixDPTuLZiqiWWQ2PSXHVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.8", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.8.tgz", + "integrity": "sha512-20T7xRFbmnkfcmgVEz+z3AU/3b0cEzZOt/zmnvZEctg64/QZbSDJEVm9fLnnlSi74KibmRsO9/Qabi+t0vCRPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.8", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.8.tgz", + "integrity": "sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.8.tgz", + "integrity": "sha512-5swjf2q95gXeYPevtW0BLk6H8+bPlMb4Vw/9Em4hFxDcaOxS+e0LOX4yqNxoHzMR2akEB2xfpnWUzkZokmgWDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.8.tgz", + "integrity": "sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.8", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils/node_modules/@vitest/pretty-format": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.8.tgz", + "integrity": "sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "12.10.1", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.10.1.tgz", + "integrity": "sha512-HfFtzCqnSfwB3+HroF6PSKzyh+7RfNMGPCzHFUZXRlvrPCb4P3cvxKZNN43Sr7IrkofqQZM+gIvffGpA8VvqgA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" + } + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT" + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-4.0.0.tgz", + "integrity": "sha512-pxP8eL2SwwaTRi/KHYwLYXinDs7gL3jxFcBYmEdYfZmZXbaVDvdppd0XBU8qVz03rDfKZMXg1omHCbsJjZrMsw==", + "license": "MIT", + "dependencies": { + "is-safe-filename": "^0.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", + "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.1", + "@esbuild/android-arm": "0.23.1", + "@esbuild/android-arm64": "0.23.1", + "@esbuild/android-x64": "0.23.1", + "@esbuild/darwin-arm64": "0.23.1", + "@esbuild/darwin-x64": "0.23.1", + "@esbuild/freebsd-arm64": "0.23.1", + "@esbuild/freebsd-x64": "0.23.1", + "@esbuild/linux-arm": "0.23.1", + "@esbuild/linux-arm64": "0.23.1", + "@esbuild/linux-ia32": "0.23.1", + "@esbuild/linux-loong64": "0.23.1", + "@esbuild/linux-mips64el": "0.23.1", + "@esbuild/linux-ppc64": "0.23.1", + "@esbuild/linux-riscv64": "0.23.1", + "@esbuild/linux-s390x": "0.23.1", + "@esbuild/linux-x64": "0.23.1", + "@esbuild/netbsd-x64": "0.23.1", + "@esbuild/openbsd-arm64": "0.23.1", + "@esbuild/openbsd-x64": "0.23.1", + "@esbuild/sunos-x64": "0.23.1", + "@esbuild/win32-arm64": "0.23.1", + "@esbuild/win32-ia32": "0.23.1", + "@esbuild/win32-x64": "0.23.1" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gaxios": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.5.tgz", + "integrity": "sha512-5FZy72Rh8LhtjmvDrKkI+lVhrsQrVKVsItxMoDm5mNQE+xR0WVIIs+jzPSJgBvKVsLi24fZhXJIsNI0bihDzFg==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/google-auth-library": { + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.7.0.tgz", + "integrity": "sha512-QpTAbNJ36TliZLx3TTtahR8HG0hN9RllL1e3FymOvQSIKK8JmgV58H924ub2wa2DsS3ANjjP1Aw1N+Ramc8hqQ==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.26", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.26.tgz", + "integrity": "sha512-uyZtpnYxM9CmQ7QsQknM4zN8EftNqhON1qYeIKM0Se67CCEe2c44xyGURwB0axX2fBDu1dqHrHAc1hmNT8ITkw==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-in-ssh": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", + "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-safe-filename": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-safe-filename/-/is-safe-filename-0.1.1.tgz", + "integrity": "sha512-4SrR7AdnY11LHfDKTZY1u6Ga3RuxZdl3YKWWShO5iyuG5h8QS4GD2tOb04peBJ5I7pXbR+CGBNEhTcwK+FzN3g==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/open": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/open/-/open-11.0.0.tgz", + "integrity": "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.4.0", + "define-lazy-prop": "^3.0.0", + "is-in-ssh": "^1.0.0", + "is-inside-container": "^1.0.0", + "powershell-utils": "^0.1.0", + "wsl-utils": "^0.3.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openai": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.26.0.tgz", + "integrity": "sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/partial-json": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz", + "integrity": "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==", + "license": "MIT" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/powershell-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", + "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/protobufjs": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.4.tgz", + "integrity": "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz", + "integrity": "sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.23.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typebox": { + "version": "1.1.38", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.38.tgz", + "integrity": "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.8.tgz", + "integrity": "sha512-uPAwSr57kYjAUux+8E2j0q0Fxpn8M9VoyfGiRI8Kfktz9NcYMCenwY5RnZxnF1WTu3TGiYipirIzacLL3VVGFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.8.tgz", + "integrity": "sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.8", + "@vitest/mocker": "2.1.8", + "@vitest/pretty-format": "^2.1.8", + "@vitest/runner": "2.1.8", + "@vitest/snapshot": "2.1.8", + "@vitest/spy": "2.1.8", + "@vitest/utils": "2.1.8", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.8", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.8", + "@vitest/ui": "2.1.8", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/wsl-utils": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.1.tgz", + "integrity": "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0", + "powershell-utils": "^0.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.67", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", + "integrity": "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/src/providers/custom-providers.ts b/src/providers/custom-providers.ts index 0e81d50..092a688 100644 --- a/src/providers/custom-providers.ts +++ b/src/providers/custom-providers.ts @@ -6,10 +6,13 @@ // correctly at fusion time. // // Both custom providers are discoverable — they support the /v1/models endpoint -// so the UI can fetch the actual available models at runtime. No hardcoded +// so the server can fetch the actual available models at runtime. No hardcoded // model lists: rapid-mlx's models depend on what's loaded locally, and // ollama-cloud's catalog changes as Ollama adds new cloud models. // +// The `local` flag distinguishes local servers (may be unreachable, show +// free-text input) from cloud providers (always reachable, show dropdown). +// // At runtime, registerCustomProviders() registers static model descriptors with // the pi-ai bridge so resolveModel() works. For discovered or user-typed models, // registerCustomModel() is called on the fly. @@ -31,10 +34,17 @@ export interface CustomProviderDefinition { api: "openai-completions" | "openai-responses"; /** * Whether this provider supports /v1/models discovery. - * When true, the UI will offer a "Discover models" button that fetches the - * model list at runtime; the user can also type a model ID directly. + * When true, the /models API endpoint will query the provider's /v1/models + * for a live model list and return it as a normal dropdown. */ discoverable: boolean; + /** + * Whether this is a local provider that may be unreachable. + * When true + discoverable, the UI shows a free-text input for model IDs + * if the server is down (no models found), plus a Discover button to retry. + * Cloud providers (local=false) always show a normal dropdown. + */ + local: boolean; /** Compat overrides for the OpenAI completions API (auto-detected if not set). */ compat?: Record; } @@ -52,6 +62,7 @@ export const RAPID_MLX: CustomProviderDefinition = { baseUrl: "http://localhost:8000/v1", api: "openai-completions", discoverable: true, + local: true, compat: { supportsStore: false, supportsDeveloperRole: false, @@ -68,11 +79,12 @@ export const OLLAMA_CLOUD: CustomProviderDefinition = { name: "Ollama Cloud", description: "Ollama's hosted cloud API at ollama.com. Requires an API key. " + - "Click Discover to load available cloud models, or type a model ID directly.", + "Models are fetched from the cloud catalog automatically.", apiKeyRequired: true, baseUrl: "https://ollama.com/v1", api: "openai-completions", discoverable: true, + local: false, compat: { supportsStore: false, supportsDeveloperRole: false, diff --git a/src/server/api/providers.ts b/src/server/api/providers.ts index 0b995d1..348ac96 100644 --- a/src/server/api/providers.ts +++ b/src/server/api/providers.ts @@ -13,7 +13,7 @@ export function providersRouter(): Router { // GET /api/providers — list all providers with metadata. r.get("/", (_req, res) => { const providers = listProviders(); - // Enrich with metadata from custom provider definitions (name, description, keyless, discoverable). + // Enrich with metadata from custom provider definitions. const enriched = providers.map((id) => { const custom = CUSTOM_PROVIDERS[id]; return { @@ -22,20 +22,40 @@ export function providersRouter(): Router { description: custom?.description ?? undefined, keyless: KEYLESS_PROVIDERS.has(id), discoverable: custom?.discoverable ?? false, + local: custom?.local ?? false, }; }); res.json({ providers: enriched }); }); - // GET /api/providers/:provider/models — list models for a built-in provider. - // Custom providers use /discover instead (their models aren't static). - r.get("/:provider/models", (req, res) => { + // GET /api/providers/:provider/models — list models for any provider. + // For built-in providers: returns the static registry. + // For discoverable custom providers (both local and cloud): attempts live + // discovery from the provider's /v1/models endpoint. If the provider is + // unreachable (e.g. local server down), returns an empty list — the UI + // will show a free-text input for local providers or an error for cloud providers. + r.get("/:provider/models", async (req, res) => { const provider = req.params.provider; - // For custom discoverable providers, return empty — use /discover. - if (CUSTOM_PROVIDERS[provider]?.discoverable) { - res.json({ models: [] }); + const customDef = CUSTOM_PROVIDERS[provider]; + + if (customDef?.discoverable) { + // Attempt live discovery from the provider's /v1/models endpoint. + const apiKey = effectiveApiKey(provider, getKey(provider)); + try { + const modelIds = await discoverModels(customDef, apiKey === "no-key" ? undefined : apiKey); + // Register discovered models so resolveModel() works at fusion time. + for (const id of modelIds) { + registerCustomModel(provider, id); + } + res.json({ models: modelIds.map((id) => ({ id })) }); + } catch { + // Provider unreachable or auth failed — return empty list. + res.json({ models: [] }); + } return; } + + // Built-in pi-ai provider — delegate to the static registry. try { res.json({ models: listModels(provider) }); } catch (e) { @@ -45,21 +65,19 @@ export function providersRouter(): Router { } }); - // GET /api/providers/:provider/discover — dynamically discover models from a - // custom provider's /v1/models endpoint. Also registers discovered models with - // the pi-ai bridge so resolveModel() works at fusion time. + // GET /api/providers/:provider/discover — explicit discover endpoint. + // Only for local providers that may need a manual retry (e.g. after starting + // a local server). Cloud providers don't need this — they're always reachable. r.get("/:provider/discover", async (req, res) => { const provider = req.params.provider; const def = CUSTOM_PROVIDERS[provider]; - if (!def || !def.discoverable) { + if (!def || !def.discoverable || !def.local) { res.status(404).json({ error: `Provider '${provider}' does not support model discovery.` }); return; } - // Resolve an API key: for keyless providers use the sentinel, otherwise look up stored key. const apiKey = effectiveApiKey(provider, getKey(provider)); try { const modelIds = await discoverModels(def, apiKey === "no-key" ? undefined : apiKey); - // Register each discovered model with the bridge so resolveModel() works. for (const id of modelIds) { registerCustomModel(provider, id); } diff --git a/tests/custom-providers.test.ts b/tests/custom-providers.test.ts index 9cb75d9..1f1c730 100644 --- a/tests/custom-providers.test.ts +++ b/tests/custom-providers.test.ts @@ -127,6 +127,11 @@ describe("custom providers: definition shape", () => { expect(OLLAMA_CLOUD.discoverable).toBe(true); }); + it("rapid-mlx is local, ollama-cloud is not", () => { + expect(RAPID_MLX.local).toBe(true); + expect(OLLAMA_CLOUD.local).toBe(false); + }); + it("rapid-mlx is keyless", () => { expect(RAPID_MLX.apiKeyRequired).toBe(false); }); diff --git a/ui/src/api.ts b/ui/src/api.ts index d4a4d2c..9163c90 100644 --- a/ui/src/api.ts +++ b/ui/src/api.ts @@ -46,6 +46,8 @@ export interface ProviderInfo { keyless: boolean; /** Whether this provider supports /v1/models discovery. */ discoverable: boolean; + /** Whether this is a local provider that may be unreachable. */ + local: boolean; } export interface TestResult { ok: boolean; diff --git a/ui/src/pages/Candidates.tsx b/ui/src/pages/Candidates.tsx index d5d78d7..4bba579 100644 --- a/ui/src/pages/Candidates.tsx +++ b/ui/src/pages/Candidates.tsx @@ -47,10 +47,21 @@ export function CandidatesPage({ void api.getProviders().then((r) => setProviderList(r.providers)); }, []); + // Eagerly load models for all providers referenced in the current config. + // This ensures saved model names appear immediately, not as "Focus to load…". + useEffect(() => { + if (!config) return; + const providers = new Set(); + for (const c of config.candidates) providers.add(c.provider); + for (const j of config.judges) providers.add(j.provider); + for (const p of providers) { + void loadModels(p); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [config]); + const loadModels = async (provider: string) => { if (modelsByProvider[provider]) return; - const pInfo = providerMap.get(provider); - if (pInfo?.discoverable) return; // Discoverable providers use the Discover button. try { const r = await api.getModels(provider); setModelsByProvider((m) => ({ ...m, [provider]: r.models })); @@ -73,10 +84,7 @@ export function CandidatesPage({ const update = (id: string, patch: Partial) => { setCandidates((cs) => cs.map((c) => (c.id === id ? { ...c, ...patch } : c))); - if (patch.provider) { - const pInfo = providerMap.get(patch.provider); - if (!pInfo?.discoverable) void loadModels(patch.provider); - } + if (patch.provider) void loadModels(patch.provider); }; const add = () => { // In benchmark mode there's no max; otherwise cap at 5. @@ -186,9 +194,19 @@ export function CandidatesPage({
{candidates.map((c, i) => { const pInfo = providerMap.get(c.provider); - const isDiscoverable = pInfo?.discoverable ?? false; - const staticModels = modelsByProvider[c.provider] ?? []; + const isLocal = pInfo?.local ?? false; + const models = modelsByProvider[c.provider] ?? []; + const isLoading = loadingProvider === c.provider; const discovered = discoveredByProvider[c.provider] ?? []; + // For local discoverable providers, merge discovered models into the list. + const allModels = isLocal && discovered.length > 0 + ? [...new Map([...discovered.map((id) => [id, { id }]), ...models.map((m) => [m.id, m])]).values()] + : models; + // If the saved model isn't in the list yet, add it as an option so it's visible. + const savedModelOption = c.model && !allModels.some((m) => m.id === c.model) + ? [{ id: c.model, contextWindow: undefined }] + : []; + const displayModels = [...savedModelOption, ...allModels]; return (
{i + 1} @@ -210,8 +228,8 @@ export function CandidatesPage({ ))} - {isDiscoverable ? ( - /* Discoverable providers: show discovered models in a datalist + free-text input */ + {isLocal ? ( + /* Local discoverable providers: free-text input + Discover button */
) : ( - /* Built-in providers: static model dropdown */ + /* Built-in and cloud providers: dropdown with saved model always visible */ diff --git a/ui/src/pages/Judge.tsx b/ui/src/pages/Judge.tsx index c2b9449..78eca93 100644 --- a/ui/src/pages/Judge.tsx +++ b/ui/src/pages/Judge.tsx @@ -22,10 +22,20 @@ export function JudgePage({ config, onChanged }: { config: AppConfig | null; onC void api.getProviders().then((r) => setProviderList(r.providers)); }, []); + // Eagerly load models for all providers referenced in the current config. + // This ensures saved model names appear immediately, not as "Focus to load…". + useEffect(() => { + if (!config) return; + const providers = new Set(); + for (const j of config.judges) providers.add(j.provider); + for (const p of providers) { + void loadModels(p); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [config]); + const loadModels = async (provider: string) => { if (modelsByProvider[provider]) return; - const pInfo = providerMap.get(provider); - if (pInfo?.discoverable) return; try { const r = await api.getModels(provider); setModelsByProvider((m) => ({ ...m, [provider]: r.models })); @@ -52,10 +62,7 @@ export function JudgePage({ config, onChanged }: { config: AppConfig | null; onC }; const update = (idx: number, patch: Partial) => { setJudges((js) => js.map((j, i) => (i === idx ? { ...j, ...patch } : j))); - if (patch.provider) { - const pInfo = providerMap.get(patch.provider); - if (!pInfo?.discoverable) void loadModels(patch.provider); - } + if (patch.provider) void loadModels(patch.provider); }; const add = () => { setJudges((js) => [...js, { provider: providers[0] ?? "", model: "", enabled: false }]); @@ -113,9 +120,19 @@ export function JudgePage({ config, onChanged }: { config: AppConfig | null; onC
{judges.map((j, i) => { const pInfo = providerMap.get(j.provider); - const isDiscoverable = pInfo?.discoverable ?? false; - const staticModels = modelsByProvider[j.provider] ?? []; + const isLocal = pInfo?.local ?? false; + const models = modelsByProvider[j.provider] ?? []; + const isLoading = loadingProvider === j.provider; const discovered = discoveredByProvider[j.provider] ?? []; + // For local discoverable providers, merge discovered models into the list. + const allModels = isLocal && discovered.length > 0 + ? [...new Map([...discovered.map((id) => [id, { id }]), ...models.map((m) => [m.id, m])]).values()] + : models; + // If the saved model isn't in the list yet, add it as an option so it's visible. + const savedModelOption = j.model && !allModels.some((m) => m.id === j.model) + ? [{ id: j.model, contextWindow: undefined }] + : []; + const displayModels = [...savedModelOption, ...allModels]; return (
))} - {isDiscoverable ? ( + {isLocal ? ( + /* Local discoverable providers: free-text input + Discover button */
) : ( + /* Built-in and cloud providers: dropdown with saved model always visible */ From 80de8b6e3b48ad802c105f8e2dc5f837bcac6aed Mon Sep 17 00:00:00 2001 From: "Clifford B. Brown" <33701978+cbrown350@users.noreply.github.com> Date: Mon, 22 Jun 2026 17:56:44 -0600 Subject: [PATCH 06/11] =?UTF-8?q?fix:=20TS=20build=20errors=20=E2=80=94=20?= =?UTF-8?q?add=20loadingProvider=20state,=20fix=20model=20merge=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add loadingProvider state to both Candidates and Judge pages - Replace broken Map constructor with typed mergeModelLists helper - Fix ProviderModel typing for saved model options and contextWindow - All 22 tests pass, build clean --- ui/src/pages/Candidates.tsx | 32 ++++++++++++++++++++++++-------- ui/src/pages/Judge.tsx | 32 ++++++++++++++++++++++++-------- 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/ui/src/pages/Candidates.tsx b/ui/src/pages/Candidates.tsx index 4bba579..5fff0c4 100644 --- a/ui/src/pages/Candidates.tsx +++ b/ui/src/pages/Candidates.tsx @@ -1,6 +1,19 @@ import { useEffect, useState } from "react"; import { api, type AppConfig, type CandidateSlot, type ProviderInfo, type ProviderModel } from "../api"; +/** Merge two model lists, deduplicating by id. Keeps the entry from `b` on conflict. */ +function mergeModelLists(a: ProviderModel[], b: ProviderModel[]): ProviderModel[] { + const seen = new Set(); + const result: ProviderModel[] = []; + for (const m of a) { + if (!seen.has(m.id)) { seen.add(m.id); result.push(m); } + } + for (const m of b) { + if (!seen.has(m.id)) { seen.add(m.id); result.push(m); } + } + return result; +} + /** * Serial time budget in minutes (feature 007). Mirrors the engine constants in * src/fusion/fanout.ts (PER_CANDIDATE_MS=180_000, JUDGE_STEPS_MS=360_000). TS constants @@ -25,7 +38,8 @@ export function CandidatesPage({ const [sequential, setSequential] = useState(false); const [providerList, setProviderList] = useState([]); const [modelsByProvider, setModelsByProvider] = useState>({}); - /** Discovered model IDs for custom/discoverable providers (keyed by provider). */ + const [loadingProvider, setLoadingProvider] = useState(null); + /** Discovered model IDs for local providers (keyed by provider). */ const [discoveredByProvider, setDiscoveredByProvider] = useState>({}); const [discovering, setDiscovering] = useState(null); const [saving, setSaving] = useState(false); @@ -62,11 +76,14 @@ export function CandidatesPage({ const loadModels = async (provider: string) => { if (modelsByProvider[provider]) return; + setLoadingProvider(provider); try { const r = await api.getModels(provider); setModelsByProvider((m) => ({ ...m, [provider]: r.models })); } catch { /* ignore */ + } finally { + setLoadingProvider(null); } }; @@ -199,14 +216,13 @@ export function CandidatesPage({ const isLoading = loadingProvider === c.provider; const discovered = discoveredByProvider[c.provider] ?? []; // For local discoverable providers, merge discovered models into the list. - const allModels = isLocal && discovered.length > 0 - ? [...new Map([...discovered.map((id) => [id, { id }]), ...models.map((m) => [m.id, m])]).values()] + const allModels: ProviderModel[] = isLocal && discovered.length > 0 + ? mergeModelLists(discovered.map((id) => ({ id })), models) : models; // If the saved model isn't in the list yet, add it as an option so it's visible. - const savedModelOption = c.model && !allModels.some((m) => m.id === c.model) - ? [{ id: c.model, contextWindow: undefined }] - : []; - const displayModels = [...savedModelOption, ...allModels]; + const displayModels = c.model && !allModels.some((m) => m.id === c.model) + ? [{ id: c.model }, ...allModels] + : allModels; return (
{i + 1} @@ -265,7 +281,7 @@ export function CandidatesPage({ {displayModels.map((m) => ( ))} diff --git a/ui/src/pages/Judge.tsx b/ui/src/pages/Judge.tsx index 78eca93..2e1c9f9 100644 --- a/ui/src/pages/Judge.tsx +++ b/ui/src/pages/Judge.tsx @@ -1,11 +1,25 @@ import { useEffect, useState } from "react"; import { api, type AppConfig, type JudgeConfig, type ProviderInfo, type ProviderModel } from "../api"; +/** Merge two model lists, deduplicating by id. Keeps the entry from `b` on conflict. */ +function mergeModelLists(a: ProviderModel[], b: ProviderModel[]): ProviderModel[] { + const seen = new Set(); + const result: ProviderModel[] = []; + for (const m of a) { + if (!seen.has(m.id)) { seen.add(m.id); result.push(m); } + } + for (const m of b) { + if (!seen.has(m.id)) { seen.add(m.id); result.push(m); } + } + return result; +} + export function JudgePage({ config, onChanged }: { config: AppConfig | null; onChanged: () => void }) { const [judges, setJudges] = useState([]); const [providerList, setProviderList] = useState([]); const [modelsByProvider, setModelsByProvider] = useState>({}); - /** Discovered model IDs for custom/discoverable providers (keyed by provider). */ + const [loadingProvider, setLoadingProvider] = useState(null); + /** Discovered model IDs for local providers (keyed by provider). */ const [discoveredByProvider, setDiscoveredByProvider] = useState>({}); const [discovering, setDiscovering] = useState(null); const [saving, setSaving] = useState(false); @@ -36,11 +50,14 @@ export function JudgePage({ config, onChanged }: { config: AppConfig | null; onC const loadModels = async (provider: string) => { if (modelsByProvider[provider]) return; + setLoadingProvider(provider); try { const r = await api.getModels(provider); setModelsByProvider((m) => ({ ...m, [provider]: r.models })); } catch { /* ignore */ + } finally { + setLoadingProvider(null); } }; @@ -125,14 +142,13 @@ export function JudgePage({ config, onChanged }: { config: AppConfig | null; onC const isLoading = loadingProvider === j.provider; const discovered = discoveredByProvider[j.provider] ?? []; // For local discoverable providers, merge discovered models into the list. - const allModels = isLocal && discovered.length > 0 - ? [...new Map([...discovered.map((id) => [id, { id }]), ...models.map((m) => [m.id, m])]).values()] + const allModels: ProviderModel[] = isLocal && discovered.length > 0 + ? mergeModelLists(discovered.map((id) => ({ id })), models) : models; // If the saved model isn't in the list yet, add it as an option so it's visible. - const savedModelOption = j.model && !allModels.some((m) => m.id === j.model) - ? [{ id: j.model, contextWindow: undefined }] - : []; - const displayModels = [...savedModelOption, ...allModels]; + const displayModels = j.model && !allModels.some((m) => m.id === j.model) + ? [{ id: j.model }, ...allModels] + : allModels; return (
( ))} From de2df7fa4a74f7f4a3b946e61bc9141122e54844 Mon Sep 17 00:00:00 2001 From: "Clifford B. Brown" <33701978+cbrown350@users.noreply.github.com> Date: Mon, 22 Jun 2026 19:16:28 -0600 Subject: [PATCH 07/11] fix: register custom provider models from config at startup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Custom providers (rapid-mlx, ollama-cloud) failed at fusion time because resolveModel() couldn't find them — models were only registered when the UI called /api/providers/:provider/models, but a fresh server start had no registrations. - Add registerConfigModels() to pi-ai-bridge: reads candidates + judges from the config and calls registerCustomModel() for each custom provider entry, so resolveModel() works without any prior UI call - Call registerConfigModels() at startup in index.ts and ui-only.ts - Call registerConfigModels() after every config save (PUT /api/config) so newly-added models are immediately resolvable - Add 3 tests covering: model resolution, empty model skip, built-in skip --- src/index.ts | 12 ++++++- src/providers/pi-ai-bridge.ts | 17 ++++++++++ src/server/api/config.ts | 4 +++ src/ui-only.ts | 12 ++++++- tests/custom-providers.test.ts | 60 ++++++++++++++++++++++++++++++++++ 5 files changed, 103 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index c44f3ad..737d521 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,13 +7,23 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" import { createMcpServer } from "./server/mcp-server.js"; import { startUiServer } from "./server/ui-server.js"; import { printStartupBanner } from "./util/startup.js"; -import { registerCustomProviders } from "./providers/pi-ai-bridge.js"; +import { registerCustomProviders, registerConfigModels } from "./providers/pi-ai-bridge.js"; +import { loadConfig } from "./config/store.js"; async function main(): Promise { // Register custom providers (rapid-mlx, ollama-cloud) so they appear in the UI // and resolve correctly at fusion time. registerCustomProviders(); + // Register any custom provider models from the saved config so resolveModel() + // works at fusion time without requiring a prior UI call to /models. + try { + const cfg = loadConfig(); + registerConfigModels(cfg); + } catch { + // Config may not exist yet (first run) — that's fine. + } + // First-run banner (stderr) + auto-open the dashboard on a fresh install. await printStartupBanner(); diff --git a/src/providers/pi-ai-bridge.ts b/src/providers/pi-ai-bridge.ts index a541a30..9ac976c 100644 --- a/src/providers/pi-ai-bridge.ts +++ b/src/providers/pi-ai-bridge.ts @@ -193,4 +193,21 @@ export function registerCustomProviders(): void { // No static models to register — they're discovered at runtime. // This function is kept as a no-op placeholder for future static registrations // and to maintain the startup call site in index.ts / ui-only.ts. +} + +/** + * Register all custom provider models referenced in a config (candidates + judges). + * This must be called at startup after loading the config so that resolveModel() + * works for custom providers like rapid-mlx and ollama-cloud. Without this, a + * fusion request fails because the models were never registered — they only get + * registered when the UI calls /api/providers/:provider/models, which may not + * have happened yet. + */ +export function registerConfigModels(config: { candidates: Array<{ provider: string; model: string }>; judges: Array<{ provider: string; model: string }> }): void { + const entries = [...config.candidates, ...config.judges]; + for (const { provider, model } of entries) { + if (CUSTOM_PROVIDERS[provider] && model) { + registerCustomModel(provider, model); + } + } } \ No newline at end of file diff --git a/src/server/api/config.ts b/src/server/api/config.ts index cccb7d3..9ab4ace 100644 --- a/src/server/api/config.ts +++ b/src/server/api/config.ts @@ -2,6 +2,7 @@ import { Router } from "express"; import { loadConfig, saveConfig, mergeAndValidate, emptyConfig } from "../../config/store.js"; import { isConfigured } from "../../config/completeness.js"; +import { registerConfigModels } from "../../providers/pi-ai-bridge.js"; export function configRouter(): Router { const r = Router(); @@ -21,6 +22,9 @@ export function configRouter(): Router { const merged = mergeAndValidate(loadConfig(), req.body); saveConfig(merged); const config = loadConfig(); + // Re-register custom provider models so resolveModel() works for the + // newly-saved providers without requiring a page reload or /models call. + registerConfigModels(config); res.json({ ...config, configured: isConfigured(config).configured }); }); diff --git a/src/ui-only.ts b/src/ui-only.ts index 407c7c2..5be8382 100644 --- a/src/ui-only.ts +++ b/src/ui-only.ts @@ -4,10 +4,20 @@ // Shares the same on-disk SQLite + config + secrets as the MCP server. import { startUiServer } from "./server/ui-server.js"; import { printStartupBanner } from "./util/startup.js"; -import { registerCustomProviders } from "./providers/pi-ai-bridge.js"; +import { registerCustomProviders, registerConfigModels } from "./providers/pi-ai-bridge.js"; +import { loadConfig } from "./config/store.js"; async function main(): Promise { registerCustomProviders(); + + // Register any custom provider models from the saved config so resolveModel() + // works at fusion time without requiring a prior UI call to /models. + try { + const cfg = loadConfig(); + registerConfigModels(cfg); + } catch { + // Config may not exist yet (first run) — that's fine. + } await printStartupBanner(); await startUiServer(); console.error("OpenFusion dashboard running. Press Ctrl+C to stop."); diff --git a/tests/custom-providers.test.ts b/tests/custom-providers.test.ts index 1f1c730..015d5bb 100644 --- a/tests/custom-providers.test.ts +++ b/tests/custom-providers.test.ts @@ -7,6 +7,7 @@ import { registerCustomProviders, clearModelDescriptors, registerCustomModel, + registerConfigModels, effectiveApiKey, } from "../src/providers/pi-ai-bridge.js"; import { @@ -159,4 +160,63 @@ describe("custom providers: completeness gate", () => { // rapid-mlx is keyless, so only openai needs a key. expect(needsKey).toEqual(["openai"]); }); +}); + +describe("custom providers: registerConfigModels", () => { + // registerConfigModels is imported with registerCustomProviders above. + // It must be called AFTER registerCustomProviders (which is done in beforeEach). + + it("registers custom provider models from config so resolveModel works", () => { + registerConfigModels({ + candidates: [ + { id: "c1", provider: "rapid-mlx", model: "mlx-community/Qwen3-35B-A3B-OptiQ-4bit", enabled: true }, + { id: "c2", provider: "ollama-cloud", model: "gpt-oss:120b-cloud", enabled: true }, + { id: "c3", provider: "openai", model: "gpt-4o", enabled: true }, + ], + judges: [ + { provider: "rapid-mlx", model: "mlx-community/Qwen3-35B-A3B-OptiQ-4bit", enabled: true }, + ], + }); + // rapid-mlx model should now resolve. + const rapidModel = resolveModel("rapid-mlx", "mlx-community/Qwen3-35B-A3B-OptiQ-4bit"); + expect(rapidModel.provider).toBe("rapid-mlx"); + expect(rapidModel.baseUrl).toBe("http://localhost:8000/v1"); + + // ollama-cloud model should now resolve. + const ollamaModel = resolveModel("ollama-cloud", "gpt-oss:120b-cloud"); + expect(ollamaModel.provider).toBe("ollama-cloud"); + expect(ollamaModel.baseUrl).toBe("https://ollama.com/v1"); + + // Built-in provider model should still work (not affected by registerConfigModels). + const openaiModel = resolveModel("openai", "gpt-4o"); + expect(openaiModel.provider).toBe("openai"); + }); + + it("skips entries with empty model strings", () => { + clearModelDescriptors(); + registerCustomProviders(); + registerConfigModels({ + candidates: [ + { id: "c1", provider: "rapid-mlx", model: "", enabled: true }, + ], + judges: [], + }); + // Should not throw; empty model is simply skipped. + expect(() => resolveModel("rapid-mlx", "")).toThrow(); + }); + + it("skips providers that are not custom", () => { + clearModelDescriptors(); + registerCustomProviders(); + // openai is a built-in provider, not a custom one — registerConfigModels should skip it. + registerConfigModels({ + candidates: [ + { id: "c1", provider: "openai", model: "gpt-4o", enabled: true }, + ], + judges: [], + }); + // gpt-4o resolves via pi-ai's built-in registry, not via modelOverrides. + const model = resolveModel("openai", "gpt-4o"); + expect(model.provider).toBe("openai"); + }); }); \ No newline at end of file From 8f04d2cf5df2e5488fcebde5eb1f57740c426db5 Mon Sep 17 00:00:00 2001 From: "Clifford B. Brown" <33701978+cbrown350@users.noreply.github.com> Date: Mon, 22 Jun 2026 22:53:35 -0600 Subject: [PATCH 08/11] fix: harden custom-provider discovery + UI per pre-PR review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses the pre-PR review of the custom-providers feature. No engine (fusion) changes; only the provider bridge, API routes, UI, and tests. - server/api/providers.ts: surface discovery failures as {models:[],error} instead of silently returning [] (Constitution V — no silent failures); route discovery auth through a new discoveryApiKey() helper using KEYLESS_PROVIDERS, dropping the cross-module `=== "no-key"` sentinel comparison. - ui/pages/Candidates + Judge: load models lazily on focus, not eagerly on every mount (the eager fetch hung the UI up to 10s per unreachable local provider on each page visit); don't cache an empty list on error so a retry can fire; restore the "Focus to load…" placeholder; surface the discovery error via setMsg. - providers/pi-ai-bridge: remove the registerCustomProviders() no-op (YAGNI / Constitution VII); document that the keyless sentinel is module-private and callers must treat effectiveApiKey()'s value as opaque. - index + ui-only: drop the silent startup try/catch — loadConfig() already returns emptyConfig for a missing file, so a corrupt config.json now fails loudly instead of being swallowed. - Extract mergeModelLists to ui/src/lib/models.ts (was duplicated in Candidates + Judge). - config/completeness: document the keyless-provider exemption of the Constitution VI key check; add isConfigured tests covering keyless-pass, keyed-fail, and keyed-with-key. - providers/custom-providers: document the hardcoded contextWindow/ maxTokens/cost defaults, and that the feature covers BOTH a local server (rapid-mlx) AND a cloud provider (ollama-cloud). Verified: pnpm typecheck, pnpm build, pnpm test (199 passed, incl. 28 custom-provider tests) all green. --- src/config/completeness.ts | 9 ++- src/index.ts | 20 +++---- src/providers/custom-providers.ts | 28 +++++++--- src/providers/pi-ai-bridge.ts | 28 +++++----- src/server/api/providers.ts | 40 +++++++++----- src/ui-only.ts | 16 ++---- tests/custom-providers.test.ts | 91 ++++++++++++++++++++++++++++--- ui/src/api.ts | 5 +- ui/src/lib/models.ts | 26 +++++++++ ui/src/pages/Candidates.tsx | 52 ++++++++---------- ui/src/pages/Judge.tsx | 49 +++++++---------- 11 files changed, 234 insertions(+), 130 deletions(-) create mode 100644 ui/src/lib/models.ts diff --git a/src/config/completeness.ts b/src/config/completeness.ts index 49ccc36..aa0e66d 100644 --- a/src/config/completeness.ts +++ b/src/config/completeness.ts @@ -1,6 +1,13 @@ // The configuration gate (Constitution VI). // isConfigured() = >=2 ENABLED candidates (<=5 unless benchmarkMode) + -// >=1 ENABLED judge + a key for every referenced provider. +// >=1 ENABLED judge + a key for every referenced provider that needs one. +// +// Keyless providers (e.g. the local rapid-mlx server) are EXEMPT from the +// "key for every referenced provider" clause: they run without auth, so +// requiring a stored key would block a valid local-only setup. This does not +// weaken the gate — keyed providers (e.g. ollama-cloud) are still required to +// have a stored key, and the >=2 candidates / >=1 judge rules are untouched. +// See tests/custom-providers.test.ts "completeness gate with keyless providers". import type { RawConfig } from "./schema.js"; import { referencedProviders, loadSecrets } from "./secrets.js"; import { paths } from "../util/paths.js"; diff --git a/src/index.ts b/src/index.ts index 737d521..a21d265 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,22 +7,16 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" import { createMcpServer } from "./server/mcp-server.js"; import { startUiServer } from "./server/ui-server.js"; import { printStartupBanner } from "./util/startup.js"; -import { registerCustomProviders, registerConfigModels } from "./providers/pi-ai-bridge.js"; +import { registerConfigModels } from "./providers/pi-ai-bridge.js"; import { loadConfig } from "./config/store.js"; async function main(): Promise { - // Register custom providers (rapid-mlx, ollama-cloud) so they appear in the UI - // and resolve correctly at fusion time. - registerCustomProviders(); - - // Register any custom provider models from the saved config so resolveModel() - // works at fusion time without requiring a prior UI call to /models. - try { - const cfg = loadConfig(); - registerConfigModels(cfg); - } catch { - // Config may not exist yet (first run) — that's fine. - } + // Register any custom provider models referenced by the saved config so + // resolveModel() works at fusion time without requiring a prior UI /models + // call. loadConfig() returns an empty config (no throw) when the file is + // absent (first run), so a genuinely corrupt config.json fails loudly here + // rather than being silently swallowed. + registerConfigModels(loadConfig()); // First-run banner (stderr) + auto-open the dashboard on a fresh install. await printStartupBanner(); diff --git a/src/providers/custom-providers.ts b/src/providers/custom-providers.ts index 092a688..6972713 100644 --- a/src/providers/custom-providers.ts +++ b/src/providers/custom-providers.ts @@ -1,20 +1,30 @@ // Custom provider definitions for OpenFusion. // -// pi-ai's static registry covers cloud providers, but local/OpenAI-compatible -// endpoints (like rapid-mlx and ollama-cloud) aren't in that registry. This -// module defines them so they appear in the web config dropdowns and resolve -// correctly at fusion time. +// pi-ai's static registry covers the well-known cloud providers, but two +// OpenAI-compatible endpoints aren't in it: rapid-mlx (a LOCAL MLX inference +// server on Apple Silicon) and ollama-cloud (Ollama's hosted CLOUD API). This +// module defines both so they appear in the web config dropdowns and resolve +// correctly at fusion time. Despite the branch name ("local-providers"), this +// feature intentionally covers BOTH a local server and a cloud provider. // -// Both custom providers are discoverable — they support the /v1/models endpoint +// Both custom providers are discoverable — they expose a /v1/models endpoint // so the server can fetch the actual available models at runtime. No hardcoded // model lists: rapid-mlx's models depend on what's loaded locally, and // ollama-cloud's catalog changes as Ollama adds new cloud models. // -// The `local` flag distinguishes local servers (may be unreachable, show -// free-text input) from cloud providers (always reachable, show dropdown). +// The `local` flag distinguishes local servers (may be unreachable, so the UI +// shows a free-text input + a Discover button to retry) from cloud providers +// (always reachable, show a normal dropdown). // -// At runtime, registerCustomProviders() registers static model descriptors with -// the pi-ai bridge so resolveModel() works. For discovered or user-typed models, +// KNOWN LIMITATION: buildModelDescriptor() bakes in default contextWindow +// (131072) and maxTokens (8192) for every discovered/typed model, because the +// OpenAI /v1/models response doesn't carry those fields. Cost is reported as 0 +// for the same reason. If a provider under-reports, the dashboard's per-model +// context badge may be inaccurate; this does not affect fusion correctness. +// +// At runtime, registerConfigModels() (called at startup + after each config +// save) registers descriptors for models referenced in the saved config so +// resolveModel() works. For discovered or user-typed models, // registerCustomModel() is called on the fly. import type { AnyModel } from "./pi-ai-bridge.js"; diff --git a/src/providers/pi-ai-bridge.ts b/src/providers/pi-ai-bridge.ts index 9ac976c..b386ef3 100644 --- a/src/providers/pi-ai-bridge.ts +++ b/src/providers/pi-ai-bridge.ts @@ -83,10 +83,18 @@ export function listModels(provider: string) { } /** - * Resolve the effective API key for a provider. Keyless providers (e.g. rapid-mlx) - * that have no stored key get a sentinel value so pi-ai doesn't reject the call; - * all others pass through the stored key unchanged. + * Resolve the effective API key for a provider for the COMPLETION path. pi-ai's + * openai-completions provider throws on a falsy apiKey, so keyless providers + * (e.g. rapid-mlx) that have no stored key get a module-private sentinel — the + * local server ignores it. Everyone else passes through their stored key. * If a keyless provider has a stored key (user explicitly saved one), respect it. + * + * NOTE: this sentinel is intentionally NOT exported and is never compared + * against outside this module. Discovery (/v1/models) routes auth through + * KEYLESS_PROVIDERS directly (see server/api/providers.ts) and sends no + * Authorization header for keyless providers, so the two paths don't share the + * magic string. Callers MUST treat the returned value as opaque and must never + * compare it against literals or branch on its contents. */ export function effectiveApiKey(provider: string, storedKey: string | undefined): string { if (KEYLESS_PROVIDERS.has(provider) && !storedKey) return NO_KEY_SENTINEL; @@ -184,17 +192,6 @@ export class BridgeError extends Error { } } -/** - * Register custom provider base descriptors with pi-ai so listProviders() works. - * Custom models are registered dynamically via registerCustomModel() when the user - * selects a discovered model or types a model ID. Called once at startup. - */ -export function registerCustomProviders(): void { - // No static models to register — they're discovered at runtime. - // This function is kept as a no-op placeholder for future static registrations - // and to maintain the startup call site in index.ts / ui-only.ts. -} - /** * Register all custom provider models referenced in a config (candidates + judges). * This must be called at startup after loading the config so that resolveModel() @@ -202,6 +199,9 @@ export function registerCustomProviders(): void { * fusion request fails because the models were never registered — they only get * registered when the UI calls /api/providers/:provider/models, which may not * have happened yet. + * + * Note: listProviders() already returns custom provider ids via CUSTOM_PROVIDERS, + * so there is no separate "register providers" step — only models need registering. */ export function registerConfigModels(config: { candidates: Array<{ provider: string; model: string }>; judges: Array<{ provider: string; model: string }> }): void { const entries = [...config.candidates, ...config.judges]; diff --git a/src/server/api/providers.ts b/src/server/api/providers.ts index 348ac96..80f9b34 100644 --- a/src/server/api/providers.ts +++ b/src/server/api/providers.ts @@ -1,11 +1,22 @@ // GET /api/providers, GET /api/providers/:provider/models, and // GET /api/providers/:provider/discover — provider listing, model listing, -// and dynamic model discovery for custom providers. +// and dynamic model discovery for custom providers (local + cloud). import { Router } from "express"; import { listProviders, listModels, registerCustomModel } from "../../providers/pi-ai-bridge.js"; import { CUSTOM_PROVIDERS, KEYLESS_PROVIDERS, discoverModels } from "../../providers/custom-providers.js"; import { getKey } from "../../config/secrets.js"; -import { effectiveApiKey } from "../../providers/pi-ai-bridge.js"; + +/** + * The API key to send to a provider's /v1/models discovery endpoint, or + * undefined to send no Authorization header. Keyless providers (local servers + * like rapid-mlx) never need auth; everyone else sends their stored key (which + * may be undefined if the user hasn't saved one yet — discovery will then 401 + * and the caller surfaces the error). + */ +function discoveryApiKey(provider: string): string | undefined { + if (KEYLESS_PROVIDERS.has(provider)) return undefined; + return getKey(provider) || undefined; +} export function providersRouter(): Router { const r = Router(); @@ -31,26 +42,26 @@ export function providersRouter(): Router { // GET /api/providers/:provider/models — list models for any provider. // For built-in providers: returns the static registry. // For discoverable custom providers (both local and cloud): attempts live - // discovery from the provider's /v1/models endpoint. If the provider is - // unreachable (e.g. local server down), returns an empty list — the UI - // will show a free-text input for local providers or an error for cloud providers. + // discovery from the provider's /v1/models endpoint. On failure (server + // unreachable or auth rejected) returns { models: [], error } so the UI can + // tell "no models" apart from "couldn't reach the provider". r.get("/:provider/models", async (req, res) => { const provider = req.params.provider; const customDef = CUSTOM_PROVIDERS[provider]; if (customDef?.discoverable) { - // Attempt live discovery from the provider's /v1/models endpoint. - const apiKey = effectiveApiKey(provider, getKey(provider)); + const apiKey = discoveryApiKey(provider); try { - const modelIds = await discoverModels(customDef, apiKey === "no-key" ? undefined : apiKey); + const modelIds = await discoverModels(customDef, apiKey); // Register discovered models so resolveModel() works at fusion time. for (const id of modelIds) { registerCustomModel(provider, id); } res.json({ models: modelIds.map((id) => ({ id })) }); - } catch { - // Provider unreachable or auth failed — return empty list. - res.json({ models: [] }); + } catch (e) { + // Provider unreachable or auth failed — return empty list WITH the error + // so the dashboard can surface it (Constitution V: no silent failures). + res.json({ models: [], error: `Model discovery failed for '${provider}': ${(e as Error).message}` }); } return; } @@ -67,7 +78,8 @@ export function providersRouter(): Router { // GET /api/providers/:provider/discover — explicit discover endpoint. // Only for local providers that may need a manual retry (e.g. after starting - // a local server). Cloud providers don't need this — they're always reachable. + // a local server). Cloud providers don't need this — they're always reachable, + // and their /models route already does discovery. r.get("/:provider/discover", async (req, res) => { const provider = req.params.provider; const def = CUSTOM_PROVIDERS[provider]; @@ -75,9 +87,9 @@ export function providersRouter(): Router { res.status(404).json({ error: `Provider '${provider}' does not support model discovery.` }); return; } - const apiKey = effectiveApiKey(provider, getKey(provider)); + const apiKey = discoveryApiKey(provider); try { - const modelIds = await discoverModels(def, apiKey === "no-key" ? undefined : apiKey); + const modelIds = await discoverModels(def, apiKey); for (const id of modelIds) { registerCustomModel(provider, id); } diff --git a/src/ui-only.ts b/src/ui-only.ts index 5be8382..e2cc3b4 100644 --- a/src/ui-only.ts +++ b/src/ui-only.ts @@ -4,20 +4,14 @@ // Shares the same on-disk SQLite + config + secrets as the MCP server. import { startUiServer } from "./server/ui-server.js"; import { printStartupBanner } from "./util/startup.js"; -import { registerCustomProviders, registerConfigModels } from "./providers/pi-ai-bridge.js"; +import { registerConfigModels } from "./providers/pi-ai-bridge.js"; import { loadConfig } from "./config/store.js"; async function main(): Promise { - registerCustomProviders(); - - // Register any custom provider models from the saved config so resolveModel() - // works at fusion time without requiring a prior UI call to /models. - try { - const cfg = loadConfig(); - registerConfigModels(cfg); - } catch { - // Config may not exist yet (first run) — that's fine. - } + // Register any custom provider models referenced by the saved config so + // resolveModel() works. loadConfig() returns an empty config (no throw) when + // the file is absent (first run); a genuinely corrupt config.json fails loudly. + registerConfigModels(loadConfig()); await printStartupBanner(); await startUiServer(); console.error("OpenFusion dashboard running. Press Ctrl+C to stop."); diff --git a/tests/custom-providers.test.ts b/tests/custom-providers.test.ts index 015d5bb..9e3394c 100644 --- a/tests/custom-providers.test.ts +++ b/tests/custom-providers.test.ts @@ -4,7 +4,6 @@ import { listProviders, listModels, resolveModel, - registerCustomProviders, clearModelDescriptors, registerCustomModel, registerConfigModels, @@ -17,11 +16,25 @@ import { KEYLESS_PROVIDERS, buildModelDescriptor, } from "../src/providers/custom-providers.js"; +import { isConfigured } from "../src/config/completeness.js"; +import { setProviderKey } from "../src/config/secrets.js"; +import { RawConfigSchema, type RawConfig } from "../src/config/schema.js"; +import { rmSync } from "node:fs"; -// Ensure custom providers are registered before each test. +// A secrets/key path that does not exist — loadSecrets() returns empty +// (unconfigured) without throwing, so we can assert the gate's key logic in +// isolation. Distinct per process to avoid cross-test bleed. +const NO_SECRETS = `/tmp/openfusion-test-secrets-${process.pid}.enc`; +const NO_KEY = `/tmp/openfusion-test-master-${process.pid}.key`; + +/** Build a valid RawConfig from bare candidate/judge lists (settings defaulted). */ +function cfg(candidates: Array<{ id: string; provider: string; model: string; enabled?: boolean }>, judges: Array<{ provider: string; model: string; enabled?: boolean }>): RawConfig { + return RawConfigSchema.parse({ candidates, judges }); +} + +// Clear the model override registry before each test so registrations don't leak. beforeEach(() => { clearModelDescriptors(); - registerCustomProviders(); }); describe("custom providers: registration", () => { @@ -163,9 +176,6 @@ describe("custom providers: completeness gate", () => { }); describe("custom providers: registerConfigModels", () => { - // registerConfigModels is imported with registerCustomProviders above. - // It must be called AFTER registerCustomProviders (which is done in beforeEach). - it("registers custom provider models from config so resolveModel works", () => { registerConfigModels({ candidates: [ @@ -194,7 +204,6 @@ describe("custom providers: registerConfigModels", () => { it("skips entries with empty model strings", () => { clearModelDescriptors(); - registerCustomProviders(); registerConfigModels({ candidates: [ { id: "c1", provider: "rapid-mlx", model: "", enabled: true }, @@ -207,7 +216,6 @@ describe("custom providers: registerConfigModels", () => { it("skips providers that are not custom", () => { clearModelDescriptors(); - registerCustomProviders(); // openai is a built-in provider, not a custom one — registerConfigModels should skip it. registerConfigModels({ candidates: [ @@ -219,4 +227,71 @@ describe("custom providers: registerConfigModels", () => { const model = resolveModel("openai", "gpt-4o"); expect(model.provider).toBe("openai"); }); +}); + +describe("custom providers: completeness gate with keyless providers", () => { + // Constitution VI: a key is required for every referenced provider that needs + // one. Keyless providers (rapid-mlx) are exempt; keyed providers (ollama-cloud) + // are not. The >=2 candidates / >=1 judge rules are independent and untouched. + + it("is configured with only keyless providers referenced and no stored key", () => { + const report = isConfigured( + cfg( + [ + { id: "c1", provider: "rapid-mlx", model: "mlx-community/Qwen3-35B", enabled: true }, + { id: "c2", provider: "rapid-mlx", model: "mlx-community/Qwen3-8B", enabled: true }, + ], + [{ provider: "rapid-mlx", model: "mlx-community/Qwen3-35B", enabled: true }], + ), + NO_SECRETS, + NO_KEY, + ); + // No secrets file and no master key -> no stored keys, but rapid-mlx is + // keyless so the gate must NOT report a missing key. + expect(report.reasons).not.toContain("missing API key for provider(s): rapid-mlx"); + expect(report.configured).toBe(true); + }); + + it("is NOT configured when a keyed cloud provider (ollama-cloud) has no stored key", () => { + const report = isConfigured( + cfg( + [ + { id: "c1", provider: "ollama-cloud", model: "gpt-oss:120b", enabled: true }, + { id: "c2", provider: "ollama-cloud", model: "gpt-oss:20b", enabled: true }, + ], + [{ provider: "ollama-cloud", model: "gpt-oss:120b", enabled: true }], + ), + NO_SECRETS, + NO_KEY, + ); + // ollama-cloud is keyed and has no stored key -> gate must fail with a clear reason. + expect(report.configured).toBe(false); + expect(report.reasons.some((r) => r.includes("ollama-cloud"))).toBe(true); + }); + + it("is configured when a keyed cloud provider (ollama-cloud) HAS a stored key", () => { + // Real positive direction: actually persist a key for a keyed provider via + // the secrets store, then confirm the gate passes. Uses throwaway temp + // paths and cleans up so it doesn't touch the user's real secrets.enc. + const secrets = `/tmp/openfusion-test-secrets-keyed-${process.pid}.enc`; + const keyPath = `/tmp/openfusion-test-master-keyed-${process.pid}.key`; + try { + setProviderKey("ollama-cloud", "test-key-abc", secrets, keyPath); + const report = isConfigured( + cfg( + [ + { id: "c1", provider: "ollama-cloud", model: "gpt-oss:120b", enabled: true }, + { id: "c2", provider: "ollama-cloud", model: "gpt-oss:20b", enabled: true }, + ], + [{ provider: "ollama-cloud", model: "gpt-oss:120b", enabled: true }], + ), + secrets, + keyPath, + ); + expect(report.configured).toBe(true); + } finally { + rmSync(secrets, { force: true }); + rmSync(keyPath, { force: true }); + } + }); }); \ No newline at end of file diff --git a/ui/src/api.ts b/ui/src/api.ts index 9163c90..3898fe6 100644 --- a/ui/src/api.ts +++ b/ui/src/api.ts @@ -151,8 +151,11 @@ export const api = { putSecret: (provider: string, apiKey: string | null) => sendJSON("PUT", "/api/secrets", { provider, apiKey }), getProviders: () => getJSON<{ providers: ProviderInfo[] }>("/api/providers"), + // `error` is present when discovery was attempted but failed (provider + // unreachable or auth rejected) — lets the UI distinguish "no models" from + // "couldn't reach the provider" (Constitution V: no silent failures). getModels: (provider: string) => - getJSON<{ models: ProviderModel[] }>(`/api/providers/${encodeURIComponent(provider)}/models`), + getJSON<{ models: ProviderModel[]; error?: string }>(`/api/providers/${encodeURIComponent(provider)}/models`), /** Discover models from a custom provider's /v1/models endpoint. */ discoverModels: (provider: string) => getJSON<{ models: string[] }>(`/api/providers/${encodeURIComponent(provider)}/discover`), diff --git a/ui/src/lib/models.ts b/ui/src/lib/models.ts new file mode 100644 index 0000000..7679342 --- /dev/null +++ b/ui/src/lib/models.ts @@ -0,0 +1,26 @@ +// Shared UI helpers for provider model lists. +import type { ProviderModel } from "../api"; + +/** + * Merge two model lists, deduplicating by id. First occurrence wins (entries + * from `a` keep their richer shape — e.g. contextWindow — over a bare `{id}` + * from discovered lists). Used by Candidates + Judge to combine discovered + * models with the provider's loaded list. + */ +export function mergeModelLists(a: ProviderModel[], b: ProviderModel[]): ProviderModel[] { + const seen = new Set(); + const result: ProviderModel[] = []; + for (const m of a) { + if (!seen.has(m.id)) { + seen.add(m.id); + result.push(m); + } + } + for (const m of b) { + if (!seen.has(m.id)) { + seen.add(m.id); + result.push(m); + } + } + return result; +} \ No newline at end of file diff --git a/ui/src/pages/Candidates.tsx b/ui/src/pages/Candidates.tsx index 5fff0c4..c90cdb1 100644 --- a/ui/src/pages/Candidates.tsx +++ b/ui/src/pages/Candidates.tsx @@ -1,18 +1,6 @@ import { useEffect, useState } from "react"; import { api, type AppConfig, type CandidateSlot, type ProviderInfo, type ProviderModel } from "../api"; - -/** Merge two model lists, deduplicating by id. Keeps the entry from `b` on conflict. */ -function mergeModelLists(a: ProviderModel[], b: ProviderModel[]): ProviderModel[] { - const seen = new Set(); - const result: ProviderModel[] = []; - for (const m of a) { - if (!seen.has(m.id)) { seen.add(m.id); result.push(m); } - } - for (const m of b) { - if (!seen.has(m.id)) { seen.add(m.id); result.push(m); } - } - return result; -} +import { mergeModelLists } from "../lib/models.js"; /** * Serial time budget in minutes (feature 007). Mirrors the engine constants in @@ -61,27 +49,29 @@ export function CandidatesPage({ void api.getProviders().then((r) => setProviderList(r.providers)); }, []); - // Eagerly load models for all providers referenced in the current config. - // This ensures saved model names appear immediately, not as "Focus to load…". - useEffect(() => { - if (!config) return; - const providers = new Set(); - for (const c of config.candidates) providers.add(c.provider); - for (const j of config.judges) providers.add(j.provider); - for (const p of providers) { - void loadModels(p); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [config]); - + // Models are loaded lazily — on focus of the model dropdown (or when the + // provider changes). We do NOT eagerly fetch on mount: that would fire a + // live network call (with up to a 10s timeout) to every referenced custom + // provider on every page visit, hanging the UI when a local server is down. + // A saved model is always shown via the displayModels prepend below, so it + // doesn't need the full list to be loaded first. const loadModels = async (provider: string) => { - if (modelsByProvider[provider]) return; + if (modelsByProvider[provider] !== undefined) return; setLoadingProvider(provider); try { const r = await api.getModels(provider); + // On a discovery failure (auth rejected / server unreachable) the server + // returns { models: [], error }. Surface the error but DON'T cache the + // empty list — leaving modelsByProvider[provider] undefined lets a later + // focus retry, instead of permanently poisoning the cache (a [] is truthy + // and would short-circuit the guard above forever). + if (r.error) { + setMsg(r.error); + return; + } setModelsByProvider((m) => ({ ...m, [provider]: r.models })); - } catch { - /* ignore */ + } catch (e) { + setMsg(`Failed to load models for ${provider}: ${(e as Error).message}`); } finally { setLoadingProvider(null); } @@ -213,6 +203,7 @@ export function CandidatesPage({ const pInfo = providerMap.get(c.provider); const isLocal = pInfo?.local ?? false; const models = modelsByProvider[c.provider] ?? []; + const loaded = modelsByProvider[c.provider] !== undefined; const isLoading = loadingProvider === c.provider; const discovered = discoveredByProvider[c.provider] ?? []; // For local discoverable providers, merge discovered models into the list. @@ -274,9 +265,10 @@ export function CandidatesPage({ className="field flex-1" value={c.model} onChange={(e) => update(c.id, { model: e.target.value })} + onFocus={() => void loadModels(c.provider)} > {displayModels.map((m) => (