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_dotenvload_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 osMODEL_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_CONFIGdef 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: intdef 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 Iteratordef 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.
