decidealot: Nu Mai Pune un Chatbot Să Scrie un Eseu Când Îți Trebuia un Nenorocit de Da sau Nu

TypeSafe a scos Jev și tot feed-ul meu a făcut spume la gură. Ei îi zic model System One: îi dai o stare dezordonată plus câteva întrebări tipizate, iar Jev îți dă înapoi o alegere, un scor sau un da/nu cu o probabilitate reală atașată. Fără eseu, fără “Sigur! Iată JSON-ul pe care l-ai cerut:”, fără să parsezi proză înapoi în singurul cuvânt pe care îl voiai. Să le dăm ce-i al lor, ideea e al dracului de corectă.

Apoi citești literele mărunte. API hosted. Acces timpuriu. Listă de așteptare. Fără weights. Iar chestia pe care vrei s-o judece e, prin definiție, chestia sensibilă, “agentul ăsta e pe cale să dea drop la tabela customers”, deci trebuie să-ți iasă din rețea și să stea la coada altcuiva înainte să primești da-ul sau nu-ul. Nici gând.

Așa că am făcut ce făcusem deja cu talkies pentru voce, cu flickies pentru video și cu predictalot pentru prognoze. Am trecut prin modelele open care fac treaba asta, le-am păstrat pe cele mai bune două, Laya și Von, și le-am bătut în cuie într-o singură imagine Docker, în spatele unui singur API. Asta e decidealot, Jev-ul offline: aceeași formă de cerere și răspuns System One, MCP pe același port, hardware-ul tău, fără factură de cloud, fără listă de așteptare, iar chestia despre care întrebi nu pleacă niciodată de pe mașina ta. Apoi l-am înfipt în aigate chiar lângă frații lui, deci dacă rulezi deja stack-ul ăla, decidealot e la o singură variabilă de mediu distanță.

Să-i Ceri unui Generator de Text o Decizie E o Tâmpenie de Căcat

Jev a explodat pentru că status quo-ul e prost ca noaptea:

  • LLM-uri pe post de clasificatoare. Un model construit să genereze text, generând text, pe care apoi îl parsezi înapoi în singurul cuvânt pe care îl voiai. Te rogi de el cu “valid JSON ONLY, no explanation” și ți-l dă învelit în fence-uri de markdown, cu o notiță drăguță despre raționamentul lui, plus o dată din cincizeci când inventează o a patra categorie pe care n-ai pus-o niciodată pe listă. “Respond only with JSON” nu e un contract, e o rugăciune, iar modurile de structured output doar mută parsarea în samplerul altcuiva. Tot plătești decodare token cu token ca să produci o etichetă.
  • Încredere autodeclarată. Întreabă un chatbot cât de sigur e și îți zice “85%”, un număr scos din cur cu cea mai serioasă față din lume, pentru că un număr arăta bine în locul ăla. Numărul ăla n-a trecut niciodată nici măcar prin preajma unui softmax. Nu poți pune un prag pe el, nu-l poți calibra și nu-l poți băga într-un audit log ca să-l aperi mai târziu.
  • Latență irosită pe nimic. Fiecare “Desigur! Pe baza contextului furnizat” e timp real care stă între evenimentul tău și decizia ta.
  • Modelele open-weight. Cele mai bune două care fac treaba asta local sunt Laya de la NandhaKishorM și Von de la wfzyx, cu weights sub Apache 2.0. Super. Fiecare vine cu propriul server, cu propriile păreri despre cum arată o cerere, cu propria instalare și cu propriul runtime de Torch, și niciunul nu ia cererea oficială TypeSafe așa cum e. Le vrei pe amândouă? Rulezi două servere, două instalări și două runtime-uri de Torch, și lipiciul dintre ele îl scrii singur.

Voiam un singur container pe care îl pornesc o dată și spre care îndrept tot. Contractul hosted, ca un cod scris pe contractul ăla să nu trebuiască să învețe un al doilea API. Ambele modele locale în spatele lui. MCP pentru agenți. Și containerul să nu stea cu un model încărcat și un runtime întreg de Torch în RAM cât timp nu-l întreabă nimeni nici măcar un căcat.

Trei Tipuri de Întrebări, O Singură Cerere

