/v1/chat/completions compatible OpenAI, accepte vos propres jetons bearer, transfère les requêtes vers Venice, prend en charge les réponses en streaming et émet des spans et métriques OpenTelemetry utiles.
Vous souhaitez voir l’implémentation complète du code ? Consultez le dépôt GitHub.
Prérequis
- Rust 1.92+
- Docker et Docker Compose
- Une clé API Venice
curl- Une familiarité de base avec les services web en Rust
Ce que nous construisons
L’implémentation de référence est un petit service Rust composé de quelques parties clairement définies :| Partie | Ce qu’elle fait |
|---|---|
| Routeur Axum | Sert /healthz et /v1/chat/completions |
| Extracteur d’auth | Valide les jetons bearer de la passerelle par rapport aux clés hachées dans Postgres |
| Limiteur de débit | Utilise des fenêtres fixes stockées dans Postgres |
| Client Venice | Proxie les requêtes de complétion de chat avec et sans streaming |
| Télémétrie | Enregistre les spans GenAI, l’utilisation des jetons, le coût de facturation Venice, la latence et les temporisations de streaming |
| Docker Compose | Exécute Postgres et la passerelle localement |

Création du service Rust
Commencez par un nouveau projet binaire Rust :Cargo.toml — les explications sont ajoutées dans l’extrait de code :
Chargement de la configuration
La passerelle lit tout depuis les variables d’environnement. Pour du code d’infrastructure comme celui-ci, les variables d’environnement sont un bon choix par défaut car le même binaire peut fonctionner localement, dans Docker Compose ou dans un environnement hébergé sans nécessiter un format de fichier de configuration distinct. De plus, de nombreux fournisseurs vous permettront de stocker vos propres variables d’environnement en tant que secrets dans leur propre runtime de conteneur. C’est souvent beaucoup plus sûr que d’essayer d’utiliser quelque chose commedotenv (ou dotenvy en Rust, puisque la crate dotenv originale est en grande partie dépréciée).
Créez src/config.rs :
- L’URL de la base de données
- Votre clé API Venice
VENICE_BASE_URL pointe vers https://api.venice.ai/api/v1, et CAPTURE_GENAI_CONTENT vaut false par défaut, de sorte que le contenu des prompts n’est pas enregistré à moins que vous ne l’activiez intentionnellement.
Cette seconde valeur par défaut est la plus importante. Une passerelle peut voir chaque prompt et chaque réponse qui la traversent, mais l’observabilité ne devrait pas devenir automatiquement une capture de contenu. Dans la plupart des systèmes de production, le nombre de jetons, la latence, les noms des modèles, les codes de statut et les métadonnées de facturation sont suffisants pour les opérations. D’une manière générale, la journalisation des prompts et des conversations en production peut non seulement constituer un risque pour la vie privée — elle peut également représenter un risque de stockage. Les ajouter signifie créer des spans et des traces avec un niveau de cardinalité extrêmement élevé (c’est-à-dire l’unicité des données dans un ensemble de données). Cela peut rendre la recherche dans vos données d’observabilité très coûteuse, en plus de potentiellement nuire aux performances lors de la recherche dans les données.
Création du schéma de base de données
Ensuite, créezmigrations/0001_api_keys.sql.
Nous ne stockerons que les 12 premiers caractères de chaque clé API de passerelle en tant que préfixe de recherche, ainsi que le hachage SHA-256 de la clé complète. Cela permet à la passerelle de trouver rapidement une ligne candidate sans stocker les identifiants bruts.
Le préfixe n’est pas secret. Il existe pour l’indexation. Le hachage est ce qui prouve que l’appelant a présenté la clé complète. C’est la même forme de base utilisée par de nombreux systèmes de clés API : afficher la clé brute une seule fois, stocker une représentation non réversible et conserver un court préfixe pour la recherche et les flux de travail de support.
- Les clés API ne sont jamais stockées en clair.
- Les paramètres de limitation de débit doivent être positifs.
- Les clés révoquées ne peuvent pas rester actives.
- Une fenêtre de limitation de débit est identifiée de manière unique par la clé, l’heure de début et la durée de la fenêtre.
Construction du client Venice
Ensuite, nous allons créersrc/venice.rs. Le client n’a besoin de connaître que l’URL des complétions de chat en amont, la clé API Venice et le nombre de fois qu’il doit réessayer en cas d’échec transitoire.
Garder ce wrapper petit est intentionnel — la passerelle ne devrait pas réimplémenter l’ensemble de l’API de Venice. À un niveau de base, le travail de la passerelle consiste à attacher l’identifiant côté serveur, à appliquer un délai d’attente, à réessayer les requêtes qu’il est sûr de réessayer et à renvoyer la réponse en amont sous une forme que le routeur peut transférer.
EventSource à partir de la même requête :
serde_json::Value. C’est un choix de compatibilité délibéré. Si nous modélisons chaque champ possible de la complétion de chat en Rust, nous devons maintenir la passerelle à jour chaque fois que l’API en amont ajoute une option utile. En n’analysant ailleurs que ce dont nous avons besoin, nous laissons passer de nouveaux paramètres Venice sans nécessiter une nouvelle version de la passerelle.
Partage de l’état de l’application
Créezsrc/state.rs :
PgPool est déjà un handle de pool partagé, et Arc<Config> maintient également la configuration peu coûteuse.
Cela donne à chaque handler accès aux trois mêmes choses : configuration immuable, connexions de base de données en pool et client Venice. Les garder dans un seul AppState simplifie également les tests plus tard, car les handlers reçoivent leurs dépendances via l’état Axum au lieu de lire des variables globales.
Authentification des clés API de la passerelle
Le client envoie sa clé de passerelle ainsi :src/auth.rs et implémentez un extracteur Axum. L’extracteur permet aux handlers protégés de déclarer qu’ils nécessitent une clé authentifiée :
- Analyser le jeton bearer.
- Prendre les 12 premiers octets comme préfixe de clé.
- Hacher le jeton candidat complet avec SHA-256.
- Charger la ligne de clé active par préfixe.
- Comparer le hachage stocké et le hachage candidat en temps constant.
AuthenticatedApiKey ne peut pas accidentellement sauter l’authentification à l’intérieur du corps de la fonction ; Axum doit construire cette valeur avant que le handler ne s’exécute. Cela rend le chemin protégé facile à auditer.
Ajout de limites de débit à fenêtre fixe
Créezsrc/rate_limit.rs. Le limiteur de débit utilise une seule instruction SQL pour insérer une nouvelle fenêtre ou incrémenter la fenêtre existante :
WHERE api_key_rate_limit_windows.request_count < $3 est le point important. Lorsque la fenêtre est déjà pleine, Postgres ne met pas à jour la ligne et RETURNING ne produit aucune ligne. Le handler peut transformer cela en une réponse 429 Too Many Requests avec un en-tête Retry-After.
Une fenêtre fixe n’est pas le limiteur de débit le plus sophistiqué, mais elle est facile à expliquer, facile à inspecter et suffisamment bonne pour un tutoriel de passerelle. Le compromis est que le trafic peut se concentrer autour des limites de fenêtre. Si vous avez besoin d’un comportement plus fluide à grande échelle, un limiteur à seau à jetons ou à fenêtre glissante soutenu par Redis est une étape naturelle suivante.
Retour d’erreurs de style OpenAI
Créezsrc/error.rs et faites en sorte que les erreurs de l’application implémentent IntoResponse :
Construction du routeur
Nous pouvons maintenant câbler les routes HTTP danssrc/router.rs :
AuthenticatedApiKey. Si l’authentification échoue, Axum n’entre jamais dans le corps du handler :
model, messages et stream. Tout le reste du corps JSON passe à Venice. Cela maintient la passerelle compatible avec les fonctionnalités du fournisseur que vous pourriez vouloir utiliser plus tard.
Le handler rend également explicites les deux modes de réponse. Les requêtes sans streaming attendent que Venice renvoie une réponse JSON complète, puis enregistrent les métadonnées de réponse avant d’envoyer les octets en aval. Les requêtes en streaming renvoient immédiatement un corps text/event-stream soutenu par un flux asynchrone. Cette séparation garde le chemin sans streaming simple tout en donnant au chemin de streaming suffisamment de contrôle pour observer les morceaux au fur et à mesure qu’ils passent.
Prise en charge des réponses en streaming
Les complétions de chat en streaming utilisent les server-sent events. Venice envoie des données SSE, et la passerelle relaie ces données au client. La passerelle doit éviter de mettre en mémoire tampon l’ensemble du flux car cela irait à l’encontre du but même du streaming. Les utilisateurs se soucient du temps avant le premier jeton, pas seulement du temps avant le jeton final. En transférant chaque événement en amont dès qu’il arrive, les clients peuvent afficher une sortie partielle pendant que le modèle est encore en train de générer. Créezsrc/sse.rs :
data: ..., et le flux se termine par data: [DONE].
L’observateur de flux est également l’endroit où nous pouvons collecter des métadonnées sans changer ce que voit le client. Chaque morceau est transféré au format SSE, mais la passerelle peut toujours surveiller les identifiants de réponse, les raisons de fin, l’utilisation des jetons, les champs de coût et les informations de temporisation à mesure que ces morceaux passent.
Enregistrement de la télémétrie GenAI
Les passerelles sont utiles parce que chaque requête passe par un seul endroit. Cela en fait un endroit idéal pour enregistrer le modèle, la latence, l’utilisation des jetons, les raisons de fin, le coût de facturation et les temporisations de streaming. Créezsrc/telemetry.rs et commencez par analyser la requête :
id de la réponse de Venice :
Démarrage du serveur
Maintenant, câblez tout ensemble danssrc/main.rs :
- Lit la configuration.
- Initialise la télémétrie.
- Se connecte à Postgres.
- Exécute les migrations SQLx.
- Construit l’état partagé de l’application.
- Démarre le serveur Axum.
docker compose up peut amener toute la pile à un état fonctionnel. Dans un déploiement de production plus important, vous préférerez peut-être exécuter les migrations comme une étape de release distincte afin que les changements de schéma soient examinés et appliqués avant le démarrage des nouvelles instances de la passerelle.
Initialisation d’une clé de passerelle locale
Pour le développement local, créezscripts/seed_api_key.sh. Le script insère une clé API de passerelle dans Postgres en stockant son préfixe et son hachage SHA-256 :
Exécution locale
Pour exécuter localement, nous utiliserons Docker Compose pour démarrer à la fois la passerelle et Postgres. Cela garde le tutoriel reproductible : les lecteurs n’ont pas besoin d’une base de données configurée manuellement, et la passerelle peut utiliser la même forme deDATABASE_URL qu’elle utiliserait dans un déploiement conteneurisé.
-d si vous voulez utiliser votre terminal pour d’autres choses après (puis utilisez docker compose down pour le supprimer).
Dans un autre terminal, initialisez la clé de passerelle de développement :
5432, supprimez le mappage de port hôte pour le service Postgres de Compose. La passerelle n’a besoin d’atteindre Postgres que sur le réseau interne de Docker.
L’important à garder à l’esprit est que la clé API Venice n’appartient qu’à l’environnement de la passerelle. Les requêtes des clients doivent utiliser la clé de passerelle initialisée. Cette séparation est tout l’intérêt de placer une passerelle devant le fournisseur du modèle.
Test de la passerelle
D’abord, vérifiez la santé :Étendre cette passerelle
Cette passerelle est intentionnellement petite, mais elle vous offre une base solide. De bonnes prochaines étapes incluent :- Ajouter des budgets par sujet et des limites de dépenses mensuelles.
- Prendre en charge plusieurs fournisseurs en amont derrière la même API compatible OpenAI.
- Stocker les métadonnées des requêtes pour les journaux d’audit tout en gardant la journalisation des prompts désactivée par défaut.
- Ajouter une API d’administration pour créer, révoquer et faire tourner les clés de passerelle.
- Ajouter des listes d’autorisation de modèles par clé API.
- Ajouter Redis ou un autre magasin partagé si vous avez besoin d’une limitation de débit à latence plus faible sur plusieurs instances de passerelle.