Harness Academy
Tài liệu gốc 15 8 phútTiếng Việt + English
docs/adr/0005-multi-llm-provider-support.mdXem bản gốc
Ngôn ngữ bài đọc
Bản dịch tiếng Việt

Bản dịch đầy đủ theo đúng cấu trúc tài liệu; code, liên kết và định danh kỹ thuật được giữ nguyên.

0005. Hỗ trợ nhiều nhà cung cấp LLM - phụ trợ suy luận có thể lựa chọn (Gemini / OpenRouter / Modal)

Trạng thái: Đã chấp nhận Ngày: 2026-06-26

Bối cảnh

Cột mốc 1 đã kết nối vòng lặp agent với một chương trình phụ trợ duy nhất — Gemini thông qua google-gla Đường dẫn khóa API - nhưng cố tình xây dựng agent/factory.py dưới dạng đường nối trao đổi nhà cung cấp (ADR-0002 §1: "Hoán đổi M2 trong OpenRouter/Modal sau cùng một nhà máy"). Đang chạy decode do đó yêu cầu phải trả tiền Chìa khóa Song Tử. Để cho phép mọi người chạy nó miễn phí, chúng tôi thêm hai phần phụ trợ suy luận nữa đằng sau cùng một đường nối:

  • OpenRouter — một cổng tương thích với OpenAI với :free tùy chọn mô hình.
  • Điểm cuối tự động theo phương thức — Tương thích với OpenAI /v1 điểm cuối phục vụ các mô hình nguồn mở trên Modal's $30 tín dụng miễn phí. Việc lựa chọn mô hình, sự cân bằng GPU/phân phối và thiết lập điểm cuối được ghi lại trong danh mục đồng hành 02_modal_endpoints.md (ảnh chụp nhanh ngày 26-06-2026) — bản ghi ADR này quyết định kết nối và tham chiếu tệp đó cho mô hình nào và cách tạo điểm cuối.

Đường nối là kiến ​​trúc duy nhất ADR-0002 còn bỏ ngỏ ở đây; vòng lặp, công cụ, quyền, bộ nhớ và TUI không bị ảnh hưởng. ADR này ghi lại các lựa chọn liên quan dưới dạng quyết định tính năng một (không phải một quyết định cho mỗi nhiệm vụ): bộ chọn rõ ràng, cấu hình theo nhà cung cấp, ba cấu trúc mô hình, tùy chọn của Modal xác thực token proxy, mặc định được gửi + cảnh báo khi gọi công cụ và các mục tiêu không phải. Bốn nhiệm vụ sự cố sống ở tasks/ (đặc điểm multi-provider-gateway, nhiệm vụ 037-040). Tất cả hệ thống dây điện đã được xác minh dựa trên ** pydantic-ai 1.107 / openai 2.43 ** đã cài đặt ** và OpenRouter trực tiếp danh sách model trong quá trình chải chuốt nên không bị kiện tụng lại ở hạ lưu.

