Bonjour,
J’ai identifié un problème dans main concernant le suivi de contexte des réponses streaming OpenAI-compatible. Il est reproductible lorsque le provider termine un stream sans fournir de bloc usage — observé avec des appels GPT-only.
Comportement actuel
Le chemin concerné est :
src/server/llm/client.ts
src/server/chat/agent-loop.ts
src/server/session/manager.ts
Dans client.ts, l’usage streaming est initialisé à :
ts
let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
Il n’est remplacé que lorsqu’un chunk contient effectivement chunk.usage :
ts
if (chunk.usage) {
usage = {
promptTokens: chunk.usage.prompt_tokens,
completionTokens: chunk.usage.completion_tokens,
totalTokens: chunk.usage.total_tokens,
}
}
À la fin du stream, la réponse done retourne toujours cet objet usage, même lorsqu’aucun chunk n’a fourni de métrique. Dans ce cas, l’usage retourné vaut donc artificiellement zéro.
Ensuite, agent-loop.ts appelle inconditionnellement :
ts
sessionManager.setCurrentContextSize(
sessionId,
result.usage.promptTokens,
config.subAgentMetadata?.subAgentId,
)
Enfin, manager.ts persiste ce 0 comme nouvel état effectif via context.state.
Conséquences
Après une réponse streaming sans usage :
Le contexte affiché/persisté tombe à 0, même si le contexte réel est élevé.
Le dernier contexte fiable est perdu.
La vérification de compaction qui suit utilise ce 0.
La compaction ne se déclenche donc pas au moment requis.
La session peut continuer jusqu’au dépassement réel de la fenêtre de contexte.
Exemple : après un contexte confirmé à 350_000 tokens, une réponse GPT streaming sans usage ramène l’état à 0, alors qu’il devrait rester à 350_000 tant qu’aucune métrique fiable ne remplace cette valeur.
C’est particulièrement problématique avec de grandes fenêtres, par exemple GPT‑5.6 Terra configuré autour de 1M tokens : la perte du suivi local empêche la compaction préventive et augmente le risque de dépassement.
Correctif proposé
Le problème de fond est que 0 et « métrique absente » sont actuellement indiscernables. Je propose de représenter explicitement cette distinction.
- Ajouter un indicateur dans l’usage interne
Dans src/server/llm/types.ts, compléter LLMCompletionResponse.usage :
ts
usage: {
promptTokens: number
completionTokens: number
totalTokens: number
reported: boolean
}
reported est initialisé à false et ne passe à true que lorsqu’un provider retourne réellement chunk.usage.
- Préserver cette information dans client.ts
ts
let usage = {
promptTokens: 0,
completionTokens: 0,
totalTokens: 0,
reported: false,
}
if (chunk.usage) {
usage = {
promptTokens: chunk.usage.prompt_tokens,
completionTokens: chunk.usage.completion_tokens,
totalTokens: chunk.usage.total_tokens,
reported: true,
}
}
Le done final retourne ensuite cet objet sans convertir une absence de métrique en une mesure à zéro.
- Ne mettre à jour le contexte que lorsqu’il est rapporté
Dans agent-loop.ts :
ts
if (result.usage.reported) {
sessionManager.setCurrentContextSize(
sessionId,
result.usage.promptTokens,
config.subAgentMetadata?.subAgentId,
)
} else {
logger.warn('LLM stream completed without usage metrics', {
sessionId,
model: llmClient.getModel(),
providerId: session.providerId,
})
}
Ainsi, une absence de métrique conserve le dernier context.state fiable au lieu de l’écraser avec zéro.
La vérification de compaction doit également s’appuyer sur cet état conservé, et ne jamais déclencher — ou supprimer une compaction nécessaire — en raison d’une métrique absente.
Pourquoi ne pas simplement ignorer les zéros ?
Un compteur à zéro peut être une valeur réellement renvoyée par un provider, un mock ou un cas limite. L’absence de métrique est une information différente ; elle doit donc être représentée explicitement par un indicateur typé tel que reported.
Tests de régression suggérés
Stream sans usage après un contexte connu
Initialiser la session avec currentTokens = 350_000.
Simuler une réponse streaming valide sans aucun chunk.usage.
Vérifier que setCurrentContextSize n’est pas appelé.
Vérifier que currentTokens reste à 350_000.
Vérifier qu’un avertissement structuré est journalisé, sans contenu conversationnel.
Vérifier que la logique de compaction n’évalue pas un contexte à zéro.
Stream avec usage
Simuler une réponse contenant prompt_tokens, completion_tokens et total_tokens.
Vérifier que reported === true.
Vérifier que setCurrentContextSize est appelé avec prompt_tokens.
Vérifier que la mise à jour du contexte et la compaction conservent le comportement actuel.
Test unitaire du client
Sans chunk.usage : usage.reported === false.
Avec chunk.usage : usage.reported === true et les compteurs sont correctement mappés.
Merci.
Bonjour,
J’ai identifié un problème dans main concernant le suivi de contexte des réponses streaming OpenAI-compatible. Il est reproductible lorsque le provider termine un stream sans fournir de bloc usage — observé avec des appels GPT-only.
Comportement actuel
Le chemin concerné est :
src/server/llm/client.ts
src/server/chat/agent-loop.ts
src/server/session/manager.ts
Dans client.ts, l’usage streaming est initialisé à :
ts
let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
Il n’est remplacé que lorsqu’un chunk contient effectivement chunk.usage :
ts
if (chunk.usage) {
usage = {
}
}
À la fin du stream, la réponse done retourne toujours cet objet usage, même lorsqu’aucun chunk n’a fourni de métrique. Dans ce cas, l’usage retourné vaut donc artificiellement zéro.
Ensuite, agent-loop.ts appelle inconditionnellement :
ts
sessionManager.setCurrentContextSize(
sessionId,
result.usage.promptTokens,
config.subAgentMetadata?.subAgentId,
)
Enfin, manager.ts persiste ce 0 comme nouvel état effectif via context.state.
Conséquences
Après une réponse streaming sans usage :
Le contexte affiché/persisté tombe à 0, même si le contexte réel est élevé.
Le dernier contexte fiable est perdu.
La vérification de compaction qui suit utilise ce 0.
La compaction ne se déclenche donc pas au moment requis.
La session peut continuer jusqu’au dépassement réel de la fenêtre de contexte.
Exemple : après un contexte confirmé à 350_000 tokens, une réponse GPT streaming sans usage ramène l’état à 0, alors qu’il devrait rester à 350_000 tant qu’aucune métrique fiable ne remplace cette valeur.
C’est particulièrement problématique avec de grandes fenêtres, par exemple GPT‑5.6 Terra configuré autour de 1M tokens : la perte du suivi local empêche la compaction préventive et augmente le risque de dépassement.
Correctif proposé
Le problème de fond est que 0 et « métrique absente » sont actuellement indiscernables. Je propose de représenter explicitement cette distinction.
Dans src/server/llm/types.ts, compléter LLMCompletionResponse.usage :
ts
usage: {
promptTokens: number
completionTokens: number
totalTokens: number
reported: boolean
}
reported est initialisé à false et ne passe à true que lorsqu’un provider retourne réellement chunk.usage.
ts
let usage = {
promptTokens: 0,
completionTokens: 0,
totalTokens: 0,
reported: false,
}
if (chunk.usage) {
usage = {
}
}
Le done final retourne ensuite cet objet sans convertir une absence de métrique en une mesure à zéro.
Dans agent-loop.ts :
ts
if (result.usage.reported) {
sessionManager.setCurrentContextSize(
)
} else {
logger.warn('LLM stream completed without usage metrics', {
})
}
Ainsi, une absence de métrique conserve le dernier context.state fiable au lieu de l’écraser avec zéro.
La vérification de compaction doit également s’appuyer sur cet état conservé, et ne jamais déclencher — ou supprimer une compaction nécessaire — en raison d’une métrique absente.
Pourquoi ne pas simplement ignorer les zéros ?
Un compteur à zéro peut être une valeur réellement renvoyée par un provider, un mock ou un cas limite. L’absence de métrique est une information différente ; elle doit donc être représentée explicitement par un indicateur typé tel que reported.
Tests de régression suggérés
Stream sans usage après un contexte connu
Initialiser la session avec currentTokens = 350_000.
Simuler une réponse streaming valide sans aucun chunk.usage.
Vérifier que setCurrentContextSize n’est pas appelé.
Vérifier que currentTokens reste à 350_000.
Vérifier qu’un avertissement structuré est journalisé, sans contenu conversationnel.
Vérifier que la logique de compaction n’évalue pas un contexte à zéro.
Stream avec usage
Simuler une réponse contenant prompt_tokens, completion_tokens et total_tokens.
Vérifier que reported === true.
Vérifier que setCurrentContextSize est appelé avec prompt_tokens.
Vérifier que la mise à jour du contexte et la compaction conservent le comportement actuel.
Test unitaire du client
Sans chunk.usage : usage.reported === false.
Avec chunk.usage : usage.reported === true et les compteurs sont correctement mappés.
Merci.