Il vendor lock-in nelle app di IA di solito non avviene tutto in una volta. Si insinua โ un import openai diretto qui, un nome modello hardcoded lร , un campo di risposta che analizzi senza verificare se altri provider restituiscono la stessa cosa. Sei mesi dopo, cambiare provider significa riscrivere metร del tuo backend.
Le quattro modalitร in cui il lock-in si verifica
La maggior parte degli sviluppatori pensa che il lock-in significhi "Sto usando l'SDK di OpenAI". Questo รจ il tipo meno pericoloso. Le vere trappole sono piรน sottili:
| Tipo di lock-in | Come accade | Conseguenza |
|---|---|---|
| Lock-in dell'SDK | from openai import OpenAI ovunque | Cambiare SDK significa toccare ogni file |
| Lock-in del nome modello | model="gpt-4o" hardcoded nella logica di business | Ogni cambio di modello รจ un cambio di codice |
| Lock-in dei parametri | Uso di logprobs, n>1 o reasoning_effort | Questi non esistono su Claude o Gemini |
| Lock-in del formato di risposta | Analisi di campi di risposta specifici del provider | Provider diversi restituiscono strutture diverse |
L'obiettivo non รจ eliminarli tutti โ alcuni sono compromessi accettabili. L'obiettivo รจ sapere quali stai accettando.
Usa un endpoint compatibile con OpenAI come livello di astrazione
Il modo piรน pulito per evitare il lock-in dell'SDK รจ usare un unico endpoint compatibile con OpenAI che instradi verso piรน provider. Mantieni l'SDK di OpenAI, ma il backend puรฒ essere qualsiasi provider.
CometAPI lo fa โ un endpoint, una chiave, oltre 500 modelli tra OpenAI, Anthropic, Google, DeepSeek, xAI e altri:
import osfrom openai import OpenAIfrom dotenv import load_dotenvโload_dotenv()โapi_key = os.environ.get("AI_API_KEY")if not api_key: ย ย raise ValueError("La variabile d'ambiente AI_API_KEY non รจ impostata")โclient = OpenAI( ย ย base_url=os.environ.get("AI_BASE_URL", "https://api.cometapi.com/v1"), ย ย api_key=api_key,)
Passare da GPT a Claude a Gemini richiede una sola riga di modifica:
# Primaresponse = client.chat.completions.create(model="gpt-5.4", messages=[...])โ# Dopo โ stesso codice, modello diversoresponse = client.chat.completions.create(model="claude-sonnet-4-6", messages=[...])
Nota: i nomi dei modelli come gpt-5.4 e claude-sonnet-4-6 sono identificatori della piattaforma CometAPI โ funzionano solo tramite https://api.cometapi.com/v1, non direttamente tramite le API di OpenAI o Anthropic. Vedi la lista completa dei modelli per il catalogo completo e i prezzi.
Tieni i nomi dei modelli fuori dalla logica di business
I nomi dei modelli sparsi nel codice sono la forma di lock-in piรน comune. La soluzione รจ una configurazione centrale che legge dalle variabili d'ambiente:
# config.py โ un solo posto per cambiare l'assegnazione dei modelliimport osโMODEL_CONFIG = { ย ย "summarize": os.environ.get("MODEL_SUMMARIZE", "claude-opus-4-7"), ย ย "code": ย ย ย os.environ.get("MODEL_CODE", ย ย ย "gpt-5.4"), ย ย "classify": ย os.environ.get("MODEL_CLASSIFY", ย "claude-haiku-4-5"), ย ย "chat": ย ย ย os.environ.get("MODEL_CHAT", ย ย ย "gpt-5.4-mini"),}โ# Valida all'avvio โ fallisci subito invece di ottenere errori API misteriosifor task, model in MODEL_CONFIG.items(): ย ย if not model: ย ย ย ย raise ValueError(f"La configurazione del modello per '{task}' non รจ impostata")
La tua logica applicativa non fa mai riferimento direttamente a un nome modello:
from config import MODEL_CONFIGโdef summarize(text: str) -> str: ย ย response = client.chat.completions.create( ย ย ย ย model=MODEL_CONFIG["summarize"], ย ย ย ย messages=[{"role": "user", "content": f"Riassumi: {text}"}], ย ย ย ย max_tokens=300 ย # sposta in config in produzione ย ) ย ย return response.choices[0].message.content
Per cambiare il modello di riassunto in tutta l'app, modifica una variabile d'ambiente. Niente grep, niente trova-e-sostituisci.
Incapsula la risposta cosรฌ che il tuo codice non dipenda da campi specifici del provider
Provider diversi restituiscono forme di risposta leggermente diverse. Se analizzi le risposte API raw in tutto il codice, sei legato al formato di quel provider.
Incapsulale in una dataclass normalizzata:
from dataclasses import dataclassfrom typing import Optionalfrom openai import OpenAI, APIStatusError, APIConnectionError, APITimeoutErrorfrom openai.types.chat import ChatCompletionimport loggingโ@dataclassclass AIResponse: ย ย content: str ย ย model: str ย ย input_tokens: int ย ย output_tokens: intโdef call_model(task: str, messages: list, **kwargs) -> AIResponse: ย ย """ ย Punto di ingresso unico per tutte le chiamate LLM. ย Restituisce un AIResponse normalizzato indipendentemente dal modello utilizzato. ย Genera eccezioni su 4xx (errori client). Logga e rilancia su 5xx/errori di rete. ย """ ย ย model = MODEL_CONFIG.get(task, "gpt-5.4-mini") ย ย if not model: ย ย ย ย raise ValueError(f"Nessun modello configurato per il task '{task}'")โ ย ย try: ย ย ย ย response: ChatCompletion = client.chat.completions.create( ย ย ย ย ย ย model=model, ย ย ย ย ย ย messages=messages, ย ย ย ย ย ย **kwargs ย ย ย ) ย ย except APIStatusError as e: ย ย ย ย logging.error(f"Errore API per task={task} model={model}: {e.status_code} {e.message}") ย ย ย ย raise ย ย except (APIConnectionError, APITimeoutError) as e: ย ย ย ย logging.error(f"Errore di rete per task={task} model={model}: {e}") ย ย ย ย raiseโ ย ย # content รจ None quando il modello attiva una chiamata a tool invece di restituire testo ย ย content = response.choices[0].message.content or ""โ ย ย # usage รจ None in modalitร streaming โ imposta a 0 se non disponibile ย ย usage = response.usage ย ย input_tokens = usage.prompt_tokens if usage else 0 ย ย output_tokens = usage.completion_tokens if usage else 0โ ย ย logging.info( ย ย ย ย f"task={task} model={model} " ย ย ย ย f"input_tokens={input_tokens} output_tokens={output_tokens}" ย )โ ย ย return AIResponse( ย ย ย ย content=content, ย ย ย ย model=response.model, ย ย ย ย input_tokens=input_tokens, ย ย ย ย output_tokens=output_tokens, ย )
Ora la tua logica applicativa lavora con oggetti AIResponse, non con risposte API raw. Se un provider cambia il proprio formato di risposta, lo correggi in un unico punto.
Aggiungi il supporto streaming al wrapper
Per le interfacce chat, vorrai lo streaming. Il wrapper lo gestisce come percorso separato:
from typing import Iteratorโdef stream_model(task: str, messages: list, **kwargs) -> Iterator[str]: ย ย """ ย Emetti in streaming i token dal modello instradato. ย Nota: lo streaming non restituisce i dati di utilizzo. ย Il fallback non รจ supportato in modalitร streaming โ hai giร ย iniziato a emettere token prima di sapere se la richiesta completa ha successo. ย """ ย ย model = MODEL_CONFIG.get(task, "gpt-5.4-mini") ย ย if not model: ย ย ย ย raise ValueError(f"Nessun modello configurato per il task '{task}'")โ ย ย stream = client.chat.completions.create( ย ย ย ย model=model, ย ย ย ย messages=messages, ย ย ย ย stream=True, ย ย ย ย **kwargs ย )โ ย ย for chunk in stream: ย ย ย ย delta = chunk.choices[0].delta.content ย ย ย ย if delta: ย ย ย ย ย ย yield deltaโ# Utilizzoper token in stream_model("chat", [{"role": "user", "content": "Ciao"}]): ย ย print(token, end="", flush=True)
Sapere quali parametri creano lock-in
Alcuni parametri esistono solo su provider specifici. Usarli va bene โ basta sapere che stai facendo una scelta deliberata:
| Parametro | Funziona su | Rischio di lock-in |
|---|---|---|
| logprobs | Solo GPT | Alto โ nessun equivalente su Claude o Gemini |
| n > 1 | GPT, Gemini (non Claude) | Medio โ Claude richiede un loop |
| reasoning_effort | Solo GPT serie o | Alto โ nessun equivalente altrove |
| temperature > 1.0 | GPT, Gemini (non Claude) | Basso โ Claude รจ limitato a 1.0 |
| tools | Tutti i principali provider | Nessuno โ sicuro da usare |
| response_format | Tutti i principali provider | Basso โ differenze di schema minori |
Se stai usando logprobs per lo scoring di confidenza, sei legato a GPT per quella funzione. ร un compromesso ragionevole โ basta documentarlo, cosรฌ il prossimo sviluppatore saprร il perchรฉ.
Rendi configurabile l'endpoint del provider
Hardcodare base_url="https://api.cometapi.com/v1" รจ comunque una forma di lock-in. Rendilo una variabile d'ambiente:
# .env โ usando CometAPIAI_BASE_URL=https://api.cometapi.com/v1AI_API_KEY=your_cometapi_keyโ# Per passare a OpenAI direttamente, cambia due righe:# AI_BASE_URL=https://api.openai.com/v1# AI_API_KEY=your_openai_key
L'inizializzazione del client dallo Step 1 legge giร da queste variabili. Passare tra CometAPI e una connessione diretta a un provider diventa cosรฌ una modifica di configurazione, non di codice.
Versione Node.js
import OpenAI from 'openai';โconst apiKey = process.env.AI_API_KEY;if (!apiKey) throw new Error('AI_API_KEY non รจ impostata');โconst client = new OpenAI({ ย baseURL: process.env.AI_BASE_URL ?? 'https://api.cometapi.com/v1', ย apiKey,});โ// Gli ID dei modelli sono identificatori della piattaforma CometAPI โ vedi cometapi.com/modelsconst MODEL_CONFIG = { ย summarize: process.env.MODEL_SUMMARIZE ?? 'claude-opus-4-7', ย code: ย ย ย process.env.MODEL_CODE ย ย ย ?? 'gpt-5.4', ย classify: ย process.env.MODEL_CLASSIFY ย ?? 'claude-haiku-4-5', ย chat: ย ย ย process.env.MODEL_CHAT ย ย ย ?? 'gpt-5.4-mini',};โ// Valida all'avvioper (const [task, model] of Object.entries(MODEL_CONFIG)) { ย if (!model) throw new Error(`La configurazione del modello per '${task}' non รจ impostata`);}โ/** * Punto di ingresso unico per tutte le chiamate LLM. * Restituisce una risposta normalizzata. Genera eccezioni su 4xx, logga e rilancia su 5xx/di rete. */async function callModel(task, messages, options = {}) { ย const model = MODEL_CONFIG[task] ?? 'gpt-5.4-mini';โ ย let response; ย try { ย ย response = await client.chat.completions.create({ ย ย ย model, ย ย ย messages, ย ย ย ...options, ย }); } catch (err) { ย ย // Non ingoiare gli errori โ logga e rilancia ย ย console.error(`Errore API task=${task} model=${model}:`, err.message); ย ย throw err; }โ ย // content รจ null quando il modello attiva una tool call ย const content = response.choices[0].message.content ?? '';โ ย // usage potrebbe essere assente in alcune configurazioni ย const inputTokens ย = response.usage?.prompt_tokens ย ย ?? 0; ย const outputTokens = response.usage?.completion_tokens ?? 0;โ ย console.log(`task=${task} model=${model} input=${inputTokens} output=${outputTokens}`);โ ย return { content, model: response.model, inputTokens, outputTokens };}โ/** * Emetti in streaming i token dal modello instradato. * I dati di utilizzo non sono disponibili in modalitร streaming. */async function* streamModel(task, messages, options = {}) { ย const model = MODEL_CONFIG[task] ?? 'gpt-5.4-mini';โ ย const stream = await client.chat.completions.create({ ย ย model, ย ย messages, ย ย stream: true, ย ย ...options, ย });โ ย for await (const chunk of stream) { ย ย const delta = chunk.choices[0]?.delta?.content; ย ย if (delta) yield delta; }}โ// Uso โ sincrono/bloccanteconst result = await callModel('classify', [ { role: 'user', content: 'Positivo o negativo? "Mi รจ piaciuto!"' }]);console.log(result.content);โ// Uso โ streamingfor await (const token of streamModel('chat', [ { role: 'user', content: 'Ciao' }])) { ย process.stdout.write(token);}
Quale lock-in รจ accettabile
- Usare l' OpenAI SDK โ ร lo standard de facto. La maggior parte dei provider lo supporta. Lock-in a basso rischio.
- Funzionalitร specifiche del provider di cui hai davvero bisogno โ Se ti servono
logprobs, usali. Isola quel codice cosรฌ che sia facile da trovare e sostituire in seguito. - Modelli fine-tuned โ Un modello fine-tuned รจ intrinsecamente legato a un provider. ร previsto.
Il lock-in da evitare รจ quello accidentale โ nomi dei modelli nella logica di business, parsing delle risposte raw sparso tra i file, chiavi API hardcoded nel sorgente.
Cosa fare dopo
Ora hai un livello di astrazione che mantiene i dettagli del provider fuori dalla tua logica di business. L'ultimo articolo di questa serie tratta cosa succede quando qualcosa va storto: come fare il debug delle generazioni non riuscite, interpretare i codici di errore e costruire un error handling che ti dica davvero cosa si รจ rotto.
Prossimo: Come eseguire il debug delle generazioni API IA non riuscite
FAQ
D: Qual รจ la differenza tra lock-in dell'SDK e lock-in del modello?
Il lock-in dell'SDK significa che il tuo codice importa una libreria specifica e dovrebbe cambiare se passassi a un altro SDK. Il lock-in del modello significa che i nomi dei modelli sono sparsi nella logica di business. Il lock-in dell'SDK รจ meno pericoloso perchรฉ la maggior parte dei provider ora supporta il formato dell'SDK di OpenAI. Il lock-in del modello รจ piรน subdolo perchรฉ รจ piรน difficile da trovare e correggere.
D: Se uso CometAPI, sto solo scambiando il lock-in di OpenAI con quello di CometAPI?
Parzialmente. Stai scambiando il lock-in del provider diretto con uno strato proxy. Il vantaggio: una chiave, un endpoint, cambio di modello semplice. Il rischio: se CometAPI ha un outage, tutti i tuoi provider vanno giรน insieme. La mitigazione รจ giร nel codice sopra โ AI_BASE_URL รจ una variabile d'ambiente. Se devi bypassare CometAPI e chiamare un provider direttamente, รจ una modifica di configurazione, non di codice.
D: Posso usare il pensiero esteso di Claude o il reasoning_effort di OpenAI con questo pattern?
Sรฌ, passali come **kwargs a call_model. Sappi solo che se instradi quel task a un modello diverso, quei parametri verranno ignorati o causeranno un errore. Documenta quali task usano funzionalitร specifiche del provider, cosรฌ il prossimo sviluppatore saprร il perchรฉ.
D: Come gestisco il limite di temperature a 1.0 quando instrado tra Claude e GPT**?****
Mantieni temperature a 1.0 o inferiore per rimanere nell'intervallo sicuro per entrambi. Se ti serve una temperatura piรน alta per task creativi specificamente su GPT, instrada quei task a GPT esplicitamente in MODEL_CONFIG invece di lasciarli passare dal router generico.
D: Devo astrarre allo stesso modo le API di generazione immagini e video?
Gli stessi principi si applicano โ configurazione centrale, wrapper di risposta normalizzato, nessun campo specifico del provider nella logica di business. Le API di immagini e video hanno differenze strutturali maggiori (async vs sync, set di parametri diversi), quindi lo strato di astrazione richiede piรน lavoro. Parti dal testo, poi estendi il pattern una volta comprovata la struttura.
D: E le differenze nella context window tra modelli?
ร un rischio reale quando instradi. GPT-5.5 ha una context window da 1M token, i modelli Claude supportano fino a 200K e Gemini 3.5 Flash supporta fino a 1M. Se instradi un task con documento lungo a un modello con finestra di contesto piรน corta, l'input viene troncato in silenzio. Aggiungi un controllo sulla lunghezza del contesto prima dell'instradamento se i tuoi task coinvolgono input lunghi โ oppure instrada sempre i task con contesto lungo a un modello specifico in MODEL_CONFIG invece di lasciarli passare a un default.
