diff --git a/.gitignore b/.gitignore index b7ddfdbb..0cac6ad4 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,6 @@ src-tauri/target settings.local.json # husky -!.husky/ \ No newline at end of file +!.husky/ + +reference/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..6a28e032 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,49 @@ +# Repository Guidelines + +## Project Structure + +- `src/`: Vite + React frontend (TypeScript). + - `src/ui/`: UI components, themes, providers, and hooks. + - `src/core/`: business logic (models/providers, toolsets, importers). + - `src/types/`: shared types. +- `src-tauri/`: Tauri (Rust) backend, bundling, and native integrations. +- `script/`: local dev utilities (instance setup/run, validation, release). +- `public/` + `resources/`: static assets and Tauri resources/icons. +- `docs/` and `screenshots/`: documentation and marketing assets. +- `dist/`: generated build output (do not hand-edit). + +## Build, Test, and Development Commands + +Prereqs: Node `>=22`, `pnpm`, Rust/Cargo, and Git LFS (`git lfs install --force && git lfs pull`). + +- Install deps: `pnpm install` +- Set up an isolated instance: `pnpm run setup [instance-name]` +- Run the app (Tauri + Vite): `pnpm run dev [instance-name]` +- Web-only dev server: `pnpm run vite:dev` +- Build (typecheck + bundle): `pnpm run build` +- Lint/format/typecheck (matches CI): `pnpm run validate && pnpm tsc` +- Generate SQL docs after DB changes: `pnpm run generate-schema` (updates `SQL_SCHEMA.md`) + +## Coding Style & Naming Conventions + +- TypeScript is strict (`tsconfig.json`); keep types explicit and avoid unsafe casts. +- Prefer path aliases over deep relatives: `@ui/*`, `@core/*`, `@/*`. +- Formatting is Prettier-first (4-space `tabWidth`) with ESLint enforcement. +- Naming: `PascalCase` components, `useX` hooks, `camelCase` vars/functions. + +## Testing Guidelines + +- JS/TS tests use Vitest: `pnpm test`. +- Name tests `*.test.ts(x)` (or place under `src/tests/`) and keep snapshots committed. +- Rust tests (if added) run from `src-tauri/`: `cargo test`. + +## Security & Local Data + +- Dev instances store data in `~/Library/Application Support/sh.chorus.app.dev./` (e.g., `auth.dat`, `chats.db`); never commit or share these files. +- Keep API keys and provider credentials out of git and logs; use local configuration only. + +## Commit & Pull Request Guidelines + +- Follow the existing commit style: `feat: ...`, `fix: ...`, `chore: ...` (optionally `(#issue)`). +- PRs should explain the “why”, link issues, and include screenshots for UI changes. +- Before opening a PR, ensure `pnpm run validate` passes; run `pnpm test` when touching core logic. diff --git a/CLAUDE.md b/CLAUDE.md index 7246fcf9..3f5de61e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,142 +1,409 @@ -# Claude's Onboarding Doc +# CLAUDE.md -## What is Chorus? +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. -Chorus is a native Mac AI chat app that lets you chat with all the AIs. +## What is Chorus? -It lets you send one prompt and see responses from Claude, o3-pro, Gemini, etc. all at once. +Chorus is a native Mac AI chat app that lets you chat with multiple AI models simultaneously. -It's built with Tauri, React, TypeScript, TanStack Query, and a local sqlite database. +It's built with Tauri (Rust), React, TypeScript, TanStack Query, and SQLite. The app sends one prompt and displays responses from Claude, o3-pro, Gemini, and other models side by side. Key features: -- MCP support -- Ambient chats (start a chat from anywhere) -- Projects +- MCP (Model Context Protocol) support +- Ambient chats (quick chats accessible from anywhere) +- Projects (organized folders of chats) - Bring your own API keys +- Multi-model comparison + +Most functionality lives in this repo. A separate Elixir backend at app.chorus.sh handles accounts, billing, and request proxying. + +## Prerequisites + +Before development, ensure you have: + +- Node.js >= 22.0.0 +- pnpm (`brew install pnpm`) +- Rust and Cargo (`rustc --version` and `cargo --version` should work) +- Git LFS (`brew install git-lfs`) +- imagemagick (optional, for icon processing) + +## Development Commands + +**Initial setup:** + +```bash +git lfs install --force # Initialize Git LFS +git lfs pull # Pull LFS objects +pnpm run setup # Install dependencies and initialize dev environment +``` + +**Development:** + +```bash +pnpm run dev # Start development instance (uses repo directory name) +./script/dev-instance.sh [name] # Run specific isolated instance with separate data directory +``` + +**Building:** + +```bash +pnpm run build # Compile TypeScript and build with Vite +``` + +**Testing:** + +```bash +pnpm run test # Run tests with Vitest +# Test files should be named *.test.ts(x) or placed under src/tests/ +``` + +**Linting and formatting:** + +```bash +pnpm run lint # Run ESLint +pnpm run lint:fix # Run ESLint with auto-fix +pnpm run format # Format all files with Prettier +pnpm run format:check # Check formatting without making changes +pnpm run validate # Run both linting and formatting checks +pnpm run validate:fix # Run both with auto-fix +``` + +**Database management:** + +```bash +pnpm run generate-schema # Generate SQL_SCHEMA.md from migrations.rs +pnpm run delete-db # Delete local development database +``` + +**Release:** + +```bash +pnpm run release # Interactive release script +``` + +Note: Pre-commit hooks automatically run linting and formatting. See `.lintstagedrc.json`. + +## Development Instances + +The `dev-instance.sh` script lets you run multiple isolated Chorus instances simultaneously: + +- Each instance has its own data directory: `~/Library/Application Support/sh.chorus.app.dev./` +- Each gets a unique port (1420-1520) based on instance name hash +- Instance name appears in the DEV MODE indicator in the sidebar +- Data persists between runs +- Can set custom icons per instance in the data directory's `icons/` folder + +This is useful for working on multiple branches or testing without affecting your main development environment. + +## Local Data Storage + +- Development data: `~/Library/Application Support/sh.chorus.app.dev./` +- Contains: `auth.dat`, `chats.db`, and other app data +- NEVER commit these files to git +- Use `pnpm run delete-db` to delete the database for the current instance + +## Architecture + +### High-Level Structure + +``` +src/ +├── ui/ # React frontend +│ ├── components/ # UI components +│ ├── hooks/ # Custom React hooks +│ ├── providers/ # Context providers +│ └── themes/ # Theme configuration +├── core/ +│ ├── chorus/ # Core business logic +│ │ ├── api/ # TanStack Query queries and mutations +│ │ ├── ModelProviders/ # Provider implementations (Anthropic, OpenAI, etc.) +│ │ ├── toolsets/ # MCP toolset implementations +│ │ └── DB.ts # Database connection +│ ├── infra/ # Infrastructure utilities +│ └── utilities/ # Shared utilities +├── types/ # Shared TypeScript types +└── polyfills.ts # Browser API polyfills for Tauri +src-tauri/ +└── src/ # Rust backend + ├── command.rs # Tauri commands exposed to frontend + ├── migrations.rs # Database migrations + ├── window.rs # Window management + ├── main.rs # Application entry point + └── lib.rs # Tauri plugin configuration +script/ # Development and release scripts +``` + +### Routing + +The app uses React Router with these routes: + +- `/` - Home view +- `/chat/:chatId` - Chat interface (MultiChat component) +- `/projects/:projectId` - Project view with chat list +- `/new-prompt` - New prompt creation +- `/prompts` - List of saved prompts + +### Data Flow + +1. **User Input** → ChatInput.tsx captures user message +2. **TanStack Mutation** → API layer (e.g., MessageAPI.ts) handles request +3. **Provider Selection** → Models.ts routes to appropriate provider (ProviderAnthropic.ts, ProviderOpenAI.ts, etc.) +4. **Streaming Response** → Provider streams back to UI via TanStack Query +5. **Database Persistence** → Rust migrations.rs defines schema, TypeScript DB queries persist data + +### Key Architectural Patterns -Most of the functionality lives in this repo. There's also a backend that handles accounts, billing, and proxying the models' requests; that lives at app.chorus.sh and is written in Elixir. +**Model Provider System:** -## Your role +- All providers implement `IProvider` interface +- Each provider handles its own API communication format +- Providers support streaming, tool calling, and attachments +- See `src/core/chorus/ModelProviders/` -Your role is to write code. You do NOT have access to the running app, so you cannot test the code. You MUST rely on me, the user, to test the code. +**Database Layer:** -If I report a bug in your code, after you fix it, you should pause and ask me to verify that the bug is fixed. +- SQLite database with migrations in `src-tauri/src/migrations.rs` +- Database schema is auto-generated to `SQL_SCHEMA.md` via `pnpm run generate-schema` +- TypeScript database operations reference this schema +- CRITICAL: Never modify existing migrations, always add new ones -You do not have full context on the project, so often you will need to ask me questions about how to proceed. +**API Layer (TanStack Query):** -Don't be shy to ask questions -- I'm here to help you! +- Queries and mutations split by entity type: ChatAPI, MessageAPI, ProjectAPI, etc. +- Located in `src/core/chorus/api/` +- Each file handles one entity's CRUD operations and related business logic -If I send you a URL, you MUST immediately fetch its contents and read it carefully, before you do anything else. +**MCP (Model Context Protocol):** -## Workflow +- Toolsets defined in `src/core/chorus/Toolsets.ts` +- Individual toolset implementations in `src/core/chorus/toolsets/` +- ToolsetsManager.ts handles toolset lifecycle -We use GitHub issues to track work we need to do, and PRs to review code. Whenever you create an issue or a PR, tag it with "by-claude". Use the `gh` bash command to interact with GitHub. +**State Management:** -To start working on a feature, you should: +- TanStack Query for server state +- Zustand for UI state (DialogStore, etc.) +- React Context for app-wide state (AppProvider, SidebarProvider) -1. Setup +**Analytics:** -- Identify the relevant GitHub issue (or create one if needed) -- Checkout main and pull the latest changes -- Create a new branch like `claude/feature-name`. NEVER commit to main. NEVER push to origin/main. +- PostHog integration for analytics and feature flags +- Configured in `src/ui/main.tsx` with PostHogProvider -2. Development +**Tauri Plugins:** -- Commit often as you write code, so that we can revert if needed. -- When you have a draft of what you're working on, ask me to test it in the app to confirm that it works as you expect. Do this early and often. +The Rust backend uses these Tauri plugins (see `src-tauri/src/lib.rs`): -3. Review +- `tauri-plugin-sql` - SQLite database with migrations +- `tauri-plugin-http` - HTTP client for API requests +- `tauri-plugin-fs` - File system access +- `tauri-plugin-dialog` - Native dialogs +- `tauri-plugin-shell` - Shell command execution +- `tauri-plugin-notification` - System notifications +- `tauri-plugin-clipboard-manager` - Clipboard operations +- `tauri-plugin-deep-link` - Deep link handling +- `tauri-plugin-updater` - Auto-update functionality +- `tauri-plugin-store` - Key-value storage +- `tauri-plugin-macos-permissions` - macOS permission management -- When the work is done, verify that the diff looks good with `git diff main` -- While you should attempt to write code that adheres to our coding style, don't worry about manually linting or formatting your changes. There are Husky pre-commit Git hooks that will do this for you. -- Push the branch to GitHub -- Open a PR. - - The PR title should not include the issue number - - The PR description should start with the issue number and a brief description of the changes. - - Next, you should write a test plan. I (not you) will execute the test plan before merging the PR. If I can't check off any of the items, I will let you know. Make sure the test plan covers both new functionality and any EXISTING functionality that might be impacted by your changes +## Important Files and Directories -4. Fixing issues +**Entry Points:** -- To reconcile different branches, always rebase or cherry-pick. Do not merge. +- `src/ui/App.tsx` - React root component with routing +- `src-tauri/src/main.rs` - Rust application entry point +- `src-tauri/src/lib.rs` - Tauri plugin configuration -Sometimes, after you've been working on one feature, I will ask you to start work on an unrelated feature. If I do, you should probably repeat this process from the beginning (checkout main, pull changes, create a new branch). When in doubt, just ask. +**Core Logic:** -We use pnpm to manage dependencies. +- `src/core/chorus/Models.ts` - Model configuration and defaults +- `src/core/chorus/Toolsets.ts` - Tool/connection definitions +- `src/core/chorus/ChatState.ts` - Chat state management -Don't combine git commands -- e.g., instead of `git add -A && git commit`, run `git add -A` and `git commit` separately. This will save me time because I won't have to grant you permission to run the combined command. +**UI Components:** -## Project Structure +- `src/ui/components/MultiChat.tsx` - Main chat interface (handles both regular and quick chats) +- `src/ui/components/ChatInput.tsx` - Message input box +- `src/ui/components/AppSidebar.tsx` - Left sidebar with projects and chats +- `src/ui/components/ManageModelsBox.tsx` - Model selection interface +- `src/ui/components/Settings.tsx` - Settings dialog -- **UI:** React components in `src/ui/components/` -- **Core:** Business logic in `src/core/chorus/` -- **Tauri:** Rust backend in `src-tauri/src/` +**Database:** -Important files and directories to be aware of: +- `src-tauri/src/migrations.rs` - Database schema migrations +- `SQL_SCHEMA.md` - Auto-generated schema documentation (DO NOT EDIT MANUALLY) +- `src/core/chorus/DB.ts` - Database connection and query utilities -- `src/core/chorus/db/` - Queries against the sqlite database, which are split up by entity type (e.g. message, chat, project) -- `src/core/chorus/api/` - TanStack Query queries and mutations, which are also split up by entity type -- `src/ui/components/MultiChat.tsx` - Main interface -- `src/ui/components/ChatInput.tsx` - The input box where the user types chat messages -- `src/ui/components/AppSidebar.tsx` - The sidebar on the left -- `src/ui/App.tsx` - The root component +**Scripts:** -You can see an up-to-date schema of all database tables in SQL_SCHEMA.md. Use this file as a reference to understand the current -database schema. +- `script/setup-instance.sh` - Set up isolated development instance +- `script/dev-instance.sh` - Run development instance with custom configuration +- `script/delete-db.sh` - Delete local development database +- `script/validate.sh` - Run linting and formatting checks +- `script/interactive_release.sh` - Interactive release workflow -Other features: +## Data Model Changes -- Model picker, which lets the user select which models are available in the chat -- implemented in`ManageModelsBox.tsx` -- Quick chats (aka Ambient Chats), a lightweight chat window -- implemented, alongside regular chats, in `MultiChat.tsx` -- Projects, which are folders of related chats -- start with `AppSidebar.tsx` -- Tools and "connections" (aka toolsets) -- start with `Toolsets.ts` -- react-router-dom for navigation -- see `App.tsx` +When modifying the data model: -## Screenshots +1. Add a new migration in `src-tauri/src/migrations.rs` (NEVER modify existing migrations) +2. Run `pnpm run generate-schema` to update SQL_SCHEMA.md +3. Update TypeScript types throughout the codebase +4. Update TanStack Query queries/mutations in `src/core/chorus/api/` +5. Stage SQL_SCHEMA.md with your migration changes -I've put some screenshots of the app in the `screenshots` directory. If you're working on the UI at all, take a look at them. Keep in mind, though, that they may not be up to date with the latest code changes. +## Git Workflow -## Data model changes +**Branch management:** -Changes to the data model will typically require most of the following steps: +- NEVER commit directly to main +- NEVER push to origin/main +- Create feature branches: `claude/feature-name` +- Use rebase or cherry-pick to reconcile branches, not merge -- Making a new migration in `src-tauri/src/migrations.rs` (if changes to the sqlite database scheme are needed) -- Modifying fetch and read functions in `src/core/chorus/DB.ts` -- Modifying data types (stored in a variety of places) -- Adding or modifying TanStack Query queries in `src/core/chorus/API.ts` +**Committing:** -## Coding style +- Run git commands separately, not chained (e.g., `git add -A` then `git commit`, not `git add -A && git commit`) +- Commit frequently to allow easy reverts +- Pre-commit hooks will automatically lint and format -- **TypeScript:** Strict typing enabled, ES2020 target. Use `as` only in exceptional - circumstances, and then only with an explanatory comment. Prefer type hints. -- **Paths:** `@ui/*`, `@core/*`, `@/*` aliases. Use these instead of relative imports. -- **Components:** PascalCase for React components -- **Interfaces:** Prefixed with "I" (e.g., `IProvider`) -- **Hooks:** camelCase with "use" prefix -- **Formatting:** 4-space indentation, Prettier formatting -- **Promise handling:** All promises must be handled (ESLint enforced) -- **Nulls:** Prefer undefined to null. Convert `null` values from the database into undefined, e.g. `parentChatId: row.parent_chat_id ?? undefined` -- **Dates:** If you ever need to render a date, format it using `displayDate` in `src/ui/lib/utils.ts`. If the date was read - from our SQLite DB, you will need to convert it to a fully qualified UTC date using `convertDate` first. -- Do not use foreign keys or other constraints, they're too hard to remove and tend to put us in tricky situations down the line +**Pull Requests:** -IMPORTANT: If you want to use any of these features, you must alert me and explicitly ask for my permission first: `setTimeout`, `useImperativeHandle`, `useRef`, or type assertions with `as`. +- Tag all issues and PRs with "by-claude" +- PR title should NOT include issue number +- PR description should START with issue number +- Include comprehensive test plan for user to execute +- Test plan must cover both new functionality and potentially impacted existing features +- Use `gh` CLI for GitHub interactions + +## Coding Style + +**TypeScript:** + +- Strict mode enabled, ES2020 target +- Use `as` type assertions only in exceptional cases with explanatory comments +- Prefer type hints over assertions + +**Imports:** + +- Use path aliases: `@ui/*`, `@core/*`, `@/*` instead of relative imports +- Configured in `tsconfig.json` and `vite.config.ts` + +**Naming:** + +- React components: PascalCase +- Interfaces: Prefixed with "I" (e.g., `IProvider`) +- Hooks: camelCase with "use" prefix +- Files: Match component name + +**Formatting:** + +- 4-space indentation (see `.prettierrc`) +- Prettier handles formatting automatically via pre-commit hooks + +**Nulls and Undefined:** + +- Prefer `undefined` over `null` +- Convert database nulls to undefined: `parentChatId: row.parent_chat_id ?? undefined` + +**Dates:** + +- Format dates with `displayDate` from `src/ui/lib/utils.ts` +- Convert SQLite dates to UTC with `convertDate` before formatting + +**Constraints:** + +- Do not use foreign keys or database constraints (difficult to remove later) + +**Promises:** + +- ESLint enforces that all promises must be handled (@typescript-eslint/no-floating-promises) + +**Key Types:** + +- `Message` - Represents a single AI or user message in `ChatState.ts` +- `MessageSet` - Group of messages at the same level (user prompt + AI responses) +- `IProvider` - Interface all model providers must implement (in `ModelProviders/IProvider.ts`) +- `StreamResponseParams` - Parameters for streaming responses from providers +- `Toolset` - Interface for MCP toolset implementations +- `LLMMessage` - Standard message format for LLM conversations + +**Restricted Features - Require Explicit Permission:** +Before using any of these, you MUST ask the user for permission: + +- `setTimeout` +- `useImperativeHandle` +- `useRef` +- Type assertions with `as` ## Troubleshooting -Whenever I report that code you wrote doesn't work, or report a bug, you should: +When investigating bugs: + +1. Form hypotheses about root causes by reading relevant code +2. Evaluate each hypothesis against reported observations +3. Design tests to eliminate hypotheses +4. Propose a troubleshooting plan (tests, logging, code changes) +5. Iterate through the plan, re-evaluating hypotheses with new evidence + +**Model Provider Debugging:** +To debug requests to model providers (prompt formatting, attachments, tool calls): + +```typescript +// Add to ProviderAnthropic.ts or relevant provider +console.log(`createParams: ${JSON.stringify(createParams, null, 2)}`); +``` + +## Testing + +You (Claude) do NOT have access to the running app. You MUST rely on the user to test your code. + +After fixing a bug, pause and ask the user to verify the fix before continuing. + +When you complete a draft implementation, ask the user to test it early and often. + +**Test Organization:** + +- Test files should be named `*.test.ts` or `*.test.tsx` +- Can also be placed under `src/tests/` directory +- Use Vitest for all JavaScript/TypeScript tests +- Keep test snapshots committed to git + +**What to Test:** + +- Core business logic in `src/core/chorus/` +- API layer mutations and queries +- Model provider implementations +- Utility functions +- Complex UI component logic + +## Role and Workflow + +Your role is to write code. When working on features: + +1. **Setup:** + + - Identify or create GitHub issue + - Checkout main and pull latest + - Create new branch `claude/feature-name` -1. Read any relevant code or documentation, looking for hypotheses about the root cause -2. For each hypothesis, check whether it's consistent with the observations I've already reported -3. For any remaining hypotheses, think about a test I could run that would tell me if that hypothesis is incorrect -4. Propose a troubleshooting plan. The plan could involve: me running a test, you writing code, you adding logging statements, me reporting the output of the log statements back to you, or any other steps you think would be helpful. +2. **Development:** -Then we'll go through the plan together. At each step, keep in mind your list of hypotheses, and remember to re-evaluate each hypothesis against the evidence we've collected. + - Ask questions when unclear about implementation approach + - Commit often for easy reversion + - Request user testing early with draft implementations -When we run into issues with the requests we're sending to model providers (e.g., the way we format system prompts, attachments, tool calls, or other parts of the conversation history) one helpful troubleshooting step is to add the line `console.log(`createParams: ${JSON.stringify(createParams, null, 2)}`);` to ProviderAnthropic.ts. +3. **Review:** -## Updating this onboarding doc + - Verify diff with `git diff main` + - Don't worry about manual linting/formatting (hooks handle it) + - Push branch to GitHub + - Open PR with issue number, description, and user-executable test plan -Whenever you discover something that you wish you'd known earlier -- and seems likely to be helpful to future developers as well -- you can add it to the scratchpad section below. Feel free to edit the scratchpad section, but don't change the rest of this doc. +4. **Context:** + - If user sends a URL, fetch and read it immediately before doing anything else + - Screenshots are in `screenshots/` directory (may not reflect latest changes) + - SQL schema is in `SQL_SCHEMA.md` (auto-generated reference) -### Scratchpad +When asked to start unrelated work, repeat the workflow from the beginning (checkout main, new branch). diff --git a/SQL_SCHEMA.md b/SQL_SCHEMA.md new file mode 100644 index 00000000..226414fe --- /dev/null +++ b/SQL_SCHEMA.md @@ -0,0 +1,381 @@ +# Database Schema + +_This file is auto-generated from migrations.rs. Do not edit manually._ + +Last updated: 2026-01-28 14:35:11 + +## Tables + +- [app_metadata](#app_metadata) +- [attachments](#attachments) +- [chats](#chats) +- [custom_toolsets](#custom_toolsets) +- [draft_attachments](#draft_attachments) +- [gc_prototype_conductors](#gc_prototype_conductors) +- [gc_prototype_messages](#gc_prototype_messages) +- [message_attachments](#message_attachments) +- [message_drafts](#message_drafts) +- [message_parts](#message_parts) +- [message_sets](#message_sets) +- [messages](#messages) +- [messages_archive_20250102](#messages_archive_20250102) +- [model_configs](#model_configs) +- [models](#models) +- [models_archive_20250111](#models_archive_20250111) +- [project_attachments](#project_attachments) +- [projects](#projects) +- [saved_model_configs_chats](#saved_model_configs_chats) +- [temp_group_parent](#temp_group_parent) +- [temp_groupings](#temp_groupings) +- [temp_hierarchy](#temp_hierarchy) +- [temp_message_sets](#temp_message_sets) +- [tool_permissions](#tool_permissions) +- [toolsets_config](#toolsets_config) + +## app_metadata + +| Column | Type | Constraints | Default | +| ---------- | -------- | ----------- | ----------------- | +| key | TEXT | PRIMARY KEY | - | +| value | TEXT | NOT NULL | - | +| created_at | DATETIME | - | CURRENT_TIMESTAMP | + +## attachments + +| Column | Type | Constraints | Default | +| ------------- | -------- | ----------- | ----------------- | +| id | TEXT | PRIMARY KEY | - | +| created_at | DATETIME | NOT NULL | CURRENT_TIMESTAMP | +| type | TEXT | NOT NULL | - | +| is_loading | BOOLEAN | NOT NULL | 0 | +| original_name | TEXT | - | - | +| path | TEXT | NOT NULL | - | +| ephemeral | BOOLEAN | NOT NULL | 0 | + +## chats + +| Column | Type | Constraints | Default | +| -------------------------------- | -------- | -------------------- | ----------------- | +| id | TEXT | NOT NULL PRIMARY KEY | - | +| title | TEXT | - | - | +| created_at | DATETIME | - | CURRENT_TIMESTAMP | +| updated_at | DATETIME | - | - | +| pinned | BOOLEAN | NOT NULL | 0 | +| quick_chat | BOOLEAN | NOT NULL | 0 | +| project_id | TEXT | NOT NULL | 'default' | +| summary | TEXT | - | - | +| is_new_chat | BOOLEAN | NOT NULL | 0 | +| parent_chat_id | TEXT | - | - | +| project_context_summary | TEXT | - | - | +| project_context_summary_is_stale | BOOLEAN | NOT NULL | 1 | +| reply_to_id | TEXT | - | - | +| gc_prototype_chat | BOOLEAN | NOT NULL | 0 | +| total_cost_usd | REAL | - | 0.0 | + +### Indices + +- **idx_chats_is_new_chat** + - Columns: is_new_chat +- **idx_chats_pinned** + - Columns: pinned +- **idx_chats_project_cost** + - Columns: project_id, total_cost_usd + +## custom_toolsets + +| Column | Type | Constraints | Default | +| ------------------ | -------- | ----------- | ----------------- | +| name | TEXT | PRIMARY KEY | - | +| command | TEXT | - | - | +| args | TEXT | - | - | +| env | JSON | - | - | +| updated_at | DATETIME | - | CURRENT_TIMESTAMP | +| default_permission | TEXT | NOT NULL | 'ask' | + +## draft_attachments + +| Column | Type | Constraints | Default | +| ------------- | ---- | -------------------- | ------- | +| chat_id | TEXT | NOT NULL PRIMARY KEY | - | +| attachment_id | TEXT | NOT NULL PRIMARY KEY | - | + +## gc_prototype_conductors + +| Column | Type | Constraints | Default | +| ------------------ | -------- | -------------------- | ----------------- | +| chat_id | TEXT | NOT NULL PRIMARY KEY | - | +| scope_id | TEXT | PRIMARY KEY | - | +| conductor_model_id | TEXT | NOT NULL | - | +| turn_count | INTEGER | - | 0 | +| is_active | BOOLEAN | - | 1 | +| created_at | DATETIME | - | CURRENT_TIMESTAMP | + +### Indices + +- **idx_gc_prototype_conductors_active** + - Columns: chat_id, is_active + +## gc_prototype_messages + +| Column | Type | Constraints | Default | +| ------------------------ | -------- | -------------------- | ----------------- | +| chat_id | TEXT | NOT NULL PRIMARY KEY | - | +| id | TEXT | NOT NULL PRIMARY KEY | - | +| text | TEXT | NOT NULL | - | +| model_config_id | TEXT | NOT NULL | - | +| created_at | DATETIME | - | CURRENT_TIMESTAMP | +| updated_at | DATETIME | - | CURRENT_TIMESTAMP | +| is_deleted | BOOLEAN | NOT NULL | 0 | +| thread_root_message_id | TEXT | - | - | +| promoted_from_message_id | TEXT | - | - | + +### Indices + +- **idx_gc_prototype_messages_chat_created** + - Columns: chat_id, created_at +- **idx_gc_prototype_messages_promoted_from** + - Columns: promoted_from_message_id +- **idx_gc_prototype_messages_thread_root** + - Columns: thread_root_message_id + +## message_attachments + +| Column | Type | Constraints | Default | +| ------------- | ---- | -------------------- | ------- | +| message_id | TEXT | NOT NULL PRIMARY KEY | - | +| attachment_id | TEXT | NOT NULL PRIMARY KEY | - | + +## message_drafts + +| Column | Type | Constraints | Default | +| ------- | ---- | ----------- | ------- | +| chat_id | TEXT | PRIMARY KEY | - | +| content | TEXT | NOT NULL | - | + +## message_parts + +| Column | Type | Constraints | Default | +| ------------ | ------- | -------------------- | ------- | +| chat_id | TEXT | NOT NULL | - | +| message_id | TEXT | NOT NULL PRIMARY KEY | - | +| level | INTEGER | NOT NULL PRIMARY KEY | - | +| content | TEXT | NOT NULL | - | +| tool_calls | TEXT | - | - | +| tool_results | TEXT | - | - | + +## message_sets + +| Column | Type | Constraints | Default | +| -------------------- | -------- | ----------- | ----------------- | +| id | TEXT | PRIMARY KEY | - | +| chat_id | TEXT | NOT NULL | - | +| deprecated_parent_id | TEXT | - | - | +| type | TEXT | NOT NULL | - | +| created_at | DATETIME | - | CURRENT_TIMESTAMP | +| selected_block_type | TEXT | NOT NULL | 'chat' | +| level | INTEGER | - | - | + +### Indices + +- **idx_message_sets_chat_level** + - Columns: chat_id, level + +## messages + +| Column | Type | Constraints | Default | +| ----------------------- | -------- | ----------- | ----------------- | +| id | TEXT | PRIMARY KEY | - | +| message_set_id | TEXT | NOT NULL | - | +| chat_id | TEXT | NOT NULL | - | +| text | TEXT | NOT NULL | - | +| model | TEXT | NOT NULL | - | +| selected | BOOLEAN | - | - | +| created_at | DATETIME | - | CURRENT_TIMESTAMP | +| streaming_token | TEXT | - | - | +| state | TEXT | - | 'streaming' | +| error_message | TEXT | - | - | +| is_review | BOOLEAN | - | 0 | +| review_state | TEXT | - | - | +| block_type | TEXT | - | - | +| level | INTEGER | - | - | +| dep_attachments_archive | TEXT | - | - | +| reply_chat_id | TEXT | - | - | +| branched_from_id | TEXT | - | - | +| prompt_tokens | INTEGER | - | - | +| completion_tokens | INTEGER | - | - | +| total_tokens | INTEGER | - | - | +| cost_usd | REAL | - | - | + +### Indices + +- **idx_messages_chat_cost** + - Columns: chat_id, cost_usd + +## messages_archive_20250102 + +| Column | Type | Constraints | Default | +| ----------- | -------- | ----------- | ----------------- | +| id | TEXT | PRIMARY KEY | - | +| chat_id | TEXT | NOT NULL | - | +| parent_id | TEXT | - | - | +| text | TEXT | NOT NULL | - | +| model | TEXT | NOT NULL | - | +| selected | BOOLEAN | - | - | +| created_at | DATETIME | - | CURRENT_TIMESTAMP | +| attachments | TEXT | - | - | + +### Indices + +- **idx_messages_attachments** + - Columns: (json_valid(attachments + +## model_configs + +| Column | Type | Constraints | Default | +| ---------------- | -------- | -------------------- | ----------------- | +| id | TEXT | NOT NULL PRIMARY KEY | - | +| model_id | TEXT | NOT NULL | - | +| display_name | TEXT | NOT NULL | - | +| author | TEXT | NOT NULL | - | +| created_at | DATETIME | - | CURRENT_TIMESTAMP | +| system_prompt | TEXT | NOT NULL | - | +| is_default | BOOLEAN | - | 0 | +| budget_tokens | INTEGER | - | - | +| reasoning_effort | TEXT | - | - | +| new_until | DATETIME | - | - | +| thinking_level | TEXT | - | - | +| show_thoughts | BOOLEAN | NOT NULL | 0 | + +## models + +| Column | Type | Constraints | Default | +| -------------------------- | ------- | ----------- | ------- | +| id | TEXT | PRIMARY KEY | - | +| display_name | TEXT | NOT NULL | - | +| is_enabled | BOOLEAN | - | 1 | +| supported_attachment_types | TEXT | NOT NULL | - | +| is_internal | BOOLEAN | NOT NULL | 0 | +| is_deprecated | BOOLEAN | NOT NULL | 0 | +| prompt_price_per_token | REAL | - | - | +| completion_price_per_token | REAL | - | - | + +## models_archive_20250111 + +| Column | Type | Constraints | Default | +| ---------------- | -------- | ----------- | ----------------- | +| id | TEXT | PRIMARY KEY | - | +| name | TEXT | NOT NULL | - | +| type | TEXT | NOT NULL | - | +| api_key | TEXT | - | - | +| model_id | TEXT | NOT NULL | - | +| system_prompt | TEXT | - | - | +| request_template | JSON | NOT NULL | - | +| created_at | DATETIME | - | CURRENT_TIMESTAMP | +| is_enabled | BOOLEAN | - | 1 | +| display_order | INTEGER | - | - | +| is_selected | BOOLEAN | - | 0 | +| short_name | TEXT | - | - | +| server_url | TEXT | - | - | + +## project_attachments + +| Column | Type | Constraints | Default | +| ------------- | ---- | -------------------- | ------- | +| project_id | TEXT | NOT NULL PRIMARY KEY | - | +| attachment_id | TEXT | NOT NULL PRIMARY KEY | - | + +## projects + +| Column | Type | Constraints | Default | +| ---------------------- | -------- | ----------- | ----------------- | +| id | TEXT | PRIMARY KEY | - | +| name | TEXT | NOT NULL | - | +| created_at | DATETIME | NOT NULL | CURRENT_TIMESTAMP | +| updated_at | DATETIME | NOT NULL | CURRENT_TIMESTAMP | +| is_collapsed | BOOLEAN | NOT NULL | 0 | +| context_text | TEXT | - | - | +| magic_projects_enabled | BOOLEAN | NOT NULL | 1 | +| is_imported | BOOLEAN | NOT NULL | 0 | +| total_cost_usd | REAL | - | 0.0 | + +## saved_model_configs_chats + +| Column | Type | Constraints | Default | +| ---------- | -------- | -------------------- | ----------------- | +| id | TEXT | NOT NULL PRIMARY KEY | - | +| chat_id | TEXT | - | - | +| model_ids | TEXT | NOT NULL | - | +| created_at | DATETIME | - | CURRENT_TIMESTAMP | +| updated_at | DATETIME | - | CURRENT_TIMESTAMP | + +### Indices + +- **idx_saved_model_configs_chats_chat_id** + - Columns: chat_id + +## temp_group_parent + +| Column | Type | Constraints | Default | +| ---------------- | ---- | ----------- | ------- | +| group_key | | - | - | +| chat_id | TEXT | - | - | +| parent_id | TEXT | - | - | +| type | | - | - | +| level | | - | - | +| parent_group_key | | - | - | + +## temp_groupings + +| Column | Type | Constraints | Default | +| --------- | ---- | ----------- | ------- | +| chat_id | TEXT | - | - | +| parent_id | TEXT | - | - | +| type | | - | - | +| level | | - | - | +| group_key | | - | - | + +## temp_hierarchy + +| Column | Type | Constraints | Default | +| ------------ | ---- | ----------- | ------- | +| id | TEXT | - | - | +| chat_id | TEXT | - | - | +| parent_id | TEXT | - | - | +| text | TEXT | - | - | +| model | TEXT | - | - | +| attachments | TEXT | - | - | +| has_children | | - | - | +| level | | - | - | +| created_at | NUM | - | - | + +## temp_message_sets + +| Column | Type | Constraints | Default | +| --------------------- | ---- | ----------- | ------- | +| group_key | TEXT | PRIMARY KEY | - | +| message_set_id | TEXT | - | - | +| chat_id | TEXT | NOT NULL | - | +| parent_group_key | TEXT | - | - | +| parent_message_set_id | TEXT | - | - | +| type | TEXT | NOT NULL | - | +| level | INT | NOT NULL | - | + +## tool_permissions + +| Column | Type | Constraints | Default | +| --------------- | -------- | -------------------- | ----------------- | +| toolset_name | TEXT | NOT NULL PRIMARY KEY | - | +| tool_name | TEXT | NOT NULL PRIMARY KEY | - | +| permission_type | TEXT | NOT NULL | - | +| last_asked_at | DATETIME | - | - | +| last_response | TEXT | - | - | +| created_at | DATETIME | - | CURRENT_TIMESTAMP | +| updated_at | DATETIME | - | CURRENT_TIMESTAMP | + +## toolsets_config + +| Column | Type | Constraints | Default | +| --------------- | ---- | ----------- | ------- | +| toolset_name | TEXT | PRIMARY KEY | - | +| parameter_id | TEXT | PRIMARY KEY | - | +| parameter_value | TEXT | - | - | diff --git a/eslint.config.mjs b/eslint.config.mjs index 0f7dd0b6..b1a5b663 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -10,6 +10,7 @@ export default tseslint.config( ignores: [ "src-tauri/target/**/*", "dist/**/*", + "reference/**/*", "postcss.config.js", "vite.config.ts", "tailwind.config.cjs", diff --git a/local-prd.md b/local-prd.md deleted file mode 100644 index 925bab2e..00000000 --- a/local-prd.md +++ /dev/null @@ -1,74 +0,0 @@ -i would like to add the ability to run local models. - -some information: - -- on the @Models.tsx page, it would be nice to have a "local" section. and what it does is use the locally running ollama api to see what is local, and then those can be used. the model then needs to be callable from @MultiChat.tsx . ollama has an API that is: - -curl http://localhost:11434/api/tags - -to list models, which returns JSON that looks like: -{ -"models": [ -{ -"name": "codellama:13b", -"modified_at": "2023-11-04T14:56:49.277302595-07:00", -"size": 7365960935, -"digest": "9f438cb9cd581fc025612d27f7c1a6669ff83a8bb0ed86c94fcf4c5440555697", -"details": { -"format": "gguf", -"family": "llama", -"families": null, -"parameter_size": "13B", -"quantization_level": "Q4_0" -} -}, -{ -"name": "llama3:latest", -"modified_at": "2023-12-07T09:32:18.757212583-08:00", -"size": 3825819519, -"digest": "fe938a131f40e6f6d40083c9f0f430a515233eb2edaa6d72eb85c50d64f2300e", -"details": { -"format": "gguf", -"family": "llama", -"families": null, -"parameter_size": "7B", -"quantization_level": "Q4_0" -} -} -] -} - -then, to make a request, we can do: -curl http://localhost:11434/api/chat -d '{ -"model": "llama3.2", -"messages": [ -{ -"role": "user", -"content": "why is the sky blue?" -} -] -}' - -which returns a stream: -{ -"model": "llama3.2", -"created_at": "2023-08-04T08:52:19.385406455-07:00", -"message": { -"role": "assistant", -"content": "The", -"images": null -}, -"done": false -} -and a final response: -{ -"model": "llama3.2", -"created_at": "2023-08-04T19:22:45.499127Z", -"done": true, -"total_duration": 4883583458, -"load_duration": 1334875, -"prompt_eval_count": 26, -"prompt_eval_duration": 342546000, -"eval_count": 282, -"eval_duration": 4535599000 -} diff --git a/package.json b/package.json index 36524bd5..565adc83 100644 --- a/package.json +++ b/package.json @@ -35,12 +35,13 @@ }, "dependencies": { "@anthropic-ai/sdk": "^0.33.1", + "@cerebras/cerebras_cloud_sdk": "^1.64.1", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", "@google/genai": "^0.8.0", "@google/generative-ai": "^0.21.0", "@hello-pangea/dnd": "^17.0.0", - "@mendable/firecrawl-js": "1.18.2", + "@mistralai/mistralai": "^1.13.0", "@modelcontextprotocol/sdk": "^1.10.2", "@octokit/rest": "^21.1.1", "@posthog/ai": "^3.3.2", @@ -100,6 +101,7 @@ "exa-js": "^1.6.13", "file-type": "^19.6.0", "framer-motion": "^12.9.1", + "groq-sdk": "^0.37.0", "highlight.js": "^11.11.1", "html-entities": "^2.6.0", "immer": "^10.1.1", @@ -113,6 +115,7 @@ "mime-types": "^2.1.35", "minimatch": "^10.0.1", "openai": "^6.10.0", + "parquetjs": "^0.11.2", "pdfjs-dist": "^5.3.31", "posthog-js": "^1.236.6", "react": "^18.3.1", @@ -139,6 +142,7 @@ "tailwindcss-animate": "^1.0.7", "tauri-plugin-macos-permissions-api": "~2.1.1", "textarea-caret": "^3.1.0", + "together-ai": "^0.33.0", "turndown": "^7.2.0", "use-editable": "^2.3.3", "use-react-query-auto-sync": "^0.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6647e9fe..55b59036 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,109 +10,112 @@ importers: dependencies: '@anthropic-ai/sdk': specifier: ^0.33.1 - version: 0.33.1 + version: 0.33.1(encoding@0.1.13) + '@cerebras/cerebras_cloud_sdk': + specifier: ^1.64.1 + version: 1.64.1(encoding@0.1.13) '@dnd-kit/core': specifier: ^6.3.1 - version: 6.3.1(react-dom@18.3.1)(react@18.3.1) + version: 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@dnd-kit/modifiers': specifier: ^9.0.0 - version: 9.0.0(@dnd-kit/core@6.3.1)(react@18.3.1) + version: 9.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) '@google/genai': specifier: ^0.8.0 - version: 0.8.0 + version: 0.8.0(encoding@0.1.13) '@google/generative-ai': specifier: ^0.21.0 version: 0.21.0 '@hello-pangea/dnd': specifier: ^17.0.0 - version: 17.0.0(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@mendable/firecrawl-js': - specifier: 1.18.2 - version: 1.18.2(ws@8.18.3) + version: 17.0.0(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@mistralai/mistralai': + specifier: ^1.13.0 + version: 1.13.0 '@modelcontextprotocol/sdk': specifier: ^1.10.2 - version: 1.24.3(zod@4.2.0) + version: 1.24.3(@cfworker/json-schema@4.1.1)(zod@4.2.0) '@octokit/rest': specifier: ^21.1.1 version: 21.1.1 '@posthog/ai': specifier: ^3.3.2 - version: 3.3.2(cheerio@1.1.2)(react@18.3.1)(ws@8.18.3) + version: 3.3.2(@opentelemetry/api@1.9.0)(axios@1.13.2)(cheerio@1.1.2)(encoding@0.1.13)(handlebars@4.7.8)(react@18.3.1)(ws@8.18.3) '@radix-ui/react-alert-dialog': specifier: ^1.1.11 - version: 1.1.15(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-avatar': specifier: ^1.1.7 - version: 1.1.11(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + version: 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-checkbox': specifier: ^1.2.3 - version: 1.3.3(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + version: 1.3.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-collapsible': specifier: ^1.1.8 - version: 1.1.12(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + version: 1.1.12(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-context-menu': specifier: ^2.2.12 - version: 2.2.16(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + version: 2.2.16(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-dialog': specifier: ^1.1.11 - version: 1.1.15(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-dropdown-menu': specifier: ^2.1.12 - version: 2.1.16(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + version: 2.1.16(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-hover-card': specifier: ^1.1.11 - version: 1.1.15(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-icons': specifier: ^1.3.2 version: 1.3.2(react@18.3.1) '@radix-ui/react-label': specifier: ^2.1.1 - version: 2.1.8(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + version: 2.1.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-menubar': specifier: ^1.1.4 - version: 1.1.16(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + version: 1.1.16(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-popover': specifier: ^1.1.4 - version: 1.1.15(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-progress': specifier: ^1.1.1 - version: 1.1.8(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + version: 1.1.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-radio-group': specifier: ^1.2.2 - version: 1.3.8(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + version: 1.3.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-scroll-area': specifier: ^1.2.2 - version: 1.2.10(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + version: 1.2.10(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-select': specifier: ^2.1.4 - version: 2.2.6(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + version: 2.2.6(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-separator': specifier: ^1.1.1 - version: 1.1.8(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + version: 1.1.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': specifier: ^1.2.0 version: 1.2.4(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-switch': specifier: ^1.1.2 - version: 1.2.6(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + version: 1.2.6(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-tabs': specifier: ^1.1.2 - version: 1.1.13(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + version: 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-toast': specifier: ^1.2.4 - version: 1.2.15(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + version: 1.2.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-toggle': specifier: ^1.1.1 - version: 1.1.10(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + version: 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-tooltip': specifier: ^1.1.6 - version: 1.2.8(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + version: 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/react-query': specifier: ^5.69.0 version: 5.90.12(react@18.3.1) '@tanstack/react-query-devtools': specifier: ^5.69.0 - version: 5.91.1(@tanstack/react-query@5.90.12)(react@18.3.1) + version: 5.91.1(@tanstack/react-query@5.90.12(react@18.3.1))(react@18.3.1) '@tauri-apps/api': specifier: 2.5.0 version: 2.5.0 @@ -187,7 +190,7 @@ importers: version: 2.1.1 cmdk: specifier: 1.1.1 - version: 1.1.1(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + version: 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) date-fns: specifier: ^4.1.0 version: 4.1.0 @@ -199,13 +202,16 @@ importers: version: 16.6.1 exa-js: specifier: ^1.6.13 - version: 1.10.2(ws@8.18.3) + version: 1.10.2(encoding@0.1.13)(ws@8.18.3) file-type: specifier: ^19.6.0 version: 19.6.0 framer-motion: specifier: ^12.9.1 - version: 12.23.26(react-dom@18.3.1)(react@18.3.1) + version: 12.23.26(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + groq-sdk: + specifier: ^0.37.0 + version: 0.37.0(encoding@0.1.13) highlight.js: specifier: ^11.11.1 version: 11.11.1 @@ -245,6 +251,9 @@ importers: openai: specifier: ^6.10.0 version: 6.10.0(ws@8.18.3)(zod@4.2.0) + parquetjs: + specifier: ^0.11.2 + version: 0.11.2 pdfjs-dist: specifier: ^5.3.31 version: 5.4.449 @@ -268,13 +277,13 @@ importers: version: 9.1.0(@types/react@18.3.27)(react@18.3.1) react-mermaid2: specifier: ^0.1.4 - version: 0.1.4(@testing-library/dom@10.4.1)(@types/react@18.3.27)(eslint@9.39.2)(typescript@5.9.3) + version: 0.1.4(@testing-library/dom@10.4.1)(@types/react@18.3.27)(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) react-resizable-panels: specifier: ^2.1.8 - version: 2.1.9(react-dom@18.3.1)(react@18.3.1) + version: 2.1.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-router-dom: specifier: ^6.30.0 - version: 6.30.2(react-dom@18.3.1)(react@18.3.1) + version: 6.30.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-scan: specifier: ^0.0.4 version: 0.0.4 @@ -283,10 +292,10 @@ importers: version: 15.6.6(react@18.3.1) react-window: specifier: ^1.8.11 - version: 1.8.11(react-dom@18.3.1)(react@18.3.1) + version: 1.8.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1) reagraph: specifier: ^4.22.0 - version: 4.30.7(@types/react@18.3.27)(@types/three@0.182.0)(graphology-types@0.24.8)(immer@10.2.0)(react-dom@18.3.1)(react@18.3.1) + version: 4.30.7(@types/react@18.3.27)(@types/three@0.182.0)(graphology-types@0.24.8)(immer@10.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)) rehype-highlight: specifier: ^7.0.2 version: 7.0.2 @@ -301,13 +310,13 @@ importers: version: 4.0.1 selection-popover: specifier: ^0.3.0 - version: 0.3.0(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + version: 0.3.0(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) simple-icons: specifier: ^13.21.0 version: 13.21.0 sonner: specifier: ^2.0.5 - version: 2.0.7(react-dom@18.3.1)(react@18.3.1) + version: 2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) sqlite3: specifier: ^5.1.7 version: 5.1.7 @@ -316,13 +325,16 @@ importers: version: 0.3.2 tailwindcss-animate: specifier: ^1.0.7 - version: 1.0.7(tailwindcss@3.4.19) + version: 1.0.7(tailwindcss@3.4.19(yaml@2.8.2)) tauri-plugin-macos-permissions-api: specifier: ~2.1.1 version: 2.1.1 textarea-caret: specifier: ^3.1.0 version: 3.1.0 + together-ai: + specifier: ^0.33.0 + version: 0.33.0 turndown: specifier: ^7.2.0 version: 7.2.2 @@ -331,7 +343,7 @@ importers: version: 2.3.3(react@18.3.1) use-react-query-auto-sync: specifier: ^0.1.0 - version: 0.1.0(@tanstack/react-query@5.90.12)(react-dom@18.3.1)(react@18.3.1) + version: 0.1.0(@tanstack/react-query@5.90.12(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) usehooks-ts: specifier: ^3.1.1 version: 3.1.1(react@18.3.1) @@ -340,26 +352,26 @@ importers: version: 9.0.1 vitest: specifier: ^2.1.9 - version: 2.1.9(@types/node@22.19.3) + version: 2.1.9(@types/node@22.19.3)(jsdom@14.1.0) zustand: specifier: ^5.0.5 - version: 5.0.9(@types/react@18.3.27)(immer@10.2.0)(react@18.3.1)(use-sync-external-store@1.6.0) + version: 5.0.9(@types/react@18.3.27)(immer@10.2.0)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)) devDependencies: '@eslint/js': specifier: ^9.25.1 version: 9.39.2 '@tailwindcss/container-queries': specifier: ^0.1.1 - version: 0.1.1(tailwindcss@3.4.19) + version: 0.1.1(tailwindcss@3.4.19(yaml@2.8.2)) '@tailwindcss/forms': specifier: ^0.5.10 - version: 0.5.10(tailwindcss@3.4.19) + version: 0.5.10(tailwindcss@3.4.19(yaml@2.8.2)) '@tailwindcss/typography': specifier: ^0.5.16 - version: 0.5.19(tailwindcss@3.4.19) + version: 0.5.19(tailwindcss@3.4.19(yaml@2.8.2)) '@tanstack/eslint-plugin-query': specifier: ^5.74.7 - version: 5.91.2(eslint@9.39.2)(typescript@5.9.3) + version: 5.91.2(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) '@tauri-apps/cli': specifier: ^2.5.0 version: 2.9.6 @@ -404,7 +416,7 @@ importers: version: 9.0.8 '@vitejs/plugin-react': specifier: ^4.4.1 - version: 4.7.0(vite@5.4.21) + version: 4.7.0(vite@5.4.21(@types/node@22.19.3)) autoprefixer: specifier: ^10.4.21 version: 10.4.23(postcss@8.5.6) @@ -413,19 +425,19 @@ importers: version: 7.0.3 eslint: specifier: ^9.25.1 - version: 9.39.2 + version: 9.39.2(jiti@1.21.7) eslint-config-react-app: specifier: ^7.0.1 - version: 7.0.1(@babel/plugin-syntax-flow@7.27.1)(@babel/plugin-transform-react-jsx@7.27.1)(eslint@9.39.2)(jest@29.7.0)(typescript@5.9.3) + version: 7.0.1(@babel/plugin-syntax-flow@7.27.1(@babel/core@7.28.5))(@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.28.5))(eslint@9.39.2(jiti@1.21.7))(jest@29.7.0(@types/node@22.19.3)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)))(typescript@5.9.3) eslint-plugin-react: specifier: ^7.37.5 - version: 7.37.5(eslint@9.39.2) + version: 7.37.5(eslint@9.39.2(jiti@1.21.7)) eslint-plugin-react-hooks: specifier: ^5.2.0 - version: 5.2.0(eslint@9.39.2) + version: 5.2.0(eslint@9.39.2(jiti@1.21.7)) eslint-plugin-react-refresh: specifier: ^0.4.20 - version: 0.4.25(eslint@9.39.2) + version: 0.4.25(eslint@9.39.2(jiti@1.21.7)) fast-diff: specifier: ^1.3.0 version: 1.3.0 @@ -437,7 +449,7 @@ importers: version: 9.1.7 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.19.3)(ts-node@10.9.2) + version: 29.7.0(@types/node@22.19.3)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)) lint-staged: specifier: ^16.1.0 version: 16.2.7 @@ -461,10 +473,10 @@ importers: version: 2.6.0 tailwindcss: specifier: ^3.4.17 - version: 3.4.19 + version: 3.4.19(yaml@2.8.2) ts-jest: specifier: ^29.3.2 - version: 29.4.6(@babel/core@7.7.4)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.6(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.19.3)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)))(typescript@5.9.3) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.19.3)(typescript@5.9.3) @@ -473,13 +485,13 @@ importers: version: 5.9.3 typescript-eslint: specifier: ^8.31.0 - version: 8.49.0(eslint@9.39.2)(typescript@5.9.3) + version: 8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) vite: specifier: ^5.4.18 version: 5.4.21(@types/node@22.19.3) vite-plugin-node-polyfills: specifier: ^0.22.0 - version: 0.22.0(vite@5.4.21) + version: 0.22.0(rollup@4.53.4)(vite@5.4.21(@types/node@22.19.3)) packages: @@ -1385,6 +1397,9 @@ packages: resolution: {integrity: sha512-GcIY79elgB+azP74j8vqkiXz8xLFfIzbQJdlwOPisgbKT00tviJQuEghOXSMVxJ00HoYJbGswr4kcllUc4xCcg==} deprecated: Potential XSS vulnerability patched in v6.0.0. + '@cerebras/cerebras_cloud_sdk@1.64.1': + resolution: {integrity: sha512-eQ0udGHS9xrWANi56yCS/FMcbwtysugD73YWipp89+zarbm2pd5hxqrmGlFqafS4Pwyo7cU7Qv31am5jdjqXFg==} + '@cfworker/json-schema@4.1.1': resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==} @@ -1879,8 +1894,8 @@ packages: '@mediapipe/tasks-vision@0.10.17': resolution: {integrity: sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==} - '@mendable/firecrawl-js@1.18.2': - resolution: {integrity: sha512-KkTm5iFI8v1p50xsxFclHNt54XkS9qDYbkGIyc63eBGW8vnJZwc/mmgf+RFBOrQOfMZIQnAAfbbMNUJsdKl3AQ==} + '@mistralai/mistralai@1.13.0': + resolution: {integrity: sha512-eg7/8hQp91SshYDJ5XdWDsJENdIaSu52npJfKLCPuhla8Vm6GlESv2RROQyxESglCl7b1OULWiAv1ERbsrZO7w==} '@mixmark-io/domino@2.2.0': resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} @@ -4298,6 +4313,9 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + bindings@1.2.1: + resolution: {integrity: sha512-u4cBQNepWxYA55FunZSM7wMi55yQaN0otnhhilNoWHq0MfOfJeQx0v0mRRpolGOExPjZcl6FtB0BB8Xkb88F0g==} + bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} @@ -4344,6 +4362,9 @@ packages: brorand@1.1.0: resolution: {integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==} + brotli@1.3.3: + resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==} + browser-process-hrtime@1.0.0: resolution: {integrity: sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==} @@ -4389,6 +4410,10 @@ packages: bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + bson@1.1.6: + resolution: {integrity: sha512-EvVNVeGo4tHxwi8L6bPj3y3itEvStdwvvlojVxxbyYfoaxJ6keLgrTuKdyfEAszFK+H3olzBuafE0yoh0D1gdg==} + engines: {node: '>=0.6.19'} + buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} @@ -6636,6 +6661,9 @@ packages: peerDependencies: graphology-types: '>=0.24.0' + groq-sdk@0.37.0: + resolution: {integrity: sha512-lT72pcT8b/X5XrzdKf+rWVzUGW1OQSKESmL8fFN5cTbsf02gq6oFam4SVeNtzELt9cYE2Pt3pdGgSImuTbHFDg==} + growly@1.3.0: resolution: {integrity: sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==} @@ -7020,6 +7048,9 @@ packages: resolution: {integrity: sha512-Bu5Td5+j11sCkqfqmUTiwv+tWisMtP0L7Q8WrqA2C/BbBhy1YTdFrvjjlrKq8oagA/tLQBski2Gcx/Sqyi2qSQ==} engines: {node: '>=6.0.0'} + int53@0.2.4: + resolution: {integrity: sha512-a5jlKftS7HUOhkUyYD7j2sJ/ZnvWiNlZS1ldR+g1ifQ+/UuZXIE+YTc/lK1qGj/GwAU5F8Z0e1eVq2t1J5Ob2g==} + internal-ip@4.3.0: resolution: {integrity: sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg==} engines: {node: '>=6'} @@ -7379,11 +7410,6 @@ packages: resolution: {integrity: sha512-u4sej9B1LPSxTGKB/HiuzvEQnXH0ECYkSVQU39koSwmFAxhlEAFl9RdTvLv4TOTQUgBS5O3O5fwUxk6byBZ+IQ==} engines: {node: '>=10'} - isows@1.0.7: - resolution: {integrity: sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==} - peerDependencies: - ws: '*' - isstream@0.1.2: resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} @@ -8086,6 +8112,9 @@ packages: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true + lzo@0.4.11: + resolution: {integrity: sha512-apQHNoW2Alg72FMqaC/7pn03I7umdgSVFt2KRkCXXils4Z9u3QBh1uOtl2O5WmZIDLd9g6Lu4lIdOLmiSTFVCQ==} + maath@0.10.8: resolution: {integrity: sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==} peerDependencies: @@ -8716,6 +8745,10 @@ packages: resolution: {integrity: sha512-ICbQN+aw/eAASDtaC7+SJXSAruz7fvvNjxMFfS3mTdvZaaiuuw81XXYu+9CSJeUVrS3YpRhTr862YGywMQUOWg==} engines: {node: '>=0.10.0'} + object-stream@0.0.1: + resolution: {integrity: sha512-+NPJnRvX9RDMRY9mOWOo/NDppBjbZhXirNNSu2IBnuNboClC9h1ZGHXgHBLDbJMHsxeJDq922aVmG5xs24a/cA==} + engines: {node: '>=0.10'} + object-visit@1.0.1: resolution: {integrity: sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==} engines: {node: '>=0.10.0'} @@ -8960,6 +8993,10 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parquetjs@0.11.2: + resolution: {integrity: sha512-Y6FOc3Oi2AxY4TzJPz7fhICCR8tQNL3p+2xGQoUAMbmlJBR7+JJmMrwuyMjIpDiM7G8Wj/8oqOH4UDUmu4I5ZA==} + engines: {node: '>=7.6'} + parse-asn1@5.1.9: resolution: {integrity: sha512-fIYNuZ/HastSb80baGOuPRo1O9cf4baWw5WsAp7dBuUzeTD/BoaG8sVTdlPFksBE2lF21dN+A1AnrpIjSWqHHg==} engines: {node: '>= 0.10'} @@ -9715,7 +9752,6 @@ packages: resolution: {integrity: sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==} peerDependencies: react: ^16.14.0 - bundledDependencies: false react-dom@18.3.1: resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} @@ -9874,7 +9910,6 @@ packages: react@16.14.0: resolution: {integrity: sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==} engines: {node: '>=0.10.0'} - bundledDependencies: false react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} @@ -10467,6 +10502,9 @@ packages: resolution: {integrity: sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==} engines: {node: '>=0.10.0'} + snappyjs@0.6.1: + resolution: {integrity: sha512-YIK6I2lsH072UE0aOFxxY1dPDCS43I5ktqHpeAsuLNYWkE5pGxRGWfDM4/vSUfNzXjC1Ivzt3qx31PCLmc9yqg==} + sockjs-client@1.4.0: resolution: {integrity: sha512-5zaLyO8/nri5cua0VtOrFXBPK1jbL4+1cebT/mmKA1E1ZXOvJrII75bPu0l0k843G/+iAbhEqzyKr0w/eCCj7g==} @@ -10554,9 +10592,6 @@ packages: sqlite3@5.1.7: resolution: {integrity: sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==} - peerDependenciesMeta: - node-gyp: - optional: true sshpk@1.18.0: resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==} @@ -10944,6 +10979,10 @@ packages: three@0.180.0: resolution: {integrity: sha512-o+qycAMZrh+TsE01GqWUxUIKR1AL0S8pq7zDkYOQw8GqfX8b8VoCKYUoHbhiX5j+7hr8XsuHDVU6+gkQJQKg9w==} + thrift@0.11.0: + resolution: {integrity: sha512-UpsBhOC45a45TpeHOXE4wwYwL8uD2apbHTbtBvkwtUU4dNwCjC7DpQTjw2Q6eIdfNtw+dKthdwq94uLXTJPfFw==} + engines: {node: '>= 4.1.0'} + throat@4.1.0: resolution: {integrity: sha512-wCVxLDcFxw7ujDxaeJC6nfl2XfHJNYs8yUYJnvMgtPEFlttP9tHSfRUv2vBe6C4hkVFPWoP1P6ZccbYjmSEkKA==} @@ -11022,6 +11061,10 @@ packages: resolution: {integrity: sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==} engines: {node: '>=0.10.0'} + together-ai@0.33.0: + resolution: {integrity: sha512-2JdxYwbw+Xw2bW2PHBGqbMTtYsQHoWO9UXvdwIfQkde/swoKp2x/hpxEjtTERzrMP4O5SdDPGxsjfcPXewDJ9A==} + hasBin: true + toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} @@ -11214,9 +11257,6 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - typescript-event-target@1.1.1: - resolution: {integrity: sha512-dFSOFBKV6uwaloBCCUhxlD3Pr/P1a/tJdcmPrTXCHlEFD3faj0mztjcGn6VBAhQ0/Bdy8K3VWrrqwbt/ffsYsg==} - typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -11473,6 +11513,9 @@ packages: validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + varint@5.0.2: + resolution: {integrity: sha512-lKxKYG6H03yCZUpAGOPOsMcGxd1RHCu1iKvEHYDPmTyq2HueGhD73ssNBqqQWfvYs04G9iUFRvmAVLW20Jw6ow==} + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -11990,6 +12033,7 @@ snapshots: react: 18.3.1 swr: 2.3.7(react@18.3.1) throttleit: 2.1.0 + optionalDependencies: zod: 3.25.76 '@ai-sdk/ui-utils@1.2.11(zod@3.25.76)': @@ -12001,7 +12045,7 @@ snapshots: '@alloc/quick-lru@5.2.0': {} - '@anthropic-ai/sdk@0.33.1': + '@anthropic-ai/sdk@0.33.1(encoding@0.1.13)': dependencies: '@types/node': 18.19.130 '@types/node-fetch': 2.6.13 @@ -12009,11 +12053,11 @@ snapshots: agentkeepalive: 4.6.0 form-data-encoder: 1.7.2 formdata-node: 4.4.1 - node-fetch: 2.7.0 + node-fetch: 2.7.0(encoding@0.1.13) transitivePeerDependencies: - encoding - '@anthropic-ai/sdk@0.36.3': + '@anthropic-ai/sdk@0.36.3(encoding@0.1.13)': dependencies: '@types/node': 18.19.130 '@types/node-fetch': 2.6.13 @@ -12021,7 +12065,7 @@ snapshots: agentkeepalive: 4.6.0 form-data-encoder: 1.7.2 formdata-node: 4.4.1 - node-fetch: 2.7.0 + node-fetch: 2.7.0(encoding@0.1.13) transitivePeerDependencies: - encoding @@ -12097,11 +12141,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/eslint-parser@7.28.5(@babel/core@7.28.5)(eslint@9.39.2)': + '@babel/eslint-parser@7.28.5(@babel/core@7.28.5)(eslint@9.39.2(jiti@1.21.7))': dependencies: '@babel/core': 7.28.5 '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1 - eslint: 9.39.2 + eslint: 9.39.2(jiti@1.21.7) eslint-visitor-keys: 2.1.0 semver: 6.3.1 @@ -12608,11 +12652,6 @@ snapshots: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-flow@7.27.1(@babel/core@7.7.4)': - dependencies: - '@babel/core': 7.7.4 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-flow@7.27.1(@babel/core@7.9.0)': dependencies: '@babel/core': 7.9.0 @@ -14128,6 +14167,18 @@ snapshots: '@braintree/sanitize-url@3.1.0': {} + '@cerebras/cerebras_cloud_sdk@1.64.1(encoding@0.1.13)': + dependencies: + '@types/node': 18.19.130 + '@types/node-fetch': 2.6.13 + abort-controller: 3.0.0 + agentkeepalive: 4.6.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0(encoding@0.1.13) + transitivePeerDependencies: + - encoding + '@cfworker/json-schema@4.1.1': {} '@cnakazawa/watch@1.0.4': @@ -14150,7 +14201,7 @@ snapshots: react: 18.3.1 tslib: 2.8.1 - '@dnd-kit/core@6.3.1(react-dom@18.3.1)(react@18.3.1)': + '@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@dnd-kit/accessibility': 3.1.1(react@18.3.1) '@dnd-kit/utilities': 3.2.2(react@18.3.1) @@ -14158,9 +14209,9 @@ snapshots: react-dom: 18.3.1(react@18.3.1) tslib: 2.8.1 - '@dnd-kit/modifiers@9.0.0(@dnd-kit/core@6.3.1)(react@18.3.1)': + '@dnd-kit/modifiers@9.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': dependencies: - '@dnd-kit/core': 6.3.1(react-dom@18.3.1)(react@18.3.1) + '@dnd-kit/core': 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@dnd-kit/utilities': 3.2.2(react@18.3.1) react: 18.3.1 tslib: 2.8.1 @@ -14239,9 +14290,9 @@ snapshots: '@esbuild/win32-x64@0.21.5': optional: true - '@eslint-community/eslint-utils@4.9.0(eslint@9.39.2)': + '@eslint-community/eslint-utils@4.9.0(eslint@9.39.2(jiti@1.21.7))': dependencies: - eslint: 9.39.2 + eslint: 9.39.2(jiti@1.21.7) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} @@ -14294,13 +14345,13 @@ snapshots: '@floating-ui/core': 1.7.3 '@floating-ui/utils': 0.2.10 - '@floating-ui/react-dom@1.3.0(react-dom@18.3.1)(react@18.3.1)': + '@floating-ui/react-dom@1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@floating-ui/dom': 1.7.4 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@floating-ui/react-dom@2.1.6(react-dom@18.3.1)(react@18.3.1)': + '@floating-ui/react-dom@2.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@floating-ui/dom': 1.7.4 react: 18.3.1 @@ -14311,9 +14362,9 @@ snapshots: '@gar/promisify@1.1.3': optional: true - '@google/genai@0.8.0': + '@google/genai@0.8.0(encoding@0.1.13)': dependencies: - google-auth-library: 9.15.1 + google-auth-library: 9.15.1(encoding@0.1.13) ws: 8.18.3 transitivePeerDependencies: - bufferutil @@ -14340,7 +14391,7 @@ snapshots: dependencies: '@hapi/hoek': 8.5.1 - '@hello-pangea/dnd@17.0.0(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1)': + '@hello-pangea/dnd@17.0.0(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.28.4 css-box-model: 1.2.1 @@ -14431,7 +14482,7 @@ snapshots: - supports-color - utf-8-validate - '@jest/core@29.7.0(ts-node@10.9.2)': + '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -14445,7 +14496,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.19.3)(ts-node@10.9.2) + jest-config: 29.7.0(@types/node@22.19.3)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -14543,9 +14594,7 @@ snapshots: source-map: 0.6.1 string-length: 2.0.0 transitivePeerDependencies: - - bufferutil - supports-color - - utf-8-validate '@jest/reporters@29.7.0': dependencies: @@ -14746,14 +14795,14 @@ snapshots: '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) tslib: 2.8.1 - '@langchain/core@0.3.79(openai@6.10.0)': + '@langchain/core@0.3.79(@opentelemetry/api@1.9.0)(openai@6.10.0(ws@8.18.3)(zod@4.2.0))': dependencies: '@cfworker/json-schema': 4.1.1 ansi-styles: 5.2.0 camelcase: 6.3.0 decamelize: 1.2.0 js-tiktoken: 1.0.21 - langsmith: 0.3.87(openai@6.10.0) + langsmith: 0.3.87(@opentelemetry/api@1.9.0)(openai@6.10.0(ws@8.18.3)(zod@4.2.0)) mustache: 4.2.0 p-queue: 6.6.2 p-retry: 4.6.2 @@ -14766,36 +14815,30 @@ snapshots: - '@opentelemetry/sdk-trace-base' - openai - '@langchain/openai@0.6.16(@langchain/core@0.3.79)(ws@8.18.3)': + '@langchain/openai@0.6.16(@langchain/core@0.3.79(@opentelemetry/api@1.9.0)(openai@4.104.0(encoding@0.1.13)(ws@8.18.3)(zod@3.25.76)))(ws@8.18.3)': dependencies: - '@langchain/core': 0.3.79(openai@6.10.0) + '@langchain/core': 0.3.79(@opentelemetry/api@1.9.0)(openai@6.10.0(ws@8.18.3)(zod@4.2.0)) js-tiktoken: 1.0.21 openai: 5.12.2(ws@8.18.3)(zod@3.25.76) zod: 3.25.76 transitivePeerDependencies: - ws - '@langchain/textsplitters@0.1.0(@langchain/core@0.3.79)': + '@langchain/textsplitters@0.1.0(@langchain/core@0.3.79(@opentelemetry/api@1.9.0)(openai@4.104.0(encoding@0.1.13)(ws@8.18.3)(zod@3.25.76)))': dependencies: - '@langchain/core': 0.3.79(openai@6.10.0) + '@langchain/core': 0.3.79(@opentelemetry/api@1.9.0)(openai@6.10.0(ws@8.18.3)(zod@4.2.0)) js-tiktoken: 1.0.21 '@mediapipe/tasks-vision@0.10.17': {} - '@mendable/firecrawl-js@1.18.2(ws@8.18.3)': + '@mistralai/mistralai@1.13.0': dependencies: - axios: 1.13.2 - isows: 1.0.7(ws@8.18.3) - typescript-event-target: 1.1.1 - zod: 3.25.76 - zod-to-json-schema: 3.25.0(zod@3.25.76) - transitivePeerDependencies: - - debug - - ws + zod: 4.2.0 + zod-to-json-schema: 3.25.0(zod@4.2.0) '@mixmark-io/domino@2.2.0': {} - '@modelcontextprotocol/sdk@1.24.3(zod@4.2.0)': + '@modelcontextprotocol/sdk@1.24.3(@cfworker/json-schema@4.1.1)(zod@4.2.0)': dependencies: ajv: 8.17.1 ajv-formats: 3.0.1(ajv@8.17.1) @@ -14811,6 +14854,8 @@ snapshots: raw-body: 3.0.2 zod: 4.2.0 zod-to-json-schema: 3.25.0(zod@4.2.0) + optionalDependencies: + '@cfworker/json-schema': 4.1.1 transitivePeerDependencies: - supports-color @@ -14968,13 +15013,13 @@ snapshots: '@opentelemetry/api@1.9.0': {} - '@posthog/ai@3.3.2(cheerio@1.1.2)(react@18.3.1)(ws@8.18.3)': + '@posthog/ai@3.3.2(@opentelemetry/api@1.9.0)(axios@1.13.2)(cheerio@1.1.2)(encoding@0.1.13)(handlebars@4.7.8)(react@18.3.1)(ws@8.18.3)': dependencies: - '@anthropic-ai/sdk': 0.36.3 - '@langchain/core': 0.3.79(openai@6.10.0) + '@anthropic-ai/sdk': 0.36.3(encoding@0.1.13) + '@langchain/core': 0.3.79(@opentelemetry/api@1.9.0)(openai@6.10.0(ws@8.18.3)(zod@4.2.0)) ai: 4.3.19(react@18.3.1)(zod@3.25.76) - langchain: 0.3.36(@langchain/core@0.3.79)(cheerio@1.1.2)(openai@4.104.0)(ws@8.18.3) - openai: 4.104.0(ws@8.18.3)(zod@3.25.76) + langchain: 0.3.36(@langchain/core@0.3.79(@opentelemetry/api@1.9.0)(openai@4.104.0(encoding@0.1.13)(ws@8.18.3)(zod@3.25.76)))(@opentelemetry/api@1.9.0)(axios@1.13.2)(cheerio@1.1.2)(handlebars@4.7.8)(openai@4.104.0(encoding@0.1.13)(ws@8.18.3)(zod@3.25.76))(ws@8.18.3) + openai: 4.104.0(encoding@0.1.13)(ws@8.18.3)(zod@3.25.76) uuid: 11.1.0 zod: 3.25.76 transitivePeerDependencies: @@ -15014,86 +15059,92 @@ snapshots: '@radix-ui/primitive@1.1.3': {} - '@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.3 '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-context': 1.1.2(@types/react@18.3.27)(react@18.3.1) - '@radix-ui/react-dialog': 1.1.15(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': 1.2.3(@types/react@18.3.27)(react@18.3.1) - '@types/react': 18.3.27 - '@types/react-dom': 18.3.7(@types/react@18.3.27) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) - '@radix-ui/react-arrow@1.0.1(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-arrow@1.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.28.4 - '@radix-ui/react-primitive': 1.0.1(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@radix-ui/react-arrow@1.1.7(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-arrow@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@types/react': 18.3.27 - '@types/react-dom': 18.3.7(@types/react@18.3.27) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) - '@radix-ui/react-avatar@1.1.11(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-avatar@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-context': 1.1.3(@types/react@18.3.27)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.4(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.27)(react@18.3.1) - '@types/react': 18.3.27 - '@types/react-dom': 18.3.7(@types/react@18.3.27) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) - '@radix-ui/react-checkbox@1.3.3(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-checkbox@1.3.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.3 '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-context': 1.1.2(@types/react@18.3.27)(react@18.3.1) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.27)(react@18.3.1) - '@types/react': 18.3.27 - '@types/react-dom': 18.3.7(@types/react@18.3.27) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) - '@radix-ui/react-collapsible@1.1.12(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-collapsible@1.1.12(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.3 '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-context': 1.1.2(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-id': 1.1.1(@types/react@18.3.27)(react@18.3.1) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.27)(react@18.3.1) - '@types/react': 18.3.27 - '@types/react-dom': 18.3.7(@types/react@18.3.27) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) - '@radix-ui/react-collection@1.1.7(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-collection@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-context': 1.1.2(@types/react@18.3.27)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': 1.2.3(@types/react@18.3.27)(react@18.3.1) - '@types/react': 18.3.27 - '@types/react-dom': 18.3.7(@types/react@18.3.27) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) '@radix-ui/react-compose-refs@1.0.0(react@18.3.1)': dependencies: @@ -15103,26 +15154,29 @@ snapshots: '@radix-ui/react-compose-refs@1.0.1(@types/react@18.3.27)(react@18.3.1)': dependencies: '@babel/runtime': 7.28.4 - '@types/react': 18.3.27 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 '@radix-ui/react-compose-refs@1.1.2(@types/react@18.3.27)(react@18.3.1)': dependencies: - '@types/react': 18.3.27 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 - '@radix-ui/react-context-menu@2.2.16(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-context-menu@2.2.16(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.3 '@radix-ui/react-context': 1.1.2(@types/react@18.3.27)(react@18.3.1) - '@radix-ui/react-menu': 2.1.16(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.27)(react@18.3.1) - '@types/react': 18.3.27 - '@types/react-dom': 18.3.7(@types/react@18.3.27) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) '@radix-ui/react-context@1.0.0(react@18.3.1)': dependencies: @@ -15131,109 +15185,119 @@ snapshots: '@radix-ui/react-context@1.1.2(@types/react@18.3.27)(react@18.3.1)': dependencies: - '@types/react': 18.3.27 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 '@radix-ui/react-context@1.1.3(@types/react@18.3.27)(react@18.3.1)': dependencies: - '@types/react': 18.3.27 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 - '@radix-ui/react-dialog@1.1.15(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-dialog@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.3 '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-context': 1.1.2(@types/react@18.3.27)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.27)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-id': 1.1.1(@types/react@18.3.27)(react@18.3.1) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': 1.2.3(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.27)(react@18.3.1) - '@types/react': 18.3.27 - '@types/react-dom': 18.3.7(@types/react@18.3.27) aria-hidden: 1.2.6 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-remove-scroll: 2.7.2(@types/react@18.3.27)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) '@radix-ui/react-direction@1.1.1(@types/react@18.3.27)(react@18.3.1)': dependencies: - '@types/react': 18.3.27 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 - '@radix-ui/react-dismissable-layer@1.0.4(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-dismissable-layer@1.0.4(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.28.4 '@radix-ui/primitive': 1.0.1 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.27)(react@18.3.1) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-use-escape-keydown': 1.0.3(@types/react@18.3.27)(react@18.3.1) - '@types/react': 18.3.27 - '@types/react-dom': 18.3.7(@types/react@18.3.27) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) - '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.3 '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@18.3.27)(react@18.3.1) - '@types/react': 18.3.27 - '@types/react-dom': 18.3.7(@types/react@18.3.27) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) - '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.3 '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-context': 1.1.2(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-id': 1.1.1(@types/react@18.3.27)(react@18.3.1) - '@radix-ui/react-menu': 2.1.16(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.27)(react@18.3.1) - '@types/react': 18.3.27 - '@types/react-dom': 18.3.7(@types/react@18.3.27) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) '@radix-ui/react-focus-guards@1.1.3(@types/react@18.3.27)(react@18.3.1)': dependencies: - '@types/react': 18.3.27 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 - '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.27)(react@18.3.1) - '@types/react': 18.3.27 - '@types/react-dom': 18.3.7(@types/react@18.3.27) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) - '@radix-ui/react-hover-card@1.1.15(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-hover-card@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.3 '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-context': 1.1.2(@types/react@18.3.27)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.27)(react@18.3.1) - '@types/react': 18.3.27 - '@types/react-dom': 18.3.7(@types/react@18.3.27) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) '@radix-ui/react-icons@1.3.2(react@18.3.1)': dependencies: @@ -15242,115 +15306,122 @@ snapshots: '@radix-ui/react-id@1.1.1(@types/react@18.3.27)(react@18.3.1)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.27)(react@18.3.1) - '@types/react': 18.3.27 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 - '@radix-ui/react-label@2.1.8(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-label@2.1.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-primitive': 2.1.4(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@types/react': 18.3.27 - '@types/react-dom': 18.3.7(@types/react@18.3.27) + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) - '@radix-ui/react-menu@2.1.16(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-menu@2.1.16(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-context': 1.1.2(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-direction': 1.1.1(@types/react@18.3.27)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.27)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-id': 1.1.1(@types/react@18.3.27)(react@18.3.1) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': 1.2.3(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.27)(react@18.3.1) - '@types/react': 18.3.27 - '@types/react-dom': 18.3.7(@types/react@18.3.27) aria-hidden: 1.2.6 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-remove-scroll: 2.7.2(@types/react@18.3.27)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) - '@radix-ui/react-menubar@1.1.16(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-menubar@1.1.16(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-context': 1.1.2(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-direction': 1.1.1(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-id': 1.1.1(@types/react@18.3.27)(react@18.3.1) - '@radix-ui/react-menu': 2.1.16(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.27)(react@18.3.1) - '@types/react': 18.3.27 - '@types/react-dom': 18.3.7(@types/react@18.3.27) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) - '@radix-ui/react-popover@1.1.15(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-popover@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.3 '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-context': 1.1.2(@types/react@18.3.27)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.27)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-id': 1.1.1(@types/react@18.3.27)(react@18.3.1) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': 1.2.3(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.27)(react@18.3.1) - '@types/react': 18.3.27 - '@types/react-dom': 18.3.7(@types/react@18.3.27) aria-hidden: 1.2.6 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-remove-scroll: 2.7.2(@types/react@18.3.27)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) - '@radix-ui/react-popper@1.2.8(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-popper@1.2.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@floating-ui/react-dom': 2.1.6(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-arrow': 1.1.7(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@floating-ui/react-dom': 2.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-context': 1.1.2(@types/react@18.3.27)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-use-rect': 1.1.1(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.27)(react@18.3.1) '@radix-ui/rect': 1.1.1 - '@types/react': 18.3.27 - '@types/react-dom': 18.3.7(@types/react@18.3.27) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) - '@radix-ui/react-portal@1.0.1(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-portal@1.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.28.4 - '@radix-ui/react-primitive': 1.0.1(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@radix-ui/react-portal@1.1.9(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-portal@1.1.9(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.27)(react@18.3.1) - '@types/react': 18.3.27 - '@types/react-dom': 18.3.7(@types/react@18.3.27) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) - '@radix-ui/react-presence@1.0.0(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-presence@1.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.28.4 '@radix-ui/react-compose-refs': 1.0.0(react@18.3.1) @@ -15358,140 +15429,150 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@radix-ui/react-presence@1.1.5(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-presence@1.1.5(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.27)(react@18.3.1) - '@types/react': 18.3.27 - '@types/react-dom': 18.3.7(@types/react@18.3.27) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) - '@radix-ui/react-primitive@1.0.1(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-primitive@1.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.28.4 '@radix-ui/react-slot': 1.0.1(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@radix-ui/react-primitive@1.0.3(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-primitive@1.0.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.28.4 '@radix-ui/react-slot': 1.0.2(@types/react@18.3.27)(react@18.3.1) - '@types/react': 18.3.27 - '@types/react-dom': 18.3.7(@types/react@18.3.27) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) - '@radix-ui/react-primitive@2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-primitive@2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-slot': 1.2.3(@types/react@18.3.27)(react@18.3.1) - '@types/react': 18.3.27 - '@types/react-dom': 18.3.7(@types/react@18.3.27) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) - '@radix-ui/react-primitive@2.1.4(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-primitive@2.1.4(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-slot': 1.2.4(@types/react@18.3.27)(react@18.3.1) - '@types/react': 18.3.27 - '@types/react-dom': 18.3.7(@types/react@18.3.27) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) - '@radix-ui/react-progress@1.1.8(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-progress@1.1.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-context': 1.1.3(@types/react@18.3.27)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.4(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@types/react': 18.3.27 - '@types/react-dom': 18.3.7(@types/react@18.3.27) + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) - '@radix-ui/react-radio-group@1.3.8(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-radio-group@1.3.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.3 '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-context': 1.1.2(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-direction': 1.1.1(@types/react@18.3.27)(react@18.3.1) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.27)(react@18.3.1) - '@types/react': 18.3.27 - '@types/react-dom': 18.3.7(@types/react@18.3.27) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) - '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-context': 1.1.2(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-direction': 1.1.1(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-id': 1.1.1(@types/react@18.3.27)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.27)(react@18.3.1) - '@types/react': 18.3.27 - '@types/react-dom': 18.3.7(@types/react@18.3.27) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) - '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/number': 1.1.1 '@radix-ui/primitive': 1.1.3 '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-context': 1.1.2(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-direction': 1.1.1(@types/react@18.3.27)(react@18.3.1) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.27)(react@18.3.1) - '@types/react': 18.3.27 - '@types/react-dom': 18.3.7(@types/react@18.3.27) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) - '@radix-ui/react-select@2.2.6(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-select@2.2.6(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/number': 1.1.1 '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-context': 1.1.2(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-direction': 1.1.1(@types/react@18.3.27)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.27)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-id': 1.1.1(@types/react@18.3.27)(react@18.3.1) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': 1.2.3(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.27)(react@18.3.1) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@types/react': 18.3.27 - '@types/react-dom': 18.3.7(@types/react@18.3.27) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) aria-hidden: 1.2.6 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-remove-scroll: 2.7.2(@types/react@18.3.27)(react@18.3.1) - - '@radix-ui/react-separator@1.1.8(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1)': - dependencies: - '@radix-ui/react-primitive': 2.1.4(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + optionalDependencies: '@types/react': 18.3.27 '@types/react-dom': 18.3.7(@types/react@18.3.27) + + '@radix-ui/react-separator@1.1.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) '@radix-ui/react-slot@1.0.1(react@18.3.1)': dependencies: @@ -15503,97 +15584,105 @@ snapshots: dependencies: '@babel/runtime': 7.28.4 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.27)(react@18.3.1) - '@types/react': 18.3.27 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 '@radix-ui/react-slot@1.2.3(@types/react@18.3.27)(react@18.3.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) - '@types/react': 18.3.27 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 '@radix-ui/react-slot@1.2.4(@types/react@18.3.27)(react@18.3.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) - '@types/react': 18.3.27 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 - '@radix-ui/react-switch@1.2.6(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-switch@1.2.6(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.3 '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-context': 1.1.2(@types/react@18.3.27)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.27)(react@18.3.1) - '@types/react': 18.3.27 - '@types/react-dom': 18.3.7(@types/react@18.3.27) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) - '@radix-ui/react-tabs@1.1.13(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-tabs@1.1.13(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.3 '@radix-ui/react-context': 1.1.2(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-direction': 1.1.1(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-id': 1.1.1(@types/react@18.3.27)(react@18.3.1) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.27)(react@18.3.1) - '@types/react': 18.3.27 - '@types/react-dom': 18.3.7(@types/react@18.3.27) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) - '@radix-ui/react-toast@1.2.15(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-toast@1.2.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-context': 1.1.2(@types/react@18.3.27)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.27)(react@18.3.1) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@types/react': 18.3.27 - '@types/react-dom': 18.3.7(@types/react@18.3.27) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) - '@radix-ui/react-toggle@1.1.10(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-toggle@1.1.10(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.27)(react@18.3.1) - '@types/react': 18.3.27 - '@types/react-dom': 18.3.7(@types/react@18.3.27) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) - '@radix-ui/react-tooltip@1.2.8(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.3 '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-context': 1.1.2(@types/react@18.3.27)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-id': 1.1.1(@types/react@18.3.27)(react@18.3.1) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': 1.2.3(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.27)(react@18.3.1) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@types/react': 18.3.27 - '@types/react-dom': 18.3.7(@types/react@18.3.27) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) '@radix-ui/react-use-callback-ref@1.0.0(react@18.3.1)': dependencies: @@ -15603,13 +15692,15 @@ snapshots: '@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.3.27)(react@18.3.1)': dependencies: '@babel/runtime': 7.28.4 - '@types/react': 18.3.27 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 '@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.27)(react@18.3.1)': dependencies: - '@types/react': 18.3.27 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 '@radix-ui/react-use-controllable-state@1.0.0(react@18.3.1)': dependencies: @@ -15621,33 +15712,38 @@ snapshots: dependencies: '@radix-ui/react-use-effect-event': 0.0.2(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.27)(react@18.3.1) - '@types/react': 18.3.27 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 '@radix-ui/react-use-effect-event@0.0.2(@types/react@18.3.27)(react@18.3.1)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.27)(react@18.3.1) - '@types/react': 18.3.27 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 '@radix-ui/react-use-escape-keydown@1.0.3(@types/react@18.3.27)(react@18.3.1)': dependencies: '@babel/runtime': 7.28.4 '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.27)(react@18.3.1) - '@types/react': 18.3.27 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@18.3.27)(react@18.3.1)': dependencies: '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.27)(react@18.3.1) - '@types/react': 18.3.27 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 '@radix-ui/react-use-is-hydrated@0.1.0(@types/react@18.3.27)(react@18.3.1)': dependencies: - '@types/react': 18.3.27 react: 18.3.1 use-sync-external-store: 1.6.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 '@radix-ui/react-use-layout-effect@1.0.0(react@18.3.1)': dependencies: @@ -15656,19 +15752,22 @@ snapshots: '@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.3.27)(react@18.3.1)': dependencies: - '@types/react': 18.3.27 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 '@radix-ui/react-use-previous@1.1.1(@types/react@18.3.27)(react@18.3.1)': dependencies: - '@types/react': 18.3.27 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 '@radix-ui/react-use-rect@1.1.1(@types/react@18.3.27)(react@18.3.1)': dependencies: '@radix-ui/rect': 1.1.1 - '@types/react': 18.3.27 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 '@radix-ui/react-use-size@1.0.0(react@18.3.1)': dependencies: @@ -15679,16 +15778,18 @@ snapshots: '@radix-ui/react-use-size@1.1.1(@types/react@18.3.27)(react@18.3.1)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.27)(react@18.3.1) - '@types/react': 18.3.27 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 - '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@types/react': 18.3.27 - '@types/react-dom': 18.3.7(@types/react@18.3.27) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) '@radix-ui/rect@1.1.1': {} @@ -15713,24 +15814,24 @@ snapshots: '@react-spring/types': 10.0.3 react: 18.3.1 - '@react-spring/three@10.0.3(@react-three/fiber@9.3.0)(react@18.3.1)(three@0.180.0)': + '@react-spring/three@10.0.3(@react-three/fiber@9.3.0(@types/react@18.3.27)(immer@10.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.180.0))(react@18.3.1)(three@0.180.0)': dependencies: '@react-spring/animated': 10.0.3(react@18.3.1) '@react-spring/core': 10.0.3(react@18.3.1) '@react-spring/shared': 10.0.3(react@18.3.1) '@react-spring/types': 10.0.3 - '@react-three/fiber': 9.3.0(@types/react@18.3.27)(immer@10.2.0)(react-dom@18.3.1)(react@18.3.1)(three@0.180.0) + '@react-three/fiber': 9.3.0(@types/react@18.3.27)(immer@10.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.180.0) react: 18.3.1 three: 0.180.0 '@react-spring/types@10.0.3': {} - '@react-three/drei@10.7.7(@react-three/fiber@9.3.0)(@types/react@18.3.27)(@types/three@0.182.0)(immer@10.2.0)(react-dom@18.3.1)(react@18.3.1)(three@0.180.0)': + '@react-three/drei@10.7.7(@react-three/fiber@9.3.0(@types/react@18.3.27)(immer@10.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.180.0))(@types/react@18.3.27)(@types/three@0.182.0)(immer@10.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.180.0)': dependencies: '@babel/runtime': 7.28.4 '@mediapipe/tasks-vision': 0.10.17 '@monogrid/gainmap-js': 3.4.0(three@0.180.0) - '@react-three/fiber': 9.3.0(@types/react@18.3.27)(immer@10.2.0)(react-dom@18.3.1)(react@18.3.1)(three@0.180.0) + '@react-three/fiber': 9.3.0(@types/react@18.3.27)(immer@10.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.180.0) '@use-gesture/react': 10.3.1(react@18.3.1) camera-controls: 3.1.2(three@0.180.0) cross-env: 7.0.3 @@ -15740,7 +15841,6 @@ snapshots: maath: 0.10.8(@types/three@0.182.0)(three@0.180.0) meshline: 3.3.1(three@0.180.0) react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) stats-gl: 2.4.2(@types/three@0.182.0)(three@0.180.0) stats.js: 0.17.0 suspend-react: 0.1.3(react@18.3.1) @@ -15751,13 +15851,15 @@ snapshots: tunnel-rat: 0.1.2(@types/react@18.3.27)(immer@10.2.0)(react@18.3.1) use-sync-external-store: 1.6.0(react@18.3.1) utility-types: 3.11.0 - zustand: 5.0.9(@types/react@18.3.27)(immer@10.2.0)(react@18.3.1)(use-sync-external-store@1.6.0) + zustand: 5.0.9(@types/react@18.3.27)(immer@10.2.0)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)) + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) transitivePeerDependencies: - '@types/react' - '@types/three' - immer - '@react-three/fiber@9.3.0(@types/react@18.3.27)(immer@10.2.0)(react-dom@18.3.1)(react@18.3.1)(three@0.180.0)': + '@react-three/fiber@9.3.0(@types/react@18.3.27)(immer@10.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.180.0)': dependencies: '@babel/runtime': 7.28.4 '@types/react-reconciler': 0.32.3(@types/react@18.3.27) @@ -15766,14 +15868,15 @@ snapshots: buffer: 6.0.3 its-fine: 2.0.0(@types/react@18.3.27)(react@18.3.1) react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) react-reconciler: 0.31.0(react@18.3.1) - react-use-measure: 2.1.7(react-dom@18.3.1)(react@18.3.1) + react-use-measure: 2.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) scheduler: 0.25.0 suspend-react: 0.1.3(react@18.3.1) three: 0.180.0 use-sync-external-store: 1.6.0(react@18.3.1) - zustand: 5.0.9(@types/react@18.3.27)(immer@10.2.0)(react@18.3.1)(use-sync-external-store@1.6.0) + zustand: 5.0.9(@types/react@18.3.27)(immer@10.2.0)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)) + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) transitivePeerDependencies: - '@types/react' - immer @@ -15782,17 +15885,21 @@ snapshots: '@rolldown/pluginutils@1.0.0-beta.27': {} - '@rollup/plugin-inject@5.0.5': + '@rollup/plugin-inject@5.0.5(rollup@4.53.4)': dependencies: - '@rollup/pluginutils': 5.3.0 + '@rollup/pluginutils': 5.3.0(rollup@4.53.4) estree-walker: 2.0.2 magic-string: 0.30.21 + optionalDependencies: + rollup: 4.53.4 - '@rollup/pluginutils@5.3.0': + '@rollup/pluginutils@5.3.0(rollup@4.53.4)': dependencies: '@types/estree': 1.0.8 estree-walker: 2.0.2 picomatch: 4.0.3 + optionalDependencies: + rollup: 4.53.4 '@rollup/rollup-android-arm-eabi@4.53.4': optional: true @@ -15945,24 +16052,24 @@ snapshots: transitivePeerDependencies: - supports-color - '@tailwindcss/container-queries@0.1.1(tailwindcss@3.4.19)': + '@tailwindcss/container-queries@0.1.1(tailwindcss@3.4.19(yaml@2.8.2))': dependencies: - tailwindcss: 3.4.19 + tailwindcss: 3.4.19(yaml@2.8.2) - '@tailwindcss/forms@0.5.10(tailwindcss@3.4.19)': + '@tailwindcss/forms@0.5.10(tailwindcss@3.4.19(yaml@2.8.2))': dependencies: mini-svg-data-uri: 1.4.4 - tailwindcss: 3.4.19 + tailwindcss: 3.4.19(yaml@2.8.2) - '@tailwindcss/typography@0.5.19(tailwindcss@3.4.19)': + '@tailwindcss/typography@0.5.19(tailwindcss@3.4.19(yaml@2.8.2))': dependencies: postcss-selector-parser: 6.0.10 - tailwindcss: 3.4.19 + tailwindcss: 3.4.19(yaml@2.8.2) - '@tanstack/eslint-plugin-query@5.91.2(eslint@9.39.2)(typescript@5.9.3)': + '@tanstack/eslint-plugin-query@5.91.2(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': dependencies: - '@typescript-eslint/utils': 8.49.0(eslint@9.39.2)(typescript@5.9.3) - eslint: 9.39.2 + '@typescript-eslint/utils': 8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + eslint: 9.39.2(jiti@1.21.7) transitivePeerDependencies: - supports-color - typescript @@ -15971,7 +16078,7 @@ snapshots: '@tanstack/query-devtools@5.91.1': {} - '@tanstack/react-query-devtools@5.91.1(@tanstack/react-query@5.90.12)(react@18.3.1)': + '@tanstack/react-query-devtools@5.91.1(@tanstack/react-query@5.90.12(react@18.3.1))(react@18.3.1)': dependencies: '@tanstack/query-devtools': 5.91.1 '@tanstack/react-query': 5.90.12(react@18.3.1) @@ -16124,7 +16231,7 @@ snapshots: pretty-format: 24.9.0 redent: 3.0.0 - '@testing-library/react@9.5.0(@types/react@18.3.27)(react-dom@16.14.0)(react@16.14.0)': + '@testing-library/react@9.5.0(@types/react@18.3.27)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)': dependencies: '@babel/runtime': 7.28.4 '@testing-library/dom': 6.16.0 @@ -16375,45 +16482,47 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@2.34.0(@typescript-eslint/parser@2.34.0)(eslint@9.39.2)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@2.34.0(@typescript-eslint/parser@2.34.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': dependencies: - '@typescript-eslint/experimental-utils': 2.34.0(eslint@9.39.2)(typescript@5.9.3) - '@typescript-eslint/parser': 2.34.0(eslint@9.39.2)(typescript@5.9.3) - eslint: 9.39.2 + '@typescript-eslint/experimental-utils': 2.34.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/parser': 2.34.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + eslint: 9.39.2(jiti@1.21.7) functional-red-black-tree: 1.0.1 regexpp: 3.2.0 tsutils: 3.21.0(typescript@5.9.3) + optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0)(eslint@9.39.2)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 5.62.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/parser': 5.62.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/scope-manager': 5.62.0 - '@typescript-eslint/type-utils': 5.62.0(eslint@9.39.2)(typescript@5.9.3) - '@typescript-eslint/utils': 5.62.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/type-utils': 5.62.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 5.62.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) debug: 4.4.3(supports-color@6.1.0) - eslint: 9.39.2 + eslint: 9.39.2(jiti@1.21.7) graphemer: 1.4.0 ignore: 5.3.2 natural-compare-lite: 1.4.0 semver: 7.7.3 tsutils: 3.21.0(typescript@5.9.3) + optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@8.49.0(@typescript-eslint/parser@8.49.0)(eslint@9.39.2)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.49.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/parser': 8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.49.0 - '@typescript-eslint/type-utils': 8.49.0(eslint@9.39.2)(typescript@5.9.3) - '@typescript-eslint/utils': 8.49.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.49.0 - eslint: 9.39.2 + eslint: 9.39.2(jiti@1.21.7) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.1.0(typescript@5.9.3) @@ -16421,55 +16530,57 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/experimental-utils@2.34.0(eslint@9.39.2)(typescript@5.9.3)': + '@typescript-eslint/experimental-utils@2.34.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': dependencies: '@types/json-schema': 7.0.15 '@typescript-eslint/typescript-estree': 2.34.0(typescript@5.9.3) - eslint: 9.39.2 + eslint: 9.39.2(jiti@1.21.7) eslint-scope: 5.1.1 eslint-utils: 2.1.0 transitivePeerDependencies: - supports-color - typescript - '@typescript-eslint/experimental-utils@5.62.0(eslint@9.39.2)(typescript@5.9.3)': + '@typescript-eslint/experimental-utils@5.62.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': dependencies: - '@typescript-eslint/utils': 5.62.0(eslint@9.39.2)(typescript@5.9.3) - eslint: 9.39.2 + '@typescript-eslint/utils': 5.62.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + eslint: 9.39.2(jiti@1.21.7) transitivePeerDependencies: - supports-color - typescript - '@typescript-eslint/parser@2.34.0(eslint@9.39.2)(typescript@5.9.3)': + '@typescript-eslint/parser@2.34.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': dependencies: '@types/eslint-visitor-keys': 1.0.0 - '@typescript-eslint/experimental-utils': 2.34.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/experimental-utils': 2.34.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/typescript-estree': 2.34.0(typescript@5.9.3) - eslint: 9.39.2 + eslint: 9.39.2(jiti@1.21.7) eslint-visitor-keys: 1.3.0 + optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@5.62.0(eslint@9.39.2)(typescript@5.9.3)': + '@typescript-eslint/parser@5.62.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.9.3) debug: 4.4.3(supports-color@6.1.0) - eslint: 9.39.2 + eslint: 9.39.2(jiti@1.21.7) + optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.49.0(eslint@9.39.2)(typescript@5.9.3)': + '@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.49.0 '@typescript-eslint/types': 8.49.0 '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.49.0 debug: 4.4.3(supports-color@6.1.0) - eslint: 9.39.2 + eslint: 9.39.2(jiti@1.21.7) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -16497,24 +16608,25 @@ snapshots: dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@5.62.0(eslint@9.39.2)(typescript@5.9.3)': + '@typescript-eslint/type-utils@5.62.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': dependencies: '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.9.3) - '@typescript-eslint/utils': 5.62.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/utils': 5.62.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) debug: 4.4.3(supports-color@6.1.0) - eslint: 9.39.2 + eslint: 9.39.2(jiti@1.21.7) tsutils: 3.21.0(typescript@5.9.3) + optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/type-utils@8.49.0(eslint@9.39.2)(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.49.0 '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.49.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/utils': 8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) debug: 4.4.3(supports-color@6.1.0) - eslint: 9.39.2 + eslint: 9.39.2(jiti@1.21.7) ts-api-utils: 2.1.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: @@ -16533,6 +16645,7 @@ snapshots: lodash: 4.17.21 semver: 7.7.3 tsutils: 3.21.0(typescript@5.9.3) + optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -16546,6 +16659,7 @@ snapshots: is-glob: 4.0.3 semver: 7.7.3 tsutils: 3.21.0(typescript@5.9.3) + optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -16565,28 +16679,28 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@5.62.0(eslint@9.39.2)(typescript@5.9.3)': + '@typescript-eslint/utils@5.62.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@1.21.7)) '@types/json-schema': 7.0.15 '@types/semver': 7.7.1 '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.9.3) - eslint: 9.39.2 + eslint: 9.39.2(jiti@1.21.7) eslint-scope: 5.1.1 semver: 7.7.3 transitivePeerDependencies: - supports-color - typescript - '@typescript-eslint/utils@8.49.0(eslint@9.39.2)(typescript@5.9.3)': + '@typescript-eslint/utils@8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@1.21.7)) '@typescript-eslint/scope-manager': 8.49.0 '@typescript-eslint/types': 8.49.0 '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) - eslint: 9.39.2 + eslint: 9.39.2(jiti@1.21.7) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -16610,7 +16724,7 @@ snapshots: '@use-gesture/core': 10.3.1 react: 18.3.1 - '@vitejs/plugin-react@4.7.0(vite@5.4.21)': + '@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@22.19.3))': dependencies: '@babel/core': 7.28.5 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) @@ -16629,11 +16743,12 @@ snapshots: chai: 5.3.3 tinyrainbow: 1.2.0 - '@vitest/mocker@2.1.9(vite@5.4.21)': + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@22.19.3))': dependencies: '@vitest/spy': 2.1.9 estree-walker: 3.0.3 magic-string: 0.30.21 + optionalDependencies: vite: 5.4.21(@types/node@22.19.3) '@vitest/pretty-format@2.1.9': @@ -16837,15 +16952,16 @@ snapshots: '@ai-sdk/ui-utils': 1.2.11(zod@3.25.76) '@opentelemetry/api': 1.9.0 jsondiffpatch: 0.6.0 - react: 18.3.1 zod: 3.25.76 + optionalDependencies: + react: 18.3.1 ajv-errors@1.0.1(ajv@6.12.6): dependencies: ajv: 6.12.6 ajv-formats@3.0.1(ajv@8.17.1): - dependencies: + optionalDependencies: ajv: 8.17.1 ajv-keywords@3.5.2(ajv@6.12.6): @@ -17149,11 +17265,12 @@ snapshots: axios@1.13.2: dependencies: - follow-redirects: 1.15.11(debug@4.4.3) + follow-redirects: 1.15.11(debug@4.4.3(supports-color@6.1.0)) form-data: 4.0.5 proxy-from-env: 1.1.0 transitivePeerDependencies: - debug + optional: true axobject-query@2.2.0: {} @@ -17165,13 +17282,13 @@ snapshots: esutils: 2.0.3 js-tokens: 3.0.2 - babel-eslint@10.0.3(eslint@9.39.2): + babel-eslint@10.0.3(eslint@9.39.2(jiti@1.21.7)): dependencies: '@babel/code-frame': 7.27.1 '@babel/parser': 7.28.5 '@babel/traverse': 7.28.5 '@babel/types': 7.28.5 - eslint: 9.39.2 + eslint: 9.39.2(jiti@1.21.7) eslint-visitor-keys: 1.3.0 resolve: 1.12.2 transitivePeerDependencies: @@ -17437,6 +17554,9 @@ snapshots: binary-extensions@2.3.0: {} + bindings@1.2.1: + optional: true + bindings@1.5.0: dependencies: file-uri-to-path: 1.0.0 @@ -17525,6 +17645,10 @@ snapshots: brorand@1.1.0: {} + brotli@1.3.3: + dependencies: + base64-js: 1.5.1 + browser-process-hrtime@1.0.0: {} browser-resolve@1.11.3: @@ -17602,6 +17726,8 @@ snapshots: dependencies: node-int64: 0.4.0 + bson@1.1.6: {} + buffer-equal-constant-time@1.0.1: {} buffer-from@1.1.2: {} @@ -17969,12 +18095,12 @@ snapshots: clsx@2.1.1: {} - cmdk@1.1.1(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1): + cmdk@1.1.1(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) - '@radix-ui/react-dialog': 1.1.15(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-id': 1.1.1(@types/react@18.3.27)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.4(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) transitivePeerDependencies: @@ -18198,13 +18324,13 @@ snapshots: safe-buffer: 5.2.1 sha.js: 2.4.12 - create-jest@29.7.0(@types/node@22.19.3)(ts-node@10.9.2): + create-jest@29.7.0(@types/node@22.19.3)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@22.19.3)(ts-node@10.9.2) + jest-config: 29.7.0(@types/node@22.19.3)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -18219,9 +18345,9 @@ snapshots: dependencies: cross-spawn: 7.0.6 - cross-fetch@4.1.0: + cross-fetch@4.1.0(encoding@0.1.13): dependencies: - node-fetch: 2.7.0 + node-fetch: 2.7.0(encoding@0.1.13) transitivePeerDependencies: - encoding @@ -18786,16 +18912,19 @@ snapshots: debug@2.6.9(supports-color@6.1.0): dependencies: ms: 2.0.0 + optionalDependencies: supports-color: 6.1.0 debug@3.2.7(supports-color@6.1.0): dependencies: ms: 2.1.3 + optionalDependencies: supports-color: 6.1.0 debug@4.4.3(supports-color@6.1.0): dependencies: ms: 2.1.3 + optionalDependencies: supports-color: 6.1.0 decamelize@1.2.0: {} @@ -18810,7 +18939,9 @@ snapshots: dependencies: mimic-response: 3.1.0 - dedent@1.7.0: {} + dedent@1.7.0(babel-plugin-macros@3.1.0): + optionalDependencies: + babel-plugin-macros: 3.1.0 deep-eql@5.0.2: {} @@ -19313,37 +19444,39 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-config-react-app@5.2.1(@typescript-eslint/eslint-plugin@2.34.0)(@typescript-eslint/parser@2.34.0)(babel-eslint@10.0.3)(eslint-plugin-flowtype@3.13.0)(eslint-plugin-import@2.18.2)(eslint-plugin-jsx-a11y@6.2.3)(eslint-plugin-react-hooks@1.7.0)(eslint-plugin-react@7.16.0)(eslint@9.39.2)(typescript@5.9.3): + eslint-config-react-app@5.2.1(@typescript-eslint/eslint-plugin@2.34.0(@typescript-eslint/parser@2.34.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(@typescript-eslint/parser@2.34.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(babel-eslint@10.0.3(eslint@9.39.2(jiti@1.21.7)))(eslint-plugin-flowtype@3.13.0(eslint@9.39.2(jiti@1.21.7)))(eslint-plugin-import@2.18.2(@typescript-eslint/parser@2.34.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7)))(eslint-plugin-jsx-a11y@6.2.3(eslint@9.39.2(jiti@1.21.7)))(eslint-plugin-react-hooks@1.7.0(eslint@9.39.2(jiti@1.21.7)))(eslint-plugin-react@7.16.0(eslint@9.39.2(jiti@1.21.7)))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 2.34.0(@typescript-eslint/parser@2.34.0)(eslint@9.39.2)(typescript@5.9.3) - '@typescript-eslint/parser': 2.34.0(eslint@9.39.2)(typescript@5.9.3) - babel-eslint: 10.0.3(eslint@9.39.2) + '@typescript-eslint/eslint-plugin': 2.34.0(@typescript-eslint/parser@2.34.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/parser': 2.34.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + babel-eslint: 10.0.3(eslint@9.39.2(jiti@1.21.7)) confusing-browser-globals: 1.0.11 - eslint: 9.39.2 - eslint-plugin-flowtype: 3.13.0(eslint@9.39.2) - eslint-plugin-import: 2.18.2(@typescript-eslint/parser@2.34.0)(eslint@9.39.2) - eslint-plugin-jsx-a11y: 6.2.3(eslint@9.39.2) - eslint-plugin-react: 7.16.0(eslint@9.39.2) - eslint-plugin-react-hooks: 1.7.0(eslint@9.39.2) + eslint: 9.39.2(jiti@1.21.7) + eslint-plugin-flowtype: 3.13.0(eslint@9.39.2(jiti@1.21.7)) + eslint-plugin-import: 2.18.2(@typescript-eslint/parser@2.34.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7)) + eslint-plugin-jsx-a11y: 6.2.3(eslint@9.39.2(jiti@1.21.7)) + eslint-plugin-react: 7.16.0(eslint@9.39.2(jiti@1.21.7)) + eslint-plugin-react-hooks: 1.7.0(eslint@9.39.2(jiti@1.21.7)) + optionalDependencies: typescript: 5.9.3 - eslint-config-react-app@7.0.1(@babel/plugin-syntax-flow@7.27.1)(@babel/plugin-transform-react-jsx@7.27.1)(eslint@9.39.2)(jest@29.7.0)(typescript@5.9.3): + eslint-config-react-app@7.0.1(@babel/plugin-syntax-flow@7.27.1(@babel/core@7.28.5))(@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.28.5))(eslint@9.39.2(jiti@1.21.7))(jest@29.7.0(@types/node@22.19.3)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)))(typescript@5.9.3): dependencies: '@babel/core': 7.28.5 - '@babel/eslint-parser': 7.28.5(@babel/core@7.28.5)(eslint@9.39.2) + '@babel/eslint-parser': 7.28.5(@babel/core@7.28.5)(eslint@9.39.2(jiti@1.21.7)) '@rushstack/eslint-patch': 1.15.0 - '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@9.39.2)(typescript@5.9.3) - '@typescript-eslint/parser': 5.62.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/parser': 5.62.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) babel-preset-react-app: 10.1.0 confusing-browser-globals: 1.0.11 - eslint: 9.39.2 - eslint-plugin-flowtype: 8.0.3(@babel/plugin-syntax-flow@7.27.1)(@babel/plugin-transform-react-jsx@7.27.1)(eslint@9.39.2) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0)(eslint@9.39.2) - eslint-plugin-jest: 25.7.0(@typescript-eslint/eslint-plugin@5.62.0)(eslint@9.39.2)(jest@29.7.0)(typescript@5.9.3) - eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2) - eslint-plugin-react: 7.37.5(eslint@9.39.2) - eslint-plugin-react-hooks: 4.6.2(eslint@9.39.2) - eslint-plugin-testing-library: 5.11.1(eslint@9.39.2)(typescript@5.9.3) + eslint: 9.39.2(jiti@1.21.7) + eslint-plugin-flowtype: 8.0.3(@babel/plugin-syntax-flow@7.27.1(@babel/core@7.28.5))(@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.28.5))(eslint@9.39.2(jiti@1.21.7)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7)) + eslint-plugin-jest: 25.7.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(jest@29.7.0(@types/node@22.19.3)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)))(typescript@5.9.3) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@1.21.7)) + eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@1.21.7)) + eslint-plugin-react-hooks: 4.6.2(eslint@9.39.2(jiti@1.21.7)) + eslint-plugin-testing-library: 5.11.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: - '@babel/plugin-syntax-flow' @@ -19361,9 +19494,9 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-loader@3.0.2(eslint@9.39.2)(webpack@4.41.2): + eslint-loader@3.0.2(eslint@9.39.2(jiti@1.21.7))(webpack@4.41.2): dependencies: - eslint: 9.39.2 + eslint: 9.39.2(jiti@1.21.7) fs-extra: 8.1.0 loader-fs-cache: 1.0.3 loader-utils: 1.4.2 @@ -19371,70 +19504,72 @@ snapshots: schema-utils: 2.7.1 webpack: 4.41.2 - eslint-module-utils@2.12.1(@typescript-eslint/parser@2.34.0)(eslint-import-resolver-node@0.3.9)(eslint@9.39.2): + eslint-module-utils@2.12.1(@typescript-eslint/parser@2.34.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@1.21.7)): dependencies: - '@typescript-eslint/parser': 2.34.0(eslint@9.39.2)(typescript@5.9.3) debug: 3.2.7(supports-color@6.1.0) - eslint: 9.39.2 + optionalDependencies: + '@typescript-eslint/parser': 2.34.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + eslint: 9.39.2(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint@9.39.2): + eslint-module-utils@2.12.1(@typescript-eslint/parser@5.62.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@1.21.7)): dependencies: - '@typescript-eslint/parser': 5.62.0(eslint@9.39.2)(typescript@5.9.3) debug: 3.2.7(supports-color@6.1.0) - eslint: 9.39.2 + optionalDependencies: + '@typescript-eslint/parser': 5.62.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + eslint: 9.39.2(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color - eslint-plugin-flowtype@3.13.0(eslint@9.39.2): + eslint-plugin-flowtype@3.13.0(eslint@9.39.2(jiti@1.21.7)): dependencies: - eslint: 9.39.2 + eslint: 9.39.2(jiti@1.21.7) lodash: 4.17.21 - eslint-plugin-flowtype@8.0.3(@babel/plugin-syntax-flow@7.27.1)(@babel/plugin-transform-react-jsx@7.27.1)(eslint@9.39.2): + eslint-plugin-flowtype@8.0.3(@babel/plugin-syntax-flow@7.27.1(@babel/core@7.28.5))(@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.28.5))(eslint@9.39.2(jiti@1.21.7)): dependencies: - '@babel/plugin-syntax-flow': 7.27.1(@babel/core@7.7.4) - '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.7.4) - eslint: 9.39.2 + '@babel/plugin-syntax-flow': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.28.5) + eslint: 9.39.2(jiti@1.21.7) lodash: 4.17.21 string-natural-compare: 3.0.1 - eslint-plugin-import@2.18.2(@typescript-eslint/parser@2.34.0)(eslint@9.39.2): + eslint-plugin-import@2.18.2(@typescript-eslint/parser@2.34.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7)): dependencies: - '@typescript-eslint/parser': 2.34.0(eslint@9.39.2)(typescript@5.9.3) array-includes: 3.1.9 contains-path: 0.1.0 debug: 2.6.9(supports-color@6.1.0) doctrine: 1.5.0 - eslint: 9.39.2 + eslint: 9.39.2(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@2.34.0)(eslint-import-resolver-node@0.3.9)(eslint@9.39.2) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@2.34.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@1.21.7)) has: 1.0.4 minimatch: 3.1.2 object.values: 1.2.1 read-pkg-up: 2.0.0 resolve: 1.12.2 + optionalDependencies: + '@typescript-eslint/parser': 2.34.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0)(eslint@9.39.2): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7)): dependencies: '@rtsao/scc': 1.1.0 - '@typescript-eslint/parser': 5.62.0(eslint@9.39.2)(typescript@5.9.3) array-includes: 3.1.9 array.prototype.findlastindex: 1.2.6 array.prototype.flat: 1.3.3 array.prototype.flatmap: 1.3.3 debug: 3.2.7(supports-color@6.1.0) doctrine: 2.1.0 - eslint: 9.39.2 + eslint: 9.39.2(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint@9.39.2) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@5.62.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@1.21.7)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -19445,22 +19580,25 @@ snapshots: semver: 6.3.1 string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 5.62.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-jest@25.7.0(@typescript-eslint/eslint-plugin@5.62.0)(eslint@9.39.2)(jest@29.7.0)(typescript@5.9.3): + eslint-plugin-jest@25.7.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(jest@29.7.0(@types/node@22.19.3)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@9.39.2)(typescript@5.9.3) - '@typescript-eslint/experimental-utils': 5.62.0(eslint@9.39.2)(typescript@5.9.3) - eslint: 9.39.2 - jest: 29.7.0(@types/node@22.19.3)(ts-node@10.9.2) + '@typescript-eslint/experimental-utils': 5.62.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + eslint: 9.39.2(jiti@1.21.7) + optionalDependencies: + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + jest: 29.7.0(@types/node@22.19.3)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)) transitivePeerDependencies: - supports-color - typescript - eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.2): + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.2(jiti@1.21.7)): dependencies: aria-query: 5.3.2 array-includes: 3.1.9 @@ -19470,7 +19608,7 @@ snapshots: axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 9.39.2 + eslint: 9.39.2(jiti@1.21.7) hasown: 2.0.2 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -19479,7 +19617,7 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-jsx-a11y@6.2.3(eslint@9.39.2): + eslint-plugin-jsx-a11y@6.2.3(eslint@9.39.2(jiti@1.21.7)): dependencies: '@babel/runtime': 7.28.4 aria-query: 3.0.0 @@ -19488,31 +19626,31 @@ snapshots: axobject-query: 2.2.0 damerau-levenshtein: 1.0.8 emoji-regex: 7.0.3 - eslint: 9.39.2 + eslint: 9.39.2(jiti@1.21.7) has: 1.0.4 jsx-ast-utils: 2.4.1 - eslint-plugin-react-hooks@1.7.0(eslint@9.39.2): + eslint-plugin-react-hooks@1.7.0(eslint@9.39.2(jiti@1.21.7)): dependencies: - eslint: 9.39.2 + eslint: 9.39.2(jiti@1.21.7) - eslint-plugin-react-hooks@4.6.2(eslint@9.39.2): + eslint-plugin-react-hooks@4.6.2(eslint@9.39.2(jiti@1.21.7)): dependencies: - eslint: 9.39.2 + eslint: 9.39.2(jiti@1.21.7) - eslint-plugin-react-hooks@5.2.0(eslint@9.39.2): + eslint-plugin-react-hooks@5.2.0(eslint@9.39.2(jiti@1.21.7)): dependencies: - eslint: 9.39.2 + eslint: 9.39.2(jiti@1.21.7) - eslint-plugin-react-refresh@0.4.25(eslint@9.39.2): + eslint-plugin-react-refresh@0.4.25(eslint@9.39.2(jiti@1.21.7)): dependencies: - eslint: 9.39.2 + eslint: 9.39.2(jiti@1.21.7) - eslint-plugin-react@7.16.0(eslint@9.39.2): + eslint-plugin-react@7.16.0(eslint@9.39.2(jiti@1.21.7)): dependencies: array-includes: 3.1.9 doctrine: 2.1.0 - eslint: 9.39.2 + eslint: 9.39.2(jiti@1.21.7) has: 1.0.4 jsx-ast-utils: 2.4.1 object.entries: 1.1.9 @@ -19521,7 +19659,7 @@ snapshots: prop-types: 15.8.1 resolve: 1.12.2 - eslint-plugin-react@7.37.5(eslint@9.39.2): + eslint-plugin-react@7.37.5(eslint@9.39.2(jiti@1.21.7)): dependencies: array-includes: 3.1.9 array.prototype.findlast: 1.2.5 @@ -19529,7 +19667,7 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.2.2 - eslint: 9.39.2 + eslint: 9.39.2(jiti@1.21.7) estraverse: 5.3.0 hasown: 2.0.2 jsx-ast-utils: 3.3.5 @@ -19543,10 +19681,10 @@ snapshots: string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 - eslint-plugin-testing-library@5.11.1(eslint@9.39.2)(typescript@5.9.3): + eslint-plugin-testing-library@5.11.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3): dependencies: - '@typescript-eslint/utils': 5.62.0(eslint@9.39.2)(typescript@5.9.3) - eslint: 9.39.2 + '@typescript-eslint/utils': 5.62.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + eslint: 9.39.2(jiti@1.21.7) transitivePeerDependencies: - supports-color - typescript @@ -19578,9 +19716,9 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@9.39.2: + eslint@9.39.2(jiti@1.21.7): dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@1.21.7)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.21.1 '@eslint/config-helpers': 0.4.2 @@ -19614,6 +19752,8 @@ snapshots: minimatch: 3.1.2 natural-compare: 1.4.0 optionator: 0.9.4 + optionalDependencies: + jiti: 1.21.7 transitivePeerDependencies: - supports-color @@ -19682,9 +19822,9 @@ snapshots: md5.js: 1.3.5 safe-buffer: 5.2.1 - exa-js@1.10.2(ws@8.18.3): + exa-js@1.10.2(encoding@0.1.13)(ws@8.18.3): dependencies: - cross-fetch: 4.1.0 + cross-fetch: 4.1.0(encoding@0.1.13) dotenv: 16.4.7 openai: 5.23.2(ws@8.18.3)(zod@3.25.76) zod: 3.25.76 @@ -19919,7 +20059,7 @@ snapshots: bser: 2.1.1 fdir@6.5.0(picomatch@4.0.3): - dependencies: + optionalDependencies: picomatch: 4.0.3 fflate@0.4.8: {} @@ -20044,8 +20184,8 @@ snapshots: inherits: 2.0.4 readable-stream: 2.3.8 - follow-redirects@1.15.11(debug@4.4.3): - dependencies: + follow-redirects@1.15.11(debug@4.4.3(supports-color@6.1.0)): + optionalDependencies: debug: 4.4.3(supports-color@6.1.0) for-each@0.3.5: @@ -20062,12 +20202,11 @@ snapshots: forever-agent@0.6.1: {} - fork-ts-checker-webpack-plugin@3.1.1(eslint@9.39.2)(typescript@5.9.3)(webpack@4.41.2): + fork-ts-checker-webpack-plugin@3.1.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)(webpack@4.41.2): dependencies: babel-code-frame: 6.26.0 chalk: 2.4.2 chokidar: 3.6.0 - eslint: 9.39.2 micromatch: 3.1.10(supports-color@6.1.0) minimatch: 3.1.2 semver: 5.7.2 @@ -20075,6 +20214,8 @@ snapshots: typescript: 5.9.3 webpack: 4.41.2 worker-rpc: 0.1.1 + optionalDependencies: + eslint: 9.39.2(jiti@1.21.7) transitivePeerDependencies: - supports-color @@ -20109,13 +20250,14 @@ snapshots: dependencies: map-cache: 0.2.2 - framer-motion@12.23.26(react-dom@18.3.1)(react@18.3.1): + framer-motion@12.23.26(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: motion-dom: 12.23.23 motion-utils: 12.23.6 + tslib: 2.8.1 + optionalDependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - tslib: 2.8.1 fresh@0.5.2: {} @@ -20198,20 +20340,20 @@ snapshots: wide-align: 1.1.5 optional: true - gaxios@6.7.1: + gaxios@6.7.1(encoding@0.1.13): dependencies: extend: 3.0.2 https-proxy-agent: 7.0.6 is-stream: 2.0.1 - node-fetch: 2.7.0 + node-fetch: 2.7.0(encoding@0.1.13) uuid: 9.0.1 transitivePeerDependencies: - encoding - supports-color - gcp-metadata@6.1.1: + gcp-metadata@6.1.1(encoding@0.1.13): dependencies: - gaxios: 6.7.1 + gaxios: 6.7.1(encoding@0.1.13) google-logging-utils: 0.0.2 json-bigint: 1.0.0 transitivePeerDependencies: @@ -20355,13 +20497,13 @@ snapshots: glsl-noise@0.0.0: {} - google-auth-library@9.15.1: + google-auth-library@9.15.1(encoding@0.1.13): dependencies: base64-js: 1.5.1 ecdsa-sig-formatter: 1.0.11 - gaxios: 6.7.1 - gcp-metadata: 6.1.1 - gtoken: 7.1.0 + gaxios: 6.7.1(encoding@0.1.13) + gcp-metadata: 6.1.1(encoding@0.1.13) + gtoken: 7.1.0(encoding@0.1.13) jws: 4.0.1 transitivePeerDependencies: - encoding @@ -20429,11 +20571,23 @@ snapshots: events: 3.3.0 graphology-types: 0.24.8 + groq-sdk@0.37.0(encoding@0.1.13): + dependencies: + '@types/node': 18.19.130 + '@types/node-fetch': 2.6.13 + abort-controller: 3.0.0 + agentkeepalive: 4.6.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0(encoding@0.1.13) + transitivePeerDependencies: + - encoding + growly@1.3.0: {} - gtoken@7.1.0: + gtoken@7.1.0(encoding@0.1.13): dependencies: - gaxios: 6.7.1 + gaxios: 6.7.1(encoding@0.1.13) jws: 4.0.1 transitivePeerDependencies: - encoding @@ -20749,9 +20903,9 @@ snapshots: - supports-color optional: true - http-proxy-middleware@0.19.1(debug@4.4.3)(supports-color@6.1.0): + http-proxy-middleware@0.19.1(debug@4.4.3(supports-color@6.1.0))(supports-color@6.1.0): dependencies: - http-proxy: 1.18.1(debug@4.4.3) + http-proxy: 1.18.1(debug@4.4.3(supports-color@6.1.0)) is-glob: 4.0.3 lodash: 4.17.21 micromatch: 3.1.10(supports-color@6.1.0) @@ -20759,10 +20913,10 @@ snapshots: - debug - supports-color - http-proxy@1.18.1(debug@4.4.3): + http-proxy@1.18.1(debug@4.4.3(supports-color@6.1.0)): dependencies: eventemitter3: 4.0.7 - follow-redirects: 1.15.11(debug@4.4.3) + follow-redirects: 1.15.11(debug@4.4.3(supports-color@6.1.0)) requires-port: 1.0.0 transitivePeerDependencies: - debug @@ -20903,6 +21057,8 @@ snapshots: strip-ansi: 5.2.0 through: 2.3.8 + int53@0.2.4: {} + internal-ip@4.3.0: dependencies: default-gateway: 4.2.0 @@ -21217,10 +21373,6 @@ snapshots: isomorphic-timers-promises@1.0.1: {} - isows@1.0.7(ws@8.18.3): - dependencies: - ws: 8.18.3 - isstream@0.1.2: {} istanbul-lib-coverage@2.0.5: {} @@ -21326,7 +21478,7 @@ snapshots: jest-util: 29.7.0 p-limit: 3.1.0 - jest-circus@29.7.0: + jest-circus@29.7.0(babel-plugin-macros@3.1.0): dependencies: '@jest/environment': 29.7.0 '@jest/expect': 29.7.0 @@ -21335,7 +21487,7 @@ snapshots: '@types/node': 22.19.3 chalk: 4.1.2 co: 4.6.0 - dedent: 1.7.0 + dedent: 1.7.0(babel-plugin-macros@3.1.0) is-generator-fn: 2.1.0 jest-each: 29.7.0 jest-matcher-utils: 29.7.0 @@ -21372,16 +21524,16 @@ snapshots: - supports-color - utf-8-validate - jest-cli@29.7.0(@types/node@22.19.3)(ts-node@10.9.2): + jest-cli@29.7.0(@types/node@22.19.3)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2) + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@22.19.3)(ts-node@10.9.2) + create-jest: 29.7.0(@types/node@22.19.3)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@22.19.3)(ts-node@10.9.2) + jest-config: 29.7.0(@types/node@22.19.3)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -21415,19 +21567,18 @@ snapshots: - supports-color - utf-8-validate - jest-config@29.7.0(@types/node@22.19.3)(ts-node@10.9.2): + jest-config@29.7.0(@types/node@22.19.3)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)): dependencies: '@babel/core': 7.28.5 '@jest/test-sequencer': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.19.3 babel-jest: 29.7.0(@babel/core@7.28.5) chalk: 4.1.2 ci-info: 3.9.0 deepmerge: 4.3.1 glob: 7.2.3 graceful-fs: 4.2.11 - jest-circus: 29.7.0 + jest-circus: 29.7.0(babel-plugin-macros@3.1.0) jest-environment-node: 29.7.0 jest-get-type: 29.6.3 jest-regex-util: 29.6.3 @@ -21440,6 +21591,8 @@ snapshots: pretty-format: 29.7.0 slash: 3.0.0 strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 22.19.3 ts-node: 10.9.2(@types/node@22.19.3)(typescript@5.9.3) transitivePeerDependencies: - babel-plugin-macros @@ -21646,11 +21799,11 @@ snapshots: jest-util: 29.7.0 jest-pnp-resolver@1.2.3(jest-resolve@24.9.0): - dependencies: + optionalDependencies: jest-resolve: 24.9.0 jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): - dependencies: + optionalDependencies: jest-resolve: 29.7.0 jest-regex-util@24.9.0: {} @@ -21946,12 +22099,12 @@ snapshots: - supports-color - utf-8-validate - jest@29.7.0(@types/node@22.19.3)(ts-node@10.9.2): + jest@29.7.0(@types/node@22.19.3)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2) + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)) '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@22.19.3)(ts-node@10.9.2) + jest-cli: 29.7.0(@types/node@22.19.3)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -22162,21 +22315,24 @@ snapshots: kleur@3.0.3: {} - langchain@0.3.36(@langchain/core@0.3.79)(cheerio@1.1.2)(openai@4.104.0)(ws@8.18.3): + langchain@0.3.36(@langchain/core@0.3.79(@opentelemetry/api@1.9.0)(openai@4.104.0(encoding@0.1.13)(ws@8.18.3)(zod@3.25.76)))(@opentelemetry/api@1.9.0)(axios@1.13.2)(cheerio@1.1.2)(handlebars@4.7.8)(openai@4.104.0(encoding@0.1.13)(ws@8.18.3)(zod@3.25.76))(ws@8.18.3): dependencies: - '@langchain/core': 0.3.79(openai@6.10.0) - '@langchain/openai': 0.6.16(@langchain/core@0.3.79)(ws@8.18.3) - '@langchain/textsplitters': 0.1.0(@langchain/core@0.3.79) - cheerio: 1.1.2 + '@langchain/core': 0.3.79(@opentelemetry/api@1.9.0)(openai@6.10.0(ws@8.18.3)(zod@4.2.0)) + '@langchain/openai': 0.6.16(@langchain/core@0.3.79(@opentelemetry/api@1.9.0)(openai@4.104.0(encoding@0.1.13)(ws@8.18.3)(zod@3.25.76)))(ws@8.18.3) + '@langchain/textsplitters': 0.1.0(@langchain/core@0.3.79(@opentelemetry/api@1.9.0)(openai@4.104.0(encoding@0.1.13)(ws@8.18.3)(zod@3.25.76))) js-tiktoken: 1.0.21 js-yaml: 4.1.1 jsonpointer: 5.0.1 - langsmith: 0.3.87(openai@4.104.0) + langsmith: 0.3.87(@opentelemetry/api@1.9.0)(openai@4.104.0(encoding@0.1.13)(ws@8.18.3)(zod@3.25.76)) openapi-types: 12.1.3 p-retry: 4.6.2 uuid: 10.0.0 yaml: 2.8.2 zod: 3.25.76 + optionalDependencies: + axios: 1.13.2 + cheerio: 1.1.2 + handlebars: 4.7.8 transitivePeerDependencies: - '@opentelemetry/api' - '@opentelemetry/exporter-trace-otlp-proto' @@ -22184,25 +22340,29 @@ snapshots: - openai - ws - langsmith@0.3.87(openai@4.104.0): + langsmith@0.3.87(@opentelemetry/api@1.9.0)(openai@4.104.0(encoding@0.1.13)(ws@8.18.3)(zod@3.25.76)): dependencies: '@types/uuid': 10.0.0 chalk: 4.1.2 console-table-printer: 2.15.0 - openai: 4.104.0(ws@8.18.3)(zod@3.25.76) p-queue: 6.6.2 semver: 7.7.3 uuid: 10.0.0 + optionalDependencies: + '@opentelemetry/api': 1.9.0 + openai: 4.104.0(encoding@0.1.13)(ws@8.18.3)(zod@3.25.76) - langsmith@0.3.87(openai@6.10.0): + langsmith@0.3.87(@opentelemetry/api@1.9.0)(openai@6.10.0(ws@8.18.3)(zod@4.2.0)): dependencies: '@types/uuid': 10.0.0 chalk: 4.1.2 console-table-printer: 2.15.0 - openai: 6.10.0(ws@8.18.3)(zod@4.2.0) p-queue: 6.6.2 semver: 7.7.3 uuid: 10.0.0 + optionalDependencies: + '@opentelemetry/api': 1.9.0 + openai: 6.10.0(ws@8.18.3)(zod@4.2.0) language-subtag-registry@0.3.23: {} @@ -22394,6 +22554,11 @@ snapshots: lz-string@1.5.0: {} + lzo@0.4.11: + dependencies: + bindings: 1.2.1 + optional: true + maath@0.10.8(@types/three@0.182.0)(three@0.180.0): dependencies: '@types/three': 0.182.0 @@ -23136,9 +23301,11 @@ snapshots: node-domexception@1.0.0: {} - node-fetch@2.7.0: + node-fetch@2.7.0(encoding@0.1.13): dependencies: whatwg-url: 5.0.0 + optionalDependencies: + encoding: 0.1.13 node-forge@0.10.0: {} @@ -23313,6 +23480,8 @@ snapshots: object-path@0.11.4: {} + object-stream@0.0.1: {} + object-visit@1.0.1: dependencies: isobject: 3.0.1 @@ -23394,7 +23563,7 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 - openai@4.104.0(ws@8.18.3)(zod@3.25.76): + openai@4.104.0(encoding@0.1.13)(ws@8.18.3)(zod@3.25.76): dependencies: '@types/node': 18.19.130 '@types/node-fetch': 2.6.13 @@ -23402,24 +23571,25 @@ snapshots: agentkeepalive: 4.6.0 form-data-encoder: 1.7.2 formdata-node: 4.4.1 - node-fetch: 2.7.0 + node-fetch: 2.7.0(encoding@0.1.13) + optionalDependencies: ws: 8.18.3 zod: 3.25.76 transitivePeerDependencies: - encoding openai@5.12.2(ws@8.18.3)(zod@3.25.76): - dependencies: + optionalDependencies: ws: 8.18.3 zod: 3.25.76 openai@5.23.2(ws@8.18.3)(zod@3.25.76): - dependencies: + optionalDependencies: ws: 8.18.3 zod: 3.25.76 openai@6.10.0(ws@8.18.3)(zod@4.2.0): - dependencies: + optionalDependencies: ws: 8.18.3 zod: 4.2.0 @@ -23562,6 +23732,21 @@ snapshots: dependencies: callsites: 3.1.0 + parquetjs@0.11.2: + dependencies: + brotli: 1.3.3 + bson: 1.1.6 + int53: 0.2.4 + object-stream: 0.0.1 + snappyjs: 0.6.1 + thrift: 0.11.0 + varint: 5.0.2 + optionalDependencies: + lzo: 0.4.11 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + parse-asn1@5.1.9: dependencies: asn1.js: 4.10.1 @@ -23903,11 +24088,13 @@ snapshots: cosmiconfig: 5.2.1 import-cwd: 2.1.0 - postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6): + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6)(yaml@2.8.2): dependencies: - jiti: 1.21.7 lilconfig: 3.1.3 + optionalDependencies: + jiti: 1.21.7 postcss: 8.5.6 + yaml: 2.8.2 postcss-loader@3.0.0: dependencies: @@ -24294,7 +24481,7 @@ snapshots: process@0.11.10: {} promise-inflight@1.0.1(bluebird@3.7.2): - dependencies: + optionalDependencies: bluebird: 3.7.2 promise-retry@2.0.1: @@ -24334,7 +24521,8 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 - proxy-from-env@1.1.0: {} + proxy-from-env@1.1.0: + optional: true prr@1.0.1: {} @@ -24439,7 +24627,7 @@ snapshots: regenerator-runtime: 0.13.11 whatwg-fetch: 3.6.20 - react-dev-utils@10.2.1(eslint@9.39.2)(typescript@5.9.3)(webpack@4.41.2): + react-dev-utils@10.2.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)(webpack@4.41.2): dependencies: '@babel/code-frame': 7.8.3 address: 1.1.2 @@ -24450,7 +24638,7 @@ snapshots: escape-string-regexp: 2.0.0 filesize: 6.0.1 find-up: 4.1.0 - fork-ts-checker-webpack-plugin: 3.1.1(eslint@9.39.2)(typescript@5.9.3)(webpack@4.41.2) + fork-ts-checker-webpack-plugin: 3.1.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)(webpack@4.41.2) global-modules: 2.0.0 globby: 8.0.2 gzip-size: 5.1.1 @@ -24465,8 +24653,9 @@ snapshots: shell-quote: 1.7.2 strip-ansi: 6.0.0 text-table: 0.2.0 - typescript: 5.9.3 webpack: 4.41.2 + optionalDependencies: + typescript: 5.9.3 transitivePeerDependencies: - eslint - supports-color @@ -24506,9 +24695,10 @@ snapshots: react-lowlight@3.1.0(highlight.js@11.11.1)(react@18.3.1): dependencies: - highlight.js: 11.11.1 lowlight: 2.9.0 react: 18.3.1 + optionalDependencies: + highlight.js: 11.11.1 react-markdown@9.1.0(@types/react@18.3.27)(react@18.3.1): dependencies: @@ -24528,15 +24718,15 @@ snapshots: transitivePeerDependencies: - supports-color - react-mermaid2@0.1.4(@testing-library/dom@10.4.1)(@types/react@18.3.27)(eslint@9.39.2)(typescript@5.9.3): + react-mermaid2@0.1.4(@testing-library/dom@10.4.1)(@types/react@18.3.27)(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3): dependencies: '@testing-library/jest-dom': 4.2.4 - '@testing-library/react': 9.5.0(@types/react@18.3.27)(react-dom@16.14.0)(react@16.14.0) + '@testing-library/react': 9.5.0(@types/react@18.3.27)(react-dom@16.14.0(react@16.14.0))(react@16.14.0) '@testing-library/user-event': 7.2.1(@testing-library/dom@10.4.1) mermaid: 8.14.0 react: 16.14.0 react-dom: 16.14.0(react@16.14.0) - react-scripts: 3.3.0(eslint@9.39.2)(react@16.14.0)(typescript@5.9.3) + react-scripts: 3.3.0(eslint@9.39.2(jiti@1.21.7))(react@16.14.0)(typescript@5.9.3) transitivePeerDependencies: - '@testing-library/dom' - '@types/react' @@ -24562,37 +24752,40 @@ snapshots: react-redux@9.2.0(@types/react@18.3.27)(react@18.3.1)(redux@5.0.1): dependencies: - '@types/react': 18.3.27 '@types/use-sync-external-store': 0.0.6 react: 18.3.1 - redux: 5.0.1 use-sync-external-store: 1.6.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + redux: 5.0.1 react-refresh@0.17.0: {} react-remove-scroll-bar@2.3.8(@types/react@18.3.27)(react@18.3.1): dependencies: - '@types/react': 18.3.27 react: 18.3.1 react-style-singleton: 2.2.3(@types/react@18.3.27)(react@18.3.1) tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.27 react-remove-scroll@2.7.2(@types/react@18.3.27)(react@18.3.1): dependencies: - '@types/react': 18.3.27 react: 18.3.1 react-remove-scroll-bar: 2.3.8(@types/react@18.3.27)(react@18.3.1) react-style-singleton: 2.2.3(@types/react@18.3.27)(react@18.3.1) tslib: 2.8.1 use-callback-ref: 1.3.3(@types/react@18.3.27)(react@18.3.1) use-sidecar: 1.1.3(@types/react@18.3.27)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 - react-resizable-panels@2.1.9(react-dom@18.3.1)(react@18.3.1): + react-resizable-panels@2.1.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-router-dom@6.30.2(react-dom@18.3.1)(react@18.3.1): + react-router-dom@6.30.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@remix-run/router': 1.23.1 react: 18.3.1 @@ -24606,13 +24799,13 @@ snapshots: react-scan@0.0.4: {} - react-scripts@3.3.0(eslint@9.39.2)(react@16.14.0)(typescript@5.9.3): + react-scripts@3.3.0(eslint@9.39.2(jiti@1.21.7))(react@16.14.0)(typescript@5.9.3): dependencies: '@babel/core': 7.7.4 '@svgr/webpack': 4.3.3 - '@typescript-eslint/eslint-plugin': 2.34.0(@typescript-eslint/parser@2.34.0)(eslint@9.39.2)(typescript@5.9.3) - '@typescript-eslint/parser': 2.34.0(eslint@9.39.2)(typescript@5.9.3) - babel-eslint: 10.0.3(eslint@9.39.2) + '@typescript-eslint/eslint-plugin': 2.34.0(@typescript-eslint/parser@2.34.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/parser': 2.34.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + babel-eslint: 10.0.3(eslint@9.39.2(jiti@1.21.7)) babel-jest: 24.9.0(@babel/core@7.7.4) babel-loader: 8.0.6(@babel/core@7.7.4)(webpack@4.41.2) babel-plugin-named-asset-import: 0.3.8(@babel/core@7.7.4) @@ -24622,14 +24815,14 @@ snapshots: css-loader: 3.2.0(webpack@4.41.2) dotenv: 8.2.0 dotenv-expand: 5.1.0 - eslint: 9.39.2 - eslint-config-react-app: 5.2.1(@typescript-eslint/eslint-plugin@2.34.0)(@typescript-eslint/parser@2.34.0)(babel-eslint@10.0.3)(eslint-plugin-flowtype@3.13.0)(eslint-plugin-import@2.18.2)(eslint-plugin-jsx-a11y@6.2.3)(eslint-plugin-react-hooks@1.7.0)(eslint-plugin-react@7.16.0)(eslint@9.39.2)(typescript@5.9.3) - eslint-loader: 3.0.2(eslint@9.39.2)(webpack@4.41.2) - eslint-plugin-flowtype: 3.13.0(eslint@9.39.2) - eslint-plugin-import: 2.18.2(@typescript-eslint/parser@2.34.0)(eslint@9.39.2) - eslint-plugin-jsx-a11y: 6.2.3(eslint@9.39.2) - eslint-plugin-react: 7.16.0(eslint@9.39.2) - eslint-plugin-react-hooks: 1.7.0(eslint@9.39.2) + eslint: 9.39.2(jiti@1.21.7) + eslint-config-react-app: 5.2.1(@typescript-eslint/eslint-plugin@2.34.0(@typescript-eslint/parser@2.34.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(@typescript-eslint/parser@2.34.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(babel-eslint@10.0.3(eslint@9.39.2(jiti@1.21.7)))(eslint-plugin-flowtype@3.13.0(eslint@9.39.2(jiti@1.21.7)))(eslint-plugin-import@2.18.2(@typescript-eslint/parser@2.34.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7)))(eslint-plugin-jsx-a11y@6.2.3(eslint@9.39.2(jiti@1.21.7)))(eslint-plugin-react-hooks@1.7.0(eslint@9.39.2(jiti@1.21.7)))(eslint-plugin-react@7.16.0(eslint@9.39.2(jiti@1.21.7)))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + eslint-loader: 3.0.2(eslint@9.39.2(jiti@1.21.7))(webpack@4.41.2) + eslint-plugin-flowtype: 3.13.0(eslint@9.39.2(jiti@1.21.7)) + eslint-plugin-import: 2.18.2(@typescript-eslint/parser@2.34.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7)) + eslint-plugin-jsx-a11y: 6.2.3(eslint@9.39.2(jiti@1.21.7)) + eslint-plugin-react: 7.16.0(eslint@9.39.2(jiti@1.21.7)) + eslint-plugin-react-hooks: 1.7.0(eslint@9.39.2(jiti@1.21.7)) file-loader: 4.3.0(webpack@4.41.2) fs-extra: 8.1.0 html-webpack-plugin: 4.0.0-beta.5(webpack@4.41.2) @@ -24648,7 +24841,7 @@ snapshots: postcss-safe-parser: 4.0.1 react: 16.14.0 react-app-polyfill: 1.0.6 - react-dev-utils: 10.2.1(eslint@9.39.2)(typescript@5.9.3)(webpack@4.41.2) + react-dev-utils: 10.2.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)(webpack@4.41.2) resolve: 1.12.2 resolve-url-loader: 3.1.1 sass-loader: 8.0.0(webpack@4.41.2) @@ -24656,14 +24849,14 @@ snapshots: style-loader: 1.0.0(webpack@4.41.2) terser-webpack-plugin: 2.2.1(webpack@4.41.2) ts-pnp: 1.1.5(typescript@5.9.3) - typescript: 5.9.3 - url-loader: 2.3.0(file-loader@4.3.0)(webpack@4.41.2) + url-loader: 2.3.0(file-loader@4.3.0(webpack@4.41.2))(webpack@4.41.2) webpack: 4.41.2 webpack-dev-server: 3.9.0(webpack@4.41.2) webpack-manifest-plugin: 2.2.0(webpack@4.41.2) workbox-webpack-plugin: 4.3.1(webpack@4.41.2) optionalDependencies: fsevents: 2.1.2 + typescript: 5.9.3 transitivePeerDependencies: - bluebird - bufferutil @@ -24680,10 +24873,11 @@ snapshots: react-style-singleton@2.2.3(@types/react@18.3.27)(react@18.3.1): dependencies: - '@types/react': 18.3.27 get-nonce: 1.0.1 react: 18.3.1 tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.27 react-syntax-highlighter@15.6.6(react@18.3.1): dependencies: @@ -24695,12 +24889,13 @@ snapshots: react: 18.3.1 refractor: 3.6.0 - react-use-measure@2.1.7(react-dom@18.3.1)(react@18.3.1): + react-use-measure@2.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 + optionalDependencies: react-dom: 18.3.1(react@18.3.1) - react-window@1.8.11(react-dom@18.3.1)(react@18.3.1): + react-window@1.8.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.28.4 memoize-one: 5.2.1 @@ -24771,11 +24966,11 @@ snapshots: dependencies: picomatch: 2.3.1 - reagraph@4.30.7(@types/react@18.3.27)(@types/three@0.182.0)(graphology-types@0.24.8)(immer@10.2.0)(react-dom@18.3.1)(react@18.3.1): + reagraph@4.30.7(@types/react@18.3.27)(@types/three@0.182.0)(graphology-types@0.24.8)(immer@10.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)): dependencies: - '@react-spring/three': 10.0.3(@react-three/fiber@9.3.0)(react@18.3.1)(three@0.180.0) - '@react-three/drei': 10.7.7(@react-three/fiber@9.3.0)(@types/react@18.3.27)(@types/three@0.182.0)(immer@10.2.0)(react-dom@18.3.1)(react@18.3.1)(three@0.180.0) - '@react-three/fiber': 9.3.0(@types/react@18.3.27)(immer@10.2.0)(react-dom@18.3.1)(react@18.3.1)(three@0.180.0) + '@react-spring/three': 10.0.3(@react-three/fiber@9.3.0(@types/react@18.3.27)(immer@10.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.180.0))(react@18.3.1)(three@0.180.0) + '@react-three/drei': 10.7.7(@react-three/fiber@9.3.0(@types/react@18.3.27)(immer@10.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.180.0))(@types/react@18.3.27)(@types/three@0.182.0)(immer@10.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.180.0) + '@react-three/fiber': 9.3.0(@types/react@18.3.27)(immer@10.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.180.0) '@use-gesture/react': 10.3.1(react@18.3.1) camera-controls: 3.1.2(three@0.180.0) classnames: 2.5.1 @@ -24795,7 +24990,7 @@ snapshots: react-dom: 18.3.1(react@18.3.1) three: 0.180.0 three-stdlib: 2.36.1(three@0.180.0) - zustand: 5.0.8(@types/react@18.3.27)(immer@10.2.0)(react@18.3.1) + zustand: 5.0.8(@types/react@18.3.27)(immer@10.2.0)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)) transitivePeerDependencies: - '@types/react' - '@types/three' @@ -25239,16 +25434,16 @@ snapshots: select-hose@2.0.0: {} - selection-popover@0.3.0(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1): + selection-popover@0.3.0(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@floating-ui/react-dom': 1.3.0(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-arrow': 1.0.1(react-dom@18.3.1)(react@18.3.1) + '@floating-ui/react-dom': 1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-arrow': 1.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-compose-refs': 1.0.0(react@18.3.1) '@radix-ui/react-context': 1.0.0(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.0.4(@types/react-dom@18.3.7)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-portal': 1.0.1(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-presence': 1.0.0(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-primitive': 1.0.1(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.0.4(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 1.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.0.0(react@18.3.1) '@radix-ui/react-use-layout-effect': 1.0.0(react@18.3.1) '@radix-ui/react-use-size': 1.0.0(react@18.3.1) @@ -25517,6 +25712,8 @@ snapshots: transitivePeerDependencies: - supports-color + snappyjs@0.6.1: {} + sockjs-client@1.4.0(supports-color@6.1.0): dependencies: debug: 3.2.7(supports-color@6.1.0) @@ -25548,7 +25745,7 @@ snapshots: smart-buffer: 4.2.0 optional: true - sonner@2.0.7(react-dom@18.3.1)(react@18.3.1): + sonner@2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -25997,11 +26194,11 @@ snapshots: tailwind-merge@2.6.0: {} - tailwindcss-animate@1.0.7(tailwindcss@3.4.19): + tailwindcss-animate@1.0.7(tailwindcss@3.4.19(yaml@2.8.2)): dependencies: - tailwindcss: 3.4.19 + tailwindcss: 3.4.19(yaml@2.8.2) - tailwindcss@3.4.19: + tailwindcss@3.4.19(yaml@2.8.2): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -26020,7 +26217,7 @@ snapshots: postcss: 8.5.6 postcss-import: 15.1.0(postcss@8.5.6) postcss-js: 4.1.0(postcss@8.5.6) - postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6)(yaml@2.8.2) postcss-nested: 6.2.0(postcss@8.5.6) postcss-selector-parser: 6.1.2 resolve: 1.22.11 @@ -26138,6 +26335,15 @@ snapshots: three@0.180.0: {} + thrift@0.11.0: + dependencies: + node-int64: 0.4.0 + q: 1.5.1 + ws: 8.18.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + throat@4.1.0: {} throttleit@2.1.0: {} @@ -26208,6 +26414,8 @@ snapshots: regex-not: 1.0.2 safe-regex: 1.1.0 + together-ai@0.33.0: {} + toidentifier@1.0.1: {} token-types@6.1.1: @@ -26255,13 +26463,12 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.4.6(@babel/core@7.7.4)(jest@29.7.0)(typescript@5.9.3): + ts-jest@29.4.6(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.19.3)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)))(typescript@5.9.3): dependencies: - '@babel/core': 7.7.4 bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 handlebars: 4.7.8 - jest: 29.7.0(@types/node@22.19.3)(ts-node@10.9.2) + jest: 29.7.0(@types/node@22.19.3)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 @@ -26269,6 +26476,12 @@ snapshots: type-fest: 4.41.0 typescript: 5.9.3 yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.28.5 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.28.5) + jest-util: 29.7.0 ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3): dependencies: @@ -26289,7 +26502,7 @@ snapshots: yn: 3.1.1 ts-pnp@1.1.5(typescript@5.9.3): - dependencies: + optionalDependencies: typescript: 5.9.3 tsconfig-paths@3.15.0: @@ -26392,19 +26605,17 @@ snapshots: typedarray@0.0.6: {} - typescript-eslint@8.49.0(eslint@9.39.2)(typescript@5.9.3): + typescript-eslint@8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.49.0(@typescript-eslint/parser@8.49.0)(eslint@9.39.2)(typescript@5.9.3) - '@typescript-eslint/parser': 8.49.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/parser': 8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.49.0(eslint@9.39.2)(typescript@5.9.3) - eslint: 9.39.2 + '@typescript-eslint/utils': 8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + eslint: 9.39.2(jiti@1.21.7) typescript: 5.9.3 transitivePeerDependencies: - supports-color - typescript-event-target@1.1.1: {} - typescript@5.9.3: {} uglify-js@3.19.3: @@ -26527,13 +26738,14 @@ snapshots: urix@0.1.0: {} - url-loader@2.3.0(file-loader@4.3.0)(webpack@4.41.2): + url-loader@2.3.0(file-loader@4.3.0(webpack@4.41.2))(webpack@4.41.2): dependencies: - file-loader: 4.3.0(webpack@4.41.2) loader-utils: 1.4.2 mime: 2.6.0 schema-utils: 2.7.1 webpack: 4.41.2 + optionalDependencies: + file-loader: 4.3.0(webpack@4.41.2) url-parse@1.5.10: dependencies: @@ -26547,9 +26759,10 @@ snapshots: use-callback-ref@1.3.3(@types/react@18.3.27)(react@18.3.1): dependencies: - '@types/react': 18.3.27 react: 18.3.1 tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.27 use-editable@2.3.3(react@18.3.1): dependencies: @@ -26559,7 +26772,7 @@ snapshots: dependencies: react: 18.3.1 - use-react-query-auto-sync@0.1.0(@tanstack/react-query@5.90.12)(react-dom@18.3.1)(react@18.3.1): + use-react-query-auto-sync@0.1.0(@tanstack/react-query@5.90.12(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@tanstack/react-query': 5.90.12(react@18.3.1) lodash.debounce: 4.0.8 @@ -26568,10 +26781,11 @@ snapshots: use-sidecar@1.1.3(@types/react@18.3.27)(react@18.3.1): dependencies: - '@types/react': 18.3.27 detect-node-es: 1.1.0 react: 18.3.1 tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.27 use-sync-external-store@1.6.0(react@18.3.1): dependencies: @@ -26660,6 +26874,8 @@ snapshots: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 + varint@5.0.2: {} + vary@1.1.2: {} vendors@1.0.4: {} @@ -26703,9 +26919,9 @@ snapshots: - supports-color - terser - vite-plugin-node-polyfills@0.22.0(vite@5.4.21): + vite-plugin-node-polyfills@0.22.0(rollup@4.53.4)(vite@5.4.21(@types/node@22.19.3)): dependencies: - '@rollup/plugin-inject': 5.0.5 + '@rollup/plugin-inject': 5.0.5(rollup@4.53.4) node-stdlib-browser: 1.3.1 vite: 5.4.21(@types/node@22.19.3) transitivePeerDependencies: @@ -26713,18 +26929,17 @@ snapshots: vite@5.4.21(@types/node@22.19.3): dependencies: - '@types/node': 22.19.3 esbuild: 0.21.5 postcss: 8.5.6 rollup: 4.53.4 optionalDependencies: + '@types/node': 22.19.3 fsevents: 2.3.3 - vitest@2.1.9(@types/node@22.19.3): + vitest@2.1.9(@types/node@22.19.3)(jsdom@14.1.0): dependencies: - '@types/node': 22.19.3 '@vitest/expect': 2.1.9 - '@vitest/mocker': 2.1.9(vite@5.4.21) + '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.19.3)) '@vitest/pretty-format': 2.1.9 '@vitest/runner': 2.1.9 '@vitest/snapshot': 2.1.9 @@ -26743,6 +26958,9 @@ snapshots: vite: 5.4.21(@types/node@22.19.3) vite-node: 2.1.9(@types/node@22.19.3) why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.19.3 + jsdom: 14.1.0 transitivePeerDependencies: - less - lightningcss @@ -26827,7 +27045,7 @@ snapshots: del: 4.1.1 express: 4.22.1(supports-color@6.1.0) html-entities: 1.4.0 - http-proxy-middleware: 0.19.1(debug@4.4.3)(supports-color@6.1.0) + http-proxy-middleware: 0.19.1(debug@4.4.3(supports-color@6.1.0))(supports-color@6.1.0) import-local: 2.0.0 internal-ip: 4.3.0 ip: 1.1.9 @@ -27234,19 +27452,21 @@ snapshots: zustand@4.5.7(@types/react@18.3.27)(immer@10.2.0)(react@18.3.1): dependencies: + use-sync-external-store: 1.6.0(react@18.3.1) + optionalDependencies: '@types/react': 18.3.27 immer: 10.2.0 react: 18.3.1 - use-sync-external-store: 1.6.0(react@18.3.1) - zustand@5.0.8(@types/react@18.3.27)(immer@10.2.0)(react@18.3.1): - dependencies: + zustand@5.0.8(@types/react@18.3.27)(immer@10.2.0)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)): + optionalDependencies: '@types/react': 18.3.27 immer: 10.2.0 react: 18.3.1 + use-sync-external-store: 1.6.0(react@18.3.1) - zustand@5.0.9(@types/react@18.3.27)(immer@10.2.0)(react@18.3.1)(use-sync-external-store@1.6.0): - dependencies: + zustand@5.0.9(@types/react@18.3.27)(immer@10.2.0)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)): + optionalDependencies: '@types/react': 18.3.27 immer: 10.2.0 react: 18.3.1 diff --git a/public/cerebras.png b/public/cerebras.png new file mode 100644 index 00000000..ca8dbcd5 Binary files /dev/null and b/public/cerebras.png differ diff --git a/public/cerebras.svg b/public/cerebras.svg new file mode 100644 index 00000000..2700677b --- /dev/null +++ b/public/cerebras.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/fireworks.png b/public/fireworks.png new file mode 100644 index 00000000..02b77f3a Binary files /dev/null and b/public/fireworks.png differ diff --git a/public/fireworks.svg b/public/fireworks.svg new file mode 100644 index 00000000..7854475d --- /dev/null +++ b/public/fireworks.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/groq.png b/public/groq.png new file mode 100644 index 00000000..72ed59f6 Binary files /dev/null and b/public/groq.png differ diff --git a/public/groq.svg b/public/groq.svg new file mode 100644 index 00000000..fd3f8a74 --- /dev/null +++ b/public/groq.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/mistral.png b/public/mistral.png new file mode 100644 index 00000000..99332bb8 Binary files /dev/null and b/public/mistral.png differ diff --git a/public/mistral.svg b/public/mistral.svg new file mode 100644 index 00000000..0dc04d57 --- /dev/null +++ b/public/mistral.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/nvidia.png b/public/nvidia.png new file mode 100644 index 00000000..0fb0cfb7 Binary files /dev/null and b/public/nvidia.png differ diff --git a/public/together.png b/public/together.png new file mode 100644 index 00000000..1f0c623b Binary files /dev/null and b/public/together.png differ diff --git a/src-tauri/binaries/run-mcp-aarch64-apple-darwin b/src-tauri/binaries/run-mcp-aarch64-apple-darwin index 8f2f638d..49f38ec6 100755 --- a/src-tauri/binaries/run-mcp-aarch64-apple-darwin +++ b/src-tauri/binaries/run-mcp-aarch64-apple-darwin @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0ec5a7bd296b17e588436748d95e41197c9a25918e0b4140ab6c0caffa6176d6 -size 753 +oid sha256:b3cdfc94c1300dc7558415c1a9e72a9eaa2793c02c4f596f686ad8c0b2d4826b +size 805 diff --git a/src-tauri/binaries/run-mcp-x86_64-apple-darwin b/src-tauri/binaries/run-mcp-x86_64-apple-darwin index 8f2f638d..49f38ec6 100755 --- a/src-tauri/binaries/run-mcp-x86_64-apple-darwin +++ b/src-tauri/binaries/run-mcp-x86_64-apple-darwin @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0ec5a7bd296b17e588436748d95e41197c9a25918e0b4140ab6c0caffa6176d6 -size 753 +oid sha256:b3cdfc94c1300dc7558415c1a9e72a9eaa2793c02c4f596f686ad8c0b2d4826b +size 805 diff --git a/src-tauri/src/migrations.rs b/src-tauri/src/migrations.rs index 87622999..c9c19ee2 100644 --- a/src-tauri/src/migrations.rs +++ b/src-tauri/src/migrations.rs @@ -2554,5 +2554,57 @@ You have full access to bash commands on the user''''s computer. If you write a UPDATE projects SET total_cost_usd = 0.0 WHERE total_cost_usd IS NULL; "#, }, + Migration { + version: 139, + description: "add thinking_level column to model_configs for Gemini 3 thinking mode", + kind: MigrationKind::Up, + sql: r#" + ALTER TABLE model_configs ADD COLUMN thinking_level TEXT; + "#, + }, + Migration { + version: 140, + description: "update reasoning_effort CHECK constraint to allow 'xhigh' for OpenAI models", + kind: MigrationKind::Up, + sql: r#" + -- SQLite doesn't support ALTER COLUMN, so we need to recreate the table + -- First, create a backup of the data + CREATE TABLE model_configs_backup AS SELECT * FROM model_configs; + + -- Drop the old table + DROP TABLE model_configs; + + -- Recreate the table with the updated constraint + CREATE TABLE model_configs ( + id TEXT NOT NULL PRIMARY KEY, + model_id TEXT NOT NULL, + display_name TEXT NOT NULL, + author TEXT NOT NULL CHECK (author IN ('user', 'system')), + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + system_prompt TEXT NOT NULL, + is_default BOOLEAN DEFAULT 0, + budget_tokens INTEGER, + reasoning_effort TEXT CHECK (reasoning_effort IN ('low', 'medium', 'high', 'xhigh') OR reasoning_effort IS NULL), + new_until DATETIME, + thinking_level TEXT + ); + + -- Restore the data + INSERT INTO model_configs (id, model_id, display_name, author, created_at, system_prompt, is_default, budget_tokens, reasoning_effort, new_until, thinking_level) + SELECT id, model_id, display_name, author, created_at, system_prompt, is_default, budget_tokens, reasoning_effort, new_until, thinking_level + FROM model_configs_backup; + + -- Drop the backup table + DROP TABLE model_configs_backup; + "#, + }, + Migration { + version: 141, + description: "add show_thoughts to model_configs", + kind: MigrationKind::Up, + sql: r#" + ALTER TABLE model_configs ADD COLUMN show_thoughts BOOLEAN NOT NULL DEFAULT 0; + "#, + }, ]; } diff --git a/src/core/chorus/AttachmentsHelpers.ts b/src/core/chorus/AttachmentsHelpers.ts index 0a4b878f..8a6fb93c 100644 --- a/src/core/chorus/AttachmentsHelpers.ts +++ b/src/core/chorus/AttachmentsHelpers.ts @@ -2,13 +2,13 @@ import { appDataDir } from "@tauri-apps/api/path"; import { mkdir, readFile } from "@tauri-apps/plugin-fs"; import { allowedExtensions, AttachmentType } from "@core/chorus/Models"; import { v4 as uuidv4 } from "uuid"; -import FirecrawlApp from "@mendable/firecrawl-js"; import { fileTypeFromBuffer } from "file-type"; import path from "path"; import mime from "mime-types"; import { invoke } from "@tauri-apps/api/core"; import * as pdfjsLib from "pdfjs-dist/legacy/build/pdf.mjs"; import { Attachment } from "./api/AttachmentsAPI"; +import { fetch as tauriFetch } from "@tauri-apps/plugin-http"; // Initialize PDF.js worker pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( @@ -18,13 +18,10 @@ pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( export const MAX_ATTACHMENTS = 10; export const MAX_SCRAPES_PER_MINUTE = 10; +const MAX_SCRAPE_CHARS = 200_000; // This should match TARGET_SIZE_BYTES in src-tauri/src/command.rs (3.5MB) export const TARGET_IMAGE_SIZE_BYTES = 4.5 * 1024 * 1024; // 4.5MB in bytes -// Create FirecrawlApp instance with provided API key -export const createFirecrawlClient = (apiKey: string) => - new FirecrawlApp({ apiKey }); - // Add rate limiting tracker export const scrapeTimestamps: number[] = []; @@ -303,32 +300,83 @@ export const getScreenshotAttachment = async ( export async function scrapeUrlAndWriteToPath( url: string, path: string, - firecrawlApiKey?: string, ): Promise<{ success: boolean; error?: string }> { - if (!firecrawlApiKey) { - return { success: false, error: "Firecrawl API key not configured" }; - } + const normalizeUrl = (raw: string): string => { + const trimmed = raw.trim(); + if (!trimmed) { + throw new Error("Missing URL"); + } + if (/^https?:\/\//i.test(trimmed)) { + return trimmed; + } + return `https://${trimmed}`; + }; + + const extractTextFromHtml = ( + html: string, + ): { title?: string; text: string } => { + try { + const doc = new DOMParser().parseFromString(html, "text/html"); + const title = doc.querySelector("title")?.textContent?.trim(); + doc.querySelectorAll("script, style, noscript").forEach((el) => + el.remove(), + ); + const rawText = + doc.body?.textContent || doc.documentElement.textContent || ""; + + const cleaned = rawText + .replace(/\r\n/g, "\n") + .replace(/[ \t\u00A0]+/g, " ") + .replace(/\n{3,}/g, "\n\n") + .trim(); + + return { title: title || undefined, text: cleaned }; + } catch { + return { text: html }; + } + }; + + const truncate = (text: string) => { + if (text.length <= MAX_SCRAPE_CHARS) return text; + return `${text.slice(0, MAX_SCRAPE_CHARS)}\n\n[truncated]`; + }; try { - const firecrawl = createFirecrawlClient(firecrawlApiKey); - const mockScrapeAPI = false; - const scrapeResult = mockScrapeAPI - ? { - success: true as const, - markdown: `test ${url}`, - } - : await firecrawl.scrapeUrl(url, { - formats: ["markdown"], - }); - - if (!scrapeResult.success) { + const normalizedUrl = normalizeUrl(url); + const response = await tauriFetch(normalizedUrl, { + method: "GET", + headers: { + Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "User-Agent": "Chorus", + }, + maxRedirections: 5, + connectTimeout: 20_000, + }); + + if (!response.ok) { + const errorText = await response.text().catch(() => ""); throw new Error( - `Failed to scrape: ${"error" in scrapeResult ? scrapeResult.error : "Unknown error"}`, + `HTTP ${response.status}: ${errorText || response.statusText}`, ); } + const contentType = response.headers.get("content-type") || ""; + const bodyText = await response.text(); + + const trimmedStart = bodyText.trim().slice(0, 64).toLowerCase(); + const isHtml = + contentType.includes("html") || + trimmedStart.startsWith(" void; onerror?: (error: Error) => void; onmessage?: (message: JSONRPCMessage) => void; + onstderr?: (data: string) => void; constructor(server: StdioServerParameters) { this._serverParams = server; @@ -99,14 +100,18 @@ export class StdioClientTransportChorus implements Transport { ); } + const env = { + ...getDefaultEnvironment(), + ...(this._serverParams.env ?? {}), + }; + this._command = this._serverParams.type === "sidecar" ? Command.sidecar( this._serverParams.sidecarBinary, this._serverParams.args ?? [], { - env: - this._serverParams.env ?? getDefaultEnvironment(), + env, }, ) : Command.sidecar( @@ -116,8 +121,7 @@ export class StdioClientTransportChorus implements Transport { ...(this._serverParams.args ?? []), ], { - env: - this._serverParams.env ?? getDefaultEnvironment(), + env, }, ); @@ -153,14 +157,12 @@ export class StdioClientTransportChorus implements Transport { // }); this._command.stdout?.on("data", (chunk) => { - console.log("Received chunk:", chunk); this._readBuffer.append(Buffer.from(chunk)); this.processReadBuffer(); }); this._command.stderr?.on("data", (chunk) => { - console.log("Received stderr chunk:", chunk, this.onerror); - this.onerror?.(new Error(chunk)); + this.onstderr?.(chunk); }); // this._command.stdout?.on("error", (error) => { @@ -189,6 +191,16 @@ export class StdioClientTransportChorus implements Transport { async close(): Promise { this._abortController.abort(); + + try { + await this._process?.kill(); + } catch (e) { + const errorMessage = e instanceof Error ? e.message : String(e); + this.onerror?.( + new Error(`Failed to kill MCP process: ${errorMessage}`), + ); + } + this._process = undefined; this._command = undefined; this._readBuffer.clear(); diff --git a/src/core/chorus/ModelProviders/ProviderAnthropic.ts b/src/core/chorus/ModelProviders/ProviderAnthropic.ts index f6bc95e1..0d61fd30 100644 --- a/src/core/chorus/ModelProviders/ProviderAnthropic.ts +++ b/src/core/chorus/ModelProviders/ProviderAnthropic.ts @@ -1,4 +1,5 @@ import Anthropic from "@anthropic-ai/sdk"; +import { fetch as tauriFetch } from "@tauri-apps/plugin-http"; import { LLMMessage, readImageAttachment, @@ -10,6 +11,12 @@ import { import { IProvider } from "./IProvider"; import { canProceedWithProvider } from "@core/utilities/ProxyUtils"; import { getUserToolNamespacedName, UserToolCall } from "@core/chorus/Toolsets"; +import * as Prompts from "@core/chorus/prompts/prompts"; +import { + clampAnthropicThinkingBudgetTokens, + getAnthropicMaxTokens, + getAnthropicModelName, +} from "./anthropicModels"; type AcceptedImageType = | "image/jpeg" @@ -27,68 +34,6 @@ type MeltyAnthrMessageParam = { hasAttachments: boolean; }; -type AnthropicModelConfig = { - inputModelName: string; - anthropicModelName: string; - maxTokens: number; -}; - -const ANTHROPIC_MODELS: AnthropicModelConfig[] = [ - { - inputModelName: "claude-3-5-sonnet-latest", - anthropicModelName: "claude-3-5-sonnet-latest", - maxTokens: 8192, - }, - { - inputModelName: "claude-3-7-sonnet-latest", - anthropicModelName: "claude-3-7-sonnet-latest", - maxTokens: 20000, - }, - { - inputModelName: "claude-3-7-sonnet-latest-thinking", - anthropicModelName: "claude-3-7-sonnet-latest", - maxTokens: 10000, - }, - { - inputModelName: "claude-sonnet-4-latest", - // https://docs.anthropic.com/en/docs/about-claude/models/overview 0 is the new alias for latest - anthropicModelName: "claude-sonnet-4-0", - maxTokens: 10000, - }, - { - inputModelName: "claude-sonnet-4-5-20250929", - anthropicModelName: "claude-sonnet-4-5-20250929", - maxTokens: 10000, - }, - { - inputModelName: "claude-opus-4-latest", - anthropicModelName: "claude-opus-4-0", - maxTokens: 10000, - }, - { - inputModelName: "claude-opus-4.1-latest", - anthropicModelName: "claude-opus-4-1-20250805", - maxTokens: 10000, - }, - { - inputModelName: "claude-haiku-4-5-20251001", - anthropicModelName: "claude-haiku-4-5-20251001", - maxTokens: 20000, - }, - { - inputModelName: "claude-opus-4-5-20251101", - anthropicModelName: "claude-opus-4-5-20251101", - maxTokens: 20000, - }, -]; - -function getAnthropicModelName(modelName: string): string | undefined { - const modelConfig = ANTHROPIC_MODELS.find( - (m) => m.inputModelName === modelName, - ); - return modelConfig?.anthropicModelName; -} - export class ProviderAnthropic implements IProvider { async streamResponse({ modelConfig, @@ -99,13 +44,11 @@ export class ProviderAnthropic implements IProvider { onError, additionalHeaders, tools, + enabledToolsets, customBaseUrl, }: StreamResponseParams) { const modelName = modelConfig.modelId.split("::")[1]; const anthropicModelName = getAnthropicModelName(modelName); - if (!anthropicModelName) { - throw new Error(`Unsupported model: ${modelConfig.modelId}`); - } const { canProceed, reason } = canProceedWithProvider( "anthropic", @@ -120,7 +63,28 @@ export class ProviderAnthropic implements IProvider { const messages = await convertConversationToAnthropic(llmConversation); - const isThinking = modelConfig.budgetTokens !== undefined; + const systemPrompt = [ + modelConfig.showThoughts + ? Prompts.THOUGHTS_SYSTEM_PROMPT + : undefined, + modelConfig.systemPrompt, + ] + .filter(Boolean) + .join("\n\n"); + + const maxTokens = getAnthropicMaxTokens(modelName); + const thinkingBudgetTokens = + modelConfig.budgetTokens !== undefined + ? clampAnthropicThinkingBudgetTokens({ + budgetTokens: modelConfig.budgetTokens, + maxTokens, + }) + : undefined; + + const isThinking = thinkingBudgetTokens !== undefined; + const nativeWebSearchEnabled = + enabledToolsets?.includes("web") ?? false; + const shouldUseNativeWebSearch = nativeWebSearchEnabled; // Map tools to Claude's tool format const anthropicTools: Anthropic.Messages.Tool[] | undefined = tools @@ -155,34 +119,81 @@ export class ProviderAnthropic implements IProvider { const createParams: Anthropic.Messages.MessageCreateParamsStreaming = { model: anthropicModelName, messages, - system: modelConfig.systemPrompt, + system: systemPrompt, stream: true, - max_tokens: getMaxTokens(modelName), - ...(isThinking && { + max_tokens: maxTokens, + ...(thinkingBudgetTokens !== undefined && { thinking: { type: "enabled", - budget_tokens: modelConfig.budgetTokens, + budget_tokens: thinkingBudgetTokens, }, }), - ...(tools && - tools.length > 0 && { - tools: anthropicTools, - }), }; + const requestTools: Anthropic.Messages.Tool[] = []; + if (shouldUseNativeWebSearch) { + requestTools.push({ + type: "web_search_20250305", + name: "web_search", + } as unknown as Anthropic.Messages.Tool); + } + if (anthropicTools && anthropicTools.length > 0) { + requestTools.push(...anthropicTools); + } + if (requestTools.length > 0) { + createParams.tools = requestTools; + } + + // Debug: Log thinking parameters + console.log(`[ProviderAnthropic] Model: ${anthropicModelName}`); + console.log(`[ProviderAnthropic] isThinking: ${isThinking}`); + console.log( + `[ProviderAnthropic] modelConfig.budgetTokens: ${modelConfig.budgetTokens}`, + ); + if ( + modelConfig.budgetTokens !== undefined && + thinkingBudgetTokens !== undefined && + modelConfig.budgetTokens !== thinkingBudgetTokens + ) { + console.warn( + `[ProviderAnthropic] Clamped thinking budget_tokens from ${modelConfig.budgetTokens} to ${thinkingBudgetTokens} (max_tokens=${maxTokens}).`, + ); + } + console.log( + `[ProviderAnthropic] createParams.thinking:`, + (createParams as unknown as Record).thinking, + ); + // Configure headers const headers: Record = { ...(additionalHeaders ?? {}), }; + const anthropicBetaHeaderValue = shouldUseNativeWebSearch + ? mergeAnthropicBetaHeader( + headers["anthropic-beta"], + "web-search-2025-03-05", + ) + : undefined; + const client = new Anthropic({ apiKey: apiKeys.anthropic, baseURL: customBaseUrl, + fetch: tauriFetch, dangerouslyAllowBrowser: true, defaultHeaders: headers, }); - const stream = client.messages.stream(createParams); + const stream = client.messages.stream( + createParams, + anthropicBetaHeaderValue + ? { + headers: { + "anthropic-beta": anthropicBetaHeaderValue, + }, + } + : undefined, + ); stream.on("error", (error) => { console.error( @@ -197,19 +208,117 @@ export class ProviderAnthropic implements IProvider { onChunk(text); }); + // Extended thinking support: + // Some Claude models stream reasoning as separate content blocks (e.g. type: "thinking"). + // When we detect these blocks, we wrap them in ... so the UI can render + // them as a collapsible ThinkBlock. + const contentBlockTypesByIndex = new Map(); + let inThinkingBlock = false; + let sawThinkingContent = false; + let wroteRedactedPlaceholder = false; + let thinkingStartedAtMs: number | undefined; + + const closeThinkingBlock = () => { + if (!inThinkingBlock) return; + inThinkingBlock = false; + onChunk(""); + if (thinkingStartedAtMs !== undefined) { + const seconds = Math.max( + 1, + Math.round((Date.now() - thinkingStartedAtMs) / 1000), + ); + onChunk(``); + } + thinkingStartedAtMs = undefined; + wroteRedactedPlaceholder = false; + }; + + stream.on("streamEvent", (event: unknown, messageSnapshot: unknown) => { + const ev = event as { + type?: string; + index?: number; + content_block?: { type?: string }; + delta?: { type?: string; text?: string }; + }; + + if (ev.type === "content_block_start") { + const idx = typeof ev.index === "number" ? ev.index : null; + const blockType = ev.content_block?.type; + if (idx !== null && typeof blockType === "string") { + contentBlockTypesByIndex.set(idx, blockType); + } + } + + if (ev.type === "content_block_delta") { + const idx = typeof ev.index === "number" ? ev.index : null; + if (idx === null) return; + + const snapshot = messageSnapshot as { + content?: Array<{ type?: string }>; + }; + const snapshotBlockType = + snapshot?.content?.[idx]?.type ?? undefined; + const blockType = + snapshotBlockType ?? contentBlockTypesByIndex.get(idx); + + if ( + blockType === "thinking" || + blockType === "redacted_thinking" + ) { + const deltaText = + typeof ev.delta?.text === "string" ? ev.delta.text : ""; + + if (!inThinkingBlock) { + inThinkingBlock = true; + sawThinkingContent = true; + thinkingStartedAtMs = Date.now(); + wroteRedactedPlaceholder = false; + onChunk(""); + } + + // redacted_thinking is not human-readable; skip content. + if (blockType === "thinking") { + onChunk(deltaText); + } else if (!wroteRedactedPlaceholder) { + wroteRedactedPlaceholder = true; + onChunk("[redacted]"); + } + } + } + + if (ev.type === "content_block_stop") { + closeThinkingBlock(); + } + }); + // get final message so we can get the tool calls from it // (we're building up most of final message ourselves using onChunk, and then // at the last moment merging in the tool calls) const finalMessage = (await stream.finalMessage()) as Anthropic.Message; + if (inThinkingBlock) { + closeThinkingBlock(); + } else if (sawThinkingContent) { + // If we saw thinking blocks, ensure a trailing newline so markdown layout is clean. + onChunk("\n\n"); + } + console.log( "Raw tool calls from Anthropic", finalMessage.content.filter((item) => item.type === "tool_use"), ); + // Only surface tool calls that correspond to user-configured tools. + // This prevents server-side tools (e.g., Anthropic web search) from being routed + // through our MCP/custom tool execution loop. + const allowedToolNames = new Set( + (tools ?? []).map((t) => getUserToolNamespacedName(t)), + ); + const toolCalls: UserToolCall[] = finalMessage.content .filter((item) => item.type === "tool_use") + .filter((tool) => allowedToolNames.has(tool.name)) .map((tool) => { const calledTool = tools?.find( (t) => getUserToolNamespacedName(t) === tool.name, @@ -229,6 +338,20 @@ export class ProviderAnthropic implements IProvider { } } +function mergeAnthropicBetaHeader( + existingValue: string | undefined, + betaToAdd: string, +): string { + const betas = (existingValue ?? "") + .split(",") + .map((v) => v.trim()) + .filter(Boolean); + if (!betas.includes(betaToAdd)) { + betas.push(betaToAdd); + } + return betas.join(", "); +} + async function formatMessageWithAttachments( message: LLMMessage, ): Promise { @@ -396,16 +519,6 @@ async function formatMessageWithAttachments( }; } -const getMaxTokens = (modelId: string) => { - const modelConfig = ANTHROPIC_MODELS.find( - (m) => m.inputModelName === modelId, - ); - if (!modelConfig) { - throw new Error(`Unsupported model: ${modelId}`); - } - return modelConfig.maxTokens; -}; - /** * Adds cache control block to the last message in `messages` * that contains attachments (per the hasAttachments flag). diff --git a/src/core/chorus/ModelProviders/ProviderCerebras.ts b/src/core/chorus/ModelProviders/ProviderCerebras.ts new file mode 100644 index 00000000..0f90ca98 --- /dev/null +++ b/src/core/chorus/ModelProviders/ProviderCerebras.ts @@ -0,0 +1,177 @@ +import OpenAI from "openai"; +import { fetch as tauriFetch } from "@tauri-apps/plugin-http"; +import { StreamResponseParams } from "../Models"; +import { IProvider } from "./IProvider"; +import { canProceedWithProvider } from "@core/utilities/ProxyUtils"; +import OpenAICompletionsAPIUtils from "@core/chorus/OpenAICompletionsAPIUtils"; +import * as Prompts from "@core/chorus/prompts/prompts"; + +export class ProviderCerebras implements IProvider { + async streamResponse({ + modelConfig, + llmConversation, + apiKeys, + onChunk, + onComplete, + additionalHeaders, + tools, + customBaseUrl, + }: StreamResponseParams) { + const modelName = modelConfig.modelId.split("::")[1]; + + const { canProceed, reason } = canProceedWithProvider( + "cerebras", + apiKeys, + ); + if (!canProceed) { + throw new Error( + reason || "Please add your Cerebras API key in Settings.", + ); + } + + const client = new OpenAI({ + baseURL: customBaseUrl || "https://api.cerebras.ai/v1", + apiKey: apiKeys.cerebras, + fetch: tauriFetch, + defaultHeaders: { + ...(additionalHeaders ?? {}), + }, + dangerouslyAllowBrowser: true, + }); + + const functionSupport = (tools?.length ?? 0) > 0; + const messages = await OpenAICompletionsAPIUtils.convertConversation( + llmConversation, + { + imageSupport: false, + functionSupport, + }, + ); + + const lowerModelName = modelName.toLowerCase(); + const isZaiGlm = lowerModelName.includes("glm"); + const canSafelyEnableNativeReasoning = isZaiGlm && !functionSupport; // Cerebras docs: tool calling + reasoning streaming may be unsupported. + + const systemPrompt = [ + modelConfig.showThoughts && !canSafelyEnableNativeReasoning + ? Prompts.THOUGHTS_SYSTEM_PROMPT + : undefined, + modelConfig.systemPrompt, + ] + .filter(Boolean) + .join("\n\n"); + + const params: OpenAI.ChatCompletionCreateParamsStreaming & { + disable_reasoning?: boolean; + clear_thinking?: boolean; + } = { + model: modelName, + messages: [ + ...(systemPrompt + ? [ + { + role: "system" as const, + content: systemPrompt, + }, + ] + : []), + ...messages, + ], + stream: true, + }; + + if (isZaiGlm) { + // Cerebras GLM supports non-standard flags (OpenAI-compatible endpoints differ here). + // When showThoughts is off, disable reasoning to avoid token spend. + // When showThoughts is on, explicitly enable reasoning (when safe). + if (!modelConfig.showThoughts || canSafelyEnableNativeReasoning) { + params.disable_reasoning = !modelConfig.showThoughts; + params.clear_thinking = !modelConfig.showThoughts; + } + } + + if (tools && tools.length > 0) { + params.tools = + OpenAICompletionsAPIUtils.convertToolDefinitions(tools); + params.tool_choice = "auto"; + } + + const chunks: OpenAI.ChatCompletionChunk[] = []; + let inReasoning = false; + let reasoningStartedAtMs: number | undefined; + + const closeReasoning = () => { + if (!inReasoning) return; + inReasoning = false; + onChunk(""); + if (reasoningStartedAtMs !== undefined) { + const seconds = Math.max( + 1, + Math.round((Date.now() - reasoningStartedAtMs) / 1000), + ); + onChunk(``); + } + reasoningStartedAtMs = undefined; + }; + let stream: AsyncIterable; + try { + stream = await client.chat.completions.create(params); + } catch (error) { + // Some OpenAI-compatible endpoints reject non-standard flags. + // If that happens, retry once without them. + if ( + isZaiGlm && + (params.disable_reasoning !== undefined || + params.clear_thinking !== undefined) + ) { + delete params.disable_reasoning; + delete params.clear_thinking; + stream = await client.chat.completions.create(params); + } else { + throw error; + } + } + + for await (const chunk of stream) { + chunks.push(chunk); + const delta = chunk.choices[0]?.delta as unknown as { + content?: string; + reasoning?: string; + reasoning_content?: string; + }; + + const reasoningDelta = + typeof delta?.reasoning_content === "string" + ? delta.reasoning_content + : typeof delta?.reasoning === "string" + ? delta.reasoning + : undefined; + + if (modelConfig.showThoughts && reasoningDelta) { + if (!inReasoning) { + inReasoning = true; + reasoningStartedAtMs = Date.now(); + onChunk(""); + } + onChunk(reasoningDelta); + } + + if (typeof delta?.content === "string" && delta.content) { + closeReasoning(); + onChunk(delta.content); + } + } + + closeReasoning(); + + const toolCalls = OpenAICompletionsAPIUtils.convertToolCalls( + chunks, + tools ?? [], + ); + + await onComplete( + undefined, + toolCalls.length > 0 ? toolCalls : undefined, + ); + } +} diff --git a/src/core/chorus/ModelProviders/ProviderCustomAnthropic.ts b/src/core/chorus/ModelProviders/ProviderCustomAnthropic.ts new file mode 100644 index 00000000..9d4e0c41 --- /dev/null +++ b/src/core/chorus/ModelProviders/ProviderCustomAnthropic.ts @@ -0,0 +1,74 @@ +import { SettingsManager } from "@core/utilities/Settings"; +import { StreamResponseParams } from "../Models"; +import { IProvider, ModelDisabled } from "./IProvider"; +import { ProviderAnthropic } from "./ProviderAnthropic"; + +function parseCustomProviderModelId(modelId: string): { + providerId: string; + modelName: string; +} { + const parts = modelId.split("::"); + if (parts.length < 3) { + throw new Error(`Invalid custom provider model id: ${modelId}`); + } + + const providerId = parts[1] ?? ""; + const modelName = parts.slice(2).join("::"); + + if (!providerId || !modelName) { + throw new Error(`Invalid custom provider model id: ${modelId}`); + } + + return { providerId, modelName }; +} + +export class ProviderCustomAnthropic implements IProvider { + async streamResponse( + params: StreamResponseParams, + ): Promise { + const { providerId, modelName } = parseCustomProviderModelId( + params.modelConfig.modelId, + ); + + const settings = await SettingsManager.getInstance().get(); + const provider = (settings.customProviders ?? []).find( + (p) => p.id === providerId && p.kind === "anthropic", + ); + + if (!provider) { + throw new Error( + "Custom provider not found. Please check your Providers settings.", + ); + } + + if (!provider.apiBaseUrl.trim()) { + throw new Error( + `Please add an API base URL for "${provider.name}" in Settings.`, + ); + } + + if (!provider.apiKey.trim()) { + throw new Error( + `Please add an API key for "${provider.name}" in Settings.`, + ); + } + + const mappedModelConfig = { + ...params.modelConfig, + modelId: `anthropic::${modelName}`, + }; + + const mappedApiKeys = { + ...params.apiKeys, + anthropic: provider.apiKey, + }; + + const anthropic = new ProviderAnthropic(); + return anthropic.streamResponse({ + ...params, + modelConfig: mappedModelConfig, + apiKeys: mappedApiKeys, + customBaseUrl: params.customBaseUrl || provider.apiBaseUrl, + }); + } +} diff --git a/src/core/chorus/ModelProviders/ProviderCustomOpenAI.ts b/src/core/chorus/ModelProviders/ProviderCustomOpenAI.ts new file mode 100644 index 00000000..e9d01a3c --- /dev/null +++ b/src/core/chorus/ModelProviders/ProviderCustomOpenAI.ts @@ -0,0 +1,203 @@ +import OpenAI from "openai"; +import OpenAICompletionsAPIUtils from "@core/chorus/OpenAICompletionsAPIUtils"; +import { SettingsManager } from "@core/utilities/Settings"; +import { StreamResponseParams } from "../Models"; +import { IProvider, ModelDisabled } from "./IProvider"; +import JSON5 from "json5"; +import { fetch as tauriFetch } from "@tauri-apps/plugin-http"; + +interface ProviderError { + message: string; + error?: { + message?: string; + metadata?: { raw?: string }; + }; + metadata?: { raw?: string }; +} + +function isProviderError(error: unknown): error is ProviderError { + return ( + typeof error === "object" && + error !== null && + "message" in error && + ("error" in error || "metadata" in error) && + error.message === "Provider returned error" + ); +} + +function parseCustomProviderModelId(modelId: string): { + providerId: string; + modelName: string; +} { + const parts = modelId.split("::"); + if (parts.length < 3) { + throw new Error(`Invalid custom provider model id: ${modelId}`); + } + + const providerId = parts[1] ?? ""; + const modelName = parts.slice(2).join("::"); + + if (!providerId || !modelName) { + throw new Error(`Invalid custom provider model id: ${modelId}`); + } + + return { providerId, modelName }; +} + +function getErrorMessage(error: unknown): string { + if (typeof error === "object" && error !== null && "message" in error) { + return (error as { message: string }).message; + } else if (typeof error === "string") { + return error; + } + return "Unknown error"; +} + +export class ProviderCustomOpenAI implements IProvider { + async streamResponse({ + llmConversation, + modelConfig, + onChunk, + onComplete, + additionalHeaders, + tools, + onError, + customBaseUrl, + }: StreamResponseParams): Promise { + const { providerId, modelName } = parseCustomProviderModelId( + modelConfig.modelId, + ); + + const settings = await SettingsManager.getInstance().get(); + const provider = (settings.customProviders ?? []).find( + (p) => p.id === providerId && p.kind === "openai", + ); + + if (!provider) { + throw new Error( + "Custom provider not found. Please check your Providers settings.", + ); + } + + if (!provider.apiBaseUrl.trim()) { + throw new Error( + `Please add an API base URL for "${provider.name}" in Settings.`, + ); + } + + if (!provider.apiKey.trim()) { + throw new Error( + `Please add an API key for "${provider.name}" in Settings.`, + ); + } + + const baseURL = customBaseUrl || provider.apiBaseUrl; + + const client = new OpenAI({ + baseURL, + apiKey: provider.apiKey, + fetch: tauriFetch, + defaultHeaders: { + ...(additionalHeaders ?? {}), + }, + dangerouslyAllowBrowser: true, + }); + + let messages: OpenAI.ChatCompletionMessageParam[] = + await OpenAICompletionsAPIUtils.convertConversation( + llmConversation, + { + imageSupport: + modelConfig.supportedAttachmentTypes?.includes( + "image", + ) ?? false, + functionSupport: true, + }, + ); + + if (modelConfig.systemPrompt) { + messages = [ + { + role: "system", + content: modelConfig.systemPrompt, + }, + ...messages, + ]; + } + + const streamParams: OpenAI.ChatCompletionCreateParamsStreaming = { + model: modelName, + messages, + stream: true, + }; + + if (tools && tools.length > 0) { + streamParams.tools = + OpenAICompletionsAPIUtils.convertToolDefinitions(tools); + streamParams.tool_choice = "auto"; + } + + const chunks: OpenAI.ChatCompletionChunk[] = []; + + try { + const stream = await client.chat.completions.create(streamParams); + for await (const chunk of stream) { + chunks.push(chunk); + if (chunk.choices[0]?.delta?.content) { + onChunk(chunk.choices[0].delta.content); + } + } + } catch (error: unknown) { + console.error( + "Raw error from ProviderCustomOpenAI:", + error, + modelName, + provider.apiBaseUrl, + messages, + ); + console.error(JSON.stringify(error, null, 2)); + + if ( + isProviderError(error) && + error.message === "Provider returned error" + ) { + let errorDetails: ProviderError; + try { + errorDetails = JSON5.parse( + error.error?.metadata?.raw || + error.metadata?.raw || + "{}", + ); + } catch { + errorDetails = { + message: "Failed to parse error details", + error: { message: "Failed to parse error details" }, + }; + } + const errorMessage = `Provider returned error: ${errorDetails.error?.message || error.message}`; + if (onError) { + onError(errorMessage); + } else { + throw new Error(errorMessage); + } + } else { + if (onError) { + onError(getErrorMessage(error)); + } else { + throw error; + } + } + return undefined; + } + + const toolCalls = OpenAICompletionsAPIUtils.convertToolCalls( + chunks, + tools ?? [], + ); + + await onComplete( + undefined, + toolCalls.length > 0 ? toolCalls : undefined, + ); + } +} diff --git a/src/core/chorus/ModelProviders/ProviderFireworks.ts b/src/core/chorus/ModelProviders/ProviderFireworks.ts new file mode 100644 index 00000000..8bd281b8 --- /dev/null +++ b/src/core/chorus/ModelProviders/ProviderFireworks.ts @@ -0,0 +1,177 @@ +import OpenAI from "openai"; +import { fetch as tauriFetch } from "@tauri-apps/plugin-http"; +import { StreamResponseParams } from "../Models"; +import { IProvider } from "./IProvider"; +import { canProceedWithProvider } from "@core/utilities/ProxyUtils"; +import OpenAICompletionsAPIUtils from "@core/chorus/OpenAICompletionsAPIUtils"; + +export class ProviderFireworks implements IProvider { + async streamResponse({ + modelConfig, + llmConversation, + apiKeys, + onChunk, + onComplete, + additionalHeaders, + tools, + customBaseUrl, + }: StreamResponseParams) { + const modelName = modelConfig.modelId.split("::")[1]; + + const { canProceed, reason } = canProceedWithProvider( + "fireworks", + apiKeys, + ); + if (!canProceed) { + throw new Error( + reason || "Please add your Fireworks API key in Settings.", + ); + } + + const client = new OpenAI({ + baseURL: customBaseUrl || "https://api.fireworks.ai/inference/v1", + apiKey: apiKeys.fireworks, + fetch: tauriFetch, + defaultHeaders: { + ...(additionalHeaders ?? {}), + }, + dangerouslyAllowBrowser: true, + }); + + const functionSupport = (tools?.length ?? 0) > 0; + const messages = await OpenAICompletionsAPIUtils.convertConversation( + llmConversation, + { imageSupport: true, functionSupport }, + ); + + const params: OpenAI.ChatCompletionCreateParamsStreaming & { + reasoning_effort?: string; + } = { + model: modelName, + messages: [ + ...(modelConfig.systemPrompt + ? [ + { + role: "system" as const, + content: modelConfig.systemPrompt, + }, + ] + : []), + ...messages, + ], + stream: true, + }; + + const normalizedEffort = ( + effort: "low" | "medium" | "high" | "xhigh" | null | undefined, + ): "low" | "medium" | "high" => { + if (effort === "low" || effort === "medium" || effort === "high") { + return effort; + } + if (effort === "xhigh") { + return "high"; + } + return "medium"; + }; + + const lowerModelName = modelName.toLowerCase(); + const canDisableReasoning = + !lowerModelName.includes("gpt-oss") && + !lowerModelName.includes("minimax") && + !lowerModelName.includes("m2"); + + if (modelConfig.showThoughts) { + // Fireworks streams reasoning in `reasoning_content` when enabled. + // Default to medium when user hasn't set anything. + params.reasoning_effort = normalizedEffort( + modelConfig.reasoningEffort, + ); + } else if (canDisableReasoning) { + // Best-effort: disable reasoning so we don't pay tokens for it. + params.reasoning_effort = "none"; + } + + if (tools && tools.length > 0) { + params.tools = + OpenAICompletionsAPIUtils.convertToolDefinitions(tools); + params.tool_choice = "auto"; + } + + const chunks: OpenAI.ChatCompletionChunk[] = []; + let inReasoning = false; + let reasoningStartedAtMs: number | undefined; + + const closeReasoning = () => { + if (!inReasoning) return; + inReasoning = false; + onChunk(""); + if (reasoningStartedAtMs !== undefined) { + const seconds = Math.max( + 1, + Math.round((Date.now() - reasoningStartedAtMs) / 1000), + ); + onChunk(``); + } + reasoningStartedAtMs = undefined; + }; + + let stream: AsyncIterable; + try { + stream = await client.chat.completions.create(params); + } catch (error) { + // Some Fireworks models reject reasoning_effort="none". + // If disabling reasoning fails, retry once without the param. + if ( + !modelConfig.showThoughts && + params.reasoning_effort === "none" + ) { + delete params.reasoning_effort; + stream = await client.chat.completions.create(params); + } else { + throw error; + } + } + + for await (const chunk of stream) { + chunks.push(chunk); + const delta = chunk.choices[0]?.delta as unknown as { + content?: string; + reasoning?: string; + reasoning_content?: string; + }; + + const reasoningDelta = + typeof delta?.reasoning_content === "string" + ? delta.reasoning_content + : typeof delta?.reasoning === "string" + ? delta.reasoning + : undefined; + + if (modelConfig.showThoughts && reasoningDelta) { + if (!inReasoning) { + inReasoning = true; + reasoningStartedAtMs = Date.now(); + onChunk(""); + } + onChunk(reasoningDelta); + } + + if (typeof delta?.content === "string" && delta.content) { + closeReasoning(); + onChunk(delta.content); + } + } + + closeReasoning(); + + const toolCalls = OpenAICompletionsAPIUtils.convertToolCalls( + chunks, + tools ?? [], + ); + + await onComplete( + undefined, + toolCalls.length > 0 ? toolCalls : undefined, + ); + } +} diff --git a/src/core/chorus/ModelProviders/ProviderGoogle.ts b/src/core/chorus/ModelProviders/ProviderGoogle.ts index 9547397b..cf69d23f 100644 --- a/src/core/chorus/ModelProviders/ProviderGoogle.ts +++ b/src/core/chorus/ModelProviders/ProviderGoogle.ts @@ -5,6 +5,7 @@ import { IProvider, ModelDisabled } from "./IProvider"; import OpenAICompletionsAPIUtils from "@core/chorus/OpenAICompletionsAPIUtils"; import { canProceedWithProvider } from "@core/utilities/ProxyUtils"; import JSON5 from "json5"; +import { fetch } from "@tauri-apps/plugin-http"; interface ProviderError { message: string; @@ -25,7 +26,7 @@ function isProviderError(error: unknown): error is ProviderError { ); } -function getGoogleModelName(modelName: string): string | undefined { +function getGoogleModelName(modelName: string): string { if ( [ "gemini-2.0-flash-exp", @@ -49,7 +50,9 @@ function getGoogleModelName(modelName: string): string | undefined { // alias deprecated preview model to stable version return "gemini-2.5-flash"; } - return undefined; + // If not found in hardcoded mappings, return the model name as-is + // (supports dynamically fetched models from API) + return modelName; } // uses OpenAI provider to format the messages @@ -62,14 +65,12 @@ export class ProviderGoogle implements IProvider { apiKeys, additionalHeaders, tools, + enabledToolsets, onError, customBaseUrl, }: StreamResponseParams): Promise { const modelName = modelConfig.modelId.split("::")[1]; const googleModelName = getGoogleModelName(modelName); - if (!googleModelName) { - throw new Error(`Unsupported model: ${modelName}`); - } const { canProceed, reason } = canProceedWithProvider( "google", @@ -82,6 +83,28 @@ export class ProviderGoogle implements IProvider { ); } + const googleApiKey = apiKeys.google; + if (!googleApiKey) { + throw new Error("Please add your Google AI API key in Settings."); + } + + const nativeWebSearchEnabled = + enabledToolsets?.includes("web") ?? false; + + if (nativeWebSearchEnabled) { + await streamGroundedResponseWithGoogleSearch({ + googleModelName, + llmConversation, + systemPrompt: modelConfig.systemPrompt, + apiKey: googleApiKey, + onChunk, + onComplete, + onError, + customBaseUrl, + }); + return; + } + // Google AI uses the generativelanguage.googleapis.com endpoint with OpenAI compatibility const baseURL = customBaseUrl || @@ -102,7 +125,7 @@ export class ProviderGoogle implements IProvider { }; const client = new OpenAI({ baseURL, - apiKey: apiKeys.google, + apiKey: googleApiKey, defaultHeaders: headers, dangerouslyAllowBrowser: true, }); @@ -132,6 +155,41 @@ export class ProviderGoogle implements IProvider { stream: true, }; + // Add Gemini thinking parameters + const isGemini3 = googleModelName.includes("gemini-3"); + const isGemini25 = googleModelName.includes("gemini-2.5"); + + if (isGemini3 && modelConfig.thinkingLevel) { + // Gemini 3 uses thinking_level parameter + ( + streamParams as unknown as Record + ).thinking_level = modelConfig.thinkingLevel; + } else if (isGemini25 && modelConfig.budgetTokens) { + // Gemini 2.5 uses thinking_budget parameter + ( + streamParams as unknown as Record + ).thinking_budget = modelConfig.budgetTokens; + } + + // Debug: Log thinking parameters + console.log(`[ProviderGoogle] Model: ${googleModelName}`); + console.log( + `[ProviderGoogle] isGemini3: ${isGemini3}, isGemini25: ${isGemini25}`, + ); + console.log( + `[ProviderGoogle] modelConfig.thinkingLevel: ${modelConfig.thinkingLevel}`, + ); + console.log( + `[ProviderGoogle] modelConfig.budgetTokens: ${modelConfig.budgetTokens}`, + ); + console.log(`[ProviderGoogle] streamParams thinking params:`, { + thinking_level: (streamParams as unknown as Record) + .thinking_level, + thinking_budget: ( + streamParams as unknown as Record + ).thinking_budget, + }); + // Add tools definitions if (tools && tools.length > 0) { streamParams.tools = @@ -203,3 +261,260 @@ function getErrorMessage(error: unknown): string { return "Unknown error"; } } + +async function streamGroundedResponseWithGoogleSearch(params: { + googleModelName: string; + llmConversation: StreamResponseParams["llmConversation"]; + systemPrompt: string | undefined; + apiKey: string; + onChunk: StreamResponseParams["onChunk"]; + onComplete: StreamResponseParams["onComplete"]; + onError: StreamResponseParams["onError"]; + customBaseUrl: string | undefined; +}): Promise { + try { + const { googleModelName, llmConversation, systemPrompt, apiKey } = + params; + + const messages = await OpenAICompletionsAPIUtils.convertConversation( + llmConversation, + { + imageSupport: false, + functionSupport: false, + }, + ); + + const contents: Array<{ + role: "user" | "model"; + parts: Array<{ text: string }>; + }> = messages.map((m) => { + const text = normalizeToText(m.content); + return { + role: m.role === "assistant" ? "model" : "user", + parts: [{ text: text.trim() === "" ? "..." : text }], + }; + }); + + if (systemPrompt) { + if (contents.length > 0 && contents[0].role === "user") { + contents[0].parts[0].text = + `${systemPrompt}\n\n` + contents[0].parts[0].text; + } else { + contents.unshift({ + role: "user", + parts: [{ text: systemPrompt }], + }); + } + } + + const geminiBaseUrl = params.customBaseUrl + ? params.customBaseUrl.replace(/\/openai\/?$/, "") + : "https://generativelanguage.googleapis.com/v1beta"; + + const response = await fetch( + `${geminiBaseUrl}/models/${googleModelName}:generateContent`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-goog-api-key": apiKey, + }, + body: JSON.stringify({ + contents, + tools: [ + { + google_search: {}, + }, + ], + }), + }, + ); + + if (!response.ok) { + const errorBody = await safeJson(response); + throw new Error( + getGeminiErrorMessageFromBody(errorBody) ?? response.statusText, + ); + } + + const data = (await response.json()) as unknown; + const { text, sources } = extractGeminiTextAndSources(data); + + if (text) { + params.onChunk(text); + } + if (sources.length > 0) { + const sourcesText = sources + .map((s, i) => `${i + 1}. [${s.title || s.url}](${s.url})`) + .join("\n"); + params.onChunk(`\n\nSources:\n${sourcesText}`); + } + + await params.onComplete(); + } catch (error) { + console.error("Error using Gemini google_search grounding:", error); + params.onError(getErrorMessage(error)); + } +} + +function normalizeToText( + content: OpenAI.ChatCompletionMessageParam["content"], +): string { + if (typeof content === "string") { + return content; + } + + if (Array.isArray(content)) { + return content + .map((part) => (isOpenAITextPart(part) ? part.text : "")) + .filter(Boolean) + .join("\n"); + } + + try { + return JSON.stringify(content); + } catch { + return String(content); + } +} + +async function safeJson(response: Response): Promise { + try { + return (await response.json()) as unknown; + } catch { + return undefined; + } +} + +function getGeminiErrorMessageFromBody(body: unknown): string | undefined { + if (!isPlainObject(body)) { + return undefined; + } + + const errorValue = body["error"]; + if (!isPlainObject(errorValue)) { + return undefined; + } + + const messageValue = errorValue["message"]; + return typeof messageValue === "string" ? messageValue : undefined; +} + +function extractGeminiTextAndSources(data: unknown): { + text: string; + sources: Array<{ url: string; title?: string }>; +} { + if (!isPlainObject(data)) { + return { text: "", sources: [] }; + } + + const candidatesValue = data["candidates"]; + if (!isUnknownArray(candidatesValue) || candidatesValue.length === 0) { + return { text: "", sources: [] }; + } + + const firstCandidate = candidatesValue[0]; + if (!isPlainObject(firstCandidate)) { + return { text: "", sources: [] }; + } + + const text = extractTextFromCandidate(firstCandidate); + const grounding = firstCandidate["groundingMetadata"]; + const sources: Array<{ url: string; title?: string }> = []; + + if (isPlainObject(grounding)) { + const groundingChunksValue = grounding["groundingChunks"]; + if (Array.isArray(groundingChunksValue)) { + for (const chunk of groundingChunksValue) { + if (!isPlainObject(chunk)) continue; + const webValue = chunk["web"]; + if (!isPlainObject(webValue)) continue; + const uriValue = webValue["uri"]; + if (typeof uriValue !== "string" || !uriValue) continue; + const titleValue = webValue["title"]; + sources.push({ + url: uriValue, + title: + typeof titleValue === "string" ? titleValue : undefined, + }); + } + } + + const webResultsValue = grounding["webResults"]; + if (Array.isArray(webResultsValue)) { + for (const result of webResultsValue) { + if (!isPlainObject(result)) continue; + const urlValue = result["url"]; + if (typeof urlValue !== "string" || !urlValue) continue; + const titleValue = result["title"]; + sources.push({ + url: urlValue, + title: + typeof titleValue === "string" ? titleValue : undefined, + }); + } + } + + const citationsValue = grounding["citations"]; + if (Array.isArray(citationsValue)) { + for (const citation of citationsValue) { + if (!isPlainObject(citation)) continue; + const uriValue = citation["uri"]; + if (typeof uriValue !== "string" || !uriValue) continue; + sources.push({ url: uriValue }); + } + } + } + + const deduped = new Map(); + for (const source of sources) { + if (!deduped.has(source.url)) { + deduped.set(source.url, source); + } + } + + return { + text, + sources: Array.from(deduped.values()), + }; +} + +function extractTextFromCandidate(candidate: Record): string { + const contentValue = candidate["content"]; + if (!isPlainObject(contentValue)) { + return ""; + } + + const partsValue = contentValue["parts"]; + if (!isUnknownArray(partsValue)) { + return ""; + } + + return partsValue + .map((part) => { + if (!isPlainObject(part)) return ""; + const textValue = part["text"]; + return typeof textValue === "string" ? textValue : ""; + }) + .filter(Boolean) + .join("") + .trim(); +} + +type OpenAITextPart = { type: "text"; text: string }; + +function isOpenAITextPart(part: unknown): part is OpenAITextPart { + return ( + isPlainObject(part) && + part["type"] === "text" && + typeof part["text"] === "string" + ); +} + +function isPlainObject(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function isUnknownArray(value: unknown): value is unknown[] { + return Array.isArray(value); +} diff --git a/src/core/chorus/ModelProviders/ProviderGrok.ts b/src/core/chorus/ModelProviders/ProviderGrok.ts index ac09d065..193e2455 100644 --- a/src/core/chorus/ModelProviders/ProviderGrok.ts +++ b/src/core/chorus/ModelProviders/ProviderGrok.ts @@ -4,7 +4,7 @@ import { IProvider } from "./IProvider"; import { canProceedWithProvider } from "@core/utilities/ProxyUtils"; import OpenAICompletionsAPIUtils from "@core/chorus/OpenAICompletionsAPIUtils"; import JSON5 from "json5"; -import _ from "lodash"; +import * as Prompts from "@core/chorus/prompts/prompts"; interface ProviderError { message: string; @@ -33,17 +33,10 @@ export class ProviderGrok implements IProvider { onChunk, onComplete, additionalHeaders, + enabledToolsets, customBaseUrl, }: StreamResponseParams) { const modelName = modelConfig.modelId.split("::")[1]; - if ( - modelName !== "grok-3-beta" && - modelName !== "grok-3-mini-beta" && - modelName !== "grok-3-mini-fast-beta" && - modelName !== "grok-3-fast-beta" - ) { - throw new Error(`Unsupported model: ${modelName}`); - } const { canProceed, reason } = canProceedWithProvider("grok", apiKeys); @@ -73,34 +66,191 @@ export class ProviderGrok implements IProvider { }, ); - if (modelConfig.systemPrompt) { - messages = [ - { - role: "system", - content: modelConfig.systemPrompt, - }, - ...messages, - ]; + const systemPrompt = [ + modelConfig.showThoughts + ? Prompts.THOUGHTS_SYSTEM_PROMPT + : undefined, + modelConfig.systemPrompt, + ] + .filter(Boolean) + .join("\n\n"); + + if (systemPrompt) { + messages = [{ role: "system", content: systemPrompt }, ...messages]; + } + + const nativeWebSearchEnabled = + enabledToolsets?.includes("web") ?? false; + const supportsNativeWebSearch = modelName.startsWith("grok-4"); + const shouldUseNativeWebSearch = + nativeWebSearchEnabled && supportsNativeWebSearch; + + if (shouldUseNativeWebSearch) { + try { + // Convert chat messages to the OpenAI Responses API input format. + const input = + convertChatCompletionMessagesToResponsesInput(messages); + + const runSearch = async (tools: OpenAI.Responses.Tool[]) => { + const stream = client.responses.stream({ + model: modelName, + input, + tools, + tool_choice: "auto", + }); + + for await (const event of stream) { + if ( + isPlainObject(event) && + event["type"] === "response.output_text.delta" && + typeof event["delta"] === "string" + ) { + onChunk(event["delta"]); + } + } + + const finalResponse = await stream.finalResponse(); + const citations = ( + finalResponse as unknown as Record + )?.["citations"]; + if (Array.isArray(citations) && citations.length > 0) { + const sourcesText = citations + .map((url, index) => + typeof url === "string" + ? `${index + 1}. [${url}](${url})` + : `${index + 1}. ${safeToString(url)}`, + ) + .join("\n"); + onChunk(`\n\nSources:\n${sourcesText}`); + } + }; + + try { + const tools: OpenAI.Responses.Tool[] = [ + { type: "web_search" }, + // @ts-expect-error xAI supports an additional server-side tool for searching X posts. + { type: "x_search" }, + ]; + await runSearch(tools); + } catch (error) { + const errorText = safeToString(error).toLowerCase(); + const xSearchUnsupported = + errorText.includes("x_search") && + (errorText.includes("unknown") || + errorText.includes("unsupported") || + errorText.includes("not supported")); + if (!xSearchUnsupported) { + throw error; + } + + await runSearch([{ type: "web_search" }]); + } + + await onComplete(); + return; + } catch (error: unknown) { + console.error("Raw error:", error); + console.error(JSON.stringify(error, null, 2)); + + if ( + isProviderError(error) && + error.message === "Provider returned error" + ) { + const errorDetails: ProviderError = JSON5.parse( + error.error?.metadata?.raw || + error.metadata?.raw || + "{}", + ); + throw new Error( + `Provider returned error: ${errorDetails.error?.message || error.message}`, + ); + } + throw error; + } } + // Grok 3 Mini is the only model that supports configurable reasoning effort control + // Per Roo Code docs: "only the Grok 3 Mini models support configurable reasoning effort control" + // Other models (grok-4, grok-4-fast, grok-4-1-fast, grok-code-fast-1, grok-3, grok-3-fast) + // are reasoning-capable but don't expose the reasoning_effort parameter + const isGrok3Mini = modelName.includes("grok-3-mini"); + const streamParams: OpenAI.ChatCompletionCreateParamsStreaming & { - include_reasoning: boolean; + reasoning_effort?: string; } = { model: modelName, messages, stream: true, - include_reasoning: true, }; + // Only Grok 3 Mini supports reasoning effort control + if (isGrok3Mini && modelConfig.reasoningEffort) { + // Map effort levels to supported values + // Grok 3 Mini only supports: "low" | "high" + const effortMap: Record< + string, + "low" | "medium" | "high" | "xhigh" | "minimal" + > = { + low: "low", + medium: "high" as const, // medium maps to high + high: "high", + xhigh: "high" as const, // xhigh maps to high + }; + streamParams.reasoning_effort = + effortMap[modelConfig.reasoningEffort] || "low"; + } + try { const stream = await client.chat.completions.create(streamParams); + let inReasoning = false; + let reasoningStartedAtMs: number | undefined; + + const closeReasoning = () => { + if (!inReasoning) return; + inReasoning = false; + onChunk(""); + if (reasoningStartedAtMs !== undefined) { + const seconds = Math.max( + 1, + Math.round((Date.now() - reasoningStartedAtMs) / 1000), + ); + onChunk(``); + } + reasoningStartedAtMs = undefined; + }; + for await (const chunk of stream) { - if (chunk.choices[0]?.delta?.content) { - onChunk(chunk.choices[0].delta.content); + const delta = chunk.choices[0]?.delta as unknown as { + content?: string; + reasoning?: string; + reasoning_content?: string; + }; + + const reasoningDelta = + typeof delta?.reasoning === "string" + ? delta.reasoning + : typeof delta?.reasoning_content === "string" + ? delta.reasoning_content + : undefined; + + if (reasoningDelta) { + if (!inReasoning) { + inReasoning = true; + reasoningStartedAtMs = Date.now(); + onChunk(""); + } + onChunk(reasoningDelta); + } + + if (delta?.content) { + closeReasoning(); + onChunk(delta.content); } } + closeReasoning(); + await onComplete(); } catch (error: unknown) { console.error("Raw error:", error); @@ -121,3 +271,112 @@ export class ProviderGrok implements IProvider { } } } + +function convertChatCompletionMessagesToResponsesInput( + messages: OpenAI.ChatCompletionMessageParam[], +): OpenAI.Responses.ResponseInputItem[] { + return messages.map((message) => { + const role = normalizeResponsesRole(message.role); + const content = message.content; + + if (typeof content === "string") { + return { role, content }; + } + + if (Array.isArray(content)) { + const parts: OpenAI.Responses.ResponseInputContent[] = []; + + for (const part of content) { + if (!isPlainObject(part) || typeof part["type"] !== "string") { + parts.push({ + type: "input_text", + text: safeToString(part), + }); + continue; + } + + if (part["type"] === "text") { + parts.push({ + type: "input_text", + text: + typeof part["text"] === "string" + ? part["text"] + : safeToString(part["text"]), + }); + continue; + } + + if (part["type"] === "image_url") { + const imageUrlValue = part["image_url"]; + const imageUrl = + isPlainObject(imageUrlValue) && + typeof imageUrlValue["url"] === "string" + ? imageUrlValue["url"] + : null; + const detail = isPlainObject(imageUrlValue) + ? normalizeImageDetail(imageUrlValue["detail"]) + : "auto"; + + parts.push({ + type: "input_image", + detail, + image_url: imageUrl, + }); + continue; + } + + parts.push({ + type: "input_text", + text: safeToString(part), + }); + } + + return { role, content: parts }; + } + + if (content === null || content === undefined) { + return { role, content: "" }; + } + + return { role, content: safeToString(content) }; + }); +} + +type ResponsesRole = "user" | "assistant" | "system" | "developer"; + +function normalizeResponsesRole( + role: OpenAI.ChatCompletionMessageParam["role"], +): ResponsesRole { + if ( + role === "user" || + role === "assistant" || + role === "system" || + role === "developer" + ) { + return role; + } + return "user"; +} + +function normalizeImageDetail(detail: unknown): "low" | "high" | "auto" { + if (detail === "low" || detail === "high" || detail === "auto") { + return detail; + } + return "auto"; +} + +function isPlainObject(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function safeToString(value: unknown): string { + if (typeof value === "string") { + return value; + } + try { + const jsonValue = JSON.stringify(value); + return jsonValue ?? String(value); + } catch { + return String(value); + } +} diff --git a/src/core/chorus/ModelProviders/ProviderGroq.ts b/src/core/chorus/ModelProviders/ProviderGroq.ts new file mode 100644 index 00000000..2abf3be7 --- /dev/null +++ b/src/core/chorus/ModelProviders/ProviderGroq.ts @@ -0,0 +1,86 @@ +import OpenAI from "openai"; +import { fetch as tauriFetch } from "@tauri-apps/plugin-http"; +import { StreamResponseParams } from "../Models"; +import { IProvider } from "./IProvider"; +import { canProceedWithProvider } from "@core/utilities/ProxyUtils"; +import OpenAICompletionsAPIUtils from "@core/chorus/OpenAICompletionsAPIUtils"; + +export class ProviderGroq implements IProvider { + async streamResponse({ + modelConfig, + llmConversation, + apiKeys, + onChunk, + onComplete, + additionalHeaders, + tools, + customBaseUrl, + }: StreamResponseParams) { + const modelName = modelConfig.modelId.split("::")[1]; + + const { canProceed, reason } = canProceedWithProvider("groq", apiKeys); + if (!canProceed) { + throw new Error( + reason || "Please add your Groq API key in Settings.", + ); + } + + const client = new OpenAI({ + baseURL: customBaseUrl || "https://api.groq.com/openai/v1", + apiKey: apiKeys.groq, + fetch: tauriFetch, + defaultHeaders: { + ...(additionalHeaders ?? {}), + }, + dangerouslyAllowBrowser: true, + }); + + const functionSupport = (tools?.length ?? 0) > 0; + const messages = await OpenAICompletionsAPIUtils.convertConversation( + llmConversation, + { imageSupport: true, functionSupport }, + ); + + const params: OpenAI.ChatCompletionCreateParamsStreaming = { + model: modelName, + messages: [ + ...(modelConfig.systemPrompt + ? [ + { + role: "system" as const, + content: modelConfig.systemPrompt, + }, + ] + : []), + ...messages, + ], + stream: true, + }; + + if (tools && tools.length > 0) { + params.tools = + OpenAICompletionsAPIUtils.convertToolDefinitions(tools); + params.tool_choice = "auto"; + } + + const chunks: OpenAI.ChatCompletionChunk[] = []; + const stream = await client.chat.completions.create(params); + + for await (const chunk of stream) { + chunks.push(chunk); + if (chunk.choices[0]?.delta?.content) { + onChunk(chunk.choices[0].delta.content); + } + } + + const toolCalls = OpenAICompletionsAPIUtils.convertToolCalls( + chunks, + tools ?? [], + ); + + await onComplete( + undefined, + toolCalls.length > 0 ? toolCalls : undefined, + ); + } +} diff --git a/src/core/chorus/ModelProviders/ProviderLMStudio.ts b/src/core/chorus/ModelProviders/ProviderLMStudio.ts deleted file mode 100644 index c09ee259..00000000 --- a/src/core/chorus/ModelProviders/ProviderLMStudio.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { IProvider } from "./IProvider"; -import { llmMessageToString, StreamResponseParams } from "../Models"; -import OpenAI from "openai"; -import { SettingsManager } from "@core/utilities/Settings"; - -export class ProviderLMStudio implements IProvider { - async streamResponse({ - llmConversation, - modelConfig, - onChunk, - onComplete, - }: StreamResponseParams): Promise { - const settings = await SettingsManager.getInstance().get(); - const baseURL = settings.lmStudioBaseUrl || "http://localhost:1234/v1"; - - const client = new OpenAI({ - baseURL, - apiKey: "not-needed", // LM Studio doesn't require an API key - dangerouslyAllowBrowser: true, - }); - - const messages = llmConversation.map((msg) => ({ - role: msg.role === "tool_results" ? "user" : msg.role, - content: llmMessageToString(msg), - })); - - const stream = await client.chat.completions.create({ - model: modelConfig.modelId.split("::")[1], - messages, - stream: true, - }); - - for await (const chunk of stream) { - if (chunk.choices[0]?.delta?.content) { - onChunk(chunk.choices[0].delta.content); - } - } - - await onComplete(); - } -} diff --git a/src/core/chorus/ModelProviders/ProviderMistral.ts b/src/core/chorus/ModelProviders/ProviderMistral.ts new file mode 100644 index 00000000..820c6e9c --- /dev/null +++ b/src/core/chorus/ModelProviders/ProviderMistral.ts @@ -0,0 +1,89 @@ +import OpenAI from "openai"; +import { fetch as tauriFetch } from "@tauri-apps/plugin-http"; +import { StreamResponseParams } from "../Models"; +import { IProvider } from "./IProvider"; +import { canProceedWithProvider } from "@core/utilities/ProxyUtils"; +import OpenAICompletionsAPIUtils from "@core/chorus/OpenAICompletionsAPIUtils"; + +export class ProviderMistral implements IProvider { + async streamResponse({ + modelConfig, + llmConversation, + apiKeys, + onChunk, + onComplete, + additionalHeaders, + tools, + customBaseUrl, + }: StreamResponseParams) { + const modelName = modelConfig.modelId.split("::")[1]; + + const { canProceed, reason } = canProceedWithProvider( + "mistral", + apiKeys, + ); + if (!canProceed) { + throw new Error( + reason || "Please add your Mistral API key in Settings.", + ); + } + + const client = new OpenAI({ + baseURL: customBaseUrl || "https://api.mistral.ai/v1", + apiKey: apiKeys.mistral, + fetch: tauriFetch, + defaultHeaders: { + ...(additionalHeaders ?? {}), + }, + dangerouslyAllowBrowser: true, + }); + + const functionSupport = (tools?.length ?? 0) > 0; + const messages = await OpenAICompletionsAPIUtils.convertConversation( + llmConversation, + { imageSupport: true, functionSupport }, + ); + + const params: OpenAI.ChatCompletionCreateParamsStreaming = { + model: modelName, + messages: [ + ...(modelConfig.systemPrompt + ? [ + { + role: "system" as const, + content: modelConfig.systemPrompt, + }, + ] + : []), + ...messages, + ], + stream: true, + }; + + if (tools && tools.length > 0) { + params.tools = + OpenAICompletionsAPIUtils.convertToolDefinitions(tools); + params.tool_choice = "auto"; + } + + const chunks: OpenAI.ChatCompletionChunk[] = []; + const stream = await client.chat.completions.create(params); + + for await (const chunk of stream) { + chunks.push(chunk); + if (chunk.choices[0]?.delta?.content) { + onChunk(chunk.choices[0].delta.content); + } + } + + const toolCalls = OpenAICompletionsAPIUtils.convertToolCalls( + chunks, + tools ?? [], + ); + + await onComplete( + undefined, + toolCalls.length > 0 ? toolCalls : undefined, + ); + } +} diff --git a/src/core/chorus/ModelProviders/ProviderNvidia.ts b/src/core/chorus/ModelProviders/ProviderNvidia.ts new file mode 100644 index 00000000..7f262c57 --- /dev/null +++ b/src/core/chorus/ModelProviders/ProviderNvidia.ts @@ -0,0 +1,244 @@ +import OpenAI from "openai"; +import { fetch as tauriFetch } from "@tauri-apps/plugin-http"; +import { + StreamResponseParams, + LLMMessage, + readImageAttachment, + encodeTextAttachment, + attachmentMissingFlag, + encodeWebpageAttachment, +} from "../Models"; +import { IProvider } from "./IProvider"; +import { canProceedWithProvider } from "@core/utilities/ProxyUtils"; +import { UserToolCall, getUserToolNamespacedName } from "@core/chorus/Toolsets"; +import { parseToolCallArguments } from "@core/chorus/ToolCallArgs"; + +export class ProviderNvidia implements IProvider { + async streamResponse({ + modelConfig, + llmConversation, + apiKeys, + onChunk, + onComplete, + tools, + }: StreamResponseParams) { + const modelId = modelConfig.modelId.split("::")[1]; + + const { canProceed, reason } = canProceedWithProvider( + "nvidia", + apiKeys, + ); + + if (!canProceed) { + throw new Error( + reason || "Please add your Nvidia API key in Settings.", + ); + } + + // Convert conversation to OpenAI-compatible format + const messages = await convertConversationToNvidia(llmConversation); + + // Add system prompt if provided + if (modelConfig.systemPrompt) { + messages.unshift({ + role: "system", + content: modelConfig.systemPrompt, + }); + } + + // Create OpenAI client with Nvidia base URL + const client = new OpenAI({ + apiKey: apiKeys.nvidia, + baseURL: "https://integrate.api.nvidia.com/v1", + fetch: tauriFetch, + dangerouslyAllowBrowser: true, + }); + + // Build chat completion parameters + const createParams: OpenAI.Chat.ChatCompletionCreateParams = { + model: modelId, + messages, + stream: true, + ...(tools && tools.length > 0 + ? { + tools: tools.map((tool) => ({ + type: "function", + function: { + name: getUserToolNamespacedName(tool), + description: tool.description, + parameters: tool.inputSchema, + }, + })), + tool_choice: "auto", + } + : {}), + }; + + const stream = await client.chat.completions.create(createParams); + + let fullContent = ""; + const toolCalls: UserToolCall[] = []; + const accumulatedToolCalls: Record< + number, + { + id: string; + name: string; + arguments: string; + } + > = {}; + + try { + for await (const chunk of stream) { + const delta = chunk.choices[0]?.delta; + + if (!delta) continue; + + // Handle text content + if (delta.content) { + fullContent += delta.content; + onChunk(delta.content); + } + + // Handle tool calls + if (delta.tool_calls) { + for (const toolCall of delta.tool_calls) { + const index = toolCall.index; + + // Initialize tool call if new + if (!accumulatedToolCalls[index]) { + accumulatedToolCalls[index] = { + id: toolCall.id || "", + name: toolCall.function?.name || "", + arguments: "", + }; + } + + // Accumulate function arguments + if (toolCall.function?.arguments) { + accumulatedToolCalls[index].arguments += + toolCall.function.arguments; + } + } + } + } + + // Process completed tool calls + for (const accumulated of Object.values(accumulatedToolCalls)) { + const calledTool = tools?.find( + (t) => getUserToolNamespacedName(t) === accumulated.name, + ); + const { args, parseError } = parseToolCallArguments( + accumulated.arguments, + ); + + toolCalls.push({ + id: accumulated.id, + namespacedToolName: accumulated.name, + args, + toolMetadata: { + description: calledTool?.description, + inputSchema: calledTool?.inputSchema, + ...(parseError ? { parseError } : {}), + }, + }); + } + + await onComplete( + fullContent || undefined, + toolCalls.length > 0 ? toolCalls : undefined, + ); + } catch (error) { + console.error("Nvidia streaming error:", error); + throw error; + } + } +} + +/** + * Converts LLM conversation format to OpenAI-compatible format for Nvidia NIM + */ +async function convertConversationToNvidia( + messages: LLMMessage[], +): Promise { + const nvidiaMessages: OpenAI.Chat.ChatCompletionMessageParam[] = []; + + for (const message of messages) { + if (message.role === "user") { + const content: Array< + | OpenAI.Chat.ChatCompletionContentPartText + | OpenAI.Chat.ChatCompletionContentPartImage + > = []; + + // Add text and webpage attachments as text content + let textContent = ""; + for (const attachment of message.attachments) { + if (attachment.type === "text") { + textContent += await encodeTextAttachment(attachment); + } else if (attachment.type === "webpage") { + textContent += await encodeWebpageAttachment(attachment); + } else if (attachment.type === "image") { + // Add image as separate content part + const fileExt = + attachment.path.split(".").pop()?.toLowerCase() || ""; + const mimeType = fileExt === "jpg" ? "jpeg" : fileExt; + content.push({ + type: "image_url", + image_url: { + url: `data:image/${mimeType};base64,${await readImageAttachment(attachment)}`, + }, + }); + } else if (attachment.type === "pdf") { + // PDFs not natively supported, add as missing flag + textContent += attachmentMissingFlag(attachment); + } + } + + // Add text content + if (textContent || message.content) { + content.push({ + type: "text", + text: textContent + message.content, + }); + } + + nvidiaMessages.push({ + role: "user", + content: + content.length === 1 && content[0].type === "text" + ? content[0].text + : content, + }); + } else if (message.role === "assistant") { + const assistantMessage: OpenAI.Chat.ChatCompletionAssistantMessageParam = + { + role: "assistant", + content: message.content || undefined, + }; + + // Add tool calls if present + if (message.toolCalls && message.toolCalls.length > 0) { + assistantMessage.tool_calls = message.toolCalls.map((tc) => ({ + id: tc.id, + type: "function", + function: { + name: tc.namespacedToolName, + arguments: JSON.stringify(tc.args), + }, + })); + } + + nvidiaMessages.push(assistantMessage); + } else if (message.role === "tool_results") { + // Add tool results + for (const result of message.toolResults) { + nvidiaMessages.push({ + role: "tool", + tool_call_id: result.id, + content: result.content, + }); + } + } + } + + return nvidiaMessages; +} diff --git a/src/core/chorus/ModelProviders/ProviderOllama.ts b/src/core/chorus/ModelProviders/ProviderOllama.ts deleted file mode 100644 index 00fea875..00000000 --- a/src/core/chorus/ModelProviders/ProviderOllama.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { IProvider } from "./IProvider"; -import { - LLMMessage, - StreamResponseParams, - llmMessageToString, - readTextAttachment, - readWebpageAttachment, -} from "../Models"; -import { ollamaClient } from "../OllamaClient"; - -interface FormattedOllamaMessage { - role: "user" | "assistant"; - content: string; - images?: string[]; -} - -export class ProviderOllama implements IProvider { - async streamResponse({ - llmConversation, - modelConfig, - onChunk, - onComplete, - }: StreamResponseParams): Promise { - const messages = await Promise.all( - llmConversation.map(formatMessageWithAttachments), - ); - - for await (const chunk of ollamaClient.streamChat( - modelConfig.modelId.split("::")[1], - messages, - )) { - onChunk(chunk); - } - - await onComplete(); - } -} - -async function formatMessageWithAttachments( - message: LLMMessage, -): Promise { - if (message.role === "tool_results") { - return { - role: "user", - content: llmMessageToString(message), // will encode tool results as XML - }; - } - - if (message.role === "assistant") { - return { - role: "assistant", - content: message.content, - }; - } - - const content = message.content; - const messageData: FormattedOllamaMessage = { - role: message.role, - content: content, - }; - - // Check for image attachments first and throw error if found - const imageAttachments = message.attachments.filter( - (a) => a.type === "image", - ); - if (imageAttachments.length) { - throw new Error( - "Image attachments are not currently supported for Ollama models", - ); - } - - // Handle text and webpage attachments by appending their content - const textAndWebAttachments = message.attachments.filter( - (a) => a.type === "text" || a.type === "webpage", - ); - - if (textAndWebAttachments.length) { - const attachmentContents = await Promise.all( - textAndWebAttachments.map(async (attachment) => { - let content = ""; - if (attachment.type === "text") { - content = await readTextAttachment(attachment); - } else if (attachment.type === "webpage") { - content = await readWebpageAttachment(attachment); - } - return `\n\n[${attachment.originalName}]:\n${content}`; - }), - ); - messageData.content += attachmentContents.join("\n"); - } - - return messageData; -} diff --git a/src/core/chorus/ModelProviders/ProviderOpenAI.ts b/src/core/chorus/ModelProviders/ProviderOpenAI.ts index a194a1c3..a6ca7602 100644 --- a/src/core/chorus/ModelProviders/ProviderOpenAI.ts +++ b/src/core/chorus/ModelProviders/ProviderOpenAI.ts @@ -1,4 +1,5 @@ import OpenAI from "openai"; +import { fetch as tauriFetch } from "@tauri-apps/plugin-http"; import { StreamResponseParams, LLMMessage, @@ -14,6 +15,7 @@ import { IProvider } from "./IProvider"; import { canProceedWithProvider } from "@core/utilities/ProxyUtils"; import { UserToolCall, getUserToolNamespacedName } from "@core/chorus/Toolsets"; import { O3_DEEP_RESEARCH_SYSTEM_PROMPT } from "@core/chorus/prompts/prompts"; +import { parseToolCallArguments } from "@core/chorus/ToolCallArgs"; export class ProviderOpenAI implements IProvider { async streamResponse({ @@ -24,31 +26,18 @@ export class ProviderOpenAI implements IProvider { onComplete, additionalHeaders, tools, + enabledToolsets, customBaseUrl, }: StreamResponseParams) { const modelId = modelConfig.modelId.split("::")[1]; - if ( - modelId !== "gpt-4o" && - modelId !== "gpt-4o-mini" && - modelId !== "o1" && - modelId !== "o3-mini" && - modelId !== "gpt-4.5-preview" && - modelId !== "o1-pro" && - modelId !== "gpt-4.1" && - modelId !== "gpt-4.1-mini" && - modelId !== "gpt-4.1-nano" && - modelId !== "o3" && - modelId !== "o4-mini" && - modelId !== "o3-pro" && - modelId !== "o3-deep-research" && - modelId !== "gpt-5" && - modelId !== "gpt-5-mini" && - modelId !== "gpt-5-nano" - ) { - throw new Error(`Unsupported model: ${modelId}`); - } - const imageSupport = modelId !== "o3-mini" && modelId !== "o1"; + // Determine image support based on model naming patterns + const imageSupport = + modelId.includes("gpt-4") || + modelId.includes("gpt-5") || + (modelId.startsWith("o") && + !modelId.includes("mini") && + modelId !== "o1"); const { canProceed, reason } = canProceedWithProvider( "openai", @@ -73,50 +62,66 @@ export class ProviderOpenAI implements IProvider { modelId === "o3" || modelId === "o3-pro" || modelId === "o4-mini" || - modelId === "o3-deep-research"; + modelId === "o3-deep-research" || + modelId.startsWith("gpt-5") || // All GPT-5 models support reasoning + modelId.startsWith("o"); // All o-series models support reasoning + + const shouldRequestReasoningSummary = + Boolean(modelConfig.showThoughts) && isReasoningModel; // Add system message if needed - if (isReasoningModel || modelConfig.systemPrompt) { - let systemContent = ""; + let systemContent = ""; - // Always add formatting message for reasoning models - if (isReasoningModel) { - systemContent = "Markdown formatting re-enabled."; - } + // OpenAI does not reliably support returning full chain-of-thought for non-reasoning models. + // When showThoughts is enabled, we only request server-provided reasoning summaries for + // reasoning-capable models (o-series / GPT-5) rather than prompting for tags. - // Add special system prompt for o3-deep-research - if (modelId === "o3-deep-research") { - if (systemContent) { - systemContent += "\n" + O3_DEEP_RESEARCH_SYSTEM_PROMPT; - } else { - systemContent = O3_DEEP_RESEARCH_SYSTEM_PROMPT; - } + // Always add formatting message for reasoning models + if (isReasoningModel) { + systemContent = systemContent + ? systemContent + "\n\nMarkdown formatting re-enabled." + : "Markdown formatting re-enabled."; + } + + // Add special system prompt for o3-deep-research + if (modelId === "o3-deep-research") { + if (systemContent) { + systemContent += "\n" + O3_DEEP_RESEARCH_SYSTEM_PROMPT; + } else { + systemContent = O3_DEEP_RESEARCH_SYSTEM_PROMPT; } + } - // Append system prompt if provided - if (modelConfig.systemPrompt) { - if (systemContent) { - systemContent += `\n ${modelConfig.systemPrompt}`; - } else { - systemContent = modelConfig.systemPrompt; - } + // Append system prompt if provided + if (modelConfig.systemPrompt) { + if (systemContent) { + systemContent += `\n ${modelConfig.systemPrompt}`; + } else { + systemContent = modelConfig.systemPrompt; } + } + if (systemContent) { messages = [ { role: "developer", content: systemContent }, ...messages, ]; } - // Convert tools to OpenAI format - // For o3-deep-research, filter out native web search since we use OpenAI's web_search_preview - const filteredTools = - modelId === "o3-deep-research" - ? tools?.filter((tool) => tool.toolsetName !== "web") - : tools; + const nativeWebSearchEnabled = + enabledToolsets?.includes("web") ?? false; + + const supportsNativeWebSearch = + modelId.startsWith("o") || + modelId.startsWith("gpt-4o") || + modelId.startsWith("gpt-4.1") || + modelId.startsWith("gpt-5"); + + const shouldUseNativeWebSearch = + nativeWebSearchEnabled && supportsNativeWebSearch; const openaiTools: Array | undefined = - filteredTools?.map((tool) => ({ + tools?.map((tool) => ({ type: "function", name: getUserToolNamespacedName(tool), // name goes at this level for OpenAI description: tool.description, @@ -128,27 +133,52 @@ export class ProviderOpenAI implements IProvider { strict: false, })); + const nativeWebSearchTools = shouldUseNativeWebSearch + ? [ + { + type: "web_search_preview", + search_context_size: "medium", + } as const, + ] + : []; + // eslint-disable-next-line @typescript-eslint/no-explicit-any const createParams: any = { model: modelId, input: messages, - tools: openaiTools || [], + tools: [...nativeWebSearchTools, ...(openaiTools || [])], tool_choice: - tools && tools.length > 0 + nativeWebSearchTools.length > 0 || (tools && tools.length > 0) ? ("auto" as const) : ("none" as const), stream: true as const, + ...(nativeWebSearchTools.length > 0 && { + include: ["web_search_call.action.sources"], + }), ...(isReasoningModel && { reasoning: { effort: modelConfig.reasoningEffort || "medium", + ...(shouldRequestReasoningSummary && { summary: "auto" }), }, }), }; + // Debug: Log reasoning parameters + console.log(`[ProviderOpenAI] Model: ${modelId}`); + console.log(`[ProviderOpenAI] Is reasoning model: ${isReasoningModel}`); + console.log( + `[ProviderOpenAI] modelConfig.reasoningEffort: ${modelConfig.reasoningEffort}`, + ); + console.log( + `[ProviderOpenAI] createParams.reasoning:`, + (createParams as { reasoning?: unknown }).reasoning, + ); + // Add special tools for o3-deep-research if (modelId === "o3-deep-research") { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access createParams.reasoning = { + effort: modelConfig.reasoningEffort || "medium", summary: "auto", }; // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access @@ -168,11 +198,15 @@ export class ProviderOpenAI implements IProvider { // o3-deep-research requires tool_choice to be "auto" when using code_interpreter // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access createParams.tool_choice = "auto"; + // Include sources so we can surface citations/URLs when available + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + createParams.include = ["web_search_call.action.sources"]; } const client = new OpenAI({ apiKey: apiKeys.openai, baseURL: customBaseUrl, + fetch: tauriFetch, dangerouslyAllowBrowser: true, defaultHeaders: { ...(additionalHeaders ?? {}), @@ -192,15 +226,32 @@ export class ProviderOpenAI implements IProvider { type: "response.output_text.delta"; delta: string; } + | { + // Text done event (some SDKs may emit this instead of deltas) + type: "response.output_text.done"; + text: string; + } + | { + // A new content part was added to a message item + type: "response.content_part.added"; + part: { + type: string; + text?: string; + }; + item_id?: string; + output_index?: number; + content_index?: number; + } | { // Tool call started event type: "response.output_item.added"; item: { - type: "function_call"; + type: string; id: string; - call_id: string; - name: string; - arguments: string; + call_id?: string; + name?: string; + arguments?: string; + status?: string; }; } | { @@ -219,11 +270,11 @@ export class ProviderOpenAI implements IProvider { // Tool call fully completed type: "response.output_item.done"; item: { - type: "function_call"; + type: string; id: string; - call_id: string; - name: string; - arguments: string; + call_id?: string; + name?: string; + arguments?: string; }; } | { @@ -240,6 +291,42 @@ export class ProviderOpenAI implements IProvider { }>; }>; }>; + } + | { + // Some SDKs use response.completed instead of response.done + type: "response.completed"; + response?: { + output?: Array<{ + content?: Array<{ + text?: string; + annotations?: Array<{ + title: string; + url: string; + start_index: number; + end_index: number; + }>; + }>; + }>; + }; + } + | { + type: "response.reasoning_summary_text.delta"; + delta: string; + item_id?: string; + output_index?: number; + summary_index?: number; + } + | { + type: "response.reasoning_summary_text.done"; + text: string; + item_id?: string; + output_index?: number; + summary_index?: number; + } + | { + // Some SDKs emit this alternate event name. + type: "response.reasoning_summary.delta"; + delta: string; }; // Track tool calls in the streamed response @@ -254,99 +341,244 @@ export class ProviderOpenAI implements IProvider { } > = {}; - // Process the streaming response - for await (const event of stream as unknown as AsyncIterable) { - // Handle text streaming - if (event.type === "response.output_text.delta") { - onChunk(event.delta); - } - // TOOL CALL HANDLING - OpenAI streams tool calls in multiple events: - // 1. Tool call initialization - else if ( - event.type === "response.output_item.added" && - event.item.type === "function_call" + let inReasoningSummary = false; + let streamedReasoningSummaryText = false; + let reasoningSummaryStartedAtMs: number | undefined; + let sawOutputText = false; + let appendedCitations = false; + + const sanitizeTextDelta = (text: string) => { + if ( + shouldRequestReasoningSummary && + (text.includes(" { + if (inReasoningSummary) return ""; + inReasoningSummary = true; + streamedReasoningSummaryText = false; + reasoningSummaryStartedAtMs = Date.now(); + return "\n"; + }; + + const closeReasoningSummaryBlockIfNeeded = () => { + if (!inReasoningSummary) return ""; + inReasoningSummary = false; + + let block = "\n"; + if (reasoningSummaryStartedAtMs !== undefined) { + const seconds = Math.max( + 1, + Math.round( + (Date.now() - reasoningSummaryStartedAtMs) / 1000, + ), + ); + block += ``; } - // 3. Tool call arguments complete (contains full arguments) - else if (event.type === "response.function_call_arguments.done") { - // Use the complete arguments - if (accumulatedToolCalls[event.item_id]) { - accumulatedToolCalls[event.item_id].arguments = - event.arguments; + block += "\n\n"; + + reasoningSummaryStartedAtMs = undefined; + streamedReasoningSummaryText = false; + return block; + }; + + const appendCitationsFromOutput = ( + output: + | Array<{ + content?: Array<{ + text?: string; + annotations?: Array<{ + title: string; + url: string; + start_index: number; + end_index: number; + }>; + }>; + }> + | undefined, + ) => { + if (!output || appendedCitations) return; + for (const outputItem of output) { + if (!outputItem.content) continue; + for (const content of outputItem.content) { + if ( + !content.annotations || + content.annotations.length === 0 + ) + continue; + + // Format citations as plain text + let citationText = "\n\n---\n**Citations:**\n"; + + for (const citation of content.annotations) { + citationText += `\n- **${citation.title}**\n`; + citationText += ` URL: ${citation.url}\n`; + + // Extract the cited text if we have the full text + if (content.text) { + const citedText = content.text.substring( + citation.start_index, + citation.end_index, + ); + citationText += ` Cited text: "${citedText}"\n`; + } + } + + onChunk(citationText); + appendedCitations = true; + return; } } - // 4. Tool call fully complete - else if ( - event.type === "response.output_item.done" && - event.item.type === "function_call" - ) { - // Convert completed tool call to our internal ToolCall format - const namespacedToolName = event.item.name; - const calledTool = tools?.find( - (t) => getUserToolNamespacedName(t) === namespacedToolName, - ); + }; - // Add to our collection of tool calls - toolCalls.push({ - id: event.item.call_id, - namespacedToolName, - args: JSON.parse(event.item.arguments), - toolMetadata: { - description: calledTool?.description, - inputSchema: calledTool?.inputSchema, - }, - }); - } - // 5. Handle response.done event for citations - else if (event.type === "response.done" && event.output) { - // Process citations from o3-deep-research - for (const output of event.output) { - if (output.content) { - for (const content of output.content) { - if ( - content.annotations && - content.annotations.length > 0 - ) { - // Format citations as plain text - let citationText = "\n\n---\n**Citations:**\n"; - - for (const citation of content.annotations) { - citationText += `\n- **${citation.title}**\n`; - citationText += ` URL: ${citation.url}\n`; - - // Extract the cited text if we have the full text - if (content.text) { - const citedText = - content.text.substring( - citation.start_index, - citation.end_index, - ); - citationText += ` Cited text: "${citedText}"\n`; - } - } - - // Send the citations as a text chunk - onChunk(citationText); - } - } + try { + // Process the streaming response + for await (const event of stream as unknown as AsyncIterable) { + if ( + shouldRequestReasoningSummary && + !sawOutputText && + (event.type === "response.reasoning_summary_text.delta" || + event.type === "response.reasoning_summary.delta") + ) { + const openBlock = openReasoningSummaryBlockIfNeeded(); + streamedReasoningSummaryText ||= Boolean(event.delta); + onChunk(openBlock + sanitizeTextDelta(event.delta)); + } else if ( + shouldRequestReasoningSummary && + !sawOutputText && + event.type === "response.reasoning_summary_text.done" + ) { + // Some SDKs only emit a done event with full text. + if (!streamedReasoningSummaryText && event.text) { + const openBlock = openReasoningSummaryBlockIfNeeded(); + streamedReasoningSummaryText = true; + onChunk(openBlock + sanitizeTextDelta(event.text)); + } + const closeBlock = closeReasoningSummaryBlockIfNeeded(); + if (closeBlock) onChunk(closeBlock); + } + // Some SDKs emit an initial output_text part via content_part.added. + // Flush any pending reasoning summary before we start the assistant's visible output. + else if ( + event.type === "response.content_part.added" && + event.part.type === "output_text" + ) { + if ( + typeof event.part.text === "string" && + event.part.text + ) { + sawOutputText = true; + const closeBlock = closeReasoningSummaryBlockIfNeeded(); + onChunk( + closeBlock + sanitizeTextDelta(event.part.text), + ); + } else { + const closeBlock = closeReasoningSummaryBlockIfNeeded(); + if (closeBlock) onChunk(closeBlock); + } + } + // Handle text streaming + else if (event.type === "response.output_text.delta") { + sawOutputText = true; + const closeBlock = closeReasoningSummaryBlockIfNeeded(); + onChunk(closeBlock + sanitizeTextDelta(event.delta)); + } + // Handle text done event (if no deltas were emitted) + else if (event.type === "response.output_text.done") { + if (!sawOutputText && event.text) { + sawOutputText = true; + const closeBlock = closeReasoningSummaryBlockIfNeeded(); + onChunk(closeBlock + sanitizeTextDelta(event.text)); + } + } + // TOOL CALL HANDLING - OpenAI streams tool calls in multiple events: + // 1. Tool call initialization + else if ( + event.type === "response.output_item.added" && + event.item.type === "function_call" + ) { + // Initialize the tool call structure when first encountered + accumulatedToolCalls[event.item.id] = { + id: event.item.id, + call_id: event.item.call_id || "", + name: event.item.name || "", + arguments: event.item.arguments || "", + }; + } + // 2. Tool call arguments streaming (may come in multiple chunks) + else if ( + event.type === "response.function_call_arguments.delta" + ) { + // Accumulate argument JSON as it streams in + if (accumulatedToolCalls[event.item_id]) { + accumulatedToolCalls[event.item_id].arguments += + event.delta; } } + // 3. Tool call arguments complete (contains full arguments) + else if ( + event.type === "response.function_call_arguments.done" + ) { + // Use the complete arguments + if (accumulatedToolCalls[event.item_id]) { + accumulatedToolCalls[event.item_id].arguments = + event.arguments; + } + } + // 4. Tool call fully complete + else if ( + event.type === "response.output_item.done" && + event.item.type === "function_call" + ) { + // Convert completed tool call to our internal ToolCall format + const namespacedToolName = event.item.name || ""; + const calledTool = tools?.find( + (t) => + getUserToolNamespacedName(t) === namespacedToolName, + ); + const { args, parseError } = parseToolCallArguments( + event.item.arguments || "", + ); + + // Add to our collection of tool calls + toolCalls.push({ + id: event.item.call_id || "", + namespacedToolName, + args, + toolMetadata: { + description: calledTool?.description, + inputSchema: calledTool?.inputSchema, + ...(parseError ? { parseError } : {}), + }, + }); + } + // 5. Handle response.done event for citations + else if (event.type === "response.done" && event.output) { + const closeBlock = closeReasoningSummaryBlockIfNeeded(); + if (closeBlock) onChunk(closeBlock); + appendCitationsFromOutput(event.output); + } + // Some SDKs emit response.completed with the full response payload (including output annotations) + else if (event.type === "response.completed") { + const closeBlock = closeReasoningSummaryBlockIfNeeded(); + if (closeBlock) onChunk(closeBlock); + appendCitationsFromOutput(event.response?.output); + } } + } finally { + const closeBlock = closeReasoningSummaryBlockIfNeeded(); + if (closeBlock) onChunk(closeBlock); } await onComplete( @@ -374,11 +606,19 @@ async function formatBasicMessage( } else { return { role: message.role, - content: message.content, + content: stripThinkBlocks(message.content || ""), }; } } +function stripThinkBlocks(text: string): string { + return text + .replace(/]*?)?>[\s\S]*?<\/think\s*>/g, "") + .replace(/]*?)?>[\s\S]*?<\/thought\s*>/g, "") + .replace(//g, "") + .trim(); +} + async function formamtUserMessageWithAttachments( message: LLMMessageUser, imageSupport: boolean, @@ -478,7 +718,7 @@ async function convertConversationToOpenAI( // First add the assistant message with content openaiMessages.push({ role: "assistant", - content: message.content || "", + content: stripThinkBlocks(message.content || ""), }); // Then add each tool call as a separate message in OpenAI format diff --git a/src/core/chorus/ModelProviders/ProviderOpenRouter.ts b/src/core/chorus/ModelProviders/ProviderOpenRouter.ts index 31c4a8a5..0ef9d8bf 100644 --- a/src/core/chorus/ModelProviders/ProviderOpenRouter.ts +++ b/src/core/chorus/ModelProviders/ProviderOpenRouter.ts @@ -1,4 +1,3 @@ -import _ from "lodash"; import OpenAI from "openai"; import { StreamResponseParams } from "../Models"; import { IProvider, ModelDisabled } from "./IProvider"; @@ -79,10 +78,7 @@ export class ProviderOpenRouter implements IProvider { if (modelConfig.systemPrompt) { messages = [ - { - role: "system", - content: modelConfig.systemPrompt, - }, + { role: "system", content: modelConfig.systemPrompt }, ...messages, ]; } @@ -93,7 +89,7 @@ export class ProviderOpenRouter implements IProvider { model: modelName, messages, stream: true, - include_reasoning: true, + include_reasoning: Boolean(modelConfig.showThoughts), }; // Add tools definitions @@ -105,6 +101,40 @@ export class ProviderOpenRouter implements IProvider { const chunks: OpenAI.ChatCompletionChunk[] = []; let generationId: string | undefined; + let inReasoning = false; + let reasoningStartedAtMs: number | undefined; + let sawNativeReasoning = false; + + const closeReasoning = () => { + if (!inReasoning) return; + inReasoning = false; + onChunk(""); + if (reasoningStartedAtMs !== undefined) { + const seconds = Math.max( + 1, + Math.round((Date.now() - reasoningStartedAtMs) / 1000), + ); + onChunk(``); + } + reasoningStartedAtMs = undefined; + }; + + const sanitizeTextDelta = (text: string) => { + if ( + sawNativeReasoning && + (text.includes(""); + } + onChunk(reasoningDelta); + } + + if (typeof delta?.content === "string" && delta.content) { + closeReasoning(); + onChunk(sanitizeTextDelta(delta.content)); } } } catch (error: unknown) { @@ -161,6 +216,8 @@ export class ProviderOpenRouter implements IProvider { return undefined; } + closeReasoning(); + // Extract usage data from the last chunk const lastChunk = chunks[chunks.length - 1]; let usageData: diff --git a/src/core/chorus/ModelProviders/ProviderTogether.ts b/src/core/chorus/ModelProviders/ProviderTogether.ts new file mode 100644 index 00000000..99fba72f --- /dev/null +++ b/src/core/chorus/ModelProviders/ProviderTogether.ts @@ -0,0 +1,204 @@ +import Together from "together-ai"; +import type { CompletionCreateParamsStreaming } from "together-ai/resources/chat/completions"; +import { + StreamResponseParams, + LLMMessage, + encodeTextAttachment, + attachmentMissingFlag, + encodeWebpageAttachment, +} from "../Models"; +import { IProvider } from "./IProvider"; +import { canProceedWithProvider } from "@core/utilities/ProxyUtils"; +import { UserToolCall, getUserToolNamespacedName } from "@core/chorus/Toolsets"; +import { parseToolCallArguments } from "@core/chorus/ToolCallArgs"; + +export class ProviderTogether implements IProvider { + async streamResponse({ + modelConfig, + llmConversation, + apiKeys, + onChunk, + onComplete, + tools, + }: StreamResponseParams) { + const modelId = modelConfig.modelId.split("::")[1]; + + const { canProceed, reason } = canProceedWithProvider( + "together", + apiKeys, + ); + + if (!canProceed) { + throw new Error( + reason || "Please add your Together.ai API key in Settings.", + ); + } + + // Convert conversation to Together.ai format + const messages = await convertConversationToTogether(llmConversation); + + // Add system prompt if provided + if (modelConfig.systemPrompt) { + messages.unshift({ + role: "system", + content: modelConfig.systemPrompt, + }); + } + + const client = new Together({ + apiKey: apiKeys.together, + }); + + // Build Together.ai chat completion parameters + const createParams: CompletionCreateParamsStreaming = { + model: modelId, + messages, + stream: true, + ...(tools && tools.length > 0 + ? { + tools: tools.map((tool) => ({ + type: "function" as const, + function: { + name: getUserToolNamespacedName(tool), + description: tool.description, + parameters: tool.inputSchema, + }, + })), + tool_choice: "auto", + } + : {}), + }; + + const stream = await client.chat.completions.create(createParams); + + let fullContent = ""; + const toolCalls: UserToolCall[] = []; + const accumulatedToolCalls: Record< + string, + { + id: string; + name: string; + arguments: string; + } + > = {}; + + try { + for await (const chunk of stream) { + const delta = chunk.choices[0]?.delta; + + if (!delta) continue; + + // Handle text content + if (delta.content) { + fullContent += delta.content; + onChunk(delta.content); + } + + // Handle tool calls + if (delta.tool_calls) { + for (const toolCall of delta.tool_calls) { + // Use tool call ID as key to handle multiple concurrent tool calls + const toolId = toolCall.id || ""; + if (!toolId) continue; + + // Initialize tool call if new + if (!accumulatedToolCalls[toolId]) { + accumulatedToolCalls[toolId] = { + id: toolId, + name: toolCall.function?.name || "", + arguments: "", + }; + } + + // Accumulate function arguments + if (toolCall.function?.arguments) { + accumulatedToolCalls[toolId].arguments += + toolCall.function.arguments; + } + } + } + } + + // Process completed tool calls + for (const accumulated of Object.values(accumulatedToolCalls)) { + const calledTool = tools?.find( + (t) => getUserToolNamespacedName(t) === accumulated.name, + ); + const { args, parseError } = parseToolCallArguments( + accumulated.arguments, + ); + + toolCalls.push({ + id: accumulated.id, + namespacedToolName: accumulated.name, + args, + toolMetadata: { + description: calledTool?.description, + inputSchema: calledTool?.inputSchema, + ...(parseError ? { parseError } : {}), + }, + }); + } + + await onComplete( + fullContent || undefined, + toolCalls.length > 0 ? toolCalls : undefined, + ); + } catch (error) { + console.error("Together.ai streaming error:", error); + throw error; + } + } +} + +/** + * Converts LLM conversation format to Together.ai's chat completion format + */ +async function convertConversationToTogether( + messages: LLMMessage[], +): Promise { + const togetherMessages: CompletionCreateParamsStreaming["messages"] = []; + + for (const message of messages) { + if (message.role === "user") { + // Build text content from attachments + let textContent = ""; + for (const attachment of message.attachments) { + if (attachment.type === "text") { + textContent += await encodeTextAttachment(attachment); + } else if (attachment.type === "webpage") { + textContent += await encodeWebpageAttachment(attachment); + } else if (attachment.type === "image") { + // Together AI doesn't support image URLs in the same way as OpenAI + // We'll encode as a note for now + textContent += attachmentMissingFlag(attachment); + } else if (attachment.type === "pdf") { + textContent += attachmentMissingFlag(attachment); + } + } + + togetherMessages.push({ + role: "user", + content: textContent + message.content, + }); + } else if (message.role === "assistant") { + // For now, Together AI doesn't support tool calls in the same format + // Just send the content + togetherMessages.push({ + role: "assistant", + content: message.content || "", + }); + } else if (message.role === "tool_results") { + // Add tool results as tool messages + for (const result of message.toolResults) { + togetherMessages.push({ + role: "tool", + content: result.content || "", + tool_call_id: result.id, + }); + } + } + } + + return togetherMessages; +} diff --git a/src/core/chorus/ModelProviders/ProviderVertex.ts b/src/core/chorus/ModelProviders/ProviderVertex.ts new file mode 100644 index 00000000..28739068 --- /dev/null +++ b/src/core/chorus/ModelProviders/ProviderVertex.ts @@ -0,0 +1,491 @@ +import OpenAI from "openai"; +import OpenAICompletionsAPIUtils from "@core/chorus/OpenAICompletionsAPIUtils"; +import { SettingsManager } from "@core/utilities/Settings"; +import { StreamResponseParams } from "../Models"; +import { IProvider, ModelDisabled } from "./IProvider"; +import { fetch as tauriFetch } from "@tauri-apps/plugin-http"; +import JSON5 from "json5"; + +interface ProviderError { + message: string; + error?: { + message?: string; + metadata?: { raw?: string }; + }; + metadata?: { raw?: string }; +} + +function isProviderError(error: unknown): error is ProviderError { + return ( + typeof error === "object" && + error !== null && + "message" in error && + ("error" in error || "metadata" in error) && + error.message === "Provider returned error" + ); +} + +function getErrorMessage(error: unknown): string { + if (typeof error === "object" && error !== null && "message" in error) { + return (error as { message: string }).message; + } else if (typeof error === "string") { + return error; + } + return "Unknown error"; +} + +function base64UrlEncodeFromBytes(bytes: Uint8Array): string { + const base64 = btoa(String.fromCharCode(...bytes)); + return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, ""); +} + +function base64UrlEncodeJson(value: unknown): string { + return base64UrlEncodeFromBytes( + new TextEncoder().encode(JSON.stringify(value)), + ); +} + +function pemToDerBytes(pem: string): Uint8Array { + const normalized = pem + .trim() + // Handle pasting from service account JSON where newlines are escaped. + .replace(/\\r\\n/g, "\n") + .replace(/\\n/g, "\n") + .replace(/\\r/g, "\n") + // Remove wrapping quotes if the value is copied as a JSON string. + .replace(/^['"]/, "") + .replace(/['"]$/, ""); + + const clean = normalized + .replace(/-----BEGIN [^-]+-----/g, "") + .replace(/-----END [^-]+-----/g, "") + .replace(/\s+/g, "") + .trim(); + let binary: string; + try { + binary = atob(clean); + } catch (_error) { + throw new Error( + "Invalid service account private key. Paste the PEM contents (with real newlines), or paste the JSON value (it should contain \\n sequences).", + ); + } + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i += 1) { + bytes[i] = binary.charCodeAt(i); + } + return bytes; +} + +async function signJwtRS256(params: { + payload: Record; + privateKeyPem: string; +}): Promise { + const header = { alg: "RS256", typ: "JWT" }; + + const encodedHeader = base64UrlEncodeJson(header); + const encodedPayload = base64UrlEncodeJson(params.payload); + const signingInput = `${encodedHeader}.${encodedPayload}`; + + // Work around TS DOM lib typing differences between ArrayBuffer and SharedArrayBuffer. + const keyBytes = new Uint8Array(pemToDerBytes(params.privateKeyPem)); + const key = await crypto.subtle.importKey( + "pkcs8", + keyBytes, + { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, + false, + ["sign"], + ); + + const signature = await crypto.subtle.sign( + "RSASSA-PKCS1-v1_5", + key, + new TextEncoder().encode(signingInput), + ); + + const encodedSignature = base64UrlEncodeFromBytes( + new Uint8Array(signature), + ); + + return `${signingInput}.${encodedSignature}`; +} + +type CachedAccessToken = { + token: string; + expiresAtMs: number; +}; + +let cachedAccessToken: CachedAccessToken | undefined; + +async function getGoogleAccessToken(params: { + clientEmail: string; + privateKey: string; +}): Promise { + const nowMs = Date.now(); + if (cachedAccessToken && cachedAccessToken.expiresAtMs - nowMs > 60_000) { + return cachedAccessToken.token; + } + + const iat = Math.floor(nowMs / 1000); + const exp = iat + 60 * 60; + + const jwt = await signJwtRS256({ + privateKeyPem: params.privateKey, + payload: { + iss: params.clientEmail, + sub: params.clientEmail, + aud: "https://oauth2.googleapis.com/token", + iat, + exp, + scope: "https://www.googleapis.com/auth/cloud-platform", + }, + }); + + const body = new URLSearchParams({ + grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", + assertion: jwt, + }).toString(); + + const response = await tauriFetch("https://oauth2.googleapis.com/token", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body, + }); + + if (!response.ok) { + const errorText = await response.text().catch(() => ""); + throw new Error( + `Failed to fetch Google access token (${response.status}): ${errorText || response.statusText}`, + ); + } + + const tokenResponse = (await response.json()) as { + access_token: string; + expires_in: number; + token_type: string; + }; + + cachedAccessToken = { + token: tokenResponse.access_token, + expiresAtMs: nowMs + tokenResponse.expires_in * 1000, + }; + + return tokenResponse.access_token; +} + +function getVertexBaseUrl(params: { projectId: string; location: string }) { + // Vertex AI OpenAI-compatible API base URL. + // Ref: https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/openai + const host = + params.location === "global" + ? "aiplatform.googleapis.com" + : `${params.location}-aiplatform.googleapis.com`; + return `https://${host}/v1beta1/projects/${params.projectId}/locations/${params.location}/endpoints/openapi`; +} + +function normalizeVertexPublisherModel(model: string): string { + const trimmed = model.trim(); + if (!trimmed) return trimmed; + + // Allow pasting full resource names. + // e.g. "publishers/google/models/gemini-2.5-pro" -> "google/gemini-2.5-pro" + const publishersMatch = trimmed.match( + /^publishers\/([^/]+)\/models\/(.+)$/, + ); + if (publishersMatch) { + return `${publishersMatch[1]}/${publishersMatch[2]}`; + } + + // e.g. "projects//locations//publishers/google/models/gemini-2.5-pro" -> "google/gemini-2.5-pro" + const fullMatch = trimmed.match( + /^projects\/[^/]+\/locations\/[^/]+\/publishers\/([^/]+)\/models\/(.+)$/, + ); + if (fullMatch) { + return `${fullMatch[1]}/${fullMatch[2]}`; + } + + // e.g. "models/gemini-2.5-pro" -> "google/gemini-2.5-pro" + const modelsMatch = trimmed.match(/^models\/(.+)$/); + if (modelsMatch) { + return `google/${modelsMatch[1]}`; + } + + // If it already looks like "/", keep it. + if (trimmed.includes("/")) { + return trimmed; + } + + // Default to Google publisher models for convenience. + return `google/${trimmed}`; +} + +export class ProviderVertex implements IProvider { + async streamResponse({ + llmConversation, + modelConfig, + onChunk, + onComplete, + additionalHeaders, + tools, + enabledToolsets, + onError, + customBaseUrl, + }: StreamResponseParams): Promise { + const rawModelName = modelConfig.modelId + .split("::") + .slice(1) + .join("::"); + if (!rawModelName) { + throw new Error(`Invalid model id: ${modelConfig.modelId}`); + } + const modelName = normalizeVertexPublisherModel(rawModelName); + + const settings = await SettingsManager.getInstance().get(); + const vertex = settings.vertexAI; + + if ( + !vertex || + !vertex.projectId.trim() || + !vertex.location.trim() || + !vertex.serviceAccountClientEmail.trim() || + !vertex.serviceAccountPrivateKey.trim() + ) { + throw new Error( + "Please configure Vertex AI credentials in Settings.", + ); + } + + const accessToken = await getGoogleAccessToken({ + clientEmail: vertex.serviceAccountClientEmail, + privateKey: vertex.serviceAccountPrivateKey, + }); + + const baseURL = + customBaseUrl || + getVertexBaseUrl({ + projectId: vertex.projectId, + location: vertex.location, + }); + + const client = new OpenAI({ + baseURL, + apiKey: accessToken, + fetch: tauriFetch, + defaultHeaders: { + ...(additionalHeaders ?? {}), + }, + dangerouslyAllowBrowser: true, + }); + + let messages: OpenAI.ChatCompletionMessageParam[] = + await OpenAICompletionsAPIUtils.convertConversation( + llmConversation, + { + imageSupport: + modelConfig.supportedAttachmentTypes?.includes( + "image", + ) ?? false, + functionSupport: true, + }, + ); + + if (modelConfig.systemPrompt) { + messages = [ + { + role: "system", + content: modelConfig.systemPrompt, + }, + ...messages, + ]; + } + + const streamParams: OpenAI.ChatCompletionCreateParamsStreaming = { + model: modelName, + messages, + stream: true, + }; + + const nativeWebSearchEnabled = + enabledToolsets?.includes("web") ?? false; + const supportsNativeWebSearch = + modelName.startsWith("google/") && modelName.includes("gemini"); + const shouldUseNativeWebSearch = + nativeWebSearchEnabled && supportsNativeWebSearch; + + if (shouldUseNativeWebSearch) { + // Vertex AI OpenAI-compatible web search grounding. + // The API accepts `web_search_options` but does not support sub-options. + // https://docs.cloud.google.com/vertex-ai/generative-ai/docs/migrate/openai/overview + ( + streamParams as unknown as Record + ).web_search_options = {}; + } + + // Gemini thinking via extra_body.google (Vertex OpenAI-compatible API) + // Ref: https://cloud.google.com/vertex-ai/generative-ai/docs/migrate/openai/overview + const isGemini3 = modelName.includes("gemini-3"); + const isGemini25 = modelName.includes("gemini-2.5"); + + // Debug: Log thinking parameters + console.log(`[ProviderVertex] Model: ${modelName}`); + console.log( + `[ProviderVertex] isGemini3: ${isGemini3}, isGemini25: ${isGemini25}`, + ); + console.log( + `[ProviderVertex] modelConfig.thinkingLevel: ${modelConfig.thinkingLevel}`, + ); + console.log( + `[ProviderVertex] modelConfig.budgetTokens: ${modelConfig.budgetTokens}`, + ); + + const showThoughts = Boolean(modelConfig.showThoughts); + + const thinkingEnabled = + (isGemini3 && (modelConfig.thinkingLevel || showThoughts)) || + (isGemini25 && + (modelConfig.budgetTokens !== undefined || showThoughts)); + + if (thinkingEnabled) { + const thinkingConfig: Record = {}; + + if (isGemini3) { + thinkingConfig.thinking_level = + modelConfig.thinkingLevel || "HIGH"; + } else if (isGemini25) { + thinkingConfig.thinking_budget = modelConfig.budgetTokens ?? -1; + } + + if (showThoughts) { + thinkingConfig.include_thoughts = true; + } + + const googleExtra: Record = { + thinking_config: thinkingConfig, + ...(showThoughts ? { thought_tag_marker: "think" } : {}), + }; + + (streamParams as unknown as Record).extra_body = { + google: googleExtra, + }; + } + + console.log(`[ProviderVertex] thinkingEnabled: ${thinkingEnabled}`); + console.log( + `[ProviderVertex] streamParams:`, + JSON.stringify(streamParams, null, 2), + ); + + if (tools && tools.length > 0) { + streamParams.tools = + OpenAICompletionsAPIUtils.convertToolDefinitions(tools); + streamParams.tool_choice = "auto"; + } + + const chunks: OpenAI.ChatCompletionChunk[] = []; + let inReasoning = false; + let reasoningStartedAtMs: number | undefined; + + const closeReasoning = () => { + if (!inReasoning) return; + inReasoning = false; + onChunk(""); + if (reasoningStartedAtMs !== undefined) { + const seconds = Math.max( + 1, + Math.round((Date.now() - reasoningStartedAtMs) / 1000), + ); + onChunk(``); + } + onChunk("\n\n"); + reasoningStartedAtMs = undefined; + }; + + try { + const stream = await client.chat.completions.create(streamParams); + for await (const chunk of stream) { + chunks.push(chunk); + const delta = chunk.choices[0]?.delta as unknown as { + content?: string; + reasoning?: string; + reasoning_content?: string; + }; + + const reasoningDelta = + typeof delta?.reasoning_content === "string" + ? delta.reasoning_content + : typeof delta?.reasoning === "string" + ? delta.reasoning + : undefined; + + // Vertex may return thoughts in a separate reasoning field OR inline via / tags. + if (showThoughts && reasoningDelta) { + if (!inReasoning) { + inReasoning = true; + reasoningStartedAtMs = Date.now(); + onChunk(""); + } + onChunk(reasoningDelta); + } + + if (typeof delta?.content === "string" && delta.content) { + closeReasoning(); + onChunk(delta.content); + } + } + } catch (error: unknown) { + console.error( + "Raw error from ProviderVertex:", + error, + modelName, + baseURL, + messages, + ); + console.error(JSON.stringify(error, null, 2)); + + if ( + isProviderError(error) && + error.message === "Provider returned error" + ) { + let errorDetails: ProviderError; + try { + errorDetails = JSON5.parse( + error.error?.metadata?.raw || + error.metadata?.raw || + "{}", + ); + } catch { + errorDetails = { + message: "Failed to parse error details", + error: { message: "Failed to parse error details" }, + }; + } + const errorMessage = `Provider returned error: ${errorDetails.error?.message || error.message}`; + if (onError) { + onError(errorMessage); + } else { + throw new Error(errorMessage); + } + } else { + if (onError) { + onError(getErrorMessage(error)); + } else { + throw error; + } + } + return undefined; + } + + const toolCalls = OpenAICompletionsAPIUtils.convertToolCalls( + chunks, + tools ?? [], + ); + + closeReasoning(); + + await onComplete( + undefined, + toolCalls.length > 0 ? toolCalls : undefined, + ); + } +} diff --git a/src/core/chorus/ModelProviders/anthropicModels.ts b/src/core/chorus/ModelProviders/anthropicModels.ts new file mode 100644 index 00000000..85d5d171 --- /dev/null +++ b/src/core/chorus/ModelProviders/anthropicModels.ts @@ -0,0 +1,99 @@ +export type AnthropicModelConfig = { + inputModelName: string; + anthropicModelName: string; + maxTokens: number; +}; + +export const ANTHROPIC_THINKING_MIN_BUDGET_TOKENS = 1024; + +const DEFAULT_ANTHROPIC_MAX_TOKENS = 8192; + +const ANTHROPIC_MODELS: AnthropicModelConfig[] = [ + { + inputModelName: "claude-3-5-sonnet-latest", + anthropicModelName: "claude-3-5-sonnet-latest", + maxTokens: 8192, + }, + { + inputModelName: "claude-3-7-sonnet-latest", + anthropicModelName: "claude-3-7-sonnet-latest", + maxTokens: 20000, + }, + { + inputModelName: "claude-3-7-sonnet-latest-thinking", + anthropicModelName: "claude-3-7-sonnet-latest", + maxTokens: 10000, + }, + { + inputModelName: "claude-sonnet-4-latest", + // https://docs.anthropic.com/en/docs/about-claude/models/overview 0 is the new alias for latest + anthropicModelName: "claude-sonnet-4-0", + maxTokens: 10000, + }, + { + inputModelName: "claude-sonnet-4-5-20250929", + anthropicModelName: "claude-sonnet-4-5-20250929", + maxTokens: 10000, + }, + { + inputModelName: "claude-opus-4-latest", + anthropicModelName: "claude-opus-4-0", + maxTokens: 10000, + }, + { + inputModelName: "claude-opus-4.1-latest", + anthropicModelName: "claude-opus-4-1-20250805", + maxTokens: 10000, + }, + { + inputModelName: "claude-haiku-4-5-20251001", + anthropicModelName: "claude-haiku-4-5-20251001", + maxTokens: 20000, + }, + { + inputModelName: "claude-opus-4-5-20251101", + anthropicModelName: "claude-opus-4-5-20251101", + maxTokens: 20000, + }, +]; + +export function getAnthropicModelName(modelName: string): string { + const modelConfig = ANTHROPIC_MODELS.find( + (m) => m.inputModelName === modelName, + ); + // If not found in hardcoded list, return the model name as-is + // (supports dynamically fetched models from API) + return modelConfig?.anthropicModelName ?? modelName; +} + +export function getAnthropicMaxTokens(modelName: string): number { + const modelConfig = ANTHROPIC_MODELS.find( + (m) => m.inputModelName === modelName, + ); + // Return default max tokens if model not found in hardcoded list + // (supports dynamically fetched models from API) + return modelConfig?.maxTokens ?? DEFAULT_ANTHROPIC_MAX_TOKENS; +} + +export function clampAnthropicThinkingBudgetTokens(params: { + budgetTokens: number; + maxTokens: number; +}): number { + const normalizedBudgetTokens = Math.floor(params.budgetTokens); + if ( + !Number.isFinite(normalizedBudgetTokens) || + Number.isNaN(normalizedBudgetTokens) + ) { + return ANTHROPIC_THINKING_MIN_BUDGET_TOKENS; + } + + const maxBudgetTokens = Math.max( + ANTHROPIC_THINKING_MIN_BUDGET_TOKENS, + params.maxTokens - 1, + ); + + return Math.min( + maxBudgetTokens, + Math.max(ANTHROPIC_THINKING_MIN_BUDGET_TOKENS, normalizedBudgetTokens), + ); +} diff --git a/src/core/chorus/Models.ts b/src/core/chorus/Models.ts index 1563d354..983552ff 100644 --- a/src/core/chorus/Models.ts +++ b/src/core/chorus/Models.ts @@ -12,14 +12,26 @@ import { ProviderPerplexity } from "./ModelProviders/ProviderPerplexity"; import { IProvider } from "./ModelProviders/IProvider"; import Database from "@tauri-apps/plugin-sql"; import { readFile } from "@tauri-apps/plugin-fs"; +import { fetch as tauriFetch } from "@tauri-apps/plugin-http"; import { ProviderGoogle } from "./ModelProviders/ProviderGoogle"; -import { ollamaClient } from "./OllamaClient"; -import { ProviderOllama } from "./ModelProviders/ProviderOllama"; -import { ProviderLMStudio } from "./ModelProviders/ProviderLMStudio"; import { ProviderGrok } from "./ModelProviders/ProviderGrok"; +import { ProviderVertex } from "./ModelProviders/ProviderVertex"; +import { ProviderCustomOpenAI } from "./ModelProviders/ProviderCustomOpenAI"; +import { ProviderCustomAnthropic } from "./ModelProviders/ProviderCustomAnthropic"; +import { ProviderGroq } from "./ModelProviders/ProviderGroq"; +import { ProviderMistral } from "./ModelProviders/ProviderMistral"; +import { ProviderCerebras } from "./ModelProviders/ProviderCerebras"; +import { ProviderFireworks } from "./ModelProviders/ProviderFireworks"; +import { ProviderTogether } from "./ModelProviders/ProviderTogether"; +import { ProviderNvidia } from "./ModelProviders/ProviderNvidia"; import posthog from "posthog-js"; import { UserTool, UserToolCall, UserToolResult } from "./Toolsets"; import { Attachment } from "./api/AttachmentsAPI"; +import { + CustomProviderSettings, + SettingsManager, + VertexAISettings, +} from "@core/utilities/Settings"; /// ------------------------------------------------------------------------------------------------ /// Basic Types @@ -157,6 +169,12 @@ export type ApiKeys = { openrouter?: string; google?: string; grok?: string; + groq?: string; + mistral?: string; + cerebras?: string; + fireworks?: string; + together?: string; + nvidia?: string; }; export type Model = { @@ -198,8 +216,10 @@ export type ModelConfig = { modelId: string; systemPrompt?: string; isDefault: boolean; - budgetTokens?: number; // optional token budget for thinking mode - reasoningEffort?: "low" | "medium" | "high"; + budgetTokens?: number; // optional token budget for thinking mode (Anthropic, Gemini 2.5) + reasoningEffort?: "low" | "medium" | "high" | "xhigh"; // OpenAI o1/o3/GPT-5, xAI Grok + thinkingLevel?: "LOW" | "HIGH"; // Google Gemini 3 thinking level + showThoughts?: boolean; // request a collapsible block in the output (persisted) // pricing (from models table) promptPricePerToken?: number; @@ -217,6 +237,11 @@ export type StreamResponseParams = { modelConfig: ModelConfig; llmConversation: LLMMessage[]; tools?: UserTool[]; + /** + * Names of enabled toolsets (e.g. ["web", "terminal"]). + * Useful when a toolset influences provider behavior without exposing function tools. + */ + enabledToolsets?: string[]; apiKeys: ApiKeys; onChunk: (chunk: string) => void; onComplete: ( @@ -239,10 +264,17 @@ export type ProviderName = | "google" | "perplexity" | "openrouter" - | "ollama" - | "lmstudio" | "grok" - | "meta"; + | "vertex" + | "custom_openai" + | "custom_anthropic" + | "meta" + | "groq" + | "mistral" + | "cerebras" + | "fireworks" + | "together" + | "nvidia"; /** * Returns a human readable label for the provider @@ -278,6 +310,25 @@ export function getProviderName(modelId: string): ProviderName { return providerName as ProviderName; } +/** + * Returns the model name from a model id. + * Ex: "openrouter::meta-llama/llama-4-scout" -> "meta-llama/llama-4-scout" + * Ex: "custom_openai::::gpt-4o-mini" -> "gpt-4o-mini" + */ +export function getModelName(modelId: string): string { + const parts = modelId.split("::"); + if (parts.length <= 1) { + return modelId; + } + + const provider = parts[0]; + if (provider === "custom_openai" || provider === "custom_anthropic") { + return parts.slice(2).join("::"); + } + + return parts.slice(1).join("::"); +} + function getProvider(providerName: string): IProvider { switch (providerName) { case "openai": @@ -290,12 +341,26 @@ function getProvider(providerName: string): IProvider { return new ProviderOpenRouter(); case "perplexity": return new ProviderPerplexity(); - case "ollama": - return new ProviderOllama(); - case "lmstudio": - return new ProviderLMStudio(); case "grok": return new ProviderGrok(); + case "vertex": + return new ProviderVertex(); + case "custom_openai": + return new ProviderCustomOpenAI(); + case "custom_anthropic": + return new ProviderCustomAnthropic(); + case "groq": + return new ProviderGroq(); + case "mistral": + return new ProviderMistral(); + case "cerebras": + return new ProviderCerebras(); + case "fireworks": + return new ProviderFireworks(); + case "together": + return new ProviderTogether(); + case "nvidia": + return new ProviderNvidia(); default: throw new Error(`Unknown provider: ${providerName}`); } @@ -330,9 +395,32 @@ export async function saveModelAndDefaultConfig( completionPricePerToken?: number; }, ): Promise { - // insert or replace is important. this way I can have a refresh where Ollama / LM studio models are set to disabled if they're not running, and enabled if they are + // For remote providers, preserve user-controlled enable/disable state across refreshes. + // For local/curated providers (Vertex/Custom Providers), treat refresh as + // authoritative and overwrite is_enabled. + const providerId = model.id.split("::")[0] ?? ""; + const shouldOverwriteIsEnabled = + providerId === "vertex" || + providerId === "custom_openai" || + providerId === "custom_anthropic"; + await db.execute( - "INSERT OR REPLACE INTO models (id, display_name, is_enabled, supported_attachment_types, is_internal, prompt_price_per_token, completion_price_per_token) VALUES (?, ?, ?, ?, ?, ?, ?)", + `INSERT INTO models ( + id, + display_name, + is_enabled, + supported_attachment_types, + is_internal, + prompt_price_per_token, + completion_price_per_token + ) VALUES (?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(id) DO UPDATE SET + display_name = excluded.display_name, + supported_attachment_types = excluded.supported_attachment_types, + is_internal = excluded.is_internal, + prompt_price_per_token = excluded.prompt_price_per_token, + completion_price_per_token = excluded.completion_price_per_token + ${shouldOverwriteIsEnabled ? ", is_enabled = excluded.is_enabled" : ""}`, [ model.id, model.displayName, @@ -343,9 +431,146 @@ export async function saveModelAndDefaultConfig( pricing?.completionPricePerToken ?? null, ], ); + + // Create the default model_config row if missing; on refresh, only update the display name + // and linkage, preserving user-controlled fields (system_prompt, thinking params, etc). + // For Gemini 3 models, set default thinking_level = 'HIGH' and show_thoughts = 1 + const modelName = model.id.split("::").slice(1).join("::"); + const isGemini3 = modelName.includes("gemini-3"); + + if (isGemini3) { + await db.execute( + `INSERT INTO model_configs (id, display_name, author, model_id, system_prompt, thinking_level, show_thoughts) + VALUES (?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(id) DO UPDATE SET + display_name = excluded.display_name, + author = excluded.author, + model_id = excluded.model_id, + thinking_level = COALESCE(model_configs.thinking_level, excluded.thinking_level), + show_thoughts = COALESCE(model_configs.show_thoughts, excluded.show_thoughts)`, + [ + model.id, + modelConfigDisplayName, + "system", + model.id, + "", + "HIGH", + 1, + ], + ); + } else { + await db.execute( + `INSERT INTO model_configs (id, display_name, author, model_id, system_prompt) + VALUES (?, ?, ?, ?, ?) + ON CONFLICT(id) DO UPDATE SET + display_name = excluded.display_name, + author = excluded.author, + model_id = excluded.model_id`, + [model.id, modelConfigDisplayName, "system", model.id, ""], + ); + } +} + +/** + * Deletes all models and model configs for a specific provider. + * This should be called when an API key changes to clear old models. + */ +export async function deleteProviderModels( + db: Database, + provider: string, +): Promise { + // Delete from model_configs first (child table) + await db.execute(`DELETE FROM model_configs WHERE model_id LIKE ?`, [ + `${provider}::%`, + ]); + // Then delete from models (parent table) + await db.execute(`DELETE FROM models WHERE id LIKE ?`, [`${provider}::%`]); +} + +function hasVertexCredentials(vertex: VertexAISettings): boolean { + return Boolean( + vertex.projectId.trim() && + vertex.location.trim() && + vertex.serviceAccountClientEmail.trim() && + vertex.serviceAccountPrivateKey.trim(), + ); +} + +export async function syncVertexModels(db: Database): Promise { await db.execute( - "INSERT OR REPLACE INTO model_configs (id, display_name, author, model_id, system_prompt) VALUES (?, ?, ?, ?, ?)", - [model.id, modelConfigDisplayName, "system", model.id, ""], + "UPDATE models SET is_enabled = 0 WHERE id LIKE 'vertex::%'", + ); + + const settings = await SettingsManager.getInstance().get(); + const vertex = settings.vertexAI; + + if ( + !vertex || + !hasVertexCredentials(vertex) || + vertex.models.length === 0 + ) { + return; + } + + await Promise.all( + vertex.models + .filter((model) => model.modelId.trim() !== "") + .map((model) => { + const modelId = model.modelId.trim(); + const displayName = model.nickname?.trim() || modelId; + return saveModelAndDefaultConfig( + db, + { + id: `vertex::${modelId}`, + displayName, + supportedAttachmentTypes: ["text", "image", "webpage"], + isEnabled: true, + isInternal: false, + }, + displayName, + ); + }), + ); +} + +export async function syncCustomProviderModels(db: Database): Promise { + await db.execute( + "UPDATE models SET is_enabled = 0 WHERE id LIKE 'custom_openai::%' OR id LIKE 'custom_anthropic::%'", + ); + + const settings = await SettingsManager.getInstance().get(); + const customProviders = settings.customProviders ?? []; + + await Promise.all( + customProviders.flatMap((provider: CustomProviderSettings) => { + const prefix = + provider.kind === "anthropic" + ? "custom_anthropic" + : "custom_openai"; + + const supportedAttachmentTypes: AttachmentType[] = + provider.kind === "anthropic" + ? ["text", "image", "pdf", "webpage"] + : ["text", "image", "webpage"]; + + return provider.models + .filter((model) => model.modelId.trim() !== "") + .map((model) => { + const modelId = model.modelId.trim(); + const displayName = model.nickname?.trim() || modelId; + return saveModelAndDefaultConfig( + db, + { + id: `${prefix}::${provider.id}::${modelId}`, + displayName, + supportedAttachmentTypes, + isEnabled: true, + isInternal: false, + }, + displayName, + ); + }); + }), ); } @@ -354,10 +579,19 @@ export async function saveModelAndDefaultConfig( */ export async function DEPRECATED_USE_HOOK_INSTEAD_downloadModels( db: Database, + apiKeys: ApiKeys, ): Promise { await downloadOpenRouterModels(db); - await downloadOllamaModels(db); - await downloadLMStudioModels(db); + await downloadOpenAIModels(db, apiKeys); + await downloadAnthropicModels(db, apiKeys); + await downloadGoogleModels(db, apiKeys); + await downloadGrokModels(db, apiKeys); + await downloadGroqModels(db, apiKeys); + await downloadMistralModels(db, apiKeys); + await downloadCerebrasModels(db, apiKeys); + await downloadFireworksModels(db, apiKeys); + await downloadTogetherModels(db, apiKeys); + await downloadNvidiaModels(db, apiKeys); return 0; } @@ -386,10 +620,6 @@ export async function downloadOpenRouterModels(db: Database): Promise { }[]; }; - await db.execute( - "UPDATE models SET is_enabled = 0 WHERE id LIKE 'openrouter::%'", - ); - await Promise.all( openRouterModels.map((model) => { // Check if the model supports images based on API metadata @@ -433,78 +663,277 @@ export async function downloadOpenRouterModels(db: Database): Promise { } /** - * Downloads models from Ollama to refresh the database. + * Downloads models from OpenAI to refresh the database. + * Uses OpenAI's native /v1/models API */ -export async function downloadOllamaModels(db: Database): Promise { - // first, disable all ollama models - await db.execute( - "UPDATE models SET is_enabled = 0 WHERE id LIKE 'ollama::%'", - ); +export async function downloadOpenAIModels( + db: Database, + apiKeys: ApiKeys, +): Promise { + try { + if (!apiKeys.openai) { + console.log( + "No OpenAI API key configured, skipping model download", + ); + return; + } - // health check - const health = await ollamaClient.isHealthy(); - if (!health) { - return; + const response = await fetch("https://api.openai.com/v1/models", { + headers: { + Authorization: `Bearer ${apiKeys.openai}`, + }, + }); + + if (!response.ok) { + console.error("Failed to fetch OpenAI models"); + return; + } + + const { data: models } = (await response.json()) as { + data: { + id: string; + object: string; + created: number; + owned_by: string; + }[]; + }; + + // Filter for chat completion models only + const chatModels = models.filter( + (model) => + model.id.startsWith("gpt-") || + model.id.startsWith("o1") || + model.id.startsWith("o3") || + model.id.startsWith("o4"), + ); + + for (const model of chatModels) { + // Determine image support based on model name + const supportsImages = + model.id.includes("gpt-4") || + model.id.includes("gpt-5") || + (model.id.includes("o") && !model.id.includes("mini")); + + await saveModelAndDefaultConfig( + db, + { + id: `openai::${model.id}`, + displayName: model.id, + supportedAttachmentTypes: supportsImages + ? ["text", "image", "webpage"] + : ["text", "webpage"], + isEnabled: true, + isInternal: false, + }, + model.id, + ); + } + } catch (error) { + console.error("Error downloading OpenAI models:", error); } +} - const { models } = await ollamaClient.listModels(); +/** + * Downloads models from Anthropic to refresh the database. + * Uses Anthropic's native /v1/models API + */ +export async function downloadAnthropicModels( + db: Database, + apiKeys: ApiKeys, +): Promise { + try { + if (!apiKeys.anthropic) { + console.log( + "No Anthropic API key configured, skipping model download", + ); + return; + } - // Then add/update models from Ollama - for (const model of models) { - await saveModelAndDefaultConfig( - db, - { - id: `ollama::${model.name}`, - displayName: `${model.name} (Ollama)`, - supportedAttachmentTypes: ["text", "webpage"], // Ollama models currently only support text and webpage - isEnabled: true, - isInternal: false, + const response = await fetch("https://api.anthropic.com/v1/models", { + headers: { + "anthropic-version": "2023-06-01", + "x-api-key": apiKeys.anthropic, }, - `${model.name} (Ollama)`, - ); + }); + + if (!response.ok) { + const errorText = await response.text().catch(() => ""); + console.error( + "Failed to fetch Anthropic models", + response.status, + response.statusText, + errorText, + ); + + // If the key is invalid, disable Anthropic models so they can't be selected. + // For transient failures, preserve existing models' is_enabled state (user preference). + if (response.status === 401 || response.status === 403) { + await db.execute( + "UPDATE models SET is_enabled = 0 WHERE id LIKE 'anthropic::%'", + ); + } + // For other failures, do nothing - preserve user's enable/disable preferences + return; + } + + // Don't overwrite user preferences for enabled/disabled models + // New models will be enabled via saveModelAndDefaultConfig + + const { data: models } = (await response.json()) as { + data: { + id: string; + created_at: string; + display_name: string; + type: string; + }[]; + has_more: boolean; + first_id: string; + last_id: string; + }; + + for (const model of models) { + // All Claude models support images and PDFs + await saveModelAndDefaultConfig( + db, + { + id: `anthropic::${model.id}`, + displayName: model.display_name, + supportedAttachmentTypes: [ + "text", + "image", + "pdf", + "webpage", + ], + isEnabled: true, + isInternal: false, + }, + model.display_name, + ); + } + } catch (error) { + console.error("Error downloading Anthropic models:", error); + // Preserve existing models' is_enabled state (user preference) + // New models will be enabled when they're successfully fetched } } /** - * Downloads models from LM Studio to refresh the database. + * Downloads models from Google Gemini to refresh the database. + * Uses Google's native /v1beta/models API */ -export async function downloadLMStudioModels(db: Database): Promise { +export async function downloadGoogleModels( + db: Database, + apiKeys: ApiKeys, +): Promise { try { - // Check if LM Studio is accessible - // First, disable all existing LM Studio models - await db.execute( - "UPDATE models SET is_enabled = 0 WHERE id LIKE 'lmstudio::%'", + if (!apiKeys.google) { + console.log( + "No Google API key configured, skipping model download", + ); + return; + } + + const response = await fetch( + `https://generativelanguage.googleapis.com/v1beta/models?key=${apiKeys.google}`, ); - const response = await fetch("http://localhost:1234/v1/models"); if (!response.ok) { + console.error("Failed to fetch Google models"); + return; + } + + const { models } = (await response.json()) as { + models: { + name: string; + baseModelId: string; + version: string; + displayName: string; + description: string; + inputTokenLimit: number; + outputTokenLimit: number; + supportedGenerationMethods: string[]; + thinking?: boolean; + }[]; + nextPageToken?: string; + }; + + // Filter for models that support generateContent + const chatModels = models.filter((model) => + model.supportedGenerationMethods?.includes("generateContent"), + ); + + for (const model of chatModels) { + // Extract model ID from name (format: "models/gemini-...") + const modelId = model.name.replace("models/", ""); + + // Gemini models support text, image, and webpage + await saveModelAndDefaultConfig( + db, + { + id: `google::${modelId}`, + displayName: model.displayName || modelId, + supportedAttachmentTypes: ["text", "image", "webpage"], + isEnabled: true, + isInternal: false, + }, + model.displayName || modelId, + ); + } + } catch (error) { + console.error("Error downloading Google models:", error); + } +} + +/** + * Downloads models from xAI Grok to refresh the database. + * Uses xAI's native /v1/models API + */ +export async function downloadGrokModels( + db: Database, + apiKeys: ApiKeys, +): Promise { + try { + if (!apiKeys.grok) { + console.log("No Grok API key configured, skipping model download"); + return; + } + + const response = await fetch("https://api.x.ai/v1/models", { + headers: { + Authorization: `Bearer ${apiKeys.grok}`, + }, + }); + + if (!response.ok) { + console.error("Failed to fetch xAI Grok models"); return; } const { data: models } = (await response.json()) as { - data: { id: string }[]; + data: { + id: string; + object: string; + created: number; + owned_by: string; + }[]; }; - // Then add/update models from LM Studio for (const model of models) { + // Grok models support text and image await saveModelAndDefaultConfig( db, { - id: `lmstudio::${model.id}`, - displayName: `${model.id} (LM Studio)`, - supportedAttachmentTypes: ["text", "webpage"], // LM Studio models currently support text and webpage + id: `grok::${model.id}`, + displayName: model.id, + supportedAttachmentTypes: ["text", "image", "webpage"], isEnabled: true, isInternal: false, }, - `${model.id} (LM Studio)`, + model.id, ); } } catch (error) { - // If there's an error (e.g., LM Studio is not running), disable all LM Studio models - await db.execute( - "UPDATE models SET is_enabled = 0 WHERE id LIKE 'lmstudio::%'", - ); - throw error; + console.error("Error downloading xAI Grok models:", error); } } @@ -610,9 +1039,16 @@ const CONTEXT_LIMIT_PATTERNS: Record = { grok: "maximum prompt length", openrouter: "context length", meta: "context window", // best guess - lmstudio: "context window", // best guess perplexity: "context window", // best guess - ollama: "context window", // best guess + vertex: "context window", // best guess + custom_openai: "context window", // best guess + custom_anthropic: "prompt is too long", // best guess + groq: "context window", + mistral: "context window", + cerebras: "context window", + fireworks: "context window", + together: "context window", + nvidia: "context window", }; /** @@ -640,3 +1076,284 @@ export function detectContextLimitError( return false; } + +/** + * Downloads models from Groq to refresh the database. + */ +export async function downloadGroqModels( + db: Database, + apiKeys: ApiKeys, +): Promise { + try { + if (!apiKeys.groq) { + return; + } + + const response = await fetch("https://api.groq.com/openai/v1/models", { + headers: { Authorization: `Bearer ${apiKeys.groq}` }, + }); + + if (!response.ok) return; + + const { data: models } = (await response.json()) as { + data: { id: string; owned_by: string }[]; + }; + + for (const model of models) { + await saveModelAndDefaultConfig( + db, + { + id: `groq::${model.id}`, + displayName: model.id, + supportedAttachmentTypes: ["text", "image", "webpage"], + isEnabled: true, + isInternal: false, + }, + model.id, + ); + } + } catch (error) { + console.error("Error downloading Groq models:", error); + } +} + +/** + * Downloads models from Mistral to refresh the database. + */ +export async function downloadMistralModels( + db: Database, + apiKeys: ApiKeys, +): Promise { + try { + if (!apiKeys.mistral) { + return; + } + + const response = await fetch("https://api.mistral.ai/v1/models", { + headers: { Authorization: `Bearer ${apiKeys.mistral}` }, + }); + + if (!response.ok) return; + + const { data: models } = (await response.json()) as { + data: { id: string; name?: string }[]; + }; + + for (const model of models) { + await saveModelAndDefaultConfig( + db, + { + id: `mistral::${model.id}`, + displayName: model.name || model.id, + supportedAttachmentTypes: ["text", "image", "webpage"], + isEnabled: true, + isInternal: false, + }, + model.name || model.id, + ); + } + } catch (error) { + console.error("Error downloading Mistral models:", error); + } +} + +/** + * Downloads models from Cerebras to refresh the database. + */ +export async function downloadCerebrasModels( + db: Database, + apiKeys: ApiKeys, +): Promise { + try { + if (!apiKeys.cerebras) { + return; + } + + const response = await fetch("https://api.cerebras.ai/v1/models", { + headers: { Authorization: `Bearer ${apiKeys.cerebras}` }, + }); + + if (!response.ok) return; + + const { data: models } = (await response.json()) as { + data: { id: string }[]; + }; + + for (const model of models) { + await saveModelAndDefaultConfig( + db, + { + id: `cerebras::${model.id}`, + displayName: model.id, + supportedAttachmentTypes: ["text", "webpage"], + isEnabled: true, + isInternal: false, + }, + model.id, + ); + } + } catch (error) { + console.error("Error downloading Cerebras models:", error); + } +} + +/** + * Downloads models from Fireworks to refresh the database. + */ +export async function downloadFireworksModels( + db: Database, + apiKeys: ApiKeys, +): Promise { + try { + if (!apiKeys.fireworks) { + return; + } + + const response = await fetch( + "https://api.fireworks.ai/inference/v1/models", + { headers: { Authorization: `Bearer ${apiKeys.fireworks}` } }, + ); + + if (!response.ok) return; + + const { data: models } = (await response.json()) as { + data: { id: string }[]; + }; + + for (const model of models) { + await saveModelAndDefaultConfig( + db, + { + id: `fireworks::${model.id}`, + displayName: model.id, + supportedAttachmentTypes: ["text", "image", "webpage"], + isEnabled: true, + isInternal: false, + }, + model.id, + ); + } + } catch (error) { + console.error("Error downloading Fireworks models:", error); + } +} + +/** + * Downloads models from Together.ai to refresh the database. + */ +export async function downloadTogetherModels( + db: Database, + apiKeys: ApiKeys, +): Promise { + try { + if (!apiKeys.together) { + return; + } + + const response = await tauriFetch( + "https://api.together.xyz/v1/models", + { + headers: { Authorization: `Bearer ${apiKeys.together}` }, + }, + ); + + if (!response.ok) return; + + interface TogetherModel { + id: string; + display_name?: string; + type?: string; + } + + const models = (await response.json()) as TogetherModel[]; + + // Filter for chat models only + const chatModels = models.filter( + (model) => + model.type === "chat" || + model.id.includes("chat") || + model.id.includes("instruct"), + ); + + for (const model of chatModels) { + // Determine if model supports images based on model name + const supportsImages = + model.id.includes("vision") || + model.id.includes("llama-3.2-90b") || + model.id.includes("llama-3.2-11b"); + + await saveModelAndDefaultConfig( + db, + { + id: `together::${model.id}`, + displayName: model.display_name || model.id, + supportedAttachmentTypes: supportsImages + ? ["text", "image", "webpage"] + : ["text", "webpage"], + isEnabled: true, + isInternal: false, + }, + model.display_name || model.id, + ); + } + } catch (error) { + console.error("Error downloading Together.ai models:", error); + } +} + +/** + * Downloads models from Nvidia NIM to refresh the database. + */ +export async function downloadNvidiaModels( + db: Database, + apiKeys: ApiKeys, +): Promise { + try { + if (!apiKeys.nvidia) { + return; + } + + const response = await tauriFetch( + "https://integrate.api.nvidia.com/v1/models", + { + headers: { Authorization: `Bearer ${apiKeys.nvidia}` }, + }, + ); + + if (!response.ok) return; + + interface NvidiaModelResponse { + data: { + id: string; + owned_by?: string; + }[]; + } + + const responseData = (await response.json()) as NvidiaModelResponse; + const models = responseData.data; + + for (const model of models) { + // Determine if model supports images based on model name + const supportsImages = + model.id.includes("vision") || + model.id.includes("llava") || + model.id.includes("llama-3.2-neva"); + + await saveModelAndDefaultConfig( + db, + { + id: `nvidia::${model.id}`, + displayName: model.id, + supportedAttachmentTypes: supportsImages + ? ["text", "image", "webpage"] + : ["text", "webpage"], + isEnabled: true, + isInternal: false, + }, + model.id, + ); + } + } catch (error) { + console.error("Error downloading Nvidia models:", error); + } +} diff --git a/src/core/chorus/OllamaClient.ts b/src/core/chorus/OllamaClient.ts deleted file mode 100644 index 6e23355d..00000000 --- a/src/core/chorus/OllamaClient.ts +++ /dev/null @@ -1,106 +0,0 @@ -interface OllamaResponse { - model: string; - created_at: string; - message: { - role: "assistant"; - content: string; - images?: string[] | null; - }; - done: boolean; - total_duration?: number; - load_duration?: number; - prompt_eval_count?: number; - eval_count?: number; - eval_duration?: number; -} - -interface OllamaMessage { - role: "user" | "assistant"; - content: string; - images?: string[]; -} - -export class OllamaClient { - private baseUrl: string; - - constructor(baseUrl: string = "http://localhost:11434") { - this.baseUrl = baseUrl; - } - - async *streamChat( - model: string, - messages: OllamaMessage[], - options: { - format?: string; - } = {}, - ): AsyncGenerator { - const response = await fetch(`${this.baseUrl}/api/chat`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - model, - messages, - format: options.format, - }), - }); - - if (!response.ok) { - throw new Error(`Ollama API error: ${response.statusText}`); - } - - const reader = response.body?.getReader(); - if (!reader) { - throw new Error("No reader available"); - } - - const decoder = new TextDecoder(); - let buffer = ""; - - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split("\n"); - buffer = lines.pop() || ""; // Keep the last incomplete line in the buffer - - for (const line of lines) { - if (!line.trim()) continue; - try { - const data = JSON.parse(line) as OllamaResponse; - if (data.message?.content) { - yield data.message.content; - } - } catch (e) { - console.error("Error parsing Ollama response:", e); - } - } - } - } finally { - reader.releaseLock(); - } - } - - async listModels() { - const response = await fetch(`${this.baseUrl}/api/tags`); - if (!response.ok) { - throw new Error(`Ollama API error: ${response.statusText}`); - } - return response.json() as Promise<{ models: { name: string }[] }>; - } - - async isHealthy() { - try { - const response = await fetch(`${this.baseUrl}`); - return response.ok; - } catch { - return false; - } - } -} - -// Create a singleton instance -export const ollamaClient = new OllamaClient(); diff --git a/src/core/chorus/OpenAICompletionsAPIUtils.ts b/src/core/chorus/OpenAICompletionsAPIUtils.ts index ec057da1..987d21be 100644 --- a/src/core/chorus/OpenAICompletionsAPIUtils.ts +++ b/src/core/chorus/OpenAICompletionsAPIUtils.ts @@ -11,6 +11,7 @@ import { UserTool, UserToolCall, } from "@core/chorus/Toolsets"; +import { parseToolCallArguments } from "@core/chorus/ToolCallArgs"; import _ from "lodash"; import { convertPdfToPng } from "@core/chorus/AttachmentsHelpers"; import { v4 as uuidv4 } from "uuid"; @@ -235,12 +236,15 @@ function convertToolCalls( ); return undefined; } - let args: unknown; - try { - args = JSON.parse(toolCall.function?.arguments ?? "{}"); - } catch (e) { - console.error("Error parsing tool call arguments", e, toolCall); - return undefined; + const { args, parseError } = parseToolCallArguments( + toolCall.function?.arguments, + ); + if (parseError) { + console.warn( + "Failed to parse tool call arguments", + parseError, + toolCall.function?.name, + ); } const toolDefinition = toolDefinitions.find( @@ -257,6 +261,7 @@ function convertToolCalls( toolMetadata: { description: toolDefinition?.description, inputSchema: toolDefinition?.inputSchema, + ...(parseError ? { parseError } : {}), }, }; }) diff --git a/src/core/chorus/ToolCallArgs.ts b/src/core/chorus/ToolCallArgs.ts new file mode 100644 index 00000000..a2db5b41 --- /dev/null +++ b/src/core/chorus/ToolCallArgs.ts @@ -0,0 +1,70 @@ +import JSON5 from "json5"; + +function stripMarkdownCodeFences(text: string): string { + const trimmed = text.trim(); + if (!trimmed.startsWith("```")) return trimmed; + + return trimmed + .replace(/^\s*```(?:json)?\s*/i, "") + .replace(/\s*```\s*$/i, "") + .trim(); +} + +function isPlainObject(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +export function parseToolCallArguments(raw: string | null | undefined): { + args: Record; + parseError?: string; +} { + const rawText = (raw ?? "").trim(); + if (!rawText) { + return { args: {} }; + } + + const normalized = stripMarkdownCodeFences(rawText); + + const candidates: string[] = [normalized]; + const objectStart = normalized.indexOf("{"); + const objectEnd = normalized.lastIndexOf("}"); + if (objectStart >= 0 && objectEnd > objectStart) { + const sliced = normalized.slice(objectStart, objectEnd + 1).trim(); + if (sliced && sliced !== normalized) { + candidates.push(sliced); + } + } + + let lastError: string | undefined; + for (const candidate of candidates) { + const parsers: Array<(text: string) => unknown> = [ + (text) => { + const parsed: unknown = JSON.parse(text); + return parsed; + }, + (text) => { + const parsed: unknown = JSON5.parse(text); + return parsed; + }, + ]; + for (const parser of parsers) { + try { + const parsed = parser(candidate); + if (!isPlainObject(parsed)) { + lastError = + "Expected a JSON object for tool arguments (got a non-object value)."; + continue; + } + return { args: parsed }; + } catch (error) { + lastError = + error instanceof Error ? error.message : String(error); + } + } + } + + return { + args: {}, + parseError: `Invalid JSON for tool arguments: ${lastError ?? "unknown error"}`, + }; +} diff --git a/src/core/chorus/Toolsets.ts b/src/core/chorus/Toolsets.ts index f04db223..c91ee512 100644 --- a/src/core/chorus/Toolsets.ts +++ b/src/core/chorus/Toolsets.ts @@ -38,6 +38,8 @@ import { SiElevenlabs, SiStripe, SiSupabase } from "react-icons/si"; export const TOOL_CALL_INTERRUPTED_MESSAGE = "Tool call interrupted"; +export const TOOLSET_ENABLED_TOOLS_CONFIG_KEY = "enabled_tools"; + // THIS DATA STRUCTURE IS SERIALIZED INTO THE DATABASE. DO NOT EDIT EXISTING FIELDS AND DO NOT ADD NEW REQUIRED FIELDS. export type UserToolCall = { /** @@ -63,6 +65,11 @@ export type UserToolCall = { toolMetadata?: { description?: string; inputSchema?: Record; + /** + * If present, tool arguments could not be parsed from the model output. + * This is used to provide a clear error back to the model without executing anything. + */ + parseError?: string; }; }; @@ -104,6 +111,12 @@ export interface ToolImplementation { execute(args: Record): Promise; } +export type AvailableTool = { + id: string; + description?: string; + inputSchema: Record; +}; + export function getUserToolNamespacedName(tool: UserTool): string { return `${tool.toolsetName}_${tool.displayNameSuffix}`; } @@ -326,6 +339,9 @@ export abstract class MCPServer { }; const transport = new StdioClientTransportChorus(serverParams); + transport.onstderr = (data) => { + this._logs += data.endsWith("\n") ? data : data + "\n"; + }; this.transport = transport; await this.mcp.connect(this.transport); @@ -468,6 +484,7 @@ export class Toolset { >(); private servers: MCPServer[] = []; private _status: ToolsetStatus = { status: "stopped" }; + private _availableTools: AvailableTool[] = []; constructor( public readonly name: string, // used to namespace tool names. alphanumeric only, must not contain special characters. @@ -615,23 +632,66 @@ export class Toolset { return this.servers.map((server) => server.logs).join("\n"); } + get availableTools(): AvailableTool[] { + return this._availableTools; + } + + private clearRegisteredServerTools(): void { + for (const [key, entry] of this.toolRegistry.entries()) { + if (entry.implementation instanceof ServerToolImplementation) { + this.toolRegistry.delete(key); + } + } + } + + private getServerStartConfig(config: Record) { + const serverConfig = { ...config }; + delete serverConfig[TOOLSET_ENABLED_TOOLS_CONFIG_KEY]; + return serverConfig; + } + + private parseEnabledToolIds( + config: Record, + ): Set | undefined { + const raw = config[TOOLSET_ENABLED_TOOLS_CONFIG_KEY]; + if (raw === undefined || raw.trim() === "") { + return undefined; // default: all enabled + } + + try { + const parsed: unknown = JSON.parse(raw); + if (!Array.isArray(parsed)) { + return undefined; + } + + const ids: string[] = []; + for (const item of parsed) { + if (typeof item === "string") { + ids.push(item); + } + } + + return new Set(ids); + } catch { + return undefined; + } + } + /** * Start all servers with the given configuration and auto-register tools * based on registration options */ async ensureStart(config: Record): Promise { - if (this._status.status === "running") { - return true; - } - this._status = { status: "starting", }; + const serverConfig = this.getServerStartConfig(config); + // Start all servers in parallel const allStarted = _.every( await Promise.all( - this.servers.map((server) => server.ensureStart(config)), + this.servers.map((server) => server.ensureStart(serverConfig)), ), Boolean, ); @@ -640,9 +700,19 @@ export class Toolset { console.error( `Failed to start all servers for toolset ${this.name}`, ); + this._status = { + status: "stopped", + }; return false; } + // Re-register server tools every time so config changes (e.g. enabled tools) + // take effect without requiring a full stop/start. + this.clearRegisteredServerTools(); + + const enabledToolIds = this.parseEnabledToolIds(config); + const availableTools: AvailableTool[] = []; + // Auto-register tools based on registration options for (const server of this.servers) { const options = this._serverRegistrationOptions.get(server); @@ -655,6 +725,18 @@ export class Toolset { // Get all tools from the server const serverTools = await server.listTools(); + for (const tool of serverTools) { + const id = + options.renameMap?.[tool.nameOnServer] ?? tool.nameOnServer; + availableTools.push({ + id, + description: + options.descriptionMap?.[tool.nameOnServer] ?? + tool.description, + inputSchema: tool.inputSchema, + }); + } + // Apply registration options let filteredTools: ServerTool[] = serverTools; @@ -669,6 +751,15 @@ export class Toolset { ); } + if (enabledToolIds !== undefined) { + filteredTools = filteredTools.filter((serverTool) => { + const id = + options.renameMap?.[serverTool.nameOnServer] ?? + serverTool.nameOnServer; + return enabledToolIds.has(id); + }); + } + // Import the filtered tools with any rename mappings and description overrides this.importServerTools(server, filteredTools, { renameMap: options.renameMap, @@ -676,6 +767,10 @@ export class Toolset { }); } + this._availableTools = _.uniqBy(availableTools, (t) => t.id).sort( + (a, b) => a.id.localeCompare(b.id), + ); + this._status = { status: "running", }; diff --git a/src/core/chorus/ToolsetsManager.ts b/src/core/chorus/ToolsetsManager.ts index 0e987992..746a8514 100644 --- a/src/core/chorus/ToolsetsManager.ts +++ b/src/core/chorus/ToolsetsManager.ts @@ -73,6 +73,13 @@ export class ToolsetsManager { } try { + if (toolCall.toolMetadata?.parseError) { + return { + id: toolCall.id, + content: `Invalid tool arguments for "${toolCall.namespacedToolName}": ${toolCall.toolMetadata.parseError}. Please retry the tool call with a valid JSON object for arguments.`, + }; + } + // Check if YOLO mode is enabled const appMetadata = await fetchAppMetadata(); const yoloMode = appMetadata?.["yolo_mode"] === "true"; @@ -244,7 +251,9 @@ export class ToolsetsManager { toolset = new CustomToolset(customConfig.name); this._customToolsets.push(toolset); } + const persistedConfig = toolsetsConfig[customConfig.name] ?? {}; const config = { + ...persistedConfig, command: customConfig.command, args: customConfig.args, env: customConfig.env, diff --git a/src/core/chorus/WebTools.ts b/src/core/chorus/WebTools.ts deleted file mode 100644 index 5f216d8c..00000000 --- a/src/core/chorus/WebTools.ts +++ /dev/null @@ -1,258 +0,0 @@ -import { fetch } from "@tauri-apps/plugin-http"; -import OpenAI from "openai"; -import { ApiKeys } from "./Models"; - -type FetchOptions = { - maxLength?: number; - startIndex?: number; - raw?: boolean; - headers?: Record; -}; - -type FetchResult = { - content: string; - truncated: boolean; - nextStartIndex?: number; - error?: string; -}; - -type SearchResult = { - content: string; - error?: string; -}; - -type SearchProviderConfig = { - name: "perplexity" | "openrouter"; - baseURL: string; - apiKey: string; - model: string; - defaultHeaders?: Record; -}; - -const OPENROUTER_PERPLEXITY_MODEL = "perplexity/sonar"; - -function normalizeUrl(url: string): string { - if (url.startsWith("http://") && !url.startsWith("https://")) { - return "https://" + url; - } else { - return url; - } -} - -function getErrorMessage(error: unknown): string { - if (error instanceof Error) { - return error.message; - } else { - return "Unknown error"; - } -} - -function getSearchProviderConfig( - apiKeys: ApiKeys, -): SearchProviderConfig | null { - if (apiKeys.perplexity) { - return { - name: "perplexity", - baseURL: "https://api.perplexity.ai", - apiKey: apiKeys.perplexity, - model: "sonar", - defaultHeaders: { - "Content-Type": "application/json", - // unset headers that are not supported by the Perplexity API - // Perplexity's API does not allow x-stainless-* headers added by the OpenAI JS SDK - "x-stainless-arch": null, - "x-stainless-lang": null, - "x-stainless-os": null, - "x-stainless-package-version": null, - "x-stainless-retry-count": null, - "x-stainless-runtime": null, - "x-stainless-runtime-version": null, - "x-stainless-timeout": null, - }, - }; - } - - if (apiKeys.openrouter) { - return { - name: "openrouter", - baseURL: "https://openrouter.ai/api/v1", - apiKey: apiKeys.openrouter, - model: OPENROUTER_PERPLEXITY_MODEL, - defaultHeaders: { - "HTTP-Referer": "https://chorus.sh", - "X-Title": "Chorus", - }, - }; - } - - return null; -} - -export class WebTools { - private static async _fetch( - url: string, - headers: Record, - ): Promise { - console.log("fetching url", url); - const response = await fetch(url, { - headers: { - "User-Agent": - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", - ...headers, - }, - }); - console.log("response", response); - - if (!response.ok) { - throw new Error( - `HTTP error: ${response.status} ${response.statusText}`, - ); - } - return response; - } - - static async search( - query: string, - apiKeys: ApiKeys, - ): Promise { - try { - const providerConfig = getSearchProviderConfig(apiKeys); - - if (!providerConfig) { - return { - content: - "Please add your Perplexity or OpenRouter API key in Settings to use web search.", - error: "No web search API key configured", - }; - } - - const client = new OpenAI({ - baseURL: providerConfig.baseURL, - apiKey: providerConfig.apiKey, - defaultHeaders: providerConfig.defaultHeaders - ? Object.fromEntries( - Object.entries(providerConfig.defaultHeaders).filter( - ([, value]) => value !== null, - ), - ) - : undefined, - dangerouslyAllowBrowser: true, - }); - - const completion = await client.chat.completions.create({ - model: providerConfig.model, - messages: [ - { - role: "system", - content: - "Search the web for information about the user's query. Provide relevant search results with links to sources.", - }, - { - role: "user", - content: query, - }, - ], - stream: false, - }); - - const content = completion.choices[0]?.message?.content || ""; - - // Extract citations if they exist - // Perplexity returns citations in the completion object - const completionWithCitations = - completion as OpenAI.ChatCompletion & { - citations?: string[]; - }; - const citations = completionWithCitations.citations; - let finalContent = content; - - if (citations && citations.length > 0) { - const sources = citations - .map((url, i) => `${i + 1}. [${url}](${url})`) - .join("\n"); - finalContent += "\n\nSources:\n" + sources; - } - - return { - content: finalContent, - }; - } catch (error) { - return { - content: `Error searching the web: ${getErrorMessage(error)}`, - error: getErrorMessage(error), - }; - } - } - - static async fetchWebpage( - url: string, - options: FetchOptions = {}, - ): Promise { - const { - maxLength = 50000, - startIndex = 0, - raw = false, - headers = {}, - } = options; - - try { - const response = await this._fetch( - raw ? normalizeUrl(url) : `https://r.jina.ai/${url}`, - headers, - ); - let content = await response.text(); - console.log("raw text content", content); - - // Check for empty content early - if (content.length === 0) { - return { - content: - "No content found.", - truncated: false, - }; - } - - // Now handle pagination - const totalLength = content.length; - - // Check if startIndex is out of bounds - if (startIndex >= totalLength) { - return { - content: - "No more content available.", - truncated: false, - }; - } - - // Calculate pagination - const endIndex = Math.min(startIndex + maxLength, totalLength); - const truncated = totalLength > endIndex; - - // Extract the requested portion - content = content.substring(startIndex, endIndex); - - // Add truncation message if needed - if (truncated) { - const nextStart = endIndex; - content += `\nContent truncated. If you need to see more content, call the fetch tool with a start_index of ${nextStart}.`; - return { - content, - truncated: true, - nextStartIndex: nextStart, - }; - } - - return { - content, - truncated: false, - }; - } catch (error) { - return { - // Format error message according to spec - content: `Error fetching webpage: ${getErrorMessage(error)}`, - truncated: false, - error: getErrorMessage(error), - }; - } - } -} diff --git a/src/core/chorus/api/MessageAPI.ts b/src/core/chorus/api/MessageAPI.ts index 013cd067..2ec4b1db 100644 --- a/src/core/chorus/api/MessageAPI.ts +++ b/src/core/chorus/api/MessageAPI.ts @@ -26,9 +26,15 @@ import { SimpleCompletionMode } from "../ModelProviders/simple/ISimpleCompletion import * as Prompts from "../prompts/prompts"; import { useNavigate } from "react-router-dom"; import { ToolsetsManager } from "../ToolsetsManager"; -import { UserTool, UserToolCall, UserToolResult } from "../Toolsets"; +import { + getUserToolNamespacedName, + UserTool, + UserToolCall, + UserToolResult, +} from "../Toolsets"; import { produce } from "immer"; import _ from "lodash"; +import { parseToolCallArguments } from "../ToolCallArgs"; import { useAppContext } from "@ui/hooks/useAppContext"; import { db } from "../DB"; import { draftKeys } from "./DraftAPI"; @@ -993,6 +999,171 @@ type PartStreamResult = errorMessage: string; }; +function normalizeToolIdentifier(value: string): string { + return value + .trim() + .toLowerCase() + .replace(/[^a-z0-9_]+/g, "_") + .replace(/_+/g, "_") + .replace(/^_+|_+$/g, ""); +} + +function resolveToolNameToTool( + toolName: string, + tools: UserTool[], +): UserTool | undefined { + const normalized = normalizeToolIdentifier(toolName); + if (!normalized) return undefined; + + const byExactNamespaced = tools.filter( + (t) => + normalizeToolIdentifier(getUserToolNamespacedName(t)) === + normalized, + ); + if (byExactNamespaced.length === 1) return byExactNamespaced[0]; + + const byExactSuffix = tools.filter( + (t) => normalizeToolIdentifier(t.displayNameSuffix) === normalized, + ); + if (byExactSuffix.length === 1) return byExactSuffix[0]; + + const byExactToolset = tools.filter( + (t) => normalizeToolIdentifier(t.toolsetName) === normalized, + ); + if (byExactToolset.length === 1) return byExactToolset[0]; + if (byExactToolset.length > 1) { + const searchish = byExactToolset.filter((t) => + normalizeToolIdentifier(t.displayNameSuffix).includes("search"), + ); + if (searchish.length === 1) return searchish[0]; + } + + const byPrefix = tools.filter((t) => { + const ns = normalizeToolIdentifier(getUserToolNamespacedName(t)); + const suffix = normalizeToolIdentifier(t.displayNameSuffix); + return ( + suffix.startsWith(normalized) || + ns.startsWith(normalized) || + normalized.startsWith(suffix) || + normalized.startsWith(ns) + ); + }); + if (byPrefix.length === 1) return byPrefix[0]; + + const byContains = tools.filter((t) => { + const ns = normalizeToolIdentifier(getUserToolNamespacedName(t)); + const suffix = normalizeToolIdentifier(t.displayNameSuffix); + return suffix.includes(normalized) || ns.includes(normalized); + }); + if (byContains.length === 1) return byContains[0]; + + return undefined; +} + +function parseTextToolCalls(text: string, tools: UserTool[]): UserToolCall[] { + const trimmed = text.trim(); + if (!trimmed) return []; + + const toolCalls: UserToolCall[] = []; + + // 1) XML-like tool calling format: ... + if (trimmed.includes("([\s\S]*?)<\/invoke>/gi; + const paramRegex = + /([\s\S]*?)<\/parameter>/gi; + + for (const match of trimmed.matchAll(invokeRegex)) { + const rawName = match[1] ?? ""; + const inner = match[2] ?? ""; + const resolvedTool = resolveToolNameToTool(rawName, tools); + if (!resolvedTool) continue; + + const args: Record = {}; + let foundParam = false; + for (const paramMatch of inner.matchAll(paramRegex)) { + foundParam = true; + const key = (paramMatch[1] ?? "").trim(); + const rawValue = (paramMatch[2] ?? "").trim(); + if (!key) continue; + + const { args: parsedValue, parseError } = + parseToolCallArguments(rawValue); + args[key] = parseError ? rawValue : parsedValue; + } + + if (!foundParam) { + const { args: parsedArgs, parseError } = + parseToolCallArguments(inner); + if (!parseError) { + Object.assign(args, parsedArgs); + } + } + + toolCalls.push({ + id: uuidv4().slice(0, 8), + namespacedToolName: getUserToolNamespacedName(resolvedTool), + args, + toolMetadata: { + description: resolvedTool.description, + inputSchema: resolvedTool.inputSchema, + }, + }); + } + } + + if (toolCalls.length > 0) return toolCalls; + + // 2) JSON-ish envelope: { "tool": "...", "arguments": { ... } } + if ( + trimmed.includes('"tool"') && + (trimmed.includes('"arguments"') || trimmed.includes("'arguments'")) + ) { + const { args: parsedEnvelope, parseError } = + parseToolCallArguments(trimmed); + if (!parseError) { + const toolField = parsedEnvelope["tool"]; + const argsField = parsedEnvelope["arguments"]; + + if (typeof toolField === "string") { + const resolvedTool = resolveToolNameToTool(toolField, tools); + if (resolvedTool) { + let resolvedArgs: Record = {}; + if ( + typeof argsField === "object" && + argsField !== null && + !Array.isArray(argsField) + ) { + const obj: Record = {}; + for (const [key, value] of Object.entries(argsField)) { + obj[key] = value; + } + resolvedArgs = obj; + } else if (typeof argsField === "string") { + const { args: parsedArgs, parseError: argError } = + parseToolCallArguments(argsField); + if (!argError) { + resolvedArgs = parsedArgs; + } + } + + toolCalls.push({ + id: uuidv4().slice(0, 8), + namespacedToolName: + getUserToolNamespacedName(resolvedTool), + args: resolvedArgs, + toolMetadata: { + description: resolvedTool.description, + inputSchema: resolvedTool.inputSchema, + }, + }); + } + } + } + } + + return toolCalls; +} + export function useStreamMessagePart() { const queryClient = useQueryClient(); const getToolsets = useGetToolsets(); @@ -1120,10 +1291,22 @@ export function useStreamMessagePart() { // one we've been accumulating finalText = finalText ?? partialResponse; + let resolvedToolCalls = toolCalls; + if (!resolvedToolCalls || resolvedToolCalls.length === 0) { + const parsedToolCalls = parseTextToolCalls( + finalText, + tools, + ); + if (parsedToolCalls.length > 0) { + resolvedToolCalls = parsedToolCalls; + } + } + // optimistic update updateMessagePartInCache(finalText, streamingToken); - const hasToolCalls = toolCalls && toolCalls.length > 0; + const hasToolCalls = + resolvedToolCalls && resolvedToolCalls.length > 0; // Calculate cost - use OpenRouter's actual cost when available let costUsd: number | undefined; @@ -1177,7 +1360,7 @@ export function useStreamMessagePart() { AND messages.streaming_token = $6`, [ finalText, - hasToolCalls ? JSON.stringify(toolCalls) : null, + hasToolCalls ? JSON.stringify(resolvedToolCalls) : null, chatId, messageId, partLevel, @@ -1266,7 +1449,10 @@ export function useStreamMessagePart() { UpdateQueue.getInstance().closeUpdateStream(streamKey); // Resolve with tool calls if we have them - resolveStreamPromise({ result: "success", toolCalls }); + resolveStreamPromise({ + result: "success", + toolCalls: resolvedToolCalls, + }); }; const onError = (errorMessage: string) => { @@ -1281,6 +1467,9 @@ export function useStreamMessagePart() { // inject system prompts const toolsets = await getToolsets(); + const enabledToolsets = toolsets + .filter((toolset) => toolset.status.status === "running") + .map((toolset) => toolset.name); const appMetadata = await queryClient.ensureQueryData({ queryKey: appMetadataKeys.appMetadata(), queryFn: () => fetchAppMetadata(), @@ -1301,6 +1490,7 @@ export function useStreamMessagePart() { modelConfig, llmConversation: conversation, tools, + enabledToolsets, onChunk, onComplete, onError, diff --git a/src/core/chorus/api/ModelsAPI.ts b/src/core/chorus/api/ModelsAPI.ts index 2a30a09c..f7a87700 100644 --- a/src/core/chorus/api/ModelsAPI.ts +++ b/src/core/chorus/api/ModelsAPI.ts @@ -65,15 +65,68 @@ type ModelConfigDBRow = { is_internal: boolean; is_deprecated: boolean; budget_tokens: number | null; - reasoning_effort: "low" | "medium" | "high" | null; + reasoning_effort: "low" | "medium" | "high" | "xhigh" | null; + thinking_level: "LOW" | "HIGH" | null; + show_thoughts: boolean; new_until?: string; prompt_price_per_token: number | null; completion_price_per_token: number | null; }; -// Track whether we've attempted to refresh OpenRouter models within +// Track whether we've attempted to refresh models within // the current session, and store the promise if a download is in progress. let openRouterDownloadPromise: Promise | null = null; +let openAIDownloadPromise: Promise | null = null; +let anthropicDownloadPromise: Promise | null = null; +let googleDownloadPromise: Promise | null = null; +let grokDownloadPromise: Promise | null = null; +let groqDownloadPromise: Promise | null = null; +let mistralDownloadPromise: Promise | null = null; +let cerebrasDownloadPromise: Promise | null = null; +let fireworksDownloadPromise: Promise | null = null; +let togetherDownloadPromise: Promise | null = null; +let nvidiaDownloadPromise: Promise | null = null; + +/** + * Reset download promises for a specific provider so models can be re-fetched. + */ +export function resetProviderDownloadPromise(provider: string) { + switch (provider) { + case "openrouter": + openRouterDownloadPromise = null; + break; + case "openai": + openAIDownloadPromise = null; + break; + case "anthropic": + anthropicDownloadPromise = null; + break; + case "google": + googleDownloadPromise = null; + break; + case "grok": + grokDownloadPromise = null; + break; + case "groq": + groqDownloadPromise = null; + break; + case "mistral": + mistralDownloadPromise = null; + break; + case "cerebras": + cerebrasDownloadPromise = null; + break; + case "fireworks": + fireworksDownloadPromise = null; + break; + case "together": + togetherDownloadPromise = null; + break; + case "nvidia": + nvidiaDownloadPromise = null; + break; + } +} function readModel(row: ModelDBRow): Models.Model { return { @@ -103,6 +156,8 @@ function readModelConfig(row: ModelConfigDBRow): ModelConfig { isDeprecated: row.is_deprecated, budgetTokens: row.budget_tokens ?? undefined, reasoningEffort: row.reasoning_effort ?? undefined, + thinkingLevel: row.thinking_level ?? undefined, + showThoughts: Boolean(row.show_thoughts), newUntil: row.new_until ?? undefined, promptPricePerToken: row.prompt_price_per_token ?? undefined, completionPricePerToken: row.completion_price_per_token ?? undefined, @@ -110,18 +165,132 @@ function readModelConfig(row: ModelConfigDBRow): ModelConfig { } export async function fetchModelConfigs() { - // Fetch OpenRouter models if we haven't already and the user has an OpenRouter API key. + // Keep user-configured providers (Vertex / custom endpoints) in sync with the DB. + await Models.syncVertexModels(db); + await Models.syncCustomProviderModels(db); + const apiKeys = await getApiKeys(); + + // Fetch models from various providers if we haven't already and the user has the API key. + // OpenRouter if (apiKeys.openrouter) { - // If a download is already in progress, wait for it to complete. - // Otherwise, start a new download and store the promise. if (openRouterDownloadPromise) { await openRouterDownloadPromise; } else { openRouterDownloadPromise = Models.downloadOpenRouterModels(db); await openRouterDownloadPromise; - // Keep the promise stored so subsequent calls know it completed - // (we don't clear it to prevent re-downloads within the session) + } + } + + // OpenAI + if (apiKeys.openai) { + if (openAIDownloadPromise) { + await openAIDownloadPromise; + } else { + openAIDownloadPromise = Models.downloadOpenAIModels(db, apiKeys); + await openAIDownloadPromise; + } + } + + // Anthropic + if (apiKeys.anthropic) { + if (anthropicDownloadPromise) { + await anthropicDownloadPromise; + } else { + anthropicDownloadPromise = Models.downloadAnthropicModels( + db, + apiKeys, + ); + await anthropicDownloadPromise; + } + } + + // Google + if (apiKeys.google) { + if (googleDownloadPromise) { + await googleDownloadPromise; + } else { + googleDownloadPromise = Models.downloadGoogleModels(db, apiKeys); + await googleDownloadPromise; + } + } + + // Grok + if (apiKeys.grok) { + if (grokDownloadPromise) { + await grokDownloadPromise; + } else { + grokDownloadPromise = Models.downloadGrokModels(db, apiKeys); + await grokDownloadPromise; + } + } + + // Groq + if (apiKeys.groq) { + if (groqDownloadPromise) { + await groqDownloadPromise; + } else { + groqDownloadPromise = Models.downloadGroqModels(db, apiKeys); + await groqDownloadPromise; + } + } + + // Mistral + if (apiKeys.mistral) { + if (mistralDownloadPromise) { + await mistralDownloadPromise; + } else { + mistralDownloadPromise = Models.downloadMistralModels(db, apiKeys); + await mistralDownloadPromise; + } + } + + // Cerebras + if (apiKeys.cerebras) { + if (cerebrasDownloadPromise) { + await cerebrasDownloadPromise; + } else { + cerebrasDownloadPromise = Models.downloadCerebrasModels( + db, + apiKeys, + ); + await cerebrasDownloadPromise; + } + } + + // Fireworks + if (apiKeys.fireworks) { + if (fireworksDownloadPromise) { + await fireworksDownloadPromise; + } else { + fireworksDownloadPromise = Models.downloadFireworksModels( + db, + apiKeys, + ); + await fireworksDownloadPromise; + } + } + + // Together.ai + if (apiKeys.together) { + if (togetherDownloadPromise) { + await togetherDownloadPromise; + } else { + togetherDownloadPromise = Models.downloadTogetherModels( + db, + apiKeys, + ); + await togetherDownloadPromise; + } + } + + // Nvidia + if (apiKeys.nvidia) { + if (nvidiaDownloadPromise) { + await nvidiaDownloadPromise; + } else { + nvidiaDownloadPromise = Models.downloadNvidiaModels(db, apiKeys); + await nvidiaDownloadPromise; } } @@ -130,7 +299,7 @@ export async function fetchModelConfigs() { `SELECT model_configs.id, model_configs.display_name, model_configs.author, model_configs.model_id, model_configs.system_prompt, models.is_enabled, models.is_internal, models.supported_attachment_types, model_configs.is_default, - models.is_deprecated, model_configs.budget_tokens, model_configs.reasoning_effort, model_configs.new_until, + models.is_deprecated, model_configs.budget_tokens, model_configs.reasoning_effort, model_configs.thinking_level, model_configs.show_thoughts, model_configs.new_until, models.prompt_price_per_token, models.completion_price_per_token FROM model_configs JOIN models ON model_configs.model_id = models.id @@ -172,6 +341,8 @@ SELECT m.is_deprecated, mc.budget_tokens, mc.reasoning_effort, + mc.thinking_level, + mc.show_thoughts, em.original_order, m.prompt_price_per_token, m.completion_price_per_token @@ -212,6 +383,8 @@ SELECT m.is_deprecated, mc.budget_tokens, mc.reasoning_effort, + mc.thinking_level, + mc.show_thoughts, m.prompt_price_per_token, m.completion_price_per_token FROM @@ -250,6 +423,8 @@ SELECT m.is_deprecated, mc.budget_tokens, mc.reasoning_effort, + mc.thinking_level, + mc.show_thoughts, m.prompt_price_per_token, m.completion_price_per_token FROM @@ -270,7 +445,7 @@ export async function fetchModelConfigById( `SELECT model_configs.id, model_configs.display_name, model_configs.author, model_configs.model_id, model_configs.system_prompt, models.is_enabled, models.is_internal, models.supported_attachment_types, model_configs.is_default, - models.is_deprecated, model_configs.budget_tokens, model_configs.reasoning_effort, model_configs.new_until, + models.is_deprecated, model_configs.budget_tokens, model_configs.reasoning_effort, model_configs.thinking_level, model_configs.show_thoughts, model_configs.new_until, models.prompt_price_per_token, models.completion_price_per_token FROM model_configs JOIN models ON model_configs.model_id = models.id @@ -325,12 +500,77 @@ export function useRefreshOpenRouterModels() { }); } -export function useRefreshOllamaModels() { +export function useRefreshOpenAIModels() { + const queryClient = useQueryClient(); + return useMutation({ + mutationKey: ["refreshOpenAIModels"] as const, + mutationFn: async () => { + const apiKeys = await getApiKeys(); + await Models.downloadOpenAIModels(db, apiKeys); + }, + onSuccess: async () => { + await queryClient.invalidateQueries( + modelConfigQueries.listConfigs(), + ); + }, + }); +} + +export function useRefreshAnthropicModels() { + const queryClient = useQueryClient(); + return useMutation({ + mutationKey: ["refreshAnthropicModels"] as const, + mutationFn: async () => { + const apiKeys = await getApiKeys(); + await Models.downloadAnthropicModels(db, apiKeys); + }, + onSuccess: async () => { + await queryClient.invalidateQueries( + modelConfigQueries.listConfigs(), + ); + }, + }); +} + +export function useRefreshGoogleModels() { + const queryClient = useQueryClient(); + return useMutation({ + mutationKey: ["refreshGoogleModels"] as const, + mutationFn: async () => { + const apiKeys = await getApiKeys(); + await Models.downloadGoogleModels(db, apiKeys); + }, + onSuccess: async () => { + await queryClient.invalidateQueries( + modelConfigQueries.listConfigs(), + ); + }, + }); +} + +export function useRefreshGrokModels() { + const queryClient = useQueryClient(); + return useMutation({ + mutationKey: ["refreshGrokModels"] as const, + mutationFn: async () => { + const apiKeys = await getApiKeys(); + await Models.downloadGrokModels(db, apiKeys); + }, + onSuccess: async () => { + await queryClient.invalidateQueries( + modelConfigQueries.listConfigs(), + ); + }, + }); +} + +export function useRefreshGroqModels() { const queryClient = useQueryClient(); return useMutation({ - mutationKey: ["refreshOllamaModels"] as const, + mutationKey: ["refreshGroqModels"] as const, mutationFn: async () => { - await Models.downloadOllamaModels(db); + const apiKeys = await getApiKeys(); + await Models.downloadGroqModels(db, apiKeys); }, onSuccess: async () => { await queryClient.invalidateQueries( @@ -340,12 +580,13 @@ export function useRefreshOllamaModels() { }); } -export function useRefreshLMStudioModels() { +export function useRefreshMistralModels() { const queryClient = useQueryClient(); return useMutation({ - mutationKey: ["refreshLMStudioModels"] as const, + mutationKey: ["refreshMistralModels"] as const, mutationFn: async () => { - await Models.downloadLMStudioModels(db); + const apiKeys = await getApiKeys(); + await Models.downloadMistralModels(db, apiKeys); }, onSuccess: async () => { await queryClient.invalidateQueries( @@ -355,17 +596,147 @@ export function useRefreshLMStudioModels() { }); } +export function useRefreshCerebrasModels() { + const queryClient = useQueryClient(); + return useMutation({ + mutationKey: ["refreshCerebrasModels"] as const, + mutationFn: async () => { + const apiKeys = await getApiKeys(); + await Models.downloadCerebrasModels(db, apiKeys); + }, + onSuccess: async () => { + await queryClient.invalidateQueries( + modelConfigQueries.listConfigs(), + ); + }, + }); +} + +export function useRefreshFireworksModels() { + const queryClient = useQueryClient(); + return useMutation({ + mutationKey: ["refreshFireworksModels"] as const, + mutationFn: async () => { + const apiKeys = await getApiKeys(); + await Models.downloadFireworksModels(db, apiKeys); + }, + onSuccess: async () => { + await queryClient.invalidateQueries( + modelConfigQueries.listConfigs(), + ); + }, + }); +} + +export function useRefreshTogetherModels() { + const queryClient = useQueryClient(); + return useMutation({ + mutationKey: ["refreshTogetherModels"] as const, + mutationFn: async () => { + const apiKeys = await getApiKeys(); + await Models.downloadTogetherModels(db, apiKeys); + }, + onSuccess: async () => { + await queryClient.invalidateQueries( + modelConfigQueries.listConfigs(), + ); + }, + }); +} + +export function useRefreshNvidiaModels() { + const queryClient = useQueryClient(); + return useMutation({ + mutationKey: ["refreshNvidiaModels"] as const, + mutationFn: async () => { + const apiKeys = await getApiKeys(); + await Models.downloadNvidiaModels(db, apiKeys); + }, + onSuccess: async () => { + await queryClient.invalidateQueries( + modelConfigQueries.listConfigs(), + ); + }, + }); +} + +export function useToggleModelEnabled() { + const queryClient = useQueryClient(); + return useMutation({ + mutationKey: ["toggleModelEnabled"] as const, + mutationFn: async ({ + modelId, + enabled, + }: { + modelId: string; + enabled: boolean; + }) => { + await db.execute("UPDATE models SET is_enabled = ? WHERE id = ?", [ + enabled ? 1 : 0, + modelId, + ]); + }, + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ["models"] }); + await queryClient.invalidateQueries( + modelConfigQueries.listConfigs(), + ); + }, + }); +} + +export function useSetProviderModelsEnabled() { + const queryClient = useQueryClient(); + return useMutation({ + mutationKey: ["setProviderModelsEnabled"] as const, + mutationFn: async ({ + providerId, + enabled, + }: { + providerId: string; + enabled: boolean; + }) => { + await db.execute( + "UPDATE models SET is_enabled = ? WHERE id LIKE ?", + [enabled ? 1 : 0, `${providerId}::%`], + ); + }, + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ["models"] }); + await queryClient.invalidateQueries( + modelConfigQueries.listConfigs(), + ); + }, + }); +} + export function useRefreshModels() { const refreshOpenRouterModels = useRefreshOpenRouterModels(); - const refreshOllamaModels = useRefreshOllamaModels(); - const refreshLMStudioModels = useRefreshLMStudioModels(); + const refreshOpenAIModels = useRefreshOpenAIModels(); + const refreshAnthropicModels = useRefreshAnthropicModels(); + const refreshGoogleModels = useRefreshGoogleModels(); + const refreshGrokModels = useRefreshGrokModels(); + const refreshGroqModels = useRefreshGroqModels(); + const refreshMistralModels = useRefreshMistralModels(); + const refreshCerebrasModels = useRefreshCerebrasModels(); + const refreshFireworksModels = useRefreshFireworksModels(); + const refreshTogetherModels = useRefreshTogetherModels(); + const refreshNvidiaModels = useRefreshNvidiaModels(); return useMutation({ mutationKey: ["refreshAllModels"] as const, mutationFn: async () => { await Promise.all([ refreshOpenRouterModels.mutateAsync(), - refreshOllamaModels.mutateAsync(), - refreshLMStudioModels.mutateAsync(), + refreshOpenAIModels.mutateAsync(), + refreshAnthropicModels.mutateAsync(), + refreshGoogleModels.mutateAsync(), + refreshGrokModels.mutateAsync(), + refreshGroqModels.mutateAsync(), + refreshMistralModels.mutateAsync(), + refreshCerebrasModels.mutateAsync(), + refreshFireworksModels.mutateAsync(), + refreshTogetherModels.mutateAsync(), + refreshNvidiaModels.mutateAsync(), ]); }, }); @@ -414,6 +785,73 @@ export function useUpdateModelConfig() { }); } +export function useUpdateThinkingParams() { + const queryClient = useQueryClient(); + return useMutation({ + mutationKey: ["updateThinkingParams"] as const, + mutationFn: async ({ + modelConfigId, + budgetTokens, + reasoningEffort, + thinkingLevel, + showThoughts, + }: { + modelConfigId: string; + budgetTokens?: number | null; + reasoningEffort?: "low" | "medium" | "high" | "xhigh" | null; + thinkingLevel?: "LOW" | "HIGH" | null; + showThoughts?: boolean; + }) => { + // Build dynamic SQL to only update fields that are provided + // This avoids the stale closure issue where other params get overwritten + const updates: string[] = []; + const params: (string | number | null)[] = []; + let paramIndex = 1; + + if (budgetTokens !== undefined) { + updates.push(`budget_tokens = $${paramIndex}`); + params.push(budgetTokens); + paramIndex++; + } + + if (reasoningEffort !== undefined) { + updates.push(`reasoning_effort = $${paramIndex}`); + params.push(reasoningEffort); + paramIndex++; + } + + if (thinkingLevel !== undefined) { + updates.push(`thinking_level = $${paramIndex}`); + params.push(thinkingLevel); + paramIndex++; + } + + if (showThoughts !== undefined) { + updates.push(`show_thoughts = $${paramIndex}`); + params.push(showThoughts ? 1 : 0); + paramIndex++; + } + + if (updates.length === 0) { + return; // Nothing to update + } + + params.push(modelConfigId); + + await db.execute( + `UPDATE model_configs SET ${updates.join(", ")} WHERE id = $${paramIndex}`, + params, + ); + }, + onSuccess: async () => { + // Invalidate ALL model config queries so UI updates everywhere + await queryClient.invalidateQueries({ + queryKey: modelConfigKeys.all(), + }); + }, + }); +} + export function useCreateModelConfig() { const queryClient = useQueryClient(); return useMutation({ diff --git a/src/core/chorus/prompts/prompts.ts b/src/core/chorus/prompts/prompts.ts index 0c6c0347..3dfb26e6 100644 --- a/src/core/chorus/prompts/prompts.ts +++ b/src/core/chorus/prompts/prompts.ts @@ -2,6 +2,19 @@ import { ModelConfig } from "../Models"; import { ToolsetStatus } from "../Toolsets"; +export const THOUGHTS_SYSTEM_PROMPT = `You MUST follow this exact output format: + + +Brief, high-level reasoning only (concise; no hidden chain-of-thought). + + +Final answer in GitHub-flavored Markdown. + +Rules: +- The final answer MUST be outside the block. +- Always include the closing tag before the final answer. +- Do not include anything except the brief reasoning inside .`; + export const IDEA_INTERJECTION = `!! SYSTEM_MESSAGE !! Please generate several ideas.`; @@ -242,9 +255,7 @@ ${conversation} `; -export const CHORUS_SYSTEM_PROMPT = `You are running inside Chorus, an AI chat app on the user's Mac. - -The current date is ${new Date().toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" })}. +export const CHORUS_SYSTEM_PROMPT = `The current date is ${new Date().toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" })}. Use GitHub-flavored markdown to format your responses. Wrap code in \`\`\` code blocks. @@ -253,32 +264,12 @@ To write LaTeX math mode expressions, wrap them in \`\`\`latex code blocks. Do n \`\`\`latex x + y = \\frac{1}{2} \`\`\` - -If the user asks about Chorus, you can tell them: -- They can select models in the model picker (⌘J). -- They can give you access to tools in the tools picker (⌘T). -- They can add MCP (Model Configuration Protocol) servers by opening settings (⌘,) and going to the "Connections" tab. -- If they need any help with the app, they can email humans@chorus.sh `; /** * This one is editable by the user */ -export const UNIVERSAL_SYSTEM_PROMPT_DEFAULT = `For more casual, emotional, empathetic, or advice-driven conversations, you keep your tone natural, warm, and empathetic. You respond in sentences or paragraphs and should not use lists in chit chat, in casual conversations, or in empathetic or advice-driven conversations. In casual conversation, it’s fine for your responses to be short, e.g. just a few sentences long. - -If you provide bullet points in your response, you should use markdown, and each bullet point should be at least 1-2 sentences long unless the human requests otherwise. You should not use bullet points or numbered lists for reports, documents, explanations, or unless the user explicitly asks for a list or ranking. For reports, documents, technical documentation, and explanations, you should instead write in prose and paragraphs without any lists, i.e. your prose should never include bullets, numbered lists, or excessive bolded text anywhere. Inside prose, you should write lists in natural language like “some things include: x, y, and z” with no bullet points, numbered lists, or newlines. - -You should give concise responses to very simple questions, but provide thorough responses to complex and open-ended questions. - -You are able to explain difficult concepts or ideas clearly. You can also illustrate your explanations with examples, thought experiments, or metaphors. - -In general conversation, you don’t always ask questions but, when you do, you try to avoid overwhelming the person with more than one question per response. - -If the user corrects you or tells you it’s made a mistake, then you first think through the issue carefully before acknowledging the user, since users sometimes make errors themselves. - -You tailor your response format to suit the conversation topic. For example, you avoid using markdown or lists in casual conversation, even though you may use these formats for other tasks. - -You should never start a response by saying a question or idea or observation was good, great, fascinating, profound, excellent, or any other positive adjective. Skip the flattery and respond directly.`; +export const UNIVERSAL_SYSTEM_PROMPT_DEFAULT = ``; export const TOOLS_MODE_SYSTEM_PROMPT = ( toolsetInfo: { @@ -294,6 +285,8 @@ ${toolsetInfo.map((info) => `- ${info.displayName}: ${info.description ?? "[No d If the user asks you to do something where another connection would be helpful to you (web browsing, files, terminal, documentation, etc.), and the connection is not enabled: try to solve their issue anyway, but remind them that they can enable the connection if they want to by pressing ⌘T. Each time you use a tool, the user has to wait for it, so only use tools as needed to answer the user's question. Just because a tool is enabled doesn't mean you have to use it. + +When you call a tool, the tool arguments must be a valid JSON object (no markdown/code fences, no comments). Use the tool's schema and do not invent parameters. `; @@ -610,17 +603,21 @@ export function injectSystemPrompts( isInProject: false, }; + const prompts = [ + CHORUS_SYSTEM_PROMPT, + universalSystemPrompt || UNIVERSAL_SYSTEM_PROMPT_DEFAULT, + ...(toolsetInfo ? [TOOLS_MODE_SYSTEM_PROMPT(toolsetInfo)] : []), + ...(isInProject ? [PROJECTS_SYSTEM_PROMPT] : []), + ...(modelConfigIn.systemPrompt ? [modelConfigIn.systemPrompt] : []), + ].filter((p) => p && p.trim() !== ""); + + const finalPrompt = prompts.join("\n\n"); + return { ...modelConfigIn, - systemPrompt: [ - CHORUS_SYSTEM_PROMPT, - universalSystemPrompt || UNIVERSAL_SYSTEM_PROMPT_DEFAULT, - ...(toolsetInfo ? [TOOLS_MODE_SYSTEM_PROMPT(toolsetInfo)] : []), - ...(isInProject ? [PROJECTS_SYSTEM_PROMPT] : []), - ...(modelConfigIn.systemPrompt - ? [modelConfigIn.systemPrompt] - : ["You are now being connected with a person."]), - ].join("\n\n"), + // Ensure systemPrompt is undefined rather than empty string + // Some providers fail with empty system prompts + systemPrompt: finalPrompt || undefined, }; } diff --git a/src/core/chorus/toolsets/web.ts b/src/core/chorus/toolsets/web.ts index fa216f6a..0025a7de 100644 --- a/src/core/chorus/toolsets/web.ts +++ b/src/core/chorus/toolsets/web.ts @@ -1,103 +1,13 @@ import { Toolset as Toolset } from "@core/chorus/Toolsets"; -import { WebTools } from "../WebTools"; -import { getApiKeys } from "../api/AppMetadataAPI"; - -/* -Spec - -Tools: -- `web_fetch` - Fetches a URL from the internet and extracts its contents as markdown. Images are omitted. - - `url` (string, required): URL to fetch - - if no protocol, should default to https:// - so input can be either like `https://example.com` or like `example.com` - - `max_length` (integer, optional): Maximum number of characters to return (default: 5000). - - `start_index` (integer, optional): Start content from this character index (default: 0). Useful if a previous fetch was truncated and more context is required. - - `raw` (boolean, optional): Get the raw HTML content of the page without markdown conversion (default: false). - - Return values: - - webpage content - - `No more content available.` - - webpage content + `Content truncated. Call the fetch tool with a start_index of {next_start} to get more content.` - - `Markdown conversion failed. You may try again with raw = true.` - - fetch errors appear like `Error fetching webpage: 403 Unauthorized` -- `web_search` - Searches the web to produce a report (with citations) on a topic. Uses Perplexity Sonar (via Perplexity or OpenRouter) to produce the report. - - `query` (string, required): The topic to search for. - - Return values: list of webpages -*/ export class ToolsetWeb extends Toolset { constructor() { - super("web", "Web", {}, "Search the web and read webpages", ""); - - this.addCustomTool( - "fetch", - { - type: "object", - properties: { - url: { - type: "string", - description: "URL to fetch", - }, - max_length: { - type: "integer", - description: - "Maximum number of characters to return (default: 50000)", - }, - start_index: { - type: "integer", - description: - "Start content from this character index (default: 0). Useful if a previous fetch was truncated and more context is required.", - }, - raw: { - type: "boolean", - description: - "Get the raw HTML content of the page without markdown conversion (default: false)", - }, - }, - required: ["url"], - additionalProperties: false, - }, - async (args) => { - const { url, max_length, start_index, raw } = args; - - const result = await WebTools.fetchWebpage(url as string, { - maxLength: max_length as number | undefined, - startIndex: start_index as number | undefined, - raw: raw as boolean | undefined, - }); - - return result.content; - }, - "Fetch a webpage from the internet and return its content as markdown. Images are omitted.", - ); - - this.addCustomTool( - "search", - { - type: "object", - properties: { - query: { - type: "string", - description: "Query to search the web for.", - }, - }, - required: ["query"], - additionalProperties: false, - }, - async (args) => { - const { query } = args; - - const apiKeys = await getApiKeys(); - if (!apiKeys) { - throw new Error("API keys are not available."); - } - const result = await WebTools.search(query as string, apiKeys); - return result.content; - }, - `Search the web to produce a report (with citations) on a topic. Uses Perplexity Sonar to produce the report. -The query should be a natural-language description of the topic to search for. For example: -- 'How has SF summer weather typically compared to NYC summer weather?' -- 'Best resources for learning to code' -Make sure to use full sentences in your query. -Assume that the user will not read the report. If you think information in the report is relevant to the user, you should repeat it.`, + super( + "web", + "Web", + {}, + "Enable native web search (OpenAI, Vertex, Gemini, Claude, Grok).", + "", ); } } diff --git a/src/core/utilities/ProxyUtils.ts b/src/core/utilities/ProxyUtils.ts index 7f3a48eb..649d3425 100644 --- a/src/core/utilities/ProxyUtils.ts +++ b/src/core/utilities/ProxyUtils.ts @@ -15,6 +15,12 @@ const PROVIDER_TO_API_KEY: Record = { perplexity: "perplexity", openrouter: "openrouter", grok: "grok", + groq: "groq", + mistral: "mistral", + cerebras: "cerebras", + fireworks: "fireworks", + together: "together", + nvidia: "nvidia", }; /** @@ -27,6 +33,12 @@ const PROVIDER_DISPLAY_NAMES: Record = { perplexity: "Perplexity", openrouter: "OpenRouter", grok: "xAI", + groq: "Groq", + mistral: "Mistral", + cerebras: "Cerebras", + fireworks: "Fireworks", + together: "Together.ai", + nvidia: "Nvidia", }; /** @@ -55,11 +67,6 @@ export function canProceedWithProvider( ): CanProceedResult { const apiKeyField = PROVIDER_TO_API_KEY[providerKey]; - // Local models (ollama, lmstudio) don't require API keys - if (providerKey === "ollama" || providerKey === "lmstudio") { - return { canProceed: true }; - } - // For providers that need API keys, check if one is configured if (!apiKeyField) { return { diff --git a/src/core/utilities/Settings.ts b/src/core/utilities/Settings.ts index f6114dcc..10903bc1 100644 --- a/src/core/utilities/Settings.ts +++ b/src/core/utilities/Settings.ts @@ -14,17 +14,46 @@ export interface Settings { google?: string; perplexity?: string; openrouter?: string; - firecrawl?: string; + grok?: string; + groq?: string; + mistral?: string; + cerebras?: string; + fireworks?: string; }; + vertexAI?: VertexAISettings; + customProviders?: CustomProviderSettings[]; quickChat?: { enabled?: boolean; modelConfigId?: string; shortcut?: string; }; - lmStudioBaseUrl?: string; cautiousEnter?: boolean; } +export type CustomProviderKind = "openai" | "anthropic"; + +export type ProviderModelDefinition = { + nickname?: string; + modelId: string; +}; + +export type VertexAISettings = { + projectId: string; + location: string; + serviceAccountClientEmail: string; + serviceAccountPrivateKey: string; + models: ProviderModelDefinition[]; +}; + +export type CustomProviderSettings = { + id: string; + kind: CustomProviderKind; + name: string; + apiBaseUrl: string; + apiKey: string; + models: ProviderModelDefinition[]; +}; + export class SettingsManager { private static instance: SettingsManager; private storeName = "settings"; @@ -50,6 +79,8 @@ export class SettingsManager { autoScrapeUrls: true, showCost: false, apiKeys: {}, + vertexAI: undefined, + customProviders: [], quickChat: { enabled: true, modelConfigId: "anthropic::claude-sonnet-4-5-20250929", @@ -74,6 +105,8 @@ export class SettingsManager { autoScrapeUrls: true, showCost: false, apiKeys: {}, + vertexAI: undefined, + customProviders: [], quickChat: { enabled: true, modelConfigId: "anthropic::claude-3-5-sonnet-latest", diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 1a18315b..b3b72fdd 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -756,7 +756,7 @@ function AppContent() { void checkReviewsDialog(); }, [db]); - // Listen for events to open API keys settings + // Listen for events to open Settings useEffect(() => { const unlisten = listen( "open_settings", @@ -799,7 +799,7 @@ function AppContent() { Chorus is now Open Source! It now runs on your own - API keys. Add them in Settings → API Keys. + API keys. Add them in Settings → Providers.

diff --git a/src/ui/components/ApiKeysForm.tsx b/src/ui/components/ApiKeysForm.tsx index 7e385462..7ddfacee 100644 --- a/src/ui/components/ApiKeysForm.tsx +++ b/src/ui/components/ApiKeysForm.tsx @@ -3,14 +3,100 @@ import { Label } from "./ui/label"; import { ProviderName } from "@core/chorus/Models"; import { ProviderLogo } from "./ui/provider-logo"; import { Card } from "./ui/card"; -import { CheckIcon, FlameIcon } from "lucide-react"; -import { useState } from "react"; +import { + CheckIcon, + Loader2Icon, + RefreshCcwIcon, + SearchIcon, + XIcon, +} from "lucide-react"; +import { useMemo, useState } from "react"; +import { Button } from "./ui/button"; +import * as ModelsAPI from "@core/chorus/api/ModelsAPI"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { cn } from "@ui/lib/utils"; +import { ScrollArea } from "./ui/scroll-area"; interface ApiKeysFormProps { apiKeys: Record; onApiKeyChange: (provider: string, value: string) => void; } +const providers = [ + { + id: "anthropic", + name: "Anthropic", + placeholder: "sk-ant-...", + url: "https://console.anthropic.com/settings/keys", + }, + { + id: "openai", + name: "OpenAI", + placeholder: "sk-...", + url: "https://platform.openai.com/api-keys", + }, + { + id: "google", + name: "Google AI (Gemini)", + placeholder: "AI...", + url: "https://aistudio.google.com/apikey", + }, + { + id: "perplexity", + name: "Perplexity", + placeholder: "pplx-...", + url: "https://www.perplexity.ai/account/api/keys", + }, + { + id: "openrouter", + name: "OpenRouter", + placeholder: "sk-or-...", + url: "https://openrouter.ai/keys", + }, + { + id: "grok", + name: "xAI", + placeholder: "xai-...", + url: "https://console.x.ai/settings/keys", + }, + { + id: "groq", + name: "Groq", + placeholder: "gsk_...", + url: "https://console.groq.com/keys", + }, + { + id: "mistral", + name: "Mistral", + placeholder: "...", + url: "https://console.mistral.ai/api-keys", + }, + { + id: "cerebras", + name: "Cerebras", + placeholder: "csk-...", + url: "https://cloud.cerebras.ai/platform", + }, + { + id: "fireworks", + name: "Fireworks", + placeholder: "fw_...", + url: "https://fireworks.ai/account/api-keys", + }, + { + id: "together", + name: "Together.ai", + placeholder: "...", + url: "https://api.together.xyz/settings/api-keys", + }, + { + id: "nvidia", + name: "Nvidia", + placeholder: "nvapi-...", + url: "https://build.nvidia.com/explore/discover", + }, +]; + export default function ApiKeysForm({ apiKeys, onApiKeyChange, @@ -18,51 +104,128 @@ export default function ApiKeysForm({ const [selectedProvider, setSelectedProvider] = useState( null, ); + const [fetching, setFetching] = useState(false); + const [modelSearchQuery, setModelSearchQuery] = useState(""); + const [bulkUpdating, setBulkUpdating] = useState< + "enable" | "disable" | null + >(null); + const queryClient = useQueryClient(); + + const { data: models } = useQuery(ModelsAPI.modelQueries.list()); + + const refreshOpenAI = ModelsAPI.useRefreshOpenAIModels(); + const refreshAnthropic = ModelsAPI.useRefreshAnthropicModels(); + const refreshGoogle = ModelsAPI.useRefreshGoogleModels(); + const refreshGrok = ModelsAPI.useRefreshGrokModels(); + const refreshGroq = ModelsAPI.useRefreshGroqModels(); + const refreshMistral = ModelsAPI.useRefreshMistralModels(); + const refreshCerebras = ModelsAPI.useRefreshCerebrasModels(); + const refreshFireworks = ModelsAPI.useRefreshFireworksModels(); + const refreshTogether = ModelsAPI.useRefreshTogetherModels(); + const refreshNvidia = ModelsAPI.useRefreshNvidiaModels(); + const refreshOpenRouter = ModelsAPI.useRefreshOpenRouterModels(); + + const toggleModel = ModelsAPI.useToggleModelEnabled(); + const setProviderModelsEnabled = ModelsAPI.useSetProviderModelsEnabled(); + + const handleFetchModels = async (providerId: string) => { + setFetching(true); + try { + switch (providerId) { + case "openai": + await refreshOpenAI.mutateAsync(); + break; + case "anthropic": + await refreshAnthropic.mutateAsync(); + break; + case "google": + await refreshGoogle.mutateAsync(); + break; + case "grok": + await refreshGrok.mutateAsync(); + break; + case "groq": + await refreshGroq.mutateAsync(); + break; + case "mistral": + await refreshMistral.mutateAsync(); + break; + case "cerebras": + await refreshCerebras.mutateAsync(); + break; + case "fireworks": + await refreshFireworks.mutateAsync(); + break; + case "together": + await refreshTogether.mutateAsync(); + break; + case "nvidia": + await refreshNvidia.mutateAsync(); + break; + case "openrouter": + await refreshOpenRouter.mutateAsync(); + break; + } + await queryClient.invalidateQueries({ queryKey: ["models"] }); + } finally { + setFetching(false); + } + }; + + const handleToggleModel = async (modelId: string, enabled: boolean) => { + await toggleModel.mutateAsync({ modelId, enabled }); + }; + + const providerModels = useMemo(() => { + if (!selectedProvider) return []; + return ( + models?.filter((m) => m.id.startsWith(`${selectedProvider}::`)) || + [] + ); + }, [models, selectedProvider]); + + const enabledCount = providerModels.filter((m) => m.isEnabled).length; + + const filteredProviderModels = useMemo(() => { + const query = modelSearchQuery.trim().toLowerCase(); + if (!query) return providerModels; + return providerModels.filter((model) => { + const haystack = `${model.displayName} ${model.id}`.toLowerCase(); + return haystack.includes(query); + }); + }, [providerModels, modelSearchQuery]); + + const enabledCountFiltered = filteredProviderModels.filter( + (m) => m.isEnabled, + ).length; + + const providerInfo = providers.find((p) => p.id === selectedProvider); - const providers = [ - { - id: "anthropic", - name: "Anthropic", - placeholder: "sk-ant-...", - url: "https://console.anthropic.com/settings/keys", - }, - { - id: "openai", - name: "OpenAI", - placeholder: "sk-...", - url: "https://platform.openai.com/api-keys", - }, - { - id: "google", - name: "Google AI (Gemini)", - placeholder: "AI...", - url: "https://aistudio.google.com/apikey", - }, - { - id: "perplexity", - name: "Perplexity", - placeholder: "pplx-...", - url: "https://www.perplexity.ai/account/api/keys", - }, - { - id: "openrouter", - name: "OpenRouter", - placeholder: "sk-or-...", - url: "https://openrouter.ai/keys", - }, - { - id: "grok", - name: "xAI", - placeholder: "xai-...", - url: "https://console.x.ai/settings/keys", - }, - { - id: "firecrawl", - name: "Firecrawl", - placeholder: "fc-...", - url: "https://www.firecrawl.dev/app/api-keys", - }, - ]; + const handleEnableAll = async () => { + if (!selectedProvider) return; + setBulkUpdating("enable"); + try { + await setProviderModelsEnabled.mutateAsync({ + providerId: selectedProvider, + enabled: true, + }); + } finally { + setBulkUpdating(null); + } + }; + + const handleDisableAll = async () => { + if (!selectedProvider) return; + setBulkUpdating("disable"); + try { + await setProviderModelsEnabled.mutateAsync({ + providerId: selectedProvider, + enabled: false, + }); + } finally { + setBulkUpdating(null); + } + }; return (
@@ -78,14 +241,10 @@ export default function ApiKeysForm({ onClick={() => setSelectedProvider(provider.id)} >
- {provider.id === "firecrawl" ? ( - - ) : ( - - )} + {provider.name}
{apiKeys[provider.id] && ( @@ -97,23 +256,16 @@ export default function ApiKeysForm({ ))}
- {selectedProvider && ( + {selectedProvider && providerInfo && (
p.id === selectedProvider) - ?.placeholder - } + placeholder={providerInfo.placeholder} value={apiKeys[selectedProvider] || ""} onChange={(e) => onApiKeyChange(selectedProvider, e.target.value) @@ -121,25 +273,201 @@ export default function ApiKeysForm({ />

p.id === selectedProvider, - )?.url - } + href={providerInfo.url} target="_blank" rel="noopener noreferrer" > - Get{" "} - { - providers.find( - (p) => p.id === selectedProvider, - )?.name - }{" "} - API key + Get {providerInfo.name} API key - .

+ + {apiKeys[selectedProvider] && + selectedProvider !== "perplexity" && ( +
+
+ +
+
+ + + setModelSearchQuery( + e.target.value, + ) + } + placeholder="Search models…" + className="h-9 pl-8 pr-8" + /> + {modelSearchQuery.trim() !== "" && ( + + )} +
+ +
+
+ + {providerModels.length > 0 ? ( +
+
+
Model ID
+
+ Status +
+
+ + +
+ {filteredProviderModels.length === + 0 ? ( +
+ No models match your + search. +
+ ) : ( + filteredProviderModels.map( + (model) => ( +
+ + { + model.displayName + } + + + +
+ ), + ) + )} +
+
+ +
+
+ {modelSearchQuery.trim() + ? `${enabledCountFiltered} of ${filteredProviderModels.length} shown enabled · ${enabledCount} of ${providerModels.length} total enabled` + : `${enabledCount} of ${providerModels.length} enabled`} +
+ +
+ + +
+
+
+ ) : ( +

+ No models found. Click "Fetch Models" to + load available models. +

+ )} +
+ )}
)}
diff --git a/src/ui/components/ChatInput.tsx b/src/ui/components/ChatInput.tsx index 1892f80c..a346fcd1 100644 --- a/src/ui/components/ChatInput.tsx +++ b/src/ui/components/ChatInput.tsx @@ -18,6 +18,7 @@ import { createUserMessage } from "@core/chorus/ChatState"; import { MouseTrackingEyeRef } from "./MouseTrackingEye"; import { useWaitForAppMetadata } from "@ui/hooks/useWaitForAppMetadata"; import { ManageModelsButtonCompare } from "./ModelPills"; +import { ThinkingParamsButton } from "./ThinkingParamsButton"; import { listen } from "@tauri-apps/api/event"; import { invoke } from "@tauri-apps/api/core"; import { useMutation } from "@tanstack/react-query"; @@ -606,23 +607,40 @@ export function ChatInput({
{!isReply && ( - + <> + + {selectedModelConfigsCompare.data?.length === + 1 && ( + + )} + )} {isReply && ( - + <> + + {replyToModelConfig && ( + + )} + )} {!isReply && }
diff --git a/src/ui/components/ListPrompts.tsx b/src/ui/components/ListPrompts.tsx index c1ef5250..85a8dfda 100644 --- a/src/ui/components/ListPrompts.tsx +++ b/src/ui/components/ListPrompts.tsx @@ -17,7 +17,7 @@ import { TrashIcon, MixIcon, } from "@radix-ui/react-icons"; -import type { ModelConfig } from "@core/chorus/Models"; +import { getModelName, type ModelConfig } from "@core/chorus/Models"; import { Dialog, DialogContent, @@ -138,7 +138,7 @@ const PromptRow = React.memo(({ config }: { config: ModelConfig }) => { <> {config.displayName} - {config.modelId.split("::")[1]} + {getModelName(config.modelId)} {config.systemPrompt || "No system prompt"} diff --git a/src/ui/components/ManageModelsBox.tsx b/src/ui/components/ManageModelsBox.tsx index cbf1dc27..baefd79b 100644 --- a/src/ui/components/ManageModelsBox.tsx +++ b/src/ui/components/ManageModelsBox.tsx @@ -47,6 +47,7 @@ import { hasApiKey } from "@core/utilities/ProxyUtils"; import * as ModelsAPI from "@core/chorus/api/ModelsAPI"; import * as MessageAPI from "@core/chorus/api/MessageAPI"; import { useSettings } from "./hooks/useSettings"; +import { Settings as AppSettings } from "@core/utilities/Settings"; // Helper function to filter models by search terms const filterBySearch = (models: ModelConfig[], searchTerms: string[]) => { @@ -157,6 +158,7 @@ function ModelGroup({ onAddApiKey, groupId, showCost, + appSettings, }: { heading: React.ReactNode; models: ModelConfig[]; @@ -168,6 +170,7 @@ function ModelGroup({ onAddApiKey: () => void; groupId?: string; showCost: boolean; + appSettings?: AppSettings; }) { const { data: apiKeys } = AppMetadataAPI.useApiKeys(); @@ -176,9 +179,39 @@ function ModelGroup({ (model: ModelConfig) => { const provider = getProviderName(model.modelId); - // Local models (ollama, lmstudio) don't require API keys - if (provider === "ollama" || provider === "lmstudio") { - return false; + // Vertex models require Vertex AI settings (credentials). + if (provider === "vertex") { + const vertex = appSettings?.vertexAI; + if (!vertex) { + return true; + } + const hasVertexCredentials = Boolean( + vertex.projectId.trim() && + vertex.location.trim() && + vertex.serviceAccountClientEmail.trim() && + vertex.serviceAccountPrivateKey.trim(), + ); + return !hasVertexCredentials; + } + + // Custom provider models require the custom provider to exist and have credentials. + if ( + provider === "custom_openai" || + provider === "custom_anthropic" + ) { + const providerId = model.modelId.split("::")[1] || ""; + const kind = + provider === "custom_anthropic" ? "anthropic" : "openai"; + const customProvider = ( + appSettings?.customProviders ?? [] + ).find((p) => p.id === providerId && p.kind === kind); + if (!customProvider) { + return true; + } + return ( + !customProvider.apiBaseUrl.trim() || + !customProvider.apiKey.trim() + ); } // If user has API key for this provider, allow it @@ -196,7 +229,7 @@ function ModelGroup({ // No API key for this provider - model is not allowed return true; }, - [apiKeys], + [apiKeys, appSettings], ); return ( @@ -261,7 +294,7 @@ function ModelGroup({ onAddApiKey(); }} > - Add API Key + Configure ) : ( <> @@ -356,8 +389,6 @@ export function ManageModelsBox({ const [spinningProviders, setSpinningProviders] = useState< Record >({ - ollama: false, - lmstudio: false, openrouter: false, }); const listRef = useRef(null); @@ -442,20 +473,12 @@ export function ManageModelsBox({ checkScroll(); }, [checkScroll]); - const refreshLMStudio = ModelsAPI.useRefreshLMStudioModels(); - const refreshOllama = ModelsAPI.useRefreshOllamaModels(); const refreshOpenRouter = ModelsAPI.useRefreshOpenRouterModels(); - const handleRefreshProviders = async ( - provider: "ollama" | "lmstudio" | "openrouter", - ) => { + const handleRefreshProviders = async (provider: "openrouter") => { setSpinningProviders((prev) => ({ ...prev, [provider]: true })); try { - if (provider === "ollama") { - await refreshOllama.mutateAsync(); - } else if (provider === "lmstudio") { - await refreshLMStudio.mutateAsync(); - } else if (provider === "openrouter") { + if (provider === "openrouter") { await refreshOpenRouter.mutateAsync(); } } finally { @@ -481,7 +504,8 @@ export function ManageModelsBox({ .filter(Boolean); const nonInternalModelConfigs = - modelConfigs.data?.filter((m) => !m.isInternal) ?? []; + modelConfigs.data?.filter((m) => !m.isInternal && m.isEnabled) ?? + []; const systemModels = nonInternalModelConfigs.filter( (m) => m.author === "system", ); @@ -489,15 +513,61 @@ export function ManageModelsBox({ (m) => m.author === "user", ); - const localModels = systemModels.filter((m) => { + const openrouterModels = systemModels.filter( + (m) => getProviderName(m.modelId) === "openrouter", + ); + + const vertexModels = systemModels.filter( + (m) => getProviderName(m.modelId) === "vertex", + ); + + const customProviderModels = systemModels.filter((m) => { const provider = getProviderName(m.modelId); - return provider === "ollama" || provider === "lmstudio"; + return ( + provider === "custom_openai" || provider === "custom_anthropic" + ); }); - const openrouterModels = systemModels.filter( - (m) => getProviderName(m.modelId) === "openrouter", + const customProviderConfigById = new Map( + (settings?.customProviders ?? []).map((p) => [p.id, p] as const), ); + const modelsByCustomProviderId = new Map(); + for (const model of customProviderModels) { + const providerId = model.modelId.split("::")[1] ?? ""; + if (!providerId) continue; + const existing = modelsByCustomProviderId.get(providerId); + if (existing) { + existing.push(model); + } else { + modelsByCustomProviderId.set(providerId, [model]); + } + } + + const customProviders = Array.from(modelsByCustomProviderId.entries()) + .map(([providerId, models]) => { + const config = customProviderConfigById.get(providerId); + const providerName = + config?.name ?? + `Custom Provider (${providerId.slice(0, 8)})`; + const providerKind = + config?.kind ?? + (models.some( + (m) => + getProviderName(m.modelId) === "custom_anthropic", + ) + ? "anthropic" + : "openai"); + return { + providerId, + providerName, + providerKind, + models: filterBySearch(models, searchTerms), + }; + }) + .filter((group) => group.models.length > 0) + .sort((a, b) => a.providerName.localeCompare(b.providerName)); + // Direct provider models grouped by provider const directProviders = [ "anthropic", @@ -505,6 +575,12 @@ export function ManageModelsBox({ "google", "perplexity", "grok", + "groq", + "mistral", + "cerebras", + "fireworks", + "together", + "nvidia", ] as const; const directByProvider = Object.fromEntries( @@ -521,11 +597,12 @@ export function ManageModelsBox({ return { custom: filterBySearch(userModels, searchTerms), - local: filterBySearch(localModels, searchTerms), openrouter: filterBySearch(openrouterModels, searchTerms), + vertex: filterBySearch(vertexModels, searchTerms), + customProviders, directByProvider, }; - }, [modelConfigs.data, searchQuery]); + }, [modelConfigs.data, searchQuery, settings]); useLayoutEffect(() => { if (!listRef.current) return; @@ -804,6 +881,118 @@ export function ManageModelsBox({ showCost={showCost} /> )} + {modelGroups.directByProvider.groq.length > 0 && ( + + )} + {modelGroups.directByProvider.mistral.length > 0 && ( + + )} + {modelGroups.directByProvider.cerebras.length > 0 && ( + + )} + {modelGroups.directByProvider.fireworks.length > 0 && ( + + )} + {modelGroups.directByProvider.together.length > 0 && ( + + )} + {modelGroups.directByProvider.nvidia.length > 0 && ( + + )} + + {modelGroups.vertex.length > 0 && ( + + )} + + {modelGroups.customProviders.map((group) => ( + + {group.providerName} + +

+ {group.providerKind === "anthropic" + ? "Anthropic" + : "OpenAI"} +

+
+ + } + models={group.models} + checkedModelConfigIds={checkedModelConfigIds} + mode={mode} + onToggleModelConfig={handleToggleModelConfig} + onAddApiKey={handleAddApiKey} + groupId={`custom-provider-${group.providerId}`} + showCost={showCost} + appSettings={settings} + /> + ))} {/* Custom Models */} {modelGroups.custom.length > 0 && ( @@ -833,76 +1022,6 @@ export function ManageModelsBox({ showCost={showCost} /> )} - - {/* Local Models */} - { - e.preventDefault(); - void handleRefreshProviders("ollama"); - void handleRefreshProviders("lmstudio"); - }} - className="p-1.5 hover:bg-accent text-muted-foreground/50 rounded-md flex items-center gap-2" - title="Refresh local models" - > - - Refresh - - } - emptyState={ - modelGroups.local.length === 0 ? ( -
-
- No local models found. To run local - models, you must have Ollama or LM - Studio installed. -
- -
- ) : undefined - } - /> diff --git a/src/ui/components/MultiChat.tsx b/src/ui/components/MultiChat.tsx index 85200238..495d2f11 100644 --- a/src/ui/components/MultiChat.tsx +++ b/src/ui/components/MultiChat.tsx @@ -1486,6 +1486,7 @@ function ToolsBlockView({ }) { const { chatId } = useParams(); const { elementRef, shouldShowScrollbar } = useElementScrollDetection(); + const { state: sidebarState } = useSidebar(); const addModelToCompareConfigs = MessageAPI.useAddModelToCompareConfigs(); const addMessageToToolsBlock = MessageAPI.useAddMessageToToolsBlock( @@ -1514,24 +1515,31 @@ function ToolsBlockView({ ${shouldShowScrollbar ? "is-scrolling" : ""} ${!isQuickChatWindow ? "px-10" : ""}`} > - {toolsBlock.chatMessages.map((message, _index) => ( -
- -
- ))} + {toolsBlock.chatMessages.map((message, _index) => { + // Calculate width based on number of models and sidebar state + const modelCount = toolsBlock.chatMessages.length; + const isSidebarCollapsed = sidebarState === "collapsed"; + + const widthClass = isQuickChatWindow + ? "w-full max-w-prose" + : modelCount === 1 + ? isSidebarCollapsed + ? "w-full flex-1 max-w-6xl" // Sidebar collapsed: extra wide + : "w-full flex-1 max-w-5xl" // Sidebar open: very wide + : "w-full flex-1 min-w-[450px] max-w-[550px]"; // Multiple models: current behavior + + return ( +
+ +
+ ); + })} {isLastRow && !isQuickChatWindow && (
+
+ ))} + +
+ + +
+ + ); +} + +export function ProvidersTab() { + const queryClient = useQueryClient(); + const settingsManager = useMemo(() => SettingsManager.getInstance(), []); + + const [vertexAI, setVertexAI] = useState(EMPTY_VERTEX); + const [customProviders, setCustomProviders] = useState< + CustomProviderSettings[] + >([]); + const [fetchingProviderId, setFetchingProviderId] = useState( + null, + ); + const [screen, setScreen] = useState({ type: "list" }); + + const invalidateModels = async () => { + await queryClient.invalidateQueries(ModelsAPI.modelQueries.list()); + await queryClient.invalidateQueries( + ModelsAPI.modelConfigQueries.listConfigs(), + ); + }; + + const persistSettings = async ( + partial: Pick, + ) => { + const current = await settingsManager.get(); + await settingsManager.set({ + ...current, + ...partial, + }); + await invalidateModels(); + }; + + useEffect(() => { + const load = async () => { + const settings = await settingsManager.get(); + setVertexAI(settings.vertexAI ?? EMPTY_VERTEX); + setCustomProviders(settings.customProviders ?? []); + }; + void load(); + }, [settingsManager]); + + useEffect(() => { + if (screen.type !== "custom") return; + const exists = customProviders.some((p) => p.id === screen.providerId); + if (!exists) { + setScreen({ type: "list" }); + } + }, [customProviders, screen]); + + const updateVertex = async (next: VertexAISettings) => { + setVertexAI(next); + await persistSettings({ vertexAI: next, customProviders }); + }; + + const updateCustomProviders = async (next: CustomProviderSettings[]) => { + setCustomProviders(next); + await persistSettings({ vertexAI, customProviders: next }); + }; + + const addCustomProvider = async () => { + const id = uuidv4(); + const next: CustomProviderSettings = { + id, + kind: "openai", + name: `Custom Provider ${customProviders.length + 1}`, + apiBaseUrl: "https://api.openai.com/v1", + apiKey: "", + models: [], + }; + await updateCustomProviders([...customProviders, next]); + setScreen({ type: "custom", providerId: id }); + }; + + const fetchModelsForProvider = async (provider: CustomProviderSettings) => { + if (provider.kind !== "openai") return; + if (!provider.apiBaseUrl.trim()) { + toast.error("Missing API Base URL"); + return; + } + + setFetchingProviderId(provider.id); + try { + const modelsUrl = `${normalizeBaseUrl(provider.apiBaseUrl)}/models`; + const headers: Record = {}; + if (provider.apiKey.trim()) { + headers.Authorization = `Bearer ${provider.apiKey}`; + } + + const response = await fetch(modelsUrl, { headers }); + if (!response.ok) { + const errorText = await response.text().catch(() => ""); + throw new Error( + errorText || + `HTTP ${response.status}: ${response.statusText}`, + ); + } + + const data = (await response.json()) as unknown; + const candidates = (() => { + if ( + typeof data === "object" && + data !== null && + "data" in data && + Array.isArray((data as { data?: unknown }).data) + ) { + return (data as { data: unknown[] }).data; + } + if ( + typeof data === "object" && + data !== null && + "models" in data && + Array.isArray((data as { models?: unknown }).models) + ) { + return (data as { models: unknown[] }).models; + } + return []; + })(); + + const modelIds = candidates + .map((m) => { + if (typeof m === "object" && m !== null && "id" in m) { + const value = (m as { id?: unknown }).id; + return typeof value === "string" ? value : undefined; + } + return undefined; + }) + .filter((v): v is string => Boolean(v)); + + const nextProviders = customProviders.map((p) => + p.id === provider.id + ? { + ...p, + models: modelIds.map((id) => ({ + nickname: "", + modelId: id, + })), + } + : p, + ); + await updateCustomProviders(nextProviders); + toast.success("Models fetched", { + description: `Added ${modelIds.length} models.`, + }); + } catch (error) { + console.error("Failed to fetch models:", error); + toast.error("Failed to fetch models", { + description: + error instanceof Error ? error.message : String(error), + }); + } finally { + setFetchingProviderId(null); + } + }; + + const vertexHasCredentials = hasVertexCredentials(vertexAI); + + const upsertCustomProvider = async ( + providerId: string, + updater: (provider: CustomProviderSettings) => CustomProviderSettings, + ) => { + const next = customProviders.map((p) => + p.id === providerId ? updater(p) : p, + ); + await updateCustomProviders(next); + }; + + const deleteCustomProvider = async (providerId: string) => { + await updateCustomProviders( + customProviders.filter((p) => p.id !== providerId), + ); + setScreen({ type: "list" }); + }; + + if (screen.type === "vertex") { + return ( +
+
+ +
+ +
+

Vertex AI

+

+ Create a service account in Google Cloud Console and + paste the credentials below. +

+
+ +
+
+ + + void updateVertex({ + ...vertexAI, + projectId: e.target.value, + }) + } + placeholder="my-gcp-project" + className="font-mono" + /> +
+ +
+ + + void updateVertex({ + ...vertexAI, + location: e.target.value, + }) + } + placeholder="us-central1 (or global)" + className="font-mono" + /> +
+ +
+ + + void updateVertex({ + ...vertexAI, + serviceAccountClientEmail: e.target.value, + }) + } + placeholder="my-sa@my-gcp-project.iam.gserviceaccount.com" + className="font-mono" + /> +
+ +
+ +