diff --git a/packages/i18n/src/locales/en/settings.yaml b/packages/i18n/src/locales/en/settings.yaml index e9e416045..04ef1a459 100644 --- a/packages/i18n/src/locales/en/settings.yaml +++ b/packages/i18n/src/locales/en/settings.yaml @@ -743,6 +743,9 @@ pages: openai-compatible: description: OpenAI Compatible title: OpenAI Compatible + nvidia: + description: build.nvidia.com + title: NVIDIA NIM n1n: description: n1n.ai - High-performance AI API provider. title: n1n diff --git a/packages/i18n/src/locales/es/settings.yaml b/packages/i18n/src/locales/es/settings.yaml index 4fc58ac1f..254672fec 100644 --- a/packages/i18n/src/locales/es/settings.yaml +++ b/packages/i18n/src/locales/es/settings.yaml @@ -711,6 +711,9 @@ pages: openai-compatible: description: OpenAI Compatible title: OpenAI Compatible + nvidia: + description: build.nvidia.com + title: NVIDIA NIM n1n: description: n1n.ai - High-performance AI API provider. title: n1n diff --git a/packages/i18n/src/locales/fr/settings.yaml b/packages/i18n/src/locales/fr/settings.yaml index d32e200c2..4e67c1599 100644 --- a/packages/i18n/src/locales/fr/settings.yaml +++ b/packages/i18n/src/locales/fr/settings.yaml @@ -711,6 +711,9 @@ pages: openai-compatible: description: Compatible avec OpenAI title: Compatible avec OpenAI + nvidia: + description: build.nvidia.com + title: NVIDIA NIM n1n: description: n1n.ai - Fournisseur d'API d'IA haute performance. title: n1n diff --git a/packages/i18n/src/locales/ja/settings.yaml b/packages/i18n/src/locales/ja/settings.yaml index 0890cc32a..4f984fb8f 100644 --- a/packages/i18n/src/locales/ja/settings.yaml +++ b/packages/i18n/src/locales/ja/settings.yaml @@ -711,6 +711,9 @@ pages: openai-compatible: description: OpenAI互換 title: OpenAI互換 + nvidia: + description: build.nvidia.com + title: NVIDIA NIM n1n: description: n1n.ai - 高性能なAI APIプロバイダー。 title: n1n diff --git a/packages/i18n/src/locales/ko/settings.yaml b/packages/i18n/src/locales/ko/settings.yaml index b28f3f638..267a54f7b 100644 --- a/packages/i18n/src/locales/ko/settings.yaml +++ b/packages/i18n/src/locales/ko/settings.yaml @@ -711,6 +711,9 @@ pages: openai-compatible: description: OpenAI 호환 title: OpenAI 호환 + nvidia: + description: build.nvidia.com + title: NVIDIA NIM n1n: description: n1n.ai - 고성능 AI API 제공자. title: n1n diff --git a/packages/i18n/src/locales/ru/settings.yaml b/packages/i18n/src/locales/ru/settings.yaml index 8bf1c4ff4..d95d71633 100644 --- a/packages/i18n/src/locales/ru/settings.yaml +++ b/packages/i18n/src/locales/ru/settings.yaml @@ -711,6 +711,9 @@ pages: openai-compatible: description: Совместимый с OpenAI title: OpenAI Compatible + nvidia: + description: build.nvidia.com + title: NVIDIA NIM n1n: description: n1n.ai - High-performance AI API provider. title: n1n diff --git a/packages/i18n/src/locales/vi/settings.yaml b/packages/i18n/src/locales/vi/settings.yaml index c1e7d96c3..313164f21 100644 --- a/packages/i18n/src/locales/vi/settings.yaml +++ b/packages/i18n/src/locales/vi/settings.yaml @@ -711,6 +711,9 @@ pages: openai-compatible: description: Tương thích OpenAI title: OpenAI Compatible + nvidia: + description: build.nvidia.com + title: NVIDIA NIM n1n: description: n1n.ai - High-performance AI API provider. title: n1n diff --git a/packages/i18n/src/locales/zh-Hans/settings.yaml b/packages/i18n/src/locales/zh-Hans/settings.yaml index 73daf66a3..590fb13a3 100644 --- a/packages/i18n/src/locales/zh-Hans/settings.yaml +++ b/packages/i18n/src/locales/zh-Hans/settings.yaml @@ -711,6 +711,9 @@ pages: openai-compatible: description: OpenAI 兼容 API title: OpenAI 兼容 API + nvidia: + description: build.nvidia.com + title: NVIDIA NIM n1n: description: n1n.ai - 高性能的 AI API 提供商 title: n1n diff --git a/packages/i18n/src/locales/zh-Hant/settings.yaml b/packages/i18n/src/locales/zh-Hant/settings.yaml index 292310e06..54074b6b5 100644 --- a/packages/i18n/src/locales/zh-Hant/settings.yaml +++ b/packages/i18n/src/locales/zh-Hant/settings.yaml @@ -711,6 +711,9 @@ pages: openai-compatible: description: OpenAI 相容 title: OpenAI Compatible + nvidia: + description: build.nvidia.com + title: NVIDIA NIM n1n: description: n1n.ai - High-performance AI API provider. title: n1n diff --git a/packages/stage-ui/src/components/scenarios/dialogs/onboarding/onboarding.vue b/packages/stage-ui/src/components/scenarios/dialogs/onboarding/onboarding.vue index d9151cf2f..8106d7780 100644 --- a/packages/stage-ui/src/components/scenarios/dialogs/onboarding/onboarding.vue +++ b/packages/stage-ui/src/components/scenarios/dialogs/onboarding/onboarding.vue @@ -30,7 +30,7 @@ const { // Popular providers for first-time setup const popularProviders = computed(() => { - const popular = ['openai', 'anthropic', 'google-generative-ai', 'groq', 'openrouter-ai', 'ollama', 'deepseek', 'player2', 'openai-compatible'] + const popular = ['openai', 'anthropic', 'google-generative-ai', 'groq', 'nvidia', 'openrouter-ai', 'ollama', 'deepseek', 'player2', 'openai-compatible'] return allChatProvidersMetadata.value .filter(provider => popular.includes(provider.id)) .sort((a, b) => popular.indexOf(a.id) - popular.indexOf(b.id)) diff --git a/packages/stage-ui/src/components/scenarios/dialogs/onboarding/step-model-selection.vue b/packages/stage-ui/src/components/scenarios/dialogs/onboarding/step-model-selection.vue index de4de2c22..e437a1ae5 100644 --- a/packages/stage-ui/src/components/scenarios/dialogs/onboarding/step-model-selection.vue +++ b/packages/stage-ui/src/components/scenarios/dialogs/onboarding/step-model-selection.vue @@ -35,9 +35,22 @@ const { -
+
+ + + + + - - - - -
diff --git a/packages/stage-ui/src/components/scenarios/dialogs/onboarding/step-provider-configuration.vue b/packages/stage-ui/src/components/scenarios/dialogs/onboarding/step-provider-configuration.vue index b3d436560..beeaf4e61 100644 --- a/packages/stage-ui/src/components/scenarios/dialogs/onboarding/step-provider-configuration.vue +++ b/packages/stage-ui/src/components/scenarios/dialogs/onboarding/step-provider-configuration.vue @@ -146,6 +146,7 @@ function getApiKeyPlaceholder(providerId: string): string { 'modelscope': 'ms-...', 'fireworks-ai': 'fw-...', 'featherless-ai': 'fw-...', + 'nvidia': 'nvapi-...', 'novita-ai': 'nvt-...', } diff --git a/packages/stage-ui/src/libs/providers/providers/index.ts b/packages/stage-ui/src/libs/providers/providers/index.ts index 5c169cc95..6f6f8bb2d 100644 --- a/packages/stage-ui/src/libs/providers/providers/index.ts +++ b/packages/stage-ui/src/libs/providers/providers/index.ts @@ -4,6 +4,7 @@ import './lm-studio' import './openai-compatible' import './n1n' import './openrouter-ai' +import './nvidia' import './groq' import './anthropic' import './google-generative-ai' diff --git a/packages/stage-ui/src/libs/providers/providers/nvidia/index.ts b/packages/stage-ui/src/libs/providers/providers/nvidia/index.ts new file mode 100644 index 000000000..e53ad5afd --- /dev/null +++ b/packages/stage-ui/src/libs/providers/providers/nvidia/index.ts @@ -0,0 +1,54 @@ +import { isStageTamagotchi } from '@proj-airi/stage-shared' +import { createOpenAI } from '@xsai-ext/providers/create' +import { z } from 'zod' + +import { createOpenAICompatibleValidators } from '../../validators/openai-compatible' +import { defineProvider } from '../registry' + +const nvidiaConfigSchema = z.object({ + apiKey: z + .string('API Key'), + baseUrl: z + .string('Base URL') + .optional() + .default('https://integrate.api.nvidia.com/v1/'), +}) + +type NvidiaConfig = z.input + +export const providerNvidia = defineProvider({ + id: 'nvidia', + name: 'NVIDIA NIM', + nameLocalize: ({ t }) => t('settings.pages.providers.provider.nvidia.title'), + description: 'build.nvidia.com', + descriptionLocalize: ({ t }) => t('settings.pages.providers.provider.nvidia.description'), + tasks: ['chat'], + icon: 'i-simple-icons:nvidia', + isAvailableBy: isStageTamagotchi, + + createProviderConfig: ({ t }) => nvidiaConfigSchema.extend({ + apiKey: nvidiaConfigSchema.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: nvidiaConfigSchema.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 createOpenAI(config.apiKey, config.baseUrl) + }, + + validationRequiredWhen(config) { + return !!config.apiKey?.trim() + }, + validators: { + ...createOpenAICompatibleValidators({ + checks: ['connectivity', 'model_list'], + }), + }, +}) diff --git a/packages/stage-ui/src/libs/providers/validators/openai-compatible.test.ts b/packages/stage-ui/src/libs/providers/validators/openai-compatible.test.ts new file mode 100644 index 000000000..dde3cb3e7 --- /dev/null +++ b/packages/stage-ui/src/libs/providers/validators/openai-compatible.test.ts @@ -0,0 +1,77 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { createOpenAICompatibleValidators } from './openai-compatible' + +const { + generateTextMock, + listModelsMock, +} = vi.hoisted(() => ({ + generateTextMock: vi.fn(), + listModelsMock: vi.fn(), +})) + +vi.mock('@xsai/generate-text', () => ({ + generateText: generateTextMock, +})) + +vi.mock('@xsai/model', () => ({ + listModels: listModelsMock, +})) + +function getProviderValidators(options?: Parameters[0]) { + const validators = createOpenAICompatibleValidators(options) + + return (validators?.validateProvider || []).map(create => create({ + t: (input: string) => input, + } as any)) +} + +describe('createOpenAICompatibleValidators', () => { + const config = { + apiKey: 'test-key', + baseUrl: 'https://example.com/v1/', + } + const provider = { + model: () => ({ + apiKey: config.apiKey, + baseURL: config.baseUrl, + }), + } + + beforeEach(() => { + vi.clearAllMocks() + }) + + it('does not probe chat completions with a synthetic fallback model', async () => { + listModelsMock.mockResolvedValue([]) + + const [connectivityValidator, chatValidator] = getProviderValidators({ + checks: ['connectivity', 'chat_completions'], + }) + + const connectivityResult = await connectivityValidator.validator(config, provider as any, undefined as any, undefined as any) + const chatResult = await chatValidator.validator(config, provider as any, undefined as any, undefined as any) + + expect(connectivityResult.valid).toBe(false) + expect(connectivityResult.reason).toContain('No model available for validation.') + expect(chatResult.valid).toBe(false) + expect(chatResult.reason).toContain('No model available for validation.') + expect(generateTextMock).not.toHaveBeenCalled() + }) + + it('allows providers to skip chat probing when they do not expose model listing', async () => { + listModelsMock.mockResolvedValue([]) + + const [connectivityValidator, chatValidator] = getProviderValidators({ + checks: ['connectivity', 'chat_completions'], + allowValidationWithoutModel: true, + }) + + const connectivityResult = await connectivityValidator.validator(config, provider as any, undefined as any, undefined as any) + const chatResult = await chatValidator.validator(config, provider as any, undefined as any, undefined as any) + + expect(connectivityResult.valid).toBe(true) + expect(chatResult.valid).toBe(true) + expect(generateTextMock).not.toHaveBeenCalled() + }) +}) 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 8079f3ba0..71bce86d2 100644 --- a/packages/stage-ui/src/libs/providers/validators/openai-compatible.ts +++ b/packages/stage-ui/src/libs/providers/validators/openai-compatible.ts @@ -15,6 +15,7 @@ type OpenAICompatibleValidationCheck = 'connectivity' | 'model_list' | 'chat_com interface OpenAICompatibleValidationOptions { checks?: OpenAICompatibleValidationCheck[] additionalHeaders?: Record + allowValidationWithoutModel?: boolean schedule?: { mode: 'once' | 'interval' intervalMs?: number @@ -88,16 +89,14 @@ async function pickValidationModel | undefined, -): Promise { - const fallback = 'test' - +): Promise { try { const models = await resolveModels(config, provider, providerExtra) const modelId = extractModelId(models.find(model => !shouldSkipModelId(extractModelId(model)))) - return modelId || fallback + return modelId || null } catch { - return fallback + return null } } @@ -106,6 +105,7 @@ export function createOpenAICompatibleValidators['validators'] { const checks = options?.checks ?? ['connectivity', 'model_list', 'chat_completions'] const additionalHeaders = options?.additionalHeaders + const missingValidationModelReason = 'No model available for validation. Configure a model manually and try again.' interface ChatCheckResult { connectivityOk: boolean @@ -114,6 +114,48 @@ export function createOpenAICompatibleValidators | undefined, + ): Promise { + const model = await pickValidationModel(config, provider, providerExtra) + + if (!model) { + if (options?.allowValidationWithoutModel) { + return { connectivityOk: true, chatOk: true } + } + + return { + connectivityOk: false, + chatOk: false, + errorMessage: missingValidationModelReason, + } + } + + try { + await generateText({ + apiKey: config.apiKey, + baseURL: config.baseUrl!, + headers: additionalHeaders, + model, + messages: message.messages(message.user('ping')), + max_tokens: 1, + }) + + return { connectivityOk: true, chatOk: true } + } + catch (e) { + if (isNetworkError(e)) { + return { connectivityOk: false, chatOk: false, error: e, errorMessage: errorMessageFrom(e) } + } + + const status = extractStatusCode(e) + const chatOk = status === 400 || Boolean(status && status >= 200 && status < 300) + return { connectivityOk: true, chatOk, errorMessage: errorMessageFrom(e) } + } + } + const chatCheckCacheKey = 'openai-compatible:chat-check' const chatCheckMutexKey = 'openai-compatible:chat-check:mutex' const getChatCheckResult = async ( @@ -128,28 +170,7 @@ export function createOpenAICompatibleValidators= 200 && status < 300) - return { connectivityOk: true, chatOk, errorMessage: errorMessageFrom(e) } - } + return runChatCheck(config, provider, providerExtra) } let mutex = cache.get(chatCheckMutexKey) as Mutex | undefined @@ -165,30 +186,7 @@ export function createOpenAICompatibleValidators { - const model = await pickValidationModel(config, provider, providerExtra) - try { - await generateText({ - apiKey: config.apiKey, - baseURL: config.baseUrl!, - headers: additionalHeaders, - model, - messages: message.messages(message.user('ping')), - max_tokens: 1, - }) - - return { connectivityOk: true, chatOk: true } - } - catch (e) { - if (isNetworkError(e)) { - return { connectivityOk: false, chatOk: false, error: e, errorMessage: errorMessageFrom(e) } - } - - const status = extractStatusCode(e) - const chatOk = status === 400 || Boolean(status && status >= 200 && status < 300) - return { connectivityOk: true, chatOk, errorMessage: errorMessageFrom(e) } - } - })() + const sharedCheck = runChatCheck(config, provider, providerExtra) cache.set(chatCheckCacheKey, sharedCheck)