Skip to content

5. Verify Requests and Routing

Illustrative mutation fragments

The management mutations on this page are non-standalone fragments. Before adapting them, follow this site's Example Quality Contract and complete Quickstart lifecycle. Do not treat HTTP success or management readback alone as runtime proof.

Converged guide overview · Previous: 4. Configure Gateway Routing · Next: 6. Switch Modes, Clean Up and Roll Back

Before you start

Keep the working workers and Gateway rule from Step 4.

Focus of this page: Send complete chat/SSE through the Gateway and correlate independent worker evidence.

Topology and execution locations

flowchart TD
    C["Client workload"] --> G["Dedicated Gateway<br/>selected VIP and PORT"]
    G --> A["Worker 1<br/>WORKER1:BACKEND_PORT"]
    G --> B["Worker 2<br/>WORKER2:BACKEND_PORT"]
    G --> D["Worker 3<br/>WORKER3:BACKEND_PORT"]
    O["Operator<br/>model and image selection"] -.->|"configuration and readback"| G
    O -.->|"direct inference and engine receipts"| A
    style G fill:#e1f5fe,stroke:#0288d1,color:#1f2937
    style A fill:#e8f5e9,stroke:#43a047,color:#1f2937
    style B fill:#e8f5e9,stroke:#43a047,color:#1f2937
    style D fill:#e8f5e9,stroke:#43a047,color:#1f2937

This diagram shows the converged three-worker guide. Choose one engine and one model per run: workers 1/2/3 are replicas of that choice, not vLLM, SGLang and TensorRT-LLM mixed together. Select actual host addresses in the model-selection step; documentation addresses are examples. Each converged worker performs both prefill and decode.

Where What happens here
Operator terminal Select the version tuple, private run directory, model snapshot and profiles.
Gateway host / namespace Start the dedicated Gateway; apply and read back only the selected service.
Each worker host Launch the chosen engine and prove direct inference before Gateway traffic.
Client Send the same request through the Gateway and retain its HTTP/SSE observations.
Independent evidence Correlate worker requests/receipts with the selected rule and request identifiers.

Walkthrough

Verify requests and outcomes

curl -fsS --max-time 180 -H 'Content-Type: application/json' \
  --data-binary @"$PACKAGE/requests/$MODEL_KEY-chat.json" \
  "http://$VIP:$PORT/v1/chat/completions" > "$RUN/chat-response.json"
