DurinDoor
Features

Media routes

Default model and fallbacks for each media endpoint, so requests can leave out the model.

A media route is the ordered list of models an endpoint uses when a request has no model. Leave model out, or send "model": "auto". DurinDoor tries the route's models in order and moves to the next one on a retryable failure (429, 5xx, missing or exhausted credentials). Any other error comes back unchanged. A 200 with an empty result (a silent audio file, a page with no text) is returned as is, not retried. If the request names a model or a combo, the route is not used.

Routes exist for these endpoints:

RouteEndpointNotes
Text to speechPOST /v1/audio/speech
Speech to textPOST /v1/audio/transcriptions, POST /v1/audio/translationsTranslations only use providers with a translations endpoint (OpenAI, Groq, Local Whisper).
Web searchPOST /v1/searchSend no model or provider.
Web fetchPOST /v1/web/fetchSend no model or provider.
EmbeddingsPOST /v1/embeddingsOnly the first available model runs. Vectors from different models are not comparable, so embeddings never switch models. They still move to the next account of that provider.
Image generationPOST /v1/images/generations
Video generationPOST /v1/video/generations, POST /v1/videos/{action}/v1/video/generations tries the models it can run, in order. /v1/videos creates a billable job, so it uses only the first route model whose provider runs async jobs and never retries. A /v1/videos poll without x-connection-id works only when one async video provider is connected; otherwise it returns 400 asking for the header.
Music generationPOST /v1/music/generations, POST /v1/audio/music

Connect a provider for the endpoint

DurinDoor does not ship a default model. A route can only hold models that GET /v1/models/{kind} lists right now. Each one has to meet all of these:

  • It comes from a provider with an active connection, or from an enabled keyless provider that is installed and working:
    • A keyless provider that calls a server counts only if the URL this request would use answers within 1.5 seconds. That covers Local Whisper, Coqui, Tortoise, SearXNG, self-hosted Firecrawl and veoaifree-web.
    • For an unscoped request, Local Whisper uses its first active connection's host, or its default host when none is saved. Self-hosted Firecrawl uses a valid self-hosted Firecrawl URL setting, then its first active connection's host, then FIRECRAWL_BASE_URL, then its default. For an API key scoped to provider accounts, the provider counts if the first connection that key may use (by priority) answers.
    • The check uses the same outbound URL guard as provider requests, so a blocked host counts as not working and is never contacted.
    • local-device counts only when the OS voice list loads (macOS say or Windows SAPI).
    • Keyless libraries with no server (edge-tts, google-tts) count as working.
    • Checks are cached per URL for 30 seconds.
  • The provider serves that kind.
  • For image and embedding routes, the provider has an image or embedding adapter in DurinDoor. A catalog that lists such models for a provider without one (Venice, for example) does not put them on the route.
  • The model itself is that kind.
  • The provider's visible-model allowlist and disabled-model list allow it.

If nothing qualifies, a request without a model gets HTTP 400 with error.code: "no_provider_for_kind". The message depends on why:

  • No connected provider serves the kind at all:
{
  "error": {
    "message": "No connected provider supports text to speech on this endpoint. Connect one in Dashboard > Media Providers, or pass a model.",
    "type": "invalid_request_error",
    "code": "no_provider_for_kind"
  }
}
  • A saved route exists, but none of its models are available right now (connection removed or disabled, model hidden): "None of the models in the text to speech route are available. Update it in Dashboard > Media Routes, or pass a model."
  • A saved route exists and some models are available, but this endpoint can't run any of them (a sync-only video model on /v1/videos, a Deepgram-only speech-to-text route on /v1/audio/translations): "None of the models in the text to speech route can run on this endpoint. Add one it supports in Dashboard > Media Routes, or pass a model."

Save a default and fallback order

Open Dashboard → Media Routes (/dashboard/media-routes). Each endpoint has a card:

  • Automatic is used until you save an order. It tries every available model in catalog order.
  • Customize starts an editable list from the current first model. Add fallbacks from the available models, drag to reorder, remove models, then Save. The first model is the default and the rest are fallbacks.
  • Use automatic order clears the saved list.
  • A saved model that later becomes unavailable (connection removed or disabled, model hidden) is skipped and shown as unavailable, skipped. If every saved model is unavailable, the request fails with no_provider_for_kind. DurinDoor does not quietly switch to the automatic order. The same applies when the saved models are available but the endpoint can't run any of them, for example a veoaifree-web-only video route on /v1/videos, or a Deepgram-only speech-to-text route on /v1/audio/translations. The video and speech-to-text cards list what each endpoint will run and flag an endpoint that can't run any model in the route.

Routes are stored in settings as mediaRoutes, for example { "tts": ["elevenlabs/eleven_v3", "openai/tts-1"] }. You can also write them through:

  • PUT /api/media-providers/routes with { "kind": "tts", "models": [...] }. An empty list resets the route to automatic.
  • PATCH /api/settings with { "mediaRoutes": {...} }.

Both reject unknown kinds, and ids without a provider/ prefix.

Verify a request without a model

curl http://localhost:20128/v1/audio/speech \
  -H "Authorization: Bearer $DURINDOOR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"input":"Speak, friend, and enter."}' --output door.mp3

Expect a nonempty door.mp3 when a connected text-to-speech provider succeeds. Inspect the HTTP status before using the file because a provider error can be written to the same output path.

API key model policies still apply to each route model, the same way they do when the model is named explicitly. For an API key scoped to provider accounts, the route leaves out providers the key has no connection for, so embeddings and /v1/videos pick the first model that key can call.

Recover an unavailable route

Check active connections and hidden or disabled models. Update the saved route, or choose Use automatic order. A saved but unavailable list is not replaced automatically. For embeddings, changing the model changes the vector space; rebuild downstream indexes deliberately. For async video, poll the original job instead of creating another billable job.

On this page

Edit on GitHub