Quyết định

  1. Rõ ràng LLM_PROVIDER bộ chọn - không tự động phát hiện. Một llm_provider: Trường Literal["gemini","openrouter","modal"] = "gemini" trên phần cài đặt chọn phần phụ trợ; pydantic xác nhận giá trị khi tải. Mặc định gemini tương thích ngược — hiện có .env cái đó chỉ đặt GEMINI_API_KEY tiếp tục làm việc mà không có dòng mới. “Chạy miễn phí” được phục vụ bởi ghi lại việc chọn tham gia, không phải bằng cách lật ngược mặc định. Chúng tôi từ chối việc tự động phát hiện nhà cung cấp từ đó các khóa tình cờ xuất hiện: nó ẩn ý, đáng ngạc nhiên và mơ hồ khi một số khóa được đặt.

  2. Các trường cấu hình theo nhà cung cấp, trên một trình đọc cài đặt. OpenRouter: openrouter_api_key (SecretStr), openrouter_model. Modal điểm cuối (tên theo 02_modal_endpoints.md §6): modal_endpoint_url (không có mặc định - đầu ra triển khai cho mỗi người dùng; được sử dụng làm base_url = f"{...}/v1"), modal_endpoint_model, modal_proxy_token_id (SecretStr, cái Modal-Key: wk-... tiêu đề) và modal_proxy_token_secret (SecretStr, cái Modal-Secret: ws-... tiêu đề). Các điểm cuối phương thức này các vars khác biệt với các vars hiện có MODAL_TOKEN_ID/MODAL_TOKEN_SECRET tài khoản token (modal token set, đối với CLI/sandbox — 02_modal_endpoints.md §5.1): việc nạp quá nhiều chúng sẽ kết hợp hai phạm vi thông tin xác thực không liên quan. Tất cả các vars mới được nhân đôi trong .env.example; không có gì đọc os.environ trong các trang web cuộc gọi.

  3. Một _build_model() Nhà cung cấp đường may tại nhà máy. build_agent() đại biểu xây dựng mô hình đến một tư nhân nhỏ _build_model() đó là nhánh trên settings.llm_provider; phần còn lại của build_agent() — deps_type, output_type=[str, DeferredToolRequests], hướng dẫn, register_tools, và ba @agent.instructions hooks - không phụ thuộc vào nhà cung cấp và không thay đổi. A giá trị vượt qua ba nhánh tăng ValueError (phòng thủ; Literal chặn nó ngược dòng).

  4. Ba cấu trúc mô hình (được xác minh dựa trên SDK đã cài đặt):

    • song tử (không thay đổi): GoogleModel(settings.gemini_model, provider=GoogleProvider(api_key=...)) — google-gla, không bao giờ vertexai= (filterwarnings=["error"]).
    • openrouter: OpenAIChatModel(settings.openrouter_model, nhà cung cấp=OpenRouterProvider(api_key=...)) (model.system == "openrouter").
    • phương thức: OpenAIChatModel(settings.modal_endpoint_model, nhà cung cấp=OpenAIProvider(openai_client=AsyncOpenAI(...))) — một ứng dụng khách tùy chỉnh (xem §5).
  5. Xác thực token proxy theo phương thức là TÙY CHỌN — hỗ trợ --unauthenticated điểm cuối. Điểm cuối tự động theo phương thức yêu cầu xác thực theo mặc định, thông qua cặp tiêu đề Modal-Key (Ư-...) + Modal-Secret (v-...) từ modal workspace proxy-tokens create — không phải OpenAI Authorization: Bearer kế hoạch (02_modal_endpoints.md §5.3, §6.3). Nhưng điểm cuối của nhà phát triển được tạo bằng --unauthenticated không cần tiêu đề. Vì vậy, nhánh phương thức luôn xây dựng một tùy chỉnh AsyncOpenAI khách hàng (đối với tùy chỉnh base_url) và:

    • cả hai token proxy được đặt → AsyncOpenAI(base_url=f"{modal_endpoint_url}/v1", api_key=<modal_proxy_token_secret>, default_headers={"Modal-Key": <modal_proxy_token_id>, "Modal-Bí mật": <modal_proxy_token_secret>}).
    • không đặt → AsyncOpenAI(base_url=f"{modal_endpoint_url}/v1", api_key="EMPTY") không có phương thức tiêu đề (SDK openai yêu cầu không trống api_key, vì vậy một trình giữ chỗ được thông qua). Máy khách một kênh sẽ không xác thực điểm cuối được xác thực. Bất biến cả hai hoặc không được thực thi ở bộ bảo vệ cli (§6): chính xác một bộ token là cấu hình sai thân thiện.
  6. Bảo vệ khởi động tổng quát. Nhiệm vụ-004 GEMINI_API_KEY-chỉ canh gác cli.py khái quát hóa thành xác thực cấu hình yêu cầu của nhà cung cấp ** đã chọn **, in MỘT dòng stderr thân thiện và thoát khác không (không có dấu vết), phản ánh phong cách và vị trí của người bảo vệ hiện có (trước agent/chế độ xác thực và trước khi agent được xây dựng): gemini → GEMINI_API_KEY; bộ định tuyến mở → OPENROUTER_API_KEY; phương thức → MODAL_ENDPOINT_URL + MODAL_ENDPOINT_MODEL chỉ (token proxy không bắt buộc), cộng với kiểm tra cả hai hoặc không gắn cờ chính xác một bộ token proxy.

  7. Mặc định đã vận chuyển, cảnh báo khi gọi công cụ và không phải mục tiêu. openrouter_model mặc định là một hiện tại mô hình miễn phí nổi tiếng hỗ trợ gọi công cụ + phát trực tuyến (đã xác minh qwen/qwen3-coder:free chống lại danh sách OpenRouter trực tiếp vào ngày 26-06-2026; tài liệu thay thế meta-llama/llama-3.3-70b-instruct:free). modal_endpoint_model mặc định là openai/gpt-oss-120b — Lựa chọn phù hợp nhất của 02_modal_endpoints.md (gọi công cụ OpenAI gốc = tiêu chí khai thác số 1, đơn B200); tài liệu thay thế Qwen/Qwen3.6-35B-A3B-FP8 (nhà phát triển giá rẻ, 1×H100) và zai-org/GLM-5.2-FP8 (tối đa). Vòng lặp agent cần một mô hình có khả năng gọi công cụ + phát trực tuyến; tài liệu có dòng chữ "nếu bạn trao đổi các mô hình, hãy chọn một mô hình có khả năng sử dụng công cụ" cho cả OpenRouter và Modal. Không có mục tiêu (điều đơn giản nhất hoạt động): không có tính năng tự động phát hiện nhà cung cấp và không có dự phòng của nhà cung cấp / FallbackModel - một nhà cung cấp bị định cấu hình sai sẽ thất bại nhanh chóng ở mức bảo vệ hoặc yêu cầu đầu tiên, nó không âm thầm quay trở lại.

Sơ đồ

Nhà cung cấp đường may sau quyết định này. Bộ chọn và bộ bảo vệ đọc các cài đặt đơn lẻ; _build_model() trả về một trong ba mô hình; máy khách phương thức thích ứng với xác thực so với --unauthenticated; vòng lặp là nhà cung cấp bất khả tri.