jq -e '.usage.prompt_tokens>0 and .usage.completion_tokens>0 and
  (.choices[0].finish_reason=="stop" or .choices[0].finish_reason=="length") and
  (((.choices[0].message.content // "")|length)>0 or
   ((.choices[0].message.reasoning_content // "")|length)>0)' "$RUN/chat-response.json"
curl -N -fsS --max-time 180 -H 'Content-Type: application/json' \
  --data-binary @"$PACKAGE/requests/$MODEL_KEY-stream.json" \
  "http://$VIP:$PORT/v1/chat/completions" > "$RUN/stream-response.sse"
cat "$RUN/stream-response.sse"

Validate complete SSE

On the controller, validate the saved stream. For a worker's direct check in Step 3, set SSE_FILE="$RUN/direct-stream.sse" in that worker's shell instead. These checks handle the OpenAI-style data: events emitted by the selected engines and reject a DONE-only or truncated stream.

export SSE_FILE="$RUN/stream-response.sse"
test "$(grep -Ec '^data: ?\[DONE\][[:space:]]*$' "$SSE_FILE")" -eq 1
sed -n 's/^data: *//p' "$SSE_FILE" | sed '/^\[DONE\][[:space:]]*$/d' \
  | jq -s -e 'length>0 and all(.[]; has("error")|not) and
    ([.[].choices[]?] | any(.finish_reason=="stop" or .finish_reason=="length")) and
    ([.[].choices[]? | (.delta.content // "") + (.delta.reasoning_content // "")] |
      any(length>0))'

Expected: DONE count one, valid JSON events, terminal finish and actual content or reasoning. Keep successful curl exit status as well. Stop on any failed gate; a later success does not turn a partial earlier stream into normal completion.

For an authenticated or TLS-protected data plane, add that service's protected client headers/TLS settings. Normal completion requires generated content or reasoning, positive completion tokens, a stop or length finish reason, exactly one SSE [DONE] and no error objects. A DONE count or HTTP status alone is insufficient.

  • RR: Send repeated requests across the three eligible workers and reconcile actual backend receipt counts. This sequential comparison used six requests, checking state together with receipts.
  • Exact: Retain an unwarmed user-prefix control. Prewarm a fixed prefix longer than one block on a single worker. Verify that Gateway requests with that prefix reach the prewarmed worker and that inventory/cache-hit metrics increase. Also record completion and receipt paths for a cold control with a leading nonce. Common system/template blocks can remain cached; a cold control need not produce zero cache hits.
  • Independent receipt evidence: Match request IDs and request/response hashes at the client and backend. This QA used a small receipt collector without changing product code. It is optional QA instrumentation. For equivalent verification on a native engine path, obtain approved backend-side receipts. Successful requests alone do not establish cache reuse or an Exact benefit.

Controlled routing check on the isolated receipt path

Use this block only when Step 3's collectors are running on all workers and Step 4 used BACKEND_PORT=8102. For native port 8000, supply equivalent backend receipts before claiming routing verification. Completion alone verifies serving, not which worker handled it.

Prepare a long shared-prefix request, preserving its exact bytes:

jq '.messages=[{role:"user",content:(("Shared routing verification prefix. " * 64) +
  "Explain prefix caching in one sentence.")}] | .stream=false | del(.stream_options)' \
  "$PACKAGE/requests/$MODEL_KEY-chat.json" > "$RUN/routing-probe.json"
sha256sum "$RUN/routing-probe.json"

For Exact only, send that prefix directly to Worker A before Gateway traffic, then allow native events to populate inventory. RR skips this prewarm.

curl -fsS --max-time 180 -H 'Content-Type: application/json' \
  -H "X-ModeQA-Request-ID: $RUN_ID-prewarm" \
  --data-binary @"$RUN/routing-probe.json" \
  "http://$WORKER1:8102/v1/chat/completions" > "$RUN/prewarm.json"

Enable/read metrics and preserve before/after counters. On the shared host, record other traffic; an aggregate counter change cannot identify this rule by itself. Filter by the actual service/endpoint labels and correlate receipts.

curl -fsS --max-time 10 --request POST --header @"$CONTROL_HEADERS" \
  "$CONTROL_API/config/metrics" > "$RUN/metrics-enable.json"
curl -fsS --max-time 10 --header @"$CONTROL_HEADERS" \
  "$CONTROL_API/metrics" > "$RUN/metrics-before.txt"
for i in 1 2 3 4 5 6; do
  curl -fsS --max-time 180 -H 'Content-Type: application/json' \
    -H "X-ModeQA-Request-ID: $RUN_ID-$ROUTING_MODE-$i" \
    --data-binary @"$RUN/routing-probe.json" \
    -D "$RUN/routing-$i.headers" \
    "http://$VIP:$PORT/v1/chat/completions" > "$RUN/routing-$i.json" || break
  jq -e '.usage.completion_tokens>0 and
    (.choices[0].finish_reason=="stop" or .choices[0].finish_reason=="length")' \
    "$RUN/routing-$i.json" || break
  sha256sum "$RUN/routing-$i.json"
done
curl -fsS --max-time 10 --header @"$CONTROL_HEADERS" \
  "$CONTROL_API/metrics" > "$RUN/metrics-after.txt"

Require all six completions. X-ModeQA-Backend in the saved response headers identifies the forwarding collector. On each worker, inspect its independent records (restore its session first). Copy RUN_ID from the controller if the worker's initial timestamp differs:

jq -c 'select(.requestId|contains("-rr-") or contains("-exact-")) |
  {requestId,worker,httpStatus,upstreamComplete,requestSha256,responseSha256,error}' \
  "$RUN/receipts/receipts.jsonl"

Match this run's six exact IDs, status 200, upstreamComplete:true, null error, the common request hash and each response hash. Ignore records from other runs. With all three eligible workers and no other traffic, RR should distribute two receipts per worker. Exact should engage the warmed inventory, increase the selected hit counter and deliver to the prewarmed worker; load relief can change placement, so retain actual state and receipts instead of assuming a hit. Create a distinct cold-prefix control and retain its own request/response hash:

jq --arg nonce "$(openssl rand -hex 16)" \
  '.messages[0].content=($nonce + " " + .messages[0].content)' \
  "$RUN/routing-probe.json" > "$RUN/cold-probe.json"
curl -fsS --max-time 180 -H 'Content-Type: application/json' \
  -H "X-ModeQA-Request-ID: $RUN_ID-$ROUTING_MODE-cold" \
  --data-binary @"$RUN/cold-probe.json" \
  "http://$VIP:$PORT/v1/chat/completions" > "$RUN/cold-response.json"
sha256sum "$RUN/cold-probe.json" "$RUN/cold-response.json"

Apply the same completion and independent-receipt checks to this control. System/template blocks may still be shared; cold need not mean zero hits. See KV verification for inventory/subscriber diagnosis. Keep raw prompts/receipts private.

The rejection check below submits invalid kvExactMode:2. Preserve the same model/VIP/port configuration immediately before and after submission. Require HTTP 400 with an error body; a changed configuration fails the check.

# docs-example: expect-schema-error
curl -fsS --max-time 10 --header @"$CONTROL_HEADERS" \
  "$CONTROL_API/config/loadbalancer/all" > "$RUN/before-invalid.json"
jq '.serviceArguments.kvExactMode=2' "$RUN/model-engine-gateway.json" \
  > "$RUN/invalid-model-engine-rule.json"
curl -sS --max-time 30 --request POST --header @"$CONTROL_HEADERS" \
  -H 'Content-Type: application/json' --data-binary @"$RUN/invalid-model-engine-rule.json" \
  --output "$RUN/invalid-response.json" --write-out '%{http_code}\n' \
  "$CONTROL_API/config/loadbalancer" > "$RUN/invalid-http-code.txt"
cat "$RUN/invalid-http-code.txt" "$RUN/invalid-response.json"
test "$(cat "$RUN/invalid-http-code.txt")" = 400
curl -fsS --max-time 10 --header @"$CONTROL_HEADERS" \
  "$CONTROL_API/config/loadbalancer/all" > "$RUN/after-invalid.json"
jq -S --arg m "$MODEL" --arg v "$VIP" --argjson p "$PORT" \
  '[.lbAttr[] | select(.serviceArguments.model_name==$m and
    .serviceArguments.externalIP==$v and .serviceArguments.host==$v and .serviceArguments.port==$p) |
    {serviceArguments:(.serviceArguments|del(.fc_effective.reservation_epoch)),
     endpoints:([.endpoints[]|{endpointIP,targetPort,weight,ep_role}]|sort_by(.endpointIP,.targetPort))}]' \
  "$RUN/before-invalid.json" > "$RUN/config-before-invalid.json"
jq -S --arg m "$MODEL" --arg v "$VIP" --argjson p "$PORT" \
  '[.lbAttr[] | select(.serviceArguments.model_name==$m and
    .serviceArguments.externalIP==$v and .serviceArguments.host==$v and .serviceArguments.port==$p) |
    {serviceArguments:(.serviceArguments|del(.fc_effective.reservation_epoch)),
     endpoints:([.endpoints[]|{endpointIP,targetPort,weight,ep_role}]|sort_by(.endpointIP,.targetPort))}]' \
  "$RUN/after-invalid.json" > "$RUN/config-after-invalid.json"
