0

Mình đã chuyển endpoint suy luận của Claude Code sang gateway riêng như thế nào (và 4 lệnh để biết chắc nó đã chạ

Cấu hình endpoint bên thứ ba cho Claude Code

Mở đầu

Chào mọi người 👋

Hôm trước mình có việc cần trỏ request suy luận của Claude Code sang một gateway khác thay vì endpoint chính thức. Nghe thì đơn giản — chỉ là vài biến môi trường thôi mà. Và đúng là phần cấu hình thì đơn giản thật.

Cái làm mình mất thời gian lại nằm ở chỗ khác: làm sao biết nó đã thực sự chạy?

Mình gõ claude --version, nó trả về số version ngon lành, mình tưởng xong rồi. Nhưng nghĩ kỹ thì cái đó chỉ chứng minh được client đã cài thôi. Endpoint có tới được không, key có hợp lệ không, model có trả về gì không — chưa có cái nào được kiểm chứng cả.

Trong bài này mình sẽ chia sẻ toàn bộ quá trình: từ chỗ xác định nên dùng script nào, script thực sự ghi gì vào máy, cấu hình qua giao diện desktop, và quan trọng nhất là 4 lệnh để tách bạch "đã cài" với "đã chạy thông".

Mọi số liệu trong bài đều là mình chạy thật trên máy mình, không phải copy từ tài liệu.

Kiến thức nền

Cơ chế bên dưới — hiểu cái này trước sẽ đỡ được nửa số lần bối rối sau

Mình dành riêng một mục nhỏ cho phần này, vì nếu không có mô hình trong đầu thì mấy thông báo lỗi ở phía sau đọc không ra nghĩa gì.

Claude Code chỉ là một client, đúng nghĩa. Lúc khởi động nó cần biết hai thứ: gửi đi đâu và xác thực bằng gì. Nó đọc cả hai từ cấu hình trên máy, rồi dựng một request HTTPS bình thường và gửi đi — giống y như browser mở một trang web. Không có daemon nào chạy nền, không có bước đăng ký ở đâu cả, không có gì "thương lượng" giúp bạn.

Vì vậy chỗ có thể hỏng cũng chỉ có ba:

Vùng hỏng Biểu hiện
Giá trị không nằm ở nơi client đọc Nó im lặng dùng endpoint mặc định, hoặc không chạy
Giá trị có nhưng sai format Request vẫn bay đi, nhưng tới một chỗ không tồn tại
Giá trị đúng nhưng phía kia từ chối Key hết hạn, hết quota, model ID không được nhận

Và đây là điểm mấu chốt của cả bài: claude --version — lệnh ai cũng gõ đầu tiên — chỉ chạm tới vùng thứ nhất, mà còn chưa chạm hết. Vùng hai và vùng ba với nó là hoàn toàn vô hình.

Claude Code đọc cấu hình từ đâu?

Dù bạn cấu hình bằng script hay bằng giao diện, thứ cuối cùng được ghi vào hệ thống là 6 biến môi trường ở mức user:

ANTHROPIC_BASE_URL     địa chỉ gateway (KHÔNG có /v1)
ANTHROPIC_AUTH_TOKEN   token
ANTHROPIC_MODEL        model mặc định
CLAUDE_MODEL           model mặc định
OPENAI_API_KEY         cùng token đó
OPENAI_BASE_URL        địa chỉ gateway (CÓ /v1)

Tại sao lại ghi hai bộ? Vì Claude Code đọc theo quy ước của Anthropic, còn kha khá tool khác trong hệ sinh thái lại đọc theo quy ước OpenAI-compatible. Ghi cả hai thì một token dùng được cho cả hai phía.

Chỗ này là cái bẫy đầu tiên: hai địa chỉ không đối xứng nhau. ANTHROPIC_BASE_URL chỉ điền domain gốc, client sẽ tự ghép /v1/messages vào. Nếu bạn tự thêm /v1 thì nó thành /v1/v1/messages — đây là cách viết sai. Gateway nào route chặt theo path sẽ trả 404 để bạn thấy ngay; nhưng cũng có gateway dễ tính, vẫn trả 200 như thường. Mình thử lại cái gateway mình đang dùng thì đúng là trường hợp thứ hai, và nó khó phát hiện hơn nhiều vì màn hình không báo gì cả. Còn OPENAI_BASE_URL thì bắt buộc phải có /v1.

Mình nói trước vì đây là lỗi mà nếu cấu hình tay, rất dễ "thống nhất format" cho đẹp rồi dính.

Nơi lưu trữ

  • Windows: ghi thẳng vào biến môi trường mức User
  • macOS / Linux: ghi vào file ~/.crazyrouter-claude-code.env, rồi thêm một dòng load file đó vào shell startup file

Một chi tiết mình thấy khá tinh tế: script chọn startup file dựa trên login shell thật sự của bạn, chứ không phải dựa trên file nào đang tồn tại. Trên server nhiều khi có sẵn một cái .zshrc mà bạn chẳng dùng, nếu check theo kiểu "file có tồn tại không" thì sẽ ghi nhầm chỗ.

Nếu bạn tự ghi tay thì nhớ kiểm tra trước bằng echo $SHELL, rồi mới quyết định viết vào .zshrc hay .bashrc.

Ghi tay 6 biến (khi không muốn chạy script)

Trường hợp bạn không muốn chạy script của ai cả — hoàn toàn hợp lý — thì đây là toàn bộ nội dung nó ghi, viết bằng tay:

$gw    = 'https://api.crazyrouter.com'
$token = 'sk-...'
$model = 'claude-opus-4-8'

[Environment]::SetEnvironmentVariable('ANTHROPIC_BASE_URL',   $gw,      'User')
[Environment]::SetEnvironmentVariable('ANTHROPIC_AUTH_TOKEN', $token,   'User')
[Environment]::SetEnvironmentVariable('ANTHROPIC_MODEL',      $model,   'User')
[Environment]::SetEnvironmentVariable('CLAUDE_MODEL',         $model,   'User')
[Environment]::SetEnvironmentVariable('OPENAI_API_KEY',       $token,   'User')
[Environment]::SetEnvironmentVariable('OPENAI_BASE_URL',      "$gw/v1", 'User')

Để ý /v1 chỉ xuất hiện ở dòng cuối. Tham số 'User' nghĩa là giá trị thuộc về account của bạn và sống qua reboot — đừng dùng 'Machine' trừ khi bạn thật sự muốn mọi account trên máy dùng chung token.

Trên macOS / Linux thì cũng sáu giá trị đó, export vào file env trong home rồi load từ startup file của shell.

Ba cách cấu hình, chọn cách nào?

Thực ra có tới ba đường để cấu hình, và mình nghĩ nên biết cả ba trước khi bắt đầu:

Cách 1 — dùng script. Nhanh nhất, ba câu hỏi là xong. Phù hợp khi bạn cấu hình cho cả máy hoặc phải làm lại nhiều lần trên nhiều máy.

Cách 2 — dùng giao diện desktop. Chỉ ảnh hưởng tới app desktop, không ảnh hưởng tới CLI. Phù hợp khi bạn chỉ dùng app và không muốn đụng vào biến môi trường hệ thống.

Cách 3 — cấu hình theo từng project. Tạo file .claude/settings.json ở gốc project:

{
  "env": {
    "ANTHROPIC_BASE_URL": "https://api.crazyrouter.com",
    "ANTHROPIC_AUTH_TOKEN": "sk-token-cua-ban",
    "ANTHROPIC_MODEL": "claude-opus-4-8"
  }
}

Cách này mình thấy hữu ích nhất khi bạn có nhiều project dùng gateway khác nhau, hoặc khi làm việc trên máy chung mà không muốn ghi token vào biến môi trường toàn hệ thống.

Nhưng lưu ý: file này là plaintext và rất dễ bị commit lên repo do vô ý. Nếu chọn cách 3, nhớ thêm nó vào .gitignore ngay từ đầu, đừng để lần sau mới nghĩ tới.

Mình liệt kê nhanh ưu nhược điểm để dễ so:

Script GUI desktop .claude/settings.json
Tốc độ Nhanh nhất Trung bình Nhanh
Phạm vi ảnh hưởng Toàn máy Chỉ app desktop Chỉ project đó
Tự dọn format URL Có Không Không
Rủi ro lộ token Biến môi trường Lưu trong app Dễ commit nhầm
Phù hợp với Server, nhiều máy Một máy, ít thay đổi Nhiều project khác gateway

Thực hành từng bước

Bước 1: Xác định mình đang ở tình huống nào

Trước tiên chạy lệnh này:

claude --version
Kết quả Dùng script nào
Ra số version configure — chỉ ghi cấu hình
Báo không tìm thấy lệnh setup — cài dependency rồi mới cấu hình

Tin vui là script configure có hard check: việc đầu tiên nó làm là tìm claude trong PATH, không thấy thì báo lỗi thoát luôn, không đi tiếp. Nên chọn sai cũng không hỏng gì, nó chỉ từ chối chạy thôi 😄

Chiều ngược lại thì không được như vậy — lấy setup chạy trên môi trường đã cấu hình xong sẽ cài lại dependency. Nên đừng bỏ qua bước này.

Bước 2: Chạy script

Lệnh cài đặt cho hai nền tảng

Đã có Claude Code rồi (chỉ cấu hình):

curl -fsSL https://raw.githubusercontent.com/xujfcn/crazyrouter-claude-code/main/configure.sh | bash
irm https://raw.githubusercontent.com/xujfcn/crazyrouter-claude-code/main/windows/configure.ps1 | iex

Chưa có gì cả thì dùng bản cài đầy đủ, nằm cùng repo cùng thư mục — chỉ cần đổi chữ configure trong lệnh trên thành setup (setup.sh / windows/setup.ps1). Bản đó cài Git, Node.js, Claude Code trước rồi mới ghi cấu hình.

Chiều ngược lại thì đừng: chạy setup trên môi trường đã cấu hình xong sẽ cài lại dependency.

Mình luôn khuyên mở link bằng trình duyệt đọc qua một lượt trước khi chạy. Cái này không liên quan đến việc ai viết script — hễ là dạng | bash hay | iex thì nên làm vậy. Mấy chi tiết mình viết dưới đây chính là đọc source ra chứ không phải đoán.

Bước 3: Script hỏi gì

Chỉ ba thứ thôi, hai cái sau có giá trị mặc định nên Enter là xong:

Hỏi gì Bắt buộc Hành vi thực tế
Token Có Gõ vào không hiện lên màn hình. Paste xong nhìn trống trơn là bình thường, đừng paste lại nhiều lần. Để trống thì báo lỗi thoát
Base URL Không Có mặc định. Tự động bỏ dấu / thừa ở cuối
Model Không Có mặc định, sau đổi lúc nào cũng được

Hai điểm mình đọc source mới biết:

  • Token có pre-check định dạng (prefix sk- / cr- / rk-), nhưng không khớp thì chỉ in warning chứ không chặn. Thấy warning đừng vội làm lại từ đầu, xem phía sau có lỗi thật không đã.
  • Output của script là tiếng Anh. Thấy một màn hình tiếng Anh là bình thường nhé.

Nếu bạn cần deploy không tương tác (trên server chẳng hạn), truyền token qua biến môi trường trước:

export CRAZYROUTER_TOKEN="token-cua-ban"
curl -fsSL https://raw.githubusercontent.com/xujfcn/crazyrouter-claude-code/main/configure.sh | bash

(Server trắng chưa có Claude Code thì cũng đổi configure thành setup như trên.)

Nhánh này còn xử lý thêm một chuyện: curl | bash thường chạy trong non-login shell nên không thấy đường dẫn npm global. Script sẽ chủ động tìm claude ở /usr/local/bin, ~/.local/bin, ~/.npm-global/bin. Kể cả cuối cùng vẫn không định vị được, nó vẫn ghi xong cấu hình và in thông tin chẩn đoán, không để lại trạng thái dở dang.

Bước 4: Cấu hình bằng giao diện desktop

Nếu dùng app desktop thì không cần đụng vào biến môi trường. Nhưng màn hình này mặc định bị ẩn — mình mò trong Settings mãi không thấy, hóa ra nó nằm dưới developer mode.

Help → Troubleshooting → Enable Developer Mode

Xác nhận xong app tự restart một lần, lúc đó menu bên trái mới mọc thêm mục Developer:

Mục Developer xuất hiện sau khi bật developer mode

Mở ra, chọn Configure third-party inference:

Lối vào cấu hình third-party inference

Vào trong chọn tab Connection bên trái. Đây là sau khi mình cấu hình xong:

Màn hình cấu hình gateway

Nói rõ một chỗ cho khỏi ngộ nhận: máy mình vốn đã bật developer mode từ trước, nên mình không tự kiểm chứng được rằng tắt nó đi thì màn hình này biến mất. Đường dẫn menu ở trên mình ghi theo tài liệu chính thức, không phải theo kết quả thử nghiệm của mình.

Giải thích từng ô:

  • Credential kind: chọn Static API key. Ý nghĩa là "khóa nguồn credential" — chọn rồi thì chỉ dùng đúng nguồn này, không fallback về login session hay biến môi trường nữa. Nếu điền xong mà request vẫn đi về địa chỉ cũ, kiểm tra ô này đầu tiên.
  • Gateway base URL: giao diện không tự dọn format như script. Không có / ở cuối, không tự thêm /v1 (client sẽ ghép path, thêm vào là thành /v1/v1/...), không mang query parameter.
  • Gateway API key: paste xong bấm con mắt bên phải xem lại. Copy từ web hay dính một khoảng trắng vô hình, và lỗi lúc đó trông y hệt "key sai".
  • Gateway auth scheme: bearer hoặc x-api-key. Cái này quyết định key được gửi dưới dạng Authorization: Bearer xxx hay x-api-key: xxx.
  • Artifact preview iframe origin: không có nhu cầu gì đặc biệt thì để trống.

Thứ tự hai nút cuối không được sai: bấm Test connection ở góc trên phải trước, qua rồi mới bấm Apply Changes ở góc dưới phải. Quên cái thứ hai là cấu hình không được lưu.

Một hướng debug nữa: bên trái còn có mục Sandbox & workspace và Egress — bản desktop có sandbox cho traffic của tool. Nếu điền đúng hết mà Test connection vẫn không qua, thử xem domain gateway của bạn đã nằm trong danh sách host được phép ra ngoài chưa. Cái này máy mình không gặp (cấu hình hiện tại thông thẳng), mình chỉ ghi lại làm hướng debug thôi.

Kết quả và nhận xét

4 lệnh để biết chắc nó đã chạy

Đây là phần mình muốn chia sẻ nhất trong bài. Đừng dừng ở claude --version.

Kết quả thực tế của 4 lệnh kiểm tra

Lệnh 1 — client đã cài chưa?

claude --version

Máy mình ra 2.1.283.

Lệnh 2 — runtime còn không?

node --version

Máy mình ra v24.21.0.

Lệnh 3 — cấu hình có thực sự được ghi vào không?

env | grep -E 'ANTHROPIC|OPENAI'
'ANTHROPIC_BASE_URL','ANTHROPIC_AUTH_TOKEN','ANTHROPIC_MODEL','CLAUDE_MODEL','OPENAI_API_KEY','OPENAI_BASE_URL' |
  ForEach-Object { "{0,-22} {1}" -f $_, [Environment]::GetEnvironmentVariable($_,'User') }

Nhìn hai thứ: đủ 6 biến chưa, và OPENAI_BASE_URL có /v1 ở cuối còn ANTHROPIC_BASE_URL thì không.

Lệnh 4 — endpoint, key, model có thông cùng lúc không? (quan trọng nhất)

curl -s https://api.crazyrouter.com/v1/messages \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ANTHROPIC_AUTH_TOKEN" \
  -H "anthropic-version: 2023-06-01" \
  -d '{
    "model": "claude-opus-4-8",
    "max_tokens": 16,
    "messages": [{"role": "user", "content": "reply with: pong"}]
  }'