flowchart TB
    user(["User: LLM_PROVIDER + secrets in .env"])

    subgraph cfg["config/settings.py (singleton)"]
        sel["llm_provider: Literal[gemini|openrouter|modal]<br/>+ per-provider fields & secrets"]
    end

    guard["cli.py startup guard<br/>(required config for the selected provider;<br/>modal: URL+MODEL only, proxy tokens both-or-neither<br/>→ 1 friendly line + non-zero exit, no traceback)"]

    subgraph factory["agent/factory.py"]
        build["build_agent()<br/>(deps_type · output_type=[str, DeferredToolRequests]<br/>· instructions hooks · register_tools — provider-agnostic)"]
        seam{"_build_model()<br/>Provider Seam<br/>branch on llm_provider"}
    end

    gem["gemini:<br/>GoogleModel + GoogleProvider<br/>(google-gla, system=google)"]
    orouter["openrouter:<br/>OpenAIChatModel + OpenRouterProvider<br/>(system=openrouter, :free model)"]
    modal["modal:<br/>OpenAIChatModel + OpenAIProvider<br/>custom AsyncOpenAI(base_url=.../v1)<br/>auth → headers Modal-Key + Modal-Secret<br/>--unauthenticated → api_key=EMPTY, no headers"]

    loop["Agent loop (Pydantic AI ReAct)<br/>unchanged — ADR-0002"]

    user --> cfg
    sel --> guard
    guard -->|config OK| build
    build --> seam
    seam -->|gemini default| gem
    seam -->|openrouter| orouter
    seam -->|modal| modal
    gem --> loop
    orouter --> loop
    modal --> loop

    classDef gemini fill:#2e7d32,stroke:#1b5e20,color:#ffffff;
    classDef router fill:#1565c0,stroke:#0d47a1,color:#ffffff;
    classDef modalc fill:#e65100,stroke:#bf360c,color:#ffffff;
    classDef seamc fill:#6a1b9a,stroke:#4a148c,color:#ffffff;
    classDef plain fill:#eceff1,stroke:#607d8b,color:#102027;
    class gem gemini;
    class orouter router;
    class modal modalc;
    class seam,build seamc;
    class user,cfg,sel,guard,loop plain;

hậu quả

  • Miễn phí decode trở nên khả thi mà không cần thay đổi mã cho mỗi người dùng — hãy chọn tham gia thông qua LLM_PROVIDER + cấu hình của nhà cung cấp đó; mặc định gemini giữ mọi thứ hiện có .env làm việc không bị ảnh hưởng.
  • ** Đường nối trao đổi nhà cung cấp ADR-0002 §1 được trì hoãn hiện đã được thực hiện ** dưới dạng _build_model(); phụ trợ trong tương lai thêm một nhánh + trường cài đặt của chúng + một trường hợp bảo vệ, không có gì khác.
  • Các chương trình phụ trợ tương thích với OpenAI sử dụng một lớp mô hình (OpenAIChatModel); chỉ Modal mới cần một tùy chỉnh khách hàng, bị cô lập với chi nhánh của nó vì phong tục base_url và tiêu đề kép tùy chọn xác thực token proxy.
  • --unauthenticated Điểm cuối phương thức hoạt động cho nhà phát triển cục bộ (không có hệ thống ống nước tiêu đề); xác thực điểm cuối nhận được Modal-Key/Modal-Secret tiêu đề. Việc bảo vệ cả hai hoặc không ngăn cản trạng thái được cấu hình một nửa sẽ là 401 ở yêu cầu đầu tiên.
  • Id mô hình OpenRouter miễn phí bị xáo trộn — mặc định được vận chuyển được xác minh khi chuẩn bị và được xác minh lại lúc thực hiện; id cũ xuất hiện dưới dạng lỗi yêu cầu đầu tiên, không phải lỗi thầm lặng (không có dự phòng). Mặc định phương thức openai/gpt-oss-120b được ghim vào vị trí phù hợp nhất của danh mục; 02_modal_endpoints.md là nơi kinh điển để chọn một người thay thế.
  • Hai phạm vi thông tin xác thực phương thức hiện cùng tồn tại (token tài khoản và token proxy điểm cuối); .env.example, README và 02_modal_endpoints.md §6 phải tách biệt chúng rõ ràng để tránh nhầm lẫn cho người dùng.
  • Không có dự phòng là một lựa chọn đơn giản có chủ ý — một nhà cung cấp bị định cấu hình sai sẽ nhanh chóng thất bại (bảo vệ hoặc yêu cầu đầu tiên) thay vì che giấu cấu hình sai bằng cách chuyển đổi phần phụ trợ; xem lại nếu có nhiều mô hình định tuyến là muốn sau này.
  • CI vẫn ngoại tuyến: việc xây dựng mô hình gặp sự cố không có cuộc gọi mạng, do đó, nhà máy đã kiểm tra tham số hóa khẳng định mô hình loại + hình dạng máy khách (base_url, tiêu đề/giữ chỗ api_key) cho mỗi nhà cung cấp với mocker.patchcài đặt ed; Xác thực nhà cung cấp trực tiếp là mối quan tâm thủ công của e2e.
Mục lục nguồn