Salta al contenuto principale

Helicone multi-provider: track OpenAI + Anthropic + Ollama in unified trace

Helicone multi-provider: track OpenAI + Anthropic + Ollama in unified trace Stai usando Claude Sonnet per i task complessi di reasoning, GPT-4o-mini per...

Cosa ne pensi di questo articolo?

Helicone multi-provider: track OpenAI + Anthropic + Ollama in unified trace
AI ObservabilityFoto:AS PhotographyPexels

In Breve

Helicone multi-provider: track OpenAI + Anthropic + Ollama in unified trace Stai usando Claude Sonnet per i task complessi di reasoning, GPT-4o-mini per...

Helicone multi-provider: track OpenAI + Anthropic + Ollama in unified trace

Stai usando Claude Sonnet per i task complessi di reasoning, GPT-4o-mini per le query massive a basso costo, Ollama on-prem quando vuoi privacy assoluta o quando il rate limit cloud ti morde. Risultato: tre dashboard separate, tre fatture, tre formati di trace, e quando a fine mese il CFO ti chiede "quanto stiamo spendendo davvero per LLM e dove" tu apri tre tab, esporti tre CSV diversi, sommi a mano e speri che il totale sia plausibile. Non è osservabilita: è archeologia contabile.

Helicone risolve esattamente questo problema con un pattern semplice ma efficace: si mette in mezzo come proxy HTTP tra la tua applicazione e il provider LLM, intercetta ogni request/response, normalizza i metadati (token, latency, cost, model_version, user_id) e li scrive in un unico storage interrogabile. Cambia letteralmente una sola riga del tuo codice, un override del base_url nel client SDK, e improvvisamente tutte le chiamate finiscono nello stesso cruscotto cost-aware. Niente strumentazione manuale, niente decoratori sparsi nel codebase, niente span da gestire a mano.

Nel mio ml-service del portfolio il provider primario è Ollama on-prem sul VPS Hetzner (vincolo di stack non negoziabile per costo zero e privacy), quindi Helicone-as-a-service non si applica direttamente. Ho però studiato il pattern e replicato il modello dati su un middleware FastAPI custom che logga su Langfuse self-hosted mantenendo lo stesso schema metadata. Questo articolo copre entrambi i mondi: l'uso reale di Helicone con OpenAI e Anthropic per chi già consuma cloud LLM, e la versione "Helicone-style" custom per chi sta su Ollama o vuole evitare un terzo SaaS in mezzo al data plane.

Cosa Imparerai

  • Configurare Helicone come proxy zero-config per OpenAI e Anthropic via override del base_url
  • Costruire un router Python multi-provider che instrada query tra OpenAI, Anthropic e Ollama in modo trasparente
  • Esportare report cost comparison JSON cross-provider per analisi FinOps mensile
  • Tracciare l'hit rate della prompt cache Anthropic e identificare prompt template che sprecano cache
  • Implementare un middleware FastAPI custom che replica il pattern Helicone per Ollama on-prem
  • Esportare dataset Helicone in pandas DataFrame per analytics avanzata, pivot per modello, ARPU
  • Costruire un router A/B cheap-vs-good con metriche di qualita misurabili per ottimizzare cost

Prerequisiti

  • Lettura degli episodi precedenti della serie (EP02 Langfuse, EP03 Phoenix)
  • Familiarita con gli SDK ufficiali OpenAI Python e Anthropic Python
  • Esperienza con FastAPI middleware e dependency injection
  • Conoscenza base di token pricing e calcolo del cost LLM
  • Account opzionale Helicone (free tier 100K request/mese) per replicare gli esempi

Sezione 1 - Architettura proxy Helicone e setup base_url override

L'idea fondante di Helicone è brutale nella sua semplicità: non è un SDK che ti cammina dentro come Langfuse o Phoenix, è un endpoint HTTP che si comporta esattamente come l'API ufficiale del provider. Tu chiami https://oai.helicone.ai/v1/chat/completions al posto di https://api.openai.com/v1/chat/completions, Helicone proxa la richiesta verso OpenAI con la tua API key, riceve la response, la passa al tuo client e contemporaneamente persiste tutta la conversazione (request body, response body, headers, latency, token usage) sul proprio storage. Stesso pattern per Anthropic (https://anthropic.helicone.ai), Azure OpenAI, Cohere, Mistral cloud e una manciata di provider minori.

Il vantaggio operativo enorme è che non tocchi il codice di business. Cambi una variabile d'ambiente, riavvii il processo, e tutta la flotta di chiamate inizia ad apparire sulla dashboard Helicone senza che nessuno se ne accorga. Questo lo rende adatto a integrazione in codebase legacy dove non vuoi propagare un SDK osservabilita ovunque, o a team che hanno gia tanti file con client OpenAI istanziati e non vogliono fare un refactor massiccio. Lo svantaggio è che dipendi da un provider terzo per il data plane: se Helicone ha un outage, le tue chiamate LLM falliscono a meno di non aver configurato un fallback al base_url originale. Per mitigare questo rischio in produzione la pratica corrente è configurare due client paralleli (uno via Helicone, uno diretto al provider) e usare un pattern try/except che committa al diretto solo dopo N fallimenti consecutivi del proxy: cosi mantieni l'osservabilita di default ma non perdi disponibilita quando il proxy ha problemi.

Un secondo aspetto sottovalutato del pattern proxy è la latenza aggiunta. Helicone tipicamente aggiunge 30-80 ms al round trip rispetto alla chiamata diretta al provider, perché la richiesta passa per un'infrastruttura intermedia che la persiste prima di forwardarla. Per la maggior parte dei carichi LLM (dove la generazione impiega 500-3000 ms in totale) questo overhead è nel rumore. Se invece stai facendo classificatore real-time con modelli rapidi (gpt-4o-mini su prompt corti, latency target sotto 200 ms) l'aggiunta del 30-40% di overhead diventa visibile e potresti preferire la chiamata diretta con strumentazione SDK manuale. Misurare prima, decidere dopo: questo vale per ogni layer architetturale che inserisci.

Il setup minimo per OpenAI in Python è letteralmente questo:

# requirements: openai>=1.30, python-dotenv
# Variabili ambiente: OPENAI_API_KEY, HELICONE_API_KEY
import os
from openai import OpenAI

# Override del base_url verso il proxy Helicone
# Helicone-Auth header autentica con la tua chiave Helicone (separata da OpenAI)
client = OpenAI(
    api_key=os.environ["OPENAI_API_KEY"],
    base_url="https://oai.helicone.ai/v1",
    default_headers={
        "Helicone-Auth": f"Bearer {os.environ['HELICONE_API_KEY']}",
        # User-Id permette di filtrare cost per utente nella dashboard
        "Helicone-User-Id": "federico@portfolio",
        # Property custom: tag arbitrari per segmentazione (feature, tenant, env)
        "Helicone-Property-Feature": "blog-summarizer",
        "Helicone-Property-Env": "production",
    },
)