jq -e 'length==1' "$RUN/config-before-invalid.json"
jq -e 'length==1' "$RUN/config-after-invalid.json"
cmp --silent "$RUN/config-before-invalid.json" "$RUN/config-after-invalid.json"

reservation_epoch is a readback value updated over time, not a configuration input. Preserve both raw responses and exclude only this field from the configuration comparison. The first Llama RR verdict differed only on this value; its original failure and independent reevaluation were retained. A negative control introducing a real configuration change was also detected. Do not remove other differences to obtain a pass. An unregistered-model request must be rejected without backend dispatch. Record errors, timeouts and partial SSE as failures.

Separately verify worker loss/recovery, interrupted SSE, cancellation, resource return and capacity using the operations checks. Report only the executed scope. Fail-over tuning also separates observations from the independent P/D tests.

Troubleshooting

Symptom Diagnosis and next step
Model download 401/403 Check account access and accepted terms; authenticate interactively; do not publish the token
Engine exits or memory error Read startup logs, nvidia-smi, dtype/context and available allocation; reduce one demand or provision capacity, then requalify
Engine port open, chat unavailable Wait for model loading and direct /health, /v1/models, chat; TCP health is insufficient
Gateway not ready Inspect startup logs, kernel/eBPF compatibility, listener and mounted registry permissions; resolve before POST
Missing/mismatched profile Stop Exact; prepare the matching engine-specific QA registry from the manual, or a matching integrated release tuple; do not invent profile geometry
identity_mismatch / token_mismatch Compare image, revision, tokenizer, official template and required launch arguments
challenge_timeout / empty inventory Check worker event port, publisher, seeds, block size and subscriber connectivity
Exact not READY Keep raw state/reason codes; apply the status guide; do not relabel degraded/functional-only as ready
SSE 200 but no content/DONE Retain partial stream and engine logs; inspect waiting/timeouts; classify incomplete
Stopped worker stays active Recheck monitor flag, probe address/port and timeout; this is not successful failure detection
High latency or incomplete load Lower arrivals/concurrency/output; compare RR under the same workload or add GPU capacity; follow the QA appendix
Deleted rule returns after restart Inspect persisted snapshot/effective state and supported persistence workflow; do not delete unrelated snapshots

