revert(provider-inference): remove API Route provider

This reverts commit c097190cb7.
This commit is contained in:
Neko Ayaka
2026-10-05 16:48:31 +08:00
parent 9e0877e0b3
commit edbbcf5384
5 changed files with 184 additions and 253 deletions
@@ -1,53 +0,0 @@
import { createOpenAI } from '@xsai-ext/providers/create'
import { z } from 'zod'
import { ProviderValidationCheck } from '../../../types'
import { createOpenAICompatibleValidators } from '../../../validators'
import { defineProvider } from '../../registry'
const apiRouteConfigSchema = z.object({
apiKey: z
.string('API Key'),
baseUrl: z
.string('Base URL')
.optional()
.default('https://global.api-route.com/v1'),
})
type ApiRouteConfig = z.input<typeof apiRouteConfigSchema>
export const providerApiRoute = defineProvider<ApiRouteConfig, 'api-route'>({
id: 'api-route',
name: 'API Route',
nameLocalize: () => 'API Route',
description: 'api-route.com',
descriptionLocalize: () => 'api-route.com',
tasks: ['chat'],
icon: 'i-lobe-icons:openai',
createProviderConfig: ({ t }) => apiRouteConfigSchema.extend({
apiKey: apiRouteConfigSchema.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: apiRouteConfigSchema.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: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList, ProviderValidationCheck.ChatCompletions],
}),
},
})
+139 -141
View File
@@ -1,150 +1,148 @@
import type { ProviderDefinition } from '../types'
import { provider302AI } from './cloud/302-ai'
import type { ProviderDefinition } from '../types'
import { provider302AI } from './cloud/302-ai'
import { providerAIHubMix } from './cloud/aihubmix'
import { providerAmazonBedrock } from './cloud/amazon-bedrock'
import { providerAnthropic } from './cloud/anthropic'
import { providerApiRoute } from './cloud/api-route'
import { providerAtlasCloud } from './cloud/atlascloud'
import { providerAzureAIFoundry } from './cloud/azure-ai-foundry'
import { providerAzureOpenAI } from './cloud/azure-openai'
import { providerBytePlus } from './cloud/byteplus'
import { providerBytePlusCodingPlan } from './cloud/byteplus-coding-plan'
import { providerCerebrasAI } from './cloud/cerebras-ai'
import { providerCloudflareWorkersAI } from './cloud/cloudflare-workers-ai'
import { providerCometAPI, providerCometAPISpeech, providerCometAPITranscription } from './cloud/comet-api'
import { providerDeepSeek } from './cloud/deepseek'
import { providerElevenLabs } from './cloud/elevenlabs'
import { providerFeatherlessAI } from './cloud/featherless-ai'
import { providerFireworksAI } from './cloud/fireworks-ai'
import { providerGoogleGeminiAudioSpeech } from './cloud/google-gemini-audio-speech'
import { providerGoogleGenerativeAI } from './cloud/google-generative-ai'
import { providerGroq } from './cloud/groq'
import { providerMimo } from './cloud/mimo'
import { providerMimoAudioSpeech, providerMimoAudioTranscription } from './cloud/mimo-audio'
import { providerMinimax, providerMinimaxGlobal } from './cloud/minimax'
import { providerMinimaxSpeech } from './cloud/minimax-speech'
import { providerMistralAI } from './cloud/mistral-ai'
import { providerModelScope } from './cloud/modelscope'
import { providerMoonshotAI } from './cloud/moonshot-ai'
import { providerN1N } from './cloud/n1n'
import { providerNovitaAI } from './cloud/novita-ai'
import { providerOpenAI } from './cloud/openai'
import {
providerOpenAIAudioSpeech,
providerOpenAIAudioTranscription,
providerOpenAICompatibleAudioSpeech,
providerOpenAICompatibleAudioTranscription,
} from './cloud/openai-audio'
import { providerOpenAICompatible } from './cloud/openai-compatible'
import { providerOpenPaths } from './cloud/openpaths'
import { providerOpenRouterAI } from './cloud/openrouter-ai'
import { providerOpenRouterAudioSpeech } from './cloud/openrouter-audio-speech'
import { providerPerplexityAI } from './cloud/perplexity-ai'
import { providerTogetherAI } from './cloud/together-ai'
import {
providerAlibabaCloudModelStudio,
providerDeepgramTts,
providerMicrosoftSpeech,
providerVolcengineSpeech,
} from './cloud/unspeech'
import { providerVolcengineCodingPlan } from './cloud/volcengine-coding-plan'
import { providerXAI } from './cloud/xai'
import { providerZai } from './cloud/zai'
import { providerBrowserWebSpeechApi } from './local/browser-web-speech-api'
import { providerIndexTtsVllm } from './local/index-tts-vllm'
import { providerLmStudio } from './local/lm-studio'
import { providerOllama } from './local/ollama'
import { providerPlayer2Speech } from './local/player2-speech'
import { providerSpeechNoop } from './local/speech-noop'
import { providerAivisSpeech, providerVoicevox } from './local/voicevox'
/**
* Erases configuration types at the registry boundary.
*
* A registry selects definitions by a runtime id. It cannot know the selected
* configuration type. Individual provider exports preserve their exact type.
*/
type ProviderDefinitionRegistration<TId extends string = string> = Pick<ProviderDefinition, 'name' | 'order'> & { id: TId }
type ErasedProviderDefinitions<TDefinitions extends readonly ProviderDefinitionRegistration[]> = {
[K in keyof TDefinitions]: TDefinitions[K] extends ProviderDefinitionRegistration<infer TId>
? ProviderDefinition<Record<string, unknown>, TId>
: never
}
function eraseProviderDefinitions<const TDefinitions extends readonly ProviderDefinitionRegistration[]>(
...definitions: TDefinitions
): ErasedProviderDefinitions<TDefinitions> {
return definitions as unknown as ErasedProviderDefinitions<TDefinitions>
}
/**
* Definitions that can load without Vue, Pinia, persistence, or Electron.
*
* This list includes Browser-only definitions. Hosts must call
* `isAvailableBy` before they offer one to a user in the current runtime.
*/
export const portableProviderDefinitions = eraseProviderDefinitions(
import { providerAzureAIFoundry } from './cloud/azure-ai-foundry'
import { providerAzureOpenAI } from './cloud/azure-openai'
import { providerBytePlus } from './cloud/byteplus'
import { providerBytePlusCodingPlan } from './cloud/byteplus-coding-plan'
import { providerCerebrasAI } from './cloud/cerebras-ai'
import { providerCloudflareWorkersAI } from './cloud/cloudflare-workers-ai'
import { providerCometAPI, providerCometAPISpeech, providerCometAPITranscription } from './cloud/comet-api'
import { providerDeepSeek } from './cloud/deepseek'
import { providerElevenLabs } from './cloud/elevenlabs'
import { providerFeatherlessAI } from './cloud/featherless-ai'
import { providerFireworksAI } from './cloud/fireworks-ai'
import { providerGoogleGeminiAudioSpeech } from './cloud/google-gemini-audio-speech'
import { providerGoogleGenerativeAI } from './cloud/google-generative-ai'
import { providerGroq } from './cloud/groq'
import { providerMimo } from './cloud/mimo'
import { providerMimoAudioSpeech, providerMimoAudioTranscription } from './cloud/mimo-audio'
import { providerMinimax, providerMinimaxGlobal } from './cloud/minimax'
import { providerMinimaxSpeech } from './cloud/minimax-speech'
import { providerMistralAI } from './cloud/mistral-ai'
import { providerModelScope } from './cloud/modelscope'
import { providerMoonshotAI } from './cloud/moonshot-ai'
import { providerN1N } from './cloud/n1n'
import { providerNovitaAI } from './cloud/novita-ai'
import { providerOpenAI } from './cloud/openai'
import {
providerOpenAIAudioSpeech,
providerOpenAIAudioTranscription,
providerOpenAICompatibleAudioSpeech,
providerOpenAICompatibleAudioTranscription,
} from './cloud/openai-audio'
import { providerOpenAICompatible } from './cloud/openai-compatible'
import { providerOpenPaths } from './cloud/openpaths'
import { providerOpenRouterAI } from './cloud/openrouter-ai'
import { providerOpenRouterAudioSpeech } from './cloud/openrouter-audio-speech'
import { providerPerplexityAI } from './cloud/perplexity-ai'
import { providerTogetherAI } from './cloud/together-ai'
import {
providerAlibabaCloudModelStudio,
providerDeepgramTts,
providerMicrosoftSpeech,
providerVolcengineSpeech,
} from './cloud/unspeech'
import { providerVolcengineCodingPlan } from './cloud/volcengine-coding-plan'
import { providerXAI } from './cloud/xai'
import { providerZai } from './cloud/zai'
import { providerBrowserWebSpeechApi } from './local/browser-web-speech-api'
import { providerIndexTtsVllm } from './local/index-tts-vllm'
import { providerLmStudio } from './local/lm-studio'
import { providerOllama } from './local/ollama'
import { providerPlayer2Speech } from './local/player2-speech'
import { providerSpeechNoop } from './local/speech-noop'
import { providerAivisSpeech, providerVoicevox } from './local/voicevox'
/**
* Erases configuration types at the registry boundary.
*
* A registry selects definitions by a runtime id. It cannot know the selected
* configuration type. Individual provider exports preserve their exact type.
*/
type ProviderDefinitionRegistration<TId extends string = string> = Pick<ProviderDefinition, 'name' | 'order'> & { id: TId }
type ErasedProviderDefinitions<TDefinitions extends readonly ProviderDefinitionRegistration[]> = {
[K in keyof TDefinitions]: TDefinitions[K] extends ProviderDefinitionRegistration<infer TId>
? ProviderDefinition<Record<string, unknown>, TId>
: never
}
function eraseProviderDefinitions<const TDefinitions extends readonly ProviderDefinitionRegistration[]>(
...definitions: TDefinitions
): ErasedProviderDefinitions<TDefinitions> {
return definitions as unknown as ErasedProviderDefinitions<TDefinitions>
}
/**
* Definitions that can load without Vue, Pinia, persistence, or Electron.
*
* This list includes Browser-only definitions. Hosts must call
* `isAvailableBy` before they offer one to a user in the current runtime.
*/
export const portableProviderDefinitions = eraseProviderDefinitions(
provider302AI,
providerAIHubMix,
providerAmazonBedrock,
providerAnthropic,
providerApiRoute,
providerAtlasCloud,
providerAzureAIFoundry,
providerAzureOpenAI,
providerBytePlus,
providerBytePlusCodingPlan,
providerCerebrasAI,
providerCloudflareWorkersAI,
providerCometAPI,
providerCometAPISpeech,
providerCometAPITranscription,
providerDeepSeek,
providerElevenLabs,
providerFeatherlessAI,
providerFireworksAI,
providerGoogleGeminiAudioSpeech,
providerGoogleGenerativeAI,
providerGroq,
providerMinimax,
providerMinimaxGlobal,
providerMinimaxSpeech,
providerMimo,
providerMimoAudioSpeech,
providerMimoAudioTranscription,
providerMistralAI,
providerModelScope,
providerMoonshotAI,
providerN1N,
providerNovitaAI,
providerOpenAI,
providerOpenAIAudioSpeech,
providerOpenAIAudioTranscription,
providerOpenAICompatibleAudioSpeech,
providerOpenAICompatibleAudioTranscription,
providerOpenAICompatible,
providerOpenPaths,
providerOpenRouterAI,
providerOpenRouterAudioSpeech,
providerPerplexityAI,
providerTogetherAI,
providerAlibabaCloudModelStudio,
providerDeepgramTts,
providerMicrosoftSpeech,
providerVolcengineSpeech,
providerVolcengineCodingPlan,
providerXAI,
providerZai,
providerBrowserWebSpeechApi,
providerIndexTtsVllm,
providerLmStudio,
providerOllama,
providerPlayer2Speech,
providerSpeechNoop,
providerAivisSpeech,
providerVoicevox,
)
export { createProviderRegistry, defineProvider } from './registry'
providerAzureAIFoundry,
providerAzureOpenAI,
providerBytePlus,
providerBytePlusCodingPlan,
providerCerebrasAI,
providerCloudflareWorkersAI,
providerCometAPI,
providerCometAPISpeech,
providerCometAPITranscription,
providerDeepSeek,
providerElevenLabs,
providerFeatherlessAI,
providerFireworksAI,
providerGoogleGeminiAudioSpeech,
providerGoogleGenerativeAI,
providerGroq,
providerMinimax,
providerMinimaxGlobal,
providerMinimaxSpeech,
providerMimo,
providerMimoAudioSpeech,
providerMimoAudioTranscription,
providerMistralAI,
providerModelScope,
providerMoonshotAI,
providerN1N,
providerNovitaAI,
providerOpenAI,
providerOpenAIAudioSpeech,
providerOpenAIAudioTranscription,
providerOpenAICompatibleAudioSpeech,
providerOpenAICompatibleAudioTranscription,
providerOpenAICompatible,
providerOpenPaths,
providerOpenRouterAI,
providerOpenRouterAudioSpeech,
providerPerplexityAI,
providerTogetherAI,
providerAlibabaCloudModelStudio,
providerDeepgramTts,
providerMicrosoftSpeech,
providerVolcengineSpeech,
providerVolcengineCodingPlan,
providerXAI,
providerZai,
providerBrowserWebSpeechApi,
providerIndexTtsVllm,
providerLmStudio,
providerOllama,
providerPlayer2Speech,
providerSpeechNoop,
providerAivisSpeech,
providerVoicevox,
)
export { createProviderRegistry, defineProvider } from './registry'
@@ -1,50 +1,48 @@
import type { PortableProviderId } from '../index'
import { describe, expect, expectTypeOf, it } from 'vitest'
import { createProviderRegistry, getDefinedProvider, listProviders, portableProviderDefinitions } from '../index'
describe('portable provider registry', () => {
import type { PortableProviderId } from '../index'
import { describe, expect, expectTypeOf, it } from 'vitest'
import { createProviderRegistry, getDefinedProvider, listProviders, portableProviderDefinitions } from '../index'
describe('portable provider registry', () => {
it('exports the portable provider id union', () => {
expectTypeOf<'api-route'>().toExtend<PortableProviderId>()
expectTypeOf<'openai'>().toExtend<PortableProviderId>()
expectTypeOf<string>().not.toExtend<PortableProviderId>()
expect(getDefinedProvider('api-route')).toBeDefined()
expectTypeOf<string>().not.toExtend<PortableProviderId>()
expect(getDefinedProvider('openai')).toBeDefined()
})
it('registers each portable definition by id', () => {
for (const definition of portableProviderDefinitions)
expect(getDefinedProvider(definition.id)).toBe(definition)
})
it('lists definitions in deterministic display order', () => {
expect(listProviders()).toEqual(listProviders())
})
it('places definitions without an order after all ordered definitions', () => {
const [template] = portableProviderDefinitions
const registry = createProviderRegistry([
{ ...template, id: 'unordered-z', name: 'Zeta', order: undefined },
{ ...template, id: 'high-order', name: 'High order', order: 100_000 },
{ ...template, id: 'unordered-a', name: 'Alpha', order: undefined },
])
expect(registry.list().map(provider => provider.id)).toEqual([
'high-order',
'unordered-a',
'unordered-z',
])
})
it('does not include providers that require application runtime adapters', () => {
const providerIds = new Set(listProviders().map(provider => provider.id))
expect(providerIds).not.toContain('official-provider')
expect(providerIds).not.toContain('apple-speech-transcription')
expect(providerIds).not.toContain('kokoro-local')
expect(providerIds).not.toContain('browser-local-audio-speech')
expect(providerIds).not.toContain('aliyun-nls-transcription')
})
})
})
it('registers each portable definition by id', () => {
for (const definition of portableProviderDefinitions)
expect(getDefinedProvider(definition.id)).toBe(definition)
})
it('lists definitions in deterministic display order', () => {
expect(listProviders()).toEqual(listProviders())
})
it('places definitions without an order after all ordered definitions', () => {
const [template] = portableProviderDefinitions
const registry = createProviderRegistry([
{ ...template, id: 'unordered-z', name: 'Zeta', order: undefined },
{ ...template, id: 'high-order', name: 'High order', order: 100_000 },
{ ...template, id: 'unordered-a', name: 'Alpha', order: undefined },
])
expect(registry.list().map(provider => provider.id)).toEqual([
'high-order',
'unordered-a',
'unordered-z',
])
})
it('does not include providers that require application runtime adapters', () => {
const providerIds = new Set(listProviders().map(provider => provider.id))
expect(providerIds).not.toContain('official-provider')
expect(providerIds).not.toContain('apple-speech-transcription')
expect(providerIds).not.toContain('kokoro-local')
expect(providerIds).not.toContain('browser-local-audio-speech')
expect(providerIds).not.toContain('aliyun-nls-transcription')
})
})
@@ -44,7 +44,6 @@ const providerAttributesById = {
'aliyun-nls-transcription': paidCloud,
'amazon-bedrock': paidCloud,
'anthropic': paidCloud,
'api-route': paidCloud,
'app-local-audio-speech': freeLocal,
'app-local-audio-transcription': freeLocal,
'apple-vision': freeLocal,
@@ -72,15 +72,4 @@ describe('provider metadata selector', () => {
expect(definition.id).toBe('test-provider')
expect(definition.tasks).toEqual(['chat'])
})
// https://github.com/moeru-ai/airi/pull/2526#discussion_r3994122656
it('keeps API Route in the paid and cloud catalogue filters', async () => {
const metadata = await selectProviderMetadata({ ...definition, id: 'api-route' }, t)
const paidProviders = [metadata].filter(provider => provider.pricing === 'paid')
const cloudProviders = [metadata].filter(provider => provider.deployment === 'cloud')
expect(paidProviders).toEqual([metadata])
expect(cloudProviders).toEqual([metadata])
})
})