# Da qui in poi il codice è identico al client OpenAI standard.
# Helicone intercetta in modo trasparente.
response = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[
        {"role": "system", "content": "Riassumi articoli tecnici in 3 bullet."},
        {"role": "user", "content": "Articolo lungo qui..."},
    ],
    temperature=0.3,
)

print(response.choices[0].message.content)
# La chiamata è gia loggata su https://www.helicone.ai/dashboard

Lo stesso pattern vale per Anthropic con base_url https://anthropic.helicone.ai. La cosa che rende Helicone particolarmente comodo per stack multi-provider è che gli header Helicone-Property-* sono arbitrari: puoi aggiungere fino a 10 property custom (feature, tenant_id, ab_variant, prompt_version, routing_decision) e poi filtrare/aggregare nella dashboard senza dover modificare lo schema dei trace. È lo stesso concetto dei tag Datadog applicato a un dominio LLM. Nel mio middleware custom ho replicato questa convenzione 1:1 per tenere la portabilita: se domani switchassi a Helicone hosted basterebbe rimappare i nomi metadata.

Una convenzione che ho adottato dopo qualche mese di uso: marchio sempre tre property minime su ogni chiamata, indipendentemente dal contesto. Helicone-Property-Feature identifica la feature applicativa (chat, summarize, code-review, recommend), Helicone-Property-Env distingue dev/staging/production cosi non mischio statistiche di test con produzione, Helicone-Property-PromptVersion traccia la versione del template di prompt usato (v1, v2-with-examples, v3-strict-json). Questa terna copre il 90% dei filtri che mi serviranno dopo: posso isolare "la nuova versione del prompt summarize in produzione consuma il 30% di token in piu rispetto alla precedente, rollback" senza dover scrivere query SQL custom o esportare dati. Aggiungere property a posteriori è difficile (i trace storici non le avranno), quindi vale la pena pensarci una volta sola e applicarle sempre dal giorno zero.

Sezione 2 - Router Python multi-provider OpenAI + Anthropic + Ollama

Una volta che hai Helicone (o equivalente custom) come proxy, il passo successivo è costruire un router applicativo che decida a runtime quale provider chiamare per quale query. Le strategie comuni sono tre: (1) routing statico per task type (riassunti su gpt-4o-mini, code review su claude-sonnet, classification su llama3.1:8b), (2) routing per priorita di costo (prima Ollama on-prem, fallback a OpenAI se Ollama overloaded, fallback ad Anthropic se OpenAI rate-limited), (3) routing per qualita richiesta (utente premium ottiene Claude, utente free ottiene Llama). Tutte queste strategie diventano interrogabili e ottimizzabili solo se l'observability cattura il routing_decision come metadato di ogni trace.

Ecco un router minimale ma realistico che uso come scheletro nei progetti che mescolano provider:

# requirements: openai>=1.30, anthropic>=0.30, httpx, pydantic
import os
import time
from enum import Enum
from typing import Literal
from openai import OpenAI
from anthropic import Anthropic
import httpx
from pydantic import BaseModel

class Provider(str, Enum):
    """Enum chiuso dei provider supportati dal router."""
    OPENAI = "openai"
    ANTHROPIC = "anthropic"
    OLLAMA = "ollama"

class CompletionResult(BaseModel):
    """Risultato normalizzato cross-provider per uniformare il response shape."""
    provider: Provider
    model: str
    text: str
    tokens_in: int
    tokens_out: int
    latency_ms: float
    cost_usd: float

# Pricing per 1K token (input, output) - aggiornare periodicamente
PRICING = {
    "gpt-4o-mini": (0.00015, 0.0006),
    "gpt-4o": (0.0025, 0.01),
    "claude-sonnet-4-5-20250929": (0.003, 0.015),
    "llama3.1:8b": (0.0, 0.0),  # on-prem, costo marginale = 0
}

def _calc_cost(model: str, tokens_in: int, tokens_out: int) -> float:
    """Calcola cost USD basato su pricing per 1K token."""
    p_in, p_out = PRICING.get(model, (0.0, 0.0))
    return (tokens_in / 1000.0) * p_in + (tokens_out / 1000.0) * p_out

def _call_openai(prompt: str, model: str = "gpt-4o-mini") -> CompletionResult:
    """Chiamata OpenAI via proxy Helicone con metadata feature."""
    client = OpenAI(
        api_key=os.environ["OPENAI_API_KEY"],
        base_url="https://oai.helicone.ai/v1",
        default_headers={
            "Helicone-Auth": f"Bearer {os.environ['HELICONE_API_KEY']}",
            "Helicone-Property-Router": "multi-provider-v1",
        },
    )
    t0 = time.time()
    resp = client.chat.completions.create(
        model=model,
        messages=[{"role": "user", "content": prompt}],
    )
    latency = (time.time() - t0) * 1000
    usage = resp.usage
    return CompletionResult(
        provider=Provider.OPENAI,
        model=model,
        text=resp.choices[0].message.content,
        tokens_in=usage.prompt_tokens,
        tokens_out=usage.completion_tokens,
        latency_ms=latency,
        cost_usd=_calc_cost(model, usage.prompt_tokens, usage.completion_tokens),
    )

def _call_anthropic(prompt: str, model: str = "claude-sonnet-4-5-20250929") -> CompletionResult:
    """Chiamata Anthropic via proxy Helicone."""
    client = Anthropic(
        api_key=os.environ["ANTHROPIC_API_KEY"],
        base_url="https://anthropic.helicone.ai",
        default_headers={
            "Helicone-Auth": f"Bearer {os.environ['HELICONE_API_KEY']}",
            "Helicone-Property-Router": "multi-provider-v1",
        },
    )
    t0 = time.time()
    resp = client.messages.create(
        model=model,
        max_tokens=1024,
        messages=[{"role": "user", "content": prompt}],
    )
    latency = (time.time() - t0) * 1000
    usage = resp.usage
    return CompletionResult(
        provider=Provider.ANTHROPIC,
        model=model,
        text=resp.content[0].text,
        tokens_in=usage.input_tokens,
        tokens_out=usage.output_tokens,
        latency_ms=latency,
        cost_usd=_calc_cost(model, usage.input_tokens, usage.output_tokens),
    )

def _call_ollama(prompt: str, model: str = "llama3.1:8b") -> CompletionResult:
    """Chiamata Ollama on-prem. NB: Helicone non lo intercetta,
    il logging avviene via middleware FastAPI custom (Sezione 5)."""
    t0 = time.time()
    resp = httpx.post(
        "http://localhost:11434/api/chat",
        json={
            "model": model,
            "messages": [{"role": "user", "content": prompt}],
            "stream": False,
        },
        timeout=120.0,
    )
    resp.raise_for_status()
    data = resp.json()
    latency = (time.time() - t0) * 1000
    return CompletionResult(
        provider=Provider.OLLAMA,
        model=model,
        text=data["message"]["content"],
        tokens_in=data.get("prompt_eval_count", 0),
        tokens_out=data.get("eval_count", 0),
        latency_ms=latency,
        cost_usd=0.0,  # on-prem
    )