Contractul e mic. Trimiți un model, un state și o mapă de questions cu nume. State-ul e ce vrei să fie judecat: un string, un obiect JSON sau un array. Numele întrebărilor sunt ale tale și se întorc drept chei sub answers. Fiecare întrebare e de unul din trei tipuri:

  • choice alege o etichetă dintre cheile din criteria. Cheile alea sunt singurele răspunsuri posibile. Modelul nu poate inventa o a patra categorie, pentru că nu scrie niciun nenorocit de cuvânt. Primești choice, un confidence și o probabilitate pentru fiecare etichetă.
  • score ia un array ordonat de criterii în care poziția e scorul, începând de la 0. Întoarce un scor așteptat, deci 1.9 pe o grilă cu trei niveluri e un răspuns real care înseamnă “blocant, cu un pic de curând”, plus probabilități pe fiecare nivel și un legend care mapează pozițiile înapoi la formularea ta.
  • noul e da sau nu. Un singur câmp, noul, probabilitatea ca afirmația să fie adevărată. Niciun câmp separat de încredere, pentru că numărul ăla deja e încrederea.

Pune-le pe toate trei într-o singură cerere și primesc răspuns pe baza aceluiași state:

curl --fail http://127.0.0.1:8080/v1/systemone \
  --header 'Content-Type: application/json' \
  --data '{
    "model": "laya",
    "state": "You billed me twice for March. Refund the duplicate today or I am cancelling.",
    "questions": {
      "department": {
        "type": "choice",
        "instructions": "Which team should handle this?",
        "criteria": {
          "billing": "Invoices, payments, refunds.",
          "technical": "Bugs, outages, errors.",
          "other": "Everything else."
        }
      },
      "urgency": {
        "type": "score",
        "criteria": ["not urgent", "soon", "blocking"]
      },
      "churn_risk": {
        "type": "noul",
        "instructions": "Does the customer threaten to leave?"
      }
    }
  }'

Nu e un exemplu inventat. Asta a trimis Laya înapoi pe bune, rulând pe imaginea CUDA în spatele serverului meu de aigate:

{
  "model": "laya",
  "answers": {
    "department": {
      "type": "choice",
      "choice": "billing",
      "confidence": 0.8055,
      "probabilities": { "billing": 0.9543, "technical": 0.0317, "other": 0.014 }
    },
    "urgency": {
      "type": "score",
      "score": 1.8986,
      "confidence": 0.7053,
      "legend": { "0": "not urgent", "1": "soon", "2": "blocking" },
      "probabilities": { "0": 0.0222, "1": 0.0571, "2": 0.9208 }
    },
    "churn_risk": {
      "type": "noul",
      "noul": 0.2673
    }
  },
  "usage": { "input_tokens": 156, "output_tokens": 0 }
}

Uită-te la output_tokens. Zero. Laya n-a scris niciun token, deci nu are cum să se întoarcă nimic de genul “Sigur! Iată”. Aceeași cerere trimisă la Von raportează trei, ceea ce tot e departe rău de un paragraf. Oricum ar fi, forma răspunsului e fixată de schemă, nu de cheful modelului de a-ți băga azi în seamă instrucțiunile sau de a le da dracului.

Acum uită-te la churn_risk. Clientul a scris literalmente “or I am cancelling”, iar Laya a pus amenințarea la 0.27. I-am trimis lui Von aceeași cerere, ca a doua opinie, și el a zis 0.23. La billing și la blocking, amândoi au nimerit-o fix. La amenințarea cu plecarea, amândoi au ridicat din umeri. Nu e decidealot care strică ceva pe drum, asta au răspuns modelele, și e exact motivul pentru care există paragraful următor. Testează-ți întrebările pe cazurile tale înainte să legi un prag de ele. Să reformulezi întrebarea e ieftin. Să afli în producție nu e.

decidealot îți dă numerele și se oprește acolo. Nu acționează. Pragul e treaba codului tău: permiți când allow trece de 0.95, bagi tot restul la coadă pentru un om, stochezi răspunsul întreg lângă înregistrarea acțiunii. Aceeași poziție ca la predictalot, doar un strat mai sus. Tu primești numere, iar decizia despre ce faci cu ele rămâne a ta.

Două Modele, Zece Selectori, Unul Singur în Memorie

