/v1/chat/completions compatibile con OpenAI, accetta i tuoi bearer token, inoltra le richieste a Venice, supporta risposte in streaming ed emette utili span e metriche OpenTelemetry.
Ti interessa l’implementazione completa del codice? Dai un’occhiata a il repository GitHub.
Prerequisiti
- Rust 1.92+
- Docker e Docker Compose
- Una chiave API Venice
curl- Familiarità di base con i servizi web in Rust
Cosa costruiremo
L’implementazione di riferimento è un piccolo servizio Rust con alcune parti ben distinte:| Parte | Cosa fa |
|---|---|
| Router Axum | Serve /healthz e /v1/chat/completions |
| Estrattore di auth | Valida i bearer token del gateway confrontandoli con le chiavi hashate in Postgres |
| Rate limiter | Usa finestre fisse memorizzate in Postgres |
| Client Venice | Fa da proxy per le richieste di chat completion, sia in streaming che non |
| Telemetria | Registra span GenAI, uso dei token, costo di fatturazione Venice, latenza e tempi di streaming |
| Docker Compose | Esegue Postgres e il gateway in locale |

Creazione del servizio Rust
Inizia con un nuovo progetto binario Rust:Cargo.toml — le spiegazioni sono inserite nello snippet:
Caricamento della configurazione
Il gateway legge tutto dalle variabili d’ambiente. Per codice infrastrutturale come questo, le variabili d’ambiente sono una buona scelta predefinita perché lo stesso binario può essere eseguito in locale, in Docker Compose o in un ambiente hosted senza un formato di file di configurazione separato. Inoltre, molti provider ti permettono di memorizzare le tue variabili d’ambiente come secret nel loro runtime container. Questo è spesso molto più sicuro rispetto a usare qualcosa comedotenv (o dotenvy in Rust, dato che il crate originale dotenv è per lo più deprecato).
Crea src/config.rs:
- L’URL del database
- La tua chiave API Venice
VENICE_BASE_URL punta a https://api.venice.ai/api/v1, e CAPTURE_GENAI_CONTENT ha come default false, in modo che il contenuto dei prompt non venga registrato a meno che tu non lo abiliti intenzionalmente.
Il secondo default è quello più importante. Un gateway può vedere ogni prompt e risposta che vi transita, ma l’osservabilità non dovrebbe diventare automaticamente cattura di contenuti. Nella maggior parte dei sistemi in produzione, conteggi dei token, latenza, nomi dei modelli, codici di stato e metadati di fatturazione sono sufficienti per le operazioni. In generale, registrare prompt e conversazioni in produzione potrebbe non essere solo un problema di privacy — può essere anche un problema di storage. Aggiungerli significa creare span e trace con un livello di cardinalità estremamente alto (cioè l’unicità dei dati all’interno di un dataset). Ciò può rendere la ricerca nei tuoi dati di osservabilità molto costosa, oltre a potenzialmente compromettere le prestazioni durante la ricerca nei dati.
Creazione dello schema del database
Poi, creamigrations/0001_api_keys.sql.
Memorizzeremo solo i primi 12 caratteri di ogni chiave API del gateway come prefisso di lookup, più l’hash SHA-256 della chiave completa. Ciò permette al gateway di trovare rapidamente una riga candidata senza memorizzare credenziali in chiaro.
Il prefisso non è segreto. Esiste per l’indicizzazione. L’hash è ciò che dimostra che il chiamante ha presentato la chiave completa. Questa è la stessa forma di base usata da molti sistemi di chiavi API: mostrare la chiave in chiaro una sola volta, memorizzarne una rappresentazione non reversibile e conservare un breve prefisso per il lookup e i flussi di supporto.
- Le chiavi API non vengono mai memorizzate in chiaro.
- Le impostazioni di rate limit devono essere positive.
- Le chiavi revocate non possono rimanere attive.
- Una finestra di rate limit è identificata univocamente da chiave, ora di inizio e durata della finestra.
Costruire il client Venice
Poi, creeremosrc/venice.rs. Il client deve conoscere solo l’URL delle chat completions a monte, la chiave API Venice e quante volte riprovare in caso di errori transitori.
Mantenere questo wrapper piccolo è intenzionale — il gateway non dovrebbe reimplementare l’intera API di Venice. A livello base, il compito del gateway è collegare la credenziale lato server, applicare un timeout, riprovare le richieste che è sicuro riprovare e restituire la risposta upstream in una forma che il router può inoltrare.
EventSource a partire dalla stessa richiesta:
serde_json::Value. Questa è una scelta di compatibilità intenzionale. Se modellassimo in Rust ogni possibile campo di chat completion, dovremmo mantenere aggiornato il gateway ogni volta che l’API upstream aggiunge un’opzione utile. Analizzando altrove solo ciò di cui abbiamo bisogno, permettiamo ai parametri più recenti di Venice di passare senza rilasciare una nuova versione del gateway.
Condivisione dello stato dell’applicazione
Creasrc/state.rs:
PgPool è già un handle di pool condiviso, e Arc<Config> mantiene la configurazione altrettanto economica.
Questo dà a ogni handler accesso alle stesse tre cose: configurazione immutabile, connessioni al database in pool e il client Venice. Tenerli in un unico AppState semplifica anche i test in seguito, perché gli handler ricevono le loro dipendenze tramite lo state di Axum invece di leggere delle globali.
Autenticazione delle chiavi API del gateway
Il client invia la sua chiave gateway in questo modo:src/auth.rs e implementa un estrattore di Axum. L’estrattore permette agli handler protetti di dichiarare che richiedono una chiave autenticata:
- Analizzare il bearer token.
- Prendere i primi 12 byte come prefisso della chiave.
- Calcolare l’hash SHA-256 del token candidato completo.
- Caricare la riga della chiave attiva tramite il prefisso.
- Confrontare l’hash memorizzato e l’hash candidato in tempo costante.
AuthenticatedApiKey non può accidentalmente saltare l’autenticazione all’interno del corpo della funzione; Axum deve costruire quel valore prima che l’handler venga eseguito. Questo rende il percorso protetto facile da auditare.
Aggiungere rate limit a finestra fissa
Creasrc/rate_limit.rs. Il rate limiter usa una singola istruzione SQL per inserire una nuova finestra o incrementare quella esistente:
WHERE api_key_rate_limit_windows.request_count < $3 è la parte importante. Quando la finestra è già piena, Postgres non aggiorna la riga e RETURNING non produce alcuna riga. L’handler può trasformarla in una risposta 429 Too Many Requests con un header Retry-After.
Una finestra fissa non è il rate limiter più sofisticato, ma è facile da spiegare, facile da ispezionare ed è abbastanza buono per un tutorial su un gateway. Il compromesso è che il traffico può accumularsi attorno ai confini della finestra. Se hai bisogno di un comportamento più uniforme su larga scala, un token bucket o un limiter a finestra scorrevole supportato da Redis è il passo successivo naturale.
Restituire errori in stile OpenAI
Creasrc/error.rs e fai in modo che gli errori dell’applicazione implementino IntoResponse:
Costruire il router
Ora possiamo cablare le route HTTP insrc/router.rs:
AuthenticatedApiKey. Se l’autenticazione fallisce, Axum non entra mai nel corpo dell’handler:
model, messages e stream. Tutto il resto nel corpo JSON viene passato a Venice. Questo mantiene il gateway compatibile con le funzionalità del provider che potresti voler usare in seguito.
L’handler rende anche esplicite le due modalità di risposta. Le richieste non in streaming aspettano che Venice restituisca una risposta JSON completa, poi registrano i metadati della risposta prima di inviare i byte al client. Le richieste in streaming ritornano immediatamente con un corpo text/event-stream supportato da uno stream asincrono. Questa separazione mantiene semplice il percorso non-streaming, dando allo stesso tempo al percorso di streaming abbastanza controllo per osservare i chunk mentre passano.
Supportare le risposte in streaming
Le chat completions in streaming usano server-sent events. Venice invia dati SSE, e il gateway ritrasmette quei dati al client. Il gateway dovrebbe evitare di bufferizzare l’intero stream, perché ciò vanificherebbe lo scopo dello streaming. Agli utenti interessa il tempo al primo token, non solo il tempo al token finale. Inoltrando ogni evento upstream mentre arriva, i client possono renderizzare l’output parziale mentre il modello sta ancora generando. Creasrc/sse.rs:
data: ..., e lo stream termina con data: [DONE].
L’osservatore dello stream è anche il punto in cui possiamo raccogliere metadati senza cambiare ciò che il client vede. Ogni chunk viene inoltrato in formato SSE, ma il gateway può comunque osservare gli ID di risposta, i motivi di terminazione, l’uso dei token, i campi di costo e le informazioni sui tempi mentre quei chunk passano.
Registrazione della telemetria GenAI
I gateway sono utili perché ogni richiesta passa attraverso un unico punto. Questo li rende un ottimo posto per registrare modello, latenza, uso dei token, motivi di terminazione, costo di fatturazione e tempi di streaming. Creasrc/telemetry.rs e inizia analizzando la richiesta:
id della risposta di Venice:
Avvio del server
Ora colleghiamo tutto insrc/main.rs:
- Legge la configurazione.
- Inizializza la telemetria.
- Si connette a Postgres.
- Esegue le migrazioni SQLx.
- Costruisce lo stato condiviso dell’app.
- Avvia il server Axum.
docker compose up può portare l’intero stack a uno stato funzionante. In un deployment di produzione più grande, potresti preferire eseguire le migrazioni come passo di rilascio separato, in modo che i cambiamenti di schema vengano revisionati e applicati prima che le nuove istanze del gateway partano.
Popolare una chiave gateway locale
Per lo sviluppo locale, creascripts/seed_api_key.sh. Lo script inserisce una chiave API del gateway in Postgres memorizzando il suo prefisso e l’hash SHA-256:
Esecuzione in locale
Per l’esecuzione in locale, useremo Docker Compose per avviare sia il gateway che Postgres. Questo mantiene il tutorial riproducibile: i lettori non hanno bisogno di un database configurato manualmente, e il gateway può usare la stessa forma diDATABASE_URL che userebbe in un deployment containerizzato.
-d se vuoi poi usare il terminale per altro (e poi usare docker compose down per rimuoverlo).
In un altro terminale, popola la chiave gateway di sviluppo:
5432, rimuovi il mapping della porta host per il servizio Postgres di Compose. Il gateway deve solo raggiungere Postgres sulla rete interna di Docker.
La cosa importante da tenere a mente è che la chiave API di Venice appartiene solo all’ambiente del gateway. Le richieste dei client dovrebbero usare la chiave gateway seedata. Questa separazione è l’intero punto di mettere un gateway davanti al provider del modello.
Testare il gateway
Prima, controlla lo stato di salute:Estendere questo gateway
Questo gateway è volutamente piccolo, ma ti dà una base solida. Buoni prossimi passi includono:- Aggiungere budget per soggetto e tetti di spesa mensili.
- Supportare più provider upstream dietro la stessa API compatibile con OpenAI.
- Memorizzare i metadati delle richieste per gli audit log mantenendo il logging dei prompt disabilitato di default.
- Aggiungere un’API di amministrazione per creare, revocare e ruotare le chiavi del gateway.
- Aggiungere allowlist di modelli per chiave API.
- Aggiungere Redis o un altro store condiviso se hai bisogno di rate limit a latenza più bassa tra molte istanze del gateway.