flexway
FRENParler à un expert

Servir l'inférence

GuideExploitationDepuis la version 0.1.0

Markdown

Sur cette page

Guide d'exploitation : configurer, déployer et observer la passerelle. Ce que fait la capacité côté produit : guide produit ; l'appeler : guide développeur.

Objectif : servir la surface OpenAI-compatible (/v1/chat/completions, /v1/models, /v1/embeddings depuis 1.5.0) par le même plan de données que l'API - même identité, même débit, mêmes politiques, même trace -, routée par politique vers des backends, sous budget en tokens par tenant.

Commande - une section ai:. Elle ne demande aucun contrat : la passerelle déclare elle-même ses opérations (ai.chatCompletions, ai.listModels, ai.embeddings), servies sur chaque hôte.

ai:
  security:                          # les identités acceptées, une suffit - comme `security` au contrat
    - oidc: { scopes: [inference] }
    - api_key: {}                    # une clé liée à ai.chatCompletions (voir Clés d'API)
  sensitive_header: X-Sensitive      # "true" épingle la requête sur sensitive_backend
  default_backend: eu-provider
  sensitive_backend: on-prem         # interne ; injoignable, ou aucun backend interne : refus, jamais un externe
  backends:
    eu-provider:
      url: https://api.provider.example/v1
      api_key: "file:/etc/flexway/secrets/provider-key"   # une référence, relue à chaque appel
      default_model: mistral-small-3.2-24b                # la passerelle choisit le modèle
      external: true
    on-prem:
      url: http://vllm.inference.svc:8000/v1
      default_model: mistral-7b-instruct
  budget:
    store: "${COUNTERS_URL}"         # redis://..., partagé par les répliques
    monthly_tokens: 40000000         # par tenant, 0 pour aucun
    daily_tokens: 2000000
  overrides:                         # la première qui correspond gagne
    - { match: { tenant: acme, agent: batch-summaries }, backend: on-prem }

Quatre identités possibles, chacune une alternative de ai.security :

Alternative L'appelant présente Tenant
trusted_headers Les en-têtes qu'un ingress de maillage a écrits (auth.trusted_headers), crus d'un pair de server.trusted_proxies seulement L'en-tête tenant ; subject nomme l'agent
oidc Un jeton de l'un des émetteurs, avec les portées ou rôles déclarés La claim tenant_claim
api_key Authorization: ApiKey <clé>, la clé liée à ai.chatCompletions Celui de la clé
mtls Un certificat client déclaré, lié à l'opération Celui du certificat

Derrière un ingress de maillage qui a déjà authentifié l'appelant (les noms d'en-têtes sont ceux du déploiement, jamais du produit) :

server:
  trusted_proxies: [10.42.0.0/16]    # l'ingress, et lui seul : de quiconque d'autre, l'en-tête est une forgerie
auth:
  trusted_headers: { tenant: X-Mesh-Tenant, subject: X-Mesh-Agent }
ai:
  security: [ { trusted_headers: {} } ]

Coût, plafond souple et repli

Chaque backend porte son prix par million de tokens ; le budget en euros s'ajoute à celui en tokens - le premier atteint décide (exemple complet : Coût, plafond souple et repli) :

ai:
  backends:
    eu-large:
      # ...
      price: { input_per_mtok: 2.00, output_per_mtok: 6.00 }   # euros par million de tokens
      fallback: [eu-small, on-prem]   # dans l'ordre, avant tout octet renvoyé au client
  budget:
    monthly_eur: 50                   # par tenant, sur le mois calendaire
    soft: { at: 0.8, backend: eu-small }
  overrides:
    - { match: { tenant: acme }, monthly_eur: 500 }
Mécanisme Comportement
Coût Pré-réservé (entrée estimée à la taille du corps / 4, sortie à max_tokens), réconcilié sur l'usage réel au prix du backend qui a servi ; un appel en échec ou refusé rend sa réservation, jetons et euros ; porté par l'audit, la métrique flexway.inference.cost (euros, par tenant et backend)
Plafond souple Au-delà de at x budget : en-tête Flexway-Budget: soft, métrique, journal ; l'appel part sur soft.backend - sauf une requête sensible, qui reste épinglée
Plafond dur Une pré-réservation qui dépasserait le budget en euros (ou en tokens) : 429 budget_exceeded, avant tout appel
Repli Sur erreur de connexion, 5xx, délai ou circuit ouvert : le suivant de fallback, chaque backend une fois ; jamais sur un 4xx (dont le 429 du fournisseur), ni après le premier octet d'un flux, ni vers un externe pour une requête sensible. Réponse marquée Flexway-Degraded: backend-fallback