Fiecare cerere numește un model. Nu există default, ca deciziile tale să nu-și schimbe creierul pe tăcute pentru că vreun dobitoc a editat o variabilă de mediu. GET /v1/models întoarce catalogul, și sunt fix astea zece:

  • laya, laya-auto, laya-latest: Laya cu rutare automată de checkpoint. Laya se uită la alfabetul și la limba din state și alege singură checkpointul englezesc sau pe cel multilingv.
  • laya-english: checkpointul englezesc, pentru engleză în alfabet latin.
  • laya-multilingual: checkpointul multilingv, pentru tot restul, inclusiv text scurt în alfabet latin care nu e clar engleză.
  • laya-typed-decisions: checkpointul reglat pentru apeluri structurate repetate dintr-un workflow, gen policy, rutare, triaj și aprobări. Testează-l pe cazurile tale înainte să te încrezi în el.
  • von, von-latest, von-1.1, von-1.1.0: Von, un model independent, doar pentru engleză, făcut pentru decizii scurte și bine formulate. Util de unul singur și util ca a doua opinie înainte să standardizezi un workflow pe Laya.

Laya e o familie de modele cu trei checkpointuri. Von e un model diferit, făcut de alți oameni. Amândouă modelele primesc aceeași cerere și întorc aceleași tipuri de răspuns, și exact ăsta e rostul să le pui în spatele unui singur contract: schimbi stringul model și compari.

Unload Înseamnă Că Moare Procesul

Asta e partea care chiar mă interesează. În imagine sunt trei virtualenv-uri de Python. /opt/app-venv e gateway-ul: FastAPI, httpx, SDK-ul MCP, pydantic, uvicorn. Fără Torch, nici măcar un singur import din el nicăieri în sursa gateway-ului. /opt/laya-venv și /opt/von-venv țin fiecare stack-ul unui singur model. Momentan amândouă se întâmplă să fixeze aceleași versiuni de torch și transformers, deci asta nu e un workaround pentru o bătaie care are deja loc. Înseamnă că un upgrade la Laya nu poate ajunge niciodată în mediul lui Von, iar procesul care îți răspunde la cererile HTTP nu are niciodată un model în el.

Fiecare model rulează ca proces copil al gateway-ului, pornit de un supervisor dintr-o comandă fixă, fără shell, ascultând pe un port de loopback fix. Doar unul e rezident la un moment dat. Cere-l pe Von cât timp Laya e încărcată și supervisorul așteaptă să se termine fiecare cerere Laya în curs, o omoară pe Laya, îl pornește pe Von și verifică /health la Von la fiecare sfert de secundă până răspunde. Trecerea între selectorii Laya rămâne în procesul Laya, pentru că sunt checkpointuri ale aceleiași chestii. Tăiat la esențial, gate-ul arată așa:

async with self._provider_switch_condition:
    spec = self._require_spec(provider_name)
    while self._has_active_other_provider(provider_name):
        await self._provider_switch_condition.wait()
    await self._unload_other_idle_providers_locked(provider_name)
    await self._start_provider_locked(spec)
    self._active_requests[provider_name] += 1
try:
    yield
finally:
    async with self._provider_switch_condition:
        self._active_requests[provider_name] -= 1
        self._last_used_at[provider_name] = asyncio.get_running_loop().time()
        self._provider_switch_condition.notify_all()

De ce un proces întreg în loc de del model și o rugăciune la garbage collector? Pentru că așa nu-ți primești memoria înapoi. Caching allocator-ul din PyTorch se agață de ce a apucat, iar contextul CUDA stă pe loc cât trăiește procesul. Să omori procesul e singurul unload care chiar e unload, deci asta face unload-ul: terminate, zece secunde de grație, apoi kill dacă face pe pizda și nu vrea să plece. Weights-urile, alocările Torch, thread-urile de worker și contextul CUDA pleacă toate odată cu el. Următoarea cerere pornește procesul din nou.

Pornirea aia nu e gratis și n-o să mă prefac că e. Pe mașina mea cu GPU, prin aigate, prima cerere Laya după un cold start a durat cam 61 de secunde, pentru că procesul a trebuit să pornească și să încarce modelul. Următoarea cerere identică s-a întors în 55 de milisecunde cap-coadă, cu tot cu rețea, cu exact același răspuns, byte cu byte. Trecerea pe Von a durat cam două minute, pentru că întâi a trebuit oprită Laya și apoi pornit Von. Cu Von deja încărcat, răspunsul a venit în cam 115 milisecunde. Deci timeoutul de idle e un compromis real: setează-l destul de lung încât traficul tău să nu plătească mereu cold startul, și destul de scurt încât GPU-ul să nu facă pe dădaca unui model pe care nu-l folosește nimeni.

