@f5-sales-demo/pi-ai (英语)
统一LLM API,具有自动模型发现,提供商配置,信使和成本跟踪,以及简单的上下文持久性并交接到其他模型的会中.
说明: 此库只包含支持调用工具( 函数调用) 的模型, 因为这对于代理工作流程至关重要 .
目录
支持供应商
- 开放AI (通过用户平台API访问)
OPENAI_API_KEY(中文(简体) ) - OpenAI 代码 (ChatGPT Plus/Pro通过设备代码或浏览器OAuth订阅)
- 安东尼
- 谷歌
- Vertex 大赦国际 (Gemini通过Vertex AI)
- 迷雾
- 格罗克
- 心管
- 一起来
- 月光拍摄 (所需经费)
MOONSHOT_API_KEY(中文(简体) ) - 钱范 (所需经费)
QIANFAN_API_KEY(中文(简体) ) - 纳米比亚 (所需经费)
NVIDIA_API_KEY(中文(简体) ) - 纳诺GPT (所需经费)
NANO_GPT_API_KEY(中文(简体) ) - 拥抱面部推论
- 页:1 大赦国际
- 威尼斯语 (所需经费)
VENICE_API_KEY(中文(简体) ) - 打开路透社
- Kilo 网关 (支持奥奥特)
/login kilo或者说KILO_API_KEY(中文(简体) ) - 列表 (所需经费)
LITELLM_API_KEY(中文(简体) ) - 扎西岛 (所需经费)
ZAI_API_KEY(中文(简体) ) - 迷你Max 编码计划 (所需经费)
MINIMAX_CODE_API_KEY或者说MINIMAX_CODE_CN_API_KEY(中文(简体) ) - 小美,小美,小美 (所需经费)
XIAOMI_API_KEY(中文(简体) ) - 禅穆 (所需经费)
ZENMUX_API_KEY(中文(简体) ) - Quen 门户网站 (支助)
QWEN_OAUTH_TOKEN或者说QWEN_PORTAL_API_KEY(中文(简体) ) - Cloudflare AI 网关 (所需经费)
CLOUDFLARE_AI_GATEWAY_API_KEY和供应商专用网关基址) - 奥拉玛 (本地 OpenAI 兼容运行时间; 可选)
OLLAMA_API_KEY(中文(简体) ) - 拉玛.cpp (本地 OpenAI 和 Anthropic 兼容推论服务器)
- vLLM 数据 (OpenAI兼容服务器;
VLLM_API_KEY(用于安全部署) - GitHub 副驾驶 (要求OAuth,见下文)
- 谷歌双子座CLI (要求OAuth,见下文)
- 反重力 (要求OAuth,见下文)
- 任意 OpenAI 兼容的 API: LM Studio,自定义代理等.
安装
npm install @f5-sales-demo/pi-ai
快速启动
import { Type, getModel, stream, complete, Context, Tool, StringEnum } from "@f5-sales-demo/pi-ai";
// Fully typed with auto-complete support for both providers and models
const model = getModel("openai", "gpt-4o-mini");
// Define tools with TypeBox schemas for type safety and validation
const tools: Tool[] = [
{
name: "get_time",
description: "Get the current time",
parameters: Type.Object({
timezone: Type.Optional(Type.String({ description: "Optional timezone (e.g., America/New_York)" })),
}),
},
];
// Build a conversation context (easily serializable and transferable between models)
const context: Context = {
systemPrompt: "You are a helpful assistant.",
messages: [{ role: "user", content: "What time is it?" }],
tools,
};
// Option 1: Streaming with all event types
const s = stream(model, context);
for await (const event of s) {
switch (event.type) {
case "start":
console.log(`Starting with ${event.partial.model}`);
break;
case "text_start":
console.log("\n[Text started]");
break;
case "text_delta":
process.stdout.write(event.delta);
break;
case "text_end":
console.log("\n[Text ended]");
break;
case "thinking_start":
console.log("[Model is thinking...]");
break;
case "thinking_delta":
process.stdout.write(event.delta);
break;
case "thinking_end":
console.log("[Thinking complete]");
break;
case "toolcall_start":
console.log(`\n[Tool call started: index ${event.contentIndex}]`);
break;
case "toolcall_delta":
// Partial tool arguments are being streamed
const partialCall = event.partial.content[event.contentIndex];
if (partialCall.type === "toolCall") {
console.log(`[Streaming args for ${partialCall.name}]`);
}
break;
case "toolcall_end":
console.log(`\nTool called: ${event.toolCall.name}`);
console.log(`Arguments: ${JSON.stringify(event.toolCall.arguments)}`);
break;
case "done":
console.log(`\nFinished: ${event.reason}`);
break;
case "error":
console.error(`Error: ${event.error}`);
break;
}
}
// Get the final message after streaming, add it to the context
const finalMessage = await s.result();
context.messages.push(finalMessage);
// Handle tool calls if any
const toolCalls = finalMessage.content.filter((b) => b.type === "toolCall");
for (const call of toolCalls) {
// Execute the tool
const result =
call.name === "get_time"
? new Date().toLocaleString("en-US", {
timeZone: call.arguments.timezone || "UTC",
dateStyle: "full",
timeStyle: "long",
})
: "Unknown tool";
// Add tool result to context (supports text and images)
context.messages.push({
role: "toolResult",
toolCallId: call.id,
toolName: call.name,
content: [{ type: "text", text: result }],
isError: false,
timestamp: Date.now(),
});
}
// Continue if there were tool calls
if (toolCalls.length > 0) {
const continuation = await complete(model, context);
context.messages.push(continuation);
console.log("After tool execution:", continuation.content);
}
console.log(`Total tokens: ${finalMessage.usage.input} in, ${finalMessage.usage.output} out`);
console.log(`Cost: $${finalMessage.usage.cost.total.toFixed(4)}`);
// Option 2: Get complete response without streaming
const response = await complete(model, context);
for (const block of response.content) {
if (block.type === "text") {
console.log(block.text);
} else if (block.type === "toolCall") {
console.log(`Tool: ${block.name}(${JSON.stringify(block.arguments)})`);
}
}
工具
工具使LLMs能够与外部系统交互. 这个库使用TypeBox schemas进行类型安全工具定义,使用AJV进行自动验证. TypeBox schemas可以被序列化并被去序列化为平原JSON,使它们成为分布式系统的理想.
定义工具
import { Type, Tool, StringEnum } from "@f5-sales-demo/pi-ai";
// Define tool parameters with TypeBox
const weatherTool: Tool = {
name: "get_weather",
description: "Get current weather for a location",
parameters: Type.Object({
location: Type.String({ description: "City name or coordinates" }),
units: StringEnum(["celsius", "fahrenheit"], { default: "celsius" }),
}),
};
// Note: For Google API compatibility, use StringEnum helper instead of Type.Enum
// Type.Enum generates anyOf/const patterns that Google doesn't support
const bookMeetingTool: Tool = {
name: "book_meeting",
description: "Schedule a meeting",
parameters: Type.Object({
title: Type.String({ minLength: 1 }),
startTime: Type.String({ format: "date-time" }),
endTime: Type.String({ format: "date-time" }),
attendees: Type.Array(Type.String({ format: "email" }), { minItems: 1 }),
}),
};
处理工具呼叫
工具结果使用内容块,可以包括文本和图像:
import * as fs from "node:fs";
const context: Context = {
messages: [{ role: "user", content: "What is the weather in London?" }],
tools: [weatherTool],
};
const response = await complete(model, context);
// Check for tool calls in the response
for (const block of response.content) {
if (block.type === "toolCall") {
// Execute your tool with the arguments
// See "Validating Tool Arguments" section for validation
const result = await executeWeatherApi(block.arguments);
// Add tool result with text content
context.messages.push({
role: "toolResult",
toolCallId: block.id,
toolName: block.name,
content: [{ type: "text", text: JSON.stringify(result) }],
isError: false,
timestamp: Date.now(),
});
}
}
// Tool results can also include images (for vision-capable models)
const imageBuffer = fs.readFileSync("chart.png");
context.messages.push({
role: "toolResult",
toolCallId: "tool_xyz",
toolName: "generate_chart",
content: [
{ type: "text", text: "Generated chart showing temperature trends" },
{ type: "image", data: imageBuffer.toBase64(), mimeType: "image/png" },
],
isError: false,
timestamp: Date.now(),
});
带部分 JSON 的流动工具调用
在流化过程中,工具调用参数在到达时逐渐被解析. 这使得可以在完整参数可用前实时 UI 更新 :
const s = stream(model, context);
for await (const event of s) {
if (event.type === "toolcall_delta") {
const toolCall = event.partial.content[event.contentIndex];
// toolCall.arguments contains partially parsed JSON during streaming
// This allows for progressive UI updates
if (toolCall.type === "toolCall" && toolCall.arguments) {
// BE DEFENSIVE: arguments may be incomplete
// Example: Show file path being written even before content is complete
if (toolCall.name === "write_file" && toolCall.arguments.path) {
console.log(`Writing to: ${toolCall.arguments.path}`);
// Content might be partial or missing
if (toolCall.arguments.content) {
console.log(`Content preview: ${toolCall.arguments.content.substring(0, 100)}...`);
}
}
}
}
if (event.type === "toolcall_end") {
// Here toolCall.arguments is complete (but not yet validated)
const toolCall = event.toolCall;
console.log(`Tool completed: ${toolCall.name}`, toolCall.arguments);
}
}
关于部分工具参数的重要说明 :
- 期间
toolcall_delta事件,arguments包含部分 JSON 的最佳解析 - 字段可能缺失或不完整 - 在使用前总是检查是否存在
- 字符串值可切入中字
- 阵列可能不完整
- 嵌入物可能部分被填入
- 起码,
arguments将是一个空对象{}永远undefined - Google供应商不支持函数调用流线. 相反,你会得到一个单身
toolcall_delta带有全部参数的事件.
验证工具参数
使用时 agentLoop,工具参数在执行前会自动针对您的 TypeBox 计划验证。 如果验证失败,错误会作为工具结果返回到模型中,使其可以重试.
当执行您自己的工具执行循环时 stream() 或者说 complete(),使用时 validateToolCall 以验证参数,然后将其传递给工具:
import { stream, validateToolCall, Tool } from "@f5-sales-demo/pi-ai";
const tools: Tool[] = [weatherTool, calculatorTool];
const s = stream(model, { messages, tools });
for await (const event of s) {
if (event.type === "toolcall_end") {
const toolCall = event.toolCall;
try {
// Validate arguments against the tool's schema (throws on invalid args)
const validatedArgs = validateToolCall(tools, toolCall);
const result = await executeMyTool(toolCall.name, validatedArgs);
// ... add tool result to context
} catch (error) {
// Validation failed - return error as tool result so model can retry
context.messages.push({
role: "toolResult",
toolCallId: toolCall.id,
toolName: toolCall.name,
content: [{ type: "text", text: error.message }],
isError: true,
timestamp: Date.now(),
});
}
}
}
完整事件
在助理信件生成过程中释放出的所有流动事件 :
| 事件类型 | 说明 | 密钥属性 |
|---|---|---|
start | 流线开始 | partial: 初始助理信件结构 |
text_start | 文本块开始 | contentIndex: 内容阵列中的位置 |
text_delta | 收到文本块 | delta: 新文本, (中文(简体) ), contentIndex页:1 |
text_end | 文本块完成 | content:全文, contentIndex页:1 |
thinking_start | 思维障碍开始 | contentIndex: 内容阵列中的位置 |
thinking_delta | 思维块收到 | delta: 新文本, (中文(简体) ), contentIndex页:1 |
thinking_end | 思维区块完成 | content: 充分思考, contentIndex页:1 |
toolcall_start | 工具调用开始 | contentIndex: 内容阵列中的位置 |
toolcall_delta | 工具参数流 | deltaJSON块, partial.content[contentIndex].arguments: 部分解析参数 |
toolcall_end | 工具调用完成 | toolCall: 完全验证的工具调用 id, (中文(简体) ), name, (中文(简体) ), arguments |
done | 流线完成 | reason:停止理性("停止","长度","工具使用"), message页:1 |
error | 发生错误 | reason: 出错类型 ("错误"或"被放弃"), error: 有部分内容的助理信息 |
图像输入
有视觉能力的模型可以处理图像. 您可以检查模型是否通过 input 属性。 如果将图像传递给非视觉模型,它们会被默默地忽略.
import * as fs from "node:fs";
import { getModel, complete } from "@f5-sales-demo/pi-ai";
const model = getModel("openai", "gpt-4o-mini");
// Check if model supports images
if (model.input.includes("image")) {
console.log("Model supports vision");
}
const imageBuffer = fs.readFileSync("image.png");
const base64Image = imageBuffer.toBase64();
const response = await complete(model, {
messages: [
{
role: "user",
content: [
{ type: "text", text: "What is in this image?" },
{ type: "image", data: base64Image, mimeType: "image/png" },
],
},
],
});
// Access the response
for (const block of response.content) {
if (block.type === "text") {
console.log(block.text);
}
}
思考/反思
许多模型支持思考/推理能力,在这种能力中,它们可以显示其内部思考过程。 您可以检查模型是否通过 reasoning 属性。 如果将推理选项传递给非理性模式,则被默默地忽略.
统一接口(流程简便/完整简便)
import { getModel, streamSimple, completeSimple } from "@f5-sales-demo/pi-ai";
// Many models across providers support thinking/reasoning
const model = getModel("anthropic", "claude-sonnet-4-20250514");
// or getModel('openai', 'gpt-5-mini');
// or getModel('google', 'gemini-2.5-flash');
// or getModel('xai', 'grok-code-fast-1');
// or getModel('groq', 'openai/gpt-oss-20b');
// or getModel('cerebras', 'gpt-oss-120b');
// or getModel('openrouter', 'z-ai/glm-4.5v');
// Check if model supports reasoning
if (model.reasoning) {
console.log("Model supports reasoning/thinking");
}
// Use the simplified reasoning option
const response = await completeSimple(
model,
{
messages: [{ role: "user", content: "Solve: 2x + 5 = 13" }],
},
{
reasoning: "medium", // 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' (xhigh maps to high on non-OpenAI providers)
}
);
// Access thinking and text blocks
for (const block of response.content) {
if (block.type === "thinking") {
console.log("Thinking:", block.thinking);
} else if (block.type === "text") {
console.log("Response:", block.text);
}
}
供应商 -- -- 特定选项(流程/完成)
对于精细的控件,使用特定提供者选项:
import { getModel, complete } from "@f5-sales-demo/pi-ai";
// OpenAI Reasoning (o1, o3, gpt-5)
const openaiModel = getModel("openai", "gpt-5-mini");
await complete(openaiModel, context, {
reasoningEffort: "medium",
reasoningSummary: "detailed", // OpenAI Responses API only
});
// Anthropic Thinking (Claude Sonnet 4)
const anthropicModel = getModel("anthropic", "claude-sonnet-4-20250514");
await complete(anthropicModel, context, {
thinkingEnabled: true,
thinkingBudgetTokens: 8192, // Optional token limit
});
// Google Gemini Thinking
const googleModel = getModel("google", "gemini-2.5-flash");
await complete(googleModel, context, {
thinking: {
enabled: true,
budgetTokens: 8192, // -1 for dynamic, 0 to disable
},
});
流出思维内容
当流出时,思考内容通过特定事件传递:
const s = streamSimple(model, context, { reasoning: "high" });
for await (const event of s) {
switch (event.type) {
case "thinking_start":
console.log("[Model started thinking]");
break;
case "thinking_delta":
process.stdout.write(event.delta); // Stream thinking content
break;
case "thinking_end":
console.log("\n[Thinking complete]");
break;
}
}
停止原因
每个 AssistantMessage 包含a stopReason 字段,表示该世代如何结束:
"stop"- 正常完成,模型完成反应"length"- 输出达到最高值"toolUse"- 模型调用工具并期望工具结果"error"- 生成时发生错误"aborted"- 请求通过中止信号被取消
处理错误
当请求以出错(包括中止和工具调用错误)而结束时,流出 API 释放出出一个出错事件 :
// In streaming
for await (const event of stream) {
if (event.type === "error") {
// event.reason is either "error" or "aborted"
// event.error is the AssistantMessage with partial content
console.error(`Error (${event.reason}):`, event.error.errorMessage);
console.log("Partial content:", event.error.content);
}
}
// The final message will have the error details
const message = await stream.result();
if (message.stopReason === "error" || message.stopReason === "aborted") {
console.error("Request failed:", message.errorMessage);
// message.content contains any partial content received before the error
// message.usage contains partial token counts and costs
}
中止请求
中止信号允许您取消进行中的请求 。 中止的请求已 stopReason === 'aborted'数字 :
import { getModel, stream } from "@f5-sales-demo/pi-ai";
const model = getModel("openai", "gpt-4o-mini");
// Abort after 2 seconds
const signal = AbortSignal.timeout(2000);
const s = stream(
model,
{
messages: [{ role: "user", content: "Write a long story" }],
},
{
signal,
}
);
for await (const event of s) {
if (event.type === "text_delta") {
process.stdout.write(event.delta);
} else if (event.type === "error") {
// event.reason tells you if it was "error" or "aborted"
console.log(`${event.reason === "aborted" ? "Aborted" : "Error"}:`, event.error.errorMessage);
}
}
// Get results (may be partial if aborted)
const response = await s.result();
if (response.stopReason === "aborted") {
console.log("Request was aborted:", response.errorMessage);
console.log("Partial content received:", response.content);
console.log("Tokens used:", response.usage);
}
中止后继续
中止的消息可以添加到对话上下文中,并在后续请求中继续:
const context = {
messages: [{ role: "user", content: "Explain quantum computing in detail" }],
};
// First request gets aborted after 2 seconds
const controller1 = new AbortController();
setTimeout(() => controller1.abort(), 2000);
const partial = await complete(model, context, { signal: controller1.signal });
// Add the partial response to context
context.messages.push(partial);
context.messages.push({ role: "user", content: "Please continue" });
// Continue the conversation
const continuation = await complete(model, context);
常见流选项
所有提供者接受基准 StreamOptions (除特定供应商选项外):
apiKey: 覆盖提供者 API 密钥headers: 将额外请求头合并到模型定义头上sessionId: 供应商专用会话标识符(即时缓存/路由)signal: 中止飞行请求onPayload: 在发送前立即与供应商一起请求有效载荷
示例
const response = await complete(model, context, {
apiKey: "sk-live",
headers: { "X-Debug-Trace": "true" },
onPayload: (payload) => {
console.log("request payload", payload);
},
});
API、模型和供应商
库执行4个API接口,每个接口都有自己的流式功能和选项:
anthropic-messages: Anthropic 信件 API (英语:streamAnthropic, (中文(简体) ),AnthropicOptions(中文(简体) )google-generative-ai: Google的Generative AI API (英语:streamGoogle, (中文(简体) ),GoogleOptions(中文(简体) )openai-completions: OpenAI的聊天补全API(streamOpenAICompletions, (中文(简体) ),OpenAICompletionsOptions(中文(简体) )openai-responses: OpenAI 的回复 API(streamOpenAIResponses, (中文(简体) ),OpenAIResponsesOptions(中文(简体) )
供应商和模式
A 类 提供者 通过特定的API提供模型. 例如:
- 安东尼 模型使用
anthropic-messagesAPI 密码 - 谷歌 模型使用
google-generative-aiAPI 密码 - 开放AI 模型使用
openai-responsesAPI 密码 - 米斯特拉尔,xAI, Cerebras, Groq等. 模型使用
openai-completionsAPI( 开放AI 兼容)
查询供应商和模式
import { getProviders, getModels, getModel } from "@f5-sales-demo/pi-ai";
// Get all available providers
const providers = getProviders();
console.log(providers); // ['openai', 'anthropic', 'google', 'xai', 'groq', ...]
// Get all models from a provider (fully typed)
const anthropicModels = getModels("anthropic");
for (const model of anthropicModels) {
console.log(`${model.id}: ${model.name}`);
console.log(` API: ${model.api}`); // 'anthropic-messages'
console.log(` Context: ${model.contextWindow} tokens`);
console.log(` Vision: ${model.input.includes("image")}`);
console.log(` Reasoning: ${model.reasoning}`);
}
// Get a specific model (both provider and model ID are auto-completed in IDEs)
const model = getModel("openai", "gpt-4o-mini");
console.log(`Using ${model.name} via ${model.api} API`);
自定义模型
您可以为本地推断服务器或自定义端点创建自定义模型:
为了奥拉玛, OLLAMA_API_KEY 是可选的,主要用于认证/自我托管的网关.
import { Model, stream } from "@f5-sales-demo/pi-ai";
// Example: Ollama using OpenAI-compatible API
const ollamaModel: Model<"openai-completions"> = {
id: "llama-3.1-8b",
name: "Llama 3.1 8B (Ollama)",
api: "openai-completions",
provider: "ollama",
baseUrl: "http://localhost:11434/v1",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 32000,
};
// Example: LiteLLM proxy with explicit compat settings
const litellmModel: Model<"openai-completions"> = {
id: "gpt-4o",
name: "GPT-4o (via LiteLLM)",
api: "openai-completions",
provider: "litellm",
baseUrl: "http://localhost:4000/v1",
reasoning: false,
input: ["text", "image"],
cost: { input: 2.5, output: 10, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 16384,
compat: {
supportsStore: false, // LiteLLM doesn't support the store field
},
};
// Example: Custom endpoint with headers (bypassing Cloudflare bot detection)
const proxyModel: Model<"anthropic-messages"> = {
id: "claude-sonnet-4",
name: "Claude Sonnet 4 (Proxied)",
api: "anthropic-messages",
provider: "custom-proxy",
baseUrl: "https://proxy.example.com/v1",
reasoning: true,
input: ["text", "image"],
cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
contextWindow: 200000,
maxTokens: 8192,
headers: {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
"X-Custom-Auth": "bearer-token-here",
},
};
// Use the custom model
const response = await stream(ollamaModel, context, {
apiKey: process.env.OLLAMA_API_KEY, // Optional; local Ollama usually runs without auth
});
OpenAI 兼容性设置
这个 openai-completions API由许多提供商实施,两者差别不大. 默认情况下,基于 baseUrl 用于已知供应商(Cerebras、xAI、Mistral、Chutes等)。 对于自定义代理或未知的端点,您可以通过 compat 字段 :
interface OpenAICompat {
supportsStore?: boolean; // Whether provider supports the `store` field (default: true)
supportsDeveloperRole?: boolean; // Whether provider supports `developer` role vs `system` (default: true)
supportsReasoningEffort?: boolean; // Whether provider supports `reasoning_effort` (default: true)
maxTokensField?: "max_completion_tokens" | "max_tokens"; // Which field name to use (default: max_completion_tokens)
extraBody?: Record<string, unknown>; // Extra request-body fields for custom proxy routing or provider-specific options
}
若为 compat 未设置,库会返回基于 URL 的检测。 若为 compat 未指定字段使用检测到的默认值。 这对于:
- LiteLLM 代词: 可能不支持
store字段 - 自定义推断服务器: 可使用非标准字段名称
- 自办终点: 可能有不同的特性支持
类型安全
模型按其API打入,确保类型安全选项:
// TypeScript knows this is an Anthropic model
const claude = getModel("anthropic", "claude-sonnet-4-20250514");
// So these options are type-checked for AnthropicOptions
await stream(claude, context, {
thinkingEnabled: true, // ✓ Valid for anthropic-messages
thinkingBudgetTokens: 2048, // ✓ Valid for anthropic-messages
// reasoningEffort: 'high' // ✗ TypeScript error: not valid for anthropic-messages
});
交叉处理
库支持不同LLM提供者在同一对话中进行无缝的交接. 这使得您可以在保存上下文的同时切换模型中的对话,包括思维块,工具调用,以及工具结果.
如何运作
当一个提供者发送到另一个提供者的邮件时,库会自动变换,以便兼容:
- 用户和工具结果信息 转经未变
- 同一供应商/API的助理信息 保存为
- 来自不同供应商的助理信息 将思维块转换为文本
<thinking>标记 - 工具和正则文本 保持不变
示例:多服务商对话
import { getModel, complete, Context } from "@f5-sales-demo/pi-ai";
// Start with Claude
const claude = getModel("anthropic", "claude-sonnet-4-20250514");
const context: Context = {
messages: [],
};
context.messages.push({ role: "user", content: "What is 25 * 18?" });
const claudeResponse = await complete(claude, context, {
thinkingEnabled: true,
});
context.messages.push(claudeResponse);
// Switch to GPT-5 - it will see Claude's thinking as <thinking> tagged text
const gpt5 = getModel("openai", "gpt-5-mini");
context.messages.push({ role: "user", content: "Is that calculation correct?" });
const gptResponse = await complete(gpt5, context);
context.messages.push(gptResponse);
// Switch to Gemini
const gemini = getModel("google", "gemini-2.5-flash");
context.messages.push({ role: "user", content: "What was the original question?" });
const geminiResponse = await complete(gemini, context);
提供者兼容性
所有提供者可以处理其他提供者发来的信息,包括:
- 文本内容
- 工具调用和工具结果(包括工具结果中的图像)
- 思考/推理块(转换为标有标签的文本,以便交叉提供者兼容)
- 有部分内容的中止信件
这使得工作流程灵活,可以:
- 以快速模式启动初始响应
- 切换到更有能力的复杂推理模型
- 为特定任务使用专门模型
- 在供应商停电期间保持对话连续性
背景序列化
这个 Context 对象可以很容易地使用标准的JSON方法进行序列化和去序列化,使得持续对话,执行聊天历史,或者在服务间传输上下文变得简单:
import { Context, getModel, complete } from "@f5-sales-demo/pi-ai";
// Create and use a context
const context: Context = {
systemPrompt: "You are a helpful assistant.",
messages: [{ role: "user", content: "What is TypeScript?" }],
};
const model = getModel("openai", "gpt-4o-mini");
const response = await complete(model, context);
context.messages.push(response);
// Serialize the entire context
const serialized = JSON.stringify(context);
console.log("Serialized context size:", serialized.length, "bytes");
// Save to database, localStorage, file, etc.
localStorage.setItem("conversation", serialized);
// Later: deserialize and continue the conversation
const restored: Context = JSON.parse(localStorage.getItem("conversation")!);
restored.messages.push({ role: "user", content: "Tell me more about its type system" });
// Continue with any model
const newModel = getModel("anthropic", "claude-haiku-4-5-20251001");
const continuation = await complete(newModel, restored);
说明:如果上下文包含图像(如图像输入部分所示被编码为base64),这些图像也会被序列化.
浏览器使用
库支持浏览器环境. 您必须明确通过 API 密钥, 因为浏览器中没有环境变量 :
import { getModel, complete } from "@f5-sales-demo/pi-ai";
// API key must be passed explicitly in browser
const model = getModel("anthropic", "claude-haiku-4-5-20251001");
const response = await complete(
model,
{
messages: [{ role: "user", content: "Hello!" }],
},
{
apiKey: "your-api-key",
}
);
安全警告:在前端代码中曝光API密钥是危险的. 任何人都可以提取和滥用你的钥匙。 仅用于内部工具或演示。 对于生产应用程序,使用一个后端代理来保证您的API密钥的安全.
环境变量(仅节点.js)
在Node.js环境中,可以设置环境变量来避免通过API密钥:
| 提供者 | 环境变量 |
|---|---|
| 开放AI | OPENAI_API_KEY |
| 安东尼 | ANTHROPIC_API_KEY 或者说 ANTHROPIC_OAUTH_TOKEN (或 ANTHROPIC_FOUNDRY_API_KEY 何时 CLAUDE_CODE_USE_FOUNDRY=true(中文(简体) ) |
| 谷歌 | GEMINI_API_KEY |
| Vertex 大赦国际 | GOOGLE_CLOUD_PROJECT (或 GCLOUD_PROJECT页:1 GOOGLE_CLOUD_LOCATION + ADC 组合键 |
| 迷雾 | MISTRAL_API_KEY |
| 格罗克 | GROQ_API_KEY |
| 心管 | CEREBRAS_API_KEY |
| 一起来 | TOGETHER_API_KEY |
| 钱范 | QIANFAN_API_KEY |
| 拥抱相貌 | HUGGINGFACE_HUB_TOKEN 或者说 HF_TOKEN |
| 合成 | SYNTHETIC_API_KEY |
| 纳米比亚 | NVIDIA_API_KEY |
| 纳诺GPT | NANO_GPT_API_KEY |
| 威尼斯语 | VENICE_API_KEY |
| 月光拍摄 | MOONSHOT_API_KEY |
| 页:1 大赦国际 | XAI_API_KEY |
| 打开路透社 | OPENROUTER_API_KEY |
| 列表 | LITELLM_API_KEY |
| 奥拉玛 | OLLAMA_API_KEY (当地部署可选) |
| Quen 门户网站 | QWEN_OAUTH_TOKEN 或者说 QWEN_PORTAL_API_KEY |
| 扎西岛 | ZAI_API_KEY |
| 迷你麦克斯码 | MINIMAX_CODE_API_KEY (国际)或 MINIMAX_CODE_CN_API_KEY (中国) |
| 小美,小美,小美 | XIAOMI_API_KEY |
| 禅穆 | ZENMUX_API_KEY |
| vLLM 数据 | VLLM_API_KEY |
| Cloudflare AI 网关 | CLOUDFLARE_AI_GATEWAY_API_KEY |
| GitHub 副驾驶 | COPILOT_GITHUB_TOKEN 或者说 GH_TOKEN 或者说 GITHUB_TOKEN |
对于 Cloudflare AI 网关模型,使用提供者基础 URL 格式
https://gateway.ai.cloudflare.com/v1/<account>/<gateway>/anthropic。 。 。 .
用于Anthropic 创始路线,设置 CLAUDE_CODE_USE_FOUNDRY=true 加:
FOUNDRY_BASE_URL, (中文(简体) ), ANTHROPIC_FOUNDRY_API_KEY选项 ANTHROPIC_CUSTOM_HEADERS, (中文(简体) ),
和可选mTLS材料(CLAUDE_CODE_CLIENT_CERT, (中文(简体) ), CLAUDE_CODE_CLIENT_KEY, (中文(简体) ), NODE_EXTRA_CA_CERTS) (中文(简体) ).
当前 OpenAI 兼容集成的提供者端点默认 :
- 一起来:
https://api.together.xyz/v1 - 月光拍摄:
https://api.moonshot.ai/v1 - Q:
https://qianfan.baidubce.com/v2 - 尼维迪娅:
https://integrate.api.nvidia.com/v1 - 纳诺GPT:
https://nano-gpt.com/api/v1 - 拥抱相貌推论 :
https://router.huggingface.co/v1 - 威尼斯语:
https://api.venice.ai/api/v1 - 晓美美摩:
https://api.xiaomimimo.com/anthropic - ZenMux(开放AI):
https://zenmux.ai/api/v1 - ZenMux( 人类模型):
https://zenmux.ai/api/anthropic - vLLM : (英语)
http://127.0.0.1:8000/v1 - Ollama: 本地 OpenAI 兼容运行时间
- 文学LM:
http://localhost:4000/v1 - Cloudflare AI网关:
https://gateway.ai.cloudflare.com/v1/<account>/<gateway>/anthropic - Quen门户网站:
https://portal.qwen.ai/v1设定时,库会自动使用这些密钥:
// Uses OPENAI_API_KEY from environment
const model = getModel("openai", "gpt-4o-mini");
const response = await complete(model, context);
// Or override with explicit key
const response = await complete(model, context, {
apiKey: "sk-different-key",
});
正在检查环境变量
import { getEnvApiKey } from "@f5-sales-demo/pi-ai";
// Check if an API key is set in environment variables
const key = getEnvApiKey("openai"); // checks OPENAI_API_KEY
OAuth 供应商
多个提供者支持 OAuth 认证( 有的还支持静态 API 密钥):
- 安东尼 (赞成/最大订阅)
- GitHub 副驾驶 (飞行员订阅)
- 谷歌双子座CLI (Gemini 2.0/2.5通过 Google Cloud Code Assistance;免费分级或付费订阅)
- 反重力 (自由双子座3,克洛德,GPT-OSS通过谷歌云)
- Quen 门户网站 (Quen OAuth 令牌或 API 密钥)
对于付费的云码 协助订阅,设置 GOOGLE_CLOUD_PROJECT 或者说 GOOGLE_CLOUD_PROJECT_ID 到您的项目标识.
Vertex AI (ADC) (英语)
Vertex AI模型使用应用程序默认证书(ADC):
- 地方发展运行
gcloud auth application-default login - CI/生产设置
GOOGLE_APPLICATION_CREDENTIALS指向服务账户 JSON 密钥文件
也设定 GOOGLE_CLOUD_PROJECT (或 GCLOUD_PROJECT和 GOOGLE_CLOUD_LOCATION。也可以通过 project编号 :location 在呼叫选项中.
示例
# Local (uses your user credentials)
gcloud auth application-default login
export GOOGLE_CLOUD_PROJECT="my-project"
export GOOGLE_CLOUD_LOCATION="us-central1"
# CI/Production (service account key file)
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account.json"
import { getModel, complete } from "@f5-sales-demo/pi-ai";
(async () => {
const model = getModel("google-vertex", "gemini-2.5-flash");
const response = await complete(model, {
messages: [{ role: "user", content: "Hello from Vertex AI" }],
});
for (const block of response.content) {
if (block.type === "text") console.log(block.text);
}
})().catch(console.error);
官方文件: 应用程序默认证书
CLI 登录
最快速的认证方式:
bunx @f5-sales-demo/pi-ai login # interactive provider selection
bunx @f5-sales-demo/pi-ai login anthropic # login to specific provider
bunx @f5-sales-demo/pi-ai login vllm # store vLLM API key (or placeholder for local no-auth)
bunx @f5-sales-demo/pi-ai list # list available providers
证书保存到 agent.db 在代理目录中. /login qianfan 打开 Qianfan 控制台并存储已粘贴的 API 密钥.
login 支持OAuth提供商(Anthropic,GitHub Copilot,双子座CLI,Antigravity)和API-键入载流.
OpenAI有两个认证路径. 使用 OPENAI_API_KEY 用于基于用户的平台API访问。 使用 login openai-codex 为ChatGPT Plus/Pro订阅 OAuth;SSH/无头终端自动接收到一个短设备代码,而本地桌面则使用浏览器PKCE. login openai 解释 API 键路径 .
对于目前OpenAI相容的集成,API-键入载封面相通,月光相通,相相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相通相 Ollama 通常是本地的,未经认证; 设置 OLLAMA_API_KEY 只有在你的奥拉马部署执行无记名认证时.
程序操作
库提供登录和令牌刷新功能. 信用存储是呼叫者的责任.
import {
// Login functions (return credentials, do not store)
loginAnthropic,
loginGitHubCopilot,
loginGeminiCli,
loginAntigravity,
loginCloudflareAiGateway,
loginHuggingface,
loginLiteLLM,
loginMoonshot,
loginNvidia,
loginNanoGPT,
loginQianfan,
loginQwenPortal,
loginTogether,
loginVenice,
loginVllm,
loginXiaomi,
// Token management
refreshOAuthToken, // (provider, credentials) => new credentials
getOAuthApiKey, // (provider, credentialsMap) => { newCredentials, apiKey } | null
// Types
type OAuthProvider,
type OAuthCredentials,
} from "@f5-sales-demo/pi-ai";
登录流程示例
import { loginGitHubCopilot } from "@f5-sales-demo/pi-ai";
import * as fs from "node:fs";
const credentials = await loginGitHubCopilot({
onAuth: (url, instructions) => {
console.log(`Open: ${url}`);
if (instructions) console.log(instructions);
},
onPrompt: async (prompt) => {
return await getUserInput(prompt.message);
},
onProgress: (message) => console.log(message),
});
// Store credentials yourself
const auth = { "github-copilot": { type: "oauth", ...credentials } };
fs.writeFileSync("credentials.json", JSON.stringify(auth, null, 2));
使用 OAuth 键
使用 getOAuthApiKey() 以获取 API 密钥,如果过期,则自动刷新 :
import { getModel, complete, getOAuthApiKey } from "@f5-sales-demo/pi-ai";
import * as fs from "node:fs";
// Load your stored credentials
const auth = JSON.parse(fs.readFileSync("credentials.json", "utf-8"));
// Get API key (refreshes if expired)
const result = await getOAuthApiKey("github-copilot", auth);
if (!result) throw new Error("Not logged in");
// Save refreshed credentials
auth["github-copilot"] = { type: "oauth", ...result.newCredentials };
fs.writeFileSync("credentials.json", JSON.stringify(auth, null, 2));
// Use the API key
const model = getModel("github-copilot", "gpt-4o");
const response = await complete(
model,
{
messages: [{ role: "user", content: "Hello!" }],
},
{ apiKey: result.apiKey }
);
提供方说明
开放AI设置 OPENAI_API_KEY 用于基于用户的平台 API 访问,或使用 openai-codex ChatGPT Plus/Pro订阅访问的 OAuth 提供者. 远程终端使用设备编码认证,不设隧道或本地host召回.
GitHub 副驾驶:如果得到"请求的模型不支持"错误,则在VS代码中手动启用模型:打开Copilot Chat,点击模型选择器,选择模型(警告图标),并点击"Enable".
谷歌双子座 CLI / 反重力:这些使用Google Cloud OAuth. 这个 apiKey 返回时间 getOAuthApiKey() 是一个 JSON 字符串,既包含符号,又包含项目ID,由库自动处理.
许可证
麻省理工学院