跳到主内容
创建时间:最后更新:作者:白川

接入不同模型服务

按"注册 Provider、查目录、确认凭据、发起请求"四步接入不同服务,理解统一接口的边界与各家差异。

接入不同模型服务

不同模型服务的连接方式有共同部分,也有不能互换的参数。本篇使用同一段任务代码分别连接三家服务,观察哪些配置需要变化,而不是同时创建三份智能体程序。

先按配置和切换模型建立项目,准备 src/connection.ts。只配置你实际拥有权限的服务即可,不要求为了学习申请所有账号。

接入一条路径的四步

不管接哪家服务,模型层的接入都走同一条路径:

text
读取配置 → 注册 Provider → 查目录确认模型 → 确认凭据 → 发起请求

这四步分别对应 Models 上的不同能力:

步骤调用失败时的表现
注册 Provider
models.setProvider(p)
后续查找全部返回空
查目录
models.getModel(provider, id)
返回 undefined
确认凭据
models.checkAuth(provider)
返回 undefined
发起请求
models.completeSimple(model, ctx)
抛 ModelsError 或返回 stopReason: "error"

把这四步拆开的好处是:每一步的失败都在不同的层,可以分别验证。把四步揉进一个 try/catch 里,出问题时你只知道"连不上",不知道卡在哪。

按服务准备配置

Provider本例读取的 Key 变量连接实现
anthropic
ANTHROPIC_API_KEY
Anthropic 原生消息接口
openai
OPENAI_API_KEY
OpenAI Responses 接口
google
GEMINI_API_KEY
Gemini 原生接口

这些是本例所用 Provider 的默认行为。聊天订阅、API Key 和第三方网关凭据是不同来源,不要因为名字里有同一个模型品牌就混用。

例如切换到 OpenAI 时,将 .env 改为:

dotenv
PROVIDER=openai
MODEL_ID=gpt-4o-mini
OPENAI_API_KEY=replace-with-your-key

切换 Gemini 可使用如下配置形式:

dotenv
PROVIDER=google
MODEL_ID=gemini-2.5-flash
GEMINI_API_KEY=replace-with-your-key

上面模型 ID 是示例。服务可能调整可访问模型,应以你账号、当前目录与实际请求为准,不把它们当成永久可用清单。

环境变量名不是猜的,它们写在 packages/ai/src/env-api-keys.ts 的映射表里:

typescript
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 的关系

三家服务用的协议不同:

Providermodel.api 取值协议
anthropic
anthropic-messages
Anthropic 原生消息协议
openai
openai-responses
OpenAI Responses 协议
google
google-generative-ai
Gemini 原生协议

但同一个 API 会被多家服务共用。xAI、Groq、Cerebras、OpenRouter 大多走 openai-completions;GitHub Copilot 和 OpenCode Zen 是混合 API Provider,按模型逐个分发。

这带来一个实用结论:在写代码时区分"Provider 差异"和"API 差异"。前者是服务层的(凭据、地址、模型目录),后者是协议层的(参数格式、事件格式、能力边界)。

使用同一个程序验证

新建 src/compare-providers.ts:

typescript
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))

运行:

bash
node --env-file=.env --import tsx src/compare-providers.ts

getAuth() 用来观察凭据来自哪里,不代表它已经向模型服务发出了成功推理请求。本例为了诊断先读取一次认证信息,真正调用时集合仍会解析认证;正常业务不必在每次请求前额外执行这一步。

只打印 source,不要照搬调试对象输出完整 Key 或认证头。凭据存储、OAuth 和环境变量之间的优先级属于 Provider 的认证逻辑,不要在调用代码里重新拼一套。

getAuth() 有两个重载:传 Provider ID 得到 provider 级认证;传模型则会把模型自带的 headers 一并合并进来:

typescript
const providerAuth = await models.getAuth(model.provider);
const modelAuth = await models.getAuth(model);

返回的 AuthResult 结构:

typescript
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() 收窄类型:

typescript
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() 的定义:

typescript
export function hasApi<TApi extends Api>(model: AnyModel, api: TApi): model is Model<TApi> {
	return isModelType(model, "chat") && model.api === api;
}

非聊天模型永远不匹配,即使它的 api 字段值相同。

三家服务的凭据形态不一样

同一个 createModels() 集合可以同时持有三家 Provider,但它们的凭据来源机制完全不同:

Provider凭据形态是否支持 OAuthcheckAuth() 的 type
anthropic
环境变量或交互式登录
是
"api_key" 或 "oauth"
openai
环境变量
否
"api_key"
google
环境变量
否
"api_key"
google-vertex
项目 + 位置 + gcloud ADC
是
"api_key" 或 "oauth"
amazon-bedrock
AWS profile / IAM / ECS 角色 / IRSA
是
"api_key" 或 "oauth"

Vertex AI 和 Bedrock 属于环境型凭据(ambient credentials),它们不读固定的 API Key 变量,而是检查一整组来源。getEnvApiKey() 对它们返回的是字符串 "<authenticated>" 而不是真实密钥——因为根本没有一个"密钥"这个概念。

Bedrock 支持的来源在源码里逐条列出:

typescript
// 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 的判定需要三者齐备:

typescript
if (hasCredentials && hasProject && hasLocation) {
	return "<authenticated>";
}

分别对应 ADC 文件、GOOGLE_CLOUD_PROJECT(或 GCLOUD_PROJECT)、GOOGLE_CLOUD_LOCATION。缺一个都不算配置好——这是"目录里有这个模型但连不上"的典型原因。

把多家服务放进同一个集合

需要同时比较三家时,不必写三段代码。Models 集合本身就是多 Provider 的容器:

typescript
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 一起:

typescript
const model = models.getModel("anthropic", "claude-sonnet-4-6");

只按 ID 搜会得到第一个匹配项,这在多 Provider 场景下是隐蔽的错误来源。

getAvailable() 在多 Provider 场景下更有用——它只返回凭据已配置的 Provider 下的模型:

typescript
const usable = await models.getAvailable();
for (const item of usable) {
  console.log(item.provider, item.id);
}

集合里注册了但没配凭据的 Provider 会被自动跳过。

能力差异从模型元数据读

模型目录里的字段就是能力声明:

字段含义读取示例
input
支持的输入模态
model.input.includes("image")
reasoning
是否支持推理
model.reasoning
contextWindow
上下文窗口
model.contextWindow
maxTokens
单次输出上限
model.maxTokens
thinkingLevelMap
各推理档位的支持情况
model.thinkingLevelMap
promptCache
缓存生存期
model.promptCache
cost
价格(每百万 token)
model.cost.input

推理档位有统一的名字,但不是每个模型都支持每一档。getSupportedThinkingLevels() 负责这个判断:

typescript
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、结束状态与结果。先比较是否完成同一任务,再看输出形式。一次较快响应不能证明长期更快;一次正确回答也不能证明工具能力已经接好。

一份可用的对比记录:

json
{
  "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 联调;三家服务的实际可访问模型、账号权限与响应差异需要在你的环境分别确认。

参考资料