Negative and recovery checks

On an isolated owned rule, retain before/after state and independent backend receipts as prescribed by the quickstart. Do not send disruptive probes to shared production services.

Check Action Required observation
Invalid rule Make a separate copy with mode: 4 but invalid kvExactMode: 2; POST that copy Rejection, no added/changed rule; compare normalized before/after configuration
Invalid chat Send malformed JSON or a request exceeding the selected context directly, then through Gateway Explicit rejection/error, no normal-completion claim; use receipts to determine which layer rejected it
Strict profile rejection Try an unknown profile on a separately allocated test identity Admission rejects; that exact rule remains absent; retain original rule unchanged
Worker loss After draining test traffic, gracefully stop one named owned worker Monitor eventually marks it inactive; fresh requests complete on healthy workers and have independent receipts
Worker recovery Start that same container; wait for health/model and direct chat Endpoint returns, new chat/SSE complete; Exact attestation/events recover before optimization claims
Stream truncation Inspect a deliberately interrupted client stream in an isolated trial Mark interrupted, not normal; do not calculate full-run latency from only surviving requests

A surviving request is not recovery redistribution or active-stream replay proof. Keep per-endpoint receipts before and after recovery; an interrupted SSE attempt remains a failure even when a later request succeeds.


Previous: 4. Configure Gateway Routing · Next: 6. Switch Modes, Clean Up and Roll Back

Check your result

A returned response is not Exact placement or cache reuse proof; keep negative mutations and receipts.

Select a supported model/engine tuple before interpreting results. Keep direct-worker success separate from Gateway delivery, routing evidence, engine cache reuse, performance and release qualification. Historical PASS, FAIL and BLOCKED results retain their original artifact identities.

Next steps and cleanup

Use the canonical deployment sequence rather than combining fragments from different engine/version pages. Drain before switching or deleting owned resources; retained result pages are evidence, not another deployment recipe. Read the lab starting point if required values are missing. Use cleanup and recovery when ending the run.