def route_completion(prompt: str, task_type: Literal["simple", "complex", "private"]) -> CompletionResult:
    """Routing decision: simple -> OpenAI cheap, complex -> Anthropic, private -> Ollama on-prem."""
    if task_type == "private":
        return _call_ollama(prompt)
    if task_type == "complex":
        return _call_anthropic(prompt)
    return _call_openai(prompt)

Il router è deliberatamente semplice: una funzione pura senza stato, decisione basata su un parametro esplicito. In produzione vorrai aggiungere fallback (try/except con switch al provider successivo), un classifier upstream che inferisce task_type dalla query, e magari un budget tracker che blocca chiamate Anthropic se l'utente ha gia speso oltre quota mensile. La cosa critica per l'osservabilita è che il routing_decision finisce nei metadata: cosi sulla dashboard Helicone (o Langfuse) puoi filtrare "mostrami tutti i trace con router=multi-provider-v1 e task_type=complex e calcola P95 latency" senza riscrivere SQL custom ogni volta.

Tre antipattern che ho visto in router multi-provider artigianali e che evito sempre: (1) hardcoding del pricing nelle constanti applicative senza un meccanismo di reload - i prezzi cambiano, OpenAI ha tagliato gpt-4o due volte negli ultimi 12 mesi, e se il tuo PRICING dict è inline rischi che il calcolo cost in dashboard sia sbagliato per settimane. Soluzione: tenere il pricing in un file YAML/JSON versionato e ricaricato ad ogni boot del servizio, oppure pullarlo da un endpoint del provider quando esiste. (2) Decisione di routing fatta dentro la funzione di completion invece che a monte - accoppi la logica di business al provider, e quando vuoi cambiare strategia (es. forzare tutto su Ollama in caso di emergency cost cap) devi modificare ogni call site. Soluzione: separare classify_then_route da execute_completion, cosi puoi sostituire la strategia in un punto solo. (3) Mancanza di metrica "fallback rate": se il router cade su provider B perché provider A ha fallito, devi saperlo. Se il fallback rate sale dal 1% al 15% in una settimana significa che A è degradato e il tuo cost su B sta esplodendo silenziosamente.

Sezione 3 - Cost comparison cross-provider e report JSON FinOps

Una volta che hai i trace in un unico storage, la prima query che ti chiederanno (oltre "quanto stiamo spendendo") è "stiamo spendendo bene? Se invece di OpenAI usassimo Anthropic per quei task, risparmieremmo o saremmo piu lenti?". Per rispondere serve un report cost comparison cross-provider che metta in fila gli stessi task eseguiti su modelli diversi e calcoli delta cost + delta latency + delta quality. Ecco un esempio di report JSON che genero settimanalmente nel mio setup:

{
  "report_period": "2026-05-11_to_2026-05-17",
  "total_requests": 12847,
  "total_cost_usd": 18.42,
  "providers": {
    "openai": {
      "requests": 8214,
      "cost_usd": 11.87,
      "tokens_in_total": 4120000,
      "tokens_out_total": 980000,
      "avg_latency_ms": 1240,
      "p95_latency_ms": 2890,
      "models": {
        "gpt-4o-mini": {"requests": 7500, "cost_usd": 6.22},
        "gpt-4o": {"requests": 714, "cost_usd": 5.65}
      }
    },
    "anthropic": {
      "requests": 1923,
      "cost_usd": 6.55,
      "tokens_in_total": 1450000,
      "tokens_out_total": 280000,
      "avg_latency_ms": 1820,
      "p95_latency_ms": 4100,
      "cache_hit_rate": 0.42,
      "cache_savings_usd": 2.18,
      "models": {
        "claude-sonnet-4-5-20250929": {"requests": 1923, "cost_usd": 6.55}
      }
    },
    "ollama": {
      "requests": 2710,
      "cost_usd": 0.0,
      "tokens_in_total": 1080000,
      "tokens_out_total": 320000,
      "avg_latency_ms": 3450,
      "p95_latency_ms": 8900,
      "models": {
        "llama3.1:8b": {"requests": 2710, "cost_usd": 0.0}
      }
    }
  },
  "cost_per_routing_decision": {
    "simple": {"requests": 7500, "cost_usd": 6.22, "avg_cost_per_req": 0.000829},
    "complex": {"requests": 2637, "cost_usd": 12.20, "avg_cost_per_req": 0.004626},
    "private": {"requests": 2710, "cost_usd": 0.0, "avg_cost_per_req": 0.0}
  },
  "what_if_analysis": {
    "if_all_simple_to_ollama": {"projected_savings_usd": 6.22, "projected_latency_increase_ms": 2200},
    "if_all_complex_to_gpt4o": {"projected_savings_usd": 0.95, "projected_quality_delta": "needs_eval"}
  }
}

Il valore di questo formato non è tanto nell'aggregazione (qualunque dashboard ce l'ha gia) quanto nel campo what_if_analysis: è la simulazione che stima cosa succederebbe se ribilanciassi il routing. if_all_simple_to_ollama ti dice che spostare tutte le query simple da gpt-4o-mini a Llama on-prem ti farebbe risparmiare 6.22 USD/settimana ma aggiungerebbe 2.2 secondi medi di latency: sei disposto al trade-off? if_all_complex_to_gpt4o invece avverte che il delta quality è ignoto e va misurato con eval prima di decidere. Questo formato lo passo al management mensilmente come allegato di un report di una pagina che traduce i numeri in decisioni: niente grafici colorati, solo tre numeri e una raccomandazione.

Per la prospettiva storica, ecco la matrice di confronto cost-per-token che uso come reference card mentale quando devo prendere una decisione di routing al volo:

Modello Provider Cost input /1K token Cost output /1K token Cost tipico per query 2K in / 500 out
gpt-4o-mini OpenAI 0.00015 USD 0.0006 USD 0.0006 USD
gpt-4o OpenAI 0.0025 USD 0.01 USD 0.01 USD
claude-haiku-4.5 Anthropic 0.00025 USD 0.00125 USD 0.00113 USD
claude-sonnet-4.5 Anthropic 0.003 USD 0.015 USD 0.0135 USD
claude-sonnet-4.5 (cached read) Anthropic 0.0003 USD 0.015 USD 0.00735 USD
llama3.1:8b on Ollama VPS Self-hosted 0.0 USD (marginale) 0.0 USD (marginale) 0.0 USD + costo VPS gia sunk
llama3.3:70b on Ollama (CPU only) Self-hosted 0.0 USD 0.0 USD 0.0 USD ma 30-90s latency su VPS senza GPU

Notare la colonna "claude-sonnet-4.5 (cached read)": la prompt cache di Anthropic riduce il costo input del 90% (da 0.003 a 0.0003 per 1K token) per le porzioni cacheable del prompt. Su prompt sistema ricchi (10K+ token di context fissi) la differenza si vede subito in bolletta. La sezione successiva entra nel dettaglio del tracking di questa cache.

