feat(stage-ui/providers): added 302.AI

This commit is contained in:
Neko Ayaka
2025-09-25 15:14:14 +08:00
parent 7a0d7f3e58
commit 4a31fda922
11 changed files with 507 additions and 171 deletions
+1
View File
@@ -183,6 +183,7 @@ npx bumpp --no-commit --no-tag
## Support of LLM API Providers (powered by [xsai](https://github.com/moeru-ai/xsai))
- [x] [302.AI (sponsored)](https://share.302.ai/514k2v)
- [x] [OpenRouter](https://openrouter.ai/)
- [x] [vLLM](https://github.com/vllm-project/vllm)
- [x] [SGLang](https://github.com/sgl-project/sglang)
@@ -0,0 +1,104 @@
<script setup lang="ts">
import type { RemovableRef } from '@vueuse/core'
import {
Alert,
ProviderAdvancedSettings,
ProviderApiKeyInput,
ProviderBaseUrlInput,
ProviderBasicSettings,
ProviderSettingsContainer,
ProviderSettingsLayout,
} from '@proj-airi/stage-ui/components'
import { useProviderValidation } from '@proj-airi/stage-ui/composables/useProviderValidation'
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
import { storeToRefs } from 'pinia'
import { computed } from 'vue'
const providerId = '302-ai'
const providersStore = useProvidersStore()
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
// Define computed properties for credentials
const apiKey = computed({
get: () => providers.value[providerId]?.apiKey || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].apiKey = value
},
})
const baseUrl = computed({
get: () => providers.value[providerId]?.baseUrl || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].baseUrl = value
},
})
// Use the composable to get validation logic and state
const {
t,
router,
providerMetadata,
isValidating,
isValid,
validationMessage,
handleResetSettings,
} = useProviderValidation(providerId)
</script>
<template>
<ProviderSettingsLayout
:provider-name="providerMetadata?.localizedName"
:provider-icon-color="providerMetadata?.iconColor"
:on-back="() => router.back()"
>
<ProviderSettingsContainer>
<ProviderBasicSettings
:title="t('settings.pages.providers.common.section.basic.title')"
:description="t('settings.pages.providers.common.section.basic.description')"
:on-reset="handleResetSettings"
>
<ProviderApiKeyInput
v-model="apiKey"
:provider-name="providerMetadata?.localizedName"
placeholder="sk-..."
/>
</ProviderBasicSettings>
<ProviderAdvancedSettings :title="t('settings.pages.providers.common.section.advanced.title')">
<ProviderBaseUrlInput
v-model="baseUrl"
placeholder="https://api.302.ai/v1/"
/>
</ProviderAdvancedSettings>
<!-- Validation Status -->
<Alert v-if="!isValid && isValidating === 0 && validationMessage" type="error">
<template #title>
{{ t('settings.dialogs.onboarding.validationFailed') }}
</template>
<template v-if="validationMessage" #content>
<div class="whitespace-pre-wrap break-all">
{{ validationMessage }}
</div>
</template>
</Alert>
<Alert v-if="isValid && isValidating === 0" type="success">
<template #title>
{{ t('settings.dialogs.onboarding.validationSuccess') }}
</template>
</Alert>
</ProviderSettingsContainer>
</ProviderSettingsLayout>
</template>
<route lang="yaml">
meta:
layout: settings
stageTransition:
name: slide
</route>
@@ -0,0 +1,104 @@
<script setup lang="ts">
import type { RemovableRef } from '@vueuse/core'
import {
Alert,
ProviderAdvancedSettings,
ProviderApiKeyInput,
ProviderBaseUrlInput,
ProviderBasicSettings,
ProviderSettingsContainer,
ProviderSettingsLayout,
} from '@proj-airi/stage-ui/components'
import { useProviderValidation } from '@proj-airi/stage-ui/composables/useProviderValidation'
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
import { storeToRefs } from 'pinia'
import { computed } from 'vue'
const providerId = '302-ai'
const providersStore = useProvidersStore()
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
// Define computed properties for credentials
const apiKey = computed({
get: () => providers.value[providerId]?.apiKey || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].apiKey = value
},
})
const baseUrl = computed({
get: () => providers.value[providerId]?.baseUrl || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].baseUrl = value
},
})
// Use the composable to get validation logic and state
const {
t,
router,
providerMetadata,
isValidating,
isValid,
validationMessage,
handleResetSettings,
} = useProviderValidation(providerId)
</script>
<template>
<ProviderSettingsLayout
:provider-name="providerMetadata?.localizedName"
:provider-icon-color="providerMetadata?.iconColor"
:on-back="() => router.back()"
>
<ProviderSettingsContainer>
<ProviderBasicSettings
:title="t('settings.pages.providers.common.section.basic.title')"
:description="t('settings.pages.providers.common.section.basic.description')"
:on-reset="handleResetSettings"
>
<ProviderApiKeyInput
v-model="apiKey"
:provider-name="providerMetadata?.localizedName"
placeholder="sk-..."
/>
</ProviderBasicSettings>
<ProviderAdvancedSettings :title="t('settings.pages.providers.common.section.advanced.title')">
<ProviderBaseUrlInput
v-model="baseUrl"
placeholder="https://api.302.ai/v1/"
/>
</ProviderAdvancedSettings>
<!-- Validation Status -->
<Alert v-if="!isValid && isValidating === 0 && validationMessage" type="error">
<template #title>
{{ t('settings.dialogs.onboarding.validationFailed') }}
</template>
<template v-if="validationMessage" #content>
<div class="whitespace-pre-wrap break-all">
{{ validationMessage }}
</div>
</template>
</Alert>
<Alert v-if="isValid && isValidating === 0" type="success">
<template #title>
{{ t('settings.dialogs.onboarding.validationSuccess') }}
</template>
</Alert>
</ProviderSettingsContainer>
</ProviderSettingsLayout>
</template>
<route lang="yaml">
meta:
layout: settings
stageTransition:
name: slide
</route>
@@ -0,0 +1,104 @@
<script setup lang="ts">
import type { RemovableRef } from '@vueuse/core'
import {
Alert,
ProviderAdvancedSettings,
ProviderApiKeyInput,
ProviderBaseUrlInput,
ProviderBasicSettings,
ProviderSettingsContainer,
ProviderSettingsLayout,
} from '@proj-airi/stage-ui/components'
import { useProviderValidation } from '@proj-airi/stage-ui/composables/useProviderValidation'
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
import { storeToRefs } from 'pinia'
import { computed } from 'vue'
const providerId = '302-ai'
const providersStore = useProvidersStore()
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
// Define computed properties for credentials
const apiKey = computed({
get: () => providers.value[providerId]?.apiKey || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].apiKey = value
},
})
const baseUrl = computed({
get: () => providers.value[providerId]?.baseUrl || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].baseUrl = value
},
})
// Use the composable to get validation logic and state
const {
t,
router,
providerMetadata,
isValidating,
isValid,
validationMessage,
handleResetSettings,
} = useProviderValidation(providerId)
</script>
<template>
<ProviderSettingsLayout
:provider-name="providerMetadata?.localizedName"
:provider-icon-color="providerMetadata?.iconColor"
:on-back="() => router.back()"
>
<ProviderSettingsContainer>
<ProviderBasicSettings
:title="t('settings.pages.providers.common.section.basic.title')"
:description="t('settings.pages.providers.common.section.basic.description')"
:on-reset="handleResetSettings"
>
<ProviderApiKeyInput
v-model="apiKey"
:provider-name="providerMetadata?.localizedName"
placeholder="sk-..."
/>
</ProviderBasicSettings>
<ProviderAdvancedSettings :title="t('settings.pages.providers.common.section.advanced.title')">
<ProviderBaseUrlInput
v-model="baseUrl"
placeholder="https://api.302.ai/v1/"
/>
</ProviderAdvancedSettings>
<!-- Validation Status -->
<Alert v-if="!isValid && isValidating === 0 && validationMessage" type="error">
<template #title>
{{ t('settings.dialogs.onboarding.validationFailed') }}
</template>
<template v-if="validationMessage" #content>
<div class="whitespace-pre-wrap break-all">
{{ validationMessage }}
</div>
</template>
</Alert>
<Alert v-if="isValid && isValidating === 0" type="success">
<template #title>
{{ t('settings.dialogs.onboarding.validationSuccess') }}
</template>
</Alert>
</ProviderSettingsContainer>
</ProviderSettingsLayout>
</template>
<route lang="yaml">
meta:
layout: settings
stageTransition:
name: slide
</route>
+1
View File
@@ -181,6 +181,7 @@ npx bumpp --no-commit --no-tag
## Support des fournisseurs d'API LLM suivants (propulsé par [xsai](https://github.com/moeru-ai/xsai))
- [x] [302.AI](https://share.302.ai/514k2v)
- [x] [OpenRouter](https://openrouter.ai/)
- [x] [vLLM](https://github.com/vllm-project/vllm)
- [x] [SGLang](https://github.com/sgl-project/sglang)
+1
View File
@@ -158,6 +158,7 @@ pnpm dev:docs
## サポートされているLLM APIプロバイダー([xsai](https://github.com/moeru-ai/xsai)によって提供)
- [x] [302.AI](https://share.302.ai/514k2v)
- [x] [OpenRouter](https://openrouter.ai/)
- [x] [vLLM](https://github.com/vllm-project/vllm)
- [x] [SGLang](https://github.com/sgl-project/sglang)
+1
View File
@@ -182,6 +182,7 @@ npx bumpp --no-commit --no-tag
## Поддержка провайдеров LLM API (на базе [xsai](https://github.com/moeru-ai/xsai))
- [x] [302.AI](https://share.302.ai/514k2v)
- [x] [OpenRouter](https://openrouter.ai/)
- [x] [vLLM](https://github.com/vllm-project/vllm)
- [x] [SGLang](https://github.com/sgl-project/sglang)
+1
View File
@@ -180,6 +180,7 @@ npx bumpp --no-commit --no-tag
## Các LLM API hỗ trợ (cung cấp bởi [xsai](https://github.com/moeru-ai/xsai))
- [x] [302.AI](https://share.302.ai/514k2v)
- [x] [OpenRouter](https://openrouter.ai/)
- [x] [vLLM](https://github.com/vllm-project/vllm)
- [x] [SGLang](https://github.com/sgl-project/sglang)
+1
View File
@@ -150,6 +150,7 @@ pnpm -F @proj-airi/docs dev
## 原生支持的 LLM API 服务来源列表(由 [xsai](https://github.com/moeru-ai/xsai) 驱动)
- [x] [302.AI](https://share.302.ai/514k2v)
- [x] [OpenRouter](https://openrouter.ai/)
- [x] [vLLM](https://github.com/vllm-project/vllm)
- [x] [SGLang](https://github.com/sgl-project/sglang)
+172 -156
View File
@@ -36,6 +36,7 @@ import {
import { createOllama, createPlayer2 } from '@xsai-ext/providers-local'
import {
createChatProvider,
createEmbedProvider,
createMetadataProvider,
createModelProvider,
merge,
@@ -555,102 +556,6 @@ export const useProvidersStore = defineStore('providers', () => {
},
},
},
'vllm': {
id: 'vllm',
category: 'chat',
tasks: ['text-generation'],
nameKey: 'settings.pages.providers.provider.vllm.title',
name: 'vLLM',
descriptionKey: 'settings.pages.providers.provider.vllm.description',
description: 'vllm.ai',
iconColor: 'i-lobe-icons:vllm',
createProvider: async config => createOllama((config.baseUrl as string).trim()),
capabilities: {
listModels: async () => {
return [
{
id: 'llama-2-7b',
name: 'Llama 2 (7B)',
provider: 'vllm',
description: 'Meta\'s Llama 2 7B parameter model',
contextLength: 4096,
},
{
id: 'llama-2-13b',
name: 'Llama 2 (13B)',
provider: 'vllm',
description: 'Meta\'s Llama 2 13B parameter model',
contextLength: 4096,
},
{
id: 'llama-2-70b',
name: 'Llama 2 (70B)',
provider: 'vllm',
description: 'Meta\'s Llama 2 70B parameter model',
contextLength: 4096,
},
{
id: 'mistral-7b',
name: 'Mistral (7B)',
provider: 'vllm',
description: 'Mistral AI\'s 7B parameter model',
contextLength: 8192,
},
{
id: 'mixtral-8x7b',
name: 'Mixtral (8x7B)',
provider: 'vllm',
description: 'Mistral AI\'s Mixtral 8x7B MoE model',
contextLength: 32768,
},
{
id: 'custom',
name: 'Custom Model',
provider: 'vllm',
description: 'Specify a custom model name',
contextLength: 0,
},
]
},
},
validators: {
validateProviderConfig: (config) => {
if (!config.baseUrl) {
return {
errors: [new Error('Base URL is required.')],
reason: 'Base URL is required. Default to http://localhost:8000/v1/ for vLLM.',
valid: false,
}
}
const res = baseUrlValidator.value(config.baseUrl)
if (res) {
return res
}
// Check if the vLLM is reachable
return fetch(`${(config.baseUrl as string).trim()}models`, { headers: (config.headers as HeadersInit) || undefined })
.then((response) => {
const errors = [
!response.ok && new Error(`vLLM returned non-ok status code: ${response.statusText}`),
].filter(Boolean)
return {
errors,
reason: errors.filter(e => e).map(e => String(e)).join(', ') || '',
valid: response.ok,
}
})
.catch((err) => {
return {
errors: [err],
reason: `Failed to reach vLLM, error: ${String(err)} occurred.`,
valid: false,
}
})
},
},
},
'lm-studio': {
id: 'lm-studio',
category: 'chat',
@@ -942,55 +847,6 @@ export const useProvidersStore = defineStore('providers', () => {
tasks: ['speech-to-text', 'automatic-speech-recognition', 'asr', 'stt'],
creator: createOpenAI,
}),
'azure-ai-foundry': {
id: 'azure-ai-foundry',
category: 'chat',
tasks: ['text-generation'],
nameKey: 'settings.pages.providers.provider.azure-ai-foundry.title',
name: 'Azure AI Foundry',
descriptionKey: 'settings.pages.providers.provider.azure-ai-foundry.description',
description: 'azure.com',
icon: 'i-lobe-icons:microsoft',
defaultOptions: () => ({}),
createProvider: async (config) => {
return await createAzure({
apiKey: async () => (config.apiKey as string).trim(),
resourceName: config.resourceName as string,
apiVersion: config.apiVersion as string,
})
},
capabilities: {
listModels: async (config) => {
return [{ id: config.modelId }].map((model) => {
return {
id: model.id as string,
name: model.id as string,
provider: 'azure-ai-foundry',
description: '',
contextLength: 0,
deprecated: false,
} satisfies ModelInfo
})
},
},
validators: {
validateProviderConfig: (config) => {
// return !!config.apiKey && !!config.resourceName && !!config.modelId
const errors = [
!config.apiKey && new Error('API key is required'),
!config.resourceName && new Error('Resource name is required'),
!config.modelId && new Error('Model ID is required'),
]
return {
errors,
reason: errors.filter(e => e).map(e => String(e)).join(', ') || '',
valid: !!config.apiKey && !!config.resourceName && !!config.modelId,
}
},
},
},
'anthropic': buildOpenAICompatibleProvider({
id: 'anthropic',
name: 'Anthropic',
@@ -1016,17 +872,6 @@ export const useProvidersStore = defineStore('providers', () => {
creator: createGoogleGenerativeAI,
validation: ['health', 'model_list'],
}),
'xai': buildOpenAICompatibleProvider({
id: 'xai',
name: 'xAI',
nameKey: 'settings.pages.providers.provider.xai.title',
descriptionKey: 'settings.pages.providers.provider.xai.description',
icon: 'i-lobe-icons:xai',
description: 'x.ai',
defaultBaseUrl: 'https://api.x.ai/v1/',
creator: createXAI,
validation: ['health', 'model_list'],
}),
'deepseek': buildOpenAICompatibleProvider({
id: 'deepseek',
name: 'DeepSeek',
@@ -1038,6 +883,21 @@ export const useProvidersStore = defineStore('providers', () => {
creator: createDeepSeek,
validation: ['health', 'model_list'],
}),
'302-ai': buildOpenAICompatibleProvider({
id: '302-ai',
name: '302.AI',
nameKey: 'settings.pages.providers.provider.302-ai.title',
descriptionKey: 'settings.pages.providers.provider.302-ai.description',
icon: 'i-lobe-icons:ai302',
description: '302.ai',
defaultBaseUrl: 'https://api.302.ai/v1/',
creator: (apiKey, baseURL = 'https://api.302.ai/v1/') => merge(
createChatProvider({ apiKey, baseURL }),
createEmbedProvider({ apiKey, baseURL }),
createModelProvider({ apiKey, baseURL }),
),
validation: ['model_list'],
}),
'elevenlabs': {
id: 'elevenlabs',
category: 'speech',
@@ -1402,6 +1262,162 @@ export const useProvidersStore = defineStore('providers', () => {
validation: ['health', 'model_list'],
iconColor: 'i-lobe-icons:together',
}),
'azure-ai-foundry': {
id: 'azure-ai-foundry',
category: 'chat',
tasks: ['text-generation'],
nameKey: 'settings.pages.providers.provider.azure-ai-foundry.title',
name: 'Azure AI Foundry',
descriptionKey: 'settings.pages.providers.provider.azure-ai-foundry.description',
description: 'azure.com',
icon: 'i-lobe-icons:microsoft',
defaultOptions: () => ({}),
createProvider: async (config) => {
return await createAzure({
apiKey: async () => (config.apiKey as string).trim(),
resourceName: config.resourceName as string,
apiVersion: config.apiVersion as string,
})
},
capabilities: {
listModels: async (config) => {
return [{ id: config.modelId }].map((model) => {
return {
id: model.id as string,
name: model.id as string,
provider: 'azure-ai-foundry',
description: '',
contextLength: 0,
deprecated: false,
} satisfies ModelInfo
})
},
},
validators: {
validateProviderConfig: (config) => {
// return !!config.apiKey && !!config.resourceName && !!config.modelId
const errors = [
!config.apiKey && new Error('API key is required'),
!config.resourceName && new Error('Resource name is required'),
!config.modelId && new Error('Model ID is required'),
]
return {
errors,
reason: errors.filter(e => e).map(e => String(e)).join(', ') || '',
valid: !!config.apiKey && !!config.resourceName && !!config.modelId,
}
},
},
},
'xai': buildOpenAICompatibleProvider({
id: 'xai',
name: 'xAI',
nameKey: 'settings.pages.providers.provider.xai.title',
descriptionKey: 'settings.pages.providers.provider.xai.description',
icon: 'i-lobe-icons:xai',
description: 'x.ai',
defaultBaseUrl: 'https://api.x.ai/v1/',
creator: createXAI,
validation: ['health', 'model_list'],
}),
'vllm': {
id: 'vllm',
category: 'chat',
tasks: ['text-generation'],
nameKey: 'settings.pages.providers.provider.vllm.title',
name: 'vLLM',
descriptionKey: 'settings.pages.providers.provider.vllm.description',
description: 'vllm.ai',
iconColor: 'i-lobe-icons:vllm',
createProvider: async config => createOllama((config.baseUrl as string).trim()),
capabilities: {
listModels: async () => {
return [
{
id: 'llama-2-7b',
name: 'Llama 2 (7B)',
provider: 'vllm',
description: 'Meta\'s Llama 2 7B parameter model',
contextLength: 4096,
},
{
id: 'llama-2-13b',
name: 'Llama 2 (13B)',
provider: 'vllm',
description: 'Meta\'s Llama 2 13B parameter model',
contextLength: 4096,
},
{
id: 'llama-2-70b',
name: 'Llama 2 (70B)',
provider: 'vllm',
description: 'Meta\'s Llama 2 70B parameter model',
contextLength: 4096,
},
{
id: 'mistral-7b',
name: 'Mistral (7B)',
provider: 'vllm',
description: 'Mistral AI\'s 7B parameter model',
contextLength: 8192,
},
{
id: 'mixtral-8x7b',
name: 'Mixtral (8x7B)',
provider: 'vllm',
description: 'Mistral AI\'s Mixtral 8x7B MoE model',
contextLength: 32768,
},
{
id: 'custom',
name: 'Custom Model',
provider: 'vllm',
description: 'Specify a custom model name',
contextLength: 0,
},
]
},
},
validators: {
validateProviderConfig: (config) => {
if (!config.baseUrl) {
return {
errors: [new Error('Base URL is required.')],
reason: 'Base URL is required. Default to http://localhost:8000/v1/ for vLLM.',
valid: false,
}
}
const res = baseUrlValidator.value(config.baseUrl)
if (res) {
return res
}
// Check if the vLLM is reachable
return fetch(`${(config.baseUrl as string).trim()}models`, { headers: (config.headers as HeadersInit) || undefined })
.then((response) => {
const errors = [
!response.ok && new Error(`vLLM returned non-ok status code: ${response.statusText}`),
].filter(Boolean)
return {
errors,
reason: errors.filter(e => e).map(e => String(e)).join(', ') || '',
valid: response.ok,
}
})
.catch((err) => {
return {
errors: [err],
reason: `Failed to reach vLLM, error: ${String(err)} occurred.`,
valid: false,
}
})
},
},
},
'novita-ai': buildOpenAICompatibleProvider({
id: 'novita-ai',
name: 'Novita',
@@ -28,25 +28,27 @@ export function buildOpenAICompatibleProvider(
const finalCapabilities = capabilities || {
listModels: async (config: Record<string, unknown>) => {
const provider = creator(
const provider = await creator(
(config.apiKey as string || '').trim(),
(config.baseUrl as string || '').trim(),
)
if (provider.model) {
return (await listModels({
...provider.model(),
})).map((model: any) => {
return {
id: model.id,
name: model.name || model.display_name || model.id,
provider: id,
description: model.description || '',
contextLength: model.context_length || 0,
deprecated: false,
} satisfies ModelInfo
})
if (!provider.model) {
return []
}
return []
return (await listModels({
...provider.model(),
})).map((model: any) => {
return {
id: model.id,
name: model.name || model.display_name || model.id,
provider: id,
description: model.description || '',
contextLength: model.context_length || 0,
deprecated: false,
} satisfies ModelInfo
})
},
}