diff --git a/docs/trace-request-params-feature.md b/docs/trace-request-params-feature.md new file mode 100644 index 000000000..b926d0eab --- /dev/null +++ b/docs/trace-request-params-feature.md @@ -0,0 +1,357 @@ +# Trace Request Parameters Feature + +## Overview +This feature adds a development-mode debugging tool that allows developers to inspect the actual request parameters sent to LLM providers for any assistant message. This helps understand data flow, verify prompt construction, and debug provider-specific formatting issues. + +## Architecture + +### Components + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Renderer Process │ +│ │ +│ ┌────────────────┐ ┌──────────────────┐ ┌─────────────┐ │ +│ │ MessageToolbar │→ │MessageItemAssist │→ │ MessageList │ │ +│ │ (Trace Btn) │ │ (Event) │ │ (Handler) │ │ +│ └────────────────┘ └──────────────────┘ └──────┬──────┘ │ +│ │ │ +│ ┌─────────▼──────┐ │ +│ │ TraceDialog │ │ +│ │ (UI Display) │ │ +│ └────────┬───────┘ │ +└──────────────────────────────────────────────────┼─────────┘ + │ IPC + ┌────────────────────────▼─────────────┐ + │ Main Process │ + │ │ + │ ┌───────────────────────────────┐ │ + │ │ ThreadPresenter │ │ + │ │ getMessageRequestPreview() │ │ + │ └──────────┬─────────────────────┘ │ + │ │ │ + │ ┌────────▼──────────┐ │ + │ │ LLMProviderPresenter│ │ + │ │ getProvider() │ │ + │ └────────┬───────────┘ │ + │ │ │ + │ ┌────────▼───────────┐ │ + │ │ BaseLLMProvider │ │ + │ │ getRequestPreview()│ │ + │ └────────┬───────────┘ │ + │ │ │ + │ ┌──────────▼────────────────────┐ │ + │ │ Concrete Provider Impl │ │ + │ │ - OpenAICompatibleProvider │ │ + │ │ - OpenAIResponsesProvider │ │ + │ │ - (23+ child providers) │ │ + │ └──────────┬────────────────────┘ │ + │ │ │ + │ ┌────────▼────────────┐ │ + │ │ Redaction Utility │ │ + │ │ (lib/redact.ts) │ │ + │ └─────────────────────┘ │ + └──────────────────────────────────────┘ +``` + +### Data Flow + +1. **User Action**: User clicks Trace button in MessageToolbar (DEV mode only) +2. **Event Propagation**: + - MessageToolbar emits `trace` event + - MessageItemAssistant catches and re-emits with messageId + - MessageList handles and sets `traceMessageId` +3. **IPC Call**: TraceDialog watches messageId and calls `threadPresenter.getMessageRequestPreview(messageId)` +4. **Main Process**: + - ThreadPresenter retrieves message and conversation from database + - Reconstructs prompt content using `preparePromptContent()` + - Fetches MCP tools from McpPresenter + - Gets model configuration + - Calls provider's `getRequestPreview()` method +5. **Provider Layer**: + - Provider builds request parameters (same logic as actual request) + - Returns `{ endpoint, headers, body }` +6. **Security**: Redact sensitive information using `redactRequestPreview()` +7. **Response**: Return preview data to renderer +8. **UI Display**: TraceDialog renders JSON in a modal with copy functionality + +## Key Files + +### Renderer Process + +- **`src/renderer/src/components/message/MessageToolbar.vue`** + - Adds Trace button (bug icon, visible only in DEV mode for assistant messages) + - Emits `trace` event when clicked + +- **`src/renderer/src/components/message/MessageItemAssistant.vue`** + - Listens to MessageToolbar's `trace` event + - Re-emits with message ID + +- **`src/renderer/src/components/message/MessageList.vue`** + - Manages `traceMessageId` state + - Renders TraceDialog component + - Handles trace event from MessageItemAssistant + +- **`src/renderer/src/components/trace/TraceDialog.vue`** + - Modal dialog for displaying request preview + - Shows provider, model, endpoint, headers, and body + - Provides JSON copy functionality + - Handles loading, error, and "not implemented" states + +### Main Process + +- **`src/main/presenter/threadPresenter/index.ts`** + - `getMessageRequestPreview(messageId)`: Orchestrates preview reconstruction + - Retrieves conversation context and settings + - Reconstructs prompt using `preparePromptContent()` + - Fetches MCP tools + - Calls provider's preview method + - Applies redaction + +- **`src/main/presenter/llmProviderPresenter/baseProvider.ts`** + - Defines abstract `getRequestPreview()` method + - Default implementation throws "not implemented" error + +- **`src/main/presenter/llmProviderPresenter/providers/openAICompatibleProvider.ts`** + - Implements `getRequestPreview()` for OpenAI-compatible providers + - Mirrors `handleChatCompletion()` logic without making actual API call + - Returns endpoint, headers, and body + +- **`src/main/presenter/llmProviderPresenter/providers/openAIResponsesProvider.ts`** + - Implements `getRequestPreview()` for OpenAI Responses API + - Mirrors `handleChatCompletion()` logic + +- **`src/main/lib/redact.ts`** + - `redactRequestPreview()`: Removes sensitive data from preview + - Redacts API keys, tokens, passwords, secrets + - Recursively processes nested objects and arrays + +### Shared Types + +- **`src/shared/provider-operations.ts`** + - `ProviderRequestPreview`: Type definition for preview data structure + - Fields: providerId, modelId, endpoint, headers, body, mayNotMatch, notImplemented + +- **`src/shared/types/presenters/thread.presenter.d.ts`** + - Adds `getMessageRequestPreview(messageId: string): Promise` to IThreadPresenter + +### Internationalization + +- **`src/renderer/src/i18n/[locale]/traceDialog.json`** + - UI strings for TraceDialog (title, labels, error messages) + - Supported locales: zh-CN, en-US + +- **`src/renderer/src/i18n/[locale]/thread.json`** + - Added `toolbar.trace` key for button tooltip + +## Implementation Details + +### Provider Support Status + +#### ✅ Fully Implemented +- `OpenAICompatibleProvider` (base class) +- `OpenAIResponsesProvider` + +#### 🟡 Inherited (23 providers, auto-supported via base class) +All providers extending `OpenAICompatibleProvider` automatically inherit `getRequestPreview()`: +- OpenAIProvider +- DeepseekProvider +- DashscopeProvider +- DoubaoProvider +- GrokProvider +- GroqProvider +- GithubProvider +- MinimaxProvider +- ZhipuProvider +- SiliconcloudProvider +- ModelscopeProvider +- OpenRouterProvider +- PPIOProvider +- TogetherProvider +- TokenFluxProvider +- VercelAIGatewayProvider +- CherryInProvider +- AihubmixProvider +- _302AIProvider +- PoeProvider +- JiekouProvider +- ZenmuxProvider +- LMStudioProvider + +#### ❌ Not Yet Implemented +- `AnthropicProvider` (separate base class) +- `GeminiProvider` (separate base class) +- `AwsBedrockProvider` (separate base class) +- `OllamaProvider` (separate base class) +- `GithubCopilotProvider` (separate implementation) + +### Request Reconstruction Logic + +The `getRequestPreview()` method in each provider: + +1. **Formats Messages**: Uses same `formatMessages()` method as actual requests +2. **Prepares Function Calls**: Applies `prepareFunctionCallPrompt()` for non-FC models +3. **Converts Tools**: Uses `mcpPresenter.mcpToolsToOpenAITools()` for native FC +4. **Builds Request Params**: Constructs exact request object (stream, temperature, max_tokens, etc.) +5. **Applies Model-Specific Logic**: + - Reasoning models (o1, o3, gpt-5): Remove temperature, use max_completion_tokens + - Provider-specific quirks (e.g., OpenRouter Deepseek, Dashscope response_format) +6. **Constructs Headers**: Includes Authorization, Content-Type, and custom headers +7. **Determines Endpoint**: Combines baseUrl with API path + +### Security: Sensitive Data Redaction + +The `redactRequestPreview()` function: + +- **Headers**: Redacts authorization, api_key, x-api-key, token, password +- **Body**: Recursively scans objects/arrays for sensitive keys +- **Sensitive Keys List**: + ```typescript + const SENSITIVE_KEYS = [ + 'api_key', + 'apikey', + 'authorization', + 'x-api-key', + 'accesskeyid', + 'secretaccesskey', + 'password', + 'token' + ] + ``` +- **Redaction Format**: Replaces value with `'********'` +- **Case-Insensitive**: Key matching is case-insensitive + +### UI/UX Design + +#### Trace Button +- **Icon**: `lucide:bug` (bug icon) +- **Visibility**: Only in DEV mode (`import.meta.env.DEV`) +- **Scope**: Only on assistant messages (not user/system) +- **Tooltip**: Localized "Trace Request" / "调试请求参数" + +#### TraceDialog Layout +``` +┌─────────────────────────────────────────────────┐ +│ Request Preview [×] │ +├─────────────────────────────────────────────────┤ +│ ⚠️ Note: This preview may not match actual req │ +├─────────────────────────────────────────────────┤ +│ Provider: openai Model: gpt-4 Endpoint: … │ +├─────────────────────────────────────────────────┤ +│ Request Body [Copy JSON] │ +│ ┌───────────────────────────────────────────┐ │ +│ │ { │ │ +│ │ "messages": [...], │ │ +│ │ "model": "gpt-4", │ │ +│ │ "temperature": 0.7, │ │ +│ │ ... │ │ +│ │ } │ │ +│ └───────────────────────────────────────────┘ │ +│ │ +│ [Close] │ +└─────────────────────────────────────────────────┘ +``` + +#### States +- **Loading**: Shows spinner with "Loading..." message +- **Error**: Shows error icon with retry message +- **Not Implemented**: Shows info icon with "Provider not supported" message +- **Success**: Shows formatted JSON with metadata + +### Error Handling + +1. **Null/Undefined Result**: Show error state +2. **Provider Not Implemented**: Show "not implemented" info state +3. **IPC Failure**: Caught in try-catch, shows error state +4. **Parse Error**: Logged to console, shows error state +5. **Copy Failure**: Logged to console, toast notification + +## Usage + +### Developer Workflow + +1. Run app in dev mode: `pnpm run dev` +2. Start a conversation with an LLM +3. Click the bug icon (🐛) on any assistant message +4. View the reconstructed request parameters +5. Copy JSON for debugging/testing + +### Use Cases + +- **Debugging**: Verify prompt construction and tool definitions +- **Provider Comparison**: Compare request formats across providers +- **Tool Calling**: Inspect how tools are encoded (native vs. mock) +- **Model Quirks**: Understand provider-specific parameter handling +- **Context Analysis**: Verify which messages are included in context + +## Testing + +### Manual Testing Checklist +- [x] Trace button only appears in DEV mode +- [x] Trace button only appears on assistant messages +- [x] Dialog opens when clicking Trace button +- [x] Loading state displays correctly +- [x] Error state displays for invalid messages +- [x] "Not implemented" state displays for unsupported providers +- [x] Preview displays correctly for OpenAI-compatible providers +- [x] Sensitive data is redacted (API keys, tokens) +- [x] JSON copy functionality works +- [x] Dialog closes correctly +- [ ] All 23+ child providers inherit preview functionality + +### Automated Testing (TODO) +- [ ] Unit tests for `redactRequestPreview()` (tests-main) +- [ ] Unit tests for TraceDialog component (tests-renderer) +- [ ] Unit tests for provider `getRequestPreview()` methods + +## Future Enhancements + +1. **Extend to More Providers**: + - Implement `getRequestPreview()` for AnthropicProvider + - Implement for GeminiProvider + - Implement for AwsBedrockProvider + - Implement for OllamaProvider + +2. **Enhanced UI**: + - Syntax highlighting for JSON + - Collapsible sections (headers/body) + - Diff view comparing multiple requests + - Export to file + +3. **Advanced Features**: + - Historical request archive + - Request replay/resend + - Token counting preview + - Cost estimation + +4. **Production Use**: + - Optional logging to file (with user consent) + - Telemetry for provider debugging + - Request/response matching + +## Known Issues + +1. **Reconstruction Limitations**: + - Preview is reconstructed from current DB state + - May not exactly match original request if: + - Conversation settings changed + - MCP tools updated + - Provider configuration changed + - Warning displayed to user + +2. **Provider Coverage**: + - Only OpenAI-compatible providers fully supported + - Other provider types show "not implemented" + +3. **Performance**: + - Preview reconstruction involves DB queries + - May be slow for large conversations + - No caching implemented + +## References + +- [IPC Architecture](./ipc/ipc-architecture-complete.md) +- [Provider Architecture](./provider-optimization-summary.md) +- [MCP Tool System](./mcp-architecture.md) +- [Prompt Builder](../src/main/presenter/threadPresenter/promptBuilder.ts) + diff --git a/resources/model-db/providers.json b/resources/model-db/providers.json index 9e1e8bebc..2a3cd8f37 100644 --- a/resources/model-db/providers.json +++ b/resources/model-db/providers.json @@ -3845,6 +3845,38 @@ "output": 0 } }, + { + "id": "minimaxai/minimax-m2", + "name": "MiniMax-M2", + "display_name": "MiniMax-M2", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "limit": { + "context": 128000, + "output": 16384 + }, + "temperature": true, + "tool_call": true, + "reasoning": { + "supported": true, + "default": true + }, + "attachment": false, + "open_weights": true, + "knowledge": "2024-07", + "release_date": "2025-10-27", + "last_updated": "2025-10-31", + "cost": { + "input": 0, + "output": 0 + } + }, { "id": "google/gemma-3-27b-it", "name": "Gemma-3-27B-IT", @@ -23038,6 +23070,547 @@ } ] }, + "iflowcn": { + "id": "iflowcn", + "name": "iFlow", + "display_name": "iFlow", + "api": "https://apis.iflow.cn/v1", + "doc": "https://platform.iflow.cn/en/docs", + "models": [ + { + "id": "qwen3-coder", + "name": "Qwen3-Coder-480B-A35B", + "display_name": "Qwen3-Coder-480B-A35B", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "limit": { + "context": 256000, + "output": 64000 + }, + "temperature": true, + "tool_call": true, + "reasoning": { + "supported": false + }, + "attachment": false, + "open_weights": true, + "knowledge": "2025-04", + "release_date": "2025-07-01", + "last_updated": "2025-07-01", + "cost": { + "input": 0, + "output": 0 + } + }, + { + "id": "deepseek-v3", + "name": "DeepSeek-V3-671B", + "display_name": "DeepSeek-V3-671B", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "limit": { + "context": 128000, + "output": 32000 + }, + "temperature": true, + "tool_call": true, + "reasoning": { + "supported": false + }, + "attachment": false, + "open_weights": true, + "knowledge": "2024-10", + "release_date": "2024-12-26", + "last_updated": "2024-12-26", + "cost": { + "input": 0, + "output": 0 + } + }, + { + "id": "kimi-k2", + "name": "Kimi-K2", + "display_name": "Kimi-K2", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "limit": { + "context": 128000, + "output": 64000 + }, + "temperature": true, + "tool_call": true, + "reasoning": { + "supported": false + }, + "attachment": false, + "open_weights": false, + "knowledge": "2024-10", + "release_date": "2024-12-01", + "last_updated": "2024-12-01", + "cost": { + "input": 0, + "output": 0 + } + }, + { + "id": "deepseek-r1", + "name": "DeepSeek-R1", + "display_name": "DeepSeek-R1", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "limit": { + "context": 128000, + "output": 32000 + }, + "temperature": true, + "tool_call": true, + "reasoning": { + "supported": true, + "default": true + }, + "attachment": false, + "open_weights": true, + "knowledge": "2024-12", + "release_date": "2025-01-20", + "last_updated": "2025-01-20", + "cost": { + "input": 0, + "output": 0 + } + }, + { + "id": "deepseek-v3.1", + "name": "DeepSeek-V3.1-Terminus", + "display_name": "DeepSeek-V3.1-Terminus", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "limit": { + "context": 128000, + "output": 64000 + }, + "temperature": true, + "tool_call": true, + "reasoning": { + "supported": true, + "default": true + }, + "attachment": false, + "open_weights": true, + "knowledge": "2024-12", + "release_date": "2025-01-01", + "last_updated": "2025-01-01", + "cost": { + "input": 0, + "output": 0 + } + }, + { + "id": "qwen3-235b", + "name": "Qwen3-235B-A22B", + "display_name": "Qwen3-235B-A22B", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "limit": { + "context": 128000, + "output": 32000 + }, + "temperature": true, + "tool_call": true, + "reasoning": { + "supported": true, + "default": true + }, + "attachment": false, + "open_weights": true, + "knowledge": "2024-10", + "release_date": "2024-12-01", + "last_updated": "2024-12-01", + "cost": { + "input": 0, + "output": 0 + } + }, + { + "id": "kimi-k2-0905", + "name": "Kimi-K2-Instruct-0905", + "display_name": "Kimi-K2-Instruct-0905", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "limit": { + "context": 256000, + "output": 64000 + }, + "temperature": true, + "tool_call": true, + "reasoning": { + "supported": false + }, + "attachment": false, + "open_weights": false, + "knowledge": "2024-12", + "release_date": "2025-09-05", + "last_updated": "2025-09-05", + "cost": { + "input": 0, + "output": 0 + } + }, + { + "id": "qwen3-235b-a22b-thinking-2507", + "name": "Qwen3-235B-A22B-Thinking", + "display_name": "Qwen3-235B-A22B-Thinking", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "limit": { + "context": 256000, + "output": 64000 + }, + "temperature": true, + "tool_call": true, + "reasoning": { + "supported": true, + "default": true + }, + "attachment": false, + "open_weights": true, + "knowledge": "2025-04", + "release_date": "2025-07-01", + "last_updated": "2025-07-01", + "cost": { + "input": 0, + "output": 0 + } + }, + { + "id": "qwen3-vl-plus", + "name": "Qwen3-VL-Plus", + "display_name": "Qwen3-VL-Plus", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "limit": { + "context": 256000, + "output": 32000 + }, + "temperature": true, + "tool_call": true, + "reasoning": { + "supported": false + }, + "attachment": true, + "open_weights": false, + "knowledge": "2024-12", + "release_date": "2025-01-01", + "last_updated": "2025-01-01", + "cost": { + "input": 0, + "output": 0 + } + }, + { + "id": "glm-4.6", + "name": "GLM-4.6", + "display_name": "GLM-4.6", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "limit": { + "context": 200000, + "output": 128000 + }, + "temperature": true, + "tool_call": true, + "reasoning": { + "supported": false + }, + "attachment": false, + "open_weights": false, + "knowledge": "2024-10", + "release_date": "2024-12-01", + "last_updated": "2024-12-01", + "cost": { + "input": 0, + "output": 0 + } + }, + { + "id": "tstars2.0", + "name": "TStars-2.0", + "display_name": "TStars-2.0", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "limit": { + "context": 128000, + "output": 64000 + }, + "temperature": true, + "tool_call": true, + "reasoning": { + "supported": false + }, + "attachment": false, + "open_weights": false, + "knowledge": "2024-01", + "release_date": "2024-01-01", + "last_updated": "2025-01-01", + "cost": { + "input": 0, + "output": 0 + } + }, + { + "id": "qwen3-235b-a22b-instruct", + "name": "Qwen3-235B-A22B-Instruct", + "display_name": "Qwen3-235B-A22B-Instruct", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "limit": { + "context": 256000, + "output": 64000 + }, + "temperature": true, + "tool_call": true, + "reasoning": { + "supported": false + }, + "attachment": false, + "open_weights": true, + "knowledge": "2025-04", + "release_date": "2025-07-01", + "last_updated": "2025-07-01", + "cost": { + "input": 0, + "output": 0 + } + }, + { + "id": "qwen3-max", + "name": "Qwen3-Max", + "display_name": "Qwen3-Max", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "limit": { + "context": 256000, + "output": 32000 + }, + "temperature": true, + "tool_call": true, + "reasoning": { + "supported": false + }, + "attachment": false, + "open_weights": false, + "knowledge": "2024-12", + "release_date": "2025-01-01", + "last_updated": "2025-01-01", + "cost": { + "input": 0, + "output": 0 + } + }, + { + "id": "deepseek-v3.2", + "name": "DeepSeek-V3.2-Exp", + "display_name": "DeepSeek-V3.2-Exp", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "limit": { + "context": 128000, + "output": 64000 + }, + "temperature": true, + "tool_call": true, + "reasoning": { + "supported": false + }, + "attachment": false, + "open_weights": true, + "knowledge": "2024-12", + "release_date": "2025-01-01", + "last_updated": "2025-01-01", + "cost": { + "input": 0, + "output": 0 + } + }, + { + "id": "qwen3-max-preview", + "name": "Qwen3-Max-Preview", + "display_name": "Qwen3-Max-Preview", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "limit": { + "context": 256000, + "output": 32000 + }, + "temperature": true, + "tool_call": true, + "reasoning": { + "supported": false + }, + "attachment": false, + "open_weights": false, + "knowledge": "2024-12", + "release_date": "2025-01-01", + "last_updated": "2025-01-01", + "cost": { + "input": 0, + "output": 0 + } + }, + { + "id": "qwen3-coder-plus", + "name": "Qwen3-Coder-Plus", + "display_name": "Qwen3-Coder-Plus", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "limit": { + "context": 256000, + "output": 64000 + }, + "temperature": true, + "tool_call": true, + "reasoning": { + "supported": false + }, + "attachment": false, + "open_weights": true, + "knowledge": "2025-04", + "release_date": "2025-07-01", + "last_updated": "2025-07-01", + "cost": { + "input": 0, + "output": 0 + } + }, + { + "id": "qwen3-32b", + "name": "Qwen3-32B", + "display_name": "Qwen3-32B", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "limit": { + "context": 128000, + "output": 32000 + }, + "temperature": true, + "tool_call": true, + "reasoning": { + "supported": false + }, + "attachment": false, + "open_weights": true, + "knowledge": "2024-10", + "release_date": "2024-12-01", + "last_updated": "2024-12-01", + "cost": { + "input": 0, + "output": 0 + } + } + ] + }, "synthetic": { "id": "synthetic", "name": "Synthetic", @@ -25248,6 +25821,15 @@ "supported": false } }, + { + "id": "cc-glm-4.6", + "name": "cc-glm-4.6", + "display_name": "cc-glm-4.6", + "tool_call": false, + "reasoning": { + "supported": false + } + }, { "id": "chatglm_lite", "name": "chatglm_lite", @@ -30582,6 +31164,27 @@ "supported": false } }, + { + "id": "veo-3.1-generate-preview", + "name": "veo-3.1-generate-preview", + "display_name": "veo-3.1-generate-preview", + "modalities": { + "input": [ + "text", + "image", + "video" + ], + "output": [ + "text", + "image", + "video" + ] + }, + "tool_call": false, + "reasoning": { + "supported": false + } + }, { "id": "veo3", "name": "veo3", @@ -30810,6 +31413,38 @@ "output": 1.68 } }, + { + "id": "accounts/fireworks/models/minimax-m2", + "name": "MiniMax-M2", + "display_name": "MiniMax-M2", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "limit": { + "context": 128000, + "output": 16384 + }, + "temperature": true, + "tool_call": true, + "reasoning": { + "supported": true, + "default": true + }, + "attachment": false, + "open_weights": true, + "knowledge": "2024-11", + "release_date": "2025-10-27", + "last_updated": "2025-10-27", + "cost": { + "input": 0.3, + "output": 1.2 + } + }, { "id": "accounts/fireworks/models/deepseek-v3-0324", "name": "Deepseek V3 03-24", @@ -33099,6 +33734,38 @@ "output": 1.2 } }, + { + "id": "zai-glm-4.6", + "name": "Z.AI GLM-4.6", + "display_name": "Z.AI GLM-4.6", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "limit": { + "context": 131072, + "output": 40960 + }, + "temperature": true, + "tool_call": true, + "reasoning": { + "supported": false + }, + "attachment": false, + "open_weights": true, + "release_date": "2025-11-05", + "last_updated": "2025-11-05", + "cost": { + "input": 0, + "output": 0, + "cache_read": 0, + "cache_write": 0 + } + }, { "id": "qwen-3-coder-480b", "name": "Qwen 3 Coder 480B", @@ -64602,15 +65269,6 @@ "supported": false } }, - { - "id": "openrouter/andromeda-alpha", - "name": "Andromeda Alpha", - "display_name": "Andromeda Alpha", - "tool_call": false, - "reasoning": { - "supported": false - } - }, { "id": "anthropic/claude-3-haiku", "name": "Anthropic: Claude 3 Haiku", @@ -65619,6 +66277,15 @@ "supported": false } }, + { + "id": "minimax/minimax-m2", + "name": "MiniMax: MiniMax M2", + "display_name": "MiniMax: MiniMax M2", + "tool_call": false, + "reasoning": { + "supported": false + } + }, { "id": "minimax/minimax-01", "name": "MiniMax: MiniMax-01", @@ -65898,6 +66565,15 @@ "supported": false } }, + { + "id": "mistralai/voxtral-small-24b-2507", + "name": "Mistral: Voxtral Small 24B 2507", + "display_name": "Mistral: Voxtral Small 24B 2507", + "tool_call": false, + "reasoning": { + "supported": false + } + }, { "id": "moonshotai/kimi-dev-72b", "name": "MoonshotAI: Kimi Dev 72B", @@ -66069,6 +66745,15 @@ "supported": false } }, + { + "id": "nvidia/nemotron-nano-12b-v2-vl", + "name": "NVIDIA: Nemotron Nano 12B 2 VL", + "display_name": "NVIDIA: Nemotron Nano 12B 2 VL", + "tool_call": false, + "reasoning": { + "supported": false + } + }, { "id": "nvidia/nemotron-nano-9b-v2", "name": "NVIDIA: Nemotron Nano 9B V2", @@ -66393,6 +67078,15 @@ "supported": false } }, + { + "id": "openai/gpt-oss-safeguard-20b", + "name": "OpenAI: gpt-oss-safeguard-20b", + "display_name": "OpenAI: gpt-oss-safeguard-20b", + "tool_call": false, + "reasoning": { + "supported": false + } + }, { "id": "openai/o1", "name": "OpenAI: o1", @@ -66492,6 +67186,15 @@ "supported": false } }, + { + "id": "openai/text-embedding-3-large", + "name": "OpenAI: Text Embedding 3 Large", + "display_name": "OpenAI: Text Embedding 3 Large", + "tool_call": false, + "reasoning": { + "supported": false + } + }, { "id": "opengvlab/internvl3-78b", "name": "OpenGVLab: InternVL3 78B", @@ -66528,6 +67231,15 @@ "supported": false } }, + { + "id": "perplexity/sonar-pro-search", + "name": "Perplexity: Sonar Pro Search", + "display_name": "Perplexity: Sonar Pro Search", + "tool_call": false, + "reasoning": { + "supported": false + } + }, { "id": "perplexity/sonar-reasoning", "name": "Perplexity: Sonar Reasoning", @@ -67554,6 +68266,28 @@ "supported": false } }, + { + "id": "amazon/nova-premier-v1", + "name": "Amazon: Nova Premier 1.0", + "display_name": "Amazon: Nova Premier 1.0", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "limit": { + "context": 1000000, + "output": 32000 + }, + "tool_call": false, + "reasoning": { + "supported": false + } + }, { "id": "amazon/nova-pro-v1", "name": "Amazon: Nova Pro 1.0", @@ -68221,47 +68955,6 @@ "supported": false } }, - { - "id": "cognitivecomputations/dolphin3.0-mistral-24b", - "name": "Dolphin3.0 Mistral 24B", - "display_name": "Dolphin3.0 Mistral 24B", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "limit": { - "context": 32768, - "output": 32768 - }, - "tool_call": false, - "reasoning": { - "supported": false - } - }, - { - "id": "cognitivecomputations/dolphin3.0-mistral-24b:free", - "name": "Dolphin3.0 Mistral 24B (free)", - "display_name": "Dolphin3.0 Mistral 24B (free)", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "limit": { - "context": 32768 - }, - "tool_call": false, - "reasoning": { - "supported": false - } - }, { "id": "cohere/command-a", "name": "Cohere: Command A", @@ -69208,29 +69901,7 @@ ] }, "limit": { - "context": 8192, - "output": 8192 - }, - "tool_call": false, - "reasoning": { - "supported": false - } - }, - { - "id": "google/gemma-2-9b-it:free", - "name": "Google: Gemma 2 9B (free)", - "display_name": "Google: Gemma 2 9B (free)", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "limit": { - "context": 8192, - "output": 8192 + "context": 8192 }, "tool_call": false, "reasoning": { @@ -70517,29 +71188,7 @@ ] }, "limit": { - "context": 131072, - "output": 131072 - }, - "temperature": true, - "tool_call": true, - "reasoning": { - "supported": false - } - }, - { - "id": "mistralai/devstral-small-2505:free", - "name": "Mistral: Devstral Small 2505 (free)", - "display_name": "Mistral: Devstral Small 2505 (free)", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "limit": { - "context": 32768 + "context": 128000 }, "temperature": true, "tool_call": true, @@ -71201,31 +71850,31 @@ } }, { - "id": "moonshotai/kimi-dev-72b", - "name": "MoonshotAI: Kimi Dev 72B", - "display_name": "MoonshotAI: Kimi Dev 72B", + "id": "mistralai/voxtral-small-24b-2507", + "name": "Mistral: Voxtral Small 24B 2507", + "display_name": "Mistral: Voxtral Small 24B 2507", "modalities": { "input": [ - "text" + "text", + "audio" ], "output": [ "text" ] }, "limit": { - "context": 131072, - "output": 131072 + "context": 32000 }, - "tool_call": false, + "temperature": true, + "tool_call": true, "reasoning": { - "supported": true, - "default": true + "supported": false } }, { - "id": "moonshotai/kimi-dev-72b:free", - "name": "MoonshotAI: Kimi Dev 72B (free)", - "display_name": "MoonshotAI: Kimi Dev 72B (free)", + "id": "moonshotai/kimi-dev-72b", + "name": "MoonshotAI: Kimi Dev 72B", + "display_name": "MoonshotAI: Kimi Dev 72B", "modalities": { "input": [ "text" @@ -71235,7 +71884,8 @@ ] }, "limit": { - "context": 131072 + "context": 131072, + "output": 131072 }, "tool_call": false, "reasoning": { @@ -71407,47 +72057,6 @@ "supported": false } }, - { - "id": "nousresearch/deephermes-3-llama-3-8b-preview", - "name": "Nous: DeepHermes 3 Llama 3 8B Preview", - "display_name": "Nous: DeepHermes 3 Llama 3 8B Preview", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "limit": { - "context": 131072, - "output": 131072 - }, - "tool_call": true, - "reasoning": { - "supported": false - } - }, - { - "id": "nousresearch/deephermes-3-llama-3-8b-preview:free", - "name": "Nous: DeepHermes 3 Llama 3 8B Preview (free)", - "display_name": "Nous: DeepHermes 3 Llama 3 8B Preview (free)", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "limit": { - "context": 131072 - }, - "tool_call": false, - "reasoning": { - "supported": false - } - }, { "id": "nousresearch/deephermes-3-mistral-24b-preview", "name": "Nous: DeepHermes 3 Mistral 24B Preview", @@ -72823,6 +73432,27 @@ }, "attachment": true }, + { + "id": "openai/text-embedding-3-large", + "name": "OpenAI: Text Embedding 3 Large", + "display_name": "OpenAI: Text Embedding 3 Large", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text", + "embeddings" + ] + }, + "limit": { + "context": 8192 + }, + "tool_call": false, + "reasoning": { + "supported": false + } + }, { "id": "opengvlab/internvl3-78b", "name": "OpenGVLab: InternVL3 78B", @@ -72926,6 +73556,29 @@ "supported": false } }, + { + "id": "perplexity/sonar-pro-search", + "name": "Perplexity: Sonar Pro Search", + "display_name": "Perplexity: Sonar Pro Search", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "limit": { + "context": 200000, + "output": 8000 + }, + "tool_call": false, + "reasoning": { + "supported": true, + "default": true + } + }, { "id": "perplexity/sonar-reasoning", "name": "Perplexity: Sonar Reasoning", @@ -73514,7 +74167,7 @@ }, "limit": { "context": 262144, - "output": 262144 + "output": 131072 }, "tool_call": true, "reasoning": { @@ -73608,28 +74261,6 @@ "default": true } }, - { - "id": "qwen/qwen3-8b:free", - "name": "Qwen: Qwen3 8B (free)", - "display_name": "Qwen: Qwen3 8B (free)", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "limit": { - "context": 40960, - "output": 40960 - }, - "tool_call": false, - "reasoning": { - "supported": true, - "default": true - } - }, { "id": "qwen/qwen3-coder", "name": "Qwen: Qwen3 Coder 480B A35B", @@ -73774,6 +74405,7 @@ "context": 256000, "output": 32768 }, + "temperature": true, "tool_call": true, "reasoning": { "supported": false @@ -74382,28 +75014,6 @@ "default": true } }, - { - "id": "thudm/glm-z1-32b", - "name": "THUDM: GLM Z1 32B", - "display_name": "THUDM: GLM Z1 32B", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "limit": { - "context": 32768, - "output": 32768 - }, - "tool_call": false, - "reasoning": { - "supported": true, - "default": true - } - }, { "id": "tngtech/deepseek-r1t-chimera", "name": "TNG: DeepSeek R1T Chimera", @@ -75068,6 +75678,23 @@ "supported": false } }, + { + "id": "doubao-1.5-pro-32k-character-250715", + "name": "doubao-1.5-pro-32k-character-250715", + "display_name": "doubao-1.5-pro-32k-character-250715", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "tool_call": true, + "reasoning": { + "supported": false + } + }, { "id": "baidu/ernie-4.5-300b-a47b-paddle", "name": "ERNIE 4.5 300B A47B", diff --git a/src/main/events.ts b/src/main/events.ts index f58649c1e..2b99d3289 100644 --- a/src/main/events.ts +++ b/src/main/events.ts @@ -23,6 +23,7 @@ export const CONFIG_EVENTS = { CONTENT_PROTECTION_CHANGED: 'config:content-protection-changed', SOUND_ENABLED_CHANGED: 'config:sound-enabled-changed', // 新增:声音开关变更事件 COPY_WITH_COT_CHANGED: 'config:copy-with-cot-enabled-changed', + TRACE_DEBUG_CHANGED: 'config:trace-debug-changed', // Trace 调试功能开关变更事件 PROXY_RESOLVED: 'config:proxy-resolved', LANGUAGE_CHANGED: 'config:language-changed', // 新增:语言变更事件 // 模型配置相关事件 diff --git a/src/main/lib/redact.ts b/src/main/lib/redact.ts new file mode 100644 index 000000000..37c7618de --- /dev/null +++ b/src/main/lib/redact.ts @@ -0,0 +1,138 @@ +/** + * Redaction utilities for sensitive information in request preview + */ + +/** + * Sensitive header keys that should be redacted + */ +const SENSITIVE_HEADER_KEYS = [ + 'authorization', + 'api-key', + 'x-api-key', + 'apikey', + 'bearer', + 'token', + 'secret', + 'password', + 'credential', + 'auth' +] + +/** + * Sensitive body keys that should be redacted + * Note: We use exact match to avoid filtering legitimate keys like 'max_tokens' + */ +const SENSITIVE_BODY_KEYS = ['api_key', 'apiKey', 'apikey', 'secret', 'password', 'token'] + +/** + * Body keys that should never be redacted (even if they contain sensitive keywords) + */ +const ALLOWED_BODY_KEYS = [ + 'max_tokens', + 'max_completion_tokens', + 'max_output_tokens', + 'temperature', + 'stream', + 'model', + 'messages', + 'tools' +] + +/** + * Redact sensitive values in headers + * @param headers Original headers + * @returns Redacted headers + */ +export function redactHeaders(headers: Record): Record { + const redacted: Record = {} + + for (const [key, value] of Object.entries(headers)) { + const keyLower = key.toLowerCase() + const shouldRedact = SENSITIVE_HEADER_KEYS.some((sensitiveKey) => + keyLower.includes(sensitiveKey) + ) + + if (shouldRedact) { + redacted[key] = '***REDACTED***' + } else { + redacted[key] = value + } + } + + return redacted +} + +/** + * Redact sensitive values in request body + * @param body Original body + * @returns Redacted body + */ +export function redactBody(body: unknown): unknown { + if (body === null || body === undefined) { + return body + } + + if (Array.isArray(body)) { + return body.map((item) => redactBody(item)) + } + + if (typeof body === 'object') { + const redacted: Record = {} + + for (const [key, value] of Object.entries(body)) { + // Skip redaction for allowed keys (like max_tokens, max_completion_tokens, etc.) + if (ALLOWED_BODY_KEYS.includes(key)) { + if (typeof value === 'object' && value !== null) { + redacted[key] = redactBody(value) + } else { + redacted[key] = value + } + continue + } + + // Check if key matches sensitive patterns (exact match or ends with sensitive keyword) + const keyLower = key.toLowerCase() + const shouldRedact = SENSITIVE_BODY_KEYS.some((sensitiveKey) => { + const sensitiveKeyLower = sensitiveKey.toLowerCase() + // Exact match + if (keyLower === sensitiveKeyLower) { + return true + } + // Key ends with sensitive keyword (e.g., 'api_token', 'access_token') + // But exclude keys that contain allowed patterns (e.g., 'max_tokens') + if (keyLower.endsWith(`_${sensitiveKeyLower}`) || keyLower.endsWith(sensitiveKeyLower)) { + // Double check: make sure it's not a false positive + return !ALLOWED_BODY_KEYS.some((allowed) => keyLower.includes(allowed.toLowerCase())) + } + return false + }) + + if (shouldRedact) { + redacted[key] = '***REDACTED***' + } else if (typeof value === 'object' && value !== null) { + redacted[key] = redactBody(value) + } else { + redacted[key] = value + } + } + + return redacted + } + + return body +} + +/** + * Redact sensitive information in full request preview + * @param preview Request preview data + * @returns Redacted preview + */ +export function redactRequestPreview(preview: { headers: Record; body: unknown }): { + headers: Record + body: unknown +} { + return { + headers: redactHeaders(preview.headers), + body: redactBody(preview.body) + } +} diff --git a/src/main/presenter/configPresenter/index.ts b/src/main/presenter/configPresenter/index.ts index 2657ee824..950b3ad15 100644 --- a/src/main/presenter/configPresenter/index.ts +++ b/src/main/presenter/configPresenter/index.ts @@ -1061,6 +1061,11 @@ export class ConfigPresenter implements IConfigPresenter { eventBus.sendToRenderer(CONFIG_EVENTS.COPY_WITH_COT_CHANGED, SendTarget.ALL_WINDOWS, enabled) } + setTraceDebugEnabled(enabled: boolean): void { + this.setSetting('traceDebugEnabled', enabled) + eventBus.sendToRenderer(CONFIG_EVENTS.TRACE_DEBUG_CHANGED, SendTarget.ALL_WINDOWS, enabled) + } + // Get floating button switch status getFloatingButtonEnabled(): boolean { const value = this.getSetting('floatingButtonEnabled') ?? false diff --git a/src/main/presenter/llmProviderPresenter/baseProvider.ts b/src/main/presenter/llmProviderPresenter/baseProvider.ts index 0fff123ca..a9b116482 100644 --- a/src/main/presenter/llmProviderPresenter/baseProvider.ts +++ b/src/main/presenter/llmProviderPresenter/baseProvider.ts @@ -649,6 +649,39 @@ ${this.convertToolsToXml(tools)} return null // 默认实现返回 null,表示不支持此功能 } + /** + * Get request preview for debugging (DEV mode only) + * Build the actual request parameters that would be sent to the provider API + * @param messages Conversation messages + * @param modelId Model ID + * @param modelConfig Model configuration + * @param temperature Temperature parameter + * @param maxTokens Max tokens parameter + * @param mcpTools MCP tools definitions + * @returns Preview data including endpoint, headers, and body (all redacted) + */ + public async getRequestPreview( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _messages: ChatMessage[], + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _modelId: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _modelConfig: ModelConfig, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _temperature: number, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _maxTokens: number, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _mcpTools: MCPToolDefinition[] + ): Promise<{ + endpoint: string + headers: Record + body: unknown + }> { + // Default implementation returns not implemented marker + throw new Error('Provider has not implemented getRequestPreview') + } + /** * 将 MCPToolDefinition 转换为 XML 格式 * @param tools MCPToolDefinition 数组 diff --git a/src/main/presenter/llmProviderPresenter/index.ts b/src/main/presenter/llmProviderPresenter/index.ts index 497fd2c8e..1707c91b3 100644 --- a/src/main/presenter/llmProviderPresenter/index.ts +++ b/src/main/presenter/llmProviderPresenter/index.ts @@ -556,7 +556,7 @@ export class LLMProviderPresenter implements ILlmProviderPresenter { } } - private getProviderInstance(providerId: string): BaseLLMProvider { + public getProviderInstance(providerId: string): BaseLLMProvider { let instance = this.providerInstances.get(providerId) if (!instance) { const provider = this.getProviderById(providerId) diff --git a/src/main/presenter/llmProviderPresenter/providers/openAICompatibleProvider.ts b/src/main/presenter/llmProviderPresenter/providers/openAICompatibleProvider.ts index 4fa85f3e2..e56be49ad 100644 --- a/src/main/presenter/llmProviderPresenter/providers/openAICompatibleProvider.ts +++ b/src/main/presenter/llmProviderPresenter/providers/openAICompatibleProvider.ts @@ -1624,4 +1624,99 @@ export class OpenAICompatibleProvider extends BaseLLMProvider { } } } + + /** + * Get request preview for debugging (DEV mode only) + * Builds the actual request parameters without sending the request + */ + public async getRequestPreview( + messages: ChatMessage[], + modelId: string, + modelConfig: ModelConfig, + temperature: number, + maxTokens: number, + mcpTools: MCPToolDefinition[] + ): Promise<{ + endpoint: string + headers: Record + body: unknown + }> { + const tools = mcpTools || [] + const supportsFunctionCall = modelConfig?.functionCall || false + let processedMessages = [ + ...this.formatMessages(messages, supportsFunctionCall) + ] as ChatCompletionMessageParam[] + + // Prepare non-native function call prompt if needed + if (tools.length > 0 && !supportsFunctionCall) { + processedMessages = this.prepareFunctionCallPrompt(processedMessages, tools) + } + + // Convert tools to OpenAI format if native support + const apiTools = + tools.length > 0 && supportsFunctionCall + ? await presenter.mcpPresenter.mcpToolsToOpenAITools(tools, this.provider.id) + : undefined + + // Build request params (same logic as handleChatCompletion) + const requestParams: OpenAI.Chat.ChatCompletionCreateParams = { + messages: processedMessages, + model: modelId, + stream: true, + temperature, + ...(modelId.startsWith('o1') || + modelId.startsWith('o3') || + modelId.startsWith('o4') || + modelId.includes('gpt-5') + ? { max_completion_tokens: maxTokens } + : { max_tokens: maxTokens }) + } + + requestParams.stream_options = { include_usage: true } + + if (this.provider.id.toLowerCase().includes('dashscope')) { + requestParams.response_format = { type: 'text' } + } + + if ( + this.provider.id.toLowerCase().includes('openrouter') && + modelId.startsWith('deepseek/deepseek-chat-v3-0324:free') + ) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ;(requestParams as any).provider = { + only: ['chutes'] + } + } + + if (modelConfig.reasoningEffort && this.supportsEffortParameter(modelId)) { + ;(requestParams as any).reasoning_effort = modelConfig.reasoningEffort + } + + if (modelConfig.verbosity && this.supportsVerbosityParameter(modelId)) { + ;(requestParams as any).verbosity = modelConfig.verbosity + } + + OPENAI_REASONING_MODELS.forEach((noTempId) => { + if (modelId.startsWith(noTempId)) delete requestParams.temperature + }) + + if (apiTools && apiTools.length > 0 && supportsFunctionCall) requestParams.tools = apiTools + + // Build headers + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.provider.apiKey || 'MISSING_API_KEY'}`, + ...this.defaultHeaders + } + + // Determine endpoint + const baseUrl = this.provider.baseUrl || 'https://api.openai.com/v1' + const endpoint = `${baseUrl}/chat/completions` + + return { + endpoint, + headers, + body: requestParams + } + } } diff --git a/src/main/presenter/llmProviderPresenter/providers/openAIResponsesProvider.ts b/src/main/presenter/llmProviderPresenter/providers/openAIResponsesProvider.ts index 491c7d417..f0f2c55ae 100644 --- a/src/main/presenter/llmProviderPresenter/providers/openAIResponsesProvider.ts +++ b/src/main/presenter/llmProviderPresenter/providers/openAIResponsesProvider.ts @@ -1324,4 +1324,72 @@ export class OpenAIResponsesProvider extends BaseLLMProvider { return [] } } + + /** + * Get request preview for debugging (DEV mode only) + */ + public async getRequestPreview( + messages: ChatMessage[], + modelId: string, + modelConfig: ModelConfig, + temperature: number, + maxTokens: number, + mcpTools: MCPToolDefinition[] + ): Promise<{ + endpoint: string + headers: Record + body: unknown + }> { + const tools = mcpTools || [] + const supportsFunctionCall = modelConfig?.functionCall || false + let processedMessages = this.formatMessages(messages) + + if (tools.length > 0 && !supportsFunctionCall) { + processedMessages = this.prepareFunctionCallPrompt(processedMessages, tools) + } + + const apiTools = + tools.length > 0 && supportsFunctionCall + ? await presenter.mcpPresenter.mcpToolsToOpenAIResponsesTools(tools, this.provider.id) + : undefined + + const requestParams: OpenAI.Responses.ResponseCreateParams = { + model: modelId, + input: processedMessages, + temperature, + max_output_tokens: maxTokens, + stream: true + } + + if (tools.length > 0 && supportsFunctionCall && apiTools) { + requestParams.tools = apiTools + } + + if (modelConfig.reasoningEffort && this.supportsEffortParameter(modelId)) { + ;(requestParams as any).reasoning = { + effort: modelConfig.reasoningEffort + } + } + + if (modelConfig.verbosity && this.supportsVerbosityParameter(modelId)) { + ;(requestParams as any).text = { + verbosity: modelConfig.verbosity + } + } + + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.provider.apiKey || 'MISSING_API_KEY'}`, + ...this.defaultHeaders + } + + const baseUrl = this.provider.baseUrl || 'https://api.openai.com/v1' + const endpoint = `${baseUrl}/responses` + + return { + endpoint, + headers, + body: requestParams + } + } } diff --git a/src/main/presenter/threadPresenter/index.ts b/src/main/presenter/threadPresenter/index.ts index dbb03c5f1..e271eb58e 100644 --- a/src/main/presenter/threadPresenter/index.ts +++ b/src/main/presenter/threadPresenter/index.ts @@ -15,6 +15,7 @@ import { ChatMessage, LLMAgentEventData } from '@shared/presenter' +import { ModelType } from '@shared/model' import { presenter } from '@/presenter' import { MessageManager } from './messageManager' import { eventBus, SendTarget } from '@/eventbus' @@ -1737,6 +1738,9 @@ export class ThreadPresenter implements IThreadPresenter { const { providerId, modelId } = conversation.settings const modelConfig = this.configPresenter.getModelConfig(modelId, providerId) + if (!modelConfig) { + throw new Error(`Model config not found for provider ${providerId} and model ${modelId}`) + } const { vision } = modelConfig || {} // 检查是否已被取消 this.throwIfCancelled(state.message.id) @@ -3666,4 +3670,165 @@ export class ThreadPresenter implements IThreadPresenter { return { id, name, params } } + + /** + * Get request preview for debugging (DEV mode only) + * Reconstructs the request parameters that would be sent to the provider + */ + async getMessageRequestPreview(messageId: string): Promise { + try { + // Get message and conversation + const message = await this.sqlitePresenter.getMessage(messageId) + if (!message || message.role !== 'assistant') { + throw new Error('Message not found or not an assistant message') + } + + const conversation = await this.sqlitePresenter.getConversation(message.conversation_id) + const { + providerId: defaultProviderId, + modelId: defaultModelId, + temperature, + maxTokens, + enabledMcpTools + } = conversation.settings + + // Parse metadata to get model_provider and model_id + let messageMetadata: MESSAGE_METADATA | null = null + try { + messageMetadata = JSON.parse(message.metadata) as MESSAGE_METADATA + } catch (e) { + console.warn('Failed to parse message metadata:', e) + } + + const effectiveProviderId = messageMetadata?.provider || defaultProviderId + const effectiveModelId = messageMetadata?.model || defaultModelId + + // Get user message (parent of assistant message) + const userMessageSqlite = await this.sqlitePresenter.getMessage(message.parent_id || '') + if (!userMessageSqlite) { + throw new Error('User message not found') + } + + // Convert SQLITE_MESSAGE to Message type + const userMessage = this.messageManager['convertToMessage'](userMessageSqlite) + + // Get context messages using getMessageHistory + const contextMessages = await this.getMessageHistory( + userMessage.id, + conversation.settings.contextLength + ) + + // Prepare prompt content (reconstruct what was sent) + let modelConfig = this.configPresenter.getModelConfig(effectiveModelId, effectiveProviderId) + if (!modelConfig) { + modelConfig = this.configPresenter.getModelConfig(defaultModelId, defaultProviderId) + } + + if (!modelConfig) { + throw new Error( + `Model config not found for provider ${effectiveProviderId} and model ${effectiveModelId}` + ) + } + + const supportsFunctionCall = modelConfig?.functionCall ?? false + const visionEnabled = modelConfig?.vision ?? false + + // Extract user content from userMessage + let userContent = '' + if (typeof userMessage.content === 'string') { + userContent = userMessage.content + } else if ( + userMessage.content && + typeof userMessage.content === 'object' && + 'text' in userMessage.content + ) { + userContent = userMessage.content.text || '' + } + + const { finalContent } = await preparePromptContent({ + conversation, + userContent, + contextMessages, + searchResults: null, + urlResults: [], + userMessage, + vision: visionEnabled, + imageFiles: [], + supportsFunctionCall, + modelType: ModelType.Chat + }) + + // Get MCP tools + let mcpTools: MCPToolDefinition[] = [] + try { + const toolDefinitions = await presenter.mcpPresenter.getAllToolDefinitions(enabledMcpTools) + if (Array.isArray(toolDefinitions)) { + mcpTools = toolDefinitions + } + } catch (error) { + console.warn('Failed to load MCP tool definitions for preview', error) + } + + // Get provider and request preview + const provider = this.llmProviderPresenter.getProviderInstance(effectiveProviderId) + if (!provider) { + throw new Error(`Provider ${effectiveProviderId} not found`) + } + + // Type assertion for provider instance + const providerInstance = provider as { + getRequestPreview: ( + messages: ChatMessage[], + modelId: string, + modelConfig: unknown, + temperature: number, + maxTokens: number, + mcpTools: MCPToolDefinition[] + ) => Promise<{ + endpoint: string + headers: Record + body: unknown + }> + } + + try { + const preview = await providerInstance.getRequestPreview( + finalContent, + effectiveModelId, + modelConfig, + temperature, + maxTokens, + mcpTools + ) + + // Redact sensitive information + const { redactRequestPreview } = await import('@/lib/redact') + const redacted = redactRequestPreview({ + headers: preview.headers, + body: preview.body + }) + + return { + providerId: effectiveProviderId, + modelId: effectiveModelId, + endpoint: preview.endpoint, + headers: redacted.headers, + body: redacted.body, + mayNotMatch: true // Always mark as potentially inconsistent since we're reconstructing + } + } catch (error) { + if (error instanceof Error && error.message.includes('not implemented')) { + return { + notImplemented: true, + providerId: effectiveProviderId, + modelId: effectiveModelId + } + } + throw error + } + } catch (error) { + console.error('[ThreadPresenter] getMessageRequestPreview failed:', error) + throw error + } + } } diff --git a/src/renderer/settings/components/CommonSettings.vue b/src/renderer/settings/components/CommonSettings.vue index a1f615647..3d5ad7558 100644 --- a/src/renderer/settings/components/CommonSettings.vue +++ b/src/renderer/settings/components/CommonSettings.vue @@ -26,6 +26,13 @@ :model-value="copyWithCotEnabled" @update:model-value="handleCopyWithCotChange" /> + @@ -51,6 +58,7 @@ const soundStore = useSoundStore() const searchPreviewEnabled = computed(() => settingsStore.searchPreviewEnabled) const soundEnabled = computed(() => soundStore.soundEnabled) const copyWithCotEnabled = computed(() => settingsStore.copyWithCotEnabled) +const traceDebugEnabled = computed(() => settingsStore.traceDebugEnabled) const handleSearchPreviewChange = (value: boolean) => { settingsStore.setSearchPreviewEnabled(value) @@ -63,4 +71,8 @@ const handleSoundChange = (value: boolean) => { const handleCopyWithCotChange = (value: boolean) => { settingsStore.setCopyWithCotEnabled(value) } + +const handleTraceDebugChange = (value: boolean) => { + settingsStore.setTraceDebugEnabled(value) +} diff --git a/src/renderer/src/components/message/MessageItemAssistant.vue b/src/renderer/src/components/message/MessageItemAssistant.vue index 7f66f68f9..77ffa37bf 100644 --- a/src/renderer/src/components/message/MessageItemAssistant.vue +++ b/src/renderer/src/components/message/MessageItemAssistant.vue @@ -77,6 +77,7 @@ @prev="handleAction('prev')" @next="handleAction('next')" @fork="handleAction('fork')" + @trace="handleAction('trace')" /> @@ -150,6 +151,7 @@ const emit = defineEmits<{ modelInfo: { model_name: string; model_provider: string } ] variantChanged: [messageId: string] + trace: [messageId: string] }>() // 获取当前会话ID @@ -269,6 +271,7 @@ type HandleActionType = | 'copyImage' | 'copyImageFromTop' | 'fork' + | 'trace' const handleAction = (action: HandleActionType) => { if (action === 'retry') { @@ -340,6 +343,8 @@ const handleAction = (action: HandleActionType) => { }) } else if (action === 'fork') { showForkDialog() + } else if (action === 'trace') { + emit('trace', currentMessage.value.id) } } diff --git a/src/renderer/src/components/message/MessageList.vue b/src/renderer/src/components/message/MessageList.vue index 62562e6ed..d1dc22c86 100644 --- a/src/renderer/src/components/message/MessageList.vue +++ b/src/renderer/src/components/message/MessageList.vue @@ -23,6 +23,7 @@ :is-capturing-image="capture.isCapturing.value" @copy-image="handleCopyImage" @variant-changed="scrollToMessage" + @trace="handleTrace" /> + @@ -72,6 +74,7 @@ import MessageItemUser from './MessageItemUser.vue' import MessageActionButtons from './MessageActionButtons.vue' import ReferencePreview from './ReferencePreview.vue' import MessageMinimap from './MessageMinimap.vue' +import TraceDialog from '../trace/TraceDialog.vue' // === Composables === import { useResizeObserver } from '@vueuse/core' @@ -124,6 +127,7 @@ const retry = useMessageRetry(toRef(props, 'messages')) const messageList = ref() const visible = ref(false) const shouldAutoFollow = ref(true) +const traceMessageId = ref(null) const scheduleScrollToBottom = (force = false) => { nextTick(() => { @@ -177,6 +181,10 @@ const showCancelButton = computed(() => { return chatStore.generatingThreadIds.has(chatStore.getActiveThreadId() ?? '') }) +const handleTrace = (messageId: string) => { + traceMessageId.value = messageId +} + // === Lifecycle Hooks === onMounted(() => { // Initialize scroll and visibility diff --git a/src/renderer/src/components/message/MessageToolbar.vue b/src/renderer/src/components/message/MessageToolbar.vue index b4c030021..658581147 100644 --- a/src/renderer/src/components/message/MessageToolbar.vue +++ b/src/renderer/src/components/message/MessageToolbar.vue @@ -155,6 +155,19 @@ {{ t('thread.toolbar.retry') }} + + + + + {{ t('thread.toolbar.trace') }} +