--- id: production-llm-app title: Shipping a Production LLM App code: SP subtitle: Request pipeline, retries, fallback chains, SSE streams, resume, demo limits and a fallback gateway. hue: 15 sources: phases/11-llm-engineering/13-production-app, phases/17-infrastructure-and-production/29-model-demo-interfaces, projects/streaming-answer-recovery, projects/llm-gateway-with-fallbacks source_hash: 55ab30194be15d70 --- ## Lesson tasks type: task-table span: 2 from: phases/11-llm-engineering/13-production-app/docs/en.md, phases/11-llm-engineering/13-production-app/code/production_app.py, phases/17-infrastructure-and-production/29-model-demo-interfaces/docs/en.md, phases/17-infrastructure-and-production/29-model-demo-interfaces/code/main.py | Task | Code | |---|---| | Install the server stack | `pip install fastapi uvicorn` | | Run the app with 4 workers | `uvicorn production_app:app --host 0.0.0.0 --port 8000 --workers 4` | | Pick the primary model | `LLM_MODEL=gpt-4o` | | Start the demo server | `python3 code/main.py --serve` | | Require an access token | `export DEMO_ACCESS_TOKEN=` | | Send one prediction | `curl -s -X POST http://127.0.0.1:8000/api/predict -H 'Content-Type: application/json' -d '{"text": "ok"}'` | | Watch the event stream | `curl -N -X POST http://127.0.0.1:8000/api/stream -H 'Content-Type: application/json' -d '{"text": ""}'` | | Run the demo tests | `python3 -m unittest discover -s code/tests -v` | ## Request pipeline type: architecture from: phases/11-llm-engineering/13-production-app/docs/en.md, phases/11-llm-engineering/13-production-app/code/production_app.py ```arch node | client | 1,1 | Client | web, mobile, API | client node | gw | 2,1 | API Gateway | auth, rate limit | process node | gin | 3,1 | Input guardrails | injection, PII redact | process node | router | 4,1 | Prompt Router | template, A/B version | process node | cache | 4,2 | Semantic Cache | threshold 0.92 | store node | llm | 3,2 | LLM call | retry, fallback chain | model node | gout | 2,2 | Output guardrails | flagged text replaced | process node | resp | 1,2 | Response | eval log, cost, SSE | client edge | client | gw | edge | gw | gin | edge | gin | router | pass edge | router | cache | edge | cache | llm | miss edge | llm | gout | edge | gout | resp | ``` ## Retry with backoff type: annotated-code lang: python from: phases/11-llm-engineering/13-production-app/code/production_app.py ```python async def call_llm_with_retry(prompt, model, max_retries=3): for attempt in range(max_retries + 1): try: ... # one provider call except (ConnectionError, TimeoutError): if attempt < max_retries: backoff = min(2 ** attempt + random.uniform(0, 1), 10) # cap 10 s await asyncio.sleep(backoff) else: raise raise ConnectionError(f"All {max_retries} retries exhausted for {model.value}") # call_with_fallback walks FALLBACK_CHAIN, preferred model first. ``` ## Failure handling type: troubleshooting from: phases/11-llm-engineering/13-production-app/docs/en.md | Symptom | Cause | Fix | |---|---|---| | API 429 | Rate limit | Retry with backoff. Queue the request | | API 500 | Server error | Retry 3 times. Switch to the fallback model | | API timeout over 30s | Slow provider | Retry once. Shorter prompt, smaller model | | Malformed output | Model failure | Retry with the error in the prompt | | Guardrail block | Unsafe input | No retry. Explain the block | | Vector store down | Application failure | Skip RAG context | | Cache down | Application failure | Direct LLM call | | All models fail | Chain exhausted | Cached response, then a fixed apology | ## Defaults in the code type: defaults from: phases/11-llm-engineering/13-production-app/code/production_app.py, phases/17-infrastructure-and-production/29-model-demo-interfaces/code/main.py | Setting | Default | Note | |---|---|---| | `similarity_threshold` | 0.92 | Semantic cache hit score | | `ttl_seconds` | 3600 | Cache entry life | | `max_entries` | 10000 | Cache size | | `max_retries` | 3 | Per model in the chain | | `MAX_CHARS` | 500 | Demo text limit | | `MAX_BODY_BYTES` | 4096 | Larger body gives 413 | | `BUCKET_CAPACITY` | 5 | Refill 0.5 token per second | | `DAILY_CALL_CAP` | 2000 | Over the cap gives 503 | ## Demo error codes type: command-table from: phases/17-infrastructure-and-production/29-model-demo-interfaces/docs/en.md, phases/17-infrastructure-and-production/29-model-demo-interfaces/code/main.py | Command | What it does | |---|---| | `413 body_too_large` | Body over 4,096 bytes | | `408 request_timeout` | Body stops before `Content-Length` | | `415 unsupported_media_type` | Content type is not JSON | | `400 bad_json` | Body is not a JSON object | | `422 text_too_long` | Text over 500 characters | | `422 control_characters` | Text has control characters | | `429 rate_limited` | Token bucket empty. Read `Retry-After` | | `503 usage_cap_reached` | Daily cap reached. Read `Retry-After` | ## SSE wire format type: annotated-code lang: bash from: phases/17-infrastructure-and-production/29-model-demo-interfaces/docs/en.md ```bash event: token # Content-Type text/event-stream data: {"index":0,"text":"Label:"} # a blank line ends each event event: token data: {"index":1,"text":" positive."} event: done data: {"label":"positive","score":1.0,"tokens":11} ``` ## Resume a streamed answer type: flow from: projects/streaming-answer-recovery/README.md, projects/streaming-answer-recovery/solution/main.ts, projects/streaming-answer-recovery/solution/cli.ts 1. **SSEDecoder**: joins split UTF-8 bytes into complete events. 2. **apply**: needs contiguous positive ids. A gap throws `noncontiguous answer event ID`. 3. **apply**: a repeated id with the same data is a no-op. Other data throws. 4. **apply**: payload type is `text`, `citation` or `complete`. Citations need http or https. 5. **connect**: a drop before `complete` sets status `interrupted`. 6. **connect**: the next call sends `Last-Event-ID` and gets only later events. 7. **trace**: writes `schemaVersion: 1` with accepted events. `replay` rebuilds the state. ## Gateway fallback policy type: comparison from: projects/llm-gateway-with-fallbacks/README.md, projects/llm-gateway-with-fallbacks/solution/stage2.go, projects/llm-gateway-with-fallbacks/solution/stage4.go, projects/llm-gateway-with-fallbacks/solution/config.go | Factor | 2xx | 429 or 5xx | Other status | Cancellation | |---|---|---|---|---| | `ClassifyStatus` | `success` | `retry` | `terminal` | none | | Next provider | No | Yes | No | No | | Route state | `completed` | `exhausted` at end | `terminal-error` | `cancelled` | | Proxy reply | Upstream body | 502 at end | 502 | 504 | ## Gateway providers file type: annotated-code lang: bash from: projects/llm-gateway-with-fallbacks/solution/fixtures/providers.example.json, projects/llm-gateway-with-fallbacks/README.md ```bash {"providers": [ {"name": "primary", "url": "http://127.0.0.1:11434/v1/chat/completions"}, {"name": "backup", "url": "http://127.0.0.1:11435/v1/chat/completions", "key_env": "ORCHARD_BACKUP_KEY"}], # env var name, not the key "max_attempts": 2, # 1 to 16, one visit per provider "max_response_bytes": 65536, # 1 byte to 16 MiB "timeout_ms": 4000} # 1 to 5000, whole route ``` ## Project tasks type: task-table span: 2 from: projects/streaming-answer-recovery/README.md, projects/llm-gateway-with-fallbacks/README.md | Task | Code | |---|---| | Make a work folder | `python3 scripts/project_test.py --init /tmp/-work` | | Grade one stage | `python3 scripts/project_test.py --stage 1 --path /tmp/-work --strict` | | Stream demo, drop then resume | `cd projects/streaming-answer-recovery/solution && python3 demo.py` | | Read your own SSE endpoint | `node --experimental-strip-types cli.ts /tmp/trace.json` | | Gateway demo, 503 then backup | `cd projects/llm-gateway-with-fallbacks/solution && go run .` | | Run the gateway proxy | `go run . --config /tmp/providers.json --listen 127.0.0.1:8088` | | Call the proxy | `curl --fail-with-body http://127.0.0.1:8088/v1/chat/completions --data-binary @fixtures/request.json` | ## Targets and checklist type: tips from: phases/11-llm-engineering/13-production-app/docs/en.md - P50 latency under 2s. P99 latency under 10s. - Cache hit rate over 30%. Guardrail block rate under 5%. - Cost per request under $0.01. - Rate limit each user at 10 to 50 requests per minute. - Load test with 100 concurrent users before launch. ## Q&A type: qa from: phases/17-infrastructure-and-production/29-model-demo-interfaces/docs/en.md - **Why POST and not `EventSource`?** `EventSource` sends GET only. GET puts the input in logged URLs. - **Model fails after status 200?** The status is sent. Send an `error` event in the stream. - **What is in an error body?** `code`, `message`, `hint` and `request_id`.