Skip to content

POST /api/deep-research

The ICD coding endpoint. The server keeps the connection open and streams three result tiers one after the other via server-sent events:

  1. Instant — semantic + keyword search, well under a second. Returns only confident hits (high/medium, up to 10).
  2. Smart — an LLM extracts keywords from the free text and picks codes, a few seconds.
  3. Deep — LLM research in the catalog with a consolidated recommendation, 10–30 s.

Tier 1 can be shown immediately and replaced or extended by tiers 2 and 3 as they arrive. If the user navigates away, just close the connection.

Request

{
  "query": "Patient mit Riss des vorderen Kreuzbandes rechts"
}

query is required (1–2000 characters). A bare ICD code as query (e.g. "M17.9") is treated as a direct lookup and comes back over the same stream: tier 1 then contains the exact catalog match (high) plus neighbouring catalog codes (medium); tiers 2 and 3 list the neighbours as low with a reasoning.

rerank (optional, default off) enables a legacy cross-encoder rerank for tier 1. In that mode tier 1 codes come back with confidence: "score" and a numeric score (0–100) instead of the categorical confidence. Leave it unset unless you depend on the old format.

Response — event stream

Each line is an SSE event of the form data: {...}\n\n. There are three type values:

status — progress, for a spinner or status line:

{ "type": "status", "status": "searching", "message": "Code-Lookup..." }

If something goes wrong during the stream, you get {"type":"status","status":"error","message":"..."} — not an HTTP error but a status event. After that the request is over.

partial — results from tiers 1 and 2 (layer: 1 or 2):

{
  "type": "partial",
  "layer": 1,
  "summary": "",
  "codes": [
    {
      "code": "S83.53",
      "description": "Riss des vorderen Kreuzbandes",
      "confidence": "high",
      "reasoning": "",
      "score": 0
    }
  ]
}

result — result of tier 3, after which the stream ends:

{
  "type": "result",
  "layer": 3,
  "summary": "Short reasoning behind the recommendation",
  "codes": [
    {
      "code": "S83.53",
      "description": "Riss des vorderen Kreuzbandes",
      "confidence": "high",
      "reasoning": "Why this code fits",
      "score": null
    }
  ]
}

Code fields

  • code, description: ICD-10-GM code + official catalog text.
  • confidence: "high"/"medium" for tier 1 (weaker matches are not returned), "high"/"medium"/"low" for tiers 2 and 3. With rerank: true tier 1 uses "score" instead (legacy).
  • score: legacy field — similarity 0–100 only when confidence is "score"; otherwise 0 (tier 1) or null.
  • reasoning: short justification, tiers 2 and 3 only (tier 1 leaves it empty).

Example

Runs server-side (Node 18+); from there the tiers are forwarded to your own frontend, e.g. via your own SSE endpoint:

const response = await fetch('https://medical-api.dev.unomed.ch/api/deep-research', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Authorization: `Bearer ${API_KEY}`,
  },
  body: JSON.stringify({ query: 'Kreuzbandriss rechts' }),
});

if (!response.ok) {
  // 401 / 422 / 503 arrive as regular HTTP errors, not as a stream
  throw new Error(`ICD API ${response.status}: ${await response.text()}`);
}

const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  buffer += decoder.decode(value, { stream: true });
  const lines = buffer.split('\n');
  buffer = lines.pop() || '';
  for (const line of lines) {
    if (!line.startsWith('data: ')) continue;
    const event = JSON.parse(line.slice(6));
    if (event.type === 'partial') forwardTier(event.layer, event.codes);
    if (event.type === 'result') forwardTier(3, event.codes);
    if (event.type === 'status' && event.status === 'error') forwardError(event.message);
  }
}

The client timeout should be at least 120 seconds — tier 3 can take a while.