Four provider interfaces, one rule: no vendor name appears in an agent. An agent asks for text,
voice, image, video or music, and configuration decides who serves it.
The two questions
Every provider has two independent answers, and conflating them is how a project comes to believe it is licensed when it is merely configured.
| Question | Field | Values |
|---|---|---|
| Can it serve a request right now? | status |
AVAILABLE, CONFIGURED, DEGRADED, UNAVAILABLE |
| May its output be monetized? | commercial_use |
LICENSE_VERIFIED, UNVERIFIED, PROHIBITED, NOT_APPLICABLE |
A provider can be AVAILABLE and UNVERIFIED simultaneously. That combination may be used for
development and may not enter a monetized publication. PROHIBITED may not run at all.
A third field, implemented, separates "declared in configuration" from "an adapter exists". Kokoro
is declared and unimplemented; it reports as such rather than being silently omitted or quietly
simulated.
Current standing
| Kind | Provider | Implemented | Enabled | Status | Commercial use | Cost class |
|---|---|---|---|---|---|---|
| text | manual_text |
yes | yes | AVAILABLE | NOT_APPLICABLE | FREE_LOCAL |
| text | deepseek |
yes | no | UNAVAILABLE | UNVERIFIED | PAID |
| voice | manual_voice |
yes | yes | AVAILABLE | NOT_APPLICABLE | FREE_LOCAL |
| voice | kokoro |
no | no | UNAVAILABLE | UNVERIFIED | FREE_LOCAL |
| image | manual_image |
yes | yes | AVAILABLE | NOT_APPLICABLE | FREE_LOCAL |
| image | comfyui |
no | no | UNAVAILABLE | UNVERIFIED | FREE_LOCAL |
| video | manual_video |
yes | yes | AVAILABLE | NOT_APPLICABLE | FREE_LOCAL |
| video | ffmpeg |
no | no | UNAVAILABLE | LICENSE_VERIFIED | FREE_LOCAL |
| music | manual_music |
yes | yes | AVAILABLE | NOT_APPLICABLE | FREE_LOCAL |
| music | youtube_audio_library |
no | no | UNAVAILABLE | UNVERIFIED | FREE_EXTERNAL |
No model has been downloaded and no paid adapter is active. ffmpeg is not installed on the current
machine, so even the assembly path is unavailable until it is.
Configuration
config/providers.toml at the repository root. Path overridable with GIA_PROVIDER_CONFIG.
[text.deepseek]
enabled = false # enabling is not authorizing; the budget gate still refuses PAID calls
model = "deepseek-flash"
input_per_mtok = 0.28
output_per_mtok = 0.42
license_status = "UNVERIFIED"
cost_class = "PAID"
Pricing lives in configuration so an outdated number in source cannot become a silent billing
surprise. Provider credentials come from the environment only — GIA_DEEPSEEK_API_KEY — and are
never written to configuration, logs, or the dashboard.
The budget gate
app/budget.py. One question, one answer, before any call leaves the process.
FREE_LOCAL/FREE_EXTERNAL→ allowed, no invoice possible.PAIDwith a zero authorized ceiling →REQUIRES_AUTHORIZATION.PAIDwith per-request approval on →REQUIRES_AUTHORIZATION, reported differently from a zero ceiling, because "raise the ceiling" and "approve this call" are different decisions.PAIDexceeding the remaining ceiling →BLOCKED.UNKNOWN→BLOCKED. An undeclared cost is treated as a possible charge.
There is also a rule at the boundary of the text adapters: the coding agent's access to a model is
not application access. DeepSeekTextProvider requires its own key for this process, refuses to run
when disabled, and never scrapes a consumer interface or replays a session.
Manual import is a first-class path
With no authorized API access, manual import is how a production actually moves. It is therefore as traceable as an automated path:
- Text:
<manual_drop_dir>/<task_id>.json({"text": "...", "author": "..."}) or.txt. - Voice:
<segment_id>.wav/.mp3per segment, so one paragraph can be re-recorded alone. - Image:
<character_id>.png, with the prompt recorded for the operator. - Video: a finished render, imported with provenance.
- Music:
music.jsoncatalogue; a track withouttitle,creator,sourceandlicenseis refused, because an unlicensed track cannot enter a production.
An absent artifact raises ManualInputRequired, classified HUMAN_APPROVAL — not a technical
failure. The job resumes from that point; nothing upstream is regenerated.
Failure classification
Failures are not interchangeable, and merging them hides what actually went wrong:
| Class | Meaning | Response |
|---|---|---|
TECHNICAL |
transport, timeout, 5xx, rate limit | retry with backoff |
LICENSING |
rights unknown or refused | stop; do not substitute a provider |
THEOLOGICAL |
the audit gate refused the content | revise, never publish |
QUALITY |
produced, but below standard | re-render, do not downgrade |
BUDGET |
would incur unauthorized spend | stop; ask the owner |
HUMAN_APPROVAL |
a person must supply or decide something | wait; resume from here |
An insufficient-balance response is a BUDGET failure, not a technical one, and topping up is never
automatic.
Adding an adapter
- Implement the interface in
app/providers/. - Declare a
ProviderSpecwith honeststatus,commercial_use, andcost_class. - Register it in
_build_adapter. - Add it to
config/providers.toml, disabled. - Prove the refusals in a test before proving the happy path.
Installing a model, downloading a checkpoint, or verifying a commercial licence are separate steps from writing an adapter, and each needs its own approval.
Source
Rendered from docs/PROVIDERS.md at build time. Return to the
documentation index.