Summary
The bitHuman cloud lane behind livekit-plugins-bithuman (POST auth.api.bithuman.ai/v1/runtime-tokens/request) serves Expression 1 to an agent whose only built model is expression-2 — a silent downgrade, billed at the Expression-1 rate. The docs for this lane promise the opposite.
Expected
Per the LiveKit deployment guide:
a second-generation agent serves its own model automatically … no model= kwarg is needed
And per models-v2, expression-2 is served on every cloud tier (GPU / ANE / CPU) and through every delivery surface including the LiveKit plugin.
Actual
Agent A57CQP1723 (Pro account) is built as expression-2 (model_status.expression-2.state: "ready"; supported_models: ["essence-2-max", "expression-2"]). No gen-1 model is built on it at all. Same agent, same account, same day:
| lane |
billing_type |
video track |
face |
| bitHuman embed viewer |
usage_expression_2_model_cloud |
416x720 |
renders correctly |
runtime-tokens (this plugin's endpoint) |
usage_expression_model_cloud |
512x512 |
featureless smear |
The agent's stylized (non-photoreal) portrait is only renderable by Expression 2, so on this lane the face is destroyed — and the session bills 4.0 cr/min for the older engine vs 3.81 for Expression 2 on the viewer lane.
Note the launcher asymmetry: mode: "cpu" correctly refuses the agent (503: "that launcher serves essence-1 models, and this agent's available model(s) are: essence-2-max, expression-2"), while mode: "gpu" silently substitutes Expression 1 instead of refusing or serving expression-2.
Repro
POST https://auth.api.bithuman.ai/v1/runtime-tokens/request
headers: api-secret: <key>
json: {"livekit_url": ..., "livekit_token": <agent-kind token>,
"room_name": ..., "mode": "gpu", "agent_id": "A57CQP1723"}
Worker joins and publishes 512x512; the account ledger (GET /v2/{user_id}/runtime-sessions) records billing_type: "usage_expression_model_cloud".
Ruled out
- Extra body fields (
model, model_version, billing_type) return 200 and are ignored; mode accepts only gpu/cpu.
- Plugin v1.7.1 exposes no gen-2 selector (
model: Literal["expression","essence"] only flips mode).
- The plugin's exact JWT attribute set on the worker token (
api_secret + agent_id + lk.publish_on_behalf, avatar.py L326–350) — tested, no change.
- The model itself is fine:
POST /v1/video/generate with model: "expression-2" renders the same agent flawlessly.
Ask
Serve expression-2 on the runtime-tokens lane for agents that have it built (as documented), or expose a parameter that selects it — or at minimum make the gpu launcher fail closed like the cpu launcher does, instead of silently downgrading and billing for it.
Happy to share room names, timestamps and session ids privately. (cc @sgu-bithuman)
Summary
The bitHuman cloud lane behind
livekit-plugins-bithuman(POST auth.api.bithuman.ai/v1/runtime-tokens/request) serves Expression 1 to an agent whose only built model is expression-2 — a silent downgrade, billed at the Expression-1 rate. The docs for this lane promise the opposite.Expected
Per the LiveKit deployment guide:
And per models-v2, expression-2 is served on every cloud tier (GPU / ANE / CPU) and through every delivery surface including the LiveKit plugin.
Actual
Agent
A57CQP1723(Pro account) is built asexpression-2(model_status.expression-2.state: "ready";supported_models: ["essence-2-max", "expression-2"]). No gen-1 model is built on it at all. Same agent, same account, same day:usage_expression_2_model_cloudruntime-tokens(this plugin's endpoint)usage_expression_model_cloudThe agent's stylized (non-photoreal) portrait is only renderable by Expression 2, so on this lane the face is destroyed — and the session bills 4.0 cr/min for the older engine vs 3.81 for Expression 2 on the viewer lane.
Note the launcher asymmetry:
mode: "cpu"correctly refuses the agent (503: "that launcher serves essence-1 models, and this agent's available model(s) are: essence-2-max, expression-2"), whilemode: "gpu"silently substitutes Expression 1 instead of refusing or serving expression-2.Repro
Worker joins and publishes 512x512; the account ledger (
GET /v2/{user_id}/runtime-sessions) recordsbilling_type: "usage_expression_model_cloud".Ruled out
model,model_version,billing_type) return 200 and are ignored;modeaccepts onlygpu/cpu.model: Literal["expression","essence"]only flipsmode).api_secret+agent_id+lk.publish_on_behalf,avatar.pyL326–350) — tested, no change.POST /v1/video/generatewithmodel: "expression-2"renders the same agent flawlessly.Ask
Serve expression-2 on the
runtime-tokenslane for agents that have it built (as documented), or expose a parameter that selects it — or at minimum make the gpu launcher fail closed like the cpu launcher does, instead of silently downgrading and billing for it.Happy to share room names, timestamps and session ids privately. (cc @sgu-bithuman)