Claude Opus 5 is now live on CometAPI →

Sådan bygger du AI-apps, der ikke er låst til én leverandør

CometAPI
AnnaJun 7, 2026
Sådan bygger du AI-apps, der ikke er låst til én leverandør

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-typeHvordan det opstårKonsekvens
SDK-lock-infrom openai import OpenAI overaltSkift af SDK betyder ændringer i alle filer
Modelnavn-lock-inmodel="gpt-4o" hårdkodet i forretningslogikHvert modelskift er en kodeændring
Parameter-lock-inBrug af logprobs, n>1 eller reasoning_effortDisse findes ikke på Claude eller Gemini
Responsformat-lock-inParser udbyderspecifikke responsfelterForskellige 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_dotenv​load_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 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"),}​# 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_CONFIG​def 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: int​def 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 Iterator​def 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:

ParameterVirker påLock-in-risiko
logprobsKun GPTHøj — ingen ækvivalent på Claude eller Gemini
n > 1GPT, Gemini (ikke Claude)Middel — Claude kræver looping
reasoning_effortKun GPT o-serienHøj — ingen ækvivalent andre steder
temperature > 1.0GPT, Gemini (ikke Claude)Lav — Claude begrænser til 1.0
toolsAlle større udbydereIngen — sikker at bruge
response_formatAlle større udbydereLav — 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.

Klar til at skære AI-udviklingsomkostninger med 20%?

Kom gratis i gang på få minutter. Gratis prøvekreditter inkluderet. Intet kreditkort påkrævet.

Læs mere