fix: second round of fixes

This commit is contained in:
2026-03-01 15:24:15 +01:00
parent 202ea1b7cc
commit e2c46e94aa
11 changed files with 78 additions and 119 deletions

View File

@@ -27,6 +27,7 @@ import type { PostMediaEngine } from './PostMediaEngine';
import { ModelCatalogEngine, DEFAULT_MAX_OUTPUT_TOKENS } from './ModelCatalogEngine'; import { ModelCatalogEngine, DEFAULT_MAX_OUTPUT_TOKENS } from './ModelCatalogEngine';
import { isRenderTool, generateFromToolCall } from '../a2ui/generator'; import { isRenderTool, generateFromToolCall } from '../a2ui/generator';
import type { A2UIServerMessage } from '../a2ui/types'; import type { A2UIServerMessage } from '../a2ui/types';
import type { ChatModel } from '../shared/electronApi';
// OpenCode Zen API endpoints // OpenCode Zen API endpoints
const ZEN_ANTHROPIC_URL = 'https://opencode.ai/zen/v1/messages'; const ZEN_ANTHROPIC_URL = 'https://opencode.ai/zen/v1/messages';
@@ -130,13 +131,6 @@ const MODEL_CAPABILITIES: Record<string, { vision: boolean }> = {
'devstral-large-latest': { vision: false }, 'devstral-large-latest': { vision: false },
}; };
export interface ModelInfo {
id: string;
name: string;
provider: string;
vision?: boolean;
}
export interface SendMessageOptions { export interface SendMessageOptions {
metadata?: { metadata?: {
surface?: 'tab' | 'sidebar'; surface?: 'tab' | 'sidebar';
@@ -222,7 +216,7 @@ export class OpenCodeManager {
private apiKey: string = ''; private apiKey: string = '';
private mistralApiKey: string = ''; private mistralApiKey: string = '';
private abortControllers: Map<string, AbortController> = new Map(); private abortControllers: Map<string, AbortController> = new Map();
private cachedModels: ModelInfo[] | null = null; private cachedModels: ChatModel[] | null = null;
private cachedModelsAt: number = 0; private cachedModelsAt: number = 0;
private static MODEL_CACHE_TTL = 5 * 60 * 1000; // 5 minutes private static MODEL_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
private modelCatalogEngine = new ModelCatalogEngine(); private modelCatalogEngine = new ModelCatalogEngine();
@@ -298,7 +292,7 @@ export class OpenCodeManager {
/** /**
* Validate an OpenCode API key by calling the models endpoint * Validate an OpenCode API key by calling the models endpoint
*/ */
async validateApiKey(apiKey: string): Promise<{ isValid: boolean; models: ModelInfo[] }> { async validateApiKey(apiKey: string): Promise<{ isValid: boolean; models: ChatModel[] }> {
if (!apiKey || apiKey.length < 3) { if (!apiKey || apiKey.length < 3) {
return { isValid: false, models: [] }; return { isValid: false, models: [] };
} }
@@ -333,7 +327,7 @@ export class OpenCodeManager {
/** /**
* Validate a Mistral API key by calling the Mistral models endpoint * Validate a Mistral API key by calling the Mistral models endpoint
*/ */
async validateMistralApiKey(apiKey: string): Promise<{ isValid: boolean; models: ModelInfo[] }> { async validateMistralApiKey(apiKey: string): Promise<{ isValid: boolean; models: ChatModel[] }> {
if (!apiKey || apiKey.length < 3) { if (!apiKey || apiKey.length < 3) {
return { isValid: false, models: [] }; return { isValid: false, models: [] };
} }
@@ -367,13 +361,13 @@ export class OpenCodeManager {
* Get available models (cached with 5-minute TTL) * Get available models (cached with 5-minute TTL)
* Merges models from all configured providers. * Merges models from all configured providers.
*/ */
async getAvailableModels(): Promise<ModelInfo[]> { async getAvailableModels(): Promise<ChatModel[]> {
// Return cached models if within TTL // Return cached models if within TTL
if (this.cachedModels && Date.now() - this.cachedModelsAt < OpenCodeManager.MODEL_CACHE_TTL) { if (this.cachedModels && Date.now() - this.cachedModelsAt < OpenCodeManager.MODEL_CACHE_TTL) {
return this.cachedModels; return this.cachedModels;
} }
const allModels: ModelInfo[] = []; const allModels: ChatModel[] = [];
let fetched = false; let fetched = false;
// Fetch OpenCode models // Fetch OpenCode models
@@ -908,7 +902,7 @@ export class OpenCodeManager {
private async sendOpenAIMessage( private async sendOpenAIMessage(
modelId: string, modelId: string,
systemPrompt: string, systemPrompt: string,
dbMessages: Array<{ role: string; content?: string }>, dbMessages: Array<{ role: string; content?: string; toolCalls?: string; toolCallId?: string }>,
signal: AbortSignal, signal: AbortSignal,
callbacks: { callbacks: {
onDelta?: (delta: string) => void; onDelta?: (delta: string) => void;
@@ -922,15 +916,18 @@ export class OpenCodeManager {
apiKey: string = this.apiKey, apiKey: string = this.apiKey,
providerOptions?: { parallelToolCalls?: boolean }, providerOptions?: { parallelToolCalls?: boolean },
): Promise<{ content: string; toolCalls: Array<{ name: string; args: unknown }> }> { ): Promise<{ content: string; toolCalls: Array<{ name: string; args: unknown }> }> {
// Build OpenAI-format messages // Build OpenAI-format messages (with tool-call summaries for context parity with Anthropic path)
const allMessages: Array<Record<string, unknown>> = [ const allMessages: Array<Record<string, unknown>> = [
{ role: 'system', content: systemPrompt }, { role: 'system', content: systemPrompt },
...dbMessages ...dbMessages
.filter(m => m.role === 'user' || m.role === 'assistant') .filter(m => m.role === 'user' || m.role === 'assistant')
.map(m => ({ .map(m => {
role: m.role, let content = m.content || '';
content: m.content || '', if (m.role === 'assistant') {
})), content += this.buildToolCallSummary(m.toolCalls);
}
return { role: m.role, content };
}),
]; ];
// Build OpenAI tools format // Build OpenAI tools format
@@ -2016,6 +2013,25 @@ export class OpenCodeManager {
return truncated; return truncated;
} }
/**
* Build a human-readable summary of tool calls from a serialized JSON string.
* Used by both Anthropic and OpenAI message builders to annotate assistant
* messages with tool-use context when resuming a conversation from DB history.
*/
private buildToolCallSummary(toolCallsJson?: string): string {
if (!toolCallsJson) return '';
try {
const toolCalls = JSON.parse(toolCallsJson) as Array<{ name: string; args: unknown }>;
if (toolCalls.length === 0) return '';
const summary = toolCalls
.map(tc => `- ${tc.name}(${JSON.stringify(tc.args)})`)
.join('\n');
return `\n\n[Tools used in this turn:\n${summary}\n]`;
} catch {
return '';
}
}
/** /**
* Build Anthropic-format messages from DB message history. * Build Anthropic-format messages from DB message history.
* For assistant messages that had tool calls, appends a summary annotation * For assistant messages that had tool calls, appends a summary annotation
@@ -2030,23 +2046,7 @@ export class OpenCodeManager {
if (msg.role === 'user') { if (msg.role === 'user') {
messages.push({ role: 'user', content: msg.content || '' }); messages.push({ role: 'user', content: msg.content || '' });
} else if (msg.role === 'assistant') { } else if (msg.role === 'assistant') {
let content = msg.content || ''; const content = (msg.content || '') + this.buildToolCallSummary(msg.toolCalls);
// If this message had tool calls, append a summary for context on resume
if (msg.toolCalls) {
try {
const toolCalls = JSON.parse(msg.toolCalls) as Array<{ name: string; args: unknown }>;
if (toolCalls.length > 0) {
const summary = toolCalls
.map(tc => `- ${tc.name}(${JSON.stringify(tc.args)})`)
.join('\n');
content += `\n\n[Tools used in this turn:\n${summary}\n]`;
}
} catch {
// Ignore malformed toolCalls JSON
}
}
messages.push({ role: 'assistant', content }); messages.push({ role: 'assistant', content });
} }
} }

View File

@@ -33,7 +33,6 @@ export {
OpenCodeManager, OpenCodeManager,
type SendMessageOptions, type SendMessageOptions,
type SendMessageResult, type SendMessageResult,
type ModelInfo,
} from './OpenCodeManager'; } from './OpenCodeManager';
export { export {
WxrParser, WxrParser,

View File

@@ -421,7 +421,7 @@ export interface ChatMessage {
export interface ChatModel { export interface ChatModel {
id: string; id: string;
name: string; name: string;
provider?: string; provider: string;
vision?: boolean; vision?: boolean;
} }

View File

@@ -23,9 +23,6 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ conversationId }) => {
const [availableModels, setAvailableModels] = useState<ChatModel[]>([]); const [availableModels, setAvailableModels] = useState<ChatModel[]>([]);
const [showModelSelector, setShowModelSelector] = useState(false); const [showModelSelector, setShowModelSelector] = useState(false);
const [needsApiKey, setNeedsApiKey] = useState(false); const [needsApiKey, setNeedsApiKey] = useState(false);
const [apiKeyInput, setApiKeyInput] = useState('');
const [apiKeyError, setApiKeyError] = useState('');
const [isValidating, setIsValidating] = useState(false);
const [actionError, setActionError] = useState<string | null>(null); const [actionError, setActionError] = useState<string | null>(null);
const messagesEndRef = useRef<HTMLDivElement>(null); const messagesEndRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLTextAreaElement>(null); const inputRef = useRef<HTMLTextAreaElement>(null);
@@ -174,29 +171,6 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ conversationId }) => {
scrollToBottom(); scrollToBottom();
}, [messages, streamingContent, scrollToBottom]); }, [messages, streamingContent, scrollToBottom]);
const handleApiKeySubmit = async () => {
if (!apiKeyInput.trim()) return;
setIsValidating(true);
setApiKeyError('');
try {
const result = await window.electronAPI?.chat.validateApiKey(apiKeyInput.trim());
if (result?.isValid) {
await window.electronAPI?.chat.setApiKey(apiKeyInput.trim());
setNeedsApiKey(false);
setApiKeyInput('');
loadData();
} else {
setApiKeyError(tr('chat.apiKeyInvalid'));
}
} catch {
setApiKeyError(tr('chat.apiKeyValidationFailed'));
} finally {
setIsValidating(false);
}
};
const handleSend = async () => { const handleSend = async () => {
const message = inputValue.trim(); const message = inputValue.trim();
if (!message || isStreaming) return; if (!message || isStreaming) return;
@@ -303,6 +277,11 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ conversationId }) => {
// API key setup screen // API key setup screen
if (needsApiKey) { if (needsApiKey) {
const handleOpenSettings = () => {
useAppStore.getState().setActiveView('settings');
useAppStore.getState().openTab({ type: 'settings', id: 'settings', isTransient: false });
};
return ( return (
<div className="chat-panel chat-surface"> <div className="chat-panel chat-surface">
<div className="chat-panel-header"> <div className="chat-panel-header">
@@ -314,23 +293,12 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ conversationId }) => {
<h2>{tr('chat.apiKeyRequiredTitle')}</h2> <h2>{tr('chat.apiKeyRequiredTitle')}</h2>
<p>{tr('chat.apiKeyRequiredDescription')}</p> <p>{tr('chat.apiKeyRequiredDescription')}</p>
<div className="api-key-form"> <div className="api-key-form">
<input
type="password"
className="api-key-input"
value={apiKeyInput}
onChange={(e) => setApiKeyInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleApiKeySubmit()}
placeholder={tr('chat.apiKeyPlaceholder')}
disabled={isValidating}
/>
<button <button
className="api-key-submit" className="api-key-submit"
onClick={handleApiKeySubmit} onClick={handleOpenSettings}
disabled={!apiKeyInput.trim() || isValidating}
> >
{isValidating ? tr('chat.apiKeyValidating') : tr('chat.apiKeySave')} {tr('chat.openSettings')}
</button> </button>
{apiKeyError && <div className="api-key-error">{apiKeyError}</div>}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -192,13 +192,11 @@
"settings.toast.thumbnailsComplete": "Generierung der Vorschaubilder abgeschlossen", "settings.toast.thumbnailsComplete": "Generierung der Vorschaubilder abgeschlossen",
"settings.toast.thumbnailsFailed": "Vorschaubilder konnten nicht erzeugt werden", "settings.toast.thumbnailsFailed": "Vorschaubilder konnten nicht erzeugt werden",
"chat.setupTitle": "KI-Chat-Einrichtung", "chat.setupTitle": "KI-Chat-Einrichtung",
"chat.apiKeyRequiredTitle": "OpenCode Zen API-Schlüssel erforderlich", "chat.apiKeyRequiredTitle": "API-Schlüssel erforderlich",
"chat.apiKeyRequiredDescription": "Gib deinen OpenCode API-Schlüssel ein, um den KI-Chat zu aktivieren.", "chat.apiKeyRequiredDescription": "Konfiguriere einen API-Schlüssel in den Einstellungen, um den KI-Chat zu aktivieren.",
"chat.openSettings": "Einstellungen öffnen",
"chat.apiKeyPlaceholder": "API-Schlüssel eingeben...", "chat.apiKeyPlaceholder": "API-Schlüssel eingeben...",
"chat.apiKeySave": "Schlüssel speichern", "chat.apiKeySave": "Schlüssel speichern",
"chat.apiKeyValidating": "Wird validiert...",
"chat.apiKeyInvalid": "Ungültiger API-Schlüssel. Bitte prüfen und erneut versuchen.",
"chat.apiKeyValidationFailed": "API-Schlüssel konnte nicht validiert werden.",
"chat.newChat": "Neuer Chat", "chat.newChat": "Neuer Chat",
"chat.welcomeTitle": "Willkommen beim KI-Assistenten", "chat.welcomeTitle": "Willkommen beim KI-Assistenten",
"chat.welcomeDescription": "Ich kann dir helfen, deinen Blog mit anschaulichen Darstellungen zu verwalten. Frag mich zum Beispiel:", "chat.welcomeDescription": "Ich kann dir helfen, deinen Blog mit anschaulichen Darstellungen zu verwalten. Frag mich zum Beispiel:",

View File

@@ -192,13 +192,11 @@
"settings.toast.thumbnailsComplete": "Thumbnail generation complete", "settings.toast.thumbnailsComplete": "Thumbnail generation complete",
"settings.toast.thumbnailsFailed": "Failed to generate thumbnails", "settings.toast.thumbnailsFailed": "Failed to generate thumbnails",
"chat.setupTitle": "AI Chat Setup", "chat.setupTitle": "AI Chat Setup",
"chat.apiKeyRequiredTitle": "OpenCode Zen API Key Required", "chat.apiKeyRequiredTitle": "API Key Required",
"chat.apiKeyRequiredDescription": "Enter your OpenCode API key to enable AI chat.", "chat.apiKeyRequiredDescription": "Configure an API key in Settings to enable AI chat.",
"chat.openSettings": "Open Settings",
"chat.apiKeyPlaceholder": "Enter your API key...", "chat.apiKeyPlaceholder": "Enter your API key...",
"chat.apiKeySave": "Save Key", "chat.apiKeySave": "Save Key",
"chat.apiKeyValidating": "Validating...",
"chat.apiKeyInvalid": "Invalid API key. Please check and try again.",
"chat.apiKeyValidationFailed": "Failed to validate API key.",
"chat.newChat": "New Chat", "chat.newChat": "New Chat",
"chat.welcomeTitle": "Welcome to the AI Assistant", "chat.welcomeTitle": "Welcome to the AI Assistant",
"chat.welcomeDescription": "I can help you manage your blog with rich visualizations. Try asking me to:", "chat.welcomeDescription": "I can help you manage your blog with rich visualizations. Try asking me to:",

View File

@@ -192,13 +192,11 @@
"settings.toast.thumbnailsComplete": "Generación de miniaturas completa", "settings.toast.thumbnailsComplete": "Generación de miniaturas completa",
"settings.toast.thumbnailsFailed": "No se pudieron generar miniaturas", "settings.toast.thumbnailsFailed": "No se pudieron generar miniaturas",
"chat.setupTitle": "Configuración de chat IA", "chat.setupTitle": "Configuración de chat IA",
"chat.apiKeyRequiredTitle": "Se requiere clave API de OpenCode Zen", "chat.apiKeyRequiredTitle": "Clave API requerida",
"chat.apiKeyRequiredDescription": "Introduce tu clave API de OpenCode para habilitar el chat de IA.", "chat.apiKeyRequiredDescription": "Configura una clave API en Ajustes para habilitar el chat de IA.",
"chat.openSettings": "Abrir Ajustes",
"chat.apiKeyPlaceholder": "Introduce tu clave API...", "chat.apiKeyPlaceholder": "Introduce tu clave API...",
"chat.apiKeySave": "Guardar clave", "chat.apiKeySave": "Guardar clave",
"chat.apiKeyValidating": "Validando...",
"chat.apiKeyInvalid": "Clave API no válida. Compruébala e inténtalo de nuevo.",
"chat.apiKeyValidationFailed": "No se pudo validar la clave API.",
"chat.newChat": "Nuevo chat", "chat.newChat": "Nuevo chat",
"chat.welcomeTitle": "Bienvenido al asistente de IA", "chat.welcomeTitle": "Bienvenido al asistente de IA",
"chat.welcomeDescription": "Puedo ayudarte a gestionar tu blog con visualizaciones interactivas. Prueba a pedirme que:", "chat.welcomeDescription": "Puedo ayudarte a gestionar tu blog con visualizaciones interactivas. Prueba a pedirme que:",

View File

@@ -190,13 +190,11 @@
"settings.toast.thumbnailsComplete": "Génération des miniatures terminée", "settings.toast.thumbnailsComplete": "Génération des miniatures terminée",
"settings.toast.thumbnailsFailed": "Impossible de générer les miniatures", "settings.toast.thumbnailsFailed": "Impossible de générer les miniatures",
"chat.setupTitle": "Configuration du chat IA", "chat.setupTitle": "Configuration du chat IA",
"chat.apiKeyRequiredTitle": "Clé API OpenCode Zen requise", "chat.apiKeyRequiredTitle": "Clé API requise",
"chat.apiKeyRequiredDescription": "Saisissez votre clé API OpenCode pour activer le chat IA.", "chat.apiKeyRequiredDescription": "Configurez une clé API dans les Réglages pour activer le chat IA.",
"chat.openSettings": "Ouvrir les Réglages",
"chat.apiKeyPlaceholder": "Saisissez votre clé API...", "chat.apiKeyPlaceholder": "Saisissez votre clé API...",
"chat.apiKeySave": "Enregistrer la clé", "chat.apiKeySave": "Enregistrer la clé",
"chat.apiKeyValidating": "Validation...",
"chat.apiKeyInvalid": "Clé API invalide. Veuillez vérifier et réessayer.",
"chat.apiKeyValidationFailed": "Impossible de valider la clé API.",
"chat.newChat": "Nouveau chat", "chat.newChat": "Nouveau chat",
"chat.welcomeTitle": "Bienvenue dans lassistant IA", "chat.welcomeTitle": "Bienvenue dans lassistant IA",
"chat.welcomeDescription": "Je peux vous aider à gérer votre blog avec des visualisations riches. Essayez par exemple :", "chat.welcomeDescription": "Je peux vous aider à gérer votre blog avec des visualisations riches. Essayez par exemple :",

View File

@@ -190,13 +190,11 @@
"settings.toast.thumbnailsComplete": "Generazione miniature completata", "settings.toast.thumbnailsComplete": "Generazione miniature completata",
"settings.toast.thumbnailsFailed": "Impossibile generare le miniature", "settings.toast.thumbnailsFailed": "Impossibile generare le miniature",
"chat.setupTitle": "Configurazione chat IA", "chat.setupTitle": "Configurazione chat IA",
"chat.apiKeyRequiredTitle": "Chiave API OpenCode Zen richiesta", "chat.apiKeyRequiredTitle": "Chiave API richiesta",
"chat.apiKeyRequiredDescription": "Inserisci la tua chiave API OpenCode per abilitare la chat IA.", "chat.apiKeyRequiredDescription": "Configura una chiave API nelle Impostazioni per abilitare la chat IA.",
"chat.openSettings": "Apri Impostazioni",
"chat.apiKeyPlaceholder": "Inserisci la tua chiave API...", "chat.apiKeyPlaceholder": "Inserisci la tua chiave API...",
"chat.apiKeySave": "Salva chiave", "chat.apiKeySave": "Salva chiave",
"chat.apiKeyValidating": "Convalida in corso...",
"chat.apiKeyInvalid": "Chiave API non valida. Controlla e riprova.",
"chat.apiKeyValidationFailed": "Impossibile convalidare la chiave API.",
"chat.newChat": "Nuova chat", "chat.newChat": "Nuova chat",
"chat.welcomeTitle": "Benvenuto nellassistente IA", "chat.welcomeTitle": "Benvenuto nellassistente IA",
"chat.welcomeDescription": "Posso aiutarti a gestire il tuo blog con visualizzazioni interattive. Prova a chiedermi di:", "chat.welcomeDescription": "Posso aiutarti a gestire il tuo blog con visualizzazioni interattive. Prova a chiedermi di:",

View File

@@ -45,7 +45,8 @@ vi.mock('../../src/main/database', () => ({
getDatabase: vi.fn(() => ({})), getDatabase: vi.fn(() => ({})),
})); }));
import { OpenCodeManager, type ModelInfo } from '../../src/main/engine/OpenCodeManager'; import { OpenCodeManager } from '../../src/main/engine/OpenCodeManager';
import type { ChatModel } from '../../src/main/shared/electronApi';
// Helper to create manager with mocked httpRequest // Helper to create manager with mocked httpRequest
function createManager(): OpenCodeManager { function createManager(): OpenCodeManager {
@@ -278,7 +279,7 @@ describe('OpenCodeManager Mistral integration', () => {
}); });
const models = await manager.getAvailableModels(); const models = await manager.getAvailableModels();
const providers = new Set(models.map((m: ModelInfo) => m.provider)); const providers = new Set(models.map((m: ChatModel) => m.provider));
expect(providers.has('mistral')).toBe(false); expect(providers.has('mistral')).toBe(false);
}); });
@@ -301,7 +302,7 @@ describe('OpenCodeManager Mistral integration', () => {
const models = await manager.getAvailableModels(); const models = await manager.getAvailableModels();
expect(models.length).toBe(2); expect(models.length).toBe(2);
expect(models.every((m: ModelInfo) => m.provider === 'mistral')).toBe(true); expect(models.every((m: ChatModel) => m.provider === 'mistral')).toBe(true);
}); });
it('merges models from both providers when both keys are set', async () => { it('merges models from both providers when both keys are set', async () => {
@@ -330,7 +331,7 @@ describe('OpenCodeManager Mistral integration', () => {
const models = await manager.getAvailableModels(); const models = await manager.getAvailableModels();
expect(models.length).toBe(4); expect(models.length).toBe(4);
const providers = new Set(models.map((m: ModelInfo) => m.provider)); const providers = new Set(models.map((m: ChatModel) => m.provider));
expect(providers.has('anthropic')).toBe(true); expect(providers.has('anthropic')).toBe(true);
expect(providers.has('mistral')).toBe(true); expect(providers.has('mistral')).toBe(true);
}); });
@@ -353,8 +354,8 @@ describe('OpenCodeManager Mistral integration', () => {
}); });
const models = await manager.getAvailableModels(); const models = await manager.getAvailableModels();
const large = models.find((m: ModelInfo) => m.id === 'mistral-large-latest'); const large = models.find((m: ChatModel) => m.id === 'mistral-large-latest');
const devstral = models.find((m: ModelInfo) => m.id === 'devstral-small-latest'); const devstral = models.find((m: ChatModel) => m.id === 'devstral-small-latest');
expect(large?.vision).toBe(true); expect(large?.vision).toBe(true);
expect(devstral?.vision).toBe(false); expect(devstral?.vision).toBe(false);
@@ -369,7 +370,7 @@ describe('OpenCodeManager Mistral integration', () => {
const models = await manager.getAvailableModels(); const models = await manager.getAvailableModels();
// Should only have Mistral models from fallback // Should only have Mistral models from fallback
const providers = new Set(models.map((m: ModelInfo) => m.provider)); const providers = new Set(models.map((m: ChatModel) => m.provider));
expect(providers.has('mistral')).toBe(true); expect(providers.has('mistral')).toBe(true);
expect(providers.has('anthropic')).toBe(false); expect(providers.has('anthropic')).toBe(false);
expect(providers.has('openai')).toBe(false); expect(providers.has('openai')).toBe(false);
@@ -580,13 +581,13 @@ describe('OpenCodeManager Mistral integration', () => {
const models = await manager.getAvailableModels(); const models = await manager.getAvailableModels();
// Vision-capable models // Vision-capable models
expect(models.find((m: ModelInfo) => m.id === 'mistral-large-latest')?.vision).toBe(true); expect(models.find((m: ChatModel) => m.id === 'mistral-large-latest')?.vision).toBe(true);
expect(models.find((m: ModelInfo) => m.id === 'mistral-medium-latest')?.vision).toBe(true); expect(models.find((m: ChatModel) => m.id === 'mistral-medium-latest')?.vision).toBe(true);
expect(models.find((m: ModelInfo) => m.id === 'mistral-small-latest')?.vision).toBe(true); expect(models.find((m: ChatModel) => m.id === 'mistral-small-latest')?.vision).toBe(true);
// Non-vision models // Non-vision models
expect(models.find((m: ModelInfo) => m.id === 'devstral-small-latest')?.vision).toBe(false); expect(models.find((m: ChatModel) => m.id === 'devstral-small-latest')?.vision).toBe(false);
expect(models.find((m: ModelInfo) => m.id === 'devstral-large-latest')?.vision).toBe(false); expect(models.find((m: ChatModel) => m.id === 'devstral-large-latest')?.vision).toBe(false);
}); });
}); });

View File

@@ -29,7 +29,8 @@ vi.mock('../../src/main/database', () => ({
getDatabase: vi.fn(() => ({})), getDatabase: vi.fn(() => ({})),
})); }));
import { OpenCodeManager, ModelInfo } from '../../src/main/engine/OpenCodeManager'; import { OpenCodeManager } from '../../src/main/engine/OpenCodeManager';
import type { ChatModel } from '../../src/main/shared/electronApi';
// Helper to create manager with mocked httpRequest // Helper to create manager with mocked httpRequest
function createManager(): OpenCodeManager { function createManager(): OpenCodeManager {
@@ -163,13 +164,13 @@ describe('OpenCodeManager model discovery', () => {
expect(models.length).toBeGreaterThan(0); expect(models.length).toBeGreaterThan(0);
// Should include well-known models from the display name map // Should include well-known models from the display name map
const ids = models.map((m: ModelInfo) => m.id); const ids = models.map((m: ChatModel) => m.id);
expect(ids).toContain('claude-sonnet-4'); expect(ids).toContain('claude-sonnet-4');
expect(ids).toContain('gpt-5'); expect(ids).toContain('gpt-5');
// Every model should have proper provider detection // Every model should have proper provider detection
const claudeModel = models.find((m: ModelInfo) => m.id === 'claude-sonnet-4'); const claudeModel = models.find((m: ChatModel) => m.id === 'claude-sonnet-4');
expect(claudeModel?.provider).toBe('anthropic'); expect(claudeModel?.provider).toBe('anthropic');
const gptModel = models.find((m: ModelInfo) => m.id === 'gpt-5'); const gptModel = models.find((m: ChatModel) => m.id === 'gpt-5');
expect(gptModel?.provider).toBe('openai'); expect(gptModel?.provider).toBe('openai');
}); });
@@ -183,7 +184,7 @@ describe('OpenCodeManager model discovery', () => {
const models = await manager.getAvailableModels(); const models = await manager.getAvailableModels();
expect(models.length).toBeGreaterThan(0); expect(models.length).toBeGreaterThan(0);
const ids = models.map((m: ModelInfo) => m.id); const ids = models.map((m: ChatModel) => m.id);
expect(ids).toContain('claude-sonnet-4'); expect(ids).toContain('claude-sonnet-4');
}); });
@@ -245,7 +246,7 @@ describe('OpenCodeManager model discovery', () => {
// Only Mistral models will be in fallback since only Mistral key is set // Only Mistral models will be in fallback since only Mistral key is set
expect(models.length).toBeGreaterThan(0); expect(models.length).toBeGreaterThan(0);
const providers = new Set(models.map((m: ModelInfo) => m.provider)); const providers = new Set(models.map((m: ChatModel) => m.provider));
expect(providers.has('mistral')).toBe(true); expect(providers.has('mistral')).toBe(true);
}); });
}); });