refactor(stage-ui): extract provider-inference package for runtime-neutral definitions (#2444)
## Summary Extract runtime-neutral provider definitions from `@proj-airi/stage-ui` into a new `@proj-airi/provider-inference` package that runs in both Node.js and browser without Vue/Pinia dependencies. ## What changed - **New package** `packages/provider-inference/` — owns `ProviderDefinition`, `ProviderRegistry`, validators, and all provider modules (cloud + local) - **Provider modules relocated** — ~45 providers moved from `stage-ui/src/libs/providers/providers/` to `provider-inference/src/providers/cloud/` and `provider-inference/src/providers/local/` - **Type decoupling** — `ProviderContext.t` changed from `ComposerTranslation` (Vue) to generic `ProviderTranslator`, removing the Vue dependency from core types - **stage-ui types slimmed** — `types.ts` now re-exports from `provider-inference` and only adds the Vue-specific `ProviderViews` extension - **validators/run.ts** — becomes a pass-through re-export - **Registry** — portable `createProviderRegistry()` with deterministic ordering and duplicate-id detection ## Verification ```bash pnpm -F @proj-airi/provider-inference typecheck pnpm -F @proj-airi/provider-inference test:node pnpm -F @proj-airi/provider-inference test:browser pnpm -F @proj-airi/provider-inference build pnpm -F @proj-airi/stage-ui typecheck pnpm -F @proj-airi/stage-web typecheck pnpm -F @proj-airi/stage-tamagotchi typecheck pnpm lint ``` ## Why Provider definitions were tightly coupled to Vue (`ComposerTranslation`, `Component`), preventing reuse in non-Vue runtimes (Node.js services, tests, future Electron backend). This extraction creates a clean boundary: runtime-neutral definitions live in `provider-inference`, while Vue/Pinia-specific concerns stay in `stage-ui`.
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
# Provider Inference
|
||||
|
||||
`@proj-airi/provider-inference` owns runtime-neutral AIRI provider definitions.
|
||||
|
||||
Use this package to list built-in providers, create provider configuration schemas, and create provider instances. The package runs in Node.js and Browser runtimes.
|
||||
|
||||
Do not use this package for provider configuration persistence, Vue views, Pinia state, authentication, or Electron-native providers. Those concerns remain in `@proj-airi/stage-ui`.
|
||||
|
||||
## Use
|
||||
|
||||
```ts
|
||||
import { getDefinedProvider, listProviders } from '@proj-airi/provider-inference'
|
||||
|
||||
const provider = getDefinedProvider('openai')
|
||||
const providers = listProviders()
|
||||
```
|
||||
|
||||
Browser-only definitions, such as Web Speech API, load in Node.js. Their availability hook returns `false` when the required Browser capability is absent.
|
||||
|
||||
Use `@proj-airi/stage-ui` for saved provider configuration, Vue settings views, Pinia state, authentication, and Electron-native providers. Do not use this package to manage those application concerns.
|
||||
|
||||
## Verify
|
||||
|
||||
Run the package checks from the workspace root:
|
||||
|
||||
```text
|
||||
pnpm -F @proj-airi/provider-inference typecheck
|
||||
pnpm -F @proj-airi/provider-inference test:node
|
||||
pnpm -F @proj-airi/provider-inference test:browser
|
||||
pnpm -F @proj-airi/provider-inference build
|
||||
```
|
||||
@@ -0,0 +1,809 @@
|
||||
# Provider Inference Extraction Specification
|
||||
|
||||
Status: Proposed for architecture review
|
||||
|
||||
Phase: 1 of the provider extraction
|
||||
|
||||
Target package: `@proj-airi/provider-inference`
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
This specification defines the first provider extraction from `@proj-airi/stage-ui`.
|
||||
|
||||
Phase 1 extracts provider definitions from `packages/stage-ui/src/libs/providers/providers`.
|
||||
|
||||
The package must not depend on Vue, Vue Pinia, or Browser data persistence APIs.
|
||||
|
||||
Browser-specific APIs are allowed when a provider needs them.
|
||||
|
||||
The package must load in Node.js and Browser test environments.
|
||||
|
||||
Each provider must declare or demonstrate the runtime that it can execute in.
|
||||
|
||||
This phase keeps the existing provider definition design unless a change removes a forbidden dependency.
|
||||
|
||||
## 2. Terms
|
||||
|
||||
- A **provider definition** describes configuration, validation, capabilities, and provider creation.
|
||||
- A **provider instance** performs an operation such as text generation or speech generation.
|
||||
- A **cloud-api provider** calls a hosted remote service.
|
||||
- A **local-api provider** calls a service that runs on the user's machine or network.
|
||||
- A **local-run provider** runs a model or platform capability in the current application runtime.
|
||||
- A **portable provider** passes tests in both Node.js and Browser runtimes.
|
||||
- A **Browser-only provider** executes in Browser but not in Node.js.
|
||||
- A **runtime adapter** supplies a capability that differs between runtimes.
|
||||
- A **provider registry** stores provider definitions and returns them by identifier.
|
||||
|
||||
## 3. Phase 1 Goals
|
||||
|
||||
Phase 1 has these goals:
|
||||
|
||||
1. Create `packages/provider-inference` as an independent workspace package.
|
||||
2. Extract provider definitions that do not depend on Vue, Vue Pinia, or Browser persistence.
|
||||
3. Allow Browser APIs inside provider implementations when the provider needs them.
|
||||
4. Classify providers as `cloud-api`, `local-api`, or `local-run`.
|
||||
5. Verify the runtime support of each provider with Node.js and Browser tests.
|
||||
6. Use `vitest-plugin-fakemic` for ASR pipeline acceptance.
|
||||
7. Keep the provider definition interface close to its current shape.
|
||||
8. Keep UI, Pinia state, persistence, and application assembly in stage-ui.
|
||||
|
||||
## 4. Scope
|
||||
|
||||
### 4.1 Included
|
||||
|
||||
Phase 1 includes:
|
||||
|
||||
- Provider definitions under `packages/stage-ui/src/libs/providers/providers`.
|
||||
- The provider registry in `providers/registry.ts`.
|
||||
- Core provider types that portable or Browser provider definitions require.
|
||||
- Provider validators and capability types that do not depend on Vue or Pinia.
|
||||
- Provider tests that use Node.js, Browser, or explicit runtime fakes.
|
||||
- Cross-runtime and Browser-specific test configuration.
|
||||
- Package exports, TypeScript configuration, and the `tsdown` build configuration.
|
||||
|
||||
The package can use Browser APIs such as `window`, `navigator`, `MediaStream`, `SpeechRecognition`, `FileReader`, `Worker`, and WebGPU.
|
||||
|
||||
The package can use shared APIs such as `fetch`, `URL`, `AbortSignal`, `ReadableStream`, `Response`, `Blob`, and `ArrayBuffer`.
|
||||
|
||||
### 4.2 Excluded
|
||||
|
||||
Phase 1 excludes:
|
||||
|
||||
- Vue components and Vue composables.
|
||||
- Vue Pinia stores and Pinia synchronization.
|
||||
- Browser persistence, including `localStorage`, IndexedDB, Cache API, and cookie access.
|
||||
- Electron-only native implementations.
|
||||
- Stage-ui stores, authentication, server configuration, and analytics.
|
||||
- `packages/stage-ui/src/services/inference-service-providers.ts`.
|
||||
- The runtime implementation under `packages/stage-ui/src/libs/inference`.
|
||||
- The full `@proj-airi/testing-audio` package as a dependency of the core package.
|
||||
|
||||
Phase 1 does not add a compatibility layer for the old provider import path.
|
||||
|
||||
## 5. Runtime Policy
|
||||
|
||||
### 5.1 Forbidden dependencies
|
||||
|
||||
The new package must not import or use:
|
||||
|
||||
- `vue`
|
||||
- `vue-i18n`
|
||||
- `pinia`
|
||||
- `pinia-plugin-synced`
|
||||
- `@proj-airi/stage-ui`
|
||||
- Stage-ui stores or persistence modules
|
||||
- `localStorage`
|
||||
- `indexedDB`
|
||||
- `caches`
|
||||
- `document.cookie`
|
||||
- Browser storage wrappers that use these APIs internally
|
||||
|
||||
Type-only imports also count as dependencies when they enter the package type graph.
|
||||
|
||||
### 5.2 Allowed Browser APIs
|
||||
|
||||
Browser APIs are not a package-wide restriction.
|
||||
|
||||
A provider can use a Browser API when its definition loads safely in Node.js and reports the correct availability there.
|
||||
|
||||
A Browser-only provider must not access the Browser API during module import.
|
||||
|
||||
A Browser-only provider must access the API inside a guarded availability function or inside provider execution.
|
||||
|
||||
The Node.js test must load the provider definition without defining Browser globals.
|
||||
|
||||
The Browser test must exercise the provider with a real or explicit fake Browser capability.
|
||||
|
||||
### 5.3 Persistence rule
|
||||
|
||||
Provider configuration remains in the host application.
|
||||
|
||||
The provider package receives configuration values as function input.
|
||||
|
||||
The provider package does not save credentials, model selection, validation state, or runtime state.
|
||||
|
||||
## 6. Runtime Classification
|
||||
|
||||
The classification describes provider behavior.
|
||||
|
||||
It does not require a new field in `ProviderDefinition` during Phase 1.
|
||||
|
||||
The classification uses the fixed `cloud` and `local` source folders.
|
||||
|
||||
### 6.1 Cloud API
|
||||
|
||||
Cloud API providers call hosted services.
|
||||
|
||||
Expected cloud API providers include:
|
||||
|
||||
- `302-ai`
|
||||
- `aihubmix`
|
||||
- `aliyun-nls`
|
||||
- `amazon-bedrock`
|
||||
- `anthropic`
|
||||
- `atlascloud`
|
||||
- `azure-ai-foundry`
|
||||
- `azure-openai`
|
||||
- `byteplus`
|
||||
- `byteplus-coding-plan`
|
||||
- `cerebras-ai`
|
||||
- `cloudflare-workers-ai`
|
||||
- `comet-api`
|
||||
- `deepseek`
|
||||
- `elevenlabs`
|
||||
- `featherless-ai`
|
||||
- `fireworks-ai`
|
||||
- `google-gemini-audio-speech`
|
||||
- `google-generative-ai`
|
||||
- `groq`
|
||||
- `mimo`
|
||||
- `mimo-audio`
|
||||
- `minimax`
|
||||
- `minimax-speech`
|
||||
- `mistral-ai`
|
||||
- `modelscope`
|
||||
- `moonshot-ai`
|
||||
- `n1n`
|
||||
- `novita-ai`
|
||||
- `nvidia`
|
||||
- `openai`
|
||||
- `openai-audio`
|
||||
- `openai-compatible`
|
||||
- `openpaths`
|
||||
- `openrouter-ai`
|
||||
- `openrouter-audio-speech`
|
||||
- `perplexity-ai`
|
||||
- `together-ai`
|
||||
- `unspeech`
|
||||
- `volcengine-coding-plan`
|
||||
- `xai`
|
||||
- `zai`
|
||||
|
||||
`official` is also a cloud API provider by behavior.
|
||||
|
||||
Its current implementation remains excluded because it depends on Vue, Pinia, authentication, and stage-ui server state.
|
||||
|
||||
Some cloud API definitions also support a user-selected local endpoint.
|
||||
|
||||
`openai-compatible` is one example.
|
||||
|
||||
`cloudflare-workers-ai` remains a cloud API provider. Its name does not mean that it uses a Browser Worker.
|
||||
|
||||
The endpoint configuration, and not the folder name, decides the actual service location.
|
||||
|
||||
### 6.2 Local API
|
||||
|
||||
Local API providers call a local HTTP or WebSocket service.
|
||||
|
||||
Expected local API providers include:
|
||||
|
||||
- `index-tts-vllm`
|
||||
- `lm-studio`
|
||||
- `ollama`
|
||||
- `player2-speech`
|
||||
- `voicevox`
|
||||
|
||||
A local API provider can run in Node.js and Browser when it uses a compatible transport.
|
||||
|
||||
Browser tests must account for CORS and local network access.
|
||||
|
||||
CORS is a deployment constraint, not a Vue or persistence dependency.
|
||||
|
||||
The test suite must use a fake transport for deterministic provider contract tests.
|
||||
|
||||
The suite can use a live local service in a separate opt-in test.
|
||||
|
||||
### 6.3 Local run
|
||||
|
||||
Local run providers execute in the current runtime.
|
||||
|
||||
Expected local run providers include:
|
||||
|
||||
- `browser-web-speech-api`
|
||||
- `kokoro-local`
|
||||
- `local-audio`
|
||||
- `apple-speech`
|
||||
- `speech-noop`
|
||||
|
||||
`browser-web-speech-api` is Browser-only because Node.js does not provide Web Speech recognition.
|
||||
|
||||
`kokoro-local` is Browser-oriented because it uses WebGPU and a Worker-based inference adapter.
|
||||
|
||||
`local-audio` contains Browser and Electron local implementations.
|
||||
|
||||
`apple-speech` uses a native Electron implementation.
|
||||
|
||||
`speech-noop` has no external runtime requirement.
|
||||
|
||||
The classification does not mean that every local run provider enters the portable package.
|
||||
|
||||
Each implementation must pass the dependency and runtime audit before it moves.
|
||||
|
||||
## 7. Folder Layout
|
||||
|
||||
Phase 1 uses `cloud` and `local` as the top-level provider folders.
|
||||
|
||||
This layout is a Phase 1 decision.
|
||||
|
||||
### 7.1 Cloud and local folders
|
||||
|
||||
```text
|
||||
src/providers/
|
||||
cloud/
|
||||
local/
|
||||
```
|
||||
|
||||
The `cloud` folder contains cloud API providers.
|
||||
|
||||
The `local` folder contains local API and local run providers.
|
||||
|
||||
Local API and local run providers can share the `local` folder.
|
||||
|
||||
The provider classification and test matrix must keep these two local categories distinct.
|
||||
|
||||
Providers with configurable endpoints use their primary provider behavior for folder placement.
|
||||
|
||||
For example, `openai-compatible` can call a cloud or local endpoint.
|
||||
|
||||
Its folder path does not define the configured endpoint.
|
||||
|
||||
Do not create `local-api` or `local-run` subfolders during Phase 1.
|
||||
|
||||
Add more folders only in a later architecture-reviewed change.
|
||||
|
||||
### 7.2 Layout rules
|
||||
|
||||
The implementation must follow these rules:
|
||||
|
||||
- The registry imports definitions without hidden side effects.
|
||||
- Browser-only definitions load without breaking Node.js imports.
|
||||
- The test matrix maps to each provider category.
|
||||
|
||||
The folder name is an organization aid.
|
||||
|
||||
The folder name is not a runtime capability contract.
|
||||
|
||||
The provider definition must not gain a `kind` or `category` field only to support a folder layout.
|
||||
|
||||
If a machine-readable category is required later, add an optional field in a separate architecture-reviewed change.
|
||||
|
||||
## 8. Runtime Compatibility Verification
|
||||
|
||||
The leader's statement is directionally correct for most cloud API and local API providers.
|
||||
|
||||
The statement is not correct for every current provider.
|
||||
|
||||
The source audit found these groups:
|
||||
|
||||
- Most HTTP providers use `@xsai`, `fetch`, `URL`, `Response`, or `ReadableStream`.
|
||||
- Most HTTP providers have no Vue or Pinia runtime import.
|
||||
- Browser Web Speech requires a Browser capability.
|
||||
- Kokoro local requires WebGPU and a Worker-based inference adapter.
|
||||
- Apple Speech requires Electron IPC and a native plugin.
|
||||
- Official providers require Vue, Pinia, authentication, and stage-ui server state.
|
||||
- Aliyun NLS creates a WebSocket and needs a Node transport decision.
|
||||
- Mimo audio uses `FileReader` and needs a small file-reading change for Node.js.
|
||||
- Several definitions use a type-only vue-i18n import.
|
||||
|
||||
The final runtime result must come from tests, not from the source classification alone.
|
||||
|
||||
### 8.1 Node.js test requirements
|
||||
|
||||
Run every moved provider definition in a Node.js Vitest project.
|
||||
|
||||
The Node.js project must use no Browser environment.
|
||||
|
||||
The Node.js project must not define `window`, `document`, `navigator`, `localStorage`, `indexedDB`, or `Worker`.
|
||||
|
||||
For a dual-runtime provider, test configuration, provider creation, validators, and provider request behavior.
|
||||
|
||||
For a Browser-only provider, test import safety and `isAvailableBy` in Node.js.
|
||||
|
||||
For an Electron-only provider, keep the provider outside the portable package.
|
||||
|
||||
### 8.2 Browser test requirements
|
||||
|
||||
Run every moved provider definition in a Vitest Browser project.
|
||||
|
||||
Use the repository Playwright Browser provider.
|
||||
|
||||
For a dual-runtime provider, test the same public behavior as the Node.js test.
|
||||
|
||||
For a Browser-only provider, test the Browser capability and provider execution.
|
||||
|
||||
For Browser APIs that do not exist in headless Chromium, use an explicit fake.
|
||||
|
||||
Do not use Browser persistence to prepare a unit test.
|
||||
|
||||
### 8.3 Runtime result categories
|
||||
|
||||
Each provider test report must use one of these results:
|
||||
|
||||
- `node-and-browser`: provider execution passes in both runtimes.
|
||||
- `browser-only`: provider loads in Node.js and executes in Browser.
|
||||
- `node-only`: provider loads in Browser and executes in Node.js.
|
||||
- `runtime-excluded`: provider remains outside the package because it needs Electron, Vue, Pinia, or persistence.
|
||||
|
||||
Phase 1 accepts `browser-only` for providers such as Web Speech.
|
||||
|
||||
Phase 1 expects `node-and-browser` for the HTTP cloud API and local API candidates.
|
||||
|
||||
## 9. Minimal Provider Definition Changes
|
||||
|
||||
The current provider definition remains the primary interface.
|
||||
|
||||
Phase 1 keeps these fields and behaviors:
|
||||
|
||||
- `id`
|
||||
- `name`
|
||||
- `description`
|
||||
- `nameLocalize`
|
||||
- `descriptionLocalize`
|
||||
- `tasks`
|
||||
- `createProviderConfig`
|
||||
- `createProvider`
|
||||
- `extraMethods`
|
||||
- `validationRequiredWhen`
|
||||
- `validators`
|
||||
- `capabilities`
|
||||
- `requiresCredentials`
|
||||
- `configuredBy`
|
||||
- `business`
|
||||
|
||||
The following changes are required:
|
||||
|
||||
1. Replace `ComposerTranslation` with a plain translator function type.
|
||||
2. Keep the existing `{ t }` context shape for provider definitions.
|
||||
3. Move the Vue `views` field to a stage-ui view registry.
|
||||
4. Keep `isAvailableBy` as the existing runtime availability hook.
|
||||
5. Keep Browser capability checks inside `isAvailableBy` or provider execution.
|
||||
|
||||
The translator change removes the vue-i18n type dependency.
|
||||
|
||||
The view change removes the Vue component dependency.
|
||||
|
||||
The existing provider creation, validation, capability, and extra method contracts stay unchanged.
|
||||
|
||||
Phase 1 does not add a provider category field.
|
||||
|
||||
Phase 1 does not add a dependency container to every provider definition.
|
||||
|
||||
An adapter is added only when a provider cannot run in both target runtimes without one.
|
||||
|
||||
### 9.1 Mimo audio change
|
||||
|
||||
`providers/mimo-audio/index.ts` uses `FileReader` for transcription input.
|
||||
|
||||
Replace that helper with `Blob.arrayBuffer()` and a cross-runtime base64 conversion.
|
||||
|
||||
This change removes a Node.js execution failure.
|
||||
|
||||
It does not change the provider definition shape.
|
||||
|
||||
### 9.2 Aliyun NLS change
|
||||
|
||||
`providers/aliyun-nls/provider.ts` creates a global WebSocket.
|
||||
|
||||
Browser WebSocket support is available in the target Browser runtime.
|
||||
|
||||
Node.js needs an explicit WebSocket adapter or a Node transport.
|
||||
|
||||
The Phase 1 decision is pending architecture review and the Node test result.
|
||||
|
||||
The token and URL utility modules can move independently when their import graph stays separate.
|
||||
|
||||
### 9.3 Official provider change
|
||||
|
||||
The official provider definitions remain in stage-ui during Phase 1.
|
||||
|
||||
The definitions use Vue state and Pinia chat session state.
|
||||
|
||||
They also use stage-ui authentication and server configuration.
|
||||
|
||||
Moving them requires a reviewed application adapter.
|
||||
|
||||
That adapter is not part of the core provider definition redesign.
|
||||
|
||||
## 10. Restricted Parts
|
||||
|
||||
The following parts cannot enter the core package without a separate change.
|
||||
|
||||
### 10.1 Provider barrel
|
||||
|
||||
`providers/index.ts` imports every provider for side effects.
|
||||
|
||||
It currently loads Vue, Electron, local inference, and Browser runtime modules together.
|
||||
|
||||
Split it into a portable or approved-runtime barrel and a stage-ui runtime barrel.
|
||||
|
||||
The root package entry point must not load excluded providers.
|
||||
|
||||
### 10.2 Vue and Pinia dependencies
|
||||
|
||||
The following files contain forbidden Vue or Pinia dependencies:
|
||||
|
||||
- `providers/official/index.ts`
|
||||
- `providers/official/shared.ts`
|
||||
- `providers/apple-speech/hearing-settings.vue`
|
||||
- `providers/provider-definitions.test.ts`
|
||||
- `providers/voicevox/define.test.ts`
|
||||
- `providers/elevenlabs/index.ts`, through type-only vue-i18n usage
|
||||
- `providers/openai-audio/index.ts`, through type-only vue-i18n usage
|
||||
- `providers/unspeech/index.ts`, through type-only vue-i18n usage
|
||||
- `providers/local-audio/index.ts`, through type-only vue-i18n usage
|
||||
|
||||
The type-only imports require replacement with the core translator type.
|
||||
|
||||
The runtime Vue and Pinia imports require stage-ui ownership or a reviewed adapter.
|
||||
|
||||
### 10.3 Browser persistence
|
||||
|
||||
Provider definitions must not import provider stores or persistence helpers.
|
||||
|
||||
`packages/stage-ui/src/stores/providers/config.ts` remains in stage-ui.
|
||||
|
||||
It owns Pinia state and `useLocalStorage` persistence.
|
||||
|
||||
The provider package receives the resulting configuration as input.
|
||||
|
||||
The existing `packages/testing-audio` setup also uses `localStorage` for application preparation.
|
||||
|
||||
That usage is valid in the integration harness but forbidden in the provider package.
|
||||
|
||||
### 10.4 Apple Speech
|
||||
|
||||
The following files remain in stage-ui:
|
||||
|
||||
- `providers/apple-speech/index.ts`
|
||||
- `providers/apple-speech/provider.ts`
|
||||
- `providers/apple-speech/hearing-settings.vue`
|
||||
- `providers/apple-speech/index.test.ts`
|
||||
|
||||
The provider uses Electron renderer IPC and the Apple Speech Electron plugin.
|
||||
|
||||
It accesses `window.electron.ipcRenderer` and native platform state.
|
||||
|
||||
The view uses Vue and VueUse.
|
||||
|
||||
This provider is an Electron local-run provider, not a Web and Node provider.
|
||||
|
||||
### 10.5 Browser Web Speech
|
||||
|
||||
`providers/browser-web-speech-api` can enter the package after the type graph is clean.
|
||||
|
||||
Browser API use is allowed for this provider.
|
||||
|
||||
The provider uses `window.SpeechRecognition` and Browser media behavior.
|
||||
|
||||
Node.js does not provide Speech Recognition.
|
||||
|
||||
The Node test must load the definition and report unavailable support.
|
||||
|
||||
The Browser test must use a real or explicit fake Speech Recognition implementation.
|
||||
|
||||
This provider is `browser-only`, not `runtime-excluded`.
|
||||
|
||||
### 10.6 Kokoro local provider
|
||||
|
||||
`providers/kokoro-local/index.ts` needs review before it moves.
|
||||
|
||||
It uses `navigator.gpu` and stage-shared WebGPU state.
|
||||
|
||||
It calls the stage-ui inference adapter.
|
||||
|
||||
That adapter uses a Worker and local model runtime behavior.
|
||||
|
||||
Browser API use is allowed, but the stage-ui inference import is not a core package dependency.
|
||||
|
||||
The provider can move after the local inference capability has a package-owned or injected adapter.
|
||||
|
||||
### 10.7 Local audio providers
|
||||
|
||||
`providers/local-audio/index.ts` needs review before it moves.
|
||||
|
||||
It contains Browser and Electron local implementations.
|
||||
|
||||
It uses device memory and WebGPU checks.
|
||||
|
||||
It uses stage-shared platform detection and local audio runtime modules.
|
||||
|
||||
Its Browser branch can enter a later package revision after the runtime adapter is isolated.
|
||||
|
||||
Its Electron branch remains in stage-ui.
|
||||
|
||||
### 10.8 Mimo audio provider
|
||||
|
||||
`providers/mimo-audio/index.ts` can enter the cloud API group after the `FileReader` helper changes.
|
||||
|
||||
The current helper is not available in Node.js.
|
||||
|
||||
The required change is local to audio input conversion.
|
||||
|
||||
The provider definition interface does not need to change.
|
||||
|
||||
### 10.9 Aliyun NLS provider
|
||||
|
||||
The following files need a Node transport decision:
|
||||
|
||||
- `providers/aliyun-nls/provider.ts`
|
||||
- `providers/aliyun-nls/session.ts`
|
||||
- `providers/aliyun-nls/provider.test.ts`
|
||||
|
||||
The provider creates and manages WebSocket connections.
|
||||
|
||||
Browser WebSocket is allowed.
|
||||
|
||||
Node.js requires an injected adapter or a compatible Node implementation.
|
||||
|
||||
Keep the provider outside the `node-and-browser` set until the adapter test passes.
|
||||
|
||||
The following files can move as utility modules when their import graph stays independent:
|
||||
|
||||
- `providers/aliyun-nls/token.ts`
|
||||
- `providers/aliyun-nls/token.test.ts`
|
||||
- `providers/aliyun-nls/utils.ts`
|
||||
|
||||
### 10.10 NVIDIA availability dependency
|
||||
|
||||
`providers/nvidia/index.ts` uses the stage-shared `isStageTamagotchi` helper.
|
||||
|
||||
That helper reads Vite environment state.
|
||||
|
||||
The new package cannot depend on application build environment state for Node.js execution.
|
||||
|
||||
Move the availability decision to stage-ui or replace it with a reviewed runtime-neutral hook.
|
||||
|
||||
The HTTP provider implementation itself remains a cloud API candidate.
|
||||
|
||||
### 10.11 UI metadata
|
||||
|
||||
The following files remain in stage-ui:
|
||||
|
||||
- `packages/stage-ui/src/libs/providers/metadata.ts`
|
||||
- `packages/stage-ui/src/libs/providers/hearing-view.ts`
|
||||
|
||||
The metadata module uses Vue i18n and stage-ui schema helpers.
|
||||
|
||||
The hearing view module uses Vue injection and computed references.
|
||||
|
||||
These modules render or prepare UI behavior.
|
||||
|
||||
They do not belong in the provider core.
|
||||
|
||||
## 11. ASR Acceptance Feasibility
|
||||
|
||||
Using `@proj-airi/vitest-plugin-fakemic` for ASR acceptance is feasible.
|
||||
|
||||
The plugin starts a Playwright Web or Electron runtime.
|
||||
|
||||
It gives Chromium a file-backed fake microphone input.
|
||||
|
||||
It can therefore test the real audio path from microphone input to ASR output.
|
||||
|
||||
The plugin does not replace provider unit tests.
|
||||
|
||||
It does not directly test an isolated provider function.
|
||||
|
||||
### 11.1 Test ownership
|
||||
|
||||
Keep full audio pipeline acceptance in `@proj-airi/testing-audio`.
|
||||
|
||||
That package already owns runtime startup, routes, selectors, application preparation, and audio observations.
|
||||
|
||||
Update its provider setup to import definitions from `@proj-airi/provider-inference` after the extraction.
|
||||
|
||||
Do not add `@proj-airi/testing-audio` as a dependency of the provider package.
|
||||
|
||||
The provider package can use `@proj-airi/vitest-plugin-fakemic` as a development dependency only if it later owns a package-specific runtime harness.
|
||||
|
||||
### 11.2 Audio fixtures
|
||||
|
||||
The existing `testing-audio` package contains these usable WAV fixtures:
|
||||
|
||||
- `cases/long-leading-silence/input.test.wav`
|
||||
- `cases/single-utterance-pipeline/input.test.wav`
|
||||
- `cases/two-utterance-streaming/input.test.wav`
|
||||
|
||||
Copy the required WAV files into package-owned test fixtures when a package-local acceptance suite needs them.
|
||||
|
||||
Copy the files without copying the `testing-audio` application harness.
|
||||
|
||||
Record the source path and fixture purpose in the package test documentation.
|
||||
|
||||
The current fixtures cover leading silence, one utterance, and streaming multiple utterances.
|
||||
|
||||
### 11.3 ASR test layers
|
||||
|
||||
Use three test layers:
|
||||
|
||||
1. Node.js provider tests for request construction, response parsing, errors, and stream handling.
|
||||
2. Browser provider tests for Browser APIs and Browser runtime behavior.
|
||||
3. Fakemic pipeline acceptance for real microphone, VAD, ASR, and UI integration.
|
||||
|
||||
Use `*.audio.test.ts` for cases that run in both configured audio runtimes.
|
||||
|
||||
Use `*.audio.web.test.ts` for Browser-only cases.
|
||||
|
||||
Use `*.audio.electron.test.ts` only for Electron native cases.
|
||||
|
||||
Do not use `@proj-airi/testing-audio` persistence setup in Node.js provider tests.
|
||||
|
||||
## 12. Test Plan
|
||||
|
||||
### 12.1 Registry tests
|
||||
|
||||
Test provider registration.
|
||||
|
||||
Test lookup by provider identifier.
|
||||
|
||||
Test deterministic provider listing.
|
||||
|
||||
Test that the core entry point does not register excluded Electron or Vue providers.
|
||||
|
||||
Test Browser-only definitions through the core registry without Browser globals in Node.js.
|
||||
|
||||
### 12.2 Provider tests
|
||||
|
||||
Test configuration creation for each moved provider.
|
||||
|
||||
Test configuration validation.
|
||||
|
||||
Test runtime validation with deterministic transports.
|
||||
|
||||
Test provider instance creation.
|
||||
|
||||
Test declared chat, transcription, and speech capabilities.
|
||||
|
||||
Test model and voice discovery where the provider defines those methods.
|
||||
|
||||
Use explicit fetch fakes for HTTP providers.
|
||||
|
||||
Use explicit Browser capability fakes for Browser-only providers.
|
||||
|
||||
Use an explicit WebSocket adapter for Aliyun NLS if it enters the package.
|
||||
|
||||
### 12.3 Cross-runtime matrix
|
||||
|
||||
Run Node.js and Browser projects for every moved provider.
|
||||
|
||||
Record one runtime result for every provider.
|
||||
|
||||
Require `node-and-browser` for cloud API and local API candidates unless the provider has a documented runtime exception.
|
||||
|
||||
Require `browser-only` for Browser-specific local-run providers.
|
||||
|
||||
Keep Electron-only providers outside the portable package.
|
||||
|
||||
### 12.4 Import audit
|
||||
|
||||
Scan source imports and declaration output for forbidden packages.
|
||||
|
||||
Scan source code for Browser persistence APIs.
|
||||
|
||||
The audit must reject:
|
||||
|
||||
- `vue`
|
||||
- `vue-i18n`
|
||||
- `pinia`
|
||||
- `pinia-plugin-synced`
|
||||
- `localStorage`
|
||||
- `indexedDB`
|
||||
- `caches`
|
||||
- `document.cookie`
|
||||
- `@proj-airi/stage-ui`
|
||||
- Stage-ui stores and persistence modules
|
||||
|
||||
The audit must allow approved Browser APIs.
|
||||
|
||||
The audit must also report Browser-only APIs so that the runtime test matrix includes them.
|
||||
|
||||
### 12.5 Workspace checks
|
||||
|
||||
Run the provider package typecheck.
|
||||
|
||||
Run the provider package Node.js tests.
|
||||
|
||||
Run the provider package Browser tests.
|
||||
|
||||
Run the ASR acceptance project with `@proj-airi/vitest-plugin-fakemic`.
|
||||
|
||||
Run the stage-ui typecheck.
|
||||
|
||||
Run the repository typecheck.
|
||||
|
||||
Run the repository lint command.
|
||||
|
||||
## 13. Migration Rules
|
||||
|
||||
The implementation must follow these rules:
|
||||
|
||||
1. Move the core provider contract before moving provider definitions.
|
||||
2. Replace Vue i18n types with the runtime-neutral translator type.
|
||||
3. Keep the `{ t }` context shape to reduce provider changes.
|
||||
4. Move Vue views to a stage-ui view registry.
|
||||
5. Split the side-effect barrel into approved and runtime-specific barrels.
|
||||
6. Move cloud API and local API candidates after the runtime audit.
|
||||
7. Move Browser-only providers only when Node.js import safety passes.
|
||||
8. Keep Electron providers in stage-ui.
|
||||
9. Keep Pinia and persistence in stage-ui.
|
||||
10. Update stage-ui imports in one migration.
|
||||
11. Remove old provider imports after the migration.
|
||||
12. Do not add a provider category field only for folder organization.
|
||||
13. Do not hide a forbidden dependency behind a runtime check.
|
||||
|
||||
The provider package owns provider definitions and their registry.
|
||||
|
||||
Stage-ui owns UI, persistence, authentication, platform assembly, and Electron integration.
|
||||
|
||||
## 14. Acceptance Criteria
|
||||
|
||||
Phase 1 is complete when all conditions pass:
|
||||
|
||||
- `packages/provider-inference` exists as a workspace package.
|
||||
- The package root loads in Node.js without Browser globals.
|
||||
- The package root loads in a Browser test project.
|
||||
- The package has no Vue, Vue Pinia, or Browser persistence dependency.
|
||||
- Browser-specific providers use guarded runtime access.
|
||||
- Every moved provider has a Node.js and Browser test result.
|
||||
- Cloud API and local API candidates pass both runtime tests, or have a documented exception.
|
||||
- Browser-only local-run providers pass Browser behavior tests and Node.js import tests.
|
||||
- Electron-only providers remain in stage-ui.
|
||||
- `vitest-plugin-fakemic` validates the ASR pipeline.
|
||||
- The ASR acceptance suite uses a fixture from the existing audio test set or a documented copy.
|
||||
- The provider definition design changes only for Vue removal, view ownership, or a required runtime adapter.
|
||||
- Stage-ui uses the new provider core interface.
|
||||
- `inference-service-providers.ts` remains a stage-ui configuration service.
|
||||
- Typecheck and lint pass for the repository.
|
||||
|
||||
## 15. Later Phases
|
||||
|
||||
Later phases can address:
|
||||
|
||||
- Additional provider subfolders after an architecture review.
|
||||
- A Node WebSocket adapter for Aliyun NLS.
|
||||
- An injected local inference adapter for Kokoro.
|
||||
- A split Browser and Electron implementation for local audio.
|
||||
- A native speech adapter for Apple Speech.
|
||||
- A reviewed application adapter for official providers.
|
||||
- A separate provider configuration package for remote CRUD.
|
||||
- Public package release and external package documentation.
|
||||
|
||||
These changes require new interfaces or runtime adapters.
|
||||
|
||||
They do not change the Phase 1 provider contract without architecture review.
|
||||
|
||||
## 16. Fixed Decisions
|
||||
|
||||
The following decisions are fixed for Phase 1:
|
||||
|
||||
- The extraction source is `packages/stage-ui/src/libs/providers/providers`.
|
||||
- The top-level provider folders are `cloud` and `local`.
|
||||
- `local-api` and `local-run` share the `local` folder during Phase 1.
|
||||
- Vue, Vue Pinia, and Browser persistence remain forbidden.
|
||||
- Browser-specific APIs are allowed.
|
||||
- `inference-service-providers.ts` does not move.
|
||||
- Provider definitions keep their current shape as far as possible.
|
||||
- Node.js and Browser tests both run for moved providers.
|
||||
- `vitest-plugin-fakemic` validates ASR pipeline behavior.
|
||||
- `testing-audio` WAV fixtures can be copied for package-owned acceptance tests.
|
||||
- `testing-audio` application setup does not become a core package dependency.
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"name": "@proj-airi/provider-inference",
|
||||
"type": "module",
|
||||
"version": "0.12.0-beta.5",
|
||||
"private": true,
|
||||
"description": "Runtime-neutral provider definitions for AIRI inference",
|
||||
"author": {
|
||||
"name": "Moeru AI Project AIRI Team",
|
||||
"email": "airi@moeru.ai",
|
||||
"url": "https://github.com/moeru-ai"
|
||||
},
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/moeru-ai/airi.git",
|
||||
"directory": "packages/provider-inference"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.mts",
|
||||
"default": "./dist/index.mjs"
|
||||
}
|
||||
},
|
||||
"main": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.mts",
|
||||
"files": [
|
||||
"README.md",
|
||||
"dist",
|
||||
"package.json"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "pnpm run build",
|
||||
"build": "tsdown",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test:run": "vitest run",
|
||||
"test:node": "vitest run --project node",
|
||||
"test:browser": "vitest run --project browser"
|
||||
},
|
||||
"dependencies": {
|
||||
"@moeru/std": "catalog:",
|
||||
"@proj-airi/audio": "workspace:^",
|
||||
"@xsai-ext/providers": "catalog:",
|
||||
"@xsai-transformers/shared": "catalog:",
|
||||
"@xsai/generate-text": "catalog:",
|
||||
"@xsai/model": "catalog:",
|
||||
"@xsai/stream-transcription": "catalog:",
|
||||
"@xsai/utils-chat": "catalog:",
|
||||
"clustr": "catalog:",
|
||||
"es-toolkit": "catalog:",
|
||||
"is-network-error": "catalog:",
|
||||
"unspeech": "catalog:xsai",
|
||||
"xsschema": "catalog:",
|
||||
"zod": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@vitest/browser-playwright": "catalog:vitest",
|
||||
"@xsai/generate-speech": "catalog:",
|
||||
"playwright": "catalog:",
|
||||
"tsdown": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"vitest": "catalog:vitest"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { portableProviderDefinitions } from './providers'
|
||||
import { createProviderRegistry } from './providers/registry'
|
||||
|
||||
const providerRegistry = createProviderRegistry(portableProviderDefinitions)
|
||||
|
||||
/** IDs of the provider definitions included in the portable registry. */
|
||||
export type PortableProviderId = typeof portableProviderDefinitions[number]['id']
|
||||
|
||||
/** Returns the portable definition with this stable provider id. */
|
||||
export function getDefinedProvider(id: PortableProviderId) {
|
||||
return providerRegistry.get(id)
|
||||
}
|
||||
|
||||
/** Returns portable definitions in their deterministic display order. */
|
||||
export function listProviders() {
|
||||
return providerRegistry.list()
|
||||
}
|
||||
|
||||
export { portableProviderDefinitions }
|
||||
export { createWebSpeechAPIProvider, streamWebSpeechAPITranscription } from './providers/local/browser-web-speech-api'
|
||||
export * from './providers/registry'
|
||||
export * from './types'
|
||||
export * from './validators'
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
import { createChatProvider, createEmbedProvider, createModelProvider, merge } from '@xsai-ext/providers/utils'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
import { ProviderValidationCheck } from '../../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../../validators'
|
||||
import { defineProvider } from '../../registry'
|
||||
|
||||
const ai302ConfigSchema = z.object({
|
||||
apiKey: z
|
||||
@@ -16,7 +16,7 @@ const ai302ConfigSchema = z.object({
|
||||
|
||||
type AI302Config = z.input<typeof ai302ConfigSchema>
|
||||
|
||||
export const provider302AI = defineProvider<AI302Config>({
|
||||
export const provider302AI = defineProvider<AI302Config, '302-ai'>({
|
||||
id: '302-ai',
|
||||
order: 7,
|
||||
name: '302.AI',
|
||||
+5
-5
@@ -1,11 +1,11 @@
|
||||
import type { ChatRequestOptions } from '../../types'
|
||||
import type { ChatRequestOptions } from '../../../types'
|
||||
|
||||
import { createChatProvider, createEmbedProvider, createModelProvider, merge } from '@xsai-ext/providers/utils'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
import { ProviderValidationCheck } from '../../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../../validators'
|
||||
import { defineProvider } from '../../registry'
|
||||
|
||||
const aihubmixConfigSchema = z.object({
|
||||
apiKey: z
|
||||
@@ -18,7 +18,7 @@ const aihubmixConfigSchema = z.object({
|
||||
|
||||
type AIHubMixConfig = z.input<typeof aihubmixConfigSchema>
|
||||
|
||||
export const providerAIHubMix = defineProvider<AIHubMixConfig>({
|
||||
export const providerAIHubMix = defineProvider<AIHubMixConfig, 'aihubmix'>({
|
||||
id: 'aihubmix',
|
||||
order: 1,
|
||||
name: 'AIHubMix',
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
import type { ModelInfo } from '../../types'
|
||||
import type { ModelInfo } from '../../../types'
|
||||
|
||||
import { createModelProvider, merge } from '@xsai-ext/providers/utils'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { defineProvider } from '../registry'
|
||||
import { defineProvider } from '../../registry'
|
||||
|
||||
const amazonBedrockConfigSchema = z.object({
|
||||
apiKey: z
|
||||
@@ -182,7 +182,7 @@ function createBedrockConverseProvider(config: {
|
||||
}
|
||||
}
|
||||
|
||||
export const providerAmazonBedrock = defineProvider<AmazonBedrockConfig>({
|
||||
export const providerAmazonBedrock = defineProvider<AmazonBedrockConfig, 'amazon-bedrock'>({
|
||||
id: 'amazon-bedrock',
|
||||
order: 18,
|
||||
name: 'Amazon Bedrock',
|
||||
+5
-5
@@ -1,11 +1,11 @@
|
||||
import type { ModelInfo } from '../../types'
|
||||
import type { ModelInfo } from '../../../types'
|
||||
|
||||
import { createChatProvider, createModelProvider, merge } from '@xsai-ext/providers/utils'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
import { ProviderValidationCheck } from '../../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../../validators'
|
||||
import { defineProvider } from '../../registry'
|
||||
|
||||
const anthropicConfigSchema = z.object({
|
||||
apiKey: z
|
||||
@@ -37,7 +37,7 @@ function createAnthropic(apiKey: string, baseURL: string = 'https://api.anthropi
|
||||
)
|
||||
}
|
||||
|
||||
export const providerAnthropic = defineProvider<AnthropicConfig>({
|
||||
export const providerAnthropic = defineProvider<AnthropicConfig, 'anthropic'>({
|
||||
id: 'anthropic',
|
||||
order: 6,
|
||||
name: 'Anthropic',
|
||||
+12
-15
@@ -1,13 +1,16 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { parse } from 'zod/v4/core'
|
||||
|
||||
const createOpenAIMock = vi.fn((apiKey: string, baseURL: string) => ({
|
||||
apiKey,
|
||||
baseURL,
|
||||
chat: vi.fn((model: string) => ({
|
||||
import { createProviderRegistry } from '../registry'
|
||||
import { providerBytePlus } from './byteplus'
|
||||
import { providerBytePlusCodingPlan } from './byteplus-coding-plan'
|
||||
import { providerVolcengineCodingPlan } from './volcengine-coding-plan'
|
||||
|
||||
const { createOpenAIMock } = vi.hoisted(() => ({
|
||||
createOpenAIMock: vi.fn((apiKey: string, baseURL: string) => ({
|
||||
apiKey,
|
||||
baseURL,
|
||||
model,
|
||||
chat: vi.fn((model: string) => ({ apiKey, baseURL, model })),
|
||||
})),
|
||||
}))
|
||||
|
||||
@@ -22,10 +25,7 @@ describe('ark chat provider definitions', () => {
|
||||
})
|
||||
|
||||
it('lists prefixed models and strips the prefix before chat requests', async () => {
|
||||
const { getDefinedProvider } = await import('./registry')
|
||||
await import('./volcengine-coding-plan')
|
||||
|
||||
const provider = getDefinedProvider('volcengine-coding-plan')
|
||||
const provider = createProviderRegistry([providerVolcengineCodingPlan]).get('volcengine-coding-plan')
|
||||
expect(provider).toBeDefined()
|
||||
|
||||
const schema = await provider!.createProviderConfig({ t: input => input })
|
||||
@@ -88,12 +88,9 @@ describe('ark chat provider definitions', () => {
|
||||
})
|
||||
|
||||
it('registers byteplus providers with the spec base urls', async () => {
|
||||
const { getDefinedProvider } = await import('./registry')
|
||||
await import('./byteplus')
|
||||
await import('./byteplus-coding-plan')
|
||||
|
||||
const byteplus = getDefinedProvider('byteplus')
|
||||
const byteplusCodingPlan = getDefinedProvider('byteplus-coding-plan')
|
||||
const registry = createProviderRegistry([providerBytePlus, providerBytePlusCodingPlan])
|
||||
const byteplus = registry.get('byteplus')
|
||||
const byteplusCodingPlan = registry.get('byteplus-coding-plan')
|
||||
|
||||
expect(byteplus).toBeDefined()
|
||||
expect(byteplusCodingPlan).toBeDefined()
|
||||
+7
-7
@@ -1,11 +1,11 @@
|
||||
import type { ChatRequestOptions, ModelInfo } from '../types'
|
||||
import type { ChatRequestOptions, ModelInfo } from '../../types'
|
||||
|
||||
import { createOpenAI } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { ProviderValidationCheck } from '../types'
|
||||
import { createOpenAICompatibleValidators } from '../validators'
|
||||
import { defineProvider } from './registry'
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
|
||||
const arkProviderConfigSchema = z.object({
|
||||
apiKey: z
|
||||
@@ -21,8 +21,8 @@ interface ArkModelSpec {
|
||||
descriptionKey?: string
|
||||
}
|
||||
|
||||
interface ArkProviderDefinitionOptions {
|
||||
id: string
|
||||
interface ArkProviderDefinitionOptions<TId extends string = string> {
|
||||
id: TId
|
||||
order: number
|
||||
name: string
|
||||
nameKey: string
|
||||
@@ -41,7 +41,7 @@ function stripModelPrefix(modelId: string, modelPrefix: string) {
|
||||
: modelId
|
||||
}
|
||||
|
||||
export function createArkChatProviderDefinition(options: ArkProviderDefinitionOptions) {
|
||||
export function createArkChatProviderDefinition<const TId extends string>(options: ArkProviderDefinitionOptions<TId>) {
|
||||
const {
|
||||
id,
|
||||
order,
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
import { createOpenAI } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
import { ProviderValidationCheck } from '../../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../../validators'
|
||||
import { defineProvider } from '../../registry'
|
||||
|
||||
export const ATLASCLOUD_DEFAULT_BASE_URL = 'https://api.atlascloud.ai/v1'
|
||||
|
||||
@@ -18,7 +18,7 @@ const atlasCloudConfigSchema = z.object({
|
||||
|
||||
type AtlasCloudConfig = z.input<typeof atlasCloudConfigSchema>
|
||||
|
||||
export const providerAtlasCloud = defineProvider<AtlasCloudConfig>({
|
||||
export const providerAtlasCloud = defineProvider<AtlasCloudConfig, 'atlascloud'>({
|
||||
id: 'atlascloud',
|
||||
order: 5,
|
||||
name: 'Atlas Cloud',
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
import type { ChatRequestOptions, ModelInfo, ProviderInstance } from '../../types'
|
||||
import type { ChatRequestOptions, ModelInfo, ProviderInstance } from '../../../types'
|
||||
|
||||
import { createAzure } from '@xsai-ext/providers/special/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { defineProvider } from '../registry'
|
||||
import { defineProvider } from '../../registry'
|
||||
|
||||
const azureAIFoundryConfigSchema = z.object({
|
||||
apiKey: z.string('API Key'),
|
||||
@@ -14,7 +14,7 @@ const azureAIFoundryConfigSchema = z.object({
|
||||
|
||||
type AzureAIFoundryConfig = z.input<typeof azureAIFoundryConfigSchema>
|
||||
|
||||
export const providerAzureAIFoundry = defineProvider<AzureAIFoundryConfig>({
|
||||
export const providerAzureAIFoundry = defineProvider<AzureAIFoundryConfig, 'azure-ai-foundry'>({
|
||||
id: 'azure-ai-foundry',
|
||||
order: 17,
|
||||
name: 'Azure AI Foundry',
|
||||
+2
-2
@@ -4,8 +4,8 @@ import { errorMessageFrom } from '@moeru/std'
|
||||
import { createOpenAI } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { defineProvider } from '../../registry'
|
||||
import { collapseToolSchemaPrimitiveAnyOf } from '../../tool-schema'
|
||||
import { defineProvider } from '../registry'
|
||||
|
||||
const AZURE_OPENAI_PROVIDER_ID = 'azure-openai' as const
|
||||
const DEFAULT_COMPLETIONS_API_VERSION = '2024-04-01-preview'
|
||||
@@ -196,7 +196,7 @@ function createAzureOpenAIFetch(config: AzureOpenAIConfig) {
|
||||
}
|
||||
}
|
||||
|
||||
export const providerAzureOpenAI = defineProvider<AzureOpenAIConfig>({
|
||||
export const providerAzureOpenAI = defineProvider<AzureOpenAIConfig, 'azure-openai'>({
|
||||
id: 'azure-openai',
|
||||
order: 2,
|
||||
name: 'Azure OpenAI',
|
||||
+5
-5
@@ -1,11 +1,11 @@
|
||||
import type { ChatRequestOptions } from '../../types'
|
||||
import type { ChatRequestOptions } from '../../../types'
|
||||
|
||||
import { createCerebras } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
import { ProviderValidationCheck } from '../../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../../validators'
|
||||
import { defineProvider } from '../../registry'
|
||||
|
||||
const cerebrasConfigSchema = z.object({
|
||||
apiKey: z
|
||||
@@ -18,7 +18,7 @@ const cerebrasConfigSchema = z.object({
|
||||
|
||||
type CerebrasConfig = z.input<typeof cerebrasConfigSchema>
|
||||
|
||||
export const providerCerebrasAI = defineProvider<CerebrasConfig>({
|
||||
export const providerCerebrasAI = defineProvider<CerebrasConfig, 'cerebras-ai'>({
|
||||
id: 'cerebras-ai',
|
||||
name: 'Cerebras',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.cerebras.title'),
|
||||
+12
-5
@@ -1,9 +1,16 @@
|
||||
import { createWorkersAI } from '@xsai-ext/providers/special/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { defineProvider } from '../registry'
|
||||
import { defineProvider } from '../../registry'
|
||||
|
||||
export const providerCloudflareWorkersAI = defineProvider<{ accountId: string, apiKey: string }>({
|
||||
const cloudflareWorkersAIConfigSchema = z.object({
|
||||
apiKey: z.string('API Key'),
|
||||
accountId: z.string('Account ID'),
|
||||
})
|
||||
|
||||
type CloudflareWorkersAIConfig = z.input<typeof cloudflareWorkersAIConfigSchema>
|
||||
|
||||
export const providerCloudflareWorkersAI = defineProvider<CloudflareWorkersAIConfig, 'cloudflare-workers-ai'>({
|
||||
id: 'cloudflare-workers-ai',
|
||||
name: 'Cloudflare Workers AI',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.cloudflare-workers-ai.title'),
|
||||
@@ -13,14 +20,14 @@ export const providerCloudflareWorkersAI = defineProvider<{ accountId: string, a
|
||||
icon: 'i-simple-icons:cloudflare',
|
||||
iconColor: 'i-lobe-icons:cloudflare-color',
|
||||
|
||||
createProviderConfig: ({ t }) => z.object({
|
||||
apiKey: z.string().meta({
|
||||
createProviderConfig: ({ t }) => cloudflareWorkersAIConfigSchema.extend({
|
||||
apiKey: cloudflareWorkersAIConfigSchema.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.provider.cloudflare-workers-ai.fields.field.api-key.placeholder'),
|
||||
type: 'password',
|
||||
}),
|
||||
accountId: z.string().meta({
|
||||
accountId: cloudflareWorkersAIConfigSchema.shape.accountId.meta({
|
||||
labelLocalized: t('settings.pages.providers.provider.cloudflare-workers-ai.fields.field.account-id.label'),
|
||||
descriptionLocalized: t('settings.pages.providers.provider.cloudflare-workers-ai.fields.field.account-id.description'),
|
||||
placeholderLocalized: t('settings.pages.providers.provider.cloudflare-workers-ai.fields.field.account-id.placeholder'),
|
||||
+6
-6
@@ -1,9 +1,9 @@
|
||||
import { createChatProvider, createModelProvider, createSpeechProvider, createTranscriptionProvider, merge } from '@xsai-ext/providers/utils'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
import { ProviderValidationCheck } from '../../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../../validators'
|
||||
import { defineProvider } from '../../registry'
|
||||
|
||||
const cometApiConfigSchema = z.object({
|
||||
apiKey: z
|
||||
@@ -16,7 +16,7 @@ const cometApiConfigSchema = z.object({
|
||||
|
||||
type CometApiConfig = z.input<typeof cometApiConfigSchema>
|
||||
|
||||
export const providerCometAPI = defineProvider<CometApiConfig>({
|
||||
export const providerCometAPI = defineProvider<CometApiConfig, 'comet-api'>({
|
||||
id: 'comet-api',
|
||||
name: 'CometAPI',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.comet-api.title'),
|
||||
@@ -56,7 +56,7 @@ export const providerCometAPI = defineProvider<CometApiConfig>({
|
||||
},
|
||||
})
|
||||
|
||||
export const providerCometAPISpeech = defineProvider<CometApiConfig>({
|
||||
export const providerCometAPISpeech = defineProvider<CometApiConfig, 'comet-api-speech'>({
|
||||
id: 'comet-api-speech',
|
||||
name: 'CometAPI Speech',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.comet-api.title'),
|
||||
@@ -75,7 +75,7 @@ export const providerCometAPISpeech = defineProvider<CometApiConfig>({
|
||||
validators: createOpenAICompatibleValidators({ checks: [ProviderValidationCheck.ModelList] }),
|
||||
})
|
||||
|
||||
export const providerCometAPITranscription = defineProvider<CometApiConfig>({
|
||||
export const providerCometAPITranscription = defineProvider<CometApiConfig, 'comet-api-transcription'>({
|
||||
id: 'comet-api-transcription',
|
||||
name: 'CometAPI Transcription',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.comet-api.title'),
|
||||
+1
-1
@@ -2,7 +2,7 @@ import type {
|
||||
ChatProviderWithExtraOptions,
|
||||
} from '@xsai-ext/providers/utils'
|
||||
|
||||
import type { ChatRequestOptions, ProviderInstance } from '../../types'
|
||||
import type { ChatRequestOptions, ProviderInstance } from '../../../types'
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
+5
-5
@@ -1,11 +1,11 @@
|
||||
import type { ChatRequestOptions } from '../../types'
|
||||
import type { ChatRequestOptions } from '../../../types'
|
||||
|
||||
import { createDeepSeek } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
import { ProviderValidationCheck } from '../../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../../validators'
|
||||
import { defineProvider } from '../../registry'
|
||||
|
||||
type DeepSeekThinkingMode = 'auto' | 'disable' | 'enable'
|
||||
|
||||
@@ -48,7 +48,7 @@ function resolveDeepSeekThinking(modeRaw: unknown): { type: 'disabled' | 'enable
|
||||
}
|
||||
}
|
||||
|
||||
export const providerDeepSeek = defineProvider<DeepSeekConfig>({
|
||||
export const providerDeepSeek = defineProvider<DeepSeekConfig, 'deepseek'>({
|
||||
id: 'deepseek',
|
||||
order: 4,
|
||||
name: 'DeepSeek',
|
||||
+5
-4
@@ -1,11 +1,12 @@
|
||||
import type { SpeechProviderWithExtraOptions } from '@xsai-ext/providers/utils'
|
||||
import type { ListVoicesOptions, UnElevenLabsOptions, VoiceProviderWithExtraOptions } from 'unspeech'
|
||||
import type { ComposerTranslation } from 'vue-i18n'
|
||||
|
||||
import type { ProviderContext } from '../../../types'
|
||||
|
||||
import { createUnElevenLabs, listVoices } from 'unspeech'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { defineProvider } from '../registry'
|
||||
import { defineProvider } from '../../registry'
|
||||
import { models as elevenLabsModels } from './list-models'
|
||||
|
||||
const elevenLabsConfigSchema = z.object({
|
||||
@@ -27,7 +28,7 @@ function toListVoicesOptions(provider: VoiceProviderWithExtraOptions<UnElevenLab
|
||||
function createElevenLabsValidators() {
|
||||
return {
|
||||
validateConfig: [
|
||||
({ t }: { t: ComposerTranslation }) => ({
|
||||
({ t }: ProviderContext) => ({
|
||||
id: 'elevenlabs:check-config',
|
||||
name: t('settings.pages.providers.catalog.edit.validators.openai-compatible.check-config.title'),
|
||||
validator: async (config: ElevenLabsConfig) => {
|
||||
@@ -65,7 +66,7 @@ function createElevenLabsValidators() {
|
||||
}
|
||||
}
|
||||
|
||||
export const providerElevenLabs = defineProvider<ElevenLabsConfig>({
|
||||
export const providerElevenLabs = defineProvider<ElevenLabsConfig, 'elevenlabs'>({
|
||||
id: 'elevenlabs',
|
||||
name: 'ElevenLabs',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.elevenlabs.title'),
|
||||
+5
-5
@@ -1,11 +1,11 @@
|
||||
import type { ChatRequestOptions } from '../../types'
|
||||
import type { ChatRequestOptions } from '../../../types'
|
||||
|
||||
import { createOpenAI } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
import { ProviderValidationCheck } from '../../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../../validators'
|
||||
import { defineProvider } from '../../registry'
|
||||
|
||||
const featherlessConfigSchema = z.object({
|
||||
apiKey: z
|
||||
@@ -18,7 +18,7 @@ const featherlessConfigSchema = z.object({
|
||||
|
||||
type FeatherlessConfig = z.input<typeof featherlessConfigSchema>
|
||||
|
||||
export const providerFeatherlessAI = defineProvider<FeatherlessConfig>({
|
||||
export const providerFeatherlessAI = defineProvider<FeatherlessConfig, 'featherless-ai'>({
|
||||
id: 'featherless-ai',
|
||||
name: 'Featherless.ai',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.featherless.title'),
|
||||
+5
-5
@@ -1,11 +1,11 @@
|
||||
import type { ChatRequestOptions } from '../../types'
|
||||
import type { ChatRequestOptions } from '../../../types'
|
||||
|
||||
import { createFireworks } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
import { ProviderValidationCheck } from '../../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../../validators'
|
||||
import { defineProvider } from '../../registry'
|
||||
|
||||
const fireworksConfigSchema = z.object({
|
||||
apiKey: z
|
||||
@@ -18,7 +18,7 @@ const fireworksConfigSchema = z.object({
|
||||
|
||||
type FireworksConfig = z.input<typeof fireworksConfigSchema>
|
||||
|
||||
export const providerFireworksAI = defineProvider<FireworksConfig>({
|
||||
export const providerFireworksAI = defineProvider<FireworksConfig, 'fireworks-ai'>({
|
||||
id: 'fireworks-ai',
|
||||
name: 'Fireworks.ai',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.fireworks.title'),
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { toWavFromPCM16 } from '@proj-airi/audio/encoding'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { defineProvider } from '../registry'
|
||||
import { defineProvider } from '../../registry'
|
||||
|
||||
const DEFAULT_BASE_URL = 'https://generativelanguage.googleapis.com/v1beta/'
|
||||
const DEFAULT_MODEL = 'gemini-2.5-flash-preview-tts'
|
||||
@@ -112,7 +112,7 @@ function createAudioFetch(apiKey: string, baseUrl: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export const providerGoogleGeminiAudioSpeech = defineProvider<GoogleGeminiSpeechConfig>({
|
||||
export const providerGoogleGeminiAudioSpeech = defineProvider<GoogleGeminiSpeechConfig, 'google-gemini-audio-speech'>({
|
||||
id: 'google-gemini-audio-speech',
|
||||
name: 'Google Gemini',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.google-gemini-audio-speech.title'),
|
||||
+5
-5
@@ -1,11 +1,11 @@
|
||||
import type { ChatRequestOptions } from '../../types'
|
||||
import type { ChatRequestOptions } from '../../../types'
|
||||
|
||||
import { createGoogleGenerativeAI } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
import { ProviderValidationCheck } from '../../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../../validators'
|
||||
import { defineProvider } from '../../registry'
|
||||
|
||||
const googleGenerativeConfigSchema = z.object({
|
||||
apiKey: z
|
||||
@@ -18,7 +18,7 @@ const googleGenerativeConfigSchema = z.object({
|
||||
|
||||
type GoogleGenerativeConfig = z.input<typeof googleGenerativeConfigSchema>
|
||||
|
||||
export const providerGoogleGenerativeAI = defineProvider<GoogleGenerativeConfig>({
|
||||
export const providerGoogleGenerativeAI = defineProvider<GoogleGenerativeConfig, 'google-generative-ai'>({
|
||||
id: 'google-generative-ai',
|
||||
order: 8,
|
||||
name: 'Google Gemini',
|
||||
+5
-5
@@ -1,11 +1,11 @@
|
||||
import type { ChatRequestOptions } from '../../types'
|
||||
import type { ChatRequestOptions } from '../../../types'
|
||||
|
||||
import { createOpenAI } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
import { ProviderValidationCheck } from '../../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../../validators'
|
||||
import { defineProvider } from '../../registry'
|
||||
|
||||
const groqConfigSchema = z.object({
|
||||
apiKey: z
|
||||
@@ -18,7 +18,7 @@ const groqConfigSchema = z.object({
|
||||
|
||||
type GroqConfig = z.input<typeof groqConfigSchema>
|
||||
|
||||
export const providerGroq = defineProvider<GroqConfig>({
|
||||
export const providerGroq = defineProvider<GroqConfig, 'groq'>({
|
||||
id: 'groq',
|
||||
name: 'Groq',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.groq.title'),
|
||||
+14
-11
@@ -1,6 +1,8 @@
|
||||
import type { ProviderContext } from '../../../types'
|
||||
|
||||
import { z } from 'zod'
|
||||
|
||||
import { defineProvider } from '../registry'
|
||||
import { defineProvider } from '../../registry'
|
||||
|
||||
const mimoSpeechConfigSchema = z.object({
|
||||
apiKey: z.string(),
|
||||
@@ -29,7 +31,7 @@ function normalizeBaseUrl(baseUrl: string | undefined) {
|
||||
function createMimoValidators<TConfig extends MimoConfig>(id: string) {
|
||||
return {
|
||||
validateConfig: [
|
||||
({ t }: { t: (key: string) => string }) => ({
|
||||
({ t }: ProviderContext) => ({
|
||||
id: `${id}:check-config`,
|
||||
name: t('settings.pages.providers.catalog.edit.validators.openai-compatible.check-config.title'),
|
||||
validator: async (config: TConfig) => {
|
||||
@@ -147,13 +149,14 @@ function audioFormatFromDataUri(dataUri: string) {
|
||||
return 'wav'
|
||||
}
|
||||
|
||||
function readBlobAsDataUri(file: Blob) {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => resolve(String(reader.result))
|
||||
reader.onerror = () => reject(new Error('Failed to read audio file'))
|
||||
reader.readAsDataURL(file)
|
||||
})
|
||||
async function readBlobAsDataUri(file: Blob): Promise<string> {
|
||||
const bytes = new Uint8Array(await file.arrayBuffer())
|
||||
let binary = ''
|
||||
|
||||
for (const byte of bytes)
|
||||
binary += String.fromCharCode(byte)
|
||||
|
||||
return `data:${file.type || 'audio/wav'};base64,${btoa(binary)}`
|
||||
}
|
||||
|
||||
function createMimoTranscriptionProvider(config: MimoTranscriptionConfig) {
|
||||
@@ -206,7 +209,7 @@ function createMimoTranscriptionProvider(config: MimoTranscriptionConfig) {
|
||||
}
|
||||
}
|
||||
|
||||
export const providerMimoAudioSpeech = defineProvider<MimoSpeechConfig>({
|
||||
export const providerMimoAudioSpeech = defineProvider<MimoSpeechConfig, 'mimo-audio-speech'>({
|
||||
id: 'mimo-audio-speech',
|
||||
name: 'Xiaomi MiMo',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.mimo.title'),
|
||||
@@ -238,7 +241,7 @@ export const providerMimoAudioSpeech = defineProvider<MimoSpeechConfig>({
|
||||
},
|
||||
})
|
||||
|
||||
export const providerMimoAudioTranscription = defineProvider<MimoTranscriptionConfig>({
|
||||
export const providerMimoAudioTranscription = defineProvider<MimoTranscriptionConfig, 'mimo-audio-transcription'>({
|
||||
id: 'mimo-audio-transcription',
|
||||
name: 'Xiaomi MiMo',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.mimo.title'),
|
||||
+5
-5
@@ -1,11 +1,11 @@
|
||||
import type { ChatRequestOptions } from '../../types'
|
||||
import type { ChatRequestOptions } from '../../../types'
|
||||
|
||||
import { createXiaomi } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
import { ProviderValidationCheck } from '../../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../../validators'
|
||||
import { defineProvider } from '../../registry'
|
||||
|
||||
const mimoConfigSchema = z.object({
|
||||
apiKey: z
|
||||
@@ -18,7 +18,7 @@ const mimoConfigSchema = z.object({
|
||||
|
||||
type MimoConfig = z.input<typeof mimoConfigSchema>
|
||||
|
||||
export const providerMimo = defineProvider<MimoConfig>({
|
||||
export const providerMimo = defineProvider<MimoConfig, 'mimo'>({
|
||||
id: 'mimo',
|
||||
order: 4,
|
||||
name: 'Xiaomi MiMo',
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
import { defineProvider } from '../registry'
|
||||
import { defineProvider } from '../../registry'
|
||||
|
||||
const minimaxSpeechConfigSchema = z.object({
|
||||
apiKey: z.string(),
|
||||
@@ -9,7 +9,7 @@ const minimaxSpeechConfigSchema = z.object({
|
||||
|
||||
type MinimaxSpeechConfig = z.input<typeof minimaxSpeechConfigSchema>
|
||||
|
||||
export const providerMinimaxSpeech = defineProvider<MinimaxSpeechConfig>({
|
||||
export const providerMinimaxSpeech = defineProvider<MinimaxSpeechConfig, 'minimax-speech'>({
|
||||
id: 'minimax-speech',
|
||||
name: 'MiniMax Speech',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.minimax-speech.title'),
|
||||
+5
-5
@@ -1,9 +1,9 @@
|
||||
import { createMinimax, createMinimaxCn } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
import { ProviderValidationCheck } from '../../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../../validators'
|
||||
import { defineProvider } from '../../registry'
|
||||
|
||||
const minimaxCnConfigSchema = z.object({
|
||||
apiKey: z
|
||||
@@ -27,7 +27,7 @@ const minimaxGlobalConfigSchema = z.object({
|
||||
|
||||
type MinimaxGlobalConfig = z.input<typeof minimaxGlobalConfigSchema>
|
||||
|
||||
export const providerMinimax = defineProvider<MinimaxCnConfig>({
|
||||
export const providerMinimax = defineProvider<MinimaxCnConfig, 'minimax'>({
|
||||
id: 'minimax',
|
||||
name: 'MiniMax',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.minimax.title'),
|
||||
@@ -64,7 +64,7 @@ export const providerMinimax = defineProvider<MinimaxCnConfig>({
|
||||
},
|
||||
})
|
||||
|
||||
export const providerMinimaxGlobal = defineProvider<MinimaxGlobalConfig>({
|
||||
export const providerMinimaxGlobal = defineProvider<MinimaxGlobalConfig, 'minimax-global'>({
|
||||
id: 'minimax-global',
|
||||
name: 'MiniMax Global',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.minimax-global.title'),
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
import { createMistral } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
import { ProviderValidationCheck } from '../../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../../validators'
|
||||
import { defineProvider } from '../../registry'
|
||||
|
||||
const mistralConfigSchema = z.object({
|
||||
apiKey: z
|
||||
@@ -16,7 +16,7 @@ const mistralConfigSchema = z.object({
|
||||
|
||||
type MistralConfig = z.input<typeof mistralConfigSchema>
|
||||
|
||||
export const providerMistralAI = defineProvider<MistralConfig>({
|
||||
export const providerMistralAI = defineProvider<MistralConfig, 'mistral-ai'>({
|
||||
id: 'mistral-ai',
|
||||
name: 'Mistral',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.mistral.title'),
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
import { createOpenAI } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
import { ProviderValidationCheck } from '../../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../../validators'
|
||||
import { defineProvider } from '../../registry'
|
||||
|
||||
const modelscopeConfigSchema = z.object({
|
||||
apiKey: z
|
||||
@@ -16,7 +16,7 @@ const modelscopeConfigSchema = z.object({
|
||||
|
||||
type ModelscopeConfig = z.input<typeof modelscopeConfigSchema>
|
||||
|
||||
export const providerModelScope = defineProvider<ModelscopeConfig>({
|
||||
export const providerModelScope = defineProvider<ModelscopeConfig, 'modelscope'>({
|
||||
id: 'modelscope',
|
||||
name: 'ModelScope',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.modelscope.title'),
|
||||
+5
-5
@@ -1,11 +1,11 @@
|
||||
import type { ChatRequestOptions } from '../../types'
|
||||
import type { ChatRequestOptions } from '../../../types'
|
||||
|
||||
import { createMoonshotai } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
import { ProviderValidationCheck } from '../../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../../validators'
|
||||
import { defineProvider } from '../../registry'
|
||||
|
||||
const moonshotConfigSchema = z.object({
|
||||
apiKey: z
|
||||
@@ -18,7 +18,7 @@ const moonshotConfigSchema = z.object({
|
||||
|
||||
type MoonshotConfig = z.input<typeof moonshotConfigSchema>
|
||||
|
||||
export const providerMoonshotAI = defineProvider<MoonshotConfig>({
|
||||
export const providerMoonshotAI = defineProvider<MoonshotConfig, 'moonshot-ai'>({
|
||||
id: 'moonshot-ai',
|
||||
name: 'Moonshot AI',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.moonshot.title'),
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
import { createOpenAI } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
import { ProviderValidationCheck } from '../../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../../validators'
|
||||
import { defineProvider } from '../../registry'
|
||||
|
||||
const n1nConfigSchema = z.object({
|
||||
apiKey: z
|
||||
@@ -17,7 +17,7 @@ const n1nConfigSchema = z.object({
|
||||
|
||||
type N1NConfig = z.input<typeof n1nConfigSchema>
|
||||
|
||||
export const providerN1N = defineProvider<N1NConfig>({
|
||||
export const providerN1N = defineProvider<N1NConfig, 'n1n'>({
|
||||
id: 'n1n',
|
||||
order: 9,
|
||||
name: 'n1n',
|
||||
+5
-5
@@ -1,11 +1,11 @@
|
||||
import type { ChatRequestOptions } from '../../types'
|
||||
import type { ChatRequestOptions } from '../../../types'
|
||||
|
||||
import { createNovita } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
import { ProviderValidationCheck } from '../../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../../validators'
|
||||
import { defineProvider } from '../../registry'
|
||||
|
||||
const novitaConfigSchema = z.object({
|
||||
apiKey: z
|
||||
@@ -18,7 +18,7 @@ const novitaConfigSchema = z.object({
|
||||
|
||||
type NovitaConfig = z.input<typeof novitaConfigSchema>
|
||||
|
||||
export const providerNovitaAI = defineProvider<NovitaConfig>({
|
||||
export const providerNovitaAI = defineProvider<NovitaConfig, 'novita-ai'>({
|
||||
id: 'novita-ai',
|
||||
name: 'Novita',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.novita.title'),
|
||||
+8
-8
@@ -1,10 +1,10 @@
|
||||
import type { ComposerTranslation } from 'vue-i18n'
|
||||
import type { ProviderContext, ProviderTranslator } from '../../../types'
|
||||
|
||||
import { createOpenAI } from '@xsai-ext/providers/create'
|
||||
import { listModels } from '@xsai/model'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { defineProvider } from '../registry'
|
||||
import { defineProvider } from '../../registry'
|
||||
|
||||
const OPENAI_BASE_URL = 'https://api.openai.com/v1/'
|
||||
|
||||
@@ -22,7 +22,7 @@ type OpenAIAudioConfig = z.input<typeof openAIAudioConfigSchema>
|
||||
type OpenAICompatibleAudioConfig = z.input<typeof openAICompatibleAudioConfigSchema>
|
||||
type AudioConfig = OpenAIAudioConfig | OpenAICompatibleAudioConfig
|
||||
|
||||
function createAudioConfigSchema<T extends typeof openAIAudioConfigSchema | typeof openAICompatibleAudioConfigSchema>(schema: T, t: ComposerTranslation) {
|
||||
function createAudioConfigSchema<T extends typeof openAIAudioConfigSchema | typeof openAICompatibleAudioConfigSchema>(schema: T, t: ProviderTranslator) {
|
||||
return schema.extend({
|
||||
apiKey: schema.shape.apiKey.meta({
|
||||
labelLocalized: t('settings.pages.providers.catalog.edit.config.common.fields.field.api-key.label'),
|
||||
@@ -60,7 +60,7 @@ function createTranscriptionProvider(config: AudioConfig) {
|
||||
function createAudioValidators<TConfig extends AudioConfig>() {
|
||||
return {
|
||||
validateConfig: [
|
||||
({ t }: { t: ComposerTranslation }) => ({
|
||||
({ t }: ProviderContext) => ({
|
||||
id: 'openai-audio:check-config',
|
||||
name: t('settings.pages.providers.catalog.edit.validators.openai-compatible.check-config.title'),
|
||||
validator: async (config: TConfig) => {
|
||||
@@ -173,7 +173,7 @@ const openAITranscriptionModels = [
|
||||
deprecated: false,
|
||||
}))
|
||||
|
||||
export const providerOpenAIAudioSpeech = defineProvider<OpenAIAudioConfig>({
|
||||
export const providerOpenAIAudioSpeech = defineProvider<OpenAIAudioConfig, 'openai-audio-speech'>({
|
||||
id: 'openai-audio-speech',
|
||||
name: 'OpenAI',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.openai.title'),
|
||||
@@ -191,7 +191,7 @@ export const providerOpenAIAudioSpeech = defineProvider<OpenAIAudioConfig>({
|
||||
},
|
||||
})
|
||||
|
||||
export const providerOpenAICompatibleAudioSpeech = defineProvider<OpenAICompatibleAudioConfig>({
|
||||
export const providerOpenAICompatibleAudioSpeech = defineProvider<OpenAICompatibleAudioConfig, 'openai-compatible-audio-speech'>({
|
||||
id: 'openai-compatible-audio-speech',
|
||||
name: 'OpenAI Compatible',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.openai-compatible.title'),
|
||||
@@ -226,7 +226,7 @@ export const providerOpenAICompatibleAudioSpeech = defineProvider<OpenAICompatib
|
||||
},
|
||||
})
|
||||
|
||||
export const providerOpenAIAudioTranscription = defineProvider<OpenAIAudioConfig>({
|
||||
export const providerOpenAIAudioTranscription = defineProvider<OpenAIAudioConfig, 'openai-audio-transcription'>({
|
||||
id: 'openai-audio-transcription',
|
||||
name: 'OpenAI',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.openai.title'),
|
||||
@@ -246,7 +246,7 @@ export const providerOpenAIAudioTranscription = defineProvider<OpenAIAudioConfig
|
||||
},
|
||||
})
|
||||
|
||||
export const providerOpenAICompatibleAudioTranscription = defineProvider<OpenAICompatibleAudioConfig>({
|
||||
export const providerOpenAICompatibleAudioTranscription = defineProvider<OpenAICompatibleAudioConfig, 'openai-compatible-audio-transcription'>({
|
||||
id: 'openai-compatible-audio-transcription',
|
||||
name: 'OpenAI Compatible',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.openai-compatible.title'),
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
import { createOpenAI } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
import { ProviderValidationCheck } from '../../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../../validators'
|
||||
import { defineProvider } from '../../registry'
|
||||
|
||||
const openAICompatibleConfigSchema = z.object({
|
||||
apiKey: z
|
||||
@@ -17,7 +17,7 @@ const openAICompatibleConfigSchema = z.object({
|
||||
|
||||
type OpenAICompatibleConfig = z.input<typeof openAICompatibleConfigSchema>
|
||||
|
||||
export const providerOpenAICompatible = defineProvider<OpenAICompatibleConfig>({
|
||||
export const providerOpenAICompatible = defineProvider<OpenAICompatibleConfig, 'openai-compatible'>({
|
||||
id: 'openai-compatible',
|
||||
order: 4,
|
||||
name: 'OpenAI Compatible',
|
||||
+5
-5
@@ -1,11 +1,11 @@
|
||||
import type { ChatRequestOptions } from '../../types'
|
||||
import type { ChatRequestOptions } from '../../../types'
|
||||
|
||||
import { createOpenAI } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
import { ProviderValidationCheck } from '../../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../../validators'
|
||||
import { defineProvider } from '../../registry'
|
||||
|
||||
const openAICompatibleConfigSchema = z.object({
|
||||
apiKey: z
|
||||
@@ -18,7 +18,7 @@ const openAICompatibleConfigSchema = z.object({
|
||||
|
||||
type OpenAICompatibleConfig = z.input<typeof openAICompatibleConfigSchema>
|
||||
|
||||
export const providerOpenAI = defineProvider<OpenAICompatibleConfig>({
|
||||
export const providerOpenAI = defineProvider<OpenAICompatibleConfig, 'openai'>({
|
||||
id: 'openai',
|
||||
order: 5,
|
||||
name: 'OpenAI',
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
import { createOpenAI } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
import { ProviderValidationCheck } from '../../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../../validators'
|
||||
import { defineProvider } from '../../registry'
|
||||
|
||||
const openPathsConfigSchema = z.object({
|
||||
apiKey: z
|
||||
@@ -16,7 +16,7 @@ const openPathsConfigSchema = z.object({
|
||||
|
||||
type OpenPathsConfig = z.input<typeof openPathsConfigSchema>
|
||||
|
||||
export const providerOpenPaths = defineProvider<OpenPathsConfig>({
|
||||
export const providerOpenPaths = defineProvider<OpenPathsConfig, 'openpaths'>({
|
||||
id: 'openpaths',
|
||||
name: 'OpenPaths',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.openpaths.title'),
|
||||
+5
-5
@@ -1,11 +1,11 @@
|
||||
import type { ChatRequestOptions } from '../../types'
|
||||
import type { ChatRequestOptions } from '../../../types'
|
||||
|
||||
import { createOpenRouter } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
import { ProviderValidationCheck } from '../../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../../validators'
|
||||
import { defineProvider } from '../../registry'
|
||||
|
||||
export const OPENROUTER_ATTRIBUTION_HEADERS: Record<string, string> = {
|
||||
'HTTP-Referer': 'https://airi.moeru.ai/',
|
||||
@@ -23,7 +23,7 @@ const openRouterConfigSchema = z.object({
|
||||
|
||||
type OpenRouterConfig = z.input<typeof openRouterConfigSchema>
|
||||
|
||||
export const providerOpenRouterAI = defineProvider<OpenRouterConfig>({
|
||||
export const providerOpenRouterAI = defineProvider<OpenRouterConfig, 'openrouter-ai'>({
|
||||
id: 'openrouter-ai',
|
||||
order: 0,
|
||||
name: 'OpenRouter',
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { toWavFromPCM16 } from '@proj-airi/audio/encoding'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { defineProvider } from '../../registry'
|
||||
import { OPENROUTER_ATTRIBUTION_HEADERS } from '../openrouter-ai'
|
||||
import { defineProvider } from '../registry'
|
||||
|
||||
const DEFAULT_BASE_URL = 'https://openrouter.ai/api/v1/'
|
||||
const DEFAULT_MODEL = 'openai/gpt-audio-mini'
|
||||
@@ -123,7 +123,7 @@ function createAudioFetch(apiKey: string, baseUrl: string, model: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export const providerOpenRouterAudioSpeech = defineProvider<OpenRouterAudioConfig>({
|
||||
export const providerOpenRouterAudioSpeech = defineProvider<OpenRouterAudioConfig, 'openrouter-audio-speech'>({
|
||||
id: 'openrouter-audio-speech',
|
||||
name: 'OpenRouter',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.openrouter-audio-speech.title'),
|
||||
+5
-5
@@ -1,11 +1,11 @@
|
||||
import type { ModelInfo } from '../../types'
|
||||
import type { ModelInfo } from '../../../types'
|
||||
|
||||
import { createPerplexity } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
import { ProviderValidationCheck } from '../../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../../validators'
|
||||
import { defineProvider } from '../../registry'
|
||||
|
||||
const perplexityConfigSchema = z.object({
|
||||
apiKey: z
|
||||
@@ -18,7 +18,7 @@ const perplexityConfigSchema = z.object({
|
||||
|
||||
type PerplexityConfig = z.input<typeof perplexityConfigSchema>
|
||||
|
||||
export const providerPerplexityAI = defineProvider<PerplexityConfig>({
|
||||
export const providerPerplexityAI = defineProvider<PerplexityConfig, 'perplexity-ai'>({
|
||||
id: 'perplexity-ai',
|
||||
name: 'Perplexity',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.perplexity.title'),
|
||||
+5
-5
@@ -1,11 +1,11 @@
|
||||
import type { ChatRequestOptions } from '../../types'
|
||||
import type { ChatRequestOptions } from '../../../types'
|
||||
|
||||
import { createTogetherAI } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
import { ProviderValidationCheck } from '../../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../../validators'
|
||||
import { defineProvider } from '../../registry'
|
||||
|
||||
const togetherConfigSchema = z.object({
|
||||
apiKey: z
|
||||
@@ -18,7 +18,7 @@ const togetherConfigSchema = z.object({
|
||||
|
||||
type TogetherConfig = z.input<typeof togetherConfigSchema>
|
||||
|
||||
export const providerTogetherAI = defineProvider<TogetherConfig>({
|
||||
export const providerTogetherAI = defineProvider<TogetherConfig, 'together-ai'>({
|
||||
id: 'together-ai',
|
||||
name: 'Together.ai',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.together.title'),
|
||||
+12
-11
@@ -6,7 +6,8 @@ import type {
|
||||
UnVolcengineOptions,
|
||||
VoiceProviderWithExtraOptions,
|
||||
} from 'unspeech'
|
||||
import type { ComposerTranslation } from 'vue-i18n'
|
||||
|
||||
import type { ProviderContext, ProviderTranslator } from '../../../types'
|
||||
|
||||
import {
|
||||
createUnAlibabaCloud,
|
||||
@@ -17,7 +18,7 @@ import {
|
||||
} from 'unspeech'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { defineProvider } from '../registry'
|
||||
import { defineProvider } from '../../registry'
|
||||
|
||||
const unspeechConfigSchema = z.object({
|
||||
apiKey: z.string(),
|
||||
@@ -36,12 +37,12 @@ type UnspeechConfig = z.input<typeof unspeechConfigSchema>
|
||||
type MicrosoftSpeechConfig = z.input<typeof microsoftSpeechConfigSchema>
|
||||
type VolcengineSpeechConfig = z.input<typeof volcengineSpeechConfigSchema>
|
||||
|
||||
function createUnspeechConfigSchema(schema: typeof volcengineSpeechConfigSchema, t: ComposerTranslation): typeof volcengineSpeechConfigSchema
|
||||
function createUnspeechConfigSchema(schema: typeof microsoftSpeechConfigSchema, t: ComposerTranslation): typeof microsoftSpeechConfigSchema
|
||||
function createUnspeechConfigSchema(schema: typeof unspeechConfigSchema, t: ComposerTranslation): typeof unspeechConfigSchema
|
||||
function createUnspeechConfigSchema(schema: typeof volcengineSpeechConfigSchema, t: ProviderTranslator): typeof volcengineSpeechConfigSchema
|
||||
function createUnspeechConfigSchema(schema: typeof microsoftSpeechConfigSchema, t: ProviderTranslator): typeof microsoftSpeechConfigSchema
|
||||
function createUnspeechConfigSchema(schema: typeof unspeechConfigSchema, t: ProviderTranslator): typeof unspeechConfigSchema
|
||||
function createUnspeechConfigSchema(
|
||||
schema: typeof unspeechConfigSchema | typeof microsoftSpeechConfigSchema | typeof volcengineSpeechConfigSchema,
|
||||
t: ComposerTranslation,
|
||||
t: ProviderTranslator,
|
||||
) {
|
||||
return schema.extend({
|
||||
apiKey: schema.shape.apiKey.meta({
|
||||
@@ -105,7 +106,7 @@ function validateUnspeechConfig(config: UnspeechConfig, requireAppId = false) {
|
||||
function createUnspeechValidators<TConfig extends UnspeechConfig>(id: string, requireAppId = false) {
|
||||
return {
|
||||
validateConfig: [
|
||||
({ t }: { t: ComposerTranslation }) => ({
|
||||
({ t }: ProviderContext) => ({
|
||||
id: `${id}:check-config`,
|
||||
name: t('settings.pages.providers.catalog.edit.validators.openai-compatible.check-config.title'),
|
||||
validator: async (config: TConfig) => validateUnspeechConfig(config, requireAppId),
|
||||
@@ -114,7 +115,7 @@ function createUnspeechValidators<TConfig extends UnspeechConfig>(id: string, re
|
||||
}
|
||||
}
|
||||
|
||||
export const providerDeepgramTts = defineProvider<UnspeechConfig>({
|
||||
export const providerDeepgramTts = defineProvider<UnspeechConfig, 'deepgram-tts'>({
|
||||
id: 'deepgram-tts',
|
||||
name: 'Deepgram',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.deepgram-tts.title'),
|
||||
@@ -147,7 +148,7 @@ export const providerDeepgramTts = defineProvider<UnspeechConfig>({
|
||||
},
|
||||
})
|
||||
|
||||
export const providerMicrosoftSpeech = defineProvider<MicrosoftSpeechConfig>({
|
||||
export const providerMicrosoftSpeech = defineProvider<MicrosoftSpeechConfig, 'microsoft-speech'>({
|
||||
id: 'microsoft-speech',
|
||||
name: 'Microsoft / Azure Speech',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.microsoft-speech.title'),
|
||||
@@ -176,7 +177,7 @@ export const providerMicrosoftSpeech = defineProvider<MicrosoftSpeechConfig>({
|
||||
},
|
||||
})
|
||||
|
||||
export const providerAlibabaCloudModelStudio = defineProvider<UnspeechConfig>({
|
||||
export const providerAlibabaCloudModelStudio = defineProvider<UnspeechConfig, 'alibaba-cloud-model-studio'>({
|
||||
id: 'alibaba-cloud-model-studio',
|
||||
name: 'Alibaba Cloud Model Studio',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.alibaba-cloud-model-studio.title'),
|
||||
@@ -209,7 +210,7 @@ export const providerAlibabaCloudModelStudio = defineProvider<UnspeechConfig>({
|
||||
},
|
||||
})
|
||||
|
||||
export const providerVolcengineSpeech = defineProvider<VolcengineSpeechConfig>({
|
||||
export const providerVolcengineSpeech = defineProvider<VolcengineSpeechConfig, 'volcengine'>({
|
||||
id: 'volcengine',
|
||||
name: 'Volcengine',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.volcengine.title'),
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
import { createXai } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
import { ProviderValidationCheck } from '../../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../../validators'
|
||||
import { defineProvider } from '../../registry'
|
||||
|
||||
const xaiConfigSchema = z.object({
|
||||
apiKey: z
|
||||
@@ -16,7 +16,7 @@ const xaiConfigSchema = z.object({
|
||||
|
||||
type XAIConfig = z.input<typeof xaiConfigSchema>
|
||||
|
||||
export const providerXAI = defineProvider<XAIConfig>({
|
||||
export const providerXAI = defineProvider<XAIConfig, 'xai'>({
|
||||
id: 'xai',
|
||||
name: 'xAI',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.xai.title'),
|
||||
+5
-5
@@ -1,11 +1,11 @@
|
||||
import type { ChatRequestOptions } from '../../types'
|
||||
import type { ChatRequestOptions } from '../../../types'
|
||||
|
||||
import { createZai } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
import { ProviderValidationCheck } from '../../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../../validators'
|
||||
import { defineProvider } from '../../registry'
|
||||
|
||||
const zaiConfigSchema = z.object({
|
||||
apiKey: z
|
||||
@@ -18,7 +18,7 @@ const zaiConfigSchema = z.object({
|
||||
|
||||
type ZaiConfig = z.input<typeof zaiConfigSchema>
|
||||
|
||||
export const providerZai = defineProvider<ZaiConfig>({
|
||||
export const providerZai = defineProvider<ZaiConfig, 'zai'>({
|
||||
id: 'zai',
|
||||
name: 'Z.ai',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.zai.title'),
|
||||
@@ -0,0 +1,148 @@
|
||||
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 { 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(
|
||||
provider302AI,
|
||||
providerAIHubMix,
|
||||
providerAmazonBedrock,
|
||||
providerAnthropic,
|
||||
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'
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { providerBrowserWebSpeechApi } from '.'
|
||||
|
||||
class FakeSpeechRecognition {
|
||||
continuous = false
|
||||
interimResults = false
|
||||
lang = ''
|
||||
maxAlternatives = 1
|
||||
onend: (() => void) | undefined
|
||||
|
||||
start(): void {
|
||||
this.onend?.()
|
||||
}
|
||||
|
||||
stop(): void {}
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
describe('browser Web Speech provider', () => {
|
||||
it('uses an explicit Browser capability fake', async () => {
|
||||
vi.stubGlobal('SpeechRecognition', FakeSpeechRecognition)
|
||||
|
||||
expect(await providerBrowserWebSpeechApi.isAvailableBy?.()).toBe(true)
|
||||
|
||||
const provider = await providerBrowserWebSpeechApi.createProvider({
|
||||
continuous: true,
|
||||
interimResults: true,
|
||||
language: 'en-US',
|
||||
maxAlternatives: 1,
|
||||
})
|
||||
|
||||
if (!('transcription' in provider))
|
||||
throw new Error('Web Speech API did not create a transcription provider.')
|
||||
|
||||
const request = provider.transcription?.('web-speech-api')
|
||||
if (!request?.fetch)
|
||||
throw new Error('Web Speech API did not create a transcription request.')
|
||||
|
||||
const response = await request.fetch(new URL('https://provider.test/transcription'), {})
|
||||
|
||||
expect(response).toBeInstanceOf(ReadableStream)
|
||||
})
|
||||
})
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
import { defineProvider } from '../registry'
|
||||
import { defineProvider } from '../../registry'
|
||||
import { createWebSpeechAPIProvider } from './provider'
|
||||
|
||||
const webSpeechApiConfigSchema = z.object({
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import type { TranscriptionProviderWithExtraOptions } from '@xsai-ext/providers/utils'
|
||||
import type { StreamTranscriptionDelta, StreamTranscriptionResult } from '@xsai/stream-transcription'
|
||||
|
||||
import { errorMessageFromValue } from '@proj-airi/stage-shared'
|
||||
import { errorMessageFrom } from '@moeru/std'
|
||||
|
||||
// NOTICE: Copied/adapted from @xsai/stream-transcription delayed promise helper.
|
||||
// Ref: @xsai/stream-transcription@0.4.0-beta.8 (dist/index.js DelayedPromise usage).
|
||||
@@ -308,7 +308,7 @@ export function streamWebSpeechAPITranscription(
|
||||
}
|
||||
catch (newErr) {
|
||||
console.error('Web Speech API failed to create new instance:', newErr)
|
||||
const error = new Error(`Failed to restart recognition: ${errorMessageFromValue(newErr)}`)
|
||||
const error = new Error(`Failed to restart recognition: ${errorMessageFrom(newErr) ?? 'Unknown error'}`)
|
||||
fullStreamCtrl?.error(error)
|
||||
textStreamCtrl?.error(error)
|
||||
deferredText.reject(error)
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { errorMessageFrom } from '@moeru/std'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { defineProvider } from '../registry'
|
||||
import { defineProvider } from '../../registry'
|
||||
|
||||
const indexTtsConfigSchema = z.object({
|
||||
baseUrl: z.string().default('http://localhost:11996/tts/'),
|
||||
@@ -14,7 +14,7 @@ function voicesUrl(config: IndexTtsConfig) {
|
||||
return `${config.baseUrl ?? 'http://localhost:11996/tts/'}audio/voices`
|
||||
}
|
||||
|
||||
export const providerIndexTtsVllm = defineProvider<IndexTtsConfig>({
|
||||
export const providerIndexTtsVllm = defineProvider<IndexTtsConfig, 'index-tts-vllm'>({
|
||||
id: 'index-tts-vllm',
|
||||
name: 'Index-TTS by Bilibili',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.index-tts-vllm.title'),
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
import { createChatProvider, createEmbedProvider, createModelProvider, merge } from '@xsai-ext/providers/utils'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
import { ProviderValidationCheck } from '../../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../../validators'
|
||||
import { defineProvider } from '../../registry'
|
||||
|
||||
const lmStudioConfigSchema = z.object({
|
||||
apiKey: z
|
||||
@@ -17,7 +17,7 @@ const lmStudioConfigSchema = z.object({
|
||||
|
||||
type LMStudioConfig = z.input<typeof lmStudioConfigSchema>
|
||||
|
||||
export const providerLmStudio = defineProvider<LMStudioConfig>({
|
||||
export const providerLmStudio = defineProvider<LMStudioConfig, 'lm-studio'>({
|
||||
id: 'lm-studio',
|
||||
order: 3,
|
||||
name: 'LM Studio',
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import type { ChatProviderWithExtraOptions } from '@xsai-ext/providers/utils'
|
||||
|
||||
import type { ChatRequestOptions, ProviderInstance } from '../../types'
|
||||
import type { ChatRequestOptions, ProviderInstance } from '../../../types'
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
+5
-5
@@ -1,11 +1,11 @@
|
||||
import type { ChatRequestOptions } from '../../types'
|
||||
import type { ChatRequestOptions } from '../../../types'
|
||||
|
||||
import { createOllama } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
import { ProviderValidationCheck } from '../../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../../validators'
|
||||
import { defineProvider } from '../../registry'
|
||||
|
||||
type OllamaReasoningEffort = 'high' | 'low' | 'medium' | 'none'
|
||||
type OllamaThinkingMode = 'auto' | 'disable' | 'enable' | 'high' | 'low' | 'medium'
|
||||
@@ -61,7 +61,7 @@ export function resolveOllamaReasoningEffort(modeRaw: unknown): OllamaReasoningE
|
||||
}
|
||||
}
|
||||
|
||||
export const providerOllama = defineProvider<OllamaConfig>({
|
||||
export const providerOllama = defineProvider<OllamaConfig, 'ollama'>({
|
||||
id: 'ollama',
|
||||
order: 2,
|
||||
name: 'Ollama',
|
||||
+2
-2
@@ -2,7 +2,7 @@ import { errorMessageFrom } from '@moeru/std'
|
||||
import { createPlayer2 } from '@xsai-ext/providers/special/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { defineProvider } from '../registry'
|
||||
import { defineProvider } from '../../registry'
|
||||
|
||||
const player2ConfigSchema = z.object({
|
||||
baseUrl: z.string().default('http://localhost:4315/v1/'),
|
||||
@@ -28,7 +28,7 @@ function normalizeBaseUrl(baseUrl: string | undefined) {
|
||||
return value.endsWith('/') ? value : `${value}/`
|
||||
}
|
||||
|
||||
export const providerPlayer2Speech = defineProvider<Player2Config>({
|
||||
export const providerPlayer2Speech = defineProvider<Player2Config, 'player2-speech'>({
|
||||
id: 'player2-speech',
|
||||
name: 'Player2 Speech',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.player2.title'),
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
import { defineProvider } from '../registry'
|
||||
import { defineProvider } from '../../registry'
|
||||
|
||||
const speechNoopConfigSchema = z.object({})
|
||||
|
||||
+4
-3
@@ -1,14 +1,15 @@
|
||||
import type { SpeechProvider } from '@xsai-ext/providers/utils'
|
||||
import type { ComposerTranslation } from 'vue-i18n'
|
||||
|
||||
import type { ProviderTranslator } from '../../../types'
|
||||
|
||||
import { generateSpeech } from '@xsai/generate-speech'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { providerAivisSpeech, providerVoicevox } from '.'
|
||||
import { getProviderValidationIntervalMs } from '../../validators/run'
|
||||
import { getProviderValidationIntervalMs } from '../../../validators/run'
|
||||
|
||||
const translate = ((key: string) => key) as unknown as ComposerTranslation
|
||||
const translate = ((key: string) => key) as unknown as ProviderTranslator
|
||||
|
||||
interface EngineCall {
|
||||
init: RequestInit
|
||||
+8
-6
@@ -1,10 +1,10 @@
|
||||
import type { ProviderDefinition, VoiceInfo } from '../../types'
|
||||
import type { ProviderDefinition, VoiceInfo } from '../../../types'
|
||||
import type { VoicevoxSynthesisParameters } from './engine'
|
||||
|
||||
import { errorMessageFrom } from '@moeru/std'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { defineProvider } from '../registry'
|
||||
import { defineProvider } from '../../registry'
|
||||
import { fetchEngineVersion, fetchSpeakers, synthesizeSpeech } from './engine'
|
||||
|
||||
/**
|
||||
@@ -50,12 +50,12 @@ const voicevoxVoiceSettingsSchema = z.object({
|
||||
|
||||
export type VoicevoxFamilyConfig = z.input<ReturnType<typeof createVoicevoxConfigSchema>>
|
||||
|
||||
export interface VoicevoxFamilyProviderOptions {
|
||||
export interface VoicevoxFamilyProviderOptions<TId extends string = string> {
|
||||
/** Prefilled Base URL, and the address the validator names when the field is empty. */
|
||||
defaultBaseUrl: string
|
||||
/** Fallback description, shown when the locale has no entry. */
|
||||
description: string
|
||||
id: string
|
||||
id: TId
|
||||
/** Fallback name, shown when the locale has no entry. */
|
||||
name: string
|
||||
}
|
||||
@@ -67,10 +67,12 @@ export interface VoicevoxFamilyProviderOptions {
|
||||
* OpenAI-shaped request that `generateSpeech` builds. {@link synthesizeSpeech}
|
||||
* makes the two engine requests instead.
|
||||
*/
|
||||
export function defineVoicevoxFamilyProvider(options: VoicevoxFamilyProviderOptions): ProviderDefinition<VoicevoxFamilyConfig> {
|
||||
export function defineVoicevoxFamilyProvider<const TId extends string>(
|
||||
options: VoicevoxFamilyProviderOptions<TId>,
|
||||
): ProviderDefinition<VoicevoxFamilyConfig, TId> {
|
||||
const configSchema = createVoicevoxConfigSchema(options.defaultBaseUrl)
|
||||
|
||||
return defineProvider<VoicevoxFamilyConfig>({
|
||||
return defineProvider<VoicevoxFamilyConfig, TId>({
|
||||
createProvider(config) {
|
||||
return {
|
||||
speech: () => ({
|
||||
@@ -0,0 +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', () => {
|
||||
it('exports the portable provider id union', () => {
|
||||
expectTypeOf<'openai'>().toExtend<PortableProviderId>()
|
||||
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')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { ProviderDefinition } from '../types'
|
||||
|
||||
/**
|
||||
* A read-only lookup surface for provider definitions.
|
||||
*
|
||||
* The caller supplies the definitions. Importing a provider module does not
|
||||
* change a shared registry.
|
||||
*/
|
||||
export interface ProviderRegistry {
|
||||
get: (id: string) => ProviderDefinition | undefined
|
||||
list: () => ProviderDefinition[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps the existing declaration style while making provider modules pure.
|
||||
*
|
||||
* The application registry receives the returned definitions explicitly.
|
||||
*/
|
||||
export function defineProvider<TConfig, const TId extends string = string>(
|
||||
definition: ProviderDefinition<TConfig, TId>,
|
||||
): ProviderDefinition<TConfig, TId> {
|
||||
return definition
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a deterministic provider registry from explicit definitions.
|
||||
*
|
||||
* @throws {Error} When more than one definition declares the same id.
|
||||
*/
|
||||
export function createProviderRegistry(definitions: readonly ProviderDefinition[]): ProviderRegistry {
|
||||
const definitionsById = new Map<string, ProviderDefinition>()
|
||||
|
||||
for (const definition of definitions) {
|
||||
if (definitionsById.has(definition.id))
|
||||
throw new Error(`Provider definition "${definition.id}" is registered more than once.`)
|
||||
|
||||
definitionsById.set(definition.id, definition)
|
||||
}
|
||||
|
||||
const sortedDefinitions = [...definitionsById.values()].toSorted((left, right) => {
|
||||
if (left.order === undefined && right.order !== undefined)
|
||||
return 1
|
||||
if (left.order !== undefined && right.order === undefined)
|
||||
return -1
|
||||
if (left.order !== undefined && right.order !== undefined && left.order !== right.order)
|
||||
return left.order - right.order
|
||||
if (left.name < right.name)
|
||||
return -1
|
||||
if (left.name > right.name)
|
||||
return 1
|
||||
return 0
|
||||
})
|
||||
|
||||
return {
|
||||
get: id => definitionsById.get(id),
|
||||
list: () => [...sortedDefinitions],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { ProviderTranslator } from '../types'
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { portableProviderDefinitions } from '.'
|
||||
|
||||
const translate: ProviderTranslator = key => key
|
||||
const browserOnlyProviderIds = new Set(['browser-web-speech-api'])
|
||||
const localApiProviderIds = new Set([
|
||||
'aivis-speech',
|
||||
'index-tts-vllm',
|
||||
'lm-studio',
|
||||
'ollama',
|
||||
'player2-speech',
|
||||
'voicevox',
|
||||
])
|
||||
const localRunProviderIds = new Set([
|
||||
'browser-web-speech-api',
|
||||
'speech-noop',
|
||||
])
|
||||
|
||||
function testCategory(providerId: string): 'cloud-api' | 'local-api' | 'local-run' {
|
||||
if (localApiProviderIds.has(providerId))
|
||||
return 'local-api'
|
||||
if (localRunProviderIds.has(providerId))
|
||||
return 'local-run'
|
||||
return 'cloud-api'
|
||||
}
|
||||
|
||||
function testRuntimeResult(providerId: string): 'browser-only' | 'node-and-browser' {
|
||||
return browserOnlyProviderIds.has(providerId) ? 'browser-only' : 'node-and-browser'
|
||||
}
|
||||
|
||||
describe('portable provider runtime matrix in Browser', () => {
|
||||
for (const definition of portableProviderDefinitions) {
|
||||
const category = testCategory(definition.id)
|
||||
const result = testRuntimeResult(definition.id)
|
||||
|
||||
it(`[${category}/${result}] loads ${definition.id}`, async () => {
|
||||
const schema = await definition.createProviderConfig({ t: translate })
|
||||
|
||||
expect(schema).toBeDefined()
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { ProviderTranslator } from '../types'
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { portableProviderDefinitions } from '.'
|
||||
|
||||
const translate: ProviderTranslator = key => key
|
||||
const browserOnlyProviderIds = new Set(['browser-web-speech-api'])
|
||||
const localApiProviderIds = new Set([
|
||||
'aivis-speech',
|
||||
'index-tts-vllm',
|
||||
'lm-studio',
|
||||
'ollama',
|
||||
'player2-speech',
|
||||
'voicevox',
|
||||
])
|
||||
const localRunProviderIds = new Set([
|
||||
'browser-web-speech-api',
|
||||
'speech-noop',
|
||||
])
|
||||
|
||||
function testCategory(providerId: string): 'cloud-api' | 'local-api' | 'local-run' {
|
||||
if (localApiProviderIds.has(providerId))
|
||||
return 'local-api'
|
||||
if (localRunProviderIds.has(providerId))
|
||||
return 'local-run'
|
||||
return 'cloud-api'
|
||||
}
|
||||
|
||||
function testRuntimeResult(providerId: string): 'browser-only' | 'node-and-browser' {
|
||||
return browserOnlyProviderIds.has(providerId) ? 'browser-only' : 'node-and-browser'
|
||||
}
|
||||
|
||||
describe('portable provider runtime matrix in Node.js', () => {
|
||||
for (const definition of portableProviderDefinitions) {
|
||||
const category = testCategory(definition.id)
|
||||
const result = testRuntimeResult(definition.id)
|
||||
|
||||
it(`[${category}/${result}] loads ${definition.id}`, async () => {
|
||||
const schema = await definition.createProviderConfig({ t: translate })
|
||||
|
||||
expect(schema).toBeDefined()
|
||||
|
||||
if (browserOnlyProviderIds.has(definition.id))
|
||||
expect(await definition.isAvailableBy?.()).toBe(false)
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,317 @@
|
||||
import type {
|
||||
ChatProvider,
|
||||
ChatProviderWithExtraOptions,
|
||||
EmbedProvider,
|
||||
EmbedProviderWithExtraOptions,
|
||||
ModelProvider,
|
||||
ModelProviderWithExtraOptions,
|
||||
SpeechProvider,
|
||||
SpeechProviderWithExtraOptions,
|
||||
TranscriptionProvider,
|
||||
TranscriptionProviderWithExtraOptions,
|
||||
} from '@xsai-ext/providers/utils'
|
||||
import type { ProgressInfo } from '@xsai-transformers/shared/types'
|
||||
import type { MaybePromise } from 'clustr'
|
||||
import type { $ZodType } from 'zod/v4/core'
|
||||
|
||||
/** Translates a provider label or description for the active interface locale. */
|
||||
export type ProviderTranslator = (input: string) => string
|
||||
|
||||
/** Context shared by provider callbacks that need interface translation. */
|
||||
export interface ProviderContext {
|
||||
/** Translates labels and descriptions for the active interface locale. */
|
||||
t: ProviderTranslator
|
||||
}
|
||||
|
||||
export type ProviderInstance
|
||||
= | ChatProvider
|
||||
| ChatProviderWithExtraOptions
|
||||
| EmbedProvider
|
||||
| EmbedProviderWithExtraOptions
|
||||
| SpeechProvider
|
||||
| SpeechProviderWithExtraOptions
|
||||
| TranscriptionProvider
|
||||
| TranscriptionProviderWithExtraOptions
|
||||
| ModelProvider
|
||||
| ModelProviderWithExtraOptions
|
||||
|
||||
/** Validation lifecycle for one serializable provider configuration. */
|
||||
export type ProviderValidationStatus = 'unconfigured' | 'validating' | 'configured' | 'invalid' | 'bypassed'
|
||||
export type ProviderConfiguredBy = 'user' | 'authentication'
|
||||
|
||||
/** Serializable configuration for one provider instance. */
|
||||
export interface InferenceServiceProvider {
|
||||
/** Stable provider instance id. */
|
||||
id: string
|
||||
/** Provider definition id from the built-in provider registry. */
|
||||
definitionId: string
|
||||
/** Provider-specific configuration values. */
|
||||
config: Record<string, unknown>
|
||||
/** Current validation state for this provider configuration. */
|
||||
status: ProviderValidationStatus
|
||||
/** Lifecycle owner that creates and revokes this provider configuration. */
|
||||
configuredBy: ProviderConfiguredBy
|
||||
}
|
||||
|
||||
export function isModelProvider(providerInstance: ProviderInstance): providerInstance is ModelProvider | ModelProviderWithExtraOptions {
|
||||
if ('model' in providerInstance && typeof providerInstance.model === 'function') {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export interface ProviderOnboardingField {
|
||||
key: string
|
||||
type: 'text' | 'password'
|
||||
label: string
|
||||
description?: string
|
||||
placeholder?: string
|
||||
required?: boolean
|
||||
defaultValue?: string
|
||||
}
|
||||
|
||||
/** Inputs available while a Provider builds its configuration schema. */
|
||||
export interface ProviderConfigContext<TConfig> extends ProviderContext {
|
||||
/** Cancels runtime discovery that contributes schema metadata. */
|
||||
abortSignal?: AbortSignal
|
||||
/** Current draft values. Providers can use them to resolve dependent fields. */
|
||||
config?: Partial<TConfig>
|
||||
}
|
||||
|
||||
/** Serializable model discovery result returned across renderer boundaries. */
|
||||
export interface ProviderModelCatalog {
|
||||
/** Models discovered for this provider. */
|
||||
models: ModelInfo[]
|
||||
/** Whether the server exposes this catalog. Absent when discovery did not return an authoritative state. */
|
||||
available?: boolean
|
||||
/** Server-selected model id, or null when the server has no default. */
|
||||
defaultModel?: string | null
|
||||
}
|
||||
|
||||
export interface ProviderExtraMethods<TConfig> {
|
||||
listModelCatalog?: (config: TConfig, provider: ProviderInstance, contextOptions?: ProviderContext) => Promise<ProviderModelCatalog>
|
||||
listModels?: (config: TConfig, provider: ProviderInstance, contextOptions?: ProviderContext) => Promise<ModelInfo[]>
|
||||
/**
|
||||
* Returns the voice catalogue. `model` lets providers whose voices vary by
|
||||
* model variant (Volcengine streaming TTS 1.0 vs 2.0 differ in catalogue)
|
||||
* narrow the result. Providers with a single catalogue ignore it.
|
||||
*/
|
||||
listVoices?: (config: TConfig, provider: ProviderInstance, model?: string) => Promise<VoiceInfo[]>
|
||||
loadModel?: (config: TConfig, provider: ProviderInstance, hooks?: { onProgress?: (progress: ProgressInfo) => Promise<void> | void }) => Promise<void>
|
||||
}
|
||||
|
||||
export interface ProviderValidationResult {
|
||||
errors: Array<{ error: unknown, errorKey?: string }>
|
||||
reason: string
|
||||
reasonKey: string
|
||||
valid: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Validator ID fragment for the chat completions probe.
|
||||
* Matched via `.includes()` against validator instance ids
|
||||
* (e.g. `openai-compatible:check-chat-completions`).
|
||||
*/
|
||||
export const CHAT_COMPLETIONS_VALIDATOR_ID = 'check-chat-completions'
|
||||
|
||||
export enum ProviderValidationCheck {
|
||||
/** Lightweight GET to /models endpoint to check reachability (definition system) */
|
||||
Connectivity = 'connectivity',
|
||||
/** Fetch model list and verify non-empty */
|
||||
ModelList = 'model_list',
|
||||
/** Send generateText ping with fine-grained error handling and caching (definition system) */
|
||||
ChatCompletions = 'chat_completions',
|
||||
/**
|
||||
* @deprecated
|
||||
* Being used in builder system (a deprecated provider creation protocol),
|
||||
* currently used by only OpenAI TTS && OpenAI Transcription.
|
||||
* Send generateText ping with simple pass/fail, fallback to 'test' model (builder system)
|
||||
*/
|
||||
Health = 'health',
|
||||
}
|
||||
|
||||
export interface ProviderValidatorSchedule {
|
||||
mode: 'once' | 'interval'
|
||||
intervalMs?: number
|
||||
}
|
||||
|
||||
export interface ProviderConfigValidator<TConfig> {
|
||||
id: string
|
||||
name: string
|
||||
validator: (config: TConfig, contextOptions: ProviderContext) => MaybePromise<ProviderValidationResult>
|
||||
schedule?: ProviderValidatorSchedule
|
||||
}
|
||||
|
||||
export interface ProviderRuntimeValidator<TConfig> {
|
||||
id: string
|
||||
name: string
|
||||
validator: (config: TConfig, provider: ProviderInstance, providerExtra: ProviderExtraMethods<TConfig>, contextOptions: ProviderContext) => MaybePromise<ProviderValidationResult>
|
||||
schedule?: ProviderValidatorSchedule
|
||||
}
|
||||
|
||||
export interface ModelInfo {
|
||||
id: string
|
||||
name: string
|
||||
provider: string
|
||||
description?: string
|
||||
capabilities?: string[]
|
||||
contextLength?: number
|
||||
deprecated?: boolean
|
||||
}
|
||||
|
||||
export interface VoiceInfo {
|
||||
id: string
|
||||
name: string
|
||||
provider: string
|
||||
compatibleModels?: string[]
|
||||
description?: string
|
||||
gender?: string
|
||||
deprecated?: boolean
|
||||
previewURL?: string
|
||||
languages: {
|
||||
code: string
|
||||
title: string
|
||||
}[]
|
||||
}
|
||||
|
||||
/** A provider definition and its configuration contract. */
|
||||
export interface ProviderDefinition<TConfig = Record<string, unknown>, TId extends string = string> {
|
||||
/** Stable provider definition id. */
|
||||
id: TId
|
||||
order?: number
|
||||
tasks: string[]
|
||||
nameLocalize: (ctx: ProviderContext) => string // i18n key for provider name
|
||||
name: string // Default name (fallback)
|
||||
descriptionLocalize: (ctx: ProviderContext) => string // i18n key for provider description
|
||||
description: string // Default description (fallback)
|
||||
/**
|
||||
* Iconify JSON icon name for the provider.
|
||||
*
|
||||
* Icons are available for most of the AI provides under @proj-airi/lobe-icons.
|
||||
*/
|
||||
icon?: string
|
||||
iconColor?: string
|
||||
/**
|
||||
* In case of having image instead of icon, you can specify the image URL here.
|
||||
*/
|
||||
iconImage?: string
|
||||
|
||||
/**
|
||||
* Indicates whether the provider is available.
|
||||
* If not specified, the provider is always available.
|
||||
*
|
||||
* May be specified when any of the following criteria is required:
|
||||
*
|
||||
* Platform requirements:
|
||||
*
|
||||
* - app-* providers are only available on desktop, this is responsible for Tauri runtime checks
|
||||
* - web-* providers are only available on web, this means Node.js and Tauri should not be imported or used
|
||||
*
|
||||
* System spec requirements:
|
||||
*
|
||||
* - may requires WebGPU / NVIDIA / other types of GPU,
|
||||
* on Web, WebGPU will automatically compiled to use targeting GPU hardware
|
||||
* - may requires significant amount of GPU memory to run, especially for
|
||||
* using of small language models within browser or Tauri app
|
||||
* - may requires significant amount of memory to run, especially for those
|
||||
* non-WebGPU supported environments.
|
||||
*/
|
||||
isAvailableBy?: () => MaybePromise<boolean>
|
||||
|
||||
/**
|
||||
* If false, the provider does not require user-provided credentials (e.g. API keys).
|
||||
* Used for built-in providers that authenticate via JWT Bearer tokens.
|
||||
*/
|
||||
requiresCredentials?: boolean
|
||||
|
||||
/**
|
||||
* Lifecycle owner for provider configurations created from this definition.
|
||||
*
|
||||
* @default 'user'
|
||||
*/
|
||||
configuredBy?: ProviderConfiguredBy
|
||||
|
||||
/** Builds the validation schema and its UI metadata for the current draft. */
|
||||
createProviderConfig: (contextOptions: ProviderConfigContext<TConfig>) => MaybePromise<$ZodType<TConfig>>
|
||||
onboardingFields?: (ctx: ProviderContext) => MaybePromise<ProviderOnboardingField[]>
|
||||
createProvider: (config: TConfig) => MaybePromise<ProviderInstance>
|
||||
extraMethods?: ProviderExtraMethods<TConfig>
|
||||
/**
|
||||
* Returns true when the configuration has enough input for automatic validation.
|
||||
* Provider settings keep the status unconfigured while this function returns false.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
validationRequiredWhen?: (config: TConfig) => MaybePromise<boolean>
|
||||
validators?: {
|
||||
validateConfig?: Array<(contextOptions: ProviderContext) => MaybePromise<ProviderConfigValidator<TConfig>>>
|
||||
validateProvider?: Array<(contextOptions: ProviderContext) => MaybePromise<ProviderRuntimeValidator<TConfig>>>
|
||||
}
|
||||
capabilities?: {
|
||||
chat?: {
|
||||
reasoning?: ChatReasoningCapability
|
||||
}
|
||||
transcription?: {
|
||||
protocol: 'websocket' | 'http' | 'native'
|
||||
generateOutput: boolean
|
||||
streamOutput: boolean
|
||||
streamInput: boolean
|
||||
}
|
||||
/**
|
||||
* Declares the TTS transport this provider speaks. The host uses it to
|
||||
* select its TTS session adapter:
|
||||
*
|
||||
* - `rest` (default when this whole block is absent): the host opens
|
||||
* a `pipelines-audio` IntentHandle and the provider's `speech()` is
|
||||
* called per-segment by the speech-pipeline `tts()` callback. This
|
||||
* matches every OpenAI-shaped HTTP TTS provider.
|
||||
* - `bidirectional-ws`: the host opens one streaming TTS WebSocket
|
||||
* for the whole LLM intent and forwards raw token chunks without
|
||||
* client-side segmentation. The provider's `speech()` is unused
|
||||
* for synthesis on this path (kept only for legacy fallback).
|
||||
*
|
||||
* Designed so a future provider (ElevenLabs streaming, OpenAI Realtime
|
||||
* Voice, etc.) only needs to set this flag — Stage and the session
|
||||
* factory do not need to know each provider's id.
|
||||
*/
|
||||
speech?: {
|
||||
transport: 'rest' | 'bidirectional-ws'
|
||||
}
|
||||
}
|
||||
/**
|
||||
* When true, hides the "skip chat ping check" checkbox in the UI even
|
||||
* when the provider defines a ChatCompletions validator.
|
||||
*
|
||||
* By default, the checkbox is shown automatically whenever a provider
|
||||
* includes a ChatCompletions runtime validator. Set this to `true` for
|
||||
* providers where skipping that check is not meaningful or has not been
|
||||
* verified yet.
|
||||
*/
|
||||
disableChatPingCheckUI?: boolean
|
||||
business?: (contextOptions: ProviderContext) => {
|
||||
troubleshooting?: {
|
||||
validators?: {
|
||||
openaiCompatibleCheckConnectivity?: {
|
||||
label?: string
|
||||
content?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Reasoning modes that AIRI can request from a chat provider. */
|
||||
export type ChatReasoningMode = 'disabled' | 'enabled'
|
||||
|
||||
/** User-selected options that a provider applies to one chat request. */
|
||||
export interface ChatRequestOptions {
|
||||
/** Requested reasoning mode. */
|
||||
reasoning: ChatReasoningMode
|
||||
}
|
||||
|
||||
/** Describes the reasoning controls that AIRI implements for a provider. */
|
||||
export interface ChatReasoningCapability {
|
||||
/** Modes that AIRI can pass to the provider. */
|
||||
modes: readonly ChatReasoningMode[]
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './openai-compatible'
|
||||
export * from './run'
|
||||
+3
-5
@@ -1,10 +1,8 @@
|
||||
import type { ComposerTranslation } from 'vue-i18n'
|
||||
|
||||
import type { ProviderExtraMethods, ProviderInstance } from '../types'
|
||||
import type { ProviderExtraMethods, ProviderInstance, ProviderTranslator } from '../types'
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { providerOpenAI } from '../providers/openai'
|
||||
import { providerOpenAI } from '../providers/cloud/openai'
|
||||
import { ProviderValidationCheck } from '../types'
|
||||
import { createOpenAICompatibleValidators } from './openai-compatible'
|
||||
|
||||
@@ -24,7 +22,7 @@ vi.mock('@xsai/model', () => ({
|
||||
listModels: listModelsMock,
|
||||
}))
|
||||
|
||||
const mockT = vi.fn((key: string) => key) as unknown as ComposerTranslation
|
||||
const mockT = vi.fn((key: string) => key) as ProviderTranslator
|
||||
|
||||
async function getProviderValidators(options?: Parameters<typeof createOpenAICompatibleValidators>[0]) {
|
||||
const validators = createOpenAICompatibleValidators(options)
|
||||
+2
-4
@@ -1,13 +1,11 @@
|
||||
import type { ComposerTranslation } from 'vue-i18n'
|
||||
|
||||
import type { ProviderDefinition } from '../types'
|
||||
import type { ProviderDefinition, ProviderTranslator } from '../types'
|
||||
|
||||
import { createChatProvider } from '@xsai-ext/providers/utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { getValidatorsOfProvider, validateProvider } from './run'
|
||||
|
||||
const mockT = ((key: string) => key) as unknown as ComposerTranslation
|
||||
const mockT = ((key: string) => key) as ProviderTranslator
|
||||
|
||||
describe('validateProvider', () => {
|
||||
it('resolves async validator factories and validation requirements', async () => {
|
||||
@@ -0,0 +1,213 @@
|
||||
import type {
|
||||
ProviderConfigValidator,
|
||||
ProviderContext,
|
||||
ProviderDefinition,
|
||||
ProviderExtraMethods,
|
||||
ProviderInstance,
|
||||
ProviderRuntimeValidator,
|
||||
} from '../types'
|
||||
|
||||
import { errorMessageFrom, merge } from '@moeru/std'
|
||||
|
||||
export type ProviderValidationStepStatus = 'idle' | 'validating' | 'valid' | 'invalid'
|
||||
export type ProviderValidationStepKind = 'config' | 'provider'
|
||||
export interface ProviderValidationStep {
|
||||
id: string
|
||||
label: string
|
||||
status: ProviderValidationStepStatus
|
||||
reason: string
|
||||
kind: ProviderValidationStepKind
|
||||
}
|
||||
|
||||
export interface ProviderValidationPlan {
|
||||
steps: ProviderValidationStep[]
|
||||
config: Record<string, unknown>
|
||||
definition: ProviderDefinition
|
||||
configValidators: ProviderConfigValidator<Record<string, unknown>>[]
|
||||
providerValidators: ProviderRuntimeValidator<Record<string, unknown>>[]
|
||||
providerExtra: ProviderExtraMethods<Record<string, unknown>> | undefined
|
||||
shouldValidate: boolean
|
||||
}
|
||||
|
||||
interface ProviderValidationValidatorInfo {
|
||||
kind: ProviderValidationStepKind
|
||||
index: number
|
||||
step: ProviderValidationStep
|
||||
}
|
||||
|
||||
interface ProviderValidationValidatorResult {
|
||||
reason: string
|
||||
valid: boolean
|
||||
}
|
||||
|
||||
interface ProviderValidationValidatorSuccessInfo extends ProviderValidationValidatorInfo {
|
||||
result: ProviderValidationValidatorResult
|
||||
}
|
||||
|
||||
interface ProviderValidationValidatorErrorInfo extends ProviderValidationValidatorInfo {
|
||||
error: unknown
|
||||
}
|
||||
|
||||
export interface ProviderValidationCallbacks {
|
||||
onValidatorStart?: (info: ProviderValidationValidatorInfo) => void
|
||||
onValidatorSuccess?: (info: ProviderValidationValidatorSuccessInfo) => void
|
||||
onValidatorError?: (info: ProviderValidationValidatorErrorInfo) => void
|
||||
}
|
||||
|
||||
interface GetProviderValidationIntervalOptions {
|
||||
definition: ProviderDefinition
|
||||
contextOptions: ProviderContext
|
||||
defaultIntervalMs?: number
|
||||
}
|
||||
|
||||
interface GetValidatorsOfProviderOptions {
|
||||
definition: ProviderDefinition
|
||||
config: Record<string, unknown>
|
||||
schemaDefaults: Record<string, unknown>
|
||||
contextOptions: ProviderContext
|
||||
}
|
||||
|
||||
export function createConfigValidationSteps(configValidators: ProviderConfigValidator<Record<string, unknown>>[]): ProviderValidationStep[] {
|
||||
return configValidators.map(validator => ({
|
||||
id: validator.id,
|
||||
label: validator.name,
|
||||
status: 'idle' as ProviderValidationStepStatus,
|
||||
reason: '',
|
||||
kind: 'config' as ProviderValidationStepKind,
|
||||
}))
|
||||
}
|
||||
|
||||
export function createProviderValidationSteps(providerValidators: ProviderRuntimeValidator<Record<string, unknown>>[]): ProviderValidationStep[] {
|
||||
return providerValidators.map(validator => ({
|
||||
id: validator.id,
|
||||
label: validator.name,
|
||||
status: 'idle' as ProviderValidationStepStatus,
|
||||
reason: '',
|
||||
kind: 'provider' as ProviderValidationStepKind,
|
||||
}))
|
||||
}
|
||||
|
||||
export async function getProviderValidationIntervalMs(options: GetProviderValidationIntervalOptions) {
|
||||
const validators = await Promise.all((options.definition.validators?.validateProvider || []).map(creator => creator(options.contextOptions)))
|
||||
const defaultIntervalMs = options.defaultIntervalMs ?? 15_000
|
||||
const intervals = validators
|
||||
.filter(validator => validator.schedule?.mode === 'interval')
|
||||
.map(validator => validator.schedule?.intervalMs || defaultIntervalMs)
|
||||
|
||||
if (intervals.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return Math.min(...intervals)
|
||||
}
|
||||
|
||||
export async function getValidatorsOfProvider(options: GetValidatorsOfProviderOptions): Promise<ProviderValidationPlan> {
|
||||
const { definition } = options
|
||||
|
||||
const configValidators = await Promise.all((definition.validators?.validateConfig || []).map(creator => creator(options.contextOptions)))
|
||||
const allProviderValidators = await Promise.all((definition.validators?.validateProvider || []).map(creator => creator(options.contextOptions)))
|
||||
|
||||
const providerValidators = allProviderValidators
|
||||
|
||||
const steps: ProviderValidationStep[] = [
|
||||
...createConfigValidationSteps(configValidators),
|
||||
...createProviderValidationSteps(providerValidators),
|
||||
]
|
||||
|
||||
const normalizedConfig = merge(options.schemaDefaults, options.config)
|
||||
const validationRequired = definition.validationRequiredWhen ?? (() => false)
|
||||
const shouldValidate = await validationRequired(normalizedConfig)
|
||||
|
||||
return {
|
||||
steps,
|
||||
config: normalizedConfig,
|
||||
definition,
|
||||
configValidators: configValidators as ProviderValidationPlan['configValidators'],
|
||||
providerValidators: providerValidators as ProviderValidationPlan['providerValidators'],
|
||||
providerExtra: definition.extraMethods as ProviderValidationPlan['providerExtra'],
|
||||
shouldValidate,
|
||||
}
|
||||
}
|
||||
|
||||
export async function validateProvider(
|
||||
plan: ProviderValidationPlan,
|
||||
contextOptions: ProviderContext,
|
||||
callbacks: ProviderValidationCallbacks = {},
|
||||
) {
|
||||
const { configValidators, providerValidators, steps, config, definition, providerExtra } = plan
|
||||
const runContext = {
|
||||
...contextOptions,
|
||||
validationCache: new Map<string, unknown>(),
|
||||
}
|
||||
const { onValidatorError, onValidatorStart, onValidatorSuccess } = callbacks
|
||||
|
||||
const configResults = await Promise.all(configValidators.map(async (validatorDefinition, index) => {
|
||||
const step = steps[index]
|
||||
step.status = 'validating'
|
||||
step.reason = ''
|
||||
onValidatorStart?.({ kind: 'config', index, step })
|
||||
try {
|
||||
const result = await validatorDefinition.validator(config, runContext)
|
||||
step.status = result.valid ? 'valid' : 'invalid'
|
||||
step.reason = result.valid ? '' : result.reason
|
||||
onValidatorSuccess?.({ kind: 'config', index, step, result })
|
||||
return result
|
||||
}
|
||||
catch (error) {
|
||||
step.status = 'invalid'
|
||||
step.reason = errorMessageFrom(error) ?? 'Unknown error'
|
||||
onValidatorError?.({ kind: 'config', index, step, error })
|
||||
return { valid: false, reason: step.reason }
|
||||
}
|
||||
}))
|
||||
|
||||
const configIsValid = configResults.every(result => result.valid)
|
||||
|
||||
const providerStepOffset = configValidators.length
|
||||
if (!configIsValid) {
|
||||
for (let i = 0; i < providerValidators.length; i++) {
|
||||
const step = steps[providerStepOffset + i]
|
||||
step.status = 'invalid'
|
||||
step.reason = 'Fix configuration checks first.'
|
||||
}
|
||||
return steps
|
||||
}
|
||||
|
||||
let providerInstance: ProviderInstance
|
||||
try {
|
||||
providerInstance = await definition.createProvider(config)
|
||||
}
|
||||
catch (error) {
|
||||
for (let i = 0; i < providerValidators.length; i++) {
|
||||
const step = steps[providerStepOffset + i]
|
||||
step.status = 'invalid'
|
||||
step.reason = errorMessageFrom(error) ?? 'Unknown error'
|
||||
}
|
||||
return steps
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all(providerValidators.map(async (validatorDefinition, index) => {
|
||||
const step = steps[providerStepOffset + index]
|
||||
step.status = 'validating'
|
||||
step.reason = ''
|
||||
onValidatorStart?.({ kind: 'provider', index, step })
|
||||
try {
|
||||
const result = await validatorDefinition.validator(config, providerInstance, providerExtra ?? {}, runContext)
|
||||
step.status = result.valid ? 'valid' : 'invalid'
|
||||
step.reason = result.valid ? '' : result.reason
|
||||
onValidatorSuccess?.({ kind: 'provider', index, step, result })
|
||||
}
|
||||
catch (error) {
|
||||
step.status = 'invalid'
|
||||
step.reason = errorMessageFrom(error) ?? 'Unknown error'
|
||||
onValidatorError?.({ kind: 'provider', index, step, error })
|
||||
}
|
||||
}))
|
||||
}
|
||||
finally {
|
||||
await (providerInstance as ProviderInstance & { dispose?: () => Promise<void> | void }).dispose?.()
|
||||
}
|
||||
|
||||
return steps
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"lib": [
|
||||
"DOM",
|
||||
"DOM.AsyncIterable",
|
||||
"ESNext"
|
||||
],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"types": [
|
||||
"node",
|
||||
"vitest"
|
||||
],
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"noUnusedLocals": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"tsdown.config.ts",
|
||||
"vitest.config.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"dist",
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'tsdown'
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['./src/index.ts'],
|
||||
sourcemap: true,
|
||||
unused: true,
|
||||
})
|
||||
@@ -0,0 +1,31 @@
|
||||
import { playwright } from '@vitest/browser-playwright'
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
root: import.meta.dirname,
|
||||
test: {
|
||||
projects: [
|
||||
{
|
||||
extends: true,
|
||||
test: {
|
||||
name: 'node',
|
||||
include: ['src/**/*.test.ts'],
|
||||
exclude: ['src/**/*.browser.test.ts'],
|
||||
},
|
||||
},
|
||||
{
|
||||
extends: true,
|
||||
test: {
|
||||
name: 'browser',
|
||||
include: ['src/**/*.browser.test.ts'],
|
||||
browser: {
|
||||
enabled: true,
|
||||
headless: true,
|
||||
provider: playwright(),
|
||||
instances: [{ browser: 'chromium' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
@@ -27,6 +27,7 @@
|
||||
"@proj-airi/audio": "workspace:^",
|
||||
"@proj-airi/ccc": "workspace:*",
|
||||
"@proj-airi/i18n": "workspace:*",
|
||||
"@proj-airi/provider-inference": "workspace:^",
|
||||
"@proj-airi/server-sdk": "workspace:*",
|
||||
"@proj-airi/stage-ui": "workspace:*",
|
||||
"@proj-airi/stage-ui-live2d": "workspace:*",
|
||||
|
||||
+1
-1
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { RemovableRef } from '@vueuse/core'
|
||||
|
||||
import { streamWebSpeechAPITranscription } from '@proj-airi/provider-inference'
|
||||
import { errorMessageFromValue } from '@proj-airi/stage-shared'
|
||||
import {
|
||||
Alert,
|
||||
@@ -10,7 +11,6 @@ import {
|
||||
ProviderSettingsLayout,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { selectProviderMetadata } from '@proj-airi/stage-ui/libs'
|
||||
import { streamWebSpeechAPITranscription } from '@proj-airi/stage-ui/libs/providers/providers/browser-web-speech-api'
|
||||
import { useProviderConfigStore } from '@proj-airi/stage-ui/stores/providers/config'
|
||||
import { useProviderStore } from '@proj-airi/stage-ui/stores/providers/provider'
|
||||
import { useSettingsAudioDevice } from '@proj-airi/stage-ui/stores/settings'
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
"./libs/pinia": "./src/libs/pinia/index.ts",
|
||||
"./libs/providers/stream-transcription": "./src/libs/providers/stream-transcription/index.ts",
|
||||
"./libs/providers/providers/aliyun-nls": "./src/libs/providers/providers/aliyun-nls/index.ts",
|
||||
"./libs/providers/providers/browser-web-speech-api": "./src/libs/providers/providers/browser-web-speech-api/index.ts",
|
||||
"./libs/*": "./src/libs/*.ts",
|
||||
"./libs": "./src/libs/index.ts",
|
||||
"./models/*": "./src/models/*.ts",
|
||||
@@ -95,6 +94,7 @@
|
||||
"@proj-airi/model-driver-magic-live2d": "workspace:^",
|
||||
"@proj-airi/motion-driver-magic": "workspace:^",
|
||||
"@proj-airi/pipelines-audio": "workspace:^",
|
||||
"@proj-airi/provider-inference": "workspace:^",
|
||||
"@proj-airi/server-sdk": "workspace:^",
|
||||
"@proj-airi/server-sdk-shared": "workspace:^",
|
||||
"@proj-airi/stage-shared": "workspace:^",
|
||||
|
||||
@@ -4,3 +4,4 @@ export * from './metadata'
|
||||
export * from './providers'
|
||||
export * from './types'
|
||||
export * from './validators'
|
||||
export * from './views'
|
||||
|
||||
@@ -60,8 +60,8 @@ export function getProviderCategory(tasks: string[]): ProviderCategory {
|
||||
}
|
||||
|
||||
/** Selects the serializable metadata fields of a provider definition. */
|
||||
export async function selectProviderMetadata(
|
||||
definition: ProviderDefinition,
|
||||
export async function selectProviderMetadata<TConfig>(
|
||||
definition: ProviderDefinition<TConfig>,
|
||||
t: ComposerTranslation,
|
||||
options: {
|
||||
category?: ProviderCategory
|
||||
@@ -113,7 +113,7 @@ export async function selectProviderMetadata(
|
||||
}
|
||||
|
||||
/** Selects serializable metadata for a provider definition list. */
|
||||
export async function selectProvidersMetadata(definitions: ProviderDefinition[], t: ComposerTranslation) {
|
||||
export async function selectProvidersMetadata<TConfig>(definitions: ProviderDefinition<TConfig>[], t: ComposerTranslation) {
|
||||
return Object.fromEntries(await Promise.all(definitions.map(async definition => [
|
||||
definition.id,
|
||||
await selectProviderMetadata(definition, t),
|
||||
|
||||
@@ -25,7 +25,7 @@ const aliyunNlsConfigSchema = z.object({
|
||||
|
||||
type AliyunNlsConfig = z.input<typeof aliyunNlsConfigSchema>
|
||||
|
||||
export const providerAliyunNlsTranscription = defineProvider<AliyunNlsConfig>({
|
||||
export const providerAliyunNlsTranscription = defineProvider<AliyunNlsConfig, 'aliyun-nls-transcription'>({
|
||||
id: 'aliyun-nls-transcription',
|
||||
name: 'Aliyun NLS',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.aliyun-nls.title'),
|
||||
|
||||
@@ -61,6 +61,10 @@ describe('apple speech transcription provider', () => {
|
||||
expect(mocks.dispose).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('exposes its Hearing settings view through the provider definition', () => {
|
||||
expect(providerAppleSpeechTranscription.views?.hearing).toBeTypeOf('function')
|
||||
})
|
||||
|
||||
it('converts PCM16 input and emits AIRI transcript snapshots', async () => {
|
||||
const writtenSamples: Float32Array[] = []
|
||||
const finalResult: TranscriptionResult = {
|
||||
|
||||
@@ -22,6 +22,7 @@ export type { AppleSpeechConfig } from './provider'
|
||||
export { listAppleSpeechLocaleOptions } from './provider'
|
||||
|
||||
export const APPLE_SPEECH_TRANSCRIPTION_PROVIDER_ID = 'apple-speech-transcription'
|
||||
type AppleSpeechTranscriptionProviderId = typeof APPLE_SPEECH_TRANSCRIPTION_PROVIDER_ID
|
||||
|
||||
/** Request options applied by AIRI before Apple Speech creates a batch or live session. */
|
||||
export interface AppleSpeechProviderOptions {
|
||||
@@ -214,7 +215,7 @@ export function executeAppleSpeechStream(options: StreamTranscriptionOptions): A
|
||||
}
|
||||
}
|
||||
|
||||
export const providerAppleSpeechTranscription = defineProvider<AppleSpeechConfig>({
|
||||
export const providerAppleSpeechTranscription = defineProvider<AppleSpeechConfig, AppleSpeechTranscriptionProviderId>({
|
||||
id: APPLE_SPEECH_TRANSCRIPTION_PROVIDER_ID,
|
||||
name: 'Apple Speech',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.apple-speech-transcription.title'),
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { JsonSchema } from 'xsschema'
|
||||
|
||||
import { getDefinedProvider } from '@proj-airi/provider-inference'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { createSparkCommandTool } from '../../../../tools/character/orchestrator/spark-command'
|
||||
import { providerAzureOpenAI } from './index'
|
||||
|
||||
interface ChatRequestBody {
|
||||
tools: Array<{
|
||||
@@ -41,7 +41,11 @@ describe('providerAzureOpenAI tool schemas', () => {
|
||||
const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(new Response('{}'))
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
|
||||
const provider = await providerAzureOpenAI.createProvider({
|
||||
const providerDefinition = getDefinedProvider('azure-openai')
|
||||
if (!providerDefinition)
|
||||
throw new Error('Azure OpenAI provider definition is not registered.')
|
||||
|
||||
const provider = await providerDefinition.createProvider({
|
||||
apiKey: 'test-key',
|
||||
baseUrl: 'https://example.openai.azure.com/openai/',
|
||||
})
|
||||
|
||||
@@ -1,56 +1,15 @@
|
||||
import './amazon-bedrock'
|
||||
import './apple-speech'
|
||||
import './openai'
|
||||
import './openai-audio'
|
||||
import './aihubmix'
|
||||
import { portableProviderDefinitions } from '@proj-airi/provider-inference'
|
||||
|
||||
import { registerProviders } from './registry'
|
||||
|
||||
import './aliyun-nls'
|
||||
import './lm-studio'
|
||||
import './apple-speech'
|
||||
import './local-audio'
|
||||
import './index-tts-vllm'
|
||||
import './kokoro-local'
|
||||
import './azure-openai'
|
||||
import './openai-compatible'
|
||||
import './atlascloud'
|
||||
import './volcengine-coding-plan'
|
||||
import './byteplus'
|
||||
import './byteplus-coding-plan'
|
||||
import './browser-web-speech-api'
|
||||
import './n1n'
|
||||
import './openpaths'
|
||||
import './openrouter-ai'
|
||||
import './openrouter-audio-speech'
|
||||
import './nvidia'
|
||||
import './groq'
|
||||
import './anthropic'
|
||||
import './google-generative-ai'
|
||||
import './google-gemini-audio-speech'
|
||||
import './deepseek'
|
||||
import './elevenlabs'
|
||||
import './302-ai'
|
||||
import './cerebras-ai'
|
||||
import './together-ai'
|
||||
import './xai'
|
||||
import './zai'
|
||||
import './novita-ai'
|
||||
import './fireworks-ai'
|
||||
import './featherless-ai'
|
||||
import './comet-api'
|
||||
import './perplexity-ai'
|
||||
import './player2-speech'
|
||||
import './minimax'
|
||||
import './minimax-speech'
|
||||
import './mistral-ai'
|
||||
import './moonshot-ai'
|
||||
import './modelscope'
|
||||
import './ollama'
|
||||
import './mimo'
|
||||
import './mimo-audio'
|
||||
import './cloudflare-workers-ai'
|
||||
import './azure-ai-foundry'
|
||||
import './official'
|
||||
import './speech-noop'
|
||||
import './unspeech'
|
||||
import './voicevox'
|
||||
|
||||
registerProviders(portableProviderDefinitions)
|
||||
|
||||
export {
|
||||
getDefaultStreamingModel,
|
||||
@@ -59,5 +18,8 @@ export {
|
||||
|
||||
export {
|
||||
getDefinedProvider,
|
||||
isProviderId,
|
||||
listProviders,
|
||||
} from './registry'
|
||||
|
||||
export type { StageProviderId } from './registry'
|
||||
|
||||
@@ -87,7 +87,7 @@ async function isBrowserAndMemoryEnough() {
|
||||
return false
|
||||
}
|
||||
|
||||
export const providerAppLocalAudioSpeech = defineProvider<LocalAudioConfig>({
|
||||
export const providerAppLocalAudioSpeech = defineProvider<LocalAudioConfig, 'app-local-audio-speech'>({
|
||||
id: 'app-local-audio-speech',
|
||||
name: 'App (Local)',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.app-local-audio-speech.title'),
|
||||
@@ -101,7 +101,7 @@ export const providerAppLocalAudioSpeech = defineProvider<LocalAudioConfig>({
|
||||
validators: createLocalAudioValidators(),
|
||||
})
|
||||
|
||||
export const providerAppLocalAudioTranscription = defineProvider<LocalAudioConfig>({
|
||||
export const providerAppLocalAudioTranscription = defineProvider<LocalAudioConfig, 'app-local-audio-transcription'>({
|
||||
id: 'app-local-audio-transcription',
|
||||
name: 'App (Local)',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.app-local-audio-transcription.title'),
|
||||
@@ -123,7 +123,7 @@ export const providerAppLocalAudioTranscription = defineProvider<LocalAudioConfi
|
||||
validators: createLocalAudioValidators(),
|
||||
})
|
||||
|
||||
export const providerBrowserLocalAudioSpeech = defineProvider<LocalAudioConfig>({
|
||||
export const providerBrowserLocalAudioSpeech = defineProvider<LocalAudioConfig, 'browser-local-audio-speech'>({
|
||||
id: 'browser-local-audio-speech',
|
||||
name: 'Browser (Local)',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.browser-local-audio-speech.title'),
|
||||
@@ -137,7 +137,7 @@ export const providerBrowserLocalAudioSpeech = defineProvider<LocalAudioConfig>(
|
||||
validators: createLocalAudioValidators(),
|
||||
})
|
||||
|
||||
export const providerBrowserLocalAudioTranscription = defineProvider<LocalAudioConfig>({
|
||||
export const providerBrowserLocalAudioTranscription = defineProvider<LocalAudioConfig, 'browser-local-audio-transcription'>({
|
||||
id: 'browser-local-audio-transcription',
|
||||
name: 'Browser (Local)',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.browser-local-audio-transcription.title'),
|
||||
|
||||
@@ -19,7 +19,7 @@ const nvidiaConfigSchema = z.object({
|
||||
|
||||
type NvidiaConfig = z.input<typeof nvidiaConfigSchema>
|
||||
|
||||
export const providerNvidia = defineProvider<NvidiaConfig>({
|
||||
export const providerNvidia = defineProvider<NvidiaConfig, 'nvidia'>({
|
||||
id: 'nvidia',
|
||||
name: 'NVIDIA NIM',
|
||||
nameLocalize: ({ t }) => t('settings.pages.providers.provider.nvidia.title'),
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import type { ChatRequestOptions } from '@proj-airi/provider-inference'
|
||||
import type { ChatProviderWithExtraOptions } from '@xsai-ext/providers/utils'
|
||||
import type { JsonSchema } from 'xsschema'
|
||||
|
||||
import type { ChatRequestOptions } from '../../types'
|
||||
|
||||
import { getDefinedProvider } from '@proj-airi/provider-inference'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { createSparkCommandTool } from '../../../../tools/character/orchestrator/spark-command'
|
||||
import { providerOpenRouterAI } from './index'
|
||||
|
||||
interface ChatRequestBody {
|
||||
tools: Array<{
|
||||
@@ -37,7 +36,11 @@ describe('providerOpenRouterAI tool schemas', () => {
|
||||
})
|
||||
|
||||
it('maps AIRI reasoning modes to OpenRouter request fields', async () => {
|
||||
const provider = await providerOpenRouterAI.createProvider({
|
||||
const providerDefinition = getDefinedProvider('openrouter-ai')
|
||||
if (!providerDefinition)
|
||||
throw new Error('OpenRouter provider definition is not registered.')
|
||||
|
||||
const provider = await providerDefinition.createProvider({
|
||||
apiKey: 'test-key',
|
||||
}) as ChatProviderWithExtraOptions<string, ChatRequestOptions>
|
||||
|
||||
@@ -56,7 +59,11 @@ describe('providerOpenRouterAI tool schemas', () => {
|
||||
const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(new Response('{}'))
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
|
||||
const provider = await providerOpenRouterAI.createProvider({
|
||||
const providerDefinition = getDefinedProvider('openrouter-ai')
|
||||
if (!providerDefinition)
|
||||
throw new Error('OpenRouter provider definition is not registered.')
|
||||
|
||||
const provider = await providerDefinition.createProvider({
|
||||
apiKey: 'test-key',
|
||||
})
|
||||
if (!('chat' in provider))
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { ComposerTranslation } from 'vue-i18n'
|
||||
import type { ProviderTranslator } from '@proj-airi/provider-inference'
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { StageProviderId } from './registry'
|
||||
|
||||
import { describe, expect, expectTypeOf, it } from 'vitest'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { providerAliyunNlsTranscription } from './aliyun-nls'
|
||||
import { providerBrowserWebSpeechApi } from './browser-web-speech-api'
|
||||
import { providerElevenLabs } from './elevenlabs'
|
||||
import {
|
||||
providerAppLocalAudioSpeech,
|
||||
providerAppLocalAudioTranscription,
|
||||
@@ -13,13 +13,30 @@ import {
|
||||
providerBrowserLocalAudioTranscription,
|
||||
} from './local-audio'
|
||||
import { getDefinedProvider } from './registry'
|
||||
import { providerSpeechNoop } from './speech-noop'
|
||||
|
||||
import './index'
|
||||
|
||||
const translate = ((key: string) => key) as unknown as ComposerTranslation
|
||||
const translate = ((key: string) => key) as ProviderTranslator
|
||||
|
||||
function getRequiredProvider(id: string) {
|
||||
const provider = getDefinedProvider(id)
|
||||
if (!provider)
|
||||
throw new Error(`Provider definition "${id}" is not registered.`)
|
||||
|
||||
return provider
|
||||
}
|
||||
|
||||
describe('migrated provider definitions', () => {
|
||||
it('exposes a closed provider id union to stage-ui consumers', () => {
|
||||
expectTypeOf<'openai'>().toExtend<StageProviderId>()
|
||||
expectTypeOf<'official-provider'>().toExtend<StageProviderId>()
|
||||
expectTypeOf<string>().not.toExtend<StageProviderId>()
|
||||
})
|
||||
|
||||
it('returns no definition for an unknown runtime provider id', () => {
|
||||
expect(getDefinedProvider('unknown-provider')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('registers every provider that moved out of the legacy store', () => {
|
||||
const providerIds = [
|
||||
'speech-noop',
|
||||
@@ -57,7 +74,8 @@ describe('migrated provider definitions', () => {
|
||||
})
|
||||
|
||||
it('creates the no-op speech provider through ProviderDefinition', async () => {
|
||||
const provider = await providerSpeechNoop.createProvider({})
|
||||
const definition = getRequiredProvider('speech-noop')
|
||||
const provider = await definition.createProvider({})
|
||||
|
||||
expect(provider).toHaveProperty('speech')
|
||||
expect('speech' in provider && provider.speech('unused')).toMatchObject({
|
||||
@@ -111,7 +129,8 @@ describe('migrated provider definitions', () => {
|
||||
})
|
||||
|
||||
it('describes Web Speech API streaming support without runtime state', async () => {
|
||||
const defaults = z.parse(await providerBrowserWebSpeechApi.createProviderConfig({ t: translate }), {})
|
||||
const definition = getRequiredProvider('browser-web-speech-api')
|
||||
const defaults = z.parse(await definition.createProviderConfig({ t: translate }), {})
|
||||
|
||||
expect(defaults).toEqual({
|
||||
language: 'en-US',
|
||||
@@ -119,19 +138,20 @@ describe('migrated provider definitions', () => {
|
||||
interimResults: true,
|
||||
maxAlternatives: 1,
|
||||
})
|
||||
expect(providerBrowserWebSpeechApi.capabilities?.transcription).toEqual({
|
||||
expect(definition.capabilities?.transcription).toEqual({
|
||||
protocol: 'http',
|
||||
generateOutput: false,
|
||||
streamOutput: true,
|
||||
streamInput: true,
|
||||
})
|
||||
expect(await providerBrowserWebSpeechApi.isAvailableBy?.()).toBe(false)
|
||||
expect(await definition.isAvailableBy?.()).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps ElevenLabs configuration and model discovery in the definition', async () => {
|
||||
const defaults = z.parse(await providerElevenLabs.createProviderConfig({ t: translate }), { apiKey: 'test' })
|
||||
const provider = await providerElevenLabs.createProvider(defaults)
|
||||
const models = await providerElevenLabs.extraMethods?.listModels?.(defaults, provider)
|
||||
const definition = getRequiredProvider('elevenlabs')
|
||||
const defaults = z.parse(await definition.createProviderConfig({ t: translate }), { apiKey: 'test' })
|
||||
const provider = await definition.createProvider(defaults)
|
||||
const models = await definition.extraMethods?.listModels?.(defaults, provider)
|
||||
|
||||
expect(defaults).toMatchObject({
|
||||
baseUrl: 'https://unspeech.hyp3r.link/v1/',
|
||||
|
||||
@@ -1,30 +1,87 @@
|
||||
import type { PortableProviderId } from '@proj-airi/provider-inference'
|
||||
import type { MaybePromise } from 'clustr'
|
||||
import type { $ZodType } from 'zod/v4/core'
|
||||
|
||||
import type { ProviderConfigContext, ProviderDefinition } from '../types'
|
||||
import type { providerAliyunNlsTranscription } from './aliyun-nls'
|
||||
import type { providerAppleSpeechTranscription } from './apple-speech'
|
||||
import type { providerKokoroLocal } from './kokoro-local'
|
||||
import type {
|
||||
providerAppLocalAudioSpeech,
|
||||
providerAppLocalAudioTranscription,
|
||||
providerBrowserLocalAudioSpeech,
|
||||
providerBrowserLocalAudioTranscription,
|
||||
} from './local-audio'
|
||||
import type { providerNvidia } from './nvidia'
|
||||
import type {
|
||||
providerOfficialChat,
|
||||
providerOfficialSpeech,
|
||||
providerOfficialSpeechStreaming,
|
||||
providerOfficialTranscription,
|
||||
} from './official'
|
||||
|
||||
import { orderBy } from 'es-toolkit'
|
||||
|
||||
const providerRegistry = new Map<string, ProviderDefinition>()
|
||||
|
||||
type StageOnlyProviderId
|
||||
= | typeof providerAliyunNlsTranscription.id
|
||||
| typeof providerAppleSpeechTranscription.id
|
||||
| typeof providerAppLocalAudioSpeech.id
|
||||
| typeof providerAppLocalAudioTranscription.id
|
||||
| typeof providerBrowserLocalAudioSpeech.id
|
||||
| typeof providerBrowserLocalAudioTranscription.id
|
||||
| typeof providerKokoroLocal.id
|
||||
| typeof providerNvidia.id
|
||||
| typeof providerOfficialChat.id
|
||||
| typeof providerOfficialSpeech.id
|
||||
| typeof providerOfficialSpeechStreaming.id
|
||||
| typeof providerOfficialTranscription.id
|
||||
|
||||
/** IDs of definitions registered by stage-ui, including portable definitions. */
|
||||
export type StageProviderId = PortableProviderId | StageOnlyProviderId
|
||||
|
||||
/** Adds portable definitions to the stage-ui registry after the runtime definitions load. */
|
||||
export function registerProviders(definitions: readonly ProviderDefinition[]): void {
|
||||
for (const definition of definitions) {
|
||||
if (providerRegistry.has(definition.id))
|
||||
throw new Error(`Provider definition "${definition.id}" is registered more than once.`)
|
||||
|
||||
providerRegistry.set(definition.id, definition)
|
||||
}
|
||||
}
|
||||
|
||||
export function listProviders(): ProviderDefinition[] {
|
||||
const providerDefs = Array.from(providerRegistry.values()).map(def => ({ order: 99999, ...def }))
|
||||
const sorted = orderBy(providerDefs, [p => p.order, 'name'], ['asc', 'asc'])
|
||||
return sorted
|
||||
}
|
||||
|
||||
/** Narrows a runtime provider ID to a definition registered by stage-ui. */
|
||||
export function isProviderId(id: string): id is StageProviderId {
|
||||
return providerRegistry.has(id)
|
||||
}
|
||||
|
||||
/** Returns a registered definition for a literal or runtime provider ID. */
|
||||
export function getDefinedProvider(id: string): ProviderDefinition | undefined {
|
||||
if (!providerRegistry.has(id))
|
||||
return undefined
|
||||
|
||||
return providerRegistry.get(id)
|
||||
}
|
||||
|
||||
export function defineProvider<T>(definition: {
|
||||
interface ProviderDefinitionOptions<T, TId extends string = string> extends ProviderDefinition<T, TId> {
|
||||
createProviderConfig: (contextOptions: ProviderConfigContext<T>) => MaybePromise<$ZodType<T>>
|
||||
} & ProviderDefinition<T>): ProviderDefinition<T> {
|
||||
}
|
||||
|
||||
export function defineProvider<T, const TId extends string = string>(definition: ProviderDefinitionOptions<T, TId>): ProviderDefinition<T, TId> {
|
||||
const provider = {
|
||||
...definition,
|
||||
}
|
||||
|
||||
providerRegistry.set(definition.id, definition)
|
||||
// The registry selects a provider by id. The selected configuration shape is
|
||||
// only known by the caller after that lookup.
|
||||
providerRegistry.set(definition.id, definition as unknown as ProviderDefinition)
|
||||
|
||||
return provider
|
||||
}
|
||||
|
||||
@@ -1,317 +1,40 @@
|
||||
import type {
|
||||
ChatProvider,
|
||||
ChatProviderWithExtraOptions,
|
||||
EmbedProvider,
|
||||
EmbedProviderWithExtraOptions,
|
||||
ModelProvider,
|
||||
ModelProviderWithExtraOptions,
|
||||
SpeechProvider,
|
||||
SpeechProviderWithExtraOptions,
|
||||
TranscriptionProvider,
|
||||
TranscriptionProviderWithExtraOptions,
|
||||
} from '@xsai-ext/providers/utils'
|
||||
import type { ProgressInfo } from '@xsai-transformers/shared/types'
|
||||
import type { MaybePromise } from 'clustr'
|
||||
import type { Component } from 'vue'
|
||||
import type { ComposerTranslation } from 'vue-i18n'
|
||||
import type { $ZodType } from 'zod/v4/core'
|
||||
import type { ProviderDefinition as CoreProviderDefinition } from '@proj-airi/provider-inference'
|
||||
|
||||
export type ProviderInstance
|
||||
= | ChatProvider
|
||||
| ChatProviderWithExtraOptions
|
||||
| EmbedProvider
|
||||
| EmbedProviderWithExtraOptions
|
||||
| SpeechProvider
|
||||
| SpeechProviderWithExtraOptions
|
||||
| TranscriptionProvider
|
||||
| TranscriptionProviderWithExtraOptions
|
||||
| ModelProvider
|
||||
| ModelProviderWithExtraOptions
|
||||
|
||||
/** Validation lifecycle for one serializable provider configuration. */
|
||||
export type ProviderValidationStatus = 'unconfigured' | 'validating' | 'configured' | 'invalid' | 'bypassed'
|
||||
export type ProviderConfiguredBy = 'user' | 'authentication'
|
||||
|
||||
/** Serializable configuration for one provider instance. */
|
||||
export interface InferenceServiceProvider {
|
||||
/** Stable provider instance id. */
|
||||
id: string
|
||||
/** Provider definition id from the built-in provider registry. */
|
||||
definitionId: string
|
||||
/** Provider-specific configuration values. */
|
||||
config: Record<string, unknown>
|
||||
/** Current validation state for this provider configuration. */
|
||||
status: ProviderValidationStatus
|
||||
/** Lifecycle owner that creates and revokes this provider configuration. */
|
||||
configuredBy: ProviderConfiguredBy
|
||||
}
|
||||
|
||||
export function isModelProvider(providerInstance: ProviderInstance): providerInstance is ModelProvider | ModelProviderWithExtraOptions {
|
||||
if ('model' in providerInstance && typeof providerInstance.model === 'function') {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export interface ProviderOnboardingField {
|
||||
key: string
|
||||
type: 'text' | 'password'
|
||||
label: string
|
||||
description?: string
|
||||
placeholder?: string
|
||||
required?: boolean
|
||||
defaultValue?: string
|
||||
}
|
||||
|
||||
/** Inputs available while a Provider builds its configuration schema. */
|
||||
export interface ProviderConfigContext<TConfig> {
|
||||
/** Cancels runtime discovery that contributes schema metadata. */
|
||||
abortSignal?: AbortSignal
|
||||
/** Current draft values. Providers can use them to resolve dependent fields. */
|
||||
config?: Partial<TConfig>
|
||||
/** Translates labels and descriptions for the active interface locale. */
|
||||
t: ComposerTranslation
|
||||
}
|
||||
|
||||
/** Serializable model discovery result returned across renderer boundaries. */
|
||||
export interface ProviderModelCatalog {
|
||||
/** Models discovered for this provider. */
|
||||
models: ModelInfo[]
|
||||
/** Whether the server exposes this catalog. Absent when discovery did not return an authoritative state. */
|
||||
available?: boolean
|
||||
/** Server-selected model id, or null when the server has no default. */
|
||||
defaultModel?: string | null
|
||||
}
|
||||
|
||||
export interface ProviderExtraMethods<TConfig> {
|
||||
listModelCatalog?: (config: TConfig, provider: ProviderInstance, contextOptions?: { t: (input: string) => string }) => Promise<ProviderModelCatalog>
|
||||
listModels?: (config: TConfig, provider: ProviderInstance, contextOptions?: { t: (input: string) => string }) => Promise<ModelInfo[]>
|
||||
/**
|
||||
* Returns the voice catalogue. `model` lets providers whose voices vary by
|
||||
* model variant (Volcengine streaming TTS 1.0 vs 2.0 differ in catalogue)
|
||||
* narrow the result. Providers with a single catalogue ignore it.
|
||||
*/
|
||||
listVoices?: (config: TConfig, provider: ProviderInstance, model?: string) => Promise<VoiceInfo[]>
|
||||
loadModel?: (config: TConfig, provider: ProviderInstance, hooks?: { onProgress?: (progress: ProgressInfo) => Promise<void> | void }) => Promise<void>
|
||||
}
|
||||
|
||||
export interface ProviderValidationResult {
|
||||
errors: Array<{ error: unknown, errorKey?: string }>
|
||||
reason: string
|
||||
reasonKey: string
|
||||
valid: boolean
|
||||
}
|
||||
import type { ProviderViews } from './views'
|
||||
|
||||
/**
|
||||
* Validator ID fragment for the chat completions probe.
|
||||
* Matched via `.includes()` against validator instance ids
|
||||
* (e.g. `openai-compatible:check-chat-completions`).
|
||||
* Stage-ui extends portable definitions with Vue-owned views.
|
||||
*
|
||||
* The core provider contract remains runtime-neutral.
|
||||
*/
|
||||
export const CHAT_COMPLETIONS_VALIDATOR_ID = 'check-chat-completions'
|
||||
|
||||
export enum ProviderValidationCheck {
|
||||
/** Lightweight GET to /models endpoint to check reachability (definition system) */
|
||||
Connectivity = 'connectivity',
|
||||
/** Fetch model list and verify non-empty */
|
||||
ModelList = 'model_list',
|
||||
/** Send generateText ping with fine-grained error handling and caching (definition system) */
|
||||
ChatCompletions = 'chat_completions',
|
||||
/**
|
||||
* @deprecated
|
||||
* Being used in builder system (a deprecated provider creation protocol),
|
||||
* currently used by only OpenAI TTS && OpenAI Transcription.
|
||||
* Send generateText ping with simple pass/fail, fallback to 'test' model (builder system)
|
||||
*/
|
||||
Health = 'health',
|
||||
export interface ProviderDefinition<TConfig = Record<string, unknown>, TId extends string = string> extends CoreProviderDefinition<TConfig, TId> {
|
||||
/** Optional stage-ui views for this Provider. */
|
||||
views?: ProviderViews
|
||||
}
|
||||
|
||||
export interface ProviderValidatorSchedule {
|
||||
mode: 'once' | 'interval'
|
||||
intervalMs?: number
|
||||
}
|
||||
export {
|
||||
CHAT_COMPLETIONS_VALIDATOR_ID,
|
||||
isModelProvider,
|
||||
ProviderValidationCheck,
|
||||
} from '@proj-airi/provider-inference'
|
||||
|
||||
export interface ProviderConfigValidator<TConfig> {
|
||||
id: string
|
||||
name: string
|
||||
validator: (config: TConfig, contextOptions: { t: ComposerTranslation }) => MaybePromise<ProviderValidationResult>
|
||||
schedule?: ProviderValidatorSchedule
|
||||
}
|
||||
|
||||
export interface ProviderRuntimeValidator<TConfig> {
|
||||
id: string
|
||||
name: string
|
||||
validator: (config: TConfig, provider: ProviderInstance, providerExtra: ProviderExtraMethods<TConfig>, contextOptions: { t: ComposerTranslation }) => MaybePromise<ProviderValidationResult>
|
||||
schedule?: ProviderValidatorSchedule
|
||||
}
|
||||
|
||||
export interface ModelInfo {
|
||||
id: string
|
||||
name: string
|
||||
provider: string
|
||||
description?: string
|
||||
capabilities?: string[]
|
||||
contextLength?: number
|
||||
deprecated?: boolean
|
||||
}
|
||||
|
||||
export interface VoiceInfo {
|
||||
id: string
|
||||
name: string
|
||||
provider: string
|
||||
compatibleModels?: string[]
|
||||
description?: string
|
||||
gender?: string
|
||||
deprecated?: boolean
|
||||
previewURL?: string
|
||||
languages: {
|
||||
code: string
|
||||
title: string
|
||||
}[]
|
||||
}
|
||||
|
||||
// eslint-disable-next-line ts/no-unnecessary-type-constraint
|
||||
export interface ProviderDefinition<TConfig extends any = any> {
|
||||
id: string
|
||||
order?: number
|
||||
tasks: string[]
|
||||
nameLocalize: (ctx: { t: (input: string) => string }) => string // i18n key for provider name
|
||||
name: string // Default name (fallback)
|
||||
descriptionLocalize: (ctx: { t: (input: string) => string }) => string // i18n key for provider description
|
||||
description: string // Default description (fallback)
|
||||
/**
|
||||
* Iconify JSON icon name for the provider.
|
||||
*
|
||||
* Icons are available for most of the AI provides under @proj-airi/lobe-icons.
|
||||
*/
|
||||
icon?: string
|
||||
iconColor?: string
|
||||
/**
|
||||
* In case of having image instead of icon, you can specify the image URL here.
|
||||
*/
|
||||
iconImage?: string
|
||||
|
||||
/**
|
||||
* Indicates whether the provider is available.
|
||||
* If not specified, the provider is always available.
|
||||
*
|
||||
* May be specified when any of the following criteria is required:
|
||||
*
|
||||
* Platform requirements:
|
||||
*
|
||||
* - app-* providers are only available on desktop, this is responsible for Tauri runtime checks
|
||||
* - web-* providers are only available on web, this means Node.js and Tauri should not be imported or used
|
||||
*
|
||||
* System spec requirements:
|
||||
*
|
||||
* - may requires WebGPU / NVIDIA / other types of GPU,
|
||||
* on Web, WebGPU will automatically compiled to use targeting GPU hardware
|
||||
* - may requires significant amount of GPU memory to run, especially for
|
||||
* using of small language models within browser or Tauri app
|
||||
* - may requires significant amount of memory to run, especially for those
|
||||
* non-WebGPU supported environments.
|
||||
*/
|
||||
isAvailableBy?: () => MaybePromise<boolean>
|
||||
|
||||
/**
|
||||
* If false, the provider does not require user-provided credentials (e.g. API keys).
|
||||
* Used for built-in providers that authenticate via JWT Bearer tokens.
|
||||
*/
|
||||
requiresCredentials?: boolean
|
||||
|
||||
/**
|
||||
* Lifecycle owner for provider configurations created from this definition.
|
||||
*
|
||||
* @default 'user'
|
||||
*/
|
||||
configuredBy?: ProviderConfiguredBy
|
||||
|
||||
/** Provider-owned controls for module settings pages. */
|
||||
views?: {
|
||||
/** Lazily loads additional controls shown for this Provider in the Hearing module. */
|
||||
hearing?: () => Promise<{ default: Component }>
|
||||
}
|
||||
|
||||
/** Builds the validation schema and its UI metadata for the current draft. */
|
||||
createProviderConfig: (contextOptions: ProviderConfigContext<TConfig>) => MaybePromise<$ZodType<TConfig>>
|
||||
onboardingFields?: (ctx: { t: ComposerTranslation }) => MaybePromise<ProviderOnboardingField[]>
|
||||
createProvider: (config: TConfig) => MaybePromise<ProviderInstance>
|
||||
extraMethods?: ProviderExtraMethods<TConfig>
|
||||
/**
|
||||
* Returns true when the configuration has enough input for automatic validation.
|
||||
* Provider settings keep the status unconfigured while this function returns false.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
validationRequiredWhen?: (config: TConfig) => MaybePromise<boolean>
|
||||
validators?: {
|
||||
validateConfig?: Array<(contextOptions: { t: ComposerTranslation }) => MaybePromise<ProviderConfigValidator<TConfig>>>
|
||||
validateProvider?: Array<(contextOptions: { t: ComposerTranslation }) => MaybePromise<ProviderRuntimeValidator<TConfig>>>
|
||||
}
|
||||
capabilities?: {
|
||||
chat?: {
|
||||
reasoning?: ChatReasoningCapability
|
||||
}
|
||||
transcription?: {
|
||||
protocol: 'websocket' | 'http' | 'native'
|
||||
generateOutput: boolean
|
||||
streamOutput: boolean
|
||||
streamInput: boolean
|
||||
}
|
||||
/**
|
||||
* Declares the TTS transport this provider speaks. Drives Stage's TTS
|
||||
* session adapter selection (`@proj-airi/stage-ui/libs/speech/tts-session`):
|
||||
*
|
||||
* - `rest` (default when this whole block is absent): the host opens
|
||||
* a `pipelines-audio` IntentHandle and the provider's `speech()` is
|
||||
* called per-segment by the speech-pipeline `tts()` callback. This
|
||||
* matches every OpenAI-shaped HTTP TTS provider.
|
||||
* - `bidirectional-ws`: the host opens one streaming TTS WebSocket
|
||||
* for the whole LLM intent and forwards raw token chunks without
|
||||
* client-side segmentation. The provider's `speech()` is unused
|
||||
* for synthesis on this path (kept only for legacy fallback).
|
||||
*
|
||||
* Designed so a future provider (ElevenLabs streaming, OpenAI Realtime
|
||||
* Voice, etc.) only needs to set this flag — Stage and the session
|
||||
* factory do not need to know each provider's id.
|
||||
*/
|
||||
speech?: {
|
||||
transport: 'rest' | 'bidirectional-ws'
|
||||
}
|
||||
}
|
||||
/**
|
||||
* When true, hides the "skip chat ping check" checkbox in the UI even
|
||||
* when the provider defines a ChatCompletions validator.
|
||||
*
|
||||
* By default, the checkbox is shown automatically whenever a provider
|
||||
* includes a ChatCompletions runtime validator. Set this to `true` for
|
||||
* providers where skipping that check is not meaningful or has not been
|
||||
* verified yet.
|
||||
*/
|
||||
disableChatPingCheckUI?: boolean
|
||||
business?: (contextOptions: { t: ComposerTranslation }) => {
|
||||
troubleshooting?: {
|
||||
validators?: {
|
||||
openaiCompatibleCheckConnectivity?: {
|
||||
label?: string
|
||||
content?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Reasoning modes that AIRI can request from a chat provider. */
|
||||
export type ChatReasoningMode = 'disabled' | 'enabled'
|
||||
|
||||
/** User-selected options that a provider applies to one chat request. */
|
||||
export interface ChatRequestOptions {
|
||||
/** Requested reasoning mode. */
|
||||
reasoning: ChatReasoningMode
|
||||
}
|
||||
|
||||
/** Describes the reasoning controls that AIRI implements for a provider. */
|
||||
export interface ChatReasoningCapability {
|
||||
/** Modes that AIRI can pass to the provider. */
|
||||
modes: readonly ChatReasoningMode[]
|
||||
}
|
||||
export type {
|
||||
ChatReasoningCapability,
|
||||
ChatReasoningMode,
|
||||
ChatRequestOptions,
|
||||
InferenceServiceProvider,
|
||||
ModelInfo,
|
||||
ProviderConfigContext,
|
||||
ProviderConfiguredBy,
|
||||
ProviderConfigValidator,
|
||||
ProviderExtraMethods,
|
||||
ProviderInstance,
|
||||
ProviderModelCatalog,
|
||||
ProviderOnboardingField,
|
||||
ProviderRuntimeValidator,
|
||||
ProviderTranslator,
|
||||
ProviderValidationResult,
|
||||
ProviderValidationStatus,
|
||||
ProviderValidatorSchedule,
|
||||
VoiceInfo,
|
||||
} from '@proj-airi/provider-inference'
|
||||
|
||||
@@ -1,2 +1,15 @@
|
||||
export * from './openai-compatible'
|
||||
export * from './run'
|
||||
export {
|
||||
createConfigValidationSteps,
|
||||
createOpenAICompatibleValidators,
|
||||
createProviderValidationSteps,
|
||||
getProviderValidationIntervalMs,
|
||||
getValidatorsOfProvider,
|
||||
validateProvider,
|
||||
} from '@proj-airi/provider-inference'
|
||||
export type {
|
||||
ProviderValidationCallbacks,
|
||||
ProviderValidationPlan,
|
||||
ProviderValidationStep,
|
||||
ProviderValidationStepKind,
|
||||
ProviderValidationStepStatus,
|
||||
} from '@proj-airi/provider-inference'
|
||||
|
||||
@@ -1,191 +1 @@
|
||||
import type { ComposerTranslation } from 'vue-i18n'
|
||||
|
||||
import type {
|
||||
ProviderConfigValidator,
|
||||
ProviderDefinition,
|
||||
ProviderExtraMethods,
|
||||
ProviderInstance,
|
||||
ProviderRuntimeValidator,
|
||||
} from '../types'
|
||||
|
||||
import { errorMessageFrom, merge } from '@moeru/std'
|
||||
|
||||
export type ProviderValidationStepStatus = 'idle' | 'validating' | 'valid' | 'invalid'
|
||||
export type ProviderValidationStepKind = 'config' | 'provider'
|
||||
export interface ProviderValidationStep {
|
||||
id: string
|
||||
label: string
|
||||
status: ProviderValidationStepStatus
|
||||
reason: string
|
||||
kind: ProviderValidationStepKind
|
||||
}
|
||||
|
||||
export interface ProviderValidationPlan {
|
||||
steps: ProviderValidationStep[]
|
||||
config: Record<string, unknown>
|
||||
definition: ProviderDefinition
|
||||
configValidators: ProviderConfigValidator<Record<string, unknown>>[]
|
||||
providerValidators: ProviderRuntimeValidator<Record<string, unknown>>[]
|
||||
providerExtra: ProviderExtraMethods<Record<string, unknown>> | undefined
|
||||
shouldValidate: boolean
|
||||
}
|
||||
|
||||
export interface ProviderValidationCallbacks {
|
||||
onValidatorStart?: (info: { kind: ProviderValidationStepKind, index: number, step: ProviderValidationStep }) => void
|
||||
onValidatorSuccess?: (info: { kind: ProviderValidationStepKind, index: number, step: ProviderValidationStep, result: { reason: string, valid: boolean } }) => void
|
||||
onValidatorError?: (info: { kind: ProviderValidationStepKind, index: number, step: ProviderValidationStep, error: unknown }) => void
|
||||
}
|
||||
|
||||
export function createConfigValidationSteps(configValidators: ProviderConfigValidator<Record<string, unknown>>[]): ProviderValidationStep[] {
|
||||
return configValidators.map(validator => ({
|
||||
id: validator.id,
|
||||
label: validator.name,
|
||||
status: 'idle' as ProviderValidationStepStatus,
|
||||
reason: '',
|
||||
kind: 'config' as ProviderValidationStepKind,
|
||||
}))
|
||||
}
|
||||
|
||||
export function createProviderValidationSteps(providerValidators: ProviderRuntimeValidator<Record<string, unknown>>[]): ProviderValidationStep[] {
|
||||
return providerValidators.map(validator => ({
|
||||
id: validator.id,
|
||||
label: validator.name,
|
||||
status: 'idle' as ProviderValidationStepStatus,
|
||||
reason: '',
|
||||
kind: 'provider' as ProviderValidationStepKind,
|
||||
}))
|
||||
}
|
||||
|
||||
export async function getProviderValidationIntervalMs(options: {
|
||||
definition: ProviderDefinition
|
||||
contextOptions: { t: ComposerTranslation }
|
||||
defaultIntervalMs?: number
|
||||
}) {
|
||||
const validators = await Promise.all((options.definition.validators?.validateProvider || []).map(creator => creator(options.contextOptions)))
|
||||
const defaultIntervalMs = options.defaultIntervalMs ?? 15_000
|
||||
const intervals = validators
|
||||
.filter(validator => validator.schedule?.mode === 'interval')
|
||||
.map(validator => validator.schedule?.intervalMs || defaultIntervalMs)
|
||||
|
||||
if (intervals.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return Math.min(...intervals)
|
||||
}
|
||||
|
||||
export async function getValidatorsOfProvider(options: {
|
||||
definition: ProviderDefinition
|
||||
config: Record<string, unknown>
|
||||
schemaDefaults: Record<string, unknown>
|
||||
contextOptions: { t: ComposerTranslation }
|
||||
}): Promise<ProviderValidationPlan> {
|
||||
const { definition } = options
|
||||
|
||||
const configValidators = await Promise.all((definition.validators?.validateConfig || []).map(creator => creator(options.contextOptions)))
|
||||
const allProviderValidators = await Promise.all((definition.validators?.validateProvider || []).map(creator => creator(options.contextOptions)))
|
||||
|
||||
const providerValidators = allProviderValidators
|
||||
|
||||
const steps: ProviderValidationStep[] = [
|
||||
...createConfigValidationSteps(configValidators),
|
||||
...createProviderValidationSteps(providerValidators),
|
||||
]
|
||||
|
||||
const normalizedConfig = merge(options.schemaDefaults, options.config)
|
||||
const validationRequired = definition.validationRequiredWhen || (<TConfig extends Record<string, any>>(_: TConfig) => false)
|
||||
const shouldValidate = await validationRequired(normalizedConfig)
|
||||
|
||||
return {
|
||||
steps,
|
||||
config: normalizedConfig,
|
||||
definition,
|
||||
configValidators: configValidators as ProviderValidationPlan['configValidators'],
|
||||
providerValidators: providerValidators as ProviderValidationPlan['providerValidators'],
|
||||
providerExtra: definition.extraMethods as ProviderValidationPlan['providerExtra'],
|
||||
shouldValidate,
|
||||
}
|
||||
}
|
||||
|
||||
export async function validateProvider(
|
||||
plan: ProviderValidationPlan,
|
||||
contextOptions: { t: ComposerTranslation },
|
||||
callbacks: ProviderValidationCallbacks = {},
|
||||
) {
|
||||
const { configValidators, providerValidators, steps, config, definition, providerExtra } = plan
|
||||
const runContext = {
|
||||
...contextOptions,
|
||||
validationCache: new Map<string, unknown>(),
|
||||
}
|
||||
const { onValidatorError, onValidatorStart, onValidatorSuccess } = callbacks
|
||||
|
||||
const configResults = await Promise.all(configValidators.map(async (validatorDefinition, index) => {
|
||||
const step = steps[index]
|
||||
step.status = 'validating'
|
||||
step.reason = ''
|
||||
onValidatorStart?.({ kind: 'config', index, step })
|
||||
try {
|
||||
const result = await validatorDefinition.validator(config, runContext)
|
||||
step.status = result.valid ? 'valid' : 'invalid'
|
||||
step.reason = result.valid ? '' : result.reason
|
||||
onValidatorSuccess?.({ kind: 'config', index, step, result })
|
||||
return result
|
||||
}
|
||||
catch (error) {
|
||||
step.status = 'invalid'
|
||||
step.reason = errorMessageFrom(error) ?? 'Unknown error'
|
||||
onValidatorError?.({ kind: 'config', index, step, error })
|
||||
return { valid: false, reason: step.reason }
|
||||
}
|
||||
}))
|
||||
|
||||
const configIsValid = configResults.every(result => result.valid)
|
||||
|
||||
const providerStepOffset = configValidators.length
|
||||
if (!configIsValid) {
|
||||
for (let i = 0; i < providerValidators.length; i++) {
|
||||
const step = steps[providerStepOffset + i]
|
||||
step.status = 'invalid'
|
||||
step.reason = 'Fix configuration checks first.'
|
||||
}
|
||||
return steps
|
||||
}
|
||||
|
||||
let providerInstance: ProviderInstance
|
||||
try {
|
||||
providerInstance = await definition.createProvider(config)
|
||||
}
|
||||
catch (error) {
|
||||
for (let i = 0; i < providerValidators.length; i++) {
|
||||
const step = steps[providerStepOffset + i]
|
||||
step.status = 'invalid'
|
||||
step.reason = errorMessageFrom(error) ?? 'Unknown error'
|
||||
}
|
||||
return steps
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all(providerValidators.map(async (validatorDefinition, index) => {
|
||||
const step = steps[providerStepOffset + index]
|
||||
step.status = 'validating'
|
||||
step.reason = ''
|
||||
onValidatorStart?.({ kind: 'provider', index, step })
|
||||
try {
|
||||
const result = await validatorDefinition.validator(config, providerInstance, providerExtra as any, runContext)
|
||||
step.status = result.valid ? 'valid' : 'invalid'
|
||||
step.reason = result.valid ? '' : result.reason
|
||||
onValidatorSuccess?.({ kind: 'provider', index, step, result })
|
||||
}
|
||||
catch (error) {
|
||||
step.status = 'invalid'
|
||||
step.reason = errorMessageFrom(error) ?? 'Unknown error'
|
||||
onValidatorError?.({ kind: 'provider', index, step, error })
|
||||
}
|
||||
}))
|
||||
}
|
||||
finally {
|
||||
await (providerInstance as ProviderInstance & { dispose?: () => Promise<void> | void }).dispose?.()
|
||||
}
|
||||
|
||||
return steps
|
||||
}
|
||||
export * from '@proj-airi/provider-inference'
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { Component } from 'vue'
|
||||
|
||||
/** Loads the extra controls that a Provider shows in Hearing settings. */
|
||||
export type HearingProviderViewLoader = () => Promise<{ default: Component }>
|
||||
|
||||
/** Vue-owned views that a Provider can expose in stage-ui. */
|
||||
export interface ProviderViews {
|
||||
/** Extra controls for the Hearing settings page. */
|
||||
hearing?: HearingProviderViewLoader
|
||||
}
|
||||
@@ -1,11 +1,21 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { parse as parseSchema } from 'zod/v4/core'
|
||||
|
||||
import { ATLASCLOUD_DEFAULT_BASE_URL, providerAtlasCloud } from '../libs/providers/providers/atlascloud'
|
||||
import { getDefinedProvider } from '../libs/providers/providers'
|
||||
import { OFFICIAL_CHAT_PROVIDER_ID } from '../libs/providers/providers/official'
|
||||
import { providerOpenAICompatible } from '../libs/providers/providers/openai-compatible'
|
||||
import { inferenceServiceProvidersService } from './inference-service-providers'
|
||||
|
||||
function getRequiredProvider(id: string) {
|
||||
const provider = getDefinedProvider(id)
|
||||
if (!provider)
|
||||
throw new Error(`Provider definition "${id}" is not registered.`)
|
||||
|
||||
return provider
|
||||
}
|
||||
|
||||
const atlasCloudProvider = getRequiredProvider('atlascloud')
|
||||
const openAICompatibleProvider = getRequiredProvider('openai-compatible')
|
||||
|
||||
/**
|
||||
* @example
|
||||
* describe('services inference-service-providers', () => {})
|
||||
@@ -16,10 +26,10 @@ describe('services inference-service-providers', () => {
|
||||
* const provider = inferenceServiceProvidersService.buildLocal('openai-compatible')
|
||||
*/
|
||||
it('builds a local provider from a known definition', () => {
|
||||
const provider = inferenceServiceProvidersService.buildLocal(providerOpenAICompatible.id, {})
|
||||
const provider = inferenceServiceProvidersService.buildLocal(openAICompatibleProvider.id, {})
|
||||
|
||||
expect(provider.id).toBeDefined()
|
||||
expect(provider.definitionId).toBe(providerOpenAICompatible.id)
|
||||
expect(provider.definitionId).toBe(openAICompatibleProvider.id)
|
||||
expect(provider.config).toEqual({})
|
||||
expect(provider.status).toBe('unconfigured')
|
||||
expect(provider.configuredBy).toBe('user')
|
||||
@@ -36,15 +46,15 @@ describe('services inference-service-providers', () => {
|
||||
* const provider = inferenceServiceProvidersService.buildLocal('atlascloud', { apiKey: '...' })
|
||||
*/
|
||||
it('lists Atlas Cloud as a built-in OpenAI-compatible provider', async () => {
|
||||
const schema = await providerAtlasCloud.createProviderConfig({ t: (key: string) => key })
|
||||
const schema = await atlasCloudProvider.createProviderConfig({ t: (key: string) => key })
|
||||
|
||||
expect(providerAtlasCloud.name).toBe('Atlas Cloud')
|
||||
expect(atlasCloudProvider.name).toBe('Atlas Cloud')
|
||||
expect(parseSchema(schema, { apiKey: 'test-key' })).toEqual({
|
||||
apiKey: 'test-key',
|
||||
baseUrl: ATLASCLOUD_DEFAULT_BASE_URL,
|
||||
baseUrl: 'https://api.atlascloud.ai/v1',
|
||||
})
|
||||
expect(inferenceServiceProvidersService.buildLocal(providerAtlasCloud.id, { apiKey: 'test-key' })).toEqual(expect.objectContaining({
|
||||
definitionId: providerAtlasCloud.id,
|
||||
expect(inferenceServiceProvidersService.buildLocal(atlasCloudProvider.id, { apiKey: 'test-key' })).toEqual(expect.objectContaining({
|
||||
definitionId: atlasCloudProvider.id,
|
||||
config: { apiKey: 'test-key' },
|
||||
}))
|
||||
})
|
||||
@@ -70,7 +80,7 @@ describe('services inference-service-providers', () => {
|
||||
ok: true,
|
||||
json: async () => [{
|
||||
id: 'provider-1',
|
||||
definitionId: providerOpenAICompatible.id,
|
||||
definitionId: openAICompatibleProvider.id,
|
||||
name: 'OpenAI Compatible',
|
||||
config: { baseUrl: 'https://example.com/v1/' },
|
||||
validated: true,
|
||||
@@ -81,7 +91,7 @@ describe('services inference-service-providers', () => {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
id: 'provider-1',
|
||||
definitionId: providerOpenAICompatible.id,
|
||||
definitionId: openAICompatibleProvider.id,
|
||||
name: 'OpenAI Compatible',
|
||||
config: {},
|
||||
validated: false,
|
||||
@@ -94,7 +104,7 @@ describe('services inference-service-providers', () => {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
id: 'provider-1',
|
||||
definitionId: providerOpenAICompatible.id,
|
||||
definitionId: openAICompatibleProvider.id,
|
||||
name: 'OpenAI Compatible',
|
||||
config: {},
|
||||
validated: false,
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { StreamingTranscriptionCallbacks, StreamingTranscriptionConsumer }
|
||||
|
||||
import { errorMessageFrom, tryCatch } from '@moeru/std'
|
||||
import { toPCM16FromFloat32 } from '@proj-airi/audio/encoding'
|
||||
import { streamWebSpeechAPITranscription } from '@proj-airi/provider-inference'
|
||||
import { errorMessageFromValue, IOAttributes, IOEvents, IOSpanNames, IOSubsystems } from '@proj-airi/stage-shared'
|
||||
import { useLocalStorageManualReset } from '@proj-airi/stage-shared/composables'
|
||||
import { refManualReset } from '@vueuse/core'
|
||||
@@ -23,7 +24,6 @@ import { activeTurnSpan, startSpan } from '../../composables/use-io-tracer'
|
||||
import { createVadStreamingSession } from '../../libs/audio/vad-streaming-session'
|
||||
import { OFFICIAL_TRANSCRIPTION_PROVIDER_ID } from '../../libs/providers'
|
||||
import { APPLE_SPEECH_TRANSCRIPTION_PROVIDER_ID, executeAppleSpeechStream } from '../../libs/providers/providers/apple-speech'
|
||||
import { streamWebSpeechAPITranscription } from '../../libs/providers/providers/browser-web-speech-api'
|
||||
import { streamTranscription } from '../../libs/providers/stream-transcription'
|
||||
import { useVAD } from '../ai/models/vad'
|
||||
import { useProviderConfigStore } from '../providers/config'
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user