Două lucruri declanșează asta. Un reaper de idle face unload la un provider care a stat nefolosit DECIDEALOT_PROVIDER_IDLE_UNLOAD_SECONDS, 600 implicit, și verifică la fiecare a zecea parte din fereastra aia, limitat între 10 milisecunde și 30 de secunde. Setează variabila pe 0 și se oprește doar timerul. Iar POST /v1/models/unload face asta la cerere. Endpointul ăla e totul sau nimic: dacă vreun provider e în mijlocul unei cereri primești un 409 PROVIDER_BUSY și nu se eliberează nimic. Endpointul nu-i smulge niciodată modelul de sub picioare cuiva care îl folosește.

Descarci O Dată, Apoi Adio Internet

Montezi un singur director de pe host la /models, iar decidealot gestionează /models/laya și /models/von în el. La prima pornire, supervisorul rulează un pas prepare pentru fiecare model, în venv-ul propriu al modelului respectiv, iar pasul ăsta trage snapshotul de pe Hugging Face, convaiinnovations/laya și wfzyx/von, fiecare fixat pe o revizie exactă de commit. Supervisorul verifică apoi că fiecare fișier de care are nevoie runtime-ul chiar e acolo: patru fișiere în fiecare dintre cele trei directoare de checkpoint de la Laya, șase pentru Von. Nimic din toate astea nu importă Torch. /health răspunde 503 până sunt gata ambele bundle-uri, adică vreo 5.3 GB și câteva minute prima dată. După aia fișierele sunt deja acolo și nu se mai descarcă nimic.

Pe urmă, înainte să se încarce un model, decidealot setează HF_HUB_OFFLINE=1 și TRANSFORMERS_OFFLINE=1. Odată ce bundle-ul e verificat, niciun model nu mai are voie să se plimbe înapoi pe Hub în mijlocul unei rulări și să aducă vreun căcat pe care nu l-ai fixat tu.

Cum Bagi Două Servere Upstream Sub Același Contract

Schema oficială zice că instructions e opțional și poate fi o valoare JSON imbricată, la fel și valorile criteriilor. Laya vrea un câmp instructions pe absolut fiecare întrebare. Von îl vrea string. Trimite-i oricăruia dintre ei o cerere pe care API-ul oficial o acceptă fără nicio problemă și ești respins pentru o diferență de formă pe care n-a cerut-o nimeni.

Așa că decidealot îți validează întâi cererea față de modelele de request oficiale, apoi construiește din ea body-ul nativ al fiecărui model: un instructions lipsă devine string gol, iar valorile imbricate sunt randate ca text JSON compact. Etichetele tale de choice, ordinea scorurilor și state-ul trec neatinse. La întoarcere, răspunsul providerului e validat față de schema oficială de răspuns. Mizeriile care țin doar de provider, cum e blocul de rutare de la Laya, sunt aruncate, iar model primește numele public al selectorului care a răspuns de fapt. Dacă un provider îți dă înapoi ceva care nu se potrivește cu schema, primești un 503 curat în loc de gunoi deghizat în decizie. Dacă un provider respinge o cerere cu un mesaj sec, mesajul e reformulat în anvelopa oficială de validare {"detail": [...]}, deci error handling-ul tău vede mereu o singură formă.

Apoi mai e serverul lui Von, care își găsește backendul printr-un singleton la nivel de proces, al cărui constructor public nu are cum să afle unde stă checkpointul. Așa că decidealot construiește singur engine-ul și îl îndeasă în slotul din care citește serverul:

engine = engine_type(
    backend_name=os.environ.get(_von_backend_env, _default_von_backend),
    device=os.environ.get(_von_device_env),
)
engine.backend = option_marker_backend_type(checkpoint_dir=str(model_dir), device=engine.device)
# Von's server resolves its backend from this singleton, whose public constructor
# has no checkpoint-directory argument.
with engine_type._lock:
    engine_type._instance = engine

Da, asta înseamnă să bagi mâna într-un singleton privat. E urât ca dracu’, și e exact atât de urât cât trebuie ca să-l îndrepți pe Von spre un director pe care îl controlezi tu.

MCP pe Același Port

