tinyhumansai/openhuman · error
no injected channel model source for '{provider_name}'; prod
Error message
no injected channel model source for '{provider_name}'; production routes use crate-native model sources What it means
`get_or_create_turn_model_source` resolves the `TurnModelSource` for a chat turn from the runtime context's injected sources: the default provider requires `ctx.turn_model_source` to be set, any other provider requires an entry in `ctx.turn_model_source_cache`. If neither is present it bails — the function only ever returns injected sources; production builds crate-native model sources elsewhere and should not reach this path. The dispatcher (runtime/dispatch/processor.rs:236) only calls it when `ctx.config.is_none()` and on error reports it to observability as `provider_init` and tells the user to run `/models`.
Source
Thrown at src/openhuman/channels/routes.rs:189
provider_name: &str,
) -> anyhow::Result<crate::openhuman::agent::tinyagents::TurnModelSource> {
if provider_name == ctx.default_provider.as_str() {
return ctx.turn_model_source.as_ref().cloned().ok_or_else(|| {
anyhow::anyhow!("no injected channel model source for '{provider_name}'")
});
}
if let Some(existing) = ctx
.turn_model_source_cache
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(provider_name)
.cloned()
{
return Ok(existing);
}
anyhow::bail!(
"no injected channel model source for '{provider_name}'; production routes use crate-native model sources"
)
}
fn build_models_help_response(current: &ChannelRouteSelection, workspace_dir: &Path) -> String {
let mut response = String::new();
let _ = writeln!(
response,
"Current provider: `{}`\nCurrent model: `{}`",
current.provider, current.model
);
response.push_str("\nSwitch model with `/model <model-id>`.\n");
let cached_models = load_cached_model_preview(workspace_dir, ¤t.provider);
if cached_models.is_empty() {
let _ = writeln!(
response,
"\nNo cached model list found for `{}`. Ask the operator to refresh the model list in the web UI.",View on GitHub (pinned to 7491200858)
Solutions
- Wire the context before dispatch: set `turn_model_source` for the default provider and pre-populate `turn_model_source_cache` for every provider a route can select — or attach `config` so the crate-native model-source path is used.
- As the end user, run `/models` in the channel chat and switch to a provider that is configured.
- Clear the stale route override for the conversation (`/model` back to the default) if it points at a provider with no registered source.
Example fix
// before — context built without sources (test/embedding harness)
let ctx = ChannelRuntimeContext { config: None, ..Default::default() };
// after — inject the default + cached sources, or attach config
let mut cache = HashMap::new();
cache.insert("openai".to_string(), default_source.clone());
let ctx = ChannelRuntimeContext {
config: None,
default_provider: default_provider.clone(),
turn_model_source: Some(default_source),
turn_model_source_cache: Mutex::new(cache),
..Default::default()
}; Defensive patterns
Strategy: try-catch
Validate before calling
// Before dispatching turns, assert the context can resolve every selectable provider
if ctx.config.is_none() {
assert!(ctx.turn_model_source.is_some(),
"no turn_model_source injected for default provider {}");
let cache = ctx.turn_model_source_cache.lock().unwrap();
for provider in selectable_route_providers(ctx) {
assert!(cache.contains_key(provider),
"no turn_model_source_cache entry for route provider {provider}");
}
} Try / catch
match get_or_create_turn_model_source(ctx.as_ref(), &route.provider).await {
Ok(source) => Some(source),
Err(err) => {
crate::core::observability::report_error(
&err, "channels", "provider_init",
&[("channel", msg.channel.as_str()), ("provider", route.provider.as_str())],
);
// tell the user and skip the model call for this turn — do not retry blindly
reply("Failed to initialize provider. Run /models to choose another provider.");
None
}
} Prevention
- When embedding the channel host, inject turn_model_source (default provider) and pre-populate turn_model_source_cache for every provider a route can select
- Attach config to the runtime context in production so crate-native model sources are used instead of injected ones
- Route overrides persist per sender_key — clear them when the provider set changes
When it happens
Trigger: Dispatching a channel message with a runtime context that has no `config` and no injected source for the active route provider — e.g. a test or embedding harness constructing `ChannelRuntimeContext` without wiring `turn_model_source`/`turn_model_source_cache`, or a per-conversation `/model` route override pointing at a provider whose source was never registered.
Common situations: Embedding the channel host in tests without injecting model sources; stale route overrides (sender_key → provider) left over after the provider set changed; refactors that stopped wiring the default source into the context.
Related errors
- Channel connect response missing status
- cannot persist empty identity
- telegram channel config is missing in config.toml
- allowlist persist unsupported for channel '{other}'
AI-assisted analysis of tinyhumansai/openhuman@7491200858 (2026-08-17).
Data as JSON: /api/errors/82208131a482dd13.
Report an issue: GitHub.