Una nota sulla riga "llama3.3:70b on Ollama (CPU only)": il VPS Hetzner CCX13 dove gira il mio ml-service non ha GPU dedicata, quindi i modelli grandi sopra i 13 miliardi di parametri sono praticamente inutilizzabili per latency real-time. Ho fatto il test una volta caricando llama3.3:70b in quantizzazione Q4: una singola query da 200 token in input e 300 in output ha impiegato 47 secondi. Per use case batch offline (es. preprocessing notturno di un dataset) è accettabile, per qualsiasi cosa user-facing sincrono è fuori discussione. Ecco perché nel mio router pratico Llama 8B copre i task on-prem rapidi, e per i task complessi che richiedono un modello da 70B+ esce dal VPS verso Anthropic o OpenAI: la qualita la pago, ma solo dove serve davvero.

Sezione 4 - Tracking prompt cache hit rate Anthropic

La prompt cache di Anthropic è una delle feature di pricing piu sottovalutate del 2025-2026. Funziona cosi: marchi una porzione del messaggio system o user con il flag cache_control: {"type": "ephemeral"}, Anthropic memorizza l'embedding di quella porzione per 5 minuti (estensibile a 1 ora con header beta), e nelle chiamate successive che condividono lo stesso prefix paghi quei token al 10% del prezzo standard. Per chatbot RAG con system prompt da 8K token che vengono riusati 100 volte/ora, parliamo di risparmi reali del 70-80% sul cost input.

Il problema è che la cache è invisibile al codice applicativo: non c'è un return value che dice "hai cachato!". Devi parsare i campi usage.cache_creation_input_tokens (token scritti nella cache, costo 1.25x del prezzo input) e usage.cache_read_input_tokens (token letti dalla cache, costo 0.1x) dalla response Anthropic. Helicone parsa questi campi automaticamente e calcola il cache_hit_rate nei suoi metadata, ma se sei su middleware custom devi farlo tu:

# requirements: anthropic>=0.30, langfuse>=2.40
import os
from anthropic import Anthropic
from langfuse import Langfuse

langfuse = Langfuse(
    public_key=os.environ["LANGFUSE_PUBLIC_KEY"],
    secret_key=os.environ["LANGFUSE_SECRET_KEY"],
    host="http://localhost:3000",
)

client = Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])

SYSTEM_PROMPT_LARGE = "..." * 5000  # circa 8K token, candidato ideale per cache

def call_with_cache_tracking(user_query: str, prompt_template_id: str) -> str:
    """Chiama Claude con prompt cache attiva e logga hit rate su Langfuse."""
    response = client.messages.create(
        model="claude-sonnet-4-5-20250929",
        max_tokens=1024,
        system=[
            {
                "type": "text",
                "text": SYSTEM_PROMPT_LARGE,
                # Marcatore cache: questa porzione è cacheable per 5 min
                "cache_control": {"type": "ephemeral"},
            }
        ],
        messages=[{"role": "user", "content": user_query}],
    )

    usage = response.usage
    # Estrazione metriche cache (campi opzionali, default 0)
    cache_creation = getattr(usage, "cache_creation_input_tokens", 0)
    cache_read = getattr(usage, "cache_read_input_tokens", 0)
    input_uncached = usage.input_tokens

    # Calcolo hit rate: token serviti da cache / token input totali
    total_input = cache_creation + cache_read + input_uncached
    cache_hit_rate = cache_read / total_input if total_input > 0 else 0.0

    # Cost calculation con pricing differenziato cache
    cost_uncached = (input_uncached / 1000) * 0.003
    cost_cache_write = (cache_creation / 1000) * 0.00375  # 1.25x
    cost_cache_read = (cache_read / 1000) * 0.0003  # 0.1x
    cost_output = (usage.output_tokens / 1000) * 0.015
    cost_total = cost_uncached + cost_cache_write + cost_cache_read + cost_output

    # Log dettagliato su Langfuse con metadata cache-aware
    langfuse.trace(
        name="anthropic_cached_call",
        input={"query": user_query, "prompt_template_id": prompt_template_id},
        output={"text": response.content[0].text},
        metadata={
            "model": "claude-sonnet-4-5-20250929",
            "tokens_input_uncached": input_uncached,
            "tokens_cache_creation": cache_creation,
            "tokens_cache_read": cache_read,
            "tokens_output": usage.output_tokens,
            "cache_hit_rate": round(cache_hit_rate, 3),
            "cost_total_usd": round(cost_total, 6),
            "cost_savings_vs_no_cache_usd": round(
                (cache_read / 1000) * 0.003 - cost_cache_read, 6
            ),
            "prompt_template_id": prompt_template_id,
        },
    )
    langfuse.flush()
    return response.content[0].text

Il metadato chiave per l'ottimizzazione è cache_hit_rate aggregato per prompt_template_id. Su Langfuse (o Helicone) filtri per template e vedi distribuzioni: se un template ha hit rate medio sotto 30% probabilmente la prefix non è realmente stabile (qualcuno sta inserendo timestamp dinamici, o user_id, o variabili che cambiano ogni chiamata) e la cache si invalida ogni volta. Il refactor consiste nello spostare le parti dinamiche fuori dalla porzione marcata cache_control, lasciando dentro solo testo veramente immutabile (system role, esempi few-shot, regole).

Tre cose da sapere sulla prompt cache di Anthropic che la documentazione cita ma a cui spesso non dai peso al primo passaggio: (1) la cache vive 5 minuti dall'ultima lettura, non dall'ultima scrittura - quindi se il template viene chiamato regolarmente la cache resta calda indefinitamente, ma se hai picchi sparsi rischi continui rebuild costosi. Esiste un beta header per estendere a 1 ora ma costa di piu in scrittura (2x invece di 1.25x). (2) La porzione cacheable deve essere almeno 1024 token su Sonnet (4096 su Haiku): prompt sistema corti non sono cacheable, devi avere abbastanza ciccia. (3) La cache è per-organizzazione su API key, non condivisa cross-account: se hai piu API key che chiamano lo stesso template ognuna paga la sua creation. Un'ottimizzazione pratica è centralizzare le chiamate cacheable su una sola API key di servizio invece di distribuirle per microservizio.

Nel mio ml-service ho un caso concreto su questo: il system prompt del tool "blog summarizer" includeva uno stile guide di 3500 token e veniva chiamato 50-80 volte/ora durante le sessioni di publishing. Senza cache avrei pagato 3500 * 0.003 / 1000 = 0.0105 USD per chiamata solo di input. Con cache attiva: prima chiamata 0.013 USD (1.25x per la creation), chiamate 2-N a 0.001 USD (0.1x), media settimanale 0.0014 USD per chiamata. Risparmio del 87% sul cost input, che si traduce in circa 22 USD/mese in meno su quel singolo template. Misurato su Langfuse via il filtro prompt_template_id=summarizer_v3 AND cache_hit_rate > 0.7.

Sezione 5 - Middleware FastAPI custom Helicone-style per Ollama

Veniamo alla parte piu rilevante per il mio setup specifico: Helicone non funziona con Ollama on-prem (per design e per privacy), ma il pattern proxy è troppo utile per buttarlo via. Soluzione: middleware FastAPI custom che si interpone tra il client interno e Ollama, intercetta ogni chiamata, calcola le stesse metriche che Helicone calcolerebbe (latency, token, cost stimato anche se zero, model, custom property) e le scrive su Langfuse self-hosted. Stesso pattern, costo zero, dati che non escono dal VPS.

