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();