Skip to content

server: forward language only where the model's contract accepts it - #400

Merged
0xShug0 merged 1 commit into
0xShug0:mainfrom
CryptVenture:pr/server-language-contract
Sep 3, 2026
Merged

server: forward language only where the model's contract accepts it#400
0xShug0 merged 1 commit into
0xShug0:mainfrom
CryptVenture:pr/server-language-contract

Conversation

@CryptVenture

Copy link
Copy Markdown
Contributor

Split out of #369 as requested: this PR is only the language contract gate. The ASR detail fields, the backend startup probe and the unload locking follow as separate PRs.

The problem

A model whose spec does not declare language returned HTTP 500 for every transcription the WebUI sent. build_openai_transcription_request wrote request.options["language"] whenever the body carried the key, clients send that field on every request whether or not the user chose one, and spec-backed models validate their request options strictly — so the model rejected its own client:

POST /v1/audio/transcriptions
{"model":"parakeet-tdt","audio":"...wav","language":"","text":"","options":{}}
-> {"error":{"message":"unknown Parakeet TDT request option: language"}}

Parakeet-TDT could not transcribe at all from the WebUI. AudioSR, ControlFoley, MiDashengLM-Gen, PersonaPlex and HeartMuLa fail the same way through /v1/tasks/run, which folds a top-level language into the option map.

The change

The option is forwarded only when the caller actually chose a language and the model's contract accepts it. Acceptance is resolved once at registration, next to the existing accepts_reference_text flag and for the same reason — resolving it per request re-reads the embedded spec on the request thread, which costs about 0.9 s for large GGUFs.

A language the caller placed inside options is left alone and still produces the strict rejection: an explicit option is a deliberate choice, and the error is the right answer to it. /v1/tasks/run gets the same treatment through drop_unsupported_language_option, which removes only the option that route synthesised from the top-level field.

The language keeps reaching every model through text_input, so no feature is lost. qwen3_forced_aligner reads it from there, which is why this belongs on the server rather than in the client's form — hiding the input would have broken forced alignment.

Validation

Backend: Metal, Apple M4 Max, macOS 15. Server built from this branch, Parakeet-TDT (parakeet-tdt-0.6b-v3-q8_0.gguf) loaded, real requests against /v1/audio/transcriptions:

Request Result
"language": "" at top level 200, transcript returned
"language": "en" at top level 200, transcript returned
"options": {"language": "en"} 500, unknown Parakeet TDT request option: language — unchanged, deliberate

Build and tests:

cmake -S . -B build/macos-metal-tests -DCMAKE_BUILD_TYPE=Release \
  -DAUDIOCPP_BUILD_NATIVE_MODEL_MANAGER=ON \
  -DAUDIOCPP_USE_SYSTEM_OPENSSL=ON -DOPENSSL_ROOT_DIR="$(brew --prefix openssl@3)"
cmake --build build/macos-metal-tests -j 12
ctest --test-dir build/macos-metal-tests        # 40/40

Scope

Server only. No engine, model or output behaviour changes; affects /v1/audio/transcriptions and /v1/tasks/run request construction. Families checked against the contract: Parakeet TDT, AudioSR, ControlFoley, MiDashengLM-Gen, PersonaPlex, HeartMuLa, Qwen3 forced aligner.

A model whose spec does not declare `language` returned HTTP 500 for every
transcription the WebUI sent, because build_openai_transcription_request wrote
request.options["language"] whenever the body carried the key. Clients send
that field on every request, empty or not, and spec-backed models validate
their request options strictly, so the model rejected its own client:

  POST /v1/audio/transcriptions
  {"model":"parakeet-tdt","audio":"...wav","language":"","text":"","options":{}}
  -> {"error":{"message":"unknown Parakeet TDT request option: language"}}

Parakeet-TDT could not transcribe at all from the WebUI. AudioSR, ControlFoley,
MiDashengLM-Gen, PersonaPlex and HeartMuLa fail the same way through
/v1/tasks/run, which folds a top-level `language` into the option map.

The option is now forwarded only when the caller actually chose a language and
the model's contract accepts it. Acceptance is resolved once at registration
next to the existing accepts_reference_text flag, and for the same reason:
resolving it per request re-reads the embedded spec on the request thread.

A `language` the caller placed inside `options` is left alone and still
produces the strict rejection, which is the correct answer to an explicit
choice. The language continues to reach every model through text_input, so
nothing loses the feature; qwen3_forced_aligner requires it from there, which
is why the fix belongs on the server rather than in the client's form.

Validation, on a server built from this branch with Parakeet-TDT loaded
(Metal, Apple M4 Max):

  language:"" at the top level      -> 200, transcript returned
  language:"en" at the top level    -> 200, transcript returned
  options:{"language":"en"}         -> 500, "unknown Parakeet TDT request
                                       option: language" (unchanged, deliberate)

ctest: 40/40.
@CryptVenture
CryptVenture marked this pull request as ready for review September 3, 2026 02:12
@0xShug0
0xShug0 merged commit 57ba3a8 into 0xShug0:main Sep 3, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants