Define the task vocabulary once, and let the ABI ask about it - #544
Open
christopherthompson81 wants to merge 2 commits into
Open
Define the task vocabulary once, and let the ABI ask about it#544christopherthompson81 wants to merge 2 commits into
christopherthompson81 wants to merge 2 commits into
Conversation
This was referenced Sep 14, 2026
The fourteen task kinds were written out in four places that nobody compared:
the model-spec schema's allowed set, the model-spec parser, the runtime's
parser and to_string, and include/audiocpp.h's prose. They had drifted in
every direction.
- schema.cpp allowed "codec", which no parser maps to a task kind, so
model_specs/miocodec.json threw from parse_task_kind at load and the family
was unreachable.
- metadata.cpp accepted "audio_generation", which the schema rejects, so a
spec using it failed validation instead.
- model_specs/moss_voicegen.json declares "vdes", the ABI token, where the
spec vocabulary wants "design" -- also unreachable.
- audiocpp.h documented "diarization" and "alignment" as task spellings.
Neither has ever parsed, and audiocpp_model_supports returns 0 for an
unrecognised task, so a caller following the header was told "no" for every
model that does diarize with nothing to say the question was malformed.
- parse_voice_task_kind's error message listed the tokens a fifth time.
runtime::task_vocabulary is now the single definition. The schema's allowed
set, both parsers, to_string and the error messages all read it, so they cannot
disagree. Adding a task kind is one row.
The two specs that declared unmappable names are fixed: moss_voicegen uses
"design", and miocodec drops "codec", which named no task kind the runtime can
serve. If codec should be a real task, that is now one row rather than four
edits.
Three additive C entry points, because none of this was askable. Every
task-aware call took an audiocpp_model, so a caller deciding what to install --
reading model_specs/*.json to build a picker before anything is downloaded --
had to hardcode a copy of the mapping table. Nine of the fourteen spellings are
identical between the two vocabularies, so comparing them directly appears to
work and silently drops the other five.
size_t audiocpp_task_count(void);
const char * audiocpp_task_name(size_t index);
const char * audiocpp_task_from_spec_name(const char * spec_task);
And one more, for a different conflation of the same shape:
audiocpp_status audiocpp_request_set_text_language(audiocpp_request *, const char *);
audiocpp_request_set_text writes options["language"] as well as the transcript
language, mirroring the CLI's --language. The two could not be separated
through the ABI, and they have to be: parakeet_tdt validates request options
against its contract and refuses a "language" it does not declare, while
qwen3_forced_aligner declares no language option and requires the transcript
language anyway. So "not declared" means "must not send" for one and "must
send" for the other, and no static check decides it.
The reference server needs exactly this separation and gets it by reaching
around the ABI -- drop_unsupported_language_option in app/server/runtime.cpp
erases the option and keeps the transcript language. A C ABI client could not
express that function.
Tests: the vocabulary is asserted self-consistent in both directions, and
every spec this repository ships is checked to declare task names the engine
knows. Reverting the two spec fixes fails that test by name, which is how it
was confirmed to test anything.
Replacing the hand-written switches with a table lookup gave up something they provided without anyone having to ask: -Wswitch reported a VoiceTaskKind that was added and not handled. A table cannot, so a new kind would have silently become to_string() -> "unknown", an ABI enumeration that omits it, and a spec name that maps to nothing -- with every test still passing. Confirmed by adding a fifteenth kind: before this commit the build was silent and model_spec_system_test passed; upstream's switch warned. vocabulary_is_exhaustive is a switch with no default whose cases all fall through to one return. It carries no data, so it cannot drift from the table the way the five copies drifted from each other -- it only fails the build until whoever adds a kind opens this file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EkxqpYvUbjCpRDnFiNiVfx
christopherthompson81
force-pushed
the
abi/task-vocabulary
branch
from
September 14, 2026 02:07
1ea6c63 to
d476f48
Compare
christopherthompson81
marked this pull request as ready for review
September 14, 2026 02:07
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Define the task vocabulary once, and let the ABI ask about it
The fourteen task kinds are written out in four places that nothing compares, and they have drifted in every direction. Four separate-looking bugs turn out to be that one fact.
src/framework/model_spec/schema.cpptasksmay containsrc/framework/model_spec/metadata.cppVoiceTaskKindsrc/framework/runtime/session.cppVoiceTaskKind, and backinclude/audiocpp.hThe drift
codecis in the schema's allowed set and no parser maps it to a task kind.model_specs/miocodec.jsondeclares["codec", "vc", "s2s"], andparse_tasksiterates in order, so it throws on the first element — the family never loads.audio_generationis the mirror image:parse_task_kindaccepts it, the schema does not, so a spec using it fails validation instead.model_specs/moss_voicegen.jsondeclares"vdes", the ABI token, where the spec vocabulary wants"design". Also unreachable.audiocpp.hdocuments"diarization"and"alignment"as task spellings. Neither has ever parsed. Becauseaudiocpp_model_supportsreturns 0 for an unrecognised task, a caller following the header is told no for every model that does diarize, with nothing to indicate the question was malformed.parse_voice_task_kind's error message lists the tokens a fifth time.The change
engine::runtime::task_vocabularyis now the single definition. The schema's allowed set, both parsers,to_string, and both error messages read it. They cannot disagree, and adding a task kind is one row.The two specs that declared unmappable names are fixed:
moss_voicegenusesdesign;miocodecdropscodec, which names no task kind the runtime can serve. Ifcodecshould be a real task, that is now one row rather than four edits — flagging it as the one semantic decision here rather than deciding it quietly.Three additive C entry points
None of this was askable. Every task-aware call takes an
audiocpp_model, so a caller deciding what to install — readingmodel_specs/*.jsonto build a picker before anything is downloaded — has had to hardcode a copy of the mapping table:Nine of the fourteen spellings are identical between the two vocabularies, which is what makes comparing them directly appear to work. A binding that did so showed 0 packages for music generation when nine families serve it, and 1 of 33 for voice design.
And one more, for the same shape one layer down
audiocpp_request_set_textwritesoptions["language"]as well as the transcript language, mirroring the CLI's--language. The two cannot be separated through the ABI — and they have to be:parakeet_tdtvalidates request options against its contract and refuses alanguageit does not declare.qwen3_forced_alignerdeclares no language option and requires the transcript language anyway.So "not declared" means must not send for one and must send for the other, and no static check decides it. A client can only discover which by failing a request.
The reference server needs exactly this separation and gets it by reaching around the ABI —
drop_unsupported_language_optioninapp/server/runtime.cpperases the option and keeps the transcript language. A C ABI client cannot express that function, so the ABI is strictly less expressive than the engine it wraps, in a way the server demonstrates is load-bearing.Verification
Against
parakeet_tdton a real model, which is the exact pair from the report:Tests added to
model_spec_system_test:parse_voice_task_kind/to_string, every spec alias maps to its token, and a name that maps to nothing (codec,diarization) reports as such rather than resolving;ctest: 35/35 pass. A C probe over the new entry points covers enumeration, out-of-range, NULL handling, and the four spec→token rows that differ.