Fleeexdocs

Usage en streaming

stream_options.include_usage, la forme exacte du chunk d'usage final sur le protocole, et pourquoi ses chiffres sont ceux qui vous ont été facturés.

Sans cela, un appelant en streaming n'a aucun moyen de savoir ce qu'il a consommé. Ajoutez stream_options: { include_usage: true } et le flux se termine par un chunk supplémentaire porteur du décompte des tokens.

const stream = await client.chat.completions.create({
  model: "nova-lite",
  messages: [{ role: "user", content: "Écris un haïku." }],
  stream: true,
  stream_options: { include_usage: true },
});
 
for await (const chunk of stream) {
  // Le chunk d'usage final a un tableau choices VIDE : protégez l'index.
  const delta = chunk.choices[0]?.delta?.content;
  if (delta) process.stdout.write(delta);
 
  if (chunk.usage) {
    console.log("\nfacturé :", chunk.usage.total_tokens, "tokens");
  }
}

Le protocole, exactement

La forme est celle d'OpenAI, et c'est important : le chunk à choices vide est la seule forme de chunk qu'un client ne rencontre qu'à la toute fin d'un flux.

  1. Chaque chunk porteur d'un choix porte aussi usage: null. Le champ est toujours déclaré, donc chunk.usage vaut null, jamais undefined.
  2. Après le chunk de raison d'arrêt, un chunk supplémentaire avec un tableau choices vide et un usage renseigné (prompt_tokens, completion_tokens, total_tokens).
  3. Puis data: [DONE].
la fin du flux
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":null}
 
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[],"usage":{"prompt_tokens":14,"completion_tokens":21,"total_tokens":35}}
 
data: [DONE]

Sans l'option, le protocole ne porte aucune clé usage, à l'octet près comme un flux antérieur à l'existence de l'option. Et stream_options sans stream: true est un 400 : c'est le seul paramètre honoré qui n'existe que dans le protocole de streaming.

Les chiffres sont ceux qui vous ont été facturés

Ils sont relus depuis l'événement d'usage enregistré, les décomptes sur lesquels la facturation a été réglée, jamais une nouvelle estimation. Cela fige l'ordre : le règlement d'abord, le chunk ensuite. Un chunk écrit avant le règlement pourrait annoncer un chiffre que les comptes ne confirment jamais, sans que vous ayez le moyen de le savoir.

Deux conséquences à intégrer dans votre conception :

  • Pas de facturation enregistrée, pas de chunk d'usage. Si rien n'était facturable, si la réservation avait disparu au moment du règlement, ou si le règlement a échoué, le flux se termine par le chunk d'arrêt et [DONE], sans rien d'autre. Y publier les chiffres bruts du fournisseur reviendrait à vous annoncer des tokens que personne ne vous a facturés.
  • Un flux coupé annonce ce qui lui a été facturé. Si le flux est arrêté avant terme, parce que le plafond réservé ou le plafond d'horloge a été atteint, finish_reason vaut length et les décomptes sont ceux inscrits sur l'événement, pas des chiffres du fournisseur qui ne sont jamais arrivés.

Ce chunk est au mieux-effort, comme en amont : OpenAI documente la même réserve, « si le flux est interrompu ou annulé, il se peut que vous ne receviez pas le chunk d'usage final ». Traitez-le donc comme la réponse pratique, pas comme l'autorité.

Il n'y a pas d'argent sur le protocole

usage n'a pas de champ de coût, et n'en aura pas. C'est l'objet que chaque SDK désérialise dans un modèle typé généré, et un décodeur strict échoue sur une propriété inconnue : c'est précisément la garantie de compatibilité que cette fonctionnalité doit tenir. Le flux est aussi structurellement le mauvais endroit pour un montant, puisque ce chunk peut légitimement ne jamais arriver, alors qu'une facturation fait autorité.

Les tokens annoncés ici sont ceux qui ont été facturés. Le montant, lui, vit là où il est auditable : getBalance() et getUsageSummary().