Files
OmniRoute/open-sse/config/registryUtils.ts
Diego Rodrigues de Sa e Souza 9b5415a414 feat: add Gladia as an async speech-to-text provider (#6657) (#7603)
* 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.
2026-07-17 11:45:00 -03:00

137 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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}` };
}
}