odysseus-dev/odysseus · warning · HTTPException

model required

Error message

model required

What it means

Raised by POST /api/codex/cookbook/adopt when the model field (alias repo_id) is missing or blank after stripping. The model identifier is required so the adopted session can be tracked against a repo/model in cookbook state.

Source

Thrown at routes/codex_routes.py:841

        return await serve_endpoint(request, req)

    @router.post("/cookbook/adopt")
    async def codex_cookbook_adopt(request: Request, body: dict[str, Any] = Body(default_factory=dict)):
        """Adopt an existing tmux session (one started via raw ssh+tmux) into
        cookbook tracking. Needed when serve_model rejects a cmd and the
        agent falls back to direct ssh — without adoption the session is
        invisible to the UI. Body: {tmux_session, model, host?, port?}."""
        _require_cookbook_scope(request, COOKBOOK_LAUNCH_SCOPES)
        norm = dict(body or {})
        sess = (norm.get("tmux_session") or norm.get("session_id") or "").strip()
        model = (norm.get("model") or norm.get("repo_id") or "").strip()
        host = validate_remote_host((norm.get("host") or norm.get("remote_host") or "").strip() or None) or ""
        port = norm.get("port") or 8000
        import re as _re
        if not sess or not _re.fullmatch(r"[a-zA-Z0-9_-]+", sess):
            raise HTTPException(400, "tmux_session required, [a-zA-Z0-9_-]+ only")
        if not model:
            raise HTTPException(400, "model required")
        # Verify the tmux session exists on the target host before adopting.
        import shlex
        if host:
            check = f"ssh {shlex.quote(host)} 'tmux has-session -t {shlex.quote(sess)}'"
        else:
            check = f"tmux has-session -t {shlex.quote(sess)}"
        chk = await _run_shell(check, timeout=8)
        if chk.get("exit_code") not in (0, None):
            raise HTTPException(404, f"tmux session {sess!r} not found on {host or 'local'}")
        # Write into cookbook_state.json.
        import time as _t, json as _json
        from core.atomic_io import atomic_write_json
        from pathlib import Path as _Path
        cookbook_state_path = _Path(COOKBOOK_STATE_FILE)
        try:
            state = _json.loads(cookbook_state_path.read_text(encoding="utf-8"))
        except Exception:
            state = {}

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Include "model" (or "repo_id") in the adopt body, e.g. "Qwen/Qwen2.5-7B".
  2. Upgrade the calling client to the current body schema {tmux_session, model, host?, port?}.

Example fix

// before
{"tmux_session":"vllm-1"}

// after
{"tmux_session":"vllm-1","model":"Qwen/Qwen2.5-7B-Instruct"}
Defensive patterns

Strategy: validation

Validate before calling

body = {'tmux_session': sess, 'model': model}
assert body['model'] and body['model'].strip()

Type guard

const isAdoptBody = (b: Record<string,unknown>) =>
  typeof b.tmux_session === 'string' && typeof (b.model ?? b.repo_id) === 'string' && !!(b.model ?? b.repo_id)

Prevention

When it happens

Trigger: POST /cookbook/adopt with neither 'model' nor 'repo_id' in the body, or a whitespace-only value.

Common situations: Client sends only tmux_session; field named differently (e.g. 'model_name') in an older client; JSON body omitted the key.

Related errors


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/710a1b043db7b3e6. Report an issue: GitHub.