diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..44ac576
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,50 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+permissions:
+ contents: read
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Setup pnpm
+ uses: pnpm/action-setup@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '22'
+ cache: 'pnpm'
+
+ - name: Install dependencies - root
+ run: pnpm install
+
+ - name: Install dependencies - inspector example
+ run: pnpm install
+ working-directory: examples/inspector
+
+ - name: Install dependencies - cf-agents example
+ run: pnpm install
+ working-directory: examples/servers/cf-agents
+
+ - name: Install dependencies - hono-mcp example
+ run: pnpm install
+ working-directory: examples/servers/hono-mcp
+
+ - name: Install dependencies - test
+ run: pnpm install && pnpm exec playwright install
+ working-directory: test
+
+ - name: Run tests
+ run: pnpm test:headless
+ working-directory: test
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 3e947d1..e335d95 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -1,12 +1,11 @@
name: Publish Any Commit
on:
- # Temporarily disabled - uncomment to re-enable
- # pull_request:
- # push:
- # branches:
- # - '**'
- # tags:
- # - '!**'
+ pull_request:
+ push:
+ branches:
+ - '**'
+ tags:
+ - '!**'
jobs:
build:
diff --git a/.gitignore b/.gitignore
index 8aa36e2..73d103a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,4 +5,4 @@ dist
.aider*
**/.claude/settings.local.json
-public
+/public
diff --git a/.husky/pre-commit b/.husky/pre-commit
new file mode 100755
index 0000000..8ec9dd5
--- /dev/null
+++ b/.husky/pre-commit
@@ -0,0 +1 @@
+./scripts/pre-commit
diff --git a/AGENT.md b/AGENT.md
index a2cf9e8..5dffb50 100644
--- a/AGENT.md
+++ b/AGENT.md
@@ -1,10 +1,21 @@
# use-mcp Project Guidelines
## Build, Lint, and Test Commands
-- `pnpm dev`: Run development build with watch mode
+- `pnpm dev`: Run development build with watch mode and start all examples/servers
+ - Chat UI: http://localhost:5002
+ - Inspector: http://localhost:5001
+ - Hono MCP Server: http://localhost:5101
+ - CF Agents MCP Server: http://localhost:5102
- `pnpm build`: Build the project
- `pnpm check`: Run prettier checks and TypeScript type checking
+### Integration Tests (in /test directory)
+- `cd test && pnpm test`: Run integration tests headlessly (default)
+- `cd test && pnpm test:headed`: Run integration tests with visible browser
+- `cd test && pnpm test:headless`: Run integration tests headlessly
+- `cd test && pnpm test:watch`: Run integration tests in watch mode
+- `cd test && pnpm test:ui`: Run integration tests with interactive UI
+
## Code Style Guidelines
### Imports
diff --git a/README.md b/README.md
index 6f94dbb..b247654 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
A lightweight React hook for connecting to [Model Context Protocol (MCP)](https://github.com/modelcontextprotocol) servers. Simplifies authentication and tool calling for AI systems implementing the MCP standard.
-Try it out: [MCP Inspector](https://inspector.use-mcp.dev) | [Cloudflare Workers AI Playground](https://playground.ai.cloudflare.com/)
+Try it out: [Chat Demo](https://chat.use-mcp.dev) | [MCP Inspector](https://inspector.use-mcp.dev) | [Cloudflare Workers AI Playground](https://playground.ai.cloudflare.com/)
## Installation
@@ -20,11 +20,39 @@ pnpm add use-mcp
yarn add use-mcp
```
+## Development
+
+To run the development environment with all examples and servers:
+
+```bash
+pnpm dev
+```
+
+This starts:
+- **Inspector**: http://localhost:5001 - MCP server debugging tool
+- **Chat UI**: http://localhost:5002 - Example chat interface
+- **Hono MCP Server**: http://localhost:5101 - Example MCP server
+- **CF Agents MCP Server**: http://localhost:5102 - Cloudflare Workers AI MCP server
+
+### Testing
+
+Integration tests are located in the `test/` directory and run headlessly by default:
+
+```bash
+cd test && pnpm test # Run tests headlessly (default)
+cd test && pnpm test:headed # Run tests with visible browser
+cd test && pnpm test:watch # Run tests in watch mode
+cd test && pnpm test:ui # Run tests with interactive UI
+```
+
## Features
- ๐ Automatic connection management with reconnection and retries
- ๐ OAuth authentication flow handling with popup and fallback support
- ๐ฆ Simple React hook interface for MCP integration
+- ๐งฐ Full support for MCP tools, resources, and prompts
+- ๐ Access server resources and read their contents
+- ๐ฌ Use server-provided prompt templates
- ๐งฐ TypeScript types for editor assistance and type checking
- ๐ Comprehensive logging for debugging
- ๐ Works with both HTTP and SSE (Server-Sent Events) transports
@@ -36,10 +64,14 @@ import { useMcp } from 'use-mcp/react'
function MyAIComponent() {
const {
- state, // Connection state: 'discovering' | 'authenticating' | 'connecting' | 'loading' | 'ready' | 'failed'
+ state, // Connection state: 'discovering' | 'pending_auth' | 'authenticating' | 'connecting' | 'loading' | 'ready' | 'failed'
tools, // Available tools from MCP server
+ resources, // Available resources from MCP server
+ prompts, // Available prompts from MCP server
error, // Error message if connection failed
callTool, // Function to call tools on the MCP server
+ readResource, // Function to read resource contents
+ getPrompt, // Function to get prompt messages
retry, // Retry connection manually
authenticate, // Manually trigger authentication
clearStorage, // Clear stored tokens and credentials
@@ -83,6 +115,32 @@ function MyAIComponent() {
))}
Search
+
+ {/* Example: Display and read resources */}
+ {resources.length > 0 && (
+
+
Resources: {resources.length}
+ {
+ const content = await readResource(resources[0].uri)
+ console.log('Resource content:', content)
+ }}>
+ Read First Resource
+
+
+ )}
+
+ {/* Example: Use prompts */}
+ {prompts.length > 0 && (
+
+
Prompts: {prompts.length}
+ {
+ const result = await getPrompt(prompts[0].name)
+ console.log('Prompt messages:', result.messages)
+ }}>
+ Get First Prompt
+
+
+ )}
)
}
@@ -167,17 +225,27 @@ function useMcp(options: UseMcpOptions): UseMcpResult
| `debug` | `boolean` | Whether to enable verbose debug logging |
| `autoRetry` | `boolean \| number` | Auto retry connection if initial connection fails, with delay in ms |
| `autoReconnect` | `boolean \| number` | Auto reconnect if an established connection is lost, with delay in ms (default: 3000) |
+| `transportType` | `'auto' \| 'http' \| 'sse'` | Transport type preference: 'auto' (HTTP with SSE fallback), 'http' (HTTP only), 'sse' (SSE only) (default: 'auto') |
+| `preventAutoAuth` | `boolean` | Prevent automatic authentication popup on initial connection (default: false) |
+| `onPopupWindow` | `(url: string, features: string, window: Window \| null) => void` | Callback invoked just after the authentication popup window is opened |
#### Return Value
| Property | Type | Description |
|----------|------|-------------|
-| `state` | `string` | Current connection state: 'discovering', 'authenticating', 'connecting', 'loading', 'ready', 'failed' |
+| `state` | `string` | Current connection state: 'discovering', 'pending_auth', 'authenticating', 'connecting', 'loading', 'ready', 'failed' |
| `tools` | `Tool[]` | Available tools from the MCP server |
+| `resources` | `Resource[]` | Available resources from the MCP server |
+| `resourceTemplates` | `ResourceTemplate[]` | Available resource templates from the MCP server |
+| `prompts` | `Prompt[]` | Available prompts from the MCP server |
| `error` | `string \| undefined` | Error message if connection failed |
| `authUrl` | `string \| undefined` | Manual authentication URL if popup is blocked |
| `log` | `{ level: 'debug' \| 'info' \| 'warn' \| 'error'; message: string; timestamp: number }[]` | Array of log messages |
| `callTool` | `(name: string, args?: Record) => Promise` | Function to call a tool on the MCP server |
+| `listResources` | `() => Promise` | Refresh the list of available resources |
+| `readResource` | `(uri: string) => Promise<{ contents: Array<...> }>` | Read the contents of a specific resource |
+| `listPrompts` | `() => Promise` | Refresh the list of available prompts |
+| `getPrompt` | `(name: string, args?: Record) => Promise<{ messages: Array<...> }>` | Get a specific prompt with optional arguments |
| `retry` | `() => void` | Manually attempt to reconnect |
| `disconnect` | `() => void` | Disconnect from the MCP server |
| `authenticate` | `() => void` | Manually trigger authentication |
diff --git a/examples/chat-ui/.env.example b/examples/chat-ui/.env.example
new file mode 100644
index 0000000..06b8d52
--- /dev/null
+++ b/examples/chat-ui/.env.example
@@ -0,0 +1,3 @@
+# OpenRouter OAuth Configuration
+# Note: OpenRouter OAuth doesn't require client ID, only callback URL configuration
+VITE_OPENROUTER_REDIRECT_URI=http://localhost:5002/oauth/openrouter/callback
diff --git a/examples/chat-ui/MODEL_SELECTOR_GUIDE.md b/examples/chat-ui/MODEL_SELECTOR_GUIDE.md
new file mode 100644
index 0000000..ea8a1bc
--- /dev/null
+++ b/examples/chat-ui/MODEL_SELECTOR_GUIDE.md
@@ -0,0 +1,92 @@
+# Model Selector Guide
+
+This guide explains how to use the new favorites-based model selector system.
+
+## Overview
+
+The chat UI now supports:
+- **39 models** from 3 providers (Anthropic, Groq, OpenRouter)
+- **Favorites system** - star your preferred models
+- **Search functionality** - find models by name or provider
+- **Tool filtering** - show only models that support tool calling
+- **OAuth authentication** - seamless OpenRouter integration
+
+## Features
+
+### Model Selection
+- Click the model selector to view all available models
+- Models are loaded from the live models.dev API
+- Each model shows provider logo, name, and capabilities
+
+### Favorites System
+- โญ Star models to add them to your favorites
+- Starred models are saved to local storage
+- Use the "Favorites" filter to show only starred models
+
+### Search & Filtering
+- ๐ Search box to find models by name or provider
+- ๐ง "Tools Only" filter (appears when MCP tools are available)
+- โญ "Favorites" filter to show only starred models
+
+### Authentication
+- **API Keys**: Enter manually for Anthropic and Groq
+- **OAuth**: One-click authentication for OpenRouter
+- Authentication status shown with icons (โ or โ ๏ธ)
+
+## Provider Support
+
+### Anthropic
+- **Models**: 9 models including Claude 4 Sonnet
+- **Auth**: API key (requires manual entry)
+- **Tools**: All models support tool calling
+
+### Groq
+- **Models**: 13 models including Llama and Qwen variants
+- **Auth**: API key (requires manual entry)
+- **Tools**: 11 models support tool calling
+
+### OpenRouter
+- **Models**: 17 models from various providers
+- **Auth**: OAuth PKCE flow (one-click authentication)
+- **Tools**: All models support tool calling
+
+## Setting Up OpenRouter OAuth
+
+1. Create an OpenRouter account at https://openrouter.ai
+2. Go to your dashboard and create an OAuth app
+3. Set the redirect URI to: `http://localhost:5002/oauth/openrouter/callback`
+4. Copy your client ID to your `.env` file:
+ ```
+ VITE_OPENROUTER_CLIENT_ID=your_client_id_here
+ ```
+
+## Usage Tips
+
+1. **Star your favorites** - This makes model selection much faster
+2. **Use search** - With 39 models, search helps find what you need
+3. **Filter by tools** - When using MCP tools, enable "Tools Only" filter
+4. **Try different providers** - Each has unique models with different strengths
+
+## Model Data Updates
+
+Model data is fetched from models.dev API and can be updated with:
+
+```bash
+pnpm update-models
+```
+
+This will refresh the model list with the latest information from the API.
+
+## Local Storage
+
+The following preferences are saved locally:
+- **Favorites**: `aiChatTemplate_favorites_v1`
+- **Tokens**: `aiChatTemplate_token_[provider]`
+- **Selected Model**: `aiChatTemplate_selectedModel`
+
+## Troubleshooting
+
+- **OAuth popup blocked**: Allow popups for this site
+- **Authentication failed**: Check your API keys or re-authenticate
+- **Model not working**: Verify the model supports the features you're using
+- **Tools not showing**: Enable "Tools Only" filter when MCP tools are configured
diff --git a/examples/chat-ui/README.md b/examples/chat-ui/README.md
index 63f70b7..3893798 100644
--- a/examples/chat-ui/README.md
+++ b/examples/chat-ui/README.md
@@ -1,30 +1,36 @@
-# AI chat template
+# AI Chat with MCP
-An unofficial template for โ๏ธ React โจ โก๏ธ Vite โจ โ
๏ธ Cloudflare Workers AI.
+A React-based AI chat application demonstrating [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) integration with multiple AI providers.
-Full-stack AI chat application using Workers for the APIs (using the Cloudflare [vite-plugin](https://www.npmjs.com/package/@cloudflare/vite-plugin)) and Vite for the React application (hosted using [Workers Static Assets](https://developers.cloudflare.com/workers/static-assets/)). Provides chat functionality with [Workers AI](https://developers.cloudflare.com/workers-ai/), stores conversations in the browser's [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API), and uses [ai-sdk](https://sdk.vercel.ai/docs/introduction), [tailwindcss](https://tailwindcss.com/) and [workers-ai-provider](https://github.com/cloudflare/workers-ai-provider).
+This static web application showcases how to use the [`use-mcp`](../../) library to connect to MCP servers, providing extensible AI capabilities through external tools and services. The app supports multiple AI models, stores conversations locally in IndexedDB, and includes OAuth authentication for MCP server connections.
+
+**Live demo**: [chat.use-mcp.dev](https://chat.use-mcp.dev)
+
+## Features
+
+- **MCP Integration**: Connect to MCP servers with OAuth authentication support
+- **Multi-model Support**: Anthropic (Claude) and Groq (Llama) models with API key authentication
+- **Local Storage**: Conversations stored in browser's IndexedDB
+- **Static Deployment**: Builds to static assets for deployment anywhere
+- **Modern Stack**: React 19, TypeScript, Tailwind CSS, Vite
## Get started
-Create the project using [create-cloudflare](https://www.npmjs.com/package/create-cloudflare):
+```sh
+pnpm install
+pnpm dev
+```
+
+Build and deploy:
```sh
-npm create cloudflare@latest -- --template thomasgauvin/ai-chat-template
+pnpm build
+pnpm run deploy # deploys to Cloudflare Pages
```
-Run the project and deploy it:
+## Development
-```sh
-cd
-npm install
-npm run dev
-```
-
-```
-npm run deploy
-```
-
-## What's next?
-
-- Change the name of the package (in `package.json`)
-- Change the name of the worker (in `wrangler.jsonc`)
+- **Dev server**: `pnpm dev` (runs on port 5002)
+- **Build**: `pnpm build`
+- **Lint**: `pnpm lint`
+- **Test**: `pnpm test` (Playwright E2E tests)
diff --git a/examples/chat-ui/package.json b/examples/chat-ui/package.json
index 628ce41..8bfee13 100644
--- a/examples/chat-ui/package.json
+++ b/examples/chat-ui/package.json
@@ -4,7 +4,7 @@
"version": "0.0.0",
"type": "module",
"scripts": {
- "dev": "vite",
+ "dev": "vite --port=5002",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview",
@@ -12,11 +12,13 @@
"test": "playwright test",
"test:ui": "playwright test --ui",
"test:headed": "playwright test --headed",
- "test:html": "playwright test --reporter=html"
+ "test:html": "playwright test --reporter=html",
+ "update-models": "tsx scripts/update-models.ts"
},
"dependencies": {
"@ai-sdk/anthropic": "^1.2.12",
"@ai-sdk/groq": "^1.2.9",
+ "@ai-sdk/openai": "^1.3.23",
"@tailwindcss/typography": "^0.5.16",
"@tailwindcss/vite": "^4.0.14",
"ai": "^4.1.61",
@@ -30,7 +32,6 @@
"rehype-highlight": "^7.0.2",
"remark-gfm": "^4.0.1",
"tailwindcss": "^4.0.14",
- "use-mcp": "link:../..",
"workers-ai-provider": "^0.1.3"
},
"devDependencies": {
@@ -47,8 +48,10 @@
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^15.15.0",
"prettier": "^3.5.3",
+ "tsx": "^4.20.3",
"typescript": "~5.7.2",
"typescript-eslint": "^8.24.1",
+ "use-mcp": "link:../..",
"vite": "^6.2.0",
"wrangler": "^4.1.0"
},
diff --git a/examples/chat-ui/pnpm-lock.yaml b/examples/chat-ui/pnpm-lock.yaml
index 1058abf..a5b8882 100644
--- a/examples/chat-ui/pnpm-lock.yaml
+++ b/examples/chat-ui/pnpm-lock.yaml
@@ -14,12 +14,15 @@ importers:
'@ai-sdk/groq':
specifier: ^1.2.9
version: 1.2.9(zod@3.24.4)
+ '@ai-sdk/openai':
+ specifier: ^1.3.23
+ version: 1.3.23(zod@3.24.4)
'@tailwindcss/typography':
specifier: ^0.5.16
version: 0.5.16(tailwindcss@4.1.6)
'@tailwindcss/vite':
specifier: ^4.0.14
- version: 4.1.6(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2))
+ version: 4.1.6(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.20.3))
ai:
specifier: ^4.1.61
version: 4.3.15(react@19.1.0)(zod@3.24.4)
@@ -53,9 +56,6 @@ importers:
tailwindcss:
specifier: ^4.0.14
version: 4.1.6
- use-mcp:
- specifier: link:../..
- version: link:../..
workers-ai-provider:
specifier: ^0.1.3
version: 0.1.3(react@19.1.0)
@@ -65,7 +65,7 @@ importers:
version: 1.2.11(zod@3.24.4)
'@cloudflare/vite-plugin':
specifier: ^0.1.12
- version: 0.1.21(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2))(workerd@1.20250507.0)(wrangler@4.14.4(@cloudflare/workers-types@4.20250510.0))
+ version: 0.1.21(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.20.3))(workerd@1.20250507.0)(wrangler@4.14.4(@cloudflare/workers-types@4.20250510.0))
'@cloudflare/workers-types':
specifier: ^4.20250317.0
version: 4.20250510.0
@@ -83,7 +83,7 @@ importers:
version: 19.1.3(@types/react@19.1.3)
'@vitejs/plugin-react':
specifier: ^4.3.4
- version: 4.4.1(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2))
+ version: 4.4.1(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.20.3))
eslint:
specifier: ^9.21.0
version: 9.26.0(jiti@2.4.2)
@@ -99,15 +99,21 @@ importers:
prettier:
specifier: ^3.5.3
version: 3.5.3
+ tsx:
+ specifier: ^4.20.3
+ version: 4.20.3
typescript:
specifier: ~5.7.2
version: 5.7.3
typescript-eslint:
specifier: ^8.24.1
version: 8.32.0(eslint@9.26.0(jiti@2.4.2))(typescript@5.7.3)
+ use-mcp:
+ specifier: link:../..
+ version: link:../..
vite:
specifier: ^6.2.0
- version: 6.3.5(jiti@2.4.2)(lightningcss@1.29.2)
+ version: 6.3.5(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.20.3)
wrangler:
specifier: ^4.1.0
version: 4.14.4(@cloudflare/workers-types@4.20250510.0)
@@ -126,6 +132,12 @@ packages:
peerDependencies:
zod: ^3.0.0
+ '@ai-sdk/openai@1.3.23':
+ resolution: {integrity: sha512-86U7rFp8yacUAOE/Jz8WbGcwMCqWvjK33wk5DXkfnAOEn3mx2r7tNSJdjukQFZbAK97VMXGPPHxF+aEARDXRXQ==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ zod: ^3.0.0
+
'@ai-sdk/provider-utils@1.0.22':
resolution: {integrity: sha512-YHK2rpj++wnLVc9vPGzGFP3Pjeld2MwhKinetA0zKXOoHAT/Jit5O8kZsxcSlJPu9wvcGT1UGZEjZrtO7PfFOQ==}
engines: {node: '>=18'}
@@ -1496,6 +1508,9 @@ packages:
get-source@2.0.12:
resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==}
+ get-tsconfig@4.10.1:
+ resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==}
+
glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
@@ -2186,6 +2201,9 @@ packages:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
+ resolve-pkg-maps@1.0.0:
+ resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
+
reusify@1.1.0:
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
@@ -2358,6 +2376,11 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+ tsx@4.20.3:
+ resolution: {integrity: sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==}
+ engines: {node: '>=18.0.0'}
+ hasBin: true
+
type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
@@ -2572,6 +2595,12 @@ snapshots:
'@ai-sdk/provider-utils': 2.2.8(zod@3.24.4)
zod: 3.24.4
+ '@ai-sdk/openai@1.3.23(zod@3.24.4)':
+ dependencies:
+ '@ai-sdk/provider': 1.1.3
+ '@ai-sdk/provider-utils': 2.2.8(zod@3.24.4)
+ zod: 3.24.4
+
'@ai-sdk/provider-utils@1.0.22(zod@3.24.4)':
dependencies:
'@ai-sdk/provider': 0.0.26
@@ -2738,7 +2767,7 @@ snapshots:
optionalDependencies:
workerd: 1.20250507.0
- '@cloudflare/vite-plugin@0.1.21(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2))(workerd@1.20250507.0)(wrangler@4.14.4(@cloudflare/workers-types@4.20250510.0))':
+ '@cloudflare/vite-plugin@0.1.21(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.20.3))(workerd@1.20250507.0)(wrangler@4.14.4(@cloudflare/workers-types@4.20250510.0))':
dependencies:
'@cloudflare/unenv-preset': 2.3.1(unenv@2.0.0-rc.15)(workerd@1.20250507.0)
'@hattip/adapter-node': 0.0.49
@@ -2747,7 +2776,7 @@ snapshots:
picocolors: 1.1.1
tinyglobby: 0.2.13
unenv: 2.0.0-rc.15
- vite: 6.3.5(jiti@2.4.2)(lightningcss@1.29.2)
+ vite: 6.3.5(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.20.3)
wrangler: 4.14.4(@cloudflare/workers-types@4.20250510.0)
ws: 8.18.0
transitivePeerDependencies:
@@ -3222,12 +3251,12 @@ snapshots:
postcss-selector-parser: 6.0.10
tailwindcss: 4.1.6
- '@tailwindcss/vite@4.1.6(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2))':
+ '@tailwindcss/vite@4.1.6(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.20.3))':
dependencies:
'@tailwindcss/node': 4.1.6
'@tailwindcss/oxide': 4.1.6
tailwindcss: 4.1.6
- vite: 6.3.5(jiti@2.4.2)(lightningcss@1.29.2)
+ vite: 6.3.5(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.20.3)
'@types/babel__core@7.20.5':
dependencies:
@@ -3365,14 +3394,14 @@ snapshots:
'@ungap/structured-clone@1.3.0': {}
- '@vitejs/plugin-react@4.4.1(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2))':
+ '@vitejs/plugin-react@4.4.1(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.20.3))':
dependencies:
'@babel/core': 7.27.1
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.1)
'@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.27.1)
'@types/babel__core': 7.20.5
react-refresh: 0.17.0
- vite: 6.3.5(jiti@2.4.2)(lightningcss@1.29.2)
+ vite: 6.3.5(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.20.3)
transitivePeerDependencies:
- supports-color
@@ -3884,6 +3913,10 @@ snapshots:
data-uri-to-buffer: 2.0.2
source-map: 0.6.1
+ get-tsconfig@4.10.1:
+ dependencies:
+ resolve-pkg-maps: 1.0.0
+
glob-parent@5.1.2:
dependencies:
is-glob: 4.0.3
@@ -4758,6 +4791,8 @@ snapshots:
resolve-from@4.0.0: {}
+ resolve-pkg-maps@1.0.0: {}
+
reusify@1.1.0: {}
rollup@4.40.2:
@@ -4985,6 +5020,13 @@ snapshots:
tslib@2.8.1: {}
+ tsx@4.20.3:
+ dependencies:
+ esbuild: 0.25.4
+ get-tsconfig: 4.10.1
+ optionalDependencies:
+ fsevents: 2.3.3
+
type-check@0.4.0:
dependencies:
prelude-ls: 1.2.1
@@ -5091,7 +5133,7 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.2
- vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2):
+ vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.20.3):
dependencies:
esbuild: 0.25.4
fdir: 6.4.4(picomatch@4.0.2)
@@ -5103,6 +5145,7 @@ snapshots:
fsevents: 2.3.3
jiti: 2.4.2
lightningcss: 1.29.2
+ tsx: 4.20.3
which@2.0.2:
dependencies:
diff --git a/examples/chat-ui/scripts/update-models.ts b/examples/chat-ui/scripts/update-models.ts
new file mode 100644
index 0000000..b42fc7e
--- /dev/null
+++ b/examples/chat-ui/scripts/update-models.ts
@@ -0,0 +1,105 @@
+#!/usr/bin/env tsx
+
+import { writeFileSync } from 'fs'
+import { join } from 'path'
+
+interface ModelData {
+ id: string
+ name: string
+ attachment: boolean
+ reasoning: boolean
+ temperature: boolean
+ tool_call: boolean
+ knowledge: string
+ release_date: string
+ last_updated: string
+ modalities: {
+ input: string[]
+ output: string[]
+ }
+ open_weights: boolean
+ limit: {
+ context: number
+ output: number
+ }
+ cost?: {
+ input: number
+ output: number
+ cache_read?: number
+ cache_write?: number
+ }
+}
+
+interface ProviderData {
+ models: Record
+}
+
+interface ModelsDevData {
+ anthropic: ProviderData
+ groq: ProviderData
+ openrouter: ProviderData
+ [key: string]: ProviderData
+}
+
+const SUPPORTED_PROVIDERS = ['anthropic', 'groq', 'openrouter'] as const
+type SupportedProvider = (typeof SUPPORTED_PROVIDERS)[number]
+
+async function fetchModelsData(): Promise {
+ console.log('Fetching models data from models.dev...')
+
+ const response = await fetch('https://models.dev/api.json')
+ if (!response.ok) {
+ throw new Error(`Failed to fetch models data: ${response.status} ${response.statusText}`)
+ }
+
+ return await response.json()
+}
+
+function filterAndTransformModels(data: ModelsDevData) {
+ const filtered: Record> = {
+ anthropic: {},
+ groq: {},
+ openrouter: {},
+ }
+
+ // Filter by supported providers
+ for (const provider of SUPPORTED_PROVIDERS) {
+ if (data[provider] && data[provider].models) {
+ filtered[provider] = data[provider].models
+ }
+ }
+
+ return filtered
+}
+
+async function main() {
+ try {
+ const data = await fetchModelsData()
+ const filteredData = filterAndTransformModels(data)
+
+ const outputPath = join(process.cwd(), 'src', 'data', 'models.json')
+ writeFileSync(outputPath, JSON.stringify(filteredData, null, 2))
+
+ console.log(`โ
Models data updated successfully at ${outputPath}`)
+
+ // Print summary
+ let totalModels = 0
+ let toolSupportingModels = 0
+
+ for (const [provider, models] of Object.entries(filteredData)) {
+ const modelCount = Object.keys(models).length
+ const toolModels = Object.values(models).filter((m) => m.tool_call).length
+
+ console.log(` ${provider}: ${modelCount} models (${toolModels} support tools)`)
+ totalModels += modelCount
+ toolSupportingModels += toolModels
+ }
+
+ console.log(`\nTotal: ${totalModels} models (${toolSupportingModels} support tools)`)
+ } catch (error) {
+ console.error('โ Failed to update models data:', error)
+ process.exit(1)
+ }
+}
+
+main()
diff --git a/examples/chat-ui/src/App.tsx b/examples/chat-ui/src/App.tsx
index eb3f347..59c2878 100644
--- a/examples/chat-ui/src/App.tsx
+++ b/examples/chat-ui/src/App.tsx
@@ -1,11 +1,14 @@
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import ChatApp from './components/ChatApp'
-import { OAuthCallback } from './components/OAuthCallback'
+import PkceCallback from './components/PkceCallback.tsx'
+import { OAuthCallback } from './components/OAuthCallback.tsx'
function App() {
return (
+ } />
+ } />
} />
} />
diff --git a/examples/chat-ui/src/components/ApiKeyModal.tsx b/examples/chat-ui/src/components/ApiKeyModal.tsx
index 5bd40cc..ed4cf69 100644
--- a/examples/chat-ui/src/components/ApiKeyModal.tsx
+++ b/examples/chat-ui/src/components/ApiKeyModal.tsx
@@ -1,12 +1,12 @@
import React, { useState } from 'react'
import { createPortal } from 'react-dom'
import { X, Eye, EyeOff } from 'lucide-react'
-import { type ModelProvider } from '../types/models'
+import { type Provider } from '../types/models'
interface ApiKeyModalProps {
isOpen: boolean
onClose: () => void
- provider: ModelProvider
+ provider: Provider
onSave: (apiKey: string) => void
}
diff --git a/examples/chat-ui/src/components/ChatApp.tsx b/examples/chat-ui/src/components/ChatApp.tsx
index 7de0313..847ac3e 100644
--- a/examples/chat-ui/src/components/ChatApp.tsx
+++ b/examples/chat-ui/src/components/ChatApp.tsx
@@ -1,10 +1,13 @@
import React, { useState, useEffect } from 'react'
import ConversationThread from './ConversationThread.tsx'
+import ChatSidebar from './ChatSidebar'
+import ChatNavbar from './ChatNavbar'
import { storeName } from '../consts.ts'
import { type Conversation } from '../types'
import { useIndexedDB } from '../hooks/useIndexedDB'
import { type Model } from '../types/models'
import { getSelectedModel, setSelectedModel as saveSelectedModel } from '../utils/modelPreferences'
+import { useModels } from '../hooks/useModels'
import { type IDBPDatabase } from 'idb'
import { type Tool } from 'use-mcp/react'
@@ -14,17 +17,123 @@ interface ChatAppProps {}
const ChatApp: React.FC = () => {
const [conversations, setConversations] = useState([])
const [conversationId, setConversationId] = useState(undefined)
+ const [sidebarVisible, setSidebarVisible] = useState(false)
const [selectedModel, setSelectedModel] = useState(getSelectedModel())
const [apiKeyUpdateTrigger, setApiKeyUpdateTrigger] = useState(0)
const [mcpTools, setMcpTools] = useState([])
const [animationDelay] = useState(() => -Math.random() * 60)
const db = useIndexedDB()
+ const { models, addToFavorites, toggleFavorite, isFavorite, getFavoriteModels } = useModels()
const handleApiKeyUpdate = () => {
setApiKeyUpdateTrigger((prev) => prev + 1)
}
+ // Handle OAuth success messages from popups
+ useEffect(() => {
+ const handleMessage = (event: MessageEvent) => {
+ console.log('DEBUG: Received message in parent window:', event.data)
+ if (event.data.type === 'oauth_success') {
+ console.log('DEBUG: OAuth success message received, triggering API key update')
+ handleApiKeyUpdate()
+ }
+ }
+
+ window.addEventListener('message', handleMessage)
+ return () => window.removeEventListener('message', handleMessage)
+ }, [])
+
+ // Poll for OAuth token changes (fallback for when popup messaging doesn't work)
+ useEffect(() => {
+ const oauthProviders = ['groq', 'openrouter'] as const
+ let initialTokens: Record = {}
+
+ // Capture initial token state
+ const captureInitialTokens = () => {
+ for (const providerId of oauthProviders) {
+ const tokenKey = `aiChatTemplate_token_${providerId}`
+ initialTokens[providerId] = localStorage.getItem(tokenKey)
+ }
+ }
+
+ const checkForNewTokens = () => {
+ for (const providerId of oauthProviders) {
+ const tokenKey = `aiChatTemplate_token_${providerId}`
+ const currentToken = localStorage.getItem(tokenKey)
+
+ // Check if token was added or changed
+ if (currentToken !== initialTokens[providerId]) {
+ try {
+ const parsedToken = JSON.parse(currentToken || '{}')
+ if (parsedToken.access_token) {
+ console.log('DEBUG: New OAuth token detected for', providerId, 'via polling')
+ handleApiKeyUpdate()
+ return true // Stop polling once we find a new token
+ }
+ } catch (e) {
+ // Invalid token format, continue checking
+ }
+ }
+ }
+ return false
+ }
+
+ let pollInterval: number | null = null
+
+ const startPolling = () => {
+ // Capture initial state
+ captureInitialTokens()
+
+ // Check immediately for existing valid tokens (in case we just redirected from OAuth)
+ console.log('DEBUG: Checking for existing OAuth tokens on startup')
+ for (const providerId of oauthProviders) {
+ const tokenKey = `aiChatTemplate_token_${providerId}`
+ const currentToken = localStorage.getItem(tokenKey)
+
+ console.log(`DEBUG: Checking ${providerId} token:`, currentToken ? 'exists' : 'not found')
+
+ if (currentToken) {
+ try {
+ const parsedToken = JSON.parse(currentToken)
+ console.log(`DEBUG: Parsed ${providerId} token:`, parsedToken)
+ if (parsedToken.access_token) {
+ console.log('DEBUG: Found existing OAuth token for', providerId, 'on startup')
+ handleApiKeyUpdate()
+ return // Don't start polling if we found a valid token
+ }
+ } catch (e) {
+ console.log(`DEBUG: Failed to parse ${providerId} token:`, e)
+ }
+ }
+ }
+
+ // Start polling every 500ms for new tokens
+ pollInterval = setInterval(() => {
+ if (checkForNewTokens()) {
+ if (pollInterval) {
+ clearInterval(pollInterval)
+ pollInterval = null
+ console.log('DEBUG: Stopped polling for OAuth tokens')
+ }
+ }
+ }, 500)
+
+ console.log('DEBUG: Started polling for OAuth token changes')
+ }
+
+ // Start polling after a short delay to allow for popup messages first
+ const delayedStart = setTimeout(startPolling, 100)
+
+ return () => {
+ if (delayedStart) clearTimeout(delayedStart)
+ if (pollInterval) {
+ clearInterval(pollInterval)
+ console.log('DEBUG: Cleaned up OAuth token polling')
+ }
+ }
+ }, [])
+
const handleModelChange = (model: Model) => {
setSelectedModel(model)
saveSelectedModel(model)
@@ -65,14 +174,12 @@ const ChatApp: React.FC = () => {
}
}
- // const editConversationTitle = async (id: number, newTitle: string) => {
- // const conversation = (await db!.get(storeName, id)) as Conversation;
- // conversation.title = newTitle;
- // await db!.put(storeName, conversation);
- // setConversations((prev) =>
- // prev.map((conv) => (conv.id === id ? { ...conv, title: newTitle } : conv))
- // );
- // };
+ const editConversationTitle = async (id: number, newTitle: string) => {
+ const conversation = (await db!.get(storeName, id)) as Conversation
+ conversation.title = newTitle
+ await db!.put(storeName, conversation)
+ setConversations((prev) => prev.map((conv) => (conv.id === id ? { ...conv, title: newTitle } : conv)))
+ }
const startNewConversation = async () => {
//create unique id for new conversation
@@ -99,29 +206,27 @@ const ChatApp: React.FC = () => {
style={{ '--random-delay': `${animationDelay}s` } as React.CSSProperties}
>
- {/* Sidebar and Navbar components hidden but kept in codebase */}
- {/*{false && (*/}
- {/* <>*/}
- {/*
*/}
- {/*
*/}
- {/* >*/}
- {/*)}*/}
+ {/* Sidebar and Navbar components */}
+ {false && (
+ <>
+
+
+ >
+ )}
= () => {
apiKeyUpdateTrigger={apiKeyUpdateTrigger}
mcpTools={mcpTools}
onMcpToolsUpdate={setMcpTools}
+ addToFavorites={addToFavorites}
+ models={models}
+ toggleFavorite={toggleFavorite}
+ isFavorite={isFavorite}
+ getFavoriteModels={getFavoriteModels}
/>
diff --git a/examples/chat-ui/src/components/ChatSidebar.tsx b/examples/chat-ui/src/components/ChatSidebar.tsx
index 77a019c..753ff5f 100644
--- a/examples/chat-ui/src/components/ChatSidebar.tsx
+++ b/examples/chat-ui/src/components/ChatSidebar.tsx
@@ -25,6 +25,7 @@ interface ChatSidebarProps {
onModelChange: (model: Model) => void
apiKeyUpdateTrigger: number
onMcpToolsUpdate: (tools: Tool[]) => void
+ mcpTools: Tool[]
}
const ChatSidebar: React.FC = ({
@@ -40,6 +41,7 @@ const ChatSidebar: React.FC = ({
onModelChange,
apiKeyUpdateTrigger,
onMcpToolsUpdate,
+ mcpTools,
}) => {
const handleConversationClick = (id: number | undefined) => {
setConversationId(id)
@@ -135,7 +137,12 @@ const ChatSidebar: React.FC = ({
{/* Model Selector and MCP Servers at bottom */}
-
+ 0}
+ />
diff --git a/examples/chat-ui/src/components/ConversationThread.tsx b/examples/chat-ui/src/components/ConversationThread.tsx
index c1a572c..aac7ef1 100644
--- a/examples/chat-ui/src/components/ConversationThread.tsx
+++ b/examples/chat-ui/src/components/ConversationThread.tsx
@@ -2,6 +2,7 @@ import React, { useState, useEffect, FormEvent } from 'react'
import { storeName } from '../consts'
import '../styles/scrollbar.css'
import '../styles/github.css'
+import '../styles/markdown.css'
import { type Conversation, type Message } from '../types'
import { type Model } from '../types/models'
import { type IDBPDatabase } from 'idb'
@@ -29,6 +30,11 @@ interface ConversationThreadProps {
apiKeyUpdateTrigger: number
mcpTools: Tool[]
onMcpToolsUpdate: (tools: Tool[]) => void
+ addToFavorites: (modelId: string) => void
+ models: Model[]
+ toggleFavorite: (modelId: string) => void
+ isFavorite: (modelId: string) => boolean
+ getFavoriteModels: () => Model[]
}
const ConversationThread: React.FC = ({
@@ -43,6 +49,11 @@ const ConversationThread: React.FC = ({
apiKeyUpdateTrigger,
mcpTools,
onMcpToolsUpdate,
+ addToFavorites,
+ models,
+ toggleFavorite,
+ isFavorite,
+ getFavoriteModels,
}) => {
const [input, setInput] = useState('')
const [apiKeyModal, setApiKeyModal] = useState<{ isOpen: boolean; model: Model | null }>({
@@ -257,7 +268,17 @@ const ConversationThread: React.FC = ({
@@ -267,6 +288,11 @@ const ConversationThread: React.FC = ({
selectedModel={selectedModel}
onModelChange={onModelChange}
apiKeyUpdateTrigger={apiKeyUpdateTrigger}
+ addToFavorites={addToFavorites}
+ models={models}
+ toggleFavorite={toggleFavorite}
+ isFavorite={isFavorite}
+ getFavoriteModels={getFavoriteModels}
/>
setMcpServerModal(false)} onToolsUpdate={onMcpToolsUpdate} />
diff --git a/examples/chat-ui/src/components/McpServerModal.tsx b/examples/chat-ui/src/components/McpServerModal.tsx
index 6b5cee1..cd4acf9 100644
--- a/examples/chat-ui/src/components/McpServerModal.tsx
+++ b/examples/chat-ui/src/components/McpServerModal.tsx
@@ -11,13 +11,7 @@ interface McpServer {
}
// MCP Connection wrapper for a single server
-function McpConnection({
- server,
- onConnectionUpdate,
-}: {
- server: McpServer
- onConnectionUpdate: (serverId: string, data: any) => void
-}) {
+function McpConnection({ server, onConnectionUpdate }: { server: McpServer; onConnectionUpdate: (serverId: string, data: any) => void }) {
// Use the MCP hook with the server URL
const connection = useMcp({
url: server.url,
@@ -25,6 +19,7 @@ function McpConnection({
autoRetry: false,
popupFeatures: 'width=500,height=600,resizable=yes,scrollbars=yes',
transportType: server.transportType,
+ preventAutoAuth: true, // Prevent automatic popups on page load
})
// Update parent component with connection data
@@ -54,28 +49,12 @@ const McpServerModal: React.FC = ({ isOpen, onClose, onTool
const stored = localStorage.getItem('mcpServerToolCounts')
return stored ? JSON.parse(stored) : {}
})
- const [transportType, setTransportType] = useState<'auto' | 'http' | 'sse'>(() => {
+ const [transportType, _setTransportType] = useState<'auto' | 'http' | 'sse'>(() => {
const stored = localStorage.getItem('mcpTransportType')
return (stored as 'auto' | 'http' | 'sse') || 'http'
})
const [newServerTransportType, setNewServerTransportType] = useState<'auto' | 'http' | 'sse'>('http')
- // Helper to cycle through transport types
- const cycleTransportType = () => {
- setTransportType((current) => {
- switch (current) {
- case 'auto':
- return 'http'
- case 'http':
- return 'sse'
- case 'sse':
- return 'auto'
- default:
- return 'auto'
- }
- })
- }
-
// Helper to cycle through new server transport types
const cycleNewServerTransportType = () => {
setNewServerTransportType((current) => {
@@ -222,6 +201,8 @@ const McpServerModal: React.FC = ({ isOpen, onClose, onTool
switch (state) {
case 'discovering':
return Discovering
+ case 'pending_auth':
+ return Authentication Required
case 'authenticating':
return Authenticating
case 'connecting':
@@ -315,18 +296,31 @@ const McpServerModal: React.FC = ({ isOpen, onClose, onTool
{error}
)}
- {authUrl && (
-
-
Authentication required. Please click the link below:
-
handleManualAuth(server.id)}
- >
- Authenticate in new window
-
+ {(state === 'pending_auth' || authUrl) && (
+
+
+ {state === 'pending_auth'
+ ? 'Authentication is required to connect to this server.'
+ : 'Authentication popup was blocked. You can open the authentication page manually:'}
+
+
)}
diff --git a/examples/chat-ui/src/components/McpServers.tsx b/examples/chat-ui/src/components/McpServers.tsx
index fa734ab..037cce7 100644
--- a/examples/chat-ui/src/components/McpServers.tsx
+++ b/examples/chat-ui/src/components/McpServers.tsx
@@ -10,6 +10,7 @@ function McpConnection({ serverUrl, onConnectionUpdate }: { serverUrl: string; o
debug: true,
autoRetry: false,
popupFeatures: 'width=500,height=600,resizable=yes,scrollbars=yes',
+ preventAutoAuth: true, // Prevent automatic popups on page load
})
// Update parent component with connection data
@@ -103,6 +104,8 @@ export function McpServers({ onToolsUpdate }: { onToolsUpdate?: (tools: Tool[])
switch (state) {
case 'discovering':
return
Discovering
+ case 'pending_auth':
+ return
Authentication Required
case 'authenticating':
return
Authenticating
case 'connecting':
@@ -184,19 +187,32 @@ export function McpServers({ onToolsUpdate }: { onToolsUpdate?: (tools: Tool[])
)}
- {/* Authentication Link if needed */}
- {authUrl && (
-
-
Authentication required. Please click the link below:
-
- Authenticate in new window
-
+ {/* Authentication Action for pending_auth or existing authUrl */}
+ {(state === 'pending_auth' || authUrl) && (
+
+
+ {state === 'pending_auth'
+ ? 'Authentication is required to connect to this server.'
+ : 'Authentication popup was blocked. You can open the authentication page manually:'}
+
+
)}
diff --git a/examples/chat-ui/src/components/ModelSelectionModal.tsx b/examples/chat-ui/src/components/ModelSelectionModal.tsx
index 7812c44..2207a86 100644
--- a/examples/chat-ui/src/components/ModelSelectionModal.tsx
+++ b/examples/chat-ui/src/components/ModelSelectionModal.tsx
@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react'
-import { X, AlertCircle, CheckCircle } from 'lucide-react'
-import { type Model, availableModels } from '../types/models'
-import { hasApiKey, setApiKey, clearApiKey } from '../utils/apiKeys'
+import { X, AlertCircle, CheckCircle, Star, ArrowLeft, Search } from 'lucide-react'
+import { type Model, type Provider, providers } from '../types/models'
+import { hasApiKey, setApiKey, clearApiKey, beginOAuthFlow } from '../utils/auth'
import ApiKeyModal from './ApiKeyModal'
interface ModelSelectionModalProps {
@@ -10,6 +10,11 @@ interface ModelSelectionModalProps {
selectedModel: Model
onModelChange: (model: Model) => void
apiKeyUpdateTrigger: number
+ addToFavorites: (modelId: string) => void
+ models: Model[]
+ toggleFavorite: (modelId: string) => void
+ isFavorite: (modelId: string) => boolean
+ getFavoriteModels: () => Model[]
}
const ModelSelectionModal: React.FC
= ({
@@ -18,20 +23,28 @@ const ModelSelectionModal: React.FC = ({
selectedModel,
onModelChange,
apiKeyUpdateTrigger,
+ addToFavorites,
+ models,
+ toggleFavorite,
+ isFavorite,
+ getFavoriteModels,
}) => {
const [apiKeyModal, setApiKeyModal] = useState<{ isOpen: boolean; model: Model | null }>({
isOpen: false,
model: null,
})
+ const [currentView, setCurrentView] = useState<'main' | 'provider'>('main')
+ const [selectedProvider, setSelectedProvider] = useState(null)
+ const [searchTerm, setSearchTerm] = useState('')
const [apiKeyStatuses, setApiKeyStatuses] = useState>({})
useEffect(() => {
const statuses: Record = {}
- availableModels.forEach((model) => {
+ models.forEach((model) => {
statuses[model.provider.id] = hasApiKey(model.provider.id)
})
setApiKeyStatuses(statuses)
- }, [apiKeyUpdateTrigger])
+ }, [apiKeyUpdateTrigger, models])
useEffect(() => {
if (isOpen) {
@@ -45,33 +58,104 @@ const ModelSelectionModal: React.FC = ({
}
}, [isOpen])
- const handleModelSelect = (model: Model) => {
- const hasKey = hasApiKey(model.provider.id)
-
- if (!hasKey) {
- setApiKeyModal({ isOpen: true, model })
- } else {
- onModelChange(model)
- onClose()
+ // Handle OAuth success - show provider models when OAuth completes
+ useEffect(() => {
+ const handleMessage = (event: MessageEvent) => {
+ if (event.data.type === 'oauth_success' && event.data.provider && isOpen) {
+ const provider = providers[event.data.provider as keyof typeof providers]
+ if (provider) {
+ setCurrentView('provider')
+ setSelectedProvider(provider)
+ }
+ }
}
- }
+
+ window.addEventListener('message', handleMessage)
+ return () => window.removeEventListener('message', handleMessage)
+ }, [isOpen])
+
+ // Reset view when modal closes
+ useEffect(() => {
+ if (!isOpen) {
+ setCurrentView('main')
+ setSelectedProvider(null)
+ setSearchTerm('')
+ }
+ }, [isOpen])
const handleApiKeySave = (apiKey: string) => {
if (apiKeyModal.model) {
setApiKey(apiKeyModal.model.provider.id, apiKey)
setApiKeyStatuses((prev) => ({ ...prev, [apiKeyModal.model!.provider.id]: true }))
- onModelChange(apiKeyModal.model)
+
+ // Show provider models after API key is saved
+ const provider = apiKeyModal.model.provider
setApiKeyModal({ isOpen: false, model: null })
- onClose()
+ setCurrentView('provider')
+ setSelectedProvider(provider)
}
}
const handleClearApiKey = (providerId: string, e: React.MouseEvent) => {
e.stopPropagation()
- clearApiKey(providerId)
+ clearApiKey(providerId as any)
setApiKeyStatuses((prev) => ({ ...prev, [providerId]: false }))
}
+ const handleProviderSelect = (provider: Provider) => {
+ if (provider.authType === 'oauth') {
+ // Check if already authenticated
+ if (hasApiKey(provider.id)) {
+ // Show provider models to select from
+ setCurrentView('provider')
+ setSelectedProvider(provider)
+ } else {
+ // Start OAuth flow
+ beginOAuthFlow(provider.id)
+ .then(() => {
+ // OAuth flow started successfully - modal will be shown via OAuth success handler
+ })
+ .catch((error) => {
+ console.error('Failed to start OAuth flow:', error)
+ alert('Failed to start authentication. Please try again.')
+ })
+ }
+ } else {
+ // Check if already has API key
+ if (hasApiKey(provider.id)) {
+ // Show provider models to select from
+ setCurrentView('provider')
+ setSelectedProvider(provider)
+ } else {
+ // Show API key modal first
+ setApiKeyModal({ isOpen: true, model: { provider } as Model })
+ }
+ }
+ }
+
+ const handleBackToMain = () => {
+ setCurrentView('main')
+ setSelectedProvider(null)
+ setSearchTerm('')
+ }
+
+ const getProviderModels = () => {
+ if (!selectedProvider) return []
+ return models.filter((model) => model.provider.id === selectedProvider.id)
+ }
+
+ const getFilteredModels = () => {
+ const providerModels = getProviderModels()
+ if (!searchTerm) return providerModels
+ return providerModels.filter((model) => model.name.toLowerCase().includes(searchTerm.toLowerCase()))
+ }
+
+ const handleModelSelect = (model: Model) => {
+ onModelChange(model)
+ addToFavorites(model.id)
+ onClose()
+ }
+
const getStatusIcon = (providerId: string) => {
const hasKey = apiKeyStatuses[providerId]
@@ -91,57 +175,211 @@ const ModelSelectionModal: React.FC = ({
style={{ backgroundColor: 'rgba(0,0,0,0.8)' }}
onClick={onClose}
>
- e.stopPropagation()}>
-
-
Select Model
+
e.stopPropagation()}
+ >
+
+ {currentView === 'provider' && selectedProvider ? (
+
+
+
+
+
{selectedProvider.logo}
+
{selectedProvider.name} Models
+
+ ) : (
+
Select Model
+ )}
-
-
- {availableModels.map((model) => {
- const isSelected = model.id === selectedModel.id
- const hasKey = apiKeyStatuses[model.provider.id]
-
- return (
-
handleModelSelect(model)}
- >
-
-
-
{model.provider.logo}
-
-
{model.name}
-
{model.provider.name}
+
+ {currentView === 'main' ? (
+ <>
+ {/* Providers Section */}
+
+
Providers
+
+ {Object.values(providers).map((provider) => (
+
handleProviderSelect(provider)}
+ >
+
+
+
{provider.logo}
+
+
{provider.name}
+
+ {provider.authType === 'oauth' ? 'OAuth Authentication' : 'API Key Authentication'}
+
+
+
+
+ {getStatusIcon(provider.id)}
+ {apiKeyStatuses[provider.id] && (
+ handleClearApiKey(provider.id, e)}
+ className="text-zinc-400 hover:text-red-500 p-1"
+ title="Clear credentials"
+ >
+
+
+ )}
+
+ ))}
+
+
-
- {getStatusIcon(model.provider.id)}
-
- {hasKey ? 'Configured' : 'Not configured'}
-
- {hasKey && (
-
handleClearApiKey(model.provider.id, e)}
- className="text-zinc-400 hover:text-red-500 p-1 ml-2"
- title="Clear API key"
+ {/* Starred Models Section */}
+
+
+
+ Starred Models
+
+ {getFavoriteModels().length === 0 ? (
+
+
+
No starred models yet
+
Click on a provider above to explore and star models
+
+ ) : (
+
+ {getFavoriteModels().map((model) => {
+ const isConfigured = apiKeyStatuses[model.provider.id]
+ const isSelected = model.id === selectedModel.id
+ return (
+
{
+ if (isConfigured) {
+ onModelChange(model)
+ addToFavorites(model.id)
+ } else {
+ handleProviderSelect(model.provider)
+ }
+ }}
>
-
-
- )}
-
+
+
+
+ {isSelected &&
โ }
+
{model.provider.logo}
+
+
{model.name}
+
+ {model.provider.name}
+ {model.supportsTools && ๐ง }
+ {model.reasoning && ๐ง }
+ {model.attachment && ๐ }
+
+
+
+
+ {!isConfigured && (
+
+ )}
+
{
+ e.stopPropagation()
+ toggleFavorite(model.id)
+ }}
+ className="text-yellow-500 hover:text-yellow-600 p-1"
+ title="Remove from favorites"
+ >
+
+
+
+
+
+ )
+ })}
+
+ )}
+
+ >
+ ) : (
+ /* Provider Models View */
+ selectedProvider && (
+ <>
+
+
Select models to add to your favorites
+
+ {/* Search Bar */}
+
+
+ setSearchTerm(e.target.value)}
+ className="w-full pl-10 pr-4 py-2 border border-zinc-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
+ />
- )
- })}
-
+
+ {/* Models List */}
+
+ {getFilteredModels().map((model) => (
+
handleModelSelect(model)}
+ >
+
+
+
{
+ e.stopPropagation()
+ toggleFavorite(model.id)
+ }}
+ className={`p-1 rounded ${isFavorite(model.id) ? 'text-yellow-500' : 'text-zinc-300 hover:text-yellow-500'}`}
+ title={isFavorite(model.id) ? 'Remove from favorites' : 'Add to favorites'}
+ >
+
+
+
+
{model.name}
+
+ {model.supportsTools && ๐ง Tools }
+ {model.reasoning && ๐ง Reasoning }
+ {model.attachment && ๐ Attachments }
+ โข
+ {(model.contextLimit / 1000).toFixed(0)}K context
+
+
+
+
Click to select
+
+
+ ))}
+
+
+ {getFilteredModels().length === 0 && (
+
+
No models found matching "{searchTerm}"
+
+ )}
+
+
+ {getProviderModels().length} model{getProviderModels().length !== 1 ? 's' : ''} available
+
+ >
+ )
+ )}
@@ -149,7 +387,17 @@ const ModelSelectionModal: React.FC
= ({
setApiKeyModal({ isOpen: false, model: null })}
- provider={apiKeyModal.model?.provider ?? { id: '', name: '', baseUrl: '', apiKeyHeader: '', documentationUrl: '' }}
+ provider={
+ apiKeyModal.model?.provider ?? {
+ id: 'unknown' as any,
+ name: 'Unknown',
+ baseUrl: '',
+ logo: '',
+ documentationUrl: '',
+ authType: 'apiKey',
+ apiKeyHeader: '',
+ }
+ }
onSave={handleApiKeySave}
/>
>
diff --git a/examples/chat-ui/src/components/ModelSelector.tsx b/examples/chat-ui/src/components/ModelSelector.tsx
index 91544e3..b87592c 100644
--- a/examples/chat-ui/src/components/ModelSelector.tsx
+++ b/examples/chat-ui/src/components/ModelSelector.tsx
@@ -1,31 +1,57 @@
import React, { useState, useEffect, useRef } from 'react'
-import { ChevronDown, AlertCircle, X } from 'lucide-react'
-import { availableModels, type Model } from '../types/models'
-import { hasApiKey, setApiKey, clearApiKey } from '../utils/apiKeys'
+import { ChevronDown, AlertCircle, X, Star } from 'lucide-react'
+import { type Model, type Provider, providers } from '../types/models'
+import { useModels } from '../hooks/useModels'
+import { hasApiKey, setApiKey, clearApiKey, beginOAuthFlow, clearOAuthToken } from '../utils/auth'
import ApiKeyModal from './ApiKeyModal'
+import ProviderModelsModal from './ProviderModelsModal'
interface ModelSelectorProps {
selectedModel: Model
onModelChange: (model: Model) => void
apiKeyUpdateTrigger: number
+ toolsAvailable?: boolean
}
-const ModelSelector: React.FC = ({ selectedModel, onModelChange, apiKeyUpdateTrigger }) => {
+const ModelSelector: React.FC = ({ selectedModel, onModelChange, apiKeyUpdateTrigger, toolsAvailable = false }) => {
const [isOpen, setIsOpen] = useState(false)
const [apiKeyModal, setApiKeyModal] = useState<{ isOpen: boolean; model: Model | null }>({
isOpen: false,
model: null,
})
+ const [providerModelsModal, setProviderModelsModal] = useState<{ isOpen: boolean; provider: Provider | null }>({
+ isOpen: false,
+ provider: null,
+ })
const [apiKeyStatuses, setApiKeyStatuses] = useState>({})
const dropdownRef = useRef(null)
+ const { models, loading, toggleFavorite, isFavorite, getFavoriteModels } = useModels()
+
useEffect(() => {
const statuses: Record = {}
- availableModels.forEach((model) => {
+ models.forEach((model) => {
statuses[model.provider.id] = hasApiKey(model.provider.id)
})
setApiKeyStatuses(statuses)
- }, [apiKeyUpdateTrigger])
+ }, [apiKeyUpdateTrigger, models])
+
+ // Handle OAuth success - show provider models when OAuth completes
+ useEffect(() => {
+ const handleMessage = (event: MessageEvent) => {
+ console.log('DEBUG: ModelSelector received message:', event.data)
+ if (event.data.type === 'oauth_success' && event.data.provider) {
+ console.log('DEBUG: ModelSelector opening provider models modal for:', event.data.provider)
+ const provider = providers[event.data.provider as keyof typeof providers]
+ if (provider) {
+ setProviderModelsModal({ isOpen: true, provider })
+ }
+ }
+ }
+
+ window.addEventListener('message', handleMessage)
+ return () => window.removeEventListener('message', handleMessage)
+ }, [])
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
@@ -44,7 +70,21 @@ const ModelSelector: React.FC = ({ selectedModel, onModelCha
const hasKey = hasApiKey(model.provider.id)
if (!hasKey) {
- setApiKeyModal({ isOpen: true, model })
+ if (model.provider.authType === 'oauth') {
+ // Start OAuth flow
+ beginOAuthFlow(model.provider.id)
+ .then(() => {
+ // OAuth flow started successfully
+ setIsOpen(false)
+ })
+ .catch((error) => {
+ console.error('Failed to start OAuth flow:', error)
+ alert('Failed to start authentication. Please try again.')
+ })
+ } else {
+ // Show API key modal
+ setApiKeyModal({ isOpen: true, model })
+ }
} else {
onModelChange(model)
}
@@ -55,16 +95,58 @@ const ModelSelector: React.FC = ({ selectedModel, onModelCha
if (apiKeyModal.model) {
setApiKey(apiKeyModal.model.provider.id, apiKey)
setApiKeyStatuses((prev) => ({ ...prev, [apiKeyModal.model!.provider.id]: true }))
- onModelChange(apiKeyModal.model)
+
+ // If this was triggered from provider selection, show the provider models
+ const provider = apiKeyModal.model.provider
+ setApiKeyModal({ isOpen: false, model: null })
+ setProviderModelsModal({ isOpen: true, provider })
}
}
const handleClearApiKey = (providerId: string, e: React.MouseEvent) => {
e.stopPropagation()
- clearApiKey(providerId)
+ const provider = models.find((m) => m.provider.id === providerId)?.provider
+ if (provider?.authType === 'oauth') {
+ clearOAuthToken(providerId as any)
+ } else {
+ clearApiKey(providerId as any)
+ }
setApiKeyStatuses((prev) => ({ ...prev, [providerId]: false }))
}
+ const handleProviderSelect = (provider: Provider) => {
+ if (provider.authType === 'oauth') {
+ // Check if already authenticated
+ if (hasApiKey(provider.id)) {
+ // Show provider models to select from
+ setProviderModelsModal({ isOpen: true, provider })
+ setIsOpen(false)
+ } else {
+ // Start OAuth flow
+ beginOAuthFlow(provider.id)
+ .then(() => {
+ // OAuth flow started successfully
+ setIsOpen(false)
+ })
+ .catch((error) => {
+ console.error('Failed to start OAuth flow:', error)
+ alert('Failed to start authentication. Please try again.')
+ })
+ }
+ } else {
+ // Check if already has API key
+ if (hasApiKey(provider.id)) {
+ // Show provider models to select from
+ setProviderModelsModal({ isOpen: true, provider })
+ setIsOpen(false)
+ } else {
+ // Show API key modal first
+ setApiKeyModal({ isOpen: true, model: { provider } as Model })
+ setIsOpen(false)
+ }
+ }
+ }
+
const getApiKeyIcon = (providerId: string) => {
const hasKey = apiKeyStatuses[providerId]
@@ -83,6 +165,10 @@ const ModelSelector: React.FC = ({ selectedModel, onModelCha
}
}
+ if (loading) {
+ return Loading models...
+ }
+
return (
<>
@@ -105,22 +191,74 @@ const ModelSelector: React.FC
= ({ selectedModel, onModelCha
{isOpen && (
- {availableModels.map((model) => (
-
handleModelSelect(model)}
- className="w-full flex items-center justify-between p-3 text-left hover:bg-zinc-50 first:rounded-t-lg last:rounded-b-lg"
- >
-
-
{model.provider.logo}
-
-
{model.name}
-
{model.provider.name}
-
+ {/* Providers Section */}
+
+
Providers
+
+ {Object.values(providers).map((provider) => (
+
handleProviderSelect(provider)}
+ className="w-full flex items-center justify-between p-2 text-left hover:bg-zinc-50 rounded-md"
+ >
+
+
{provider.logo}
+
+
{provider.name}
+
{provider.authType === 'oauth' ? 'OAuth' : 'API Key'}
+
+
+ {getApiKeyIcon(provider.id)}
+
+ ))}
+
+
+
+ {/* Starred Models Section */}
+
+
+
+ {getFavoriteModels().length === 0 ? (
+
+ No starred models yet. Click on a provider above to explore and star models.
- {getApiKeyIcon(model.provider.id)}
-
- ))}
+ ) : (
+
+ {getFavoriteModels().map((model) => {
+ const isConfigured = apiKeyStatuses[model.provider.id]
+ return (
+
(isConfigured ? handleModelSelect(model) : handleProviderSelect(model.provider))}
+ className={`w-full flex items-center justify-between p-3 text-left hover:bg-zinc-50 border-b border-zinc-100 last:border-b-0 ${
+ !isConfigured ? 'opacity-50' : ''
+ }`}
+ >
+
+
+
{model.provider.logo}
+
+
{model.name}
+
+ {model.provider.name}
+ {model.supportsTools && ๐ง }
+ {model.reasoning && ๐ง }
+
+
+
+ {!isConfigured && (
+
+ )}
+
+ )
+ })}
+
+ )}
+
)}
@@ -128,9 +266,34 @@ const ModelSelector: React.FC = ({ selectedModel, onModelCha
setApiKeyModal({ isOpen: false, model: null })}
- provider={apiKeyModal.model?.provider ?? { id: '', name: '', baseUrl: '', apiKeyHeader: '', documentationUrl: '' }}
+ provider={
+ apiKeyModal.model?.provider ?? {
+ id: 'unknown' as any,
+ name: 'Unknown',
+ baseUrl: '',
+ logo: '',
+ documentationUrl: '',
+ authType: 'apiKey',
+ apiKeyHeader: '',
+ }
+ }
onSave={handleApiKeySave}
/>
+
+ setProviderModelsModal({ isOpen: false, provider: null })}
+ provider={providerModelsModal.provider}
+ models={models}
+ favorites={[]}
+ onToggleFavorite={toggleFavorite}
+ isFavorite={isFavorite}
+ onModelSelect={(model) => {
+ onModelChange(model)
+ setProviderModelsModal({ isOpen: false, provider: null })
+ }}
+ toolsAvailable={toolsAvailable}
+ />
>
)
}
diff --git a/examples/chat-ui/src/components/PkceCallback.tsx b/examples/chat-ui/src/components/PkceCallback.tsx
new file mode 100644
index 0000000..0502ef3
--- /dev/null
+++ b/examples/chat-ui/src/components/PkceCallback.tsx
@@ -0,0 +1,153 @@
+import React, { useEffect, useState, useRef } from 'react'
+import { useSearchParams } from 'react-router-dom'
+import { completeOAuthFlow } from '../utils/auth'
+import { SupportedProvider } from '../types/models'
+
+interface OAuthCallbackProps {
+ provider: SupportedProvider
+}
+
+const PkceCallback: React.FC = ({ provider }) => {
+ const [searchParams] = useSearchParams()
+ const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading')
+ const [error, setError] = useState(null)
+ const executedRef = useRef(false)
+
+ useEffect(() => {
+ const handleCallback = async () => {
+ if (executedRef.current) {
+ console.log('DEBUG: Skipping duplicate OAuth callback execution')
+ return
+ }
+ executedRef.current = true
+
+ try {
+ const code = searchParams.get('code')
+ const error = searchParams.get('error')
+
+ if (error) {
+ throw new Error(`OAuth error: ${error}`)
+ }
+
+ if (!code) {
+ throw new Error('Missing authorization code')
+ }
+
+ // TODO: Add state parameter handling back if needed later
+ // const stateToUse = state || 'no-state'
+ await completeOAuthFlow(provider, code)
+ setStatus('success')
+
+ console.log('DEBUG: OAuth flow completed successfully')
+ console.log('DEBUG: window.opener exists:', !!window.opener)
+ console.log('DEBUG: window.opener closed:', window.opener?.closed)
+ console.log('DEBUG: window.parent exists:', !!window.parent)
+ console.log('DEBUG: window.parent === window:', window.parent === window)
+
+ // Try to send success message to parent if possible
+ const sendSuccessMessage = () => {
+ const message = { type: 'oauth_success', provider }
+
+ // Try window.opener first
+ if (window.opener && !window.opener.closed) {
+ console.log('DEBUG: Sending message via window.opener')
+ window.opener.postMessage(message, '*')
+ return true
+ }
+
+ // Also try window.parent as fallback
+ if (window.parent && window.parent !== window) {
+ console.log('DEBUG: Sending message via window.parent')
+ window.parent.postMessage(message, '*')
+ return true
+ }
+
+ return false
+ }
+
+ // Try to send success message
+ const messageSent = sendSuccessMessage()
+ console.log({ messageSent })
+
+ console.log('DEBUG: Closing popup in 100ms')
+ setTimeout(() => {
+ console.log('DEBUG: Attempting to close popup')
+ window.close()
+ }, 100)
+ } catch (err) {
+ console.error('OAuth callback error:', err)
+ setError(err instanceof Error ? err.message : 'Unknown error')
+ setStatus('error')
+ }
+ }
+
+ handleCallback()
+ }, [searchParams, provider])
+
+ return (
+
+
+
+ {status === 'loading' && (
+ <>
+
+
Completing Authentication
+
Connecting to {provider}...
+ >
+ )}
+
+ {status === 'success' && (
+ <>
+
+
Authentication Successful!
+
Successfully connected to {provider}.
+
+ {
+ window.opener?.postMessage({ type: 'oauth_success', provider }, '*')
+ window.close()
+ }}
+ className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
+ >
+ Close Window
+
+
+ >
+ )}
+
+ {status === 'error' && (
+ <>
+
+
Authentication Failed
+
{error || 'An error occurred during authentication'}
+
window.close()}
+ className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
+ >
+ Close Window
+
+ >
+ )}
+
+
+
+ )
+}
+
+export default PkceCallback
diff --git a/examples/chat-ui/src/components/ProviderModelsModal.tsx b/examples/chat-ui/src/components/ProviderModelsModal.tsx
new file mode 100644
index 0000000..8674057
--- /dev/null
+++ b/examples/chat-ui/src/components/ProviderModelsModal.tsx
@@ -0,0 +1,143 @@
+import React, { useState } from 'react'
+import { createPortal } from 'react-dom'
+import { X, Star, Search } from 'lucide-react'
+import { type Model, type Provider } from '../types/models'
+
+interface ProviderModelsModalProps {
+ isOpen: boolean
+ onClose: () => void
+ provider: Provider | null
+ models: Model[]
+ favorites: string[]
+ onToggleFavorite: (modelId: string) => void
+ isFavorite: (modelId: string) => boolean
+ onModelSelect: (model: Model) => void
+ toolsAvailable?: boolean
+}
+
+const ProviderModelsModal: React.FC = ({
+ isOpen,
+ onClose,
+ provider,
+ models,
+ onToggleFavorite,
+ isFavorite,
+ onModelSelect,
+ toolsAvailable = false,
+}) => {
+ const [searchTerm, setSearchTerm] = useState('')
+ const [showToolsOnly, setShowToolsOnly] = useState(false)
+
+ if (!isOpen || !provider) return null
+
+ const providerModels = models.filter((model) => model.provider.id === provider.id)
+
+ const filteredModels = providerModels.filter((model) => {
+ const matchesSearch = model.name.toLowerCase().includes(searchTerm.toLowerCase())
+ const matchesTools = !showToolsOnly || model.supportsTools
+ return matchesSearch && matchesTools
+ })
+
+ const handleStarClick = (modelId: string, e: React.MouseEvent) => {
+ e.stopPropagation()
+ onToggleFavorite(modelId)
+ }
+
+ const modalContent = (
+
+
+ {/* Header */}
+
+
+
{provider.logo}
+
+
{provider.name} Models
+
Select models to add to your favorites
+
+
+
+
+
+
+
+ {/* Search and Filters */}
+
+
+
+ setSearchTerm(e.target.value)}
+ placeholder="Search models..."
+ className="w-full pl-10 pr-3 py-2 text-sm border border-zinc-200 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
+ />
+
+
+ {toolsAvailable && (
+
setShowToolsOnly(!showToolsOnly)}
+ className={`px-3 py-1 text-xs rounded-full border transition-colors ${
+ showToolsOnly ? 'bg-green-100 text-green-700 border-green-300' : 'bg-white text-zinc-600 border-zinc-200 hover:bg-zinc-50'
+ }`}
+ >
+ ๐ง Tools Only
+
+ )}
+
+
+ {/* Models List */}
+
+ {filteredModels.length === 0 ? (
+
No models found matching your criteria
+ ) : (
+ filteredModels.map((model) => (
+
+
+
handleStarClick(model.id, e)}
+ className={`transition-colors ${isFavorite(model.id) ? 'text-yellow-500' : 'text-zinc-300 hover:text-yellow-400'}`}
+ >
+
+
+
+
+
{model.name}
+
+ {model.supportsTools && ๐ง Tools }
+ {model.reasoning && ๐ง Reasoning }
+ {model.attachment && ๐ Attachments }
+ โข
+ {(model.contextLimit / 1000).toFixed(0)}K context
+
+
+
+
+
onModelSelect(model)}
+ className="px-3 py-1 text-sm bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"
+ >
+ Select
+
+
+ ))
+ )}
+
+
+ {/* Footer */}
+
+
+ {filteredModels.length} model{filteredModels.length !== 1 ? 's' : ''} available
+ {toolsAvailable && showToolsOnly && ' with tool support'}
+
+
+
+
+ )
+
+ return createPortal(modalContent, document.body)
+}
+
+export default ProviderModelsModal
diff --git a/examples/chat-ui/src/components/messages/AssistantMessage.tsx b/examples/chat-ui/src/components/messages/AssistantMessage.tsx
index ccddfdb..03d05d3 100644
--- a/examples/chat-ui/src/components/messages/AssistantMessage.tsx
+++ b/examples/chat-ui/src/components/messages/AssistantMessage.tsx
@@ -12,7 +12,7 @@ const AssistantMessage: React.FC = ({ message }) => {
return (
-
+
= ({ message }) => {
return
}
+ if (message.role === 'error') {
+ return
+ }
+
if (message.role === 'assistant') {
if (message.type === 'reasoning') {
return
@@ -34,7 +39,7 @@ const ChatMessage: React.FC = ({ message }) => {
return (
-
+
= ({ error }) => {
+ const [isExpanded, setIsExpanded] = useState(false)
+
+ return (
+
+
+
โ ๏ธ Error:
+
+
+ {error}
+
+ {error.length > 200 && (
+
setIsExpanded(!isExpanded)}
+ className="text-red-600 hover:text-red-800 text-xs mt-1 underline focus:outline-none"
+ >
+ {isExpanded ? 'Show less' : 'Show more'}
+
+ )}
+
+
+
+ )
+}
+
+export default ErrorMessage
diff --git a/examples/chat-ui/src/data/models.json b/examples/chat-ui/src/data/models.json
new file mode 100644
index 0000000..40397d9
--- /dev/null
+++ b/examples/chat-ui/src/data/models.json
@@ -0,0 +1,2022 @@
+{
+ "anthropic": {
+ "claude-3-5-haiku-20241022": {
+ "id": "claude-3-5-haiku-20241022",
+ "name": "Claude Haiku 3.5",
+ "attachment": true,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2024-07-31",
+ "release_date": "2024-10-22",
+ "last_updated": "2024-10-22",
+ "modalities": {
+ "input": ["text", "image"],
+ "output": ["text"]
+ },
+ "open_weights": false,
+ "cost": {
+ "input": 0.8,
+ "output": 4,
+ "cache_read": 0.08,
+ "cache_write": 1
+ },
+ "limit": {
+ "context": 200000,
+ "output": 8192
+ }
+ },
+ "claude-3-5-sonnet-20241022": {
+ "id": "claude-3-5-sonnet-20241022",
+ "name": "Claude Sonnet 3.5 v2",
+ "attachment": true,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2024-04-30",
+ "release_date": "2024-10-22",
+ "last_updated": "2024-10-22",
+ "modalities": {
+ "input": ["text", "image"],
+ "output": ["text"]
+ },
+ "open_weights": false,
+ "cost": {
+ "input": 3,
+ "output": 15,
+ "cache_read": 0.3,
+ "cache_write": 3.75
+ },
+ "limit": {
+ "context": 200000,
+ "output": 8192
+ }
+ },
+ "claude-sonnet-4-20250514": {
+ "id": "claude-sonnet-4-20250514",
+ "name": "Claude Sonnet 4",
+ "attachment": true,
+ "reasoning": true,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2025-03-31",
+ "release_date": "2025-05-22",
+ "last_updated": "2025-05-22",
+ "modalities": {
+ "input": ["text", "image"],
+ "output": ["text"]
+ },
+ "open_weights": false,
+ "cost": {
+ "input": 3,
+ "output": 15,
+ "cache_read": 0.3,
+ "cache_write": 3.75
+ },
+ "limit": {
+ "context": 200000,
+ "output": 64000
+ }
+ },
+ "claude-opus-4-20250514": {
+ "id": "claude-opus-4-20250514",
+ "name": "Claude Opus 4",
+ "attachment": true,
+ "reasoning": true,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2025-03-31",
+ "release_date": "2025-05-22",
+ "last_updated": "2025-05-22",
+ "modalities": {
+ "input": ["text", "image"],
+ "output": ["text"]
+ },
+ "open_weights": false,
+ "cost": {
+ "input": 15,
+ "output": 75,
+ "cache_read": 1.5,
+ "cache_write": 18.75
+ },
+ "limit": {
+ "context": 200000,
+ "output": 32000
+ }
+ },
+ "claude-3-opus-20240229": {
+ "id": "claude-3-opus-20240229",
+ "name": "Claude Opus 3",
+ "attachment": true,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2023-08-31",
+ "release_date": "2024-02-29",
+ "last_updated": "2024-02-29",
+ "modalities": {
+ "input": ["text", "image"],
+ "output": ["text"]
+ },
+ "open_weights": false,
+ "cost": {
+ "input": 15,
+ "output": 75,
+ "cache_read": 1.5,
+ "cache_write": 18.75
+ },
+ "limit": {
+ "context": 200000,
+ "output": 4096
+ }
+ },
+ "claude-3-5-sonnet-20240620": {
+ "id": "claude-3-5-sonnet-20240620",
+ "name": "Claude Sonnet 3.5",
+ "attachment": true,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2024-04-30",
+ "release_date": "2024-06-20",
+ "last_updated": "2024-06-20",
+ "modalities": {
+ "input": ["text", "image"],
+ "output": ["text"]
+ },
+ "open_weights": false,
+ "cost": {
+ "input": 3,
+ "output": 15,
+ "cache_read": 0.3,
+ "cache_write": 3.75
+ },
+ "limit": {
+ "context": 200000,
+ "output": 8192
+ }
+ },
+ "claude-3-haiku-20240307": {
+ "id": "claude-3-haiku-20240307",
+ "name": "Claude Haiku 3",
+ "attachment": true,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2023-08-31",
+ "release_date": "2024-03-13",
+ "last_updated": "2024-03-13",
+ "modalities": {
+ "input": ["text", "image"],
+ "output": ["text"]
+ },
+ "open_weights": false,
+ "cost": {
+ "input": 0.25,
+ "output": 1.25,
+ "cache_read": 0.03,
+ "cache_write": 0.3
+ },
+ "limit": {
+ "context": 200000,
+ "output": 4096
+ }
+ },
+ "claude-3-7-sonnet-20250219": {
+ "id": "claude-3-7-sonnet-20250219",
+ "name": "Claude Sonnet 3.7",
+ "attachment": true,
+ "reasoning": true,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2024-10-31",
+ "release_date": "2025-02-19",
+ "last_updated": "2025-02-19",
+ "modalities": {
+ "input": ["text", "image"],
+ "output": ["text"]
+ },
+ "open_weights": false,
+ "cost": {
+ "input": 3,
+ "output": 15,
+ "cache_read": 0.3,
+ "cache_write": 3.75
+ },
+ "limit": {
+ "context": 200000,
+ "output": 64000
+ }
+ },
+ "claude-3-sonnet-20240229": {
+ "id": "claude-3-sonnet-20240229",
+ "name": "Claude Sonnet 3",
+ "attachment": true,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2023-08-31",
+ "release_date": "2024-03-04",
+ "last_updated": "2024-03-04",
+ "modalities": {
+ "input": ["text", "image"],
+ "output": ["text"]
+ },
+ "open_weights": false,
+ "cost": {
+ "input": 3,
+ "output": 15,
+ "cache_read": 0.3,
+ "cache_write": 0.3
+ },
+ "limit": {
+ "context": 200000,
+ "output": 4096
+ }
+ }
+ },
+ "groq": {
+ "mistral-saba-24b": {
+ "id": "mistral-saba-24b",
+ "name": "Mistral Saba 24B",
+ "attachment": false,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2024-08",
+ "release_date": "2025-02-06",
+ "last_updated": "2025-02-06",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": false,
+ "cost": {
+ "input": 0.79,
+ "output": 0.79
+ },
+ "limit": {
+ "context": 32768,
+ "output": 32768
+ }
+ },
+ "deepseek-r1-distill-llama-70b": {
+ "id": "deepseek-r1-distill-llama-70b",
+ "name": "DeepSeek R1 Distill Llama 70B",
+ "attachment": false,
+ "reasoning": true,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2024-07",
+ "release_date": "2025-01-20",
+ "last_updated": "2025-01-20",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0.75,
+ "output": 0.99
+ },
+ "limit": {
+ "context": 131072,
+ "output": 8192
+ }
+ },
+ "llama-3.3-70b-versatile": {
+ "id": "llama-3.3-70b-versatile",
+ "name": "Llama 3.3 70B Versatile",
+ "attachment": false,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2023-12",
+ "release_date": "2024-12-06",
+ "last_updated": "2024-12-06",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0.59,
+ "output": 0.79
+ },
+ "limit": {
+ "context": 131072,
+ "output": 32768
+ }
+ },
+ "moonshotai/kimi-k2-instruct": {
+ "id": "moonshotai/kimi-k2-instruct",
+ "name": "Kimi K2 Instruct",
+ "attachment": false,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2024-10",
+ "release_date": "2025-07-14",
+ "last_updated": "2025-07-14",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 1,
+ "output": 3
+ },
+ "limit": {
+ "context": 131072,
+ "output": 16384
+ }
+ },
+ "meta-llama/llama-4-maverick-17b-128e-instruct": {
+ "id": "meta-llama/llama-4-maverick-17b-128e-instruct",
+ "name": "Llama 4 Maverick 17B",
+ "attachment": false,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2024-08",
+ "release_date": "2025-04-05",
+ "last_updated": "2025-04-05",
+ "modalities": {
+ "input": ["text", "image"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0.2,
+ "output": 0.6
+ },
+ "limit": {
+ "context": 131072,
+ "output": 8192
+ }
+ },
+ "meta-llama/llama-4-scout-17b-16e-instruct": {
+ "id": "meta-llama/llama-4-scout-17b-16e-instruct",
+ "name": "Llama 4 Scout 17B",
+ "attachment": false,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2024-08",
+ "release_date": "2025-04-05",
+ "last_updated": "2025-04-05",
+ "modalities": {
+ "input": ["text", "image"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0.11,
+ "output": 0.34
+ },
+ "limit": {
+ "context": 131072,
+ "output": 8192
+ }
+ },
+ "qwen/qwen3-32b": {
+ "id": "qwen/qwen3-32b",
+ "name": "Qwen3 32B",
+ "attachment": false,
+ "reasoning": true,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2024-11-08",
+ "release_date": "2024-12-23",
+ "last_updated": "2024-12-23",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0.29,
+ "output": 0.59
+ },
+ "limit": {
+ "context": 131072,
+ "output": 16384
+ }
+ }
+ },
+ "openrouter": {
+ "openai/gpt-4.1-mini": {
+ "id": "openai/gpt-4.1-mini",
+ "name": "GPT-4.1 Mini",
+ "attachment": true,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2024-04",
+ "release_date": "2025-04-14",
+ "last_updated": "2025-04-14",
+ "modalities": {
+ "input": ["text", "image"],
+ "output": ["text"]
+ },
+ "open_weights": false,
+ "cost": {
+ "input": 0.4,
+ "output": 1.6,
+ "cache_read": 0.1
+ },
+ "limit": {
+ "context": 1047576,
+ "output": 32768
+ }
+ },
+ "openai/o4-mini": {
+ "id": "openai/o4-mini",
+ "name": "o4 Mini",
+ "attachment": true,
+ "reasoning": true,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2024-06",
+ "release_date": "2025-04-16",
+ "last_updated": "2025-04-16",
+ "modalities": {
+ "input": ["text", "image"],
+ "output": ["text"]
+ },
+ "open_weights": false,
+ "cost": {
+ "input": 1.1,
+ "output": 4.4,
+ "cache_read": 0.28
+ },
+ "limit": {
+ "context": 200000,
+ "output": 100000
+ }
+ },
+ "openai/gpt-4o-mini": {
+ "id": "openai/gpt-4o-mini",
+ "name": "GPT-4o-mini",
+ "attachment": true,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2024-10",
+ "release_date": "2024-07-18",
+ "last_updated": "2024-07-18",
+ "modalities": {
+ "input": ["text", "image"],
+ "output": ["text"]
+ },
+ "open_weights": false,
+ "cost": {
+ "input": 0.15,
+ "output": 0.6,
+ "cache_read": 0.08
+ },
+ "limit": {
+ "context": 128000,
+ "output": 16384
+ }
+ },
+ "openai/gpt-4.1": {
+ "id": "openai/gpt-4.1",
+ "name": "GPT-4.1",
+ "attachment": true,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2024-04",
+ "release_date": "2025-04-14",
+ "last_updated": "2025-04-14",
+ "modalities": {
+ "input": ["text", "image"],
+ "output": ["text"]
+ },
+ "open_weights": false,
+ "cost": {
+ "input": 2,
+ "output": 8,
+ "cache_read": 0.5
+ },
+ "limit": {
+ "context": 1047576,
+ "output": 32768
+ }
+ },
+ "sarvamai/sarvam-m:free": {
+ "id": "sarvamai/sarvam-m:free",
+ "name": "Sarvam-M (free)",
+ "attachment": false,
+ "reasoning": true,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2025-05",
+ "release_date": "2025-05-25",
+ "last_updated": "2025-05-25",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 32768,
+ "output": 32768
+ }
+ },
+ "deepseek/deepseek-r1-distill-qwen-14b": {
+ "id": "deepseek/deepseek-r1-distill-qwen-14b",
+ "name": "DeepSeek R1 Distill Qwen 14B",
+ "attachment": false,
+ "reasoning": true,
+ "temperature": true,
+ "tool_call": false,
+ "knowledge": "2024-10",
+ "release_date": "2025-01-29",
+ "last_updated": "2025-01-29",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 64000,
+ "output": 8192
+ }
+ },
+ "deepseek/deepseek-r1:free": {
+ "id": "deepseek/deepseek-r1:free",
+ "name": "R1 (free)",
+ "attachment": false,
+ "reasoning": true,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2025-01",
+ "release_date": "2025-01-20",
+ "last_updated": "2025-01-20",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 163840,
+ "output": 163840
+ }
+ },
+ "deepseek/deepseek-r1-distill-llama-70b": {
+ "id": "deepseek/deepseek-r1-distill-llama-70b",
+ "name": "DeepSeek R1 Distill Llama 70B",
+ "attachment": false,
+ "reasoning": true,
+ "temperature": true,
+ "tool_call": false,
+ "knowledge": "2024-10",
+ "release_date": "2025-01-23",
+ "last_updated": "2025-01-23",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 8192,
+ "output": 8192
+ }
+ },
+ "deepseek/deepseek-r1-0528:free": {
+ "id": "deepseek/deepseek-r1-0528:free",
+ "name": "R1 0528 (free)",
+ "attachment": false,
+ "reasoning": true,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2025-05",
+ "release_date": "2025-05-28",
+ "last_updated": "2025-05-28",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 163840,
+ "output": 163840
+ }
+ },
+ "deepseek/deepseek-v3-base:free": {
+ "id": "deepseek/deepseek-v3-base:free",
+ "name": "DeepSeek V3 Base (free)",
+ "attachment": false,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": false,
+ "knowledge": "2025-03",
+ "release_date": "2025-03-29",
+ "last_updated": "2025-03-29",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 163840,
+ "output": 163840
+ }
+ },
+ "deepseek/deepseek-chat-v3-0324": {
+ "id": "deepseek/deepseek-chat-v3-0324",
+ "name": "DeepSeek V3 0324",
+ "attachment": false,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": false,
+ "knowledge": "2024-10",
+ "release_date": "2025-03-24",
+ "last_updated": "2025-03-24",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 16384,
+ "output": 8192
+ }
+ },
+ "deepseek/deepseek-r1-0528-qwen3-8b:free": {
+ "id": "deepseek/deepseek-r1-0528-qwen3-8b:free",
+ "name": "Deepseek R1 0528 Qwen3 8B (free)",
+ "attachment": false,
+ "reasoning": true,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2025-05",
+ "release_date": "2025-05-29",
+ "last_updated": "2025-05-29",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 131072,
+ "output": 131072
+ }
+ },
+ "cognitivecomputations/dolphin3.0-mistral-24b": {
+ "id": "cognitivecomputations/dolphin3.0-mistral-24b",
+ "name": "Dolphin3.0 Mistral 24B",
+ "attachment": false,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2024-10",
+ "release_date": "2025-02-13",
+ "last_updated": "2025-02-13",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 32768,
+ "output": 8192
+ }
+ },
+ "cognitivecomputations/dolphin3.0-r1-mistral-24b": {
+ "id": "cognitivecomputations/dolphin3.0-r1-mistral-24b",
+ "name": "Dolphin3.0 R1 Mistral 24B",
+ "attachment": false,
+ "reasoning": true,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2024-10",
+ "release_date": "2025-02-13",
+ "last_updated": "2025-02-13",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 32768,
+ "output": 8192
+ }
+ },
+ "anthropic/claude-4-sonnet-20250522": {
+ "id": "anthropic/claude-4-sonnet-20250522",
+ "name": "Claude Sonnet 4",
+ "attachment": true,
+ "reasoning": true,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2025-03-31",
+ "release_date": "2025-05-22",
+ "last_updated": "2025-05-22",
+ "modalities": {
+ "input": ["text", "image"],
+ "output": ["text"]
+ },
+ "open_weights": false,
+ "cost": {
+ "input": 3,
+ "output": 15,
+ "cache_read": 0.3,
+ "cache_write": 3.75
+ },
+ "limit": {
+ "context": 200000,
+ "output": 64000
+ }
+ },
+ "anthropic/claude-3.5-haiku": {
+ "id": "anthropic/claude-3.5-haiku",
+ "name": "Claude Haiku 3.5",
+ "attachment": true,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2024-07-31",
+ "release_date": "2024-10-22",
+ "last_updated": "2024-10-22",
+ "modalities": {
+ "input": ["text", "image"],
+ "output": ["text"]
+ },
+ "open_weights": false,
+ "cost": {
+ "input": 0.8,
+ "output": 4,
+ "cache_read": 0.08,
+ "cache_write": 1
+ },
+ "limit": {
+ "context": 200000,
+ "output": 8192
+ }
+ },
+ "anthropic/claude-3.7-sonnet": {
+ "id": "anthropic/claude-3.7-sonnet",
+ "name": "Claude Sonnet 3.7",
+ "attachment": true,
+ "reasoning": true,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2024-01",
+ "release_date": "2025-02-19",
+ "last_updated": "2025-02-19",
+ "modalities": {
+ "input": ["text", "image"],
+ "output": ["text"]
+ },
+ "open_weights": false,
+ "cost": {
+ "input": 15,
+ "output": 75,
+ "cache_read": 1.5,
+ "cache_write": 18.75
+ },
+ "limit": {
+ "context": 200000,
+ "output": 128000
+ }
+ },
+ "anthropic/claude-opus-4": {
+ "id": "anthropic/claude-opus-4",
+ "name": "Claude Opus 4",
+ "attachment": true,
+ "reasoning": true,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2025-03-31",
+ "release_date": "2025-05-22",
+ "last_updated": "2025-05-22",
+ "modalities": {
+ "input": ["text", "image"],
+ "output": ["text"]
+ },
+ "open_weights": false,
+ "cost": {
+ "input": 15,
+ "output": 75,
+ "cache_read": 1.5,
+ "cache_write": 18.75
+ },
+ "limit": {
+ "context": 200000,
+ "output": 32000
+ }
+ },
+ "featherless/qwerky-72b": {
+ "id": "featherless/qwerky-72b",
+ "name": "Qwerky 72B",
+ "attachment": false,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": false,
+ "knowledge": "2024-10",
+ "release_date": "2025-03-20",
+ "last_updated": "2025-03-20",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 32768,
+ "output": 8192
+ }
+ },
+ "rekaai/reka-flash-3": {
+ "id": "rekaai/reka-flash-3",
+ "name": "Reka Flash 3",
+ "attachment": false,
+ "reasoning": true,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2024-10",
+ "release_date": "2025-03-12",
+ "last_updated": "2025-03-12",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 32768,
+ "output": 8192
+ }
+ },
+ "moonshotai/kimi-dev-72b:free": {
+ "id": "moonshotai/kimi-dev-72b:free",
+ "name": "Kimi Dev 72b (free)",
+ "attachment": false,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2025-06",
+ "release_date": "2025-06-16",
+ "last_updated": "2025-06-16",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 131072,
+ "output": 131072
+ }
+ },
+ "moonshotai/kimi-k2": {
+ "id": "moonshotai/kimi-k2",
+ "name": "Kimi K2",
+ "attachment": false,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2024-10",
+ "release_date": "2025-07-11",
+ "last_updated": "2025-07-11",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": false,
+ "cost": {
+ "input": 0.57,
+ "output": 2.3
+ },
+ "limit": {
+ "context": 131072,
+ "output": 32768
+ }
+ },
+ "x-ai/grok-3-mini": {
+ "id": "x-ai/grok-3-mini",
+ "name": "Grok 3 Mini",
+ "attachment": false,
+ "reasoning": true,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2024-11",
+ "release_date": "2025-02-17",
+ "last_updated": "2025-02-17",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": false,
+ "cost": {
+ "input": 0.3,
+ "output": 0.5,
+ "cache_read": 0.075,
+ "cache_write": 0.5
+ },
+ "limit": {
+ "context": 131072,
+ "output": 8192
+ }
+ },
+ "x-ai/grok-3": {
+ "id": "x-ai/grok-3",
+ "name": "Grok 3",
+ "attachment": false,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2024-11",
+ "release_date": "2025-02-17",
+ "last_updated": "2025-02-17",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": false,
+ "cost": {
+ "input": 3,
+ "output": 15,
+ "cache_read": 0.75,
+ "cache_write": 15
+ },
+ "limit": {
+ "context": 131072,
+ "output": 8192
+ }
+ },
+ "x-ai/grok-3-beta": {
+ "id": "x-ai/grok-3-beta",
+ "name": "Grok 3 Beta",
+ "attachment": false,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2024-11",
+ "release_date": "2025-02-17",
+ "last_updated": "2025-02-17",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": false,
+ "cost": {
+ "input": 3,
+ "output": 15,
+ "cache_read": 0.75,
+ "cache_write": 15
+ },
+ "limit": {
+ "context": 131072,
+ "output": 8192
+ }
+ },
+ "x-ai/grok-4": {
+ "id": "x-ai/grok-4",
+ "name": "Grok 4",
+ "attachment": false,
+ "reasoning": true,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2025-07",
+ "release_date": "2025-07-09",
+ "last_updated": "2025-07-09",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": false,
+ "cost": {
+ "input": 3,
+ "output": 15,
+ "cache_read": 0.75,
+ "cache_write": 15
+ },
+ "limit": {
+ "context": 256000,
+ "output": 64000
+ }
+ },
+ "x-ai/grok-3-mini-beta": {
+ "id": "x-ai/grok-3-mini-beta",
+ "name": "Grok 3 Mini Beta",
+ "attachment": false,
+ "reasoning": true,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2024-11",
+ "release_date": "2025-02-17",
+ "last_updated": "2025-02-17",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": false,
+ "cost": {
+ "input": 0.3,
+ "output": 0.5,
+ "cache_read": 0.075,
+ "cache_write": 0.5
+ },
+ "limit": {
+ "context": 131072,
+ "output": 8192
+ }
+ },
+ "microsoft/mai-ds-r1:free": {
+ "id": "microsoft/mai-ds-r1:free",
+ "name": "MAI DS R1 (free)",
+ "attachment": false,
+ "reasoning": true,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2025-04",
+ "release_date": "2025-04-21",
+ "last_updated": "2025-04-21",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 163840,
+ "output": 163840
+ }
+ },
+ "openrouter/cypher-alpha:free": {
+ "id": "openrouter/cypher-alpha:free",
+ "name": "Cypher Alpha (free)",
+ "attachment": false,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2025-07",
+ "release_date": "2025-07-01",
+ "last_updated": "2025-07-01",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": false,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 1000000,
+ "output": 1000000
+ }
+ },
+ "meta-llama/llama-3.3-70b-instruct:free": {
+ "id": "meta-llama/llama-3.3-70b-instruct:free",
+ "name": "Llama 3.3 70B Instruct (free)",
+ "attachment": false,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2024-12",
+ "release_date": "2024-12-06",
+ "last_updated": "2024-12-06",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 65536,
+ "output": 65536
+ }
+ },
+ "meta-llama/llama-3.2-11b-vision-instruct": {
+ "id": "meta-llama/llama-3.2-11b-vision-instruct",
+ "name": "Llama 3.2 11B Vision Instruct",
+ "attachment": true,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": false,
+ "knowledge": "2023-12",
+ "release_date": "2024-09-25",
+ "last_updated": "2024-09-25",
+ "modalities": {
+ "input": ["text", "image"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 131072,
+ "output": 8192
+ }
+ },
+ "meta-llama/llama-4-scout:free": {
+ "id": "meta-llama/llama-4-scout:free",
+ "name": "Llama 4 Scout (free)",
+ "attachment": true,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2024-08",
+ "release_date": "2025-04-05",
+ "last_updated": "2025-04-05",
+ "modalities": {
+ "input": ["text", "image"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 64000,
+ "output": 64000
+ }
+ },
+ "google/gemma-3n-e4b-it:free": {
+ "id": "google/gemma-3n-e4b-it:free",
+ "name": "Gemma 3n 4B (free)",
+ "attachment": true,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2025-05",
+ "release_date": "2025-05-20",
+ "last_updated": "2025-05-20",
+ "modalities": {
+ "input": ["text", "image", "audio"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 8192,
+ "output": 8192
+ }
+ },
+ "google/gemini-2.5-flash-preview-05-20": {
+ "id": "google/gemini-2.5-flash-preview-05-20",
+ "name": "Gemini 2.5 Flash Preview 05-20",
+ "attachment": true,
+ "reasoning": true,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2025-01",
+ "release_date": "2025-05-20",
+ "last_updated": "2025-05-20",
+ "modalities": {
+ "input": ["text", "image", "audio", "video", "pdf"],
+ "output": ["text"]
+ },
+ "open_weights": false,
+ "cost": {
+ "input": 0.15,
+ "output": 0.6,
+ "cache_read": 0.0375
+ },
+ "limit": {
+ "context": 1048576,
+ "output": 65536
+ }
+ },
+ "google/gemma-3-27b-it": {
+ "id": "google/gemma-3-27b-it",
+ "name": "Gemma 3 27B IT",
+ "attachment": true,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2024-10",
+ "release_date": "2025-03-12",
+ "last_updated": "2025-03-12",
+ "modalities": {
+ "input": ["text", "image"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 96000,
+ "output": 8192
+ }
+ },
+ "google/gemini-2.5-pro-preview-06-05": {
+ "id": "google/gemini-2.5-pro-preview-06-05",
+ "name": "Gemini 2.5 Pro Preview 06-05",
+ "attachment": true,
+ "reasoning": true,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2025-01",
+ "release_date": "2025-06-05",
+ "last_updated": "2025-06-05",
+ "modalities": {
+ "input": ["text", "image", "audio", "video", "pdf"],
+ "output": ["text"]
+ },
+ "open_weights": false,
+ "cost": {
+ "input": 1.25,
+ "output": 10,
+ "cache_read": 0.31
+ },
+ "limit": {
+ "context": 1048576,
+ "output": 65536
+ }
+ },
+ "google/gemini-2.5-flash-preview-04-17": {
+ "id": "google/gemini-2.5-flash-preview-04-17",
+ "name": "Gemini 2.5 Flash Preview 04-17",
+ "attachment": true,
+ "reasoning": true,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2025-01",
+ "release_date": "2025-04-17",
+ "last_updated": "2025-04-17",
+ "modalities": {
+ "input": ["text", "image", "audio", "video", "pdf"],
+ "output": ["text"]
+ },
+ "open_weights": false,
+ "cost": {
+ "input": 0.15,
+ "output": 0.6,
+ "cache_read": 0.0375
+ },
+ "limit": {
+ "context": 1048576,
+ "output": 65536
+ }
+ },
+ "google/gemini-2.0-flash-exp:free": {
+ "id": "google/gemini-2.0-flash-exp:free",
+ "name": "Gemini 2.0 Flash Experimental (free)",
+ "attachment": true,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2024-12",
+ "release_date": "2024-12-11",
+ "last_updated": "2024-12-11",
+ "modalities": {
+ "input": ["text", "image"],
+ "output": ["text"]
+ },
+ "open_weights": false,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 1048576,
+ "output": 1048576
+ }
+ },
+ "google/gemma-2-9b-it:free": {
+ "id": "google/gemma-2-9b-it:free",
+ "name": "Gemma 2 9B (free)",
+ "attachment": false,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2024-06",
+ "release_date": "2024-06-28",
+ "last_updated": "2024-06-28",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 8192,
+ "output": 8192
+ }
+ },
+ "google/gemma-3-12b-it": {
+ "id": "google/gemma-3-12b-it",
+ "name": "Gemma 3 12B IT",
+ "attachment": true,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2024-10",
+ "release_date": "2025-03-13",
+ "last_updated": "2025-03-13",
+ "modalities": {
+ "input": ["text", "image"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 96000,
+ "output": 8192
+ }
+ },
+ "google/gemini-2.5-pro-preview-05-06": {
+ "id": "google/gemini-2.5-pro-preview-05-06",
+ "name": "Gemini 2.5 Pro Preview 05-06",
+ "attachment": true,
+ "reasoning": true,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2025-01",
+ "release_date": "2025-05-06",
+ "last_updated": "2025-05-06",
+ "modalities": {
+ "input": ["text", "image", "audio", "video", "pdf"],
+ "output": ["text"]
+ },
+ "open_weights": false,
+ "cost": {
+ "input": 1.25,
+ "output": 10,
+ "cache_read": 0.31
+ },
+ "limit": {
+ "context": 1048576,
+ "output": 65536
+ }
+ },
+ "google/gemini-2.0-flash-001": {
+ "id": "google/gemini-2.0-flash-001",
+ "name": "Gemini 2.0 Flash",
+ "attachment": true,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2024-06",
+ "release_date": "2024-12-11",
+ "last_updated": "2024-12-11",
+ "modalities": {
+ "input": ["text", "image", "audio", "video", "pdf"],
+ "output": ["text"]
+ },
+ "open_weights": false,
+ "cost": {
+ "input": 0.1,
+ "output": 0.4,
+ "cache_read": 0.025
+ },
+ "limit": {
+ "context": 1048576,
+ "output": 8192
+ }
+ },
+ "google/gemma-3n-e4b-it": {
+ "id": "google/gemma-3n-e4b-it",
+ "name": "Gemma 3n E4B IT",
+ "attachment": true,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": false,
+ "knowledge": "2024-10",
+ "release_date": "2025-05-20",
+ "last_updated": "2025-05-20",
+ "modalities": {
+ "input": ["text", "image", "audio"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 8192,
+ "output": 8192
+ }
+ },
+ "google/gemini-2.5-pro": {
+ "id": "google/gemini-2.5-pro",
+ "name": "Gemini 2.5 Pro",
+ "attachment": true,
+ "reasoning": true,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2025-01",
+ "release_date": "2025-03-20",
+ "last_updated": "2025-06-05",
+ "modalities": {
+ "input": ["text", "image", "audio", "video", "pdf"],
+ "output": ["text"]
+ },
+ "open_weights": false,
+ "cost": {
+ "input": 1.25,
+ "output": 10,
+ "cache_read": 0.31
+ },
+ "limit": {
+ "context": 1048576,
+ "output": 65536
+ }
+ },
+ "thudm/glm-z1-32b:free": {
+ "id": "thudm/glm-z1-32b:free",
+ "name": "GLM Z1 32B (free)",
+ "attachment": false,
+ "reasoning": true,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2025-04",
+ "release_date": "2025-04-17",
+ "last_updated": "2025-04-17",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 32768,
+ "output": 32768
+ }
+ },
+ "mistralai/mistral-small-3.1-24b-instruct": {
+ "id": "mistralai/mistral-small-3.1-24b-instruct",
+ "name": "Mistral Small 3.1 24B Instruct",
+ "attachment": true,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2024-10",
+ "release_date": "2025-03-17",
+ "last_updated": "2025-03-17",
+ "modalities": {
+ "input": ["text", "image"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 128000,
+ "output": 8192
+ }
+ },
+ "mistralai/devstral-small-2507": {
+ "id": "mistralai/devstral-small-2507",
+ "name": "Devstral Small 1.1",
+ "attachment": false,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2025-05",
+ "release_date": "2025-07-10",
+ "last_updated": "2025-07-10",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0.1,
+ "output": 0.3
+ },
+ "limit": {
+ "context": 131072,
+ "output": 131072
+ }
+ },
+ "mistralai/mistral-small-3.2-24b-instruct": {
+ "id": "mistralai/mistral-small-3.2-24b-instruct",
+ "name": "Mistral Small 3.2 24B Instruct",
+ "attachment": true,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2024-10",
+ "release_date": "2025-06-20",
+ "last_updated": "2025-06-20",
+ "modalities": {
+ "input": ["text", "image"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 96000,
+ "output": 8192
+ }
+ },
+ "mistralai/mistral-7b-instruct:free": {
+ "id": "mistralai/mistral-7b-instruct:free",
+ "name": "Mistral 7B Instruct (free)",
+ "attachment": false,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2024-05",
+ "release_date": "2024-05-27",
+ "last_updated": "2024-05-27",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 32768,
+ "output": 32768
+ }
+ },
+ "mistralai/devstral-medium-2507": {
+ "id": "mistralai/devstral-medium-2507",
+ "name": "Devstral Medium",
+ "attachment": false,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2025-05",
+ "release_date": "2025-07-10",
+ "last_updated": "2025-07-10",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0.4,
+ "output": 2
+ },
+ "limit": {
+ "context": 131072,
+ "output": 131072
+ }
+ },
+ "mistralai/mistral-small-3.2-24b-instruct:free": {
+ "id": "mistralai/mistral-small-3.2-24b-instruct:free",
+ "name": "Mistral Small 3.2 24B (free)",
+ "attachment": true,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2025-06",
+ "release_date": "2025-06-20",
+ "last_updated": "2025-06-20",
+ "modalities": {
+ "input": ["text", "image"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 96000,
+ "output": 96000
+ }
+ },
+ "mistralai/devstral-small-2505:free": {
+ "id": "mistralai/devstral-small-2505:free",
+ "name": "Devstral Small 2505 (free)",
+ "attachment": false,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2025-05",
+ "release_date": "2025-05-21",
+ "last_updated": "2025-05-21",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 32768,
+ "output": 32768
+ }
+ },
+ "mistralai/mistral-nemo:free": {
+ "id": "mistralai/mistral-nemo:free",
+ "name": "Mistral Nemo (free)",
+ "attachment": false,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2024-07",
+ "release_date": "2024-07-19",
+ "last_updated": "2024-07-19",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 131072,
+ "output": 131072
+ }
+ },
+ "mistralai/devstral-small-2505": {
+ "id": "mistralai/devstral-small-2505",
+ "name": "Devstral Small",
+ "attachment": false,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2025-05",
+ "release_date": "2025-05-07",
+ "last_updated": "2025-05-07",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0.06,
+ "output": 0.12
+ },
+ "limit": {
+ "context": 128000,
+ "output": 128000
+ }
+ },
+ "nousresearch/deephermes-3-llama-3-8b-preview": {
+ "id": "nousresearch/deephermes-3-llama-3-8b-preview",
+ "name": "DeepHermes 3 Llama 3 8B Preview",
+ "attachment": false,
+ "reasoning": true,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2024-04",
+ "release_date": "2025-02-28",
+ "last_updated": "2025-02-28",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 131072,
+ "output": 8192
+ }
+ },
+ "tngtech/deepseek-r1t2-chimera:free": {
+ "id": "tngtech/deepseek-r1t2-chimera:free",
+ "name": "DeepSeek R1T2 Chimera (free)",
+ "attachment": false,
+ "reasoning": true,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2025-07",
+ "release_date": "2025-07-08",
+ "last_updated": "2025-07-08",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 163840,
+ "output": 163840
+ }
+ },
+ "qwen/qwen3-30b-a3b:free": {
+ "id": "qwen/qwen3-30b-a3b:free",
+ "name": "Qwen3 30B A3B (free)",
+ "attachment": false,
+ "reasoning": true,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2025-04",
+ "release_date": "2025-04-28",
+ "last_updated": "2025-04-28",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 40960,
+ "output": 40960
+ }
+ },
+ "qwen/qwen3-235b-a22b:free": {
+ "id": "qwen/qwen3-235b-a22b:free",
+ "name": "Qwen3 235B A22B (free)",
+ "attachment": false,
+ "reasoning": true,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2025-04",
+ "release_date": "2025-04-28",
+ "last_updated": "2025-04-28",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 131072,
+ "output": 131072
+ }
+ },
+ "qwen/qwq-32b:free": {
+ "id": "qwen/qwq-32b:free",
+ "name": "QwQ 32B (free)",
+ "attachment": false,
+ "reasoning": true,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2025-03",
+ "release_date": "2025-03-05",
+ "last_updated": "2025-03-05",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 32768,
+ "output": 32768
+ }
+ },
+ "qwen/qwen-2.5-coder-32b-instruct": {
+ "id": "qwen/qwen-2.5-coder-32b-instruct",
+ "name": "Qwen2.5 Coder 32B Instruct",
+ "attachment": false,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": false,
+ "knowledge": "2024-10",
+ "release_date": "2024-11-11",
+ "last_updated": "2024-11-11",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 32768,
+ "output": 8192
+ }
+ },
+ "qwen/qwen3-8b:free": {
+ "id": "qwen/qwen3-8b:free",
+ "name": "Qwen3 8B (free)",
+ "attachment": false,
+ "reasoning": true,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2025-04",
+ "release_date": "2025-04-28",
+ "last_updated": "2025-04-28",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 40960,
+ "output": 40960
+ }
+ },
+ "qwen/qwen3-14b:free": {
+ "id": "qwen/qwen3-14b:free",
+ "name": "Qwen3 14B (free)",
+ "attachment": false,
+ "reasoning": true,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2025-04",
+ "release_date": "2025-04-28",
+ "last_updated": "2025-04-28",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 40960,
+ "output": 40960
+ }
+ },
+ "qwen/qwen2.5-vl-72b-instruct": {
+ "id": "qwen/qwen2.5-vl-72b-instruct",
+ "name": "Qwen2.5 VL 72B Instruct",
+ "attachment": true,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": false,
+ "knowledge": "2024-10",
+ "release_date": "2025-02-01",
+ "last_updated": "2025-02-01",
+ "modalities": {
+ "input": ["text", "image"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 32768,
+ "output": 8192
+ }
+ },
+ "qwen/qwen2.5-vl-72b-instruct:free": {
+ "id": "qwen/qwen2.5-vl-72b-instruct:free",
+ "name": "Qwen2.5 VL 72B Instruct (free)",
+ "attachment": true,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2025-02",
+ "release_date": "2025-02-01",
+ "last_updated": "2025-02-01",
+ "modalities": {
+ "input": ["text", "image"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 32768,
+ "output": 32768
+ }
+ },
+ "qwen/qwen2.5-vl-32b-instruct:free": {
+ "id": "qwen/qwen2.5-vl-32b-instruct:free",
+ "name": "Qwen2.5 VL 32B Instruct (free)",
+ "attachment": true,
+ "reasoning": false,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2025-03",
+ "release_date": "2025-03-24",
+ "last_updated": "2025-03-24",
+ "modalities": {
+ "input": ["text", "image", "video"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 8192,
+ "output": 8192
+ }
+ },
+ "qwen/qwen3-32b:free": {
+ "id": "qwen/qwen3-32b:free",
+ "name": "Qwen3 32B (free)",
+ "attachment": false,
+ "reasoning": true,
+ "temperature": true,
+ "tool_call": true,
+ "knowledge": "2025-04",
+ "release_date": "2025-04-28",
+ "last_updated": "2025-04-28",
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ },
+ "open_weights": true,
+ "cost": {
+ "input": 0,
+ "output": 0
+ },
+ "limit": {
+ "context": 40960,
+ "output": 40960
+ }
+ }
+ }
+}
diff --git a/examples/chat-ui/src/hooks/useConversationUpdater.ts b/examples/chat-ui/src/hooks/useConversationUpdater.ts
index 4293f57..d1eecff 100644
--- a/examples/chat-ui/src/hooks/useConversationUpdater.ts
+++ b/examples/chat-ui/src/hooks/useConversationUpdater.ts
@@ -6,7 +6,6 @@ interface UseConversationUpdaterProps {
}
export const useConversationUpdater = ({ conversationId, setConversations }: UseConversationUpdaterProps) => {
-
const updateConversation = (updater: (conversation: Conversation) => Conversation) => {
setConversations((prev) => {
const conversation = prev.find((c) => c.id === conversationId)
@@ -14,12 +13,10 @@ export const useConversationUpdater = ({ conversationId, setConversations }: Use
console.error(`Missing conversation for ID ${conversationId}`)
return prev
}
-
+
const updatedConversation = updater(conversation)
-
- return prev.map((c) =>
- c.id === conversationId ? updatedConversation : c
- )
+
+ return prev.map((c) => (c.id === conversationId ? updatedConversation : c))
})
}
diff --git a/examples/chat-ui/src/hooks/useModels.ts b/examples/chat-ui/src/hooks/useModels.ts
new file mode 100644
index 0000000..28d0809
--- /dev/null
+++ b/examples/chat-ui/src/hooks/useModels.ts
@@ -0,0 +1,115 @@
+import { useState, useEffect, useCallback } from 'react'
+import { Model, providers, SUPPORTED_PROVIDERS, FAVORITES_KEY } from '../types/models'
+
+// Load models data from generated JSON
+import modelsData from '../data/models.json'
+
+export function useModels() {
+ const [models, setModels] = useState([])
+ const [favorites, setFavorites] = useState([])
+ const [loading, setLoading] = useState(true)
+
+ // Load models from data and favorites from localStorage
+ useEffect(() => {
+ const loadModels = () => {
+ const allModels: Model[] = []
+
+ // Process each provider's models
+ for (const providerId of SUPPORTED_PROVIDERS) {
+ const provider = providers[providerId]
+ const providerModels = modelsData[providerId] || {}
+
+ for (const [modelId, modelData] of Object.entries(providerModels)) {
+ const model: Model = {
+ id: `${providerId}:${modelId}`,
+ name: modelData.name,
+ provider,
+ modelId,
+ supportsTools: modelData.tool_call,
+ reasoning: modelData.reasoning,
+ attachment: modelData.attachment,
+ contextLimit: modelData.limit.context,
+ outputLimit: modelData.limit.output,
+ cost: modelData.cost,
+ }
+ allModels.push(model)
+ }
+ }
+
+ setModels(allModels)
+ setLoading(false)
+ }
+
+ const loadFavorites = () => {
+ try {
+ const saved = localStorage.getItem(FAVORITES_KEY)
+ if (saved) {
+ const parsed = JSON.parse(saved)
+ setFavorites(parsed)
+ }
+ } catch (error) {
+ console.error('Failed to load favorites from localStorage:', error)
+ }
+ }
+
+ loadModels()
+ loadFavorites()
+ }, [])
+
+ // Save favorites to localStorage when they change
+ useEffect(() => {
+ // Only save if favorites array has been loaded (not during initial state)
+ if (loading) return
+
+ try {
+ localStorage.setItem(FAVORITES_KEY, JSON.stringify(favorites))
+ } catch (error) {
+ console.error('Failed to save favorites to localStorage:', error)
+ }
+ }, [favorites, loading])
+
+ const toggleFavorite = useCallback((modelId: string) => {
+ setFavorites((prev) => {
+ if (prev.includes(modelId)) {
+ return prev.filter((id) => id !== modelId)
+ } else {
+ return [...prev, modelId]
+ }
+ })
+ }, [])
+
+ const addToFavorites = useCallback((modelId: string) => {
+ setFavorites((prev) => {
+ if (!prev.includes(modelId)) {
+ return [...prev, modelId]
+ }
+ return prev
+ })
+ }, [])
+
+ const isFavorite = useCallback(
+ (modelId: string) => {
+ return favorites.includes(modelId)
+ },
+ [favorites]
+ )
+
+ const getFavoriteModels = useCallback(() => {
+ return models.filter((model) => favorites.includes(model.id))
+ }, [models, favorites])
+
+ const getToolSupportingModels = useCallback(() => {
+ return models.filter((model) => model.supportsTools)
+ }, [models])
+
+ return {
+ models,
+ favorites,
+ loading,
+ toggleFavorite,
+ addToFavorites,
+ isFavorite,
+ getFavoriteModels,
+ getToolSupportingModels,
+ }
+}
diff --git a/examples/chat-ui/src/hooks/useStreamResponse.ts b/examples/chat-ui/src/hooks/useStreamResponse.ts
index a803cbe..f8e9b6b 100644
--- a/examples/chat-ui/src/hooks/useStreamResponse.ts
+++ b/examples/chat-ui/src/hooks/useStreamResponse.ts
@@ -2,11 +2,13 @@ import { useRef, useState } from 'react'
import { CoreMessage, jsonSchema, streamText, tool } from 'ai'
import { createGroq } from '@ai-sdk/groq'
import { createAnthropic } from '@ai-sdk/anthropic'
+import { createOpenAI } from '@ai-sdk/openai'
import { type AssistantMessage, type Conversation, type Message, type SystemMessage, type UserMessage } from '../types'
import { type Model } from '../types/models'
-import { getApiKey } from '../utils/apiKeys'
+import { getAuthHeaders } from '../utils/auth'
import { type Tool } from 'use-mcp/react'
import { useConversationUpdater } from './useConversationUpdater'
+import { getProviderOptions } from '../utils/modelOptions.ts'
// Type guard for messages with content
const hasContent = (message: Message): message is UserMessage | AssistantMessage | SystemMessage => {
@@ -82,16 +84,18 @@ export const useStreamResponse = ({
// return reasoningModels.includes(model.modelId)
// }
- const getModelInstance = (model: Model, apiKey: string) => {
+ const getModelInstance = async (model: Model, authHeaders: Record) => {
let baseModel
switch (model.provider.id) {
case 'groq': {
- const groqProvider = createGroq({ apiKey })
+ const apiKey = authHeaders.Authorization?.replace('Bearer ', '')
+ const groqProvider = createGroq({ apiKey, baseURL: model.provider.baseUrl })
baseModel = groqProvider(model.modelId)
break
}
case 'anthropic': {
+ const apiKey = authHeaders['x-api-key']
const anthropicProvider = createAnthropic({
apiKey,
headers: {
@@ -101,6 +105,15 @@ export const useStreamResponse = ({
baseModel = anthropicProvider(model.modelId)
break
}
+ case 'openrouter': {
+ const apiKey = authHeaders.Authorization?.replace('Bearer ', '')
+ const openrouterProvider = createOpenAI({
+ apiKey,
+ baseURL: 'https://openrouter.ai/api/v1',
+ })
+ baseModel = openrouterProvider(model.modelId)
+ break
+ }
default:
throw new Error(`Unsupported provider: ${model.provider.id}`)
}
@@ -112,21 +125,25 @@ export const useStreamResponse = ({
}
const streamResponse = async (messages: Message[]) => {
- // Check if API key is available
- let apiKey = getApiKey(selectedModel.provider.id)
- if (!apiKey) {
+ // Check if authentication is available
+ let authHeaders: Record
+ try {
+ authHeaders = await getAuthHeaders(selectedModel.provider.id)
+ } catch (error) {
+ // Authentication not available, prompt user
const keyProvided = await onApiKeyRequired(selectedModel)
if (!keyProvided) {
return // User cancelled
}
- apiKey = getApiKey(selectedModel.provider.id)
- if (!apiKey) {
- throw new Error('No API key provided')
+ try {
+ authHeaders = await getAuthHeaders(selectedModel.provider.id)
+ } catch (error) {
+ throw new Error('No valid authentication found')
}
}
try {
- const modelInstance = getModelInstance(selectedModel, apiKey)
+ const modelInstance = await getModelInstance(selectedModel, authHeaders)
setStreamStarted(true)
@@ -143,16 +160,15 @@ export const useStreamResponse = ({
content: contentMsg.content,
}
})
- // Prepare stream options
- const streamOptions = {
+
+ const result = streamText({
model: modelInstance,
messages: messagesToSend,
tools: Object.keys(aiTools).length > 0 ? aiTools : undefined,
maxSteps: 5, // Allow up to 5 steps for tool calling
abortSignal: controller.signal,
- }
-
- const result = await streamText(streamOptions)
+ providerOptions: getProviderOptions(selectedModel.provider.id, selectedModel.modelId),
+ })
// Use fullStream to get all events including tool calls, results, and text
for await (const event of result.fullStream) {
@@ -164,6 +180,7 @@ export const useStreamResponse = ({
let updatedMessage: Message | undefined
let newMessage: Message | undefined
try {
+ console.log({ event })
if (event.type === 'reasoning') {
if (lastMessage?.role === 'assistant' && lastMessage.type === 'reasoning') {
// We have an existing reasoning block!
@@ -266,8 +283,23 @@ export const useStreamResponse = ({
}
} catch (error: unknown) {
if (controller.signal.aborted) {
+ // Request was cancelled, don't show error
} else {
console.error('Error generating response:', error)
+
+ // Add error message to the conversation
+ const errorMessage = error instanceof Error ? error.message : String(error)
+ updateConversation((conv) => ({
+ ...conv,
+ messages: [
+ ...conv.messages,
+ {
+ role: 'error',
+ content: errorMessage,
+ timestamp: Date.now(),
+ },
+ ],
+ }))
}
} finally {
setStreamStarted(false)
diff --git a/examples/chat-ui/src/styles/markdown.css b/examples/chat-ui/src/styles/markdown.css
new file mode 100644
index 0000000..ae02739
--- /dev/null
+++ b/examples/chat-ui/src/styles/markdown.css
@@ -0,0 +1,66 @@
+/* Tighter markdown spacing */
+.prose-tight {
+ line-height: 1.5;
+}
+
+.prose-tight h1,
+.prose-tight h2,
+.prose-tight h3,
+.prose-tight h4,
+.prose-tight h5,
+.prose-tight h6 {
+ margin-top: 1rem;
+ margin-bottom: 0.5rem;
+}
+
+.prose-tight h1:first-child,
+.prose-tight h2:first-child,
+.prose-tight h3:first-child,
+.prose-tight h4:first-child,
+.prose-tight h5:first-child,
+.prose-tight h6:first-child {
+ margin-top: 0;
+}
+
+.prose-tight p {
+ margin-top: 0.75rem;
+ margin-bottom: 0.75rem;
+}
+
+.prose-tight p:first-child {
+ margin-top: 0;
+}
+
+.prose-tight p:last-child {
+ margin-bottom: 0;
+}
+
+.prose-tight ul,
+.prose-tight ol {
+ margin-top: 0.75rem;
+ margin-bottom: 0.75rem;
+}
+
+.prose-tight li {
+ margin-top: 0.25rem;
+ margin-bottom: 0.25rem;
+}
+
+.prose-tight ul > li,
+.prose-tight ol > li {
+ padding-left: 0.25rem;
+}
+
+.prose-tight blockquote {
+ margin-top: 1rem;
+ margin-bottom: 1rem;
+}
+
+.prose-tight code {
+ font-size: 0.875em;
+}
+
+.prose-tight pre {
+ margin-top: 1rem;
+ margin-bottom: 1rem;
+}
diff --git a/examples/chat-ui/src/types/index.ts b/examples/chat-ui/src/types/index.ts
index 023c17a..d289fc3 100644
--- a/examples/chat-ui/src/types/index.ts
+++ b/examples/chat-ui/src/types/index.ts
@@ -38,7 +38,13 @@ export interface ToolResultMessage {
callId: string
}
-export type Message = UserMessage | SystemMessage | AssistantMessage | ReasoningMessage | ToolCallMessage | ToolResultMessage
+export interface ErrorMessage {
+ role: 'error'
+ content: string
+ timestamp: number
+}
+
+export type Message = UserMessage | SystemMessage | AssistantMessage | ReasoningMessage | ToolCallMessage | ToolResultMessage | ErrorMessage
export interface Conversation {
id?: number
diff --git a/examples/chat-ui/src/types/models.ts b/examples/chat-ui/src/types/models.ts
index 7f24b70..2e373a6 100644
--- a/examples/chat-ui/src/types/models.ts
+++ b/examples/chat-ui/src/types/models.ts
@@ -1,67 +1,113 @@
-export interface ModelProvider {
+// Types for models.dev API data
+export interface ModelData {
id: string
name: string
+ attachment: boolean
+ reasoning: boolean
+ temperature: boolean
+ tool_call: boolean
+ knowledge: string
+ release_date: string
+ last_updated: string
+ modalities: {
+ input: string[]
+ output: string[]
+ }
+ open_weights: boolean
+ limit: {
+ context: number
+ output: number
+ }
+ cost?: {
+ input: number
+ output: number
+ cache_read?: number
+ cache_write?: number
+ }
+}
+
+export type SupportedProvider = 'anthropic' | 'groq' | 'openrouter'
+
+export interface Provider {
+ id: SupportedProvider
+ name: string
baseUrl: string
- apiKeyHeader: string
+ logo: string
documentationUrl: string
- logo?: string
+ authType: 'apiKey' | 'oauth'
+ apiKeyHeader?: string
+ oauth?: {
+ authorizeUrl: string
+ tokenUrl: string
+ }
}
export interface Model {
id: string
name: string
- provider: ModelProvider
+ provider: Provider
modelId: string
+ supportsTools: boolean
+ reasoning: boolean
+ attachment: boolean
+ contextLimit: number
+ outputLimit: number
+ cost?: {
+ input: number
+ output: number
+ cache_read?: number
+ cache_write?: number
+ }
}
-export const providers: Record = {
+export const providers: Record = {
groq: {
id: 'groq',
name: 'Groq',
baseUrl: 'https://api.groq.com/openai/v1',
- apiKeyHeader: 'Authorization',
- documentationUrl: 'https://console.groq.com/docs',
logo: '๐',
+ documentationUrl: 'https://console.groq.com/docs',
+ apiKeyHeader: 'Authorization',
+ authType: 'apiKey',
},
+ // groq: {
+ // id: 'groq',
+ // name: 'Groq',
+ // baseUrl: 'http://localhost:8000/api/openai/v1',
+ // logo: '๐',
+ // documentationUrl: 'https://console.groq.com/docs',
+ // apiKeyHeader: 'Authorization',
+ // authType: 'oauth',
+ // oauth: {
+ // authorizeUrl: 'http://localhost:3000/keys/request',
+ // tokenUrl: 'http://localhost:3000/keys/request/exchange',
+ // },
+ // },
anthropic: {
id: 'anthropic',
name: 'Anthropic',
baseUrl: 'https://api.anthropic.com/v1',
- apiKeyHeader: 'x-api-key',
- documentationUrl: 'https://docs.anthropic.com/',
logo: '๐ค',
+ documentationUrl: 'https://docs.anthropic.com/',
+ authType: 'apiKey',
+ apiKeyHeader: 'x-api-key',
+ },
+ openrouter: {
+ id: 'openrouter',
+ name: 'OpenRouter',
+ baseUrl: 'https://openrouter.ai/api/v1',
+ logo: '๐',
+ documentationUrl: 'https://openrouter.ai/docs',
+ authType: 'oauth',
+ oauth: {
+ authorizeUrl: 'https://openrouter.ai/auth',
+ tokenUrl: 'https://openrouter.ai/api/v1/auth/keys',
+ },
},
}
-export const availableModels: Model[] = [
- {
- id: 'llama-3.3-70b-versatile',
- name: 'Llama 3.3 70B Versatile',
- provider: providers.groq,
- modelId: 'llama-3.3-70b-versatile',
- },
- {
- id: 'qwen-3-32b',
- name: 'Qwen3 32B',
- provider: providers.groq,
- modelId: 'qwen/qwen3-32b',
- },
- // {
- // id: 'qwen-qwq-32b',
- // name: 'Qwen QwQ 32B (Reasoning)',
- // provider: providers.groq,
- // modelId: 'qwen-qwq-32b',
- // },
- // {
- // id: 'deepseek-r1-distill-llama-70b',
- // name: 'DeepSeek R1 Distil Llama 70B (Reasoning)',
- // provider: providers.groq,
- // modelId: 'deepseek-r1-distill-llama-70b',
- // },
- {
- id: 'claude-3-5-sonnet-20241022',
- name: 'Claude 4 Sonnet',
- provider: providers.anthropic,
- modelId: 'claude-3-5-sonnet-20241022',
- },
-]
+export const SUPPORTED_PROVIDERS: readonly SupportedProvider[] = ['anthropic', 'groq', 'openrouter']
+
+// Storage keys for user preferences
+export const FAVORITES_KEY = 'aiChatTemplate_favorites_v1'
+export const PROVIDER_TOKEN_KEY_PREFIX = 'aiChatTemplate_token_'
diff --git a/examples/chat-ui/src/utils/apiKeys.ts b/examples/chat-ui/src/utils/apiKeys.ts
index 3bb862f..989595e 100644
--- a/examples/chat-ui/src/utils/apiKeys.ts
+++ b/examples/chat-ui/src/utils/apiKeys.ts
@@ -1,17 +1,2 @@
-const API_KEY_PREFIX = 'aiChatTemplate_apiKey_'
-
-export const getApiKey = (providerId: string): string | null => {
- return localStorage.getItem(`${API_KEY_PREFIX}${providerId}`)
-}
-
-export const setApiKey = (providerId: string, apiKey: string): void => {
- localStorage.setItem(`${API_KEY_PREFIX}${providerId}`, apiKey)
-}
-
-export const clearApiKey = (providerId: string): void => {
- localStorage.removeItem(`${API_KEY_PREFIX}${providerId}`)
-}
-
-export const hasApiKey = (providerId: string): boolean => {
- return getApiKey(providerId) !== null
-}
+// Re-export functions from new auth system for backward compatibility
+export { getApiKey, setApiKey, clearApiKey, hasApiKey } from './auth'
diff --git a/examples/chat-ui/src/utils/auth.ts b/examples/chat-ui/src/utils/auth.ts
new file mode 100644
index 0000000..b1f46ff
--- /dev/null
+++ b/examples/chat-ui/src/utils/auth.ts
@@ -0,0 +1,364 @@
+import { SupportedProvider, providers, PROVIDER_TOKEN_KEY_PREFIX } from '../types/models'
+
+// Types for OAuth tokens
+export interface OAuthToken {
+ access_token: string
+ refresh_token?: string
+ expires_at?: number
+ token_type: 'Bearer'
+}
+
+// Types for PKCE flow
+interface PKCEState {
+ code_verifier: string
+ // TODO: Add state support back if needed later
+ // state: string
+}
+
+// Generate a random code verifier for PKCE
+function generateCodeVerifier(): string {
+ const array = new Uint8Array(32)
+ crypto.getRandomValues(array)
+ return btoa(String.fromCharCode(...array))
+ .replace(/\+/g, '-')
+ .replace(/\//g, '_')
+ .replace(/=/g, '')
+}
+
+// Generate code challenge from verifier
+async function generateCodeChallenge(verifier: string): Promise {
+ const encoder = new TextEncoder()
+ const data = encoder.encode(verifier)
+ const digest = await crypto.subtle.digest('SHA-256', data)
+ return btoa(String.fromCharCode(...new Uint8Array(digest)))
+ .replace(/\+/g, '-')
+ .replace(/\//g, '_')
+ .replace(/=/g, '')
+}
+
+// TODO: Add state generation back if needed later
+// function generateState(): string {
+// const array = new Uint8Array(16)
+// crypto.getRandomValues(array)
+// return btoa(String.fromCharCode(...array))
+// .replace(/\+/g, '-')
+// .replace(/\//g, '_')
+// .replace(/=/g, '')
+// }
+
+// API Key functions (existing functionality)
+export function hasApiKey(providerId: SupportedProvider): boolean {
+ const provider = providers[providerId]
+ if (provider.authType === 'oauth') {
+ return hasOAuthToken(providerId)
+ }
+
+ const key = localStorage.getItem(PROVIDER_TOKEN_KEY_PREFIX + providerId)
+ return key !== null && key.length > 0
+}
+
+export function getApiKey(providerId: SupportedProvider): string | null {
+ return localStorage.getItem(PROVIDER_TOKEN_KEY_PREFIX + providerId)
+}
+
+export function setApiKey(providerId: SupportedProvider, apiKey: string): void {
+ localStorage.setItem(PROVIDER_TOKEN_KEY_PREFIX + providerId, apiKey)
+}
+
+export function clearApiKey(providerId: SupportedProvider): void {
+ localStorage.removeItem(PROVIDER_TOKEN_KEY_PREFIX + providerId)
+}
+
+// OAuth functions
+export function hasOAuthToken(providerId: SupportedProvider): boolean {
+ const token = getOAuthToken(providerId)
+ if (!token) return false
+
+ // Check if token is expired
+ if (token.expires_at && token.expires_at < Date.now()) {
+ return false
+ }
+
+ return true
+}
+
+export function getOAuthToken(providerId: SupportedProvider): OAuthToken | null {
+ try {
+ const tokenJson = localStorage.getItem(PROVIDER_TOKEN_KEY_PREFIX + providerId)
+ if (!tokenJson) return null
+
+ const token = JSON.parse(tokenJson) as OAuthToken
+ return token
+ } catch (error) {
+ console.error('Failed to parse OAuth token:', error)
+ return null
+ }
+}
+
+export function setOAuthToken(providerId: SupportedProvider, token: OAuthToken): void {
+ localStorage.setItem(PROVIDER_TOKEN_KEY_PREFIX + providerId, JSON.stringify(token))
+}
+
+export function clearOAuthToken(providerId: SupportedProvider): void {
+ localStorage.removeItem(PROVIDER_TOKEN_KEY_PREFIX + providerId)
+}
+
+// PKCE OAuth flow functions
+export async function beginOAuthFlow(providerId: SupportedProvider): Promise {
+ const provider = providers[providerId]
+ if (!provider.oauth) {
+ throw new Error(`Provider ${providerId} does not support OAuth`)
+ }
+
+ const codeVerifier = generateCodeVerifier()
+ const codeChallenge = await generateCodeChallenge(codeVerifier)
+
+ console.log('DEBUG: Generated PKCE values for', providerId, {
+ codeVerifier: codeVerifier,
+ codeChallenge: codeChallenge,
+ verifierLength: codeVerifier.length,
+ challengeLength: codeChallenge.length,
+ })
+
+ // Store PKCE state in sessionStorage (one key per provider)
+ const pkceState: PKCEState = { code_verifier: codeVerifier }
+ const storageKey = `pkce_${providerId}`
+ sessionStorage.setItem(storageKey, JSON.stringify(pkceState))
+
+ // Construct authorization URL based on provider
+ let authUrl: URL
+
+ if (providerId === 'openrouter') {
+ // OpenRouter uses a different redirect name flow
+ authUrl = new URL(provider.oauth.authorizeUrl)
+ authUrl.searchParams.set('callback_url', getRedirectUri(providerId))
+ } else {
+ // Groq uses redirect_uri
+ authUrl = new URL(provider.oauth.authorizeUrl)
+ authUrl.searchParams.set('redirect_uri', getRedirectUri(providerId))
+ }
+ authUrl.searchParams.set('code_challenge', codeChallenge)
+ authUrl.searchParams.set('code_challenge_method', 'S256')
+ // TODO: Add state parameter back if needed later
+ // authUrl.searchParams.set('state', state)
+
+ // Open popup or redirect
+ console.log('DEBUG: Opening OAuth popup for', providerId, 'with URL:', authUrl.toString())
+ const popup = window.open(authUrl.toString(), `oauth_${providerId}`, 'width=600,height=700,popup=1')
+
+ if (!popup) {
+ console.log('DEBUG: Failed to open popup, trying redirect in same window')
+ // Fallback to redirect in same window
+ window.location.href = authUrl.toString()
+ return
+ }
+
+ console.log('DEBUG: Popup opened successfully:', popup)
+ console.log('DEBUG: Popup closed:', popup.closed)
+ console.log('DEBUG: Popup window object:', popup.window)
+}
+
+export async function completeOAuthFlow(providerId: SupportedProvider, code: string): Promise {
+ console.log('DEBUG: Starting OAuth completion for', providerId, 'with code:', code?.substring(0, 10) + '...')
+ console.log('DEBUG: Full code for debugging:', code)
+ console.log('DEBUG: Provider config:', providers[providerId])
+
+ const provider = providers[providerId]
+ if (!provider.oauth) {
+ throw new Error(`Provider ${providerId} does not support OAuth`)
+ }
+
+ // Retrieve PKCE state (single key per provider)
+ const storageKey = `pkce_${providerId}`
+ const pkceStateJson = sessionStorage.getItem(storageKey)
+
+ console.log('DEBUG: Looking for PKCE key:', storageKey)
+
+ if (!pkceStateJson) {
+ throw new Error('PKCE state not found. Please try again.')
+ }
+
+ const pkceState: PKCEState = JSON.parse(pkceStateJson)
+
+ console.log('DEBUG: Using PKCE state:', { key: storageKey, state: pkceState })
+ console.log('DEBUG: Code verifier length:', pkceState.code_verifier.length)
+ console.log('DEBUG: Code verifier sample:', pkceState.code_verifier.substring(0, 20) + '...')
+
+ // Test: regenerate challenge from verifier to verify it matches server expectation
+ const recomputedChallenge = await generateCodeChallenge(pkceState.code_verifier)
+ console.log('DEBUG: Recomputed challenge from verifier:', recomputedChallenge)
+ console.log('DEBUG: Server reported challenge was: dW2iEvNljlkhcRcryo3Z0GITcJM1liKcHlB5v8CDEu8')
+
+ // Clean up the state
+ sessionStorage.removeItem(storageKey)
+
+ // Exchange code for token
+ let tokenResponse: Response
+
+ if (providerId === 'openrouter') {
+ // OpenRouter uses JSON body instead of form data
+ const requestBody = {
+ code,
+ code_verifier: pkceState.code_verifier,
+ code_challenge_method: 'S256',
+ }
+ console.log('DEBUG: OpenRouter token request:', {
+ url: provider.oauth.tokenUrl,
+ body: requestBody,
+ })
+
+ const startTime = performance.now()
+ tokenResponse = await fetch(provider.oauth.tokenUrl, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(requestBody),
+ })
+ const endTime = performance.now()
+
+ console.log('DEBUG: OpenRouter token response:', {
+ status: tokenResponse.status,
+ statusText: tokenResponse.statusText,
+ headers: Object.fromEntries(tokenResponse.headers.entries()),
+ duration: `${endTime - startTime}ms`,
+ })
+ } else {
+ // Standard OAuth2 flow for other providers (Groq)
+ const requestBody = new URLSearchParams({
+ grant_type: 'authorization_code',
+ code,
+ redirect_uri: getRedirectUri(providerId),
+ code_verifier: pkceState.code_verifier,
+ })
+
+ console.log('DEBUG: Groq token request:', {
+ url: provider.oauth.tokenUrl,
+ body: Object.fromEntries(requestBody.entries()),
+ codeVerifierLength: pkceState.code_verifier.length,
+ codeVerifierSample: pkceState.code_verifier.substring(0, 20) + '...',
+ fullCodeVerifier: pkceState.code_verifier, // For debugging
+ })
+
+ const startTime = performance.now()
+ tokenResponse = await fetch(provider.oauth.tokenUrl, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ body: requestBody,
+ })
+ const endTime = performance.now()
+
+ console.log('DEBUG: Groq token response:', {
+ status: tokenResponse.status,
+ statusText: tokenResponse.statusText,
+ headers: Object.fromEntries(tokenResponse.headers.entries()),
+ duration: `${endTime - startTime}ms`,
+ })
+ }
+
+ if (!tokenResponse.ok) {
+ const errorText = await tokenResponse.text()
+ console.error('DEBUG: Token exchange error response:', errorText)
+ throw new Error(`Token exchange failed: ${tokenResponse.status} ${tokenResponse.statusText}`)
+ }
+
+ const tokenData = await tokenResponse.json()
+ console.log('DEBUG: Token response data:', tokenData)
+
+ // Store token with expiration
+ let token: OAuthToken
+
+ if (providerId === 'openrouter') {
+ // OpenRouter returns { key: "..." }
+ token = {
+ access_token: tokenData.key,
+ token_type: 'Bearer',
+ }
+ } else if (providerId === 'groq') {
+ // Groq returns { api_key: "..." }
+ token = {
+ access_token: tokenData.api_key,
+ token_type: 'Bearer',
+ }
+ } else {
+ // Standard OAuth2 response
+ token = {
+ access_token: tokenData.access_token,
+ refresh_token: tokenData.refresh_token,
+ expires_at: tokenData.expires_in ? Date.now() + tokenData.expires_in * 1000 : undefined,
+ token_type: 'Bearer',
+ }
+ }
+
+ console.log('DEBUG: Saving token for', providerId, ':', token)
+ setOAuthToken(providerId, token)
+
+ // PKCE state already cleaned up above
+}
+
+// Get authentication headers for API calls
+export async function getAuthHeaders(providerId: SupportedProvider): Promise> {
+ const provider = providers[providerId]
+
+ if (provider.authType === 'oauth') {
+ const token = getOAuthToken(providerId)
+ if (!token) {
+ throw new Error(`No OAuth token found for ${providerId}`)
+ }
+
+ return {
+ Authorization: `Bearer ${token.access_token}`,
+ }
+ } else {
+ // API key authentication
+ const apiKey = getApiKey(providerId)
+ if (!apiKey) {
+ throw new Error(`No API key found for ${providerId}`)
+ }
+
+ if (provider.apiKeyHeader === 'Authorization') {
+ return {
+ Authorization: `Bearer ${apiKey}`,
+ }
+ } else {
+ return {
+ [provider.apiKeyHeader!]: apiKey,
+ }
+ }
+ }
+}
+
+// Helper function to get redirect URI
+function getRedirectUri(providerId: SupportedProvider): string {
+ const baseUrl = window.location.origin
+ return `${baseUrl}/oauth/${providerId}/callback`
+}
+
+// Test function to verify PKCE implementation with known values
+export async function testPKCEImplementation(): Promise {
+ console.log('=== TESTING PKCE IMPLEMENTATION ===')
+
+ // Test with a known code verifier (from RFC 7636 example)
+ const testVerifier = 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk'
+ const expectedChallenge = 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM'
+
+ const computedChallenge = await generateCodeChallenge(testVerifier)
+
+ console.log('DEBUG: Test verifier:', testVerifier)
+ console.log('DEBUG: Expected challenge:', expectedChallenge)
+ console.log('DEBUG: Computed challenge:', computedChallenge)
+ console.log('DEBUG: Challenges match:', computedChallenge === expectedChallenge)
+
+ // Test with current implementation
+ const currentVerifier = generateCodeVerifier()
+ const currentChallenge = await generateCodeChallenge(currentVerifier)
+
+ console.log('DEBUG: Current verifier:', currentVerifier)
+ console.log('DEBUG: Current challenge:', currentChallenge)
+ console.log('DEBUG: Verifier length:', currentVerifier.length)
+ console.log('DEBUG: Challenge length:', currentChallenge.length)
+
+ console.log('=== END PKCE TEST ===')
+}
diff --git a/examples/chat-ui/src/utils/modelOptions.ts b/examples/chat-ui/src/utils/modelOptions.ts
new file mode 100644
index 0000000..26d275b
--- /dev/null
+++ b/examples/chat-ui/src/utils/modelOptions.ts
@@ -0,0 +1,35 @@
+import { type SupportedProvider } from '../types/models'
+import modelsData from '../data/models.json'
+
+// Helper function to check if a model supports reasoning
+export function supportsReasoning(providerId: SupportedProvider, modelId: string): boolean {
+ const providerModels = modelsData[providerId] as Record
+ if (!providerModels) return false
+
+ const model = providerModels[modelId]
+ return model?.reasoning === true
+}
+
+// Helper function to get provider-specific options
+export function getProviderOptions(providerId: SupportedProvider, modelId: string): Record | undefined {
+ // Add any provider-specific options here
+ switch (providerId) {
+ case 'groq':
+ // Handle specific Groq model options - set reasoningFormat for all reasoning models
+ if (supportsReasoning(providerId, modelId)) {
+ return {
+ groq: {
+ reasoningFormat: 'parsed',
+ },
+ }
+ }
+ break
+ case 'anthropic':
+ // Handle specific Anthropic model options
+ break
+ case 'openrouter':
+ // Handle specific OpenRouter model options
+ break
+ }
+ return undefined
+}
diff --git a/examples/chat-ui/src/utils/modelPreferences.ts b/examples/chat-ui/src/utils/modelPreferences.ts
index 13bb3c4..0a77658 100644
--- a/examples/chat-ui/src/utils/modelPreferences.ts
+++ b/examples/chat-ui/src/utils/modelPreferences.ts
@@ -1,13 +1,42 @@
-import { availableModels, type Model } from '../types/models'
+import { type Model, providers } from '../types/models'
+
+// Import models data directly
+import modelsData from '../data/models.json'
const MODEL_PREFERENCE_KEY = 'aiChatTemplate_selectedModel'
+function getAvailableModels(): Model[] {
+ const models: Model[] = []
+ for (const [providerId, providerModels] of Object.entries(modelsData)) {
+ const provider = providers[providerId as keyof typeof providers]
+ if (!provider) continue
+
+ for (const [modelId, modelData] of Object.entries(providerModels)) {
+ const model: Model = {
+ id: `${providerId}:${modelId}`,
+ name: modelData.name,
+ provider,
+ modelId,
+ supportsTools: modelData.tool_call,
+ reasoning: modelData.reasoning,
+ attachment: modelData.attachment,
+ contextLimit: modelData.limit.context,
+ outputLimit: modelData.limit.output,
+ cost: modelData.cost,
+ }
+ models.push(model)
+ }
+ }
+ return models
+}
+
export const getSelectedModel = (): Model => {
const saved = localStorage.getItem(MODEL_PREFERENCE_KEY)
if (saved) {
try {
const parsed = JSON.parse(saved)
// Find the model by ID to ensure it still exists
+ const availableModels = getAvailableModels()
const model = availableModels.find((m) => m.id === parsed.id)
if (model) {
return model
@@ -17,6 +46,7 @@ export const getSelectedModel = (): Model => {
}
}
// Default to first available model
+ const availableModels = getAvailableModels()
return availableModels[0]
}
diff --git a/examples/chat-ui/tailwind.config.js b/examples/chat-ui/tailwind.config.js
index c16995f..cfa55a4 100644
--- a/examples/chat-ui/tailwind.config.js
+++ b/examples/chat-ui/tailwind.config.js
@@ -11,51 +11,6 @@ module.exports = {
border: '0',
backgroundColor: 'transparent',
},
- lineHeight: '1.5',
- h1: {
- marginTop: '1rem',
- marginBottom: '0.5rem',
- },
- h2: {
- marginTop: '1rem',
- marginBottom: '0.5rem',
- },
- h3: {
- marginTop: '0.75rem',
- marginBottom: '0.5rem',
- },
- h4: {
- marginTop: '0.75rem',
- marginBottom: '0.5rem',
- },
- h5: {
- marginTop: '0.75rem',
- marginBottom: '0.5rem',
- },
- h6: {
- marginTop: '0.75rem',
- marginBottom: '0.5rem',
- },
- p: {
- marginTop: '0.5rem',
- marginBottom: '0.5rem',
- },
- ul: {
- marginTop: '0.5rem',
- marginBottom: '0.5rem',
- },
- ol: {
- marginTop: '0.5rem',
- marginBottom: '0.5rem',
- },
- li: {
- marginTop: '0.25rem',
- marginBottom: '0.25rem',
- },
- blockquote: {
- marginTop: '0.5rem',
- marginBottom: '0.5rem',
- },
},
},
},
diff --git a/examples/example-server/vengabus/src/index.ts b/examples/example-server/vengabus/src/index.ts
deleted file mode 100644
index 5fc2067..0000000
--- a/examples/example-server/vengabus/src/index.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
-import { StreamableHTTPTransport } from '@hono/mcp'
-import { Hono } from 'hono'
-import { z } from 'zod'
-import { cors } from 'hono/cors'
-
-const app = new Hono()
-
-app.use(
- '/*',
- cors({
- origin: '*',
- allowHeaders: ['content-type', 'mcp-session-id', 'mcp-protocol-version'],
- exposeHeaders: ['mcp-session-id'],
- }),
-)
-
-// Your MCP server implementation
-const mcpServer = new McpServer({
- name: 'my-mcp-server',
- version: '1.0.0',
-})
-
-mcpServer.registerTool(
- 'add',
- {
- title: 'Addition Tool',
- description: 'Add two numbers',
- inputSchema: { a: z.number(), b: z.number() },
- },
- async ({ a, b }) => ({
- content: [{ type: 'text', text: String(a + b) }],
- }),
-)
-
-mcpServer.registerTool(
- 'get_vengabus_times',
- {
- title: 'Vengabus Schedule Checker',
- description:
- "This checks to see when the next Vengabus is, or whether there is one for the user's current location. Please display all the information to the user in a tabulated form.",
- inputSchema: {},
- },
- async () => {
- await scheduler.wait(100 + Math.random() * 100)
- return {
- content: [
- {
- type: 'text',
- text: `Next Vengabus: imminent. Current user's route: New York to San Francisco. Route name: "Intercity Disco".`,
- },
- ],
- }
- },
-)
-
-app.post('/mcp', async (c) => {
- const transport = new StreamableHTTPTransport()
- await mcpServer.connect(transport)
- return transport.handleRequest(c)
-})
-
-export default app
diff --git a/examples/inspector/README.md b/examples/inspector/README.md
index d137ed3..b29c779 100644
--- a/examples/inspector/README.md
+++ b/examples/inspector/README.md
@@ -6,6 +6,8 @@ A minimal demo showcasing the `use-mcp` React hook for connecting to Model Conte
- Connect to any MCP server via URL
- View available tools and their schemas
+- Browse and read server resources
+- Interact with server-provided prompts
- Real-time connection status monitoring
- Debug logging for troubleshooting
- Clean, minimal UI focused on MCP functionality
@@ -31,7 +33,7 @@ pnpm dev
3. Open your browser and navigate to the displayed local URL
-4. Enter an MCP server URL to test the connection and explore available tools
+4. Enter an MCP server URL to test the connection and explore available tools, resources, and prompts
## What This Demonstrates
@@ -46,7 +48,17 @@ const connection = useMcp({
autoRetry: false
})
-// Access connection.state, connection.tools, connection.error, etc.
+// Access connection.state, connection.tools, connection.resources,
+// connection.prompts, connection.error, etc.
```
-The `McpServers` component wraps this hook to provide a complete UI for server management and tool inspection.
+The `McpServers` component wraps this hook to provide a complete UI for server management, tool inspection, resource browsing, and prompt interaction.
+
+## Supported MCP Features
+
+- **Tools**: Execute server-provided tools with custom arguments and view results
+- **Resources**: Browse available resources and read their contents (text or binary)
+- **Resource Templates**: View dynamic resource templates with URI patterns
+- **Prompts**: Interact with server prompts, provide arguments, and view generated messages
+
+Note: Not all MCP servers implement all features. The inspector will gracefully handle servers that only support a subset of the MCP specification.
diff --git a/examples/inspector/package.json b/examples/inspector/package.json
index 8cfad10..8048a1e 100644
--- a/examples/inspector/package.json
+++ b/examples/inspector/package.json
@@ -4,7 +4,7 @@
"version": "0.0.0",
"type": "module",
"scripts": {
- "dev": "vite",
+ "dev": "vite --port=5001",
"build": "tsc -b && pnpm run build:only",
"build:only": "vite build",
"lint": "eslint .",
@@ -17,8 +17,7 @@
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.6.2",
- "tailwindcss": "^4.0.14",
- "use-mcp": "link:../.."
+ "tailwindcss": "^4.0.14"
},
"devDependencies": {
"@cloudflare/vite-plugin": "^1.5.0",
@@ -34,6 +33,7 @@
"prettier": "^3.5.3",
"typescript": "~5.7.2",
"typescript-eslint": "^8.24.1",
+ "use-mcp": "link:../..",
"vite": "^6.2.0",
"wrangler": "^4.19.1"
},
diff --git a/examples/inspector/pnpm-lock.yaml b/examples/inspector/pnpm-lock.yaml
index 45feb59..c86ac98 100644
--- a/examples/inspector/pnpm-lock.yaml
+++ b/examples/inspector/pnpm-lock.yaml
@@ -26,9 +26,6 @@ importers:
tailwindcss:
specifier: ^4.0.14
version: 4.1.6
- use-mcp:
- specifier: link:../..
- version: link:../..
devDependencies:
'@cloudflare/vite-plugin':
specifier: ^1.5.0
@@ -69,6 +66,9 @@ importers:
typescript-eslint:
specifier: ^8.24.1
version: 8.32.0(eslint@9.26.0(jiti@2.4.2))(typescript@5.7.3)
+ use-mcp:
+ specifier: link:../..
+ version: link:../..
vite:
specifier: ^6.2.0
version: 6.3.5(jiti@2.4.2)(lightningcss@1.29.2)
diff --git a/examples/inspector/src/components/McpServers.tsx b/examples/inspector/src/components/McpServers.tsx
index 16780bd..7f397c6 100644
--- a/examples/inspector/src/components/McpServers.tsx
+++ b/examples/inspector/src/components/McpServers.tsx
@@ -1,21 +1,39 @@
import { useState, useRef, useEffect } from 'react'
-import { useMcp, type Tool } from 'use-mcp/react'
-import { Info, X, ChevronRight, ChevronDown } from 'lucide-react'
+import { useMcp, type Tool, type Resource, type ResourceTemplate, type Prompt } from 'use-mcp/react'
+import { Info, X, ChevronRight, ChevronDown, FileText, MessageSquare } from 'lucide-react'
// MCP Connection wrapper that only renders when active
-function McpConnection({ serverUrl, onConnectionUpdate }: { serverUrl: string; onConnectionUpdate: (data: any) => void }) {
+function McpConnection({
+ serverUrl,
+ transportType,
+ onConnectionUpdate,
+}: {
+ serverUrl: string
+ transportType: 'auto' | 'http' | 'sse'
+ onConnectionUpdate: (data: any) => void
+}) {
// Use the MCP hook with the server URL
const connection = useMcp({
url: serverUrl,
debug: true,
autoRetry: false,
popupFeatures: 'width=500,height=600,resizable=yes,scrollbars=yes',
+ transportType,
})
// Update parent component with connection data
useEffect(() => {
onConnectionUpdate(connection)
- }, [connection.state, connection.tools, connection.error, connection.log.length, connection.authUrl])
+ }, [
+ connection.state,
+ connection.tools,
+ connection.resources,
+ connection.resourceTemplates,
+ connection.prompts,
+ connection.error,
+ connection.log.length,
+ connection.authUrl,
+ ])
// Return null as this is just a hook wrapper
return null
@@ -25,11 +43,17 @@ export function McpServers({ onToolsUpdate }: { onToolsUpdate?: (tools: Tool[])
const [serverUrl, setServerUrl] = useState(() => {
return sessionStorage.getItem('mcpServerUrl') || ''
})
+ const [transportType, setTransportType] = useState<'auto' | 'http' | 'sse'>(() => {
+ return (sessionStorage.getItem('mcpTransportType') as 'auto' | 'http' | 'sse') || 'auto'
+ })
const [isActive, setIsActive] = useState(false)
const [connectionData, setConnectionData] = useState({
state: 'not-connected',
tools: [],
+ resources: [],
+ resourceTemplates: [],
+ prompts: [],
error: undefined,
log: [],
authUrl: undefined,
@@ -37,16 +61,26 @@ export function McpServers({ onToolsUpdate }: { onToolsUpdate?: (tools: Tool[])
disconnect: () => {},
authenticate: () => Promise.resolve(undefined),
callTool: (_name: string, _args?: Record) => Promise.resolve(undefined),
+ listResources: () => Promise.resolve(),
+ readResource: (_uri: string) => Promise.resolve({ contents: [] }),
+ listPrompts: () => Promise.resolve(),
+ getPrompt: (_name: string, _args?: Record) => Promise.resolve({ messages: [] }),
clearStorage: () => {},
})
const [toolForms, setToolForms] = useState>>({})
const [toolExecutionLogs, setToolExecutionLogs] = useState>({})
const [expandedTools, setExpandedTools] = useState>({})
+ const [expandedResources, setExpandedResources] = useState>({})
+ const [expandedPrompts, setExpandedPrompts] = useState>({})
+ const [resourceContents, setResourceContents] = useState>({})
+ const [promptResults, setPromptResults] = useState>({})
+ const [promptArgs, setPromptArgs] = useState>>({})
const logRef = useRef(null)
const executionLogRefs = useRef>({})
// Extract connection properties
- const { state, tools, log, authUrl, disconnect, authenticate } = connectionData
+ const { state, tools, resources, resourceTemplates, prompts, log, authUrl, disconnect, authenticate, readResource, getPrompt } =
+ connectionData
// Notify parent component when tools change
useEffect(() => {
@@ -73,6 +107,9 @@ export function McpServers({ onToolsUpdate }: { onToolsUpdate?: (tools: Tool[])
setConnectionData({
state: 'not-connected',
tools: [],
+ resources: [],
+ resourceTemplates: [],
+ prompts: [],
error: undefined,
log: [],
authUrl: undefined,
@@ -80,6 +117,10 @@ export function McpServers({ onToolsUpdate }: { onToolsUpdate?: (tools: Tool[])
disconnect: () => {},
authenticate: () => Promise.resolve(undefined),
callTool: (_name: string, _args?: Record) => Promise.resolve(undefined),
+ listResources: () => Promise.resolve(),
+ readResource: (_uri: string) => Promise.resolve({ contents: [] }),
+ listPrompts: () => Promise.resolve(),
+ getPrompt: (_name: string, _args?: Record) => Promise.resolve({ messages: [] }),
clearStorage: () => {},
})
}
@@ -292,6 +333,20 @@ export function McpServers({ onToolsUpdate }: { onToolsUpdate?: (tools: Tool[])
}}
disabled={isActive && state !== 'failed'}
/>
+ {
+ const newValue = e.target.value as 'auto' | 'http' | 'sse'
+ setTransportType(newValue)
+ sessionStorage.setItem('mcpTransportType', newValue)
+ }}
+ disabled={isActive && state !== 'failed'}
+ >
+ Auto
+ HTTP
+ SSE
+
{state === 'ready' || (isActive && state !== 'not-connected' && state !== 'failed') ? (
+ {/* Available Resources section */}
+
+
Available Resources ({resources.length + resourceTemplates.length})
+
+ {resources.length === 0 && resourceTemplates.length === 0 ? (
+
+ No resources available. Connect to an MCP server to see available resources.
+
+ ) : (
+
+ {/* Direct Resources */}
+ {resources.map((resource: Resource, index: number) => {
+ const isExpanded = expandedResources[resource.uri] || false
+ const contents = resourceContents[resource.uri]
+
+ return (
+
+
+
+
setExpandedResources((prev) => ({ ...prev, [resource.uri]: !isExpanded }))}
+ className="text-gray-500 hover:text-gray-700 mt-0.5"
+ >
+ {isExpanded ? : }
+
+
+
+
+
{resource.name}
+
+ {resource.description &&
{resource.description}
}
+
{resource.uri}
+ {resource.mimeType &&
({resource.mimeType}) }
+
+
+
+ {isExpanded && (
+
+
{
+ try {
+ const result = await readResource(resource.uri)
+ setResourceContents((prev) => ({ ...prev, [resource.uri]: result }))
+ } catch (error: any) {
+ setResourceContents((prev) => ({ ...prev, [resource.uri]: { error: error.message } }))
+ }
+ }}
+ className="px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white rounded text-xs font-medium"
+ >
+ Read Resource
+
+
+ {contents && (
+
+ {contents.error ? (
+
+ Error: {contents.error}
+
+ ) : (
+
+ {contents.contents?.map((content: any, idx: number) => (
+
+
+ {content.uri} {content.mimeType && `(${content.mimeType})`}
+
+
+ {content.text || (content.blob && '[Binary content]') || '[No content]'}
+
+
+ ))}
+
+ )}
+
+ )}
+
+ )}
+
+
+ )
+ })}
+
+ {/* Resource Templates */}
+ {resourceTemplates.map((template: ResourceTemplate, index: number) => (
+
+
+
+
+
{template.name}
+ Template
+
+ {template.description &&
{template.description}
}
+
{template.uriTemplate}
+ {template.mimeType &&
({template.mimeType}) }
+
+
+ ))}
+
+ )}
+
+
+ {/* Available Prompts section */}
+
+
Available Prompts ({prompts.length})
+
+ {prompts.length === 0 ? (
+
+ No prompts available. Connect to an MCP server to see available prompts.
+
+ ) : (
+
+ {prompts.map((prompt: Prompt, index: number) => {
+ const isExpanded = expandedPrompts[prompt.name] || false
+ const result = promptResults[prompt.name]
+ const args = promptArgs[prompt.name] || {}
+
+ return (
+
+
+
+
setExpandedPrompts((prev) => ({ ...prev, [prompt.name]: !isExpanded }))}
+ className="text-gray-500 hover:text-gray-700 mt-0.5"
+ >
+ {isExpanded ? : }
+
+
+
+
+
{prompt.name}
+
+ {prompt.description &&
{prompt.description}
}
+
+
+
+ {isExpanded && (
+
+ {/* Prompt Arguments */}
+ {prompt.arguments && prompt.arguments.length > 0 && (
+
+
Arguments:
+ {prompt.arguments.map((arg: any) => (
+
+
+ {arg.name}
+ {arg.required && * }
+ {arg.description && ({arg.description}) }
+
+
+ setPromptArgs((prev) => ({
+ ...prev,
+ [prompt.name]: {
+ ...prev[prompt.name],
+ [arg.name]: e.target.value,
+ },
+ }))
+ }
+ placeholder={arg.description || `Enter ${arg.name}`}
+ />
+
+ ))}
+
+ )}
+
+
{
+ try {
+ const result = await getPrompt(prompt.name, args)
+ setPromptResults((prev) => ({ ...prev, [prompt.name]: result }))
+ } catch (error: any) {
+ setPromptResults((prev) => ({ ...prev, [prompt.name]: { error: error.message } }))
+ }
+ }}
+ className="px-3 py-1 bg-purple-600 hover:bg-purple-700 text-white rounded text-xs font-medium"
+ >
+ Get Prompt
+
+
+ {result && (
+
+ {result.error ? (
+
+ Error: {result.error}
+
+ ) : (
+
+
Messages:
+ {result.messages?.map((message: any, idx: number) => (
+
+
{message.role}:
+
+ {message.content.text || JSON.stringify(message.content, null, 2)}
+
+
+ ))}
+
+ )}
+
+ )}
+
+ )}
+
+
+ )
+ })}
+
+ )}
+
+
{/* Debug Log */}
Debug Log
@@ -476,7 +743,7 @@ export function McpServers({ onToolsUpdate }: { onToolsUpdate?: (tools: Tool[])
{/* Only render the actual MCP connection when active */}
- {isActive && }
+ {isActive && }
)
}
diff --git a/examples/inspector/vite.config.ts b/examples/inspector/vite.config.ts
index c4069b7..beaea7c 100644
--- a/examples/inspector/vite.config.ts
+++ b/examples/inspector/vite.config.ts
@@ -5,4 +5,8 @@ import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss()],
+ build: {
+ // @ts-expect-error I don't want to install @types/node for this one line
+ minify: process.env.NO_MINIFY !== 'true',
+ },
})
diff --git a/examples/servers/cf-agents/.gitignore b/examples/servers/cf-agents/.gitignore
new file mode 100644
index 0000000..3b0fe33
--- /dev/null
+++ b/examples/servers/cf-agents/.gitignore
@@ -0,0 +1,172 @@
+# Logs
+
+logs
+_.log
+npm-debug.log_
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+
+report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
+
+# Runtime data
+
+pids
+_.pid
+_.seed
+\*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+
+lib-cov
+
+# Coverage directory used by tools like istanbul
+
+coverage
+\*.lcov
+
+# nyc test coverage
+
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+
+bower_components
+
+# node-waf configuration
+
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+
+build/Release
+
+# Dependency directories
+
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+
+web_modules/
+
+# TypeScript cache
+
+\*.tsbuildinfo
+
+# Optional npm cache directory
+
+.npm
+
+# Optional eslint cache
+
+.eslintcache
+
+# Optional stylelint cache
+
+.stylelintcache
+
+# Microbundle cache
+
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+
+.node_repl_history
+
+# Output of 'npm pack'
+
+\*.tgz
+
+# Yarn Integrity file
+
+.yarn-integrity
+
+# dotenv environment variable files
+
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# parcel-bundler cache (https://parceljs.org/)
+
+.cache
+.parcel-cache
+
+# Next.js build output
+
+.next
+out
+
+# Nuxt.js build / generate output
+
+.nuxt
+dist
+
+# Gatsby files
+
+.cache/
+
+# Comment in the public line in if your project uses Gatsby and not Next.js
+
+# https://nextjs.org/blog/next-9-1#public-directory-support
+
+# public
+
+# vuepress build output
+
+.vuepress/dist
+
+# vuepress v2.x temp and cache directory
+
+.temp
+.cache
+
+# Docusaurus cache and generated files
+
+.docusaurus
+
+# Serverless directories
+
+.serverless/
+
+# FuseBox cache
+
+.fusebox/
+
+# DynamoDB Local files
+
+.dynamodb/
+
+# TernJS port file
+
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+
+.vscode-test
+
+# yarn v2
+
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.\*
+
+# wrangler project
+
+.dev.vars
+.wrangler/
diff --git a/examples/servers/cf-agents/README.md b/examples/servers/cf-agents/README.md
new file mode 100644
index 0000000..0796838
--- /dev/null
+++ b/examples/servers/cf-agents/README.md
@@ -0,0 +1,50 @@
+# Building a Remote MCP Server on Cloudflare (Without Auth)
+
+This example allows you to deploy a remote MCP server that doesn't require authentication on Cloudflare Workers.
+
+## Get started:
+
+[](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/ai/tree/main/demos/remote-mcp-authless)
+
+This will deploy your MCP server to a URL like: `remote-mcp-server-authless..workers.dev/sse`
+
+Alternatively, you can use the command line below to get the remote MCP Server created on your local machine:
+```bash
+npm create cloudflare@latest -- my-mcp-server --template=cloudflare/ai/demos/remote-mcp-authless
+```
+
+## Customizing your MCP Server
+
+To add your own [tools](https://developers.cloudflare.com/agents/model-context-protocol/tools/) to the MCP server, define each tool inside the `init()` method of `src/index.ts` using `this.server.tool(...)`.
+
+## Connect to Cloudflare AI Playground
+
+You can connect to your MCP server from the Cloudflare AI Playground, which is a remote MCP client:
+
+1. Go to https://playground.ai.cloudflare.com/
+2. Enter your deployed MCP server URL (`remote-mcp-server-authless..workers.dev/sse`)
+3. You can now use your MCP tools directly from the playground!
+
+## Connect Claude Desktop to your MCP server
+
+You can also connect to your remote MCP server from local MCP clients, by using the [mcp-remote proxy](https://www.npmjs.com/package/mcp-remote).
+
+To connect to your MCP server from Claude Desktop, follow [Anthropic's Quickstart](https://modelcontextprotocol.io/quickstart/user) and within Claude Desktop go to Settings > Developer > Edit Config.
+
+Update with this configuration:
+
+```json
+{
+ "mcpServers": {
+ "calculator": {
+ "command": "npx",
+ "args": [
+ "mcp-remote",
+ "http://localhost:8787/sse" // or remote-mcp-server-authless.your-account.workers.dev/sse
+ ]
+ }
+ }
+}
+```
+
+Restart Claude and you should see the tools become available.
diff --git a/examples/servers/cf-agents/biome.json b/examples/servers/cf-agents/biome.json
new file mode 100644
index 0000000..825f1dd
--- /dev/null
+++ b/examples/servers/cf-agents/biome.json
@@ -0,0 +1,49 @@
+{
+ "$schema": "https://biomejs.dev/schemas/2.0.4/schema.json",
+ "assist": {
+ "actions": {
+ "source": {
+ "useSortedKeys": "on"
+ }
+ },
+ "enabled": true
+ },
+ "files": {
+ "includes": ["!worker-configuration.d.ts"]
+ },
+ "formatter": {
+ "enabled": true,
+ "indentWidth": 4,
+ "lineWidth": 100
+ },
+ "linter": {
+ "enabled": true,
+ "rules": {
+ "recommended": true,
+ "style": {
+ "noInferrableTypes": "error",
+ "noNonNullAssertion": "off",
+ "noParameterAssign": "error",
+ "noUnusedTemplateLiteral": "error",
+ "noUselessElse": "error",
+ "useAsConstAssertion": "error",
+ "useDefaultParameterLast": "error",
+ "useEnumInitializers": "error",
+ "useNumberNamespace": "error",
+ "useSelfClosingElements": "error",
+ "useSingleVarDeclarator": "error"
+ },
+ "suspicious": {
+ "noConfusingVoidType": "off",
+ "noDebugger": "off",
+ "noExplicitAny": "off"
+ }
+ }
+ },
+ "root": false,
+ "vcs": {
+ "clientKind": "git",
+ "enabled": true,
+ "useIgnoreFile": true
+ }
+}
diff --git a/examples/servers/cf-agents/package.json b/examples/servers/cf-agents/package.json
new file mode 100644
index 0000000..a9f909a
--- /dev/null
+++ b/examples/servers/cf-agents/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "remote-mcp-server-authless",
+ "version": "0.0.0",
+ "private": true,
+ "scripts": {
+ "deploy": "wrangler deploy",
+ "dev": "wrangler dev --port 5102",
+ "format": "biome format --write",
+ "lint:fix": "biome lint --fix",
+ "start": "wrangler dev",
+ "cf-typegen": "wrangler types",
+ "type-check": "tsc --noEmit"
+ },
+ "dependencies": {
+ "@modelcontextprotocol/sdk": "^1.13.3",
+ "zod": "^3.25.67"
+ },
+ "devDependencies": {
+ "@biomejs/biome": "^2.0.4",
+ "@cloudflare/workers-oauth-provider": "^0.0.5",
+ "@types/node": "^24.0.7",
+ "agents": "^0.0.100",
+ "hono": "^4.8.3",
+ "typescript": "^5.8.3",
+ "wrangler": "^4.23.0"
+ }
+}
diff --git a/examples/servers/cf-agents/pnpm-lock.yaml b/examples/servers/cf-agents/pnpm-lock.yaml
new file mode 100644
index 0000000..f0ff232
--- /dev/null
+++ b/examples/servers/cf-agents/pnpm-lock.yaml
@@ -0,0 +1,1979 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ '@modelcontextprotocol/sdk':
+ specifier: ^1.13.3
+ version: 1.13.3
+ zod:
+ specifier: ^3.25.67
+ version: 3.25.67
+ devDependencies:
+ '@biomejs/biome':
+ specifier: ^2.0.4
+ version: 2.0.6
+ '@cloudflare/workers-oauth-provider':
+ specifier: ^0.0.5
+ version: 0.0.5
+ '@types/node':
+ specifier: ^24.0.7
+ version: 24.0.7
+ agents:
+ specifier: ^0.0.100
+ version: 0.0.100(@cloudflare/workers-types@4.20250628.0)(react@19.1.0)
+ hono:
+ specifier: ^4.8.3
+ version: 4.8.3
+ typescript:
+ specifier: ^5.8.3
+ version: 5.8.3
+ wrangler:
+ specifier: ^4.23.0
+ version: 4.23.0(@cloudflare/workers-types@4.20250628.0)
+
+packages:
+
+ '@ai-sdk/provider-utils@2.2.8':
+ resolution: {integrity: sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ zod: ^3.23.8
+
+ '@ai-sdk/provider@1.1.3':
+ resolution: {integrity: sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==}
+ engines: {node: '>=18'}
+
+ '@ai-sdk/react@1.2.12':
+ resolution: {integrity: sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ react: ^18 || ^19 || ^19.0.0-rc
+ zod: ^3.23.8
+ peerDependenciesMeta:
+ zod:
+ optional: true
+
+ '@ai-sdk/ui-utils@1.2.11':
+ resolution: {integrity: sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ zod: ^3.23.8
+
+ '@biomejs/biome@2.0.6':
+ resolution: {integrity: sha512-RRP+9cdh5qwe2t0gORwXaa27oTOiQRQvrFf49x2PA1tnpsyU7FIHX4ZOFMtBC4QNtyWsN7Dqkf5EDbg4X+9iqA==}
+ engines: {node: '>=14.21.3'}
+ hasBin: true
+
+ '@biomejs/cli-darwin-arm64@2.0.6':
+ resolution: {integrity: sha512-AzdiNNjNzsE6LfqWyBvcL29uWoIuZUkndu+wwlXW13EKcBHbbKjNQEZIJKYDc6IL+p7bmWGx3v9ZtcRyIoIz5A==}
+ engines: {node: '>=14.21.3'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@biomejs/cli-darwin-x64@2.0.6':
+ resolution: {integrity: sha512-wJjjP4E7bO4WJmiQaLnsdXMa516dbtC6542qeRkyJg0MqMXP0fvs4gdsHhZ7p9XWTAmGIjZHFKXdsjBvKGIJJQ==}
+ engines: {node: '>=14.21.3'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@biomejs/cli-linux-arm64-musl@2.0.6':
+ resolution: {integrity: sha512-CVPEMlin3bW49sBqLBg2x016Pws7eUXA27XYDFlEtponD0luYjg2zQaMJ2nOqlkKG9fqzzkamdYxHdMDc2gZFw==}
+ engines: {node: '>=14.21.3'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@biomejs/cli-linux-arm64@2.0.6':
+ resolution: {integrity: sha512-ZSVf6TYo5rNMUHIW1tww+rs/krol7U5A1Is/yzWyHVZguuB0lBnIodqyFuwCNqG9aJGyk7xIMS8HG0qGUPz0SA==}
+ engines: {node: '>=14.21.3'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@biomejs/cli-linux-x64-musl@2.0.6':
+ resolution: {integrity: sha512-mKHE/e954hR/hSnAcJSjkf4xGqZc/53Kh39HVW1EgO5iFi0JutTN07TSjEMg616julRtfSNJi0KNyxvc30Y4rQ==}
+ engines: {node: '>=14.21.3'}
+ cpu: [x64]
+ os: [linux]
+
+ '@biomejs/cli-linux-x64@2.0.6':
+ resolution: {integrity: sha512-geM1MkHTV1Kh2Cs/Xzot9BOF3WBacihw6bkEmxkz4nSga8B9/hWy5BDiOG3gHDGIBa8WxT0nzsJs2f/hPqQIQw==}
+ engines: {node: '>=14.21.3'}
+ cpu: [x64]
+ os: [linux]
+
+ '@biomejs/cli-win32-arm64@2.0.6':
+ resolution: {integrity: sha512-290V4oSFoKaprKE1zkYVsDfAdn0An5DowZ+GIABgjoq1ndhvNxkJcpxPsiYtT7slbVe3xmlT0ncdfOsN7KruzA==}
+ engines: {node: '>=14.21.3'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@biomejs/cli-win32-x64@2.0.6':
+ resolution: {integrity: sha512-bfM1Bce0d69Ao7pjTjUS+AWSZ02+5UHdiAP85Th8e9yV5xzw6JrHXbL5YWlcEKQ84FIZMdDc7ncuti1wd2sdbw==}
+ engines: {node: '>=14.21.3'}
+ cpu: [x64]
+ os: [win32]
+
+ '@cloudflare/kv-asset-handler@0.4.0':
+ resolution: {integrity: sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==}
+ engines: {node: '>=18.0.0'}
+
+ '@cloudflare/unenv-preset@2.3.3':
+ resolution: {integrity: sha512-/M3MEcj3V2WHIRSW1eAQBPRJ6JnGQHc6JKMAPLkDb7pLs3m6X9ES/+K3ceGqxI6TKeF32AWAi7ls0AYzVxCP0A==}
+ peerDependencies:
+ unenv: 2.0.0-rc.17
+ workerd: ^1.20250508.0
+ peerDependenciesMeta:
+ workerd:
+ optional: true
+
+ '@cloudflare/workerd-darwin-64@1.20250617.0':
+ resolution: {integrity: sha512-toG8JUKVLIks4oOJLe9FeuixE84pDpMZ32ip7mCpE7JaFc5BqGFvevk0YC/db3T71AQlialjRwioH3jS/dzItA==}
+ engines: {node: '>=16'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@cloudflare/workerd-darwin-arm64@1.20250617.0':
+ resolution: {integrity: sha512-JTX0exbC9/ZtMmQQA8tDZEZFMXZrxOpTUj2hHnsUkErWYkr5SSZH04RBhPg6dU4VL8bXuB5/eJAh7+P9cZAp7g==}
+ engines: {node: '>=16'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@cloudflare/workerd-linux-64@1.20250617.0':
+ resolution: {integrity: sha512-8jkSoVRJ+1bOx3tuWlZCGaGCV2ew7/jFMl6V3CPXOoEtERUHsZBQLVkQIGKcmC/LKSj7f/mpyBUeu2EPTo2HEg==}
+ engines: {node: '>=16'}
+ cpu: [x64]
+ os: [linux]
+
+ '@cloudflare/workerd-linux-arm64@1.20250617.0':
+ resolution: {integrity: sha512-YAzcOyu897z5dQKFzme1oujGWMGEJCR7/Wrrm1nSP6dqutxFPTubRADM8BHn2CV3ij//vaPnAeLmZE3jVwOwig==}
+ engines: {node: '>=16'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@cloudflare/workerd-windows-64@1.20250617.0':
+ resolution: {integrity: sha512-XWM/6sagDrO0CYDKhXhPjM23qusvIN1ju9ZEml6gOQs8tNOFnq6Cn6X9FAmnyapRFCGUSEC3HZYJAm7zwVKaMA==}
+ engines: {node: '>=16'}
+ cpu: [x64]
+ os: [win32]
+
+ '@cloudflare/workers-oauth-provider@0.0.5':
+ resolution: {integrity: sha512-t1x5KAzsubCvb4APnJ93z407X1x7SGj/ga5ziRnwIb/iLy4PMkT/hgd1y5z7Bbsdy5Fy6mywhCP4lym24bX66w==}
+
+ '@cloudflare/workers-types@4.20250628.0':
+ resolution: {integrity: sha512-V4HthfhtQU2vTpwLeUic8FTLgGSjglsGZMJc9jKBNYEU/k0A1rE55UgQoTb5blKQdGtpQKfVKs3FROeY/lXmbw==}
+
+ '@cspotcode/source-map-support@0.8.1':
+ resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
+ engines: {node: '>=12'}
+
+ '@emnapi/runtime@1.4.3':
+ resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==}
+
+ '@esbuild/aix-ppc64@0.25.4':
+ resolution: {integrity: sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [aix]
+
+ '@esbuild/android-arm64@0.25.4':
+ resolution: {integrity: sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [android]
+
+ '@esbuild/android-arm@0.25.4':
+ resolution: {integrity: sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-x64@0.25.4':
+ resolution: {integrity: sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/darwin-arm64@0.25.4':
+ resolution: {integrity: sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.25.4':
+ resolution: {integrity: sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/freebsd-arm64@0.25.4':
+ resolution: {integrity: sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.25.4':
+ resolution: {integrity: sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/linux-arm64@0.25.4':
+ resolution: {integrity: sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@esbuild/linux-arm@0.25.4':
+ resolution: {integrity: sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [linux]
+
+ '@esbuild/linux-ia32@0.25.4':
+ resolution: {integrity: sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [linux]
+
+ '@esbuild/linux-loong64@0.25.4':
+ resolution: {integrity: sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==}
+ engines: {node: '>=18'}
+ cpu: [loong64]
+ os: [linux]
+
+ '@esbuild/linux-mips64el@0.25.4':
+ resolution: {integrity: sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==}
+ engines: {node: '>=18'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.25.4':
+ resolution: {integrity: sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.25.4':
+ resolution: {integrity: sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==}
+ engines: {node: '>=18'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@esbuild/linux-s390x@0.25.4':
+ resolution: {integrity: sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==}
+ engines: {node: '>=18'}
+ cpu: [s390x]
+ os: [linux]
+
+ '@esbuild/linux-x64@0.25.4':
+ resolution: {integrity: sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [linux]
+
+ '@esbuild/netbsd-arm64@0.25.4':
+ resolution: {integrity: sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [netbsd]
+
+ '@esbuild/netbsd-x64@0.25.4':
+ resolution: {integrity: sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [netbsd]
+
+ '@esbuild/openbsd-arm64@0.25.4':
+ resolution: {integrity: sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openbsd]
+
+ '@esbuild/openbsd-x64@0.25.4':
+ resolution: {integrity: sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@esbuild/sunos-x64@0.25.4':
+ resolution: {integrity: sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/win32-arm64@0.25.4':
+ resolution: {integrity: sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.25.4':
+ resolution: {integrity: sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@esbuild/win32-x64@0.25.4':
+ resolution: {integrity: sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [win32]
+
+ '@fastify/busboy@2.1.1':
+ resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==}
+ engines: {node: '>=14'}
+
+ '@img/sharp-darwin-arm64@0.33.5':
+ resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@img/sharp-darwin-x64@0.33.5':
+ resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@img/sharp-libvips-darwin-arm64@1.0.4':
+ resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@img/sharp-libvips-darwin-x64@1.0.4':
+ resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@img/sharp-libvips-linux-arm64@1.0.4':
+ resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@img/sharp-libvips-linux-arm@1.0.5':
+ resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==}
+ cpu: [arm]
+ os: [linux]
+
+ '@img/sharp-libvips-linux-s390x@1.0.4':
+ resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==}
+ cpu: [s390x]
+ os: [linux]
+
+ '@img/sharp-libvips-linux-x64@1.0.4':
+ resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==}
+ cpu: [x64]
+ os: [linux]
+
+ '@img/sharp-libvips-linuxmusl-arm64@1.0.4':
+ resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@img/sharp-libvips-linuxmusl-x64@1.0.4':
+ resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==}
+ cpu: [x64]
+ os: [linux]
+
+ '@img/sharp-linux-arm64@0.33.5':
+ resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@img/sharp-linux-arm@0.33.5':
+ resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@img/sharp-linux-s390x@0.33.5':
+ resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [s390x]
+ os: [linux]
+
+ '@img/sharp-linux-x64@0.33.5':
+ resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@img/sharp-linuxmusl-arm64@0.33.5':
+ resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@img/sharp-linuxmusl-x64@0.33.5':
+ resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@img/sharp-wasm32@0.33.5':
+ resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [wasm32]
+
+ '@img/sharp-win32-ia32@0.33.5':
+ resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [ia32]
+ os: [win32]
+
+ '@img/sharp-win32-x64@0.33.5':
+ resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [x64]
+ os: [win32]
+
+ '@jridgewell/resolve-uri@3.1.2':
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/sourcemap-codec@1.5.2':
+ resolution: {integrity: sha512-gKYheCylLIedI+CSZoDtGkFV9YEBxRRVcfCH7OfAqh4TyUyRjEE6WVE/aXDXX0p8BIe/QgLcaAoI0220KRRFgg==}
+
+ '@jridgewell/trace-mapping@0.3.9':
+ resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
+
+ '@modelcontextprotocol/sdk@1.13.3':
+ resolution: {integrity: sha512-bGwA78F/U5G2jrnsdRkPY3IwIwZeWUEfb5o764b79lb0rJmMT76TLwKhdNZOWakOQtedYefwIR4emisEMvInKA==}
+ engines: {node: '>=18'}
+
+ '@opentelemetry/api@1.9.0':
+ resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
+ engines: {node: '>=8.0.0'}
+
+ '@types/diff-match-patch@1.0.36':
+ resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==}
+
+ '@types/node@24.0.7':
+ resolution: {integrity: sha512-YIEUUr4yf8q8oQoXPpSlnvKNVKDQlPMWrmOcgzoduo7kvA2UF0/BwJ/eMKFTiTtkNL17I0M6Xe2tvwFU7be6iw==}
+
+ accepts@2.0.0:
+ resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
+ engines: {node: '>= 0.6'}
+
+ acorn-walk@8.3.2:
+ resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==}
+ engines: {node: '>=0.4.0'}
+
+ acorn@8.14.0:
+ resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
+ agents@0.0.100:
+ resolution: {integrity: sha512-xI/CJmMlK8Y3aEEXLu95kGC4wZXYTBTCS/XlguEjytSnAnCmNJtJQCnmmFi7oTAGXj753DslqQHjTSftfT/01g==}
+ peerDependencies:
+ react: '*'
+
+ ai@4.3.16:
+ resolution: {integrity: sha512-KUDwlThJ5tr2Vw0A1ZkbDKNME3wzWhuVfAOwIvFUzl1TPVDFAXDFTXio3p+jaKneB+dKNCvFFlolYmmgHttG1g==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ react: ^18 || ^19 || ^19.0.0-rc
+ zod: ^3.23.8
+ peerDependenciesMeta:
+ react:
+ optional: true
+
+ ajv@6.12.6:
+ resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
+
+ as-table@1.0.55:
+ resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==}
+
+ blake3-wasm@2.1.5:
+ resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==}
+
+ body-parser@2.2.0:
+ resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==}
+ engines: {node: '>=18'}
+
+ bytes@3.1.2:
+ resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
+ engines: {node: '>= 0.8'}
+
+ call-bind-apply-helpers@1.0.2:
+ resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
+ engines: {node: '>= 0.4'}
+
+ call-bound@1.0.4:
+ resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
+ engines: {node: '>= 0.4'}
+
+ chalk@5.4.1:
+ resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==}
+ engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
+
+ color-convert@2.0.1:
+ resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+ engines: {node: '>=7.0.0'}
+
+ color-name@1.1.4:
+ resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+
+ color-string@1.9.1:
+ resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
+
+ color@4.2.3:
+ resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
+ engines: {node: '>=12.5.0'}
+
+ content-disposition@1.0.0:
+ resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==}
+ engines: {node: '>= 0.6'}
+
+ content-type@1.0.5:
+ resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
+ engines: {node: '>= 0.6'}
+
+ cookie-signature@1.2.2:
+ resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
+ engines: {node: '>=6.6.0'}
+
+ cookie@0.7.2:
+ resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
+ engines: {node: '>= 0.6'}
+
+ cors@2.8.5:
+ resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
+ engines: {node: '>= 0.10'}
+
+ cron-schedule@5.0.4:
+ resolution: {integrity: sha512-nH0a49E/kSVk6BeFgKZy4uUsy6D2A16p120h5bYD9ILBhQu7o2sJFH+WI4R731TSBQ0dB1Ik7inB/dRAB4C8QQ==}
+ engines: {node: '>=18'}
+
+ cross-spawn@7.0.6:
+ resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
+ engines: {node: '>= 8'}
+
+ data-uri-to-buffer@2.0.2:
+ resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==}
+
+ debug@4.4.1:
+ resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ defu@6.1.4:
+ resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
+
+ depd@2.0.0:
+ resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
+ engines: {node: '>= 0.8'}
+
+ dequal@2.0.3:
+ resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
+ engines: {node: '>=6'}
+
+ detect-libc@2.0.4:
+ resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
+ engines: {node: '>=8'}
+
+ diff-match-patch@1.0.5:
+ resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==}
+
+ dunder-proto@1.0.1:
+ resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
+ engines: {node: '>= 0.4'}
+
+ ee-first@1.1.1:
+ resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
+
+ encodeurl@2.0.0:
+ resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
+ engines: {node: '>= 0.8'}
+
+ es-define-property@1.0.1:
+ resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
+ engines: {node: '>= 0.4'}
+
+ es-errors@1.3.0:
+ resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
+ engines: {node: '>= 0.4'}
+
+ es-object-atoms@1.1.1:
+ resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
+ engines: {node: '>= 0.4'}
+
+ esbuild@0.25.4:
+ resolution: {integrity: sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ escape-html@1.0.3:
+ resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
+
+ etag@1.8.1:
+ resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
+ engines: {node: '>= 0.6'}
+
+ event-target-polyfill@0.0.4:
+ resolution: {integrity: sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ==}
+
+ eventsource-parser@3.0.3:
+ resolution: {integrity: sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA==}
+ engines: {node: '>=20.0.0'}
+
+ eventsource@3.0.7:
+ resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==}
+ engines: {node: '>=18.0.0'}
+
+ exit-hook@2.2.1:
+ resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==}
+ engines: {node: '>=6'}
+
+ express-rate-limit@7.5.1:
+ resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==}
+ engines: {node: '>= 16'}
+ peerDependencies:
+ express: '>= 4.11'
+
+ express@5.1.0:
+ resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==}
+ engines: {node: '>= 18'}
+
+ exsolve@1.0.7:
+ resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==}
+
+ fast-deep-equal@3.1.3:
+ resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+
+ fast-json-stable-stringify@2.1.0:
+ resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
+
+ finalhandler@2.1.0:
+ resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==}
+ engines: {node: '>= 0.8'}
+
+ forwarded@0.2.0:
+ resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
+ engines: {node: '>= 0.6'}
+
+ fresh@2.0.0:
+ resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
+ engines: {node: '>= 0.8'}
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ function-bind@1.1.2:
+ resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
+
+ get-intrinsic@1.3.0:
+ resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
+ engines: {node: '>= 0.4'}
+
+ get-proto@1.0.1:
+ resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
+ engines: {node: '>= 0.4'}
+
+ get-source@2.0.12:
+ resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==}
+
+ glob-to-regexp@0.4.1:
+ resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
+
+ gopd@1.2.0:
+ resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
+ engines: {node: '>= 0.4'}
+
+ has-symbols@1.1.0:
+ resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
+ engines: {node: '>= 0.4'}
+
+ hasown@2.0.2:
+ resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
+ engines: {node: '>= 0.4'}
+
+ hono@4.8.3:
+ resolution: {integrity: sha512-jYZ6ZtfWjzBdh8H/0CIFfCBHaFL75k+KMzaM177hrWWm2TWL39YMYaJgB74uK/niRc866NMlH9B8uCvIo284WQ==}
+ engines: {node: '>=16.9.0'}
+
+ http-errors@2.0.0:
+ resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
+ engines: {node: '>= 0.8'}
+
+ iconv-lite@0.6.3:
+ resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
+ engines: {node: '>=0.10.0'}
+
+ inherits@2.0.4:
+ resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
+
+ ipaddr.js@1.9.1:
+ resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
+ engines: {node: '>= 0.10'}
+
+ is-arrayish@0.3.2:
+ resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
+
+ is-promise@4.0.0:
+ resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
+
+ isexe@2.0.0:
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+
+ json-schema-traverse@0.4.1:
+ resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
+
+ json-schema@0.4.0:
+ resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==}
+
+ jsondiffpatch@0.6.0:
+ resolution: {integrity: sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==}
+ engines: {node: ^18.0.0 || >=20.0.0}
+ hasBin: true
+
+ math-intrinsics@1.1.0:
+ resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
+ engines: {node: '>= 0.4'}
+
+ media-typer@1.1.0:
+ resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
+ engines: {node: '>= 0.8'}
+
+ merge-descriptors@2.0.0:
+ resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==}
+ engines: {node: '>=18'}
+
+ mime-db@1.54.0:
+ resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
+ engines: {node: '>= 0.6'}
+
+ mime-types@3.0.1:
+ resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==}
+ engines: {node: '>= 0.6'}
+
+ mime@3.0.0:
+ resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==}
+ engines: {node: '>=10.0.0'}
+ hasBin: true
+
+ miniflare@4.20250617.5:
+ resolution: {integrity: sha512-Qqn30jR6dCjXaKVizT6vH4KOb+GyLccoxLNOJEfu63yBPn8eoXa7PrdiSGTmjs2RY8/tr7eTO8Wu/Yr14k0xVA==}
+ engines: {node: '>=18.0.0'}
+ hasBin: true
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+ mustache@4.2.0:
+ resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==}
+ hasBin: true
+
+ nanoid@3.3.11:
+ resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ nanoid@5.1.5:
+ resolution: {integrity: sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==}
+ engines: {node: ^18 || >=20}
+ hasBin: true
+
+ negotiator@1.0.0:
+ resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
+ engines: {node: '>= 0.6'}
+
+ object-assign@4.1.1:
+ resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
+ engines: {node: '>=0.10.0'}
+
+ object-inspect@1.13.4:
+ resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
+ engines: {node: '>= 0.4'}
+
+ ohash@2.0.11:
+ resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==}
+
+ on-finished@2.4.1:
+ resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
+ engines: {node: '>= 0.8'}
+
+ once@1.4.0:
+ resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
+
+ parseurl@1.3.3:
+ resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
+ engines: {node: '>= 0.8'}
+
+ partyserver@0.0.72:
+ resolution: {integrity: sha512-mYkCQ6Q4KBIy4lFFuA6upmvNeD/FC+CQVTd4V3DYU6nsitKVI3NXxBrNNvmIxJLSwk3JQzYcEOPBkebB7ITVpQ==}
+ peerDependencies:
+ '@cloudflare/workers-types': ^4.20240729.0
+
+ partysocket@1.1.4:
+ resolution: {integrity: sha512-jXP7PFj2h5/v4UjDS8P7MZy6NJUQ7sspiFyxL4uc/+oKOL+KdtXzHnTV8INPGxBrLTXgalyG3kd12Qm7WrYc3A==}
+
+ path-key@3.1.1:
+ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
+ engines: {node: '>=8'}
+
+ path-to-regexp@6.3.0:
+ resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==}
+
+ path-to-regexp@8.2.0:
+ resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==}
+ engines: {node: '>=16'}
+
+ pathe@2.0.3:
+ resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
+
+ pkce-challenge@5.0.0:
+ resolution: {integrity: sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==}
+ engines: {node: '>=16.20.0'}
+
+ printable-characters@1.0.42:
+ resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==}
+
+ proxy-addr@2.0.7:
+ resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
+ engines: {node: '>= 0.10'}
+
+ punycode@2.3.1:
+ resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
+ engines: {node: '>=6'}
+
+ qs@6.14.0:
+ resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==}
+ engines: {node: '>=0.6'}
+
+ range-parser@1.2.1:
+ resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
+ engines: {node: '>= 0.6'}
+
+ raw-body@3.0.0:
+ resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==}
+ engines: {node: '>= 0.8'}
+
+ react@19.1.0:
+ resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
+ engines: {node: '>=0.10.0'}
+
+ router@2.2.0:
+ resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
+ engines: {node: '>= 18'}
+
+ safe-buffer@5.2.1:
+ resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
+
+ safer-buffer@2.1.2:
+ resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
+
+ secure-json-parse@2.7.0:
+ resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==}
+
+ semver@7.7.2:
+ resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
+ send@1.2.0:
+ resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==}
+ engines: {node: '>= 18'}
+
+ serve-static@2.2.0:
+ resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==}
+ engines: {node: '>= 18'}
+
+ setprototypeof@1.2.0:
+ resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
+
+ sharp@0.33.5:
+ resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+
+ shebang-command@2.0.0:
+ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
+ engines: {node: '>=8'}
+
+ shebang-regex@3.0.0:
+ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
+ engines: {node: '>=8'}
+
+ side-channel-list@1.0.0:
+ resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
+ engines: {node: '>= 0.4'}
+
+ side-channel-map@1.0.1:
+ resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==}
+ engines: {node: '>= 0.4'}
+
+ side-channel-weakmap@1.0.2:
+ resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
+ engines: {node: '>= 0.4'}
+
+ side-channel@1.1.0:
+ resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
+ engines: {node: '>= 0.4'}
+
+ simple-swizzle@0.2.2:
+ resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
+
+ source-map@0.6.1:
+ resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
+ engines: {node: '>=0.10.0'}
+
+ stacktracey@2.1.8:
+ resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==}
+
+ statuses@2.0.1:
+ resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
+ engines: {node: '>= 0.8'}
+
+ statuses@2.0.2:
+ resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
+ engines: {node: '>= 0.8'}
+
+ stoppable@1.1.0:
+ resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==}
+ engines: {node: '>=4', npm: '>=6'}
+
+ swr@2.3.3:
+ resolution: {integrity: sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A==}
+ peerDependencies:
+ react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
+ throttleit@2.1.0:
+ resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==}
+ engines: {node: '>=18'}
+
+ toidentifier@1.0.1:
+ resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
+ engines: {node: '>=0.6'}
+
+ tslib@2.8.1:
+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
+ type-is@2.0.1:
+ resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
+ engines: {node: '>= 0.6'}
+
+ typescript@5.8.3:
+ resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ ufo@1.6.1:
+ resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
+
+ undici-types@7.8.0:
+ resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==}
+
+ undici@5.29.0:
+ resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==}
+ engines: {node: '>=14.0'}
+
+ unenv@2.0.0-rc.17:
+ resolution: {integrity: sha512-B06u0wXkEd+o5gOCMl/ZHl5cfpYbDZKAT+HWTL+Hws6jWu7dCiqBBXXXzMFcFVJb8D4ytAnYmxJA83uwOQRSsg==}
+
+ unpipe@1.0.0:
+ resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
+ engines: {node: '>= 0.8'}
+
+ uri-js@4.4.1:
+ resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+
+ use-sync-external-store@1.5.0:
+ resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
+ vary@1.1.2:
+ resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
+ engines: {node: '>= 0.8'}
+
+ which@2.0.2:
+ resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
+ engines: {node: '>= 8'}
+ hasBin: true
+
+ workerd@1.20250617.0:
+ resolution: {integrity: sha512-Uv6p0PYUHp/W/aWfUPLkZVAoAjapisM27JJlwcX9wCPTfCfnuegGOxFMvvlYpmNaX4YCwEdLCwuNn3xkpSkuZw==}
+ engines: {node: '>=16'}
+ hasBin: true
+
+ wrangler@4.23.0:
+ resolution: {integrity: sha512-JSeDt3IwA4TEmg/V3tRblImPjdxynBt9PUVO/acQJ83XGlMMSwswDKL1FuwvbFzgX6+JXc3GMHeu7r8AQIxw9w==}
+ engines: {node: '>=18.0.0'}
+ hasBin: true
+ peerDependencies:
+ '@cloudflare/workers-types': ^4.20250617.0
+ peerDependenciesMeta:
+ '@cloudflare/workers-types':
+ optional: true
+
+ wrappy@1.0.2:
+ resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
+
+ ws@8.18.0:
+ resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==}
+ engines: {node: '>=10.0.0'}
+ peerDependencies:
+ bufferutil: ^4.0.1
+ utf-8-validate: '>=5.0.2'
+ peerDependenciesMeta:
+ bufferutil:
+ optional: true
+ utf-8-validate:
+ optional: true
+
+ youch@3.3.4:
+ resolution: {integrity: sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==}
+
+ zod-to-json-schema@3.24.6:
+ resolution: {integrity: sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==}
+ peerDependencies:
+ zod: ^3.24.1
+
+ zod@3.22.3:
+ resolution: {integrity: sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==}
+
+ zod@3.25.67:
+ resolution: {integrity: sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==}
+
+snapshots:
+
+ '@ai-sdk/provider-utils@2.2.8(zod@3.25.67)':
+ dependencies:
+ '@ai-sdk/provider': 1.1.3
+ nanoid: 3.3.11
+ secure-json-parse: 2.7.0
+ zod: 3.25.67
+
+ '@ai-sdk/provider@1.1.3':
+ dependencies:
+ json-schema: 0.4.0
+
+ '@ai-sdk/react@1.2.12(react@19.1.0)(zod@3.25.67)':
+ dependencies:
+ '@ai-sdk/provider-utils': 2.2.8(zod@3.25.67)
+ '@ai-sdk/ui-utils': 1.2.11(zod@3.25.67)
+ react: 19.1.0
+ swr: 2.3.3(react@19.1.0)
+ throttleit: 2.1.0
+ optionalDependencies:
+ zod: 3.25.67
+
+ '@ai-sdk/ui-utils@1.2.11(zod@3.25.67)':
+ dependencies:
+ '@ai-sdk/provider': 1.1.3
+ '@ai-sdk/provider-utils': 2.2.8(zod@3.25.67)
+ zod: 3.25.67
+ zod-to-json-schema: 3.24.6(zod@3.25.67)
+
+ '@biomejs/biome@2.0.6':
+ optionalDependencies:
+ '@biomejs/cli-darwin-arm64': 2.0.6
+ '@biomejs/cli-darwin-x64': 2.0.6
+ '@biomejs/cli-linux-arm64': 2.0.6
+ '@biomejs/cli-linux-arm64-musl': 2.0.6
+ '@biomejs/cli-linux-x64': 2.0.6
+ '@biomejs/cli-linux-x64-musl': 2.0.6
+ '@biomejs/cli-win32-arm64': 2.0.6
+ '@biomejs/cli-win32-x64': 2.0.6
+
+ '@biomejs/cli-darwin-arm64@2.0.6':
+ optional: true
+
+ '@biomejs/cli-darwin-x64@2.0.6':
+ optional: true
+
+ '@biomejs/cli-linux-arm64-musl@2.0.6':
+ optional: true
+
+ '@biomejs/cli-linux-arm64@2.0.6':
+ optional: true
+
+ '@biomejs/cli-linux-x64-musl@2.0.6':
+ optional: true
+
+ '@biomejs/cli-linux-x64@2.0.6':
+ optional: true
+
+ '@biomejs/cli-win32-arm64@2.0.6':
+ optional: true
+
+ '@biomejs/cli-win32-x64@2.0.6':
+ optional: true
+
+ '@cloudflare/kv-asset-handler@0.4.0':
+ dependencies:
+ mime: 3.0.0
+
+ '@cloudflare/unenv-preset@2.3.3(unenv@2.0.0-rc.17)(workerd@1.20250617.0)':
+ dependencies:
+ unenv: 2.0.0-rc.17
+ optionalDependencies:
+ workerd: 1.20250617.0
+
+ '@cloudflare/workerd-darwin-64@1.20250617.0':
+ optional: true
+
+ '@cloudflare/workerd-darwin-arm64@1.20250617.0':
+ optional: true
+
+ '@cloudflare/workerd-linux-64@1.20250617.0':
+ optional: true
+
+ '@cloudflare/workerd-linux-arm64@1.20250617.0':
+ optional: true
+
+ '@cloudflare/workerd-windows-64@1.20250617.0':
+ optional: true
+
+ '@cloudflare/workers-oauth-provider@0.0.5':
+ dependencies:
+ '@cloudflare/workers-types': 4.20250628.0
+
+ '@cloudflare/workers-types@4.20250628.0': {}
+
+ '@cspotcode/source-map-support@0.8.1':
+ dependencies:
+ '@jridgewell/trace-mapping': 0.3.9
+
+ '@emnapi/runtime@1.4.3':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@esbuild/aix-ppc64@0.25.4':
+ optional: true
+
+ '@esbuild/android-arm64@0.25.4':
+ optional: true
+
+ '@esbuild/android-arm@0.25.4':
+ optional: true
+
+ '@esbuild/android-x64@0.25.4':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.25.4':
+ optional: true
+
+ '@esbuild/darwin-x64@0.25.4':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.25.4':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.25.4':
+ optional: true
+
+ '@esbuild/linux-arm64@0.25.4':
+ optional: true
+
+ '@esbuild/linux-arm@0.25.4':
+ optional: true
+
+ '@esbuild/linux-ia32@0.25.4':
+ optional: true
+
+ '@esbuild/linux-loong64@0.25.4':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.25.4':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.25.4':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.25.4':
+ optional: true
+
+ '@esbuild/linux-s390x@0.25.4':
+ optional: true
+
+ '@esbuild/linux-x64@0.25.4':
+ optional: true
+
+ '@esbuild/netbsd-arm64@0.25.4':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.25.4':
+ optional: true
+
+ '@esbuild/openbsd-arm64@0.25.4':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.25.4':
+ optional: true
+
+ '@esbuild/sunos-x64@0.25.4':
+ optional: true
+
+ '@esbuild/win32-arm64@0.25.4':
+ optional: true
+
+ '@esbuild/win32-ia32@0.25.4':
+ optional: true
+
+ '@esbuild/win32-x64@0.25.4':
+ optional: true
+
+ '@fastify/busboy@2.1.1': {}
+
+ '@img/sharp-darwin-arm64@0.33.5':
+ optionalDependencies:
+ '@img/sharp-libvips-darwin-arm64': 1.0.4
+ optional: true
+
+ '@img/sharp-darwin-x64@0.33.5':
+ optionalDependencies:
+ '@img/sharp-libvips-darwin-x64': 1.0.4
+ optional: true
+
+ '@img/sharp-libvips-darwin-arm64@1.0.4':
+ optional: true
+
+ '@img/sharp-libvips-darwin-x64@1.0.4':
+ optional: true
+
+ '@img/sharp-libvips-linux-arm64@1.0.4':
+ optional: true
+
+ '@img/sharp-libvips-linux-arm@1.0.5':
+ optional: true
+
+ '@img/sharp-libvips-linux-s390x@1.0.4':
+ optional: true
+
+ '@img/sharp-libvips-linux-x64@1.0.4':
+ optional: true
+
+ '@img/sharp-libvips-linuxmusl-arm64@1.0.4':
+ optional: true
+
+ '@img/sharp-libvips-linuxmusl-x64@1.0.4':
+ optional: true
+
+ '@img/sharp-linux-arm64@0.33.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-arm64': 1.0.4
+ optional: true
+
+ '@img/sharp-linux-arm@0.33.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-arm': 1.0.5
+ optional: true
+
+ '@img/sharp-linux-s390x@0.33.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-s390x': 1.0.4
+ optional: true
+
+ '@img/sharp-linux-x64@0.33.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-x64': 1.0.4
+ optional: true
+
+ '@img/sharp-linuxmusl-arm64@0.33.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linuxmusl-arm64': 1.0.4
+ optional: true
+
+ '@img/sharp-linuxmusl-x64@0.33.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linuxmusl-x64': 1.0.4
+ optional: true
+
+ '@img/sharp-wasm32@0.33.5':
+ dependencies:
+ '@emnapi/runtime': 1.4.3
+ optional: true
+
+ '@img/sharp-win32-ia32@0.33.5':
+ optional: true
+
+ '@img/sharp-win32-x64@0.33.5':
+ optional: true
+
+ '@jridgewell/resolve-uri@3.1.2': {}
+
+ '@jridgewell/sourcemap-codec@1.5.2': {}
+
+ '@jridgewell/trace-mapping@0.3.9':
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.5.2
+
+ '@modelcontextprotocol/sdk@1.13.3':
+ dependencies:
+ ajv: 6.12.6
+ content-type: 1.0.5
+ cors: 2.8.5
+ cross-spawn: 7.0.6
+ eventsource: 3.0.7
+ eventsource-parser: 3.0.3
+ express: 5.1.0
+ express-rate-limit: 7.5.1(express@5.1.0)
+ pkce-challenge: 5.0.0
+ raw-body: 3.0.0
+ zod: 3.25.67
+ zod-to-json-schema: 3.24.6(zod@3.25.67)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@opentelemetry/api@1.9.0': {}
+
+ '@types/diff-match-patch@1.0.36': {}
+
+ '@types/node@24.0.7':
+ dependencies:
+ undici-types: 7.8.0
+
+ accepts@2.0.0:
+ dependencies:
+ mime-types: 3.0.1
+ negotiator: 1.0.0
+
+ acorn-walk@8.3.2: {}
+
+ acorn@8.14.0: {}
+
+ agents@0.0.100(@cloudflare/workers-types@4.20250628.0)(react@19.1.0):
+ dependencies:
+ '@modelcontextprotocol/sdk': 1.13.3
+ ai: 4.3.16(react@19.1.0)(zod@3.25.67)
+ cron-schedule: 5.0.4
+ nanoid: 5.1.5
+ partyserver: 0.0.72(@cloudflare/workers-types@4.20250628.0)
+ partysocket: 1.1.4
+ react: 19.1.0
+ zod: 3.25.67
+ transitivePeerDependencies:
+ - '@cloudflare/workers-types'
+ - supports-color
+
+ ai@4.3.16(react@19.1.0)(zod@3.25.67):
+ dependencies:
+ '@ai-sdk/provider': 1.1.3
+ '@ai-sdk/provider-utils': 2.2.8(zod@3.25.67)
+ '@ai-sdk/react': 1.2.12(react@19.1.0)(zod@3.25.67)
+ '@ai-sdk/ui-utils': 1.2.11(zod@3.25.67)
+ '@opentelemetry/api': 1.9.0
+ jsondiffpatch: 0.6.0
+ zod: 3.25.67
+ optionalDependencies:
+ react: 19.1.0
+
+ ajv@6.12.6:
+ dependencies:
+ fast-deep-equal: 3.1.3
+ fast-json-stable-stringify: 2.1.0
+ json-schema-traverse: 0.4.1
+ uri-js: 4.4.1
+
+ as-table@1.0.55:
+ dependencies:
+ printable-characters: 1.0.42
+
+ blake3-wasm@2.1.5: {}
+
+ body-parser@2.2.0:
+ dependencies:
+ bytes: 3.1.2
+ content-type: 1.0.5
+ debug: 4.4.1
+ http-errors: 2.0.0
+ iconv-lite: 0.6.3
+ on-finished: 2.4.1
+ qs: 6.14.0
+ raw-body: 3.0.0
+ type-is: 2.0.1
+ transitivePeerDependencies:
+ - supports-color
+
+ bytes@3.1.2: {}
+
+ call-bind-apply-helpers@1.0.2:
+ dependencies:
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+
+ call-bound@1.0.4:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ get-intrinsic: 1.3.0
+
+ chalk@5.4.1: {}
+
+ color-convert@2.0.1:
+ dependencies:
+ color-name: 1.1.4
+
+ color-name@1.1.4: {}
+
+ color-string@1.9.1:
+ dependencies:
+ color-name: 1.1.4
+ simple-swizzle: 0.2.2
+
+ color@4.2.3:
+ dependencies:
+ color-convert: 2.0.1
+ color-string: 1.9.1
+
+ content-disposition@1.0.0:
+ dependencies:
+ safe-buffer: 5.2.1
+
+ content-type@1.0.5: {}
+
+ cookie-signature@1.2.2: {}
+
+ cookie@0.7.2: {}
+
+ cors@2.8.5:
+ dependencies:
+ object-assign: 4.1.1
+ vary: 1.1.2
+
+ cron-schedule@5.0.4: {}
+
+ cross-spawn@7.0.6:
+ dependencies:
+ path-key: 3.1.1
+ shebang-command: 2.0.0
+ which: 2.0.2
+
+ data-uri-to-buffer@2.0.2: {}
+
+ debug@4.4.1:
+ dependencies:
+ ms: 2.1.3
+
+ defu@6.1.4: {}
+
+ depd@2.0.0: {}
+
+ dequal@2.0.3: {}
+
+ detect-libc@2.0.4: {}
+
+ diff-match-patch@1.0.5: {}
+
+ dunder-proto@1.0.1:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-errors: 1.3.0
+ gopd: 1.2.0
+
+ ee-first@1.1.1: {}
+
+ encodeurl@2.0.0: {}
+
+ es-define-property@1.0.1: {}
+
+ es-errors@1.3.0: {}
+
+ es-object-atoms@1.1.1:
+ dependencies:
+ es-errors: 1.3.0
+
+ esbuild@0.25.4:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.25.4
+ '@esbuild/android-arm': 0.25.4
+ '@esbuild/android-arm64': 0.25.4
+ '@esbuild/android-x64': 0.25.4
+ '@esbuild/darwin-arm64': 0.25.4
+ '@esbuild/darwin-x64': 0.25.4
+ '@esbuild/freebsd-arm64': 0.25.4
+ '@esbuild/freebsd-x64': 0.25.4
+ '@esbuild/linux-arm': 0.25.4
+ '@esbuild/linux-arm64': 0.25.4
+ '@esbuild/linux-ia32': 0.25.4
+ '@esbuild/linux-loong64': 0.25.4
+ '@esbuild/linux-mips64el': 0.25.4
+ '@esbuild/linux-ppc64': 0.25.4
+ '@esbuild/linux-riscv64': 0.25.4
+ '@esbuild/linux-s390x': 0.25.4
+ '@esbuild/linux-x64': 0.25.4
+ '@esbuild/netbsd-arm64': 0.25.4
+ '@esbuild/netbsd-x64': 0.25.4
+ '@esbuild/openbsd-arm64': 0.25.4
+ '@esbuild/openbsd-x64': 0.25.4
+ '@esbuild/sunos-x64': 0.25.4
+ '@esbuild/win32-arm64': 0.25.4
+ '@esbuild/win32-ia32': 0.25.4
+ '@esbuild/win32-x64': 0.25.4
+
+ escape-html@1.0.3: {}
+
+ etag@1.8.1: {}
+
+ event-target-polyfill@0.0.4: {}
+
+ eventsource-parser@3.0.3: {}
+
+ eventsource@3.0.7:
+ dependencies:
+ eventsource-parser: 3.0.3
+
+ exit-hook@2.2.1: {}
+
+ express-rate-limit@7.5.1(express@5.1.0):
+ dependencies:
+ express: 5.1.0
+
+ express@5.1.0:
+ dependencies:
+ accepts: 2.0.0
+ body-parser: 2.2.0
+ content-disposition: 1.0.0
+ content-type: 1.0.5
+ cookie: 0.7.2
+ cookie-signature: 1.2.2
+ debug: 4.4.1
+ encodeurl: 2.0.0
+ escape-html: 1.0.3
+ etag: 1.8.1
+ finalhandler: 2.1.0
+ fresh: 2.0.0
+ http-errors: 2.0.0
+ merge-descriptors: 2.0.0
+ mime-types: 3.0.1
+ on-finished: 2.4.1
+ once: 1.4.0
+ parseurl: 1.3.3
+ proxy-addr: 2.0.7
+ qs: 6.14.0
+ range-parser: 1.2.1
+ router: 2.2.0
+ send: 1.2.0
+ serve-static: 2.2.0
+ statuses: 2.0.2
+ type-is: 2.0.1
+ vary: 1.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ exsolve@1.0.7: {}
+
+ fast-deep-equal@3.1.3: {}
+
+ fast-json-stable-stringify@2.1.0: {}
+
+ finalhandler@2.1.0:
+ dependencies:
+ debug: 4.4.1
+ encodeurl: 2.0.0
+ escape-html: 1.0.3
+ on-finished: 2.4.1
+ parseurl: 1.3.3
+ statuses: 2.0.2
+ transitivePeerDependencies:
+ - supports-color
+
+ forwarded@0.2.0: {}
+
+ fresh@2.0.0: {}
+
+ fsevents@2.3.3:
+ optional: true
+
+ function-bind@1.1.2: {}
+
+ get-intrinsic@1.3.0:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-define-property: 1.0.1
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+ function-bind: 1.1.2
+ get-proto: 1.0.1
+ gopd: 1.2.0
+ has-symbols: 1.1.0
+ hasown: 2.0.2
+ math-intrinsics: 1.1.0
+
+ get-proto@1.0.1:
+ dependencies:
+ dunder-proto: 1.0.1
+ es-object-atoms: 1.1.1
+
+ get-source@2.0.12:
+ dependencies:
+ data-uri-to-buffer: 2.0.2
+ source-map: 0.6.1
+
+ glob-to-regexp@0.4.1: {}
+
+ gopd@1.2.0: {}
+
+ has-symbols@1.1.0: {}
+
+ hasown@2.0.2:
+ dependencies:
+ function-bind: 1.1.2
+
+ hono@4.8.3: {}
+
+ http-errors@2.0.0:
+ dependencies:
+ depd: 2.0.0
+ inherits: 2.0.4
+ setprototypeof: 1.2.0
+ statuses: 2.0.1
+ toidentifier: 1.0.1
+
+ iconv-lite@0.6.3:
+ dependencies:
+ safer-buffer: 2.1.2
+
+ inherits@2.0.4: {}
+
+ ipaddr.js@1.9.1: {}
+
+ is-arrayish@0.3.2: {}
+
+ is-promise@4.0.0: {}
+
+ isexe@2.0.0: {}
+
+ json-schema-traverse@0.4.1: {}
+
+ json-schema@0.4.0: {}
+
+ jsondiffpatch@0.6.0:
+ dependencies:
+ '@types/diff-match-patch': 1.0.36
+ chalk: 5.4.1
+ diff-match-patch: 1.0.5
+
+ math-intrinsics@1.1.0: {}
+
+ media-typer@1.1.0: {}
+
+ merge-descriptors@2.0.0: {}
+
+ mime-db@1.54.0: {}
+
+ mime-types@3.0.1:
+ dependencies:
+ mime-db: 1.54.0
+
+ mime@3.0.0: {}
+
+ miniflare@4.20250617.5:
+ dependencies:
+ '@cspotcode/source-map-support': 0.8.1
+ acorn: 8.14.0
+ acorn-walk: 8.3.2
+ exit-hook: 2.2.1
+ glob-to-regexp: 0.4.1
+ sharp: 0.33.5
+ stoppable: 1.1.0
+ undici: 5.29.0
+ workerd: 1.20250617.0
+ ws: 8.18.0
+ youch: 3.3.4
+ zod: 3.22.3
+ transitivePeerDependencies:
+ - bufferutil
+ - utf-8-validate
+
+ ms@2.1.3: {}
+
+ mustache@4.2.0: {}
+
+ nanoid@3.3.11: {}
+
+ nanoid@5.1.5: {}
+
+ negotiator@1.0.0: {}
+
+ object-assign@4.1.1: {}
+
+ object-inspect@1.13.4: {}
+
+ ohash@2.0.11: {}
+
+ on-finished@2.4.1:
+ dependencies:
+ ee-first: 1.1.1
+
+ once@1.4.0:
+ dependencies:
+ wrappy: 1.0.2
+
+ parseurl@1.3.3: {}
+
+ partyserver@0.0.72(@cloudflare/workers-types@4.20250628.0):
+ dependencies:
+ '@cloudflare/workers-types': 4.20250628.0
+ nanoid: 5.1.5
+
+ partysocket@1.1.4:
+ dependencies:
+ event-target-polyfill: 0.0.4
+
+ path-key@3.1.1: {}
+
+ path-to-regexp@6.3.0: {}
+
+ path-to-regexp@8.2.0: {}
+
+ pathe@2.0.3: {}
+
+ pkce-challenge@5.0.0: {}
+
+ printable-characters@1.0.42: {}
+
+ proxy-addr@2.0.7:
+ dependencies:
+ forwarded: 0.2.0
+ ipaddr.js: 1.9.1
+
+ punycode@2.3.1: {}
+
+ qs@6.14.0:
+ dependencies:
+ side-channel: 1.1.0
+
+ range-parser@1.2.1: {}
+
+ raw-body@3.0.0:
+ dependencies:
+ bytes: 3.1.2
+ http-errors: 2.0.0
+ iconv-lite: 0.6.3
+ unpipe: 1.0.0
+
+ react@19.1.0: {}
+
+ router@2.2.0:
+ dependencies:
+ debug: 4.4.1
+ depd: 2.0.0
+ is-promise: 4.0.0
+ parseurl: 1.3.3
+ path-to-regexp: 8.2.0
+ transitivePeerDependencies:
+ - supports-color
+
+ safe-buffer@5.2.1: {}
+
+ safer-buffer@2.1.2: {}
+
+ secure-json-parse@2.7.0: {}
+
+ semver@7.7.2: {}
+
+ send@1.2.0:
+ dependencies:
+ debug: 4.4.1
+ encodeurl: 2.0.0
+ escape-html: 1.0.3
+ etag: 1.8.1
+ fresh: 2.0.0
+ http-errors: 2.0.0
+ mime-types: 3.0.1
+ ms: 2.1.3
+ on-finished: 2.4.1
+ range-parser: 1.2.1
+ statuses: 2.0.2
+ transitivePeerDependencies:
+ - supports-color
+
+ serve-static@2.2.0:
+ dependencies:
+ encodeurl: 2.0.0
+ escape-html: 1.0.3
+ parseurl: 1.3.3
+ send: 1.2.0
+ transitivePeerDependencies:
+ - supports-color
+
+ setprototypeof@1.2.0: {}
+
+ sharp@0.33.5:
+ dependencies:
+ color: 4.2.3
+ detect-libc: 2.0.4
+ semver: 7.7.2
+ optionalDependencies:
+ '@img/sharp-darwin-arm64': 0.33.5
+ '@img/sharp-darwin-x64': 0.33.5
+ '@img/sharp-libvips-darwin-arm64': 1.0.4
+ '@img/sharp-libvips-darwin-x64': 1.0.4
+ '@img/sharp-libvips-linux-arm': 1.0.5
+ '@img/sharp-libvips-linux-arm64': 1.0.4
+ '@img/sharp-libvips-linux-s390x': 1.0.4
+ '@img/sharp-libvips-linux-x64': 1.0.4
+ '@img/sharp-libvips-linuxmusl-arm64': 1.0.4
+ '@img/sharp-libvips-linuxmusl-x64': 1.0.4
+ '@img/sharp-linux-arm': 0.33.5
+ '@img/sharp-linux-arm64': 0.33.5
+ '@img/sharp-linux-s390x': 0.33.5
+ '@img/sharp-linux-x64': 0.33.5
+ '@img/sharp-linuxmusl-arm64': 0.33.5
+ '@img/sharp-linuxmusl-x64': 0.33.5
+ '@img/sharp-wasm32': 0.33.5
+ '@img/sharp-win32-ia32': 0.33.5
+ '@img/sharp-win32-x64': 0.33.5
+
+ shebang-command@2.0.0:
+ dependencies:
+ shebang-regex: 3.0.0
+
+ shebang-regex@3.0.0: {}
+
+ side-channel-list@1.0.0:
+ dependencies:
+ es-errors: 1.3.0
+ object-inspect: 1.13.4
+
+ side-channel-map@1.0.1:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ object-inspect: 1.13.4
+
+ side-channel-weakmap@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ object-inspect: 1.13.4
+ side-channel-map: 1.0.1
+
+ side-channel@1.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ object-inspect: 1.13.4
+ side-channel-list: 1.0.0
+ side-channel-map: 1.0.1
+ side-channel-weakmap: 1.0.2
+
+ simple-swizzle@0.2.2:
+ dependencies:
+ is-arrayish: 0.3.2
+
+ source-map@0.6.1: {}
+
+ stacktracey@2.1.8:
+ dependencies:
+ as-table: 1.0.55
+ get-source: 2.0.12
+
+ statuses@2.0.1: {}
+
+ statuses@2.0.2: {}
+
+ stoppable@1.1.0: {}
+
+ swr@2.3.3(react@19.1.0):
+ dependencies:
+ dequal: 2.0.3
+ react: 19.1.0
+ use-sync-external-store: 1.5.0(react@19.1.0)
+
+ throttleit@2.1.0: {}
+
+ toidentifier@1.0.1: {}
+
+ tslib@2.8.1:
+ optional: true
+
+ type-is@2.0.1:
+ dependencies:
+ content-type: 1.0.5
+ media-typer: 1.1.0
+ mime-types: 3.0.1
+
+ typescript@5.8.3: {}
+
+ ufo@1.6.1: {}
+
+ undici-types@7.8.0: {}
+
+ undici@5.29.0:
+ dependencies:
+ '@fastify/busboy': 2.1.1
+
+ unenv@2.0.0-rc.17:
+ dependencies:
+ defu: 6.1.4
+ exsolve: 1.0.7
+ ohash: 2.0.11
+ pathe: 2.0.3
+ ufo: 1.6.1
+
+ unpipe@1.0.0: {}
+
+ uri-js@4.4.1:
+ dependencies:
+ punycode: 2.3.1
+
+ use-sync-external-store@1.5.0(react@19.1.0):
+ dependencies:
+ react: 19.1.0
+
+ vary@1.1.2: {}
+
+ which@2.0.2:
+ dependencies:
+ isexe: 2.0.0
+
+ workerd@1.20250617.0:
+ optionalDependencies:
+ '@cloudflare/workerd-darwin-64': 1.20250617.0
+ '@cloudflare/workerd-darwin-arm64': 1.20250617.0
+ '@cloudflare/workerd-linux-64': 1.20250617.0
+ '@cloudflare/workerd-linux-arm64': 1.20250617.0
+ '@cloudflare/workerd-windows-64': 1.20250617.0
+
+ wrangler@4.23.0(@cloudflare/workers-types@4.20250628.0):
+ dependencies:
+ '@cloudflare/kv-asset-handler': 0.4.0
+ '@cloudflare/unenv-preset': 2.3.3(unenv@2.0.0-rc.17)(workerd@1.20250617.0)
+ blake3-wasm: 2.1.5
+ esbuild: 0.25.4
+ miniflare: 4.20250617.5
+ path-to-regexp: 6.3.0
+ unenv: 2.0.0-rc.17
+ workerd: 1.20250617.0
+ optionalDependencies:
+ '@cloudflare/workers-types': 4.20250628.0
+ fsevents: 2.3.3
+ transitivePeerDependencies:
+ - bufferutil
+ - utf-8-validate
+
+ wrappy@1.0.2: {}
+
+ ws@8.18.0: {}
+
+ youch@3.3.4:
+ dependencies:
+ cookie: 0.7.2
+ mustache: 4.2.0
+ stacktracey: 2.1.8
+
+ zod-to-json-schema@3.24.6(zod@3.25.67):
+ dependencies:
+ zod: 3.25.67
+
+ zod@3.22.3: {}
+
+ zod@3.25.67: {}
diff --git a/examples/servers/cf-agents/src/index.ts b/examples/servers/cf-agents/src/index.ts
new file mode 100644
index 0000000..81f43ae
--- /dev/null
+++ b/examples/servers/cf-agents/src/index.ts
@@ -0,0 +1,249 @@
+import { McpAgent } from 'agents/mcp'
+import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
+import { z } from 'zod'
+import OAuthProvider, { type OAuthHelpers } from '@cloudflare/workers-oauth-provider'
+import { Hono } from 'hono'
+
+// Define our MCP agent with tools
+export class MyMCP extends McpAgent {
+ server = new McpServer({
+ name: 'Authless Calculator',
+ version: '1.0.0',
+ })
+
+ async init() {
+ // Simple addition tool
+ this.server.tool('add', { a: z.number(), b: z.number() }, async ({ a, b }) => ({
+ content: [{ type: 'text', text: String(a + b) }],
+ }))
+
+ // Calculator tool with multiple operations
+ this.server.tool(
+ 'calculate',
+ {
+ operation: z.enum(['add', 'subtract', 'multiply', 'divide']),
+ a: z.number(),
+ b: z.number(),
+ },
+ async ({ operation, a, b }) => {
+ let result: number
+ switch (operation) {
+ case 'add':
+ result = a + b
+ break
+ case 'subtract':
+ result = a - b
+ break
+ case 'multiply':
+ result = a * b
+ break
+ case 'divide':
+ if (b === 0)
+ return {
+ content: [
+ {
+ type: 'text',
+ text: 'Error: Cannot divide by zero',
+ },
+ ],
+ }
+ result = a / b
+ break
+ }
+ return { content: [{ type: 'text', text: String(result) }] }
+ },
+ )
+
+ // Register sample resources using the agents framework API
+ this.server.resource('calc://history', 'Calculation History', async () => ({
+ contents: [
+ {
+ uri: 'calc://history',
+ mimeType: 'application/json',
+ text: JSON.stringify(
+ {
+ calculations: [
+ { operation: 'add', a: 5, b: 3, result: 8, timestamp: '2024-01-01T10:00:00Z' },
+ { operation: 'multiply', a: 4, b: 7, result: 28, timestamp: '2024-01-01T10:01:00Z' },
+ ],
+ },
+ null,
+ 2,
+ ),
+ },
+ ],
+ }))
+
+ this.server.resource('calc://settings', 'Calculator Settings', async () => ({
+ contents: [
+ {
+ uri: 'calc://settings',
+ mimeType: 'application/json',
+ text: JSON.stringify(
+ {
+ precision: 2,
+ allowNegative: true,
+ maxValue: 1000000,
+ },
+ null,
+ 2,
+ ),
+ },
+ ],
+ }))
+
+ // Register a dynamic resource
+ this.server.resource('calc://stats', 'Calculation Statistics', async () => ({
+ contents: [
+ {
+ uri: 'calc://stats',
+ mimeType: 'application/json',
+ text: JSON.stringify(
+ {
+ totalCalculations: Math.floor(Math.random() * 100),
+ lastUpdated: new Date().toISOString(),
+ },
+ null,
+ 2,
+ ),
+ },
+ ],
+ }))
+
+ // Register sample prompts using the agents framework API
+ this.server.prompt(
+ 'math_problem',
+ 'Generate a math problem',
+ {
+ difficulty: z.string(),
+ topic: z.string().optional(),
+ },
+ async ({ difficulty, topic }) => ({
+ messages: [
+ {
+ role: 'user',
+ content: {
+ type: 'text',
+ text: `Generate a ${difficulty} math problem${topic ? ` about ${topic}` : ''}`,
+ },
+ },
+ ],
+ }),
+ )
+
+ this.server.prompt('explain_calculation', 'Explain a calculation', { operation: z.string() }, async ({ operation }) => ({
+ messages: [
+ {
+ role: 'user',
+ content: {
+ type: 'text',
+ text: `Please explain how to perform ${operation} step by step with examples`,
+ },
+ },
+ ],
+ }))
+ }
+}
+
+export type Bindings = Env & {
+ OAUTH_PROVIDER: OAuthHelpers
+}
+
+const app = new Hono<{
+ Bindings: Bindings
+}>()
+
+app.get('/authorize', async (c) => {
+ const oauthReqInfo = await c.env.OAUTH_PROVIDER.parseAuthRequest(c.req.raw)
+ const url = new URL(c.req.url)
+
+ const approveHref = url.toString()
+ return c.html(/*html*/ `
+
+
+ Authorize access
+
+
+
Authorize access
+
Allow Unknown client to access your calculator?
+
+
+ Deny
+
+
User: example@dotcom.com
+
+ `)
+})
+
+app.post('/authorize', async (c) => {
+ const email = 'example@dotcom.com'
+ const formData = await c.req.formData()
+ const oauthReqInfoRaw = formData.get('oauthReqInfo')
+ if (!oauthReqInfoRaw || typeof oauthReqInfoRaw !== 'string') {
+ return c.text('Missing oauthReqInfo', 400)
+ }
+ let oauthReqInfo
+ try {
+ oauthReqInfo = JSON.parse(decodeURIComponent(oauthReqInfoRaw))
+ } catch (e) {
+ return c.text('Invalid oauthReqInfo', 400)
+ }
+ const { redirectTo } = await c.env.OAUTH_PROVIDER.completeAuthorization({
+ request: oauthReqInfo,
+ userId: email,
+ metadata: {
+ label: 'Test User',
+ },
+ scope: oauthReqInfo.scope,
+ props: {
+ userEmail: email,
+ },
+ })
+ return Response.redirect(redirectTo)
+})
+
+export default {
+ fetch(request: Request, env: Env, ctx: ExecutionContext) {
+ const url = new URL(request.url)
+
+ if (url.pathname === '/public/sse' || url.pathname === '/public/sse/message') {
+ return MyMCP.serveSSE('/public/sse').fetch(request, env, ctx)
+ }
+
+ if (url.pathname === '/public/mcp') {
+ return MyMCP.serve('/public/mcp').fetch(request, env, ctx)
+ }
+
+ return new OAuthProvider({
+ apiRoute: ['/sse', '/mcp'],
+ apiHandler: {
+ // @ts-ignore
+ fetch: (request, env, ctx) => {
+ const { pathname } = new URL(request.url)
+ if (pathname.startsWith('/sse')) return MyMCP.serveSSE('/sse').fetch(request as any, env, ctx)
+ if (pathname === '/mcp') return MyMCP.serve('/mcp').fetch(request as any, env, ctx)
+ return new Response('Not found', { status: 404 })
+ },
+ },
+ // @ts-ignore
+ defaultHandler: app,
+ authorizeEndpoint: '/authorize',
+ tokenEndpoint: '/token',
+ clientRegistrationEndpoint: '/register',
+ }).fetch(request, env, ctx)
+ },
+}
diff --git a/examples/servers/cf-agents/tsconfig.json b/examples/servers/cf-agents/tsconfig.json
new file mode 100644
index 0000000..3ab6553
--- /dev/null
+++ b/examples/servers/cf-agents/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "es2021",
+ "lib": ["es2021"],
+ "jsx": "react-jsx",
+ "module": "es2022",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "allowJs": true,
+ "checkJs": false,
+ "noEmit": true,
+ "isolatedModules": true,
+ "allowSyntheticDefaultImports": true,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "skipLibCheck": true,
+ "types": ["./worker-configuration.d.ts", "node"]
+ },
+ "include": ["worker-configuration.d.ts", "src/**/*.ts"]
+}
diff --git a/examples/servers/cf-agents/worker-configuration.d.ts b/examples/servers/cf-agents/worker-configuration.d.ts
new file mode 100644
index 0000000..3c82704
--- /dev/null
+++ b/examples/servers/cf-agents/worker-configuration.d.ts
@@ -0,0 +1,7944 @@
+/* eslint-disable */
+// Generated by Wrangler by running `wrangler types` (hash: 71a1247d25eb5ef8b66311103d5f9180)
+// Runtime types generated with workerd@1.20250617.0 2025-03-10 nodejs_compat
+declare namespace Cloudflare {
+ interface Env {
+ OAUTH_KV: KVNamespace
+ MCP_OBJECT: DurableObjectNamespace
+ }
+}
+interface Env extends Cloudflare.Env {}
+
+// Begin runtime types
+/*! *****************************************************************************
+Copyright (c) Cloudflare. All rights reserved.
+Copyright (c) Microsoft Corporation. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may not use
+this file except in compliance with the License. You may obtain a copy of the
+License at http://www.apache.org/licenses/LICENSE-2.0
+THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
+WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
+MERCHANTABLITY OR NON-INFRINGEMENT.
+See the Apache Version 2.0 License for specific language governing permissions
+and limitations under the License.
+***************************************************************************** */
+/* eslint-disable */
+// noinspection JSUnusedGlobalSymbols
+declare var onmessage: never
+/**
+ * An abnormal event (called an exception) which occurs as a result of calling a method or accessing a property of a web API.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException)
+ */
+declare class DOMException extends Error {
+ constructor(message?: string, name?: string)
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/message) */
+ readonly message: string
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/name) */
+ readonly name: string
+ /**
+ * @deprecated
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/code)
+ */
+ readonly code: number
+ static readonly INDEX_SIZE_ERR: number
+ static readonly DOMSTRING_SIZE_ERR: number
+ static readonly HIERARCHY_REQUEST_ERR: number
+ static readonly WRONG_DOCUMENT_ERR: number
+ static readonly INVALID_CHARACTER_ERR: number
+ static readonly NO_DATA_ALLOWED_ERR: number
+ static readonly NO_MODIFICATION_ALLOWED_ERR: number
+ static readonly NOT_FOUND_ERR: number
+ static readonly NOT_SUPPORTED_ERR: number
+ static readonly INUSE_ATTRIBUTE_ERR: number
+ static readonly INVALID_STATE_ERR: number
+ static readonly SYNTAX_ERR: number
+ static readonly INVALID_MODIFICATION_ERR: number
+ static readonly NAMESPACE_ERR: number
+ static readonly INVALID_ACCESS_ERR: number
+ static readonly VALIDATION_ERR: number
+ static readonly TYPE_MISMATCH_ERR: number
+ static readonly SECURITY_ERR: number
+ static readonly NETWORK_ERR: number
+ static readonly ABORT_ERR: number
+ static readonly URL_MISMATCH_ERR: number
+ static readonly QUOTA_EXCEEDED_ERR: number
+ static readonly TIMEOUT_ERR: number
+ static readonly INVALID_NODE_TYPE_ERR: number
+ static readonly DATA_CLONE_ERR: number
+ get stack(): any
+ set stack(value: any)
+}
+type WorkerGlobalScopeEventMap = {
+ fetch: FetchEvent
+ scheduled: ScheduledEvent
+ queue: QueueEvent
+ unhandledrejection: PromiseRejectionEvent
+ rejectionhandled: PromiseRejectionEvent
+}
+declare abstract class WorkerGlobalScope extends EventTarget {
+ EventTarget: typeof EventTarget
+}
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console) */
+interface Console {
+ 'assert'(condition?: boolean, ...data: any[]): void
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/clear_static) */
+ clear(): void
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/count_static) */
+ count(label?: string): void
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/countReset_static) */
+ countReset(label?: string): void
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/debug_static) */
+ debug(...data: any[]): void
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/dir_static) */
+ dir(item?: any, options?: any): void
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/dirxml_static) */
+ dirxml(...data: any[]): void
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/error_static) */
+ error(...data: any[]): void
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/group_static) */
+ group(...data: any[]): void
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/groupCollapsed_static) */
+ groupCollapsed(...data: any[]): void
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/groupEnd_static) */
+ groupEnd(): void
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/info_static) */
+ info(...data: any[]): void
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static) */
+ log(...data: any[]): void
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/table_static) */
+ table(tabularData?: any, properties?: string[]): void
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/time_static) */
+ time(label?: string): void
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/timeEnd_static) */
+ timeEnd(label?: string): void
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/timeLog_static) */
+ timeLog(label?: string, ...data: any[]): void
+ timeStamp(label?: string): void
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/trace_static) */
+ trace(...data: any[]): void
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/warn_static) */
+ warn(...data: any[]): void
+}
+declare const console: Console
+type BufferSource = ArrayBufferView | ArrayBuffer
+type TypedArray =
+ | Int8Array
+ | Uint8Array
+ | Uint8ClampedArray
+ | Int16Array
+ | Uint16Array
+ | Int32Array
+ | Uint32Array
+ | Float32Array
+ | Float64Array
+ | BigInt64Array
+ | BigUint64Array
+declare namespace WebAssembly {
+ class CompileError extends Error {
+ constructor(message?: string)
+ }
+ class RuntimeError extends Error {
+ constructor(message?: string)
+ }
+ type ValueType = 'anyfunc' | 'externref' | 'f32' | 'f64' | 'i32' | 'i64' | 'v128'
+ interface GlobalDescriptor {
+ value: ValueType
+ mutable?: boolean
+ }
+ class Global {
+ constructor(descriptor: GlobalDescriptor, value?: any)
+ value: any
+ valueOf(): any
+ }
+ type ImportValue = ExportValue | number
+ type ModuleImports = Record
+ type Imports = Record
+ type ExportValue = Function | Global | Memory | Table
+ type Exports = Record
+ class Instance {
+ constructor(module: Module, imports?: Imports)
+ readonly exports: Exports
+ }
+ interface MemoryDescriptor {
+ initial: number
+ maximum?: number
+ shared?: boolean
+ }
+ class Memory {
+ constructor(descriptor: MemoryDescriptor)
+ readonly buffer: ArrayBuffer
+ grow(delta: number): number
+ }
+ type ImportExportKind = 'function' | 'global' | 'memory' | 'table'
+ interface ModuleExportDescriptor {
+ kind: ImportExportKind
+ name: string
+ }
+ interface ModuleImportDescriptor {
+ kind: ImportExportKind
+ module: string
+ name: string
+ }
+ abstract class Module {
+ static customSections(module: Module, sectionName: string): ArrayBuffer[]
+ static exports(module: Module): ModuleExportDescriptor[]
+ static imports(module: Module): ModuleImportDescriptor[]
+ }
+ type TableKind = 'anyfunc' | 'externref'
+ interface TableDescriptor {
+ element: TableKind
+ initial: number
+ maximum?: number
+ }
+ class Table {
+ constructor(descriptor: TableDescriptor, value?: any)
+ readonly length: number
+ get(index: number): any
+ grow(delta: number, value?: any): number
+ set(index: number, value?: any): void
+ }
+ function instantiate(module: Module, imports?: Imports): Promise
+ function validate(bytes: BufferSource): boolean
+}
+/**
+ * This ServiceWorker API interface represents the global execution context of a service worker.
+ * Available only in secure contexts.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ServiceWorkerGlobalScope)
+ */
+interface ServiceWorkerGlobalScope extends WorkerGlobalScope {
+ DOMException: typeof DOMException
+ WorkerGlobalScope: typeof WorkerGlobalScope
+ btoa(data: string): string
+ atob(data: string): string
+ setTimeout(callback: (...args: any[]) => void, msDelay?: number): number
+ setTimeout(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number
+ clearTimeout(timeoutId: number | null): void
+ setInterval(callback: (...args: any[]) => void, msDelay?: number): number
+ setInterval(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number
+ clearInterval(timeoutId: number | null): void
+ queueMicrotask(task: Function): void
+ structuredClone(value: T, options?: StructuredSerializeOptions): T
+ reportError(error: any): void
+ fetch(input: RequestInfo | URL, init?: RequestInit): Promise
+ self: ServiceWorkerGlobalScope
+ crypto: Crypto
+ caches: CacheStorage
+ scheduler: Scheduler
+ performance: Performance
+ Cloudflare: Cloudflare
+ readonly origin: string
+ Event: typeof Event
+ ExtendableEvent: typeof ExtendableEvent
+ CustomEvent: typeof CustomEvent
+ PromiseRejectionEvent: typeof PromiseRejectionEvent
+ FetchEvent: typeof FetchEvent
+ TailEvent: typeof TailEvent
+ TraceEvent: typeof TailEvent
+ ScheduledEvent: typeof ScheduledEvent
+ MessageEvent: typeof MessageEvent
+ CloseEvent: typeof CloseEvent
+ ReadableStreamDefaultReader: typeof ReadableStreamDefaultReader
+ ReadableStreamBYOBReader: typeof ReadableStreamBYOBReader
+ ReadableStream: typeof ReadableStream
+ WritableStream: typeof WritableStream
+ WritableStreamDefaultWriter: typeof WritableStreamDefaultWriter
+ TransformStream: typeof TransformStream
+ ByteLengthQueuingStrategy: typeof ByteLengthQueuingStrategy
+ CountQueuingStrategy: typeof CountQueuingStrategy
+ ErrorEvent: typeof ErrorEvent
+ EventSource: typeof EventSource
+ ReadableStreamBYOBRequest: typeof ReadableStreamBYOBRequest
+ ReadableStreamDefaultController: typeof ReadableStreamDefaultController
+ ReadableByteStreamController: typeof ReadableByteStreamController
+ WritableStreamDefaultController: typeof WritableStreamDefaultController
+ TransformStreamDefaultController: typeof TransformStreamDefaultController
+ CompressionStream: typeof CompressionStream
+ DecompressionStream: typeof DecompressionStream
+ TextEncoderStream: typeof TextEncoderStream
+ TextDecoderStream: typeof TextDecoderStream
+ Headers: typeof Headers
+ Body: typeof Body
+ Request: typeof Request
+ Response: typeof Response
+ WebSocket: typeof WebSocket
+ WebSocketPair: typeof WebSocketPair
+ WebSocketRequestResponsePair: typeof WebSocketRequestResponsePair
+ AbortController: typeof AbortController
+ AbortSignal: typeof AbortSignal
+ TextDecoder: typeof TextDecoder
+ TextEncoder: typeof TextEncoder
+ navigator: Navigator
+ Navigator: typeof Navigator
+ URL: typeof URL
+ URLSearchParams: typeof URLSearchParams
+ URLPattern: typeof URLPattern
+ Blob: typeof Blob
+ File: typeof File
+ FormData: typeof FormData
+ Crypto: typeof Crypto
+ SubtleCrypto: typeof SubtleCrypto
+ CryptoKey: typeof CryptoKey
+ CacheStorage: typeof CacheStorage
+ Cache: typeof Cache
+ FixedLengthStream: typeof FixedLengthStream
+ IdentityTransformStream: typeof IdentityTransformStream
+ HTMLRewriter: typeof HTMLRewriter
+}
+declare function addEventListener(
+ type: Type,
+ handler: EventListenerOrEventListenerObject,
+ options?: EventTargetAddEventListenerOptions | boolean,
+): void
+declare function removeEventListener(
+ type: Type,
+ handler: EventListenerOrEventListenerObject,
+ options?: EventTargetEventListenerOptions | boolean,
+): void
+/**
+ * Dispatches a synthetic event event to target and returns true if either event's cancelable attribute value is false or its preventDefault() method was not invoked, and false otherwise.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/dispatchEvent)
+ */
+declare function dispatchEvent(event: WorkerGlobalScopeEventMap[keyof WorkerGlobalScopeEventMap]): boolean
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/btoa) */
+declare function btoa(data: string): string
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/atob) */
+declare function atob(data: string): string
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setTimeout) */
+declare function setTimeout(callback: (...args: any[]) => void, msDelay?: number): number
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setTimeout) */
+declare function setTimeout(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/clearTimeout) */
+declare function clearTimeout(timeoutId: number | null): void
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setInterval) */
+declare function setInterval(callback: (...args: any[]) => void, msDelay?: number): number
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setInterval) */
+declare function setInterval(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/clearInterval) */
+declare function clearInterval(timeoutId: number | null): void
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/queueMicrotask) */
+declare function queueMicrotask(task: Function): void
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/structuredClone) */
+declare function structuredClone(value: T, options?: StructuredSerializeOptions): T
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/reportError) */
+declare function reportError(error: any): void
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/fetch) */
+declare function fetch(input: RequestInfo | URL, init?: RequestInit): Promise
+declare const self: ServiceWorkerGlobalScope
+/**
+ * The Web Crypto API provides a set of low-level functions for common cryptographic tasks.
+ * The Workers runtime implements the full surface of this API, but with some differences in
+ * the [supported algorithms](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/#supported-algorithms)
+ * compared to those implemented in most browsers.
+ *
+ * [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/)
+ */
+declare const crypto: Crypto
+/**
+ * The Cache API allows fine grained control of reading and writing from the Cloudflare global network cache.
+ *
+ * [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/)
+ */
+declare const caches: CacheStorage
+declare const scheduler: Scheduler
+/**
+ * The Workers runtime supports a subset of the Performance API, used to measure timing and performance,
+ * as well as timing of subrequests and other operations.
+ *
+ * [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/)
+ */
+declare const performance: Performance
+declare const Cloudflare: Cloudflare
+declare const origin: string
+declare const navigator: Navigator
+interface TestController {}
+interface ExecutionContext {
+ waitUntil(promise: Promise): void
+ passThroughOnException(): void
+ props: any
+}
+type ExportedHandlerFetchHandler = (
+ request: Request>,
+ env: Env,
+ ctx: ExecutionContext,
+) => Response | Promise
+type ExportedHandlerTailHandler = (events: TraceItem[], env: Env, ctx: ExecutionContext) => void | Promise
+type ExportedHandlerTraceHandler = (traces: TraceItem[], env: Env, ctx: ExecutionContext) => void | Promise
+type ExportedHandlerTailStreamHandler = (
+ event: TailStream.TailEvent,
+ env: Env,
+ ctx: ExecutionContext,
+) => TailStream.TailEventHandlerType | Promise
+type ExportedHandlerScheduledHandler = (
+ controller: ScheduledController,
+ env: Env,
+ ctx: ExecutionContext,
+) => void | Promise
+type ExportedHandlerQueueHandler = (
+ batch: MessageBatch,
+ env: Env,
+ ctx: ExecutionContext,
+) => void | Promise
+type ExportedHandlerTestHandler = (controller: TestController, env: Env, ctx: ExecutionContext) => void | Promise
+interface ExportedHandler {
+ fetch?: ExportedHandlerFetchHandler
+ tail?: ExportedHandlerTailHandler
+ trace?: ExportedHandlerTraceHandler
+ tailStream?: ExportedHandlerTailStreamHandler
+ scheduled?: ExportedHandlerScheduledHandler
+ test?: ExportedHandlerTestHandler
+ email?: EmailExportedHandler
+ queue?: ExportedHandlerQueueHandler
+}
+interface StructuredSerializeOptions {
+ transfer?: any[]
+}
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/PromiseRejectionEvent) */
+declare abstract class PromiseRejectionEvent extends Event {
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/PromiseRejectionEvent/promise) */
+ readonly promise: Promise
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/PromiseRejectionEvent/reason) */
+ readonly reason: any
+}
+declare abstract class Navigator {
+ sendBeacon(
+ url: string,
+ body?: ReadableStream | string | (ArrayBuffer | ArrayBufferView) | Blob | FormData | URLSearchParams | URLSearchParams,
+ ): boolean
+ readonly userAgent: string
+ readonly hardwareConcurrency: number
+}
+/**
+ * The Workers runtime supports a subset of the Performance API, used to measure timing and performance,
+ * as well as timing of subrequests and other operations.
+ *
+ * [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/)
+ */
+interface Performance {
+ /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/#performancetimeorigin) */
+ readonly timeOrigin: number
+ /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/#performancenow) */
+ now(): number
+}
+interface AlarmInvocationInfo {
+ readonly isRetry: boolean
+ readonly retryCount: number
+}
+interface Cloudflare {
+ readonly compatibilityFlags: Record
+}
+interface DurableObject {
+ fetch(request: Request): Response | Promise
+ alarm?(alarmInfo?: AlarmInvocationInfo): void | Promise
+ webSocketMessage?(ws: WebSocket, message: string | ArrayBuffer): void | Promise
+ webSocketClose?(ws: WebSocket, code: number, reason: string, wasClean: boolean): void | Promise
+ webSocketError?(ws: WebSocket, error: unknown): void | Promise
+}
+type DurableObjectStub = Fetcher<
+ T,
+ 'alarm' | 'webSocketMessage' | 'webSocketClose' | 'webSocketError'
+> & {
+ readonly id: DurableObjectId
+ readonly name?: string
+}
+interface DurableObjectId {
+ toString(): string
+ equals(other: DurableObjectId): boolean
+ readonly name?: string
+}
+interface DurableObjectNamespace {
+ newUniqueId(options?: DurableObjectNamespaceNewUniqueIdOptions): DurableObjectId
+ idFromName(name: string): DurableObjectId
+ idFromString(id: string): DurableObjectId
+ get(id: DurableObjectId, options?: DurableObjectNamespaceGetDurableObjectOptions): DurableObjectStub
+ jurisdiction(jurisdiction: DurableObjectJurisdiction): DurableObjectNamespace
+}
+type DurableObjectJurisdiction = 'eu' | 'fedramp' | 'fedramp-high'
+interface DurableObjectNamespaceNewUniqueIdOptions {
+ jurisdiction?: DurableObjectJurisdiction
+}
+type DurableObjectLocationHint = 'wnam' | 'enam' | 'sam' | 'weur' | 'eeur' | 'apac' | 'oc' | 'afr' | 'me'
+interface DurableObjectNamespaceGetDurableObjectOptions {
+ locationHint?: DurableObjectLocationHint
+}
+interface DurableObjectState {
+ waitUntil(promise: Promise): void
+ readonly id: DurableObjectId
+ readonly storage: DurableObjectStorage
+ container?: Container
+ blockConcurrencyWhile(callback: () => Promise): Promise
+ acceptWebSocket(ws: WebSocket, tags?: string[]): void
+ getWebSockets(tag?: string): WebSocket[]
+ setWebSocketAutoResponse(maybeReqResp?: WebSocketRequestResponsePair): void
+ getWebSocketAutoResponse(): WebSocketRequestResponsePair | null
+ getWebSocketAutoResponseTimestamp(ws: WebSocket): Date | null
+ setHibernatableWebSocketEventTimeout(timeoutMs?: number): void
+ getHibernatableWebSocketEventTimeout(): number | null
+ getTags(ws: WebSocket): string[]
+ abort(reason?: string): void
+}
+interface DurableObjectTransaction {
+ get(key: string, options?: DurableObjectGetOptions): Promise
+ get(keys: string[], options?: DurableObjectGetOptions): Promise>
+ list(options?: DurableObjectListOptions): Promise>
+ put(key: string, value: T, options?: DurableObjectPutOptions): Promise
+ put(entries: Record, options?: DurableObjectPutOptions): Promise
+ delete(key: string, options?: DurableObjectPutOptions): Promise
+ delete(keys: string[], options?: DurableObjectPutOptions): Promise
+ rollback(): void
+ getAlarm(options?: DurableObjectGetAlarmOptions): Promise
+ setAlarm(scheduledTime: number | Date, options?: DurableObjectSetAlarmOptions): Promise
+ deleteAlarm(options?: DurableObjectSetAlarmOptions): Promise
+}
+interface DurableObjectStorage {
+ get(key: string, options?: DurableObjectGetOptions): Promise
+ get(keys: string[], options?: DurableObjectGetOptions): Promise>
+ list(options?: DurableObjectListOptions): Promise>
+ put(key: string, value: T, options?: DurableObjectPutOptions): Promise
+ put(entries: Record, options?: DurableObjectPutOptions): Promise
+ delete(key: string, options?: DurableObjectPutOptions): Promise
+ delete(keys: string[], options?: DurableObjectPutOptions): Promise
+ deleteAll(options?: DurableObjectPutOptions): Promise
+ transaction(closure: (txn: DurableObjectTransaction) => Promise): Promise
+ getAlarm(options?: DurableObjectGetAlarmOptions): Promise
+ setAlarm(scheduledTime: number | Date, options?: DurableObjectSetAlarmOptions): Promise
+ deleteAlarm(options?: DurableObjectSetAlarmOptions): Promise
+ sync(): Promise
+ sql: SqlStorage
+ transactionSync(closure: () => T): T
+ getCurrentBookmark(): Promise
+ getBookmarkForTime(timestamp: number | Date): Promise
+ onNextSessionRestoreBookmark(bookmark: string): Promise
+}
+interface DurableObjectListOptions {
+ start?: string
+ startAfter?: string
+ end?: string
+ prefix?: string
+ reverse?: boolean
+ limit?: number
+ allowConcurrency?: boolean
+ noCache?: boolean
+}
+interface DurableObjectGetOptions {
+ allowConcurrency?: boolean
+ noCache?: boolean
+}
+interface DurableObjectGetAlarmOptions {
+ allowConcurrency?: boolean
+}
+interface DurableObjectPutOptions {
+ allowConcurrency?: boolean
+ allowUnconfirmed?: boolean
+ noCache?: boolean
+}
+interface DurableObjectSetAlarmOptions {
+ allowConcurrency?: boolean
+ allowUnconfirmed?: boolean
+}
+declare class WebSocketRequestResponsePair {
+ constructor(request: string, response: string)
+ get request(): string
+ get response(): string
+}
+interface AnalyticsEngineDataset {
+ writeDataPoint(event?: AnalyticsEngineDataPoint): void
+}
+interface AnalyticsEngineDataPoint {
+ indexes?: ((ArrayBuffer | string) | null)[]
+ doubles?: number[]
+ blobs?: ((ArrayBuffer | string) | null)[]
+}
+/**
+ * An event which takes place in the DOM.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event)
+ */
+declare class Event {
+ constructor(type: string, init?: EventInit)
+ /**
+ * Returns the type of event, e.g. "click", "hashchange", or "submit".
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/type)
+ */
+ get type(): string
+ /**
+ * Returns the event's phase, which is one of NONE, CAPTURING_PHASE, AT_TARGET, and BUBBLING_PHASE.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/eventPhase)
+ */
+ get eventPhase(): number
+ /**
+ * Returns true or false depending on how event was initialized. True if event invokes listeners past a ShadowRoot node that is the root of its target, and false otherwise.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/composed)
+ */
+ get composed(): boolean
+ /**
+ * Returns true or false depending on how event was initialized. True if event goes through its target's ancestors in reverse tree order, and false otherwise.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/bubbles)
+ */
+ get bubbles(): boolean
+ /**
+ * Returns true or false depending on how event was initialized. Its return value does not always carry meaning, but true can indicate that part of the operation during which event was dispatched, can be canceled by invoking the preventDefault() method.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/cancelable)
+ */
+ get cancelable(): boolean
+ /**
+ * Returns true if preventDefault() was invoked successfully to indicate cancelation, and false otherwise.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/defaultPrevented)
+ */
+ get defaultPrevented(): boolean
+ /**
+ * @deprecated
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/returnValue)
+ */
+ get returnValue(): boolean
+ /**
+ * Returns the object whose event listener's callback is currently being invoked.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/currentTarget)
+ */
+ get currentTarget(): EventTarget | undefined
+ /**
+ * Returns the object to which event is dispatched (its target).
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/target)
+ */
+ get target(): EventTarget | undefined
+ /**
+ * @deprecated
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/srcElement)
+ */
+ get srcElement(): EventTarget | undefined
+ /**
+ * Returns the event's timestamp as the number of milliseconds measured relative to the time origin.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/timeStamp)
+ */
+ get timeStamp(): number
+ /**
+ * Returns true if event was dispatched by the user agent, and false otherwise.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/isTrusted)
+ */
+ get isTrusted(): boolean
+ /**
+ * @deprecated
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/cancelBubble)
+ */
+ get cancelBubble(): boolean
+ /**
+ * @deprecated
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/cancelBubble)
+ */
+ set cancelBubble(value: boolean)
+ /**
+ * Invoking this method prevents event from reaching any registered event listeners after the current one finishes running and, when dispatched in a tree, also prevents event from reaching any other objects.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/stopImmediatePropagation)
+ */
+ stopImmediatePropagation(): void
+ /**
+ * If invoked when the cancelable attribute value is true, and while executing a listener for the event with passive set to false, signals to the operation that caused event to be dispatched that it needs to be canceled.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/preventDefault)
+ */
+ preventDefault(): void
+ /**
+ * When dispatched in a tree, invoking this method prevents event from reaching any objects other than the current object.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/stopPropagation)
+ */
+ stopPropagation(): void
+ /**
+ * Returns the invocation target objects of event's path (objects on which listeners will be invoked), except for any nodes in shadow trees of which the shadow root's mode is "closed" that are not reachable from event's currentTarget.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/composedPath)
+ */
+ composedPath(): EventTarget[]
+ static readonly NONE: number
+ static readonly CAPTURING_PHASE: number
+ static readonly AT_TARGET: number
+ static readonly BUBBLING_PHASE: number
+}
+interface EventInit {
+ bubbles?: boolean
+ cancelable?: boolean
+ composed?: boolean
+}
+type EventListener = (event: EventType) => void
+interface EventListenerObject {
+ handleEvent(event: EventType): void
+}
+type EventListenerOrEventListenerObject = EventListener | EventListenerObject
+/**
+ * EventTarget is a DOM interface implemented by objects that can receive events and may have listeners for them.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget)
+ */
+declare class EventTarget = Record> {
+ constructor()
+ /**
+ * Appends an event listener for events whose type attribute value is type. The callback argument sets the callback that will be invoked when the event is dispatched.
+ *
+ * The options argument sets listener-specific options. For compatibility this can be a boolean, in which case the method behaves exactly as if the value was specified as options's capture.
+ *
+ * When set to true, options's capture prevents callback from being invoked when the event's eventPhase attribute value is BUBBLING_PHASE. When false (or not present), callback will not be invoked when event's eventPhase attribute value is CAPTURING_PHASE. Either way, callback will be invoked if event's eventPhase attribute value is AT_TARGET.
+ *
+ * When set to true, options's passive indicates that the callback will not cancel the event by invoking preventDefault(). This is used to enable performance optimizations described in ยง 2.8 Observing event listeners.
+ *
+ * When set to true, options's once indicates that the callback will only be invoked once after which the event listener will be removed.
+ *
+ * If an AbortSignal is passed for options's signal, then the event listener will be removed when signal is aborted.
+ *
+ * The event listener is appended to target's event listener list and is not appended if it has the same type, callback, and capture.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/addEventListener)
+ */
+ addEventListener(
+ type: Type,
+ handler: EventListenerOrEventListenerObject,
+ options?: EventTargetAddEventListenerOptions | boolean,
+ ): void
+ /**
+ * Removes the event listener in target's event listener list with the same type, callback, and options.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/removeEventListener)
+ */
+ removeEventListener(
+ type: Type,
+ handler: EventListenerOrEventListenerObject,
+ options?: EventTargetEventListenerOptions | boolean,
+ ): void
+ /**
+ * Dispatches a synthetic event event to target and returns true if either event's cancelable attribute value is false or its preventDefault() method was not invoked, and false otherwise.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/dispatchEvent)
+ */
+ dispatchEvent(event: EventMap[keyof EventMap]): boolean
+}
+interface EventTargetEventListenerOptions {
+ capture?: boolean
+}
+interface EventTargetAddEventListenerOptions {
+ capture?: boolean
+ passive?: boolean
+ once?: boolean
+ signal?: AbortSignal
+}
+interface EventTargetHandlerObject {
+ handleEvent: (event: Event) => any | undefined
+}
+/**
+ * A controller object that allows you to abort one or more DOM requests as and when desired.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortController)
+ */
+declare class AbortController {
+ constructor()
+ /**
+ * Returns the AbortSignal object associated with this object.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortController/signal)
+ */
+ get signal(): AbortSignal
+ /**
+ * Invoking this method will set this object's AbortSignal's aborted flag and signal to any observers that the associated activity is to be aborted.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortController/abort)
+ */
+ abort(reason?: any): void
+}
+/**
+ * A signal object that allows you to communicate with a DOM request (such as a Fetch) and abort it if required via an AbortController object.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal)
+ */
+declare abstract class AbortSignal extends EventTarget {
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/abort_static) */
+ static abort(reason?: any): AbortSignal
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/timeout_static) */
+ static timeout(delay: number): AbortSignal
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/any_static) */
+ static any(signals: AbortSignal[]): AbortSignal
+ /**
+ * Returns true if this AbortSignal's AbortController has signaled to abort, and false otherwise.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/aborted)
+ */
+ get aborted(): boolean
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/reason) */
+ get reason(): any
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/abort_event) */
+ get onabort(): any | null
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/abort_event) */
+ set onabort(value: any | null)
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/throwIfAborted) */
+ throwIfAborted(): void
+}
+interface Scheduler {
+ wait(delay: number, maybeOptions?: SchedulerWaitOptions): Promise
+}
+interface SchedulerWaitOptions {
+ signal?: AbortSignal
+}
+/**
+ * Extends the lifetime of the install and activate events dispatched on the global scope as part of the service worker lifecycle. This ensures that any functional events (like FetchEvent) are not dispatched until it upgrades database schemas and deletes the outdated cache entries.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ExtendableEvent)
+ */
+declare abstract class ExtendableEvent extends Event {
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ExtendableEvent/waitUntil) */
+ waitUntil(promise: Promise): void
+}
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/CustomEvent) */
+declare class CustomEvent extends Event {
+ constructor(type: string, init?: CustomEventCustomEventInit)
+ /**
+ * Returns any custom data event was created with. Typically used for synthetic events.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CustomEvent/detail)
+ */
+ get detail(): T
+}
+interface CustomEventCustomEventInit {
+ bubbles?: boolean
+ cancelable?: boolean
+ composed?: boolean
+ detail?: any
+}
+/**
+ * A file-like object of immutable, raw data. Blobs represent data that isn't necessarily in a JavaScript-native format. The File interface is based on Blob, inheriting blob functionality and expanding it to support files on the user's system.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob)
+ */
+declare class Blob {
+ constructor(type?: ((ArrayBuffer | ArrayBufferView) | string | Blob)[], options?: BlobOptions)
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/size) */
+ get size(): number
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/type) */
+ get type(): string
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/slice) */
+ slice(start?: number, end?: number, type?: string): Blob
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/arrayBuffer) */
+ arrayBuffer(): Promise
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/bytes) */
+ bytes(): Promise
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/text) */
+ text(): Promise
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/stream) */
+ stream(): ReadableStream
+}
+interface BlobOptions {
+ type?: string
+}
+/**
+ * Provides information about files and allows JavaScript in a web page to access their content.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/File)
+ */
+declare class File extends Blob {
+ constructor(bits: ((ArrayBuffer | ArrayBufferView) | string | Blob)[] | undefined, name: string, options?: FileOptions)
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/name) */
+ get name(): string
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/lastModified) */
+ get lastModified(): number
+}
+interface FileOptions {
+ type?: string
+ lastModified?: number
+}
+/**
+ * The Cache API allows fine grained control of reading and writing from the Cloudflare global network cache.
+ *
+ * [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/)
+ */
+declare abstract class CacheStorage {
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/CacheStorage/open) */
+ open(cacheName: string): Promise
+ readonly default: Cache
+}
+/**
+ * The Cache API allows fine grained control of reading and writing from the Cloudflare global network cache.
+ *
+ * [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/)
+ */
+declare abstract class Cache {
+ /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/#delete) */
+ delete(request: RequestInfo | URL, options?: CacheQueryOptions): Promise
+ /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/#match) */
+ match(request: RequestInfo | URL, options?: CacheQueryOptions): Promise
+ /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/#put) */
+ put(request: RequestInfo | URL, response: Response): Promise
+}
+interface CacheQueryOptions {
+ ignoreMethod?: boolean
+}
+/**
+ * The Web Crypto API provides a set of low-level functions for common cryptographic tasks.
+ * The Workers runtime implements the full surface of this API, but with some differences in
+ * the [supported algorithms](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/#supported-algorithms)
+ * compared to those implemented in most browsers.
+ *
+ * [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/)
+ */
+declare abstract class Crypto {
+ /**
+ * Available only in secure contexts.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Crypto/subtle)
+ */
+ get subtle(): SubtleCrypto
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Crypto/getRandomValues) */
+ getRandomValues(
+ buffer: T,
+ ): T
+ /**
+ * Available only in secure contexts.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Crypto/randomUUID)
+ */
+ randomUUID(): string
+ DigestStream: typeof DigestStream
+}
+/**
+ * This Web Crypto API interface provides a number of low-level cryptographic functions. It is accessed via the Crypto.subtle properties available in a window context (via Window.crypto).
+ * Available only in secure contexts.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto)
+ */
+declare abstract class SubtleCrypto {
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/encrypt) */
+ encrypt(algorithm: string | SubtleCryptoEncryptAlgorithm, key: CryptoKey, plainText: ArrayBuffer | ArrayBufferView): Promise
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/decrypt) */
+ decrypt(algorithm: string | SubtleCryptoEncryptAlgorithm, key: CryptoKey, cipherText: ArrayBuffer | ArrayBufferView): Promise
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/sign) */
+ sign(algorithm: string | SubtleCryptoSignAlgorithm, key: CryptoKey, data: ArrayBuffer | ArrayBufferView): Promise
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/verify) */
+ verify(
+ algorithm: string | SubtleCryptoSignAlgorithm,
+ key: CryptoKey,
+ signature: ArrayBuffer | ArrayBufferView,
+ data: ArrayBuffer | ArrayBufferView,
+ ): Promise
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/digest) */
+ digest(algorithm: string | SubtleCryptoHashAlgorithm, data: ArrayBuffer | ArrayBufferView): Promise
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/generateKey) */
+ generateKey(
+ algorithm: string | SubtleCryptoGenerateKeyAlgorithm,
+ extractable: boolean,
+ keyUsages: string[],
+ ): Promise
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/deriveKey) */
+ deriveKey(
+ algorithm: string | SubtleCryptoDeriveKeyAlgorithm,
+ baseKey: CryptoKey,
+ derivedKeyAlgorithm: string | SubtleCryptoImportKeyAlgorithm,
+ extractable: boolean,
+ keyUsages: string[],
+ ): Promise
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/deriveBits) */
+ deriveBits(algorithm: string | SubtleCryptoDeriveKeyAlgorithm, baseKey: CryptoKey, length?: number | null): Promise
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/importKey) */
+ importKey(
+ format: string,
+ keyData: (ArrayBuffer | ArrayBufferView) | JsonWebKey,
+ algorithm: string | SubtleCryptoImportKeyAlgorithm,
+ extractable: boolean,
+ keyUsages: string[],
+ ): Promise
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/exportKey) */
+ exportKey(format: string, key: CryptoKey): Promise
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/wrapKey) */
+ wrapKey(
+ format: string,
+ key: CryptoKey,
+ wrappingKey: CryptoKey,
+ wrapAlgorithm: string | SubtleCryptoEncryptAlgorithm,
+ ): Promise
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/unwrapKey) */
+ unwrapKey(
+ format: string,
+ wrappedKey: ArrayBuffer | ArrayBufferView,
+ unwrappingKey: CryptoKey,
+ unwrapAlgorithm: string | SubtleCryptoEncryptAlgorithm,
+ unwrappedKeyAlgorithm: string | SubtleCryptoImportKeyAlgorithm,
+ extractable: boolean,
+ keyUsages: string[],
+ ): Promise
+ timingSafeEqual(a: ArrayBuffer | ArrayBufferView, b: ArrayBuffer | ArrayBufferView): boolean
+}
+/**
+ * The CryptoKey dictionary of the Web Crypto API represents a cryptographic key.
+ * Available only in secure contexts.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey)
+ */
+declare abstract class CryptoKey {
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey/type) */
+ readonly type: string
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey/extractable) */
+ readonly extractable: boolean
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey/algorithm) */
+ readonly algorithm:
+ | CryptoKeyKeyAlgorithm
+ | CryptoKeyAesKeyAlgorithm
+ | CryptoKeyHmacKeyAlgorithm
+ | CryptoKeyRsaKeyAlgorithm
+ | CryptoKeyEllipticKeyAlgorithm
+ | CryptoKeyArbitraryKeyAlgorithm
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey/usages) */
+ readonly usages: string[]
+}
+interface CryptoKeyPair {
+ publicKey: CryptoKey
+ privateKey: CryptoKey
+}
+interface JsonWebKey {
+ kty: string
+ use?: string
+ key_ops?: string[]
+ alg?: string
+ ext?: boolean
+ crv?: string
+ x?: string
+ y?: string
+ d?: string
+ n?: string
+ e?: string
+ p?: string
+ q?: string
+ dp?: string
+ dq?: string
+ qi?: string
+ oth?: RsaOtherPrimesInfo[]
+ k?: string
+}
+interface RsaOtherPrimesInfo {
+ r?: string
+ d?: string
+ t?: string
+}
+interface SubtleCryptoDeriveKeyAlgorithm {
+ name: string
+ salt?: ArrayBuffer | ArrayBufferView
+ iterations?: number
+ hash?: string | SubtleCryptoHashAlgorithm
+ $public?: CryptoKey
+ info?: ArrayBuffer | ArrayBufferView
+}
+interface SubtleCryptoEncryptAlgorithm {
+ name: string
+ iv?: ArrayBuffer | ArrayBufferView
+ additionalData?: ArrayBuffer | ArrayBufferView
+ tagLength?: number
+ counter?: ArrayBuffer | ArrayBufferView
+ length?: number
+ label?: ArrayBuffer | ArrayBufferView
+}
+interface SubtleCryptoGenerateKeyAlgorithm {
+ name: string
+ hash?: string | SubtleCryptoHashAlgorithm
+ modulusLength?: number
+ publicExponent?: ArrayBuffer | ArrayBufferView
+ length?: number
+ namedCurve?: string
+}
+interface SubtleCryptoHashAlgorithm {
+ name: string
+}
+interface SubtleCryptoImportKeyAlgorithm {
+ name: string
+ hash?: string | SubtleCryptoHashAlgorithm
+ length?: number
+ namedCurve?: string
+ compressed?: boolean
+}
+interface SubtleCryptoSignAlgorithm {
+ name: string
+ hash?: string | SubtleCryptoHashAlgorithm
+ dataLength?: number
+ saltLength?: number
+}
+interface CryptoKeyKeyAlgorithm {
+ name: string
+}
+interface CryptoKeyAesKeyAlgorithm {
+ name: string
+ length: number
+}
+interface CryptoKeyHmacKeyAlgorithm {
+ name: string
+ hash: CryptoKeyKeyAlgorithm
+ length: number
+}
+interface CryptoKeyRsaKeyAlgorithm {
+ name: string
+ modulusLength: number
+ publicExponent: ArrayBuffer | ArrayBufferView
+ hash?: CryptoKeyKeyAlgorithm
+}
+interface CryptoKeyEllipticKeyAlgorithm {
+ name: string
+ namedCurve: string
+}
+interface CryptoKeyArbitraryKeyAlgorithm {
+ name: string
+ hash?: CryptoKeyKeyAlgorithm
+ namedCurve?: string
+ length?: number
+}
+declare class DigestStream extends WritableStream {
+ constructor(algorithm: string | SubtleCryptoHashAlgorithm)
+ readonly digest: Promise
+ get bytesWritten(): number | bigint
+}
+/**
+ * A decoder for a specific method, that is a specific character encoding, like utf-8, iso-8859-2, koi8, cp1261, gbk, etc.ย A decoder takes a stream of bytes as input and emits a stream of code points. For a more scalable, non-native library, see StringView โ a C-like representation of strings based on typed arrays.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder)
+ */
+declare class TextDecoder {
+ constructor(label?: string, options?: TextDecoderConstructorOptions)
+ /**
+ * Returns the result of running encoding's decoder. The method can be invoked zero or more times with options's stream set to true, and then once without options's stream (or set to false), to process a fragmented input. If the invocation without options's stream (or set to false) has no input, it's clearest to omit both arguments.
+ *
+ * ```
+ * var string = "", decoder = new TextDecoder(encoding), buffer;
+ * while(buffer = next_chunk()) {
+ * string += decoder.decode(buffer, {stream:true});
+ * }
+ * string += decoder.decode(); // end-of-queue
+ * ```
+ *
+ * If the error mode is "fatal" and encoding's decoder returns error, throws a TypeError.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder/decode)
+ */
+ decode(input?: ArrayBuffer | ArrayBufferView, options?: TextDecoderDecodeOptions): string
+ get encoding(): string
+ get fatal(): boolean
+ get ignoreBOM(): boolean
+}
+/**
+ * TextEncoder takes a stream of code points as input and emits a stream of bytes. For a more scalable, non-native library, see StringView โ a C-like representation of strings based on typed arrays.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoder)
+ */
+declare class TextEncoder {
+ constructor()
+ /**
+ * Returns the result of running UTF-8's encoder.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoder/encode)
+ */
+ encode(input?: string): Uint8Array
+ /**
+ * Runs the UTF-8 encoder on source, stores the result of that operation into destination, and returns the progress made as an object wherein read is the number of converted code units of source and written is the number of bytes modified in destination.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoder/encodeInto)
+ */
+ encodeInto(input: string, buffer: ArrayBuffer | ArrayBufferView): TextEncoderEncodeIntoResult
+ get encoding(): string
+}
+interface TextDecoderConstructorOptions {
+ fatal: boolean
+ ignoreBOM: boolean
+}
+interface TextDecoderDecodeOptions {
+ stream: boolean
+}
+interface TextEncoderEncodeIntoResult {
+ read: number
+ written: number
+}
+/**
+ * Events providing information related to errors in scripts or in files.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent)
+ */
+declare class ErrorEvent extends Event {
+ constructor(type: string, init?: ErrorEventErrorEventInit)
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/filename) */
+ get filename(): string
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/message) */
+ get message(): string
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/lineno) */
+ get lineno(): number
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/colno) */
+ get colno(): number
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/error) */
+ get error(): any
+}
+interface ErrorEventErrorEventInit {
+ message?: string
+ filename?: string
+ lineno?: number
+ colno?: number
+ error?: any
+}
+/**
+ * Provides a way to easily construct a set of key/value pairs representing form fields and their values, which can then be easily sent using the XMLHttpRequest.send() method. It uses the same format a form would use if the encoding type were set to "multipart/form-data".
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData)
+ */
+declare class FormData {
+ constructor()
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/append) */
+ append(name: string, value: string): void
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/append) */
+ append(name: string, value: Blob, filename?: string): void
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/delete) */
+ delete(name: string): void
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/get) */
+ get(name: string): (File | string) | null
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/getAll) */
+ getAll(name: string): (File | string)[]
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/has) */
+ has(name: string): boolean
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/set) */
+ set(name: string, value: string): void
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/set) */
+ set(name: string, value: Blob, filename?: string): void
+ /* Returns an array of key, value pairs for every entry in the list. */
+ entries(): IterableIterator<[key: string, value: File | string]>
+ /* Returns a list of keys in the list. */
+ keys(): IterableIterator
+ /* Returns a list of values in the list. */
+ values(): IterableIterator
+ forEach(callback: (this: This, value: File | string, key: string, parent: FormData) => void, thisArg?: This): void
+ [Symbol.iterator](): IterableIterator<[key: string, value: File | string]>
+}
+interface ContentOptions {
+ html?: boolean
+}
+declare class HTMLRewriter {
+ constructor()
+ on(selector: string, handlers: HTMLRewriterElementContentHandlers): HTMLRewriter
+ onDocument(handlers: HTMLRewriterDocumentContentHandlers): HTMLRewriter
+ transform(response: Response): Response
+}
+interface HTMLRewriterElementContentHandlers {
+ element?(element: Element): void | Promise
+ comments?(comment: Comment): void | Promise
+ text?(element: Text): void | Promise
+}
+interface HTMLRewriterDocumentContentHandlers {
+ doctype?(doctype: Doctype): void | Promise
+ comments?(comment: Comment): void | Promise
+ text?(text: Text): void | Promise