Un prix est obligatoire pour chaque backend dès qu'un budget en euros existe : un coût que personne ne sait calculer ne se budgète pas. 0 est un prix - celui d'un backend interne.

Vérif :

Appel Réponse
Identité acceptée, budget restant relayé tel que le backend l'a écrit ; le backend reçoit le corps du client avec le modèle de la politique, et sa propre clé - jamais l'identité de l'appelant
stream: true les évènements arrivent au fil de l'eau ; le budget est réconcilié sur l'usage final
Aucune identité, ou une identité sans tenant 401 tenant_required
Budget du tenant épuisé (complétion ou flux) 429 budget_exceeded, avant tout appel au backend
X-Sensitive: true sans backend interne 403 sensitive_no_internal_backend - jamais un backend externe
Backend en panne, sans repli disponible 502 backend_unavailable ; pendant un flux, dernier évènement {"error":{"type":"backend_unavailable"}}
Refus de la passerelle sur /v1/* (portée, débit, route inconnue) enveloppe OpenAI, type OpenAI (permission_error, rate_limit_error...) et code de la passerelle
Tenant au-delà de soft.at de son budget en euros Flexway-Budget: soft ; servi par soft.backend
Backend en panne avec fallback servi par le suivant, Flexway-Degraded: backend-fallback
Politique qui nomme un backend non déclaré refusée au chargement, la clé nommée

Débit : /v1/* suit la chaîne de politiques comme toute route - une politique rate-limit propre, attachée à routes: { "/v1/**": [...] }, la borne à part (exemple 12.17). Derrière un ingress, le seau est celui du tenant et de l'agent qu'il atteste, jamais l'adresse de l'ingress.

Migrer depuis config/policy.yaml - plus lu depuis 0.1.0 : sans FLEXWAY_CONFIG, la passerelle refuse de démarrer. La correspondance, à appliquer une fois à la main :

policy.yaml flexway.yaml
default_backend, backends.<n>.default_model, external ai.default_backend, ai.backends.<n>.*
backends.<n>.base_url_env ai.backends.<n>.url - l'URL elle-même
backends.<n>.api_key_env: VAR ai.backends.<n>.api_key: "${VAR}" ou file:/...
pii_guard.sensitive_backend ai.sensitive_backend
budget.*_tokens_default ai.budget.*_tokens ; REDIS_URL -> ai.budget.store
overrides ai.overrides, même forme
En-têtes injectés par l'ingress auth.trusted_headers + server.trusted_proxies + ai.security: [ { trusted_headers: {} } ]

Plusieurs backends : images, embeddings, relais et modèles spécialisés

Depuis 1.5.0, un backend déclare où il est (kind), ce qu'il sert (capabilities), les modèles qu'un appelant peut nommer (models), comment la passerelle s'authentifie (auth) et qui mesure le coût (metering). Une configuration écrite avant reste valide telle quelle : kind: openai, capabilities: [chat], api_key vaut auth: { scheme: bearer }, metering: local.

Les trois genres :

ai:
  default_backend: platform
  sensitive_backend: local-vision
  backends:
    platform:                          # une autre Flexway : elle mesure, elle plafonne
      kind: relay
      url: https://llm.platform.example/v1
      auth: { scheme: token_file, path: /var/run/flexway/peer-token }   # relu à chaque appel
      identity_headers: { tenant: X-Tenant, agent: X-Agent, sensitive: X-Sensitive }
      metering: upstream
      external: false                  # l'exploitant le déclare : le sensible peut y aller
      capabilities: [chat, vision, embeddings]
    provider-direct:                   # un fournisseur en direct : la clé est ici
      kind: openai
      url: https://api.provider.example/v1
      auth: { scheme: header, header: X-Api-Key, key: "${PROVIDER_KEY}" }
      external: true
      capabilities: [chat, vision, embeddings]
      models: [model-a, model-vision-b, embed-c]   # servis s'ils sont demandés, sinon default_model
      default_model: model-a
      price: { input_per_mtok: 0.15, output_per_mtok: 0.35 }
    local-vision:                      # un modèle du cluster
      url: http://vision.models.svc:8080/v1
      default_model: vision-small
      auth: { scheme: none }
      capabilities: [vision]           # une image ; le texte aussi
    faces:                             # un modèle spécialisé : l'amont d'un contrat gRPC
      kind: grpc
      upstream: models                 # déclaré dans upstreams:, protocol: grpc
      capabilities: [vision]
  overrides:
    - { match: { agent: screen-reader }, backend: provider-direct }
    - { match: { capability: embeddings }, backend: provider-direct }   # route seulement, jamais un budget
  vision:
    max_image_bytes: 8MiB              # une image, décodée (défaut)
    allowed_origins: ["https://cdn.example"]   # sinon images en ligne (data:) seulement
Clé Valeurs Effet
kind openai (défaut), relay, grpc Une API compatible OpenAI ; une autre Flexway ; l'amont d'un modèle spécialisé gRPC, jamais une cible de /v1/*
capabilities chat (défaut), vision, embeddings ; model (grpc) Une image ne va qu'à un backend vision, des embeddings qu'à un backend embeddings, sinon 400 capability_unavailable
models liste Le modèle demandé est servi s'il y figure, sinon default_model ; l'audit garde le modèle servi. Un relay sans default_model transmet le modèle demandé
auth.scheme none, bearer (key), header (header, key), token_file (path) La clé est une référence (${VAR}, file:), relue à chaque appel ; token_file relit un jeton court (identité de charge de travail)
metering local (défaut), upstream Voir ci-dessous
external booléen Une requête sensible n'y va jamais ; un relay est externe sauf external: false
identity_headers tenant (obligatoire), agent, sensitive Les noms sous lesquels un relay reçoit l'identité de l'appel - ceux que l'autre Flexway déclare dans auth.trusted_headers et ai.sensitive_header
tls comme un amont Le mTLS vers un relay
match.capability (dérogation) chat, vision, embeddings Choisit le backend des appels de cette capacité, après la dérogation du tenant et de l'agent ; jamais un budget

Mesure - qui compte le coût :

metering Jetons Euros Refus de l'amont Séries
local Réservés, réconciliés sur l'usage Au prix déclaré Un 4xx du fournisseur : 502 backend_unavailable metered_by="local"
upstream Plafond local toujours appliqué, sur l'usage que l'amont rend Au prix déclaré s'il y en a un ; sans prix, l'amont fait foi Relayé tel quel, 429 budget_exceeded compris metered_by="upstream"

Une vue de consommation n'additionne que metered_by!="upstream" - jamais ="local" : une série d'avant 1.5.0 n'a pas l'étiquette -, un appel relayé étant compté local par la Flexway qui le paie. La console montre la part de l'amont à côté, sous « Mesuré en amont » (Lire avec la console). flexway validate avertit d'un relay mesuré localement.

Côté plateforme - la Flexway qui reçoit le relais croit les en-têtes d'identité de ce pair seulement :

server:
  trusted_proxies: [10.42.0.0/16]      # les Flexway des produits
auth:
  trusted_headers: { tenant: X-Tenant, subject: X-Agent }
ai:
  security: [ { trusted_headers: {} } ]
  sensitive_header: X-Sensitive

Image et embeddings :

# une image en ligne, vers un backend vision
curl -s http://localhost:8080/v1/chat/completions -H "Authorization: Bearer $TOKEN" \
  -d '{"model":"model-vision-b","messages":[{"role":"user","content":[{"type":"text","text":"Que montre cet écran ?"},{"type":"image_url","image_url":{"url":"data:image/png;base64,iVBORw0..."}}]}]}'
# des embeddings
curl -s http://localhost:8080/v1/embeddings -H "Authorization: Bearer $TOKEN" -d '{"model":"embed-c","input":["un","deux"]}'

Modèle spécialisé gRPC - une méthode du contrat du produit, mesurée par l'option ai :

rpc Classify(Image) returns (Labels) {
  option (flexway.v1.operation) = { ai: { unit: "call", price_per_1k: 0.02, capability: "vision" } };
}

Chaque appel réserve son prix dans le budget en euros du tenant (ai.budget.monthly_eur et les dérogations), avant l'amont ; unit: byte compte les octets de la requête et de la réponse au passage. Un amont en panne ne coûte rien.

Vérif :

Appel Réponse
Image vers un backend vision servie ; le corps tel quel, le modèle admis
Image sans backend vision choisi, ou embeddings sans backend embeddings 400 capability_unavailable, avant tout backend
Image au-delà de max_image_bytes 413 payload_too_large
Image par URL d'une origine non déclarée, partie input_audio 400 invalid_request
Relais : tenant au-delà du budget de la plateforme 429 budget_exceeded de la plateforme, relayé tel quel
Relais : tenant au-delà du plafond local 429 budget_exceeded, la plateforme jamais appelée
Méthode gRPC mesurée au-delà du budget RESOURCE_EXHAUSTED, raison budget_exceeded ; l'amont jamais appelé
Méthode gRPC mesurée sans backend grpc qui nomme son amont refusée au chargement

Disponible depuis 0.1.0 ; plusieurs backends depuis 1.5.0.

Étapes suivantes