Pi từ mã nguồnpi v1.4.0

Chương 5Phần 2: Nền móng

pi-ai: provider và mọi cách kết nối

Mọi con đường tới một mô hình: provider, API trên dây, credential, OAuth, đám mây và mô hình cục bộ.

  • 55 phút đọc
  • 41 đoạn code
  • 23 mục
Trong chương này23 mục
  1. Vai trò
  2. Mô hình tư duy: ba trục provider, API, credential
  3. Một provider được định nghĩa thế nào
  4. Bản đồ provider ở v1.4.0
  5. Các API trên dây và lazy loading
  6. Credential trong pi-ai: môi trường, kho và thứ tự
  7. Credential trong coding agent: auth.json, --api-key, pi auth
  8. Đăng nhập OAuth và gói thuê bao
  9. Nền tảng đám mây: Bedrock, Vertex, Azure, workload identity
  10. Cloudflare: REST, AI Gateway và AI binding trong Worker
  11. Gateway và aggregator
  12. Mô hình cục bộ và tự host
  13. models.json và cách ghép với catalog dựng sẵn
  14. Provider do extension đăng ký
  15. Mạng: proxy, base URL và fetch tuỳ biến
  16. Phía Pi Durable: host tự chọn provider
  17. Mô hình ảnh và classifier qua cùng provider
  18. Trace: pi --model openrouter/... chỉ với OPENROUTER_API_KEY
  19. Trace ngắn: một mô hình Ollama khai báo trong models.json
  20. Quyết định thiết kế
  21. Cạm bẫy và trường hợp biên
  22. Đọc code ở đâu
  23. Tóm tắt

Vai trò#

Chương pi-ai giải thích lõi: mô hình dữ liệu, giao thức luồng sự kiện, runtime Models và Provider, hai adapter Anthropic và OpenAI Responses. Chương này đi theo chiều rộng. Câu hỏi của nó là: ở v1.4.0, Pi chạm được tới một mô hình bằng những con đường nào, và mỗi con đường được hiện thực ở đâu trong code.

Mỗi request tới một mô hình phải trả lời ba câu hỏi. Ai phục vụ nó: provider (nhà cung cấp), tức một họ endpoint cùng danh mục mô hình. Nói thế nào: API, tức giao thức trên dây mà một module adapter trong src/api/ hiện thực. Bạn là ai: credential, có thể là một khoá API, một token OAuth của gói thuê bao, credential của nền tảng đám mây, hoặc không gì cả với một máy chủ cục bộ. Ba trục này độc lập với nhau, và phần lớn sự đa dạng của Pi đến từ việc ghép chúng theo nhiều cách.

Ở v1.4.0, builtinProviders() dựng 42 provider có sẵn (providers/all.ts), cộng thêm faux cho test. Coding agent đăng ký thêm llama.cpp qua một extension dựng sẵn, và người dùng thêm bao nhiêu provider tuỳ ý qua models.json hoặc extension. Về phía API có mười API chat, một API sinh ảnh và bốn API classifier (types.ts:64).

Trách nhiệm chia theo gói. pi-ai sở hữu provider, adapter của API và việc phân giải xác thực lúc gửi request. Coding agent thêm các nguồn cấu hình của người dùng (auth.json, models.json, cờ --api-key), lớp phủ catalog từ pi.dev và provider do extension đăng ký, tất cả gói trong ModelRuntime (xem thêm mục dịch vụ của chương AgentSession). Pi Durable không tự tạo provider nào: host đưa cho nó một Models đã cấu hình sẵn.

Mô hình tư duy: ba trục provider, API, credential#

Một mô hình trong catalog là dữ liệu thuần. Ba trường của nó nối ba trục lại với nhau: provider chọn provider, api chọn adapter, baseUrl là nơi adapter gửi tới. Credential không nằm trong mô hình; nó được provider phân giải ở từng request rồi đổ vào option.

Ví dụ minh hoạ: một mô hình và ba trục
  model = { id: "moonshotai/kimi-k2.6", provider: "openrouter",
            api: "openai-completions", baseUrl: "https://openrouter.ai/api/v1", ... }

  Models.streamSimple(model, ...)
    │ 1. tìm Provider theo model.provider ───────────► openrouterProvider()
    │ 2. Provider.auth phân giải credential ─────────► { apiKey } từ OPENROUTER_API_KEY
    │ 3. Provider chọn adapter theo model.api ───────► api/openai-completions.ts (nạp lười)
    ▼
  adapter: SDK openai, baseURL = model.baseUrl ──────► POST /chat/completions
Provider lo "ai" và "bạn là ai", API lo "nói thế nào". Cùng một API phục vụ hàng chục provider; cùng một provider có thể dùng nhiều API.

Credential đến từ nhiều nguồn, và mỗi nguồn ứng với một cơ chế trong code. Chương này đi qua từng cơ chế theo thứ tự trong bảng dưới.

Cách kết nốiVí dụCơ chế chính
Khoá APIOPENROUTER_API_KEY, auth.json, --api-keyenvApiKeyAuth(), CredentialStore, AuthStorage
OAuth của gói thuê bao hay tài khoảnClaude Pro/Max, Sign in with ChatGPT, CopilotOAuthAuth: trình duyệt cộng callback cục bộ, device code, dán mã
Credential của nền tảng đám mâyAWS profile, gcloud ADC, workload identity của AnthropicApiKeyAuth.resolve() chỉ báo "đã cấu hình", SDK tự lấy credential; Azure dùng khoá cộng endpoint theo resource
Gateway, aggregatorOpenRouter, Vercel, Cloudflare, OpenCode, RadiusProvider có catalog của nhiều hãng, header riêng, đôi khi giao thức riêng
Binding trong Workerenv.AI của CloudflarecreateAiBindingFetch() thay fetch
Máy chủ cục bộllama.cpp router, Ollama, LM Studio, vLLMExtension llama.cpp, hoặc models.json cộng cờ compat
ExtensionGitLab Duo, giao thức tự viếtpi.registerProvider()
Host cung cấpPi DurablecreateModels() cộng setProvider() do host gọi

Trong coding agent, một provider mà người dùng thấy là kết quả của nhiều lớp chồng lên nhau. Lớp nào vắng thì lớp dưới được dùng nguyên vẹn:

Factory của pi-ai
42 provider dựng sẵn: catalog sinh tự động, xác thực, adapter nạp lười.
Lớp phủ catalog
withRemoteCatalog() lấy catalog mới hơn từ pi.dev; riêng OpenRouter dùng withPersonalizedCatalog() để lọc theo khoá.
Native provider của extension
Một Provider hoàn chỉnh do pi.registerProvider(provider) đăng ký, thay cho provider dựng sẵn cùng id. llama.cpp đến từ đây.
models.json
Thêm hoặc thay mô hình, đổi baseUrl, header, compat, khoá; tạo provider hoàn toàn mới.
ProviderConfig của extension
Dạng đăng ký cũ theo tên: mô hình, OAuth, streamSimple tự viết.
Virtual model
Mục chọn được, định tuyến từng request tới mô hình thật (chương Pi 1.0).

Từ vựng dùng trong phần còn lại:

Credential
Bản ghi lưu cho một provider: { type: "api_key", key?, env? } hoặc { type: "oauth", access, refresh, expires, ... }. Mỗi id provider có tối đa một credential.
ModelAuth
Thứ thật sự đi vào một request: apiKey, headers, baseUrl. Chú thích của kiểu này đặt ra một ranh giới rõ: cái gì không biểu diễn được bằng ba trường đó thì là cấu hình của provider, không phải xác thực.
Credential môi trường (ambient)
Credential đọc từ biến môi trường hoặc file của máy (AWS profile, gcloud ADC), không bao giờ được chép vào kho credential của Pi.
Provider env
Một Record<string, string> cấu hình theo từng provider, ví dụ CLOUDFLARE_ACCOUNT_ID hay AWS_PROFILE. Nó đi kèm credential, và được ưu tiên hơn process.env.
ModelAuth và ApiKeyCredential: ranh giới giữa xác thực và cấu hình
pi/packages/ai/src/auth/types.tsdòng 3–21TypeScript
/** * Request auth for a single model request. If a value cannot be expressed as * `apiKey`, `headers`, or `baseUrl`, it is provider config, not auth. */export interface ModelAuth {	apiKey?: string;	headers?: ProviderHeaders;	baseUrl?: string;}/** * Stored api-key credential. `env` holds provider-scoped environment/config * values such as Cloudflare account/gateway ids. */export interface ApiKeyCredential {	type: "api_key";	key?: string;	env?: ProviderEnv;}

Một provider được định nghĩa thế nào#

Phần lớn provider là một file mười lăm dòng. Groq là ví dụ điển hình: một id, một tên hiển thị, một baseUrl, một cách xác thực chuẩn, một catalog và một lazy wrapper của API.

groq.ts: provider tối giản
pi/packages/ai/src/providers/groq.tsdòng 1–15TypeScript
import { openAICompletionsApi } from "../api/openai-completions.lazy.ts";import { envApiKeyAuth } from "../auth/helpers.ts";import { createProvider, type Provider } from "../models.ts";import { GROQ_MODELS } from "./groq.models.ts";export function groqProvider(): Provider<"openai-completions"> {	return createProvider({		id: "groq",		name: "Groq",		baseUrl: "https://api.groq.com/openai/v1",		auth: { apiKey: envApiKeyAuth("Groq API key", ["GROQ_API_KEY"]) },		models: Object.values(GROQ_MODELS),		api: openAICompletionsApi(),	});}

Bốn thứ trong factory có bốn nguồn khác nhau:

  • models đến từ groq.models.ts, một wrapper sinh tự động import ./data/groq.json rồi làm phẳng theo loại mô hình. File JSON gom mô hình theo API, khoá dạng chat:<id>, để TypeScript suy được API của từng id; flattenModelCatalog() bỏ lớp nhóm và lọc theo type (model-catalog.ts:53). Thư mục data/ bị gitignore và được sinh lại khi build (chương pi-ai, catalog).
  • auth là một ProviderAuth: apiKey, oauth, hoặc cả hai. envApiKeyAuth() là cách chuẩn; provider có nguồn credential đặc biệt tự viết resolve().
  • api là một ProviderStreams, ở đây là lazy wrapper openAICompletionsApi(), nên SDK openai chỉ được nạp ở request đầu tiên.
  • baseUrl cấp provider gần như chỉ là siêu dữ liệu. Request đi tới model.baseUrl của từng mô hình; Provider.baseUrl chỉ được chuyển vào RefreshModelsContext.baseUrl khi làm mới catalog (models.ts:555), nhờ đó một baseUrl ghi đè trong models.json cũng áp cho request lấy danh sách mô hình của OpenRouter.

Provider phức tạp hơn vẫn chỉ ghép thêm các mảnh. OpenCode Zen nói bốn API, mỗi API được bọc để chèn header định tuyến theo phiên, và có thêm một classifier:

opencode.ts: bảng API theo model.api, wrapper header, classifier
pi/packages/ai/src/providers/opencode.tsdòng 13–28TypeScript
export function opencodeProvider(): Provider<OpenCodeApi> {	return createProvider<OpenCodeApi>({		id: "opencode",		name: "OpenCode Zen",		auth: { apiKey: envApiKeyAuth("OpenCode API key", ["OPENCODE_API_KEY"]) },		models: [...Object.values(OPENCODE_MODELS), ...Object.values(OPENCODE_CLASSIFIER_MODELS)],		api: {			"anthropic-messages": withOpenCodeSessionHeader(anthropicMessagesApi()),			"google-generative-ai": withOpenCodeSessionHeader(googleGenerativeAIApi()),			"openai-completions": withOpenCodeSessionHeader(openAICompletionsApi()),			"openai-responses": withOpenCodeSessionHeader(openAIResponsesApi()),		},		// OpenCode Zen serves TypeSafe's System One protocol at /zen/v1/systemone.		classifiers: { "typesafe-system-one": typesafeSystemOneApi() },	});}
withOpenCodeSessionHeader() chép sessionId vào header x-opencode-session nếu người gọi chưa đặt (opencode-headers.ts).

Mọi factory dựng sẵn và mọi provider tự định nghĩa trong models.json đều đi qua cùng một hàm, createProvider(). Đây là toàn bộ bề mặt mà một provider có thể khai báo:

CreateProviderOptions: mọi thứ một provider có thể khai báo
pi/packages/ai/src/models.tsdòng 1032–1066TypeScript
export interface CreateProviderOptions<TApi extends Api = Api> {	id: string;	/** Display name. Default: `id`. */	name?: string;	baseUrl?: string;	headers?: ProviderHeaders;	/** Required — every provider has auth semantics, even ambient/keyless ones. */	auth: ProviderAuth;	/**	 * Static baseline models of every type (empty for purely dynamic providers).	 * Models without `type` are chat models.	 */	models: readonly ProviderModel<TApi>[];	/**	 * Fetch a dynamic model overlay of every type. createProvider restores and	 * publishes it transactionally and drops models of unknown types.	 */	fetchModels?: (context: RefreshModelsContext) => Promise<readonly ProviderModel<TApi>[]>;	/** Credential-specific chat model availability. See `Provider.filterModels`. */	filterModels?: (models: readonly Model<TApi>[], credential: Credential | undefined) => readonly Model<TApi>[];	/** Credential-specific availability across every model type. See `Provider.filterAllModels`. */	filterAllModels?: (		models: readonly ProviderModel<TApi>[],		credential: Credential | undefined,	) => readonly ProviderModel<TApi>[];	/**	 * Chat implementation: a single one for all chat models, or a map keyed by	 * `model.api` for mixed-API providers. Optional when `images` or	 * `classifiers` is given.	 */	api?: ProviderStreams | Partial<Record<TApi, ProviderStreams>>;	/** Image-generation implementations keyed by `model.api`. */	images?: Partial<Record<ImageApi, ProviderImages>>;	/** Classifier implementations keyed by `model.api`. */	classifiers?: Partial<Record<ClassifierApi, ProviderClassifier>>;

Đối chiếu với 42 factory ở v1.4.0, có sáu khuôn mẫu:

Khuôn mẫuProvider
Một API, envApiKeyAuth()20 provider tương thích OpenAI Completions (groq, deepseek, together, ...), cùng google, mistral, minimax, minimax-cn, vercel-ai-gateway
Một API, ApiKeyAuth tự viếtanthropic (Bearer, federation), amazon-bedrock, google-vertex, cloudflare-workers-ai; cloudflare-ai-gateway cũng tự viết nhưng có bảng API
Thêm OAuth qua lazyOAuth()anthropic, openai, openai-codex (chỉ OAuth), github-copilot, kimi-coding, meta, xai, openrouter, radius
Bảng API theo model.apifireworks, github-copilot, openrouter, opencode, opencode-go, azure, cloudflare-ai-gateway
Bọc ProviderStreams để biến đổi request trước khi gửiazureStreams(), cloudflareStreams(), withOpenCodeSessionHeader()
Viết tay interface Providerradius (catalog động thay hẳn bản dựng sẵn); llama.cpp ở coding agent

typesafe là trường hợp riêng: không có api chat, chỉ có classifiers, điều createProvider() cho phép vì nó chỉ đòi ít nhất một trong ba bảng api, images, classifiers (models.ts:1087).

Lược đồ của mô hình và cờ compat#

Các trường của mô hình (mức suy nghĩ, giá, giới hạn input) được khai báo bằng TypeBox trong model-schema.ts, đã đi qua ở phần Data model. compat-schema.ts khai báo cờ compat cho từng API. Mỗi API có lược đồ riêng với mặc định và mô tả riêng, nhưng models.json cho phép đặt compat ở cấp provider, nơi chưa biết mô hình nói API nào. Lời giải là một lược đồ hợp (superset):

Lược đồ compat theo API và lược đồ hợp cho models.json
pi/packages/ai/src/providers/compat-schema.tsdòng 619–640TypeScript
/** Compatibility schema for each API whose models accept compat. Mirrors the compat type of Model. */export const CompatSchemasByApi = {	"openai-completions": OpenAICompletionsCompatSchema,	"openai-responses": OpenAIResponsesCompatSchema,	"azure-openai-responses": OpenAIResponsesCompatSchema,	"openai-codex-responses": OpenAIResponsesCompatSchema,	"anthropic-messages": AnthropicMessagesCompatSchema,	"bedrock-converse-stream": BedrockCompatSchema,	"mistral-conversations": MistralConversationsCompatSchema,	"typesafe-system-one": SystemOneCompatSchema,	"cloudflare-workers-ai-system-one": SystemOneCompatSchema,} as const;const ProviderCompatProperties = mergeCompatProperties(	[...new Set(Object.values(CompatSchemasByApi))].map((schema) => schema.properties),	ProviderCompatPropertyOverrides,);export const ProviderCompatSchema = Type.Object(ProviderCompatProperties, {	description: "Provider and model compatibility overrides.",	additionalProperties: true,});

Thuộc tính trùng tên giữa các API (ví dụ supportsDeveloperRole có ở cả Completions lẫn Responses với mặc định khác nhau) phải có một bản ghi đè chung trong ProviderCompatPropertyOverrides; thiếu thì mergeCompatProperties() ném lỗi ngay lúc nạp module (compat-schema.ts:600). Nhờ vậy lược đồ hợp không thể lặng lẽ lấy mặc định của một API này áp cho API khác. additionalProperties: true giữ cho models.json cũ không bị từ chối khi một cờ bị gỡ.

Bản đồ provider ở v1.4.0#

Các bảng dưới đây dựng từ code ở tag v1.4.0, không từ trí nhớ hay từ README: cột API lấy từ lệnh import lazy wrapper trong từng factory, và với provider nhiều API thì từ luật chọn API trong scripts/generate-models.ts; cột xác thực lấy từ auth của factory. Tên id là liên kết tới file factory. Danh sách mô hình cụ thể của từng provider không có trong bản clone, vì src/providers/data/ bị gitignore; tra cứu chúng ở pi.dev/models. Cách chia nhóm là của tài liệu này, code không có khái niệm nhóm.

Hãng làm mô hình, gọi API trực tiếp (11)#

ProviderAPIXác thựcĐáng chú ý
anthropicanthropic-messagesANTHROPIC_AUTH_TOKEN (gửi dạng Bearer), ANTHROPIC_OAUTH_TOKEN, ANTHROPIC_API_KEY, rồi workload identity federation; OAuth Claude Pro/MaxToken OAuth chuyển adapter sang danh tính Claude Code (chương pi-ai)
openaiopenai-responses; classifier openai-decisionsOPENAI_API_KEY; OAuth Sign in with ChatGPTKhi đăng nhập OAuth, classifier bị ẩn vì Decisions API từ chối token ChatGPT (openai.ts:27)
googlegoogle-generative-aiGEMINI_API_KEYAdapter từ chối options.fetch tuỳ biến (google-generative-ai.ts:87)
mistralmistral-conversationsMISTRAL_API_KEYKhông dùng SDK: adapter tự POST tới v1/chat/completions bằng fetch (mistral-conversations.ts:298); id lời gọi tool 9 ký tự
xaiopenai-responsesXAI_API_KEY; OAuth device code (SuperGrok, X Premium)README ghi Completions; code dùng Responses
metaopenai-responsesMETA_API_KEY; OAuth device code (gói Muse)Identity token của OAuth không dùng để suy luận được; Pi đổi nó lấy một khoá API sống khoảng một ngày
deepseekopenai-completionsDEEPSEEK_API_KEYthinkingFormat: "deepseek", reasoning_content trên mọi assistant message, max_tokens (detectCompat)
moonshotaiopenai-completionsMOONSHOT_API_KEYapi.moonshot.ai; không gửi reasoning_effort, dùng max_tokens
minimaxanthropic-messagesMINIMAX_API_KEYEndpoint tương thích Anthropic api.minimax.io/anthropic
xiaomiopenai-completionsXIAOMI_API_KEYEndpoint tính tiền theo API; MiMo tự cắt input khi tràn (chương pi-ai)
ant-lingopenai-completionsANT_LING_API_KEYthinkingFormat: "ant-ling"

Nền tảng suy luận cho mô hình mở (7)#

ProviderAPIXác thựcĐáng chú ý
groqopenai-completionsGROQ_API_KEYapi.groq.com/openai/v1
cerebrasopenai-completionsCEREBRAS_API_KEYKhông gửi store, không dùng vai developer
togetheropenai-completionsTOGETHER_API_KEYthinkingFormat: "together", max_tokens, không có lưu cache dài
fireworksanthropic-messages cho đa số; openai-completions cho GLM và Kimi K3FIREWORKS_API_KEYCache theo session affinity; không nhận cache_control trên tool (generate-models.ts:1721)
basetenopenai-completionsBASETEN_API_KEYMức suy nghĩ qua chat_template_args; header session affinity (CHANGELOG 0.86.0)
huggingfaceopenai-completionsHF_TOKENRouter router.huggingface.co/v1
nvidiaopenai-completionsNVIDIA_API_KEYNIM công khai; header ghi nhận X-BILLING-INVOKE-ORIGIN (identity.ts:120)

Nền tảng đám mây (4)#

ProviderAPIXác thựcĐáng chú ý
amazon-bedrockbedrock-converse-streamBearer token, AWS profile, khoá IAM, ECS task role, web identity; không có OAuthSDK AWS tự chạy chuỗi credential; vùng lấy từ ARN của mô hình (mục đám mây)
google-vertexgoogle-vertexGOOGLE_CLOUD_API_KEY, hoặc ADC cộng project và locationGemini qua Vertex AI
azureazure-openai-responses cho mô hình OpenAI; openai-completions cho mô hình FoundryAZURE_OPENAI_API_KEY cộng AZURE_OPENAI_BASE_URL hoặc AZURE_OPENAI_RESOURCE_NAMEMô hình có baseUrl: "", được điền theo từng request; tên deployment ánh xạ được
cloudflare-workers-aiopenai-completions; classifier cloudflare-workers-ai-system-oneCLOUDFLARE_API_KEY cộng CLOUDFLARE_ACCOUNT_IDURL chứa placeholder {CLOUDFLARE_ACCOUNT_ID} (mục Cloudflare)

Gateway và aggregator (6)#

ProviderAPIXác thựcĐáng chú ý
openrouteranthropic-messages cho anthropic/*, openai-completions cho phần còn lại; ảnh openrouter-images; classifier typesafe-system-oneOPENROUTER_API_KEY; OAuth PKCE trả về một khoá vĩnh viễnCatalog lọc theo khoá qua /models/user; header ghi nhận; x-session-id
vercel-ai-gatewayanthropic-messages cho mọi mô hình chat; classifier typesafe-system-oneAI_GATEWAY_API_KEYMọi hãng đi qua endpoint tương thích Anthropic của gateway
cloudflare-ai-gatewayopenai-responses (OpenAI), anthropic-messages (Anthropic), openai-completions (workers-ai/*)CLOUDFLARE_API_KEY, CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_GATEWAY_IDKhoá đi trong cf-aig-authorization, header xác thực của SDK bị xoá
opencode (Zen)bốn API: anthropic-messages, google-generative-ai, openai-completions, openai-responses; classifier typesafe-system-oneOPENCODE_API_KEYx-opencode-session theo phiên, x-opencode-client ghi nhận
opencode-gobốn API như ZenOPENCODE_API_KEY (dùng chung)Endpoint opencode.ai/inference/go
radiuspi-messagesRADIUS_API_KEY; OAuth trình duyệt hoặc device codeGateway của Earendil; catalog lấy từ /v1/config thay hẳn bản dựng sẵn

Truy cập bằng gói thuê bao (2)#

Hai provider tồn tại chỉ để dùng một gói thuê bao. Các gói khác (Claude Pro/Max, ChatGPT, SuperGrok, Muse, Kimi Code) là phương thức OAuth gắn thêm vào provider của hãng ở các bảng trên.

ProviderAPIXác thựcĐáng chú ý
github-copilotanthropic-messages cho Claude, openai-responses cho GPT, Grok, OSWE, MAI, openai-completions cho phần còn lại (generate-models.ts:2338)COPILOT_GITHUB_TOKEN; OAuth device code, có GitHub EnterprisebaseUrl theo tài khoản lấy từ token; lọc mô hình theo danh sách tài khoản được dùng
openai-codexopenai-codex-responsesChỉ OAuth (ChatGPT Plus/Pro): trình duyệt hoặc device codeTên "OpenAI Codex (legacy)"; chatgpt.com/backend-api; SSE hoặc WebSocket; header chatgpt-account-id

Gói coding, token plan và biến thể theo vùng (11)#

Cùng một hãng thường có nhiều endpoint: một cho tính tiền theo API, một cho gói coding, một cho Trung Quốc đại lục, một cho từng vùng của token plan. Mỗi endpoint là một provider riêng, vì credential và catalog của chúng khác nhau. Tất cả nói openai-completions, trừ minimax-cn nói anthropic-messages.

ProviderEndpointXác thựcĐáng chú ý
kimi-codingapi.kimi.com/coding/v1KIMI_API_KEY; OAuth device code (Kimi Code)Token OAuth đi dưới dạng header Authorization, không phải apiKey
zaiapi.z.ai/api/coding/paas/v4ZAI_API_KEYCoding Plan quốc tế; thinkingFormat: "zai"; có thể tràn ngữ cảnh lặng lẽ
zai-coding-cnopen.bigmodel.cn/api/coding/paas/v4ZAI_CODING_CN_API_KEYCoding Plan Trung Quốc
qwen-token-plantoken-plan.ap-southeast-1.maas.aliyuncs.comQWEN_TOKEN_PLAN_API_KEYCatalog rộng, giữ để tương thích ngược
qwen-token-plan-individualcùng endpoint trênQWEN_TOKEN_PLAN_API_KEY (dùng chung)Chỉ mô hình của gói Individual (README)
qwen-token-plan-cntoken-plan.cn-beijing.maas.aliyuncs.comQWEN_TOKEN_PLAN_CN_API_KEY
xiaomi-token-plan-cntoken-plan-cn.xiaomimimo.com/v1XIAOMI_TOKEN_PLAN_CN_API_KEY
xiaomi-token-plan-amstoken-plan-ams.xiaomimimo.com/v1XIAOMI_TOKEN_PLAN_AMS_API_KEYAmsterdam
xiaomi-token-plan-sgptoken-plan-sgp.xiaomimimo.com/v1XIAOMI_TOKEN_PLAN_SGP_API_KEYSingapore
minimax-cnapi.minimaxi.com/anthropicMINIMAX_CN_API_KEYanthropic-messages
moonshotai-cnapi.moonshot.cn/v1MOONSHOT_API_KEY (dùng chung với moonshotai)

Không phải chat (1), và những provider ngoài danh sách dựng sẵn#

ProviderAPIXác thựcĐáng chú ý
typesafechỉ classifier typesafe-system-oneTYPESAFE_API_KEYJev của TypeSafe; không có api chat
fauxfaux (tự đặt)không cần: resolve luôn trả { auth: {} }Phát lại các phản hồi soạn sẵn, giả lập tốc độ token và deferred; dùng trong test, kể cả worker thử nghiệm của Pi Durable
llama.cppopenai-completions; classifier llama-cpp-classify, typesafe-system-oneURL máy chủ cộng khoá tuỳ chọnExtension dựng sẵn của coding agent, không nằm trong pi-ai (mục cục bộ)
tự định nghĩabất kỳ API nào có sẵn, hoặc API tự viếttuỳ cấu hìnhQua models.json hoặc pi.registerProvider()

Đếm lại: 11 cộng 7 cộng 4 cộng 6 cộng 2 cộng 11 cộng 1 là 42, khớp với builtinProviders(). Vài chi tiết dễ bỏ sót: openai-codex là provider duy nhất không có cách xác thực bằng khoá; moonshotai và moonshotai-cn, qwen-token-plan và qwen-token-plan-individual, opencode và opencode-go dùng chung biến môi trường nhưng credential lưu theo id provider, nên một lần /login chỉ cấu hình một provider.

Các API trên dây và lazy loading#

Thư mục packages/ai/src/api/ có 43 file: mười lăm adapter, mỗi adapter cho một API (mười chat, một ảnh, bốn classifier), mười lăm lazy wrapper *.lazy.ts tương ứng, và mười ba helper dùng chung. Ba kiểu API được khai báo riêng: KnownApi cho chat (chương pi-ai), cùng hai kiểu dưới đây.

API ảnh và API classifier mà pi-ai biết sẵn
pi/packages/ai/src/types.tsdòng 78–88TypeScript
export type KnownImageApi = "openrouter-images";export type ImageApi = KnownImageApi | (string & {});export type KnownClassifierApi =	| "typesafe-system-one"	| "cloudflare-workers-ai-system-one"	| "llama-cpp-classify"	| "openai-decisions";export type ClassifierApi = KnownClassifierApi | (string & {});

Bảng các adapter#

ModuleNói gì trên dâyĐi quaProvider dùng nó
anthropic-messagesAnthropic Messages, SSE@anthropic-ai/sdk (lớp con PiAnthropic)anthropic, minimax, minimax-cn, vercel-ai-gateway; theo mô hình ở fireworks, github-copilot, openrouter, opencode, opencode-go, cloudflare-ai-gateway
openai-responsesOpenAI Responses, store: falseSDK openaiopenai, xai, meta; theo mô hình ở github-copilot, opencode, opencode-go, cloudflare-ai-gateway
openai-completionsChat Completions và mọi phương ngữ "tương thích OpenAI", điều chỉnh bằng cờ compatSDK openai20 provider một API ở bảng trên, cloudflare-workers-ai; theo mô hình ở fireworks, github-copilot, openrouter, opencode, opencode-go, azure, cloudflare-ai-gateway; llama.cpp và máy chủ cục bộ
openai-codex-responsesResponses của backend ChatGPT ở /codex/responses, qua SSE hoặc WebSocketfetch và WebSocket; chỉ dùng kiểu của SDKopenai-codex
azure-openai-responsesResponses trên Azure, tên deployment thay cho id mô hìnhAzureOpenAI của SDK openaiazure
bedrock-converse-streamBedrock ConverseStream@aws-sdk/client-bedrock-runtime, ký SigV4 hoặc Beareramazon-bedrock
google-generative-aiGemini API@google/genaigoogle; theo mô hình ở opencode, opencode-go
google-vertexGemini trên Vertex AI@google/genai với vertexai: truegoogle-vertex
mistral-conversationsChat completions theo phương ngữ của Mistralfetch thuầnmistral
pi-messagesGiao thức riêng của Pi (bên dưới)fetch thuầnradius; bất kỳ provider nào trong models.json khai báo "api": "pi-messages"
openrouter-imagesSinh ảnh qua Chat Completions không stream, với modalitiesSDK openaiopenrouter
typesafe-system-oneSystem One: POST <baseUrl>/systemonefetch qua classifier-shared.tstypesafe, openrouter, vercel-ai-gateway, opencode; mô hình quyết định của llama.cpp
cloudflare-workers-ai-system-oneSystem One bọc trong phong bì /ai/run của Workers AIfetchcloudflare-workers-ai
openai-decisionsOpenAI Decisions: POST /v1/decisionsfetchopenai (chỉ với khoá API)
llama-cpp-classifyDùng mô hình chat làm classifier: /tokenize, /apply-template, /completion với n_probsfetchllama.cpp

Các helper dùng chung, theo người dùng chúng:

  • openai-responses-shared.ts: đổi message và đọc luồng của họ Responses, dùng chung cho openai-responses, azure-openai-responses, openai-codex-responses. google-shared.ts: phần chung của hai adapter Gemini. system-one-shared.ts và classifier-shared.ts: định dạng System One và HTTP của classifier, mỗi dịch vụ chỉ khai báo một "transport" gồm URL, phong bì request và cách bóc output (system-one-shared.ts:56).
  • github-copilot-headers.ts: ba header động mà Copilot đòi, X-Initiator (user hay agent tuỳ message cuối), Openai-Intent và Copilot-Vision-Request khi có ảnh (github-copilot-headers.ts:23); ba adapter Anthropic, Completions, Responses gọi nó khi model.provider === "github-copilot".
  • openai-prompt-cache.ts: cắt prompt_cache_key còn 64 ký tự, cho bốn adapter họ OpenAI. constrained-sampling.ts: làm lược đồ JSON thành dạng strict và chuẩn bị tool văn phạm, dùng ở mọi adapter chat trừ pi-messages (hai adapter Gemini dùng qua google-shared.ts). azure-openai-config.ts: endpoint và deployment của Azure (mục đám mây). cloudflare-ai-binding.ts: fetch qua binding của Worker (mục Cloudflare). cloudflare.ts chỉ chứa các hằng URL có placeholder mà bộ sinh catalog dùng.
  • transform-messages.ts (chuyển giao giữa mô hình, chương pi-ai), simple-options.ts (option chung, ngân sách suy nghĩ), lazy.ts (bên dưới).

Lazy loading: bốn cách giữ SDK ngoài bundle#

Một ứng dụng trình duyệt chỉ dùng Anthropic không nên phải tải SDK của AWS. pi-ai giữ điều này bằng cách để factory chỉ import một lazy wrapper, còn module adapter (và SDK của nó) chỉ được import() ở request đầu tiên. Có bốn biến thể, tuỳ việc module có chạy được trong trình duyệt hay không:

  1. Adapter chat bình thườnglazyApi(() => import("./anthropic-messages.ts")). Specifier là chuỗi hằng nên bundler tách được thành một chunk lười; lazyStream() biến lỗi nạp thành sự kiện error (chương pi-ai).
  2. Adapter chỉ chạy trên NodeBedrock nạp qua một specifier là biến, để bundler (bản thử trình duyệt, bản biên dịch Bun) không đi theo được vào AWS SDK; bản Bun đăng ký module tĩnh qua setBedrockProviderModule() (excerpt dưới).
  3. API một lần (ảnh, classifier)Không có luồng nên không cần lazyStream(): wrapper là một object có phương thức await import(...) rồi gọi tiếp (typesafe-system-one.lazy.ts).
  4. Flow OAuthlazyOAuth() nhận một hàm load; load.ts cũng dùng specifier là biến vì flow cần node:http, node:crypto, và bản Bun đăng ký flow tĩnh qua registerBundledOAuthFlowLoaders() (bun-oauth.ts).
Bedrock: specifier là biến, và cửa cho bản biên dịch Bun
pi/packages/ai/src/api/bedrock-converse-stream.lazy.tsdòng 4–30TypeScript
/** * Loads the bedrock implementation through a variable specifier so bundlers * (browser smoke, Bun compile) cannot follow the import into the Node-only * AWS SDK. The `.ts`/`.js` rewrite keeps the trick working from both source * and built output. */const importNodeOnlyApi = (specifier: string): Promise<unknown> => {	const runtimeSpecifier = import.meta.url.endsWith(".js") ? specifier.replace(/\.ts$/, ".js") : specifier;	return import(runtimeSpecifier);};let bedrockModuleOverride: ProviderStreams | undefined;/** * Overrides the dynamically imported bedrock implementation. Used by the Bun * binary build, where the variable-specifier import cannot be bundled; the * build registers a statically imported module instead. */export function setBedrockProviderModule(module: ProviderStreams): void {	bedrockModuleOverride = module;}export const bedrockConverseStreamApi = (): ProviderStreams =>	lazyApi(		async () =>			bedrockModuleOverride ?? ((await importNodeOnlyApi("./bedrock-converse-stream.ts")) as ProviderStreams),	);

Mẹo đổi đuôi .ts thành .js theo import.meta.url giúp cùng một dòng chạy được cả khi chạy thẳng mã nguồn TypeScript lẫn khi chạy bản build. Cái giá của specifier là biến là bundler không biết module đó tồn tại, nên bản phân phối nào đóng gói đơn file phải tự đăng ký nó, đúng như entry ./bedrock-provider và ./bun-oauth trong package.json làm.

pi-messages: giao thức riêng của Pi#

Mọi adapter khác dịch transcript của Pi sang định dạng của một hãng, rồi dịch luồng của hãng về sự kiện của Pi. pi-messages bỏ cả hai bước dịch: client gửi chính transcript đã chuẩn hoá và nhận lại chính các sự kiện của pi-ai, đã serialize. Việc nói chuyện với nhà cung cấp thật nằm ở máy chủ. Đó là giao thức của gateway Radius, nhưng chú thích đầu file nói rõ mọi backend hiện thực nó đều dùng được (pi-messages.ts:1).

Request của pi-messages: một POST mang model, context và option thống nhất
pi/packages/ai/src/api/pi-messages.tsdòng 371–406TypeScript
const url = new URL(`${model.baseUrl.replace(/\/+$/u, "")}/messages`);if (options?.debug) {	url.searchParams.set("debug", "1");}let payload: unknown = {	model: model.id,	context,	options: {		temperature: options?.temperature,		maxTokens: options?.maxTokens,		reasoning: options?.reasoning,		cacheRetention: resolveCacheRetention(options?.cacheRetention, options?.env),		sessionId: options?.sessionId,		toolChoice: options?.toolChoice,	},};const nextPayload = await options?.onPayload?.(payload, model);if (nextPayload !== undefined) {	payload = nextPayload;}const response = await (options?.fetch ?? globalThis.fetch)(url, {	method: "POST",	headers: providerHeadersToRecord(		getIdentityHeaders(model, options?.identity),		{			authorization: `Bearer ${apiKey}`,			accept: "text/event-stream",			"content-type": "application/json",		},		options?.headers,	),	body: JSON.stringify(payload),	signal: options?.signal,});

Request là một POST tới <baseUrl>/messages. context là TranscriptContext nguyên vẹn, tức system prompt và tool nằm trong các system message; options chỉ gồm option thống nhất của Pi (reasoning, cacheRetention, sessionId, toolChoice), không có tham số riêng của hãng nào. Header identity được gộp dưới header xác thực và header của người gọi, như mọi adapter. Phản hồi là một luồng SSE mà mỗi bản tin là một sự kiện:

PiMessagesEvent: sự kiện của pi-ai, bỏ trường partial
pi/packages/ai/src/api/pi-messages.tsdòng 54–88TypeScript
/** Serialized assistant-message event as sent by a pi-messages backend. */export type PiMessagesEvent =	| { type: "start" }	| { type: "text_start"; contentIndex: number }	| { type: "text_delta"; contentIndex: number; delta: string }	| { type: "text_end"; contentIndex: number; content: string; contentSignature?: string }	| { type: "thinking_start"; contentIndex: number }	| { type: "thinking_delta"; contentIndex: number; delta: string }	| {			type: "thinking_end";			contentIndex: number;			content: string;			contentSignature?: string;			redacted?: boolean;	  }	| { type: "toolcall_start"; contentIndex: number; id: string; toolName: string }	| { type: "toolcall_delta"; contentIndex: number; delta: string }	| { type: "toolcall_end"; contentIndex: number; toolCall: ToolCall }	| {			type: "done";			reason: Extract<PiMessagesStopReason, "stop" | "length" | "toolUse">;			usage: PiMessagesUsage;			responseId?: string;			providerThinkingLevel?: string;			rewrite?: PiMessagesRewriteImpact;	  }	| {			type: "error";			reason: Extract<PiMessagesStopReason, "aborted" | "error">;			usage: PiMessagesUsage;			errorMessage?: string;			responseId?: string;			providerThinkingLevel?: string;			rewrite?: PiMessagesRewriteImpact;	  };

Đây là bộ sự kiện của giao thức luồng, bỏ trường partial. Client tự dựng lại message dở bằng một bộ chuyển đổi nhỏ: delta chữ và suy nghĩ được nối vào khối theo contentIndex, mảnh JSON của tham số tool được phân tích dần bằng parseStreamingJson(), toolcall_end mang lời gọi tool đã chốt từ máy chủ (createEventConverter). Có ba khác biệt đáng nhớ so với adapter thường:

  • Usage và chi phí đến nguyên từ sự kiện done hay error của máy chủ; client không gọi calculateCost(), nên giá là giá gateway tính.
  • Sự kiện cuối có thể mang rewrite: tóm tắt việc một chính sách của gateway đã sửa message (số token, số message thay đổi, system prompt có bị đổi không). Client ghi nó thành một diagnostic pi_messages_rewrite trên message, để người dùng biết request đã bị sửa phía máy chủ (pi-messages.ts:44).
  • Không có SDK nào, nên module này nhỏ (448 dòng) và chạy ở mọi môi trường có fetch. Luồng hết mà không có sự kiện cuối là lỗi.

Credential trong pi-ai: môi trường, kho và thứ tự#

pi-ai biết ba nguồn credential: giá trị tường minh trong option của request, kho credential (CredentialStore) mà ứng dụng tiêm vào createModels(), và môi trường xung quanh, đọc qua một AuthContext. Thứ tự giữa chúng do resolveProviderAuth() quyết định, đã trích ở chương pi-ai; tóm lại:

  1. options.apiKeyNếu có, nó được bọc thành một credential api_key tạm rồi vẫn đi qua resolve() của provider. Đi đường vòng này là có chủ ý: một provider như Cloudflare cần dựng URL theo account từ cùng lần phân giải, kể cả khi khoá đến từ lời gọi (CHANGELOG 0.80.2).
  2. Credential đã lưuOAuth thì làm mới nếu còn dưới năm phút rồi gọi toAuth(); api_key thì đưa vào resolve(). Credential đã lưu "sở hữu" provider: lưu OAuth mà provider không có oauth, hoặc làm mới thất bại, thì không có đường lùi về biến môi trường.
  3. Môi trườngKhông có gì được lưu thì resolve() chạy với credential: undefined và tự đọc biến môi trường, file, profile. Kết quả undefined nghĩa là provider chưa được cấu hình.

options.env phủ lên môi trường của tiến trình cho riêng request đó. Cơ chế chỉ là một AuthContext bọc ngoài:

overlayEnvAuthContext(): biến môi trường của request đứng trước
pi/packages/ai/src/auth/resolve.tsdòng 95–100TypeScript
function overlayEnvAuthContext(base: AuthContext, env: ProviderEnv): AuthContext {	return {		env: async (name) => env[name] || (await base.env(name)),		fileExists: (path) => base.fileExists(path),	};}

Bản thân AuthContext chỉ có hai thao tác bất đồng bộ, env(name) và fileExists(path) (auth/types.ts:96). Bản mặc định đọc process.env, coi chuỗi rỗng hoặc toàn khoảng trắng là không đặt, và kiểm tra file bằng node:fs với ~ được mở rộng. Trong trình duyệt, env() luôn trả undefined và fileExists() luôn false, nên ở đó chỉ còn khoá truyền tường minh hoặc kho credential.

AuthContext mặc định: process.env và node:fs, nạp lười
pi/packages/ai/src/auth/context.tsdòng 23–45TypeScript
export function defaultProviderAuthContext(): AuthContext {	return {		async env(name: string): Promise<string | undefined> {			const value = getProcessEnv()?.[name];			return typeof value === "string" && value.trim().length > 0 ? value : undefined;		},		async fileExists(path: string): Promise<boolean> {			try {				const fs = (await importNodeModule("node:fs/promises")) as NodeFsModule;				let resolved = path;				if (resolved.startsWith("~")) {					const os = (await importNodeModule("node:os")) as NodeOsModule;					resolved = os.homedir() + resolved.slice(1);				}				await fs.access(resolved);				return true;			} catch {				return false;			}		},	};}

Vì AuthContext tiêm được, một host chạy nhiều người dùng trong một tiến trình có thể cho mỗi Models một nguồn biến môi trường riêng, và test không phải sửa process.env. Adapter thì đọc cấu hình không phải xác thực (vùng của AWS, phiên bản API của Azure, PI_CACHE_RETENTION) qua một hàm khác, getProviderEnvValue(): options.env trước, rồi process.env, rồi một đường lùi đọc /proc/self/environ cho bản biên dịch Bun, nơi process.env có thể rỗng trong sandbox Linux (provider-env.ts).

Kiểm tra mà không phân giải#

Models cần biết provider nào "có credential" để liệt kê mô hình dùng được (getAvailable()), và việc đó chạy thường xuyên. checkProviderAuth() coi một OAuth đã lưu là đủ, không làm mới; với khoá, nó dùng ApiKeyAuth.check() nếu provider có, không thì phân giải thật (models.ts:664). check() tồn tại cho những provider mà resolve() có tác dụng phụ: models.json có thể khai báo khoá là một lệnh shell, và kiểm tra không được chạy lệnh đó (auth/types.ts:177).

env-api-keys.ts ở v1.4.0#

Tên biến môi trường của từng provider giờ nằm trong factory của nó, trong envApiKeyAuth("...", ["GROQ_API_KEY"]). File env-api-keys.ts giữ một bảng song song từ thời API toàn cục (env-api-keys.ts:73), và ở v1.4.0 chỉ còn đường /compat gọi getEnvApiKey() (compat.ts:224). Hai bảng khớp nhau cho mọi provider; đáng chú ý là với Vertex và Bedrock, bảng cũ trả về chuỗi đánh dấu "<authenticated>" khi chỉ có credential môi trường, và compat bỏ qua đúng chuỗi này thay vì gửi nó đi như một khoá. Cách mới không cần mẹo đó: resolve() trả { auth: {} }, nghĩa là "đã cấu hình, nhưng không có gì để chèn vào request".

Credential trong coding agent: auth.json, --api-key, pi auth#

pi-ai mặc định giữ credential trong bộ nhớ. Coding agent tiêm vào createModels() một kho bền là AuthStorage, đọc ghi file <agent-dir>/auth.json (mặc định ~/.pi/agent/auth.json), rồi bọc nó trong RuntimeCredentials để chứa khoá của cờ --api-key (model-runtime.ts:219). File có dạng một object, khoá là id provider:

Ví dụ minh hoạ: auth.json (giá trị bí mật thay bằng …)
{
  "anthropic":  { "type": "oauth", "access": "…", "refresh": "…", "expires": 1761100000000 },
  "openrouter": { "type": "api_key", "key": "!pass show openrouter" },
  "cloudflare-ai-gateway": {
    "type": "api_key", "key": "…",
    "env": { "CLOUDFLARE_ACCOUNT_ID": "…", "CLOUDFLARE_GATEWAY_ID": "…" }
  }
}
Một credential cho mỗi provider. key bắt đầu bằng ! là một lệnh, chạy khi cần lần đầu; env là cấu hình theo provider, được ưu tiên hơn biến môi trường của tiến trình.

Ba chi tiết của AuthStorage làm file này an toàn khi nhiều tiến trình Pi chạy cùng lúc. File được tạo với quyền 0600, thư mục cha 0700 (auth-storage.ts:36). Mọi lần ghi đi qua một khoá file (proper-lockfile, coi là cũ sau 30 giây), và modify() đọc lại file bên trong khoá trước khi gộp, nên một /login ở tiến trình khác không bị ghi đè. Mỗi lần đọc so sánh "revision" của file với lần trước và đọc lại khi khác, nên một tiến trình thấy ngay token do tiến trình khác vừa làm mới.

AuthStorage: đọc có phân giải giá trị, ghi gộp dưới khoá file
pi/packages/coding-agent/src/core/auth-storage.tsdòng 442–472TypeScript
async read(provider: string, options?: AuthOperationOptions): Promise<Credential | undefined> {	const credential = (await this.readLatestData(options))[provider];	options?.signal?.throwIfAborted();	if (credential?.type !== "api_key") return credential;	if (credential.key === undefined) return credential;	return { ...credential, key: resolveConfigValue(credential.key, credential.env) };}async modify(	provider: string,	fn: (current: Credential | undefined) => Promise<Credential | undefined>,	options?: AuthOperationOptions,): Promise<Credential | undefined> {	let latestData = this.readState.data;	let revision: string | undefined;	const result = await this.storage.withLockAsync(async (content) => {		const currentData = this.parseStorageData(content);		const next = await fn(currentData[provider]);		if (next === undefined) {			latestData = currentData;			revision = this.authPath ? getFileRevision(this.authPath) : undefined;			return { result: currentData[provider] };		}		const merged: AuthStorageData = { ...currentData, [provider]: next };		latestData = merged;		return { result: next, next: JSON.stringify(merged, null, 2) };	}, options);	this.updateReadState(latestData, revision);	return result;}

read() đưa key qua resolveConfigValue(), nên trong auth.json có thể viết !lệnh hoặc $TÊN_BIẾN, với biến lấy từ env của chính credential trước rồi mới tới process.env. Kết quả của lệnh được cache suốt đời tiến trình; lệnh lỗi, hết 10 giây hay in ra chuỗi rỗng thì khoá chưa được phân giải cho tới khi khởi động lại (docs/providers.md:71, resolve-config-value.ts:185). Đây là cách dùng một trình quản lý bí mật mà không ghi khoá ra đĩa.

--api-key: một lớp phủ không bền#

RuntimeCredentials: khoá lúc chạy đứng trước auth.json
pi/packages/coding-agent/src/core/runtime-credentials.tsdòng 3–28TypeScript
/** Async credential store overlay for non-persistent runtime API keys. */export class RuntimeCredentials implements CredentialStore {	private readonly store: CredentialStore;	private readonly overrides = new Map<string, string>();	constructor(store: CredentialStore) {		this.store = store;	}	setRuntimeApiKey(providerId: string, apiKey: string): void {		this.overrides.set(providerId, apiKey);	}	removeRuntimeApiKey(providerId: string): void {		this.overrides.delete(providerId);	}	hasRuntimeApiKey(providerId: string): boolean {		return this.overrides.has(providerId);	}	async read(providerId: string, options?: AuthOperationOptions): Promise<Credential | undefined> {		options?.signal?.throwIfAborted();		const override = this.overrides.get(providerId);		return override ? { type: "api_key", key: override } : this.store.read(providerId, options);	}

main.ts gắn khoá của --api-key cho đúng provider của mô hình đã chọn, sau khi phân giải --model (main.ts:831). Vì lớp phủ nằm ở read() của kho, pi-ai thấy nó như một credential đã lưu, và nó thắng mọi thứ khác. Nó không bao giờ được ghi xuống file.

Thứ tự đầy đủ trong coding agent#

Khi models.json hoặc một extension cấu hình provider, composeApiKeyAuth() trong provider-composer.ts bọc ApiKeyAuth của provider dựng sẵn (gọi là "inherited") và thêm khoá cấu hình vào giữa chuỗi:

composeApiKeyAuth(): credential đã lưu, rồi khoá cấu hình, rồi môi trường
pi/packages/coding-agent/src/core/provider-composer.tsdòng 462–485TypeScript
	resolve: async (input) => {		let result: AuthResult | undefined;		if (input.credential) {			result = inherited				? await inherited.resolve(input)				: input.credential.key					? { auth: { apiKey: input.credential.key }, env: input.credential.env, source: "stored credential" }					: undefined;		} else if (rawKey !== undefined) {			const env = await configContextEnv([rawKey], input.ctx);			const key = resolveConfigValueOrThrow(rawKey, `API key for provider "${providerId}"`, env);			result = inherited				? await inherited.resolve({ ...input, credential: { type: "api_key", key } })				: { auth: { apiKey: key }, source: "configured API key" };		} else {			result = await inherited?.resolve(input);		}		if (!result) return undefined;		const explicitEnv = { ...(input.credential?.env ?? {}), ...(result.env ?? {}) };		const headerEnv = await configContextEnv(Object.values(rawHeaders ?? {}), input.ctx, explicitEnv);		const headers = resolveHeadersOrThrow(rawHeaders, `provider "${providerId}"`, headerEnv);		return { ...result, auth: withConfiguredAuth(result.auth, headers, authHeader) };	},};

Ghép với lớp --api-key và với resolveProviderAuth() của pi-ai, ta có đúng thứ tự mà tài liệu mô tả: --api-key, rồi auth.json, rồi apiKey trong models.json, cuối cùng là biến môi trường hoặc credential môi trường của provider (docs/models.md:23). Hai chi tiết đáng để ý. Khoá cấu hình không thay resolve() dựng sẵn mà được đưa vào nó như một credential, nên Cloudflare vẫn thêm account id, Anthropic vẫn xử lý khoá như thường. Và header cấu hình được phân giải sau cùng, với biến môi trường lấy cả từ env mà provider vừa trả về; authHeader: true thêm Authorization: Bearer <khoá> và báo lỗi nếu không có khoá (withConfiguredAuth). Header khai báo trên từng mô hình được ModelRuntime.getAuth() gộp thêm ở bước cuối (model-runtime.ts:557).

pi auth: xuất credential cho client bên ngoài#

Từ 0.83.0, pi auth print-api-key và pi auth print-bearer-token in ra credential đã cấu hình, để một công cụ khác (một script, một client HTTP) dùng chung đăng nhập của Pi; pi auth check, thêm ở 0.84.1, chỉ báo ready, not_ready hay invalid qua mã thoát (docs/cli.md:293). Cơ chế cố ý dùng lại đường phân giải của request:

Chọn đúng loại credential, đòi thời hạn tối thiểu cho token OAuth
pi/packages/coding-agent/src/cli/credential-print.tsdòng 59–73TypeScript
const credentials: Array<{ providerId: string; value: string }> = [];for (const provider of providers) {	const type = credentialTypes.get(provider.id);	if (kind === "api_key" && type === "oauth") continue;	if (kind === "bearer_token" && type !== "oauth") continue;	const authOptions = {		...(kind === "bearer_token" ? { minOAuthValidityMs: minExpiryMs ?? DEFAULT_BEARER_TOKEN_MIN_EXPIRY_MS } : {}),		signal,	};	const auth = provider.model		? await modelRuntime.getAuth(provider.model, authOptions)		: await modelRuntime.getAuth(provider.id, authOptions);	const value = getAuthCredential(auth);	if (value) credentials.push({ providerId: provider.id, value });}

print-api-key bỏ qua provider đăng nhập bằng OAuth, print-bearer-token chỉ nhận OAuth. Token OAuth còn dưới 30 phút (hoặc dưới giá trị của --min-expiry) được làm mới trước khi in, qua minOAuthValidityMs của resolveProviderAuth(); nếu sau khi làm mới token vẫn sắp hết hạn, lệnh báo lỗi thay vì in ra một token sắp chết. getAuthCredential() lấy apiKey, hoặc phần sau Bearer của header Authorization với những provider xác thực bằng header như Kimi Code (auth-command.ts:120). Lệnh ghi bí mật ra stdout, nên đừng chạy nó trong một phiên được ghi log.

Đăng nhập OAuth và gói thuê bao#

Một gói thuê bao (Claude Pro/Max, ChatGPT, Copilot, SuperGrok) không đi kèm khoá API. Muốn dùng nó từ một chương trình, chương trình phải đăng nhập như ứng dụng chính thức của hãng làm, lấy một token, và làm mới token đó khi hết hạn. pi-ai mô tả mọi flow như vậy bằng cùng một interface OAuthAuth với ba thao tác: login(), refresh() và toAuth() (auth/types.ts:216). login() không biết gì về giao diện: nó chỉ gọi prompt() (bốn loại: text, secret, select, manual_code) và notify() (bốn sự kiện: info, auth_url, device_code, progress), nên cùng một flow chạy được trong TUI, trong CLI nhỏ của pi-ai, hay trong một ứng dụng web (auth/types.ts:119).

Chín flow ở v1.4.0, dùng ba kiểu đăng nhập: trình duyệt với máy chủ callback cục bộ, device code (RFC 8628: máy hiện một mã, người dùng nhập mã đó trên một trang web), và dán mã hay URL thủ công.

FlowCách đăng nhậpCredential lưurefresh() và toAuth()
Anthropic (Claude Pro/Max)Chọn: trình duyệt cộng PKCE, callback ở cổng 53692 (lùi về cổng trống, rồi về dán URL); hoặc "copy code" cho máy không có trình duyệtaccess, refresh, hạn trừ trước năm phútGrant refresh_token; toAuth trả access token như khoá API
OpenAI (Sign in with ChatGPT)PKCE, đăng ký client động: gửi dynamic_agent_client, nhận client id thật trong callback; cổng cố định 1455 hoặc dán URLThêm clientId, scopes; hạn trừ ba phútRefresh bằng client id đã cấp; token đi thẳng tới api.openai.com
OpenAI Codex (legacy)Chọn: trình duyệt cộng PKCE (cổng 1455, dùng chung với Codex CLI) hoặc device code ở auth.openai.com/codex/deviceThêm accountId đọc từ JWTGrant refresh_token; adapter tự đọc lại account id từ token
GitHub CopilotDevice code ở github.com hoặc miền GitHub Enterpriserefresh là token GitHub, access là token Copilot; thêm availableModelIds"Refresh" là đổi lại token GitHub lấy token Copilot rồi tải lại danh sách mô hình; toAuth trả cả baseUrl
Kimi CodeDevice code ở auth.kimi.com (đổi được bằng KIMI_CODE_OAUTH_HOST)access, refreshThử lại tới ba lần khi 429 hay 5xx; toAuth trả header Authorization
Meta (Muse)Device code ở auth.meta.com, rồi đổi identity token lấy một khoá Model APIrefresh là identity token, access là khoá sống 24 giờ"Refresh" là xin khoá mới; 401 hay 403 nghĩa là phải đăng nhập lại
xAI (SuperGrok, X Premium)Device code ở auth.x.aiaccess, refresh, hạn trừ năm phútGiữ refresh token cũ nếu máy chủ không xoay vòng nó
OpenRouterPKCE, callback ở cổng ngẫu nhiên với đường dẫn ngẫu nhiên, hoặc dán URL; hết hạn sau năm phút chờMột khoá API vĩnh viễn trong access, expires: Number.MAX_SAFE_INTEGERrefresh trả nguyên credential
RadiusChọn: trình duyệt cộng PKCE (cổng 1456, endpoint ủy quyền tra ở /v1/oauth của gateway) hoặc device codeaccess, refresh, hạn trừ 60 giâyGrant refresh_token tới chính gateway

Ba khối dựng chung#

PKCE. generatePKCE() sinh 32 byte ngẫu nhiên bằng Web Crypto làm verifier và băm SHA-256 thành challenge, nên chạy được cả trên Node lẫn trình duyệt (pkce.ts:21). Anthropic dùng luôn verifier làm tham số state; OpenRouter không gửi state, nên flow của nó đặt callback ở một đường dẫn chứa UUID ngẫu nhiên để một request lạc không hoàn tất được việc đăng nhập (openrouter.ts:115).

Máy chủ callback. startOAuthCallbackServer() mở một máy chủ HTTP trên loopback, chỉ nhận đúng đường dẫn và đúng state, chỉ xử lý callback đầu tiên, và đổi mã lấy token trước khi trả trang kết quả, để lỗi đổi mã hiện được trên trình duyệt (callback-server.ts:50). Khi trình duyệt chạy trên máy khác (qua SSH, trong container), callback không bao giờ tới; vì thế mọi flow trình duyệt đua callback với một lời nhắc dán mã:

Đua callback với việc dán mã: bên nào xong trước thì huỷ bên kia
pi/packages/ai/src/auth/oauth/callback-server.tsdòng 155–183TypeScript
export async function waitForCallbackOrManualInput<T>(	interaction: ProviderAuthInteraction,	callback: OAuthCallbackServer<T> | undefined,	prompt: { message: string; placeholder: string },): Promise<{ type: "callback"; value: T } | { type: "manual"; input: string }> {	const manualAbort = new AbortController();	let manualError: Error | undefined;	const manual = interaction		.prompt({ type: "manual_code", ...prompt, signal: manualAbort.signal })		.then((input) => {			callback?.cancel();			return input;		})		.catch((error: unknown) => {			manualError = error instanceof Error ? error : new Error(String(error));			callback?.cancel();			return undefined;		});	try {		const value = await callback?.wait();		if (manualError) throw manualError;		if (value !== undefined) return { type: "callback", value };		const input = await manual;		if (manualError) throw manualError;		return { type: "manual", input: input ?? "" };	} finally {		manualAbort.abort();	}}

Địa chỉ lắng nghe mặc định là 127.0.0.1; biến PI_OAUTH_CALLBACK_HOST đổi được nó cho các flow Anthropic, ChatGPT, Codex và OpenRouter (anthropic.ts:17), còn Radius cố định ở 127.0.0.1:1456. Cổng cố định có lý do: Anthropic chọn 53692 để người dùng chuyển tiếp được cổng vào container hay qua SSH, và lùi về cổng trống vì Anthropic nhận mọi cổng loopback; Sign in with ChatGPT thì không lùi được, và báo lỗi rõ khi cổng 1455 đang bị một phiên Pi khác hay Codex CLI giữ, thay vì để callback rơi vào tiến trình kia rồi hỏng vì sai state (openai-chatgpt.ts:241).

Device code. Bốn flow chỉ dùng device code (Copilot, Kimi, Meta, xAI), hai flow có nó như một lựa chọn (Codex, Radius). Phần hỏi vòng dùng chung một hàm:

pollOAuthDeviceCodeFlow(): hỏi vòng theo RFC 8628, tôn trọng slow_down
pi/packages/ai/src/auth/oauth/device-code.tsdòng 64–97TypeScript
while (Date.now() < deadline) {	if (options.signal.aborted) {		throw new Error(CANCEL_MESSAGE);	}	const result = await options.poll();	if (result.status === "complete") {		return result.value;	}	if (result.status === "failed") {		throw new Error(result.message);	}	if (result.status === "slow_down") {		slowDownResponses += 1;		// Use the server-provided interval when given (GitHub reports the new required minimum		// in `interval`); trusting only a client-tracked value risks polling early forever under		// WSL/VM clock drift. Otherwise apply RFC 8628 section 3.5: increase by 5 seconds.		intervalMs =			typeof result.intervalSeconds === "number" &&			Number.isFinite(result.intervalSeconds) &&			result.intervalSeconds > 0				? Math.max(MINIMUM_INTERVAL_MS, Math.floor(result.intervalSeconds * 1000))				: Math.max(MINIMUM_INTERVAL_MS, intervalMs + SLOW_DOWN_INTERVAL_INCREMENT_MS);	}	const remainingMs = deadline - Date.now();	if (remainingMs <= 0) {		break;	}	await abortableSleep(Math.min(intervalMs, remainingMs), options.signal, CANCEL_MESSAGE);}throw new Error(slowDownResponses > 0 ? SLOW_DOWN_TIMEOUT_MESSAGE : TIMEOUT_MESSAGE);

Mặc định hỏi năm giây một lần, tối thiểu một giây. Khi máy chủ trả slow_down, khoảng cách lấy theo giá trị máy chủ gửi nếu có, không thì cộng năm giây như RFC quy định. Chú thích trong code giải thích vì sao tin máy chủ: trên WSL hay máy ảo bị lệch đồng hồ, một khoảng cách chỉ do client tự tính có thể hỏi sớm mãi mãi; lỗi hết giờ sau nhiều lần slow_down vì thế gợi ý đồng bộ lại đồng hồ. Với Copilot, Kimi, Meta và xAI, URL xác minh mà máy chủ gửi về được kiểm tra là http(s) (riêng xAI đòi https) trước khi đưa cho trình mở URL, để một phản hồi độc không khiến open chạy một file thực thi (github-copilot.ts:242).

Khi "refresh" không phải là refresh#

Interface tách refresh() (tạo credential mới) khỏi toAuth() (suy ra xác thực của request), và sự tách bạch đó đủ rộng để chứa những thứ không phải OAuth chuẩn. Copilot là ví dụ rõ nhất: token GitHub nằm ở trường refresh, "làm mới" là đổi nó lấy một token Copilot ngắn hạn, và toAuth() đọc endpoint của tài khoản ngay trong token:

Copilot: endpoint theo tài khoản nằm trong token (proxy-ep)
pi/packages/ai/src/auth/oauth/github-copilot.tsdòng 64–76, 493–507TypeScript
/** * Parse the proxy-ep from a Copilot token and convert to API base URL. * Token format: tid=...;exp=...;proxy-ep=proxy.individual.githubcopilot.com;... * Returns API URL like https://api.individual.githubcopilot.com */function getBaseUrlFromToken(token: string): string | null {	const match = token.match(/proxy-ep=([^;]+)/);	if (!match) return null;	const proxyHost = match[1];	// Convert proxy.xxx to api.xxx	const apiHost = proxyHost.replace(/^proxy\./, "api.");	return `https://${apiHost}`;}export const githubCopilotOAuth: OAuthAuth = {	name: "GitHub Copilot",	isSubscription: true,	login: loginGitHubCopilot,	refresh: (credential, signal) =>		refreshGitHubCopilotToken(credential.refresh, copilotEnterpriseDomain(credential), signal),	/** Derive the credential-specific proxy endpoint for each request. */	async toAuth(credential) {		return {			apiKey: credential.access,			baseUrl: getGitHubCopilotBaseUrl(credential.access, copilotEnterpriseDomain(credential)),		};	},};

Lúc đăng nhập, flow Copilot còn gọi /models để biết tài khoản được dùng mô hình nào, và bật chính sách cho những mô hình còn ở trạng thái "unconfigured" (một số mô hình như Claude, Grok phải bật trước khi dùng); filterModels của provider sau đó chỉ cho hiện các id đó (loginGitHubCopilot). Meta đi xa hơn: identity token của nó không dùng để suy luận được và cũng không làm mới được, nên Pi lưu nó vào refresh và "làm mới" bằng cách xin một khoá API mới mỗi ngày; chú thích đầu file nói rõ cách này để bộ lập lịch OAuth chuẩn tự xin lại khoá mà không cần cơ chế riêng (meta.ts:1). OpenRouter thì ngược lại: OAuth chỉ để tạo một khoá API do người dùng kiểm soát, nên không có gì để làm mới.

Sign in with ChatGPT có hai đòi hỏi riêng. Nó cần một id cài đặt ổn định dạng UUID, gửi đi như ext_agent_host_id; coding agent đưa vào qua LoginOptions.getDeviceId, lấy từ settings (interactive-mode.ts:6296). Và grant phải có scope chatgpt.tokens.use.direct, nếu không credential bị từ chối ngay khi đăng nhập (openai-chatgpt.ts:162).

Làm mới an toàn#

Việc làm mới chạy bên trong CredentialStore.modify(), với điều kiện hết hạn được kiểm tra lại sau khi đã giữ khoá, và một lần làm mới đã bắt đầu sẽ chạy tới cùng dù người gọi huỷ; cơ chế này đã được giải thích ở chương pi-ai. Changelog cho thấy nó được dựng dần qua các sự cố thật: khoá với kiểm tra kép từ 0.80.0 (CHANGELOG 0.80.0), làm mới khi còn dưới năm phút thay vì đợi hết hạn từ 0.83.0 (CHANGELOG 0.83.0), giới hạn thời gian làm mới để request treo không giữ khoá mãi từ 0.84.0 (CHANGELOG 0.84.0), và từ 1.0.3 một lần làm mới bị huỷ giữa chừng không còn vứt mất refresh token vừa xoay vòng, lỗi từng khiến Sign in with ChatGPT báo refresh_token_invalidated (CHANGELOG 1.0.3, coding-agent CHANGELOG).

Lưu ý thêm: nhiều flow đã tự trừ một khoảng an toàn vào expires lúc đăng nhập (năm phút với Anthropic, xAI, Copilot; ba phút với ChatGPT; 60 giây với Radius), rồi resolveStoredOAuth() lại làm mới khi còn dưới năm phút. Hai khoảng an toàn cộng dồn, nên token thực tế được làm mới sớm hơn hạn thật từ sáu (Radius) tới mười phút (Anthropic, xAI, Copilot).

Nền tảng đám mây: Bedrock, Vertex, Azure, workload identity#

Nền tảng đám mây không cấp "một khoá API" theo nghĩa thông thường. Credential của chúng là một chuỗi nguồn (profile, khoá IAM, vai trò của container, file tài khoản dịch vụ) mà SDK chính thức đã biết cách đi qua. pi-ai không lặp lại chuỗi đó. Với Bedrock và Vertex, resolve() của provider chỉ làm hai việc: báo provider "đã được cấu hình" để mô hình hiện lên, và chuyển cấu hình (profile, project, location) vào env của request; việc lấy credential thật thuộc về SDK lúc gửi. Azure thì ngược lại: xác thực là một khoá thường, cái khó nằm ở endpoint riêng của từng resource.

Amazon Bedrock#

bedrockAuth.resolve(): nhận ra credential AWS mà không chép chúng
pi/packages/ai/src/providers/amazon-bedrock.tsdòng 54–79TypeScript
resolve: async ({ ctx, credential, signal }) => {	const env = async (name: string) => {		signal.throwIfAborted();		const value = await ctx.env(name);		signal.throwIfAborted();		return value;	};	if (credential?.key) {		return { auth: { apiKey: credential.key }, env: credential.env, source: "stored credential" };	}	if (await env("AWS_BEARER_TOKEN_BEDROCK")) return { auth: {}, source: "AWS_BEARER_TOKEN_BEDROCK" };	if (credential?.env?.AWS_PROFILE ?? (await env("AWS_PROFILE"))) {		return {			auth: {},			env: credential?.env,			source: credential?.env?.AWS_PROFILE ? "stored credential" : "AWS_PROFILE",		};	}	if ((await env("AWS_ACCESS_KEY_ID")) && (await env("AWS_SECRET_ACCESS_KEY"))) {		return { auth: {}, source: "AWS access keys" };	}	if (await env("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI")) return { auth: {}, source: "ECS task role" };	if (await env("AWS_CONTAINER_CREDENTIALS_FULL_URI")) return { auth: {}, source: "ECS task role" };	if (await env("AWS_WEB_IDENTITY_TOKEN_FILE")) return { auth: {}, source: "web identity token" };	return undefined;},

Thứ tự là: khoá đã lưu (một bearer token của Bedrock), AWS_BEARER_TOKEN_BEDROCK, AWS_PROFILE (bản lưu trong env của credential trước), cặp AWS_ACCESS_KEY_ID và AWS_SECRET_ACCESS_KEY, vai trò của ECS, web identity (IRSA trên Kubernetes). Ngoài khoá đã lưu, mọi nhánh trả auth: {}: không có gì để chèn vào request. Lệnh đăng nhập cho chọn một trong ba cách và chỉ lưu đúng thứ người dùng nhập: token, tên profile, hoặc không gì cả với lựa chọn "chuỗi credential có sẵn" (amazon-bedrock.ts:11). Adapter quyết định phần còn lại khi dựng client:

Dựng BedrockRuntimeClient: profile, bearer, vùng, proxy
pi/packages/ai/src/api/bedrock-converse-stream.tsdòng 163–166, 185–191, 198–206, 217–230, 242–245TypeScript
const optionsProfile = options.profile || options.env?.AWS_PROFILE;const config: BedrockRuntimeClientConfig = {	profile: optionsProfile || getProviderEnvValue("AWS_PROFILE", options.env),	customUserAgent: bedrockUserAgent(options.identity),const skipAuth = getProviderEnvValue("AWS_BEDROCK_SKIP_AUTH", options.env) === "1";const bearerToken =	options.bearerToken ||	options.apiKey ||	getProviderEnvValue("AWS_BEARER_TOKEN_BEDROCK", options.env) ||	undefined;const useBearerToken = bearerToken !== undefined && !skipAuth;	const arnRegionMatch = model.id.match(/^arn:aws(?:-[a-z0-9-]+)?:bedrock:([a-z0-9-]+):/);	if (arnRegionMatch) {		config.region = arnRegionMatch[1];	} else if (configuredRegion) {		config.region = configuredRegion;	} else if (endpointRegion && useExplicitEndpoint) {		config.region = endpointRegion;	} else if (!hasAmbientConfiguredProfile) {		config.region = "us-east-1";	const credentials = getConfiguredBedrockCredentials(options.env);	if (!skipAuth && credentials && !optionsProfile) {		config.credentials = credentials;	}	const proxyUrl = resolveHttpProxyUrlForTarget(model.baseUrl, options.env);	if (proxyUrl) {		// Bedrock runtime uses NodeHttp2Handler by default since v3.798.0, which is based		// on `http2` module and has no support for http agent.		// Use NodeHttpHandler to support HTTP(S) proxy agents.		config.requestHandler = new NodeHttpHandler({			httpAgent: new HttpProxyAgent(proxyUrl),			httpsAgent: new HttpsProxyAgent(proxyUrl) as unknown as HttpsAgent,		});if (useBearerToken) {	config.token = { token: bearerToken };	config.authSchemePreference = ["httpBearerAuth"];}
  • Bearer hay SigV4. Có bearer token (từ option, từ khoá đã lưu đi qua apiKey, hoặc từ biến môi trường) thì client dùng httpBearerAuth và bỏ qua chữ ký SigV4. Không có thì SDK AWS tự đi chuỗi credential mặc định của nó.
  • Profile thắng khoá môi trường. Profile chọn qua luồng đăng nhập của Pi phải thắng cặp khoá IAM trong môi trường, mà chuỗi mặc định của SDK chỉ làm vậy khi credentials không được đặt; vì thế khoá IAM chỉ được gán tường minh khi không có profile tường minh (lỗi #6957).
  • Vùng. Vùng nhúng trong ARN của mô hình (inference profile) thắng tất cả, để một AWS_REGION đặt cho dịch vụ khác không phá request; rồi tới region, AWS_REGION, AWS_DEFAULT_REGION; cuối cùng là us-east-1 khi không có profile nào.
  • Endpoint tuỳ biến. Một baseUrl không phải endpoint chuẩn của Bedrock (VPC endpoint, proxy) luôn được giữ; endpoint chuẩn chỉ được ghim khi không có vùng hay profile cấu hình. AWS_BEDROCK_SKIP_AUTH=1 gửi credential giả cho proxy không cần xác thực; AWS_BEDROCK_FORCE_HTTP1=1 cho endpoint không nói HTTP/2.
  • Proxy. Từ v3.798.0 SDK dùng handler HTTP/2 không hỗ trợ agent, nên khi có biến proxy, adapter thay bằng NodeHttpHandler với HttpProxyAgent/HttpsProxyAgent (mục mạng).

Google Vertex AI#

vertexAuth.resolve(): khoá API, hoặc ADC cộng project và location
pi/packages/ai/src/providers/google-vertex.tsdòng 64–89TypeScript
resolve: async ({ ctx, credential, signal }) => {	const env = async (name: string) => {		signal.throwIfAborted();		const value = await ctx.env(name);		signal.throwIfAborted();		return value;	};	const key = credential?.key ?? (await env("GOOGLE_CLOUD_API_KEY"));	if (key) return { auth: { apiKey: key }, source: credential?.key ? "stored credential" : "GOOGLE_CLOUD_API_KEY" };	const adcPath = credential?.env?.GOOGLE_APPLICATION_CREDENTIALS ?? (await env("GOOGLE_APPLICATION_CREDENTIALS"));	signal.throwIfAborted();	const hasCredentials = await ctx.fileExists(adcPath ?? VERTEX_ADC_PATH);	signal.throwIfAborted();	const project =		credential?.env?.GOOGLE_CLOUD_PROJECT ?? (await env("GOOGLE_CLOUD_PROJECT")) ?? (await env("GCLOUD_PROJECT"));	const location = credential?.env?.GOOGLE_CLOUD_LOCATION ?? (await env("GOOGLE_CLOUD_LOCATION"));	if (hasCredentials && project && location) {		return {			auth: {},			env: credential?.env,			source: credential ? "stored credential" : "gcloud application default credentials",		};	}	return undefined;},

Vertex nhận hoặc một khoá Google Cloud (GOOGLE_CLOUD_API_KEY), hoặc Application Default Credentials (ADC). Với ADC, provider chỉ xác nhận ba điều: có file credential (đường dẫn trong GOOGLE_APPLICATION_CREDENTIALS, hoặc file mặc định mà gcloud auth application-default login ghi), có project, có location. Adapter khi đó dựng new GoogleGenAI({ vertexai: true, project, location }), thêm googleAuthOptions.keyFilename nếu có file tài khoản dịch vụ, và để thư viện của Google tự lấy và làm mới token (google-vertex.ts:364). Khoá dạng giữ chỗ như <authenticated> bị bỏ qua thay vì gửi đi (google-vertex.ts:442).

Azure OpenAI và Microsoft Foundry#

Mỗi người dùng Azure có một resource riêng, nên catalog không thể chứa URL. Bộ sinh catalog chép các mô hình OpenAI sang provider azure với baseUrl: "" (và DeepSeek V4 Pro với giá riêng của Azure), còn URL được tính ở từng request (generate-models.ts:3340). Thứ tự: azureBaseUrl trong option, AZURE_OPENAI_BASE_URL, azureResourceName hoặc AZURE_OPENAI_RESOURCE_NAME (thành https://<tên>.openai.azure.com/openai/v1), rồi mới tới model.baseUrl; URL gốc của *.openai.azure.com, *.cognitiveservices.azure.com, *.ai.azure.com được chuẩn hoá về /openai/v1 (azure-openai-config.ts:72). Mô hình OpenAI đi qua azure-openai-responses; mô hình Foundry đi qua Chat Completions, bọc như sau:

azureStreams(): điền endpoint và đổi tên deployment trước khi gửi
pi/packages/ai/src/providers/azure.tsdòng 14–42TypeScript
/** Send the deployment name as the request's model, keeping `model.id` as the catalog id. */function withDeploymentName<T extends StreamOptions>(model: Model<Api>, options: T | undefined): T | undefined {	const deploymentName = resolveDeploymentName(model, options);	if (deploymentName === model.id) return options;	return {		...options,		onPayload: async (payload, payloadModel) => {			const params = { ...(payload as object), model: deploymentName };			return (await options?.onPayload?.(params, payloadModel)) ?? params;		},	} as T;}/** * Resolve the Azure endpoint and deployment before dispatch, inside `lazyStream` so an * unconfigured endpoint errors on the stream instead of throwing out of `stream()`. */function azureStreams(streams: ProviderStreams): ProviderStreams {	return {		stream: (model, context, options) =>			lazyStream(model, async () =>				streams.stream(resolveAzureModel(model, options), context, withDeploymentName(model, options)),			),		streamSimple: (model, context, options) =>			lazyStream(model, async () =>				streams.streamSimple(resolveAzureModel(model, options), context, withDeploymentName(model, options)),			),	};}

Hai mẹo nhỏ đáng học. Tên deployment được thay vào payload qua onPayload, nên model.id vẫn là id trong catalog (để tính giá, hiển thị, khôi phục session) còn máy chủ nhận tên deployment; ánh xạ đến từ azureDeploymentName hoặc AZURE_OPENAI_DEPLOYMENT_NAME_MAP=id=deployment,.... Và việc tính endpoint nằm trong lazyStream(), nên thiếu cấu hình thành một sự kiện error trên luồng chứ không ném ra khỏi stream().

Anthropic workload identity federation#

Từ 0.99.2, một máy chủ CI hay một pod không có khoá Anthropic vẫn gọi được API bằng cách đổi một identity token của nền tảng (OIDC) lấy token truy cập ngắn hạn (CHANGELOG 0.99.2). Provider xét nó cuối cùng, sau mọi khoá và ANTHROPIC_AUTH_TOKEN, và chỉ chuyển các id qua env vì chúng là cấu hình chứ không phải xác thực (anthropic.ts:49). Adapter dựng cấu hình federation cho SDK:

getAnthropicFederation(): cấu hình oidc_federation cho SDK Anthropic
pi/packages/ai/src/api/anthropic-messages.tsdòng 343–370TypeScript
/** * Workload identity federation config from the ANTHROPIC_* variables the * Anthropic SDK documents; the SDK performs the token exchange and refresh. * Only for the anthropic provider, since the exchange is an Anthropic API * endpoint, and only when no key or auth header was resolved. */function getAnthropicFederation(	model: Model<"anthropic-messages">,	apiKey: string | undefined,	headers: ProviderHeaders | undefined,	env: ProviderEnv | undefined,): AnthropicFederationConfig | undefined {	if (model.provider !== "anthropic" || hasRequestAuth(apiKey, headers)) return undefined;	const federationRuleId = getProviderEnvValue(ANTHROPIC_FEDERATION_RULE_ID_ENV, env);	const organizationId = getProviderEnvValue(ANTHROPIC_ORGANIZATION_ID_ENV, env);	const identityTokenFile = getProviderEnvValue(ANTHROPIC_IDENTITY_TOKEN_FILE_ENV, env);	if (!federationRuleId || !organizationId || !identityTokenFile) return undefined;	return {		organization_id: organizationId,		workspace_id: getProviderEnvValue(ANTHROPIC_WORKSPACE_ID_ENV, env),		authentication: {			type: "oidc_federation",			federation_rule_id: federationRuleId,			service_account_id: getProviderEnvValue(ANTHROPIC_SERVICE_ACCOUNT_ID_ENV, env),			identity_token: { source: "file", path: identityTokenFile },		},	};}

SDK của Anthropic thực hiện việc đổi token và tự làm mới, đọc lại file identity token mỗi lần, nên tài liệu nhắc giữ file đó luôn mới với phiên dài (docs/providers.md:69). Vì pi dựng một client cho mỗi request mà SDK cache token theo client, adapter giữ một client federation duy nhất cho mỗi cấu hình và nhân bản nó bằng withOptions(), cách dùng chung bộ cache token (anthropic-messages.ts:1088). Mọi client khác là PiAnthropic, lớp con tắt chuỗi tìm credential riêng của SDK: federation chỉ chạy khi chính pi đã quyết định dùng nó.

Cloudflare: REST, AI Gateway và AI binding trong Worker#

Cloudflare có hai provider, và cả hai gặp cùng một vấn đề với catalog: URL chứa id tài khoản (và id gateway) của từng người dùng. Catalog vì thế lưu URL có placeholder, ví dụ https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1 cho mô hình chat của Workers AI (api/cloudflare.ts), và lời giải chia làm hai nửa. Nửa thứ nhất là xác thực: resolve() lấy khoá, account id và gateway id, mỗi trường ưu tiên giá trị trong credential rồi mới tới môi trường, để một credential chỉ lưu khoá vẫn lấy được account id từ biến môi trường (cloudflare-auth.ts:10). Các id đi ra trong env của kết quả. Nửa thứ hai là một wrapper quanh API, thay placeholder bằng giá trị trong options.env ngay trước khi gửi:

cloudflareStreams(): điền id tài khoản và gateway vào URL lúc gửi
pi/packages/ai/src/providers/cloudflare-stream.tsdòng 6–28TypeScript
export function resolveCloudflareModel<TModel extends { baseUrl: string }>(	model: TModel,	env: ProviderEnv | undefined,): TModel {	if (!env) return model;	const baseUrl = model.baseUrl		.replaceAll(`{${CLOUDFLARE_ACCOUNT_ID}}`, env[CLOUDFLARE_ACCOUNT_ID] ?? `{${CLOUDFLARE_ACCOUNT_ID}}`)		.replaceAll(`{${CLOUDFLARE_GATEWAY_ID}}`, env[CLOUDFLARE_GATEWAY_ID] ?? `{${CLOUDFLARE_GATEWAY_ID}}`);	return baseUrl === model.baseUrl ? model : { ...model, baseUrl };}/** * Wrap an API implementation so Cloudflare account/gateway endpoint * placeholders materialize from the resolved provider env before dispatch. */export function cloudflareStreams(streams: ProviderStreams): ProviderStreams {	return {		stream: (model, context, options) =>			streams.stream(resolveCloudflareModel(model, options?.env), context, options),		streamSimple: (model, context, options) =>			streams.streamSimple(resolveCloudflareModel(model, options?.env), context, options),	};}

Thiết kế này giữ đúng ranh giới của ModelAuth: account id không phải xác thực, nên nó đi qua env, và chỉ phần API của provider biết URL được dựng thế nào. Changelog của 0.80.4, 0.80.7 và 0.80.8 ghi lại ba lần sửa lỗi khiến placeholder sống sót tới request và trả về 404, ví dụ khi credential lưu bằng /login chỉ có khoá (CHANGELOG 0.80.4).

Workers AI qua REST#

Mô hình chat của cloudflare-workers-ai nói Chat Completions ở endpoint /ai/v1 tương thích OpenAI, xác thực bằng khoá Cloudflare như một Bearer thông thường. Classifier (Jev của TypeSafe, Clef của Cloudflare) đi qua API REST gốc: POST /accounts/{account}/ai/run với { model, input }, nhận về phong bì { success, result } của Cloudflare. Adapter cloudflare-workers-ai-system-one chỉ khác typesafe-system-one ở cách bọc và bóc phong bì đó, kể cả hai hình dạng kết quả khác nhau giữa mô hình bên thứ ba và mô hình do Cloudflare host (cloudflare-workers-ai-system-one.ts:17).

AI Gateway qua HTTPS#

cloudflare-ai-gateway chọn passthrough theo hãng gốc: mô hình OpenAI đi tới .../{gateway}/openai bằng Responses, mô hình Anthropic tới .../anthropic bằng Messages (id được đổi dấu chấm thành gạch ngang vì Anthropic chỉ nhận dạng đó), mô hình workers-ai/* tới .../compat bằng Chat Completions (generate-models.ts:1988). Khoá Cloudflare xác thực Pi với gateway, không phải với hãng gốc:

cloudflareAIGatewayAuth(): khoá đi trong cf-aig-authorization, header của SDK bị xoá
pi/packages/ai/src/providers/cloudflare-auth.tsdòng 87–101TypeScript
resolve: async ({ ctx, credential, signal }) => {	const resolved = await resolveCloudflareEnv("ai-gateway", ctx, credential, signal);	if (!resolved) return undefined;	return {		auth: {			headers: {				"cf-aig-authorization": `Bearer ${resolved.apiKey}`,				Authorization: null,				"x-api-key": null,			},		},		env: resolved.env,		source: resolved.source,	};},

Giá trị null xoá header mà SDK tự đặt (Authorization của SDK openai, x-api-key của SDK Anthropic). Nếu để lại, gateway sẽ coi chúng là khoá của chính người dùng gửi cho hãng gốc (bring your own key) và dùng thay cho khoá đã lưu trong gateway. Việc xác thực với hãng gốc vì thế thuộc về gateway: tính tiền hợp nhất của Cloudflare, khoá lưu trong gateway, hoặc một header Authorization mà người dùng cấu hình trong models.json (docs/providers.md:212).

AI binding: khi code chạy bên trong một Worker#

Gọi gateway qua HTTPS đòi một token API của Cloudflare, kể cả khi người gọi là một Worker nằm trong chính tài khoản đó. Một Worker có cách khác: binding env.AI có phương thức fetch() phục vụ passthrough của gateway ở https://workers-binding.ai/ai-gateway/gateways/{gateway}/{provider}/..., cùng hình dạng với URL HTTPS nhưng không có account id, vì kênh binding đã mang danh tính (cloudflare-ai-binding.ts:1). Phản hồi là định dạng gốc của hãng, nên mọi adapter chạy y như qua HTTPS. pi-ai chỉ thêm một hàm:

createAiBindingFetch(): một fetch đi qua env.AI
pi/packages/ai/src/api/cloudflare-ai-binding.tsdòng 42–45, 57, 80–90TypeScript
export interface AiBinding {	aiGatewayLogId: string | null;	fetch?(input: Request | string | URL, init?: RequestInit): Promise<Response>;}export const CLOUDFLARE_GATEWAY_BINDING_AUTH_SENTINEL = "cloudflare-gateway-binding";export function createAiBindingFetch(binding: AiBinding): FetchFunction {	// `fetch` is optional on the type, so its presence is checked here — early, rather than as a	// confusing failure on the first inference request.	if (typeof binding.fetch !== "function") {		throw new TypeError("createAiBindingFetch: the AI binding does not expose fetch()");	}	// Bound eagerly: `fetch` is a mutable property, so the narrowing above would not survive into	// the returned closure.	const bindingFetch = binding.fetch.bind(binding);	return (input, init) => bindingFetch(input, init);}

Cách dùng không đi qua provider dựng sẵn nào: host tự khai báo một mô hình có baseUrl trỏ vào route của binding, truyền fetch: createAiBindingFetch(env.AI) trong option, và đặt cf-aig-authorization: Bearer cloudflare-gateway-binding cùng Authorization: null, x-api-key: null. Giá trị "sentinel" kia chỉ để qua được bước kiểm tra "có xác thực" của adapter (anthropic-messages.ts:316); gateway bỏ nó đi với request qua binding. Hàm thay cho createGatewayBindingFetch() cũ từ 0.85.0: thay vì viết lại request, nó chuyển nguyên phương thức, header và body dạng luồng tới binding (CHANGELOG 0.85.0). Hai adapter Gemini từ chối options.fetch tuỳ biến, nên đường này dùng được với các adapter dựa trên SDK openai và Anthropic.

Gateway và aggregator#

Với pi-ai, một gateway chỉ là một provider có catalog chứa mô hình của nhiều hãng. Không có lớp "gateway" nào trong code. Cái làm chúng khác provider thường là năm việc nhỏ, mỗi việc một chỗ trong code: chọn API theo từng mô hình, cá nhân hoá catalog theo khoá, header ghi nhận ứng dụng, header định tuyến theo phiên, và tuỳ chọn định tuyến tới hãng gốc.

OpenRouter: hai API trong một catalog#

OpenRouter phục vụ cả endpoint tương thích OpenAI lẫn endpoint tương thích Anthropic. Bộ sinh catalog cho mô hình anthropic/* (trừ biến thể :batch) đi đường Anthropic, còn mọi mô hình khác đi Chat Completions. Hệ quả là Claude qua OpenRouter dùng chung adapter với mọi mô hình Claude khác, kể cả điểm ngắt cache và suy nghĩ có chữ ký:

openrouter-catalog.ts: API và baseUrl chọn theo id mô hình
pi/packages/ai/scripts/openrouter-catalog.tsdòng 47–62TypeScript
const thinkingLevelMap = getOpenRouterThinkingLevelMap(model.reasoning);const useAnthropicMessages = /^anthropic\//.test(model.id) && !model.id.endsWith(":batch");chat.push({	type: "chat",	id: model.id,	name: model.name,	api: useAnthropicMessages ? "anthropic-messages" : "openai-completions",	baseUrl: useAnthropicMessages ? "https://openrouter.ai/api" : "https://openrouter.ai/api/v1",	provider: "openrouter",	reasoning: model.supported_parameters?.includes("reasoning") || false,	...(thinkingLevelMap && { thinkingLevelMap }),	input,	cost: openRouterCost(model.pricing),	contextWindow: model.top_provider?.context_length || model.context_length || 4096,	maxTokens: model.top_provider?.max_completion_tokens || 4096,});

Một mô hình anthropic/* của provider openrouter mà vẫn đi Chat Completions (biến thể :batch, hoặc một mục thêm vào openrouter trong models.json) vẫn được gắn cache_control kiểu Anthropic, vì detectCompat() đặt cacheControlFormat: "anthropic" cho nó (openai-completions.ts:1650). Trên đường Completions, mức suy nghĩ đi dạng reasoning: { effort } (thinkingFormat: "openrouter"). Trên cả hai đường, sessionId thành header x-session-id, để OpenRouter đưa các request cùng phiên về cùng nơi cho cache trúng (anthropic-messages.ts:218).

Catalog theo khoá#

Một khoá OpenRouter có thể bị giới hạn bởi guardrail của tổ chức: chỉ một số mô hình, chỉ một số hãng gốc, với giá và giới hạn riêng. pi-ai có hàm đọc đúng danh sách đó:

personalizeOpenRouterCatalog(): lọc catalog theo GET /models/user
pi/packages/ai/src/providers/openrouter.tsdòng 24–35, 46–59TypeScript
export async function personalizeOpenRouterCatalog(	catalog: readonly AnyModel[],	context: RefreshModelsContext,): Promise<readonly AnyModel[]> {	const key = context.credential?.type === "oauth" ? context.credential.access : context.credential?.key;	if (!key) return catalog;	const response = await fetch(`${(context.baseUrl ?? OPENROUTER_BASE_URL).replace(/\/+$/, "")}/models/user`, {		headers: { accept: "application/json", authorization: `Bearer ${key}` },		signal: AbortSignal.any([context.signal, AbortSignal.timeout(OPENROUTER_USER_MODELS_TIMEOUT_MS)]),	});	if (response.status === 401 || response.status === 403 || response.status === 404) return catalog;	if (!response.ok) throw new Error(`OpenRouter model list request failed: ${response.status}`);	return catalog.flatMap((model): AnyModel[] => {		if (!isModelType(model, "chat")) return [model];		const entry = allowed.get(model.id);		if (!entry) return [];		return [			{				...model,				// `||`: OpenRouter reports a missing limit as absent, null or 0, like the generator's fallback.				contextWindow: entry.top_provider?.context_length || entry.context_length || model.contextWindow,				maxTokens: entry.top_provider?.max_completion_tokens || model.maxTokens,				cost: entry.pricing ? openRouterCost(entry.pricing) : model.cost,			},		];	});

Mô hình chat không có trong danh sách của khoá bị bỏ; mô hình còn lại nhận cửa sổ ngữ cảnh, output tối đa và giá theo khoá. Mô hình ảnh và classifier giữ nguyên. Request dùng baseUrl đang cấu hình, nên endpoint vùng như https://us.openrouter.ai/api/v1 trả về danh sách đã lọc theo vùng; endpoint từ chối khoá hoặc không có route này (một proxy chẳng hạn) thì catalog giữ nguyên. Coding agent bọc provider bằng withPersonalizedCatalog(), lưu danh sách đã cá nhân hoá vào models-store.json để dùng khi ngoại tuyến, ở chế độ pi -p và RPC (docs/providers.md:116). Giá của OpenRouter tính theo USD mỗi token dạng chuỗi; openRouterCost() đổi sang USD mỗi triệu token, biến giá theo độ dài prompt thành các bậc giá, và bỏ qua giá theo giờ trong ngày vì ModelCost không biểu diễn được (openrouter-api.ts:45).

Header ghi nhận ứng dụng (attribution)#

OpenRouter có bảng xếp hạng ứng dụng, NVIDIA và OpenCode ghi nhận ứng dụng gọi tới. Header tương ứng do identity của request quyết định (chương pi-ai, danh tính của client). Một chi tiết quan trọng là cách nhận ra đích: theo id provider hoặc theo host của baseUrl, nên một provider tự định nghĩa trỏ vào openrouter.ai cũng gửi đúng header.

Nhận ra gateway theo id hoặc theo host
pi/packages/ai/src/utils/identity.tsdòng 92–104TypeScript
function isOpenRouter(target: ClientIdentityTarget): boolean {	return target.provider === "openrouter" || target.baseUrl.includes("openrouter.ai");}function isNvidiaNim(target: ClientIdentityTarget): boolean {	return target.provider === "nvidia" || matchesHost(target.baseUrl, NVIDIA_NIM_HOST);}function isOpenCode(target: ClientIdentityTarget): boolean {	return (		target.provider === "opencode" || target.provider === "opencode-go" || matchesHost(target.baseUrl, OPENCODE_HOST)	);}

Coding agent không gửi attribution vô điều kiện. Nó gắn PI_ATTRIBUTION chỉ khi install telemetry đang bật, đọc lại ở mỗi request:

getClientIdentity(): attribution đi theo cài đặt telemetry
pi/packages/coding-agent/src/core/client-identity.tsdòng 6–16TypeScript
/** * How pi's requests identify it to providers: a detailed `User-Agent` with pi's version, and attribution to pi as a * CLI agent while install telemetry is enabled. */export function getClientIdentity(settingsManager: SettingsManager): ClientIdentity {	return {		userAgent: "detailed",		version: VERSION,		attribution: isInstallTelemetryEnabled(settingsManager) ? PI_ATTRIBUTION : null,	};}

enableInstallTelemetry: false trong settings hoặc PI_TELEMETRY=0 tắt cả báo cáo cài đặt lẫn header ghi nhận (docs/settings.md:170); User-Agent chi tiết vẫn được gửi.

Vercel AI Gateway, OpenCode, định tuyến tới hãng gốc#

  • Vercel AI Gateway có một endpoint tương thích Anthropic cho mọi hãng, nên provider chỉ cần một API cho toàn bộ catalog, và đánh dấu allowEmptySignature cho các mô hình của nó (generate-models.ts:1432). Classifier Jev đi qua endpoint System One của gateway ở /typesafe/v1.
  • OpenCode Zen và Go chọn API theo gói SDK mà models.dev ghi cho từng mô hình (@ai-sdk/anthropic thành Messages, @ai-sdk/openai thành Responses, @ai-sdk/google thành Gemini, còn lại là Completions), có sửa tay những chỗ models.dev ghi sai (generate-models.ts:2234). Header x-opencode-session là bắt buộc để OpenCode định tuyến hội thoại; nó được chèn ở hai tầng: wrapper của provider dựng sẵn, và tầng runtime (Models.applyAuth() của pi-ai, prepareRequest() của coding agent), tầng sau phủ cả provider tự định nghĩa trỏ vào opencode.ai (identity.ts:126).
  • Định tuyến tới hãng gốc là cờ compat: openRouterRouting thành trường provider của request (danh sách hãng, cho phép fallback, chính sách thu thập dữ liệu...), vercelGatewayRouting thành providerOptions.gateway với only và order (openai-completions.ts:993). Đặt chúng trong models.json bằng modelOverrides hoặc compat cấp provider.

Radius: gateway nói giao thức của Pi#

Radius là gateway do Earendil làm cho Pi, có kiểm soát theo tổ chức và thống kê (docs/providers.md:104). Nó nói pi-messages, nên client không cần SDK của hãng nào. Provider được viết tay vì catalog của nó khác mọi provider khác: chủ tổ chức có thể tắt mô hình, nên một khi đã biết catalog của tài khoản (từ GET /v1/config của gateway), catalog đó thay thế bản dựng sẵn thay vì phủ lên nó; bản dựng sẵn chỉ dùng khi chưa có gì (radius.ts:21). Một gateway Radius riêng được khai báo trong models.json bằng "oauth": "radius" cộng baseUrl; coding agent dựng một radiusProvider() mới cho id đó, với đăng nhập OAuth và catalog trỏ vào gateway ấy (model-runtime.ts:271).

Một mô hình, nhiều đường#

Vì gateway và nền tảng suy luận bán lại mô hình của hãng khác, cùng một mô hình gốc thường có nhiều mục trong catalog, mỗi mục một id, một API, một giá và một giới hạn. Lấy Kimi K2.6 của Moonshot: bảng mô hình mặc định của coding agent đã cho thấy ít nhất bảy đường (model-resolver.ts:20):

Tham chiếu provider/idĐi quaCredential
moonshotai/kimi-k2.6API của hãng, api.moonshot.aiMOONSHOT_API_KEY
moonshotai-cn/kimi-k2.6API của hãng ở Trung Quốc, api.moonshot.cnMOONSHOT_API_KEY
openrouter/moonshotai/kimi-k2.6OpenRouter, rồi một hãng gốc do OpenRouter chọnOPENROUTER_API_KEY
huggingface/moonshotai/Kimi-K2.6Router của Hugging FaceHF_TOKEN
opencode/kimi-k2.6OpenCode ZenOPENCODE_API_KEY
cloudflare-workers-ai/@cf/moonshotai/kimi-k2.6Workers AI của Cloudflarekhoá và account id Cloudflare
cloudflare-ai-gateway/workers-ai/@cf/moonshotai/kimi-k2.6AI Gateway, rồi Workers AIkhoá, account id, gateway id

Với Claude, các đường khác nhau cả ở API: anthropic gọi thẳng bằng Messages, amazon-bedrock bằng ConverseStream (id dạng us.anthropic.claude-...), còn github-copilot, openrouter, vercel-ai-gateway và cloudflare-ai-gateway đều nói Messages tới endpoint tương thích của mình, theo các luật chọn API đã thấy ở trên. Session của Pi lưu cả provider lẫn model, nên đổi đường giữa hội thoại là một lần chuyển giao giữa hai mô hình khác nhau theo nghĩa của transformMessages(): suy nghĩ của đường cũ bị bỏ (chương pi-ai).

Mô hình cục bộ và tự host#

Có hai đường cho mô hình chạy trên máy của mình. Với llama.cpp, coding agent có sẵn một provider biết router của nó. Với mọi máy chủ khác nói một API có sẵn (Ollama, LM Studio, vLLM, SGLang, một proxy), người dùng khai báo nó trong models.json (docs/models.md:37).

Extension dựng sẵn llama.cpp#

Provider này không nằm trong pi-ai. Nó là một trong bốn extension dựng sẵn của coding agent (extensions/index.ts:7), đăng ký một Provider hoàn chỉnh qua pi.registerProvider() và một lệnh /llama để nạp, gỡ, tải mô hình về router (llama/index.ts:42). Router của llama.cpp phát hiện các file GGUF trong một thư mục và nạp chúng theo yêu cầu, nên danh mục mô hình của provider là danh mục động của router chứ không phải một catalog sinh sẵn (docs/llama-cpp.md).

Xác thực của llama.cpp: URL máy chủ là cấu hình, khoá là tuỳ chọn
pi/packages/coding-agent/src/extensions/llama/provider.tsdòng 187–189, 204–226TypeScript
apiKey: {	name: "llama.cpp server",	login: async (interaction): Promise<ApiKeyCredential> => {		await new LlamaClient(serverUrl, apiKey || undefined).list({ signal: interaction.signal });		return {			type: "api_key",			key: apiKey || undefined,			env: { LLAMA_BASE_URL: serverUrl },		};	},	check: async ({ ctx, credential }) => {		const serverUrl = await resolveServerUrl(ctx, credential);		return serverUrl			? { type: "api_key", source: credential ? "stored credential" : "LLAMA_BASE_URL" }			: undefined;	},	resolve: async ({ ctx, credential }): Promise<AuthResult | undefined> => {		const serverUrl = await resolveServerUrl(ctx, credential);		if (!serverUrl) return undefined;		const apiKey = credential?.key ?? (await ctx.env("LLAMA_API_KEY")) ?? "local";		return {			auth: { apiKey, baseUrl: llamaInferenceUrl(serverUrl) },			env: { ...credential?.env, LLAMA_BASE_URL: serverUrl },			source: credential ? "stored credential" : "LLAMA_BASE_URL",		};	},
  • /login llama.cpp hỏi URL và một khoá tuỳ chọn, gọi thử GET /models để chắc máy chủ ở chế độ router, rồi lưu URL vào env.LLAMA_BASE_URL của credential. Không đăng nhập thì LLAMA_BASE_URL và LLAMA_API_KEY trong môi trường cũng đủ.
  • resolve() trả cả baseUrl, nên URL thật của request được quyết định lúc phân giải xác thực, cùng cơ chế mà Copilot dùng cho endpoint theo tài khoản. Khoá mặc định là chuỗi "local", vì SDK openai không chấp nhận khoá rỗng.
  • check() chỉ xem đã có URL hay chưa, không gọi mạng, nên việc liệt kê mô hình dùng được không chạm tới máy chủ.

refreshModels() đọc danh sách của router. Mô hình chọn được là mô hình đang nạp, đang ngủ (request sẽ đánh thức nó), hoặc preset chưa nạp khi router bật tự nạp. Với mỗi mô hình đang nạp, provider gọi thêm /props để đọc chat template và cấu hình thật của nó:

toPiModel(): một mô hình của router thành một mô hình Completions
pi/packages/coding-agent/src/extensions/llama/provider.tsdòng 127–158TypeScript
function toPiModel(	model: LlamaModelInfo,	serverUrl: string,	props?: LlamaServerProps,	cachedContextWindow?: number,): Model<"openai-completions"> {	const contextWindow = contextWindowOf(model, cachedContextWindow);	const reasoning = props?.chat_template?.includes("enable_thinking") === true;	return {		id: model.id,		name: model.id,		api: "openai-completions",		provider: LLAMA_PROVIDER_ID,		baseUrl: llamaInferenceUrl(serverUrl),		reasoning,		...(reasoning && {			thinkingLevelMap: { off: "off", minimal: null, low: null, medium: "medium", high: null, xhigh: null },		}),		input: model.architecture?.input_modalities?.includes("image") ? ["text", "image"] : ["text"],		cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },		contextWindow,		maxTokens: contextWindow,		compat: {			supportsStore: false,			supportsDeveloperRole: false,			supportsReasoningEffort: false,			supportsUsageInStreaming: true,			supportsStrictMode: false,			maxTokensField: "max_tokens",			...(reasoning && { thinkingFormat: "qwen-chat-template" }),		},	};

Mô hình được coi là có suy nghĩ khi chat template chứa enable_thinking, và khi đó mức suy nghĩ đi qua chat_template_kwargs (thinkingFormat: "qwen-chat-template") với đúng hai mức off và medium. Cửa sổ ngữ cảnh lấy theo thứ tự: n_ctx lúc chạy, cờ --ctx-size trong tham số của mô hình, giá trị đã cache, n_ctx_train, cuối cùng là 128.000 (provider.ts:71). Các cờ compat đều là khác biệt đã kiểm chứng của llama-server: không có store, không có vai developer, không có reasoning_effort, dùng max_tokens. Mỗi mô hình còn được liệt kê làm classifier: mô hình quyết định (router báo decisions trong output_modalities, từ llama.cpp 0.6.0) đi qua endpoint System One gốc của llama.cpp, mô hình chat đi qua llama-cpp-classify, đọc xác suất của nhãn đáp án ở token kế tiếp (docs/llama-cpp.md:89).

Phần stream của provider gọi stream và streamSimple của @earendil-works/pi-ai/compat, tức API toàn cục chọn adapter theo model.api (provider.ts:288). Lệnh /llama chỉ có trong TUI; sau mỗi lần nạp, gỡ hay tải, nó làm mới catalog của riêng provider này với allowNetwork: true, kể cả khi PI_OFFLINE được đặt, vì người dùng vừa chủ động liên lạc với máy chủ (llama/index.ts:46). Không muốn provider này thì tắt nó bằng "extensions": ["-builtin:llama.cpp"]. Tài liệu nvidia-rtx-llama-cpp.md hướng dẫn đổi một lệnh llama-server tối ưu cho GPU NVIDIA thành một preset của router.

Ollama, LM Studio, vLLM và máy chủ tương thích khác#

Những máy chủ này đi qua models.json với "api": "openai-completions", một baseUrl và danh sách mô hình khai báo tay. Không có bước phát hiện mô hình: muốn danh sách động như llama.cpp thì phải viết một extension có refreshModels. Ví dụ trong tài liệu dùng một khoá giả "ollama": Ollama bỏ qua nó, nhưng không có khoá thì provider bị coi là chưa cấu hình và mô hình không hiện trong /model (docs/models.md:45). Lần theo cách khai báo đó tới request ở trace thứ hai.

Với một máy chủ lạ, detectCompat() không nhận ra gì và trả về mặc định của chính OpenAI: gửi store: false, vai developer cho mô hình có suy nghĩ, reasoning_effort, max_completion_tokens. README ghi rằng Ollama, vLLM và SGLang thường cần tắt supportsDeveloperRole và supportsReasoningEffort (README). Các cờ dành cho máy chủ tự host gồm: thinkingFormat "qwen", "qwen-chat-template" hay "chat-template" với chatTemplateKwargs chứa biến $var do Pi điền; thinkingTokenBudgetField để giới hạn token suy nghĩ (thinking_token_budget của vLLM, thinking_budget của Qwen và SGLang, thinking_budget_tokens của llama.cpp); vllmPriority cho bộ lập lịch ưu tiên của vLLM (docs/models-configuration.md:123). Tài liệu nhấn mạnh một nguyên tắc: chỉ bật cờ compat cho khác biệt đã kiểm chứng, không bật chỉ vì máy chủ quảng cáo "tương thích OpenAI" (docs/models.md:140).

models.json và cách ghép với catalog dựng sẵn#

<agent-dir>/models.json là nơi người dùng sửa thế giới provider mà không viết code. Mỗi khoá dưới providers là một id provider: id của provider dựng sẵn thì sửa provider đó, id khác thì tạo provider mới, khi ấy cần baseUrl và một api ở cấp provider hoặc cấp mô hình (docs/models-configuration.md:9). Lược đồ viết bằng TypeBox, ghép lại các lược đồ mô hình và compat của pi-ai:

ProviderConfigSchema: mọi trường của một provider trong models.json
pi/packages/coding-agent/src/core/model-config.tsdòng 89–115TypeScript
const ProviderConfigSchema = Type.Object({	name: Type.Optional(Type.String({ minLength: 1, description: "Display name." })),	baseUrl: Type.Optional(		Type.String({ minLength: 1, description: "API base URL. Also applies to the provider's built-in models." }),	),	apiKey: Type.Optional(		Type.String({			minLength: 1,			description: 'Literal key, "$NAME" environment interpolation, or "!command" output.',		}),	),	api: Type.Optional(		Type.String({			minLength: 1,			description:				'API for the provider\'s models, such as "openai-completions", "openai-responses", "anthropic-messages", or "google-generative-ai".',		}),	),	oauth: Type.Optional(Type.Literal("radius", { description: "OAuth login flow. Requires baseUrl." })),	headers: Type.Optional(HeadersSchema),	compat: Type.Optional(ProviderCompatSchema),	authHeader: Type.Optional(		Type.Boolean({ default: false, description: "Whether to send the resolved API key as Authorization: Bearer." }),	),	models: Type.Optional(Type.Array(ModelDefinitionSchema)),	modelOverrides: Type.Optional(Type.Record(Type.String(), ModelOverrideSchema)),});

ModelConfig.load() bỏ comment và BOM, kiểm tra cả file bằng validator đã biên dịch, rồi đóng băng sâu kết quả. File hỏng không làm Pi dừng: cấu hình rỗng được dùng, lỗi được giữ lại để hiện cho người dùng (model-config.ts:168). File được đọc lại ở mỗi ModelRuntime.refresh(), ví dụ mỗi lần mở /model, nên sửa xong không cần khởi động lại.

Ghép theo từng provider#

Nguyên tắc đầu tiên của ModelRuntime: không có lớp phủ thì dùng nguyên provider gốc, để đăng nhập, xác thực và stream của nó chạy đúng như pi-ai định nghĩa.

composeProvider(): native provider của extension, rồi provider dựng sẵn, rồi mới ghép
pi/packages/coding-agent/src/core/model-runtime.tsdòng 309–325TypeScript
private composeProvider(providerId: string): Provider | undefined {	const base = this.nativeExtensionProviders.get(providerId) ?? this.builtins.get(providerId);	const extension = this.extensionProviders.get(providerId);	if (!this.config.getProvider(providerId) && !extension) {		// No overlays: use the builtin untouched so its auth/login/stream behavior is exact.		this.compositionErrors.delete(providerId);		return base;	}	try {		const provider = composeModelProvider(providerId, base, this.config, extension);		this.compositionErrors.delete(providerId);		return provider;	} catch (error) {		this.compositionErrors.set(providerId, error instanceof Error ? error.message : String(error));		return base;	}}

Ghép thất bại (ví dụ một mô hình mới thiếu api) thì lỗi được ghi theo provider và provider gốc vẫn chạy. Khi có lớp phủ, composeModelProvider() dựng một provider mới quanh provider gốc. Danh sách mô hình của nó được tính lại ở mỗi lần đọc, theo thứ tự: mô hình gốc, models.json, mô hình của extension, phép chiếu OAuth cũ của extension, cuối cùng là modelOverrides:

applyModelsJson(): baseUrl và compat phủ lên mô hình gốc, models thêm hoặc thay theo id
pi/packages/coding-agent/src/core/provider-composer.tsdòng 327–343TypeScript
const models: AnyModel[] = baseModels.map((model) => {	const baseUrl = config.oauth === "radius" ? model.baseUrl : (config.baseUrl ?? model.baseUrl);	if (isModelType(model, "chat")) return { ...model, baseUrl, compat: mergeCompat(model.compat, config.compat) };	// Classifier APIs read their own compat fields, such as how a System One service takes images.	if (isModelType(model, "classifier") && config.compat) {		return { ...model, baseUrl, compat: { ...model.compat, ...config.compat } };	}	return { ...model, baseUrl };});for (const definition of config.models ?? []) {	const existingIndex = models.findIndex((model) => isModelType(model, "chat") && model.id === definition.id);	const defaults = findModelDefaults(models, definition.id, definition.api ?? config.api);	const model = modelFromJson(providerId, definition, config, defaults);	if (existingIndex >= 0) models[existingIndex] = model;	else models.push(model);}return models;
  • baseUrl cấp provider áp lên mọi mô hình gốc của provider, trừ khi là gateway Radius. Đây là cách đi qua một proxy mà vẫn giữ catalog và xác thực dựng sẵn: { "anthropic": { "baseUrl": "https://proxy.example.com/v1" } } (docs/models.md:69).
  • compat cấp provider gộp vào mọi mô hình chat (bốn cờ dạng object như openRouterRouting được gộp từng trường), và vào classifier, nơi các cờ System One sống.
  • Một mục models trùng id thì thay mô hình gốc, khác id thì được thêm. Trường bỏ trống lấy mặc định của modelFromJson(): cửa sổ 128.000 token, output 16.384, giá bằng 0, input chỉ chữ, không suy nghĩ (provider-composer.ts:212). api và baseUrl nếu thiếu thì lấy từ một mô hình gốc tương tự: cùng id, hoặc cùng API, hoặc mô hình Completions đầu tiên, cuối cùng là mô hình chat đầu tiên (provider-composer.ts:253).
  • modelOverrides là lớp trên cùng: sửa siêu dữ liệu của một mô hình có sẵn mà không thay danh sách; object như cost được gộp từng trường; id không tồn tại bị bỏ qua.

Phần stream của provider ghép chọn adapter theo ba bậc:

streamWith(): stream của extension, rồi của provider gốc, rồi registry toàn cục
pi/packages/coding-agent/src/core/provider-composer.tsdòng 587–608TypeScript
const supportsBaseApi = (model: Model<Api>) => base?.getModels().some((entry) => entry.api === model.api) ?? false;const streamWith = (	model: Model<Api>,	context: TranscriptContext,	options: StreamOptions | undefined,	simple: boolean,): AssistantMessageEventStream =>	lazyStream(model, async () => {		if (extension?.streamSimple && model.api === extension.api) {			return extension.streamSimple(model, context, options as SimpleStreamOptions);		}		if (base && supportsBaseApi(model)) {			return simple				? base.streamSimple(model, context, options as SimpleStreamOptions)				: base.stream(model, context, options);		}		const api = getApiProvider(model.api);		if (!api) throw new Error(`No API provider registered for api: ${model.api}`);		return simple			? api.streamSimple(model, context, options as SimpleStreamOptions)			: api.stream(model, context, options);	});

Bậc thứ ba cho phép một mô hình trong models.json dùng một API mà provider gốc không có. Ví dụ, thêm vào provider azure một mô hình Foundry với "api": "openai-completions" là trường hợp tài liệu Azure mô tả (docs/providers.md:161); một provider hoàn toàn mới thì không có provider gốc, nên luôn đi bậc này.

Giá trị lấy từ môi trường hay từ lệnh#

apiKey và mọi giá trị header trong models.json có cùng cú pháp: chữ thường là giá trị thật; $TÊN hoặc ${TÊN} là biến môi trường, chen được giữa chữ khác ("Bearer $TOKEN"); !lệnh lấy stdout của lệnh shell; $$ và $! cho ký tự $ và ! thật (resolve-config-value.ts:138). Khác với auth.json, giá trị của models.json được phân giải ở mỗi request và không cache, qua resolveConfigValueOrThrow(); biến thiếu thì request thất bại với thông báo nêu đúng tên biến (resolve-config-value.ts:229). Biến được tra trong env của credential trước rồi mới tới môi trường của tiến trình, nên một header "X-Account: $CLOUDFLARE_ACCOUNT_ID" nhận được giá trị lưu trong auth.json.

Các lớp catalog và models-store.json#

Trước khi models.json được ghép, mỗi provider dựng sẵn đã được bọc một lớp catalog: withRemoteCatalog() cho mọi provider trừ Radius và OpenRouter, withPersonalizedCatalog() cho OpenRouter:

ModelRuntime.create(): kho credential, models.json, models-store.json và lớp catalog
pi/packages/coding-agent/src/core/model-runtime.tsdòng 218–241TypeScript
static async create(options: CreateModelRuntimeOptions = {}): Promise<ModelRuntime> {	const credentials = new RuntimeCredentials(options.credentials ?? DefaultAuthStorage.create(options.authPath));	const modelsPath =		options.modelsPath === null ? undefined : (options.modelsPath ?? join(getAgentDir(), "models.json"));	const config = await ModelConfig.load(modelsPath);	const modelsStore =		options.modelsStore ??		(modelsPath			? new FileModelsStore(options.modelsStorePath ?? join(dirname(modelsPath), "models-store.json"))			: new InMemoryCodingAgentModelsStore());	const builtinModelDataGeneratedAt = builtinProviderCatalog.getBuiltinModelDataGeneratedAt();	const providers = builtinProviderCatalog.builtinProviders().map((provider) => {		if (provider.id === "radius") return provider;		// OpenRouter narrows the catalog to the models, limits and prices of the user's key.		if (provider.id === "openrouter") {			return withPersonalizedCatalog(				provider,				personalizeOpenRouterCatalog,				options.catalogBaseUrl,				builtinModelDataGeneratedAt,			);		}		return withRemoteCatalog(provider, options.catalogBaseUrl, builtinModelDataGeneratedAt);	});

Cơ chế của lớp phủ pi.dev (kiểm tra tối đa bốn giờ một lần, ETag, chỉ dùng khi mới hơn dữ liệu đóng gói) đã ở chương pi-ai. Mọi catalog động được lưu chung trong models-store.json cạnh models.json: lớp phủ pi.dev, danh sách đã cá nhân hoá của OpenRouter, catalog của llama.cpp và của Radius. FileModelsStore dùng lại đúng backend có khoá file của auth.json (models-store.ts:46). Lúc khởi động, runtime chỉ khôi phục các catalog này từ đĩa, không gọi mạng (allowModelNetwork mặc định là false), để Pi mở nhanh và chạy được khi ngoại tuyến; chế độ interactive làm mới qua mạng ở nền, và PI_OFFLINE tắt hẳn việc đó (model-runtime.ts:248).

Provider do extension đăng ký#

Khi một dịch vụ cần đăng nhập riêng, danh mục động hay một giao thức chưa có, provider được viết trong một extension. Hai dạng đăng ký, thời điểm áp dụng và ngữ nghĩa "có models thì thay, chỉ có baseUrl thì giữ" đã được trình bày ở chương Extensions. Ở đây là vị trí của chúng trong chuỗi ghép đã thấy ở mục trước:

  • Native provider, pi.registerProvider(provider): một Provider hoàn chỉnh của pi-ai. Nó trở thành provider gốc của id đó, thay provider dựng sẵn cùng id, và models.json vẫn ghép được lên trên nó (model-runtime.ts:899). llama.cpp đi đường này.
  • ProviderConfig theo tên, pi.registerProvider("id", config): một lớp nằm sau models.json trong getAllModels(). Đăng ký lại cùng tên thì gộp các trường đã định nghĩa lên lần trước. OAuth kiểu cũ (login(callbacks), refreshToken, getApiKey) được adaptOAuth() dịch sang OAuthAuth, nối các callback cũ onAuth, onDeviceCode, onPrompt vào notify() và prompt() (provider-composer.ts:358). Khai báo streamSimple thì phải khai báo api, vì đó là khoá để streamWith() nhận ra mô hình nào đi stream tự viết.

Ủy quyền cho adapter có sẵn#

Tài liệu khuyên dùng lại adapter của pi-ai bất cứ khi nào giao thức khớp, vì như vậy giữ được việc đổi message, xử lý tool, tính usage, huỷ và compat (docs/custom-provider.md:162). Ví dụ GitLab Duo làm đúng như thế: nó tự lo đăng nhập GitLab và đổi token GitLab lấy một token truy cập trực tiếp ngắn hạn của AI Gateway của GitLab, rồi giao phần còn lại cho adapter Anthropic hoặc OpenAI Responses:

GitLab Duo: xác thực riêng, stream của pi-ai
const gitlabAccessToken = options?.apiKey;if (!gitlabAccessToken) throw new Error("No GitLab access token. Run /login gitlab-duo or set GITLAB_TOKEN");const cfg = MODEL_MAP.get(model.id);if (!cfg) throw new Error(`Unknown model: ${model.id}`);const directAccess = await getDirectAccessToken(gitlabAccessToken);const modelWithBaseUrl = { ...model, baseUrl: cfg.baseUrl };const headers = { ...directAccess.headers, Authorization: `Bearer ${directAccess.token}` };const streamOptions = { ...options, apiKey: "gitlab-duo", headers };const innerStream =	cfg.backend === "anthropic"		? anthropicMessagesApi().streamSimple(				{					...(modelWithBaseUrl as Model<"anthropic-messages">),					compat: {						...(modelWithBaseUrl as Model<"anthropic-messages">).compat,						forceAdaptiveThinking: true,					},				},				context,				streamOptions,			)		: openAIResponsesApi().streamSimple(				modelWithBaseUrl as Model<"openai-responses">,				context,				streamOptions,			);for await (const event of innerStream) stream.push(event);

Ba điều đáng học từ đoạn này. Credential mà Pi phân giải (token GitLab, từ /login gitlab-duo hoặc $GITLAB_TOKEN) chỉ là nguyên liệu; credential thật của request là token gateway, đi trong header. apiKey: "gitlab-duo" là giá trị giữ chỗ để qua bước kiểm tra "có xác thực" của adapter, vì header Authorization đã có. Và mô hình được sao ra với baseUrl của đúng backend trước khi giao, nên một provider có thể trộn mô hình của hai giao thức mà không cần bảng API.

Stream tự viết#

Ví dụ custom-provider-anthropic tự hiện thực cả luồng bằng SDK Anthropic, dưới một API tự đặt custom-anthropic-api. Hợp đồng mà một stream tự viết phải giữ được tài liệu liệt kê thành sáu bước: tạo message với stopReason: "pending" và usage bằng 0, phát start sau khi request đã được chấp nhận, phát các khối cân bằng, chốt usage và chi phí, phát đúng một sự kiện cuối, đổi việc huỷ thành aborted (docs/custom-provider.md:172). Đây chính là giao thức luồng của pi-ai, cộng thêm ba móc đo đạc onPayload, onResponse, onProviderStreamEvent.

Mạng: proxy, base URL và fetch tuỳ biến#

Proxy#

Coding agent cài một dispatcher toàn cục của undici ngay khi khởi động, nên mọi lời gọi fetch trong tiến trình, kể cả của SDK openai và Anthropic, đi qua nó:

http-dispatcher.ts: cài đặt httpProxy thành biến môi trường, EnvHttpProxyAgent làm dispatcher
pi/packages/coding-agent/src/core/http-dispatcher.tsdòng 45–50, 81–100TypeScript
export function applyHttpProxySettings(httpProxy: string | undefined): void {	const proxy = httpProxy?.trim();	if (!proxy) return;	process.env.HTTP_PROXY ??= proxy;	process.env.HTTPS_PROXY ??= proxy;}export function configureHttpDispatcher(timeoutMs: number = DEFAULT_HTTP_IDLE_TIMEOUT_MS): void {	const normalizedTimeoutMs = parseHttpIdleTimeoutMs(timeoutMs);	if (normalizedTimeoutMs === undefined) {		throw new Error(`Invalid HTTP idle timeout: ${String(timeoutMs)}`);	}	const dispatcher = withUndiciErrorListener(		new undici.EnvHttpProxyAgent({			allowH2: false,			// Keep HTTP origins on CONNECT tunnels as they were before Undici 8.7.			proxyTunnel: true,			bodyTimeout: normalizedTimeoutMs,			connect: {				autoSelectFamilyAttemptTimeout: DEFAULT_AUTO_SELECT_FAMILY_ATTEMPT_TIMEOUT_MS,			},			headersTimeout: normalizedTimeoutMs,			clientFactory: createUndiciClient,			factory: createUndiciOriginDispatcher,		}),	);	undici.setGlobalDispatcher(dispatcher);

EnvHttpProxyAgent đọc HTTP_PROXY, HTTPS_PROXY và NO_PROXY. Cài đặt httpProxy (chỉ đặt được trong settings của agent-directory) được chép vào hai biến đó bằng ??=, nên biến môi trường đã có thì thắng (docs/settings.md:125). Cùng dispatcher đặt thời gian chờ header và body theo httpIdleTimeoutMs (mặc định năm phút, 0 là tắt), và tắt HTTP/2.

Có hai chỗ không đi qua fetch, và pi-ai tự lo proxy ở đó bằng resolveHttpProxyUrlForTarget(): đọc http_proxy, https_proxy, all_proxy (chữ thường hoặc hoa, cả trong options.env), so khớp no_proxy theo miền và cổng, và từ chối proxy SOCKS hay PAC (node-http-proxy.ts:118). Chỗ thứ nhất là Bedrock, vì SDK AWS dùng handler HTTP riêng (mục đám mây ở trên). Chỗ thứ hai là WebSocket của Codex khi chạy trên Bun, vì WebSocket của Bun bỏ qua biến proxy (openai-codex-responses.ts:1016).

Base URL#

URL của một request là model.baseUrl, nhưng giá trị đó có thể bị thay ở năm chỗ, theo thứ tự từ cấu hình tới lúc gửi: baseUrl của mục mô hình trong models.json; baseUrl cấp provider trong models.json hay trong registerProvider(), áp cho mọi mô hình của provider; baseUrl mà xác thực trả về (Copilot, llama.cpp), được applyAuth() hay prepareRequest() chép đè vào mô hình; wrapper của provider (placeholder của Cloudflare, endpoint của Azure); và cuối cùng là logic riêng của adapter, ví dụ Bedrock chỉ ghim endpoint chuẩn khi chưa cấu hình vùng, Vertex bỏ qua URL còn chứa {location}.

fetch tuỳ biến#

options.fetch thay hàm fetch cho một request: hữu ích để chạy qua binding của Cloudflare, để ghi log, hay trong test. Adapter dựa trên SDK openai và Anthropic, adapter Mistral, pi-messages và các classifier truyền nó xuống. Hai adapter Gemini từ chối thẳng một fetch khác globalThis.fetch thay vì lặng lẽ bỏ qua nó, và Bedrock đi qua handler của SDK AWS nên không có khái niệm này.

Phía Pi Durable: host tự chọn provider#

Pi Durable (thử nghiệm) không có auth.json, models.json hay lớp phủ pi.dev nào. Harness nhận một Models, hoặc một hàm trả về Models, do host dựng (chương pi-ai, HarnessOptions). Host chọn provider một cách tường minh:

README của Pi Durable: đúng một provider, chọn bằng tay
pi/packages/durable/README.mddòng 49–60TypeScript
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";import { createModels } from "@earendil-works/pi-ai/models";import { openaiProvider } from "@earendil-works/pi-ai/providers/openai";import { AssistantEntry, createRegistry, Harness, MemoryStorage } from "@earendil-works/pi-durable";const context = BACKGROUND_CONTEXT;const models = createModels();models.setProvider(openaiProvider()); // reads OPENAI_API_KEYconst harness = await Harness.open(new MemoryStorage(), { models, registry: createRegistry() }, context);const root = await harness.root(context, { agent: { model: { provider: "openai", modelId: "gpt-6-sol" } } });

Đường import @earendil-works/pi-ai/models chỉ có runtime, không TypeBox, không catalog, không SDK; providers/openai chỉ kéo theo catalog của OpenAI và lazy wrapper của nó. Một Worker hay một ứng dụng trình duyệt vì thế chỉ trả giá cho đúng provider nó dùng. Xác thực ở đây là của pi-ai thuần: không truyền credentials thì kho credential là kho trong bộ nhớ, và khoá đến từ biến môi trường qua AuthContext mặc định; host muốn lưu token OAuth thì tiêm một CredentialStore bền của mình.

Conversation không lưu object mô hình mà lưu một tham chiếu { provider, modelId } (harness/types.ts:50), phân giải lại qua runtime.models.getModel() ở mỗi lượt sinh; tham chiếu không còn phân giải được thì input của lượt chạy được kết thúc là unanswered với lý do không có mô hình (generation.ts:218). Nhờ vậy host đổi được cấu hình provider (thêm khoá, đổi sang gateway) mà không phải sửa dữ liệu đã lưu. HarnessOptions.identity gộp lên DEFAULT_HARNESS_IDENTITY, tức attribution của pi theo mặc định; host đặt attribution của ứng dụng mình hoặc { attribution: null } (README).

Mô hình ảnh và classifier qua cùng provider#

Mô hình ảnh và classifier dùng chung provider, xác thực và catalog với mô hình chat; chỉ khác type và bảng thao tác (images, classifiers) trong createProvider(). Một OPENROUTER_API_KEY vì vậy phục vụ cả ba loại. Bản đồ ở v1.4.0:

LoạiAPIProvider
Sinh ảnhopenrouter-imagesopenrouter (danh sách lấy từ listing output_modalities=image của OpenRouter)
Classifier System Onetypesafe-system-onetypesafe, openrouter, vercel-ai-gateway, opencode; mô hình quyết định của llama.cpp
Classifier trên Workers AIcloudflare-workers-ai-system-onecloudflare-workers-ai (Jev, Clef)
Classifier Decisionsopenai-decisionsopenai, chỉ với khoá API
Mô hình chat làm classifierllama-cpp-classifyllama.cpp

Cùng một giao thức System One được phục vụ ở các đường dẫn khác nhau tuỳ dịch vụ, và mỗi dịch vụ nhận ảnh theo một cách (assets của TypeSafe, danh sách images của Clef, phần image_url của OpenRouter); cờ compat.imageInput chọn cách, được tự nhận diện theo provider và host (typesafe-system-one.ts:5). Trong coding agent, các mô hình này không hiện trong /model; mô hình chat chạm tới chúng qua script codemode, extension qua ctx.modelRegistry.classify() và generateImages(). Phần đó thuộc chương Pi 1.0 và chương Codemode.

Trace: pi --model openrouter/... chỉ với OPENROUTER_API_KEY#

Kịch bản: máy chưa từng chạy /login, không có models.json, chỉ có biến OPENROUTER_API_KEY. Người dùng chạy pi --model openrouter/moonshotai/kimi-k2.6 (id mặc định của OpenRouter trong bảng của coding agent), gõ một câu hỏi, mức suy nghĩ để mặc định. Mỗi bước dưới đây trỏ vào code đã đọc ở các mục trước.

  1. Khởi động mạngmain.ts đọc settings toàn cục, chép httpProxy (nếu có) vào biến môi trường rồi cài dispatcher undici toàn cục (main.ts:595).
  2. Dựng runtimeModelRuntime.create(): kho credential là RuntimeCredentials bọc auth.json (rỗng), models.json không có, 42 provider dựng sẵn, riêng openrouter bọc withPersonalizedCatalog(). Vì không có lớp phủ nào cho openrouter, composeProvider() trả nguyên provider đã bọc. Lần làm mới đầu tiên chạy với allowNetwork: false: khôi phục danh sách cá nhân hoá từ models-store.json nếu lần chạy trước đã lưu, không thì dùng catalog đóng gói.
  3. Ai có credentialLượt kiểm tra khả dụng gọi checkAuth() cho từng provider. Với openrouter: không có credential đã lưu, envApiKeyAuth không có check(), nên resolve() chạy thật và thấy OPENROUTER_API_KEY. Provider được đánh dấu đã cấu hình với nguồn "OPENROUTER_API_KEY"; trong kịch bản này các provider khác thì không.
  4. Phân giải --modelresolveCliModel() dùng mọi mô hình, kể cả mô hình chưa có credential, để --api-key dùng được cho lần đầu (model-resolver.ts:445). Trong selectReference(), tiền tố openrouter khớp một provider, phần còn lại moonshotai/kimi-k2.6 khớp đúng một id của OpenRouter, nên kết quả là mô hình đó với api: "openai-completions", baseUrl: "https://openrouter.ai/api/v1".
  5. Request của vòng lặp agentMỗi lượt, streamFn của session dựng option (timeout, chính sách thử lại, identity đọc lại theo cài đặt telemetry, transformHeaders cho hook before_provider_headers) rồi gọi modelRuntime.streamSimple() (sdk.ts:340, sdk.ts:412).
  6. Phân giải credential lúc gửiprepareRequest() gọi getAuth(model): không có apiKey tường minh; RuntimeCredentials.read() không có khoá --api-key nên hỏi auth.json, cũng không có; nhánh môi trường trả { auth: { apiKey } }. Header được gộp: header OpenCode (không áp dụng), header xác thực (không có), header của option, rồi transformHeaders (model-runtime.ts:659).
  7. Chọn adapterprovider.streamSimple() của createProvider() tra bảng theo model.api và lấy lazy wrapper openAICompletionsApi() (models.ts:1106). Ở request đầu tiên, module openai-completions.ts và SDK openai được nạp.
  8. Cờ compatdetectCompat() nhận ra OpenRouter: mức suy nghĩ dạng reasoning: { effort }, vai system thay cho developer (chỉ mô hình anthropic/* và openai/* được dùng developer), gửi store: false, max_completion_tokens, và bật header session affinity dạng x-session-id.
  9. Dựng client và headerExcerpt dưới đây. Identity cho User-Agent và ba header ghi nhận, x-session-id mang id phiên, SDK thêm Authorization: Bearer.
  10. Gửi và đọcPOST https://openrouter.ai/api/v1/chat/completions; thử lại tầng HTTP theo cài đặt; luồng SSE được đọc như ở chương pi-ai; chi phí tính bằng giá trong catalog, tức giá theo khoá nếu danh sách đã được cá nhân hoá.
createClient(): identity, header của Copilot, session affinity, rồi header của người gọi
pi/packages/ai/src/api/openai-completions.tsdòng 760, 770–803TypeScript
function createClient(	const headers: ProviderHeaders = { ...getIdentityHeaders(model, identity), ...model.headers };	if (model.provider === "github-copilot") {		const hasImages = hasCopilotVisionInput(context.messages);		const copilotHeaders = buildCopilotDynamicHeaders({			messages: context.messages,			hasImages,		});		Object.assign(headers, copilotHeaders);	}	if (sessionId && compat.sendSessionAffinityHeaders) {		if (compat.sessionAffinityFormat === "openrouter") {			headers["x-session-id"] = sessionId;		} else {			if (compat.sessionAffinityFormat === "openai") {				headers.session_id = sessionId;			}			headers["x-client-request-id"] = sessionId;			headers["x-session-affinity"] = sessionId;		}	}	// Merge options headers last so they can override defaults	if (optionsHeaders) {		Object.assign(headers, optionsHeaders);	}	return new OpenAI({		apiKey,		baseURL: model.baseUrl,		dangerouslyAllowBrowser: true,		fetch,		defaultHeaders: headers,	});
Ví dụ minh hoạ: request gửi đi (rút gọn, giá trị là minh hoạ)
POST https://openrouter.ai/api/v1/chat/completions
Authorization: Bearer …                         ← OPENROUTER_API_KEY, do SDK openai thêm
User-Agent: pi/1.4.0 (linux; node/v24.x; x64)   ← identity "detailed" của coding agent
HTTP-Referer: https://pi.dev                     ┐
X-OpenRouter-Title: pi                           ├ PI_ATTRIBUTION, chỉ khi bật install telemetry
X-OpenRouter-Categories: cli-agent               ┘
x-session-id: 019a…                             ← sessionId, vì sendSessionAffinityHeaders
{ "model": "moonshotai/kimi-k2.6", "stream": true,
  "messages": [{ "role": "system", ... }, { "role": "user", ... }],
  "stream_options": { "include_usage": true }, "store": false,
  "max_completion_tokens": …,
  "reasoning": { "effort": … } }                ← chỉ khi mô hình có suy nghĩ
Thứ tự gộp header: identity ở dưới cùng, rồi model.headers, rồi header động, rồi header của người gọi. Một header cấu hình luôn thắng identity.

Gateway khác hãng gốc ở đâu#

Đổi lệnh thành pi --model moonshotai/kimi-k2.6 với MOONSHOT_API_KEY, cùng một mô hình gốc, và gần như mọi bước ở trên khác đi:

Qua OpenRouterGọi thẳng Moonshot
Endpoint, khoáopenrouter.ai/api/v1, OPENROUTER_API_KEYapi.moonshot.ai/v1, MOONSHOT_API_KEY
CatalogLọc theo khoá qua /models/user, giá và giới hạn theo khoáCatalog đóng gói cộng lớp phủ pi.dev
Cờ compatthinkingFormat: "openrouter", store: false, max_completion_tokensKhông store, không reasoning_effort, max_tokens
Header thêmAttribution, x-session-idKhông có
Ai phục vụ thậtMột hãng gốc do OpenRouter chọn, trừ khi openRouterRouting giới hạnMoonshot
Usagechunk.usage, token cache trong prompt_tokens_details.cached_tokensCó khi nằm trong choice.usage; adapter có đường lùi cho trường hợp này (openai-completions.ts:576)

Còn nếu người dùng gõ --model moonshotai/kimi-k2.6 mà chỉ có OPENROUTER_API_KEY? Tiền tố moonshotai cũng là một provider, nên selectReference() có một ứng viên "theo tên" (kimi-k2.6 của moonshotai) và những ứng viên "nguyên văn" có id đúng bằng moonshotai/kimi-k2.6 ở provider khác, so khớp không phân biệt hoa thường. Chú thích của hàm nói rõ luật: provider/phần còn lại chỉ là một gợi ý; provider được nêu tên thắng nếu có credential, không thì một ứng viên nguyên văn duy nhất có credential thắng (model-resolver.ts:223). Ở đây moonshotai không có khoá còn openrouter có, nên lệnh lặng lẽ chạy qua OpenRouter. Nếu HF_TOKEN cũng được đặt, mục moonshotai/Kimi-K2.6 của Hugging Face trở thành ứng viên có credential thứ hai, và lệnh dừng với lỗi "ambiguous across providers" yêu cầu dùng --provider.

Cuối cùng, một id không có trong catalog (một mô hình OpenRouter vừa ra mắt hôm nay) không làm lệnh thất bại: resolveCliModel() sao mô hình mặc định của provider, đổi id, và in cảnh báo "Using custom model id" (model-resolver.ts:296). Request sẽ đi được, nhưng cửa sổ ngữ cảnh, giá và cờ compat là của mô hình mặc định chứ không phải của mô hình thật.

Trace ngắn: một mô hình Ollama khai báo trong models.json#

Ví dụ minh hoạ: ~/.pi/agent/models.json
{
  "providers": {
    "ollama": {
      "baseUrl": "http://localhost:11434/v1",
      "api": "openai-completions",
      "apiKey": "ollama",
      "compat": { "supportsDeveloperRole": false, "supportsReasoningEffort": false },
      "models": [{ "id": "qwen2.5-coder:7b" }]
    }
  }
}
Theo ví dụ của docs/models.md, thêm hai cờ compat mà README khuyên cho Ollama.
  1. Nạp và kiểm traModelConfig.load() kiểm tra file bằng lược đồ đã biên dịch. ollama không phải id dựng sẵn, nên nó là một provider mới.
  2. GhépcomposeProvider("ollama"): không có provider gốc, có cấu hình, nên composeModelProvider(..., base = undefined, ...). applyModelsJson() trên danh sách gốc rỗng tạo đúng một mô hình bằng modelFromJson(): API và baseUrl lấy từ cấp provider, cửa sổ 128.000 token, output 16.384, giá 0, compat gộp từ cấp provider.
  3. Xác thựccomposeApiKeyAuth() không có xác thực kế thừa, có khoá cấu hình "ollama": một chuỗi thường, phân giải thành chính nó. check() báo đã cấu hình, nên mô hình hiện trong /model. Bỏ apiKey đi thì provider không có cách xác thực nào trả về kết quả, và mô hình biến mất khỏi danh sách dùng được.
  4. Phân giải tên--model ollama/qwen2.5-coder:7b: id chứa dấu hai chấm, nhưng hậu tố :<mức suy nghĩ> chỉ được xét khi cả chuỗi không khớp mô hình nào, và 7b cũng không phải một mức suy nghĩ, nên id được giữ nguyên.
  5. Chọn adapterstreamWith(): không có stream của extension, không có provider gốc, nên lấy openai-completions từ registry toàn cục của /compat.
  6. RequestdetectCompat() không nhận ra provider hay host nên trả mặc định của OpenAI; model.compat tắt vai developer và reasoning_effort. Header chỉ có User-Agent (host không phải gateway nào cần attribution) và Authorization: Bearer ollama, không có session affinity. POST tới http://localhost:11434/v1/chat/completions. Chi phí bằng 0 vì giá bằng 0.

So với llama.cpp, ba khác biệt nằm ở tầng provider chứ không ở tầng API: llama.cpp có danh sách mô hình động đọc từ router, baseUrl đến từ bước phân giải xác thực thay vì từ catalog, và cờ compat được đặt từ những gì router báo về chat template. Còn API và adapter thì giống hệt.

Quyết định thiết kế#

Provider và API là hai trục riêng. Ở v1.4.0 có 42 provider dựng sẵn nhưng chỉ mười API chat. Nếu provider và giao thức là một, mỗi endpoint mới sẽ đòi một adapter mới, mà một adapter dài một đến hai nghìn dòng và phải được kiểm thử riêng. Tách hai trục ra, một endpoint "tương thích OpenAI" hay "tương thích Anthropic" chỉ còn là một factory mười lăm dòng cộng vài cờ compat, và một provider có thể trộn giao thức khi hãng phía sau dùng giao thức khác nhau cho các họ mô hình (Copilot, OpenCode, Fireworks, OpenRouter). README tóm lại đúng như vậy: provider là đơn vị runtime sở hữu catalog, xác thực và hành vi stream, còn API là giao thức dùng chung (README). Cái giá là số cờ compat phình ra; pi-ai kìm nó bằng ba cách: detectCompat() tự đoán theo provider và host, bộ sinh catalog ghi sẵn cờ đã kiểm chứng cho từng mô hình, và quy tắc chỉ bật cờ cho khác biệt đã kiểm chứng.

SDK nạp lười. pi-ai phụ thuộc vào SDK của Anthropic, OpenAI, Google và AWS, cộng các flow OAuth cần node:http. Phần lớn ứng dụng (một Worker, một trang web, một host của Pi Durable) chỉ dùng một hai provider. Lời giải là factory chỉ import lazy wrapper, entry ./models không kéo TypeBox hay catalog, và module chỉ chạy trên Node được nạp qua specifier là biến (README). Phương án bị loại là import tĩnh mọi SDK, đơn giản hơn nhưng buộc mọi bundle mang theo AWS SDK. Cái giá có hai phần: request đầu tiên của mỗi API chịu thêm thời gian nạp module, và bản đóng gói đơn file phải tự đăng ký những module mà bundler không nhìn thấy.

Credential phân giải lúc gửi request. Không provider nào giữ credential từ lúc được tạo. Lý do nằm rải rác khắp chương này: token Copilot sống ngắn, khoá của Meta sống một ngày, một tiến trình Pi khác có thể vừa làm mới token, khoá dạng !lệnh có thể đã xoay vòng, credential của ECS hay IRSA tự đổi, và Cloudflare cần account id của đúng request. Phân giải ở mỗi request, dưới khoá của kho credential, xử lý được tất cả những trường hợp đó mà không có cơ chế riêng nào. Nó cũng cho phép một Models phục vụ nhiều host (Pi Durable nhận cả một hàm trả về Models). Cái giá là mỗi request phải đọc kho credential; AuthStorage giảm nó bằng cách chỉ đọc lại file khi revision đổi.

Không chép credential của môi trường. Với AWS và Google, Pi không bao giờ đọc rồi lưu credential; resolve() chỉ báo "đã cấu hình" và SDK tự đi chuỗi credential của nó. Một bản chép sẽ cũ đi khi vai trò được xoay vòng, và sẽ đưa bí mật của đám mây vào auth.json.

Catalog vừa sinh sẵn vừa làm mới được. Catalog được sinh lúc build từ models.dev cùng danh sách của OpenRouter và Vercel, cộng các sửa tay theo từng hãng, nên id mô hình có kiểu chính xác và Pi chạy được khi ngoại tuyến. Nhưng mô hình mới ra mỗi tuần, nên trên đó có bốn lớp làm mới: lớp phủ pi.dev (chỉ khi mới hơn dữ liệu đóng gói), danh sách theo khoá của OpenRouter, catalog động của provider như Radius, llama.cpp, Copilot, và cuối cùng là đường lùi "Using custom model id" cho id chưa ai biết. Phương án chỉ dùng catalog từ xa bị loại vì Pi phải khởi động được khi không có mạng (PI_OFFLINE, chế độ -p).

Đăng nhập bằng gói thuê bao và cái giá của nó. Một số flow đóng vai ứng dụng chính thức của hãng: OAuth của Anthropic gửi danh tính của Claude Code (chương pi-ai), mô hình Copilot mang sẵn header của VS Code Copilot Chat trong catalog (generate-models.ts:163), Codex gửi originator. Điều đó nghĩa là hãng có thể đổi luật bất cứ lúc nào, và mức sử dụng được tính theo quy định của gói. Code nói thẳng một hệ quả: khi phiên dùng xác thực thuê bao của Anthropic, chế độ interactive cảnh báo rằng dùng qua harness bên thứ ba bị tính vào "extra usage" theo token, không theo hạn mức của gói Claude (interactive-mode.ts:323), tắt được bằng warnings.anthropicExtraUsage (docs/settings.md:172). Cờ isSubscription của OAuthAuth cho footer hiện chi phí kèm nhãn (sub), và Kimi For Coding được coi là thuê bao ngay cả khi dùng khoá (footer.ts:189). OpenAI Codex được giữ lại với nhãn "legacy", vì Sign in with ChatGPT trên provider openai đã thay nó (README).

Cạm bẫy và trường hợp biên#

  • Cùng một mô hình, khác provider, khác giá và giới hạn. DeepSeek V4 Pro trên Azure có giá riêng của Azure và bộ cờ compat riêng (không vai developer, không tham số cache) (generate-models.ts:3357); mô hình GPT trên Azure có cửa sổ ngữ cảnh khác OpenAI (generate-models.ts:3331); giới hạn của OpenRouter phụ thuộc vào khoá. Chi phí trong footer luôn là giá catalog của đường đang dùng.
  • provider/id chỉ là gợi ý. --model moonshotai/kimi-k2.6 có thể chạy qua OpenRouter nếu chỉ OpenRouter có credential, như đã thấy trong trace. Muốn chắc chắn, dùng --provider.
  • Biến môi trường dùng chung, credential thì không. MOONSHOT_API_KEY làm cả moonshotai lẫn moonshotai-cn hiện là đã cấu hình; đó là do bảng biến môi trường, không có nghĩa khoá dùng được ở cả hai endpoint. Ngược lại, /login lưu credential theo id, nên đăng nhập qwen-token-plan không cấu hình qwen-token-plan-individual (README).
  • Credential đã lưu thắng biến môi trường. Một khoá cũ trong auth.json thắng khoá mới vừa export. /logout chỉ xoá credential đã lưu; nó không gỡ biến môi trường, không gỡ apiKey trong models.json, không thu hồi credential ở phía hãng (docs/providers.md:16). Hệ quả lạ nhất: đã đăng nhập openai bằng ChatGPT thì classifier GPT-6 Luna biến mất dù OPENAI_API_KEY có đặt, vì OAuth đã lưu sở hữu provider (docs/models.md:157).
  • Lệnh trong auth.json và models.json cư xử khác nhau. Lệnh của auth.json chạy một lần và cache cả kết quả lỗi cho tới khi khởi động lại; lệnh của models.json chạy ở mỗi request, nên một lệnh chậm làm chậm mọi request (giới hạn 10 giây).
  • Máy chủ "tương thích OpenAI" lạ nhận mặc định của OpenAI. store: false, vai developer, reasoning_effort và max_completion_tokens có thể bị từ chối. Bật đúng cờ đã kiểm chứng, ở cấp provider hoặc cấp mô hình.
  • Nhận diện theo host không đồng đều. Một provider tự đặt id trỏ vào openrouter.ai nhận thinkingFormat, x-session-id và attribution của OpenRouter vì các phép nhận diện đó xét cả host, nhưng không nhận cacheControlFormat: "anthropic" cho mô hình anthropic/*, vì phép đó chỉ xét provider === "openrouter". Đặt cờ đó tay trong compat.
  • Khoá giả là cần thiết. Provider không có cách xác thực nào trả kết quả thì mô hình của nó không hiện ra, nên máy chủ cục bộ không cần khoá vẫn phải có "apiKey": "ollama"; llama.cpp tự dùng "local".
  • Id chưa có trong catalog vẫn chạy, với siêu dữ liệu sai. Đường lùi "Using custom model id" chép cửa sổ ngữ cảnh, giá và compat của mô hình mặc định của provider. Compaction và tính tiền sẽ dựa trên các số đó; khai báo mô hình trong models.json nếu cần số đúng.
  • Cổng của callback OAuth. Sign in with ChatGPT và Codex dùng cổng 1455, trùng với Codex CLI; ChatGPT báo lỗi khi cổng bận, Codex lùi về dán URL. Chạy trong container thì đặt PI_OAUTH_CALLBACK_HOST và chuyển tiếp cổng, hoặc dùng cách dán mã hay device code.
  • Thiếu cấu hình nền tảng chỉ lộ ra lúc gửi. Mô hình Azure có baseUrl rỗng: thiếu AZURE_OPENAI_BASE_URL lẫn AZURE_OPENAI_RESOURCE_NAME thì request kết thúc bằng một sự kiện error. ADC của Vertex cần cả project lẫn location, còn khoá GOOGLE_CLOUD_API_KEY thì không.
  • Proxy. Chỉ proxy HTTP và HTTPS được hỗ trợ, SOCKS và PAC bị từ chối; httpProxy trong settings không ghi đè biến môi trường đã có.
  • Changelog là nơi đọc các lỗi đã gặp. Ba lần sửa placeholder của Cloudflare, bốn lần làm chắc việc làm mới OAuth, đổi API của Kimi For Coding sang Completions ở 1.3.0 (chương pi-ai): phần lớn hành vi tinh tế trong chương này có một mục changelog giải thích vì sao.

Đọc code ở đâu#

Thứ tự đề nghị: bắt đầu từ tài liệu người dùng để biết có những đường nào, rồi đi từ pi-ai (provider, xác thực, flow) lên coding agent (cấu hình, ghép, extension). Đường dẫn tính từ gốc repo.

#FileĐọc để hiểu
1docs/providers.md, docs/models.md, docs/models-configuration.mdMọi cách kết nối từ phía người dùng, thứ tự credential, mọi trường của models.json.
2providers/all.tsDanh sách 42 provider dựng sẵn và các hàm đọc catalog có kiểu.
3groq.ts, opencode.ts, openrouter.ts, radius.tsBốn khuôn mẫu factory: tối giản, nhiều API, catalog theo khoá, viết tay.
4src/models.tsCreateProviderOptions, createProvider(), getAuth(), applyAuth(), checkProviderAuth().
5auth/types.ts, auth/resolve.ts, auth/helpers.ts, auth/context.tsHợp đồng xác thực, thứ tự phân giải, envApiKeyAuth(), lazyOAuth(), AuthContext.
6callback-server.ts, device-code.ts, rồi anthropic.ts, github-copilot.ts, meta.tsKhối dựng của OAuth, rồi ba flow tiêu biểu: trình duyệt, đổi token, xin khoá.
7amazon-bedrock.ts, bedrock-converse-stream.ts:155, google-vertex.ts, azure.ts, azure-openai-config.tsCredential của nền tảng đám mây, vùng, endpoint, deployment.
8cloudflare-auth.ts, cloudflare-stream.ts, cloudflare-ai-binding.tsPlaceholder theo tài khoản, header của gateway, fetch qua binding.
9api/pi-messages.tsGiao thức riêng của Pi, gọn nhất trong các adapter.
10api/lazy.ts, bedrock-converse-stream.lazy.ts, auth/oauth/load.tsBốn cách nạp lười.
11utils/identity.ts, utils/node-http-proxy.ts, utils/provider-env.tsAttribution theo gateway, proxy ngoài fetch, biến môi trường theo provider.
12scripts/generate-models.ts, scripts/openrouter-catalog.tsLuật chọn API theo mô hình cho các provider nhiều API, cờ compat sinh sẵn.
13core/model-runtime.tscreate(), composeProvider(), prepareRequest(), getAuth(), đăng ký provider của extension.
14core/provider-composer.ts, core/model-config.ts, core/resolve-config-value.tsGhép models.json và extension lên provider gốc; cú pháp $VAR và !lệnh.
15core/auth-storage.ts, core/runtime-credentials.ts, cli/credential-print.tsauth.json với khoá file, --api-key, pi auth.
16core/remote-catalog-provider.ts, core/models-store.tsLớp phủ pi.dev, catalog theo khoá, models-store.json.
17extensions/llama/provider.ts, extensions/llama/index.tsMột native provider hoàn chỉnh có catalog động và classifier.
18core/model-resolver.tsCách --model chọn provider khi cùng một id có ở nhiều nơi.
19core/http-dispatcher.ts, core/client-identity.tsProxy và timeout toàn cục, attribution theo telemetry.
20custom-provider-gitlab-duo, custom-provider-anthropic, docs/custom-provider.mdProvider của extension: ủy quyền cho adapter có sẵn và stream tự viết.
21durable/README.md, harness/generation.tsHost của Pi Durable chọn provider; ModelRef phân giải ở mỗi lượt.

Tóm tắt#

  • Mỗi request trả lời ba câu hỏi độc lập: provider nào phục vụ, nói API nào, xác thực bằng gì. Ở v1.4.0 có 42 provider dựng sẵn, mười API chat, một API ảnh, bốn API classifier; provider và API ghép nhiều với nhiều.
  • Provider phần lớn là một factory mười lăm dòng qua createProvider(): catalog sinh tự động, ProviderAuth, lazy wrapper của API. Provider nhiều API chọn adapter theo model.api; wrapper quanh ProviderStreams biến đổi request trước khi gửi.
  • pi-ai phân giải credential ở mỗi request: khoá tường minh, rồi credential đã lưu (sở hữu provider, OAuth làm mới dưới khoá), rồi môi trường. Coding agent thêm --api-key, auth.json và apiKey của models.json vào đúng chuỗi đó.
  • Chín flow OAuth dùng ba khối chung (PKCE, máy chủ callback đua với dán mã, hỏi vòng device code), và tách refresh() khỏi toAuth() đủ rộng để chứa đổi token của Copilot, xin khoá của Meta, khoá vĩnh viễn của OpenRouter.
  • Nền tảng đám mây không bị chép credential: resolve() chỉ báo đã cấu hình, SDK của AWS và Google tự lấy credential; Azure và Cloudflare dựng URL theo tài khoản lúc gửi; Anthropic có workload identity federation qua SDK.
  • Gateway là provider có catalog đa hãng cộng vài việc riêng: chọn API theo mô hình, catalog theo khoá, header attribution và định tuyến theo phiên. Cùng một mô hình gốc có nhiều đường, mỗi đường một giá, một giới hạn, một API.
  • Mô hình cục bộ đi qua extension llama.cpp (catalog động, baseUrl từ xác thực) hoặc qua models.json với cờ compat đã kiểm chứng; extension đăng ký provider hoàn chỉnh hoặc ProviderConfig, và nên ủy quyền phần stream cho adapter có sẵn.
  • Pi Durable để host tự chọn provider bằng createModels() và setProvider(), và chỉ lưu tham chiếu { provider, modelId }, nên đổi cấu hình provider không phải sửa dữ liệu.

Mục lục

Giao diện