diff --git a/changelog.d/features/14795-call-content-provenance.md b/changelog.d/features/14795-call-content-provenance.md new file mode 100644 index 00000000000..d9cea0d3241 --- /dev/null +++ b/changelog.d/features/14795-call-content-provenance.md @@ -0,0 +1 @@ +- **feat(calls):** record rendered-content presence and usage provenance per request in the call log ([#14795](https://github.com/diegosouzapw/OmniRoute/pull/14795)) — thanks @maxmad64bis diff --git a/open-sse/handlers/chatCore/attemptLogging.ts b/open-sse/handlers/chatCore/attemptLogging.ts index 08b355c187f..fb9c5de1e8d 100644 --- a/open-sse/handlers/chatCore/attemptLogging.ts +++ b/open-sse/handlers/chatCore/attemptLogging.ts @@ -240,6 +240,7 @@ export function extractResponsesId(sourceFormat: unknown, clientResponse: unknow export type PersistAttemptLogsArgs = { status: number; tokens?: unknown; + usageEstimated?: boolean | null; responseBody?: unknown; error?: string | null; providerRequest?: unknown; @@ -559,6 +560,9 @@ export function persistAttemptLogs(args: PersistAttemptLogsArgs, ctx: PersistAtt connectionId: finalConnectionId || undefined, duration: Date.now() - startTime, tokens: tokens || {}, + // Estimated-token flag, computed here where tokens still carry the marker + // (it does not survive spreads or JSON round-trips to the sink). + usageEstimated: args.usageEstimated ?? (isEstimatedUsage(tokens) ? true : null), // Encrypted-reasoning observation: stream-side flag plus duration, and // the two request bodies so the sink can read effort values // (requested from the client body, upstream from the post-strip body). diff --git a/src/i18n/messages/am.json b/src/i18n/messages/am.json index 76a082f1244..0e24eea2464 100644 --- a/src/i18n/messages/am.json +++ b/src/i18n/messages/am.json @@ -11474,6 +11474,10 @@ "requestedModel": "የተጠየቀው ሞዴል", "requestProtocol": "የጥያቄ ፕሮቶኮል", "cacheSource": "የመሸጎጫ ምንጭ", + "contentProvenance": "ምላሽ", + "contentEmpty": "ባዶ ምላሽ", + "usageAbsent": "የቶከን ብዛት አልተዘገበም", + "usageEstimated": "የቶከን ብዛት ተገምቷል", "modelPinning": "ሞዴልን መቆለፍ", "activeModelPinning": "ንቁ — ሞዴሉ በክፍለ ጊዜ መቆለፍ ተመርጧል", "rotated": "ተዘዋውሯል፦ {initial} -> {final}", diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index a2ad9a0bb3f..f0a5c367bcd 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -11474,6 +11474,10 @@ "requestedModel": "النموذج المطلوب", "requestProtocol": "بروتوكول المتطلبات", "cacheSource": "مصدر التخزين المؤقت", + "contentProvenance": "الرد", + "contentEmpty": "رد فارغ", + "usageAbsent": "لم يتم الإبلاغ عن عدد الرموز", + "usageEstimated": "عدد الرموز مُقدَّر", "modelPinning": "تثبيت النموذج", "activeModelPinning": "نشط — نموذج تم اختياره عبر تثبيت الجلسة", "rotated": "تم التدوير: {initial} -> {final}", diff --git a/src/i18n/messages/az.json b/src/i18n/messages/az.json index d4453d80b93..9e3206bac82 100644 --- a/src/i18n/messages/az.json +++ b/src/i18n/messages/az.json @@ -11474,6 +11474,10 @@ "requestedModel": "Tələb Olunan Model", "requestProtocol": "Req Protokolu", "cacheSource": "Keş Mənbəyi", + "contentProvenance": "Cavab", + "contentEmpty": "Boş cavab", + "usageAbsent": "Token sayı bildirilməyib", + "usageEstimated": "Token sayı təxmini", "modelPinning": "Modelin sabitlənməsi", "activeModelPinning": "Aktiv — sessiya sabitlənməsi vasitəsilə seçilmiş model", "rotated": "Dönmüşdür: {initial} -> {final}", diff --git a/src/i18n/messages/bg.json b/src/i18n/messages/bg.json index 4221e7d29c7..35bce1550a6 100644 --- a/src/i18n/messages/bg.json +++ b/src/i18n/messages/bg.json @@ -11474,6 +11474,10 @@ "requestedModel": "Заявеният Модел", "requestProtocol": "Протокол на заявката", "cacheSource": "Кеш Източник", + "contentProvenance": "Отговор", + "contentEmpty": "Празен отговор", + "usageAbsent": "Броят токени не е докладван", + "usageEstimated": "Броят токени е приблизителен", "modelPinning": "Закрепване на модела", "activeModelPinning": "Активен — модел, избран чрез фиксиране на сесията", "rotated": "Преместено: {initial} -> {final}", diff --git a/src/i18n/messages/bn.json b/src/i18n/messages/bn.json index 253f8e130f3..bf7e81db93c 100644 --- a/src/i18n/messages/bn.json +++ b/src/i18n/messages/bn.json @@ -11474,6 +11474,10 @@ "requestedModel": "অনুরোধ করা মডেল", "requestProtocol": "রেক প্রোটোকল", "cacheSource": "ক্যাশ সোর্স", + "contentProvenance": "উত্তর", + "contentEmpty": "খালি উত্তর", + "usageAbsent": "টোকেন সংখ্যা জানানো হয়নি", + "usageEstimated": "টোকেন সংখ্যা আনুমানিক", "modelPinning": "মডেল পিনিং", "activeModelPinning": "সক্রিয় — সেশন পিনিংয়ের মাধ্যমে মডেল নির্বাচিত", "rotated": "ঘুরানো: {initial} -> {final}", diff --git a/src/i18n/messages/bs.json b/src/i18n/messages/bs.json index e97bc0b99c9..3c8cd748cb8 100644 --- a/src/i18n/messages/bs.json +++ b/src/i18n/messages/bs.json @@ -11463,6 +11463,10 @@ "requestedModel": "Zatraženi model", "requestProtocol": "Req protokol", "cacheSource": "Izvor keša", + "contentProvenance": "Odgovor", + "contentEmpty": "Prazan odgovor", + "usageAbsent": "Broj tokena nije prijavljen", + "usageEstimated": "Broj tokena je procijenjen", "modelPinning": "Fiksiranje modela", "activeModelPinning": "Aktivno — model odabran putem fiksiranja sesije", "rotated": "Rotirano: {initial} -> {final}", diff --git a/src/i18n/messages/cs.json b/src/i18n/messages/cs.json index e9a3b8a3598..d338e2d92ed 100644 --- a/src/i18n/messages/cs.json +++ b/src/i18n/messages/cs.json @@ -11474,6 +11474,10 @@ "requestedModel": "Požadovaný model", "requestProtocol": "Protokol požadavku", "cacheSource": "Cache Zdroj", + "contentProvenance": "Odpověď", + "contentEmpty": "Prázdná odpověď", + "usageAbsent": "Počet tokenů nebyl nahlášen", + "usageEstimated": "Počet tokenů je odhadnutý", "modelPinning": "Zamykání Modelu", "activeModelPinning": "Aktivní — model vybraný pomocí připnutí relace", "rotated": "Otočeno: {initial} -> {final}", diff --git a/src/i18n/messages/da.json b/src/i18n/messages/da.json index 2e7e1c91c72..52a52c8c3bf 100644 --- a/src/i18n/messages/da.json +++ b/src/i18n/messages/da.json @@ -11474,6 +11474,10 @@ "requestedModel": "Anmodet Model", "requestProtocol": "Req Protokol", "cacheSource": "Cache Kilde", + "contentProvenance": "Svar", + "contentEmpty": "Tomt svar", + "usageAbsent": "Antal tokens ikke rapporteret", + "usageEstimated": "Antal tokens anslået", "modelPinning": "Modelbinding", "activeModelPinning": "Aktiv — model valgt via sessionsfastgørelse", "rotated": "Roteret: {initial} -> {final}", diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index 350767632d2..f2966302d2c 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -11474,6 +11474,10 @@ "requestedModel": "Angefordertes Modell", "requestProtocol": "Req-Protokoll", "cacheSource": "Cache-Quelle", + "contentProvenance": "Antwort", + "contentEmpty": "Leere Antwort", + "usageAbsent": "Token-Anzahl nicht gemeldet", + "usageEstimated": "Token-Anzahl geschätzt", "modelPinning": "Modell-Pinning", "activeModelPinning": "Aktiv — Modell über Sitzungs-Pinning ausgewählt", "rotated": "Rotiert: {initial} -> {final}", diff --git a/src/i18n/messages/el.json b/src/i18n/messages/el.json index 6a96ccdffc2..67b61a2e269 100644 --- a/src/i18n/messages/el.json +++ b/src/i18n/messages/el.json @@ -11474,6 +11474,10 @@ "requestedModel": "Μοντέλο που ζητήθηκε", "requestProtocol": "Πρωτόκολλο αιτήματος", "cacheSource": "Πηγή κρυφής μνήμης", + "contentProvenance": "Απάντηση", + "contentEmpty": "Κενή απάντηση", + "usageAbsent": "Το πλήθος tokens δεν αναφέρθηκε", + "usageEstimated": "Το πλήθος tokens εκτιμήθηκε", "modelPinning": "Δέσμευση μοντέλου", "activeModelPinning": "Ενεργό — το μοντέλο επιλέχθηκε μέσω δέσμευσης συνεδρίας", "rotated": "Εναλλαγή: {initial} -> {final}", diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 7d6a0b69087..485627f4375 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -11474,6 +11474,10 @@ "requestedModel": "Requested Model", "requestProtocol": "Req Protocol", "cacheSource": "Cache Source", + "contentProvenance": "Reply", + "contentEmpty": "Empty reply", + "usageAbsent": "Token count not reported", + "usageEstimated": "Token count estimated", "modelPinning": "Model Pinning", "activeModelPinning": "Active — model selected via session pinning", "rotated": "Rotated: {initial} -> {final}", diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index de607d6297c..4869462b60d 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -11474,6 +11474,10 @@ "requestedModel": "Modelo Solicitado", "requestProtocol": "Protocolo Req", "cacheSource": "Fuente de Caché", + "contentProvenance": "Respuesta", + "contentEmpty": "Respuesta vacía", + "usageAbsent": "Recuento de tokens no informado", + "usageEstimated": "Recuento de tokens estimado", "modelPinning": "Fijación de Modelo", "activeModelPinning": "Activo — modelo seleccionado a través de la fijación de sesión", "rotated": "Rotado: {initial} -> {final}", diff --git a/src/i18n/messages/et.json b/src/i18n/messages/et.json index 18c4669f15d..4f288e09ba6 100644 --- a/src/i18n/messages/et.json +++ b/src/i18n/messages/et.json @@ -11474,6 +11474,10 @@ "requestedModel": "Taotletud mudel", "requestProtocol": "Päringu protokoll", "cacheSource": "Vahemälu allikas", + "contentProvenance": "Vastus", + "contentEmpty": "Tühi vastus", + "usageAbsent": "Tokenite arvu ei teatatud", + "usageEstimated": "Tokenite arv on hinnanguline", "modelPinning": "Mudeli sidumine", "activeModelPinning": "Aktiivne — mudel valiti seansi sidumise kaudu", "rotated": "Vahetatud: {initial} -> {final}", diff --git a/src/i18n/messages/fa.json b/src/i18n/messages/fa.json index 65557229d76..625fd7b6b55 100644 --- a/src/i18n/messages/fa.json +++ b/src/i18n/messages/fa.json @@ -11474,6 +11474,10 @@ "requestedModel": "مدل درخواست شده", "requestProtocol": "پروتکل Req", "cacheSource": "کش منبع", + "contentProvenance": "پاسخ", + "contentEmpty": "پاسخ خالی", + "usageAbsent": "تعداد توکن گزارش نشده است", + "usageEstimated": "تعداد توکن تخمینی است", "modelPinning": "پین کردن مدل", "activeModelPinning": "فعال — مدل انتخاب شده از طریق پین کردن جلسه", "rotated": "چرخش: {initial} -> {final}", diff --git a/src/i18n/messages/fi.json b/src/i18n/messages/fi.json index ebb5d907b6d..0bb4b3a9874 100644 --- a/src/i18n/messages/fi.json +++ b/src/i18n/messages/fi.json @@ -11474,6 +11474,10 @@ "requestedModel": "Pyydetty Malli", "requestProtocol": "Req-protokolla", "cacheSource": "Välimuisti Lähde", + "contentProvenance": "Vastaus", + "contentEmpty": "Tyhjä vastaus", + "usageAbsent": "Tokenien määrää ei raportoitu", + "usageEstimated": "Tokenien määrä on arvio", "modelPinning": "Mallin Kiinnitys", "activeModelPinning": "Aktiivinen — malli valittu istunnon kiinnittämisen kautta", "rotated": "Käännetty: {initial} -> {final}", diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index ea9c0917a88..dcb58c30396 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -11474,6 +11474,10 @@ "requestedModel": "Modèle demandé", "requestProtocol": "Protocole de requête", "cacheSource": "Source du cache", + "contentProvenance": "Réponse", + "contentEmpty": "Réponse vide", + "usageAbsent": "Nombre de tokens non transmis", + "usageEstimated": "Nombre de tokens estimé", "modelPinning": "Épinglage du modèle", "activeModelPinning": "Actif — modèle sélectionné via l’épinglage de session", "rotated": "Rotation : {initial} -> {final}", diff --git a/src/i18n/messages/ga.json b/src/i18n/messages/ga.json index b30219c3e5f..36ac1662b59 100644 --- a/src/i18n/messages/ga.json +++ b/src/i18n/messages/ga.json @@ -11474,6 +11474,10 @@ "requestedModel": "Samhail iarrtha", "requestProtocol": "Prótacal iarratais", "cacheSource": "Foinse Taisce", + "contentProvenance": "Freagra", + "contentEmpty": "Freagra folamh", + "usageAbsent": "Níor tuairiscíodh líon na dtocán", + "usageEstimated": "Líon na dtocán measta", "modelPinning": "Pionnáil Mhúnla", "activeModelPinning": "Gníomhach — múnla roghnaithe trí phionnáil seisiúin", "rotated": "Rothlaithe: {initial} -> {final}", diff --git a/src/i18n/messages/gu.json b/src/i18n/messages/gu.json index 4da4434a211..73f96f9e7bd 100644 --- a/src/i18n/messages/gu.json +++ b/src/i18n/messages/gu.json @@ -11474,6 +11474,10 @@ "requestedModel": "વિનંતી કરેલ મોડેલ", "requestProtocol": "આવશ્યક પ્રોટોકોલ", "cacheSource": "કેશ સોર્સ", + "contentProvenance": "જવાબ", + "contentEmpty": "ખાલી જવાબ", + "usageAbsent": "ટોકન સંખ્યા જણાવાઈ નથી", + "usageEstimated": "ટોકન સંખ્યા અંદાજિત", "modelPinning": "મોડલ પિનિંગ", "activeModelPinning": "સક્રિય — સત્ર પિનિંગ દ્વારા પસંદ કરેલ મોડેલ", "rotated": "ઘૂમાવ્યું: {initial} -> {final}", diff --git a/src/i18n/messages/ha.json b/src/i18n/messages/ha.json index 7a2f890ff85..739407cbc6d 100644 --- a/src/i18n/messages/ha.json +++ b/src/i18n/messages/ha.json @@ -11474,6 +11474,10 @@ "requestedModel": "Samfurin da Aka Buƙata", "requestProtocol": "Ka’idar Buƙata", "cacheSource": "Tushen Ma’ajiya", + "contentProvenance": "Amsa", + "contentEmpty": "Amsa mara komai", + "usageAbsent": "Ba a bayar da adadin token ba", + "usageEstimated": "An kiyasta adadin token", "modelPinning": "Kafewar Samfuri", "activeModelPinning": "Yana aiki — an zaɓi samfurin ta hanyar kafewar zaman", "rotated": "An sauya: {initial} -> {final}", diff --git a/src/i18n/messages/he.json b/src/i18n/messages/he.json index 8a4e07df0dd..9f5b474ab38 100644 --- a/src/i18n/messages/he.json +++ b/src/i18n/messages/he.json @@ -11474,6 +11474,10 @@ "requestedModel": "מודל שביקשת", "requestProtocol": "פרוטוקול Req", "cacheSource": "מקור מטמון", + "contentProvenance": "תגובה", + "contentEmpty": "תגובה ריקה", + "usageAbsent": "מספר הטוקנים לא דווח", + "usageEstimated": "מספר הטוקנים מוערך", "modelPinning": "הצמדת מודל", "activeModelPinning": "פעיל — מודל שנבחר באמצעות נעילת סשן", "rotated": "סובב: {initial} -> {final}", diff --git a/src/i18n/messages/hi.json b/src/i18n/messages/hi.json index e3eef4b6db6..7f43795eee6 100644 --- a/src/i18n/messages/hi.json +++ b/src/i18n/messages/hi.json @@ -11474,6 +11474,10 @@ "requestedModel": "अनुरोधित मॉडल", "requestProtocol": "अनुरोध प्रोटोकॉल", "cacheSource": "कैश स्रोत", + "contentProvenance": "उत्तर", + "contentEmpty": "खाली उत्तर", + "usageAbsent": "टोकन संख्या रिपोर्ट नहीं की गई", + "usageEstimated": "टोकन संख्या अनुमानित", "modelPinning": "मॉडल पिनिंग", "activeModelPinning": "सक्रिय — सत्र पिनिंग के माध्यम से मॉडल चुना गया", "rotated": "घुमाया गया: {initial} -> {final}", diff --git a/src/i18n/messages/hr.json b/src/i18n/messages/hr.json index 0aaf6d0b13d..238a1e73da6 100644 --- a/src/i18n/messages/hr.json +++ b/src/i18n/messages/hr.json @@ -11474,6 +11474,10 @@ "requestedModel": "Zatraženi model", "requestProtocol": "Protokol zahtjeva", "cacheSource": "Izvor predmemorije", + "contentProvenance": "Odgovor", + "contentEmpty": "Prazan odgovor", + "usageAbsent": "Broj tokena nije prijavljen", + "usageEstimated": "Broj tokena je procijenjen", "modelPinning": "Prikvačivanje modela", "activeModelPinning": "Aktivno — model odabran putem prikvačivanja sesije", "rotated": "Rotirano: {initial} -> {final}", diff --git a/src/i18n/messages/hu.json b/src/i18n/messages/hu.json index 473eaee72fb..c03c0727201 100644 --- a/src/i18n/messages/hu.json +++ b/src/i18n/messages/hu.json @@ -11474,6 +11474,10 @@ "requestedModel": "Kért Modell", "requestProtocol": "Követelmény Protokoll", "cacheSource": "Gyorsítótár Forrás", + "contentProvenance": "Válasz", + "contentEmpty": "Üres válasz", + "usageAbsent": "A tokenszám nem lett jelentve", + "usageEstimated": "A tokenszám becsült", "modelPinning": "Modell Rögzítése", "activeModelPinning": "Aktív — a modell kiválasztva a munkamenet rögzítésével", "rotated": "Elforgatva: {initial} -> {final}", diff --git a/src/i18n/messages/hy.json b/src/i18n/messages/hy.json index f7d885f35d4..328c4f2ed32 100644 --- a/src/i18n/messages/hy.json +++ b/src/i18n/messages/hy.json @@ -11474,6 +11474,10 @@ "requestedModel": "Պահանջված մոդել", "requestProtocol": "Հարցման արձանագրություն", "cacheSource": "Քեշի աղբյուր", + "contentProvenance": "Պատասխան", + "contentEmpty": "Դատարկ պատասխան", + "usageAbsent": "Թոքենների քանակը չի հաղորդվել", + "usageEstimated": "Թոքենների քանակը գնահատված է", "modelPinning": "Մոդելի ամրագրում", "activeModelPinning": "Ակտիվ — մոդելն ընտրվել է սեսիայի ամրագրման միջոցով", "rotated": "Փոխարկվել է՝ {initial} -> {final}", diff --git a/src/i18n/messages/id.json b/src/i18n/messages/id.json index 193b6e81e66..b034599f1a9 100644 --- a/src/i18n/messages/id.json +++ b/src/i18n/messages/id.json @@ -11474,6 +11474,10 @@ "requestedModel": "Model yang Diminta", "requestProtocol": "Protokol Req", "cacheSource": "Sumber Cache", + "contentProvenance": "Balasan", + "contentEmpty": "Balasan kosong", + "usageAbsent": "Jumlah token tidak dilaporkan", + "usageEstimated": "Jumlah token diperkirakan", "modelPinning": "Penetapan Model", "activeModelPinning": "Aktif — model dipilih melalui penetapan sesi", "rotated": "Diputar: {initial} -> {final}", diff --git a/src/i18n/messages/ig.json b/src/i18n/messages/ig.json index f02686af5b1..20cad416d73 100644 --- a/src/i18n/messages/ig.json +++ b/src/i18n/messages/ig.json @@ -11474,6 +11474,10 @@ "requestedModel": "Ụdị A Rịọrọ", "requestProtocol": "Protokol Arịrịọ", "cacheSource": "Ebe Cache Si", + "contentProvenance": "Nzaghachi", + "contentEmpty": "Nzaghachi efu", + "usageAbsent": "Akọpụtaghị ọnụ ọgụgụ token", + "usageEstimated": "E mere atụmatụ ọnụ ọgụgụ token", "modelPinning": "Ntụdo Ụdị", "activeModelPinning": "Na-arụ ọrụ — ahọpụtara ụdị site na ntụdo nnọkọ", "rotated": "Agbanweela: {initial} -> {final}", diff --git a/src/i18n/messages/it.json b/src/i18n/messages/it.json index d2ae58b6b03..3097b20448d 100644 --- a/src/i18n/messages/it.json +++ b/src/i18n/messages/it.json @@ -11474,6 +11474,10 @@ "requestedModel": "Modello Richiesto", "requestProtocol": "Protocollo Req", "cacheSource": "Cache Sorgente", + "contentProvenance": "Risposta", + "contentEmpty": "Risposta vuota", + "usageAbsent": "Conteggio token non riportato", + "usageEstimated": "Conteggio token stimato", "modelPinning": "Pinning del Modello", "activeModelPinning": "Attivo — modello selezionato tramite il pinning della sessione", "rotated": "Ruotato: {initial} -> {final}", diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index 244253e1da6..521ef38f3a3 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -11474,6 +11474,10 @@ "requestedModel": "要求されたモデル", "requestProtocol": "要求プロトコル", "cacheSource": "キャッシュソース", + "contentProvenance": "応答", + "contentEmpty": "空の応答", + "usageAbsent": "トークン数が報告されていません", + "usageEstimated": "トークン数は推定値です", "modelPinning": "モデルピン留め", "activeModelPinning": "アクティブ — セッションピンニングを介して選択されたモデル", "rotated": "回転: {initial} -> {final}", diff --git a/src/i18n/messages/ka.json b/src/i18n/messages/ka.json index e7818732c22..173bd2f622f 100644 --- a/src/i18n/messages/ka.json +++ b/src/i18n/messages/ka.json @@ -11474,6 +11474,10 @@ "requestedModel": "მოთხოვნილი მოდელი", "requestProtocol": "მოთხოვნის პროტოკოლი", "cacheSource": "ქეშის წყარო", + "contentProvenance": "პასუხი", + "contentEmpty": "ცარიელი პასუხი", + "usageAbsent": "ტოკენების რაოდენობა არ არის მოწოდებული", + "usageEstimated": "ტოკენების რაოდენობა შეფასებულია", "modelPinning": "მოდელის მიბმა", "activeModelPinning": "აქტიური — მოდელი არჩეულია სესიის მიბმის მეშვეობით", "rotated": "როტირებულია: {initial} -> {final}", diff --git a/src/i18n/messages/km.json b/src/i18n/messages/km.json index 9eb1300add2..0aaa10c825c 100644 --- a/src/i18n/messages/km.json +++ b/src/i18n/messages/km.json @@ -11474,6 +11474,10 @@ "requestedModel": "ម៉ូដែលដែលបានស្នើ", "requestProtocol": "ពិធីការសំណើ", "cacheSource": "ប្រភពឃ្លាំងសម្ងាត់", + "contentProvenance": "ការឆ្លើយតប", + "contentEmpty": "ការឆ្លើយតបទទេ", + "usageAbsent": "ចំនួន token មិនត្រូវបានរាយការណ៍", + "usageEstimated": "ចំនួន token ជាការប៉ាន់ស្មាន", "modelPinning": "ការចងម៉ូដែល", "activeModelPinning": "សកម្ម — ម៉ូដែលត្រូវបានជ្រើសរើសតាមរយៈការចងម៉ូដែលតាមសម័យ", "rotated": "បានប្តូរវេន៖ {initial} -> {final}", diff --git a/src/i18n/messages/kn.json b/src/i18n/messages/kn.json index 6124085e96a..9be9eb2576f 100644 --- a/src/i18n/messages/kn.json +++ b/src/i18n/messages/kn.json @@ -11474,6 +11474,10 @@ "requestedModel": "ವಿನಂತಿಸಿದ ಮಾದರಿ", "requestProtocol": "ವಿನಂತಿ ಪ್ರೋಟೋಕಾಲ್", "cacheSource": "ಕ್ಯಾಶ್ ಮೂಲ", + "contentProvenance": "ಉತ್ತರ", + "contentEmpty": "ಖಾಲಿ ಉತ್ತರ", + "usageAbsent": "ಟೋಕನ್ ಸಂಖ್ಯೆ ವರದಿಯಾಗಿಲ್ಲ", + "usageEstimated": "ಟೋಕನ್ ಸಂಖ್ಯೆ ಅಂದಾಜು", "modelPinning": "ಮಾದರಿ ಪಿನ್ನಿಂಗ್", "activeModelPinning": "ಸಕ್ರಿಯ — ಸೆಷನ್ ಪಿನ್ನಿಂಗ್ ಮೂಲಕ ಮಾದರಿಯನ್ನು ಆಯ್ಕೆ ಮಾಡಲಾಗಿದೆ", "rotated": "ಬದಲಾಯಿಸಲಾಗಿದೆ: {initial} -> {final}", diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index 086dd9ca891..258c7a7cf84 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -11474,6 +11474,10 @@ "requestedModel": "요청된 모델", "requestProtocol": "요청 프로토콜", "cacheSource": "캐시 소스", + "contentProvenance": "응답", + "contentEmpty": "빈 응답", + "usageAbsent": "토큰 수가 보고되지 않음", + "usageEstimated": "토큰 수 추정됨", "modelPinning": "모델 고정", "activeModelPinning": "활성 — 세션 고정을 통해 선택된 모델", "rotated": "회전됨: {initial} -> {final}", diff --git a/src/i18n/messages/lt.json b/src/i18n/messages/lt.json index 3c4bd12dd91..04a3b1c8197 100644 --- a/src/i18n/messages/lt.json +++ b/src/i18n/messages/lt.json @@ -11474,6 +11474,10 @@ "requestedModel": "Prašomas modelis", "requestProtocol": "Užklausos protokolas", "cacheSource": "Talpyklos šaltinis", + "contentProvenance": "Atsakymas", + "contentEmpty": "Tuščias atsakymas", + "usageAbsent": "Žetonų skaičius nepateiktas", + "usageEstimated": "Žetonų skaičius įvertintas", "modelPinning": "Modelio prisegimas", "activeModelPinning": "Aktyvus — modelis pasirinktas naudojant seanso prisegimą", "rotated": "Pakeista: {initial} → {final}", diff --git a/src/i18n/messages/lv.json b/src/i18n/messages/lv.json index 24e65455b6d..8d33489d2d7 100644 --- a/src/i18n/messages/lv.json +++ b/src/i18n/messages/lv.json @@ -11474,6 +11474,10 @@ "requestedModel": "Pieprasītais modelis", "requestProtocol": "Pieprasījuma protokols", "cacheSource": "Kešatmiņas avots", + "contentProvenance": "Atbilde", + "contentEmpty": "Tukša atbilde", + "usageAbsent": "Marķieru skaits nav ziņots", + "usageEstimated": "Marķieru skaits ir aplēsts", "modelPinning": "Modeļa piespraušana", "activeModelPinning": "Aktīvs — modelis atlasīts, izmantojot sesijas piespraušanu", "rotated": "Rotēts: {initial} -> {final}", diff --git a/src/i18n/messages/ml.json b/src/i18n/messages/ml.json index 219ee48bb81..52f233e14ce 100644 --- a/src/i18n/messages/ml.json +++ b/src/i18n/messages/ml.json @@ -11474,6 +11474,10 @@ "requestedModel": "അഭ്യർത്ഥിച്ച മോഡൽ", "requestProtocol": "അഭ്യർത്ഥന പ്രോട്ടോക്കോൾ", "cacheSource": "കാഷ് ഉറവിടം", + "contentProvenance": "മറുപടി", + "contentEmpty": "ശൂന്യമായ മറുപടി", + "usageAbsent": "ടോക്കൺ എണ്ണം റിപ്പോർട്ട് ചെയ്തിട്ടില്ല", + "usageEstimated": "ടോക്കൺ എണ്ണം ഏകദേശം", "modelPinning": "മോഡൽ പിന്നിംഗ്", "activeModelPinning": "സജീവം — സെഷൻ പിന്നിംഗ് വഴി മോഡൽ തിരഞ്ഞെടുത്തു", "rotated": "റൊട്ടേറ്റ് ചെയ്തത്: {initial} -> {final}", diff --git a/src/i18n/messages/mr.json b/src/i18n/messages/mr.json index 57c98557f2f..adbee52f0d7 100644 --- a/src/i18n/messages/mr.json +++ b/src/i18n/messages/mr.json @@ -11474,6 +11474,10 @@ "requestedModel": "विनंती केलेला मॉडेल", "requestProtocol": "आवश्यक प्रोटोकॉल", "cacheSource": "कॅश स्रोत", + "contentProvenance": "उत्तर", + "contentEmpty": "रिकामे उत्तर", + "usageAbsent": "टोकन संख्या कळवली नाही", + "usageEstimated": "टोकन संख्या अंदाजे", "modelPinning": "मॉडेल पिनिंग", "activeModelPinning": "सक्रिय — सत्र पिनिंगद्वारे निवडलेला मॉडेल", "rotated": "रोटेटेड: {initial} -> {final}", diff --git a/src/i18n/messages/ms.json b/src/i18n/messages/ms.json index 4e0ede0c3bd..c3441b9791f 100644 --- a/src/i18n/messages/ms.json +++ b/src/i18n/messages/ms.json @@ -11474,6 +11474,10 @@ "requestedModel": "Model Yang Diminta", "requestProtocol": "Protokol Req", "cacheSource": "Sumber Cache", + "contentProvenance": "Balasan", + "contentEmpty": "Balasan kosong", + "usageAbsent": "Bilangan token tidak dilaporkan", + "usageEstimated": "Bilangan token dianggarkan", "modelPinning": "Penetapan Model", "activeModelPinning": "Aktif — model dipilih melalui penetapan sesi", "rotated": "Diputar: {initial} -> {final}", diff --git a/src/i18n/messages/mt.json b/src/i18n/messages/mt.json index 2e7ff94ce8e..6866ed14bd7 100644 --- a/src/i18n/messages/mt.json +++ b/src/i18n/messages/mt.json @@ -11474,6 +11474,10 @@ "requestedModel": "Mudell mitlub", "requestProtocol": "Protokoll tat-talba", "cacheSource": "Sors tal-cache", + "contentProvenance": "Tweġiba", + "contentEmpty": "Tweġiba vojta", + "usageAbsent": "L-għadd tat-tokens ma ġiex irrappurtat", + "usageEstimated": "L-għadd tat-tokens huwa stmat", "modelPinning": "Iffissar tal-Mudell", "activeModelPinning": "Attiv — il-mudell intgħażel permezz tal-iffissar tas-sessjoni", "rotated": "Rotazzjoni: {initial} -> {final}", diff --git a/src/i18n/messages/my.json b/src/i18n/messages/my.json index 9c4f496342b..dace5c6c774 100644 --- a/src/i18n/messages/my.json +++ b/src/i18n/messages/my.json @@ -11474,6 +11474,10 @@ "requestedModel": "တောင်းဆိုထားသော မော်ဒယ်", "requestProtocol": "တောင်းဆိုမှု ပရိုတိုကော", "cacheSource": "Cache ရင်းမြစ်", + "contentProvenance": "အဖြေ", + "contentEmpty": "အဖြေ ဗလာ", + "usageAbsent": "token အရေအတွက် မတင်ပြထားပါ", + "usageEstimated": "token အရေအတွက် ခန့်မှန်းထားသည်", "modelPinning": "မော်ဒယ် ပုံသေသတ်မှတ်မှု", "activeModelPinning": "အသုံးပြုနေသည် — session pinning ဖြင့် မော်ဒယ်ကို ရွေးချယ်ထားသည်", "rotated": "ပြောင်းလဲရွေးချယ်ခဲ့သည်: {initial} -> {final}", diff --git a/src/i18n/messages/ne.json b/src/i18n/messages/ne.json index 48832058255..5ed169fb78f 100644 --- a/src/i18n/messages/ne.json +++ b/src/i18n/messages/ne.json @@ -11474,6 +11474,10 @@ "requestedModel": "अनुरोध गरिएको मोडेल", "requestProtocol": "अनुरोध प्रोटोकल", "cacheSource": "क्यास स्रोत", + "contentProvenance": "जवाफ", + "contentEmpty": "खाली जवाफ", + "usageAbsent": "टोकन संख्या रिपोर्ट गरिएको छैन", + "usageEstimated": "टोकन संख्या अनुमानित", "modelPinning": "मोडेल पिनिङ", "activeModelPinning": "सक्रिय — सत्र पिनिङमार्फत मोडेल चयन गरिएको", "rotated": "रोटेट गरिएको: {initial} -> {final}", diff --git a/src/i18n/messages/nl.json b/src/i18n/messages/nl.json index 8f18815cf45..58ebca4b956 100644 --- a/src/i18n/messages/nl.json +++ b/src/i18n/messages/nl.json @@ -11474,6 +11474,10 @@ "requestedModel": "Aangevraagd Model", "requestProtocol": "Verzoekprotocol", "cacheSource": "Cachebron", + "contentProvenance": "Antwoord", + "contentEmpty": "Leeg antwoord", + "usageAbsent": "Aantal tokens niet gerapporteerd", + "usageEstimated": "Aantal tokens geschat", "modelPinning": "Model vastzetten", "activeModelPinning": "Actief — model geselecteerd via sessiepinnen", "rotated": "Geroteerd: {initial} -> {final}", diff --git a/src/i18n/messages/no.json b/src/i18n/messages/no.json index 7cd1d019426..c6ea8e857fd 100644 --- a/src/i18n/messages/no.json +++ b/src/i18n/messages/no.json @@ -11474,6 +11474,10 @@ "requestedModel": "Forespurt Modell", "requestProtocol": "Req Protokoll", "cacheSource": "Cache Kilde", + "contentProvenance": "Svar", + "contentEmpty": "Tomt svar", + "usageAbsent": "Antall tokens ikke rapportert", + "usageEstimated": "Antall tokens anslått", "modelPinning": "Modellpining", "activeModelPinning": "Aktiv — modell valgt via sesjonsfeste", "rotated": "Rotert: {initial} -> {final}", diff --git a/src/i18n/messages/or.json b/src/i18n/messages/or.json index 2f3e6a28c4e..4eea4caba57 100644 --- a/src/i18n/messages/or.json +++ b/src/i18n/messages/or.json @@ -11474,6 +11474,10 @@ "requestedModel": "ଅନୁରୋଧ କରାଯାଇଥିବା ମଡେଲ୍", "requestProtocol": "ଅନୁରୋଧ ପ୍ରୋଟୋକଲ୍", "cacheSource": "କ୍ୟାଶ୍ ଉତ୍ସ", + "contentProvenance": "ଉତ୍ତର", + "contentEmpty": "ଖାଲି ଉତ୍ତର", + "usageAbsent": "ଟୋକେନ ସଂଖ୍ୟା ରିପୋର୍ଟ ହୋଇନାହିଁ", + "usageEstimated": "ଟୋକେନ ସଂଖ୍ୟା ଆନୁମାନିକ", "modelPinning": "ମଡେଲ୍ ପିନିଂ", "activeModelPinning": "ସକ୍ରିୟ — ସେସନ୍ ପିନିଂ ମାଧ୍ୟମରେ ମଡେଲ୍ ଚୟନ କରାଯାଇଛି", "rotated": "ପରିବର୍ତ୍ତିତ: {initial} -> {final}", diff --git a/src/i18n/messages/pa.json b/src/i18n/messages/pa.json index 8aa10e01351..ca249bec1d8 100644 --- a/src/i18n/messages/pa.json +++ b/src/i18n/messages/pa.json @@ -11474,6 +11474,10 @@ "requestedModel": "ਬੇਨਤੀ ਕੀਤਾ ਗਿਆ ਮਾਡਲ", "requestProtocol": "ਬੇਨਤੀ ਪ੍ਰੋਟੋਕੋਲ", "cacheSource": "ਕੈਸ਼ ਸਰੋਤ", + "contentProvenance": "ਜਵਾਬ", + "contentEmpty": "ਖਾਲੀ ਜਵਾਬ", + "usageAbsent": "ਟੋਕਨ ਗਿਣਤੀ ਦੀ ਰਿਪੋਰਟ ਨਹੀਂ ਕੀਤੀ ਗਈ", + "usageEstimated": "ਟੋਕਨ ਗਿਣਤੀ ਅਨੁਮਾਨਿਤ", "modelPinning": "ਮਾਡਲ ਪਿੰਨਿੰਗ", "activeModelPinning": "ਸਰਗਰਮ — ਸੈਸ਼ਨ ਪਿੰਨਿੰਗ ਰਾਹੀਂ ਮਾਡਲ ਚੁਣਿਆ ਗਿਆ", "rotated": "ਰੋਟੇਟ ਕੀਤਾ ਗਿਆ: {initial} -> {final}", diff --git a/src/i18n/messages/phi.json b/src/i18n/messages/phi.json index 40eaec82f3b..751642c35ce 100644 --- a/src/i18n/messages/phi.json +++ b/src/i18n/messages/phi.json @@ -11474,6 +11474,10 @@ "requestedModel": "Hiniling na Modelo", "requestProtocol": "Protokol ng Request", "cacheSource": "Cache Pinagmulan", + "contentProvenance": "Tugon", + "contentEmpty": "Walang laman na tugon", + "usageAbsent": "Hindi naiulat ang bilang ng token", + "usageEstimated": "Tinantya ang bilang ng token", "modelPinning": "Pagpapa-pin ng Modelo", "activeModelPinning": "Aktibo — modelo na napili sa pamamagitan ng session pinning", "rotated": "Naka-ikot: {initial} -> {final}", diff --git a/src/i18n/messages/pl.json b/src/i18n/messages/pl.json index cb3896d18e1..401507c5890 100644 --- a/src/i18n/messages/pl.json +++ b/src/i18n/messages/pl.json @@ -11474,6 +11474,10 @@ "requestedModel": "Żądany model", "requestProtocol": "Protokół Req", "cacheSource": "Źródło pamięci podręcznej", + "contentProvenance": "Odpowiedź", + "contentEmpty": "Pusta odpowiedź", + "usageAbsent": "Liczba tokenów nie została zgłoszona", + "usageEstimated": "Liczba tokenów szacowana", "modelPinning": "Przypinanie Modelu", "activeModelPinning": "Aktywny — model wybrany za pomocą przypinania sesji", "rotated": "Obrócone: {initial} -> {final}", diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json index 5bf1e785e94..3c20b876712 100644 --- a/src/i18n/messages/pt-BR.json +++ b/src/i18n/messages/pt-BR.json @@ -11474,6 +11474,10 @@ "requestedModel": "Modelo Solicitado", "requestProtocol": "Protocolo Req", "cacheSource": "Fonte de Cache", + "contentProvenance": "Resposta", + "contentEmpty": "Resposta vazia", + "usageAbsent": "Contagem de tokens não informada", + "usageEstimated": "Contagem de tokens estimada", "modelPinning": "Fixação de Modelo", "activeModelPinning": "Ativo — modelo selecionado via fixação de sessão", "rotated": "Rotacionado: {initial} -> {final}", diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index 6458c586eb9..b1923852291 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -11474,6 +11474,10 @@ "requestedModel": "Modelo Solicitado", "requestProtocol": "Protocolo Req", "cacheSource": "Fonte de Cache", + "contentProvenance": "Resposta", + "contentEmpty": "Resposta vazia", + "usageAbsent": "Contagem de tokens não comunicada", + "usageEstimated": "Contagem de tokens estimada", "modelPinning": "Fixação de Modelo", "activeModelPinning": "Ativo — modelo selecionado através do fixador de sessão", "rotated": "Rotacionado: {initial} -> {final}", diff --git a/src/i18n/messages/ro.json b/src/i18n/messages/ro.json index 4c1d0a17b51..a18ae2911ed 100644 --- a/src/i18n/messages/ro.json +++ b/src/i18n/messages/ro.json @@ -11474,6 +11474,10 @@ "requestedModel": "Model Solicitat", "requestProtocol": "Protocol Req", "cacheSource": "Sursă Cache", + "contentProvenance": "Răspuns", + "contentEmpty": "Răspuns gol", + "usageAbsent": "Numărul de tokenuri nu a fost raportat", + "usageEstimated": "Numărul de tokenuri este estimat", "modelPinning": "Fixarea Modelului", "activeModelPinning": "Activ — model selectat prin fixarea sesiunii", "rotated": "Rotit: {initial} -> {final}", diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index 34314431b03..5af54c53c9d 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -11474,6 +11474,10 @@ "requestedModel": "Запрашиваемая Модель", "requestProtocol": "Протокол Req", "cacheSource": "Кэш Источника", + "contentProvenance": "Ответ", + "contentEmpty": "Пустой ответ", + "usageAbsent": "Число токенов не передано", + "usageEstimated": "Число токенов оценено", "modelPinning": "Закрепление Модели", "activeModelPinning": "Активный — модель, выбранная через закрепление сессии", "rotated": "Повернуто: {initial} -> {final}", diff --git a/src/i18n/messages/si.json b/src/i18n/messages/si.json index f88a96ef08e..de8fdae93e5 100644 --- a/src/i18n/messages/si.json +++ b/src/i18n/messages/si.json @@ -11474,6 +11474,10 @@ "requestedModel": "ඉල්ලා ඇති මාදිලිය", "requestProtocol": "ඉල්ලීම් ප්රොටෝකෝලය", "cacheSource": "කෑෂ් මූලාශ්රය", + "contentProvenance": "පිළිතුර", + "contentEmpty": "හිස් පිළිතුර", + "usageAbsent": "ටෝකන ගණන වාර්තා කර නැත", + "usageEstimated": "ටෝකන ගණන ඇස්තමේන්තුගත", "modelPinning": "මාදිලි ඇමිණීම", "activeModelPinning": "සක්රියයි — සැසි ඇමිණීම හරහා මාදිලිය තෝරා ඇත", "rotated": "මාරු කරන ලදී: {initial} -> {final}", diff --git a/src/i18n/messages/sk.json b/src/i18n/messages/sk.json index 5e16bf01cdf..0dc10045bd6 100644 --- a/src/i18n/messages/sk.json +++ b/src/i18n/messages/sk.json @@ -11474,6 +11474,10 @@ "requestedModel": "Požadovaný model", "requestProtocol": "Req Protokol", "cacheSource": "Cache Zdroj", + "contentProvenance": "Odpoveď", + "contentEmpty": "Prázdna odpoveď", + "usageAbsent": "Počet tokenov nebol nahlásený", + "usageEstimated": "Počet tokenov je odhadnutý", "modelPinning": "Pripnutie modelu", "activeModelPinning": "Aktívne — model vybraný pomocou pripevnenia relácie", "rotated": "Otočené: {initial} -> {final}", diff --git a/src/i18n/messages/sl.json b/src/i18n/messages/sl.json index 38a0517c020..0d52f78094c 100644 --- a/src/i18n/messages/sl.json +++ b/src/i18n/messages/sl.json @@ -11474,6 +11474,10 @@ "requestedModel": "Zahtevani model", "requestProtocol": "Protokol zahteve", "cacheSource": "Vir predpomnilnika", + "contentProvenance": "Odgovor", + "contentEmpty": "Prazen odgovor", + "usageAbsent": "Število žetonov ni bilo sporočeno", + "usageEstimated": "Število žetonov je ocenjeno", "modelPinning": "Pripenjanje modela", "activeModelPinning": "Aktivno — model je izbran prek pripenjanja seje", "rotated": "Zamenjano: {initial} -> {final}", diff --git a/src/i18n/messages/sr.json b/src/i18n/messages/sr.json index 228e1e763b4..6e30cafbf75 100644 --- a/src/i18n/messages/sr.json +++ b/src/i18n/messages/sr.json @@ -11474,6 +11474,10 @@ "requestedModel": "Захтевани модел", "requestProtocol": "Протокол захтева", "cacheSource": "Извор кеша", + "contentProvenance": "Одговор", + "contentEmpty": "Празан одговор", + "usageAbsent": "Број токена није пријављен", + "usageEstimated": "Број токена је процењен", "modelPinning": "Фиксирање модела", "activeModelPinning": "Активно — модел је изабран фиксирањем у оквиру сесије", "rotated": "Ротирано: {initial} -> {final}", diff --git a/src/i18n/messages/sv.json b/src/i18n/messages/sv.json index a3bed0e7aed..139bd3e6fc9 100644 --- a/src/i18n/messages/sv.json +++ b/src/i18n/messages/sv.json @@ -11474,6 +11474,10 @@ "requestedModel": "Begärd Modell", "requestProtocol": "Req Protokoll", "cacheSource": "Cache Källa", + "contentProvenance": "Svar", + "contentEmpty": "Tomt svar", + "usageAbsent": "Antal tokens har inte rapporterats", + "usageEstimated": "Antal tokens uppskattat", "modelPinning": "Modellpinning", "activeModelPinning": "Aktiv — modell vald via sessionsfästning", "rotated": "Roterad: {initial} -> {final}", diff --git a/src/i18n/messages/sw.json b/src/i18n/messages/sw.json index 35be6286231..b2c9e87afba 100644 --- a/src/i18n/messages/sw.json +++ b/src/i18n/messages/sw.json @@ -11474,6 +11474,10 @@ "requestedModel": "Mfano Uliyoombwa", "requestProtocol": "Itifaki ya Req", "cacheSource": "Chanzo cha Kumbukumbu", + "contentProvenance": "Jibu", + "contentEmpty": "Jibu tupu", + "usageAbsent": "Idadi ya tokeni haikuripotiwa", + "usageEstimated": "Idadi ya tokeni imekadiriwa", "modelPinning": "Kufunga Mfano", "activeModelPinning": "Hai — mfano umechaguliwa kupitia pini ya kikao", "rotated": "Imepinduliwa: {initial} -> {final}", diff --git a/src/i18n/messages/ta.json b/src/i18n/messages/ta.json index f1c3794fc1a..131ac0063ec 100644 --- a/src/i18n/messages/ta.json +++ b/src/i18n/messages/ta.json @@ -11474,6 +11474,10 @@ "requestedModel": "கோரிய மாடல்", "requestProtocol": "அவசியம் நெறிமுறை", "cacheSource": "கேஷ் மூலத்திடம்", + "contentProvenance": "பதில்", + "contentEmpty": "வெற்று பதில்", + "usageAbsent": "டோக்கன் எண்ணிக்கை தெரிவிக்கப்படவில்லை", + "usageEstimated": "டோக்கன் எண்ணிக்கை மதிப்பிடப்பட்டது", "modelPinning": "மாதிரி பின்வாங்குதல்", "activeModelPinning": "செயலில் — அமர்வு பின்வட்டம் மூலம் தேர்ந்தெடுக்கப்பட்ட மாதிரி", "rotated": "மாற்றப்பட்டது: {initial} -> {final}", diff --git a/src/i18n/messages/te.json b/src/i18n/messages/te.json index b98c4d155b0..cc32318a028 100644 --- a/src/i18n/messages/te.json +++ b/src/i18n/messages/te.json @@ -11474,6 +11474,10 @@ "requestedModel": "అభ్యర్థించిన మోడల్", "requestProtocol": "రిక్వెస్ట్ ప్రోటోకాల్", "cacheSource": "క్యాష్ మూలం", + "contentProvenance": "సమాధానం", + "contentEmpty": "ఖాళీ సమాధానం", + "usageAbsent": "టోకెన్ సంఖ్య నివేదించబడలేదు", + "usageEstimated": "టోకెన్ సంఖ్య అంచనా", "modelPinning": "మోడల్ పిన్నింగ్", "activeModelPinning": "సక్రియ — సెషన్ పిన్నింగ్ ద్వారా ఎంపిక చేసిన మోడల్", "rotated": "మలుపు: {initial} -> {final}", diff --git a/src/i18n/messages/th.json b/src/i18n/messages/th.json index fae0975e0d2..edda8705e4b 100644 --- a/src/i18n/messages/th.json +++ b/src/i18n/messages/th.json @@ -11474,6 +11474,10 @@ "requestedModel": "โมเดลที่ร้องขอ", "requestProtocol": "โปรโตคอล Req", "cacheSource": "แหล่งข้อมูลแคช", + "contentProvenance": "คำตอบ", + "contentEmpty": "คำตอบว่างเปล่า", + "usageAbsent": "ไม่ได้รายงานจำนวนโทเค็น", + "usageEstimated": "จำนวนโทเค็นเป็นค่าประมาณ", "modelPinning": "การตรึงโมเดล", "activeModelPinning": "ใช้งาน — โมเดลที่เลือกผ่านการตรึงเซสชัน", "rotated": "หมุน: {initial} -> {final}", diff --git a/src/i18n/messages/tr.json b/src/i18n/messages/tr.json index dd5c8f8b648..b0cff2633b6 100644 --- a/src/i18n/messages/tr.json +++ b/src/i18n/messages/tr.json @@ -11474,6 +11474,10 @@ "requestedModel": "İstenen Model", "requestProtocol": "İstek Protokolü", "cacheSource": "Önbellek Kaynağı", + "contentProvenance": "Yanıt", + "contentEmpty": "Boş yanıt", + "usageAbsent": "Token sayısı bildirilmedi", + "usageEstimated": "Token sayısı tahmini", "modelPinning": "Model Sabitleme", "activeModelPinning": "Aktif — oturum sabitleme ile seçilen model", "rotated": "Döndürüldü: {initial} -> {final}", diff --git a/src/i18n/messages/uk-UA.json b/src/i18n/messages/uk-UA.json index 62f2f6d85b2..4ecc8006c98 100644 --- a/src/i18n/messages/uk-UA.json +++ b/src/i18n/messages/uk-UA.json @@ -11474,6 +11474,10 @@ "requestedModel": "Запитана Модель", "requestProtocol": "Протокол запиту", "cacheSource": "Кеш Джерело", + "contentProvenance": "Відповідь", + "contentEmpty": "Порожня відповідь", + "usageAbsent": "Кількість токенів не повідомлено", + "usageEstimated": "Кількість токенів оцінено", "modelPinning": "Прикріплення Моделі", "activeModelPinning": "Активний — модель, вибрана через закріплення сесії", "rotated": "Повернуто: {initial} -> {final}", diff --git a/src/i18n/messages/ur.json b/src/i18n/messages/ur.json index 58de8f90376..924f8e48ecc 100644 --- a/src/i18n/messages/ur.json +++ b/src/i18n/messages/ur.json @@ -11474,6 +11474,10 @@ "requestedModel": "درخواست کردہ ماڈل", "requestProtocol": "درخواست پروٹوکول", "cacheSource": "کیچ ماخذ", + "contentProvenance": "جواب", + "contentEmpty": "خالی جواب", + "usageAbsent": "ٹوکن کی تعداد رپورٹ نہیں کی گئی", + "usageEstimated": "ٹوکن کی تعداد تخمینی ہے", "modelPinning": "ماڈل پننگ", "activeModelPinning": "فعال — سیشن پننگ کے ذریعے ماڈل منتخب کیا گیا", "rotated": "موڑا گیا: {initial} -> {final}", diff --git a/src/i18n/messages/uz.json b/src/i18n/messages/uz.json index e769e601353..190db34eac3 100644 --- a/src/i18n/messages/uz.json +++ b/src/i18n/messages/uz.json @@ -11474,6 +11474,10 @@ "requestedModel": "Soʻralgan model", "requestProtocol": "Soʻrov protokoli", "cacheSource": "Kesh manbasi", + "contentProvenance": "Javob", + "contentEmpty": "Boʻsh javob", + "usageAbsent": "Tokenlar soni xabar qilinmadi", + "usageEstimated": "Tokenlar soni taxminiy", "modelPinning": "Modelni mahkamlash", "activeModelPinning": "Faol — model sessiyaga mahkamlash orqali tanlangan", "rotated": "Almashtirildi: {initial} -> {final}", diff --git a/src/i18n/messages/vi.json b/src/i18n/messages/vi.json index 36e72eb089e..306fa3a1c8c 100644 --- a/src/i18n/messages/vi.json +++ b/src/i18n/messages/vi.json @@ -11474,6 +11474,10 @@ "requestedModel": "Mô hình được yêu cầu", "requestProtocol": "Giao thức yêu cầu", "cacheSource": "Nguồn cache", + "contentProvenance": "Phản hồi", + "contentEmpty": "Phản hồi trống", + "usageAbsent": "Không báo cáo số token", + "usageEstimated": "Số token ước tính", "modelPinning": "Ghim mô hình", "activeModelPinning": "Đang hoạt động — mô hình được chọn bằng cách ghim phiên", "rotated": "Đã xoay vòng: {initial} -> {final}", diff --git a/src/i18n/messages/yo.json b/src/i18n/messages/yo.json index 5a8ed9752b6..2f0ca12685e 100644 --- a/src/i18n/messages/yo.json +++ b/src/i18n/messages/yo.json @@ -11474,6 +11474,10 @@ "requestedModel": "Mọ́dẹ́lì Tí A Béèrè", "requestProtocol": "Ìlànà Ìbéèrè", "cacheSource": "Orísun Cache", + "contentProvenance": "Ìdáhùn", + "contentEmpty": "Ìdáhùn òfo", + "usageAbsent": "A kò jábọ̀ iye token", + "usageEstimated": "Iye token jẹ́ àfojúsùn", "modelPinning": "Ìdìmọ́ Mọ́dẹ́lì", "activeModelPinning": "Ń ṣiṣẹ́ — a yan mọ́dẹ́lì nípasẹ̀ ìdìmọ́ session", "rotated": "A yí padà: {initial} -> {final}", diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index bfb697e391d..383c89b0d62 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -11474,6 +11474,10 @@ "requestedModel": "请求的模型", "requestProtocol": "请求协议", "cacheSource": "缓存源", + "contentProvenance": "回复", + "contentEmpty": "空回复", + "usageAbsent": "未报告 token 数量", + "usageEstimated": "token 数量为估算值", "modelPinning": "模型固定", "activeModelPinning": "活动 — 通过会话固定选择的模型", "rotated": "旋转: {initial} -> {final}", diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json index 02d01f8bc8e..80c64a09d11 100644 --- a/src/i18n/messages/zh-TW.json +++ b/src/i18n/messages/zh-TW.json @@ -11474,6 +11474,10 @@ "requestedModel": "請求的模型", "requestProtocol": "需求協議", "cacheSource": "快取來源", + "contentProvenance": "回覆", + "contentEmpty": "空回覆", + "usageAbsent": "未回報 token 數量", + "usageEstimated": "token 數量為估算值", "modelPinning": "模型固定", "activeModelPinning": "活動 — 通過會話固定選擇的模型", "rotated": "旋轉: {initial} -> {final}", diff --git a/src/lib/db/migrations/190_call_logs_content_provenance.sql b/src/lib/db/migrations/190_call_logs_content_provenance.sql new file mode 100644 index 00000000000..6ec8c251076 --- /dev/null +++ b/src/lib/db/migrations/190_call_logs_content_provenance.sql @@ -0,0 +1,10 @@ +-- Per-request rendered-content presence plus usage provenance (additive, nullable). +-- Additive and nullable; readers stay intact. +-- +-- has_content: NULL when unknown (pre-migration, non-2xx, logging disabled, +-- or non-text output), else 1 when client-visible content was rendered, +-- else 0 for an empty success. NULL is never a "no content" claim. +-- usage_provenance: NULL when unknown (pre-migration, non-2xx), else +-- 'reported' (provider metering), 'estimated' (local estimate), 'absent'. +ALTER TABLE call_logs ADD COLUMN has_content INTEGER DEFAULT NULL; +ALTER TABLE call_logs ADD COLUMN usage_provenance TEXT DEFAULT NULL; diff --git a/src/lib/db/schemaColumns.ts b/src/lib/db/schemaColumns.ts index b979b796a28..d4982b66ba6 100644 --- a/src/lib/db/schemaColumns.ts +++ b/src/lib/db/schemaColumns.ts @@ -279,6 +279,17 @@ export function ensureCallLogsColumns(db: SqliteDatabase) { db.exec("ALTER TABLE call_logs ADD COLUMN reasoning_encrypted INTEGER DEFAULT NULL"); console.log("[DB] Added call_logs.reasoning_encrypted column"); } + // added by 190_call_logs_content_provenance; back-filled here for + // lineages that skipped the migration file — the call-log write path + // references these columns on every insert. + if (!columnNames.has("has_content")) { + db.exec("ALTER TABLE call_logs ADD COLUMN has_content INTEGER DEFAULT NULL"); + console.log("[DB] Added call_logs.has_content column"); + } + if (!columnNames.has("usage_provenance")) { + db.exec("ALTER TABLE call_logs ADD COLUMN usage_provenance TEXT DEFAULT NULL"); + console.log("[DB] Added call_logs.usage_provenance column"); + } db.exec( "CREATE INDEX IF NOT EXISTS idx_call_logs_requested_model ON call_logs(requested_model)" diff --git a/src/lib/usage/callContentProvenance.ts b/src/lib/usage/callContentProvenance.ts new file mode 100644 index 00000000000..ebdbad9ecc5 --- /dev/null +++ b/src/lib/usage/callContentProvenance.ts @@ -0,0 +1,201 @@ +/** + * Call content presence plus usage provenance (leaf module, no open-sse imports). + * + * Distinguishes an empty rendered reply from provider metering that never + * arrived: `has_content` is measured on the client-visible body, while the + * usage provenance tells whether token counts were reported, estimated + * locally, or absent. Nullable by design — unknown is never a claim. + */ + +export type UsageProvenance = "reported" | "estimated" | "absent"; + +// Same token-field inventory as hasValidUsage (open-sse/utils/usageTracking.ts): +// a usage object counts as reported when any known field is positive. +// NOTE: keep in sync if that inventory gains a field. +const TOKEN_FIELDS = [ + "prompt_tokens", + "completion_tokens", + "total_tokens", + "input_tokens", + "output_tokens", + "promptTokenCount", + "candidatesTokenCount", + "totalTokenCount", +] as const; + +export function hasTokenCount(tokens: unknown): boolean { + if (!tokens || typeof tokens !== "object" || Array.isArray(tokens)) return false; + const record = tokens as Record; + for (const field of TOKEN_FIELDS) { + const value = record[field]; + if (typeof value === "number" && Number.isFinite(value) && value > 0) return true; + } + return false; +} + +export function isOwnEstimatedUsage(tokens: unknown): boolean { + if (!tokens || typeof tokens !== "object" || Array.isArray(tokens)) return false; + return (tokens as Record).estimated === true; +} + +export function resolveUsageProvenance(options: { + usageEstimated: boolean; + tokens: unknown; + isSuccess: boolean; +}): UsageProvenance | null { + if (!options.isSuccess) return null; + if (options.usageEstimated || isOwnEstimatedUsage(options.tokens)) return "estimated"; + if (hasTokenCount(options.tokens)) return "reported"; + return "absent"; +} + +const NON_TEXT_REQUEST_TYPES = new Set([ + "audio", + "image", + "video", + "speech", + "transcription", + "translation", + "embedding", + "rerank", + "search", +]); + +const NON_TEXT_PATH_PATTERN = /\/v1\/(audio|images|videos)\/|audio|images|realtime|websocket/i; + +export function isNonTextRequest(requestType: unknown, path: unknown): boolean { + if (typeof requestType === "string" && NON_TEXT_REQUEST_TYPES.has(requestType)) return true; + if (typeof path === "string" && NON_TEXT_PATH_PATTERN.test(path)) return true; + return false; +} + +function asRecord(value: unknown): Record | null { + if (!value || typeof value !== "object" || Array.isArray(value)) return null; + return value as Record; +} + +function hasNonBlankText(value: unknown): boolean { + return typeof value === "string" && value.trim().length > 0; +} + +function hasToolCalls(value: unknown): boolean { + if (!Array.isArray(value)) return false; + return value.length > 0; +} + +function messageHasContent(message: Record): boolean | null { + if (hasNonBlankText(message.content)) return true; + if ( + hasToolCalls(message.tool_calls) || + (message.function_call !== undefined && message.function_call !== null) || + hasToolCalls(message.tool_uses) + ) { + return true; + } + if (hasNonBlankText(message.text)) return true; + const recognized = + "content" in message || + "tool_calls" in message || + "function_call" in message || + "tool_uses" in message || + "text" in message || + "reasoning_content" in message || + "reasoning" in message || + "thinking" in message; + if (!recognized) return null; + return false; +} + +const TOOL_CALL_TYPES = new Set(["tool_call", "function_call", "custom_tool_call", "tool_use"]); + +const MEDIA_KEY = /audio|image|video|data|base64|url/i; + +// One chat choice: true/false when measurable, "media" for non-text output, +// null when the shape says nothing. +function choiceContent(choiceRecord: Record): boolean | "media" | null { + const message = asRecord(choiceRecord.message) ?? asRecord(choiceRecord.delta); + if (!message) { + if (hasNonBlankText(choiceRecord.text)) return true; + return "finish_reason" in choiceRecord || "text" in choiceRecord ? false : null; + } + const result = messageHasContent(message); + if (result !== null) return result; + return Object.keys(message).some((key) => MEDIA_KEY.test(key)) ? "media" : null; +} + +function choicesContent(choices: unknown[]): boolean | null { + let recognizedAny = false; + for (const choice of choices) { + const choiceRecord = asRecord(choice); + const result = choiceRecord ? choiceContent(choiceRecord) : null; + if (result === true) return true; + if (result === "media") return null; + if (result === false) recognizedAny = true; + } + return recognizedAny ? false : null; +} + +// Claude Messages: `content` is an array of typed blocks (text, tool_use, +// thinking); only non-blank text or a tool call is rendered content. +function claudeBlocksContent(content: unknown): boolean | null { + if (!Array.isArray(content)) return null; + let seen = false; + for (const block of content) { + const blockRecord = asRecord(block); + if (!blockRecord) continue; + if (hasNonBlankText(blockRecord.text) || blockRecord.type === "tool_use") return true; + if (typeof blockRecord.type === "string") seen = true; + } + return seen ? false : null; +} + +function partsContent(parts: unknown[]): boolean { + return parts.some((part) => { + const partRecord = asRecord(part); + return ( + partRecord !== null && + (hasNonBlankText(partRecord.text) || TOOL_CALL_TYPES.has(partRecord.type as string)) + ); + }); +} + +function outputItemContent(itemRecord: Record): boolean | null { + if (TOOL_CALL_TYPES.has(itemRecord.type as string)) return true; + const content = itemRecord.content; + if (typeof content === "string") return content.trim().length > 0 ? true : null; + if (Array.isArray(content)) return partsContent(content); + return "type" in itemRecord ? false : null; +} + +// Responses API: `output` items carry message parts or tool calls at item level. +function responsesOutputContent(output: unknown): boolean | null { + if (!Array.isArray(output)) return null; + let recognizedAny = false; + for (const item of output) { + const itemRecord = asRecord(item); + if (!itemRecord) continue; + const result = outputItemContent(itemRecord); + if (result === true) return true; + if (result === false) recognizedAny = true; + } + return recognizedAny ? false : null; +} + +/** + * True when the client-visible body carries rendered content (text or tool + * calls). False for an empty success. Null when unmeasurable (missing body, + * unrecognized shape, or non-text output) — never a "no content" claim. + * Reasoning alone never counts: reasoning presence lives in reasoning_*. + */ +export function hasRenderedContent(clientBody: unknown): boolean | null { + const body = asRecord(clientBody); + if (!body) return null; + if (Array.isArray(body.choices)) return choicesContent(body.choices); + if ([body.content, body.text, body.output_text].some(hasNonBlankText)) return true; + const claude = claudeBlocksContent(body.content); + if (claude === true) return true; + if (hasToolCalls(body.tool_calls) || (body.function_call ?? null) !== null) return true; + const responses = responsesOutputContent(body.output); + if (responses !== null) return responses; + return claude; +} diff --git a/src/lib/usage/callLogs.ts b/src/lib/usage/callLogs.ts index 11b2d21f23c..ce13a2a9db5 100644 --- a/src/lib/usage/callLogs.ts +++ b/src/lib/usage/callLogs.ts @@ -27,6 +27,11 @@ import { getReasoningTokensOrNull, getObservedReasoning, } from "./tokenAccounting"; +import { + hasRenderedContent, + isNonTextRequest, + resolveUsageProvenance, +} from "./callContentProvenance"; import { isNoLog } from "../compliance/noLog"; import { parseStoredPayload, @@ -122,6 +127,8 @@ type CallLogSummaryRow = { correlation_id?: string | null; model_pinned?: number | null; session_tag?: string | null; + has_content?: number | null; + usage_provenance?: string | null; }; const RESOLVED_ACCOUNT_SQL = "COALESCE(NULLIF(pc.name, ''), NULLIF(pc.email, ''), cl.account)"; @@ -439,6 +446,8 @@ function mapSummaryRow(row: CallLogSummaryRow) { compressed: row.tokens_compressed != null ? toNumber(row.tokens_compressed) : null, }, cacheSource: row.cache_source || "upstream", + hasContent: row.has_content ?? null, + usageProvenance: row.usage_provenance ?? null, requestType: row.request_type, sourceFormat: row.source_format, targetFormat: row.target_format, @@ -602,6 +611,27 @@ async function saveCallLogOperation(entry: any): Promise { const errorType = toStoredErrorType( classifyCallLogError(entry.status, entry.error, entry.provider) ); + // Rendered-content presence plus usage provenance: success-only, additive, + // nullable. A 2xx is a success even with token counts at zero, so the + // success bound is 200-299 (not <400). + const numericStatus = Number(entry.status); + const isSuccess = Number.isFinite(numericStatus) && numericStatus >= 200 && numericStatus < 300; + const clientVisibleBody = + entry.clientResponse ?? + (entry.pipelinePayloads as { clientResponse?: unknown } | null | undefined)?.clientResponse ?? + (entry.pipeline as { clientResponse?: unknown } | null | undefined)?.clientResponse ?? + entry.responseBody; + const measurableContent = + isSuccess && + !noLogEnabled && + !isNonTextRequest(entry.requestType, entry.path ?? entry.method); + const renderedContent = measurableContent ? hasRenderedContent(clientVisibleBody) : null; + const hasContent = renderedContent === null ? null : renderedContent ? 1 : 0; + const usageProvenance = resolveUsageProvenance({ + usageEstimated: entry.usageEstimated === true, + tokens: entry.tokens, + isSuccess, + }); const logEntry = { id: typeof entry.id === "string" && entry.id.length > 0 ? entry.id : generateLogId(), timestamp: typeof entry.timestamp === "string" ? entry.timestamp : new Date().toISOString(), @@ -650,6 +680,8 @@ async function saveCallLogOperation(entry: any): Promise { // resolvePreviousResponseState refuses to rehydrate it as continuation // history. See src/lib/db/responsesContinuationStore.ts. videoContentRemoved: entry.videoContentRemoved ? 1 : 0, + hasContent, + usageProvenance, }; const requestSummary = noLogEnabled @@ -700,7 +732,7 @@ async function saveCallLogOperation(entry: any): Promise { artifact_relpath, artifact_size_bytes, artifact_sha256, has_request_body, has_response_body, has_pipeline_details, request_summary, correlation_id, model_pinned, session_tag, response_id, error_type, - video_content_removed + video_content_removed, has_content, usage_provenance ) VALUES ( @id, @timestamp, @method, @path, @status, @model, @requestedModel, @provider, @@ -714,7 +746,7 @@ async function saveCallLogOperation(entry: any): Promise { @artifactRelPath, @artifactSizeBytes, @artifactSha256, @hasRequestBody, @hasResponseBody, @hasPipelineDetails, @requestSummary, @correlationId, @modelPinned, @sessionTag, @responseId, @errorType, - @videoContentRemoved + @videoContentRemoved, @hasContent, @usageProvenance ) ` ).run({ diff --git a/src/shared/components/CallContentProvenanceBadges.tsx b/src/shared/components/CallContentProvenanceBadges.tsx new file mode 100644 index 00000000000..90a73161dc8 --- /dev/null +++ b/src/shared/components/CallContentProvenanceBadges.tsx @@ -0,0 +1,52 @@ +"use client"; + +import { useTranslations } from "next-intl"; + +type UsageProvenance = "reported" | "estimated" | "absent" | null | undefined; + +/** + * Translation keys (namespace `requestLogger.detail`) for what a finished request + * says about its reply: an empty reply (`hasContent === 0`) is a different fact + * from provider metering that never arrived (`usageProvenance === "absent"`) or + * token counts estimated locally. Unknown values (null) produce nothing. + */ +export function getContentProvenanceKeys( + hasContent: number | boolean | null | undefined, + usageProvenance: UsageProvenance +): string[] { + const keys: string[] = []; + if (hasContent === 0 || hasContent === false) keys.push("contentEmpty"); + if (usageProvenance === "absent") keys.push("usageAbsent"); + if (usageProvenance === "estimated") keys.push("usageEstimated"); + return keys; +} + +export function CallContentProvenanceBadges({ + hasContent, + usageProvenance, +}: { + hasContent?: number | boolean | null; + usageProvenance?: UsageProvenance; +}) { + const t = useTranslations("requestLogger.detail"); + const keys = getContentProvenanceKeys(hasContent, usageProvenance); + if (keys.length === 0) return null; + return ( +
+
+ {t("contentProvenance")} +
+
+ {keys.map((key) => ( + + {t(key)} + + ))} +
+
+ ); +} diff --git a/src/shared/components/RequestLoggerDetail.tsx b/src/shared/components/RequestLoggerDetail.tsx index acc5fd00359..4582dc84ce7 100644 --- a/src/shared/components/RequestLoggerDetail.tsx +++ b/src/shared/components/RequestLoggerDetail.tsx @@ -16,6 +16,7 @@ import { timestampMarkerCustomizeNode, } from "@/shared/hooks/useTimestampTitles"; import { JsonTreeExpandControls } from "@/shared/components/JsonTreeExpandControls"; +import { CallContentProvenanceBadges } from "@/shared/components/CallContentProvenanceBadges"; import { useJsonTreeExpandLevel } from "@/store/jsonTreeExpandStore"; import { PayloadSection, @@ -839,6 +840,10 @@ export default function RequestLoggerDetail({ {cacheSourceLabel} + {(detail?.modelPinned || log.modelPinned) && (
diff --git a/tests/unit/call-content-provenance.test.ts b/tests/unit/call-content-provenance.test.ts new file mode 100644 index 00000000000..1a359e0fcb8 --- /dev/null +++ b/tests/unit/call-content-provenance.test.ts @@ -0,0 +1,286 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { getDbInstance, resetDbInstance } from "../../src/lib/db/core.ts"; +import { getCallLogById, saveCallLog } from "../../src/lib/usage/callLogs.ts"; + +test.after(() => { + resetDbInstance(); +}); + +function rowFor(id: string) { + return getDbInstance() + .prepare("SELECT has_content, usage_provenance FROM call_logs WHERE id = ?") + .get(id) as { has_content: number | null; usage_provenance: string | null }; +} + +function cleanup(ids: string[]) { + const stmt = getDbInstance().prepare("DELETE FROM call_logs WHERE id = ?"); + for (const id of ids) stmt.run(id); +} + +test("migration exposes nullable has_content and usage_provenance columns", () => { + const columns = getDbInstance().prepare("PRAGMA table_info(call_logs)").all() as { + name: string; + notnull: number; + dflt_value: unknown; + }[]; + const byName = new Map(columns.map((c) => [c.name, c])); + assert.ok(byName.has("has_content"), "call_logs should have has_content column"); + assert.ok(byName.has("usage_provenance"), "call_logs should have usage_provenance column"); + assert.equal(byName.get("has_content")?.notnull, 0); + assert.equal(byName.get("usage_provenance")?.notnull, 0); +}); + +test("2xx with text and provider usage is content with reported provenance", async () => { + const id = "ccp-reported"; + await saveCallLog({ + id, + status: 200, + tokens: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }, + clientResponse: { choices: [{ message: { content: "hello" } }] }, + }); + try { + const row = rowFor(id); + assert.equal(row.has_content, 1); + assert.equal(row.usage_provenance, "reported"); + } finally { + cleanup([id]); + } +}); + +test("2xx empty with provider usage is no content with reported provenance", async () => { + const id = "ccp-empty-reported"; + await saveCallLog({ + id, + status: 200, + tokens: { prompt_tokens: 10, completion_tokens: 0, total_tokens: 10 }, + clientResponse: { choices: [{ message: { content: null } }] }, + }); + try { + const row = rowFor(id); + assert.equal(row.has_content, 0); + assert.equal(row.usage_provenance, "reported"); + } finally { + cleanup([id]); + } +}); + +test("2xx with content but silent provider is content with absent provenance", async () => { + const id = "ccp-absent"; + await saveCallLog({ + id, + status: 200, + tokens: {}, + clientResponse: { choices: [{ message: { content: "hello" } }] }, + }); + try { + const row = rowFor(id); + assert.equal(row.has_content, 1); + assert.equal(row.usage_provenance, "absent"); + } finally { + cleanup([id]); + } +}); + +test("2xx with threaded usage estimate is content with estimated provenance", async () => { + const id = "ccp-estimated"; + await saveCallLog({ + id, + status: 200, + tokens: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }, + usageEstimated: true, + clientResponse: { choices: [{ message: { content: "hello" } }] }, + }); + try { + const row = rowFor(id); + assert.equal(row.has_content, 1); + assert.equal(row.usage_provenance, "estimated"); + } finally { + cleanup([id]); + } +}); + +test("2xx with rebuilt estimated tokens keeps estimated provenance", async () => { + const id = "ccp-estimated-rebuilt"; + const estimated = { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15, estimated: true }; + await saveCallLog({ + id, + status: 200, + tokens: JSON.parse(JSON.stringify(estimated)), + usageEstimated: true, + clientResponse: { choices: [{ message: { content: "hello" } }] }, + }); + try { + const row = rowFor(id); + assert.equal(row.usage_provenance, "estimated"); + } finally { + cleanup([id]); + } +}); + +test("2xx reasoning alone is no content", async () => { + const id = "ccp-reasoning-only"; + await saveCallLog({ + id, + status: 200, + tokens: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }, + clientResponse: { choices: [{ message: { reasoning_content: "thinking" } }] }, + }); + try { + assert.equal(rowFor(id).has_content, 0); + } finally { + cleanup([id]); + } +}); + +test("2xx tool calls alone count as content", async () => { + const id = "ccp-tool-only"; + await saveCallLog({ + id, + status: 200, + tokens: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }, + clientResponse: { + choices: [{ message: { tool_calls: [{ id: "1", function: { name: "f" } }] } }], + }, + }); + try { + assert.equal(rowFor(id).has_content, 1); + } finally { + cleanup([id]); + } +}); + +test("non-text route leaves content unmeasured", async () => { + const id = "ccp-nontext"; + await saveCallLog({ + id, + status: 200, + path: "/v1/audio/speech", + tokens: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }, + clientResponse: { audio: "aGVsbG8=" }, + }); + try { + assert.equal(rowFor(id).has_content, null); + } finally { + cleanup([id]); + } +}); + +test("failure leaves both columns null", async () => { + const id = "ccp-failure"; + await saveCallLog({ + id, + status: 429, + tokens: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }, + clientResponse: { choices: [{ message: { content: "hello" } }] }, + }); + try { + const row = rowFor(id); + assert.equal(row.has_content, null); + assert.equal(row.usage_provenance, null); + } finally { + cleanup([id]); + } +}); + +test("2xx Responses tool_call item alone counts as content", async () => { + const id = "ccp-resp-tool-only"; + await saveCallLog({ + id, + status: 200, + tokens: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }, + clientResponse: { output: [{ type: "function_call", name: "f", arguments: "{}" }] }, + }); + try { + assert.equal(rowFor(id).has_content, 1); + } finally { + cleanup([id]); + } +}); + +test("2xx Claude tool_use block alone counts as content", async () => { + const id = "ccp-claude-tooluse-only"; + await saveCallLog({ + id, + status: 200, + tokens: { input_tokens: 10, output_tokens: 5 }, + clientResponse: { content: [{ type: "tool_use", id: "t1", name: "f", input: {} }] }, + }); + try { + assert.equal(rowFor(id).has_content, 1); + } finally { + cleanup([id]); + } +}); + +test("2xx Responses blank text block alone is no content", async () => { + const id = "ccp-blank-text-only"; + await saveCallLog({ + id, + status: 200, + tokens: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }, + clientResponse: { output: [{ type: "message", content: [{ type: "text", text: " " }] }] }, + }); + try { + assert.equal(rowFor(id).has_content, 0); + } finally { + cleanup([id]); + } +}); + +test("2xx Claude text block counts as content", async () => { + const id = "ccp-claude-text"; + await saveCallLog({ + id, + status: 200, + tokens: { input_tokens: 10, output_tokens: 5 }, + clientResponse: { content: [{ type: "text", text: "hello" }] }, + }); + try { + assert.equal(rowFor(id).has_content, 1); + } finally { + cleanup([id]); + } +}); + +test("2xx Claude blank text block alone is no content", async () => { + const id = "ccp-claude-blank-text"; + await saveCallLog({ + id, + status: 200, + tokens: { input_tokens: 10, output_tokens: 5 }, + clientResponse: { content: [{ type: "text", text: " " }] }, + }); + try { + assert.equal(rowFor(id).has_content, 0); + } finally { + cleanup([id]); + } +}); + +test("log detail exposes an empty reply apart from missing metering", async () => { + const emptyId = "ccp-detail-empty"; + const silentId = "ccp-detail-silent"; + await saveCallLog({ + id: emptyId, + status: 200, + tokens: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }, + clientResponse: { choices: [{ message: { content: "" }, finish_reason: "stop" }] }, + }); + await saveCallLog({ + id: silentId, + status: 200, + tokens: {}, + clientResponse: { choices: [{ message: { content: "hi" }, finish_reason: "stop" }] }, + }); + try { + const empty = await getCallLogById(emptyId); + assert.equal(empty?.hasContent, 0); + assert.equal(empty?.usageProvenance, "reported"); + const silent = await getCallLogById(silentId); + assert.equal(silent?.hasContent, 1); + assert.equal(silent?.usageProvenance, "absent"); + } finally { + cleanup([emptyId, silentId]); + } +}); diff --git a/tests/unit/ui/call-content-provenance-badges.test.tsx b/tests/unit/ui/call-content-provenance-badges.test.tsx new file mode 100644 index 00000000000..26916c31160 --- /dev/null +++ b/tests/unit/ui/call-content-provenance-badges.test.tsx @@ -0,0 +1,57 @@ +// @vitest-environment jsdom +import React, { act } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("next-intl", () => ({ + useTranslations: () => (key: string) => key, +})); + +const { CallContentProvenanceBadges } = + await import("@/shared/components/CallContentProvenanceBadges"); + +let container: HTMLElement; +let root: Root; + +beforeEach(() => { + container = document.createElement("div"); + document.body.appendChild(container); + root = createRoot(container); +}); + +afterEach(async () => { + await act(async () => root.unmount()); + container.remove(); +}); + +async function render(props: { hasContent?: number | null; usageProvenance?: string | null }) { + await act(async () => { + root.render(); + }); +} + +const shown = () => + [...container.querySelectorAll("[data-testid^='content-provenance-']")].map((node) => + node.getAttribute("data-testid") + ); + +describe("CallContentProvenanceBadges", () => { + it("tells an empty reply apart from a reply whose metering never arrived", async () => { + await render({ hasContent: 0, usageProvenance: "reported" }); + expect(shown()).toEqual(["content-provenance-contentEmpty"]); + + await render({ hasContent: 1, usageProvenance: "absent" }); + expect(shown()).toEqual(["content-provenance-usageAbsent"]); + + await render({ hasContent: 1, usageProvenance: "estimated" }); + expect(shown()).toEqual(["content-provenance-usageEstimated"]); + }); + + it("renders nothing for a normal reply or unknown values", async () => { + await render({ hasContent: 1, usageProvenance: "reported" }); + expect(container.textContent).toBe(""); + + await render({ hasContent: null, usageProvenance: null }); + expect(container.textContent).toBe(""); + }); +});