Gửi một request tối thiểu max_tokens=16, xem có trả 200 và có nội dung trong body không.

Tình huống 3 lệnh đầu qua hết mà lệnh 4 rớt là cực kỳ phổ biến: key hết hạn, hết quota, model ID không được chấp nhận, phần mềm proxy phân luồng domain đi hướng khác — tất cả đều dừng ở đây, và ba lệnh đầu không báo lỗi gì cả.

Về auth header, mình gửi thật mỗi loại một request: trên gateway này cả bearer lẫn x-api-key đều trả 200 và lấy được pong. Nhưng đây không phải quy luật chung — đa số gateway chỉ nhận một loại, và chọn sai thì thông báo lỗi gần như không phân biệt được với key hỏng.

Ba chỗ mình thấy phản trực giác

Một, hai Base URL không đối xứng. Nói lại lần nữa vì mình nghĩ đây là chỗ tốn thời gian nhất: một cái không /v1, một cái có.

Hai, cấu hình xong bắt buộc mở terminal mới. Biến môi trường chỉ có hiệu lực với process mới sinh ra. Kiểm tra trong cửa sổ cũ thì chắc chắn thấy "command not found" — đó là kết quả đúng, không phải cấu hình hỏng. Dòng Next step đầu tiên script in ra chính là "Open a NEW PowerShell window", nhưng nó bị một màn hình tiếng Anh che mất.

