Leverandørlåsning i AI-apps sker sjældent på én gang. Den sniger sig ind — et direkte import openai her, et hårdkodet modelnavn der, et responsfelt du parser uden at tjekke, om andre udbydere returnerer det samme. Seks måneder senere betyder skift af udbyder, at du skal omskrive halvdelen af din backend.
De fire måder, lock-in opstår på
De fleste udviklere tror, at lock-in betyder "Jeg bruger OpenAI SDK." Det er den mindst farlige slags. De virkelige fælder er mere subtile:
| Lock-in-type | Hvordan det opstår | Konsekvens |
|---|---|---|
| SDK-lock-in | from openai import OpenAI overalt | Skift af SDK betyder ændringer i alle filer |
| Modelnavn-lock-in | model="gpt-4o" hårdkodet i forretningslogik | Hvert modelskift er en kodeændring |
| Parameter-lock-in | Brug af logprobs, n>1 eller reasoning_effort | Disse findes ikke på Claude eller Gemini |
| Responsformat-lock-in | Parser udbyderspecifikke responsfelter | Forskellige udbydere returnerer forskellige strukturer |
Målet er ikke at eliminere dem alle — nogle er acceptable tradeoffs. Målet er at vide, hvilke du påtager dig.
Brug et OpenAI-kompatibelt endpoint som dit abstraktionslag
Den reneste måde at undgå SDK-lock-in på er at bruge et enkelt OpenAI-kompatibelt endpoint, der ruter til flere udbydere. Du beholder OpenAI SDK, men backend kan være enhver udbyder.
CometAPI gør dette — ét endpoint, én nøgle, 500+ modeller på tværs af OpenAI, Anthropic, Google, DeepSeek, xAI og andre:
import osfrom openai import OpenAIfrom dotenv import load_dotenvload_dotenv()api_key = os.environ.get("AI_API_KEY")if not api_key: raise ValueError("AI_API_KEY environment variable is not set")client = OpenAI( base_url=os.environ.get("AI_BASE_URL", "https://api.cometapi.com/v1"), api_key=api_key,)
Skift fra GPT til Claude til Gemini er en ændring på én linje:
# Beforeresponse = client.chat.completions.create(model="gpt-5.4", messages=[...])# After — same code, different modelresponse = client.chat.completions.create(model="claude-sonnet-4-6", messages=[...])
Bemærk: Modelnavne som gpt-5.4 og claude-sonnet-4-6 er CometAPI’s platform-id’er — de virker kun via https://api.cometapi.com/v1, ikke direkte via OpenAI eller Anthropics API’er. Se fuld modelliste for hele kataloget og priser.
Hold modelnavne ude af din forretningslogik
Modelnavne spredt gennem din kode er den mest almindelige form for lock-in. Løsningen er en central konfiguration, der læser fra miljøvariabler:
# config.py — one place to change model assignmentsimport 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"),}# Validate at startup — fail fast rather than getting mysterious API errorsfor task, model in MODEL_CONFIG.items(): if not model: raise ValueError(f"Model config for '{task}' is not set")
Din forretningslogik refererer aldrig direkte til et modelnavn:
from config import MODEL_CONFIGdef summarize(text: str) -> str: response = client.chat.completions.create( model=MODEL_CONFIG["summarize"], messages=[{"role": "user", "content": f"Summarize: {text}"}], max_tokens=300 # move to config in production ) return response.choices[0].message.content
For at skifte summariseringsmodellen i hele din app ændrer du én miljøvariabel. Ingen grep, ingen find-og-erstat.
Pak responsen ind, så din kode ikke afhænger af udbyderspecifikke felter
Forskellige udbydere returnerer lidt forskellige responsstrukturer. Hvis du parser rå API-responser overalt i din kodebase, er du låst til den udbyders format.
Pak det ind i en normaliseret dataclass:
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: """ Single entry point for all LLM calls. Returns a normalized AIResponse regardless of which model handled it. Raises on 4xx (client errors). Logs and re-raises on 5xx/network errors. """ model = MODEL_CONFIG.get(task, "gpt-5.4-mini") if not model: raise ValueError(f"No model configured for task '{task}'") try: response: ChatCompletion = client.chat.completions.create( model=model, messages=messages, **kwargs ) except APIStatusError as e: logging.error(f"API error for task={task} model={model}: {e.status_code} {e.message}") raise except (APIConnectionError, APITimeoutError) as e: logging.error(f"Network error for task={task} model={model}: {e}") raise # content is None when the model triggers a tool call instead of returning text content = response.choices[0].message.content or "" # usage is None in streaming mode — default to 0 if not available 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, )
Nu arbejder din forretningslogik med AIResponse-objekter, ikke rå API-responser. Hvis en udbyder ændrer deres responsformat, retter du det ét sted.
Tilføj streaming-understøttelse til wrapperen
Til chatgrænseflader vil du have streaming. Wrapperen håndterer det som en separat sti:
from typing import Iteratordef stream_model(task: str, messages: list, **kwargs) -> Iterator[str]: """ Stream tokens from the routed model. Note: streaming doesn't return usage data. Fallback is not supported in streaming mode — you've already started yielding tokens before you know if the full request succeeds. """ model = MODEL_CONFIG.get(task, "gpt-5.4-mini") if not model: raise ValueError(f"No model configured for 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# Usagefor token in stream_model("chat", [{"role": "user", "content": "Hello"}]): print(token, end="", flush=True)
Kend de parametre, der skaber lock-in
Nogle parametre findes kun hos specifikke udbydere. At bruge dem er fint — vær blot bevidst om, at du tager et valg:
| Parameter | Virker på | Lock-in-risiko |
|---|---|---|
| logprobs | Kun GPT | Høj — ingen ækvivalent på Claude eller Gemini |
| n > 1 | GPT, Gemini (ikke Claude) | Middel — Claude kræver looping |
| reasoning_effort | Kun GPT o-serien | Høj — ingen ækvivalent andre steder |
| temperature > 1.0 | GPT, Gemini (ikke Claude) | Lav — Claude begrænser til 1.0 |
| tools | Alle større udbydere | Ingen — sikker at bruge |
| response_format | Alle større udbydere | Lav — mindre skemaforskelle |
Hvis du bruger logprobs til confidence scoring, er du låst til GPT for den funktion. Det er et rimeligt tradeoff — dokumentér det, så den næste udvikler ved hvorfor.
Gør udbyder-endpointet konfigurerbart
At hårdkode base_url="https://api.cometapi.com/v1" er stadig en form for lock-in. Gør det til en miljøvariabel:
# .env — using CometAPIAI_BASE_URL=https://api.cometapi.com/v1AI_API_KEY=your_cometapi_key# To switch to OpenAI directly, change two lines:# AI_BASE_URL=https://api.openai.com/v1# AI_API_KEY=your_openai_key
Klientinitialiseringen fra trin 1 læser allerede fra disse variabler. Skift mellem CometAPI og en direkte udbyderforbindelse er nu en konfigurationsændring, ikke en kodeændring.
Node.js-version
import OpenAI from 'openai';const apiKey = process.env.AI_API_KEY;if (!apiKey) throw new Error('AI_API_KEY is not set');const client = new OpenAI({ baseURL: process.env.AI_BASE_URL ?? 'https://api.cometapi.com/v1', apiKey,});// Model IDs are CometAPI platform identifiers — see 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',};// Validate at startupfor (const [task, model] of Object.entries(MODEL_CONFIG)) { if (!model) throw new Error(`Model config for '${task}' is not set`);}/** * Single entry point for all LLM calls. * Returns normalized response. Raises on 4xx, logs and re-raises on 5xx/network. */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) { // Don't swallow errors — log and re-raise console.error(`API error task=${task} model=${model}:`, err.message); throw err; } // content is null when model triggers a tool call const content = response.choices[0].message.content ?? ''; // usage may be absent in some configurations 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 };}/** * Stream tokens from the routed model. * Usage data is not available in streaming mode. */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; }}// Usage — blockingconst result = await callModel('classify', [ { role: 'user', content: 'Positive or negative? "Loved it!"' }]);console.log(result.content);// Usage — streamingfor await (const token of streamModel('chat', [ { role: 'user', content: 'Hello' }])) { process.stdout.write(token);}
Hvilken lock-in er acceptabel
Ikke al lock-in er værd at kæmpe imod. Nogle tradeoffs giver mening:
- At bruge OpenAI SDK'en — Det er de facto-standarden. De fleste udbydere understøtter det. Lavrisiko lock-in.
- Udbyderspecifikke funktioner, du faktisk har brug for — Hvis du har brug for
logprobs, så brug dem. Isolér den kode, så den er let at finde og erstatte senere. - Fintunede modeller — En fintunet model er i sagens natur bundet til én udbyder. Det er forventeligt.
Den lock-in, der er værd at undgå, er den utilsigtede — modelnavne i forretningslogik, rå responsparsing spredt over filer, API-nøgler hårdkodet i kildekoden.
Hvad er det næste
Du har nu et abstraktionslag, der holder udbyderdetaljer ude af din forretningslogik. Den sidste artikel i denne serie dækker, hvad der sker, når ting går galt: hvordan du debugger mislykkede genereringer, fortolker fejlkoder, og bygger fejlhåndtering, der faktisk fortæller dig, hvad der gik i stykker.
Næste: Sådan debugger du fejlede AI API-genereringer
FAQ
Q: Hvad er forskellen på SDK-lock-in og model-lock-in?
SDK-lock-in betyder, at din kode importerer et specifikt bibliotek og skal ændres, hvis du skifter SDK. Model-lock-in betyder, at modelnavne er spredt gennem din forretningslogik. SDK-lock-in er mindre farlig, fordi de fleste udbydere nu understøtter OpenAI SDK-formatet. Model-lock-in er mere lumsk, fordi den er sværere at finde og rette.
Q: Hvis jeg bruger CometAPI, bytter jeg så bare OpenAI lock-in for CometAPI-lock-in?
Delvist. Du bytter lock-in til en direkte udbyder for et proxy-lag. Fordelen: én nøgle, ét endpoint, nem modelswitching. Risikoen: hvis CometAPI har nedetid, går alle dine udbydere ned samtidig. Afbødningen er allerede i koden ovenfor — AI_BASE_URL er en miljøvariabel. Hvis du skal omgå CometAPI og kalde en udbyder direkte, er det en konfigurationsændring, ikke en kodeændring.
Q: Kan jeg bruge Claudes extended thinking eller OpenAI’s reasoning_effort med dette mønster?
Ja, giv dem videre som **kwargs til call_model. Vær blot opmærksom på, at hvis du ruter den opgave til en anden model, vil disse parametre blive ignoreret eller give en fejl. Dokumentér hvilke opgaver der bruger udbyderspecifikke funktioner, så den næste udvikler ved hvorfor.
Q: Hvordan håndterer jeg Claudes temperature begrænsning på 1.0, når jeg ruter mellem Claude og GPT**?**
Hold temperature på eller under 1.0 for at være i det sikre område for begge. Hvis du har brug for højere temperatur til kreative opgaver specifikt på GPT, så ruter du de opgaver eksplicit til GPT i MODEL_CONFIG i stedet for at lade dem falde igennem den generiske router.
Q: Skal jeg abstrahere billed- og videogenererings-API’er på samme måde?
De samme principper gælder — central konfiguration, normaliseret respons-wrapper, ingen udbyderspecifikke felter i forretningslogik. Billed- og video-API’er har flere strukturforskelle (asynk vs. synk, forskellige parameter-sæt), så abstraktionslaget kræver mere arbejde. Start med tekst, og udvid mønstret, når strukturen er bevist.
Q: Hvad med forskelle i kontekstvindue mellem modeller?
Dette er en reel risiko ved routing. GPT-5.5 har et kontekstvindue på 1M tokens, Claude-modeller understøtter op til 200K, og Gemini 3.5 Flash understøtter op til 1M. Hvis du ruter en lang dokumentopgave til en model med kortere kontekstvindue, bliver inputtet stiltiende trunkeret. Tilføj et kontekstlængdetjek før routing, hvis dine opgaver involverer lange input — eller rout altid opgaver med langt kontekstvindue til en specifik model i MODEL_CONFIG i stedet for at lade dem falde igennem til en standard.