Ecco l'implementazione minima ma production-ready che gira nel mio ml-service:

# requirements: fastapi, httpx, langfuse>=2.40, pydantic
import os
import time
import uuid
from typing import Optional
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse
import httpx
from langfuse import Langfuse
from pydantic import BaseModel

app = FastAPI(title="Ollama Helicone-style proxy")

OLLAMA_BASE_URL = "http://localhost:11434"
langfuse = Langfuse(
    public_key=os.environ["LANGFUSE_PUBLIC_KEY"],
    secret_key=os.environ["LANGFUSE_SECRET_KEY"],
    host=os.environ.get("LANGFUSE_HOST", "http://localhost:3000"),
)

class OllamaChatRequest(BaseModel):
    """Schema della request Ollama chat (subset rilevante)."""
    model: str
    messages: list[dict]
    stream: bool = False
    options: Optional[dict] = None

@app.post("/v1/chat/completions")
async def proxy_chat_completions(request: Request):
    """Proxy Helicone-style: intercetta, forwarda, traccia.

    Header attesi (convenzione Helicone-compatible):
    - X-Helicone-User-Id: identificativo utente per cost-per-user
    - X-Helicone-Property-Feature: tag feature per segmentazione
    - X-Helicone-Property-Router: routing decision per A/B
    """
    request_id = str(uuid.uuid4())
    body = await request.json()
    headers = request.headers

    # Estrazione metadata custom Helicone-compatible
    user_id = headers.get("x-helicone-user-id", "anonymous")
    feature = headers.get("x-helicone-property-feature", "unknown")
    router_decision = headers.get("x-helicone-property-router", "default")

    t0 = time.time()
    try:
        # Forward request a Ollama via httpx async (non blocca event loop)
        async with httpx.AsyncClient(timeout=120.0) as client:
            ollama_resp = await client.post(
                f"{OLLAMA_BASE_URL}/api/chat",
                json={
                    "model": body["model"],
                    "messages": body["messages"],
                    "stream": False,
                    "options": body.get("options", {}),
                },
            )
            ollama_resp.raise_for_status()
            data = ollama_resp.json()
    except httpx.HTTPError as e:
        # Log errore su Langfuse anche in caso di fallimento
        langfuse.trace(
            id=request_id,
            name="ollama_proxy_error",
            metadata={"error": str(e), "model": body.get("model")},
            level="ERROR",
        )
        langfuse.flush()
        raise HTTPException(status_code=502, detail=f"Ollama upstream error: {e}")

    latency_ms = (time.time() - t0) * 1000
    tokens_in = data.get("prompt_eval_count", 0)
    tokens_out = data.get("eval_count", 0)

    # Trace su Langfuse con schema Helicone-compatible
    langfuse.trace(
        id=request_id,
        name=f"ollama_chat_{body['model']}",
        input={"messages": body["messages"]},
        output={"text": data["message"]["content"]},
        user_id=user_id,
        metadata={
            "provider": "ollama",
            "model": body["model"],
            "tokens_in": tokens_in,
            "tokens_out": tokens_out,
            "latency_ms": round(latency_ms, 2),
            "cost_usd": 0.0,  # on-prem, marginal cost = 0
            "feature": feature,
            "router_decision": router_decision,
            "request_id": request_id,
        },
    )
    # Fire-and-forget flush async, non blocca la response al client
    langfuse.flush()

    # Risposta in formato OpenAI-compatible (cosi i client esistenti non cambiano)
    return JSONResponse(
        content={
            "id": request_id,
            "object": "chat.completion",
            "created": int(time.time()),
            "model": body["model"],
            "choices": [
                {
                    "index": 0,
                    "message": {"role": "assistant", "content": data["message"]["content"]},
                    "finish_reason": "stop",
                }
            ],
            "usage": {
                "prompt_tokens": tokens_in,
                "completion_tokens": tokens_out,
                "total_tokens": tokens_in + tokens_out,
            },
        }
    )

@app.get("/health")
async def health():
    """Health check per Nginx upstream."""
    return {"status": "ok"}

Tre dettagli architetturali che fanno la differenza in produzione: (1) il forward a Ollama è async via httpx.AsyncClient per non bloccare l'event loop FastAPI quando Ollama impiega 30+ secondi su modelli grandi; (2) la response è modellata sullo schema OpenAI Chat Completions, cosi i client downstream (LangChain, LlamaIndex, codice custom) possono puntare a questo proxy come fosse OpenAI-compatible senza modifiche; (3) il request_id generato è lo stesso che finisce in Langfuse e nella response al client, cosi il log applicativo e il trace observability sono linkabili end-to-end con un singolo identifier.

Quattro miglioramenti che aggiungo su questa base quando il middleware passa in produzione seria: (1) streaming support: l'endpoint Ollama supporta SSE streaming (stream: true), e per chat user-facing vorrai la risposta token-by-token invece di aspettare il batch completo. La complicazione è che devi accumulare la response chunk per chunk e calcolare token usage solo a fine stream, ma httpx.AsyncClient supporta il pattern stream=True con async iteration. (2) Retry con backoff esponenziale in caso di 503 da Ollama (succede quando un altro processo lo sta caricando con un modello diverso): retry 3 volte con 1s, 2s, 4s di delay prima di propagare l'errore al client. (3) Token bucket rate limiting per utente, configurabile via header X-Helicone-User-Id: previene abuse di un singolo user che esaurisce la GPU/CPU del VPS. (4) Caching della response per query identiche entro una finestra breve (60 secondi): se due utenti pongono la stessa domanda contemporaneamente al chatbot del portfolio, una sola chiamata reale a Ollama e response servita da cache. Implementazione semplice con Redis o anche solo dict in-memory + TTL.

Il middleware sta in un singolo file Python di circa 250 righe nel mio repo (ml-service/app/middleware/ollama_proxy.py) e si configura con tre variabili d'ambiente (LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY, OLLAMA_BASE_URL). Lo monto come router FastAPI dedicato sotto il path /internal/ai/ollama, dietro l'autenticazione interna del backend Spring Boot che fa da gateway. Il client Python interno (i tool AI come summarize, recommend, code review) chiama questo endpoint invece di Ollama diretto, ottenendo gratuitamente l'observability senza che il codice di business sappia di Langfuse. Se domani decido di switchare a vLLM al posto di Ollama, cambio il forward target nel middleware e nessun altro file viene toccato. Questo è il vero valore del pattern proxy: l'observability è una preoccupazione infrastrutturale, non applicativa.

Sezione 6 - Export Helicone API in pandas per analytics avanzata

Le dashboard built-in di Helicone e Langfuse coprono l'80% delle domande quotidiane (cost ultimo giorno, top 10 user per spesa, latency P95). Per il restante 20% (analisi di trend stagionali, correlazioni cross-feature, ARPU per coorte) serve estrarre i dati grezzi e lavorarli in pandas/Jupyter. Helicone espone un endpoint REST /v1/request/query con filtri ricchi e cursor-based pagination; ecco come lo carico in DataFrame per analisi mensili:

