{"record":{"id":"710a1b043db7b3e6","repo":"odysseus-dev/odysseus","slug":"model-required","errorCode":null,"errorMessage":"model required","messagePattern":"model required","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"warning","filePath":"routes/codex_routes.py","lineNumber":841,"sourceCode":"        return await serve_endpoint(request, req)\n\n    @router.post(\"/cookbook/adopt\")\n    async def codex_cookbook_adopt(request: Request, body: dict[str, Any] = Body(default_factory=dict)):\n        \"\"\"Adopt an existing tmux session (one started via raw ssh+tmux) into\n        cookbook tracking. Needed when serve_model rejects a cmd and the\n        agent falls back to direct ssh — without adoption the session is\n        invisible to the UI. Body: {tmux_session, model, host?, port?}.\"\"\"\n        _require_cookbook_scope(request, COOKBOOK_LAUNCH_SCOPES)\n        norm = dict(body or {})\n        sess = (norm.get(\"tmux_session\") or norm.get(\"session_id\") or \"\").strip()\n        model = (norm.get(\"model\") or norm.get(\"repo_id\") or \"\").strip()\n        host = validate_remote_host((norm.get(\"host\") or norm.get(\"remote_host\") or \"\").strip() or None) or \"\"\n        port = norm.get(\"port\") or 8000\n        import re as _re\n        if not sess or not _re.fullmatch(r\"[a-zA-Z0-9_-]+\", sess):\n            raise HTTPException(400, \"tmux_session required, [a-zA-Z0-9_-]+ only\")\n        if not model:\n            raise HTTPException(400, \"model required\")\n        # Verify the tmux session exists on the target host before adopting.\n        import shlex\n        if host:\n            check = f\"ssh {shlex.quote(host)} 'tmux has-session -t {shlex.quote(sess)}'\"\n        else:\n            check = f\"tmux has-session -t {shlex.quote(sess)}\"\n        chk = await _run_shell(check, timeout=8)\n        if chk.get(\"exit_code\") not in (0, None):\n            raise HTTPException(404, f\"tmux session {sess!r} not found on {host or 'local'}\")\n        # Write into cookbook_state.json.\n        import time as _t, json as _json\n        from core.atomic_io import atomic_write_json\n        from pathlib import Path as _Path\n        cookbook_state_path = _Path(COOKBOOK_STATE_FILE)\n        try:\n            state = _json.loads(cookbook_state_path.read_text(encoding=\"utf-8\"))\n        except Exception:\n            state = {}","sourceCodeStart":823,"sourceCodeEnd":859,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/codex_routes.py#L823-L859","documentation":"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.","triggerScenarios":"POST /cookbook/adopt with neither 'model' nor 'repo_id' in the body, or a whitespace-only value.","commonSituations":"Client sends only tmux_session; field named differently (e.g. 'model_name') in an older client; JSON body omitted the key.","solutions":["Include \"model\" (or \"repo_id\") in the adopt body, e.g. \"Qwen/Qwen2.5-7B\".","Upgrade the calling client to the current body schema {tmux_session, model, host?, port?}."],"exampleFix":"// before\n{\"tmux_session\":\"vllm-1\"}\n\n// after\n{\"tmux_session\":\"vllm-1\",\"model\":\"Qwen/Qwen2.5-7B-Instruct\"}","handlingStrategy":"validation","validationCode":"body = {'tmux_session': sess, 'model': model}\nassert body['model'] and body['model'].strip()","typeGuard":"const isAdoptBody = (b: Record<string,unknown>) =>\n  typeof b.tmux_session === 'string' && typeof (b.model ?? b.repo_id) === 'string' && !!(b.model ?? b.repo_id)","tryCatchPattern":null,"preventionTips":["Build adopt payloads from one typed helper shared across callers."],"tags":["validation","http-400","adopt"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}