feat(stage-ui): new aihubmix provider, bring back LM Studio, Azure AI Foundry

This commit is contained in:
Neko Ayaka
2026-03-05 15:23:32 +08:00
parent e0e887cf13
commit 6bc43c3a02
12 changed files with 285 additions and 24 deletions
+2
View File
@@ -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
+27 -9
View File
@@ -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
<code>OLLAMA_HOST</code> explicitly when start the Ollama
application or Ollama CLI server, for detailed
information, check <a href="https://airi.moeru.ai/docs/en/docs/manual/integrations/providers/chat/ollama">Ollama integration guide</a>.
information, check <a
href="https://airi.moeru.ai/docs/en/docs/manual/integrations/providers/chat/ollama">Ollama
integration guide</a>.
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: >-
@@ -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<typeof aihubmixConfigSchema>
export const providerAIHubMix = defineProvider<AIHubMixConfig>({
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'],
}),
},
})
@@ -38,7 +38,7 @@ function createAnthropic(apiKey: string, baseURL: string = 'https://api.anthropi
export const providerAnthropic = defineProvider<AnthropicConfig>({
id: 'anthropic',
order: 5,
order: 6,
name: 'Anthropic',
nameLocalize: ({ t }) => t('settings.pages.providers.provider.anthropic.title'),
description: 'anthropic.com',
@@ -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<typeof azureAIFoundryConfigSchema>
export const providerAzureAIFoundry = defineProvider<AzureAIFoundryConfig>({
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://<prefix>.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,
}
},
}),
],
},
})
@@ -17,7 +17,7 @@ type GoogleGenerativeConfig = z.input<typeof googleGenerativeConfigSchema>
export const providerGoogleGenerativeAI = defineProvider<GoogleGenerativeConfig>({
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',
@@ -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,
@@ -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<typeof lmStudioConfigSchema>
export const providerLmStudio = defineProvider<LMStudioConfig>({
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'.`,
}),
},
})
@@ -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 }) => ({
@@ -18,7 +18,7 @@ type OpenAICompatibleConfig = z.input<typeof openAICompatibleConfigSchema>
export const providerOpenAICompatible = defineProvider<OpenAICompatibleConfig>({
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.',
@@ -17,7 +17,7 @@ type OpenAICompatibleConfig = z.input<typeof openAICompatibleConfigSchema>
export const providerOpenAI = defineProvider<OpenAICompatibleConfig>({
id: 'openai',
order: 1,
order: 5,
name: 'OpenAI',
nameLocalize: ({ t }) => t('settings.pages.providers.provider.openai.title'),
description: 'OpenAI',
@@ -12,6 +12,13 @@ import { isModelProvider } from '../types'
type OpenAICompatibleValidationCheck = 'connectivity' | 'model_list' | 'chat_completions'
interface OpenAICompatibleValidationOptions<TConfig extends { apiKey?: string, baseUrl?: string }> {
checks?: OpenAICompatibleValidationCheck[]
additionalHeaders?: Record<string, string>
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<TConfig extends { apiKey?: string | null, bas
}
}
export function createOpenAICompatibleValidators<TConfig extends { apiKey?: string, baseUrl?: string }>(options?: {
checks?: OpenAICompatibleValidationCheck[]
additionalHeaders?: Record<string, string>
}): ProviderDefinition<TConfig>['validators'] {
export function createOpenAICompatibleValidators<TConfig extends { apiKey?: string, baseUrl?: string }>(
options?: OpenAICompatibleValidationOptions<TConfig>,
): ProviderDefinition<TConfig>['validators'] {
const checks = options?.checks ?? ['connectivity', 'model_list', 'chat_completions']
const additionalHeaders = options?.additionalHeaders
@@ -100,6 +106,7 @@ export function createOpenAICompatibleValidators<TConfig extends { apiKey?: stri
connectivityOk: boolean
chatOk: boolean
errorMessage?: string
error?: unknown
}
const chatCheckCacheKey = 'openai-compatible:chat-check'
@@ -109,7 +116,7 @@ export function createOpenAICompatibleValidators<TConfig extends { apiKey?: stri
provider: ProviderInstance,
providerExtra: ProviderExtraMethods<TConfig> | undefined,
contextOptions?: { validationCache?: Map<string, unknown> },
) => {
): Promise<ChatCheckResult> => {
const cache = contextOptions?.validationCache
const existing = cache?.get(chatCheckCacheKey) as Promise<ChatCheckResult> | undefined
if (existing)
@@ -131,11 +138,11 @@ export function createOpenAICompatibleValidators<TConfig extends { apiKey?: stri
}
catch (e) {
if (isNetworkError(e)) {
return { connectivityOk: false, chatOk: false, errorMessage: errorMessageFrom(e) }
return { connectivityOk: false, chatOk: false, error: e, errorMessage: errorMessageFrom(e) }
}
const status = extractStatusCode(e)
const chatOk = status === 400 || (status && status >= 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<TConfig extends { apiKey?: stri
}
catch (e) {
if (isNetworkError(e)) {
return { connectivityOk: false, chatOk: false, errorMessage: errorMessageFrom(e) }
return { connectivityOk: false, chatOk: false, error: e, errorMessage: errorMessageFrom(e) }
}
const status = extractStatusCode(e)
const chatOk = status === 400 || (status && status >= 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<TConfig extends { apiKey?: stri
contextOptions as { validationCache?: Map<string, unknown> } | 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<TConfig extends { apiKey?: stri
}
}
catch (e) {
errors.push({ error: new Error(`Model list check failed: ${(e as Error).message}`) })
const errorMessage = errorMessageFrom(e) || 'Unknown error.'
const reason = options?.modelListFailureReason
? options.modelListFailureReason({ config, error: e, errorMessage })
: `Model list check failed: ${errorMessage}`
errors.push({ error: new Error(reason) })
}
return {