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) {