diff --git a/apps/stage-tamagotchi-electron/src/renderer/pages/settings/providers/openai-compatible-audio-speech.vue b/apps/stage-tamagotchi-electron/src/renderer/pages/settings/providers/openai-compatible-audio-speech.vue index d940f3f76..3bfdcf8eb 100644 --- a/apps/stage-tamagotchi-electron/src/renderer/pages/settings/providers/openai-compatible-audio-speech.vue +++ b/apps/stage-tamagotchi-electron/src/renderer/pages/settings/providers/openai-compatible-audio-speech.vue @@ -115,6 +115,7 @@ const { > diff --git a/apps/stage-tamagotchi/src/pages/settings/providers/openai-compatible-audio-speech.vue b/apps/stage-tamagotchi/src/pages/settings/providers/openai-compatible-audio-speech.vue index d940f3f76..3bfdcf8eb 100644 --- a/apps/stage-tamagotchi/src/pages/settings/providers/openai-compatible-audio-speech.vue +++ b/apps/stage-tamagotchi/src/pages/settings/providers/openai-compatible-audio-speech.vue @@ -115,6 +115,7 @@ const { > diff --git a/apps/stage-web/src/pages/settings/providers/openai-compatible-audio-speech.vue b/apps/stage-web/src/pages/settings/providers/openai-compatible-audio-speech.vue index 127dd1ef0..bfeaab567 100644 --- a/apps/stage-web/src/pages/settings/providers/openai-compatible-audio-speech.vue +++ b/apps/stage-web/src/pages/settings/providers/openai-compatible-audio-speech.vue @@ -139,6 +139,7 @@ function handleResetSettings() { > diff --git a/apps/stage-web/src/pages/settings/providers/openai-compatible.vue b/apps/stage-web/src/pages/settings/providers/openai-compatible.vue index d20ac2a1d..9c472d47a 100644 --- a/apps/stage-web/src/pages/settings/providers/openai-compatible.vue +++ b/apps/stage-web/src/pages/settings/providers/openai-compatible.vue @@ -83,6 +83,7 @@ function handleResetSettings() { > diff --git a/packages/stage-ui/src/stores/providers.ts b/packages/stage-ui/src/stores/providers.ts index c11c75afc..54bc2102e 100644 --- a/packages/stage-ui/src/stores/providers.ts +++ b/packages/stage-ui/src/stores/providers.ts @@ -252,7 +252,53 @@ export const useProvidersStore = defineStore('providers', () => { description: 'openrouter.ai', defaultBaseUrl: 'https://openrouter.ai/api/v1/', creator: createOpenRouter, - validation: ['health', 'model_list', 'chat_completions'], + validation: ['health', 'model_list'], + validators: { + validateProviderConfig: async (config) => { + const errors: Error[] = [] + + if (!config.apiKey) { + errors.push(new Error('API Key is required')) + } + + if (!config.baseUrl) { + errors.push(new Error('Base URL is required')) + } + + if (errors.length > 0) { + return { errors, reason: errors.map(e => e.message).join(', '), valid: false } + } + + if (!isUrl(config.baseUrl as string) || new URL(config.baseUrl as string).host.length === 0) { + errors.push(new Error('Base URL is not absolute. Check your input.')) + } + + if (!(config.baseUrl as string).endsWith('/')) { + errors.push(new Error('Base URL must end with a trailing slash (/).')) + } + + if (errors.length > 0) { + return { errors, reason: errors.map(e => e.message).join(', '), valid: false } + } + + const response = await fetch(`${config.baseUrl as string}chat/completions`, { headers: { Authorization: `Bearer ${config.apiKey}` }, method: 'POST', body: `{"model": "test","messages": [{"role": "user","content": "Hello, world"}],"stream": false}` }) + const responseJson = await response.json() + + if (!responseJson.user_id) { + return { + errors: [new Error(`OpenRouterError: ${responseJson.error.message}`)], + reason: `OpenRouterError: ${responseJson.error.message}`, + valid: false, + } + } + + return { + errors: [], + reason: '', + valid: true, + } + }, + }, }), 'app-local-audio-speech': buildOpenAICompatibleProvider({ id: 'app-local-audio-speech', diff --git a/packages/stage-ui/src/stores/providers/openai-compatible-builder.ts b/packages/stage-ui/src/stores/providers/openai-compatible-builder.ts index 6e68b635f..ca6e4b600 100644 --- a/packages/stage-ui/src/stores/providers/openai-compatible-builder.ts +++ b/packages/stage-ui/src/stores/providers/openai-compatible-builder.ts @@ -54,9 +54,6 @@ export function buildOpenAICompatibleProvider( validateProviderConfig: async (config: Record) => { const errors: Error[] = [] - if (!config.apiKey) { - errors.push(new Error('API key is required')) - } if (!config.baseUrl) { errors.push(new Error('Base URL is required')) } @@ -83,7 +80,7 @@ export function buildOpenAICompatibleProvider( if (validationChecks.includes('health')) { try { - responseChat = await fetch(`${config.baseUrl as string}chat/completions`, { headers: { Authorization: `Bearer ${config.apiKey}`, ...additionalHeaders }, method: 'POST' }) + responseChat = await fetch(`${config.baseUrl as string}chat/completions`, { headers: { Authorization: `Bearer ${config.apiKey}`, ...additionalHeaders }, method: 'POST', body: '{"model": "test"}' }) responseModelList = await fetch(`${config.baseUrl as string}models`, { headers: { Authorization: `Bearer ${config.apiKey}`, ...additionalHeaders } }) if (!([200, 400, 401].includes(responseChat.status) || [200, 400, 401].includes(responseModelList.status))) { @@ -119,7 +116,7 @@ export function buildOpenAICompatibleProvider( try { let response = responseChat if (!response) { - response = await fetch(`${config.baseUrl as string}chat/completions`, { headers: { Authorization: `Bearer ${config.apiKey}`, ...additionalHeaders }, method: 'POST' }) + response = await fetch(`${config.baseUrl as string}chat/completions`, { headers: { Authorization: `Bearer ${config.apiKey}`, ...additionalHeaders }, method: 'POST', body: '{"model": "test"}' }) } if (!response.ok) {