diff --git a/packages/stage-ui/src/stores/providers.ts b/packages/stage-ui/src/stores/providers.ts index d266a3d18..72873b061 100644 --- a/packages/stage-ui/src/stores/providers.ts +++ b/packages/stage-ui/src/stores/providers.ts @@ -48,7 +48,7 @@ import { import { computed, ref, watch } from 'vue' import { useI18n } from 'vue-i18n' -import { isAbsoluteUrl } from '../utils/string' +import { isUrl } from '../utils/url' import { models as elevenLabsModels } from './providers/elevenlabs/list-models' export interface ProviderMetadata { @@ -160,11 +160,29 @@ export interface VoiceInfo { export const useProvidersStore = defineStore('providers', () => { const providerCredentials = useLocalStorage>>('settings/credentials/providers', {}) const { t } = useI18n() - const notBaseUrlError = computed(() => ({ - errors: [new Error('Base URL is not absolute')], - reason: 'Base URL is not absolute. Check your input.', - valid: false, - })) + const baseUrlValidator = computed(() => (baseUrl: unknown) => { + let msg = '' + if (!baseUrl) { + msg = 'Base URL is required.' + } + else if (typeof baseUrl !== 'string') { + msg = 'Base URL must be a string.' + } + else if (!isUrl(baseUrl) || new URL(baseUrl).host.length === 0) { + msg = 'Base URL is not absolute. Try to include a scheme (http:// or https://).' + } + else if (!baseUrl.endsWith('/')) { + msg = 'Base URL must end with a trailing slash (/).' + } + if (msg) { + return { + errors: [new Error(msg)], + reason: msg, + valid: false, + } + } + return null + }) // Helper function to fetch OpenRouter models manually async function fetchOpenRouterModels(config: Record): Promise { @@ -223,8 +241,9 @@ export const useProvidersStore = defineStore('providers', () => { !config.baseUrl && new Error('Base URL is required'), ].filter(Boolean) - if (!!config.baseUrl && !isAbsoluteUrl(config.baseUrl as string)) { - return notBaseUrlError.value + const res = baseUrlValidator.value(config.baseUrl) + if (res) { + return res } return { @@ -504,8 +523,9 @@ export const useProvidersStore = defineStore('providers', () => { } } - if (!isAbsoluteUrl(config.baseUrl as string)) { - return notBaseUrlError.value + const res = baseUrlValidator.value(config.baseUrl) + if (res) { + return res } // Check if the Ollama server is reachable @@ -570,8 +590,9 @@ export const useProvidersStore = defineStore('providers', () => { } } - if (!isAbsoluteUrl(config.baseUrl as string)) { - return notBaseUrlError.value + const res = baseUrlValidator.value(config.baseUrl) + if (res) { + return res } // Check if the Ollama server is reachable @@ -665,8 +686,9 @@ export const useProvidersStore = defineStore('providers', () => { } } - if (!isAbsoluteUrl(config.baseUrl as string)) { - return notBaseUrlError.value + const res = baseUrlValidator.value(config.baseUrl) + if (res) { + return res } // Check if the vLLM is reachable @@ -742,6 +764,11 @@ export const useProvidersStore = defineStore('providers', () => { } } + const res = baseUrlValidator.value(config.baseUrl) + if (res) { + return res + } + // Check if the LM Studio server is reachable return fetch(`${(config.baseUrl as string).trim()}models`, { headers: (config.headers as HeadersInit) || undefined }) .then((response) => { @@ -800,8 +827,9 @@ export const useProvidersStore = defineStore('providers', () => { !config.baseUrl && new Error('Base URL is required. Default to https://api.openai.com/v1/ for official OpenAI API.'), ].filter(Boolean) - if (!!config.baseUrl && !isAbsoluteUrl(config.baseUrl as string)) { - return notBaseUrlError.value + const res = baseUrlValidator.value(config.baseUrl) + if (res) { + return res } return { @@ -848,8 +876,9 @@ export const useProvidersStore = defineStore('providers', () => { !config.baseUrl && new Error('Base URL is required'), ].filter(Boolean) - if (!!config.baseUrl && !isAbsoluteUrl(config.baseUrl as string)) { - return notBaseUrlError.value + const res = baseUrlValidator.value(config.baseUrl) + if (res) { + return res } return { @@ -965,8 +994,9 @@ export const useProvidersStore = defineStore('providers', () => { !config.baseUrl && new Error('Base URL is required. Default to https://api.openai.com/v1/ for official OpenAI API.'), ].filter(Boolean) - if (!!config.baseUrl && !isAbsoluteUrl(config.baseUrl as string)) { - return notBaseUrlError.value + const res = baseUrlValidator.value(config.baseUrl) + if (res) { + return res } return { @@ -1016,8 +1046,9 @@ export const useProvidersStore = defineStore('providers', () => { !config.baseUrl && new Error('Base URL is required'), ].filter(Boolean) - if (!!config.baseUrl && !isAbsoluteUrl(config.baseUrl as string)) { - return notBaseUrlError.value + const res = baseUrlValidator.value(config.baseUrl) + if (res) { + return res } return { @@ -1063,8 +1094,9 @@ export const useProvidersStore = defineStore('providers', () => { !config.baseUrl && new Error('Base URL is required. Default to https://api.openai.com/v1/ for official OpenAI API.'), ].filter(Boolean) - if (!!config.baseUrl && !isAbsoluteUrl(config.baseUrl as string)) { - return notBaseUrlError.value + const res = baseUrlValidator.value(config.baseUrl) + if (res) { + return res } return { @@ -1111,8 +1143,9 @@ export const useProvidersStore = defineStore('providers', () => { !config.baseUrl && new Error('Base URL is required'), ].filter(Boolean) - if (!!config.baseUrl && !isAbsoluteUrl(config.baseUrl as string)) { - return notBaseUrlError.value + const res = baseUrlValidator.value(config.baseUrl) + if (res) { + return res } return { @@ -1246,8 +1279,9 @@ export const useProvidersStore = defineStore('providers', () => { !config.baseUrl && new Error('Base URL is required. Default to https://api.anthropic.com/v1/ for official Claude API with OpenAI compatibility.'), ].filter(Boolean) - if (!!config.baseUrl && !isAbsoluteUrl(config.baseUrl as string)) { - return notBaseUrlError.value + const res = baseUrlValidator.value(config.baseUrl) + if (res) { + return res } return { @@ -1294,8 +1328,9 @@ export const useProvidersStore = defineStore('providers', () => { !config.baseUrl && new Error('Base URL is required. Default to https://generativelanguage.googleapis.com/v1beta/openai/ for official Google Gemini API with OpenAI compatibility.'), ].filter(Boolean) - if (!!config.baseUrl && !isAbsoluteUrl(config.baseUrl as string)) { - return notBaseUrlError.value + const res = baseUrlValidator.value(config.baseUrl) + if (res) { + return res } return { @@ -1383,8 +1418,9 @@ export const useProvidersStore = defineStore('providers', () => { !config.baseUrl && new Error('Base URL is required.'), ].filter(Boolean) - if (!!config.baseUrl && !isAbsoluteUrl(config.baseUrl as string)) { - return notBaseUrlError.value + const res = baseUrlValidator.value(config.baseUrl) + if (res) { + return res } return { @@ -1467,8 +1503,9 @@ export const useProvidersStore = defineStore('providers', () => { !config.baseUrl && new Error('Base URL is required.'), ].filter(Boolean) - if (!!config.baseUrl && !isAbsoluteUrl(config.baseUrl as string)) { - return notBaseUrlError.value + const res = baseUrlValidator.value(config.baseUrl) + if (res) { + return res } return { @@ -1531,8 +1568,9 @@ export const useProvidersStore = defineStore('providers', () => { !config.baseUrl && new Error('Base URL is required.'), ].filter(Boolean) - if (!!config.baseUrl && !isAbsoluteUrl(config.baseUrl as string)) { - return notBaseUrlError.value + const res = baseUrlValidator.value(config.baseUrl) + if (res) { + return res } return { @@ -1592,8 +1630,9 @@ export const useProvidersStore = defineStore('providers', () => { !config.baseUrl && new Error('Base URL is required. Default to http://localhost:11996/tts for Index-TTS.'), ].filter(Boolean) - if (!!config.baseUrl && !isAbsoluteUrl(config.baseUrl as string)) { - return notBaseUrlError.value + const res = baseUrlValidator.value(config.baseUrl) + if (res) { + return res } return { @@ -1664,8 +1703,9 @@ export const useProvidersStore = defineStore('providers', () => { !config.baseUrl && new Error('Base URL is required.'), ].filter(Boolean) - if (!!config.baseUrl && !isAbsoluteUrl(config.baseUrl as string)) { - return notBaseUrlError.value + const res = baseUrlValidator.value(config.baseUrl) + if (res) { + return res } return { @@ -1729,8 +1769,9 @@ export const useProvidersStore = defineStore('providers', () => { !((config.app as any)?.appId) && new Error('App ID is required.'), ].filter(Boolean) - if (!!config.baseUrl && !isAbsoluteUrl(config.baseUrl as string)) { - return notBaseUrlError.value + const res = baseUrlValidator.value(config.baseUrl) + if (res) { + return res } return { @@ -1900,8 +1941,9 @@ export const useProvidersStore = defineStore('providers', () => { !config.baseUrl && new Error('Base URL is required.'), ].filter(Boolean) - if (!!config.baseUrl && !isAbsoluteUrl(config.baseUrl as string)) { - return notBaseUrlError.value + const res = baseUrlValidator.value(config.baseUrl) + if (res) { + return res } return { @@ -2003,8 +2045,9 @@ export const useProvidersStore = defineStore('providers', () => { !config.baseUrl && new Error('Base URL is required.'), ].filter(Boolean) - if (!!config.baseUrl && !isAbsoluteUrl(config.baseUrl as string)) { - return notBaseUrlError.value + const res = baseUrlValidator.value(config.baseUrl) + if (res) { + return res } return { @@ -2131,12 +2174,13 @@ export const useProvidersStore = defineStore('providers', () => { } } - if (!isAbsoluteUrl(config.baseUrl as string)) { - return notBaseUrlError.value + const res = baseUrlValidator.value(config.baseUrl) + if (res) { + return res } // Check if the local running Player 2 is reachable - return await fetch(`${(config.baseUrl as string).endsWith('/') ? (config.baseUrl as string).slice(0, -1) : config.baseUrl}/health`, { + return await fetch(`${config.baseUrl}health`, { method: 'GET', headers: { 'player2-game-key': 'airi', @@ -2241,8 +2285,9 @@ export const useProvidersStore = defineStore('providers', () => { } } - if (!isAbsoluteUrl(config.baseUrl as string)) { - return notBaseUrlError.value + const res = baseUrlValidator.value(config.baseUrl) + if (res) { + return res } return { diff --git a/packages/stage-ui/src/utils/string.ts b/packages/stage-ui/src/utils/string.ts deleted file mode 100644 index 20bebbe91..000000000 --- a/packages/stage-ui/src/utils/string.ts +++ /dev/null @@ -1,11 +0,0 @@ -export function isAbsoluteUrl(url: string) { - try { - // This could be the most reliable way to check so using side-effect is acceptable - // eslint-disable-next-line no-new - new URL(url) - return true - } - catch { - return false - } -} diff --git a/packages/stage-ui/src/utils/url.ts b/packages/stage-ui/src/utils/url.ts new file mode 100644 index 000000000..e7e033f76 --- /dev/null +++ b/packages/stage-ui/src/utils/url.ts @@ -0,0 +1,10 @@ +export function isUrl(url: string) { + try { + // eslint-disable-next-line no-new + new URL(url) + return true + } + catch { + return false + } +}