# requirements: requests, pandas, python-dateutil
import os
import requests
import pandas as pd
from datetime import datetime, timedelta

HELICONE_API_KEY = os.environ["HELICONE_API_KEY"]
HELICONE_API_URL = "https://api.helicone.ai/v1/request/query"

def fetch_helicone_traces(start: datetime, end: datetime, page_size: int = 250) -> pd.DataFrame:
    """Estrae tutti i trace nel range temporale e li concatena in un DataFrame.

    Helicone API limita 60 req/min su free tier: page_size 250 è un buon compromesso
    tra throughput e rispetto del rate limit.
    """
    all_records = []
    cursor = None

    while True:
        payload = {
            "filter": {
                "request": {
                    "created_at": {
                        "gte": start.isoformat(),
                        "lte": end.isoformat(),
                    }
                }
            },
            "limit": page_size,
            "sort": {"created_at": "asc"},
        }
        if cursor:
            payload["cursor"] = cursor

        resp = requests.post(
            HELICONE_API_URL,
            headers={"Authorization": f"Bearer {HELICONE_API_KEY}"},
            json=payload,
            timeout=30,
        )
        resp.raise_for_status()
        data = resp.json()

        batch = data.get("data", [])
        if not batch:
            break
        all_records.extend(batch)

        # Cursor pagination: prendi created_at dell'ultimo record
        cursor = data.get("next_cursor")
        if not cursor:
            break

    # Normalizza in DataFrame con colonne flatten
    df = pd.json_normalize(all_records, sep="_")
    if not df.empty:
        df["created_at"] = pd.to_datetime(df["created_at"])
        df["cost_usd"] = df["cost_usd"].astype(float)
        df["latency_ms"] = df["latency_ms"].astype(float)
    return df

# Esempio uso: report ultimi 30 giorni
end = datetime.utcnow()
start = end - timedelta(days=30)
df = fetch_helicone_traces(start, end)
print(f"Caricati {len(df)} trace in DataFrame")

# Pivot cost per modello x giorno
df["date"] = df["created_at"].dt.date
pivot_cost = df.pivot_table(
    index="date",
    columns="model",
    values="cost_usd",
    aggfunc="sum",
    fill_value=0,
)
print("\nCost USD per modello per giorno:")
print(pivot_cost.tail(7))

# ARPU per user (Average Revenue Per User, in cost LLM):
# quanto ci costa servire ogni utente in media
arpu = df.groupby("user_id")["cost_usd"].agg(["sum", "count", "mean"])
arpu = arpu.sort_values("sum", ascending=False).head(20)
print("\nTop 20 user per cost cumulativo LLM:")
print(arpu)

# Identificazione outlier: user con cost > mean + 3 sigma
mean_cost = df.groupby("user_id")["cost_usd"].sum().mean()
std_cost = df.groupby("user_id")["cost_usd"].sum().std()
outliers = arpu[arpu["sum"] > mean_cost + 3 * std_cost]
print(f"\nUser outlier (cost > 3 sigma): {len(outliers)}")

Il workflow tipico che applico una volta al mese: fetch full month, salvo il DataFrame su parquet locale (cosi non rifaccio la chiamata API), apro Jupyter, faccio 3-4 analisi standard (cost per modello, ARPU, top feature spendaccione, trend giornaliero) e produco un grafico per ognuna. Quando trovo un'anomalia (es: ARPU di un singolo utente esploso 10x in una settimana) drill-down sui trace specifici per capire se è abuso, bug applicativo o feature lecita di power user. Il vantaggio di pandas vs SQL diretto sulla dashboard Helicone è che posso joinare con dati esterni (tabella utenti del backend Spring Boot, plan tier, cohort di registrazione) e calcolare metriche di business che Helicone da solo non sa.

Una metrica che ho imparato a calcolare presto e che il management apprezza molto è il cost-to-revenue ratio per feature LLM. Per ogni feature tagged via property (chat, summarize, recommend, code-review) sommo il cost LLM mensile e lo divido per il revenue attribuibile a quella feature (utenti che hanno fatto upgrade premium dopo aver usato la feature, conversion da trial). Se il ratio è sotto 5% la feature è sostenibile e puoi investire per espandere il volume. Se è tra 5% e 20% è borderline, va ottimizzato il routing o il prompt. Sopra 20% sei in rosso e devi cambiare modello, cachare aggressivamente o cap-pare l'uso. Senza i metadata Helicone-style (Property-Feature) questa metrica sarebbe impossibile da calcolare in modo automatico, dovresti ricostruirla a posteriori con join su log applicativi e fatica manuale insostenibile a scala.

Per le anomalie ricorrenti ho automatizzato un job che gira ogni notte alle 3 UTC: scarica i trace delle ultime 24 ore, calcola ARPU per user, identifica outlier (sopra mean + 3 sigma), e manda un alert Telegram al mio bot personale con la lista. La mattina vedo se c'è qualcosa da investigare. Tipicamente in un mese di osservazione gli alert sono stati 2-3 e in tutti i casi si trattava di un test di carico mio o di un crawler che chiamava le API in loop, mai un attacco vero. Ma il giorno che succedera' sapro' entro 24 ore e non a fine mese aprendo la fattura. Questo livello di automazione costa pochissimo da scrivere (50 righe Python + un cron) e ha un ritorno asimmetrico: tanto upside (incidenti bloccati presto), zero downside (false positive ignorabili).

Sezione 7 - Router A/B cheap-vs-good per ottimizzazione cost

L'ultimo pattern che voglio mostrare è quello che ti fa risparmiare il 50-70% di bolletta LLM se applicato bene: il router A/B cheap-vs-good. L'intuizione è semplice: tu hai un mix eterogeneo di query, alcune semplici (riassunto di un paragrafo, classification binary) altre complesse (reasoning multi-step, code generation). Il modello "good" (Claude Sonnet, GPT-4o) costa 10-20x il modello "cheap" (Claude Haiku, gpt-4o-mini, Llama 8B), ma per le query semplici la qualita è identica. Se riesci a routare correttamente, paghi 10x solo dove serve davvero.

L'implementazione richiede tre componenti: (1) un classifier che assegna la query a una bucket (puoi usare un mini-LLM o regole heuristiche), (2) il router che chiama il provider giusto, (3) un eval pipeline che misura la qualita output sui due modelli per confermare che il "cheap" sia davvero accettabile per la bucket assegnata. Ecco lo scheletro:

# requirements: openai, anthropic, langfuse
import os
import re
from openai import OpenAI
from anthropic import Anthropic
from langfuse import Langfuse

langfuse = Langfuse(
    public_key=os.environ["LANGFUSE_PUBLIC_KEY"],
    secret_key=os.environ["LANGFUSE_SECRET_KEY"],
    host="http://localhost:3000",
)

