diff --git a/internal/loki/backend_models.go b/internal/loki/backend_models.go
index e226007..601fd35 100644
--- a/internal/loki/backend_models.go
+++ b/internal/loki/backend_models.go
@@ -282,10 +282,96 @@ type dlState struct {
Part int `json:"part"` // tranche en cours (1-based)
Finished bool `json:"finished"`
Canceled bool `json:"canceled"`
+ Paused bool `json:"paused"`
Err string `json:"error"`
StartedAt int64 `json:"started_at"`
cancel context.CancelFunc `json:"-"` // set while in flight, cleared on finish
+ gate *dlGate `json:"-"`
+}
+
+// dlGate met un téléchargement en pause (repris d'AJEAN v0.18.1) : les
+// connexions en cours sont coupées (rien ne reste ouvert vers le CDN pendant la
+// pause), et chaque morceau attend la reprise puis repart de l'octet où il
+// s'était arrêté, par une requête de plage.
+type dlGate struct {
+ mu sync.Mutex
+ paused bool
+ wake chan struct{} // fermé à la reprise
+ attempt map[int]context.CancelFunc // requêtes en cours, coupées par la pause
+ next int
+}
+
+func newDlGate() *dlGate { return &dlGate{attempt: map[int]context.CancelFunc{}} }
+
+func (g *dlGate) setPaused(p bool) {
+ if g == nil {
+ return
+ }
+ g.mu.Lock()
+ defer g.mu.Unlock()
+ if p == g.paused {
+ return
+ }
+ g.paused = p
+ if p {
+ g.wake = make(chan struct{})
+ for _, c := range g.attempt {
+ c()
+ }
+ return
+ }
+ close(g.wake)
+}
+
+// wait bloque tant que le téléchargement est en pause. false = annulé entre-temps.
+func (g *dlGate) wait(ctx context.Context) bool {
+ if g == nil {
+ return ctx.Err() == nil
+ }
+ g.mu.Lock()
+ paused, wake := g.paused, g.wake
+ g.mu.Unlock()
+ if !paused {
+ return ctx.Err() == nil
+ }
+ select {
+ case <-wake:
+ return ctx.Err() == nil
+ case <-ctx.Done():
+ return false
+ }
+}
+
+func (g *dlGate) isPaused() bool {
+ if g == nil {
+ return false
+ }
+ g.mu.Lock()
+ defer g.mu.Unlock()
+ return g.paused
+}
+
+// begin ouvre une requête que la pause pourra couper ; la fonction rendue la referme.
+func (g *dlGate) begin(ctx context.Context) (context.Context, func()) {
+ if g == nil {
+ return ctx, func() {}
+ }
+ actx, cancel := context.WithCancel(ctx)
+ g.mu.Lock()
+ id := g.next
+ g.next++
+ g.attempt[id] = cancel
+ if g.paused {
+ cancel() // pause tombée entre wait et begin
+ }
+ g.mu.Unlock()
+ return actx, func() {
+ g.mu.Lock()
+ delete(g.attempt, id)
+ g.mu.Unlock()
+ cancel()
+ }
}
var (
@@ -429,7 +515,7 @@ func handleModelDownload(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithCancel(context.Background())
st := &dlState{
Filename: name, URL: urls[0], Dir: filepath.Dir(dests[0]),
- Parts: len(todoURLs), StartedAt: time.Now().Unix(), cancel: cancel,
+ Parts: len(todoURLs), StartedAt: time.Now().Unix(), cancel: cancel, gate: newDlGate(),
}
dlDownloads[name] = st
dlMu.Unlock()
@@ -759,7 +845,9 @@ func runDownloadSet(ctx context.Context, st *dlState, urls, dests []string) {
dt := now.Sub(lastAt).Seconds()
dlMu.Lock()
st.Done = cur
- if dt > 0 {
+ if st.Paused {
+ st.Speed = 0
+ } else if dt > 0 {
inst := int64(float64(cur-last) / dt)
if st.Speed == 0 {
st.Speed = inst
@@ -827,7 +915,7 @@ func dlOnePart(ctx context.Context, st *dlState, dlURL, dest string, total int64
return err
}
}
- if err := dlFetch(ctx, f, dlURL, total, conns, done); err != nil {
+ if err := dlFetch(ctx, st.gate, f, dlURL, total, conns, done); err != nil {
f.Close()
_ = os.Remove(tmp)
return err
@@ -845,9 +933,9 @@ func dlOnePart(ctx context.Context, st *dlState, dlURL, dest string, total int64
// dlFetch writes the whole body into f, either as one stream or as `conns`
// parallel byte ranges. done is incremented atomically as bytes land on disk.
-func dlFetch(ctx context.Context, f *os.File, dlURL string, total int64, conns int, done *int64) error {
+func dlFetch(ctx context.Context, g *dlGate, f *os.File, dlURL string, total int64, conns int, done *int64) error {
if conns <= 1 {
- return dlChunk(ctx, f, dlURL, 0, total-1, total <= 0, done)
+ return dlChunk(ctx, g, f, dlURL, 0, total-1, total <= 0, done)
}
size := total / int64(conns)
var wg sync.WaitGroup
@@ -861,7 +949,7 @@ func dlFetch(ctx context.Context, f *os.File, dlURL string, total int64, conns i
wg.Add(1)
go func(i int, start, end int64) {
defer wg.Done()
- errs[i] = dlChunk(ctx, f, dlURL, start, end, false, done)
+ errs[i] = dlChunk(ctx, g, f, dlURL, start, end, false, done)
}(i, start, end)
}
wg.Wait()
@@ -876,11 +964,15 @@ func dlFetch(ctx context.Context, f *os.File, dlURL string, total int64, conns i
// dlChunk downloads [start,end] into f at the right offset, retrying from where
// it stopped if the connection drops mid-chunk. With whole=true it streams the
// entire body sequentially (server without range support, unknown size).
-func dlChunk(ctx context.Context, f *os.File, dlURL string, start, end int64, whole bool, done *int64) error {
+func dlChunk(ctx context.Context, g *dlGate, f *os.File, dlURL string, start, end int64, whole bool, done *int64) error {
const attempts = 4
pos := start
var lastErr error
for try := 0; try < attempts; try++ {
+ // Une pause n'est pas un échec : on attend la reprise sans consommer d'essai.
+ if !g.wait(ctx) {
+ return ctx.Err()
+ }
if err := ctx.Err(); err != nil {
return err // cancelled: never retry
}
@@ -900,18 +992,26 @@ func dlChunk(ctx context.Context, f *os.File, dlURL string, start, end int64, wh
} else if pos > start {
rng = fmt.Sprintf("bytes=%d-", pos) // best-effort resume
}
- req, err := dlRequest(ctx, dlURL, rng)
+ actx, release := g.begin(ctx)
+ req, err := dlRequest(actx, dlURL, rng)
if err != nil {
+ release()
return err
}
resp, err := dlClient.Do(req)
if err != nil {
+ release()
+ if g.isPaused() && ctx.Err() == nil {
+ try--
+ continue
+ }
lastErr = err
continue
}
if resp.StatusCode != 200 && resp.StatusCode != 206 {
err := dlSourceError(resp, dlURL)
resp.Body.Close()
+ release()
return err
}
if resp.StatusCode == 200 && pos > start {
@@ -921,10 +1021,15 @@ func dlChunk(ctx context.Context, f *os.File, dlURL string, start, end int64, wh
}
n, cerr := dlCopy(f, resp.Body, pos, done)
resp.Body.Close()
+ release()
pos += n
if cerr == nil {
return nil
}
+ if g.isPaused() && ctx.Err() == nil {
+ try-- // coupé par la pause : la reprise repart de pos
+ continue
+ }
lastErr = cerr
}
return lastErr
@@ -981,6 +1086,35 @@ func handleModelDownloadCancel(w http.ResponseWriter, r *http.Request) {
sendJSON(w, 200, map[string]any{"ok": true})
}
+// handleModelDownloadPause met en pause ({"pause":true}) ou reprend
+// ({"pause":false}) un téléchargement en cours. Le fichier partiel est gardé.
+func handleModelDownloadPause(w http.ResponseWriter, r *http.Request) {
+ var req struct {
+ Filename string `json:"filename"`
+ Pause bool `json:"pause"`
+ }
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ sendJSON(w, 400, map[string]any{"ok": false, "error": err.Error()})
+ return
+ }
+ name := filepath.Base(strings.TrimSpace(req.Filename))
+ dlMu.Lock()
+ st, ok := dlDownloads[name]
+ if !ok || st.Finished {
+ dlMu.Unlock()
+ sendJSON(w, 404, map[string]any{"ok": false, "error": "aucun téléchargement en cours pour " + name})
+ return
+ }
+ st.Paused = req.Pause
+ if req.Pause {
+ st.Speed = 0
+ }
+ g := st.gate
+ dlMu.Unlock()
+ g.setPaused(req.Pause)
+ sendJSON(w, 200, map[string]any{"ok": true, "paused": req.Pause})
+}
+
// cleanStalePartFiles removes leftover *.gguf.part files in every model dir at
// startup. A download killed by a crash or a service restart can't be resumed
// (its state lived in memory), so the partial file would otherwise sit there
diff --git a/internal/loki/backend_models_dl_test.go b/internal/loki/backend_models_dl_test.go
index 6f6a8c2..7de5107 100644
--- a/internal/loki/backend_models_dl_test.go
+++ b/internal/loki/backend_models_dl_test.go
@@ -231,3 +231,96 @@ func TestDLProbeSurfacesExplainedError(t *testing.T) {
t.Errorf("message %q sans indication sur le jeton", err)
}
}
+
+type slowWriter struct{ http.ResponseWriter }
+
+func (s slowWriter) Write(b []byte) (int, error) {
+ time.Sleep(2 * time.Millisecond)
+ return s.ResponseWriter.Write(b)
+}
+
+// Une pause coupe le flux, garde ce qui est écrit, et la reprise termine un
+// fichier identique à la source (repris d'AJEAN v0.18.1).
+func TestRunDownloadPauseResume(t *testing.T) {
+ data := make([]byte, 48<<20)
+ rand.New(rand.NewSource(2)).Read(data)
+ t.Setenv("LOKI_DL_CONNS", "4")
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ http.ServeContent(slowWriter{w}, r, "m.gguf", time.Time{}, bytes.NewReader(data))
+ }))
+ defer srv.Close()
+
+ dest := filepath.Join(t.TempDir(), "m.gguf")
+ st := &dlState{Filename: "m.gguf", gate: newDlGate()}
+ done := make(chan struct{})
+ go func() {
+ runDownloadSet(context.Background(), st, []string{srv.URL + "/m.gguf"}, []string{dest})
+ close(done)
+ }()
+
+ time.Sleep(300 * time.Millisecond)
+ st.gate.setPaused(true)
+ time.Sleep(1500 * time.Millisecond) // le compteur publié se fige
+ dlMu.Lock()
+ a := st.Done
+ dlMu.Unlock()
+ time.Sleep(1500 * time.Millisecond)
+ dlMu.Lock()
+ b, fin := st.Done, st.Finished
+ dlMu.Unlock()
+ if fin || a != b || a == 0 {
+ t.Fatalf("pause inopérante : %d puis %d octets, terminé=%v", a, b, fin)
+ }
+ st.gate.setPaused(false)
+ select {
+ case <-done:
+ case <-time.After(30 * time.Second):
+ t.Fatal("pas terminé après la reprise")
+ }
+ if st.Err != "" {
+ t.Fatalf("erreur après reprise : %s", st.Err)
+ }
+ got, err := os.ReadFile(dest)
+ if err != nil || !bytes.Equal(got, data) {
+ t.Fatalf("fichier différent de la source après reprise (err=%v)", err)
+ }
+}
+
+// La route : pause puis reprise d'un téléchargement connu, refus pour
+// l'inconnu et pour un téléchargement terminé.
+func TestHandleModelDownloadPause(t *testing.T) {
+ dlMu.Lock()
+ saved := dlDownloads
+ dlDownloads = map[string]*dlState{
+ "a.gguf": {Filename: "a.gguf", Speed: 9, gate: newDlGate()},
+ "b.gguf": {Filename: "b.gguf", Finished: true},
+ }
+ dlMu.Unlock()
+ t.Cleanup(func() { dlMu.Lock(); dlDownloads = saved; dlMu.Unlock() })
+ post := func(body string) (int, string) {
+ w := httptest.NewRecorder()
+ handleModelDownloadPause(w, httptest.NewRequest("POST", "/api/models/download/pause", strings.NewReader(body)))
+ return w.Code, strings.TrimSpace(w.Body.String())
+ }
+ if code, out := post(`{"filename":"../a.gguf","pause":true}`); code != 200 || out != `{"ok":true,"paused":true}` {
+ t.Fatalf("pause : %d %s", code, out)
+ }
+ dlMu.Lock()
+ st := dlDownloads["a.gguf"]
+ paused, speed := st.Paused, st.Speed
+ dlMu.Unlock()
+ if !paused || speed != 0 || !st.gate.isPaused() {
+ t.Errorf("état après pause : paused=%v speed=%d porte=%v", paused, speed, st.gate.isPaused())
+ }
+ if code, out := post(`{"filename":"a.gguf","pause":false}`); code != 200 || out != `{"ok":true,"paused":false}` || st.gate.isPaused() {
+ t.Fatalf("reprise : %d %s", code, out)
+ }
+ for _, body := range []string{`{"filename":"b.gguf","pause":true}`, `{"filename":"x.gguf","pause":true}`} {
+ if code, _ := post(body); code != 404 {
+ t.Errorf("%s : %d, attendu 404", body, code)
+ }
+ }
+ if code, _ := post(`[`); code != 400 {
+ t.Errorf("corps illisible : %d", code)
+ }
+}
diff --git a/internal/loki/facade_chat_test.go b/internal/loki/facade_chat_test.go
index 184110c..31a692c 100644
--- a/internal/loki/facade_chat_test.go
+++ b/internal/loki/facade_chat_test.go
@@ -300,7 +300,7 @@ func TestFacadeRelaieLesModeles(t *testing.T) {
local := func(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "go") }
routes := []string{"/api/models/download", "/api/models/download/probe",
- "/api/models/download/status", "/api/models/download/cancel", "/api/models",
+ "/api/models/download/status", "/api/models/download/cancel", "/api/models/download/pause", "/api/models",
"/api/models/delete", "/api/models/dirs", "/api/hf/search", "/api/hf/files", "/api/hf/token"}
for _, p := range routes {
w := httptest.NewRecorder()
diff --git a/internal/loki/facade_models.go b/internal/loki/facade_models.go
index 8c0f776..ef6c54e 100644
--- a/internal/loki/facade_models.go
+++ b/internal/loki/facade_models.go
@@ -49,4 +49,5 @@ var facadeModelRoutes = map[string]bool{
"/api/models/download/probe": true,
"/api/models/download/status": true,
"/api/models/download/cancel": true,
+ "/api/models/download/pause": true,
}
diff --git a/internal/loki/ui/index.html b/internal/loki/ui/index.html
index 872336f..ab81304 100644
--- a/internal/loki/ui/index.html
+++ b/internal/loki/ui/index.html
@@ -3500,6 +3500,7 @@ html[data-files="1"] #files-btn{color:var(--accent)}
+
@@ -6754,6 +6755,7 @@ function dlEls(){
prog: document.getElementById('m-hf-progress'),
bar: document.getElementById('m-hf-bar'),
cancel: document.getElementById('m-hf-cancel'),
+ pause: document.getElementById('m-hf-pause'),
};
}
// Arrête le poll local sans toucher au téléchargement serveur.
@@ -6763,7 +6765,7 @@ function resetDlUI(){
const e = dlEls();
stopDlPoll(); dlName='';
e.prog.style.display='none'; e.bar.style.display='none';
- e.cancel.style.display='none'; e.btn.disabled=false;
+ e.cancel.style.display='none'; e.pause.style.display='none'; e.btn.disabled=false;
}
// Remplit le sélecteur de destination avec les dossiers de modèles connus.
// L'espace libre est dans le libellé de l'option : c'est l'info utile au moment
@@ -6880,6 +6882,22 @@ async function cancelDownload(){
await jpost('/api/models/download/cancel', {filename: dlName});
// L'état « annulé » est confirmé par le prochain tick de poll.
}
+// Met en pause ou reprend : le fichier partiel est gardé, la reprise repart
+// de là où le transfert s'était arrêté (repris d'AJEAN v0.18.1).
+let dlPaused = false;
+async function togglePauseDownload(){
+ if(!dlName) return;
+ const e = dlEls();
+ e.pause.disabled = true;
+ try{
+ const r = await jpost('/api/models/download/pause', {filename: dlName, pause: !dlPaused});
+ if(r.ok) setDlPaused(r.paused);
+ }finally{ e.pause.disabled = false; }
+}
+function setDlPaused(p){
+ dlPaused = !!p;
+ dlEls().pause.textContent = dlPaused ? 'Reprendre' : 'Mettre en pause';
+}
// Suit un téléchargement (nouveau ou déjà en cours) et pilote barre + texte.
function watchDownload(fname){
const e = dlEls();
@@ -6890,7 +6908,10 @@ function watchDownload(fname){
e.bar.style.display = 'block';
e.cancel.style.display = 'inline';
e.cancel.disabled = false;
- const stop = ()=>{ stopDlPoll(); dlName=''; e.btn.disabled=false; e.cancel.style.display='none'; };
+ e.pause.style.display = 'inline';
+ e.pause.disabled = false;
+ setDlPaused(false);
+ const stop = ()=>{ stopDlPoll(); dlName=''; e.btn.disabled=false; e.cancel.style.display='none'; e.pause.style.display='none'; };
const tick = async ()=>{
const list = await jget('/api/models/download/status');
const st = (list||[]).find(d=>d.filename===fname);
@@ -6914,6 +6935,7 @@ function watchDownload(fname){
if(dlQueue.length) startDownloadURL(dlQueue.shift());
return;
}
+ if(!!st.paused !== dlPaused) setDlPaused(st.paused);
const pct = st.total>0 ? st.done*100/st.total : 0;
if(st.total>0){
e.bar.className = 'pe-bar';
@@ -6923,7 +6945,8 @@ function watchDownload(fname){
}
const tot = st.total>0 ? ' / '+fmtSize(st.total)+' ('+Math.round(pct)+'%)' : '';
let extra = '';
- if(st.speed>0){
+ if(st.paused) extra += ' — en pause';
+ else if(st.speed>0){
extra += ' — '+fmtSize(st.speed)+'/s';
if(st.total>0){
const eta = Math.max(0, Math.round((st.total-st.done)/st.speed));
diff --git a/internal/loki/ui/src/index.tmpl.html b/internal/loki/ui/src/index.tmpl.html
index e405561..91dc1a7 100644
--- a/internal/loki/ui/src/index.tmpl.html
+++ b/internal/loki/ui/src/index.tmpl.html
@@ -1215,6 +1215,7 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
+
diff --git a/internal/loki/ui/src/js/07-models.js b/internal/loki/ui/src/js/07-models.js
index 538a025..500d589 100644
--- a/internal/loki/ui/src/js/07-models.js
+++ b/internal/loki/ui/src/js/07-models.js
@@ -1501,6 +1501,7 @@ function dlEls(){
prog: document.getElementById('m-hf-progress'),
bar: document.getElementById('m-hf-bar'),
cancel: document.getElementById('m-hf-cancel'),
+ pause: document.getElementById('m-hf-pause'),
};
}
// Arrête le poll local sans toucher au téléchargement serveur.
@@ -1510,7 +1511,7 @@ function resetDlUI(){
const e = dlEls();
stopDlPoll(); dlName='';
e.prog.style.display='none'; e.bar.style.display='none';
- e.cancel.style.display='none'; e.btn.disabled=false;
+ e.cancel.style.display='none'; e.pause.style.display='none'; e.btn.disabled=false;
}
// Remplit le sélecteur de destination avec les dossiers de modèles connus.
// L'espace libre est dans le libellé de l'option : c'est l'info utile au moment
@@ -1627,6 +1628,22 @@ async function cancelDownload(){
await jpost('/api/models/download/cancel', {filename: dlName});
// L'état « annulé » est confirmé par le prochain tick de poll.
}
+// Met en pause ou reprend : le fichier partiel est gardé, la reprise repart
+// de là où le transfert s'était arrêté (repris d'AJEAN v0.18.1).
+let dlPaused = false;
+async function togglePauseDownload(){
+ if(!dlName) return;
+ const e = dlEls();
+ e.pause.disabled = true;
+ try{
+ const r = await jpost('/api/models/download/pause', {filename: dlName, pause: !dlPaused});
+ if(r.ok) setDlPaused(r.paused);
+ }finally{ e.pause.disabled = false; }
+}
+function setDlPaused(p){
+ dlPaused = !!p;
+ dlEls().pause.textContent = dlPaused ? 'Reprendre' : 'Mettre en pause';
+}
// Suit un téléchargement (nouveau ou déjà en cours) et pilote barre + texte.
function watchDownload(fname){
const e = dlEls();
@@ -1637,7 +1654,10 @@ function watchDownload(fname){
e.bar.style.display = 'block';
e.cancel.style.display = 'inline';
e.cancel.disabled = false;
- const stop = ()=>{ stopDlPoll(); dlName=''; e.btn.disabled=false; e.cancel.style.display='none'; };
+ e.pause.style.display = 'inline';
+ e.pause.disabled = false;
+ setDlPaused(false);
+ const stop = ()=>{ stopDlPoll(); dlName=''; e.btn.disabled=false; e.cancel.style.display='none'; e.pause.style.display='none'; };
const tick = async ()=>{
const list = await jget('/api/models/download/status');
const st = (list||[]).find(d=>d.filename===fname);
@@ -1661,6 +1681,7 @@ function watchDownload(fname){
if(dlQueue.length) startDownloadURL(dlQueue.shift());
return;
}
+ if(!!st.paused !== dlPaused) setDlPaused(st.paused);
const pct = st.total>0 ? st.done*100/st.total : 0;
if(st.total>0){
e.bar.className = 'pe-bar';
@@ -1670,7 +1691,8 @@ function watchDownload(fname){
}
const tot = st.total>0 ? ' / '+fmtSize(st.total)+' ('+Math.round(pct)+'%)' : '';
let extra = '';
- if(st.speed>0){
+ if(st.paused) extra += ' — en pause';
+ else if(st.speed>0){
extra += ' — '+fmtSize(st.speed)+'/s';
if(st.total>0){
const eta = Math.max(0, Math.round((st.total-st.done)/st.speed));
diff --git a/internal/loki/web_server.go b/internal/loki/web_server.go
index fd2a623..94aac50 100644
--- a/internal/loki/web_server.go
+++ b/internal/loki/web_server.go
@@ -259,6 +259,7 @@ func newWebMux() *http.ServeMux {
api("/api/models/download/probe", handleModelDownloadProbe) // taille + espace libre avant de lancer
api("/api/models/download/status", handleModelDownloadStatus)
api("/api/models/download/cancel", handleModelDownloadCancel)
+ api("/api/models/download/pause", handleModelDownloadPause)
api("/api/hf/search", handleHFSearch) // chercher un modèle GGUF sur Hugging Face
api("/api/hf/files", handleHFFiles) // quants et projecteurs d'un dépôt
api("/api/hf/token", handleHFToken) // jeton pour les dépôts à accès restreint
diff --git a/rust/loki-rs/src/downloads.rs b/rust/loki-rs/src/downloads.rs
index 44fe294..4b728a8 100644
--- a/rust/loki-rs/src/downloads.rs
+++ b/rust/loki-rs/src/downloads.rs
@@ -3,7 +3,7 @@
//! `backend_model_dirs.go` et `backend_hf.go` (lot 8 de la migration).
//!
//! Tenus par la façade quand `LOKI_MODELS=rust` (expérimental ; le Go
-//! par défaut) : `POST /api/models/download`, `/probe`, `/cancel`,
+//! par défaut) : `POST /api/models/download`, `/probe`, `/cancel`, `/pause`,
//! `GET /api/models/download/status`. Le Go remet alors à la façade ces
//! routes quand elles lui arrivent par le tunnel du relais, et ne vide plus
//! les `.part` au démarrage (c'est la façade qui le fait).
@@ -71,6 +71,7 @@ pub fn handles(path: &str) -> bool {
| "/api/models/download/probe"
| "/api/models/download/status"
| "/api/models/download/cancel"
+ | "/api/models/download/pause"
)
}
@@ -519,6 +520,7 @@ pub struct DlState {
pub part: i64,
pub finished: bool,
pub canceled: bool,
+ pub paused: bool,
#[serde(rename = "error")]
pub err: String,
pub started_at: i64,
@@ -527,6 +529,8 @@ pub struct DlState {
struct Entry {
state: Mutex
,
cancel: watch::Sender,
+ /// `dlGate` : vrai pendant une pause.
+ pause: watch::Sender,
}
impl Entry {
@@ -549,6 +553,37 @@ impl Stop {
}
}
+/// `dlGate` : la pause d'un téléchargement. Les requêtes en cours sont
+/// coupées (rien ne reste ouvert vers le CDN pendant la pause), et chaque
+/// morceau attend la reprise puis repart de l'octet où il s'était arrêté, par
+/// une requête de plage.
+#[derive(Clone)]
+struct Gate(watch::Receiver);
+
+impl Gate {
+ fn is_paused(&self) -> bool {
+ *self.0.borrow()
+ }
+ /// Attend la reprise ; `false` : annulé entre-temps.
+ async fn wait(&self, stop: &Stop) -> bool {
+ if self.is_paused() {
+ let mut rx = self.0.clone();
+ tokio::select! {
+ _ = rx.wait_for(|p| !*p) => {}
+ _ = stop.wait() => return false,
+ }
+ }
+ !stop.is_set()
+ }
+ /// Se résout quand la pause tombe (jamais si l'émetteur a disparu).
+ async fn paused(&self) {
+ let mut rx = self.0.clone();
+ if rx.wait_for(|p| *p).await.is_err() {
+ std::future::pending::<()>().await;
+ }
+ }
+}
+
/// Ce qu'un téléchargement doit savoir de la machine.
#[derive(Clone)]
pub struct DlEnv {
@@ -615,6 +650,28 @@ impl Downloads {
}
}
+ /// `handleModelDownloadPause` : met en pause ou reprend ; `false` :
+ /// inconnu ou terminé. Le fichier partiel est gardé.
+ pub fn pause(&self, name: &str, paused: bool) -> bool {
+ let Some(e) = self.map().get(name).cloned() else {
+ return false;
+ };
+ let running = e.with(|s| {
+ if s.finished {
+ return false;
+ }
+ s.paused = paused;
+ if paused {
+ s.speed = 0;
+ }
+ true
+ });
+ if running {
+ let _ = e.pause.send_replace(paused);
+ }
+ running
+ }
+
fn request(&self, env: &DlEnv, url: &str, range: Option<&str>) -> reqwest::RequestBuilder {
let mut rb = self
.client
@@ -722,6 +779,7 @@ impl Downloads {
);
}
let (tx, rx) = watch::channel(false);
+ let (pause_tx, pause_rx) = watch::channel(false);
let entry = Arc::new(Entry {
state: Mutex::new(DlState {
filename: name.clone(),
@@ -732,12 +790,14 @@ impl Downloads {
..Default::default()
}),
cancel: tx,
+ pause: pause_tx,
});
map.insert(name.clone(), entry.clone());
drop(map);
let parts = todo.len();
let me = self.clone();
- tokio::spawn(async move { me.run_set(env, entry, Stop(rx), todo).await });
+ let gate = Gate(pause_rx);
+ tokio::spawn(async move { me.run_set(env, entry, Stop(rx), gate, todo).await });
(
StatusCode::OK,
json!({ "ok": true, "filename": name, "parts": parts }),
@@ -752,6 +812,7 @@ impl Downloads {
env: DlEnv,
entry: Arc,
stop: Stop,
+ gate: Gate,
todo: Vec<(String, String)>,
) {
let done = Arc::new(AtomicI64::new(0));
@@ -812,7 +873,9 @@ impl Downloads {
let dt = now.duration_since(last_at).as_secs_f64();
entry.with(|s| {
s.done = cur;
- if dt > 0.0 {
+ if s.paused {
+ s.speed = 0;
+ } else if dt > 0.0 {
let inst = ((cur - last) as f64 / dt) as i64;
s.speed = if s.speed == 0 {
inst
@@ -829,7 +892,7 @@ impl Downloads {
for (i, ((u, dest), (total, ranged))) in todo.iter().zip(sizes).enumerate() {
entry.with(|s| s.part = i as i64 + 1);
if let Err(h) = self
- .one_part(&env, &entry, &stop, u, dest, total, ranged, &done)
+ .one_part(&env, &entry, &stop, &gate, u, dest, total, ranged, &done)
.await
{
ticker.abort();
@@ -851,6 +914,7 @@ impl Downloads {
env: &DlEnv,
entry: &Entry,
stop: &Stop,
+ gate: &Gate,
url: &str,
dest: &str,
total: i64,
@@ -877,7 +941,7 @@ impl Downloads {
path: tmp.clone(),
});
let res = if conns <= 1 {
- self.chunk(env, stop, &file, url, 0, total - 1, total <= 0, done)
+ self.chunk(env, stop, gate, &file, url, 0, total - 1, total <= 0, done)
.await
} else {
let size = total / conns as i64;
@@ -888,7 +952,7 @@ impl Downloads {
} else {
start + size - 1
};
- self.chunk(env, stop, &file, url, start, end, false, done)
+ self.chunk(env, stop, gate, &file, url, start, end, false, done)
});
futures_util::future::join_all(parts)
.await
@@ -918,6 +982,7 @@ impl Downloads {
&self,
env: &DlEnv,
stop: &Stop,
+ gate: &Gate,
file: &Arc,
url: &str,
start: i64,
@@ -927,7 +992,13 @@ impl Downloads {
) -> Result<(), Halt> {
let mut pos = start;
let mut last_err = String::new();
- for attempt in 0..DL_ATTEMPTS {
+ let mut attempt: u64 = 0;
+ while attempt < DL_ATTEMPTS {
+ // Une pause n'est pas un échec : on attend la reprise sans
+ // consommer d'essai.
+ if !gate.wait(stop).await {
+ return Err(Halt::Canceled);
+ }
if stop.is_set() {
return Err(Halt::Canceled);
}
@@ -948,13 +1019,16 @@ impl Downloads {
None
};
let resp = tokio::select! {
- r = self.request(env, url, range.as_deref()).send() => r,
+ r = self.request(env, url, range.as_deref()).send() => r.map_err(|e| e.to_string()),
_ = stop.wait() => return Err(Halt::Canceled),
+ _ = gate.paused() => Err("en pause".to_string()),
};
let resp = match resp {
Ok(r) => r,
+ Err(_) if gate.is_paused() && !stop.is_set() => continue,
Err(e) => {
- last_err = e.to_string();
+ last_err = e;
+ attempt += 1;
continue;
}
};
@@ -967,7 +1041,7 @@ impl Downloads {
done.fetch_add(start - pos, Ordering::Relaxed);
pos = start;
}
- match copy_at(stop, file, resp, pos, done).await {
+ match copy_at(stop, gate, file, resp, pos, done).await {
(n, Ok(())) => {
let _ = n;
return Ok(());
@@ -975,7 +1049,12 @@ impl Downloads {
(_, Err(Halt::Canceled)) => return Err(Halt::Canceled),
(n, Err(Halt::Err(e))) => {
pos += n;
+ // coupé par la pause : la reprise repart de pos
+ if gate.is_paused() && !stop.is_set() {
+ continue;
+ }
last_err = e;
+ attempt += 1;
}
}
}
@@ -987,6 +1066,7 @@ impl Downloads {
/// même en cas d'erreur (pour reprendre).
async fn copy_at(
stop: &Stop,
+ gate: &Gate,
file: &Arc,
resp: reqwest::Response,
off: i64,
@@ -999,6 +1079,17 @@ async fn copy_at(
let next = tokio::select! {
n = stream.next() => n,
_ = stop.wait() => return (written, Err(Halt::Canceled)),
+ _ = gate.paused() => {
+ // pause : ce qui est reçu est écrit, le flux est coupé
+ return match flush(file, &mut buf, off + written).await {
+ Ok(k) => {
+ written += k;
+ done.fetch_add(k, Ordering::Relaxed);
+ (written, Err(Halt::Err("en pause".into())))
+ }
+ Err(we) => (written, Err(Halt::Err(we))),
+ };
+ }
};
let eof = next.is_none();
match next {
@@ -1186,6 +1277,22 @@ pub async fn serve(
)
}
}
+ "/api/models/download/pause" => {
+ let v = match body_json(body).await {
+ Ok(v) => v,
+ Err(e) => return bad(e),
+ };
+ let name = filepath_base(str_of(&v, "filename").trim());
+ let paused = v.get("pause").and_then(Value::as_bool).unwrap_or(false);
+ if dl.pause(&name, paused) {
+ send_json(StatusCode::OK, &json!({ "ok": true, "paused": paused }))
+ } else {
+ send_json(
+ StatusCode::NOT_FOUND,
+ &json!({ "ok": false, "error": format!("aucun téléchargement en cours pour {name}") }),
+ )
+ }
+ }
"/api/models/download" => {
let v = match body_json(body).await {
Ok(v) => v,
diff --git a/rust/loki-rs/tests/downloads.rs b/rust/loki-rs/tests/downloads.rs
index bf1f7cd..09b6a91 100644
--- a/rust/loki-rs/tests/downloads.rs
+++ b/rust/loki-rs/tests/downloads.rs
@@ -260,6 +260,13 @@ async fn wait_finished(dl: &Downloads, name: &str) -> DlState {
panic!("téléchargement {name} jamais terminé : {:?}", dl.status());
}
+fn state_of(dl: &Downloads, name: &str) -> DlState {
+ dl.status()
+ .into_iter()
+ .find(|s| s.filename == name)
+ .unwrap()
+}
+
fn models(home: &Path) -> PathBuf {
home.join("models")
}
@@ -537,6 +544,58 @@ async fn annulation_efface_le_part() {
assert_eq!(entries(&models(tmp.path())), Vec::::new());
}
+// `TestRunDownloadPauseResume` : la pause coupe le flux et garde ce qui est
+// écrit ; la reprise repart de là par une plage et termine un fichier
+// identique à la source.
+#[tokio::test]
+async fn pause_puis_reprise() {
+ let (addr, hf) = fake_hf().await;
+ let data = payload(8 << 20, 5);
+ hf.files
+ .lock()
+ .unwrap()
+ .insert("/m.gguf".into(), Arc::new(data.clone()));
+ *hf.slow.lock().unwrap() = true;
+ let tmp = tempfile::tempdir().unwrap();
+ let dl = Arc::new(Downloads::new());
+ let (code, _) = dl.start(
+ env(),
+ &dirs_at(tmp.path()),
+ &format!("http://{addr}/m.gguf"),
+ "",
+ );
+ assert_eq!(code, StatusCode::OK);
+ assert!(!dl.pause("autre.gguf", true));
+ tokio::time::sleep(Duration::from_millis(300)).await;
+ assert!(dl.pause("m.gguf", true));
+ tokio::time::sleep(Duration::from_millis(1500)).await;
+ let a = state_of(&dl, "m.gguf");
+ tokio::time::sleep(Duration::from_millis(1500)).await;
+ let b = state_of(&dl, "m.gguf");
+ assert!(
+ a.paused && !b.finished && a.done == b.done && a.done > 0 && b.speed == 0,
+ "pause inopérante : {a:?} puis {b:?}"
+ );
+ let before = hf.log.lock().unwrap().len();
+ assert!(dl.pause("m.gguf", false));
+ let end = wait_finished(&dl, "m.gguf").await;
+ assert!(
+ end.err.is_empty() && !end.canceled && !end.paused,
+ "{end:?}"
+ );
+ let got = std::fs::read(models(tmp.path()).join("m.gguf")).unwrap();
+ assert!(got == data, "fichier différent de la source après reprise");
+ // La reprise demande la suite, pas le début.
+ let log = hf.log.lock().unwrap().clone();
+ let resumed = log[before..]
+ .iter()
+ .filter_map(|l| l.strip_prefix("/m.gguf bytes="))
+ .any(|r| !r.starts_with("0-"));
+ assert!(resumed, "{log:?}");
+ // Terminé : plus rien à mettre en pause.
+ assert!(!dl.pause("m.gguf", true));
+}
+
#[tokio::test]
async fn refus_avant_de_lancer() {
let tmp = tempfile::tempdir().unwrap();
diff --git a/rust/loki-rs/tests/parity_bench/bench.json b/rust/loki-rs/tests/parity_bench/bench.json
index 107d753..fd87ee9 100644
--- a/rust/loki-rs/tests/parity_bench/bench.json
+++ b/rust/loki-rs/tests/parity_bench/bench.json
@@ -231,8 +231,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
},
@@ -304,7 +304,7 @@
"max_tokens": 300,
"messages": [
{
- "content": "<8102 octets f67301f37ff4>",
+ "content": "<8144 octets 5c98ee6b6194>",
"role": "user"
}
],
@@ -335,8 +335,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -358,8 +358,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -387,8 +387,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -410,8 +410,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -433,8 +433,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -542,8 +542,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
},
@@ -615,7 +615,7 @@
"max_tokens": 300,
"messages": [
{
- "content": "<8102 octets f67301f37ff4>",
+ "content": "<8144 octets 5c98ee6b6194>",
"role": "user"
}
],
@@ -855,8 +855,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -947,8 +947,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -993,8 +993,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
},
@@ -1066,7 +1066,7 @@
"max_tokens": 300,
"messages": [
{
- "content": "<8102 octets f67301f37ff4>",
+ "content": "<8144 octets 5c98ee6b6194>",
"role": "user"
}
],
@@ -1103,8 +1103,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -1132,8 +1132,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -1241,8 +1241,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
},
@@ -1314,7 +1314,7 @@
"max_tokens": 300,
"messages": [
{
- "content": "<8102 octets f67301f37ff4>",
+ "content": "<8144 octets 5c98ee6b6194>",
"role": "user"
}
],
@@ -1554,8 +1554,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -1646,8 +1646,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -1686,8 +1686,8 @@
"predicted_ms": 81920,
"predicted_n": 4096,
"predicted_per_second": 50,
- "prompt_ms": 16403,
- "prompt_n": 32806,
+ "prompt_ms": 16402,
+ "prompt_n": 32804,
"prompt_per_second": 2000,
"protocol": 2
},
@@ -1759,7 +1759,7 @@
"max_tokens": 4096,
"messages": [
{
- "content": "<131211 octets 96987502f054>",
+ "content": "<131201 octets e3226a117903>",
"role": "user"
}
],
@@ -1790,8 +1790,8 @@
"predicted_ms": 81920,
"predicted_n": 4096,
"predicted_per_second": 50,
- "prompt_ms": 16403,
- "prompt_n": 32806,
+ "prompt_ms": 16402,
+ "prompt_n": 32804,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -1813,8 +1813,8 @@
"predicted_ms": 81920,
"predicted_n": 4096,
"predicted_per_second": 50,
- "prompt_ms": 16403,
- "prompt_n": 32806,
+ "prompt_ms": 16402,
+ "prompt_n": 32804,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -2020,8 +2020,8 @@
"predicted_ms": 320,
"predicted_n": 16,
"predicted_per_second": 50,
- "prompt_ms": 469.5,
- "prompt_n": 939,
+ "prompt_ms": 470,
+ "prompt_n": 940,
"prompt_per_second": 2000,
"protocol": 2
},
@@ -2093,7 +2093,7 @@
"max_tokens": 16,
"messages": [
{
- "content": "<3743 octets 302346ae5a44>",
+ "content": "<3745 octets 55a9fdff5d02>",
"role": "user"
}
],
@@ -2124,8 +2124,8 @@
"predicted_ms": 320,
"predicted_n": 16,
"predicted_per_second": 50,
- "prompt_ms": 469.5,
- "prompt_n": 939,
+ "prompt_ms": 470,
+ "prompt_n": 940,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -2147,8 +2147,8 @@
"predicted_ms": 320,
"predicted_n": 16,
"predicted_per_second": 50,
- "prompt_ms": 469.5,
- "prompt_n": 939,
+ "prompt_ms": 470,
+ "prompt_n": 940,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -2187,8 +2187,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
},
@@ -2260,7 +2260,7 @@
"max_tokens": 300,
"messages": [
{
- "content": "<8102 octets f67301f37ff4>",
+ "content": "<8144 octets 5c98ee6b6194>",
"role": "user"
}
],
@@ -2291,8 +2291,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -2314,8 +2314,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -2420,8 +2420,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -2443,8 +2443,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -2541,7 +2541,7 @@
"max_tokens": 300,
"messages": [
{
- "content": "<8102 octets f67301f37ff4>",
+ "content": "<8144 octets 5c98ee6b6194>",
"role": "user"
}
],
@@ -2572,8 +2572,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -2595,8 +2595,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -2693,7 +2693,7 @@
"max_tokens": 300,
"messages": [
{
- "content": "<8102 octets f67301f37ff4>",
+ "content": "<8144 octets 5c98ee6b6194>",
"role": "user"
}
],
@@ -2747,8 +2747,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -2770,8 +2770,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -2868,7 +2868,7 @@
"max_tokens": 300,
"messages": [
{
- "content": "<8102 octets f67301f37ff4>",
+ "content": "<8144 octets 5c98ee6b6194>",
"role": "user"
}
],
@@ -2992,8 +2992,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -3015,8 +3015,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -3121,8 +3121,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -3144,8 +3144,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -3189,8 +3189,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
},
@@ -3287,7 +3287,7 @@
"max_tokens": 300,
"messages": [
{
- "content": "<8102 octets f67301f37ff4>",
+ "content": "<8144 octets 5c98ee6b6194>",
"role": "user"
}
],
@@ -3319,8 +3319,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -3342,8 +3342,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -3383,8 +3383,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
},
@@ -3457,7 +3457,7 @@
"max_tokens": 300,
"messages": [
{
- "content": "<8102 octets f67301f37ff4>",
+ "content": "<8144 octets 5c98ee6b6194>",
"role": "user"
}
],
@@ -3489,8 +3489,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -3512,8 +3512,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -3579,8 +3579,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
},
@@ -3652,7 +3652,7 @@
"max_tokens": 300,
"messages": [
{
- "content": "<8102 octets f67301f37ff4>",
+ "content": "<8144 octets 5c98ee6b6194>",
"role": "user"
}
],
@@ -3737,8 +3737,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -3760,8 +3760,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -3867,7 +3867,7 @@
"max_tokens": 300,
"messages": [
{
- "content": "<8102 octets f67301f37ff4>",
+ "content": "<8144 octets 5c98ee6b6194>",
"role": "user"
}
],
@@ -3952,8 +3952,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -3975,8 +3975,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -4034,8 +4034,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -4057,8 +4057,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -4089,8 +4089,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -4112,8 +4112,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
}
@@ -4135,8 +4135,8 @@
"predicted_ms": 6000,
"predicted_n": 300,
"predicted_per_second": 50,
- "prompt_ms": 1014.5,
- "prompt_n": 2029,
+ "prompt_ms": 1020,
+ "prompt_n": 2040,
"prompt_per_second": 2000,
"protocol": 2
}