bytedance/deer-flow · error · HTTPException
Model is not in the configured model allowlist
Error message
Model {model_name!r} is not in the configured model allowlist What it means
HTTP 400 raised when the caller supplies context.model_name and the Gateway cannot resolve it via app_config.get_model_config(model_name). The Gateway enforces a model allowlist at request admission: any model_name not present in config.yaml's models list is rejected before the run starts, even though the lead agent itself would silently fall back to the default model.
Solutions
- Compare the sent model_name against GET /api/models (or config.yaml models:) and use an exact configured name.
- Fix typos/whitespace/case in the model_name value the client sends.
- If the model should exist, add its full entry to config.yaml under models and restart the Gateway.
- Omit context.model_name entirely to let the server use its default model.
Example fix
# config.yaml
models:
- name: gpt-4o # client must send exactly "gpt-4o"
provider: openai
... Defensive patterns
Strategy: validation
Validate before calling
const models = await api.listModels(); // names from config.yaml
const allowed = new Set(models.map(m => m.name));
if (body.context?.model_name && !allowed.has(body.context.model_name)) {
delete body.context.model_name; // or pick allowed names interactively
} Type guard
const isConfiguredModel = (name, configured) => typeof name === 'string' && configured.some(m => m.name === name);
Try / catch
try { await api.createRun(threadId, body); } catch (e) { if (e.status === 400 && /allowlist/.test(e.detail)) { refresh model list, correct model_name, retry once; } throw e; } Prevention
- Drive the UI model picker from the server's model list, never a hardcoded list.
- Treat model names as exact-match opaque ids (case/whitespace sensitive).
- Restart Gateway after config.yaml model changes so all replicas agree.
When it happens
Trigger: POST a run with body.context.model_name set to a name that is not a key in config.yaml models (typo, removed model, env-specific name), or a non-string value coerced to str (e.g. 42 -> '42').
Common situations: Frontend hardcodes a model name from another environment (dev vs prod config.yaml differ); a model was renamed or deleted from config.yaml; the user picked a model in the UI that the backend config never defined; typo like 'gpt-4o ' with whitespace.
Related errors
- Failed to load MCP configuration
- Failed to load suggestions config
- Failed to update MCP configuration
- Failed to update MCP server
- Honcho backend: api_key over plain http requires…
AI-assisted analysis of bytedance/deer-flow@1dd6ba1acb (2026-08-14).
Data as JSON: /api/errors/927523e34c69c449.
Report an issue: GitHub.
Appendix: source
Thrown at backend/app/gateway/services.py:1097
stream_modes = normalize_stream_modes(body.stream_mode)
bridge = get_stream_bridge(request)
run_mgr = get_run_manager(request)
run_ctx = get_run_context(request)
disconnect = DisconnectMode.cancel if body.on_disconnect == "cancel" else DisconnectMode.continue_
body_context = getattr(body, "context", None) or {}
model_name = body_context.get("model_name")
# Coerce non-string model_name values to str before truncation.
if model_name is not None and not isinstance(model_name, str):
model_name = str(model_name)
# Validate model against the allowlist when a model_name is provided.
if model_name:
app_config = get_app_config()
resolved = app_config.get_model_config(model_name)
if resolved is None:
raise HTTPException(
status_code=400,
detail=f"Model {model_name!r} is not in the configured model allowlist",
)
owner_user_id = get_trusted_internal_owner_user_id(request)
# Stateless run endpoints carry thread_id in the request *body*, so the
# @require_permission(owner_check=True) decorator -- which resolves ownership
# from the path param -- cannot protect them. Enforce thread ownership here,
# before any run is created, so one user cannot start runs on (or read /wait
# checkpoint state from) another user's thread. Missing rows (auto-created
# temp threads) and NULL-owner rows (shared / pre-auth data) stay accessible
# via check_access; only a thread already owned by another user is rejected
# with 404, matching thread_runs.py's anti-enumeration behaviour. Internal
# channel runs act on behalf of the connection owner carried in
# X-DeerFlow-Owner-User-Id, so they are scoped to that owner instead of
# bypassing the check -- a leaked internal token must not grant cross-user
# thread access.
user = getattr(request.state, "user", None)View on GitHub (pinned to 1dd6ba1acb)