diff --git a/packages/stage-pages/src/pages/settings/airi-card/components/CardCreationDialog.vue b/packages/stage-pages/src/pages/settings/airi-card/components/CardCreationDialog.vue
index fc929ab90..2f2647787 100644
--- a/packages/stage-pages/src/pages/settings/airi-card/components/CardCreationDialog.vue
+++ b/packages/stage-pages/src/pages/settings/airi-card/components/CardCreationDialog.vue
@@ -4,6 +4,7 @@ import type { AiriExtension } from '@proj-airi/stage-ui/stores/modules/airi-card
import kebabcase from '@stdlib/string-base-kebabcase'
+import { isCustomProvidersDisabled } from '@proj-airi/stage-shared'
import { DEFAULT_ARTISTRY_WIDGET_INSTRUCTION } from '@proj-airi/stage-ui/constants/prompts/artistry-instruction'
import { useDisplayModelsStore } from '@proj-airi/stage-ui/stores/display-models'
import { useAiriCardStore } from '@proj-airi/stage-ui/stores/modules/airi-card'
@@ -155,9 +156,13 @@ const speechVoiceOptions = computed(() => {
const artistryProviderOptions = computed(() => {
return [
{ value: 'none', label: 'None (Disabled)' },
- { value: 'replicate', label: 'Replicate' },
{ value: 'comfyui', label: 'ComfyUI' },
- { value: 'nanobanana', label: 'Nano Banana' },
+ ...(isCustomProvidersDisabled()
+ ? []
+ : [
+ { value: 'replicate', label: 'Replicate' },
+ { value: 'nanobanana', label: 'Nano Banana' },
+ ]),
]
})
diff --git a/packages/stage-pages/src/pages/settings/modules/artistry.vue b/packages/stage-pages/src/pages/settings/modules/artistry.vue
index e5d1802d7..24eb1b3c4 100644
--- a/packages/stage-pages/src/pages/settings/modules/artistry.vue
+++ b/packages/stage-pages/src/pages/settings/modules/artistry.vue
@@ -1,4 +1,5 @@
diff --git a/packages/stage-pages/src/pages/settings/providers/index.vue b/packages/stage-pages/src/pages/settings/providers/index.vue
index a0344dcc7..d3ba78de7 100644
--- a/packages/stage-pages/src/pages/settings/providers/index.vue
+++ b/packages/stage-pages/src/pages/settings/providers/index.vue
@@ -2,6 +2,7 @@
import type { ProviderSourceDeployment, ProviderSourcePricing } from '@proj-airi/stage-ui/libs/providers/source-metadata'
import type { Ref } from 'vue'
+import { isCustomProvidersDisabled } from '@proj-airi/stage-shared'
import { IconStatusItem, RippleGrid } from '@proj-airi/stage-ui/components'
import { useAnalytics } from '@proj-airi/stage-ui/composables'
import { useRippleGridState } from '@proj-airi/stage-ui/composables/use-ripple-grid-state'
@@ -69,36 +70,40 @@ const allArtistryProvidersMetadata = computed(() => {
beginnerRecommended: true,
iconImage: undefined,
},
- {
- id: 'replicate',
- category: 'artistry',
- icon: 'i-lobe-icons:replicate',
- iconColor: 'i-lobe-icons:replicate-color',
- name: 'Replicate',
- localizedName: 'Replicate',
- description: t('settings.pages.providers.categories.artistry.items.replicate.description'),
- localizedDescription: t('settings.pages.providers.categories.artistry.items.replicate.description'),
- configured: !!artistryStore.replicateApiKey,
- to: '/settings/providers/artistry/replicate',
- pricing: 'paid',
- deployment: 'cloud',
- iconImage: undefined,
- },
- {
- id: 'nanobanana',
- category: 'artistry',
- icon: 'i-solar:gallery-round-bold-duotone',
- iconColor: 'text-amber-500',
- name: 'Nano Banana',
- localizedName: 'Nano Banana',
- description: t('settings.pages.providers.categories.artistry.items.nanobanana.description'),
- localizedDescription: t('settings.pages.providers.categories.artistry.items.nanobanana.description'),
- configured: !!artistryStore.nanobananaApiKey,
- to: '/settings/providers/artistry/nanobanana',
- pricing: 'free',
- deployment: 'cloud',
- iconImage: undefined,
- },
+ ...(isCustomProvidersDisabled()
+ ? []
+ : [
+ {
+ id: 'replicate',
+ category: 'artistry',
+ icon: 'i-lobe-icons:replicate',
+ iconColor: 'i-lobe-icons:replicate-color',
+ name: 'Replicate',
+ localizedName: 'Replicate',
+ description: t('settings.pages.providers.categories.artistry.items.replicate.description'),
+ localizedDescription: t('settings.pages.providers.categories.artistry.items.replicate.description'),
+ configured: !!artistryStore.replicateApiKey,
+ to: '/settings/providers/artistry/replicate',
+ pricing: 'paid',
+ deployment: 'cloud',
+ iconImage: undefined,
+ },
+ {
+ id: 'nanobanana',
+ category: 'artistry',
+ icon: 'i-solar:gallery-round-bold-duotone',
+ iconColor: 'text-amber-500',
+ name: 'Nano Banana',
+ localizedName: 'Nano Banana',
+ description: t('settings.pages.providers.categories.artistry.items.nanobanana.description'),
+ localizedDescription: t('settings.pages.providers.categories.artistry.items.nanobanana.description'),
+ configured: !!artistryStore.nanobananaApiKey,
+ to: '/settings/providers/artistry/nanobanana',
+ pricing: 'free',
+ deployment: 'cloud',
+ iconImage: undefined,
+ },
+ ]),
]
})
diff --git a/packages/stage-shared/src/env-vars.ts b/packages/stage-shared/src/env-vars.ts
index 894c64024..0a391dfb4 100644
--- a/packages/stage-shared/src/env-vars.ts
+++ b/packages/stage-shared/src/env-vars.ts
@@ -12,3 +12,7 @@ export function isEnvTruthy(value: string | undefined | null): boolean {
export function isFluxPurchaseDisabled(): boolean {
return isEnvTruthy(import.meta.env.VITE_DISABLE_FLUX_PURCHASE)
}
+
+export function isCustomProvidersDisabled(): boolean {
+ return isEnvTruthy(import.meta.env.VITE_DISABLE_CUSTOM_PROVIDERS)
+}
diff --git a/packages/stage-ui/src/stores/providers.ts b/packages/stage-ui/src/stores/providers.ts
index 4f676f177..75b599f19 100644
--- a/packages/stage-ui/src/stores/providers.ts
+++ b/packages/stage-ui/src/stores/providers.ts
@@ -24,7 +24,7 @@ import type { ProviderOnboardingField } from '../libs/providers/types'
import type { AliyunRealtimeSpeechExtraOptions } from './providers/aliyun/stream-transcription'
import { errorMessageFrom } from '@moeru/std'
-import { isStageTamagotchi, isUrl } from '@proj-airi/stage-shared'
+import { isCustomProvidersDisabled, isStageTamagotchi, isUrl } from '@proj-airi/stage-shared'
import { getCachedWebGPUCapabilities, isWebGPUSupported } from '@proj-airi/stage-shared/webgpu'
import { computedAsync, useIntervalFn, useLocalStorage } from '@vueuse/core'
import {
@@ -283,6 +283,7 @@ export const useProvidersStore = defineStore('providers', () => {
descriptionKey: 'settings.pages.providers.provider.speech-noop.description',
description: 'No speech output.',
icon: 'i-solar:volume-cross-bold-duotone',
+ requiresCredentials: false,
defaultOptions: () => ({}),
createProvider: async () => ({
speech: () => ({
@@ -857,6 +858,7 @@ export const useProvidersStore = defineStore('providers', () => {
descriptionKey: 'settings.pages.providers.provider.browser-web-speech-api.description',
description: 'Browser-native speech recognition. No API keys.',
icon: 'i-solar:microphone-bold-duotone',
+ requiresCredentials: false,
defaultOptions: () => ({
language: 'en-US',
continuous: true,
@@ -2033,6 +2035,7 @@ export const useProvidersStore = defineStore('providers', () => {
descriptionKey: 'settings.pages.providers.provider.kokoro-local.description',
description: 'Local text-to-speech using Kokoro-82M.',
icon: 'i-lobe-icons:speaker',
+ requiresCredentials: false,
defaultOptions: () => {
const capabilities = getCachedWebGPUCapabilities()
@@ -2744,8 +2747,11 @@ export const useProvidersStore = defineStore('providers', () => {
if (overrides[provider.id] === false)
continue
- const p = getProviderMetadata(provider.id)
- const isAvailableBy = p.isAvailableBy || (() => true)
+ const metadata = getProviderMetadata(provider.id)
+ if (isCustomProvidersDisabled() && metadata.requiresCredentials !== false)
+ continue
+
+ const isAvailableBy = metadata.isAvailableBy || (() => true)
const isAvailable = await isAvailableBy()
if (isAvailable) {
diff --git a/vite-env.d.ts b/vite-env.d.ts
index 6a644979e..11dc7cd1d 100644
--- a/vite-env.d.ts
+++ b/vite-env.d.ts
@@ -2,6 +2,7 @@
interface ImportMetaEnv {
readonly VITE_DISABLE_FLUX_PURCHASE?: string
+ readonly VITE_DISABLE_CUSTOM_PROVIDERS?: string
readonly VITE_ENABLE_POSTHOG?: string
readonly VITE_POSTHOG_PROJECT_KEY_WEB?: string
readonly VITE_POSTHOG_PROJECT_KEY_DESKTOP?: string