From a7fcf6779c0be5b97f1d4ef0439a82c93244270d Mon Sep 17 00:00:00 2001 From: John Neerdael Date: Fri, 20 Feb 2026 23:15:41 +0100 Subject: [PATCH] Fix OpenRouter model routing and add configurable model setting --- src/screens/AIChatScreen.tsx | 8 +++- src/screens/AISettingsScreen.tsx | 67 +++++++++++++++++++++++++++++++- src/services/aiService.ts | 62 +++++++++++++++++++++++++---- 3 files changed, 128 insertions(+), 9 deletions(-) diff --git a/src/screens/AIChatScreen.tsx b/src/screens/AIChatScreen.tsx index 6f49d0bc..a669bb5c 100644 --- a/src/screens/AIChatScreen.tsx +++ b/src/screens/AIChatScreen.tsx @@ -696,8 +696,14 @@ const AIChatScreen: React.FC = () => { if (error instanceof Error) { if (error.message.includes('not configured')) { errorMessage = 'Please configure your OpenRouter API key in Settings > AI Assistant.'; + } else if (/401|unauthorized|invalid api key|authentication/i.test(error.message)) { + errorMessage = 'OpenRouter rejected your API key. Please verify the key in Settings > AI Assistant.'; + } else if (/insufficient|credit|quota|429/i.test(error.message)) { + errorMessage = 'OpenRouter quota/credits were rejected for this request. Please check your OpenRouter usage and limits.'; + } else if (/model|provider|endpoint|unsupported|unavailable|not found/i.test(error.message)) { + errorMessage = 'The selected OpenRouter model is unavailable. Retry with `openrouter/free` or choose another custom model in Settings > AI Assistant.'; } else if (error.message.includes('API request failed')) { - errorMessage = 'Failed to connect to AI service. Please check your internet connection and API key.'; + errorMessage = 'Failed to connect to AI service. Please check your internet connection, API key, and OpenRouter model availability.'; } } diff --git a/src/screens/AISettingsScreen.tsx b/src/screens/AISettingsScreen.tsx index 251134af..3d88b2a4 100644 --- a/src/screens/AISettingsScreen.tsx +++ b/src/screens/AISettingsScreen.tsx @@ -25,6 +25,7 @@ import { useTranslation } from 'react-i18next'; const { width } = Dimensions.get('window'); const isTablet = width >= 768; +const DEFAULT_OPENROUTER_MODEL = 'openrouter/free'; const AISettingsScreen: React.FC = () => { const { t } = useTranslation(); @@ -75,6 +76,8 @@ const AISettingsScreen: React.FC = () => { const [apiKey, setApiKey] = useState(''); const [loading, setLoading] = useState(false); const [isKeySet, setIsKeySet] = useState(false); + const [useDefaultModel, setUseDefaultModel] = useState(true); + const [customModel, setCustomModel] = useState(''); useEffect(() => { loadApiKey(); @@ -82,11 +85,21 @@ const AISettingsScreen: React.FC = () => { const loadApiKey = async () => { try { - const savedKey = await mmkvStorage.getItem('openrouter_api_key'); + const [savedKey, savedModel] = await Promise.all([ + mmkvStorage.getItem('openrouter_api_key'), + mmkvStorage.getItem('openrouter_model'), + ]); if (savedKey) { setApiKey(savedKey); setIsKeySet(true); } + if (savedModel && savedModel.trim()) { + setUseDefaultModel(false); + setCustomModel(savedModel.trim()); + } else { + setUseDefaultModel(true); + setCustomModel(''); + } } catch (error) { if (__DEV__) console.error('Error loading OpenRouter API key:', error); } @@ -106,6 +119,11 @@ const AISettingsScreen: React.FC = () => { setLoading(true); try { await mmkvStorage.setItem('openrouter_api_key', apiKey.trim()); + if (useDefaultModel || !customModel.trim()) { + await mmkvStorage.removeItem('openrouter_model'); + } else { + await mmkvStorage.setItem('openrouter_model', customModel.trim()); + } setIsKeySet(true); openAlert(t('common.success'), t('ai_settings.success_saved')); } catch (error) { @@ -253,6 +271,44 @@ const AISettingsScreen: React.FC = () => { autoCorrect={false} /> + + + + Model + + + + + {useDefaultModel + ? `Using ${DEFAULT_OPENROUTER_MODEL} (free automatic routing).` + : 'Use a custom OpenRouter model ID (useful for paid plans).'} + + {!useDefaultModel && ( + + )} + + {!isKeySet ? ( { - if (!this.apiKey) { - await this.initialize(); - } + // Always refresh from storage so key changes in settings are picked up immediately. + await this.initialize(); return !!this.apiKey; } + private async getPreferredModels(): Promise { + const configuredModel = (await mmkvStorage.getItem('openrouter_model'))?.trim(); + if (!configuredModel) { + return [this.defaultModel]; + } + return [configuredModel]; + } + + private async parseErrorResponse(response: Response): Promise<{ + statusLine: string; + message: string; + raw: string; + }> { + const raw = await response.text(); + let message = ''; + + try { + const parsed = JSON.parse(raw) as OpenRouterErrorResponse; + message = parsed.error?.message || ''; + } catch { + message = raw; + } + + return { + statusLine: `${response.status} ${response.statusText}`, + message: (message || '').trim(), + raw, + }; + } + private createSystemPrompt(context: ContentContext): string { const isSeries = 'episodesBySeason' in (context as any); const isEpisode = !isSeries && 'showTitle' in (context as any); @@ -349,6 +386,9 @@ Answer questions about this movie using only the verified database information a }); } + const model = (await this.getPreferredModels())[0]; + if (__DEV__) console.log('[AIService] Using model:', model); + const response = await fetch(`${this.baseUrl}/chat/completions`, { method: 'POST', headers: { @@ -358,7 +398,7 @@ Answer questions about this movie using only the verified database information a 'X-Title': 'Nuvio - AI Chat', }, body: JSON.stringify({ - model: 'xiaomi/mimo-v2-flash:free', + model, messages, max_tokens: 1000, temperature: 0.7, @@ -369,9 +409,17 @@ Answer questions about this movie using only the verified database information a }); if (!response.ok) { - const errorText = await response.text(); - if (__DEV__) console.error('[AIService] API Error:', response.status, errorText); - throw new Error(`API request failed: ${response.status} ${response.statusText}`); + const parsedError = await this.parseErrorResponse(response); + + if (__DEV__) { + console.error('[AIService] API Error:', { + model, + status: parsedError.statusLine, + message: parsedError.message || parsedError.raw, + }); + } + + throw new Error(`API request failed: ${parsedError.statusLine} - ${parsedError.message || parsedError.raw || 'Request failed'}`); } const data: OpenRouterResponse = await response.json();