diff --git a/MISTRAL_PLAN.md b/MISTRAL_PLAN.md index ca69d07..ecb00d4 100644 --- a/MISTRAL_PLAN.md +++ b/MISTRAL_PLAN.md @@ -57,25 +57,27 @@ bDS currently routes all AI chat through the OpenCode Zen gateway (`opencode.ai/ - Extend `ChatReadyStatus` to report per-provider availability, e.g. `providers: { opencode: boolean, mistral: boolean }` - Callers (`Sidebar.tsx`, `sendMessage()`) must gate on the relevant provider, not a single boolean -**F. Add Mistral request path in `sendMessage()`** -- Route `provider === 'mistral'` to new `sendMistralRequest()` method -- Similar to OpenAI path but: - - URL: `MISTRAL_API_URL` (direct, not through OpenCode gateway) - - Auth: `Authorization: Bearer ${this.mistralApiKey}` - - Context budget: per-model (see Target Models table above) - - `tool_choice: "auto"` — Mistral's default; do **not** set `"any"` (which forces a tool call every turn even when the model should respond with text). Omit `tool_choice` entirely, since `"auto"` is the default, matching existing OpenCode behavior - - `parallel_tool_calls: false` — set explicitly; our tool executor runs tools sequentially, so parallel tool calls would break the execution loop +**F. Parameterize `sendOpenAIMessage()` for Mistral (no separate method)** +- Mistral uses the identical OpenAI-compatible chat/completions format — creating a separate `sendMistralRequest()` would be a near-duplicate +- Instead, parameterize `sendOpenAIMessage()` to accept URL, API key, and provider-specific options: + - Add params: `apiUrl: string`, `apiKey: string`, `providerOptions?: { parallelToolCalls?: boolean }` + - `sendMessage()` determines provider via `detectProvider()` and calls `sendOpenAIMessage()` with the correct URL/key/options + - For OpenCode OpenAI path: URL = `ZEN_OPENAI_URL`, key = `this.apiKey` + - For Mistral: URL = `MISTRAL_API_URL`, key = `this.mistralApiKey`, `parallelToolCalls: false` +- `tool_choice`: omit entirely for all OpenAI-compatible providers (default `"auto"` is correct) +- `parallel_tool_calls: false` — set explicitly for Mistral only; our tool executor runs tools sequentially, so parallel tool calls would break the execution loop **F2. Add `MODEL_CONTEXT_BUDGETS` map** - New constant map `MODEL_CONTEXT_BUDGETS: Record` with per-model token budgets - `truncateToTokenBudget()` (L1654) currently defaults to `maxContextTokens = 150000` - In `sendAnthropicMessage()` and `sendOpenAIMessage()`: pass the model's context budget from the map (defaulting to 150,000 for OpenCode models) -- In `sendMistralRequest()`: look up `MODEL_CONTEXT_BUDGETS[modelId]` and pass to truncation +- The parameterized `sendOpenAIMessage()` looks up `MODEL_CONTEXT_BUDGETS[modelId]` for Mistral models and passes to truncation - Values from Target Models table (35k, 120k, 240k) **G. Fix tool-call message history in OpenAI-compatible path** -- `sendOpenAIMessage()` currently only keeps `user`/`assistant` messages in history, discarding `tool` role messages -- Tool results must be persisted in conversation history so follow-up rounds have context +- Within a single `sendMessage()` call, the tool loop correctly tracks tool results across rounds +- However, `tool` role messages are not persisted to DB-backed conversation history — on conversation resume, the model loses context about prior tool results +- Ensure `tool` role messages are included when persisting conversation history so cross-session continuity works - This affects all OpenAI-compatible providers (OpenCode OpenAI path + Mistral) **H. Fix vision in OpenAI-compatible path (affects Mistral too)** @@ -84,10 +86,12 @@ bDS currently routes all AI chat through the OpenCode Zen gateway (`opencode.ai/ `{ type: 'image_url', image_url: { url: 'data:image/webp;base64,...' } }` - This fixes vision for all OpenAI-compatible providers, not just Mistral -**I. Update `getAvailableModels()`** -- When Mistral key is set, include Mistral models in returned list -- Add `provider` field to model entries so UI can group them -- Invalidate `cachedModels`/`cachedModelsAt` when Mistral key is added or removed +**I. Update `getAvailableModels()` — merge from both providers** +- **Model list merge strategy**: fetch models from each configured provider's API endpoint and merge into a single list. When both keys are configured, return models from both; when only one key is set, return only that provider's models; when no key is set, return an empty list (UI disables the dropdown) +- OpenCode models: fetched from existing OpenCode API (as today) +- Mistral models: fetched from `GET https://api.mistral.ai/v1/models` when Mistral key is set; cross-reference returned IDs with `MODEL_DISPLAY_NAMES` to use display names + static `vision`/`contextBudget` metadata +- Every model entry carries `provider: 'opencode' | 'mistral'` so the UI and engine can resolve the correct API URL + key +- Invalidate `cachedModels`/`cachedModelsAt` when any provider key is added or removed **J. Update `generateConversationTitle()` — make configurable in Preferences** - Currently hardcoded to `claude-haiku-4-5` via `ZEN_ANTHROPIC_URL` with OpenCode key @@ -107,9 +111,15 @@ bDS currently routes all AI chat through the OpenCode Zen gateway (`opencode.ai/ **L. Update `analyzeTaxonomy()`** - Currently uses `this.apiKey` (OpenCode) for both Anthropic and OpenAI paths +- Has an early-return guard `if (!this.apiKey)` that must become provider-aware — check Mistral key when provider is Mistral - When a Mistral model is selected: use Mistral API key + `MISTRAL_API_URL` - Must branch on provider to select correct key and URL +**L2. Update `analyzeMediaImage()` API key guard** +- Same issue: has `if (!this.apiKey)` early-return guard +- Must become provider-aware — check the relevant provider's key based on the selected image analysis model +- When routed to Mistral: check `this.mistralApiKey` instead of `this.apiKey` + **M. Convert chat HTTP calls to SSE streaming** Currently `httpRequest()` buffers the entire response body before any text reaches the UI. Users wait 5–30s per API round with only a bouncing-dots indicator. All three providers (Anthropic, OpenAI, Mistral) support `stream: true` with SSE. @@ -185,7 +195,7 @@ data: {"type":"message_stop"} **M4. Request body changes** - `sendAnthropicMessage()`: add `"stream": true` to request body -- `sendOpenAIMessage()` / `sendMistralRequest()`: add `"stream": true` and `"stream_options": { "include_usage": true }` to request body — this is **required** to receive token usage in streaming mode (without it, usage is omitted from streamed responses) +- `sendOpenAIMessage()` (used for both OpenCode OpenAI and Mistral): add `"stream": true` and `"stream_options": { "include_usage": true }` to request body — this is **required** to receive token usage in streaming mode (without it, usage is omitted from streamed responses) **M5. Tool call accumulation during streaming** - Tool call arguments arrive as partial JSON fragments across many SSE events @@ -217,9 +227,8 @@ data: {"type":"message_stop"} ### 2. `src/main/engine/ChatEngine.ts` - Settings persistence **A. Add Mistral key helpers** -- `getMistralApiKey()` - read from settings table -- `setMistralApiKey(key)` - persist to settings table -- Settings key: `'mistral_api_key'` +- Use existing generic `getSetting()`/`setSetting()` with key `'mistral_api_key'` — no dedicated methods needed, avoids unnecessary boilerplate +- ChatEngine already exposes generic helpers for reading/writing the settings table **B. Default model is user-driven** - `getSelectedModel()` defaults to `'claude-sonnet-4-5'` @@ -284,8 +293,10 @@ data: {"type":"message_stop"} - Same pattern: masked display, change button, validation on save **B. Update model selector** -- Group models by provider in dropdown (optgroup: "OpenCode Zen", "Mistral AI") -- Show provider badge next to selected model +- SettingsView uses a native `` dropdown +- When both keys configured, show merged list from both providers; when only one key set, show only that provider's models +- **Note**: `availableModels` state is currently typed as `{id: string; name: string}[]` — must be updated to `ChatModel[]` (which includes `provider` and `vision` fields) so provider grouping and vision filtering work **C. Add per-purpose model preferences** - "Title generation model" dropdown — select cheapest/fastest model for auto-titling conversations @@ -296,8 +307,9 @@ data: {"type":"message_stop"} ### 6. `src/renderer/components/ChatPanel/ChatPanel.tsx` - Chat UI **A. Update model selector in chat** -- Group by provider in dropdown -- Only show models for configured providers +- ChatPanel uses a custom dropdown (CSS `model-dropdown` with `