├── .vscode └── settings.json ├── test ├── env.d.ts ├── tsconfig.json └── convertMessagesToFalPrompt.test.ts ├── .prettierrc ├── .cursorrules ├── .editorconfig ├── vitest.config.mts ├── package.json ├── wrangler.jsonc ├── tsconfig.json ├── .gitignore ├── README.md ├── .cursor └── rules │ └── fal-openai-proxy-testing.mdc ├── src └── index.ts └── pnpm-lock.yaml /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "wrangler.json": "jsonc" 4 | } 5 | } -------------------------------------------------------------------------------- /test/env.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'cloudflare:test' { 2 | interface ProvidedEnv extends Env {} 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 140, 3 | "singleQuote": true, 4 | "semi": true, 5 | "useTabs": true 6 | } 7 | -------------------------------------------------------------------------------- /.cursorrules: -------------------------------------------------------------------------------- 1 | - Framework: Cloudflare Worker + Typescript 2 | - All Cloudflare Configuration should use wrangler.jsonc format, refer https://developers.cloudflare.com/workers/wrangler/configuration/ 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.yml] 12 | indent_style = space 13 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["@cloudflare/workers-types/experimental", "@cloudflare/vitest-pool-workers"] 5 | }, 6 | "include": ["./**/*.ts", "../worker-configuration.d.ts"], 7 | "exclude": [] 8 | } 9 | -------------------------------------------------------------------------------- /vitest.config.mts: -------------------------------------------------------------------------------- 1 | import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config'; 2 | 3 | export default defineWorkersConfig({ 4 | test: { 5 | poolOptions: { 6 | workers: { 7 | wrangler: { configPath: './wrangler.jsonc' }, 8 | }, 9 | }, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fal-openai-proxy-cf", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "deploy": "wrangler deploy", 7 | "dev": "wrangler dev", 8 | "start": "wrangler dev", 9 | "test": "vitest", 10 | "cf-typegen": "wrangler types" 11 | }, 12 | "devDependencies": { 13 | "@cloudflare/vitest-pool-workers": "^0.8.36", 14 | "@cloudflare/workers-types": "^4.20250605.0", 15 | "typescript": "^5.8.3", 16 | "vite": "6.2.7", 17 | "vitest": "~3.1.3", 18 | "wrangler": "^4.19.1" 19 | }, 20 | "dependencies": { 21 | "@fal-ai/client": "^1.5.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /wrangler.jsonc: -------------------------------------------------------------------------------- 1 | /** 2 | * For more details on how to configure Wrangler, refer to: 3 | * https://developers.cloudflare.com/workers/wrangler/configuration/ 4 | */ 5 | { 6 | "$schema": "node_modules/wrangler/config-schema.json", 7 | "name": "fal-openai", 8 | "main": "src/index.ts", 9 | "compatibility_date": "2025-04-12", 10 | "observability": { 11 | "enabled": true 12 | } 13 | /** 14 | * Smart Placement 15 | * Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement 16 | */ 17 | // "placement": { "mode": "smart" }, 18 | 19 | /** 20 | * Bindings 21 | * Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including 22 | * databases, object storage, AI inference, real-time communication and more. 23 | * https://developers.cloudflare.com/workers/runtime-apis/bindings/ 24 | */ 25 | 26 | /** 27 | * Environment Variables 28 | * https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables 29 | */ 30 | // "vars": { "MY_VARIABLE": "production_value" }, 31 | /** 32 | * Note: Use secrets to store sensitive data. 33 | * https://developers.cloudflare.com/workers/configuration/secrets/ 34 | */ 35 | 36 | /** 37 | * Static Assets 38 | * https://developers.cloudflare.com/workers/static-assets/binding/ 39 | */ 40 | // "assets": { "directory": "./public/", "binding": "ASSETS" }, 41 | 42 | /** 43 | * Service Bindings (communicate between multiple Workers) 44 | * https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings 45 | */ 46 | // "services": [{ "binding": "MY_SERVICE", "service": "my-service" }] 47 | } 48 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 6 | "target": "es2021", 7 | /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 8 | "lib": ["es2021"], 9 | /* Specify what JSX code is generated. */ 10 | "jsx": "react-jsx", 11 | 12 | /* Specify what module code is generated. */ 13 | "module": "es2022", 14 | /* Specify how TypeScript looks up a file from a given module specifier. */ 15 | "moduleResolution": "Bundler", 16 | /* Specify type package names to be included without being referenced in a source file. */ 17 | "types": [ 18 | "@cloudflare/workers-types/2023-07-01" 19 | ], 20 | /* Enable importing .json files */ 21 | "resolveJsonModule": true, 22 | 23 | /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 24 | "allowJs": true, 25 | /* Enable error reporting in type-checked JavaScript files. */ 26 | "checkJs": false, 27 | 28 | /* Disable emitting files from a compilation. */ 29 | "noEmit": true, 30 | 31 | /* Ensure that each file can be safely transpiled without relying on other imports. */ 32 | "isolatedModules": true, 33 | /* Allow 'import x from y' when a module doesn't have a default export. */ 34 | "allowSyntheticDefaultImports": true, 35 | /* Ensure that casing is correct in imports. */ 36 | "forceConsistentCasingInFileNames": true, 37 | 38 | /* Enable all strict type-checking options. */ 39 | "strict": true, 40 | 41 | /* Skip type checking all .d.ts files. */ 42 | "skipLibCheck": true 43 | }, 44 | "exclude": ["test"], 45 | "include": ["worker-configuration.d.ts", "src/**/*.ts"] 46 | } 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | 3 | logs 4 | _.log 5 | npm-debug.log_ 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | .pnpm-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | 13 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 14 | 15 | # Runtime data 16 | 17 | pids 18 | _.pid 19 | _.seed 20 | \*.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | 28 | coverage 29 | \*.lcov 30 | 31 | # nyc test coverage 32 | 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 36 | 37 | .grunt 38 | 39 | # Bower dependency directory (https://bower.io/) 40 | 41 | bower_components 42 | 43 | # node-waf configuration 44 | 45 | .lock-wscript 46 | 47 | # Compiled binary addons (https://nodejs.org/api/addons.html) 48 | 49 | build/Release 50 | 51 | # Dependency directories 52 | 53 | node_modules/ 54 | jspm_packages/ 55 | 56 | # Snowpack dependency directory (https://snowpack.dev/) 57 | 58 | web_modules/ 59 | 60 | # TypeScript cache 61 | 62 | \*.tsbuildinfo 63 | 64 | # Optional npm cache directory 65 | 66 | .npm 67 | 68 | # Optional eslint cache 69 | 70 | .eslintcache 71 | 72 | # Optional stylelint cache 73 | 74 | .stylelintcache 75 | 76 | # Microbundle cache 77 | 78 | .rpt2_cache/ 79 | .rts2_cache_cjs/ 80 | .rts2_cache_es/ 81 | .rts2_cache_umd/ 82 | 83 | # Optional REPL history 84 | 85 | .node_repl_history 86 | 87 | # Output of 'npm pack' 88 | 89 | \*.tgz 90 | 91 | # Yarn Integrity file 92 | 93 | .yarn-integrity 94 | 95 | # dotenv environment variable files 96 | 97 | .env 98 | .env.development.local 99 | .env.test.local 100 | .env.production.local 101 | .env.local 102 | 103 | # parcel-bundler cache (https://parceljs.org/) 104 | 105 | .cache 106 | .parcel-cache 107 | 108 | # Next.js build output 109 | 110 | .next 111 | out 112 | 113 | # Nuxt.js build / generate output 114 | 115 | .nuxt 116 | dist 117 | 118 | # Gatsby files 119 | 120 | .cache/ 121 | 122 | # Comment in the public line in if your project uses Gatsby and not Next.js 123 | 124 | # https://nextjs.org/blog/next-9-1#public-directory-support 125 | 126 | # public 127 | 128 | # vuepress build output 129 | 130 | .vuepress/dist 131 | 132 | # vuepress v2.x temp and cache directory 133 | 134 | .temp 135 | .cache 136 | 137 | # Docusaurus cache and generated files 138 | 139 | .docusaurus 140 | 141 | # Serverless directories 142 | 143 | .serverless/ 144 | 145 | # FuseBox cache 146 | 147 | .fusebox/ 148 | 149 | # DynamoDB Local files 150 | 151 | .dynamodb/ 152 | 153 | # TernJS port file 154 | 155 | .tern-port 156 | 157 | # Stores VSCode versions used for testing VSCode extensions 158 | 159 | .vscode-test 160 | 161 | # yarn v2 162 | 163 | .yarn/cache 164 | .yarn/unplugged 165 | .yarn/build-state.yml 166 | .yarn/install-state.gz 167 | .pnp.\* 168 | 169 | # wrangler project 170 | 171 | .dev.vars 172 | .wrangler/ 173 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fal.ai OpenAI-compatible API Proxy for Cloudflare Workers 2 | 3 | 本项目提供了一个 Cloudflare Worker,将 fal.ai API 转成 OpenAI API 兼容格式。 4 | 5 | > [!WARNING] 6 | > 仅支持文本对话,不支持文件/图片上传 7 | 8 | ## 部署 9 | 10 | 1. **Fork 本仓库:** 点击 GitHub 页面右上角的 "Fork" 按钮,将此仓库复制到您的 GitHub 账户下。 11 | 2. **登录 Cloudflare:** 打开浏览器,访问 [Cloudflare Dashboard](https://dash.cloudflare.com/) 并登录您的账户。 12 | 3. **连接 GitHub 账户 (如果尚未连接):** 13 | * 在 Cloudflare Dashboard 中,导航至 "Workers & Pages"。 14 | * 点击 "概述 (Overview)" > "连接到 Git (Connect to Git)"。 15 | * 选择 GitHub 并按照提示授权 Cloudflare 访问您的 GitHub 仓库。 16 | 4. **创建 Worker 服务:** 17 | * 在 "Workers & Pages" 页面,点击 "创建应用程序 (Create application)" > "Pages" 选项卡 > "连接到 Git (Connect to Git)"。 18 | * 选择您 Fork 的仓库,点击 "开始设置 (Begin setup)"。 19 | * **项目名称 (Project name):** 输入您想要的 Worker 服务名称 (例如 `my-fal-proxy`)。 20 | * **设置构建和部署 (Set up builds and deployments):** 此部分所有设置保持默认即可。 21 | * 点击 "保存并部署 (Save and Deploy)"。 22 | 5. **配置环境变量:** 23 | * 部署完成后,导航至您新创建的 Worker 服务的设置页面。 24 | * 选择 "设置 (Settings)" > "变量 (Variables)"。 25 | * 在 "环境变量 (Environment Variables)" 部分,点击 "添加变量 (Add variable)",添加以下两个变量: 26 | * `FAL_KEY`: 输入您从 fal.ai 获取的 API Key。勾选 "加密 (Encrypt)" 以保护密钥。 27 | * `API_KEY`: 输入您希望用于访问此代理服务的自定义 API Key (可以自行设置一个强密码)。勾选 "加密 (Encrypt)"。 28 | * 点击 "保存 (Save)"。 29 | 30 | 部署完成后,您的 Worker 服务 URL (例如 `your-worker-name.your-subdomain.workers.dev`) 即为 OpenAI 兼容的 API Endpoint。 31 | 32 | ## 本地开发 33 | 34 | 您可以使用 Cloudflare 的 Wrangler CLI 在本地进行开发和测试。您需要安装 [Node.js](https://nodejs.org/) (包含 npm)。 35 | 36 | 1. **登录 Wrangler:** 37 | ```bash 38 | npx wrangler login 39 | ``` 40 | 这将打开浏览器提示您登录 Cloudflare 账户。 41 | 2. **克隆仓库:** 42 | ```bash 43 | git clone 44 | cd 45 | ``` 46 | 3. **安装依赖:** 47 | ```bash 48 | npm install 49 | # 或者 50 | # yarn install 51 | ``` 52 | 4. **创建 `.dev.vars` 文件:** 53 | 在项目根目录下创建一个名为 `.dev.vars` 的文件,并添加您的环境变量: 54 | ``` 55 | FAL_KEY="your_fal_ai_key" 56 | API_KEY="your_custom_api_key" 57 | ``` 58 | 两个 KEY 都支持多个传入,通过英文逗号分隔,因此 API_KEY 中不能包含逗号 59 | 60 | **注意:** 不要将此文件提交到 Git 仓库。`.gitignore` 文件已配置忽略此文件。 61 | 5. **启动本地开发服务器:** 62 | ```bash 63 | npx wrangler dev 64 | ``` 65 | Wrangler 将启动一个本地服务器 (通常在 `http://localhost:8787`),模拟 Cloudflare 环境。您可以使用此本地 URL 进行测试。 66 | 67 | ## 使用 68 | 69 | 将您的 OpenAI 客户端配置为使用您的 Cloudflare Worker URL 作为 API Base URL,并使用您在环境变量中设置的 `API_KEY` 作为 Bearer Token 进行身份验证。 70 | 71 | **示例 (使用 `curl`):** 72 | 73 | ```bash 74 | curl https://your-worker-name.your-subdomain.workers.dev/v1/chat/completions \ 75 | -H "Authorization: Bearer your_custom_api_key" \ 76 | -H "Content-Type: application/json" \ 77 | -d '{ 78 | "model": "fal-ai/Mixtral-8x7B-Instruct-v0.1", # 或其他 fal.ai 支持的模型 79 | "messages": [{"role": "user", "content": "你好!"}] 80 | }' 81 | ``` 82 | 83 | 将 `your-worker-name.your-subdomain.workers.dev` 替换为您的 Worker URL,并将 `your_custom_api_key` 替换为您设置的 `API_KEY`。将 `"fal-ai/Mixtral-8x7B-Instruct-v0.1"` 替换为您想使用的 fal.ai 模型。 84 | -------------------------------------------------------------------------------- /.cursor/rules/fal-openai-proxy-testing.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: 4 | alwaysApply: true 5 | --- 6 | # FAL OpenAI CF Proxy Testing Rules 7 | 8 | ## Testing Framework & Conventions 9 | 10 | ### Test File Naming 11 | - All test files must use `.test.ts` suffix 12 | - Test files should be located in the `test/` directory 13 | 14 | ### Testing Standards 15 | 16 | #### Character Limit Testing 17 | - **Production Character Limits**: 18 | - `SYSTEM_PROMPT_LIMIT = 4800` 19 | - **Note**: The actual character length limit is 5000, but 4800 is used with pre-reserved redundancy space 20 | - **No prompt character limit**: The `prompt` part does not have character restrictions 21 | 22 | #### Test Implementation Guidelines 23 | - **Use Source Functions**: Always import and test the actual functions from [src/index.ts](mdc:src/index.ts) rather than implementing duplicate test versions 24 | - **Character Limit Testing**: Use smaller test limits (100, 500, 1000) to simulate character overflow scenarios without using actual production limits 25 | - **Edge Case Coverage**: Include tests for: 26 | - Empty messages 27 | - Null content handling 28 | - System message overflow 29 | - Message truncation scenarios 30 | - Boundary conditions 31 | 32 | #### Core Function Testing: convertMessagesToFalPrompt 33 | 34 | The main function [convertMessagesToFalPrompt](mdc:src/index.ts) implements V3 logic with these characteristics: 35 | 36 | 1. **System Message Handling**: 37 | - Only the last system message is used 38 | - System message length is validated against SYSTEM_PROMPT_LIMIT 39 | - Returns error if system message exceeds limit 40 | 41 | 2. **Message Distribution**: 42 | - **prompt**: Contains last 3 message records (previous user + assistant + current user) 43 | - **system_prompt**: Contains system message + earlier chat history (if any) 44 | - Older messages are truncated if they exceed character limits 45 | 46 | 3. **Test Coverage Requirements**: 47 | - Basic message handling (system, user, assistant combinations) 48 | - Multi-turn conversation scenarios 49 | - Character limit enforcement with small test values 50 | - Null content and edge cases 51 | - Message truncation behavior 52 | 53 | #### Example Test Pattern 54 | ```typescript 55 | // Use small limits for testing character overflow 56 | it('应该处理字符限制测试', () => { 57 | const TEST_LIMIT = 100; // Small test limit 58 | const longMessage = 'a'.repeat(TEST_LIMIT + 10); 59 | // Test character overflow handling... 60 | }); 61 | ``` 62 | 63 | ### Function Import Pattern 64 | Always import from the main source file: 65 | ```typescript 66 | import { convertMessagesToFalPrompt, OpenAIMessage, SYSTEM_PROMPT_LIMIT } from '../src/index'; 67 | ``` 68 | 69 | ### Cloudflare Worker Context 70 | - Framework: Cloudflare Worker + TypeScript 71 | - Configuration: Use `wrangler.jsonc` format for all Cloudflare configurations 72 | - Test environment: Vitest with Cloudflare Workers runtime 73 | 74 | ## Character Limit Philosophy 75 | - Production limits are set at 4800 to provide safety buffer below the actual 5000 character limit 76 | - Tests should verify truncation and error handling without using production limit values 77 | - Focus on correctness of limit enforcement rather than specific numeric values 78 | -------------------------------------------------------------------------------- /test/convertMessagesToFalPrompt.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * convertMessagesToFalPrompt 函数单元测试 3 | * 4 | * 核心逻辑:倒序遍历messages,至多取3条user/assistant消息放到prompt部分 5 | * 6 | * 实现逻辑: 7 | * 1. 分离系统消息和对话消息,只使用最后一个非空系统消息 8 | * 2. 倒序遍历,取最后3条消息,根据最后一条消息类型分两种情况: 9 | * a. 最后一条消息是user消息:取倒数第3、第2条作为chat_history(最多2条:user+assistant), 10 | * 格式为 "\nAssistant: \n\n<最新用户消息>" 11 | * b. 最后一条消息不是user消息:取最后2条消息作为chat_history,放在system_prompt中, 12 | * 格式为 "\nAssistant: \n" 13 | * 3. prompt中会自动拼接Human消息,所以chat_history最多包含2条:user和assistant 14 | * 4. 最后一个用户消息是最新提问,不属于对话历史 15 | * 5. 系统消息如果超出SYSTEM_PROMPT_LIMIT则直接返回错误 16 | * 6. 只有非空内容的消息才会被处理 17 | */ 18 | 19 | import { describe, it, expect } from 'vitest'; 20 | import { convertMessagesToFalPrompt, OpenAIMessage } from '../src/index'; 21 | 22 | // Define the constant locally for testing purposes 23 | const SYSTEM_PROMPT_LIMIT = 4800; 24 | 25 | describe('convertMessagesToFalPrompt', () => { 26 | // === 基础测试用例 === 27 | 28 | it('应该正确处理只有系统消息的情况', () => { 29 | const messages: OpenAIMessage[] = [ 30 | { role: 'system', content: 'You are a helpful assistant.' } 31 | ]; 32 | 33 | const result = convertMessagesToFalPrompt(messages); 34 | 35 | expect(result.system_prompt).toBe('You are a helpful assistant.'); 36 | expect(result.prompt).toBe(''); 37 | expect(result.error).toBeUndefined(); 38 | }); 39 | 40 | it('应该正确处理空消息数组', () => { 41 | const messages: OpenAIMessage[] = []; 42 | 43 | const result = convertMessagesToFalPrompt(messages); 44 | 45 | expect(result.system_prompt).toBe(''); 46 | expect(result.prompt).toBe(''); 47 | expect(result.error).toBeUndefined(); 48 | }); 49 | 50 | it('应该正确处理只有用户消息的情况', () => { 51 | const messages: OpenAIMessage[] = [ 52 | { role: 'user', content: 'Hello, how are you?' } 53 | ]; 54 | 55 | const result = convertMessagesToFalPrompt(messages); 56 | 57 | expect(result.system_prompt).toBe(''); 58 | expect(result.prompt).toBe('Hello, how are you?'); 59 | expect(result.error).toBeUndefined(); 60 | }); 61 | 62 | it('应该正确处理最后一条消息不是用户消息的情况', () => { 63 | const messages: OpenAIMessage[] = [ 64 | { role: 'user', content: 'Hello' }, 65 | { role: 'assistant', content: 'Hi there!' } 66 | ]; 67 | 68 | const result = convertMessagesToFalPrompt(messages); 69 | 70 | // 倒序遍历至多取3条user/assistant消息放到prompt:Hi there! + Hello 71 | // 但最后一条消息不是用户消息,按照要求格式处理 72 | expect(result.system_prompt).toBe(''); 73 | expect(result.prompt).toBe('Hello\nAssistant: Hi there!'); 74 | expect(result.error).toBeUndefined(); 75 | }); 76 | 77 | it('应该正确处理有系统消息但最后一条不是用户消息的情况', () => { 78 | const messages: OpenAIMessage[] = [ 79 | { role: 'system', content: 'You are a helpful assistant.' }, 80 | { role: 'user', content: 'Hello' }, 81 | { role: 'assistant', content: 'Hi there!' } 82 | ]; 83 | 84 | const result = convertMessagesToFalPrompt(messages); 85 | 86 | // 倒序遍历至多取3条user/assistant消息放到prompt:Hi there! + Hello 87 | // 剩余的messages放到system_prompt:system 88 | // 但最后一条消息不是用户消息,按照要求格式处理 89 | expect(result.system_prompt).toBe('You are a helpful assistant.'); 90 | expect(result.prompt).toBe('Hello\nAssistant: Hi there!'); 91 | expect(result.error).toBeUndefined(); 92 | }); 93 | 94 | // === 核心逻辑测试用例 === 95 | 96 | it('应该正确处理系统消息+用户消息', () => { 97 | const messages: OpenAIMessage[] = [ 98 | { role: 'system', content: 'You are a helpful assistant.' }, 99 | { role: 'user', content: 'What is 2+2?' } 100 | ]; 101 | 102 | const result = convertMessagesToFalPrompt(messages); 103 | 104 | expect(result.system_prompt).toBe('You are a helpful assistant.'); 105 | expect(result.prompt).toBe('What is 2+2?'); 106 | expect(result.error).toBeUndefined(); 107 | }); 108 | 109 | it('应该正确处理三条消息(用户+助手+用户)- 核心逻辑', () => { 110 | const messages: OpenAIMessage[] = [ 111 | { role: 'system', content: 'You are a helpful assistant.' }, 112 | { role: 'user', content: 'What is 2+2?' }, 113 | { role: 'assistant', content: '2+2 equals 4.' }, 114 | { role: 'user', content: 'What about 3+3?' } 115 | ]; 116 | 117 | const result = convertMessagesToFalPrompt(messages); 118 | 119 | // 倒序遍历至多取3条user/assistant消息放到prompt:What about 3+3? + 2+2 equals 4. + What is 2+2? 120 | // 剩余的messages放到system_prompt:system 121 | expect(result.system_prompt).toBe('You are a helpful assistant.'); 122 | // prompt包含倒序的最后3条消息 123 | expect(result.prompt).toBe('What is 2+2?\nAssistant: 2+2 equals 4.\nWhat about 3+3?'); 124 | expect(result.error).toBeUndefined(); 125 | }); 126 | 127 | it('应该正确处理多条消息,历史对话放在prompt中', () => { 128 | const messages: OpenAIMessage[] = [ 129 | { role: 'system', content: 'You are a helpful assistant.' }, 130 | { role: 'user', content: 'First question' }, 131 | { role: 'assistant', content: 'First answer' }, 132 | { role: 'user', content: 'Second question' }, 133 | { role: 'assistant', content: 'Second answer' }, 134 | { role: 'user', content: 'Third question' } 135 | ]; 136 | 137 | const result = convertMessagesToFalPrompt(messages); 138 | 139 | // 倒序遍历至多取3条user/assistant消息放到prompt:Third question + Second answer + Second question 140 | // 剩余的messages放到system_prompt:system + First question + First answer 141 | expect(result.system_prompt).toBe('You are a helpful assistant.\nHuman: First question\nAssistant: First answer'); 142 | 143 | // prompt包含倒序的最后3条消息 144 | expect(result.prompt).toBe('Second question\nAssistant: Second answer\nThird question'); 145 | expect(result.error).toBeUndefined(); 146 | }); 147 | 148 | it('应该正确处理null content', () => { 149 | const messages: OpenAIMessage[] = [ 150 | { role: 'system', content: null }, 151 | { role: 'user', content: null }, 152 | { role: 'assistant', content: null }, 153 | { role: 'user', content: 'Hello' } 154 | ]; 155 | 156 | const result = convertMessagesToFalPrompt(messages); 157 | 158 | // 空消息被过滤后,只剩下最后一条有效的用户消息 159 | expect(result.system_prompt).toBe(''); 160 | expect(result.prompt).toBe('Hello'); 161 | expect(result.error).toBeUndefined(); 162 | }); 163 | 164 | // === 系统消息处理测试 === 165 | 166 | it('应该只使用最后一个系统消息', () => { 167 | const messages: OpenAIMessage[] = [ 168 | { role: 'system', content: 'First system message' }, 169 | { role: 'system', content: 'Second system message' }, 170 | { role: 'user', content: 'Hello' } 171 | ]; 172 | 173 | const result = convertMessagesToFalPrompt(messages); 174 | 175 | expect(result.system_prompt).toBe('Second system message'); 176 | expect(result.prompt).toBe('Hello'); 177 | expect(result.error).toBeUndefined(); 178 | }); 179 | 180 | it('应该在系统消息超出限制时返回错误', () => { 181 | const longSystemMessage = 'a'.repeat(SYSTEM_PROMPT_LIMIT + 100); 182 | const messages: OpenAIMessage[] = [ 183 | { role: 'system', content: longSystemMessage }, 184 | { role: 'user', content: 'Hello' } 185 | ]; 186 | 187 | const result = convertMessagesToFalPrompt(messages); 188 | 189 | expect(result.system_prompt).toBe(''); 190 | expect(result.prompt).toBe(''); 191 | expect(result.error).toContain('System message too long'); 192 | expect(result.error).toContain(`${longSystemMessage.length} characters exceeds limit of ${SYSTEM_PROMPT_LIMIT}`); 193 | }); 194 | 195 | // === 字符限制测试用例 === 196 | 197 | it('应该正确处理system_prompt在限值内的情况', () => { 198 | // 创建一个接近但不超过 4800 字符限制的系统消息 199 | const systemMessage = 'You are a helpful assistant. '.repeat(150); // 约 4200 字符 200 | const messages: OpenAIMessage[] = [ 201 | { role: 'system', content: systemMessage }, 202 | { role: 'user', content: 'Hello' }, 203 | { role: 'assistant', content: 'Hi there!' }, 204 | { role: 'user', content: 'How are you?' } 205 | ]; 206 | 207 | const result = convertMessagesToFalPrompt(messages); 208 | 209 | // 验证没有错误,且系统消息被正确处理 210 | expect(result.error).toBeUndefined(); 211 | expect(result.system_prompt).toBe(systemMessage.trim()); 212 | expect(result.system_prompt.length).toBeLessThanOrEqual(SYSTEM_PROMPT_LIMIT); 213 | 214 | // 倒序遍历至多取3条user/assistant消息放到prompt:How are you? + Hi there! + Hello 215 | expect(result.prompt).toBe('Hello\nAssistant: Hi there!\nHow are you?'); 216 | }); 217 | 218 | it('应该正确截断历史消息,选取最后一个assistant,截断 user 消息以适应字符限制', () => { 219 | // 创建会导致 system_prompt 超过限制的大量历史消息 220 | const systemMessage = 'You are a helpful assistant.'; 221 | const longMessage = 'This is a very long message that will help us test the truncation behavior. '.repeat(50); // 约 4000 字符 222 | 223 | const messages: OpenAIMessage[] = [ 224 | { role: 'system', content: systemMessage }, 225 | { role: 'user', content: longMessage }, 226 | { role: 'assistant', content: longMessage }, 227 | { role: 'user', content: longMessage }, 228 | { role: 'assistant', content: longMessage }, 229 | { role: 'user', content: 'Second question' }, 230 | { role: 'assistant', content: 'Second answer' }, 231 | { role: 'user', content: 'Final question' } 232 | ]; 233 | 234 | const result = convertMessagesToFalPrompt(messages); 235 | 236 | // 验证没有错误 237 | expect(result.error).toBeUndefined(); 238 | 239 | // 验证system_prompt不超过限制 240 | expect(result.system_prompt.length).toBeLessThanOrEqual(SYSTEM_PROMPT_LIMIT); 241 | 242 | // 倒序遍历至多取3条user/assistant消息放到prompt:Final question + Second answer + Second question 243 | // 剩余的messages放到system_prompt:system + longMessage(user) + longMessage(assistant) + longMessage(user) + longMessage(assistant) 244 | // 但由于字符限制,system_prompt会被截断,优先保留系统消息 245 | expect(result.system_prompt).toContain('You are a helpful assistant.'); 246 | // 由于longMessage太长,可能只能包含系统消息和部分assistant消息 247 | expect(result.system_prompt).toContain('Assistant:'); 248 | 249 | // prompt包含倒序的最后3条消息 250 | expect(result.prompt).toBe('Second question\nAssistant: Second answer\nFinal question'); 251 | }); 252 | 253 | it('应该在多个系统消息超出限制时仍然只检查最后一个', () => { 254 | const shortSystemMessage = 'Short system message'; 255 | const longSystemMessage = 'a'.repeat(SYSTEM_PROMPT_LIMIT + 100); 256 | 257 | const messages: OpenAIMessage[] = [ 258 | { role: 'system', content: shortSystemMessage }, 259 | { role: 'system', content: longSystemMessage }, // 这个会覆盖前面的 260 | { role: 'user', content: 'Hello' } 261 | ]; 262 | 263 | const result = convertMessagesToFalPrompt(messages); 264 | 265 | expect(result.error).toContain('System message too long'); 266 | expect(result.system_prompt).toBe(''); 267 | expect(result.prompt).toBe(''); 268 | }); 269 | 270 | it('应该正确处理复杂的多轮对话场景', () => { 271 | // 测试真实的多轮对话场景 272 | const systemMessage = 'You are a helpful AI assistant that provides detailed explanations.'; 273 | const user1 = 'Can you explain how machine learning works?'; 274 | const assistant1 = 'Machine learning is a subset of artificial intelligence that enables computers to learn and improve from experience without being explicitly programmed.'; 275 | const user2 = 'What are the main types of machine learning?'; 276 | const assistant2 = 'There are three main types: supervised learning, unsupervised learning, and reinforcement learning.'; 277 | const user3 = 'Can you give me an example of supervised learning?'; 278 | 279 | const messages: OpenAIMessage[] = [ 280 | { role: 'system', content: systemMessage }, 281 | { role: 'user', content: user1 }, 282 | { role: 'assistant', content: assistant1 }, 283 | { role: 'user', content: user2 }, 284 | { role: 'assistant', content: assistant2 }, 285 | { role: 'user', content: user3 } 286 | ]; 287 | 288 | const result = convertMessagesToFalPrompt(messages); 289 | 290 | // 验证基本正确性 291 | expect(result.error).toBeUndefined(); 292 | expect(result.system_prompt.length).toBeLessThanOrEqual(SYSTEM_PROMPT_LIMIT); 293 | 294 | // 倒序遍历至多取3条user/assistant消息放到prompt:user3 + assistant2 + user2 295 | // 剩余的messages放到system_prompt:system + user1 + assistant1 296 | expect(result.system_prompt).toBe(`${systemMessage}\nHuman: ${user1}\nAssistant: ${assistant1}`); 297 | 298 | // prompt包含倒序的最后3条消息 299 | const expectedPrompt = `${user2}\nAssistant: ${assistant2}\n${user3}`; 300 | expect(result.prompt).toBe(expectedPrompt); 301 | }); 302 | 303 | // === 空内容和异常情况测试 === 304 | 305 | it('应该正确处理空字符串内容', () => { 306 | const messages: OpenAIMessage[] = [ 307 | { role: 'system', content: '' }, 308 | { role: 'user', content: '' }, 309 | { role: 'assistant', content: '' }, 310 | { role: 'user', content: 'Final message' } 311 | ]; 312 | 313 | const result = convertMessagesToFalPrompt(messages); 314 | 315 | // 空字符串消息被过滤后,只剩下最后一条有效的用户消息 316 | expect(result.error).toBeUndefined(); 317 | expect(result.system_prompt).toBe(''); 318 | expect(result.prompt).toBe('Final message'); 319 | }); 320 | 321 | it('应该正确处理只有assistant消息结尾的情况', () => { 322 | const messages: OpenAIMessage[] = [ 323 | { role: 'user', content: 'Question' }, 324 | { role: 'assistant', content: 'Answer' } 325 | ]; 326 | 327 | const result = convertMessagesToFalPrompt(messages); 328 | 329 | // 倒序遍历至多取3条user/assistant消息放到prompt:Answer + Question 330 | // 最后一条消息不是用户消息,按照要求格式处理 331 | expect(result.system_prompt).toBe(''); 332 | expect(result.prompt).toBe('Question\nAssistant: Answer'); 333 | expect(result.error).toBeUndefined(); 334 | }); 335 | 336 | it('应该过滤掉空内容消息', () => { 337 | const messages: OpenAIMessage[] = [ 338 | { role: 'system', content: 'You are a helpful assistant.' }, 339 | { role: 'user', content: 'First question' }, 340 | { role: 'assistant', content: '' }, // 空字符串,应该被过滤 341 | { role: 'user', content: null }, // null,应该被过滤 342 | { role: 'assistant', content: ' ' }, // 只有空格,应该被过滤 343 | { role: 'user', content: 'Final question' } 344 | ]; 345 | 346 | const result = convertMessagesToFalPrompt(messages); 347 | 348 | // 空的assistant消息被过滤后,只剩下2条用户消息:First question 和 Final question 349 | expect(result.system_prompt).toBe('You are a helpful assistant.'); 350 | // 倒序遍历至多取3条消息,这里只有2条:Final question + First question 351 | expect(result.prompt).toBe('First question\nFinal question'); 352 | expect(result.error).toBeUndefined(); 353 | }); 354 | }); -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Welcome to Cloudflare Workers! This is your first worker. 3 | * 4 | * - Run `npm run dev` in your terminal to start a development server 5 | * - Open a browser tab at http://localhost:8787/ to see your worker in action 6 | * - Run `npm run deploy` to publish your worker 7 | * 8 | * Bind resources to your worker in `wrangler.jsonc`. After adding bindings, a type definition for the 9 | * `Env` object can be regenerated with `npm run cf-typegen`. 10 | * 11 | * Learn more at https://developers.cloudflare.com/workers/ 12 | */ 13 | 14 | import { fal } from '@fal-ai/client'; 15 | 16 | // Define the expected environment variables/secrets 17 | interface Env { 18 | /** 19 | * Comma-separated list of Fal AI API keys for redundancy. 20 | * Example: "key1,key2,key3" 21 | * Configure this secret in your Cloudflare dashboard or wrangler.jsonc. 22 | */ 23 | FAL_KEY: string; 24 | /** 25 | * Comma-separated list of allowed API keys for client authentication. 26 | * Example: "clientkey1,clientkey2,verysecretkey" 27 | * Configure this secret in your Cloudflare dashboard or wrangler.jsonc. 28 | */ 29 | API_KEY: string; 30 | /** 31 | * Comma-separated list of allowed origins for CORS. 32 | * Example: "https://example.com,https://app.example.com" 33 | * If not set, defaults to '*' (all origins). 34 | * Configure this secret in your Cloudflare dashboard or wrangler.jsonc. 35 | */ 36 | ALLOWED_ORIGINS?: string; 37 | } 38 | 39 | // OpenAI Request/Response Types (Simplified) 40 | export interface OpenAIMessage { 41 | role: 'system' | 'user' | 'assistant'; 42 | content: string | null; 43 | } 44 | 45 | interface OpenAIChatCompletionRequest { 46 | model: string; 47 | messages: OpenAIMessage[]; 48 | stream?: boolean; 49 | // We can add other OpenAI parameters here if needed, but keep them simple for now 50 | // e.g., max_tokens, temperature, etc. 51 | } 52 | 53 | // Type for supported Fal Model IDs based on the constant array 54 | // This ensures type safety when passing to the fal client 55 | type FalModelId = typeof FAL_SUPPORTED_MODELS[number]; 56 | 57 | interface FalInput { 58 | model: FalModelId; // Use the specific type 59 | prompt: string; 60 | system_prompt?: string; 61 | // Fal-specific parameters can be added here if needed 62 | } 63 | 64 | // Payload for fal.stream or fal.subscribe 65 | interface FalPayload { 66 | input: FalInput; 67 | // Add other stream/subscribe options if needed 68 | } 69 | 70 | // Simple type for log entries in the stream 71 | interface LogEntry { 72 | message: string; 73 | level?: string; // Optional level property 74 | } 75 | 76 | // Type for Fal Stream Event Data 77 | interface FalStreamEventData { 78 | output: string; 79 | partial?: boolean; 80 | error?: any; // Or a more specific type if known 81 | logs?: LogEntry[]; 82 | } 83 | 84 | // Type for Fal Stream Event (passed in iterator) 85 | interface FalStreamEvent { 86 | output: string; 87 | partial?: boolean; 88 | error?: any; // Or a more specific type if known 89 | logs?: LogEntry[]; 90 | // Other potential properties from the stream event can be added here 91 | } 92 | 93 | // Type for Fal Subscribe Result 94 | interface FalSubscribeResult { 95 | data: FalStreamEventData; // Assuming subscribe result structure matches stream event data 96 | requestId: string; 97 | // Other potential top-level properties can be added here 98 | } 99 | 100 | // === Global Definitions === 101 | const SYSTEM_PROMPT_LIMIT = 4800; 102 | 103 | const FAL_SUPPORTED_MODELS = [ 104 | "anthropic/claude-3.7-sonnet", 105 | "anthropic/claude-3.5-sonnet", 106 | "anthropic/claude-3-5-haiku", 107 | "anthropic/claude-3-haiku", 108 | "google/gemini-pro-1.5", 109 | "google/gemini-flash-1.5", 110 | "google/gemini-flash-1.5-8b", 111 | "google/gemini-2.0-flash-001", 112 | "meta-llama/llama-3.2-1b-instruct", 113 | "meta-llama/llama-3.2-3b-instruct", 114 | "meta-llama/llama-3.1-8b-instruct", 115 | "meta-llama/llama-3.1-70b-instruct", 116 | "openai/gpt-4o-mini", 117 | "openai/gpt-4o", 118 | "deepseek/deepseek-r1", 119 | "meta-llama/llama-4-maverick", 120 | "meta-llama/llama-4-scout" 121 | ] as const; // Use 'as const' for stricter type checking 122 | 123 | // Helper function to get owner from model ID 124 | const getOwner = (modelId: string): string => { 125 | if (modelId && modelId.includes('/')) { 126 | return modelId.split('/')[0]; 127 | } 128 | return 'https://fal.ai'; // Default owner 129 | }; 130 | 131 | /** 132 | * 将 OpenAI 格式的消息转换为 Fal AI 格式的 prompt 和 system_prompt 133 | * 134 | * 核心逻辑:倒序遍历 messages,至多取 3 条 user/assistant 消息放到 prompt 部分, 135 | * chat_history 最多包含 2 条消息(user + assistant),最后一个用户消息是最新提问,不属于对话历史 136 | * 137 | * @param messages - OpenAI 格式的消息数组 138 | * @returns 包含 system_prompt、prompt 和可选错误信息的对象 139 | * 140 | * @example 141 | * // 基本用法:系统消息 + 用户消息 142 | * const messages = [ 143 | * { role: 'system', content: 'You are a helpful assistant.' }, 144 | * { role: 'user', content: 'Hello, how are you?' } 145 | * ]; 146 | * const result = convertMessagesToFalPrompt(messages); 147 | * // result.system_prompt: 'You are a helpful assistant.' 148 | * // result.prompt: 'Hello, how are you?' 149 | * 150 | * @example 151 | * // 多轮对话:最后一条是用户消息 152 | * const messages = [ 153 | * { role: 'system', content: 'You are helpful.' }, 154 | * { role: 'user', content: 'What is AI?' }, 155 | * { role: 'assistant', content: 'AI is artificial intelligence.' }, 156 | * { role: 'user', content: 'Tell me more.' } 157 | * ]; 158 | * const result = convertMessagesToFalPrompt(messages); 159 | * // result.system_prompt: 'You are helpful.\n' 160 | * // result.prompt: 'What is AI?\nAssistant: AI is artificial intelligence.\n\nTell me more.' 161 | * 162 | * @example 163 | * // 多轮对话:最后一条不是用户消息 164 | * const messages = [ 165 | * { role: 'user', content: 'Hello' }, 166 | * { role: 'assistant', content: 'Hi there!' } 167 | * ]; 168 | * const result = convertMessagesToFalPrompt(messages); 169 | * // result.system_prompt: '\nHuman: Hello\nAssistant: Hi there!\n' 170 | * // result.prompt: '' 171 | * 172 | * @description 173 | * 实现逻辑: 174 | * 1. **系统消息处理**:只使用最后一个非空系统消息,如果超出 SYSTEM_PROMPT_LIMIT 则返回错误 175 | * 2. **消息过滤**:自动过滤空内容消息(null、undefined、空字符串或纯空格) 176 | * 3. **倒序遍历**:取最后 3 条消息,根据最后一条消息类型分两种情况: 177 | * 178 | * **情况 A - 最后一条是用户消息**: 179 | * - 取倒数第 3、第 2 条作为 chat_history(最多 2 条:user + assistant) 180 | * - system_prompt: `系统消息\n` 181 | * - prompt: `\nAssistant: \n\n<最新用户消息>` 182 | * 183 | * **情况 B - 最后一条不是用户消息**: 184 | * - 取最后 2 条消息作为 chat_history,放在 system_prompt 中 185 | * - system_prompt: `系统消息\n\nHuman: \nAssistant: \n` 186 | * - prompt: `""`(空字符串) 187 | * 188 | * 4. **格式约定**: 189 | * - prompt 中会自动拼接 Human 消息,所以 user 消息不需要 "Human:" 前缀 190 | * - system_prompt 中的 user 消息需要 "Human:" 前缀 191 | * - assistant 消息始终使用 "Assistant:" 前缀 192 | * 193 | * @note 194 | * - 字符限制:系统消息长度不能超过 SYSTEM_PROMPT_LIMIT (4800) 字符 195 | * - 消息数量:最多处理最近的 3 条对话消息(倒数第 1、2、3 条) 196 | * - 历史限制:chat_history 最多包含 2 条消息,避免 prompt 过长 197 | * - 错误处理:系统消息超限时返回错误,其他情况尽力处理 198 | */ 199 | export function convertMessagesToFalPrompt(messages: OpenAIMessage[]): { system_prompt: string; prompt: string; error?: string } { 200 | // 第一步:过滤空内容消息,分离系统消息和对话消息 201 | const filtered_messages: OpenAIMessage[] = []; 202 | let system_message_content = ""; 203 | 204 | for (const message of messages) { 205 | const content = (message.content === null || message.content === undefined) ? "" : String(message.content).trim(); 206 | if (content.length > 0) { 207 | if (message.role === 'system') { 208 | system_message_content = content; // 只保留最后一个非空系统消息 209 | } else { 210 | filtered_messages.push({ 211 | ...message, 212 | content: content 213 | }); 214 | } 215 | } 216 | } 217 | 218 | // 检查系统消息长度限制 219 | if (system_message_content.length > SYSTEM_PROMPT_LIMIT) { 220 | return { 221 | system_prompt: "", 222 | prompt: "", 223 | error: `System message too long: ${system_message_content.length} characters exceeds limit of ${SYSTEM_PROMPT_LIMIT} characters` 224 | }; 225 | } 226 | 227 | // 如果没有对话消息,直接返回 228 | if (filtered_messages.length === 0) { 229 | return { 230 | system_prompt: system_message_content, 231 | prompt: "" 232 | }; 233 | } 234 | 235 | // 第二步:倒序遍历messages,至多取3条user/assistant消息放到prompt部分 236 | const prompt_messages = filtered_messages.slice(-3); // 取最后3条消息 237 | const remaining_messages = filtered_messages.slice(0, -3); // 剩余的消息 238 | 239 | // 第三步:构建prompt部分 240 | let prompt_parts: string[] = []; 241 | 242 | for (const message of prompt_messages) { 243 | if (message.role === 'user') { 244 | prompt_parts.push(String(message.content)); 245 | } else if (message.role === 'assistant') { 246 | prompt_parts.push(`Assistant: ${String(message.content)}`); 247 | } 248 | } 249 | 250 | const final_prompt = prompt_parts.join('\n'); 251 | 252 | // 第四步:构建system_prompt部分 253 | let system_prompt_parts: string[] = []; 254 | 255 | // 添加系统消息(如果存在) 256 | if (system_message_content.length > 0) { 257 | system_prompt_parts.push(system_message_content); 258 | } 259 | 260 | // 添加剩余的对话消息 261 | for (const message of remaining_messages) { 262 | if (message.role === 'user') { 263 | system_prompt_parts.push(`Human: ${String(message.content)}`); 264 | } else if (message.role === 'assistant') { 265 | system_prompt_parts.push(`Assistant: ${String(message.content)}`); 266 | } 267 | } 268 | 269 | let final_system_prompt = system_prompt_parts.join('\n'); 270 | 271 | // 第五步:检查system_prompt字符限制并截断 272 | if (final_system_prompt.length > SYSTEM_PROMPT_LIMIT) { 273 | // 优先保留系统消息,然后从最新的对话开始截断 274 | const system_part = system_message_content; 275 | let remaining_space = SYSTEM_PROMPT_LIMIT - system_part.length - 1; // -1 for newline 276 | 277 | if (remaining_space <= 0) { 278 | final_system_prompt = system_part; 279 | } else { 280 | const conversation_parts: string[] = []; 281 | 282 | // 倒序添加剩余对话,确保不超过字符限制 283 | for (let i = remaining_messages.length - 1; i >= 0; i--) { 284 | const message = remaining_messages[i]; 285 | let message_text = ""; 286 | 287 | if (message.role === 'user') { 288 | message_text = `Human: ${String(message.content)}`; 289 | } else if (message.role === 'assistant') { 290 | message_text = `Assistant: ${String(message.content)}`; 291 | } 292 | 293 | if (message_text.length + 1 <= remaining_space) { // +1 for newline 294 | conversation_parts.unshift(message_text); 295 | remaining_space -= (message_text.length + 1); 296 | } else { 297 | break; // 无法添加更多消息 298 | } 299 | } 300 | 301 | if (system_part.length > 0 && conversation_parts.length > 0) { 302 | final_system_prompt = system_part + '\n' + conversation_parts.join('\n'); 303 | } else if (system_part.length > 0) { 304 | final_system_prompt = system_part; 305 | } else { 306 | final_system_prompt = conversation_parts.join('\n'); 307 | } 308 | } 309 | } 310 | 311 | return { 312 | system_prompt: final_system_prompt, 313 | prompt: final_prompt 314 | }; 315 | } 316 | 317 | // Type guard to check if a string is a valid FalModelId 318 | function isValidFalModelId(modelId: string): modelId is FalModelId { 319 | return (FAL_SUPPORTED_MODELS as readonly string[]).includes(modelId); 320 | } 321 | 322 | // === CORS Utilities === 323 | // Default CORS headers 324 | function getCorsHeaders(): HeadersInit { 325 | return { 326 | 'Access-Control-Allow-Origin': '*', 327 | 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS, PUT, DELETE, PATCH', 328 | 'Access-Control-Allow-Headers': '*', 329 | 'Access-Control-Max-Age': '86400', // Cache preflight for 1 day 330 | }; 331 | } 332 | 333 | // Create a response with CORS headers included 334 | function createCorsResponse(body: string | null, options: ResponseInit): Response { 335 | const corsHeaders = getCorsHeaders(); 336 | const headers = { ...options.headers, ...corsHeaders }; 337 | 338 | return new Response(body, { 339 | ...options, 340 | headers 341 | }); 342 | } 343 | 344 | /** 345 | * Attempts a Fal API request (stream or subscribe) with key rotation on failure. 346 | * @param falKeys Array of Fal API keys. 347 | * @param falPayload Payload for the fal request. 348 | * @param stream Whether to use streaming. 349 | * @param modelToReport The original model name requested by the user for reporting. 350 | * @param ctx ExecutionContext for streaming requests. 351 | * @returns For stream=true: { readable: ReadableStream }. For stream=false: FalSubscribeResult. 352 | * @throws Throws an error if all keys fail or if a non-retryable error occurs. 353 | */ 354 | async function tryFalRequest( 355 | falKeys: string[], 356 | falPayload: FalPayload, 357 | stream: boolean, 358 | modelToReport: string, // Pass the original model name 359 | ctx?: ExecutionContext 360 | ): Promise<{ readable: ReadableStream } | FalSubscribeResult> { 361 | let lastError: any = null; 362 | 363 | if (falKeys.length === 0) { 364 | throw new Error("No Fal API keys configured."); 365 | } 366 | 367 | for (let i = 0; i < falKeys.length; i++) { 368 | const key = falKeys[i]; 369 | console.log(`Attempting Fal request with key index ${i}`); 370 | try { 371 | // Configure the client with the current key for this attempt 372 | fal.config({ credentials: key }); 373 | 374 | if (stream && ctx) { 375 | // --- Stream Handling --- 376 | console.log(`Initiating fal.stream with key index ${i}... Payload:`, JSON.stringify(falPayload, null, 2)); 377 | const falStream = await fal.stream("fal-ai/any-llm", falPayload as any); // Use the prepared payload object 378 | console.log(`fal.stream initiated successfully with key index ${i}.`); 379 | 380 | // Set up the response stream 381 | const { readable, writable } = new TransformStream(); 382 | const writer = writable.getWriter(); 383 | const encoder = new TextEncoder(); 384 | 385 | // Start background processing for the stream events 386 | const streamProcessing = (async () => { 387 | try { 388 | let previousOutput = ''; 389 | for await (const event of falStream) { 390 | // console.log(`Fal Stream Event (Key ${i}):`, event); // Verbose logging 391 | const currentOutput = (event && typeof event.output === 'string') ? event.output : ''; 392 | const isPartial = (event && typeof event.partial === 'boolean') ? event.partial : true; // Assume partial if not specified 393 | const errorInfo = (event && event.error) ? event.error : null; 394 | const logs = (event && Array.isArray((event as any).logs)) ? (event as any).logs : []; 395 | 396 | logs.forEach((log: LogEntry) => console.log(`[Fal Log - Key ${i}] ${log.message}`)); 397 | 398 | if (errorInfo) { 399 | console.error(`Error received in fal stream event (Key ${i}):`, errorInfo); 400 | const errorChunk = { id: `chatcmpl-${Date.now()}-error`, object: "chat.completion.chunk", created: Math.floor(Date.now() / 1000), model: modelToReport, choices: [{ index: 0, delta: {}, finish_reason: "error", logprobs: null, message: { role: 'assistant', content: `Fal Stream Error: ${JSON.stringify(errorInfo)}` } }] }; 401 | await writer.write(encoder.encode(`data: ${JSON.stringify(errorChunk)}\n\n`)); 402 | throw new Error(`Fal Stream Error: ${JSON.stringify(errorInfo)}`); // Throw to stop processing this stream 403 | } 404 | 405 | let deltaContent = ''; 406 | if (currentOutput.startsWith(previousOutput)) { 407 | deltaContent = currentOutput.substring(previousOutput.length); 408 | } else if (currentOutput.length > 0) { 409 | console.warn(`Fal stream output mismatch (Key ${i}). Sending full current output as delta.`); 410 | deltaContent = currentOutput; 411 | previousOutput = ''; // Reset 412 | } 413 | if (currentOutput.length > 0) { 414 | previousOutput = currentOutput; 415 | } 416 | 417 | if (deltaContent || !isPartial) { 418 | const choice = { 419 | index: 0, 420 | delta: { content: deltaContent }, 421 | finish_reason: isPartial === false ? "stop" : null, 422 | logprobs: null 423 | }; 424 | const openAIChunk = { 425 | id: `chatcmpl-${Date.now()}-${Math.random().toString(36).substring(2, 15)}`, 426 | object: "chat.completion.chunk", 427 | created: Math.floor(Date.now() / 1000), 428 | model: modelToReport, // Report the originally requested model 429 | choices: [choice] 430 | }; 431 | await writer.write(encoder.encode(`data: ${JSON.stringify(openAIChunk)}\n\n`)); 432 | } 433 | } 434 | console.log(`Fal stream iteration finished successfully (Key ${i}).`); 435 | await writer.write(encoder.encode(`data: [DONE]\n\n`)); 436 | 437 | } catch (streamError: any) { 438 | console.error(`Error during fal stream processing (Key ${i}):`, streamError); 439 | try { 440 | // Attempt to write an error chunk to the client stream 441 | const errorDetails = (streamError instanceof Error) ? streamError.message : JSON.stringify(streamError); 442 | const errorChunk = { error: { message: "Stream processing error", type: "proxy_error", param: null, code: null, details: errorDetails } }; 443 | await writer.write(encoder.encode(`data: ${JSON.stringify(errorChunk)}\n\n`)); 444 | // Always attempt to send DONE, even after error chunk 445 | await writer.write(encoder.encode(`data: [DONE]\n\n`)); 446 | } catch (writeError) { 447 | console.error(`Error writing final stream error message (Key ${i}):`, writeError); 448 | } 449 | // Do not re-throw here, as the initial response has already been sent. 450 | // The error is reported within the stream itself. 451 | } finally { 452 | try { 453 | console.log(`Closing stream writer (Key ${i})...`); 454 | await writer.close(); 455 | console.log(`Stream writer closed (Key ${i}).`); 456 | } catch (closeError) { 457 | console.error(`Error closing stream writer (Key ${i}):`, closeError); 458 | } 459 | } 460 | })(); 461 | 462 | ctx.waitUntil(streamProcessing); // Allow background processing 463 | 464 | // Return the readable stream immediately for the client 465 | return { readable }; // Indicate success for this key attempt 466 | 467 | } else { 468 | // --- Non-Stream Handling --- 469 | console.log(`Executing fal.subscribe with key index ${i}... Payload:`, JSON.stringify(falPayload, null, 2)); 470 | const result = await fal.subscribe("fal-ai/any-llm", falPayload as any) as FalSubscribeResult; 471 | console.log(`fal.subscribe successful with key index ${i}.`); 472 | 473 | // Log any embedded logs or errors from the non-stream result 474 | if (result?.data?.logs && Array.isArray(result.data.logs)) { 475 | result.data.logs.forEach((log: LogEntry) => console.log(`[Fal Log - Key ${i}] ${log.message}`)); 476 | } 477 | if (result?.data?.error) { 478 | // Treat error within data as failure for this key 479 | console.error(`Fal-ai returned an error in non-stream mode (Key ${i}):`, result.data.error); 480 | throw new Error(`Fal error in response data: ${JSON.stringify(result.data.error)}`); 481 | } 482 | 483 | return result; // Return successful result 484 | } 485 | 486 | } catch (error: any) { 487 | lastError = error; // Store the error from this attempt 488 | console.warn(`Fal request attempt failed for key index ${i}:`, error.message || error); 489 | 490 | // Check if the error suggests a key-specific issue (e.g., auth, rate limit, server error) 491 | // Fal client might throw custom errors or HTTP status codes might be attached. 492 | // This check might need refinement based on actual errors thrown by @fal-ai/client. 493 | // Assuming status property exists for HTTP errors. 494 | const status = error?.status || error?.response?.status; 495 | const isRetryableError = status === 401 || status === 403 || status === 429 || (status >= 500 && status < 600) || error.message?.includes('authentication') || error.message?.includes('rate limit'); 496 | 497 | 498 | if (isRetryableError && (i < falKeys.length - 1)) { 499 | console.log(`Error is potentially key-related or transient (status: ${status}). Trying next key.`); 500 | continue; // Try the next key 501 | } else { 502 | console.error(`Non-retryable error (status: ${status}) or final key attempt failed for key index ${i}.`); 503 | throw error; // Re-throw the error if it's not retryable or it's the last key 504 | } 505 | } 506 | } 507 | 508 | // If the loop finishes without returning/throwing successfully, it means all keys failed. 509 | console.error("All Fal API key attempts failed."); 510 | // Throw the last recorded error 511 | throw new Error(`All Fal API key attempts failed. Last error: ${lastError?.message || lastError}`); 512 | } 513 | 514 | // === Main Fetch Handler === 515 | export default { 516 | async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { 517 | const url = new URL(request.url); 518 | const path = url.pathname; 519 | const method = request.method; 520 | 521 | // Handle CORS preflight requests (OPTIONS) 522 | if (method === 'OPTIONS') { 523 | return new Response(null, { 524 | status: 204, 525 | headers: getCorsHeaders() 526 | }); 527 | } 528 | 529 | let falKeys: string[] = []; 530 | 531 | // --- Authentication & Fal Key Parsing --- (Run for all relevant endpoints) 532 | if (path.startsWith('/v1/')) { // Only process /v1 routes 533 | // --- Worker Authentication --- 534 | const authHeader = request.headers.get('Authorization'); 535 | const apiKeysString = env.API_KEY; 536 | 537 | if (!apiKeysString) { 538 | console.error("API_KEY secret is not set."); 539 | return createCorsResponse( 540 | JSON.stringify({ error: { message: 'Server configuration error: API Key secret missing.', type: 'server_error' } }), 541 | { status: 500, headers: { 'Content-Type': 'application/json' } } 542 | ); 543 | } 544 | 545 | const allowedApiKeys = apiKeysString.split(',').map(k => k.trim()).filter(k => k.length > 0); 546 | if (allowedApiKeys.length === 0) { 547 | console.error("API_KEY secret is set but contains no valid keys after parsing."); 548 | return createCorsResponse( 549 | JSON.stringify({ error: { message: 'Server configuration error: No valid API Keys found in secret.', type: 'server_error' } }), 550 | { status: 500, headers: { 'Content-Type': 'application/json' } } 551 | ); 552 | } 553 | 554 | const providedApiKey = authHeader?.startsWith('Bearer ') ? authHeader.substring(7) : null; 555 | 556 | if (!providedApiKey || !allowedApiKeys.includes(providedApiKey)) { 557 | console.error(`Worker auth failed. Header: ${authHeader ? 'Present' : 'Missing'}, Key check: Provided key "${providedApiKey}" not in allowed list.`); 558 | return createCorsResponse( 559 | JSON.stringify({ error: { message: 'Incorrect API key provided.', type: 'invalid_request_error', param: null, code: 'invalid_api_key'} }), 560 | { status: 401, headers: { 'Content-Type': 'application/json' } } 561 | ); 562 | } 563 | 564 | // --- Parse Fal Keys --- 565 | if (!env.FAL_KEY) { 566 | console.error("FAL_KEY secret is not set."); 567 | return createCorsResponse( 568 | JSON.stringify({ error: { message: 'Server configuration error: Fal Key missing.', type: 'server_error'} }), 569 | { status: 500, headers: { 'Content-Type': 'application/json' } } 570 | ); 571 | } 572 | falKeys = env.FAL_KEY.split(',').map(k => k.trim()).filter(k => k.length > 0); 573 | if (falKeys.length === 0) { 574 | console.error("FAL_KEY secret is set but contains no valid keys after parsing."); 575 | return createCorsResponse( 576 | JSON.stringify({ error: { message: 'Server configuration error: No valid Fal Keys found.', type: 'server_error'} }), 577 | { status: 500, headers: { 'Content-Type': 'application/json' } } 578 | ); 579 | } 580 | console.log(`Found ${falKeys.length} Fal API key(s).`); 581 | // Note: fal.config() is now called inside tryFalRequest for each attempt 582 | 583 | } else if (path === '/') { 584 | // Allow root path without auth 585 | } else { 586 | // Any other path is Not Found 587 | return createCorsResponse( 588 | JSON.stringify({ error: { message: `Invalid path: ${path}`, type: 'invalid_request_error'} }), 589 | { status: 404, headers: { 'Content-Type': 'application/json' } } 590 | ); 591 | } 592 | 593 | // --- Routing --- 594 | 595 | // GET /v1/models 596 | if (method === 'GET' && path === '/v1/models') { 597 | try { 598 | const modelsData = FAL_SUPPORTED_MODELS.map(modelId => ({ 599 | id: modelId, 600 | object: "model", 601 | created: Math.floor(Date.now() / 1000), 602 | owned_by: getOwner(modelId) 603 | })); 604 | return createCorsResponse( 605 | JSON.stringify({ object: "list", data: modelsData }), 606 | { headers: { 'Content-Type': 'application/json' } } 607 | ); 608 | } catch (error: any) { 609 | console.error("Error processing GET /v1/models:", error); 610 | return createCorsResponse( 611 | JSON.stringify({ error: { message: `Failed to retrieve model list: ${error.message}`, type: 'server_error'} }), 612 | { status: 500, headers: { 'Content-Type': 'application/json' } } 613 | ); 614 | } 615 | } 616 | 617 | // POST /v1/chat/completions 618 | if (method === 'POST' && path === '/v1/chat/completions') { 619 | let requestBody: OpenAIChatCompletionRequest; 620 | try { 621 | requestBody = await request.json(); 622 | } catch (error) { 623 | return createCorsResponse( 624 | JSON.stringify({ error: { message: 'Invalid JSON request body', type: 'invalid_request_error'} }), 625 | { status: 400, headers: { 'Content-Type': 'application/json' } } 626 | ); 627 | } 628 | 629 | const { model: requestedModel, messages, stream = false } = requestBody; 630 | 631 | if (!requestedModel || !messages || !Array.isArray(messages) || messages.length === 0) { 632 | return createCorsResponse( 633 | JSON.stringify({ error: { message: 'Missing or invalid parameters: model and messages array are required.', type: 'invalid_request_error'} }), 634 | { status: 400, headers: { 'Content-Type': 'application/json' } } 635 | ); 636 | } 637 | 638 | // Validate and determine the model to use 639 | let modelToUse: FalModelId; 640 | if (isValidFalModelId(requestedModel)) { 641 | modelToUse = requestedModel; 642 | } else { 643 | console.error(`Error: Requested model '${requestedModel}' is not supported.`); 644 | return createCorsResponse( 645 | JSON.stringify({ 646 | error: { 647 | message: `The model \`${requestedModel}\` does not exist or is not supported by this endpoint.`, 648 | type: 'invalid_request_error', 649 | param: 'model', 650 | code: 'model_not_found' 651 | } 652 | }), 653 | { status: 400, headers: { 'Content-Type': 'application/json' } } 654 | ); 655 | } 656 | 657 | 658 | 659 | try { 660 | const convertResult = convertMessagesToFalPrompt(messages); 661 | 662 | // Check for conversion error 663 | if (convertResult.error) { 664 | return createCorsResponse( 665 | JSON.stringify({ error: { message: convertResult.error, type: 'invalid_request_error' } }), 666 | { status: 400, headers: { 'Content-Type': 'application/json' } } 667 | ); 668 | } 669 | 670 | const { prompt, system_prompt } = convertResult; 671 | const falInput: FalInput = { 672 | model: modelToUse, 673 | prompt: prompt, 674 | }; 675 | if (system_prompt) { 676 | falInput.system_prompt = system_prompt; 677 | } 678 | 679 | // Prepare the final payload for Fal 680 | const falPayload: FalPayload = { input: falInput }; 681 | 682 | console.log(`Preparing request for fal-ai/any-llm. Model: ${falInput.model}, Stream: ${stream}`); 683 | 684 | // Use the tryFalRequest function with key rotation 685 | const result = await tryFalRequest(falKeys, falPayload, stream, requestedModel, ctx); 686 | 687 | if (stream) { 688 | // --- Stream Handling --- 689 | // tryFalRequest handles the stream setup internally if successful 690 | // We just need to return the response with the readable stream 691 | if ('readable' in result) { 692 | // For streaming responses, we need to add CORS headers but keep the stream 693 | const corsHeaders = getCorsHeaders(); 694 | return new Response(result.readable, { 695 | headers: { 696 | 'Content-Type': 'text/event-stream; charset=utf-8', 697 | 'Cache-Control': 'no-cache', 698 | 'Connection': 'keep-alive', 699 | ...corsHeaders 700 | } 701 | }); 702 | } else { 703 | // This case should theoretically not happen if stream=true and no error was thrown 704 | console.error("tryFalRequest returned unexpected result for stream=true"); 705 | throw new Error("Internal server error during stream setup."); 706 | } 707 | 708 | } else { 709 | // --- Non-Stream Handling --- 710 | // tryFalRequest returns the FalSubscribeResult directly if successful 711 | const falResult = result as FalSubscribeResult; // Type assertion 712 | 713 | // Log the raw result for debugging (optional) 714 | // console.log("Raw non-stream result from fal-ai:", JSON.stringify(falResult, null, 2)); 715 | 716 | // Access properties correctly from the data object returned by tryFalRequest 717 | const outputContent = falResult?.data?.output ?? ""; 718 | const reqId = falResult?.requestId || Date.now().toString(); 719 | 720 | const openAIResponse = { 721 | id: `chatcmpl-${reqId}`, 722 | object: "chat.completion", 723 | created: Math.floor(Date.now() / 1000), 724 | model: requestedModel, // Report the originally requested model 725 | choices: [{ 726 | index: 0, 727 | message: { 728 | role: "assistant", 729 | content: outputContent 730 | }, 731 | finish_reason: "stop", 732 | logprobs: null 733 | }], 734 | usage: { prompt_tokens: null, completion_tokens: null, total_tokens: null }, 735 | system_fingerprint: null, 736 | }; 737 | return createCorsResponse( 738 | JSON.stringify(openAIResponse), 739 | { headers: { 'Content-Type': 'application/json' } } 740 | ); 741 | } 742 | } catch (error: any) { 743 | // Catch errors from tryFalRequest (e.g., all keys failed) or other processing errors 744 | console.error("Error during Fal request processing or after retries:", error); 745 | const errorMessage = (error instanceof Error) ? error.message : JSON.stringify(error); 746 | return createCorsResponse( 747 | JSON.stringify({ error: { message: `Error processing request: ${errorMessage}`, type: 'server_error' } }), 748 | { status: 500, headers: { 'Content-Type': 'application/json' } } 749 | ); 750 | } 751 | } 752 | 753 | // Default response for root path or other unhandled routes 754 | return createCorsResponse( 755 | JSON.stringify({ status: 'ok', message: 'Fal AI-powered OpenAI Proxy API' }), 756 | { headers: { 'Content-Type': 'application/json' } } 757 | ); 758 | } 759 | }; -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | dependencies: 11 | '@fal-ai/client': 12 | specifier: ^1.5.0 13 | version: 1.5.0 14 | devDependencies: 15 | '@cloudflare/vitest-pool-workers': 16 | specifier: ^0.8.36 17 | version: 0.8.36(@cloudflare/workers-types@4.20250605.0)(@vitest/runner@3.2.1)(@vitest/snapshot@3.2.1)(vitest@3.1.3) 18 | '@cloudflare/workers-types': 19 | specifier: ^4.20250605.0 20 | version: 4.20250605.0 21 | typescript: 22 | specifier: ^5.8.3 23 | version: 5.8.3 24 | vite: 25 | specifier: 6.2.7 26 | version: 6.2.7 27 | vitest: 28 | specifier: ~3.1.3 29 | version: 3.1.3 30 | wrangler: 31 | specifier: ^4.19.1 32 | version: 4.19.1(@cloudflare/workers-types@4.20250605.0) 33 | 34 | packages: 35 | 36 | '@cloudflare/kv-asset-handler@0.4.0': 37 | resolution: {integrity: sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==} 38 | engines: {node: '>=18.0.0'} 39 | 40 | '@cloudflare/unenv-preset@2.3.2': 41 | resolution: {integrity: sha512-MtUgNl+QkQyhQvv5bbWP+BpBC1N0me4CHHuP2H4ktmOMKdB/6kkz/lo+zqiA4mEazb4y+1cwyNjVrQ2DWeE4mg==} 42 | peerDependencies: 43 | unenv: 2.0.0-rc.17 44 | workerd: ^1.20250508.0 45 | peerDependenciesMeta: 46 | workerd: 47 | optional: true 48 | 49 | '@cloudflare/vitest-pool-workers@0.8.36': 50 | resolution: {integrity: sha512-aJ3IP+kMv3ButnQLyfG7ZVWNa2+nPE2w2ds0PHxNPW73JIjm2Hz1fcieWD+mJT/JdxdbERXuRkI3VedJ90magw==} 51 | peerDependencies: 52 | '@vitest/runner': 2.0.x - 3.1.x 53 | '@vitest/snapshot': 2.0.x - 3.1.x 54 | vitest: 2.0.x - 3.1.x 55 | 56 | '@cloudflare/workerd-darwin-64@1.20250525.0': 57 | resolution: {integrity: sha512-L5l+7sSJJT2+riR5rS3Q3PKNNySPjWfRIeaNGMVRi1dPO6QPi4lwuxfRUFNoeUdilZJUVPfSZvTtj9RedsKznQ==} 58 | engines: {node: '>=16'} 59 | cpu: [x64] 60 | os: [darwin] 61 | 62 | '@cloudflare/workerd-darwin-arm64@1.20250525.0': 63 | resolution: {integrity: sha512-Y3IbIdrF/vJWh/WBvshwcSyUh175VAiLRW7963S1dXChrZ1N5wuKGQm9xY69cIGVtitpMJWWW3jLq7J/Xxwm0Q==} 64 | engines: {node: '>=16'} 65 | cpu: [arm64] 66 | os: [darwin] 67 | 68 | '@cloudflare/workerd-linux-64@1.20250525.0': 69 | resolution: {integrity: sha512-KSyQPAby+c6cpENoO0ayCQlY6QIh28l/+QID7VC1SLXfiNHy+hPNsH1vVBTST6CilHVAQSsy9tCZ9O9XECB8yg==} 70 | engines: {node: '>=16'} 71 | cpu: [x64] 72 | os: [linux] 73 | 74 | '@cloudflare/workerd-linux-arm64@1.20250525.0': 75 | resolution: {integrity: sha512-Nt0FUxS2kQhJUea4hMCNPaetkrAFDhPnNX/ntwcqVlGgnGt75iaAhupWJbU0GB+gIWlKeuClUUnDZqKbicoKyg==} 76 | engines: {node: '>=16'} 77 | cpu: [arm64] 78 | os: [linux] 79 | 80 | '@cloudflare/workerd-windows-64@1.20250525.0': 81 | resolution: {integrity: sha512-mwTj+9f3uIa4NEXR1cOa82PjLa6dbrb3J+KCVJFYIaq7e63VxEzOchCXS4tublT2pmOhmFqkgBMXrxozxNkR2Q==} 82 | engines: {node: '>=16'} 83 | cpu: [x64] 84 | os: [win32] 85 | 86 | '@cloudflare/workers-types@4.20250605.0': 87 | resolution: {integrity: sha512-e3/ZCXcpmk3jUNfq/2gyVYqeOqUhuQ0hsSdohSGscCgTkUI37QraVeCAOQtciKPDFXKOkaGGkmxg+RLYunbKuw==} 88 | 89 | '@cspotcode/source-map-support@0.8.1': 90 | resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} 91 | engines: {node: '>=12'} 92 | 93 | '@emnapi/runtime@1.4.0': 94 | resolution: {integrity: sha512-64WYIf4UYcdLnbKn/umDlNjQDSS8AgZrI/R9+x5ilkUVFxXcA1Ebl+gQLc/6mERA4407Xof0R7wEyEuj091CVw==} 95 | 96 | '@esbuild/aix-ppc64@0.25.4': 97 | resolution: {integrity: sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==} 98 | engines: {node: '>=18'} 99 | cpu: [ppc64] 100 | os: [aix] 101 | 102 | '@esbuild/android-arm64@0.25.4': 103 | resolution: {integrity: sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==} 104 | engines: {node: '>=18'} 105 | cpu: [arm64] 106 | os: [android] 107 | 108 | '@esbuild/android-arm@0.25.4': 109 | resolution: {integrity: sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==} 110 | engines: {node: '>=18'} 111 | cpu: [arm] 112 | os: [android] 113 | 114 | '@esbuild/android-x64@0.25.4': 115 | resolution: {integrity: sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==} 116 | engines: {node: '>=18'} 117 | cpu: [x64] 118 | os: [android] 119 | 120 | '@esbuild/darwin-arm64@0.25.4': 121 | resolution: {integrity: sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==} 122 | engines: {node: '>=18'} 123 | cpu: [arm64] 124 | os: [darwin] 125 | 126 | '@esbuild/darwin-x64@0.25.4': 127 | resolution: {integrity: sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==} 128 | engines: {node: '>=18'} 129 | cpu: [x64] 130 | os: [darwin] 131 | 132 | '@esbuild/freebsd-arm64@0.25.4': 133 | resolution: {integrity: sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==} 134 | engines: {node: '>=18'} 135 | cpu: [arm64] 136 | os: [freebsd] 137 | 138 | '@esbuild/freebsd-x64@0.25.4': 139 | resolution: {integrity: sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==} 140 | engines: {node: '>=18'} 141 | cpu: [x64] 142 | os: [freebsd] 143 | 144 | '@esbuild/linux-arm64@0.25.4': 145 | resolution: {integrity: sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==} 146 | engines: {node: '>=18'} 147 | cpu: [arm64] 148 | os: [linux] 149 | 150 | '@esbuild/linux-arm@0.25.4': 151 | resolution: {integrity: sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==} 152 | engines: {node: '>=18'} 153 | cpu: [arm] 154 | os: [linux] 155 | 156 | '@esbuild/linux-ia32@0.25.4': 157 | resolution: {integrity: sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==} 158 | engines: {node: '>=18'} 159 | cpu: [ia32] 160 | os: [linux] 161 | 162 | '@esbuild/linux-loong64@0.25.4': 163 | resolution: {integrity: sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==} 164 | engines: {node: '>=18'} 165 | cpu: [loong64] 166 | os: [linux] 167 | 168 | '@esbuild/linux-mips64el@0.25.4': 169 | resolution: {integrity: sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==} 170 | engines: {node: '>=18'} 171 | cpu: [mips64el] 172 | os: [linux] 173 | 174 | '@esbuild/linux-ppc64@0.25.4': 175 | resolution: {integrity: sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==} 176 | engines: {node: '>=18'} 177 | cpu: [ppc64] 178 | os: [linux] 179 | 180 | '@esbuild/linux-riscv64@0.25.4': 181 | resolution: {integrity: sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==} 182 | engines: {node: '>=18'} 183 | cpu: [riscv64] 184 | os: [linux] 185 | 186 | '@esbuild/linux-s390x@0.25.4': 187 | resolution: {integrity: sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==} 188 | engines: {node: '>=18'} 189 | cpu: [s390x] 190 | os: [linux] 191 | 192 | '@esbuild/linux-x64@0.25.4': 193 | resolution: {integrity: sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==} 194 | engines: {node: '>=18'} 195 | cpu: [x64] 196 | os: [linux] 197 | 198 | '@esbuild/netbsd-arm64@0.25.4': 199 | resolution: {integrity: sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==} 200 | engines: {node: '>=18'} 201 | cpu: [arm64] 202 | os: [netbsd] 203 | 204 | '@esbuild/netbsd-x64@0.25.4': 205 | resolution: {integrity: sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==} 206 | engines: {node: '>=18'} 207 | cpu: [x64] 208 | os: [netbsd] 209 | 210 | '@esbuild/openbsd-arm64@0.25.4': 211 | resolution: {integrity: sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==} 212 | engines: {node: '>=18'} 213 | cpu: [arm64] 214 | os: [openbsd] 215 | 216 | '@esbuild/openbsd-x64@0.25.4': 217 | resolution: {integrity: sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==} 218 | engines: {node: '>=18'} 219 | cpu: [x64] 220 | os: [openbsd] 221 | 222 | '@esbuild/sunos-x64@0.25.4': 223 | resolution: {integrity: sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==} 224 | engines: {node: '>=18'} 225 | cpu: [x64] 226 | os: [sunos] 227 | 228 | '@esbuild/win32-arm64@0.25.4': 229 | resolution: {integrity: sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==} 230 | engines: {node: '>=18'} 231 | cpu: [arm64] 232 | os: [win32] 233 | 234 | '@esbuild/win32-ia32@0.25.4': 235 | resolution: {integrity: sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==} 236 | engines: {node: '>=18'} 237 | cpu: [ia32] 238 | os: [win32] 239 | 240 | '@esbuild/win32-x64@0.25.4': 241 | resolution: {integrity: sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==} 242 | engines: {node: '>=18'} 243 | cpu: [x64] 244 | os: [win32] 245 | 246 | '@fal-ai/client@1.5.0': 247 | resolution: {integrity: sha512-e0WtcbSjbwKFCOwAex3lRDba1iMY42QAwLZuae7oc5xNav2+HdprE9QWYdsDS3nyr0GOttEpUgsBa/c/AKAguA==} 248 | engines: {node: '>=18.0.0'} 249 | 250 | '@fastify/busboy@2.1.1': 251 | resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} 252 | engines: {node: '>=14'} 253 | 254 | '@img/sharp-darwin-arm64@0.33.5': 255 | resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} 256 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 257 | cpu: [arm64] 258 | os: [darwin] 259 | 260 | '@img/sharp-darwin-x64@0.33.5': 261 | resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} 262 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 263 | cpu: [x64] 264 | os: [darwin] 265 | 266 | '@img/sharp-libvips-darwin-arm64@1.0.4': 267 | resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} 268 | cpu: [arm64] 269 | os: [darwin] 270 | 271 | '@img/sharp-libvips-darwin-x64@1.0.4': 272 | resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} 273 | cpu: [x64] 274 | os: [darwin] 275 | 276 | '@img/sharp-libvips-linux-arm64@1.0.4': 277 | resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} 278 | cpu: [arm64] 279 | os: [linux] 280 | 281 | '@img/sharp-libvips-linux-arm@1.0.5': 282 | resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} 283 | cpu: [arm] 284 | os: [linux] 285 | 286 | '@img/sharp-libvips-linux-s390x@1.0.4': 287 | resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} 288 | cpu: [s390x] 289 | os: [linux] 290 | 291 | '@img/sharp-libvips-linux-x64@1.0.4': 292 | resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} 293 | cpu: [x64] 294 | os: [linux] 295 | 296 | '@img/sharp-libvips-linuxmusl-arm64@1.0.4': 297 | resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} 298 | cpu: [arm64] 299 | os: [linux] 300 | 301 | '@img/sharp-libvips-linuxmusl-x64@1.0.4': 302 | resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} 303 | cpu: [x64] 304 | os: [linux] 305 | 306 | '@img/sharp-linux-arm64@0.33.5': 307 | resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} 308 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 309 | cpu: [arm64] 310 | os: [linux] 311 | 312 | '@img/sharp-linux-arm@0.33.5': 313 | resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} 314 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 315 | cpu: [arm] 316 | os: [linux] 317 | 318 | '@img/sharp-linux-s390x@0.33.5': 319 | resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} 320 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 321 | cpu: [s390x] 322 | os: [linux] 323 | 324 | '@img/sharp-linux-x64@0.33.5': 325 | resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} 326 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 327 | cpu: [x64] 328 | os: [linux] 329 | 330 | '@img/sharp-linuxmusl-arm64@0.33.5': 331 | resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} 332 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 333 | cpu: [arm64] 334 | os: [linux] 335 | 336 | '@img/sharp-linuxmusl-x64@0.33.5': 337 | resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} 338 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 339 | cpu: [x64] 340 | os: [linux] 341 | 342 | '@img/sharp-wasm32@0.33.5': 343 | resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} 344 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 345 | cpu: [wasm32] 346 | 347 | '@img/sharp-win32-ia32@0.33.5': 348 | resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} 349 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 350 | cpu: [ia32] 351 | os: [win32] 352 | 353 | '@img/sharp-win32-x64@0.33.5': 354 | resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} 355 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 356 | cpu: [x64] 357 | os: [win32] 358 | 359 | '@jridgewell/resolve-uri@3.1.2': 360 | resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} 361 | engines: {node: '>=6.0.0'} 362 | 363 | '@jridgewell/sourcemap-codec@1.5.0': 364 | resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} 365 | 366 | '@jridgewell/trace-mapping@0.3.9': 367 | resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} 368 | 369 | '@msgpack/msgpack@3.1.1': 370 | resolution: {integrity: sha512-DnBpqkMOUGayNVKyTLlkM6ILmU/m/+VUxGkuQlPQVAcvreLz5jn1OlQnWd8uHKL/ZSiljpM12rjRhr51VtvJUQ==} 371 | engines: {node: '>= 18'} 372 | 373 | '@rollup/rollup-android-arm-eabi@4.39.0': 374 | resolution: {integrity: sha512-lGVys55Qb00Wvh8DMAocp5kIcaNzEFTmGhfFd88LfaogYTRKrdxgtlO5H6S49v2Nd8R2C6wLOal0qv6/kCkOwA==} 375 | cpu: [arm] 376 | os: [android] 377 | 378 | '@rollup/rollup-android-arm64@4.39.0': 379 | resolution: {integrity: sha512-It9+M1zE31KWfqh/0cJLrrsCPiF72PoJjIChLX+rEcujVRCb4NLQ5QzFkzIZW8Kn8FTbvGQBY5TkKBau3S8cCQ==} 380 | cpu: [arm64] 381 | os: [android] 382 | 383 | '@rollup/rollup-darwin-arm64@4.39.0': 384 | resolution: {integrity: sha512-lXQnhpFDOKDXiGxsU9/l8UEGGM65comrQuZ+lDcGUx+9YQ9dKpF3rSEGepyeR5AHZ0b5RgiligsBhWZfSSQh8Q==} 385 | cpu: [arm64] 386 | os: [darwin] 387 | 388 | '@rollup/rollup-darwin-x64@4.39.0': 389 | resolution: {integrity: sha512-mKXpNZLvtEbgu6WCkNij7CGycdw9cJi2k9v0noMb++Vab12GZjFgUXD69ilAbBh034Zwn95c2PNSz9xM7KYEAQ==} 390 | cpu: [x64] 391 | os: [darwin] 392 | 393 | '@rollup/rollup-freebsd-arm64@4.39.0': 394 | resolution: {integrity: sha512-jivRRlh2Lod/KvDZx2zUR+I4iBfHcu2V/BA2vasUtdtTN2Uk3jfcZczLa81ESHZHPHy4ih3T/W5rPFZ/hX7RtQ==} 395 | cpu: [arm64] 396 | os: [freebsd] 397 | 398 | '@rollup/rollup-freebsd-x64@4.39.0': 399 | resolution: {integrity: sha512-8RXIWvYIRK9nO+bhVz8DwLBepcptw633gv/QT4015CpJ0Ht8punmoHU/DuEd3iw9Hr8UwUV+t+VNNuZIWYeY7Q==} 400 | cpu: [x64] 401 | os: [freebsd] 402 | 403 | '@rollup/rollup-linux-arm-gnueabihf@4.39.0': 404 | resolution: {integrity: sha512-mz5POx5Zu58f2xAG5RaRRhp3IZDK7zXGk5sdEDj4o96HeaXhlUwmLFzNlc4hCQi5sGdR12VDgEUqVSHer0lI9g==} 405 | cpu: [arm] 406 | os: [linux] 407 | 408 | '@rollup/rollup-linux-arm-musleabihf@4.39.0': 409 | resolution: {integrity: sha512-+YDwhM6gUAyakl0CD+bMFpdmwIoRDzZYaTWV3SDRBGkMU/VpIBYXXEvkEcTagw/7VVkL2vA29zU4UVy1mP0/Yw==} 410 | cpu: [arm] 411 | os: [linux] 412 | 413 | '@rollup/rollup-linux-arm64-gnu@4.39.0': 414 | resolution: {integrity: sha512-EKf7iF7aK36eEChvlgxGnk7pdJfzfQbNvGV/+l98iiMwU23MwvmV0Ty3pJ0p5WQfm3JRHOytSIqD9LB7Bq7xdQ==} 415 | cpu: [arm64] 416 | os: [linux] 417 | 418 | '@rollup/rollup-linux-arm64-musl@4.39.0': 419 | resolution: {integrity: sha512-vYanR6MtqC7Z2SNr8gzVnzUul09Wi1kZqJaek3KcIlI/wq5Xtq4ZPIZ0Mr/st/sv/NnaPwy/D4yXg5x0B3aUUA==} 420 | cpu: [arm64] 421 | os: [linux] 422 | 423 | '@rollup/rollup-linux-loongarch64-gnu@4.39.0': 424 | resolution: {integrity: sha512-NMRUT40+h0FBa5fb+cpxtZoGAggRem16ocVKIv5gDB5uLDgBIwrIsXlGqYbLwW8YyO3WVTk1FkFDjMETYlDqiw==} 425 | cpu: [loong64] 426 | os: [linux] 427 | 428 | '@rollup/rollup-linux-powerpc64le-gnu@4.39.0': 429 | resolution: {integrity: sha512-0pCNnmxgduJ3YRt+D+kJ6Ai/r+TaePu9ZLENl+ZDV/CdVczXl95CbIiwwswu4L+K7uOIGf6tMo2vm8uadRaICQ==} 430 | cpu: [ppc64] 431 | os: [linux] 432 | 433 | '@rollup/rollup-linux-riscv64-gnu@4.39.0': 434 | resolution: {integrity: sha512-t7j5Zhr7S4bBtksT73bO6c3Qa2AV/HqiGlj9+KB3gNF5upcVkx+HLgxTm8DK4OkzsOYqbdqbLKwvGMhylJCPhQ==} 435 | cpu: [riscv64] 436 | os: [linux] 437 | 438 | '@rollup/rollup-linux-riscv64-musl@4.39.0': 439 | resolution: {integrity: sha512-m6cwI86IvQ7M93MQ2RF5SP8tUjD39Y7rjb1qjHgYh28uAPVU8+k/xYWvxRO3/tBN2pZkSMa5RjnPuUIbrwVxeA==} 440 | cpu: [riscv64] 441 | os: [linux] 442 | 443 | '@rollup/rollup-linux-s390x-gnu@4.39.0': 444 | resolution: {integrity: sha512-iRDJd2ebMunnk2rsSBYlsptCyuINvxUfGwOUldjv5M4tpa93K8tFMeYGpNk2+Nxl+OBJnBzy2/JCscGeO507kA==} 445 | cpu: [s390x] 446 | os: [linux] 447 | 448 | '@rollup/rollup-linux-x64-gnu@4.39.0': 449 | resolution: {integrity: sha512-t9jqYw27R6Lx0XKfEFe5vUeEJ5pF3SGIM6gTfONSMb7DuG6z6wfj2yjcoZxHg129veTqU7+wOhY6GX8wmf90dA==} 450 | cpu: [x64] 451 | os: [linux] 452 | 453 | '@rollup/rollup-linux-x64-musl@4.39.0': 454 | resolution: {integrity: sha512-ThFdkrFDP55AIsIZDKSBWEt/JcWlCzydbZHinZ0F/r1h83qbGeenCt/G/wG2O0reuENDD2tawfAj2s8VK7Bugg==} 455 | cpu: [x64] 456 | os: [linux] 457 | 458 | '@rollup/rollup-win32-arm64-msvc@4.39.0': 459 | resolution: {integrity: sha512-jDrLm6yUtbOg2TYB3sBF3acUnAwsIksEYjLeHL+TJv9jg+TmTwdyjnDex27jqEMakNKf3RwwPahDIt7QXCSqRQ==} 460 | cpu: [arm64] 461 | os: [win32] 462 | 463 | '@rollup/rollup-win32-ia32-msvc@4.39.0': 464 | resolution: {integrity: sha512-6w9uMuza+LbLCVoNKL5FSLE7yvYkq9laSd09bwS0tMjkwXrmib/4KmoJcrKhLWHvw19mwU+33ndC69T7weNNjQ==} 465 | cpu: [ia32] 466 | os: [win32] 467 | 468 | '@rollup/rollup-win32-x64-msvc@4.39.0': 469 | resolution: {integrity: sha512-yAkUOkIKZlK5dl7u6dg897doBgLXmUHhIINM2c+sND3DZwnrdQkkSiDh7N75Ll4mM4dxSkYfXqU9fW3lLkMFug==} 470 | cpu: [x64] 471 | os: [win32] 472 | 473 | '@types/estree@1.0.7': 474 | resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} 475 | 476 | '@vitest/expect@3.1.3': 477 | resolution: {integrity: sha512-7FTQQuuLKmN1Ig/h+h/GO+44Q1IlglPlR2es4ab7Yvfx+Uk5xsv+Ykk+MEt/M2Yn/xGmzaLKxGw2lgy2bwuYqg==} 478 | 479 | '@vitest/mocker@3.1.3': 480 | resolution: {integrity: sha512-PJbLjonJK82uCWHjzgBJZuR7zmAOrSvKk1QBxrennDIgtH4uK0TB1PvYmc0XBCigxxtiAVPfWtAdy4lpz8SQGQ==} 481 | peerDependencies: 482 | msw: ^2.4.9 483 | vite: ^5.0.0 || ^6.0.0 484 | peerDependenciesMeta: 485 | msw: 486 | optional: true 487 | vite: 488 | optional: true 489 | 490 | '@vitest/pretty-format@3.1.3': 491 | resolution: {integrity: sha512-i6FDiBeJUGLDKADw2Gb01UtUNb12yyXAqC/mmRWuYl+m/U9GS7s8us5ONmGkGpUUo7/iAYzI2ePVfOZTYvUifA==} 492 | 493 | '@vitest/pretty-format@3.2.1': 494 | resolution: {integrity: sha512-xBh1X2GPlOGBupp6E1RcUQWIxw0w/hRLd3XyBS6H+dMdKTAqHDNsIR2AnJwPA3yYe9DFy3VUKTe3VRTrAiQ01g==} 495 | 496 | '@vitest/runner@3.1.3': 497 | resolution: {integrity: sha512-Tae+ogtlNfFei5DggOsSUvkIaSuVywujMj6HzR97AHK6XK8i3BuVyIifWAm/sE3a15lF5RH9yQIrbXYuo0IFyA==} 498 | 499 | '@vitest/runner@3.2.1': 500 | resolution: {integrity: sha512-kygXhNTu/wkMYbwYpS3z/9tBe0O8qpdBuC3dD/AW9sWa0LE/DAZEjnHtWA9sIad7lpD4nFW1yQ+zN7mEKNH3yA==} 501 | 502 | '@vitest/snapshot@3.1.3': 503 | resolution: {integrity: sha512-XVa5OPNTYUsyqG9skuUkFzAeFnEzDp8hQu7kZ0N25B1+6KjGm4hWLtURyBbsIAOekfWQ7Wuz/N/XXzgYO3deWQ==} 504 | 505 | '@vitest/snapshot@3.2.1': 506 | resolution: {integrity: sha512-5xko/ZpW2Yc65NVK9Gpfg2y4BFvcF+At7yRT5AHUpTg9JvZ4xZoyuRY4ASlmNcBZjMslV08VRLDrBOmUe2YX3g==} 507 | 508 | '@vitest/spy@3.1.3': 509 | resolution: {integrity: sha512-x6w+ctOEmEXdWaa6TO4ilb7l9DxPR5bwEb6hILKuxfU1NqWT2mpJD9NJN7t3OTfxmVlOMrvtoFJGdgyzZ605lQ==} 510 | 511 | '@vitest/utils@3.1.3': 512 | resolution: {integrity: sha512-2Ltrpht4OmHO9+c/nmHtF09HWiyWdworqnHIwjfvDyWjuwKbdkcS9AnhsDn+8E2RM4x++foD1/tNuLPVvWG1Rg==} 513 | 514 | '@vitest/utils@3.2.1': 515 | resolution: {integrity: sha512-KkHlGhePEKZSub5ViknBcN5KEF+u7dSUr9NW8QsVICusUojrgrOnnY3DEWWO877ax2Pyopuk2qHmt+gkNKnBVw==} 516 | 517 | acorn-walk@8.3.2: 518 | resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} 519 | engines: {node: '>=0.4.0'} 520 | 521 | acorn@8.14.0: 522 | resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} 523 | engines: {node: '>=0.4.0'} 524 | hasBin: true 525 | 526 | as-table@1.0.55: 527 | resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==} 528 | 529 | assertion-error@2.0.1: 530 | resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} 531 | engines: {node: '>=12'} 532 | 533 | birpc@0.2.14: 534 | resolution: {integrity: sha512-37FHE8rqsYM5JEKCnXFyHpBCzvgHEExwVVTq+nUmloInU7l8ezD1TpOhKpS8oe1DTYFqEK27rFZVKG43oTqXRA==} 535 | 536 | blake3-wasm@2.1.5: 537 | resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} 538 | 539 | cac@6.7.14: 540 | resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} 541 | engines: {node: '>=8'} 542 | 543 | chai@5.2.0: 544 | resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==} 545 | engines: {node: '>=12'} 546 | 547 | check-error@2.1.1: 548 | resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} 549 | engines: {node: '>= 16'} 550 | 551 | cjs-module-lexer@1.4.3: 552 | resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} 553 | 554 | color-convert@2.0.1: 555 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 556 | engines: {node: '>=7.0.0'} 557 | 558 | color-name@1.1.4: 559 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 560 | 561 | color-string@1.9.1: 562 | resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} 563 | 564 | color@4.2.3: 565 | resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} 566 | engines: {node: '>=12.5.0'} 567 | 568 | cookie@0.7.2: 569 | resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} 570 | engines: {node: '>= 0.6'} 571 | 572 | data-uri-to-buffer@2.0.2: 573 | resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==} 574 | 575 | debug@4.4.1: 576 | resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} 577 | engines: {node: '>=6.0'} 578 | peerDependencies: 579 | supports-color: '*' 580 | peerDependenciesMeta: 581 | supports-color: 582 | optional: true 583 | 584 | deep-eql@5.0.2: 585 | resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} 586 | engines: {node: '>=6'} 587 | 588 | defu@6.1.4: 589 | resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} 590 | 591 | detect-libc@2.0.3: 592 | resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} 593 | engines: {node: '>=8'} 594 | 595 | devalue@4.3.3: 596 | resolution: {integrity: sha512-UH8EL6H2ifcY8TbD2QsxwCC/pr5xSwPvv85LrLXVihmHVC3T3YqTCIwnR5ak0yO1KYqlxrPVOA/JVZJYPy2ATg==} 597 | 598 | es-module-lexer@1.7.0: 599 | resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} 600 | 601 | esbuild@0.25.4: 602 | resolution: {integrity: sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==} 603 | engines: {node: '>=18'} 604 | hasBin: true 605 | 606 | estree-walker@3.0.3: 607 | resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} 608 | 609 | eventsource-parser@1.1.2: 610 | resolution: {integrity: sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==} 611 | engines: {node: '>=14.18'} 612 | 613 | exit-hook@2.2.1: 614 | resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==} 615 | engines: {node: '>=6'} 616 | 617 | expect-type@1.2.1: 618 | resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==} 619 | engines: {node: '>=12.0.0'} 620 | 621 | exsolve@1.0.4: 622 | resolution: {integrity: sha512-xsZH6PXaER4XoV+NiT7JHp1bJodJVT+cxeSH1G0f0tlT0lJqYuHUP3bUx2HtfTDvOagMINYp8rsqusxud3RXhw==} 623 | 624 | fdir@6.4.5: 625 | resolution: {integrity: sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw==} 626 | peerDependencies: 627 | picomatch: ^3 || ^4 628 | peerDependenciesMeta: 629 | picomatch: 630 | optional: true 631 | 632 | fsevents@2.3.3: 633 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 634 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 635 | os: [darwin] 636 | 637 | get-source@2.0.12: 638 | resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==} 639 | 640 | glob-to-regexp@0.4.1: 641 | resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} 642 | 643 | is-arrayish@0.3.2: 644 | resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} 645 | 646 | loupe@3.1.3: 647 | resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==} 648 | 649 | magic-string@0.30.17: 650 | resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} 651 | 652 | mime@3.0.0: 653 | resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} 654 | engines: {node: '>=10.0.0'} 655 | hasBin: true 656 | 657 | miniflare@4.20250525.1: 658 | resolution: {integrity: sha512-4PJlT5WA+hfclFU5Q7xnpG1G1VGYTXaf/3iu6iKQ8IsbSi9QvPTA2bSZ5goCFxmJXDjV4cxttVxB0Wl1CLuQ0w==} 659 | engines: {node: '>=18.0.0'} 660 | hasBin: true 661 | 662 | ms@2.1.3: 663 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 664 | 665 | mustache@4.2.0: 666 | resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} 667 | hasBin: true 668 | 669 | nanoid@3.3.11: 670 | resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} 671 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 672 | hasBin: true 673 | 674 | ohash@2.0.11: 675 | resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} 676 | 677 | path-to-regexp@6.3.0: 678 | resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} 679 | 680 | pathe@2.0.3: 681 | resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} 682 | 683 | pathval@2.0.0: 684 | resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} 685 | engines: {node: '>= 14.16'} 686 | 687 | picocolors@1.1.1: 688 | resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} 689 | 690 | picomatch@4.0.2: 691 | resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} 692 | engines: {node: '>=12'} 693 | 694 | postcss@8.5.3: 695 | resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} 696 | engines: {node: ^10 || ^12 || >=14} 697 | 698 | printable-characters@1.0.42: 699 | resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==} 700 | 701 | robot3@0.4.1: 702 | resolution: {integrity: sha512-hzjy826lrxzx8eRgv80idkf8ua1JAepRc9Efdtj03N3KNJuznQCPlyCJ7gnUmDFwZCLQjxy567mQVKmdv2BsXQ==} 703 | 704 | rollup@4.39.0: 705 | resolution: {integrity: sha512-thI8kNc02yNvnmJp8dr3fNWJ9tCONDhp6TV35X6HkKGGs9E6q7YWCHbe5vKiTa7TAiNcFEmXKj3X/pG2b3ci0g==} 706 | engines: {node: '>=18.0.0', npm: '>=8.0.0'} 707 | hasBin: true 708 | 709 | semver@7.7.1: 710 | resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} 711 | engines: {node: '>=10'} 712 | hasBin: true 713 | 714 | sharp@0.33.5: 715 | resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} 716 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 717 | 718 | siginfo@2.0.0: 719 | resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} 720 | 721 | simple-swizzle@0.2.2: 722 | resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} 723 | 724 | source-map-js@1.2.1: 725 | resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} 726 | engines: {node: '>=0.10.0'} 727 | 728 | source-map@0.6.1: 729 | resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} 730 | engines: {node: '>=0.10.0'} 731 | 732 | stackback@0.0.2: 733 | resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} 734 | 735 | stacktracey@2.1.8: 736 | resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==} 737 | 738 | std-env@3.9.0: 739 | resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} 740 | 741 | stoppable@1.1.0: 742 | resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} 743 | engines: {node: '>=4', npm: '>=6'} 744 | 745 | tinybench@2.9.0: 746 | resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} 747 | 748 | tinyexec@0.3.2: 749 | resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} 750 | 751 | tinyglobby@0.2.14: 752 | resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} 753 | engines: {node: '>=12.0.0'} 754 | 755 | tinypool@1.1.0: 756 | resolution: {integrity: sha512-7CotroY9a8DKsKprEy/a14aCCm8jYVmR7aFy4fpkZM8sdpNJbKkixuNjgM50yCmip2ezc8z4N7k3oe2+rfRJCQ==} 757 | engines: {node: ^18.0.0 || >=20.0.0} 758 | 759 | tinyrainbow@2.0.0: 760 | resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} 761 | engines: {node: '>=14.0.0'} 762 | 763 | tinyspy@3.0.2: 764 | resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} 765 | engines: {node: '>=14.0.0'} 766 | 767 | tslib@2.8.1: 768 | resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} 769 | 770 | typescript@5.8.3: 771 | resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} 772 | engines: {node: '>=14.17'} 773 | hasBin: true 774 | 775 | ufo@1.6.1: 776 | resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} 777 | 778 | undici@5.29.0: 779 | resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==} 780 | engines: {node: '>=14.0'} 781 | 782 | unenv@2.0.0-rc.17: 783 | resolution: {integrity: sha512-B06u0wXkEd+o5gOCMl/ZHl5cfpYbDZKAT+HWTL+Hws6jWu7dCiqBBXXXzMFcFVJb8D4ytAnYmxJA83uwOQRSsg==} 784 | 785 | vite-node@3.1.3: 786 | resolution: {integrity: sha512-uHV4plJ2IxCl4u1up1FQRrqclylKAogbtBfOTwcuJ28xFi+89PZ57BRh+naIRvH70HPwxy5QHYzg1OrEaC7AbA==} 787 | engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} 788 | hasBin: true 789 | 790 | vite@6.2.7: 791 | resolution: {integrity: sha512-qg3LkeuinTrZoJHHF94coSaTfIPyBYoywp+ys4qu20oSJFbKMYoIJo0FWJT9q6Vp49l6z9IsJRbHdcGtiKbGoQ==} 792 | engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} 793 | hasBin: true 794 | peerDependencies: 795 | '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 796 | jiti: '>=1.21.0' 797 | less: '*' 798 | lightningcss: ^1.21.0 799 | sass: '*' 800 | sass-embedded: '*' 801 | stylus: '*' 802 | sugarss: '*' 803 | terser: ^5.16.0 804 | tsx: ^4.8.1 805 | yaml: ^2.4.2 806 | peerDependenciesMeta: 807 | '@types/node': 808 | optional: true 809 | jiti: 810 | optional: true 811 | less: 812 | optional: true 813 | lightningcss: 814 | optional: true 815 | sass: 816 | optional: true 817 | sass-embedded: 818 | optional: true 819 | stylus: 820 | optional: true 821 | sugarss: 822 | optional: true 823 | terser: 824 | optional: true 825 | tsx: 826 | optional: true 827 | yaml: 828 | optional: true 829 | 830 | vitest@3.1.3: 831 | resolution: {integrity: sha512-188iM4hAHQ0km23TN/adso1q5hhwKqUpv+Sd6p5sOuh6FhQnRNW3IsiIpvxqahtBabsJ2SLZgmGSpcYK4wQYJw==} 832 | engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} 833 | hasBin: true 834 | peerDependencies: 835 | '@edge-runtime/vm': '*' 836 | '@types/debug': ^4.1.12 837 | '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 838 | '@vitest/browser': 3.1.3 839 | '@vitest/ui': 3.1.3 840 | happy-dom: '*' 841 | jsdom: '*' 842 | peerDependenciesMeta: 843 | '@edge-runtime/vm': 844 | optional: true 845 | '@types/debug': 846 | optional: true 847 | '@types/node': 848 | optional: true 849 | '@vitest/browser': 850 | optional: true 851 | '@vitest/ui': 852 | optional: true 853 | happy-dom: 854 | optional: true 855 | jsdom: 856 | optional: true 857 | 858 | why-is-node-running@2.3.0: 859 | resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} 860 | engines: {node: '>=8'} 861 | hasBin: true 862 | 863 | workerd@1.20250525.0: 864 | resolution: {integrity: sha512-SXJgLREy/Aqw2J71Oah0Pbu+SShbqbTExjVQyRBTM1r7MG7fS5NUlknhnt6sikjA/t4cO09Bi8OJqHdTkrcnYQ==} 865 | engines: {node: '>=16'} 866 | hasBin: true 867 | 868 | wrangler@4.19.1: 869 | resolution: {integrity: sha512-b+ed2SJKauHgndl4Im1wHE+FeSSlrdlEZNuvpc8q/94k4EmRxRkXnwBAsVWuicBxG3HStFLQPGGlvL8wGKTtHw==} 870 | engines: {node: '>=18.0.0'} 871 | hasBin: true 872 | peerDependencies: 873 | '@cloudflare/workers-types': ^4.20250525.0 874 | peerDependenciesMeta: 875 | '@cloudflare/workers-types': 876 | optional: true 877 | 878 | ws@8.18.0: 879 | resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} 880 | engines: {node: '>=10.0.0'} 881 | peerDependencies: 882 | bufferutil: ^4.0.1 883 | utf-8-validate: '>=5.0.2' 884 | peerDependenciesMeta: 885 | bufferutil: 886 | optional: true 887 | utf-8-validate: 888 | optional: true 889 | 890 | youch@3.3.4: 891 | resolution: {integrity: sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==} 892 | 893 | zod@3.22.3: 894 | resolution: {integrity: sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==} 895 | 896 | zod@3.24.2: 897 | resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==} 898 | 899 | snapshots: 900 | 901 | '@cloudflare/kv-asset-handler@0.4.0': 902 | dependencies: 903 | mime: 3.0.0 904 | 905 | '@cloudflare/unenv-preset@2.3.2(unenv@2.0.0-rc.17)(workerd@1.20250525.0)': 906 | dependencies: 907 | unenv: 2.0.0-rc.17 908 | optionalDependencies: 909 | workerd: 1.20250525.0 910 | 911 | '@cloudflare/vitest-pool-workers@0.8.36(@cloudflare/workers-types@4.20250605.0)(@vitest/runner@3.2.1)(@vitest/snapshot@3.2.1)(vitest@3.1.3)': 912 | dependencies: 913 | '@vitest/runner': 3.2.1 914 | '@vitest/snapshot': 3.2.1 915 | birpc: 0.2.14 916 | cjs-module-lexer: 1.4.3 917 | devalue: 4.3.3 918 | miniflare: 4.20250525.1 919 | semver: 7.7.1 920 | vitest: 3.1.3 921 | wrangler: 4.19.1(@cloudflare/workers-types@4.20250605.0) 922 | zod: 3.24.2 923 | transitivePeerDependencies: 924 | - '@cloudflare/workers-types' 925 | - bufferutil 926 | - utf-8-validate 927 | 928 | '@cloudflare/workerd-darwin-64@1.20250525.0': 929 | optional: true 930 | 931 | '@cloudflare/workerd-darwin-arm64@1.20250525.0': 932 | optional: true 933 | 934 | '@cloudflare/workerd-linux-64@1.20250525.0': 935 | optional: true 936 | 937 | '@cloudflare/workerd-linux-arm64@1.20250525.0': 938 | optional: true 939 | 940 | '@cloudflare/workerd-windows-64@1.20250525.0': 941 | optional: true 942 | 943 | '@cloudflare/workers-types@4.20250605.0': {} 944 | 945 | '@cspotcode/source-map-support@0.8.1': 946 | dependencies: 947 | '@jridgewell/trace-mapping': 0.3.9 948 | 949 | '@emnapi/runtime@1.4.0': 950 | dependencies: 951 | tslib: 2.8.1 952 | optional: true 953 | 954 | '@esbuild/aix-ppc64@0.25.4': 955 | optional: true 956 | 957 | '@esbuild/android-arm64@0.25.4': 958 | optional: true 959 | 960 | '@esbuild/android-arm@0.25.4': 961 | optional: true 962 | 963 | '@esbuild/android-x64@0.25.4': 964 | optional: true 965 | 966 | '@esbuild/darwin-arm64@0.25.4': 967 | optional: true 968 | 969 | '@esbuild/darwin-x64@0.25.4': 970 | optional: true 971 | 972 | '@esbuild/freebsd-arm64@0.25.4': 973 | optional: true 974 | 975 | '@esbuild/freebsd-x64@0.25.4': 976 | optional: true 977 | 978 | '@esbuild/linux-arm64@0.25.4': 979 | optional: true 980 | 981 | '@esbuild/linux-arm@0.25.4': 982 | optional: true 983 | 984 | '@esbuild/linux-ia32@0.25.4': 985 | optional: true 986 | 987 | '@esbuild/linux-loong64@0.25.4': 988 | optional: true 989 | 990 | '@esbuild/linux-mips64el@0.25.4': 991 | optional: true 992 | 993 | '@esbuild/linux-ppc64@0.25.4': 994 | optional: true 995 | 996 | '@esbuild/linux-riscv64@0.25.4': 997 | optional: true 998 | 999 | '@esbuild/linux-s390x@0.25.4': 1000 | optional: true 1001 | 1002 | '@esbuild/linux-x64@0.25.4': 1003 | optional: true 1004 | 1005 | '@esbuild/netbsd-arm64@0.25.4': 1006 | optional: true 1007 | 1008 | '@esbuild/netbsd-x64@0.25.4': 1009 | optional: true 1010 | 1011 | '@esbuild/openbsd-arm64@0.25.4': 1012 | optional: true 1013 | 1014 | '@esbuild/openbsd-x64@0.25.4': 1015 | optional: true 1016 | 1017 | '@esbuild/sunos-x64@0.25.4': 1018 | optional: true 1019 | 1020 | '@esbuild/win32-arm64@0.25.4': 1021 | optional: true 1022 | 1023 | '@esbuild/win32-ia32@0.25.4': 1024 | optional: true 1025 | 1026 | '@esbuild/win32-x64@0.25.4': 1027 | optional: true 1028 | 1029 | '@fal-ai/client@1.5.0': 1030 | dependencies: 1031 | '@msgpack/msgpack': 3.1.1 1032 | eventsource-parser: 1.1.2 1033 | robot3: 0.4.1 1034 | 1035 | '@fastify/busboy@2.1.1': {} 1036 | 1037 | '@img/sharp-darwin-arm64@0.33.5': 1038 | optionalDependencies: 1039 | '@img/sharp-libvips-darwin-arm64': 1.0.4 1040 | optional: true 1041 | 1042 | '@img/sharp-darwin-x64@0.33.5': 1043 | optionalDependencies: 1044 | '@img/sharp-libvips-darwin-x64': 1.0.4 1045 | optional: true 1046 | 1047 | '@img/sharp-libvips-darwin-arm64@1.0.4': 1048 | optional: true 1049 | 1050 | '@img/sharp-libvips-darwin-x64@1.0.4': 1051 | optional: true 1052 | 1053 | '@img/sharp-libvips-linux-arm64@1.0.4': 1054 | optional: true 1055 | 1056 | '@img/sharp-libvips-linux-arm@1.0.5': 1057 | optional: true 1058 | 1059 | '@img/sharp-libvips-linux-s390x@1.0.4': 1060 | optional: true 1061 | 1062 | '@img/sharp-libvips-linux-x64@1.0.4': 1063 | optional: true 1064 | 1065 | '@img/sharp-libvips-linuxmusl-arm64@1.0.4': 1066 | optional: true 1067 | 1068 | '@img/sharp-libvips-linuxmusl-x64@1.0.4': 1069 | optional: true 1070 | 1071 | '@img/sharp-linux-arm64@0.33.5': 1072 | optionalDependencies: 1073 | '@img/sharp-libvips-linux-arm64': 1.0.4 1074 | optional: true 1075 | 1076 | '@img/sharp-linux-arm@0.33.5': 1077 | optionalDependencies: 1078 | '@img/sharp-libvips-linux-arm': 1.0.5 1079 | optional: true 1080 | 1081 | '@img/sharp-linux-s390x@0.33.5': 1082 | optionalDependencies: 1083 | '@img/sharp-libvips-linux-s390x': 1.0.4 1084 | optional: true 1085 | 1086 | '@img/sharp-linux-x64@0.33.5': 1087 | optionalDependencies: 1088 | '@img/sharp-libvips-linux-x64': 1.0.4 1089 | optional: true 1090 | 1091 | '@img/sharp-linuxmusl-arm64@0.33.5': 1092 | optionalDependencies: 1093 | '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 1094 | optional: true 1095 | 1096 | '@img/sharp-linuxmusl-x64@0.33.5': 1097 | optionalDependencies: 1098 | '@img/sharp-libvips-linuxmusl-x64': 1.0.4 1099 | optional: true 1100 | 1101 | '@img/sharp-wasm32@0.33.5': 1102 | dependencies: 1103 | '@emnapi/runtime': 1.4.0 1104 | optional: true 1105 | 1106 | '@img/sharp-win32-ia32@0.33.5': 1107 | optional: true 1108 | 1109 | '@img/sharp-win32-x64@0.33.5': 1110 | optional: true 1111 | 1112 | '@jridgewell/resolve-uri@3.1.2': {} 1113 | 1114 | '@jridgewell/sourcemap-codec@1.5.0': {} 1115 | 1116 | '@jridgewell/trace-mapping@0.3.9': 1117 | dependencies: 1118 | '@jridgewell/resolve-uri': 3.1.2 1119 | '@jridgewell/sourcemap-codec': 1.5.0 1120 | 1121 | '@msgpack/msgpack@3.1.1': {} 1122 | 1123 | '@rollup/rollup-android-arm-eabi@4.39.0': 1124 | optional: true 1125 | 1126 | '@rollup/rollup-android-arm64@4.39.0': 1127 | optional: true 1128 | 1129 | '@rollup/rollup-darwin-arm64@4.39.0': 1130 | optional: true 1131 | 1132 | '@rollup/rollup-darwin-x64@4.39.0': 1133 | optional: true 1134 | 1135 | '@rollup/rollup-freebsd-arm64@4.39.0': 1136 | optional: true 1137 | 1138 | '@rollup/rollup-freebsd-x64@4.39.0': 1139 | optional: true 1140 | 1141 | '@rollup/rollup-linux-arm-gnueabihf@4.39.0': 1142 | optional: true 1143 | 1144 | '@rollup/rollup-linux-arm-musleabihf@4.39.0': 1145 | optional: true 1146 | 1147 | '@rollup/rollup-linux-arm64-gnu@4.39.0': 1148 | optional: true 1149 | 1150 | '@rollup/rollup-linux-arm64-musl@4.39.0': 1151 | optional: true 1152 | 1153 | '@rollup/rollup-linux-loongarch64-gnu@4.39.0': 1154 | optional: true 1155 | 1156 | '@rollup/rollup-linux-powerpc64le-gnu@4.39.0': 1157 | optional: true 1158 | 1159 | '@rollup/rollup-linux-riscv64-gnu@4.39.0': 1160 | optional: true 1161 | 1162 | '@rollup/rollup-linux-riscv64-musl@4.39.0': 1163 | optional: true 1164 | 1165 | '@rollup/rollup-linux-s390x-gnu@4.39.0': 1166 | optional: true 1167 | 1168 | '@rollup/rollup-linux-x64-gnu@4.39.0': 1169 | optional: true 1170 | 1171 | '@rollup/rollup-linux-x64-musl@4.39.0': 1172 | optional: true 1173 | 1174 | '@rollup/rollup-win32-arm64-msvc@4.39.0': 1175 | optional: true 1176 | 1177 | '@rollup/rollup-win32-ia32-msvc@4.39.0': 1178 | optional: true 1179 | 1180 | '@rollup/rollup-win32-x64-msvc@4.39.0': 1181 | optional: true 1182 | 1183 | '@types/estree@1.0.7': {} 1184 | 1185 | '@vitest/expect@3.1.3': 1186 | dependencies: 1187 | '@vitest/spy': 3.1.3 1188 | '@vitest/utils': 3.1.3 1189 | chai: 5.2.0 1190 | tinyrainbow: 2.0.0 1191 | 1192 | '@vitest/mocker@3.1.3(vite@6.2.7)': 1193 | dependencies: 1194 | '@vitest/spy': 3.1.3 1195 | estree-walker: 3.0.3 1196 | magic-string: 0.30.17 1197 | optionalDependencies: 1198 | vite: 6.2.7 1199 | 1200 | '@vitest/pretty-format@3.1.3': 1201 | dependencies: 1202 | tinyrainbow: 2.0.0 1203 | 1204 | '@vitest/pretty-format@3.2.1': 1205 | dependencies: 1206 | tinyrainbow: 2.0.0 1207 | 1208 | '@vitest/runner@3.1.3': 1209 | dependencies: 1210 | '@vitest/utils': 3.1.3 1211 | pathe: 2.0.3 1212 | 1213 | '@vitest/runner@3.2.1': 1214 | dependencies: 1215 | '@vitest/utils': 3.2.1 1216 | pathe: 2.0.3 1217 | 1218 | '@vitest/snapshot@3.1.3': 1219 | dependencies: 1220 | '@vitest/pretty-format': 3.1.3 1221 | magic-string: 0.30.17 1222 | pathe: 2.0.3 1223 | 1224 | '@vitest/snapshot@3.2.1': 1225 | dependencies: 1226 | '@vitest/pretty-format': 3.2.1 1227 | magic-string: 0.30.17 1228 | pathe: 2.0.3 1229 | 1230 | '@vitest/spy@3.1.3': 1231 | dependencies: 1232 | tinyspy: 3.0.2 1233 | 1234 | '@vitest/utils@3.1.3': 1235 | dependencies: 1236 | '@vitest/pretty-format': 3.1.3 1237 | loupe: 3.1.3 1238 | tinyrainbow: 2.0.0 1239 | 1240 | '@vitest/utils@3.2.1': 1241 | dependencies: 1242 | '@vitest/pretty-format': 3.2.1 1243 | loupe: 3.1.3 1244 | tinyrainbow: 2.0.0 1245 | 1246 | acorn-walk@8.3.2: {} 1247 | 1248 | acorn@8.14.0: {} 1249 | 1250 | as-table@1.0.55: 1251 | dependencies: 1252 | printable-characters: 1.0.42 1253 | 1254 | assertion-error@2.0.1: {} 1255 | 1256 | birpc@0.2.14: {} 1257 | 1258 | blake3-wasm@2.1.5: {} 1259 | 1260 | cac@6.7.14: {} 1261 | 1262 | chai@5.2.0: 1263 | dependencies: 1264 | assertion-error: 2.0.1 1265 | check-error: 2.1.1 1266 | deep-eql: 5.0.2 1267 | loupe: 3.1.3 1268 | pathval: 2.0.0 1269 | 1270 | check-error@2.1.1: {} 1271 | 1272 | cjs-module-lexer@1.4.3: {} 1273 | 1274 | color-convert@2.0.1: 1275 | dependencies: 1276 | color-name: 1.1.4 1277 | 1278 | color-name@1.1.4: {} 1279 | 1280 | color-string@1.9.1: 1281 | dependencies: 1282 | color-name: 1.1.4 1283 | simple-swizzle: 0.2.2 1284 | 1285 | color@4.2.3: 1286 | dependencies: 1287 | color-convert: 2.0.1 1288 | color-string: 1.9.1 1289 | 1290 | cookie@0.7.2: {} 1291 | 1292 | data-uri-to-buffer@2.0.2: {} 1293 | 1294 | debug@4.4.1: 1295 | dependencies: 1296 | ms: 2.1.3 1297 | 1298 | deep-eql@5.0.2: {} 1299 | 1300 | defu@6.1.4: {} 1301 | 1302 | detect-libc@2.0.3: {} 1303 | 1304 | devalue@4.3.3: {} 1305 | 1306 | es-module-lexer@1.7.0: {} 1307 | 1308 | esbuild@0.25.4: 1309 | optionalDependencies: 1310 | '@esbuild/aix-ppc64': 0.25.4 1311 | '@esbuild/android-arm': 0.25.4 1312 | '@esbuild/android-arm64': 0.25.4 1313 | '@esbuild/android-x64': 0.25.4 1314 | '@esbuild/darwin-arm64': 0.25.4 1315 | '@esbuild/darwin-x64': 0.25.4 1316 | '@esbuild/freebsd-arm64': 0.25.4 1317 | '@esbuild/freebsd-x64': 0.25.4 1318 | '@esbuild/linux-arm': 0.25.4 1319 | '@esbuild/linux-arm64': 0.25.4 1320 | '@esbuild/linux-ia32': 0.25.4 1321 | '@esbuild/linux-loong64': 0.25.4 1322 | '@esbuild/linux-mips64el': 0.25.4 1323 | '@esbuild/linux-ppc64': 0.25.4 1324 | '@esbuild/linux-riscv64': 0.25.4 1325 | '@esbuild/linux-s390x': 0.25.4 1326 | '@esbuild/linux-x64': 0.25.4 1327 | '@esbuild/netbsd-arm64': 0.25.4 1328 | '@esbuild/netbsd-x64': 0.25.4 1329 | '@esbuild/openbsd-arm64': 0.25.4 1330 | '@esbuild/openbsd-x64': 0.25.4 1331 | '@esbuild/sunos-x64': 0.25.4 1332 | '@esbuild/win32-arm64': 0.25.4 1333 | '@esbuild/win32-ia32': 0.25.4 1334 | '@esbuild/win32-x64': 0.25.4 1335 | 1336 | estree-walker@3.0.3: 1337 | dependencies: 1338 | '@types/estree': 1.0.7 1339 | 1340 | eventsource-parser@1.1.2: {} 1341 | 1342 | exit-hook@2.2.1: {} 1343 | 1344 | expect-type@1.2.1: {} 1345 | 1346 | exsolve@1.0.4: {} 1347 | 1348 | fdir@6.4.5(picomatch@4.0.2): 1349 | optionalDependencies: 1350 | picomatch: 4.0.2 1351 | 1352 | fsevents@2.3.3: 1353 | optional: true 1354 | 1355 | get-source@2.0.12: 1356 | dependencies: 1357 | data-uri-to-buffer: 2.0.2 1358 | source-map: 0.6.1 1359 | 1360 | glob-to-regexp@0.4.1: {} 1361 | 1362 | is-arrayish@0.3.2: {} 1363 | 1364 | loupe@3.1.3: {} 1365 | 1366 | magic-string@0.30.17: 1367 | dependencies: 1368 | '@jridgewell/sourcemap-codec': 1.5.0 1369 | 1370 | mime@3.0.0: {} 1371 | 1372 | miniflare@4.20250525.1: 1373 | dependencies: 1374 | '@cspotcode/source-map-support': 0.8.1 1375 | acorn: 8.14.0 1376 | acorn-walk: 8.3.2 1377 | exit-hook: 2.2.1 1378 | glob-to-regexp: 0.4.1 1379 | sharp: 0.33.5 1380 | stoppable: 1.1.0 1381 | undici: 5.29.0 1382 | workerd: 1.20250525.0 1383 | ws: 8.18.0 1384 | youch: 3.3.4 1385 | zod: 3.22.3 1386 | transitivePeerDependencies: 1387 | - bufferutil 1388 | - utf-8-validate 1389 | 1390 | ms@2.1.3: {} 1391 | 1392 | mustache@4.2.0: {} 1393 | 1394 | nanoid@3.3.11: {} 1395 | 1396 | ohash@2.0.11: {} 1397 | 1398 | path-to-regexp@6.3.0: {} 1399 | 1400 | pathe@2.0.3: {} 1401 | 1402 | pathval@2.0.0: {} 1403 | 1404 | picocolors@1.1.1: {} 1405 | 1406 | picomatch@4.0.2: {} 1407 | 1408 | postcss@8.5.3: 1409 | dependencies: 1410 | nanoid: 3.3.11 1411 | picocolors: 1.1.1 1412 | source-map-js: 1.2.1 1413 | 1414 | printable-characters@1.0.42: {} 1415 | 1416 | robot3@0.4.1: {} 1417 | 1418 | rollup@4.39.0: 1419 | dependencies: 1420 | '@types/estree': 1.0.7 1421 | optionalDependencies: 1422 | '@rollup/rollup-android-arm-eabi': 4.39.0 1423 | '@rollup/rollup-android-arm64': 4.39.0 1424 | '@rollup/rollup-darwin-arm64': 4.39.0 1425 | '@rollup/rollup-darwin-x64': 4.39.0 1426 | '@rollup/rollup-freebsd-arm64': 4.39.0 1427 | '@rollup/rollup-freebsd-x64': 4.39.0 1428 | '@rollup/rollup-linux-arm-gnueabihf': 4.39.0 1429 | '@rollup/rollup-linux-arm-musleabihf': 4.39.0 1430 | '@rollup/rollup-linux-arm64-gnu': 4.39.0 1431 | '@rollup/rollup-linux-arm64-musl': 4.39.0 1432 | '@rollup/rollup-linux-loongarch64-gnu': 4.39.0 1433 | '@rollup/rollup-linux-powerpc64le-gnu': 4.39.0 1434 | '@rollup/rollup-linux-riscv64-gnu': 4.39.0 1435 | '@rollup/rollup-linux-riscv64-musl': 4.39.0 1436 | '@rollup/rollup-linux-s390x-gnu': 4.39.0 1437 | '@rollup/rollup-linux-x64-gnu': 4.39.0 1438 | '@rollup/rollup-linux-x64-musl': 4.39.0 1439 | '@rollup/rollup-win32-arm64-msvc': 4.39.0 1440 | '@rollup/rollup-win32-ia32-msvc': 4.39.0 1441 | '@rollup/rollup-win32-x64-msvc': 4.39.0 1442 | fsevents: 2.3.3 1443 | 1444 | semver@7.7.1: {} 1445 | 1446 | sharp@0.33.5: 1447 | dependencies: 1448 | color: 4.2.3 1449 | detect-libc: 2.0.3 1450 | semver: 7.7.1 1451 | optionalDependencies: 1452 | '@img/sharp-darwin-arm64': 0.33.5 1453 | '@img/sharp-darwin-x64': 0.33.5 1454 | '@img/sharp-libvips-darwin-arm64': 1.0.4 1455 | '@img/sharp-libvips-darwin-x64': 1.0.4 1456 | '@img/sharp-libvips-linux-arm': 1.0.5 1457 | '@img/sharp-libvips-linux-arm64': 1.0.4 1458 | '@img/sharp-libvips-linux-s390x': 1.0.4 1459 | '@img/sharp-libvips-linux-x64': 1.0.4 1460 | '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 1461 | '@img/sharp-libvips-linuxmusl-x64': 1.0.4 1462 | '@img/sharp-linux-arm': 0.33.5 1463 | '@img/sharp-linux-arm64': 0.33.5 1464 | '@img/sharp-linux-s390x': 0.33.5 1465 | '@img/sharp-linux-x64': 0.33.5 1466 | '@img/sharp-linuxmusl-arm64': 0.33.5 1467 | '@img/sharp-linuxmusl-x64': 0.33.5 1468 | '@img/sharp-wasm32': 0.33.5 1469 | '@img/sharp-win32-ia32': 0.33.5 1470 | '@img/sharp-win32-x64': 0.33.5 1471 | 1472 | siginfo@2.0.0: {} 1473 | 1474 | simple-swizzle@0.2.2: 1475 | dependencies: 1476 | is-arrayish: 0.3.2 1477 | 1478 | source-map-js@1.2.1: {} 1479 | 1480 | source-map@0.6.1: {} 1481 | 1482 | stackback@0.0.2: {} 1483 | 1484 | stacktracey@2.1.8: 1485 | dependencies: 1486 | as-table: 1.0.55 1487 | get-source: 2.0.12 1488 | 1489 | std-env@3.9.0: {} 1490 | 1491 | stoppable@1.1.0: {} 1492 | 1493 | tinybench@2.9.0: {} 1494 | 1495 | tinyexec@0.3.2: {} 1496 | 1497 | tinyglobby@0.2.14: 1498 | dependencies: 1499 | fdir: 6.4.5(picomatch@4.0.2) 1500 | picomatch: 4.0.2 1501 | 1502 | tinypool@1.1.0: {} 1503 | 1504 | tinyrainbow@2.0.0: {} 1505 | 1506 | tinyspy@3.0.2: {} 1507 | 1508 | tslib@2.8.1: 1509 | optional: true 1510 | 1511 | typescript@5.8.3: {} 1512 | 1513 | ufo@1.6.1: {} 1514 | 1515 | undici@5.29.0: 1516 | dependencies: 1517 | '@fastify/busboy': 2.1.1 1518 | 1519 | unenv@2.0.0-rc.17: 1520 | dependencies: 1521 | defu: 6.1.4 1522 | exsolve: 1.0.4 1523 | ohash: 2.0.11 1524 | pathe: 2.0.3 1525 | ufo: 1.6.1 1526 | 1527 | vite-node@3.1.3: 1528 | dependencies: 1529 | cac: 6.7.14 1530 | debug: 4.4.1 1531 | es-module-lexer: 1.7.0 1532 | pathe: 2.0.3 1533 | vite: 6.2.7 1534 | transitivePeerDependencies: 1535 | - '@types/node' 1536 | - jiti 1537 | - less 1538 | - lightningcss 1539 | - sass 1540 | - sass-embedded 1541 | - stylus 1542 | - sugarss 1543 | - supports-color 1544 | - terser 1545 | - tsx 1546 | - yaml 1547 | 1548 | vite@6.2.7: 1549 | dependencies: 1550 | esbuild: 0.25.4 1551 | postcss: 8.5.3 1552 | rollup: 4.39.0 1553 | optionalDependencies: 1554 | fsevents: 2.3.3 1555 | 1556 | vitest@3.1.3: 1557 | dependencies: 1558 | '@vitest/expect': 3.1.3 1559 | '@vitest/mocker': 3.1.3(vite@6.2.7) 1560 | '@vitest/pretty-format': 3.2.1 1561 | '@vitest/runner': 3.1.3 1562 | '@vitest/snapshot': 3.1.3 1563 | '@vitest/spy': 3.1.3 1564 | '@vitest/utils': 3.1.3 1565 | chai: 5.2.0 1566 | debug: 4.4.1 1567 | expect-type: 1.2.1 1568 | magic-string: 0.30.17 1569 | pathe: 2.0.3 1570 | std-env: 3.9.0 1571 | tinybench: 2.9.0 1572 | tinyexec: 0.3.2 1573 | tinyglobby: 0.2.14 1574 | tinypool: 1.1.0 1575 | tinyrainbow: 2.0.0 1576 | vite: 6.2.7 1577 | vite-node: 3.1.3 1578 | why-is-node-running: 2.3.0 1579 | transitivePeerDependencies: 1580 | - jiti 1581 | - less 1582 | - lightningcss 1583 | - msw 1584 | - sass 1585 | - sass-embedded 1586 | - stylus 1587 | - sugarss 1588 | - supports-color 1589 | - terser 1590 | - tsx 1591 | - yaml 1592 | 1593 | why-is-node-running@2.3.0: 1594 | dependencies: 1595 | siginfo: 2.0.0 1596 | stackback: 0.0.2 1597 | 1598 | workerd@1.20250525.0: 1599 | optionalDependencies: 1600 | '@cloudflare/workerd-darwin-64': 1.20250525.0 1601 | '@cloudflare/workerd-darwin-arm64': 1.20250525.0 1602 | '@cloudflare/workerd-linux-64': 1.20250525.0 1603 | '@cloudflare/workerd-linux-arm64': 1.20250525.0 1604 | '@cloudflare/workerd-windows-64': 1.20250525.0 1605 | 1606 | wrangler@4.19.1(@cloudflare/workers-types@4.20250605.0): 1607 | dependencies: 1608 | '@cloudflare/kv-asset-handler': 0.4.0 1609 | '@cloudflare/unenv-preset': 2.3.2(unenv@2.0.0-rc.17)(workerd@1.20250525.0) 1610 | blake3-wasm: 2.1.5 1611 | esbuild: 0.25.4 1612 | miniflare: 4.20250525.1 1613 | path-to-regexp: 6.3.0 1614 | unenv: 2.0.0-rc.17 1615 | workerd: 1.20250525.0 1616 | optionalDependencies: 1617 | '@cloudflare/workers-types': 4.20250605.0 1618 | fsevents: 2.3.3 1619 | transitivePeerDependencies: 1620 | - bufferutil 1621 | - utf-8-validate 1622 | 1623 | ws@8.18.0: {} 1624 | 1625 | youch@3.3.4: 1626 | dependencies: 1627 | cookie: 0.7.2 1628 | mustache: 4.2.0 1629 | stacktracey: 2.1.8 1630 | 1631 | zod@3.22.3: {} 1632 | 1633 | zod@3.24.2: {} 1634 | --------------------------------------------------------------------------------