# Heuristica leggera per classificazione query (in produzione: classifier LLM dedicato)
def classify_complexity(query: str) -> str:
    """Ritorna 'cheap' o 'good' in base a heuristiche.

    Cheap: query brevi, classification, FAQ, riassunti corti.
    Good: query lunghe, multi-step, code generation, reasoning esplicito.
    """
    word_count = len(query.split())
    has_code_marker = bool(re.search(r"```|def |class |function ", query))
    has_reasoning_marker = bool(re.search(
        r"perche|spiega|analizza|confronta|step by step|ragiona",
        query.lower(),
    ))

    if has_code_marker or has_reasoning_marker or word_count > 200:
        return "good"
    return "cheap"

def ab_route_completion(query: str, user_id: str, force_variant: str = None) -> dict:
    """Router A/B: classifica, instrada, traccia decisione e costo.

    force_variant permette di overridare per A/B test controllato:
    se passi 'good' a una query classified cheap, paghi di piu ma
    misuri se il good produce output migliore (eval upstream).
    """
    bucket = classify_complexity(query)
    variant = force_variant or bucket

    if variant == "cheap":
        client = OpenAI(
            api_key=os.environ["OPENAI_API_KEY"],
            base_url="https://oai.helicone.ai/v1",
            default_headers={
                "Helicone-Auth": f"Bearer {os.environ['HELICONE_API_KEY']}",
                "Helicone-User-Id": user_id,
                "Helicone-Property-AbVariant": "cheap",
                "Helicone-Property-ClassifiedAs": bucket,
            },
        )
        resp = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": query}],
        )
        return {
            "variant": "cheap",
            "model": "gpt-4o-mini",
            "text": resp.choices[0].message.content,
            "tokens_in": resp.usage.prompt_tokens,
            "tokens_out": resp.usage.completion_tokens,
            "cost_usd": (resp.usage.prompt_tokens / 1000) * 0.00015
                + (resp.usage.completion_tokens / 1000) * 0.0006,
        }
    else:
        client = Anthropic(
            api_key=os.environ["ANTHROPIC_API_KEY"],
            base_url="https://anthropic.helicone.ai",
            default_headers={
                "Helicone-Auth": f"Bearer {os.environ['HELICONE_API_KEY']}",
                "Helicone-User-Id": user_id,
                "Helicone-Property-AbVariant": "good",
                "Helicone-Property-ClassifiedAs": bucket,
            },
        )
        resp = client.messages.create(
            model="claude-sonnet-4-5-20250929",
            max_tokens=2048,
            messages=[{"role": "user", "content": query}],
        )
        return {
            "variant": "good",
            "model": "claude-sonnet-4-5-20250929",
            "text": resp.content[0].text,
            "tokens_in": resp.usage.input_tokens,
            "tokens_out": resp.usage.output_tokens,
            "cost_usd": (resp.usage.input_tokens / 1000) * 0.003
                + (resp.usage.output_tokens / 1000) * 0.015,
        }

# Esempio A/B test controllato: 10% del traffico classified 'cheap' viene
# routato 'good' per misurare delta quality
import random
def ab_test_route(query: str, user_id: str) -> dict:
    bucket = classify_complexity(query)
    if bucket == "cheap" and random.random() < 0.10:
        return ab_route_completion(query, user_id, force_variant="good")
    return ab_route_completion(query, user_id)

Il pattern di A/B test controllato (10% traffico cheap routato a good) è fondamentale: senza questo confronto continuo non sai se la tua heuristica di classificazione sta sbagliando. Su Langfuse o Helicone filtri per ab_variant=good AND classified_as=cheap, prendi gli output, li passi a un LLM judge che assegna score 1-5 di qualita rispetto agli output del cheap, e calcoli il delta medio. Se delta < 0.5 punti su scala 5, la classificazione cheap è corretta e puoi spegnere l'A/B (risparmio totale). Se delta > 1 punto, devi ricalibrare la heuristica per spostare quei pattern di query nella bucket good.

Sezione 8 - Confronto Helicone vs Langfuse vs Phoenix per multi-provider

A questo punto della serie hai visto tre piattaforme di osservabilita LLM: Langfuse (EP02), Phoenix (EP03), Helicone (questo episodio). Tutte e tre coprono lo stesso problema generale ma con angoli diversi. Quando un team mi chiede "quale scegliere", la risposta dipende dal mix di provider e dal punto del ciclo di vita in cui si trova il prodotto. Ecco la matrice decisionale che uso:

Criterio Helicone Langfuse Phoenix (Arize)
Pattern integrazione Proxy HTTP (base_url override) SDK con decorator/manual span SDK + auto-instrumentor OpenInference
Tempo setup zero-config 2 minuti (1 env var) 10 minuti (SDK init + decorator) 5 minuti in notebook, 30 min in container
Multi-provider unified Eccellente (proxy nativo OpenAI, Anthropic, Azure) Buono (un trace cross-provider via SDK manuale) Buono via OpenInference instrumentor
Supporto Ollama on-prem Limitato (richiede self-host o middleware custom) Ottimo via SDK Python diretto Ottimo via instrumentor langchain o openai-compatible
Cost tracking automatico Built-in con pricing aggiornato Built-in via model registry Manuale o calcolato in eval
Prompt cache Anthropic tracking Auto-parsato e dashboardato Manuale via metadata custom Manuale via attributi span
Prompt management UI Limitato Eccellente (versioning, A/B, deploy) Assente (per design notebook-first)
Evaluation framework Base (custom score upload) Buono (eval template + LLM judge) Eccellente (eval libreria ricca)
Self-host disponibile Si (beta, complesso) Si (Docker compose stabile) Si (Docker standalone semplice)
Pricing entry-level Free 100K req/mese, paid da 25 USD/mese Free 50K eventi/mese, self-host gratis Open source Apache 2.0, no SaaS gratis
Caso d'uso ideale Stack multi-provider cloud, FinOps focus Produzione con prompt management critico Prototipazione + experiment tracking

Riassumendo brutalmente: Helicone è la scelta giusta quando hai 3+ provider cloud e l'observability è soprattutto un problema FinOps. Langfuse vince quando il prompt management strutturato è centrale (team grande, A/B test prompt, deploy versionato). Phoenix è insostituibile per data scientist che lavorano in notebook e fanno experiment iterativi. Nel mio setup specifico (Ollama on-prem primario, ml-service FastAPI, vincolo privacy assoluto) la combinazione vincente è Langfuse self-hosted come backbone produzione + Phoenix in notebook per debug + middleware custom Helicone-style per il pattern proxy che proietta gli stessi metadata su Langfuse.

FAQ

Helicone sostituisce Langfuse per la mia stack di osservabilita LLM?

No, sono complementari. Helicone eccelle quando consumi piu provider commerciali (OpenAI, Anthropic, Azure OpenAI, Cohere, Mistral cloud) e ti serve un layer proxy zero-config che intercetti ogni chiamata HTTP e popoli un unico cruscotto cost-aware. Langfuse è un platform piu ricco lato evaluation, prompt management e governance del prompt template. Nel mio caso preciso uso Langfuse self-hosted come backbone produzione del ml-service e considero Helicone solo come riferimento mentale per il pattern proxy: dato che il provider primario è Ollama on-prem, ho replicato la stessa idea con un middleware FastAPI custom che logga su Langfuse mantenendo lo schema metadata Helicone-compatibile.

