Compare commits

...
Author SHA1 Message Date
MichaelandClaude Opus 5.5 6a6911af27 Bench de la façade : journal régénéré pour le nouvel index.html
Le banc d'essai prend l'index.html embarqué comme corpus de code et en
coupe un morceau au milieu du fichier : les formules LaTeX (CSS et
00-math.js) déplacent ce milieu. Journal tiré sous Linux
(LOKI_PARITY_UPDATE=1 go test ./internal/loki -run TestPariteBench) ;
seules changent les tailles et empreintes des textes et les prompt_n
qui en découlent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 11:28:50 +02:00
MichaelandClaude Opus 5.5 965a4d6247 Formules LaTeX rendues dans les réponses (KaTeX, repris d'AJEAN #123)
00-math.js : $…$, $$…$$, \(…\) et \[…\] mis de côté avant marked (qui
abîmerait \, _ et *), remplacés par un repère puis par le rendu KaTeX.
KaTeX n'est chargé qu'à la première formule ; d'ici là la source
s'affiche, puis est rendue sur place. Le code (blocs et en ligne) et les
montants (« 5 $ et 10 $ », « $5 et $10 », \$) restent du texte.

Logique de détection d'AJEAN, avec deux garde-fous en plus : un $ ouvrant
précédé d'un chiffre et d'une espace est un montant (« 5 $, à 10$ »), et
$…$ ne contient pas de backtick — sans quoi « 5 $, code `$x$` »
s'appariait par-dessus le code en ligne (le cas passe aussi chez AJEAN).

Fonctions pures dans un fichier à part : web_katex_math_test.go les
exécute sous node (ignoré sans node) sur les cas limites, et de bout en
bout avec marked. index.html régénéré par tools/assemble-ui ; NOTICE.md
mentionne la reprise et la licence MIT de KaTeX.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 11:23:16 +02:00
MichaelandClaude Opus 5.5 99c96a6c33 KaTeX 0.16.11 embarqué et servi sous /katex/ (Go et façade)
Repris d'AJEAN (d6136caa, #123) : script, feuille de style, polices woff2
et licence MIT sous internal/loki/ui/katex/, embarqués par le go:embed et
par build.rs de la façade. Pas de CDN : l'interface doit marcher hors
ligne.

Route /katex/ des deux côtés avec les mêmes en-têtes : katex.min.js
(application/javascript), katex.min.css (text/css), fonts/*.woff2
(font/woff2), Cache-Control public 1 jour ; script et feuille compressés
en gzip, polices non. Contrairement à AJEAN (http.FileServer), le Go
(web_katex.go) ne sert que ces fichiers, avec un Content-Type fixe : ni
index de dossier, ni type décidé par le registre MIME de l'OS. La façade
(ui.rs) rend les mêmes réponses ; le reste de /katex/ part au Go (404).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 11:23:16 +02:00
35 changed files with 712 additions and 128 deletions

No files matched your search

+15
View File
@@ -52,6 +52,21 @@ et le **chiffrement de la mémoire** (`mem_crypto.go`, `mem_vault.go`,
**pas** reprise : elle vise ajean.link, le service de l'auteur amont. Loki
expose à la place un export/import de paquet chiffré en fichier local.
Le **rendu des formules LaTeX** dans les réponses (`ui/src/js/00-math.js`,
`web_katex.go`) est repris d'AJEAN (#123) : $…$, $$…$$, \\(…\\) et \\[…\\] rendus
par KaTeX, chargé à la première formule seulement, sans toucher au code ni aux
montants en $. Loki y ajoute deux garde-fous de détection (« 5 $, » et un $
voisin d'un code en ligne restent du texte) et sert la route `/katex/` aussi
depuis la façade Rust (`rust/loki-rs/src/ui.rs`).
## KaTeX
**[KaTeX](https://katex.org/)** 0.16.11 (licence MIT, © Khan Academy et
contributeurs) est embarqué tel que publié — script, feuille de style et
polices woff2 — sous `internal/loki/ui/katex/`, avec sa licence
(`internal/loki/ui/katex/LICENSE`). Il est servi par Loki sous `/katex/`, sans
appel à un CDN.
## Moteur Strata
Le second moteur de Loki (`internal/loki/backend_strata.go`) est
+81 -1
View File
@@ -1418,6 +1418,10 @@ button:hover{border-color:var(--dim);color:var(--text)}
border:1px solid var(--border);border-radius:999px;background:var(--panel);
color:var(--dim);font-size:12px;line-height:1.5}
.stopnote::before{content:"";flex:none;width:6px;height:6px;border-radius:50%;background:var(--dim)}
/* Formules LaTeX (KaTeX, 00-math.js) : une longue formule défile au lieu de déborder. */
.katex{font-size:1.08em}
.katex-display{margin:.6em 0;overflow-x:auto;overflow-y:hidden;padding:2px 0}
.math-pending{font-family:var(--mono);font-size:.92em;color:var(--dim)}
/* ===== Fenêtres et sélecteurs =====================================
Coins arrondis et ombre douce plutôt qu'un cadre marqué. */
@@ -3820,6 +3824,76 @@ html[data-files="1"] #files-btn{color:var(--accent)}
</div>
<script src="marked.min.js"></script>
<script>
// --- Formules LaTeX (KaTeX) ---------------------------------------------------
// Repris d'AJEAN (#123). md() (00-md.js) met les formules de côté AVANT marked,
// qui abîmerait \\, _ et * : un repère (caractères à usage privé U+E000/U+E001
// autour du numéro) prend leur place, puis reçoit le rendu. Le code (blocs et
// `en ligne`) n'est jamais touché. Formes reconnues : $$…$$ et \[…\] (bloc),
// \(…\) et $…$ (en ligne). $…$ n'est une formule que s'il ne commence ni ne finit
// par une espace, n'est pas collé à un mot ni échappé (\$) avant, et n'est pas
// suivi d'un chiffre : « 5 $ et 10 $ » ou « $5 et $10 » restent du texte.
// Ajouts de Loki : un $ ouvrant précédé d'un chiffre et d'une espace (« 5 $, »)
// est un montant, et $…$ ne contient pas de ` — sans quoi « 5 $, code `$x$` »
// s'apparierait par-dessus le code en ligne.
// KaTeX (embarqué, servi sous /katex/ par web_katex.go et la façade) n'est chargé
// qu'à la première formule ; d'ici là la source s'affiche, puis est rendue sur
// place. (Pas de lookbehind dans l'expression : les iOS d'avant 16.4 refusent de
// charger le script entier. Ces contrôles-là sont faits dans le remplacement.)
//
// MATH_RE et mathExtract sont purs (ni DOM ni réseau) : web_katex_math_test.go les
// exécute sous node sur les cas limites (montants, code, échappements).
const MATH_RE = /(```[\s\S]*?(?:```|$)|~~~[\s\S]*?(?:~~~|$)|`[^`\n]*`)|\$\$([\s\S]+?)\$\$|\\\[([\s\S]+?)\\\]|\\\(([\s\S]+?)\\\)|\$(?![\s$])((?:\\.|[^$\\\n`])+?)\$(?![\d$])/g;
const MATH_MARK_RE = /\uE000(\d+)\uE001/g;
function mathExtract(s){
const list = [];
const text = s.replace(MATH_RE, (all, code, dd, br, pa, d, at)=>{
if(code !== undefined) return all;
// $…$ : pas collé à un mot ni échappé (\$) avant, pas un montant (« 5 $ »),
// pas d'espace avant le $ final.
if(d !== undefined && (/[\\$\w]/.test(s[at-1] || '') || /\d\s$/.test(s.slice(Math.max(0, at-2), at)) || /\s$/.test(d))) return all;
const display = dd !== undefined || br !== undefined;
list.push({tex: (dd ?? br ?? pa ?? d).trim(), display});
return '\uE000' + (list.length-1) + '\uE001';
});
return {text, list};
}
// mathRestore remplace chaque repère du HTML produit par marked par le rendu de
// sa formule (render : formule → HTML).
function mathRestore(html, list, render){
return list.length ? html.replace(MATH_MARK_RE, (all, i)=>list[+i] ? render(list[+i]) : all) : html;
}
const mathEsc = (s)=>s.replace(/[&<>"]/g, c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
const MATH_OPTS = {throwOnError: false, output: 'html'};
function mathHtml(f){
if(window.katex){
try{ return katex.renderToString(f.tex, {...MATH_OPTS, displayMode: f.display}); }
catch(_){ return '<code>'+mathEsc(f.tex)+'</code>'; }
}
mathLoad();
return '<span class="math-pending" data-d="'+(f.display?1:0)+'">'+mathEsc(f.tex)+'</span>';
}
let mathLoading = null;
function mathLoad(){
if(mathLoading) return mathLoading;
if(!document.querySelector('link[data-katex]')){
const css = document.createElement('link');
css.rel = 'stylesheet'; css.href = 'katex/katex.min.css'; css.dataset.katex = '1';
document.head.appendChild(css);
}
mathLoading = new Promise((ok, ko)=>{
const s = document.createElement('script');
s.src = 'katex/katex.min.js'; s.onload = ok;
// Échec (hors ligne, 404) : script retiré, nouvel essai à la prochaine formule.
s.onerror = ()=>{ s.remove(); ko(); };
document.head.appendChild(s);
}).then(()=>{
document.querySelectorAll('.math-pending').forEach(el=>{
try{ katex.render(el.textContent, el, {...MATH_OPTS, displayMode: el.dataset.d === '1'}); }catch(_){}
el.classList.remove('math-pending');
});
}).catch(()=>{ mathLoading = null; });
return mathLoading;
}
// marked: GFM on, no auto-breaks (single newlines stay inline).
marked.setOptions({ gfm: true, breaks: false });
// Pre-process to fix common LLM markdown mistakes before handing to marked:
@@ -3836,7 +3910,13 @@ function fixMd(s){
s = s.replace(/\n{3,}/g, '\n\n');
return s;
}
function md(src){ return src ? marked.parse(fixMd(src)) : ''; }
// Les formules LaTeX sont mises de côté avant marked puis rendues par KaTeX
// (00-math.js) ; le code et les montants en $ ne sont pas touchés.
function md(src){
if(!src) return '';
const m = mathExtract(fixMd(src));
return mathRestore(marked.parse(m.text), m.list, mathHtml);
}
// Les garde-fous du serveur ("[stop: trop d'appels d'outils]") sont concaténés
// au texte de la réponse : ils arrivent donc en clair, au milieu du markdown.
// markNotices les sort du fil pour qu'on ne les prenne pas pour une phrase du
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2013-2020 Khan Academy and other contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
@font-face{font-family:KaTeX_AMS;font-style:normal;font-weight:400;src:url(fonts/KaTeX_AMS-Regular.woff2) format("woff2"),url(fonts/KaTeX_AMS-Regular.woff) format("woff"),url(fonts/KaTeX_AMS-Regular.ttf) format("truetype")}@font-face{font-family:KaTeX_Caligraphic;font-style:normal;font-weight:700;src:url(fonts/KaTeX_Caligraphic-Bold.woff2) format("woff2"),url(fonts/KaTeX_Caligraphic-Bold.woff) format("woff"),url(fonts/KaTeX_Caligraphic-Bold.ttf) format("truetype")}@font-face{font-family:KaTeX_Caligraphic;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Caligraphic-Regular.woff2) format("woff2"),url(fonts/KaTeX_Caligraphic-Regular.woff) format("woff"),url(fonts/KaTeX_Caligraphic-Regular.ttf) format("truetype")}@font-face{font-family:KaTeX_Fraktur;font-style:normal;font-weight:700;src:url(fonts/KaTeX_Fraktur-Bold.woff2) format("woff2"),url(fonts/KaTeX_Fraktur-Bold.woff) format("woff"),url(fonts/KaTeX_Fraktur-Bold.ttf) format("truetype")}@font-face{font-family:KaTeX_Fraktur;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Fraktur-Regular.woff2) format("woff2"),url(fonts/KaTeX_Fraktur-Regular.woff) format("woff"),url(fonts/KaTeX_Fraktur-Regular.ttf) format("truetype")}@font-face{font-family:KaTeX_Main;font-style:normal;font-weight:700;src:url(fonts/KaTeX_Main-Bold.woff2) format("woff2"),url(fonts/KaTeX_Main-Bold.woff) format("woff"),url(fonts/KaTeX_Main-Bold.ttf) format("truetype")}@font-face{font-family:KaTeX_Main;font-style:italic;font-weight:700;src:url(fonts/KaTeX_Main-BoldItalic.woff2) format("woff2"),url(fonts/KaTeX_Main-BoldItalic.woff) format("woff"),url(fonts/KaTeX_Main-BoldItalic.ttf) format("truetype")}@font-face{font-family:KaTeX_Main;font-style:italic;font-weight:400;src:url(fonts/KaTeX_Main-Italic.woff2) format("woff2"),url(fonts/KaTeX_Main-Italic.woff) format("woff"),url(fonts/KaTeX_Main-Italic.ttf) format("truetype")}@font-face{font-family:KaTeX_Main;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Main-Regular.woff2) format("woff2"),url(fonts/KaTeX_Main-Regular.woff) format("woff"),url(fonts/KaTeX_Main-Regular.ttf) format("truetype")}@font-face{font-family:KaTeX_Math;font-style:italic;font-weight:700;src:url(fonts/KaTeX_Math-BoldItalic.woff2) format("woff2"),url(fonts/KaTeX_Math-BoldItalic.woff) format("woff"),url(fonts/KaTeX_Math-BoldItalic.ttf) format("truetype")}@font-face{font-family:KaTeX_Math;font-style:italic;font-weight:400;src:url(fonts/KaTeX_Math-Italic.woff2) format("woff2"),url(fonts/KaTeX_Math-Italic.woff) format("woff"),url(fonts/KaTeX_Math-Italic.ttf) format("truetype")}@font-face{font-family:"KaTeX_SansSerif";font-style:normal;font-weight:700;src:url(fonts/KaTeX_SansSerif-Bold.woff2) format("woff2"),url(fonts/KaTeX_SansSerif-Bold.woff) format("woff"),url(fonts/KaTeX_SansSerif-Bold.ttf) format("truetype")}@font-face{font-family:"KaTeX_SansSerif";font-style:italic;font-weight:400;src:url(fonts/KaTeX_SansSerif-Italic.woff2) format("woff2"),url(fonts/KaTeX_SansSerif-Italic.woff) format("woff"),url(fonts/KaTeX_SansSerif-Italic.ttf) format("truetype")}@font-face{font-family:"KaTeX_SansSerif";font-style:normal;font-weight:400;src:url(fonts/KaTeX_SansSerif-Regular.woff2) format("woff2"),url(fonts/KaTeX_SansSerif-Regular.woff) format("woff"),url(fonts/KaTeX_SansSerif-Regular.ttf) format("truetype")}@font-face{font-family:KaTeX_Script;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Script-Regular.woff2) format("woff2"),url(fonts/KaTeX_Script-Regular.woff) format("woff"),url(fonts/KaTeX_Script-Regular.ttf) format("truetype")}@font-face{font-family:KaTeX_Size1;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Size1-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size1-Regular.woff) format("woff"),url(fonts/KaTeX_Size1-Regular.ttf) format("truetype")}@font-face{font-family:KaTeX_Size2;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Size2-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size2-Regular.woff) format("woff"),url(fonts/KaTeX_Size2-Regular.ttf) format("truetype")}@font-face{font-family:KaTeX_Size3;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Size3-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size3-Regular.woff) format("woff"),url(fonts/KaTeX_Size3-Regular.ttf) format("truetype")}@font-face{font-family:KaTeX_Size4;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Size4-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size4-Regular.woff) format("woff"),url(fonts/KaTeX_Size4-Regular.ttf) format("truetype")}@font-face{font-family:KaTeX_Typewriter;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Typewriter-Regular.woff2) format("woff2"),url(fonts/KaTeX_Typewriter-Regular.woff) format("woff"),url(fonts/KaTeX_Typewriter-Regular.ttf) format("truetype")}.katex{font:normal 1.21em KaTeX_Main,Times New Roman,serif;line-height:1.2;text-indent:0;text-rendering:auto}.katex *{-ms-high-contrast-adjust:none!important;border-color:currentColor}.katex .katex-version:after{content:"0.16.11"}.katex .katex-mathml{clip:rect(1px,1px,1px,1px);border:0;height:1pLine truncated
+1
View File
@@ -0,0 +1 @@
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.katex=t():e.katex=t()}("undefined"!=typeof self?self:this,(function(){return function(){"use strict";var e={d:function(t,r){for(var n in r)e.o(r,n)&&!e.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:r[n]})},o:function(e,t){return Object.prototype.hasOwnProperty.call(e,t)}},t={};e.d(t,{default:function(){return Yn}});class r{constructor(e,t){this.name=void 0,this.position=void 0,this.length=void 0,this.rawMessage=void 0;let n,o,s="KaTeX parse error: "+e;const i=t&&t.loc;if(i&&i.start<=i.end){const e=i.lexer.input;n=i.start,o=i.end,n===e.length?s+=" at end of input: ":s+=" at position "+(n+1)+": ";const t=e.slice(n,o).replace(/[^]/g,"$&\u0332");let r,a;r=n>15?"\u2026"+e.slice(n-15,n):e.slice(0,n),a=o+15<e.length?e.slice(o,o+15)+"\u2026":e.slice(o),s+=r+t+a}const a=new Error(s);return a.name="ParseError",a.__proto__=r.prototype,a.position=n,null!=n&&null!=o&&(a.length=o-n),a.rawMessage=e,a}}r.prototype.__proto__=Error.prototype;var n=r;const o=/([A-Z])/g,s={"&":"&amp;",">":"&gt;","<":"&lt;",'"':"&quot;","'":"&#x27;"},i=/[&><"']/g;const a=function(e){return"ordgroup"===e.type||"color"===e.type?1===e.body.length?a(e.body[0]):e:"font"===e.type?a(e.body):e};var l={contains:function(e,t){return-1!==e.indexOf(t)},deflt:function(e,t){return void 0===e?t:e},escape:function(e){return String(e).replace(i,(e=>s[e]))},hyphenate:function(e){return e.replace(o,"-$1").toLowerCase()},getBaseElem:a,isCharacterBox:function(e){const t=a(e);return"mathord"===t.type||"textord"===t.type||"atom"===t.type},protocolFromUrl:function(e){const t=/^[\x00-\x20]*([^\\/#?]*?)(:|&#0*58|&#x0*3a|&colon)/i.exec(e);return t?":"!==t[2]?null:/^[a-zA-Z][a-zA-Z0-9+\-.]*$/.test(t[1])?t[1].toLowerCase():null:"_relative"}};const h={displayMode:{type:"boolean",description:"Render math in display mode, which puts the math in display style (so \\int and \\sum are large, for example), and centers the math on the page on its own line.",cli:"-d, --display-mode"},output:{type:{enum:["htmlAndMathml","html","mathml"]},description:"Determines the markup language of the output.",cli:"-F, --format <type>"},leqno:{type:"boolean",description:"Render display math in leqno style (left-justified tags)."},fleqn:{type:"boolean",description:"Render display math flush left."},throwOnError:{type:"boolean",default:!0,cli:"-t, --no-throw-on-error",cliDescription:"Render errors (in the color given by --error-color) instead of throwing a ParseError exception when encountering an error."},errorColor:{type:"string",default:"#cc0000",cli:"-c, --error-color <color>",cliDescription:"A color string given in the format 'rgb' or 'rrggbb' (no #). This option determines the color of errors rendered by the -t option.",cliProcessor:e=>"#"+e},macros:{type:"object",cli:"-m, --macro <def>",cliDescription:"Define custom macro of the form '\\foo:expansion' (use multiple -m arguments for multiple macros).",cliDefault:[],cliProcessor:(e,t)=>(t.push(e),t)},minRuleThickness:{type:"number",description:"Specifies a minimum thickness, in ems, for fraction lines, `\\sqrt` top lines, `{array}` vertical lines, `\\hline`, `\\hdashline`, `\\underline`, `\\overline`, and the borders of `\\fbox`, `\\boxed`, and `\\fcolorbox`.",processor:e=>Math.max(0,e),cli:"--min-rule-thickness <size>",cliProcessor:parseFloat},colorIsTextColor:{type:"boolean",description:"Makes \\color behave like LaTeX's 2-argument \\textcolor, instead of LaTeX's one-argument \\color mode change.",cli:"-b, --color-is-text-color"},strict:{type:[{enum:["warn","ignore","error"]},"boolean","function"],description:"Turn on strict / LaTeX faithfulness mode, which throws an error if the input uses features that are not supported by LaTeX.",cli:"-S, --strict",cliDefault:!1},trust:{type:["boolean","function"],description:"Trust the input, enabling all HTML features such as \\url.",cli:"-T, --trust"},maxSize:{type:"number",default:1/0,description:"If non-zero, all user-specified sizes, e.g. in \\rule{500em}{500em}, will be capped to maxSize ems. Otherwise, elements and spaces can be arbitrarily large",processor:e=>Math.max(0,e),cli:"-s, --max-size <n>",cliProcessor:parseInt},maxExpand:{type:"number",default:1e3,description:"Limit the number of macro expansions to the specified number, to prevent e.g. infinite macro loops. If set to Infinity, the macro expander will try to fully expand as in LaTeX.",processor:e=>Math.max(0,e),cli:"-e, --max-expand <n>",cliProcessor:e=>"Infinity"===e?1/0:parseInt(e)},globalGroup:{type:"boolean",cli:!1}};function c(e){if(e.default)return e.default;const t=e.type,r=Array.isArray(t)?t[0]:t;if("string"!=typeof r)return r.enum[0];switch(r){case"boolean":return!1;case"string":return"";case"number":return 0;case"object":return{}}}class m{constructor(e){this.displayMode=void 0,this.output=void 0,this.leqno=void 0,this.fleqn=void 0,this.throwOnError=void 0Line truncated
+70
View File
@@ -0,0 +1,70 @@
// --- Formules LaTeX (KaTeX) ---------------------------------------------------
// Repris d'AJEAN (#123). md() (00-md.js) met les formules de côté AVANT marked,
// qui abîmerait \\, _ et * : un repère (caractères à usage privé U+E000/U+E001
// autour du numéro) prend leur place, puis reçoit le rendu. Le code (blocs et
// `en ligne`) n'est jamais touché. Formes reconnues : $$…$$ et \[…\] (bloc),
// \(…\) et $…$ (en ligne). $…$ n'est une formule que s'il ne commence ni ne finit
// par une espace, n'est pas collé à un mot ni échappé (\$) avant, et n'est pas
// suivi d'un chiffre : « 5 $ et 10 $ » ou « $5 et $10 » restent du texte.
// Ajouts de Loki : un $ ouvrant précédé d'un chiffre et d'une espace (« 5 $, »)
// est un montant, et $…$ ne contient pas de ` — sans quoi « 5 $, code `$x$` »
// s'apparierait par-dessus le code en ligne.
// KaTeX (embarqué, servi sous /katex/ par web_katex.go et la façade) n'est chargé
// qu'à la première formule ; d'ici là la source s'affiche, puis est rendue sur
// place. (Pas de lookbehind dans l'expression : les iOS d'avant 16.4 refusent de
// charger le script entier. Ces contrôles-là sont faits dans le remplacement.)
//
// MATH_RE et mathExtract sont purs (ni DOM ni réseau) : web_katex_math_test.go les
// exécute sous node sur les cas limites (montants, code, échappements).
const MATH_RE = /(```[\s\S]*?(?:```|$)|~~~[\s\S]*?(?:~~~|$)|`[^`\n]*`)|\$\$([\s\S]+?)\$\$|\\\[([\s\S]+?)\\\]|\\\(([\s\S]+?)\\\)|\$(?![\s$])((?:\\.|[^$\\\n`])+?)\$(?![\d$])/g;
const MATH_MARK_RE = /\uE000(\d+)\uE001/g;
function mathExtract(s){
const list = [];
const text = s.replace(MATH_RE, (all, code, dd, br, pa, d, at)=>{
if(code !== undefined) return all;
// $…$ : pas collé à un mot ni échappé (\$) avant, pas un montant (« 5 $ »),
// pas d'espace avant le $ final.
if(d !== undefined && (/[\\$\w]/.test(s[at-1] || '') || /\d\s$/.test(s.slice(Math.max(0, at-2), at)) || /\s$/.test(d))) return all;
const display = dd !== undefined || br !== undefined;
list.push({tex: (dd ?? br ?? pa ?? d).trim(), display});
return '\uE000' + (list.length-1) + '\uE001';
});
return {text, list};
}
// mathRestore remplace chaque repère du HTML produit par marked par le rendu de
// sa formule (render : formule → HTML).
function mathRestore(html, list, render){
return list.length ? html.replace(MATH_MARK_RE, (all, i)=>list[+i] ? render(list[+i]) : all) : html;
}
const mathEsc = (s)=>s.replace(/[&<>"]/g, c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
const MATH_OPTS = {throwOnError: false, output: 'html'};
function mathHtml(f){
if(window.katex){
try{ return katex.renderToString(f.tex, {...MATH_OPTS, displayMode: f.display}); }
catch(_){ return '<code>'+mathEsc(f.tex)+'</code>'; }
}
mathLoad();
return '<span class="math-pending" data-d="'+(f.display?1:0)+'">'+mathEsc(f.tex)+'</span>';
}
let mathLoading = null;
function mathLoad(){
if(mathLoading) return mathLoading;
if(!document.querySelector('link[data-katex]')){
const css = document.createElement('link');
css.rel = 'stylesheet'; css.href = 'katex/katex.min.css'; css.dataset.katex = '1';
document.head.appendChild(css);
}
mathLoading = new Promise((ok, ko)=>{
const s = document.createElement('script');
s.src = 'katex/katex.min.js'; s.onload = ok;
// Échec (hors ligne, 404) : script retiré, nouvel essai à la prochaine formule.
s.onerror = ()=>{ s.remove(); ko(); };
document.head.appendChild(s);
}).then(()=>{
document.querySelectorAll('.math-pending').forEach(el=>{
try{ katex.render(el.textContent, el, {...MATH_OPTS, displayMode: el.dataset.d === '1'}); }catch(_){}
el.classList.remove('math-pending');
});
}).catch(()=>{ mathLoading = null; });
return mathLoading;
}
+7 -1
View File
@@ -14,7 +14,13 @@ function fixMd(s){
s = s.replace(/\n{3,}/g, '\n\n');
return s;
}
function md(src){ return src ? marked.parse(fixMd(src)) : ''; }
// Les formules LaTeX sont mises de côté avant marked puis rendues par KaTeX
// (00-math.js) ; le code et les montants en $ ne sont pas touchés.
function md(src){
if(!src) return '';
const m = mathExtract(fixMd(src));
return mathRestore(marked.parse(m.text), m.list, mathHtml);
}
// Les garde-fous du serveur ("[stop: trop d'appels d'outils]") sont concaténés
// au texte de la réponse : ils arrivent donc en clair, au milieu du markdown.
// markNotices les sort du fil pour qu'on ne les prenne pas pour une phrase du
+4
View File
@@ -1383,6 +1383,10 @@ button:hover{border-color:var(--dim);color:var(--text)}
border:1px solid var(--border);border-radius:999px;background:var(--panel);
color:var(--dim);font-size:12px;line-height:1.5}
.stopnote::before{content:"";flex:none;width:6px;height:6px;border-radius:50%;background:var(--dim)}
/* Formules LaTeX (KaTeX, 00-math.js) : une longue formule défile au lieu de déborder. */
.katex{font-size:1.08em}
.katex-display{margin:.6em 0;overflow-x:auto;overflow-y:hidden;padding:2px 0}
.math-pending{font-family:var(--mono);font-size:.92em;color:var(--dim)}
/* ===== Fenêtres et sélecteurs =====================================
Coins arrondis et ombre douce plutôt qu'un cadre marqué. */
+59
View File
@@ -0,0 +1,59 @@
package loki
import (
"net/http"
"path"
"strconv"
"strings"
)
// web_katex.go — KaTeX 0.16.11 (formules LaTeX des réponses), repris d'AJEAN.
//
// Embarqué sous ui/katex/ et servi sous /katex/ : l'UI ne le charge qu'à la
// première formule d'une réponse (00-md.js), et il doit marcher hors ligne,
// sans appeler un CDN. Seuls le script, la feuille de style et les polices
// woff2 sont servis — pas d'index de dossier — avec un Content-Type fixe : le
// registre MIME de l'OS (Windows notamment) ne doit pas le décider, et la
// façade Rust (rust/loki-rs/src/ui.rs) rend exactement les mêmes en-têtes.
// katexTypes : extensions servies et leur Content-Type.
var katexTypes = map[string]string{
".js": "application/javascript",
".css": "text/css; charset=utf-8",
".woff2": "font/woff2",
}
// katexFile : chemin relatif à ui/katex/ d'une ressource servable, ou "" si la
// demande n'en est pas une (racine, dossier, autre extension, remontée).
func katexFile(urlPath string) (rel, ctype string) {
rel = strings.TrimPrefix(urlPath, "/katex/")
if rel == urlPath || rel == "" || strings.Contains(rel, "..") || path.Clean(rel) != rel {
return "", ""
}
ctype, ok := katexTypes[path.Ext(rel)]
if !ok {
return "", ""
}
return rel, ctype
}
func handleKatex(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet && r.Method != http.MethodHead {
http.NotFound(w, r)
return
}
rel, ctype := katexFile(r.URL.Path)
if rel == "" {
http.NotFound(w, r)
return
}
b, err := uiFS.ReadFile("ui/katex/" + rel)
if err != nil {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", ctype)
w.Header().Set("Cache-Control", "public, max-age=86400")
w.Header().Set("Content-Length", strconv.Itoa(len(b)))
w.Write(b)
}
+143
View File
@@ -0,0 +1,143 @@
package loki
import (
"encoding/json"
"os"
"os/exec"
"path/filepath"
"reflect"
"strings"
"testing"
)
// Détection des formules (ui/src/js/00-math.js), exécutée sous node : le dépôt
// n'a pas de banc de tests JS, et MATH_RE/mathExtract sont purs. Ignoré si node
// n'est pas installé (il l'est sur les runners de la CI).
type formule struct {
Tex string `json:"tex"`
Display bool `json:"display"`
}
// harnaisMath : charge 00-math.js et marked.min.js, puis pour chaque entrée
// (JSON sur stdin) rend {text, list} de mathExtract et, pour le bout en bout, le
// HTML de marked après mathRestore (chaque formule rendue « [tex] »).
const harnaisMath = `
const fs = require('fs');
const [mathPath, markedPath] = process.argv.slice(2);
const m = new Function(fs.readFileSync(mathPath, 'utf8') + '; return {mathExtract, mathRestore};')();
const {marked} = require(markedPath);
marked.setOptions({ gfm: true, breaks: false });
const cases = JSON.parse(fs.readFileSync(0, 'utf8'));
console.log(JSON.stringify(cases.map(s => {
const r = m.mathExtract(s);
const html = m.mathRestore(marked.parse(r.text), r.list, f => '[' + (f.display ? 'D:' : '') + f.tex + ']');
return {text: r.text, list: r.list, html};
})));
`
type résultatMath struct {
Text string `json:"text"`
List []formule `json:"list"`
HTML string `json:"html"`
}
func lanceMath(t *testing.T, cas []string) []résultatMath {
t.Helper()
node, err := exec.LookPath("node")
if err != nil {
t.Skip("node absent : détection des formules non testée")
}
dir := t.TempDir()
h := filepath.Join(dir, "harnais.js")
if err := os.WriteFile(h, []byte(harnaisMath), 0o644); err != nil {
t.Fatal(err)
}
math, _ := filepath.Abs("ui/src/js/00-math.js")
marked, _ := filepath.Abs("ui/marked.min.js")
in, _ := json.Marshal(cas)
cmd := exec.Command(node, h, math, marked)
cmd.Stdin = strings.NewReader(string(in))
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("node : %v\n%s", err, out)
}
var res []résultatMath
if err := json.Unmarshal(out, &res); err != nil {
t.Fatalf("sortie illisible : %v\n%s", err, out)
}
if len(res) != len(cas) {
t.Fatalf("%d résultats pour %d cas", len(res), len(cas))
}
return res
}
func TestMathDetection(t *testing.T) {
in := func(tex string) formule { return formule{Tex: tex} }
bloc := func(tex string) formule { return formule{Tex: tex, Display: true} }
cas := []struct {
src string
want []formule
}{
// Montants : jamais des formules.
{"Ça coûte 5 $ et 10 $.", nil},
{"Entre $5 et $10 par mois.", nil},
{"Prix : 5$ ou 6$, voire 7 $.", nil},
{`Échappés : \$5 et \$6.`, nil},
{"Collé à un mot : x$y$z.", nil},
{"Espace finale : $x $.", nil},
{"De 5 $, à 10$.", nil},
{"Montant : 5 $, code `$x_1$`.", nil},
// Code : jamais touché.
{"En ligne `$x$` et `$$y$$`.", nil},
{"```\n$x$ et $$y$$\n```", nil},
{"~~~tex\n\\[a\\]\n~~~", nil},
{"```\nbloc non fermé $x$", nil},
// Formules.
{"Soit $x^2 + y^2 = z^2$ ici.", []formule{in("x^2 + y^2 = z^2")}},
{"Deux : $a$, $b$.", []formule{in("a"), in("b")}},
{"$$\n\\int_0^1 x\\,dx\n$$", []formule{bloc(`\int_0^1 x\,dx`)}},
{`En ligne \(a+b\) et en bloc \[c\].`, []formule{in("a+b"), bloc("c")}},
{"Code `$x$` puis $y$.", []formule{in("y")}},
{`Échappement dedans : $a\$b$.`, []formule{in(`a\$b`)}},
}
srcs := make([]string, len(cas))
for i, c := range cas {
srcs[i] = c.src
}
res := lanceMath(t, srcs)
for i, c := range cas {
got := res[i].List
if len(got) == 0 {
got = nil
}
if !reflect.DeepEqual(got, c.want) {
t.Errorf("%q : %+v, attendu %+v", c.src, got, c.want)
}
if c.want == nil && res[i].Text != c.src {
t.Errorf("%q : texte modifié en %q", c.src, res[i].Text)
}
}
}
// Bout en bout avec marked : \\, _ et * d'une formule arrivent intacts au rendu,
// le markdown autour reste du markdown, et aucun repère ne survit.
func TestMathAvecMarked(t *testing.T) {
res := lanceMath(t, []string{
"*Gras* : $a_1 * b_2$ et $$\\begin{matrix}a & b \\\\ c & d\\end{matrix}$$",
"Montant : 5 $, code `$x_1$`.",
})
for _, want := range []string{"<em>Gras</em>", "[a_1 * b_2]", `[D:\begin{matrix}a & b \\ c & d\end{matrix}]`} {
if !strings.Contains(res[0].HTML, want) {
t.Errorf("HTML sans %q :\n%s", want, res[0].HTML)
}
}
for _, r := range res {
if strings.ContainsAny(r.HTML, "\uE000\uE001") {
t.Errorf("repère resté dans le HTML : %q", r.HTML)
}
}
if !strings.Contains(res[1].HTML, "5 $") || !strings.Contains(res[1].HTML, "<code>$x_1$</code>") {
t.Errorf("montant ou code abîmé : %s", res[1].HTML)
}
}
+94
View File
@@ -0,0 +1,94 @@
package loki
import (
"net/http"
"net/http/httptest"
"regexp"
"strconv"
"strings"
"testing"
)
func getKatex(t *testing.T, méthode, chemin string) *httptest.ResponseRecorder {
t.Helper()
rec := httptest.NewRecorder()
handleKatex(rec, httptest.NewRequest(méthode, chemin, nil))
return rec
}
// Le script, embarqué, est servi sous /katex/ avec les en-têtes que rend aussi
// la façade Rust (ui.rs).
func TestKatexScriptServi(t *testing.T) {
rec := getKatex(t, http.MethodGet, "/katex/katex.min.js")
if rec.Code != http.StatusOK {
t.Fatalf("code %d", rec.Code)
}
h := rec.Header()
if got := h.Get("Content-Type"); got != "application/javascript" {
t.Errorf("Content-Type = %q", got)
}
if got := h.Get("Cache-Control"); got != "public, max-age=86400" {
t.Errorf("Cache-Control = %q", got)
}
if !strings.Contains(rec.Body.String(), `version:"0.16.11"`) {
t.Error("katex.min.js n'est pas KaTeX 0.16.11")
}
if h.Get("Content-Length") != strconv.Itoa(rec.Body.Len()) {
t.Errorf("Content-Length = %q pour %d octets", h.Get("Content-Length"), rec.Body.Len())
}
}
// Chaque police woff2 que la feuille de style demande est embarquée et servie.
func TestKatexFeuilleEtPolices(t *testing.T) {
rec := getKatex(t, http.MethodGet, "/katex/katex.min.css")
if rec.Code != http.StatusOK || rec.Header().Get("Content-Type") != "text/css; charset=utf-8" {
t.Fatalf("css : code %d, type %q", rec.Code, rec.Header().Get("Content-Type"))
}
polices := regexp.MustCompile(`url\((fonts/[^)]+\.woff2)\)`).FindAllStringSubmatch(rec.Body.String(), -1)
if len(polices) < 10 {
t.Fatalf("%d polices woff2 dans la feuille", len(polices))
}
for _, m := range polices {
r := getKatex(t, http.MethodGet, "/katex/"+m[1])
if r.Code != http.StatusOK || r.Header().Get("Content-Type") != "font/woff2" || r.Body.Len() == 0 {
t.Errorf("%s : code %d, type %q", m[1], r.Code, r.Header().Get("Content-Type"))
}
}
}
// Rien d'autre : ni index de dossier, ni licence, ni remontée, ni POST.
func TestKatexRefus(t *testing.T) {
for _, c := range []struct{ méthode, chemin string }{
{http.MethodGet, "/katex/"},
{http.MethodGet, "/katex/fonts/"},
{http.MethodGet, "/katex/LICENSE"},
{http.MethodGet, "/katex/fonts/absente.woff2"},
{http.MethodGet, "/katex/../index.html"},
{http.MethodGet, "/katex/fonts/../katex.min.js"},
{http.MethodGet, "/katexkatex.min.js"},
{http.MethodPost, "/katex/katex.min.js"},
} {
if rec := getKatex(t, c.méthode, c.chemin); rec.Code != http.StatusNotFound {
t.Errorf("%s %s : code %d, attendu 404", c.méthode, c.chemin, rec.Code)
}
}
}
// Derrière gzipHandler (comme en production) : script et feuille compressés,
// polices non — comme la façade.
func TestKatexCompression(t *testing.T) {
h := gzipHandler(http.HandlerFunc(handleKatex))
for chemin, gz := range map[string]bool{
"/katex/katex.min.js": true,
"/katex/katex.min.css": true,
"/katex/fonts/KaTeX_Main-Regular.woff2": false,
} {
req := httptest.NewRequest(http.MethodGet, chemin, nil)
req.Header.Set("Accept-Encoding", "gzip")
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if got := rec.Header().Get("Content-Encoding") == "gzip"; got != gz {
t.Errorf("%s : gzip %v, attendu %v", chemin, got, gz)
}
}
}
+4 -1
View File
@@ -19,7 +19,7 @@ import (
)
//go:generate go run ../../tools/assemble-ui ui
//go:embed ui/index.html ui/marked.min.js ui/sw.js ui/manifest.webmanifest ui/fonts/*.woff2
//go:embed ui/index.html ui/marked.min.js ui/sw.js ui/manifest.webmanifest ui/fonts/*.woff2 ui/katex
var uiFS embed.FS
// cmdWeb starts the HTTP server on the given port (default 8090).
@@ -217,6 +217,9 @@ func newWebMux() *http.ServeMux {
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
w.Write(b)
})
// KaTeX (formules LaTeX des réponses, web_katex.go) : chargé par l'UI
// seulement quand une réponse contient des maths. Embarqué, comme les polices.
mux.HandleFunc("/katex/", handleKatex)
// Endpoint compatible OpenAI, servi par Loki SUR SON PORT et relayé vers
// llama-server (voir mountOAI). C'est ce qui le rend joignable par le nom de
// domaine ou l'IP de l'interface, sans publier de second port ni ouvrir le
+26
View File
@@ -42,6 +42,32 @@ fn main() {
}
out.push_str("];\n");
// KaTeX (formules LaTeX, repris d'AJEAN) : script, feuille et polices
// woff2 sous ui/katex/, les polices nommées par leur chemin relatif
// (« fonts/x.woff2 »), comme les sert web_katex.go.
let katex = ui.join("katex");
println!("cargo:rerun-if-changed={}", katex.display());
for (ident, name) in [("KATEX_JS", "katex.min.js"), ("KATEX_CSS", "katex.min.css")] {
let p = katex.join(name);
assert!(p.is_file(), "fichier KaTeX manquant : {}", p.display());
println!("cargo:rerun-if-changed={}", p.display());
writeln!(out, "pub static {ident}: &[u8] = include_bytes!({p:?});").unwrap();
}
let katex_fonts = katex.join("fonts");
println!("cargo:rerun-if-changed={}", katex_fonts.display());
let mut kfonts: Vec<PathBuf> = std::fs::read_dir(&katex_fonts)
.unwrap_or_else(|e| panic!("{} : {e}", katex_fonts.display()))
.filter_map(|e| e.ok().map(|e| e.path()))
.filter(|p| p.extension().is_some_and(|x| x == "woff2"))
.collect();
kfonts.sort();
out.push_str("pub static KATEX_FONTS: &[(&str, &[u8])] = &[\n");
for p in &kfonts {
let name = format!("fonts/{}", p.file_name().unwrap().to_str().unwrap());
writeln!(out, " ({name:?}, include_bytes!({p:?})),").unwrap();
}
out.push_str("];\n");
let dst = PathBuf::from(std::env::var("OUT_DIR").unwrap()).join("ui_assets.rs");
std::fs::write(dst, out).unwrap();
}
+61
View File
@@ -48,6 +48,10 @@ static MANIFEST: Asset = Asset::new(
"application/manifest+json",
"public, max-age=3600",
);
/// KaTeX (formules LaTeX des réponses) : mêmes en-têtes que `web_katex.go`.
const KATEX_CACHE: &str = "public, max-age=86400";
static KATEX_JS: Asset = Asset::new(assets::KATEX_JS, "application/javascript", KATEX_CACHE);
static KATEX_CSS: Asset = Asset::new(assets::KATEX_CSS, "text/css; charset=utf-8", KATEX_CACHE);
/// Réponse statique pour cette requête, ou `None` si la route n'est pas une
/// ressource de l'UI (elle part alors au Go, qui rendra son 404 habituel).
@@ -76,6 +80,9 @@ pub fn serve(method: &Method, path: &str, headers: &HeaderMap) -> Option<Respons
}
"/manifest.webmanifest" => asset(&MANIFEST, accepts_gzip),
p if p.starts_with("/fonts/") => font(p)?,
"/katex/katex.min.js" => asset(&KATEX_JS, accepts_gzip),
"/katex/katex.min.css" => asset(&KATEX_CSS, accepts_gzip),
p if p.starts_with("/katex/") => katex_font(p)?,
_ => return None,
};
if accepts_gzip {
@@ -130,6 +137,20 @@ fn font(path: &str) -> Option<Response<Body>> {
Some(r)
}
/// `/katex/fonts/<nom>.woff2` — chemin exact (pas le dernier segment seul,
/// contrairement à `/fonts/`), comme `katexFile` dans `web_katex.go`. Le
/// reste de `/katex/` part au Go, qui rend son 404.
fn katex_font(path: &str) -> Option<Response<Body>> {
let rel = path.strip_prefix("/katex/")?;
let (_, body) = assets::KATEX_FONTS.iter().find(|(n, _)| *n == rel)?;
let mut r = Response::new(full(Bytes::from_static(body)));
let h = r.headers_mut();
h.insert(header::CONTENT_TYPE, HeaderValue::from_static("font/woff2"));
h.insert(header::CACHE_CONTROL, HeaderValue::from_static(KATEX_CACHE));
h.insert(header::CONTENT_LENGTH, HeaderValue::from(body.len()));
Some(r)
}
fn compress(b: &[u8]) -> Vec<u8> {
let mut enc = GzEncoder::new(Vec::with_capacity(b.len() / 3), Compression::default());
enc.write_all(b).expect("gzip en mémoire");
@@ -193,6 +214,46 @@ mod tests {
assert!(get("/fonts/inter.ttf", false).is_none());
}
#[test]
fn katex() {
let r = get("/katex/katex.min.js", true).unwrap();
assert_eq!(hv(&r, "content-type"), "application/javascript");
assert_eq!(hv(&r, "cache-control"), "public, max-age=86400");
assert_eq!(hv(&r, "content-encoding"), "gzip");
assert!(
String::from_utf8_lossy(assets::KATEX_JS).contains("version:\"0.16.11\""),
"KaTeX 0.16.11 attendu"
);
let r = get("/katex/katex.min.css", false).unwrap();
assert_eq!(hv(&r, "content-type"), "text/css; charset=utf-8");
// Chaque police woff2 demandée par la feuille est servie, non compressée.
let css = String::from_utf8_lossy(assets::KATEX_CSS);
let polices: Vec<&str> = css
.split("url(")
.skip(1)
.filter_map(|s| s.split(')').next())
.filter(|u| u.ends_with(".woff2"))
.collect();
assert!(polices.len() >= 10, "{} polices", polices.len());
for p in polices {
let r = get(&format!("/katex/{p}"), true).unwrap_or_else(|| panic!("{p}"));
assert_eq!(hv(&r, "content-type"), "font/woff2");
assert_eq!(hv(&r, "content-encoding"), "");
assert_eq!(hv(&r, "cache-control"), "public, max-age=86400");
}
// Le reste part au Go (404) : ni index, ni licence, ni dernier segment seul.
for p in [
"/katex/",
"/katex/LICENSE",
"/katex/fonts/",
"/katex/fonts/absente.woff2",
"/katex/KaTeX_Main-Regular.woff2",
"/katex/x/fonts/KaTeX_Main-Regular.woff2",
] {
assert!(get(p, false).is_none(), "{p}");
}
}
#[test]
fn gzip_si_accepte() {
let r = get("/", true).unwrap();
+125 -125
View File
@@ -231,8 +231,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
},
@@ -260,7 +260,7 @@
},
{
"body": {
"content": "<163664 octets b7452851885a>"
"content": "<163781 octets 608b16075e1c>"
},
"method": "POST",
"path": "/tokenize"
@@ -304,7 +304,7 @@
"max_tokens": 300,
"messages": [
{
"content": "<8144 octets 5c98ee6b6194>",
"content": "<8103 octets e6352865574a>",
"role": "user"
}
],
@@ -335,8 +335,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -358,8 +358,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -387,8 +387,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -410,8 +410,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -433,8 +433,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -542,8 +542,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
},
@@ -571,7 +571,7 @@
},
{
"body": {
"content": "<163664 octets b7452851885a>"
"content": "<163781 octets 608b16075e1c>"
},
"method": "POST",
"path": "/tokenize"
@@ -615,7 +615,7 @@
"max_tokens": 300,
"messages": [
{
"content": "<8144 octets 5c98ee6b6194>",
"content": "<8103 octets e6352865574a>",
"role": "user"
}
],
@@ -855,8 +855,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -947,8 +947,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -993,8 +993,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
},
@@ -1022,7 +1022,7 @@
},
{
"body": {
"content": "<163664 octets b7452851885a>"
"content": "<163781 octets 608b16075e1c>"
},
"method": "POST",
"path": "/tokenize"
@@ -1066,7 +1066,7 @@
"max_tokens": 300,
"messages": [
{
"content": "<8144 octets 5c98ee6b6194>",
"content": "<8103 octets e6352865574a>",
"role": "user"
}
],
@@ -1103,8 +1103,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -1132,8 +1132,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -1241,8 +1241,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
},
@@ -1270,7 +1270,7 @@
},
{
"body": {
"content": "<163664 octets b7452851885a>"
"content": "<163781 octets 608b16075e1c>"
},
"method": "POST",
"path": "/tokenize"
@@ -1314,7 +1314,7 @@
"max_tokens": 300,
"messages": [
{
"content": "<8144 octets 5c98ee6b6194>",
"content": "<8103 octets e6352865574a>",
"role": "user"
}
],
@@ -1554,8 +1554,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -1646,8 +1646,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -1686,8 +1686,8 @@
"predicted_ms": 81920,
"predicted_n": 4096,
"predicted_per_second": 50,
"prompt_ms": 16402,
"prompt_n": 32804,
"prompt_ms": 16400.5,
"prompt_n": 32801,
"prompt_per_second": 2000,
"protocol": 2
},
@@ -1715,7 +1715,7 @@
},
{
"body": {
"content": "<163664 octets b7452851885a>"
"content": "<163781 octets 608b16075e1c>"
},
"method": "POST",
"path": "/tokenize"
@@ -1759,7 +1759,7 @@
"max_tokens": 4096,
"messages": [
{
"content": "<131201 octets e3226a117903>",
"content": "<131191 octets 64f00ccc3d63>",
"role": "user"
}
],
@@ -1790,8 +1790,8 @@
"predicted_ms": 81920,
"predicted_n": 4096,
"predicted_per_second": 50,
"prompt_ms": 16402,
"prompt_n": 32804,
"prompt_ms": 16400.5,
"prompt_n": 32801,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -1813,8 +1813,8 @@
"predicted_ms": 81920,
"predicted_n": 4096,
"predicted_per_second": 50,
"prompt_ms": 16402,
"prompt_n": 32804,
"prompt_ms": 16400.5,
"prompt_n": 32801,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -1882,7 +1882,7 @@
},
{
"body": {
"content": "<163664 octets b7452851885a>"
"content": "<163781 octets 608b16075e1c>"
},
"method": "POST",
"path": "/tokenize"
@@ -2049,7 +2049,7 @@
},
{
"body": {
"content": "<163664 octets b7452851885a>"
"content": "<163781 octets 608b16075e1c>"
},
"method": "POST",
"path": "/tokenize"
@@ -2093,7 +2093,7 @@
"max_tokens": 16,
"messages": [
{
"content": "<3745 octets 55a9fdff5d02>",
"content": "<3744 octets dff015dced82>",
"role": "user"
}
],
@@ -2187,8 +2187,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
},
@@ -2216,7 +2216,7 @@
},
{
"body": {
"content": "<163664 octets b7452851885a>"
"content": "<163781 octets 608b16075e1c>"
},
"method": "POST",
"path": "/tokenize"
@@ -2260,7 +2260,7 @@
"max_tokens": 300,
"messages": [
{
"content": "<8144 octets 5c98ee6b6194>",
"content": "<8103 octets e6352865574a>",
"role": "user"
}
],
@@ -2291,8 +2291,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -2314,8 +2314,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -2368,7 +2368,7 @@
},
{
"body": {
"content": "<163664 octets b7452851885a>"
"content": "<163781 octets 608b16075e1c>"
},
"method": "POST",
"path": "/tokenize"
@@ -2420,8 +2420,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -2443,8 +2443,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -2497,7 +2497,7 @@
},
{
"body": {
"content": "<163664 octets b7452851885a>"
"content": "<163781 octets 608b16075e1c>"
},
"method": "POST",
"path": "/tokenize"
@@ -2541,7 +2541,7 @@
"max_tokens": 300,
"messages": [
{
"content": "<8144 octets 5c98ee6b6194>",
"content": "<8103 octets e6352865574a>",
"role": "user"
}
],
@@ -2572,8 +2572,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -2595,8 +2595,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -2649,7 +2649,7 @@
},
{
"body": {
"content": "<163664 octets b7452851885a>"
"content": "<163781 octets 608b16075e1c>"
},
"method": "POST",
"path": "/tokenize"
@@ -2693,7 +2693,7 @@
"max_tokens": 300,
"messages": [
{
"content": "<8144 octets 5c98ee6b6194>",
"content": "<8103 octets e6352865574a>",
"role": "user"
}
],
@@ -2747,8 +2747,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -2770,8 +2770,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -2824,7 +2824,7 @@
},
{
"body": {
"content": "<163664 octets b7452851885a>"
"content": "<163781 octets 608b16075e1c>"
},
"method": "POST",
"path": "/tokenize"
@@ -2868,7 +2868,7 @@
"max_tokens": 300,
"messages": [
{
"content": "<8144 octets 5c98ee6b6194>",
"content": "<8103 octets e6352865574a>",
"role": "user"
}
],
@@ -2992,8 +2992,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -3015,8 +3015,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -3069,7 +3069,7 @@
},
{
"body": {
"content": "<163664 octets b7452851885a>"
"content": "<163781 octets 608b16075e1c>"
},
"method": "POST",
"path": "/tokenize"
@@ -3121,8 +3121,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -3144,8 +3144,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -3189,8 +3189,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
},
@@ -3218,7 +3218,7 @@
},
{
"body": {
"content": "<163664 octets b7452851885a>"
"content": "<163781 octets 608b16075e1c>"
},
"method": "POST",
"path": "/tokenize"
@@ -3287,7 +3287,7 @@
"max_tokens": 300,
"messages": [
{
"content": "<8144 octets 5c98ee6b6194>",
"content": "<8103 octets e6352865574a>",
"role": "user"
}
],
@@ -3319,8 +3319,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -3342,8 +3342,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -3383,8 +3383,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
},
@@ -3412,7 +3412,7 @@
},
{
"body": {
"content": "<163664 octets b7452851885a>"
"content": "<163781 octets 608b16075e1c>"
},
"method": "POST",
"path": "/tokenize"
@@ -3457,7 +3457,7 @@
"max_tokens": 300,
"messages": [
{
"content": "<8144 octets 5c98ee6b6194>",
"content": "<8103 octets e6352865574a>",
"role": "user"
}
],
@@ -3489,8 +3489,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -3512,8 +3512,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -3579,8 +3579,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
},
@@ -3608,7 +3608,7 @@
},
{
"body": {
"content": "<163664 octets b7452851885a>"
"content": "<163781 octets 608b16075e1c>"
},
"method": "POST",
"path": "/tokenize"
@@ -3652,7 +3652,7 @@
"max_tokens": 300,
"messages": [
{
"content": "<8144 octets 5c98ee6b6194>",
"content": "<8103 octets e6352865574a>",
"role": "user"
}
],
@@ -3737,8 +3737,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -3760,8 +3760,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -3823,7 +3823,7 @@
},
{
"body": {
"content": "<163664 octets b7452851885a>"
"content": "<163781 octets 608b16075e1c>"
},
"method": "POST",
"path": "/tokenize"
@@ -3867,7 +3867,7 @@
"max_tokens": 300,
"messages": [
{
"content": "<8144 octets 5c98ee6b6194>",
"content": "<8103 octets e6352865574a>",
"role": "user"
}
],
@@ -3952,8 +3952,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -3975,8 +3975,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -4034,8 +4034,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -4057,8 +4057,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -4089,8 +4089,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -4112,8 +4112,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -4135,8 +4135,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
"prompt_ms": 1020,
"prompt_n": 2040,
"prompt_ms": 1014.5,
"prompt_n": 2029,
"prompt_per_second": 2000,
"protocol": 2
}