Ba, địa chỉ endpoint không được mang query parameter. Nhiều người copy từ link chia sẻ nên đuôi còn dính ?utm_source=.... Trên trình duyệt thì chẳng sao, điền vào endpoint API thì nhẹ là bị bỏ qua, nặng là không qua validate.

Hai chuyện mình tưởng hỏng mà hóa ra không

Script chết giữa chừng có làm hỏng cấu hình không? Mình chạy thử trong môi trường non-interactive, script dừng ở bước đọc token với exit code 1. Kiểm tra lại 6 biến môi trường sau đó — không cái nào bị thay đổi. Nó gom đủ ba câu hỏi rồi mới ghi, nên đứt giữa chừng không để lại cấu hình dở.

Cài Git thất bại có phải làm lại từ đầu không? Không. Máy mình chưa cài Git mà claude vẫn chạy ngon, lệnh 4 vẫn thông. Git không phải runtime dependency của Claude Code — script cài nó là để bạn dùng cho công việc sau này thôi.

Tổng hợp các lỗi mình gặp và cách xử lý

Để tiện tra cứu, mình gom lại thành bảng:

Biểu hiện Nguyên nhân thực sự Cách xử lý
claude: command not found sau khi cấu hình Đang kiểm tra trong cửa sổ terminal cũ Đóng hẳn cửa sổ, mở lại. Hoặc source ~/.crazyrouter-claude-code.env
Trả về 404 ANTHROPIC_BASE_URL bị thêm /v1 thủ công Chỉ để domain gốc, client tự ghép path
Tool khác trả 404 nhưng Claude Code vẫn chạy OPENAI_BASE_URL thiếu /v1 Thêm /v1 vào riêng biến này
Trả về 401 Sai auth scheme, hoặc key dính khoảng trắng vô hình Đổi scheme theo docs của provider; paste lại key
Báo "không có token" dù key hợp lệ Đang ở subprocess, biến môi trường rỗng Đọc explicit từ user scope
Test connection fail dù field đúng Có thể domain chưa nằm trong allowed egress hosts Kiểm tra mục Egress trong app desktop
Script dừng với exit code 1 Bình thường nếu chạy non-interactive Không cần lo, cấu hình cũ không bị sửa

