mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-19 21:52:21 +03:00
* feat(providers): add Gladia as an async speech-to-text provider (#6657) Adds Gladia's async pre-recorded transcription API (upload → POST /v2/pre-recorded → poll result_url) following the existing AssemblyAI/Kie.ai async-STT pattern: - New `gladia` entry in AUDIO_TRANSCRIPTION_PROVIDERS (open-sse/config/audioRegistry.ts), authenticated via the `x-gladia-key` custom header. - New `handleGladiaTranscription()` handler (open-sse/handlers/audioTranscription.ts) wired into the format dispatch table. - New `x-gladia-key` case in `buildAuthHeaders()` (open-sse/config/registryUtils.ts). - Registered `gladia` in AUDIO_ONLY_PROVIDERS (src/shared/constants/providers/audio.ts) so it appears in the auto-generated provider catalog; regenerated docs/reference/PROVIDER_REFERENCE.md (250 -> 251 providers) and synced the plain-text provider counts in README.md, AGENTS.md, and CLAUDE.md. Real-time/streaming transcription is explicitly out of scope for this change — OmniRoute has no WebSocket audio-ingestion layer today; only the async/pre-recorded path (which covers every other async STT provider already wired in) is implemented. Tests: 5 new node:test cases in tests/unit/audio-transcription-handler.test.ts covering the upload→submit→poll happy path, a terminal Gladia error, and a missing result_url guard, plus a buildAuthHeaders case in tests/unit/registry-utils.test.ts for the new x-gladia-key header. * chore(providers): sync provider counts to 253 + fix base-red APIKEY partition count 168→169 (#6657) Rebasing gladia onto the advanced release surfaced two count drifts the RUN_ALL suite trips on: (1) docs provider count is now 253 (multiple providers merged since this branch was cut); (2) providers-constants-split already expects 168 but the release has 169 APIKEY entries — a pre-existing base-red from an earlier provider merge that didn't update the test. Gladia is STT (adds no APIKEY entry), so 169 is the correct value; aligning it here also un-reds the release. All 4 partition/dedup checks still enforced.
137 lines
4.3 KiB
TypeScript
137 lines
4.3 KiB
TypeScript
import { randomUUID } from "crypto";
|
||
/**
|
||
* Shared Registry Utilities
|
||
*
|
||
* Common interfaces and helpers used by all provider registries
|
||
* (audio, image, video, music). Extracts duplicated patterns into
|
||
* reusable functions.
|
||
*/
|
||
|
||
export interface BaseModel {
|
||
id: string;
|
||
name: string;
|
||
}
|
||
|
||
export interface BaseProvider<M extends BaseModel = BaseModel> {
|
||
id: string;
|
||
alias?: string;
|
||
baseUrl: string;
|
||
authType: string; // "apikey" | "oauth" | "none"
|
||
authHeader: string; // "bearer" | "key" | "token" | "xi-api-key" | "x-api-key" | "none"
|
||
format?: string;
|
||
models: M[];
|
||
}
|
||
|
||
/**
|
||
* Per-registry ``modelId → providerId`` index for the bare-model lookup below.
|
||
* The media registries (image/video/audio/music) are static module-level objects, so the
|
||
* index is built once per registry and reused. First-wins insertion preserves the original
|
||
* Object.entries() iteration order (the same provider the linear scan would have returned).
|
||
*/
|
||
const registryModelIndexCache = new WeakMap<object, Map<string, string>>();
|
||
function getRegistryModelIndex<P extends BaseProvider>(
|
||
registry: Record<string, P>
|
||
): Map<string, string> {
|
||
let index = registryModelIndexCache.get(registry);
|
||
if (!index) {
|
||
index = new Map<string, string>();
|
||
for (const [providerId, config] of Object.entries(registry)) {
|
||
for (const model of config.models) {
|
||
if (!index.has(model.id)) index.set(model.id, providerId);
|
||
}
|
||
}
|
||
registryModelIndexCache.set(registry, index);
|
||
}
|
||
return index;
|
||
}
|
||
|
||
/**
|
||
* Parse a "provider/model" string against a registry.
|
||
* Supports both "provider/model" prefix and bare "model" lookup.
|
||
*/
|
||
export function parseModelFromRegistry<P extends BaseProvider>(
|
||
modelStr: string | null,
|
||
registry: Record<string, P>
|
||
): { provider: string | null; model: string | null } {
|
||
if (!modelStr) return { provider: null, model: null };
|
||
|
||
// Try each provider prefix
|
||
for (const [providerId, config] of Object.entries(registry)) {
|
||
if (modelStr.startsWith(providerId + "/")) {
|
||
return { provider: providerId, model: modelStr.slice(providerId.length + 1) };
|
||
}
|
||
if (config.alias && modelStr.startsWith(config.alias + "/")) {
|
||
return { provider: providerId, model: modelStr.slice(config.alias.length + 1) };
|
||
}
|
||
}
|
||
|
||
// No provider prefix — find the model via the precomputed index (was an O(providers × models)
|
||
// scan on every call).
|
||
const providerId = getRegistryModelIndex(registry).get(modelStr);
|
||
if (providerId) {
|
||
return { provider: providerId, model: modelStr };
|
||
}
|
||
|
||
return { provider: null, model: modelStr };
|
||
}
|
||
|
||
/**
|
||
* Flatten all models from a registry into a list with provider info.
|
||
* Optionally merge extra fields per provider via the `extra` callback.
|
||
*/
|
||
export function getAllModelsFromRegistry<P extends BaseProvider>(
|
||
registry: Record<string, P>,
|
||
extra?: (providerId: string, config: P) => Record<string, unknown>
|
||
): Array<{ id: string; name: string; provider: string } & Record<string, unknown>> {
|
||
const models: Array<{ id: string; name: string; provider: string } & Record<string, unknown>> =
|
||
[];
|
||
|
||
for (const [providerId, config] of Object.entries(registry)) {
|
||
const extraFields = extra ? extra(providerId, config) : {};
|
||
for (const model of config.models) {
|
||
const entries = [providerId, config.alias].filter(
|
||
(prefix): prefix is string => typeof prefix === "string" && prefix.length > 0
|
||
);
|
||
for (const prefix of entries) {
|
||
models.push({
|
||
id: `${prefix}/${model.id}`,
|
||
name: model.name,
|
||
provider: providerId,
|
||
...extraFields,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
return models;
|
||
}
|
||
|
||
/**
|
||
* Build auth headers for a provider.
|
||
* Handles bearer, key, token, xi-api-key, x-api-key, and none.
|
||
*/
|
||
export function buildAuthHeaders(
|
||
provider: BaseProvider,
|
||
token: string | null
|
||
): Record<string, string> {
|
||
if (provider.authType === "none" || provider.authHeader === "none" || !token) {
|
||
return {};
|
||
}
|
||
|
||
switch (provider.authHeader) {
|
||
case "key":
|
||
return { Authorization: `Key ${token}` };
|
||
case "token":
|
||
return { Authorization: `Token ${token}` };
|
||
case "xi-api-key":
|
||
return { "xi-api-key": token };
|
||
case "x-api-key":
|
||
return { "x-api-key": token };
|
||
case "x-gladia-key":
|
||
return { "x-gladia-key": token };
|
||
case "bearer":
|
||
default:
|
||
return { Authorization: `Bearer ${token}` };
|
||
}
|
||
}
|