Ràng buộc nhà cung cấp (vendor lock-in) trong ứng dụng AI thường không xảy ra một lần. Nó len lỏi dần — chỗ này thêm một import openai, chỗ kia hardcode tên model, rồi bạn parse một trường phản hồi mà không kiểm tra nhà cung cấp khác có trả về giống vậy không. Sáu tháng sau, đổi nhà cung cấp đồng nghĩa với việc phải viết lại nửa backend.
Bốn cách lock-in xảy ra
Hầu hết lập trình viên nghĩ lock-in nghĩa là “Tôi đang dùng OpenAI SDK.” Đó là kiểu ít nguy hiểm nhất. Bẫy thật sự tinh vi hơn:
| Loại lock-in | Cách nó xảy ra | Hệ quả |
|---|---|---|
| Lock-in SDK | from openai import OpenAI everywhere | Đổi SDK nghĩa là phải chạm vào mọi file |
| Lock-in tên model | model="gpt-4o" hardcoded trong business logic | Mỗi lần đổi model là một lần đổi code |
| Lock-in tham số | Using logprobs, n>1, or reasoning_effort | Không tồn tại trên Claude hoặc Gemini |
| Lock-in định dạng phản hồi | Parsing provider-specific response fields | Mỗi nhà cung cấp trả về cấu trúc khác nhau |
Mục tiêu không phải loại bỏ tất cả — một số là đánh đổi chấp nhận được. Mục tiêu là biết mình đang chấp nhận những lock-in nào.
Dùng một endpoint tương thích OpenAI làm lớp trừu tượng
Cách sạch nhất để tránh lock-in SDK là dùng một endpoint tương thích OpenAI duy nhất để định tuyến tới nhiều nhà cung cấp. Bạn giữ OpenAI SDK, nhưng backend có thể là bất kỳ nhà cung cấp nào.
CometAPI làm điều này — một endpoint, một key, 500+ model từ OpenAI, Anthropic, Google, DeepSeek, xAI, và các bên khác:
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,)
Chuyển từ GPT sang Claude hay Gemini chỉ cần đổi một dòng:
# 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=[...])
Lưu ý: Tên model như gpt-5.4 và claude-sonnet-4-6 là định danh trên nền tảng CometAPI — chúng chỉ hoạt động qua https://api.cometapi.com/v1, không hoạt động trực tiếp qua API của OpenAI hay Anthropic. Xem danh sách model đầy đủ để biết toàn bộ catalog và giá.
Tránh đưa tên model vào business logic
Tên model rải rác khắp code là dạng lock-in phổ biến nhất. Cách khắc phục là đặt cấu hình tập trung đọc từ biến môi trường:
# 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")
Business logic của bạn sẽ không tham chiếu trực tiếp tên model:
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
Để đổi model tóm tắt trên toàn bộ ứng dụng, chỉ cần đổi một biến môi trường. Không cần grep hay find-and-replace.
Bọc phản hồi để code không phụ thuộc vào trường riêng của từng nhà cung cấp
Mỗi nhà cung cấp trả về cấu trúc phản hồi hơi khác nhau. Nếu bạn parse raw API responses khắp codebase, bạn sẽ bị khóa vào định dạng của nhà cung cấp đó.
Hãy bọc chúng thành một dataclass chuẩn hóa:
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, )
Giờ business logic của bạn làm việc với đối tượng AIResponse, không phải phản hồi API thô. Nếu một nhà cung cấp đổi định dạng phản hồi, bạn chỉ cần sửa ở một chỗ.
Thêm hỗ trợ streaming vào wrapper
Với giao diện chat, bạn sẽ cần streaming. Wrapper xử lý nó theo một luồng riêng:
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)
Biết tham số nào tạo ra lock-in
Một số tham số chỉ tồn tại trên các nhà cung cấp nhất định. Dùng chúng là điều bình thường — chỉ cần biết bạn đang đưa ra lựa chọn có chủ đích:
| Tham số | Hoạt động trên | Rủi ro lock-in |
|---|---|---|
| logprobs | Chỉ GPT | Cao — không có tương đương trên Claude hoặc Gemini |
| n > 1 | GPT, Gemini (không Claude) | Trung bình — Claude cần lặp thủ công |
| reasoning_effort | Chỉ GPT o-series | Cao — không có tương đương ở nơi khác |
| temperature > 1.0 | GPT, Gemini (không Claude) | Thấp — Claude giới hạn ở 1.0 |
| tools | Tất cả nhà cung cấp lớn | Không — an toàn để dùng |
| response_format | Tất cả nhà cung cấp lớn | Thấp — khác biệt nhỏ về schema |
Nếu bạn dùng logprobs để chấm điểm độ tin cậy, bạn bị khóa vào GPT cho tính năng đó. Đây là đánh đổi hợp lý — chỉ cần ghi chú lại để developer tiếp theo biết lý do.
Cho phép cấu hình endpoint của nhà cung cấp
Hard-code base_url="https://api.cometapi.com/v1" vẫn là một dạng lock-in. Hãy đưa nó thành biến môi trường:
# .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
Khởi tạo client từ Bước 1 đã đọc từ các biến này. Chuyển giữa CometAPI và kết nối trực tiếp tới nhà cung cấp giờ là thay đổi cấu hình, không phải thay đổi code.
Phiên bản Node.js
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);}
Lock-in nào chấp nhận được
Không phải lock-in nào cũng đáng để chống lại. Một số đánh đổi hợp lý:
- Dùng SDK của OpenAI — Đây là tiêu chuẩn thực tế. Hầu hết nhà cung cấp hỗ trợ nó. Rủi ro lock-in thấp.
- Tính năng riêng của nhà cung cấp mà bạn thật sự cần — Nếu bạn cần
logprobs, hãy dùng. Cô lập đoạn code đó để sau này dễ tìm và thay thế. - Model fine-tuned — Model fine-tuned vốn gắn với một nhà cung cấp. Điều này là hiển nhiên.
Lock-in đáng tránh là kiểu vô tình — tên model trong business logic, parse phản hồi thô rải khắp file, API key hardcode trong source.
Tiếp theo là gì
Bạn đã có một lớp trừu tượng giúp chi tiết về nhà cung cấp không rò rỉ vào business logic. Bài cuối trong chuỗi này nói về chuyện khi mọi thứ trục trặc: cách debug các lượt sinh thất bại, diễn giải mã lỗi, và xây dựng xử lý lỗi thực sự cho biết điều gì bị hỏng.
Tiếp theo: How to Debug Failed AI API Generations
FAQ
Hỏi: Sự khác biệt giữa lock-in SDK và lock-in model là gì?
Lock-in SDK nghĩa là code của bạn import một thư viện cụ thể và sẽ phải thay đổi nếu bạn chuyển SDK. Lock-in model nghĩa là tên model rải rác trong business logic. Lock-in SDK ít nguy hiểm hơn vì hầu hết nhà cung cấp hiện hỗ trợ định dạng OpenAI SDK. Lock-in model tinh vi hơn vì khó tìm và sửa.
Hỏi: Nếu tôi dùng CometAPI, có phải tôi chỉ đổi lock-in OpenAI sang lock-in CometAPI?
Một phần. Bạn đang đổi lock-in nhà cung cấp trực tiếp sang một lớp proxy. Lợi ích: một key, một endpoint, đổi model dễ dàng. Rủi ro: nếu CometAPI gặp sự cố, tất cả các nhà cung cấp của bạn sẽ cùng ngừng hoạt động. Biện pháp giảm thiểu đã có trong code ở trên — AI_BASE_URL là biến môi trường. Nếu cần bỏ qua CometAPI và gọi trực tiếp một nhà cung cấp, đó chỉ là thay đổi cấu hình, không phải thay đổi code.
Hỏi: Tôi có thể dùng extended thinking của Claude hoặc reasoning_effort của OpenAI theo mẫu này không?
Có, truyền chúng qua **kwargs vào call_model. Chỉ cần biết rằng nếu bạn định tuyến tác vụ đó sang model khác, các tham số đó sẽ bị bỏ qua hoặc gây lỗi. Hãy ghi chú tác vụ nào dùng tính năng riêng của nhà cung cấp để developer tiếp theo biết lý do.
Hỏi: Tôi xử lý giới hạn temperature 1.0 của Claude thế nào khi định tuyến giữa Claude và GPT?
Giữ temperature ở mức 1.0 hoặc thấp hơn để an toàn cho cả hai. Nếu bạn cần temperature cao hơn cho tác vụ sáng tạo trên GPT, hãy định tuyến rõ các tác vụ đó tới GPT trong MODEL_CONFIG thay vì để chúng rơi vào router mặc định.
Hỏi: Tôi có nên trừu tượng hóa API tạo ảnh và video theo cách tương tự?
Nguyên tắc tương tự áp dụng — cấu hình tập trung, wrapper chuẩn hóa phản hồi, không đưa trường riêng của nhà cung cấp vào business logic. API ảnh và video có nhiều khác biệt về cấu trúc (bất đồng bộ vs đồng bộ, bộ tham số khác nhau) nên lớp trừu tượng sẽ tốn công hơn. Hãy bắt đầu với văn bản, rồi mở rộng khi cấu trúc đã vững.
Hỏi: Còn sự khác nhau về cửa sổ ngữ cảnh giữa các model thì sao?
Đây là rủi ro thật khi định tuyến. GPT-5.5 có cửa sổ ngữ cảnh 1M token, các model Claude hỗ trợ tới 200K, và Gemini 3.5 Flash hỗ trợ tới 1M. Nếu bạn định tuyến tài liệu dài sang model có cửa sổ ngữ cảnh ngắn hơn, input sẽ bị cắt lặng lẽ. Hãy thêm bước kiểm tra độ dài ngữ cảnh trước khi định tuyến nếu tác vụ của bạn liên quan tới đầu vào dài — hoặc luôn định tuyến các tác vụ ngữ cảnh dài tới một model cụ thể trong MODEL_CONFIG thay vì để chúng rơi vào mặc định.
