diff --git a/cspell.config.yaml b/cspell.config.yaml
index eb329e5e2..2cf9eee8e 100644
--- a/cspell.config.yaml
+++ b/cspell.config.yaml
@@ -5,6 +5,7 @@ dictionaries: []
words:
- acubismmotion
- Aerisita
+ - aihubmix
- airi
- airi-vtuber
- Alaya
@@ -153,6 +154,7 @@ words:
- live2dcubismframework
- livechat
- Llmmarker
+ - lmstudio
- lobehub
- logg
- Lorebook
diff --git a/packages/i18n/src/locales/en/settings.yaml b/packages/i18n/src/locales/en/settings.yaml
index d06e3b339..baf8b6c0b 100644
--- a/packages/i18n/src/locales/en/settings.yaml
+++ b/packages/i18n/src/locales/en/settings.yaml
@@ -121,7 +121,9 @@ pages:
scenario: You recently woke up and forgot everything about your previous life.
systemprompt: You will receive messages, answer to them like a real human.
posthistoryinstructions: Remember to imitate an human.
- modules_info: Configure modules for this character card. If not set, default module configuration will be used.
+ modules_info: >-
+ Configure modules for this character card. If not set, default module
+ configuration will be used.
use_default: Use default
use_default_not_configured: Use default (not configured)
fields_info:
@@ -155,7 +157,6 @@ pages:
systemprompt: 'Error: Please, provide a system prompt.'
posthistoryinstructions: 'Error: Post history prompt is required.'
modules: Modules
-
name_asc: Name (A-Z)
name_desc: Name (Z-A)
no_cards: No cards yet. Click the button above to upload one!
@@ -442,7 +443,9 @@ pages:
apply-and-restart: Save and restart stdio MCP
messages:
opened: Config file opened at {path}
- restarted: MCP servers restarted. Started {started}, failed {failed}, skipped {skipped}
+ restarted: >-
+ MCP servers restarted. Started {started}, failed {failed}, skipped
+ {skipped}
providers:
explained:
chat: Text generation model providers. e.g. OpenRouter, OpenAI, Ollama.
@@ -466,7 +469,9 @@ pages:
validation:
failed:
title: Please check your provider settings, validation failed.
- description: You can still save the current configuration if you want to proceed.
+ description: >-
+ You can still save the current configuration if you want to
+ proceed.
action: Save anyway
config:
common:
@@ -520,7 +525,9 @@ pages:
needed for making it pass for Ollama. You need to set
OLLAMA_HOST explicitly when start the Ollama
application or Ollama CLI server, for detailed
- information, check Ollama integration guide.
+ information, check Ollama
+ integration guide.
common:
continueAnyway: Continue Anyway
fields:
@@ -659,7 +666,9 @@ pages:
description: Smaller models load faster but may have slightly lower quality.
models:
fp32-webgpu:
- description: Full precision model using WebGPU - Recommended for supported devices
+ description: >-
+ Full precision model using WebGPU - Recommended for supported
+ devices
fp32:
description: Full precision model
fp16:
@@ -779,6 +788,9 @@ pages:
transcriptions:
playground:
title: Transcription Playground
+ aihubmix:
+ title: AIHubMix
+ description: https://aihubmix.com (10% off)
title: Providers
connection:
description: Configure WebSocket server connection
@@ -846,7 +858,9 @@ pages:
title: Polaroid
description: Utility for taking shots of models
context-flow:
- description: Inspect incoming context updates and outgoing chat stream events
+ description: >-
+ Inspect incoming context updates and outgoing chat stream
+ events
websocket-inspector:
title: WebSocket Inspector
description: Inspect raw WebSocket traffic
@@ -855,7 +869,9 @@ pages:
description: Test color extraction
providers-transcription-realtime-aliyun-nls:
title: Aliyun Real-time Transcriber
- description: Stream microphone audio to Aliyun NLS and inspect live transcripts
+ description: >-
+ Stream microphone audio to Aliyun NLS and inspect live
+ transcripts
performance-playground:
title: Performance Playground
description: 'Test performance runtime: VRM expressions + TTS lip sync'
@@ -988,7 +1004,9 @@ vrm:
websocket-secure-enabled:
title: Enable Secure WebSocket (WSS)
description: >-
- Enable WSS (WebSocket Secure) to allow HTTPS connections for mobile (pocket) in development mode. This will automatically generate a self-signed certificate.
+ Enable WSS (WebSocket Secure) to allow HTTPS connections for mobile (pocket)
+ in development mode. This will automatically generate a self-signed
+ certificate.
wip:
title: Work in Progress
description: >-
diff --git a/packages/stage-ui/src/libs/providers/providers/aihubmix/index.ts b/packages/stage-ui/src/libs/providers/providers/aihubmix/index.ts
new file mode 100644
index 000000000..1584ab0cf
--- /dev/null
+++ b/packages/stage-ui/src/libs/providers/providers/aihubmix/index.ts
@@ -0,0 +1,58 @@
+import { createChatProvider, createEmbedProvider, createModelProvider, merge } from '@xsai-ext/providers/utils'
+import { z } from 'zod'
+
+import { createOpenAICompatibleValidators } from '../../validators/openai-compatible'
+import { defineProvider } from '../registry'
+
+const aihubmixConfigSchema = z.object({
+ apiKey: z
+ .string('API Key'),
+ baseUrl: z
+ .string('Base URL')
+ .optional()
+ .default('https://aihubmix.com/v1/'),
+})
+
+type AIHubMixConfig = z.input
+
+export const providerAIHubMix = defineProvider({
+ id: 'aihubmix',
+ order: 1,
+ name: 'AIHubMix',
+ nameLocalize: ({ t }) => t('settings.pages.providers.provider.aihubmix.title'),
+ description: 'AIHubMix',
+ descriptionLocalize: ({ t }) => t('settings.pages.providers.provider.aihubmix.description'),
+ tasks: ['chat'],
+ icon: 'i-lobe-icons:aihubmix',
+ iconColor: 'i-lobe-icons:aihubmix-color',
+
+ createProviderConfig: ({ t }) => aihubmixConfigSchema.extend({
+ apiKey: aihubmixConfigSchema.shape.apiKey.meta({
+ labelLocalized: t('settings.pages.providers.catalog.edit.config.common.fields.field.api-key.label'),
+ descriptionLocalized: t('settings.pages.providers.catalog.edit.config.common.fields.field.api-key.description'),
+ placeholderLocalized: t('settings.pages.providers.catalog.edit.config.common.fields.field.api-key.placeholder'),
+ type: 'password',
+ }),
+ baseUrl: aihubmixConfigSchema.shape.baseUrl.meta({
+ labelLocalized: t('settings.pages.providers.catalog.edit.config.common.fields.field.base-url.label'),
+ descriptionLocalized: t('settings.pages.providers.catalog.edit.config.common.fields.field.base-url.description'),
+ placeholderLocalized: t('settings.pages.providers.catalog.edit.config.common.fields.field.base-url.placeholder'),
+ }),
+ }),
+ createProvider(config) {
+ return merge(
+ createChatProvider({ apiKey: config.apiKey, baseURL: config.baseUrl! }),
+ createEmbedProvider({ apiKey: config.apiKey, baseURL: config.baseUrl! }),
+ createModelProvider({ apiKey: config.apiKey, baseURL: config.baseUrl! }),
+ )
+ },
+
+ validationRequiredWhen(config) {
+ return !!config.apiKey?.trim()
+ },
+ validators: {
+ ...createOpenAICompatibleValidators({
+ checks: ['model_list'],
+ }),
+ },
+})
diff --git a/packages/stage-ui/src/libs/providers/providers/anthropic/index.ts b/packages/stage-ui/src/libs/providers/providers/anthropic/index.ts
index d0dca18ec..ec0d9992f 100644
--- a/packages/stage-ui/src/libs/providers/providers/anthropic/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/anthropic/index.ts
@@ -38,7 +38,7 @@ function createAnthropic(apiKey: string, baseURL: string = 'https://api.anthropi
export const providerAnthropic = defineProvider({
id: 'anthropic',
- order: 5,
+ order: 6,
name: 'Anthropic',
nameLocalize: ({ t }) => t('settings.pages.providers.provider.anthropic.title'),
description: 'anthropic.com',
diff --git a/packages/stage-ui/src/libs/providers/providers/azure-ai-foundry/index.ts b/packages/stage-ui/src/libs/providers/providers/azure-ai-foundry/index.ts
new file mode 100644
index 000000000..2be1bfd18
--- /dev/null
+++ b/packages/stage-ui/src/libs/providers/providers/azure-ai-foundry/index.ts
@@ -0,0 +1,101 @@
+import type { ModelInfo } from '../../types'
+
+import { createAzure } from '@xsai-ext/providers/special/create'
+import { z } from 'zod'
+
+import { defineProvider } from '../registry'
+
+const azureAIFoundryConfigSchema = z.object({
+ apiKey: z.string('API Key'),
+ resourceName: z.string('Resource Name'),
+ modelId: z.string('Model ID'),
+ apiVersion: z.string('API Version').optional(),
+})
+
+type AzureAIFoundryConfig = z.input
+
+export const providerAzureAIFoundry = defineProvider({
+ id: 'azure-ai-foundry',
+ order: 17,
+ name: 'Azure AI Foundry',
+ nameLocalize: ({ t }) => t('settings.pages.providers.provider.azure-ai-foundry.title'),
+ description: 'azure.com',
+ descriptionLocalize: ({ t }) => t('settings.pages.providers.provider.azure-ai-foundry.description'),
+ tasks: ['chat'],
+ icon: 'i-lobe-icons:microsoft',
+
+ createProviderConfig: ({ t }) => azureAIFoundryConfigSchema.extend({
+ apiKey: azureAIFoundryConfigSchema.shape.apiKey.meta({
+ labelLocalized: t('settings.pages.providers.catalog.edit.config.common.fields.field.api-key.label'),
+ descriptionLocalized: t('settings.pages.providers.catalog.edit.config.common.fields.field.api-key.description'),
+ placeholderLocalized: t('settings.pages.providers.catalog.edit.config.common.fields.field.api-key.placeholder'),
+ type: 'password',
+ }),
+ resourceName: azureAIFoundryConfigSchema.shape.resourceName.meta({
+ labelLocalized: 'Resource Name',
+ descriptionLocalized: 'Prefix used in https://.services.ai.azure.com',
+ placeholderLocalized: 'my-resource',
+ }),
+ modelId: azureAIFoundryConfigSchema.shape.modelId.meta({
+ labelLocalized: 'Model ID',
+ descriptionLocalized: 'Model ID on Azure AI Foundry',
+ placeholderLocalized: 'gpt-4o',
+ }),
+ apiVersion: azureAIFoundryConfigSchema.shape.apiVersion.meta({
+ labelLocalized: 'API Version',
+ descriptionLocalized: 'API version for snapshot of the models',
+ placeholderLocalized: '2025-04-01-preview',
+ section: 'advanced',
+ }),
+ }),
+ createProvider(config) {
+ return createAzure({
+ apiKey: async () => config.apiKey.trim(),
+ resourceName: config.resourceName.trim(),
+ apiVersion: config.apiVersion?.trim(),
+ }) as any
+ },
+
+ extraMethods: {
+ listModels: async config =>
+ [{
+ id: config.modelId,
+ name: config.modelId,
+ provider: 'azure-ai-foundry',
+ description: '',
+ contextLength: 0,
+ deprecated: false,
+ } satisfies ModelInfo],
+ },
+ validationRequiredWhen(config) {
+ return !!config.apiKey?.trim() || !!config.resourceName?.trim() || !!config.modelId?.trim()
+ },
+ validators: {
+ validateConfig: [
+ ({ t }) => ({
+ id: 'azure-ai-foundry:check-config',
+ name: t('settings.pages.providers.catalog.edit.validators.openai-compatible.check-config.title'),
+ validator: async (config) => {
+ const errors: Array<{ error: unknown }> = []
+ const apiKey = typeof config.apiKey === 'string' ? config.apiKey.trim() : ''
+ const resourceName = typeof config.resourceName === 'string' ? config.resourceName.trim() : ''
+ const modelId = typeof config.modelId === 'string' ? config.modelId.trim() : ''
+
+ if (!apiKey)
+ errors.push({ error: new Error('API key is required.') })
+ if (!resourceName)
+ errors.push({ error: new Error('Resource name is required.') })
+ if (!modelId)
+ errors.push({ error: new Error('Model ID is required.') })
+
+ return {
+ errors,
+ reason: errors.length > 0 ? errors.map(item => (item.error as Error).message).join(', ') : '',
+ reasonKey: '',
+ valid: errors.length === 0,
+ }
+ },
+ }),
+ ],
+ },
+})
diff --git a/packages/stage-ui/src/libs/providers/providers/google-generative-ai/index.ts b/packages/stage-ui/src/libs/providers/providers/google-generative-ai/index.ts
index 7792751f1..956853a99 100644
--- a/packages/stage-ui/src/libs/providers/providers/google-generative-ai/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/google-generative-ai/index.ts
@@ -17,7 +17,7 @@ type GoogleGenerativeConfig = z.input
export const providerGoogleGenerativeAI = defineProvider({
id: 'google-generative-ai',
- order: 6,
+ order: 8,
name: 'Google Gemini',
nameLocalize: ({ t }) => t('settings.pages.providers.provider.google-generative-ai.title'),
description: 'ai.google.dev',
diff --git a/packages/stage-ui/src/libs/providers/providers/index.ts b/packages/stage-ui/src/libs/providers/providers/index.ts
index d8f1f852d..5c169cc95 100644
--- a/packages/stage-ui/src/libs/providers/providers/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/index.ts
@@ -1,4 +1,6 @@
import './openai'
+import './aihubmix'
+import './lm-studio'
import './openai-compatible'
import './n1n'
import './openrouter-ai'
@@ -21,6 +23,7 @@ import './moonshot-ai'
import './modelscope'
import './ollama'
import './cloudflare-workers-ai'
+import './azure-ai-foundry'
export {
getDefinedProvider,
diff --git a/packages/stage-ui/src/libs/providers/providers/lm-studio/index.ts b/packages/stage-ui/src/libs/providers/providers/lm-studio/index.ts
new file mode 100644
index 000000000..1e2fd8f89
--- /dev/null
+++ b/packages/stage-ui/src/libs/providers/providers/lm-studio/index.ts
@@ -0,0 +1,62 @@
+import { createChatProvider, createEmbedProvider, createModelProvider, merge } from '@xsai-ext/providers/utils'
+import { z } from 'zod'
+
+import { createOpenAICompatibleValidators } from '../../validators/openai-compatible'
+import { defineProvider } from '../registry'
+
+const lmStudioConfigSchema = z.object({
+ apiKey: z
+ .string('API Key'),
+ baseUrl: z
+ .string('Base URL')
+ .optional()
+ .default('http://localhost:1234/v1/'),
+})
+
+type LMStudioConfig = z.input
+
+export const providerLmStudio = defineProvider({
+ id: 'lm-studio',
+ order: 3,
+ name: 'LM Studio',
+ nameLocalize: ({ t }) => t('settings.pages.providers.provider.lm-studio.title'),
+ description: 'LM Studio',
+ descriptionLocalize: ({ t }) => t('settings.pages.providers.provider.lm-studio.description'),
+ tasks: ['chat'],
+ icon: 'i-lobe-icons:lmstudio',
+ iconColor: 'i-lobe-icons:lmstudio',
+
+ createProviderConfig: ({ t }) => lmStudioConfigSchema.extend({
+ apiKey: lmStudioConfigSchema.shape.apiKey.meta({
+ labelLocalized: t('settings.pages.providers.catalog.edit.config.common.fields.field.api-key.label'),
+ descriptionLocalized: t('settings.pages.providers.catalog.edit.config.common.fields.field.api-key.description'),
+ placeholderLocalized: t('settings.pages.providers.catalog.edit.config.common.fields.field.api-key.placeholder'),
+ type: 'password',
+ }),
+ baseUrl: lmStudioConfigSchema.shape.baseUrl.meta({
+ labelLocalized: t('settings.pages.providers.catalog.edit.config.common.fields.field.base-url.label'),
+ descriptionLocalized: t('settings.pages.providers.catalog.edit.config.common.fields.field.base-url.description'),
+ placeholderLocalized: t('settings.pages.providers.catalog.edit.config.common.fields.field.base-url.placeholder'),
+ }),
+ }),
+ createProvider(config) {
+ return merge(
+ createChatProvider({ apiKey: config.apiKey, baseURL: config.baseUrl! }),
+ createEmbedProvider({ apiKey: config.apiKey, baseURL: config.baseUrl! }),
+ createModelProvider({ apiKey: config.apiKey, baseURL: config.baseUrl! }),
+ )
+ },
+
+ validationRequiredWhen(config) {
+ return !!config.apiKey?.trim()
+ },
+ validators: {
+ ...createOpenAICompatibleValidators({
+ checks: ['connectivity', 'model_list'],
+ connectivityFailureReason: ({ errorMessage }) =>
+ `Failed to reach LM Studio server, error: ${errorMessage} occurred.\n\nMake sure LM Studio is running and the local server is started. You can start the local server in LM Studio by going to the 'Local Server' tab and clicking 'Start Server'.`,
+ modelListFailureReason: ({ errorMessage }) =>
+ `Failed to reach LM Studio server, error: ${errorMessage} occurred.\n\nMake sure LM Studio is running and the local server is started. You can start the local server in LM Studio by going to the 'Local Server' tab and clicking 'Start Server'.`,
+ }),
+ },
+})
diff --git a/packages/stage-ui/src/libs/providers/providers/ollama/index.ts b/packages/stage-ui/src/libs/providers/providers/ollama/index.ts
index 2d16b9f92..4fbe0c54a 100644
--- a/packages/stage-ui/src/libs/providers/providers/ollama/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/ollama/index.ts
@@ -69,6 +69,8 @@ export const providerOllama = defineProvider({
],
validateProvider: createOpenAICompatibleValidators({
checks: ['connectivity', 'model_list', 'chat_completions'],
+ connectivityFailureReason: ({ errorMessage }) =>
+ `Failed to reach Ollama server, error: ${errorMessage} occurred.\n\nIf you are using Ollama locally, this is likely the CORS (Cross-Origin Resource Sharing) security issue, where you will need to set OLLAMA_ORIGINS=* or OLLAMA_ORIGINS=https://airi.moeru.ai,http://localhost environment variable before launching Ollama server to make this work.`,
})!.validateProvider,
},
business: ({ t }) => ({
diff --git a/packages/stage-ui/src/libs/providers/providers/openai-compatible/index.ts b/packages/stage-ui/src/libs/providers/providers/openai-compatible/index.ts
index 5332d4133..1a7a80e2b 100644
--- a/packages/stage-ui/src/libs/providers/providers/openai-compatible/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/openai-compatible/index.ts
@@ -18,7 +18,7 @@ type OpenAICompatibleConfig = z.input
export const providerOpenAICompatible = defineProvider({
id: 'openai-compatible',
- order: 3,
+ order: 4,
name: 'OpenAI Compatible',
nameLocalize: ({ t }) => t('settings.pages.providers.provider.openai-compatible.title'),
description: 'OpenAI-compatible chat APIs with API key authentication.',
diff --git a/packages/stage-ui/src/libs/providers/providers/openai/index.ts b/packages/stage-ui/src/libs/providers/providers/openai/index.ts
index b61f509e7..02b0b3539 100644
--- a/packages/stage-ui/src/libs/providers/providers/openai/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/openai/index.ts
@@ -17,7 +17,7 @@ type OpenAICompatibleConfig = z.input
export const providerOpenAI = defineProvider({
id: 'openai',
- order: 1,
+ order: 5,
name: 'OpenAI',
nameLocalize: ({ t }) => t('settings.pages.providers.provider.openai.title'),
description: 'OpenAI',
diff --git a/packages/stage-ui/src/libs/providers/validators/openai-compatible.ts b/packages/stage-ui/src/libs/providers/validators/openai-compatible.ts
index 6e7abf382..dda3e92dd 100644
--- a/packages/stage-ui/src/libs/providers/validators/openai-compatible.ts
+++ b/packages/stage-ui/src/libs/providers/validators/openai-compatible.ts
@@ -12,6 +12,13 @@ import { isModelProvider } from '../types'
type OpenAICompatibleValidationCheck = 'connectivity' | 'model_list' | 'chat_completions'
+interface OpenAICompatibleValidationOptions {
+ checks?: OpenAICompatibleValidationCheck[]
+ additionalHeaders?: Record
+ connectivityFailureReason?: (input: { config: TConfig, error: unknown, errorMessage: string }) => string
+ modelListFailureReason?: (input: { config: TConfig, error: unknown, errorMessage: string }) => string
+}
+
function extractStatusCode(error: unknown): number | null {
if (!error)
return null
@@ -89,10 +96,9 @@ async function pickValidationModel(options?: {
- checks?: OpenAICompatibleValidationCheck[]
- additionalHeaders?: Record
-}): ProviderDefinition['validators'] {
+export function createOpenAICompatibleValidators(
+ options?: OpenAICompatibleValidationOptions,
+): ProviderDefinition['validators'] {
const checks = options?.checks ?? ['connectivity', 'model_list', 'chat_completions']
const additionalHeaders = options?.additionalHeaders
@@ -100,6 +106,7 @@ export function createOpenAICompatibleValidators | undefined,
contextOptions?: { validationCache?: Map },
- ) => {
+ ): Promise => {
const cache = contextOptions?.validationCache
const existing = cache?.get(chatCheckCacheKey) as Promise | undefined
if (existing)
@@ -131,11 +138,11 @@ export function createOpenAICompatibleValidators= 200 && status < 300)
+ const chatOk = status === 400 || Boolean(status && status >= 200 && status < 300)
return { connectivityOk: true, chatOk, errorMessage: errorMessageFrom(e) }
}
}
@@ -169,11 +176,11 @@ export function createOpenAICompatibleValidators= 200 && status < 300)
+ const chatOk = status === 400 || Boolean(status && status >= 200 && status < 300)
return { connectivityOk: true, chatOk, errorMessage: errorMessageFrom(e) }
}
})()
@@ -238,7 +245,11 @@ export function createOpenAICompatibleValidators } | undefined,
)
if (!result.connectivityOk) {
- errors.push({ error: new Error(`Connectivity check failed: ${result.errorMessage || 'Unknown error.'}`) })
+ const errorMessage = result.errorMessage || 'Unknown error.'
+ const reason = options?.connectivityFailureReason
+ ? options.connectivityFailureReason({ config, error: result.error, errorMessage })
+ : `Connectivity check failed: ${errorMessage}`
+ errors.push({ error: new Error(reason) })
}
return {
@@ -290,7 +301,11 @@ export function createOpenAICompatibleValidators