{"record":{"id":"45fca41eb8c98cbe","repo":"odysseus-dev/odysseus","slug":"endpoint-a-endpoint-b-or-endpoint-a-id-endpoint-b","errorCode":null,"errorMessage":"endpoint_a/endpoint_b or endpoint_a_id/endpoint_b_id is required","messagePattern":"endpoint_a/endpoint_b or endpoint_a_id/endpoint_b_id is required","errorType":"http","errorClass":"HTTPException","httpStatus":422,"severity":"warning","filePath":"routes/compare/compare_routes.py","lineNumber":144,"sourceCode":"                # Prefer an explicit endpoint id: it pins the EXACT registered\n                # endpoint (and its api_key), even when two endpoints visible to\n                # the caller share a base_url with different keys — a URL-only\n                # match would copy whichever row sorts first, i.e. possibly the\n                # wrong key. Fall back to URL resolution only for legacy / admin\n                # raw-URL callers that don't send an id.\n                eid = endpoint_id.strip() if isinstance(endpoint_id, str) else \"\"\n                if eid:\n                    ep = _owned_endpoint_by_id(db, eid, user)\n                    if ep is None:\n                        # An id the caller can't see (wrong owner / deleted) must\n                        # NOT silently fall back to a same-URL row with a different\n                        # key — that's exactly the mix-up ids exist to prevent.\n                        raise HTTPException(404, \"Model endpoint not found\")\n                    # The id already resolved the endpoint; ignore any raw URL the\n                    # caller also sent and dial the stored config instead.\n                    endpoint = ep.base_url\n                elif not endpoint:\n                    raise HTTPException(\n                        422, \"endpoint_a/endpoint_b or endpoint_a_id/endpoint_b_id is required\"\n                    )\n                else:\n                    # Resolve the supplied URL to a ModelEndpoint the caller owns\n                    # (their own rows + legacy null-owner shared rows), scoped so a\n                    # comparison can't borrow another user's private endpoint key.\n                    base = normalize_base(endpoint)\n                    ep = _owned_endpoint_by_url(db, base, user)\n                # Reject *unregistered* raw URLs for signed-in non-admins; a\n                # matched registered endpoint supplies an id so the caller can\n                # still compare endpoints they own. Blanket-rejecting here (the\n                # earlier `endpoint_id=None` call) locked non-admins out of\n                # compare entirely, since compare resolves endpoints by URL with\n                # no endpoint_id. Mirrors the gallery inpaint/harmonize checks.\n                # Raised here (phase 1), before any session exists.\n                _reject_raw_endpoint_url_for_non_admin(\n                    request, user, str(ep.id) if ep is not None else None, endpoint\n                )","sourceCodeStart":126,"sourceCodeEnd":162,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/compare/compare_routes.py#L126-L162","documentation":"Raised by the compare route when neither an endpoint id nor a raw endpoint URL was supplied for one of the two sides. Each side must resolve to an endpoint somehow; an entirely blank side is a usage error (422), not a lookup failure.","triggerScenarios":"POST compare with endpoint_a/endpoint_b missing or empty and endpoint_a_id/endpoint_b_id also missing for at least one side; client sends model names only and assumes the server picks endpoints.","commonSituations":"Client schema drift — older callers sent only models; form encoding dropping empty-string fields; new frontend forgetting the endpoint pickers.","solutions":["Send endpoint_a/endpoint_b (raw URLs you own or that are registered) or endpoint_a_id/endpoint_b_id for both sides.","Prefer the *_id form; it is unambiguous about which key is used.","Validate the payload client-side before POSTing."],"exampleFix":"// before\n{model_a, model_b}  // no endpoints\n\n// after\n{model_a, model_b, endpoint_a_id: \"17\", endpoint_b_id: \"23\"}","handlingStrategy":"validation","validationCode":"assert (endpoint_a or endpoint_a_id) and (endpoint_b or endpoint_b_id), \\\n    'each compare side needs a URL or an id'","typeGuard":"type CompareSide = { endpoint?: string; endpoint_id?: string }\nconst sideOk = (s: CompareSide) => !!(s.endpoint?.trim() || s.endpoint_id?.trim())","tryCatchPattern":null,"preventionTips":["Send *_id fields preferentially; never rely on server defaults.","Validate the compare payload against a shared schema client-side."],"tags":["validation","http-422","compare"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}