Același container servește MCP Streamable HTTP la /mcp, cu trei unelte: system_one, list_models și unload_models. Uneltele trec prin același serviciu de decizie ca rutele REST, adică aceeași validare, același supervisor, aceeași limită de body și același bearer token. system_one ia exact model, state și questions pe care le-ai trimite prin POST și întoarce același rezultat structurat, deci un agent poate citi probabilitățile înainte să decidă ce face mai departe. O validare picată se întoarce ca isError: true, cu body-ul detail de la TypeSafe în text, ca agentul să vadă ce-a făcut de căcat, nu un “error” sec.

Nicio unealtă nu primește un URL, o cale din filesystem, un executabil, un director de model sau o opțiune de runtime. Routerul de modele mapează fiecare alias la un endpoint de loopback fix, iar cel care apelează nu apucă niciodată să aleagă o țintă din rețea. Cea mai mare putere pe care o are un agent aici e să aleagă un nume din catalog.

Să pui containerul în spatele unui reverse proxy sau al unui tunel nu înseamnă să oprești protecția împotriva DNS rebinding. Pui pe allowlist exact Host-ul public în DECIDEALOT_MCP_ALLOWED_HOSTS și, pentru clienții din browser, exact originea în DECIDEALOT_MCP_ALLOWED_ORIGINS. Verificările rulează într-o ordine fixă: un bearer lipsă sau greșit ia 401 primul, apoi un host necunoscut ia 421, apoi o origine de browser necunoscută ia 403. Nu pune wildcard pe o mașină expusă la internet. Setează DECIDEALOT_API_KEY la un secret adevărat înainte ca containerul să ajungă măcar în preajma unei adrese publice.

Căcaturile Plictisitoare Datorită Cărora Îl Poți Lăsa Pornit Liniștit

  • Un container care nu poate face mare lucru. Rularea documentată e cu root filesystem read-only, --cap-drop ALL, no-new-privileges, tmpfs noexec pentru /tmp și /var/run, o limită de pids, un plafon de memorie și un port publicat doar pe loopback.
  • UID-ul tău, nu al imaginii. Containerul rulează ca orice --user îi dai, deci în directorul de modele pe care tocmai l-ai făcut cu mkdir se poate scrie fără niciun dans cu chown. Imaginea cade pe non-root 1000:1000 doar când nu zici nimic.
  • O singură gaură de exec pentru CUDA, și doar una. Triton compilează la runtime mici helperi CUDA și trebuie să-i încarce de undeva, așa că rularea CUDA adaugă un singur tmpfs exec la /var/cache. Restul rămâne read-only și noexec.
  • Auth cu bearer care nu te toarnă prin timing. Opțional, oprit cât timp nu e setat DECIDEALOT_API_KEY, comparat cu hmac.compare_digest pe bytes encodați. /health rămâne deschis pentru liveness probe-ul tău.
  • Un plafon de body. 1 MiB implicit prin DECIDEALOT_MAX_REQUEST_BYTES, orice e declarat mai mare ia un 413 înainte să-l vadă vreun model.
  • ID-uri de request pe care poți da grep. Trimite un UUID sau un ULID în X-Request-Id și ID-ul urmează cererea prin loguri. Trimite gunoi sau nimic și decidealot generează un UUID. Oricum ar fi, ID-ul se întoarce în răspuns.
  • O barieră de supply chain pentru care modelele erau prea tinere. Dependențele gateway-ului stau în spatele unei bariere de vârstă exclude-newer din uv, iar stack-urile modelelor se instalează cu --require-hashes din fișiere blocate pe hash. Ambele pachete de model sunt mai tinere decât permite bariera. Release-ul fixat de la Laya nici măcar nu e pe PyPI, deci e instalat din tarball-ul commitului upstream, cu SHA-256-ul în lock. Fiecare are o excepție scrisă, aprobată de owner. Bariera și-a făcut treaba, iar eu am semnat biletul de învoire.
  • Teste cu prag minim. Branch coverage cu minimum 90%, bătut în cuie, plus rulări HTTP reale pe weights-urile descărcate, de CPU și de CUDA.

Două imagini. Cea de CPU are cam o jumătate de giga comprimată pe Docker Hub și e default-ul corect. Cea de CUDA are cam 9 GB, e construită pe CUDA 12.6, doar amd64, și are nevoie de NVIDIA Container Toolkit și de --gpus all. Ia-o doar când viteza și memoria modelului chiar justifică tot setup-ul de GPU.

Dă-i Drumul

model_directory="$HOME/.local/share/decidealot/models"
mkdir --parents "$model_directory"
docker run --detach --name decidealot --init --restart unless-stopped \
  --user "$(id -u):$(id -g)" \
  --read-only --cap-drop ALL --security-opt no-new-privileges:true \
  --pids-limit 512 --memory 8g --cpus 4 \
  --tmpfs /tmp:rw,noexec,nosuid,size=128m \
  --tmpfs /var/run:rw,noexec,nosuid,size=8m \
  --mount type=bind,source="$model_directory",target=/models \
  --publish 127.0.0.1:8080:8080 \
  psyb0t/decidealot:latest
curl --fail http://127.0.0.1:8080/health

Așteaptă ca /health să se facă verde, apoi trimite-i cererea de mai sus. Pentru GPU, adaugă --gpus all și tmpfs-ul /var/cache și folosește psyb0t/decidealot:latest-cuda. Documentul de deployment din repo are rețeta CUDA completă.

Dacă rulezi deja aigate, sari peste tot căcatul ăsta. Setează DECIDEALOT=1 pentru serviciul de CPU la /decidealot/ sau DECIDEALOT_CUDA=1 pentru cel de GPU la /decidealot-cuda/, cu MCP sub fiecare, iar uneltele intră și în /mcp/-ul agregat din aigate, lângă tot restul cu care vorbesc deja agenții tăi. Cele două servicii pot rula unul lângă altul și împart un singur director de modele, deci cei 5.3 GB ajung pe disc o singură dată.

Și de memorie are grijă tot aigate, nu tu. Înainte ca orice alt model local (Ollama, sd.cpp, talkies, vLLM, llama.cpp) să ruleze pe același hardware, resource managerul din aigate îi zice lui decidealot să facă unload. Deciziile care intră prin MCP-ul din aigate iau același lock de hardware ca tot restul, iar POST /v1/unload/cuda sau /v1/unload/cpu face unload și la decidealot odată cu restul stack-ului. O decizie aflată în curs răspunde 409 și își păstrează modelul până la timerul de idle, deci niciun model nu e smuls de sub picioarele cuiva care îl folosește. Singura gaură: o cerere trimisă direct la /decidealot/ nu dă afară pe nimeni altcineva, deci pe ruta aia jongleria cu GPU-ul rămâne tot treaba ta.

Cum Îl Instalezi în Agentul Tău

Un agent care e pe cale să acționeze pe baza unei probabilități ar trebui măcar să știe ce înseamnă probabilitatea aia. Skillul îi spune cum să deployeze containerul, cum să aleagă Laya sau Von, cum să trimită o decizie tipizată, cum să citească probabilitățile fără să le ia drept permisiune și cum să folosească MCP direct. Tot ce e sub .agents/ e catalogat într-un singur marketplace, deci sunt două comenzi:

claude plugin marketplace add psyb0t/agents
claude plugin install decidealot@psyb0t

Codex folosește același marketplace cu alt verb, codex plugin add decidealot@psyb0t, pentru că nu există codex plugin install. OpenClaw primește skillul, plus un bridge stdio opțional pentru clienții care pot vorbi doar cu un server MCP stdio local, bridge care trimite mai departe la containerul pe care îl rulezi deja:

openclaw skills install @psyb0t/decidealot
openclaw plugins install clawhub:@psyb0t/decidealot

decidealot Decide. Tu Acționezi.

Jev a avut ideea corectă, doar că a parcat-o pe serverul altuia. decidealot e aceeași idee, la tine acasă: o mulțime închisă de răspunsuri, o probabilitate reală pe fiecare și zero șanse să primești înapoi un eseu. decidealot nu știe care ar trebui să-ți fie pragul și nici nu se preface că știe. Tu alegi limita în funcție de cât te costă să greșești, iar modelul n-are niciun vot pe tema asta.

Ia-l de la github.com/psyb0t/decidealot sau trage psyb0t/decidealot de pe Docker Hub. Codul e WTFPL, deci fă ce dracu’ vrei cu el. Laya, Von, PyTorch, Transformers și weights-urile descărcate își păstrează fiecare licența proprie, deci citește-le înainte să înșurubezi un model în ceva ce vinzi. Acum du-te și nu mai cere de la chatboturi un da sau un nu.