mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-17 04:10:04 +02:00
Téléchargements de modèles : pause et reprise
Repris d'AJEAN 9c7423bf (v0.18.1) : POST /api/models/download/pause
{"filename", "pause"} met en pause ou reprend un téléchargement en cours.
La pause coupe les connexions (rien ne reste ouvert vers le CDN), garde le
fichier partiel, et chaque morceau attend la reprise puis repart de l'octet
où il s'était arrêté par une requête de plage ; une pause ne consomme pas
d'essai. /status expose « paused », la vitesse publiée tombe à zéro.
Façade Rust (downloads.rs) : même route, même porte (watch), mêmes
réponses ; la route est ajoutée à celles que le Go remet à la façade quand
LOKI_MODELS=rust. Tests portés de part et d'autre (pause puis reprise de
bout en bout, fichier identique à la source).
UI : bouton « Mettre en pause » / « Reprendre » à côté de « Annuler le
téléchargement », « en pause » dans la ligne de progression.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
615e58dd2f
commit
0ef2ed72f5
11 files changed
+580
-139
No files matched your search
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -3500,6 +3500,7 @@ html[data-files="1"] #files-btn{color:var(--accent)}
|
||||
</div>
|
||||
<div id="m-hf-bar" class="pe-bar" style="display:none"><i></i></div>
|
||||
<div id="m-hf-progress" class="pe-note" style="display:none"></div>
|
||||
<button id="m-hf-pause" class="pe-link" onclick="togglePauseDownload()" style="display:none">Mettre en pause</button>
|
||||
<button id="m-hf-cancel" class="pe-link" onclick="cancelDownload()" style="display:none">Annuler le téléchargement</button>
|
||||
</div>
|
||||
<div class="pe-row">
|
||||
@@ -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));
|
||||
|
||||
@@ -1215,6 +1215,7 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
|
||||
</div>
|
||||
<div id="m-hf-bar" class="pe-bar" style="display:none"><i></i></div>
|
||||
<div id="m-hf-progress" class="pe-note" style="display:none"></div>
|
||||
<button id="m-hf-pause" class="pe-link" onclick="togglePauseDownload()" style="display:none">Mettre en pause</button>
|
||||
<button id="m-hf-cancel" class="pe-link" onclick="cancelDownload()" style="display:none">Annuler le téléchargement</button>
|
||||
</div>
|
||||
<div class="pe-row">
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
+117
-10
@@ -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<DlState>,
|
||||
cancel: watch::Sender<bool>,
|
||||
/// `dlGate` : vrai pendant une pause.
|
||||
pause: watch::Sender<bool>,
|
||||
}
|
||||
|
||||
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<bool>);
|
||||
|
||||
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<Entry>,
|
||||
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<PartFile>,
|
||||
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<PartFile>,
|
||||
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,
|
||||
|
||||
@@ -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::<String>::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();
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user