Posso usare Helicone con Ollama installato sul mio VPS Hetzner?

In teoria si', perché Ollama espone un endpoint compatibile OpenAI su /v1/chat/completions porta 11434 e Helicone proxa qualsiasi target tu gli passi via header Helicone-Target-URL. In pratica è poco utile: Helicone è SaaS hosted (anche se esiste self-host beta), il pricing per request è pensato per provider cloud e perderesti privacy facendo passare token interni da un terzo. Per Ollama on-prem la mia scelta è un middleware FastAPI custom (poche centinaia di righe) che replica il modello dati Helicone (request_id, latency, token_in, token_out, model_version, cost_estimate) e scrive su Langfuse self-hosted. Stesso pattern proxy, costo zero, dati che non escono dal VPS.

Quanto costa Helicone in produzione e quando conviene self-hostarlo?

Il free tier copre 100K request/mese e basta per side project o MVP. I tier paid partono da 25 dollari/mese (1M request) e scalano fino a enterprise con SOC2 e dedicated cluster. Self-host (helicone-ai/helicone su GitHub) richiede un Postgres + ClickHouse + worker proxy: ha senso quando superi 10M request/mese (break-even rispetto al cloud), quando hai vincoli di privacy che impongono di tenere prompt e completion on-prem, oppure quando vuoi customizzare il routing (regole interne tipo "se model contiene gpt-4 manda al provider B"). Per un portfolio singolo come il mio non vale l'overhead operativo: meglio Langfuse self-hosted che gia copre osservabilita end-to-end.

Come traccio l'hit rate della prompt cache di Anthropic?

Anthropic restituisce nel response usage tre metriche specifiche: cache_creation_input_tokens (prima volta che scrivi nella cache, costo 1.25x), cache_read_input_tokens (hit successivi, costo 0.1x), input_tokens (token non cacheable). Helicone parsa automaticamente questi campi e calcola cache_hit_rate = cache_read / (cache_read + cache_creation + input). Sul cruscotto vedi il trend per prompt template: se un template ha hit rate sotto 30% probabilmente la prefix non è stabile (qualcosa cambia tra una chiamata e l'altra) e la cache non si attiva. Nel custom middleware ho replicato lo stesso calcolo: dopo la chiamata Anthropic estraggo response.usage e popolo metadata.cache_hit_rate per Langfuse, cosi posso filtrare e ottimizzare i prompt che sprecano cache.

Quando ha senso un router A/B "cheap vs good" tra GPT-4o-mini e Claude Sonnet?

Quando hai task eterogenei e una metrica di qualita misurabile (rouge, accuracy su test set, success rate). Pattern: classifichi la query in "simple" (FAQ, riassunto breve, classification) vs "complex" (reasoning multi-step, code generation, agentic). Mandi simple al modello cheap (gpt-4o-mini, llama3.1:8b on-prem), complex al modello good (claude-sonnet-4.5, gpt-4o). Misuri delta accuracy e delta cost: se cheap copre 70% volume con accuracy >= 95% del good, risparmi 80% costo totale. Senza observability multi-provider non sapresti che il 30% routato a good in realta ha accuracy uguale al cheap e potresti downgrade. Helicone e Langfuse ti danno il filtro per model_used + custom_property routing_decision per misurare proprio questo.

Come esporto dati Helicone verso pandas o BigQuery per analytics avanzata?

Helicone offre una REST API documentata su helicone.ai/docs: endpoint /v1/request/query con filtri (created_at_start, created_at_end, model, user_id) restituisce JSON paginato. La libreria Python helicone-python ha un client gia pronto, ma in genere preferisco fare requests diretto + paginazione manuale per controllo. Carichi tutto in pandas DataFrame (una colonna per metadata field), poi pivot per model/giorno, calcoli ARPU per user, esporti CSV o BigQuery via pandas-gbq. Stesso pattern funziona per Langfuse via /api/public/traces. La parte critica è rispettare il rate limit (60 req/min su free tier Helicone) e usare cursor-based pagination per dataset > 100K record.

Il middleware FastAPI custom che hai costruito è scalabile in produzione?

Per i volumi del portfolio (10-50K chiamate/mese sul ml-service) è largamente sovradimensionato. La parte critica per scalare è (1) async fire-and-forget verso Langfuse, mai bloccare la response al client; (2) batching: accumulo 50 trace o 10 secondi e flush, evita di hammerare Langfuse con HTTP singoli; (3) circuit breaker se Langfuse è down, accumula in queue locale (Redis o in-memory) e ritenta. Con queste 3 accortezze il middleware regge tranquillamente 1000 req/sec su una macchina modesta. Sopra quel volume conviene migrare a un sidecar dedicato (proxy Envoy con ext_authz custom o un OpenTelemetry collector con processor LLM) per non bloccare il main loop FastAPI sotto carico.

Conclusione

Il pattern proxy di Helicone è una delle idee piu pulite uscite dall'ecosistema LLM observability degli ultimi anni: zero invasivita lato codice, copertura totale del traffico cross-provider, metadati ricchi e interrogabili. Per chi consuma OpenAI + Anthropic + Azure (o sottoinsieme) è la via piu rapida verso un'osservabilita decente con setup misurabile in minuti. Le funzionalita FinOps (cost per user, per feature, per routing decision) sono di prima classe e si traducono direttamente in conversazioni piu informate con il management su dove tagliare e dove investire.

Per chi lavora su Ollama on-prem o ha vincoli di privacy che escludono SaaS terzi nel data plane (il mio caso), il pattern resta valido replicato in casa: un middleware FastAPI di poche centinaia di righe, async fire-and-forget verso Langfuse self-hosted, schema metadata Helicone-compatible per portabilita futura. La complessita è bassa, il valore osservabilita è alto, e mantieni il controllo completo del flusso dati senza dipendenze esterne. Nel mio ml-service questo middleware gira da settimane senza un singolo incidente, intercettando ogni chiamata Ollama dei tool AI del portfolio e producendo trace pulite su Langfuse per ogni feature (chat, review, recommend, summarize).

Il messaggio operativo finale: scegli la piattaforma di observability in base al mix di provider oggi e al pattern di integrazione che minimizza il refactor. Helicone vince se hai cloud eterogeneo e vuoi setup zero-config. Langfuse vince per produzione con prompt management strutturato. Phoenix vince per esplorazione notebook-first. Tutte e tre parlano OpenInference o schema interoperabile, quindi la migrazione tra loro è fattibile se cambi idea fra sei mesi. Quello che non è opzionale è avere una piattaforma centralizzata: nessun foglio Excel manuale puo competere con un cruscotto cost-aware aggiornato in tempo reale quando cominci a girare migliaia di euro al mese in token LLM.

Prossimo episodio: entriamo nel dettaglio delle semantic conventions OpenTelemetry GenAI e di come strumentare LangChain e LlamaIndex con span semanticamente corretti, attributes standardizzati e link parent-child che rendono navigabili gli agent multi-step.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti