File size: 4,708 Bytes
1b52c48
cf8503a
1ec48cb
cf8503a
 
1b52c48
2624719
 
 
cd72ff2
 
2624719
 
cf8503a
1b52c48
2624719
cd72ff2
1b52c48
 
cd72ff2
 
1ec48cb
2624719
cd72ff2
2624719
 
 
 
1b52c48
cd72ff2
1b52c48
2624719
 
 
 
 
1b52c48
 
cd72ff2
2624719
 
 
 
cf8503a
 
 
 
 
 
cd72ff2
cf8503a
 
 
 
 
 
 
 
1ec48cb
cf8503a
 
 
 
 
 
 
 
 
 
1ec48cb
 
 
cd72ff2
1ec48cb
cf8503a
cd72ff2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1ec48cb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cd72ff2
1ec48cb
cf8503a
1ec48cb
cf8503a
cd72ff2
1ec48cb
 
 
 
 
 
 
 
 
 
 
 
 
 
2624719
1b52c48
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import uvicorn
import json
import asyncio
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse, StreamingResponse
from llama_cpp import Llama

app = FastAPI(
    title="Dimercia AI",
    version="0.1.4",
    description="API OpenAI compatible souveraine - Anti-Timeout pour Cline"
)

# --- Initialisation globale ---
MODEL_PATH = "/app/models/qwen2.5-coder-1.5b-instruct-q4_k_m.gguf"

print("Chargement de Dimercia AI v0.1.4 en mémoire RAM...")
llm = Llama(
    model_path=MODEL_PATH,
    n_ctx=16384,     # 16k conserve un parfait équilibre sur CPU
    n_threads=4,     # Exploite à fond le calcul parallèle
    verbose=False
)
print("Dimercia AI v0.1.4 est prêt.")

@app.get("/")
def home():
    return {
        "name": "Dimercia AI",
        "version": "0.1.4",
        "status": "running"
    }

@app.get("/v1/models")
def models():
    return {
        "object": "list",
        "data": [
            {"id": "dimercia-coder", "object": "model", "owned_by": "dimercia"}
        ]
    }

@app.post("/v1/chat/completions")
async def chat(request: Request):
    try:
        body = await request.json()
    except Exception:
        return JSONResponse(status_code=400, content={"detail": "JSON invalide"})

    # --- Nettoyage adaptatif du Payload ---
    raw_messages = body.get("messages", [])
    cleaned_messages = []
    
    for msg in raw_messages:
        role = msg.get("role", "user")
        raw_content = msg.get("content", "")
        
        if isinstance(raw_content, list):
            text_pieces = [piece.get("text", "") if isinstance(piece, dict) else str(piece) for piece in raw_content]
            content = "".join(text_pieces)
        else:
            content = str(raw_content)
            
        cleaned_messages.append({"role": role, "content": content})

    temperature = body.get("temperature", 0.2)
    max_tokens = body.get("max_tokens", 512)
    stream = body.get("stream", False)

    temp_val = float(temperature) if temperature is not None else 0.2
    tokens_val = int(max_tokens) if max_tokens is not None else 512

    # --- Gestion du mode STREAMING (Avec système anti-timeout) ---
    if stream:
        async def chunk_generator():
            # Étape 1 : Lancer la création de l'itérateur dans un thread séparé (non-bloquant)
            task = asyncio.create_task(asyncio.to_thread(
                llm.create_chat_completion,
                messages=cleaned_messages,
                temperature=temp_val,
                max_tokens=tokens_val,
                stream=True
            ))

            # Étape 2 : Tant que llama.cpp calcule le prefill, on envoie des pings invisibles à Cline
            while not task.done():
                # Envoi d'un chunk de commentaire SSE pour garder la connexion ouverte
                yield ": heartbeat\n\n"
                await asyncio.sleep(1.0) # Attendre 1 seconde avant le prochain ping

            try:
                iterator = task.result()
            except Exception as e:
                yield f"data: {json.dumps({'error': str(e)})}\n\n"
                yield "data: [DONE]\n\n"
                return

            # Étape 3 : Consommer les tokens normalement dès qu'ils sont prêts
            def get_next_chunk(it):
                try:
                    return next(it)
                except StopIteration:
                    return None
                except Exception as ex:
                    return ex

            while True:
                chunk = await asyncio.to_thread(get_next_chunk, iterator)
                if chunk is None:
                    break
                if isinstance(chunk, Exception):
                    yield f"data: {json.dumps({'error': str(chunk)})}\n\n"
                    break
                
                if "model" in chunk:
                    chunk["model"] = "dimercia-coder"
                yield f"data: {json.dumps(chunk)}\n\n"
                
            yield "data: [DONE]\n\n"

        return StreamingResponse(chunk_generator(), media_type="text/event-stream")

    # --- Gestion du mode STANDARD ---
    else:
        try:
            response = await asyncio.to_thread(
                llm.create_chat_completion,
                messages=cleaned_messages,
                temperature=temp_val,
                max_tokens=tokens_val,
                stream=False
            )
            if isinstance(response, dict):
                response["model"] = "dimercia-coder"
            return response
        except Exception as e:
            return JSONResponse(status_code=500, content={"detail": str(e)})

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=7860)