Roche LLM via Portkey / Galileo gateway
Call an LLM (Claude, etc.) through Roche’s Galileo AI gateway, which speaks the Portkey / OpenAI-compatible chat-completions API. Requires the Roche network / VPN.
Config (env-driven; the API key is a secret)
| Var | Value |
|---|---|
PORTKEY_BASE_URL | https://eu.aigw.galileo.roche.com/v1 |
PORTKEY_API_KEY | your gateway key (never hard-code / commit) |
PORTKEY_MODEL | e.g. @org-aws-general-eu-west-3/eu.anthropic.claude-haiku-4-5-20251001-v1:0 |
PORTKEY_MAX_TOKENS | output budget — 4000+ (see truncation gotcha) |
Python call
pip install portkey-ai
from portkey_ai import Portkey # import lazily so the app boots without it / offline
client = Portkey(base_url=PORTKEY_BASE_URL, api_key=PORTKEY_API_KEY)
resp = client.chat.completions.create(
model=PORTKEY_MODEL,
max_tokens=PORTKEY_MAX_TOKENS,
messages=[{"role": "system", "content": SYSTEM},
{"role": "user", "content": user}],
)
content = resp.choices[0].message.content or ""
(The JS SDK import Portkey from 'portkey-ai' uses the identical baseURL/apiKey/model.)
Structured JSON output (robust pattern)
- In the system prompt, demand strict JSON only and show the exact shape/schema. State the output language explicitly (“write all text in English”).
- Parse tolerantly — models sometimes wrap JSON in prose/fences:
import json
def extract_json(text):
text = text.strip()
try: return json.loads(text)
except json.JSONDecodeError: pass
s, e = text.find("{"), text.rfind("}")
if s != -1 and e > s: return json.loads(text[s:e+1])
raise ValueError("LLM did not return valid JSON")
- Validate the dict with a Pydantic model (
Model.model_validate(data)).
Error handling that matters
- Truncated JSON (
Expecting ',' delimiter: line N) = you hitmax_tokensmid-output. Raisemax_tokens; keep titles/labels short in the prompt. - Distinguish causes: bad input / not configured → 422; parse or gateway failure →
502. A
json.JSONDecodeErroris a subclass ofValueError, so don’t let it fall into your “bad input → 422” branch — wrap parsing and re-raise asRuntimeErrorfor 502. - Log the raw model output (first ~2000 chars) on parse failure for debugging.
Design pattern: stateless analysis endpoint
If the frontend already holds the data (e.g. loaded rows/objects), POST it straight to the
LLM endpoint (POST /analyze {items:[...]}). No DB re-query, no cache coupling, trivially
testable with a curl -d @payload.json.
Reference implementation
bar-dashboard/backend/app/services/llm_service.py + app/routers/analyze.py
(frontend selects items → POST → drawer renders the structured result).