diff --git a/cloud/.example.dev.vars b/cloud/.example.dev.vars index aed0d8e853..1f264f5df1 100644 --- a/cloud/.example.dev.vars +++ b/cloud/.example.dev.vars @@ -8,3 +8,8 @@ GITHUB_CALLBACK_URL=http://localhost:3000/auth/github/callback GOOGLE_CLIENT_ID=... GOOGLE_CLIENT_SECRET=... GOOGLE_CALLBACK_URL=http://localhost:3000/auth/google/callback +STRIPE_SECRET_KEY=your_stripe_secret_key_here +STRIPE_ROUTER_PRICE_ID=your_router_price_id_here +ANTHROPIC_API_KEY=your_anthropic_api_key_here +GEMINI_API_KEY=your_google_api_key_here +OPENAI_API_KEY=your_openai_api_key_here diff --git a/cloud/.example.env.local b/cloud/.example.env.local index 2fdb6c6be3..81260b4d76 100644 --- a/cloud/.example.env.local +++ b/cloud/.example.env.local @@ -1,3 +1,8 @@ ENVIRONMENT=local DATABASE_URL=postgresql://postgres:postgres@localhost:5432/mirascope_cloud_dev TEST_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/mirascope_cloud_dev +STRIPE_SECRET_KEY=your_stripe_secret_key_here +STRIPE_ROUTER_PRICE_ID=your_router_price_id_here +ANTHROPIC_API_KEY=your_anthropic_api_key_here +GEMINI_API_KEY=your_google_api_key_here +OPENAI_API_KEY=your_openai_api_key_here diff --git a/cloud/api/router/cost-calculator.test.ts b/cloud/api/router/cost-calculator.test.ts new file mode 100644 index 0000000000..924d874107 --- /dev/null +++ b/cloud/api/router/cost-calculator.test.ts @@ -0,0 +1,479 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { Effect } from "effect"; +import { + OpenAICostCalculator, + AnthropicCostCalculator, + GoogleCostCalculator, +} from "@/api/router/cost-calculator"; +import { getCostCalculator } from "@/api/router/providers"; +import { clearPricingCache } from "@/api/router/pricing"; + +describe("CostCalculator", () => { + beforeEach(() => { + vi.restoreAllMocks(); + clearPricingCache(); + + // Mock pricing data fetch + const mockData = { + openai: { + id: "openai", + name: "OpenAI", + models: { + "gpt-4o-mini": { + id: "gpt-4o-mini", + name: "GPT-4o Mini", + cost: { + input: 0.15, + output: 0.6, + cache_read: 0.075, + cache_write: 0.1875, + }, + }, + }, + }, + anthropic: { + id: "anthropic", + name: "Anthropic", + models: { + "claude-3-5-haiku-20241022": { + id: "claude-3-5-haiku-20241022", + name: "Claude 3.5 Haiku", + cost: { + input: 1.0, + output: 5.0, + cache_read: 0.1, + cache_write: 1.25, + }, + }, + }, + }, + "google-ai-studio": { + id: "google-ai-studio", + name: "Google AI Studio", + models: { + "gemini-2.0-flash-exp": { + id: "gemini-2.0-flash-exp", + name: "Gemini 2.0 Flash", + cost: { + input: 0.0, + output: 0.0, + }, + }, + }, + }, + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(mockData), + }) as unknown as typeof fetch; + }); + + describe("OpenAICostCalculator", () => { + it("should extract usage from OpenAI response", async () => { + const calculator = new OpenAICostCalculator(); + const responseBody = { + choices: [{ message: { content: "Hello" } }], + usage: { + prompt_tokens: 100, + completion_tokens: 50, + total_tokens: 150, + }, + }; + + const result = await Effect.runPromise( + calculator.calculate("gpt-4o-mini", responseBody), + ); + + expect(result).toBeDefined(); + expect(result?.usage.inputTokens).toBe(100); + expect(result?.usage.outputTokens).toBe(50); + expect(result?.usage.cacheReadTokens).toBeUndefined(); + }); + + it("should return null for null body", async () => { + const calculator = new OpenAICostCalculator(); + const result = await Effect.runPromise( + calculator.calculate("gpt-4o-mini", null), + ); + + expect(result).toBeNull(); + }); + + it("should extract cache tokens from OpenAI response", async () => { + const calculator = new OpenAICostCalculator(); + const responseBody = { + usage: { + prompt_tokens: 100, + completion_tokens: 50, + total_tokens: 150, + prompt_tokens_details: { + cached_tokens: 30, + }, + }, + }; + + const result = await Effect.runPromise( + calculator.calculate("gpt-4o-mini", responseBody), + ); + + expect(result).toBeDefined(); + expect(result?.usage.cacheReadTokens).toBe(30); + }); + + it("should return null for invalid response body", async () => { + const calculator = new OpenAICostCalculator(); + const responseBody = { choices: [] }; // No usage + + const result = await Effect.runPromise( + calculator.calculate("gpt-4o-mini", responseBody), + ); + + expect(result).toBeNull(); + }); + + it("should calculate cost correctly", async () => { + const calculator = new OpenAICostCalculator(); + const responseBody = { + usage: { + prompt_tokens: 1000, + completion_tokens: 500, + total_tokens: 1500, + }, + }; + + const result = await Effect.runPromise( + calculator.calculate("gpt-4o-mini", responseBody), + ); + + expect(result).toBeDefined(); + expect(result?.cost.inputCost).toBe(0.00015); // 1000 / 1M * 0.15 + expect(result?.cost.outputCost).toBe(0.0003); // 500 / 1M * 0.6 + expect(result?.cost.totalCost).toBe(0.00045); + }); + }); + + describe("AnthropicCostCalculator", () => { + it("should extract usage from Anthropic response", async () => { + const calculator = new AnthropicCostCalculator(); + const responseBody = { + content: [{ type: "text", text: "Hello" }], + usage: { + input_tokens: 100, + output_tokens: 50, + }, + }; + + const result = await Effect.runPromise( + calculator.calculate("claude-3-5-haiku-20241022", responseBody), + ); + + expect(result).toBeDefined(); + expect(result?.usage.inputTokens).toBe(100); + expect(result?.usage.outputTokens).toBe(50); + }); + + it("should return null for null body", async () => { + const calculator = new AnthropicCostCalculator(); + const result = await Effect.runPromise( + calculator.calculate("claude-3-5-haiku-20241022", null), + ); + + expect(result).toBeNull(); + }); + + it("should return null for response without usage", async () => { + const calculator = new AnthropicCostCalculator(); + const responseBody = { + content: [{ type: "text", text: "Hello" }], + // No usage field + }; + + const result = await Effect.runPromise( + calculator.calculate("claude-3-5-haiku-20241022", responseBody), + ); + + expect(result).toBeNull(); + }); + + it("should correctly handle Anthropic cache tokens", async () => { + const calculator = new AnthropicCostCalculator(); + const responseBody = { + usage: { + input_tokens: 100, + output_tokens: 50, + cache_read_input_tokens: 30, + cache_creation_input_tokens: 20, + }, + }; + + const result = await Effect.runPromise( + calculator.calculate("claude-3-5-haiku-20241022", responseBody), + ); + + expect(result).toBeDefined(); + // inputTokens should be base + cache_read + cache_write + expect(result?.usage.inputTokens).toBe(150); // 100 + 30 + 20 + expect(result?.usage.cacheReadTokens).toBe(30); + expect(result?.usage.cacheWriteTokens).toBe(20); + }); + + it("should handle missing cache tokens", async () => { + const calculator = new AnthropicCostCalculator(); + const responseBody = { + usage: { + input_tokens: 100, + output_tokens: 50, + }, + }; + + const result = await Effect.runPromise( + calculator.calculate("claude-3-5-haiku-20241022", responseBody), + ); + + expect(result).toBeDefined(); + expect(result?.usage.inputTokens).toBe(100); + expect(result?.usage.cacheReadTokens).toBe(0); + expect(result?.usage.cacheWriteTokens).toBe(0); + }); + + it("should extract, normalize, and price 5m cache correctly", async () => { + const calculator = new AnthropicCostCalculator(); + const responseBody = { + usage: { + input_tokens: 100, + output_tokens: 50, + cache_creation_input_tokens: 1000, + cache_read_input_tokens: 0, + cache_creation: { + ephemeral_5m_input_tokens: 1000, + ephemeral_1h_input_tokens: 0, + }, + }, + }; + + const result = await Effect.runPromise( + calculator.calculate("claude-3-5-haiku-20241022", responseBody), + ); + + expect(result).toBeDefined(); + // 5m tokens remain 1:1, so cacheWriteTokens = 1000 + expect(result?.usage.cacheWriteTokens).toBe(1000); + // Cost: 1000 tokens / 1M * 1.25 (5m price) = 0.00125 + expect(result?.cost.cacheWriteCost).toBeCloseTo(0.00125, 6); + }); + + it("should extract, normalize, and price 1h cache correctly", async () => { + const calculator = new AnthropicCostCalculator(); + const responseBody = { + usage: { + input_tokens: 100, + output_tokens: 50, + cache_creation_input_tokens: 1000, + cache_read_input_tokens: 0, + cache_creation: { + ephemeral_5m_input_tokens: 0, + ephemeral_1h_input_tokens: 1000, + }, + }, + }; + + const result = await Effect.runPromise( + calculator.calculate("claude-3-5-haiku-20241022", responseBody), + ); + + expect(result).toBeDefined(); + // 1h tokens normalized: 1000 * 1.6 = 1600 + expect(result?.usage.cacheWriteTokens).toBe(1600); + // Cost: 1600 tokens / 1M * 1.25 (5m price) = 0.002 + expect(result?.cost.cacheWriteCost).toBeCloseTo(0.002, 6); + }); + + it("should extract, normalize, and price mixed 5m + 1h cache tokens correctly", async () => { + const calculator = new AnthropicCostCalculator(); + const responseBody = { + usage: { + input_tokens: 100, + output_tokens: 50, + cache_creation_input_tokens: 1500, + cache_read_input_tokens: 0, + cache_creation: { + ephemeral_5m_input_tokens: 500, + ephemeral_1h_input_tokens: 1000, + }, + }, + }; + + const result = await Effect.runPromise( + calculator.calculate("claude-3-5-haiku-20241022", responseBody), + ); + + expect(result).toBeDefined(); + // Normalized: 500 (5m) + 1000 * 1.6 (1h) = 500 + 1600 = 2100 + expect(result?.usage.cacheWriteTokens).toBe(2100); + // Cost: 2100 tokens / 1M * 1.25 (5m price) = 0.002625 + expect(result?.cost.cacheWriteCost).toBeCloseTo(0.002625, 6); + }); + }); + + describe("GoogleCostCalculator", () => { + it("should extract usage from Google response", async () => { + const calculator = new GoogleCostCalculator(); + const responseBody = { + candidates: [{ content: { parts: [{ text: "Hello" }] } }], + usageMetadata: { + promptTokenCount: 100, + candidatesTokenCount: 50, + totalTokenCount: 150, + }, + }; + + const result = await Effect.runPromise( + calculator.calculate("gemini-2.0-flash-exp", responseBody), + ); + + expect(result).toBeDefined(); + expect(result?.usage.inputTokens).toBe(100); + expect(result?.usage.outputTokens).toBe(50); + }); + + it("should return null for null body", async () => { + const calculator = new GoogleCostCalculator(); + const result = await Effect.runPromise( + calculator.calculate("gemini-2.0-flash-exp", null), + ); + + expect(result).toBeNull(); + }); + + it("should return null for response without usageMetadata", async () => { + const calculator = new GoogleCostCalculator(); + const responseBody = { + candidates: [{ content: { parts: [{ text: "Hello" }] } }], + // No usageMetadata field + }; + + const result = await Effect.runPromise( + calculator.calculate("gemini-2.0-flash-exp", responseBody), + ); + + expect(result).toBeNull(); + }); + + it("should extract cached tokens from Google response", async () => { + const calculator = new GoogleCostCalculator(); + const responseBody = { + usageMetadata: { + promptTokenCount: 100, + candidatesTokenCount: 50, + totalTokenCount: 150, + cachedContentTokenCount: 30, + }, + }; + + const result = await Effect.runPromise( + calculator.calculate("gemini-2.0-flash-exp", responseBody), + ); + + expect(result).toBeDefined(); + expect(result?.usage.cacheReadTokens).toBe(30); + }); + }); + + describe("getCostCalculator", () => { + it("should return OpenAICostCalculator for openai", () => { + const calculator = getCostCalculator("openai"); + expect(calculator).toBeInstanceOf(OpenAICostCalculator); + }); + + it("should return AnthropicCostCalculator for anthropic", () => { + const calculator = getCostCalculator("anthropic"); + expect(calculator).toBeInstanceOf(AnthropicCostCalculator); + }); + + it("should return GoogleCostCalculator for google", () => { + const calculator = getCostCalculator("google"); + expect(calculator).toBeInstanceOf(GoogleCostCalculator); + }); + }); + + describe("BaseCostCalculator - error handling", () => { + it("should handle missing pricing data gracefully", async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({}), // Empty pricing data + }) as unknown as typeof fetch; + + const calculator = new OpenAICostCalculator(); + const responseBody = { + usage: { + prompt_tokens: 1000, + completion_tokens: 500, + total_tokens: 1500, + }, + }; + + const result = await Effect.runPromise( + calculator.calculate("unknown-model", responseBody), + ); + + expect(result).toBeDefined(); + expect(result?.usage.inputTokens).toBe(1000); + expect(result?.cost.totalCost).toBe(0); + expect(result?.formattedCost.input).toBe("N/A"); + expect(result?.formattedCost.output).toBe("N/A"); + expect(result?.formattedCost.total).toBe("N/A"); + }); + + it("should handle fetch errors gracefully", async () => { + global.fetch = vi + .fn() + .mockRejectedValue( + new Error("Network error"), + ) as unknown as typeof fetch; + + const calculator = new OpenAICostCalculator(); + const responseBody = { + usage: { + prompt_tokens: 1000, + completion_tokens: 500, + total_tokens: 1500, + }, + }; + + const result = await Effect.runPromise( + calculator.calculate("gpt-4o-mini", responseBody), + ); + + expect(result).toBeDefined(); + expect(result?.cost.totalCost).toBe(0); + expect(result?.formattedCost.total).toBe("N/A"); + }); + }); + + describe("formatted cost output", () => { + it("should format costs correctly", async () => { + const calculator = new OpenAICostCalculator(); + const responseBody = { + usage: { + prompt_tokens: 1000, + completion_tokens: 500, + total_tokens: 1500, + prompt_tokens_details: { + cached_tokens: 200, + }, + }, + }; + + const result = await Effect.runPromise( + calculator.calculate("gpt-4o-mini", responseBody), + ); + + expect(result).toBeDefined(); + expect(result?.formattedCost.input).toMatch(/^\$\d+\.\d{6}$/); + expect(result?.formattedCost.output).toMatch(/^\$\d+\.\d{6}$/); + expect(result?.formattedCost.total).toMatch(/^\$\d+\.\d{6}$/); + expect(result?.formattedCost.cacheRead).toMatch(/^\$\d+\.\d{6}$/); + }); + }); +}); diff --git a/cloud/api/router/cost-calculator.ts b/cloud/api/router/cost-calculator.ts new file mode 100644 index 0000000000..8bd9c18aa6 --- /dev/null +++ b/cloud/api/router/cost-calculator.ts @@ -0,0 +1,248 @@ +/** + * @fileoverview Base cost calculator and provider-specific implementations. + * + * Provides a unified interface for extracting usage from provider responses + * and calculating costs using models.dev pricing data. + */ + +import { Effect } from "effect"; +import { + getModelPricing, + calculateCost, + formatCostBreakdown, + type TokenUsage, + type CostBreakdown, + type FormattedCostBreakdown, +} from "@/api/router/pricing"; +import type { ProviderName } from "@/api/router/providers"; + +/** + * Base cost calculator that handles shared logic for all providers. + * + * Subclasses implement provider-specific usage extraction. + */ +export abstract class BaseCostCalculator { + protected readonly provider: ProviderName; + + constructor(provider: ProviderName) { + this.provider = provider; + } + + /** + * Extracts token usage from a provider response. + * + * Must be implemented by each provider-specific calculator. + */ + protected abstract extractUsage(body: unknown): TokenUsage | null; + + /** + * Main entry point: calculates usage and cost for a request. + * + * @param modelId - The model ID from the request + * @param responseBody - The parsed provider response body + * @returns Effect with usage and cost data, or null if usage unavailable + */ + public calculate( + modelId: string, + responseBody: unknown, + ): Effect.Effect< + { + usage: TokenUsage; + cost: CostBreakdown; + formattedCost: FormattedCostBreakdown; + } | null, + Error + > { + return Effect.gen( + function* (this: BaseCostCalculator) { + // Extract usage from response + const usage = this.extractUsage(responseBody); + if (!usage) { + return null; + } + + // Get pricing data (null if unavailable) + const pricing = yield* getModelPricing(this.provider, modelId).pipe( + Effect.catchAll(() => Effect.succeed(null)), + ); + + if (!pricing) { + // Return usage without cost data + return { + usage, + cost: { + inputCost: 0, + outputCost: 0, + totalCost: 0, + }, + formattedCost: { + input: "N/A", + output: "N/A", + total: "N/A", + }, + }; + } + + // Calculate cost + const cost = calculateCost(pricing, usage); + + return { + usage, + cost, + formattedCost: formatCostBreakdown(cost), + }; + }.bind(this), + ); + } +} + +/** + * OpenAI-specific cost calculator. + */ +export class OpenAICostCalculator extends BaseCostCalculator { + constructor() { + super("openai"); + } + + protected extractUsage(body: unknown): TokenUsage | null { + if (typeof body !== "object" || body === null) return null; + + const usage = ( + body as { + usage?: { + prompt_tokens: number; + completion_tokens: number; + total_tokens: number; + prompt_tokens_details?: { + cached_tokens?: number; + }; + }; + } + ).usage; + + if (!usage) return null; + + return { + inputTokens: usage.prompt_tokens, + outputTokens: usage.completion_tokens, + cacheReadTokens: usage.prompt_tokens_details?.cached_tokens, + }; + } +} + +/** + * Anthropic-specific cost calculator. + * + * IMPORTANT: Anthropic's input_tokens does NOT include cache tokens. + * Total input = input_tokens + cache_creation_input_tokens + cache_read_input_tokens + * + * CACHE PRICING NORMALIZATION: + * Anthropic provides detailed cache breakdown via usage.cache_creation: + * - ephemeral_5m_input_tokens: 5-minute TTL caches (cost 1.25x input price) + * - ephemeral_1h_input_tokens: 1-hour TTL caches (cost 2.0x input price per Anthropic docs) + * + * We normalize these to a single cacheWriteTokens value by converting 1h tokens to + * 5m-equivalent: 1h_normalized = 1h_tokens × (2.0 / 1.25) = 1h_tokens × 1.6 + * + * This allows calculateCost() to use models.dev's 5m pricing (1.25x) for all cache writes, + * while ensuring correct costs: 5m stays 1:1, 1h effectively costs 2.0x input price. + * + * TODO: At some point we will likely want to display the per-request cost-breakdown for users. + * We'll need to update the cost calculators to store this information at that time. + */ +export class AnthropicCostCalculator extends BaseCostCalculator { + constructor() { + super("anthropic"); + } + + // Normalize: 5m tokens stay 1:1, 1h tokens scaled by 1.6 + static readonly EPHEMERAL_1H_CACHE_INPUT_TOKENS_MULTIPLIER = 1.6; + + protected extractUsage(body: unknown): TokenUsage | null { + if (typeof body !== "object" || body === null) return null; + + const usage = ( + body as { + usage?: { + input_tokens: number; + output_tokens: number; + cache_creation_input_tokens?: number; + cache_read_input_tokens?: number; + // Detailed cache breakdown (available in recent API versions) + cache_creation?: { + ephemeral_5m_input_tokens?: number; + ephemeral_1h_input_tokens?: number; + }; + }; + } + ).usage; + + if (!usage) return null; + + const cacheReadTokens = usage.cache_read_input_tokens || 0; + + // Normalize cache write tokens for accurate pricing + // If detailed breakdown is available, normalize 1h tokens to 5m-equivalent + // Since 1h costs 2.0x input and 5m costs 1.25x input: + // 1h_normalized = 1h_tokens * (2.0 / 1.25) = 1h_tokens * 1.6 + // This way, multiplying total by 5m price gives correct cost for both + let cacheWriteTokens: number; + const ephemeral5mTokens = + usage.cache_creation?.ephemeral_5m_input_tokens || 0; + const ephemeral1hTokens = + usage.cache_creation?.ephemeral_1h_input_tokens || 0; + + if (ephemeral5mTokens > 0 || ephemeral1hTokens > 0) { + cacheWriteTokens = + ephemeral5mTokens + + ephemeral1hTokens * + AnthropicCostCalculator.EPHEMERAL_1H_CACHE_INPUT_TOKENS_MULTIPLIER; + } else { + // Fallback to aggregate value (backwards compatibility) + cacheWriteTokens = usage.cache_creation_input_tokens || 0; + } + + // Total input tokens includes base + cache read + cache write + // Matches Python SDK: mirascope/llm/providers/anthropic/_utils/decode.py + const inputTokens = usage.input_tokens + cacheReadTokens + cacheWriteTokens; + + return { + inputTokens, + outputTokens: usage.output_tokens, + cacheReadTokens, + cacheWriteTokens, + }; + } +} + +/** + * Google AI-specific cost calculator. + */ +export class GoogleCostCalculator extends BaseCostCalculator { + constructor() { + super("google"); + } + + protected extractUsage(body: unknown): TokenUsage | null { + if (typeof body !== "object" || body === null) return null; + + const metadata = ( + body as { + usageMetadata?: { + promptTokenCount: number; + candidatesTokenCount: number; + totalTokenCount: number; + cachedContentTokenCount?: number; + }; + } + ).usageMetadata; + + if (!metadata) return null; + + return { + inputTokens: metadata.promptTokenCount, + outputTokens: metadata.candidatesTokenCount, + cacheReadTokens: metadata.cachedContentTokenCount, + }; + } +} diff --git a/cloud/api/router/pricing.test.ts b/cloud/api/router/pricing.test.ts new file mode 100644 index 0000000000..bda1d1fff1 --- /dev/null +++ b/cloud/api/router/pricing.test.ts @@ -0,0 +1,427 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { Effect } from "effect"; +import { + fetchModelsDotDevPricingData, + getModelsDotDevPricingData, + getModelPricing, + calculateCost, + formatCostBreakdown, + clearPricingCache, +} from "@/api/router/pricing"; + +describe("Pricing", () => { + beforeEach(() => { + vi.restoreAllMocks(); + clearPricingCache(); + }); + + describe("fetchPricingData", () => { + it("should fetch and return pricing data", async () => { + const mockData = { + openai: { + id: "openai", + name: "OpenAI", + models: { + "gpt-4o-mini": { + id: "gpt-4o-mini", + name: "GPT-4o Mini", + cost: { + input: 0.15, + output: 0.6, + }, + }, + }, + }, + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(mockData), + }) as unknown as typeof fetch; + + const result = await Effect.runPromise(fetchModelsDotDevPricingData()); + + expect(result).toEqual(mockData); + expect(fetch).toHaveBeenCalledWith("https://models.dev/api.json"); + }); + + it("should fail when fetch returns non-ok response", async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: false, + statusText: "Not Found", + }) as unknown as typeof fetch; + + const result = await Effect.runPromise( + fetchModelsDotDevPricingData().pipe(Effect.flip), + ); + + expect(result).toBeInstanceOf(Error); + expect(result.message).toContain("Failed to fetch pricing data"); + }); + + it("should fail when fetch throws an error", async () => { + global.fetch = vi + .fn() + .mockRejectedValue( + new Error("Network error"), + ) as unknown as typeof fetch; + + const result = await Effect.runPromise( + fetchModelsDotDevPricingData().pipe(Effect.flip), + ); + + expect(result).toBeInstanceOf(Error); + expect(result.message).toContain("Network error"); + }); + + it("should fail when JSON parsing fails", async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.reject(new Error("Invalid JSON")), + }) as unknown as typeof fetch; + + const result = await Effect.runPromise( + fetchModelsDotDevPricingData().pipe(Effect.flip), + ); + + expect(result).toBeInstanceOf(Error); + expect(result.message).toContain( + "Failed to parse JSON from response body", + ); + expect(result.message).toContain("Invalid JSON"); + }); + }); + + describe("getModelPricing", () => { + it("should return pricing for OpenAI model", async () => { + const mockData = { + openai: { + id: "openai", + name: "OpenAI", + models: { + "gpt-4o-mini": { + id: "gpt-4o-mini", + name: "GPT-4o Mini", + cost: { + input: 0.15, + output: 0.6, + cache_read: 0.075, + cache_write: 0.1875, + }, + }, + }, + }, + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(mockData), + }) as unknown as typeof fetch; + + const result = await Effect.runPromise( + getModelPricing("openai", "gpt-4o-mini"), + ); + + expect(result).toMatchObject({ + input: 0.15, + output: 0.6, + cache_read: 0.075, + cache_write: 0.1875, + }); + }); + + it("should return pricing for Anthropic model", async () => { + const mockData = { + anthropic: { + id: "anthropic", + name: "Anthropic", + models: { + "claude-3-5-haiku-20241022": { + id: "claude-3-5-haiku-20241022", + name: "Claude 3.5 Haiku", + cost: { + input: 1.0, + output: 5.0, + cache_read: 0.1, + cache_write: 1.25, + }, + }, + }, + }, + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(mockData), + }) as unknown as typeof fetch; + + const result = await Effect.runPromise( + getModelPricing("anthropic", "claude-3-5-haiku-20241022"), + ); + + expect(result).toMatchObject({ + input: 1.0, + output: 5.0, + cache_read: 0.1, + cache_write: 1.25, + }); + }); + + it("should try both google and google-ai-studio for Google models", async () => { + const mockData = { + "google-ai-studio": { + id: "google-ai-studio", + name: "Google AI Studio", + models: { + "gemini-2.0-flash-exp": { + id: "gemini-2.0-flash-exp", + name: "Gemini 2.0 Flash", + cost: { + input: 0.0, + output: 0.0, + }, + }, + }, + }, + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(mockData), + }) as unknown as typeof fetch; + + const result = await Effect.runPromise( + getModelPricing("google", "gemini-2.0-flash-exp"), + ); + + expect(result).toMatchObject({ + input: 0.0, + output: 0.0, + }); + expect(result?.cache_read).toBeUndefined(); + expect(result?.cache_write).toBeUndefined(); + }); + + it("should return null for unknown model", async () => { + const mockData = { + openai: { + id: "openai", + name: "OpenAI", + models: {}, + }, + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(mockData), + }) as unknown as typeof fetch; + + const result = await Effect.runPromise( + getModelPricing("openai", "unknown-model"), + ); + + expect(result).toBeNull(); + }); + }); + + describe("calculateCost", () => { + it("should calculate cost correctly for basic usage", () => { + const pricing = { + input: 0.15, // per million tokens + output: 0.6, + }; + + const usage = { + inputTokens: 1000, + outputTokens: 500, + }; + + const result = calculateCost(pricing, usage); + + expect(result.inputCost).toBe(0.00015); // 1000 / 1M * 0.15 + expect(result.outputCost).toBe(0.0003); // 500 / 1M * 0.6 + expect(result.totalCost).toBe(0.00045); + expect(result.cacheReadCost).toBeUndefined(); + expect(result.cacheWriteCost).toBeUndefined(); + }); + + it("should calculate cost with cache tokens", () => { + const pricing = { + input: 0.15, + output: 0.6, + cache_read: 0.075, + cache_write: 0.1875, + }; + + const usage = { + inputTokens: 1000, + outputTokens: 500, + cacheReadTokens: 200, + cacheWriteTokens: 100, + }; + + const result = calculateCost(pricing, usage); + + expect(result.inputCost).toBeCloseTo(0.00015, 10); + expect(result.outputCost).toBeCloseTo(0.0003, 10); + expect(result.cacheReadCost).toBeCloseTo(0.000015, 10); // 200 / 1M * 0.075 + expect(result.cacheWriteCost).toBeCloseTo(0.00001875, 10); // 100 / 1M * 0.1875 + expect(result.totalCost).toBeCloseTo( + 0.00015 + 0.0003 + 0.000015 + 0.00001875, + 10, + ); + }); + + it("should handle missing cache pricing", () => { + const pricing = { + input: 0.15, + output: 0.6, + }; + + const usage = { + inputTokens: 1000, + outputTokens: 500, + cacheReadTokens: 200, + cacheWriteTokens: 100, + }; + + const result = calculateCost(pricing, usage); + + expect(result.cacheReadCost).toBeUndefined(); + expect(result.cacheWriteCost).toBeUndefined(); + expect(result.totalCost).toBe(0.00045); + }); + + it("should handle zero tokens", () => { + const pricing = { + input: 0.15, + output: 0.6, + }; + + const usage = { + inputTokens: 0, + outputTokens: 0, + }; + + const result = calculateCost(pricing, usage); + + expect(result.inputCost).toBe(0); + expect(result.outputCost).toBe(0); + expect(result.totalCost).toBe(0); + }); + }); + + describe("formatCostBreakdown", () => { + it("should format cost breakdown with 6 decimal places", () => { + const breakdown = { + inputCost: 0.00015, + outputCost: 0.0003, + cacheReadCost: 0.000075, + cacheWriteCost: 0.0001, + totalCost: 0.000525, + }; + + const formatted = formatCostBreakdown(breakdown); + + expect(formatted.input).toBe("$0.000150"); + expect(formatted.output).toBe("$0.000300"); + expect(formatted.cacheRead).toBe("$0.000075"); + expect(formatted.cacheWrite).toBe("$0.000100"); + expect(formatted.total).toBe("$0.000525"); + }); + + it("should handle undefined cache costs", () => { + const breakdown = { + inputCost: 0.00015, + outputCost: 0.0003, + totalCost: 0.00045, + }; + + const formatted = formatCostBreakdown(breakdown); + + expect(formatted.input).toBe("$0.000150"); + expect(formatted.output).toBe("$0.000300"); + expect(formatted.cacheRead).toBeUndefined(); + expect(formatted.cacheWrite).toBeUndefined(); + expect(formatted.total).toBe("$0.000450"); + }); + }); + + describe("getPricingData", () => { + it("should use cached data when cache is fresh", async () => { + const mockData = { + openai: { + id: "openai", + name: "OpenAI", + models: { + "gpt-4o-mini": { + id: "gpt-4o-mini", + name: "GPT-4o Mini", + cost: { + input: 0.15, + output: 0.6, + }, + }, + }, + }, + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(mockData), + }) as unknown as typeof fetch; + + // First fetch - should call the API + const result1 = await Effect.runPromise(getModelsDotDevPricingData()); + expect(fetch).toHaveBeenCalledTimes(1); + expect(result1).toEqual(mockData); + + // Second fetch - should use cache, not call API again + const result2 = await Effect.runPromise(getModelsDotDevPricingData()); + expect(fetch).toHaveBeenCalledTimes(1); // Still 1, not 2 + expect(result2).toEqual(mockData); + }); + }); + + describe("clearPricingCache", () => { + it("should clear the pricing cache", async () => { + // Set up mock data + const mockData = { + openai: { + id: "openai", + name: "OpenAI", + models: { + "gpt-4o-mini": { + id: "gpt-4o-mini", + name: "GPT-4o Mini", + cost: { + input: 0.15, + output: 0.6, + }, + }, + }, + }, + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(mockData), + }) as unknown as typeof fetch; + + // Fetch pricing data to populate cache + await Effect.runPromise(getModelsDotDevPricingData()); + + // Verify fetch was called once + expect(fetch).toHaveBeenCalledTimes(1); + + // Clear the cache + clearPricingCache(); + + // Fetch again - should trigger a new fetch since cache was cleared + await Effect.runPromise(getModelsDotDevPricingData()); + + // Verify fetch was called again + expect(fetch).toHaveBeenCalledTimes(2); + }); + }); +}); diff --git a/cloud/api/router/pricing.ts b/cloud/api/router/pricing.ts new file mode 100644 index 0000000000..16ee288645 --- /dev/null +++ b/cloud/api/router/pricing.ts @@ -0,0 +1,237 @@ +/** + * @fileoverview Pricing data management for AI model cost calculation. + * + * Fetches pricing data from models.dev/api.json and provides cost calculation + * utilities for tracking usage costs across providers. + */ + +import { Effect } from "effect"; +import { + getModelsDotDevProviderIds, + type ProviderName, +} from "@/api/router/providers"; + +/** + * Pricing information for a model. + */ +export interface ModelPricing { + /** Cost per million input tokens in USD */ + input: number; + /** Cost per million output tokens in USD */ + output: number; + /** Optional: Cost per million cache read tokens in USD */ + cache_read?: number; + /** Optional: Cost per million cache write tokens in USD */ + cache_write?: number; +} + +/** + * Provider API structure from models.dev + */ +interface ProviderData { + id: string; + name: string; + models: Record< + string, + { + id: string; + name: string; + cost?: ModelPricing; + } + >; +} + +/** + * Cache for models.dev pricing data + */ +let pricingCache: Record | null = null; +let lastFetchTime: number = 0; +const CACHE_TTL_MS = 1000 * 60 * 60; // 1 hour + +/** + * Clears the pricing cache. Useful for testing. + */ +export function clearPricingCache(): void { + pricingCache = null; + lastFetchTime = 0; +} + +/** + * Fetches pricing data from models.dev/api.json + */ +export function fetchModelsDotDevPricingData() { + return Effect.gen(function* () { + const response = yield* Effect.tryPromise({ + try: async () => { + return await fetch("https://models.dev/api.json"); + }, + catch: (error) => + new Error( + `Error fetching pricing data: ${error instanceof Error ? error.message : /* v8 ignore next */ String(error)}`, + ), + }); + + if (!response.ok) { + return yield* Effect.fail(new Error("Failed to fetch pricing data")); + } + + const data = yield* Effect.tryPromise({ + try: async () => await response.json>(), + catch: (error) => + new Error( + `Failed to parse JSON from response body: ${error instanceof Error ? error.message : /* v8 ignore next */ String(error)}`, + ), + }); + + pricingCache = data; + lastFetchTime = Date.now(); + return data; + }); +} + +/** + * Gets cached pricing data, fetching if stale or missing. + */ +export function getModelsDotDevPricingData() { + return Effect.gen(function* () { + const now = Date.now(); + + // Return cache if fresh + if (pricingCache && now - lastFetchTime < CACHE_TTL_MS) { + return pricingCache; + } + + // Fetch fresh data + return yield* fetchModelsDotDevPricingData(); + }); +} + +/** + * Looks up pricing for a specific model. + * + * This uses the data fetched from models.dev/api.json for accurate, + * current pricing data. + * + * @param provider - Our provider name (openai, anthropic, google) + * @param modelId - The model ID used in the request + * @returns Pricing info or null if not found + */ +export function getModelPricing( + provider: ProviderName, + modelId: string, +): Effect.Effect { + return Effect.gen(function* () { + const data = yield* getModelsDotDevPricingData(); + + // Try each possible provider ID + const providerIds = getModelsDotDevProviderIds(provider); + + for (const providerId of providerIds) { + const providerData = data[providerId]; + const modelPricing = providerData?.models[modelId]?.cost; + if (modelPricing) { + return modelPricing; + } + } + + return null; + }); +} + +/** + * Usage data for cost calculation + */ +export interface TokenUsage { + inputTokens: number; + outputTokens: number; + cacheReadTokens?: number; + cacheWriteTokens?: number; +} + +/** + * Calculated cost breakdown + */ +export interface CostBreakdown { + inputCost: number; + outputCost: number; + cacheReadCost?: number; + cacheWriteCost?: number; + totalCost: number; +} + +/** + * Formatted cost breakdown with string representations + */ +export interface FormattedCostBreakdown { + input: string; + output: string; + cacheRead?: string; + cacheWrite?: string; + total: string; +} + +/** + * Calculates the cost for a request based on usage and pricing. + * + * @param pricing - The model's pricing information + * @param usage - Token usage from the response + * @returns Cost breakdown in USD + */ +export function calculateCost( + pricing: ModelPricing, + usage: TokenUsage, +): CostBreakdown { + // All costs in models.dev are per million tokens + const inputCost = (usage.inputTokens / 1_000_000) * pricing.input; + const outputCost = (usage.outputTokens / 1_000_000) * pricing.output; + + const cacheReadCost = + usage.cacheReadTokens && pricing.cache_read + ? (usage.cacheReadTokens / 1_000_000) * pricing.cache_read + : undefined; + + const cacheWriteCost = + usage.cacheWriteTokens && pricing.cache_write + ? (usage.cacheWriteTokens / 1_000_000) * pricing.cache_write + : undefined; + + const totalCost = + inputCost + outputCost + (cacheReadCost || 0) + (cacheWriteCost || 0); + + return { + inputCost, + outputCost, + cacheReadCost, + cacheWriteCost, + totalCost, + }; +} + +/** + * Formats a single cost value in USD as a string. + */ +function formatCostValue(cost: number): string { + return `$${cost.toFixed(6)}`; +} + +/** + * Formats a cost breakdown into string representations. + * + * @param breakdown - The cost breakdown to format + * @returns Formatted cost breakdown with all values as strings + */ +export function formatCostBreakdown( + breakdown: CostBreakdown, +): FormattedCostBreakdown { + return { + input: formatCostValue(breakdown.inputCost), + output: formatCostValue(breakdown.outputCost), + cacheRead: breakdown.cacheReadCost + ? formatCostValue(breakdown.cacheReadCost) + : undefined, + cacheWrite: breakdown.cacheWriteCost + ? formatCostValue(breakdown.cacheWriteCost) + : undefined, + total: formatCostValue(breakdown.totalCost), + }; +} diff --git a/cloud/api/router/providers.ts b/cloud/api/router/providers.ts new file mode 100644 index 0000000000..7ebbbfb5ec --- /dev/null +++ b/cloud/api/router/providers.ts @@ -0,0 +1,143 @@ +/** + * @fileoverview Centralized provider registry and configuration. + * + * Consolidates all provider-specific configuration including: + * - API proxy settings (base URLs, auth headers) + * - Provider ID mappings for pricing lookups + * - Environment variable mappings + * - Helper functions for provider operations + */ + +import { + OpenAICostCalculator, + AnthropicCostCalculator, + GoogleCostCalculator, + type BaseCostCalculator, +} from "@/api/router/cost-calculator"; + +/** + * Configuration for AI provider proxying. + */ +export interface ProxyConfig { + /** Base URL of the provider API */ + baseUrl: string; + /** Header name for authentication (e.g., "Authorization", "x-api-key") */ + authHeader: string; + /** Format for the auth header value (e.g., "Bearer {key}" or just "{key}") */ + authFormat: (key: string) => string; +} + +/** + * Provider configurations for proxying. + * Defines base URLs, authentication patterns, and models.dev IDs for each provider. + */ +export const PROVIDER_CONFIGS: Record< + "anthropic" | "google" | "openai", + ProxyConfig & { modelsDotDevIds: string[] } +> = { + anthropic: { + baseUrl: "https://api.anthropic.com", + authHeader: "x-api-key", + authFormat: (key: string) => key, + modelsDotDevIds: ["anthropic"], + }, + google: { + baseUrl: "https://generativelanguage.googleapis.com", + authHeader: "x-goog-api-key", + authFormat: (key: string) => key, + modelsDotDevIds: ["google", "google-ai-studio"], + }, + openai: { + baseUrl: "https://api.openai.com", + authHeader: "Authorization", + authFormat: (key: string) => `Bearer ${key}`, + modelsDotDevIds: ["openai"], + }, +} as const; + +/** + * Supported provider names. + */ +export type ProviderName = keyof typeof PROVIDER_CONFIGS; + +/** + * Returns list of supported provider names. + */ +export function getSupportedProviders(): ProviderName[] { + return Object.keys(PROVIDER_CONFIGS) as ProviderName[]; +} + +/** + * Checks if a provider is supported. + */ +export function isValidProvider(provider: string): provider is ProviderName { + return provider in PROVIDER_CONFIGS; +} + +/** + * Gets provider configuration for proxying. + * + * @param provider - The provider name + * @returns Provider config without API key, or null if provider not found + */ +export function getProviderConfig( + provider: string, +): Omit | null { + if (!isValidProvider(provider)) { + return null; + } + return PROVIDER_CONFIGS[provider]; +} + +/** + * Gets the API key for a provider from environment variables. + * + * @param provider - The provider name + * @returns The API key or undefined if not configured + */ +export function getProviderApiKey(provider: ProviderName): string | undefined { + switch (provider) { + case "openai": + return process.env.OPENAI_API_KEY; + case "anthropic": + return process.env.ANTHROPIC_API_KEY; + case "google": + return process.env.GEMINI_API_KEY; + default: /* v8 ignore next */ { + const exhaustiveCheck: never = provider; + throw new Error(`Unsupported provider: ${exhaustiveCheck as string}`); + } + } +} + +/** + * Gets the provider IDs used in models.dev for a given provider. + * + * @param provider - The provider name + * @returns Array of provider IDs, or [provider] as fallback + */ +export function getModelsDotDevProviderIds(provider: ProviderName): string[] { + const config = PROVIDER_CONFIGS[provider]; + return config?.modelsDotDevIds || [provider]; +} + +/** + * Gets the cost calculator for a specific provider. + * + * @param provider - The provider name + * @returns Cost calculator instance for the provider + */ +export function getCostCalculator(provider: ProviderName): BaseCostCalculator { + switch (provider) { + case "openai": + return new OpenAICostCalculator(); + case "anthropic": + return new AnthropicCostCalculator(); + case "google": + return new GoogleCostCalculator(); + default: /* v8 ignore next */ { + const exhaustiveCheck: never = provider; + throw new Error(`Unsupported provider: ${exhaustiveCheck as string}`); + } + } +} diff --git a/cloud/api/router/proxy.test.ts b/cloud/api/router/proxy.test.ts new file mode 100644 index 0000000000..a8a7341130 --- /dev/null +++ b/cloud/api/router/proxy.test.ts @@ -0,0 +1,417 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { Effect } from "effect"; +import { + proxyToProvider, + extractProviderPath, + extractModelFromRequest, +} from "@/api/router/proxy"; +import { PROVIDER_CONFIGS } from "@/api/router/providers"; +import { ProxyError } from "@/errors"; + +describe("Proxy", () => { + describe("extractProviderPath", () => { + it("should extract path for OpenAI", () => { + const path = extractProviderPath( + "/router/v0/openai/v1/chat/completions", + "openai", + ); + expect(path).toBe("v1/chat/completions"); + }); + + it("should extract path for Anthropic", () => { + const path = extractProviderPath( + "/router/v0/anthropic/v1/messages", + "anthropic", + ); + expect(path).toBe("v1/messages"); + }); + + it("should extract path for Google", () => { + const path = extractProviderPath( + "/router/v0/google/v1beta/models/gemini-pro:generateContent", + "google", + ); + expect(path).toBe("v1beta/models/gemini-pro:generateContent"); + }); + + it("should return original path if prefix doesn't match", () => { + const path = extractProviderPath("/some/other/path", "openai"); + expect(path).toBe("/some/other/path"); + }); + }); + + describe("extractModelFromRequest", () => { + it("should extract model from valid request body", () => { + const body = { model: "gpt-4o-mini", messages: [] }; + const model = extractModelFromRequest(body); + expect(model).toBe("gpt-4o-mini"); + }); + + it("should return null for body without model", () => { + const body = { messages: [] }; + const model = extractModelFromRequest(body); + expect(model).toBeNull(); + }); + + it("should return null for null body", () => { + const model = extractModelFromRequest(null); + expect(model).toBeNull(); + }); + + it("should return null for non-object body", () => { + const model = extractModelFromRequest("not an object"); + expect(model).toBeNull(); + }); + + it("should return null for non-string model", () => { + const body = { model: 123 }; + const model = extractModelFromRequest(body); + expect(model).toBeNull(); + }); + }); + + describe("proxyToProvider", () => { + beforeEach(() => { + // Reset fetch mock before each test + vi.restoreAllMocks(); + }); + + it("should fail with ProxyError when API key is missing", async () => { + const request = new Request( + "http://localhost/router/v0/openai/v1/chat/completions", + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ model: "gpt-4", messages: [] }), + }, + ); + + const result = await Effect.runPromise( + proxyToProvider( + request, + { + ...PROVIDER_CONFIGS.openai, + apiKey: "", // Empty API key should fail + }, + "openai", + ).pipe(Effect.flip), + ); + + expect(result).toBeInstanceOf(ProxyError); + expect(result.message).toContain("API key not configured"); + }); + + it("should proxy request successfully for non-streaming response", async () => { + const mockResponse = { + ok: true, + status: 200, + headers: new Headers({ "content-type": "application/json" }), + clone: () => ({ + text: () => + Promise.resolve( + JSON.stringify({ + choices: [{ message: { content: "Hello" } }], + usage: { prompt_tokens: 10, completion_tokens: 20 }, + }), + ), + }), + }; + + global.fetch = vi + .fn() + .mockResolvedValue(mockResponse) as unknown as typeof fetch; + + const request = new Request( + "http://localhost/router/v0/openai/v1/chat/completions", + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer user-key", + }, + body: JSON.stringify({ model: "gpt-4", messages: [] }), + }, + ); + + const result = await Effect.runPromise( + proxyToProvider( + request, + { + ...PROVIDER_CONFIGS.openai, + apiKey: "test-key", + }, + "openai", + ), + ); + + expect(result.response).toBeDefined(); + expect(result.body).toBeDefined(); + expect(result.body).toHaveProperty("usage"); + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining("https://api.openai.com/v1/chat/completions"), + expect.objectContaining({ + method: "POST", + headers: expect.any(Object) as Headers, + }), + ); + }); + + it("should not parse body for streaming responses", async () => { + const mockResponse = { + ok: true, + status: 200, + headers: new Headers({ "content-type": "text/event-stream" }), + clone: () => ({ + text: () => Promise.resolve("data: test\n\n"), + }), + }; + + global.fetch = vi + .fn() + .mockResolvedValue(mockResponse) as unknown as typeof fetch; + + const request = new Request( + "http://localhost/router/v0/openai/v1/chat/completions", + { + method: "POST", + body: JSON.stringify({ model: "gpt-4", messages: [], stream: true }), + }, + ); + + const result = await Effect.runPromise( + proxyToProvider( + request, + { + ...PROVIDER_CONFIGS.openai, + apiKey: "test-key", + }, + "openai", + ), + ); + + expect(result.response).toBeDefined(); + expect(result.body).toBeNull(); + }); + + it("should handle response without content-type header", async () => { + const mockResponse = { + ok: true, + status: 200, + headers: new Headers(), + clone: () => ({ + text: () => Promise.resolve(JSON.stringify({ result: "success" })), + }), + }; + + global.fetch = vi + .fn() + .mockResolvedValue(mockResponse) as unknown as typeof fetch; + + const request = new Request( + "http://localhost/router/v0/openai/v1/chat/completions", + { + method: "POST", + body: JSON.stringify({ model: "gpt-4", messages: [] }), + }, + ); + + const result = await Effect.runPromise( + proxyToProvider( + request, + { + ...PROVIDER_CONFIGS.openai, + apiKey: "test-key", + }, + "openai", + ), + ); + + expect(result.response).toBeDefined(); + expect(result.body).toEqual({ result: "success" }); + }); + + it("should handle JSON parse errors gracefully", async () => { + const mockResponse = { + ok: true, + status: 200, + headers: new Headers({ "content-type": "application/json" }), + clone: () => ({ + text: () => Promise.resolve("invalid json"), + }), + }; + + global.fetch = vi + .fn() + .mockResolvedValue(mockResponse) as unknown as typeof fetch; + + const request = new Request( + "http://localhost/router/v0/openai/v1/chat/completions", + { + method: "POST", + body: JSON.stringify({ model: "gpt-4", messages: [] }), + }, + ); + + const result = await Effect.runPromise( + proxyToProvider( + request, + { + ...PROVIDER_CONFIGS.openai, + apiKey: "test-key", + }, + "openai", + ), + ); + + expect(result.response).toBeDefined(); + expect(result.body).toBeNull(); + }); + + it("should fail when fetch throws an error", async () => { + global.fetch = vi + .fn() + .mockRejectedValue( + new Error("Network error"), + ) as unknown as typeof fetch; + + const request = new Request( + "http://localhost/router/v0/openai/v1/chat/completions", + { + method: "POST", + body: JSON.stringify({ model: "gpt-4", messages: [] }), + }, + ); + + const result = await Effect.runPromise( + proxyToProvider( + request, + { + ...PROVIDER_CONFIGS.openai, + apiKey: "test-key", + }, + "openai", + ).pipe(Effect.flip), + ); + + expect(result).toBeInstanceOf(ProxyError); + expect(result.message).toContain("Failed to proxy request"); + expect(result.message).toContain("Network error"); + }); + + it("should handle body reading errors gracefully", async () => { + const mockResponse = { + ok: true, + status: 200, + headers: new Headers({ "content-type": "application/json" }), + clone: () => ({ + text: () => Promise.reject(new Error("Failed to read body")), + }), + }; + + global.fetch = vi + .fn() + .mockResolvedValue(mockResponse) as unknown as typeof fetch; + + const request = new Request( + "http://localhost/router/v0/openai/v1/chat/completions", + { + method: "POST", + body: JSON.stringify({ model: "gpt-4", messages: [] }), + }, + ); + + const result = await Effect.runPromise( + proxyToProvider( + request, + { + ...PROVIDER_CONFIGS.openai, + apiKey: "test-key", + }, + "openai", + ), + ); + + expect(result.response).toBeDefined(); + expect(result.body).toBeNull(); + }); + + it("should exclude authorization and host headers from forwarded request", async () => { + const mockResponse = { + ok: true, + status: 200, + headers: new Headers({ "content-type": "application/json" }), + clone: () => ({ + text: () => Promise.resolve("{}"), + }), + }; + + let capturedHeaders: Headers | undefined; + global.fetch = vi + .fn() + .mockImplementation((_url, options: RequestInit | undefined) => { + capturedHeaders = options?.headers as Headers; + return Promise.resolve(mockResponse); + }) as unknown as typeof fetch; + + const request = new Request( + "http://localhost/router/v0/openai/v1/chat/completions", + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer user-key", + Host: "localhost", + "X-Custom": "value", + }, + body: JSON.stringify({ model: "gpt-4", messages: [] }), + }, + ); + + await Effect.runPromise( + proxyToProvider( + request, + { + ...PROVIDER_CONFIGS.openai, + apiKey: "test-key", + }, + "openai", + ), + ); + + expect(capturedHeaders?.get("authorization")).toBe("Bearer test-key"); + expect(capturedHeaders?.get("host")).toBeNull(); + expect(capturedHeaders?.get("content-type")).toBe("application/json"); + expect(capturedHeaders?.get("x-custom")).toBe("value"); + }); + }); + + describe("PROVIDER_CONFIGS", () => { + it("should have correct OpenAI configuration", () => { + expect(PROVIDER_CONFIGS.openai.baseUrl).toBe("https://api.openai.com"); + expect(PROVIDER_CONFIGS.openai.authHeader).toBe("Authorization"); + expect(PROVIDER_CONFIGS.openai.authFormat("test-key")).toBe( + "Bearer test-key", + ); + }); + + it("should have correct Anthropic configuration", () => { + expect(PROVIDER_CONFIGS.anthropic.baseUrl).toBe( + "https://api.anthropic.com", + ); + expect(PROVIDER_CONFIGS.anthropic.authHeader).toBe("x-api-key"); + expect(PROVIDER_CONFIGS.anthropic.authFormat("test-key")).toBe( + "test-key", + ); + }); + + it("should have correct Google configuration", () => { + expect(PROVIDER_CONFIGS.google.baseUrl).toBe( + "https://generativelanguage.googleapis.com", + ); + expect(PROVIDER_CONFIGS.google.authHeader).toBe("x-goog-api-key"); + expect(PROVIDER_CONFIGS.google.authFormat("test-key")).toBe("test-key"); + }); + }); +}); diff --git a/cloud/api/router/proxy.ts b/cloud/api/router/proxy.ts new file mode 100644 index 0000000000..c4c24ae1ac --- /dev/null +++ b/cloud/api/router/proxy.ts @@ -0,0 +1,172 @@ +/** + * @fileoverview Proxy utility for forwarding requests to AI provider APIs. + * + * Provides utilities for proxying HTTP requests to external AI providers + * (OpenAI, Anthropic, Google AI) while swapping authentication from user's + * Mirascope API key to internal provider keys. + */ + +import { Effect } from "effect"; +import { ProxyError } from "@/errors"; +import type { ProxyConfig } from "@/api/router/providers"; + +/** + * Extracts the path suffix after the provider prefix. + * + * @param pathname - The request pathname + * @param provider - The provider name (e.g., "openai") + * @returns The path to forward to the provider + * + * @example + * extractProviderPath("/router/v0/openai/v1/chat/completions", "openai") + * // Returns: "v1/chat/completions" + */ +export function extractProviderPath( + pathname: string, + provider: string, +): string { + const prefix = `/router/v0/${provider}/`; + if (pathname.startsWith(prefix)) { + return pathname.slice(prefix.length); + } + + return pathname; +} + +/** + * Extracts the model ID from a request body. + * + * @param body - The parsed request body + * @returns The model ID or null if not found + */ +export function extractModelFromRequest(body: unknown): string | null { + if (typeof body !== "object" || body === null) return null; + + const model = (body as { model?: string }).model; + return typeof model === "string" ? model : null; +} + +/** + * Result of proxying a request to a provider. + */ +export interface ProxyResult { + /** The provider's response */ + response: Response; + /** Parsed JSON body if available, null otherwise */ + body: unknown; +} + +/** + * Proxies an HTTP request to an AI provider. + * + * This function: + * 1. Extracts the path to forward + * 2. Copies request headers (excluding user auth and host) + * 3. Sets provider authentication + * 4. Forwards the request to the provider + * 5. Clones and parses the response body for usage extraction + * 6. Returns both the response and parsed body + * + * @param request - The incoming HTTP request + * @param config - Provider configuration including API key + * @param providerName - Name of the provider (for path extraction and errors) + * @returns Effect that resolves to ProxyResult with response and parsed body + * + * @example + * ```ts + * const result = yield* proxyToProvider(request, { + * ...PROVIDER_CONFIGS.openai, + * apiKey: process.env.OPENAI_API_KEY!, + * }, "openai"); + * ``` + */ +export function proxyToProvider( + request: Request, + config: ProxyConfig & { apiKey: string }, + providerName: string, +): Effect.Effect { + return Effect.gen(function* () { + // Validate API key is configured + if (!config.apiKey) { + return yield* Effect.fail( + new ProxyError({ + message: `${providerName} API key not configured`, + }), + ); + } + + // Extract the path to forward + const url = new URL(request.url); + const path = extractProviderPath(url.pathname, providerName); + const targetUrl = `${config.baseUrl}/${path}${url.search}`; + + // Copy request headers, excluding user auth and host + const headers = new Headers( + Array.from(request.headers.entries()).filter( + ([key]) => !["authorization", "host"].includes(key.toLowerCase()), + ), + ); + + // Set provider authentication + headers.set(config.authHeader, config.authFormat(config.apiKey)); + + // Forward the request to the provider + const response = yield* Effect.tryPromise({ + try: () => + fetch(targetUrl.toString(), { + method: request.method, + headers, + body: request.body, + // @ts-expect-error - duplex is needed for streaming but not in types + duplex: "half", + }), + catch: (error) => + new ProxyError({ + message: `Failed to proxy request to ${providerName}: ${ + error instanceof Error + ? error.message + : /* v8 ignore next */ String(error) + }`, + cause: error, + }), + }); + + // Check if response is streaming (has text/event-stream content-type) + const contentType = response.headers.get("content-type") || ""; + const isStreaming = + contentType.includes("text/event-stream") || + contentType.includes("application/x-ndjson"); + + let parsedBody: unknown = null; + + // Only parse body for non-streaming responses + if (!isStreaming) { + const responseClone = response.clone(); + const bodyResult = yield* Effect.tryPromise({ + try: () => responseClone.text(), + catch: (error) => + new ProxyError({ + message: `Failed to read response body: ${ + error instanceof Error + ? error.message + : /* v8 ignore next */ String(error) + }`, + cause: error, + }), + }).pipe(Effect.catchAll(() => Effect.succeed(null as string | null))); + + if (bodyResult) { + try { + parsedBody = JSON.parse(bodyResult); + } catch { + // Not JSON, that's ok + } + } + } + + return { + response, + body: parsedBody, + }; + }); +} diff --git a/cloud/app/routeTree.gen.ts b/cloud/app/routeTree.gen.ts index 76b459697a..4f061679e8 100644 --- a/cloud/app/routeTree.gen.ts +++ b/cloud/app/routeTree.gen.ts @@ -30,6 +30,7 @@ import { Route as AuthGithubCallbackRouteImport } from './routes/auth/github.cal import { Route as ApiV0HealthRouteImport } from './routes/api.v0.health' import { Route as ApiV0DocsRouteImport } from './routes/api.v0.docs' import { Route as ApiV0SplatRouteImport } from './routes/api.v0.$' +import { Route as RouterV0ProviderSplatRouteImport } from './routes/router.v0.$provider.$' const PrivacyRoute = PrivacyRouteImport.update({ id: '/privacy', @@ -136,6 +137,11 @@ const ApiV0SplatRoute = ApiV0SplatRouteImport.update({ path: '/api/v0/$', getParentRoute: () => rootRouteImport, } as any) +const RouterV0ProviderSplatRoute = RouterV0ProviderSplatRouteImport.update({ + id: '/router/v0/$provider/$', + path: '/router/v0/$provider/$', + getParentRoute: () => rootRouteImport, +} as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute @@ -159,6 +165,7 @@ export interface FileRoutesByFullPath { '/auth/github/proxy-callback': typeof AuthGithubProxyCallbackRoute '/auth/google/callback': typeof AuthGoogleCallbackRoute '/auth/google/proxy-callback': typeof AuthGoogleProxyCallbackRoute + '/router/v0/$provider/$': typeof RouterV0ProviderSplatRoute } export interface FileRoutesByTo { '/': typeof IndexRoute @@ -182,6 +189,7 @@ export interface FileRoutesByTo { '/auth/github/proxy-callback': typeof AuthGithubProxyCallbackRoute '/auth/google/callback': typeof AuthGoogleCallbackRoute '/auth/google/proxy-callback': typeof AuthGoogleProxyCallbackRoute + '/router/v0/$provider/$': typeof RouterV0ProviderSplatRoute } export interface FileRoutesById { __root__: typeof rootRouteImport @@ -206,6 +214,7 @@ export interface FileRoutesById { '/auth/github/proxy-callback': typeof AuthGithubProxyCallbackRoute '/auth/google/callback': typeof AuthGoogleCallbackRoute '/auth/google/proxy-callback': typeof AuthGoogleProxyCallbackRoute + '/router/v0/$provider/$': typeof RouterV0ProviderSplatRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath @@ -231,6 +240,7 @@ export interface FileRouteTypes { | '/auth/github/proxy-callback' | '/auth/google/callback' | '/auth/google/proxy-callback' + | '/router/v0/$provider/$' fileRoutesByTo: FileRoutesByTo to: | '/' @@ -254,6 +264,7 @@ export interface FileRouteTypes { | '/auth/github/proxy-callback' | '/auth/google/callback' | '/auth/google/proxy-callback' + | '/router/v0/$provider/$' id: | '__root__' | '/' @@ -277,6 +288,7 @@ export interface FileRouteTypes { | '/auth/github/proxy-callback' | '/auth/google/callback' | '/auth/google/proxy-callback' + | '/router/v0/$provider/$' fileRoutesById: FileRoutesById } export interface RootRouteChildren { @@ -297,6 +309,7 @@ export interface RootRouteChildren { ApiV0SplatRoute: typeof ApiV0SplatRoute ApiV0DocsRoute: typeof ApiV0DocsRoute ApiV0HealthRoute: typeof ApiV0HealthRoute + RouterV0ProviderSplatRoute: typeof RouterV0ProviderSplatRoute } declare module '@tanstack/react-router' { @@ -448,6 +461,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ApiV0SplatRouteImport parentRoute: typeof rootRouteImport } + '/router/v0/$provider/$': { + id: '/router/v0/$provider/$' + path: '/router/v0/$provider/$' + fullPath: '/router/v0/$provider/$' + preLoaderRoute: typeof RouterV0ProviderSplatRouteImport + parentRoute: typeof rootRouteImport + } } } @@ -497,6 +517,7 @@ const rootRouteChildren: RootRouteChildren = { ApiV0SplatRoute: ApiV0SplatRoute, ApiV0DocsRoute: ApiV0DocsRoute, ApiV0HealthRoute: ApiV0HealthRoute, + RouterV0ProviderSplatRoute: RouterV0ProviderSplatRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/cloud/app/routes/router.v0.$provider.$.tsx b/cloud/app/routes/router.v0.$provider.$.tsx new file mode 100644 index 0000000000..9b3f5e300e --- /dev/null +++ b/cloud/app/routes/router.v0.$provider.$.tsx @@ -0,0 +1,150 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { Effect } from "effect"; +import { authenticate } from "@/auth"; +import { Database } from "@/db"; +import { handleErrors, handleDefects } from "@/api/utils"; +import { proxyToProvider, extractModelFromRequest } from "@/api/router/proxy"; +import { + PROVIDER_CONFIGS, + isValidProvider, + getProviderApiKey, + getCostCalculator, +} from "@/api/router/providers"; +import { InternalError } from "@/errors"; + +/** + * Unified Provider Proxy Route + * + * Catches all requests to `/router/v0/{provider}/*` and proxies them to the respective + * AI provider's API. Authenticates users via Mirascope API key, uses internal provider + * API keys, extracts usage data, and calculates costs. + * + * Supported providers: openai, anthropic, google + * + * Base URL examples: + * - OpenAI: `base_url="https://mirascope.com/router/v0/openai/v1"` + * - Anthropic: `base_url="https://mirascope.com/router/v0/anthropic"` + * - Google: `base_url="https://mirascope.com/router/v0/google/v1beta"` + * + * Request examples: + * - `/router/v0/openai/v1/chat/completions` → `https://api.openai.com/v1/chat/completions` + * - `/router/v0/anthropic/v1/messages` → `https://api.anthropic.com/v1/messages` + * - `/router/v0/google/v1beta/models/{model}:generateContent` → Google API + */ +export const Route = createFileRoute("/router/v0/$provider/$")({ + server: { + handlers: { + ANY: async ({ + request, + params, + }: { + request: Request; + params: { provider: string; "*"?: string }; + }) => { + const databaseUrl = process.env.DATABASE_URL; + const provider = params.provider.toLowerCase(); + + const handler = Effect.gen(function* () { + if (!databaseUrl) { + return yield* new InternalError({ + message: "Database not configured", + }); + } + + // Validate provider + if (!isValidProvider(provider)) { + return yield* new InternalError({ + message: `Unsupported provider: ${provider}`, + }); + } + + // Authenticate user via Mirascope API key + yield* authenticate(request); + + // Get provider-specific API key from environment + const providerApiKey = getProviderApiKey(provider); + + if (!providerApiKey) { + return yield* new InternalError({ + message: `${provider} API key not configured`, + }); + } + + // Extract model from request body for cost calculation + const requestBody = yield* Effect.tryPromise({ + try: () => request.clone().text(), + catch: () => null as string | null, + }); + + let modelId: string | null = null; + if (requestBody) { + try { + const parsed: unknown = JSON.parse(requestBody); + modelId = extractModelFromRequest(parsed); + } catch { + // Not JSON, that's ok + } + } + + // Proxy to provider with internal API key + const proxyResult = yield* proxyToProvider( + request, + { + ...PROVIDER_CONFIGS[provider], + apiKey: providerApiKey, + }, + provider, + ); + + // Calculate usage and cost + if (proxyResult.body && modelId) { + const calculator = getCostCalculator(provider); + if (calculator) { + const result = yield* calculator + .calculate(modelId, proxyResult.body) + .pipe(Effect.catchAll(() => Effect.succeed(null))); + + if (result) { + console.log({ + provider, + model: modelId, + usage: { + inputTokens: result.usage.inputTokens, + outputTokens: result.usage.outputTokens, + cacheReadTokens: result.usage.cacheReadTokens || 0, + cacheWriteTokens: result.usage.cacheWriteTokens || 0, + totalTokens: + result.usage.inputTokens + result.usage.outputTokens, + }, + cost: { + input: result.formattedCost.input, + output: result.formattedCost.output, + cacheRead: result.formattedCost.cacheRead, + cacheWrite: result.formattedCost.cacheWrite, + total: result.formattedCost.total, + }, + }); + } + } + } + + return proxyResult.response; + }).pipe( + Effect.provide( + Database.Live({ + database: { connectionString: databaseUrl }, + payments: { + apiKey: process.env.STRIPE_SECRET_KEY || "", + routerPriceId: process.env.STRIPE_ROUTER_PRICE_ID || "", + }, + }), + ), + handleErrors, + handleDefects, + ); + + return Effect.runPromise(handler); + }, + }, + }, +}); diff --git a/cloud/bun.lock b/cloud/bun.lock index b042c8965f..ea9b3aa91a 100644 --- a/cloud/bun.lock +++ b/cloud/bun.lock @@ -148,7 +148,7 @@ "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.7.13", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": "^1.20251202.0" }, "optionalPeers": ["workerd"] }, "sha512-NulO1H8R/DzsJguLC0ndMuk4Ufv0KSlN+E54ay9rn9ZCQo0kpAPwwh3LhgpZ96a3Dr6L9LqW57M4CqC34iLOvw=="], - "@cloudflare/vite-plugin": ["@cloudflare/vite-plugin@1.19.0", "", { "dependencies": { "@cloudflare/unenv-preset": "2.7.13", "@remix-run/node-fetch-server": "^0.8.0", "defu": "^6.1.4", "get-port": "^7.1.0", "miniflare": "4.20251217.0", "picocolors": "^1.1.1", "tinyglobby": "^0.2.12", "unenv": "2.0.0-rc.24", "wrangler": "4.56.0", "ws": "8.18.0" }, "peerDependencies": { "vite": "^6.1.0 || ^7.0.0" } }, "sha512-5WpXYB7vwLnqlMyGSrPOO0nKynbn/nA33VXRPQg3II7q3T/3GOACYq/pnv9WBfcq4OnTdehJFm72Zn+psfhBXQ=="], + "@cloudflare/vite-plugin": ["@cloudflare/vite-plugin@1.17.1", "", { "dependencies": { "@cloudflare/unenv-preset": "2.7.13", "@remix-run/node-fetch-server": "^0.8.0", "defu": "^6.1.4", "get-port": "^7.1.0", "miniflare": "4.20251210.0", "picocolors": "^1.1.1", "tinyglobby": "^0.2.12", "unenv": "2.0.0-rc.24", "wrangler": "4.54.0", "ws": "8.18.0" }, "peerDependencies": { "vite": "^6.1.0 || ^7.0.0" } }, "sha512-QHxTDhvJakWCs4mu7Q6fB02CPT5MvZkyxufwX7dCIDqLfav5ohIQ7+wdTU7AYwPJ7tF8/a82dBpwnt7+wHooXg=="], "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20251210.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Nn9X1moUDERA9xtFdCQ2XpQXgAS9pOjiCxvOT8sVx9UJLAiBLkfSCGbpsYdarODGybXCpjRlc77Yppuolvt7oQ=="], @@ -160,7 +160,7 @@ "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20251210.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Uaz6/9XE+D6E7pCY4OvkCuJHu7HcSDzeGcCGY1HLhojXhHd7yL52c3yfiyJdS8hPatiAa0nn5qSI/42+aTdDSw=="], - "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20251225.0", "", {}, "sha512-ZZl0cNLFcsBRFKtMftKWOsfAybUYSeiTMzpQV1NlTVlByHAs1rGQt45Jw/qz8LrfHoq9PGTieSj9W350Gi4Pvg=="], + "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20251231.0", "", {}, "sha512-XOP7h2y9Nu3ECuZM9S7w3g4GSliTgj6SEEkYj6G6d3TEQtOiV/cHXuI/fKiLj8Z9+qJK/RLLcKkX14NxajrXCw=="], "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], @@ -250,7 +250,7 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], - "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="], + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], @@ -488,49 +488,49 @@ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.53.5", "", { "os": "android", "cpu": "arm" }, "sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.54.0", "", { "os": "android", "cpu": "arm" }, "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng=="], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.53.5", "", { "os": "android", "cpu": "arm64" }, "sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw=="], + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.54.0", "", { "os": "android", "cpu": "arm64" }, "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.53.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-S87zZPBmRO6u1YXQLwpveZm4JfPpAa6oHBX7/ghSiGH3rz/KDgAu1rKdGutV+WUI6tKDMbaBJomhnT30Y2t4VQ=="], + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.54.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.53.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-YTbnsAaHo6VrAczISxgpTva8EkfQus0VPEVJCEaboHtZRIb6h6j0BNxRBOwnDciFTZLDPW5r+ZBmhL/+YpTZgA=="], + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.54.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.53.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-1T8eY2J8rKJWzaznV7zedfdhD1BqVs1iqILhmHDq/bqCUZsrMt+j8VCTHhP0vdfbHK3e1IQ7VYx3jlKqwlf+vw=="], + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.54.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.53.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-sHTiuXyBJApxRn+VFMaw1U+Qsz4kcNlxQ742snICYPrY+DDL8/ZbaC4DVIB7vgZmp3jiDaKA0WpBdP0aqPJoBQ=="], + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.54.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.53.5", "", { "os": "linux", "cpu": "arm" }, "sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA=="], + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.54.0", "", { "os": "linux", "cpu": "arm" }, "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.53.5", "", { "os": "linux", "cpu": "arm" }, "sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ=="], + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.54.0", "", { "os": "linux", "cpu": "arm" }, "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.53.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg=="], + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.54.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.53.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g=="], + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.54.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg=="], - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.53.5", "", { "os": "linux", "cpu": "none" }, "sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA=="], + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.54.0", "", { "os": "linux", "cpu": "none" }, "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw=="], - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.53.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q=="], + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.54.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.53.5", "", { "os": "linux", "cpu": "none" }, "sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ=="], + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.54.0", "", { "os": "linux", "cpu": "none" }, "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.53.5", "", { "os": "linux", "cpu": "none" }, "sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w=="], + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.54.0", "", { "os": "linux", "cpu": "none" }, "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.53.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw=="], + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.54.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.53.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw=="], + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.54.0", "", { "os": "linux", "cpu": "x64" }, "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.53.5", "", { "os": "linux", "cpu": "x64" }, "sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg=="], + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.54.0", "", { "os": "linux", "cpu": "x64" }, "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw=="], - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.53.5", "", { "os": "none", "cpu": "arm64" }, "sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg=="], + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.54.0", "", { "os": "none", "cpu": "arm64" }, "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg=="], - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.53.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-nggc/wPpNTgjGg75hu+Q/3i32R00Lq1B6N1DO7MCU340MRKL3WZJMjA9U4K4gzy3dkZPXm9E1Nc81FItBVGRlA=="], + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.54.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw=="], - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.53.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-U/54pTbdQpPLBdEzCT6NBCFAfSZMvmjr0twhnD9f4EIvlm9wy3jjQ38yQj1AGznrNO65EWQMgm/QUjuIVrYF9w=="], + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.54.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ=="], - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.53.5", "", { "os": "win32", "cpu": "x64" }, "sha512-2NqKgZSuLH9SXBBV2dWNRCZmocgSOx8OJSdpRaEcRlIfX8YrKxUT6z0F1NpvDVhOsl190UFTRh2F2WDWWCYp3A=="], + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.54.0", "", { "os": "win32", "cpu": "x64" }, "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ=="], - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.53.5", "", { "os": "win32", "cpu": "x64" }, "sha512-JRpZUhCfhZ4keB5v0fe02gQJy05GqboPOaxvjugW04RLSYYoB/9t2lx2u/tMs/Na/1NXfY8QYjgRljRpN+MjTQ=="], + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.54.0", "", { "os": "win32", "cpu": "x64" }, "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg=="], "@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="], @@ -548,9 +548,9 @@ "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], - "@sindresorhus/is": ["@sindresorhus/is@7.1.1", "", {}, "sha512-rO92VvpgMc3kfiTjGT52LEtJ8Yc5kCWhZjLQ3LwlA4pSgPpQO7bVpYXParOD8Jwf+cVQECJo3yP/4I8aZtUQTQ=="], + "@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="], - "@speed-highlight/core": ["@speed-highlight/core@1.2.12", "", {}, "sha512-uilwrK0Ygyri5dToHYdZSjcvpS2ZwX0w5aSt3GCEN9hrjxWCoeV4Z2DTXuxjwbntaLQIEEAlCeNQss5SoHvAEA=="], + "@speed-highlight/core": ["@speed-highlight/core@1.2.14", "", {}, "sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA=="], "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], @@ -588,41 +588,41 @@ "@tanstack/history": ["@tanstack/history@1.141.0", "", {}, "sha512-LS54XNyxyTs5m/pl1lkwlg7uZM3lvsv2FIIV1rsJgnfwVCnI+n4ZGZ2CcjNT13BPu/3hPP+iHmliBSscJxW5FQ=="], - "@tanstack/query-core": ["@tanstack/query-core@5.90.12", "", {}, "sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg=="], + "@tanstack/query-core": ["@tanstack/query-core@5.90.16", "", {}, "sha512-MvtWckSVufs/ja463/K4PyJeqT+HMlJWtw6PrCpywznd2NSgO3m4KwO9RqbFqGg6iDE8vVMFWMeQI4Io3eEYww=="], - "@tanstack/react-query": ["@tanstack/react-query@5.90.12", "", { "dependencies": { "@tanstack/query-core": "5.90.12" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg=="], + "@tanstack/react-query": ["@tanstack/react-query@5.90.16", "", { "dependencies": { "@tanstack/query-core": "5.90.16" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-bpMGOmV4OPmif7TNMteU/Ehf/hoC0Kf98PDc0F4BZkFrEapRMEqI/V6YS0lyzwSV6PQpY1y4xxArUIfBW5LVxQ=="], - "@tanstack/react-router": ["@tanstack/react-router@1.143.11", "", { "dependencies": { "@tanstack/history": "1.141.0", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.143.6", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-FMjcJVlJa4TFkbpfH1wvfEIOvlQX2/JQbFFcTRGEVBYtegXULL8ipoMemMkKYeyd7EPjGvSPbHgTtMFwVhdtFQ=="], + "@tanstack/react-router": ["@tanstack/react-router@1.144.0", "", { "dependencies": { "@tanstack/history": "1.141.0", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.144.0", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-GmRyIGmHtGj3VLTHXepIwXAxTcHyL5W7Vw7O1CnVEtFxQQWKMVOnWgI7tPY6FhlNwMKVb3n0mPFWz9KMYyd2GA=="], - "@tanstack/react-router-devtools": ["@tanstack/react-router-devtools@1.143.11", "", { "dependencies": { "@tanstack/router-devtools-core": "1.143.6" }, "peerDependencies": { "@tanstack/react-router": "^1.143.11", "@tanstack/router-core": "^1.143.6", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" }, "optionalPeers": ["@tanstack/router-core"] }, "sha512-ACq0hEXhhrFFURUUDAvOtji5wVXwwWjLK6YYqCZrySk0G8+xbn8VaeQ4Q+nwSN1WV117Eq5igr7tbj7PrA1eOA=="], + "@tanstack/react-router-devtools": ["@tanstack/react-router-devtools@1.144.0", "", { "dependencies": { "@tanstack/router-devtools-core": "1.144.0" }, "peerDependencies": { "@tanstack/react-router": "^1.144.0", "@tanstack/router-core": "^1.144.0", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" }, "optionalPeers": ["@tanstack/router-core"] }, "sha512-nstjZvZbOM4U0/Hzi82rtsP1DsR2tfigBidK+WuaDRVVstBsnwVor3DQXTGY5CcfgIiMI3eKzI17VOy3SQDDoQ=="], - "@tanstack/react-start": ["@tanstack/react-start@1.143.12", "", { "dependencies": { "@tanstack/react-router": "1.143.11", "@tanstack/react-start-client": "1.143.12", "@tanstack/react-start-server": "1.143.12", "@tanstack/router-utils": "^1.143.11", "@tanstack/start-client-core": "1.143.12", "@tanstack/start-plugin-core": "1.143.12", "@tanstack/start-server-core": "1.143.12", "pathe": "^2.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", "vite": ">=7.0.0" } }, "sha512-8tQKu/DkpOotKXHoz7Jv+ZI1W+5wsLx9dKmnuSapZ796sO6Mlx/90gF9Xs1unVtFE0xjTUwPyp3hBoj3+exsbw=="], + "@tanstack/react-start": ["@tanstack/react-start@1.145.3", "", { "dependencies": { "@tanstack/react-router": "1.144.0", "@tanstack/react-start-client": "1.145.0", "@tanstack/react-start-server": "1.145.3", "@tanstack/router-utils": "^1.143.11", "@tanstack/start-client-core": "1.145.0", "@tanstack/start-plugin-core": "1.145.3", "@tanstack/start-server-core": "1.145.3", "pathe": "^2.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", "vite": ">=7.0.0" } }, "sha512-ZRd0VbcpPSmYTGdR7PF5LdyPnB7rd4zfyuf8bjtUbjphh4P0wjE3DUTA7Mk29RMvvo6sS7Advjsax9ZqEevLgg=="], - "@tanstack/react-start-client": ["@tanstack/react-start-client@1.143.12", "", { "dependencies": { "@tanstack/react-router": "1.143.11", "@tanstack/router-core": "1.143.6", "@tanstack/start-client-core": "1.143.12", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-wKGzJSm8R68PhM+0OYp/qGlaPf6dnwYstgltK3uYDJiuMG3D9Mb64MdoZJ3TmEFZEAtpjg95i34uMZrK1K5UGw=="], + "@tanstack/react-start-client": ["@tanstack/react-start-client@1.145.0", "", { "dependencies": { "@tanstack/react-router": "1.144.0", "@tanstack/router-core": "1.144.0", "@tanstack/start-client-core": "1.145.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-UC/+ONaOzuFnlHbOEudYS+AHOrcwAJaqbnfh9zZ5pUtTkJToBawW3YabDbMnS3o6lEiKggc8uGpsiCglUJrBcA=="], - "@tanstack/react-start-server": ["@tanstack/react-start-server@1.143.12", "", { "dependencies": { "@tanstack/history": "1.141.0", "@tanstack/react-router": "1.143.11", "@tanstack/router-core": "1.143.6", "@tanstack/start-client-core": "1.143.12", "@tanstack/start-server-core": "1.143.12" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-METPI1xGdn08wt603J7g5XQgFHhl4F78d+5U1scllgtjAEnvHBFRB3ZLn+71gghS9CMsHabwwUDh+N7aV29wFQ=="], + "@tanstack/react-start-server": ["@tanstack/react-start-server@1.145.3", "", { "dependencies": { "@tanstack/history": "1.141.0", "@tanstack/react-router": "1.144.0", "@tanstack/router-core": "1.144.0", "@tanstack/start-client-core": "1.145.0", "@tanstack/start-server-core": "1.145.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-HHFq8KTUUsgjifNpYfU7o1jJaVmrwhrjtqQuabGiRseaeIRd4qIGsIS6M1bmOM4+5sYZLKm+lkP6oxgOBuvvaQ=="], "@tanstack/react-store": ["@tanstack/react-store@0.8.0", "", { "dependencies": { "@tanstack/store": "0.8.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow=="], - "@tanstack/router-core": ["@tanstack/router-core@1.143.6", "", { "dependencies": { "@tanstack/history": "1.141.0", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.1", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-NOdkn0lQNNjtyqns8lzZrq05mK3UdIOLR6WyH/reCwTqQ5w7lflquqM0AqbDuBA4iT2hOdNf1lS9NdW9esE2ig=="], + "@tanstack/router-core": ["@tanstack/router-core@1.144.0", "", { "dependencies": { "@tanstack/history": "1.141.0", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.1", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-6oVERtK9XDHCP4XojgHsdHO56ZSj11YaWjF5g/zw39LhyA6Lx+/X86AEIHO4y0BUrMQaJfcjdAQMVSAs6Vjtdg=="], - "@tanstack/router-devtools-core": ["@tanstack/router-devtools-core@1.143.6", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16", "tiny-invariant": "^1.3.3" }, "peerDependencies": { "@tanstack/router-core": "^1.143.6", "csstype": "^3.0.10", "solid-js": ">=1.9.5" }, "optionalPeers": ["csstype"] }, "sha512-8EUfacvsOSWt+7VvBkWMSvvyfd5OWiJBAheLvC7AMD1AWF2JWUQsLLpYUbYCxShdKpU0PrT50KukwVQFl3o7/Q=="], + "@tanstack/router-devtools-core": ["@tanstack/router-devtools-core@1.144.0", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16", "tiny-invariant": "^1.3.3" }, "peerDependencies": { "@tanstack/router-core": "^1.144.0", "csstype": "^3.0.10", "solid-js": ">=1.9.5" }, "optionalPeers": ["csstype"] }, "sha512-rbpQn1aHUtcfY3U3SyJqOZRqDu0a2uPK+TE2CH50HieJApmCuNKj5RsjVQYHgwiFFvR0w0LUmueTnl2X2hiWTg=="], - "@tanstack/router-generator": ["@tanstack/router-generator@1.143.11", "", { "dependencies": { "@tanstack/router-core": "1.143.6", "@tanstack/router-utils": "1.143.11", "@tanstack/virtual-file-routes": "1.141.0", "prettier": "^3.5.0", "recast": "^0.23.11", "source-map": "^0.7.4", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-vq5LnMyN3VuZN2Stnv5B9FzkxQK+0nr4D6S1stAag1fv6ypb7/QF5IBunojs1uLfLMB4E/Kl8lFtlRImJ/KTQA=="], + "@tanstack/router-generator": ["@tanstack/router-generator@1.145.2", "", { "dependencies": { "@tanstack/router-core": "1.144.0", "@tanstack/router-utils": "1.143.11", "@tanstack/virtual-file-routes": "1.141.0", "prettier": "^3.5.0", "recast": "^0.23.11", "source-map": "^0.7.4", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-6DLwfqhexgxw2T2QuS9Y349Vb49hCXBIz9mjWyynjMrpejLXJL+PaHaKJw0Y+H7Ao6RE2vlvXCc2cMjgbz5c7Q=="], - "@tanstack/router-plugin": ["@tanstack/router-plugin@1.143.11", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.143.6", "@tanstack/router-generator": "1.143.11", "@tanstack/router-utils": "1.143.11", "@tanstack/virtual-file-routes": "1.141.0", "babel-dead-code-elimination": "^1.0.11", "chokidar": "^3.6.0", "unplugin": "^2.1.2", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", "@tanstack/react-router": "^1.143.11", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0", "vite-plugin-solid": "^2.11.10", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-mG1bP30mXA89Ury5/NrjqJnEzz5QiVUomeOjaftg/OioAlCSEpq20+1t/wrCqnGHq1y+0AZDVfArAPp5YtTdcg=="], + "@tanstack/router-plugin": ["@tanstack/router-plugin@1.145.2", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.144.0", "@tanstack/router-generator": "1.145.2", "@tanstack/router-utils": "1.143.11", "@tanstack/virtual-file-routes": "1.141.0", "babel-dead-code-elimination": "^1.0.11", "chokidar": "^3.6.0", "unplugin": "^2.1.2", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", "@tanstack/react-router": "^1.144.0", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0", "vite-plugin-solid": "^2.11.10", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-dOABjCE4M2KxB+f/mY71dDZduwVTpf+tCPb4NxmAqbF5Rxes24QaaIZQmiU12jte/L8zYyIA/yX9fi93xZue5Q=="], "@tanstack/router-utils": ["@tanstack/router-utils@1.143.11", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "ansis": "^4.1.0", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-N24G4LpfyK8dOlnP8BvNdkuxg1xQljkyl6PcrdiPSA301pOjatRT1y8wuCCJZKVVD8gkd0MpCZ0VEjRMGILOtA=="], - "@tanstack/start-client-core": ["@tanstack/start-client-core@1.143.12", "", { "dependencies": { "@tanstack/router-core": "1.143.6", "@tanstack/start-fn-stubs": "1.143.8", "@tanstack/start-storage-context": "1.143.12", "seroval": "^1.4.1", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-WixB7iQULoJkInsjWWM5FSdLN3Aqx6Ou43GB59mIHQz5/3LGT+VKXTCpzb0yvc1GCBifWEA9VWge5ZVsZTm3TQ=="], + "@tanstack/start-client-core": ["@tanstack/start-client-core@1.145.0", "", { "dependencies": { "@tanstack/router-core": "1.144.0", "@tanstack/start-fn-stubs": "1.143.8", "@tanstack/start-storage-context": "1.144.0", "seroval": "^1.4.1", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-pqINeN7ZqdfTZrkhy9C7isxRr8U3cByH5ZtLVnUxJp9fvLgwX7LlI+OWpGI0q3E8f/mHMUqJdeE56+atSs8Khw=="], "@tanstack/start-fn-stubs": ["@tanstack/start-fn-stubs@1.143.8", "", {}, "sha512-2IKUPh/TlxwzwHMiHNeFw95+L2sD4M03Es27SxMR0A60Qc4WclpaD6gpC8FsbuNASM2jBxk2UyeYClJxW1GOAQ=="], - "@tanstack/start-plugin-core": ["@tanstack/start-plugin-core@1.143.12", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.28.5", "@babel/types": "^7.28.5", "@rolldown/pluginutils": "1.0.0-beta.40", "@tanstack/router-core": "1.143.6", "@tanstack/router-generator": "1.143.11", "@tanstack/router-plugin": "1.143.11", "@tanstack/router-utils": "1.143.11", "@tanstack/start-client-core": "1.143.12", "@tanstack/start-server-core": "1.143.12", "babel-dead-code-elimination": "^1.0.11", "cheerio": "^1.0.0", "exsolve": "^1.0.7", "pathe": "^2.0.3", "srvx": "^0.9.8", "tinyglobby": "^0.2.15", "ufo": "^1.5.4", "vitefu": "^1.1.1", "xmlbuilder2": "^4.0.0", "zod": "^3.24.2" }, "peerDependencies": { "vite": ">=7.0.0" } }, "sha512-yBxuDmtDQtG7lQOY97ImJm56zYSJqQ4tRBqciclgO96sgrbOe8p+mfC2ObTacJBFWI5o6u+HUiuqAoW4uHL7xw=="], + "@tanstack/start-plugin-core": ["@tanstack/start-plugin-core@1.145.3", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.28.5", "@babel/types": "^7.28.5", "@rolldown/pluginutils": "1.0.0-beta.40", "@tanstack/router-core": "1.144.0", "@tanstack/router-generator": "1.145.2", "@tanstack/router-plugin": "1.145.2", "@tanstack/router-utils": "1.143.11", "@tanstack/start-client-core": "1.145.0", "@tanstack/start-server-core": "1.145.3", "babel-dead-code-elimination": "^1.0.11", "cheerio": "^1.0.0", "exsolve": "^1.0.7", "pathe": "^2.0.3", "srvx": "^0.10.0", "tinyglobby": "^0.2.15", "ufo": "^1.5.4", "vitefu": "^1.1.1", "xmlbuilder2": "^4.0.3", "zod": "^3.24.2" }, "peerDependencies": { "vite": ">=7.0.0" } }, "sha512-PUWKI/8OMyvq8Yjn8ccbEwenASBs5YPEHpXmUjeZ0qb8REGJ6v71Twlqtuva6/fBqZrAKl+2CZrWjgbYZr/h8g=="], - "@tanstack/start-server-core": ["@tanstack/start-server-core@1.143.12", "", { "dependencies": { "@tanstack/history": "1.141.0", "@tanstack/router-core": "1.143.6", "@tanstack/start-client-core": "1.143.12", "@tanstack/start-storage-context": "1.143.12", "h3-v2": "npm:h3@2.0.1-rc.6", "seroval": "^1.4.1", "tiny-invariant": "^1.3.3" } }, "sha512-1YMX0VDI1b72+JhgkdxRX9OCGGaIoZUtmqTYxY08rpNWnRsttjtxhonuUmLnF2ExPbxq+9yxKP9AB8bxBvRuLA=="], + "@tanstack/start-server-core": ["@tanstack/start-server-core@1.145.3", "", { "dependencies": { "@tanstack/history": "1.141.0", "@tanstack/router-core": "1.144.0", "@tanstack/start-client-core": "1.145.0", "@tanstack/start-storage-context": "1.144.0", "h3-v2": "npm:h3@2.0.1-rc.7", "seroval": "^1.4.1", "tiny-invariant": "^1.3.3" } }, "sha512-atsi0fyzymG9BRDJL4kb0oJjhCdB+Wqds+OGPDiWj5VOteCXLpop0ulDlak6wNL2QJZbqqv5BgtGbTQ6rlNyJg=="], - "@tanstack/start-storage-context": ["@tanstack/start-storage-context@1.143.12", "", { "dependencies": { "@tanstack/router-core": "1.143.6" } }, "sha512-T6pkerov1BWMF9LVJicTCY1vHGA4lNfZzvi6IvyeU3KpRW6wrrhIkLdfJsY5rH9TUwzc2mLjsaVO4QgJgFrYKA=="], + "@tanstack/start-storage-context": ["@tanstack/start-storage-context@1.144.0", "", { "dependencies": { "@tanstack/router-core": "1.144.0" } }, "sha512-DuUx5CXfLNettyJlsHDQp66y5haeqzXJkUor7kp5p10SVv24p76dTYqBOpw+wQz//RfJlOciIZFVBcKezXXY0w=="], "@tanstack/store": ["@tanstack/store@0.8.0", "", {}, "sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ=="], @@ -680,37 +680,37 @@ "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], - "@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], + "@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], "@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="], "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], - "@types/ssh2": ["@types/ssh2@0.5.52", "", { "dependencies": { "@types/node": "*", "@types/ssh2-streams": "*" } }, "sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg=="], + "@types/ssh2": ["@types/ssh2@1.15.5", "", { "dependencies": { "@types/node": "^18.11.18" } }, "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ=="], "@types/ssh2-streams": ["@types/ssh2-streams@0.1.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-faHyY3brO9oLEA0QlcO8N2wT7R0+1sHWZvQ+y3rMLwdY1ZyS1z0W3t65j9PqT4HmQ6ALzNe7RZlNuCNE0wBSWA=="], "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.50.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.50.1", "@typescript-eslint/type-utils": "8.50.1", "@typescript-eslint/utils": "8.50.1", "@typescript-eslint/visitor-keys": "8.50.1", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.50.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-PKhLGDq3JAg0Jk/aK890knnqduuI/Qj+udH7wCf0217IGi4gt+acgCyPVe79qoT+qKUvHMDQkwJeKW9fwl8Cyw=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.51.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.51.0", "@typescript-eslint/type-utils": "8.51.0", "@typescript-eslint/utils": "8.51.0", "@typescript-eslint/visitor-keys": "8.51.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.2.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.51.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XtssGWJvypyM2ytBnSnKtHYOGT+4ZwTnBVl36TA4nRO2f4PRNGz5/1OszHzcZCvcBMh+qb7I06uoCmLTRdR9og=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.50.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.50.1", "@typescript-eslint/types": "8.50.1", "@typescript-eslint/typescript-estree": "8.50.1", "@typescript-eslint/visitor-keys": "8.50.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.51.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.51.0", "@typescript-eslint/types": "8.51.0", "@typescript-eslint/typescript-estree": "8.51.0", "@typescript-eslint/visitor-keys": "8.51.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.50.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.50.1", "@typescript-eslint/types": "^8.50.1", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-E1ur1MCVf+YiP89+o4Les/oBAVzmSbeRB0MQLfSlYtbWU17HPxZ6Bhs5iYmKZRALvEuBoXIZMOIRRc/P++Ortg=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.51.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.51.0", "@typescript-eslint/types": "^8.51.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Luv/GafO07Z7HpiI7qeEW5NW8HUtZI/fo/kE0YbtQEFpJRUuR0ajcWfCE5bnMvL7QQFrmT/odMe8QZww8X2nfQ=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.50.1", "", { "dependencies": { "@typescript-eslint/types": "8.50.1", "@typescript-eslint/visitor-keys": "8.50.1" } }, "sha512-mfRx06Myt3T4vuoHaKi8ZWNTPdzKPNBhiblze5N50//TSHOAQQevl/aolqA/BcqqbJ88GUnLqjjcBc8EWdBcVw=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.51.0", "", { "dependencies": { "@typescript-eslint/types": "8.51.0", "@typescript-eslint/visitor-keys": "8.51.0" } }, "sha512-JhhJDVwsSx4hiOEQPeajGhCWgBMBwVkxC/Pet53EpBVs7zHHtayKefw1jtPaNRXpI9RA2uocdmpdfE7T+NrizA=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.50.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ooHmotT/lCWLXi55G4mvaUF60aJa012QzvLK0Y+Mp4WdSt17QhMhWOaBWeGTFVkb2gDgBe19Cxy1elPXylslDw=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.51.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Qi5bSy/vuHeWyir2C8u/uqGMIlIDu8fuiYWv48ZGlZ/k+PRPHtaAu7erpc7p5bzw2WNNSniuxoMSO4Ar6V9OXw=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.50.1", "", { "dependencies": { "@typescript-eslint/types": "8.50.1", "@typescript-eslint/typescript-estree": "8.50.1", "@typescript-eslint/utils": "8.50.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-7J3bf022QZE42tYMO6SL+6lTPKFk/WphhRPe9Tw/el+cEwzLz1Jjz2PX3GtGQVxooLDKeMVmMt7fWpYRdG5Etg=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.51.0", "", { "dependencies": { "@typescript-eslint/types": "8.51.0", "@typescript-eslint/typescript-estree": "8.51.0", "@typescript-eslint/utils": "8.51.0", "debug": "^4.3.4", "ts-api-utils": "^2.2.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-0XVtYzxnobc9K0VU7wRWg1yiUrw4oQzexCG2V2IDxxCxhqBMSMbjB+6o91A+Uc0GWtgjCa3Y8bi7hwI0Tu4n5Q=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.50.1", "", {}, "sha512-v5lFIS2feTkNyMhd7AucE/9j/4V9v5iIbpVRncjk/K0sQ6Sb+Np9fgYS/63n6nwqahHQvbmujeBL7mp07Q9mlA=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.51.0", "", {}, "sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.50.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.50.1", "@typescript-eslint/tsconfig-utils": "8.50.1", "@typescript-eslint/types": "8.50.1", "@typescript-eslint/visitor-keys": "8.50.1", "debug": "^4.3.4", "minimatch": "^9.0.4", "semver": "^7.6.0", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-woHPdW+0gj53aM+cxchymJCrh0cyS7BTIdcDxWUNsclr9VDkOSbqC13juHzxOmQ22dDkMZEpZB+3X1WpUvzgVQ=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.51.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.51.0", "@typescript-eslint/tsconfig-utils": "8.51.0", "@typescript-eslint/types": "8.51.0", "@typescript-eslint/visitor-keys": "8.51.0", "debug": "^4.3.4", "minimatch": "^9.0.4", "semver": "^7.6.0", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.2.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-1qNjGqFRmlq0VW5iVlcyHBbCjPB7y6SxpBkrbhNWMy/65ZoncXCEPJxkRZL8McrseNH6lFhaxCIaX+vBuFnRng=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.50.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.50.1", "@typescript-eslint/types": "8.50.1", "@typescript-eslint/typescript-estree": "8.50.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-lCLp8H1T9T7gPbEuJSnHwnSuO9mDf8mfK/Nion5mZmiEaQD9sWf9W4dfeFqRyqRjF06/kBuTmAqcs9sewM2NbQ=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.51.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.51.0", "@typescript-eslint/types": "8.51.0", "@typescript-eslint/typescript-estree": "8.51.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-11rZYxSe0zabiKaCP2QAwRf/dnmgFgvTmeDTtZvUvXG3UuAdg/GU02NExmmIXzz3vLGgMdtrIosI84jITQOxUA=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.50.1", "", { "dependencies": { "@typescript-eslint/types": "8.50.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-IrDKrw7pCRUR94zeuCSUWQ+w8JEf5ZX5jl/e6AHGSLi1/zIr0lgutfn/7JpfCey+urpgQEdrZVYzCaVVKiTwhQ=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.51.0", "", { "dependencies": { "@typescript-eslint/types": "8.51.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-mM/JRQOzhVN1ykejrvwnBRV3+7yTKK8tVANVN3o1O0t0v7o+jqdVu9crPy5Y9dov15TJk/FTIgoUGHrTOVL3Zg=="], "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], @@ -768,7 +768,7 @@ "ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="], - "ast-v8-to-istanbul": ["ast-v8-to-istanbul@0.3.9", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^9.0.1" } }, "sha512-dSC6tJeOJxbZrPzPbv5mMd6CMiQ1ugaVXXPRad2fXUSsy1kstFn9XQWemV9VW7Y7kpxgQ/4WMoZfwdH8XSU48w=="], + "ast-v8-to-istanbul": ["ast-v8-to-istanbul@0.3.10", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^9.0.1" } }, "sha512-p4K7vMz2ZSk3wN8l5o3y2bJAoZXT3VuJI5OLTATY/01CYWumWvwkUw0SqDBnNq6IiTO3qDa1eSQDibAV8g7XOQ=="], "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], @@ -794,7 +794,7 @@ "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.9.10", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-2VIKvDx8Z1a9rTB2eCkdPE5nSe28XnA+qivGnWHoB40hMMt/h1hSz0960Zqsn6ZyxWXUie0EBdElKv8may20AA=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.9.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ=="], "bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="], @@ -830,7 +830,7 @@ "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], - "caniuse-lite": ["caniuse-lite@1.0.30001760", "", {}, "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw=="], + "caniuse-lite": ["caniuse-lite@1.0.30001762", "", {}, "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw=="], "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], @@ -1018,7 +1018,7 @@ "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], - "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="], + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], @@ -1108,7 +1108,7 @@ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - "h3-v2": ["h3@2.0.1-rc.6", "", { "dependencies": { "rou3": "^0.7.10", "srvx": "^0.9.7" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-kKLFVFNJlDVTbQjakz1ZTFSHB9+oi9+Khf0v7xQsUKU3iOqu2qmrFzTD56YsDvvj2nBgqVDphGRXB2VRursw4w=="], + "h3-v2": ["h3@2.0.1-rc.7", "", { "dependencies": { "rou3": "^0.7.12", "srvx": "^0.10.0" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-qbrRu1OLXmUYnysWOCVrYhtC/m8ZuXu/zCbo3U/KyphJxbPFiC76jHYwVrmEcss9uNAHO5BoUguQ46yEpgI2PA=="], "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], @@ -1268,7 +1268,7 @@ "mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], - "miniflare": ["miniflare@4.20251217.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "sharp": "^0.33.5", "stoppable": "1.1.0", "undici": "7.14.0", "workerd": "1.20251217.0", "ws": "8.18.0", "youch": "4.1.0-beta.10", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-8xsTQbPS6YV+ABZl9qiJIbsum6hbpbhqiyKpOVdzZrhK+1N8EFpT8R6aBZff7kezGmxYZSntjgjqTwJmj3JLgA=="], + "miniflare": ["miniflare@4.20251210.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "sharp": "^0.33.5", "stoppable": "1.1.0", "undici": "7.14.0", "workerd": "1.20251210.0", "ws": "8.18.0", "youch": "4.1.0-beta.10", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-k6kIoXwGVqlPZb0hcn+X7BmnK+8BjIIkusQPY22kCo2RaQJ/LzAjtxHQdGXerlHSnJyQivDQsL6BJHMpQfUFyw=="], "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], @@ -1408,7 +1408,7 @@ "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], - "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], + "qs": ["qs@6.14.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ=="], "react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="], @@ -1460,7 +1460,7 @@ "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], - "rollup": ["rollup@4.53.5", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.53.5", "@rollup/rollup-android-arm64": "4.53.5", "@rollup/rollup-darwin-arm64": "4.53.5", "@rollup/rollup-darwin-x64": "4.53.5", "@rollup/rollup-freebsd-arm64": "4.53.5", "@rollup/rollup-freebsd-x64": "4.53.5", "@rollup/rollup-linux-arm-gnueabihf": "4.53.5", "@rollup/rollup-linux-arm-musleabihf": "4.53.5", "@rollup/rollup-linux-arm64-gnu": "4.53.5", "@rollup/rollup-linux-arm64-musl": "4.53.5", "@rollup/rollup-linux-loong64-gnu": "4.53.5", "@rollup/rollup-linux-ppc64-gnu": "4.53.5", "@rollup/rollup-linux-riscv64-gnu": "4.53.5", "@rollup/rollup-linux-riscv64-musl": "4.53.5", "@rollup/rollup-linux-s390x-gnu": "4.53.5", "@rollup/rollup-linux-x64-gnu": "4.53.5", "@rollup/rollup-linux-x64-musl": "4.53.5", "@rollup/rollup-openharmony-arm64": "4.53.5", "@rollup/rollup-win32-arm64-msvc": "4.53.5", "@rollup/rollup-win32-ia32-msvc": "4.53.5", "@rollup/rollup-win32-x64-gnu": "4.53.5", "@rollup/rollup-win32-x64-msvc": "4.53.5", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ=="], + "rollup": ["rollup@4.54.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.54.0", "@rollup/rollup-android-arm64": "4.54.0", "@rollup/rollup-darwin-arm64": "4.54.0", "@rollup/rollup-darwin-x64": "4.54.0", "@rollup/rollup-freebsd-arm64": "4.54.0", "@rollup/rollup-freebsd-x64": "4.54.0", "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", "@rollup/rollup-linux-arm-musleabihf": "4.54.0", "@rollup/rollup-linux-arm64-gnu": "4.54.0", "@rollup/rollup-linux-arm64-musl": "4.54.0", "@rollup/rollup-linux-loong64-gnu": "4.54.0", "@rollup/rollup-linux-ppc64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-musl": "4.54.0", "@rollup/rollup-linux-s390x-gnu": "4.54.0", "@rollup/rollup-linux-x64-gnu": "4.54.0", "@rollup/rollup-linux-x64-musl": "4.54.0", "@rollup/rollup-openharmony-arm64": "4.54.0", "@rollup/rollup-win32-arm64-msvc": "4.54.0", "@rollup/rollup-win32-ia32-msvc": "4.54.0", "@rollup/rollup-win32-x64-gnu": "4.54.0", "@rollup/rollup-win32-x64-msvc": "4.54.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw=="], "rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], @@ -1478,7 +1478,7 @@ "seroval": ["seroval@1.4.2", "", {}, "sha512-N3HEHRCZYn3cQbsC4B5ldj9j+tHdf4JZoYPlcI4rRYu0Xy4qN8MQf1Z08EibzB0WpgRG5BGK08FTrmM66eSzKQ=="], - "seroval-plugins": ["seroval-plugins@1.4.0", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-zir1aWzoiax6pbBVjoYVd0O1QQXgIL3eVGBMsBsNmM8Ukq90yGaWlfx0AB9dTS8GPqrOrbXn79vmItCUP9U3BQ=="], + "seroval-plugins": ["seroval-plugins@1.4.2", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-X7p4MEDTi+60o2sXZ4bnDBhgsUYDSkQEvzYZuJyFqWg9jcoPsHts5nrg5O956py2wyt28lUrBxk0M0/wU8URpA=="], "sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="], @@ -1518,7 +1518,7 @@ "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], - "srvx": ["srvx@0.9.8", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-RZaxTKJEE/14HYn8COLuUOJAt0U55N9l1Xf6jj+T0GoA01EUH1Xz5JtSUOI+EHn+AEgPCVn7gk6jHJffrr06fQ=="], + "srvx": ["srvx@0.10.0", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-NqIsR+wQCfkvvwczBh8J8uM4wTZx41K2lLSEp/3oMp917ODVVMtW5Me4epCmQ3gH8D+0b+/t4xxkUKutyhimTA=="], "ssh-remote-port-forward": ["ssh-remote-port-forward@1.0.4", "", { "dependencies": { "@types/ssh2": "^0.5.48", "ssh2": "^1.4.0" } }, "sha512-x0LV1eVDwjf1gmG7TTnfqIzf+3VPRz7vrNIjX6oYLbeCrf/PeVY6hkT68Mg+q02qXxQhrLjB0jfgvhevoCRmLQ=="], @@ -1594,7 +1594,7 @@ "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], - "ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="], + "ts-api-utils": ["ts-api-utils@2.3.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-6eg3Y9SF7SsAvGzRHQvvc1skDAhwI4YQ32ui1scxD1Ccr0G5qIIbUBT3pFTKX8kmWIQClHobtUdNuaBgwdfdWg=="], "tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="], @@ -1614,7 +1614,7 @@ "undici": ["undici@7.14.0", "", {}, "sha512-Vqs8HTzjpQXZeXdpsfChQTlafcMQaaIwnGwLam1wudSSjlJeQ3bw1j+TLPePgrCnCpUXx7Ba5Pdpf5OBih62NQ=="], - "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], "unenv": ["unenv@2.0.0-rc.24", "", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="], @@ -1718,7 +1718,7 @@ "zip-stream": ["zip-stream@6.0.1", "", { "dependencies": { "archiver-utils": "^5.0.0", "compress-commons": "^6.0.2", "readable-stream": "^4.0.0" } }, "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA=="], - "zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="], + "zod": ["zod@4.3.3", "", {}, "sha512-bQ7Rxwfn04DCrTjjRfD9SavY2vWdmf3REjs/mkc1LdwI1KkcHClBRJmnvmA/6epGeqlHePtIRF1J4SrMMlW7IA=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], @@ -1728,8 +1728,6 @@ "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@cloudflare/vite-plugin/wrangler": ["wrangler@4.56.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.1", "@cloudflare/unenv-preset": "2.7.13", "blake3-wasm": "2.1.5", "esbuild": "0.27.0", "miniflare": "4.20251217.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20251217.0" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20251217.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-Nqi8duQeRbA+31QrD6QlWHW3IZVnuuRxMy7DEg46deUzywivmaRV/euBN5KKXDPtA24VyhYsK7I0tkb7P5DM2w=="], - "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], @@ -1780,7 +1778,7 @@ "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], - "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.0", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA=="], + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], @@ -1826,12 +1824,8 @@ "glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - "jsdom/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], - "lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], - "miniflare/workerd": ["workerd@1.20251217.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20251217.0", "@cloudflare/workerd-darwin-arm64": "1.20251217.0", "@cloudflare/workerd-linux-64": "1.20251217.0", "@cloudflare/workerd-linux-arm64": "1.20251217.0", "@cloudflare/workerd-windows-64": "1.20251217.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-s3mHDSWwHTduyY8kpHOsl27ZJ4ziDBJlc18PfBvNMqNnhO7yBeemlxH7bo7yQyU1foJrIZ6IENHDDg0Z9N8zQA=="], - "miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="], "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], @@ -1854,6 +1848,8 @@ "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "ssh-remote-port-forward/@types/ssh2": ["@types/ssh2@0.5.52", "", { "dependencies": { "@types/node": "*", "@types/ssh2-streams": "*" } }, "sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg=="], + "testcontainers/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], "tsx/esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], @@ -1864,16 +1860,10 @@ "wrangler/esbuild": ["esbuild@0.27.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.0", "@esbuild/android-arm": "0.27.0", "@esbuild/android-arm64": "0.27.0", "@esbuild/android-x64": "0.27.0", "@esbuild/darwin-arm64": "0.27.0", "@esbuild/darwin-x64": "0.27.0", "@esbuild/freebsd-arm64": "0.27.0", "@esbuild/freebsd-x64": "0.27.0", "@esbuild/linux-arm": "0.27.0", "@esbuild/linux-arm64": "0.27.0", "@esbuild/linux-ia32": "0.27.0", "@esbuild/linux-loong64": "0.27.0", "@esbuild/linux-mips64el": "0.27.0", "@esbuild/linux-ppc64": "0.27.0", "@esbuild/linux-riscv64": "0.27.0", "@esbuild/linux-s390x": "0.27.0", "@esbuild/linux-x64": "0.27.0", "@esbuild/netbsd-arm64": "0.27.0", "@esbuild/netbsd-x64": "0.27.0", "@esbuild/openbsd-arm64": "0.27.0", "@esbuild/openbsd-x64": "0.27.0", "@esbuild/openharmony-arm64": "0.27.0", "@esbuild/sunos-x64": "0.27.0", "@esbuild/win32-arm64": "0.27.0", "@esbuild/win32-ia32": "0.27.0", "@esbuild/win32-x64": "0.27.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA=="], - "wrangler/miniflare": ["miniflare@4.20251210.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "sharp": "^0.33.5", "stoppable": "1.1.0", "undici": "7.14.0", "workerd": "1.20251210.0", "ws": "8.18.0", "youch": "4.1.0-beta.10", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-k6kIoXwGVqlPZb0hcn+X7BmnK+8BjIIkusQPY22kCo2RaQJ/LzAjtxHQdGXerlHSnJyQivDQsL6BJHMpQfUFyw=="], - "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "@cloudflare/vite-plugin/wrangler/esbuild": ["esbuild@0.27.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.0", "@esbuild/android-arm": "0.27.0", "@esbuild/android-arm64": "0.27.0", "@esbuild/android-x64": "0.27.0", "@esbuild/darwin-arm64": "0.27.0", "@esbuild/darwin-x64": "0.27.0", "@esbuild/freebsd-arm64": "0.27.0", "@esbuild/freebsd-x64": "0.27.0", "@esbuild/linux-arm": "0.27.0", "@esbuild/linux-arm64": "0.27.0", "@esbuild/linux-ia32": "0.27.0", "@esbuild/linux-loong64": "0.27.0", "@esbuild/linux-mips64el": "0.27.0", "@esbuild/linux-ppc64": "0.27.0", "@esbuild/linux-riscv64": "0.27.0", "@esbuild/linux-s390x": "0.27.0", "@esbuild/linux-x64": "0.27.0", "@esbuild/netbsd-arm64": "0.27.0", "@esbuild/netbsd-x64": "0.27.0", "@esbuild/openbsd-arm64": "0.27.0", "@esbuild/openbsd-x64": "0.27.0", "@esbuild/openharmony-arm64": "0.27.0", "@esbuild/sunos-x64": "0.27.0", "@esbuild/win32-arm64": "0.27.0", "@esbuild/win32-ia32": "0.27.0", "@esbuild/win32-x64": "0.27.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA=="], - - "@cloudflare/vite-plugin/wrangler/workerd": ["workerd@1.20251217.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20251217.0", "@cloudflare/workerd-darwin-arm64": "1.20251217.0", "@cloudflare/workerd-linux-64": "1.20251217.0", "@cloudflare/workerd-linux-arm64": "1.20251217.0", "@cloudflare/workerd-windows-64": "1.20251217.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-s3mHDSWwHTduyY8kpHOsl27ZJ4ziDBJlc18PfBvNMqNnhO7yBeemlxH7bo7yQyU1foJrIZ6IENHDDg0Z9N8zQA=="], - "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], @@ -1934,16 +1924,6 @@ "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], - "miniflare/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20251217.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-DN6vT+9ho61d/1/YuILW4VS+N1JBLaixWRL1vqNmhgbf8J8VHwWWotrRruEUYigJKx2yZyw6YsasE+yLXgx/Fw=="], - - "miniflare/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20251217.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5nZOpRTkHmtcTc4Wbr1mj/O3dLb6aHZSiJuVBgtdbVcVmOXueSay3hnw1PXEyR+vpTKGUPkM+omUIslKHWnXDw=="], - - "miniflare/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20251217.0", "", { "os": "linux", "cpu": "x64" }, "sha512-uoPGhMaZVXPpCsU0oG3HQzyVpXCGi5rU+jcHRjUI7DXM4EwctBGvZ380Knkja36qtl+ZvSKVR1pUFSGdK+45Pg=="], - - "miniflare/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20251217.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-ixHnHKsiz1Xko+eDgCJOZ7EEUZKtmnYq3AjW3nkVcLFypSLks4C29E45zVewdaN4wq8sCLeyQCl6r1kS17+DQQ=="], - - "miniflare/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20251217.0", "", { "os": "win32", "cpu": "x64" }, "sha512-rP6USX+7ctynz3AtmKi+EvlLP3Xdr1ETrSdcnv693/I5QdUwBxq4yE1Lj6CV7GJizX6opXKYg8QMq0Q4eB9zRQ=="], - "pg/pg-types/postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], "pg/pg-types/postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="], @@ -2110,70 +2090,6 @@ "wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.0", "", { "os": "win32", "cpu": "x64" }, "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg=="], - "wrangler/miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="], - - "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A=="], - - "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.0", "", { "os": "android", "cpu": "arm" }, "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ=="], - - "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.0", "", { "os": "android", "cpu": "arm64" }, "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ=="], - - "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.0", "", { "os": "android", "cpu": "x64" }, "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q=="], - - "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg=="], - - "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g=="], - - "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw=="], - - "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g=="], - - "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ=="], - - "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ=="], - - "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw=="], - - "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg=="], - - "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg=="], - - "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA=="], - - "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ=="], - - "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w=="], - - "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.0", "", { "os": "linux", "cpu": "x64" }, "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw=="], - - "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.0", "", { "os": "none", "cpu": "arm64" }, "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w=="], - - "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.0", "", { "os": "none", "cpu": "x64" }, "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA=="], - - "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ=="], - - "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A=="], - - "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.0", "", { "os": "none", "cpu": "arm64" }, "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA=="], - - "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA=="], - - "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg=="], - - "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ=="], - - "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.0", "", { "os": "win32", "cpu": "x64" }, "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg=="], - - "@cloudflare/vite-plugin/wrangler/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20251217.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-DN6vT+9ho61d/1/YuILW4VS+N1JBLaixWRL1vqNmhgbf8J8VHwWWotrRruEUYigJKx2yZyw6YsasE+yLXgx/Fw=="], - - "@cloudflare/vite-plugin/wrangler/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20251217.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5nZOpRTkHmtcTc4Wbr1mj/O3dLb6aHZSiJuVBgtdbVcVmOXueSay3hnw1PXEyR+vpTKGUPkM+omUIslKHWnXDw=="], - - "@cloudflare/vite-plugin/wrangler/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20251217.0", "", { "os": "linux", "cpu": "x64" }, "sha512-uoPGhMaZVXPpCsU0oG3HQzyVpXCGi5rU+jcHRjUI7DXM4EwctBGvZ380Knkja36qtl+ZvSKVR1pUFSGdK+45Pg=="], - - "@cloudflare/vite-plugin/wrangler/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20251217.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-ixHnHKsiz1Xko+eDgCJOZ7EEUZKtmnYq3AjW3nkVcLFypSLks4C29E45zVewdaN4wq8sCLeyQCl6r1kS17+DQQ=="], - - "@cloudflare/vite-plugin/wrangler/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20251217.0", "", { "os": "win32", "cpu": "x64" }, "sha512-rP6USX+7ctynz3AtmKi+EvlLP3Xdr1ETrSdcnv693/I5QdUwBxq4yE1Lj6CV7GJizX6opXKYg8QMq0Q4eB9zRQ=="], - "dockerode/tar-fs/tar-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], } } diff --git a/cloud/errors.ts b/cloud/errors.ts index 8599331572..0e2d166b74 100644 --- a/cloud/errors.ts +++ b/cloud/errors.ts @@ -156,3 +156,34 @@ export class StripeError extends Schema.TaggedError()( ) { static readonly status = 500 as const; } + +// ============================================================================= +// Proxy Errors +// ============================================================================= + +/** + * Error that occurs during AI provider proxy operations. + * + * This error wraps failures when proxying requests to external AI providers + * (OpenAI, Anthropic, Google AI), including: + * - Network errors + * - Provider API errors + * - Configuration errors (missing API keys) + * - Timeout errors + * + * @example + * ```ts + * const response = yield* proxyToProvider({ ... }).pipe( + * Effect.catchTag("ProxyError", (error) => { + * console.error("Proxy failed:", error.message); + * return Effect.fail(new InternalError({ message: "Provider unavailable" })); + * }) + * ); + * ``` + */ +export class ProxyError extends Schema.TaggedError()("ProxyError", { + message: Schema.String, + cause: Schema.optional(Schema.Unknown), +}) { + static readonly status = 502 as const; +} diff --git a/cloud/package.json b/cloud/package.json index aeeee65535..70372e3655 100644 --- a/cloud/package.json +++ b/cloud/package.json @@ -86,10 +86,10 @@ }, "devDependencies": { "@cloudflare/vite-plugin": "^1.9.0", - "@testcontainers/postgresql": "^10.25.0", "@cloudflare/workers-types": "^4.20241127.0", "@effect/vitest": "^0.27.0", "@eslint/js": "^9.37.0", + "@testcontainers/postgresql": "^10.25.0", "@testing-library/dom": "^10.4.0", "@testing-library/react": "^16.2.0", "@types/bun": "latest", diff --git a/cloud/settings.ts b/cloud/settings.ts index a6a4976b94..3df9ef3c1b 100644 --- a/cloud/settings.ts +++ b/cloud/settings.ts @@ -10,6 +10,10 @@ export type Settings = { readonly GITHUB_CLIENT_SECRET?: string; readonly GITHUB_CALLBACK_URL?: string; readonly SITE_URL?: string; + // ROUTER KEYS + readonly ANTHROPIC_API_KEY?: string; + readonly GEMINI_API_KEY?: string; + readonly OPENAI_API_KEY?: string; }; export class SettingsService extends Context.Tag("SettingsService")< @@ -28,5 +32,9 @@ export function getSettings(): Settings { GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET, GOOGLE_CALLBACK_URL: process.env.GOOGLE_CALLBACK_URL, SITE_URL: process.env.SITE_URL, + // ROUTER KEYS + ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY, + GEMINI_API_KEY: process.env.GEMINI_API_KEY, + OPENAI_API_KEY: process.env.OPENAI_API_KEY, }; }