Nhìn lại bảng này mình nhận ra một điều: phần lớn thời gian mình mất không phải vì lỗi khó, mà vì thông báo lỗi trỏ sai hướng. Lỗi 404 không nói gì về /v1. Thông báo "không có token" không phân biệt được giữa "không có key" và "key không đọc được ở đây". Nên có một quy trình kiểm tra cố định vẫn nhanh hơn là đi theo nội dung thông báo lỗi.

Kết luận

Cấu hình thì chỉ có 6 biến môi trường, nhưng nếu dừng việc kiểm tra ở claude --version thì rất dễ rơi vào trạng thái "tưởng là xong".

Tóm lại ba điều cần nhớ:

  1. ANTHROPIC_BASE_URL không /v1, OPENAI_BASE_URL có /v1
  2. Cấu hình xong phải mở cửa sổ terminal mới
  3. Địa chỉ endpoint phải sạch, không query parameter

Và kiểm tra thì đi tới lệnh thứ 4. Khi request max_tokens=16 trả về 200, bạn đã xác nhận được đồng thời ba thứ: endpoint tới được, key hợp lệ, model có phản hồi.

Cuối cùng một lưu ý về bảo mật: đừng commit token vào git, đừng để lọt vào ảnh chụp màn hình, đổi máy thì nhớ thu hồi key cũ.

Hy vọng bài này giúp được ai đó đỡ mất thời gian như mình 🙂 Ai có gặp case nào khác thì comment chia sẻ với mình nhé.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí