mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 17:45:38 +00:00
Fix OpenRouter model routing and add configurable model setting
This commit is contained in:
parent
a05a16f67b
commit
a7fcf6779c
3 changed files with 128 additions and 9 deletions
|
|
@ -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.';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
||||
<View style={styles.modelSection}>
|
||||
<View style={styles.modelHeader}>
|
||||
<Text style={[styles.label, { color: currentTheme.colors.highEmphasis }]}>
|
||||
Model
|
||||
</Text>
|
||||
<Switch
|
||||
value={useDefaultModel}
|
||||
onValueChange={setUseDefaultModel}
|
||||
trackColor={{ false: currentTheme.colors.elevation2, true: currentTheme.colors.primary }}
|
||||
thumbColor={useDefaultModel ? currentTheme.colors.white : currentTheme.colors.mediumEmphasis}
|
||||
ios_backgroundColor={currentTheme.colors.elevation2}
|
||||
/>
|
||||
</View>
|
||||
<Text style={[styles.description, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
{useDefaultModel
|
||||
? `Using ${DEFAULT_OPENROUTER_MODEL} (free automatic routing).`
|
||||
: 'Use a custom OpenRouter model ID (useful for paid plans).'}
|
||||
</Text>
|
||||
{!useDefaultModel && (
|
||||
<TextInput
|
||||
style={[
|
||||
styles.input,
|
||||
{
|
||||
backgroundColor: currentTheme.colors.elevation2,
|
||||
color: currentTheme.colors.highEmphasis,
|
||||
borderColor: currentTheme.colors.elevation2
|
||||
}
|
||||
]}
|
||||
value={customModel}
|
||||
onChangeText={setCustomModel}
|
||||
placeholder="e.g. openai/gpt-4o-mini"
|
||||
placeholderTextColor={currentTheme.colors.mediumEmphasis}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.buttonContainer}>
|
||||
{!isKeySet ? (
|
||||
<TouchableOpacity
|
||||
|
|
@ -468,6 +524,15 @@ const styles = StyleSheet.create({
|
|||
apiKeySection: {
|
||||
gap: 12,
|
||||
},
|
||||
modelSection: {
|
||||
gap: 8,
|
||||
marginTop: 4,
|
||||
},
|
||||
modelHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
label: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
|
|
|
|||
|
|
@ -126,10 +126,18 @@ interface OpenRouterResponse {
|
|||
};
|
||||
}
|
||||
|
||||
interface OpenRouterErrorResponse {
|
||||
error?: {
|
||||
message?: string;
|
||||
code?: string;
|
||||
};
|
||||
}
|
||||
|
||||
class AIService {
|
||||
private static instance: AIService;
|
||||
private apiKey: string | null = null;
|
||||
private baseUrl = 'https://openrouter.ai/api/v1';
|
||||
private defaultModel = 'openrouter/free';
|
||||
|
||||
private constructor() { }
|
||||
|
||||
|
|
@ -151,12 +159,41 @@ class AIService {
|
|||
}
|
||||
|
||||
async isConfigured(): Promise<boolean> {
|
||||
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<string[]> {
|
||||
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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue