CAI Technology
Menu ☰
iris · · 12 min citire

BYO-LLM adapter pattern: cum eviți lock-in pe un singur model

Bring-Your-Own-LLM cu adapters minimi de ~150 linii per provider. De ce framework-urile mono-LLM sunt rigid, cum cobori la signature uniformă.

CAI Technology · Ultima revizuire: 30.04.2026
BYO-LLM adapter pattern: cum eviți lock-in pe un singur model

BYO-LLM adapter pattern: cum scrii un agent care funcționează pe orice model

Un agent AI pe care îl construiți astăzi va supraviețui modelului pe care îl folosește la lansare. Modelele se schimbă rapid: în 18 luni, un model dominant astăzi poate fi depășit, ineconomic sau retras. Mai mult, diferiți clienți, diferite jurisdicții și diferite cerințe de cost vă pot obliga să rulați același agent pe mai multe modele simultan. Soluția nu este o re-scriere; este o arhitectură de la început care tratează modelul ca pe un component pluggable.

Acest articol descrie patternul Bring-Your-Own-LLM (BYO-LLM) cu adapters minimi pe care îl folosim intern: ~150 linii de cod per provider, signature uniformă pentru aplicație, schimbare de model fără re-engineering.

TL;DR

De ce un singur LLM e o trapă

Când porniți un proiect AI, există tentația de a folosi SDK-ul oficial al unui singur provider. Sintaxa este idiomatică, exemplele sunt bogate, suportul este disponibil. Pe termen scurt, este alegerea rapidă. Pe termen mediu, devine problematică din patru motive:

Schimbare de model în propriul vendor. Anthropic, OpenAI, Google trec prin generații de modele cu schimbări de API, parametri sau comportament. Cod legat strâns de un model specific se rupe.

Schimbare de vendor. Modelele cu cele mai bune capabilități se schimbă vertiginos. Modelul lider la lansare poate să nu fie modelul lider după 12 luni. Lock-in pe un SDK face migrarea costisitoare.

Cerințe geografice / regulatorii. Un client cu cerință GDPR strict poate cere model rulat în UE; un client cu date sensibile poate cere model self-hosted; un client public-sector poate avea restricții pe vendor specific. Un agent care suportă doar un vendor pierde aceste contracte.

Cost-aware routing. Așa cum am argumentat în articolul despre routing, pe scale, vrei să folosești modele diferite pentru sarcini diferite. Asta este imposibil dacă codul tău este împletit cu un SDK specific.

Patternul BYO-LLM

Patternul are trei piese:

1. Signature uniformă. Aplicația apelează o funcție care arată identic pentru orice provider:

response = llm.generate(
    messages=[{"role": "user", "content": "..."}],
    tools=[...],
    config={"model": "alias_X", "temperature": 0.2, "max_tokens": 4000}
)

Aplicația nu știe ce provider rulează. Aliasul alias_X este rezolvat de router (vezi articol cost-aware routing) la un model concret.

2. Adapter per provider. Fiecare provider are un adapter mic care:

3. Registry central. Un dict care mapează numele providerului (string) la clasa adapter:

PROVIDERS = {
    "claude": ClaudeAdapter,
    "openai": OpenAIAdapter,
    "gemini": GeminiAdapter,
    "claude_cli": ClaudeCLIAdapter,
    "openai_compat": OpenAICompatAdapter,  # pentru servere locale
}

def get_adapter(provider: str) -> LLMAdapter:
    return PROVIDERS[provider]()

Adapter exemplu (~150 linii)

Schiță simplificată pentru un adapter Claude:

class ClaudeAdapter:
    def __init__(self):
        self.client = anthropic.Anthropic()  # citește API key din env

    def generate(self, messages, tools=None, config=None):
        # Traducere mesaje: formatul intern e similar cu OpenAI;
        # Claude vrea system separat
        system = ""
        chat_messages = []
        for m in messages:
            if m["role"] == "system":
                system += m["content"] + "\n"
            else:
                chat_messages.append(m)

        # Traducere tools
        claude_tools = self._translate_tools(tools or [])

        # Apel SDK
        try:
            resp = self.client.messages.create(
                model=config["model"],
                system=system,
                messages=chat_messages,
                tools=claude_tools,
                temperature=config.get("temperature", 0.2),
                max_tokens=config.get("max_tokens", 4000),
            )
        except anthropic.RateLimitError as e:
            raise LLMRateLimitError(str(e))
        except anthropic.APIStatusError as e:
            if e.status_code >= 500:
                raise LLMUpstreamError(str(e))
            raise LLMClientError(str(e))

        # Normalizare răspuns
        text = ""
        tool_calls = []
        for block in resp.content:
            if block.type == "text":
                text += block.text
            elif block.type == "tool_use":
                tool_calls.append({
                    "id": block.id,
                    "name": block.name,
                    "arguments": block.input,
                })

        return LLMResponse(
            text=text,
            tool_calls=tool_calls,
            stop_reason=self._normalize_stop(resp.stop_reason),
            usage={
                "prompt_tokens": resp.usage.input_tokens,
                "completion_tokens": resp.usage.output_tokens,
            },
        )

    def _translate_tools(self, tools):
        # JSON Schema unificat → format Claude tools
        return [
            {
                "name": t["name"],
                "description": t["description"],
                "input_schema": t["parameters"],
            }
            for t in tools
        ]

    def _normalize_stop(self, claude_stop):
        # claude folosește 'end_turn', 'tool_use', 'stop_sequence', 'max_tokens'
        return {
            "end_turn": "stop",
            "tool_use": "tool_call",
            "max_tokens": "length",
            "stop_sequence": "stop",
        }.get(claude_stop, "stop")

Acest adapter este sub 80 de linii fără retry. Cu retry plus logging, atinge ~150 linii.

Adapter pentru OpenAI compatible (folosit pentru servere locale precum vLLM, Ollama API, LM Studio) este și mai scurt: API-ul OpenAI este de fapt cel mai larg implementat de runtime-uri locale și aproape identic cu signature-ul intern.

Diferențe care trebuie izolate în adapter

Diferențele între providers nu sunt cosmetice. Adapter-ul trebuie să le ascundă:

Format mesaje. OpenAI și majoritatea local-LLM-urilor folosesc array de mesaje cu role inclusiv system. Claude folosește system separat. Gemini folosește roluri user / model. Adapter-ul traduce.

Format tools. OpenAI: {"type": "function", "function": {...}}. Claude: {"name": ..., "input_schema": ...}. Gemini: function_declarations array. Adapter-ul traduce dintr-un format unificat (am ales JSON Schema OpenAI-style ca pivot).

Stop reason. Fiecare provider folosește alt vocabular. Adapter-ul mapează la un mic enum unificat.

Usage tokens. Numele câmpurilor diferă (input_tokens vs prompt_tokens). Numerele pot diferi prin câteva tokens din cauza tokenizării.

Tool calling streaming. Unele providers oferă streaming pentru tool calls (parsare incrementală), altele doar response complet. Pentru simplitate, adapter-ele noastre intern folosesc non-streaming și expun streaming doar la nivel de top-level dacă providerul îl suportă.

Rate limit shape. Limite per-minute, per-day, tokens-per-minute. Erorile de rate limit sunt detectate diferit. Adapter-ul normalizează la un singur tip LLMRateLimitError cu retry_after opțional.

Tipuri speciale: claude_cli și CLI subprocess

Unul din adapters-uri pe care îl folosim intern este pentru Claude CLI ca subprocess. Patternul este descris într-un alt articol; aici menționăm doar că adapter-ul are aceeași signature publică, dar implementarea apelează claude CLI prin subprocess în loc de SDK HTTP. Aplicația nu știe diferența.

Beneficiul: un agent poate folosi un model premium prin subscripție personală (cost fix lunar) și un model cloud prin API (cost per token), tratându-le identic.

Greșeli comune

Greșeală 1: signature prea bogată. Tentația de a expune fiecare feature al fiecărui provider duce la signature ce nu se mai poate implementa uniform. Soluție: signature minimal, features specifice expuse prin câmp extra opțional pe care doar providers-ele care îl suportă îl folosesc.

Greșeală 2: retry la nivel de adapter și la nivel de aplicație. Rezultă retry-uri multiplicate. Decizie clară: retry pe rate limit și server error la nivel de adapter (max 3); retry pe logică de business la nivel de aplicație.

Greșeală 3: lipsa de timeouts. Un provider lent care nu răspunde poate bloca un agent. Adapter-ul setează timeout default (60-120 secunde) suprascriibil prin config.

Greșeală 4: assumption că tools funcționează identic. Modelele diferite au calități diferite la tool-use. Un model mai mic poate avea probleme cu schema complex. Adapter-ul nu rezolvă asta; este responsabilitatea routerului să nu trimită sarcini complexe la model insuficient. Dar adapter-ul trebuie să raporteze erori clar.

Costul inițial vs câștigul

Implementarea unui adapter durează 1-2 zile pentru un developer familiar cu providerul. Trei adapters acoperă 95% din cazurile practice (Anthropic, OpenAI, OpenAI-compatible pentru local). Cost total inițial: 5-10 zile-om.

Câștigul în 12 luni:

Concluzie

Lock-in pe un singur LLM este o decizie arhitecturală de care, în 12-18 luni, veți regreta. Patternul BYO-LLM cu adapters minimi este o investiție de câteva zile care plătește permanent. Aplicația ramîne agnostică, modelul devine pluggable, decizia de provider devine o decizie de configurare, nu de cod.

Bonus: scrierea de adapter pentru un provider nou ne-a fortat să înțelegem profund cum funcționează acel provider — ceea ce ne-a făcut consultanți mai buni. Cunoașterea modelelor multiple este, în sine, un avantaj competitiv.

Articole conexe

Surse externe

Următorul pas

Dacă echipa dvs. construiește un agent AI și vrea să discutăm patternul BYO-LLM aplicat la stack-ul vostru, vă oferim o consultație tehnică de 30 minute fără cost.

Începem cu o conversație de 30 de minute.

Audit AI-readiness gratuit pentru companii peste 50 angajați. Răspundem în 24 de ore.