接入不同模型服务
按"注册 Provider、查目录、确认凭据、发起请求"四步接入不同服务,理解统一接口的边界与各家差异。
接入不同模型服务
不同模型服务的连接方式有共同部分,也有不能互换的参数。本篇使用同一段任务代码分别连接三家服务,观察哪些配置需要变化,而不是同时创建三份智能体程序。
先按配置和切换模型建立项目,准备 src/connection.ts。只配置你实际拥有权限的服务即可,不要求为了学习申请所有账号。
接入一条路径的四步
不管接哪家服务,模型层的接入都走同一条路径:
读取配置 → 注册 Provider → 查目录确认模型 → 确认凭据 → 发起请求这四步分别对应 Models 上的不同能力:
把这四步拆开的好处是:每一步的失败都在不同的层,可以分别验证。把四步揉进一个 try/catch 里,出问题时你只知道"连不上",不知道卡在哪。
按服务准备配置
这些是本例所用 Provider 的默认行为。聊天订阅、API Key 和第三方网关凭据是不同来源,不要因为名字里有同一个模型品牌就混用。
例如切换到 OpenAI 时,将 .env 改为:
PROVIDER=openai
MODEL_ID=gpt-4o-mini
OPENAI_API_KEY=replace-with-your-key切换 Gemini 可使用如下配置形式:
PROVIDER=google
MODEL_ID=gemini-2.5-flash
GEMINI_API_KEY=replace-with-your-key上面模型 ID 是示例。服务可能调整可访问模型,应以你账号、当前目录与实际请求为准,不把它们当成永久可用清单。
环境变量名不是猜的,它们写在 packages/ai/src/env-api-keys.ts 的映射表里:
const envMap: Record<string, string> = {
"ant-ling": "ANT_LING_API_KEY",
"qwen-token-plan": "QWEN_TOKEN_PLAN_API_KEY",
// ... 其它 provider ...
openai: "OPENAI_API_KEY",
azure: "AZURE_OPENAI_API_KEY",
nvidia: "NVIDIA_API_KEY",
deepseek: "DEEPSEEK_API_KEY",
google: "GEMINI_API_KEY",
"google-vertex": "GOOGLE_CLOUD_API_KEY",
// ... 更多 provider ...
};findEnvKeys(provider) 可以直接查某个 Provider 认哪些变量,getEnvApiKey(provider) 返回实际取到的值。
Provider 与 API 的关系
三家服务用的协议不同:
但同一个 API 会被多家服务共用。xAI、Groq、Cerebras、OpenRouter 大多走 openai-completions;GitHub Copilot 和 OpenCode Zen 是混合 API Provider,按模型逐个分发。
这带来一个实用结论:在写代码时区分"Provider 差异"和"API 差异"。前者是服务层的(凭据、地址、模型目录),后者是协议层的(参数格式、事件格式、能力边界)。
使用同一个程序验证
新建 src/compare-providers.ts:
import { createConnection, responseText } from "./connection.js"
const { models, model } = createConnection()
const auth = await models.getAuth(model, { signal: AbortSignal.timeout(10_000) })
if (!auth) {
throw new Error(`服务 ${model.provider} 未配置凭据`)
}
// 仅记录凭据来源,不输出 Key 或认证头。
console.log("凭据来源:", auth.source)
const result = await models.completeSimple(
model,
{
systemPrompt: "你是中文编程助手,不声称执行过没有执行的操作。",
messages: [
{ role: "user", content: "解释 items[0] 在空数组时的结果。", timestamp: Date.now() },
],
},
{ signal: AbortSignal.timeout(30_000), maxTokens: 512 },
)
console.log(responseText(result))运行:
node --env-file=.env --import tsx src/compare-providers.tsgetAuth() 用来观察凭据来自哪里,不代表它已经向模型服务发出了成功推理请求。本例为了诊断先读取一次认证信息,真正调用时集合仍会解析认证;正常业务不必在每次请求前额外执行这一步。
只打印 source,不要照搬调试对象输出完整 Key 或认证头。凭据存储、OAuth 和环境变量之间的优先级属于 Provider 的认证逻辑,不要在调用代码里重新拼一套。
getAuth() 有两个重载:传 Provider ID 得到 provider 级认证;传模型则会把模型自带的 headers 一并合并进来:
const providerAuth = await models.getAuth(model.provider);
const modelAuth = await models.getAuth(model);返回的 AuthResult 结构:
export interface AuthResult {
auth: ModelAuth;
/** Provider-scoped environment/config values resolved from credentials and ambient context. */
env?: ProviderEnv;
/** Human-readable label for status UI: "ANTHROPIC_API_KEY", "OAuth", "~/.aws/credentials". */
source?: string;
}auth 里可能有 apiKey、headers、baseUrl 三种。baseUrl 特别有用——GitHub Copilot 这类服务每个账号的端点不同,认证解析出来后会覆盖模型自带的 baseUrl。
统一调用不等于所有能力相同
completeSimple() 提供共同请求方式,返回统一的助手消息。它减少调用代码的分支,却不会让所有模型支持完全一样的图片、推理档位和工具约束。
先使用文本任务验证连接,再验证业务需要的能力。例如文本回答正常,但工具调用失败时,需要检查模型与协议,不应直接认定凭据错误。
系统指令、用户消息和输出长度是本例共同使用的设置。API 专属参数应在明确协议后添加,不要把某家服务的推理参数无条件传给所有模型。
要传 provider 专属参数时,先用 hasApi() 收窄类型:
import { hasApi } from "@earendil-works/pi-ai";
const m = models.getModel("anthropic", "claude-sonnet-4-5");
if (m && hasApi(m, "anthropic-messages")) {
// m: Model<'anthropic-messages'> —— stream 选项已完整定型
models.stream(m, context, { thinkingEnabled: true, thinkingBudgetTokens: 2048 });
}hasApi() 的定义:
export function hasApi<TApi extends Api>(model: AnyModel, api: TApi): model is Model<TApi> {
return isModelType(model, "chat") && model.api === api;
}非聊天模型永远不匹配,即使它的 api 字段值相同。
三家服务的凭据形态不一样
同一个 createModels() 集合可以同时持有三家 Provider,但它们的凭据来源机制完全不同:
Vertex AI 和 Bedrock 属于环境型凭据(ambient credentials),它们不读固定的 API Key 变量,而是检查一整组来源。getEnvApiKey() 对它们返回的是字符串 "<authenticated>" 而不是真实密钥——因为根本没有一个"密钥"这个概念。
Bedrock 支持的来源在源码里逐条列出:
// 1. AWS_PROFILE - named profile from ~/.aws/credentials
// 2. AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY - standard IAM keys
// 3. AWS_BEARER_TOKEN_BEDROCK - Bedrock bearer token
// 4. AWS_CONTAINER_CREDENTIALS_RELATIVE_URI - ECS task roles
// 5. AWS_CONTAINER_CREDENTIALS_FULL_URI - ECS task roles (full URI)
// 6. AWS_WEB_IDENTITY_TOKEN_FILE - IRSA (IAM Roles for Service Accounts)Vertex AI 的判定需要三者齐备:
if (hasCredentials && hasProject && hasLocation) {
return "<authenticated>";
}分别对应 ADC 文件、GOOGLE_CLOUD_PROJECT(或 GCLOUD_PROJECT)、GOOGLE_CLOUD_LOCATION。缺一个都不算配置好——这是"目录里有这个模型但连不上"的典型原因。
把多家服务放进同一个集合
需要同时比较三家时,不必写三段代码。Models 集合本身就是多 Provider 的容器:
import { createModels } from "@earendil-works/pi-ai";
import { anthropicProvider } from "@earendil-works/pi-ai/providers/anthropic";
import { googleProvider } from "@earendil-works/pi-ai/providers/google";
import { openaiProvider } from "@earendil-works/pi-ai/providers/openai";
const models = createModels();
models.setProvider(anthropicProvider());
models.setProvider(openaiProvider());
models.setProvider(googleProvider());
// 集合里的模型总数
console.log("已注册 Provider:", models.getProviders().length);
console.log("全部聊天模型:", models.getModels().length);
// 按 Provider 分组列出
for (const provider of models.getProviders()) {
const list = models.getModels(provider.id);
console.log(`${provider.id}(${provider.name}):${list.length} 个模型`);
}getModels() 不带参数时返回所有 Provider 的聊天模型合并结果。注意 ID 在单个 Provider 内唯一,跨 Provider 可能重名——所以定位一个模型永远要带 provider 一起:
const model = models.getModel("anthropic", "claude-sonnet-4-6");只按 ID 搜会得到第一个匹配项,这在多 Provider 场景下是隐蔽的错误来源。
getAvailable() 在多 Provider 场景下更有用——它只返回凭据已配置的 Provider 下的模型:
const usable = await models.getAvailable();
for (const item of usable) {
console.log(item.provider, item.id);
}集合里注册了但没配凭据的 Provider 会被自动跳过。
能力差异从模型元数据读
模型目录里的字段就是能力声明:
推理档位有统一的名字,但不是每个模型都支持每一档。getSupportedThinkingLevels() 负责这个判断:
export function getSupportedThinkingLevels<TApi extends Api>(model: Model<TApi>): ModelThinkingLevel[] {
if (!model.reasoning) return ["off"];
return EXTENDED_THINKING_LEVELS.filter((level) => {
const mapped = model.thinkingLevelMap?.[level];
if (mapped === null) return false;
if (level === "xhigh" || level === "max") return mapped !== undefined;
return true;
});
}thinkingLevelMap 里某个档位映射为 null 表示不支持;xhigh 和 max 必须在映射里明确出现才算支持。clampThinkingLevel() 会把请求的档位收敛到最接近的可用档位——往上找优先,找不到再往下找。
比较时保持任务一致
固定代码片段和问题,记录服务、模型 ID、结束状态与结果。先比较是否完成同一任务,再看输出形式。一次较快响应不能证明长期更快;一次正确回答也不能证明工具能力已经接好。
一份可用的对比记录:
{
"provider": "anthropic",
"model": "claude-sonnet-4-6",
"api": "anthropic-messages",
"authSource": "ANTHROPIC_API_KEY",
"stopReason": "stop",
"usage": { "input": 24, "output": 61, "totalTokens": 85 },
"toolCallObserved": false
}usage 是 provider 报告的真实用量,authSource 说明凭据来自哪里,toolCallObserved 记录工具能力是否验证过——这三项分开记,才能在出问题时判断是哪一层。
完成后,你能在同一程序中替换内置服务。若拿到的服务只提供兼容地址,进入自定义接口与本地模型,不要把网关 Key 直接填到原生服务配置里。
本篇示例已完成类型检查与模拟运行验证,本轮未使用真实 Key 联调;三家服务的实际可访问模型、账号权限与响应差异需要在你的环境分别确认。