摘要:拆解 openai-node 九大设计亮点------惰性 Promise、SSE 三层流水线、零拷贝流分裂、自动分页迭代器、工厂错误体系等,带你领略工业级 TypeScript SDK 的架构之美。
一、引言 🚀
作为一名长期关注 AI 应用开发的工程师,我一直在寻找一个值得深入学习的 SDK 代码库。直到我打开了 openai-node 的源码,发现这不仅是一个 API 封装库,更是一本关于"如何构建工业级 TypeScript SDK"的教科书。
为什么选择分析 openai-node?
- 官方出品:这是 OpenAI 官方维护的 TypeScript/JavaScript SDK
- Stainless 生成 :由 Stainless 代码生成平台构建,代表了现代 API SDK 的最佳实践
- 跨平台支持:支持 Node.js / Browser / Deno / Cloudflare Workers / Vercel Edge,是真正的"universal" SDK
- 设计精良:大量使用高级 TypeScript 特性,设计模式运用得当
本文分析的版本是 6.32.0,让我们一起揭开这个优秀 SDK 的神秘面纱。
二、架构全景:五层分层设计 🏗️
在深入细节之前,我们先鸟瞰整体架构。openai-node 采用了清晰的五层分层设计:
分层职责:
| 层级 | 职责 | 核心组件 |
|---|---|---|
| 用户接口层 | 提供友好的 API 调用接口 | OpenAI, client.chat.completions |
| 核心抽象层 | 封装通用的请求/响应处理 | APIPromise, Stream, AbstractPage |
| 高级助手层 | 提供流式处理、工具调用等高级功能 | ChatCompletionStream, runTools() |
| 内部基础设施层 | 处理请求构建、响应解析、平台适配 | buildRequest(), buildHeaders() |
| 传输层 | 实际的网络通信 | fetch, WebSocket |
设计洞察:每层职责清晰、依赖单向向下,这种分层让代码高度可测试、可扩展。比如 Azure 适配只需要在用户接口层做很小的改动,底层完全复用。
三、设计亮点一:惰性 Promise --- APIPromise 的精妙设计 ✨
问题引入
传统 Promise 在创建时就开始执行,但 API 响应的解析方式多种多样:
- 有时候需要 JSON 解析
- 有时候需要流式处理
- 有时候只要原始 Response 对象
- 有时候需要同时拿到数据和 headers
如何在不牺牲类型安全的前提下,提供这种灵活性?
解决方案
APIPromise 继承自 Promise,但构造函数中 resolve(null) ,不触发解析。只有在 .then/.catch/.finally 被调用时才真正解析响应。
源码分析
typescript
// src/core/api-promise.ts
export class APIPromise<T> extends Promise<WithRequestID<T>> {
private parsedPromise: Promise<WithRequestID<T>> | undefined;
#client: OpenAI;
constructor(
client: OpenAI,
private responsePromise: Promise<APIResponseProps>,
private parseResponse: (
client: OpenAI,
props: APIResponseProps,
) => PromiseOrValue<WithRequestID<T>> = defaultParseResponse,
) {
super((resolve) => {
// 关键!这里 resolve(null) 而不是立即解析
// 这让 Promise 处于 fulfilled 状态,但实际解析被延迟
resolve(null as any);
});
this.#client = client;
}
// 惰性解析:只在第一次需要时才解析,且结果被缓存
private parse(): Promise<WithRequestID<T>> {
if (!this.parsedPromise) {
this.parsedPromise = this.responsePromise.then((data) =>
this.parseResponse(this.#client, data),
) as any as Promise<WithRequestID<T>>;
}
return this.parsedPromise;
}
// 重写 then/catch/finally,重定向到 parse()
override then<TResult1 = WithRequestID<T>, TResult2 = never>(
onfulfilled?: ((value: WithRequestID<T>) => TResult1 | PromiseLike<TResult1>) | undefined | null,
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null,
): Promise<TResult1 | TResult2> {
return this.parse().then(onfulfilled, onrejected);
}
// 获取原始 Response(不解析 body)
asResponse(): Promise<Response> {
return this.responsePromise.then((p) => p.response);
}
// 同时获取解析数据 + 原始 Response + request_id
async withResponse(): Promise<{ data: T; response: Response; request_id: string | null }> {
const [data, response] = await Promise.all([this.parse(), this.asResponse()]);
return { data, response, request_id: response.headers.get('x-request-id') };
}
// 支持响应字段提取变换
_thenUnwrap<U>(transform: (data: T, props: APIResponseProps) => U): APIPromise<U> {
return new APIPromise(this.#client, this.responsePromise, async (client, props) =>
addRequestID(transform(await this.parseResponse(client, props), props), props.response),
);
}
}
使用方式展示
typescript
// 标准用法 - 自动解析 JSON
const completion = await client.chat.completions.create({
model: 'gpt-4o',
messages: [{ role: 'user', content: 'Hello!' }],
});
// 获取原始 Response(不解析 body)
const response = await client.chat.completions.create({...}).asResponse();
console.log(response.status);
console.log(response.headers.get('x-request-id'));
// 同时获取数据和元信息
const { data, response, request_id } = await client.chat.completions.create({...}).withResponse();
console.log(`Request ID: ${request_id}`);
console.log(`Data: ${data.choices[0].message.content}`);
// 字段提取变换(内部使用)
const unwrapped = promise._thenUnwrap((data) => data.choices[0].message);
设计洞察
这种"惰性 Promise"模式解决了一个常见矛盾:想获取 headers 但又想要类型安全的解析结果。
传统做法是返回一个包含 data 和 response 的对象,但这破坏了直接 await 的简洁性。APIPromise 通过继承 Promise 并重写方法,既保留了 await 的简洁,又提供了 asResponse() 和 withResponse() 两个逃生通道。
这是我见过最优雅的 API 响应处理设计。
四、设计亮点二:SSE 流处理 --- 三层流水线架构 🌊
问题引入
OpenAI API 的流式响应使用 SSE (Server-Sent Events) 格式:
vbnet
event: message
data: {"id": "chatcmpl-xxx", "choices": [{"delta": {"content": "Hello"}}]}
event: message
data: {"id": "chatcmpl-xxx", "choices": [{"delta": {"content": " World"}}]}
data: [DONE]
处理这种格式需要:
- 从
ReadableStream读取二进制块 - 按
\n\n分割成 SSE 消息 - 解析
event:和data:字段 - 同时支持
AsyncIterable接口
三层流水线
源码分析
第一层:iterSSEChunks --- 二进制块累积
typescript
// src/core/streaming.ts
async function* iterSSEChunks(iterator: AsyncIterableIterator<Bytes>): AsyncGenerator<Uint8Array> {
let data = new Uint8Array();
for await (const chunk of iterator) {
if (chunk == null) continue;
// 统一转为 Uint8Array
const binaryChunk =
chunk instanceof ArrayBuffer ? new Uint8Array(chunk)
: typeof chunk === 'string' ? encodeUTF8(chunk)
: chunk;
// 累积到 buffer
let newData = new Uint8Array(data.length + binaryChunk.length);
newData.set(data);
newData.set(binaryChunk, data.length);
data = newData;
// 扫描双换行边界(\n\n 或 \r\n\r\n)
let patternIndex;
while ((patternIndex = findDoubleNewlineIndex(data)) !== -1) {
yield data.slice(0, patternIndex);
data = data.slice(patternIndex);
}
}
if (data.length > 0) {
yield data;
}
}
第二层:LineDecoder --- 按行分割
typescript
// src/internal/decoders/line.ts
export class LineDecoder {
#buffer: Uint8Array;
#carriageReturnIndex: number | null;
constructor() {
this.#buffer = new Uint8Array();
this.#carriageReturnIndex = null;
}
decode(chunk: Bytes): string[] {
// 处理 \n 和 \r\n 两种换行格式
// 使用 #carriageReturnIndex 处理跨块的 \r\n
// ...
}
flush(): string[] {
if (!this.#buffer.length) return [];
return this.decode('\n');
}
}
第三层:SSEDecoder --- 状态机解析
typescript
// src/core/streaming.ts
class SSEDecoder {
private data: string[];
private event: string | null;
private chunks: string[];
decode(line: string) {
// 处理行尾 \r
if (line.endsWith('\r')) {
line = line.substring(0, line.length - 1);
}
// 空行表示消息结束
if (!line) {
if (!this.event && !this.data.length) return null;
const sse: ServerSentEvent = {
event: this.event,
data: this.data.join('\n'), // 多行 data 拼接
raw: this.chunks,
};
// 重置状态
this.event = null;
this.data = [];
this.chunks = [];
return sse;
}
this.chunks.push(line);
// 注释行(以 : 开头)
if (line.startsWith(':')) return null;
// 解析 field: value
let [fieldname, _, value] = partition(line, ':');
if (value.startsWith(' ')) value = value.substring(1);
if (fieldname === 'event') {
this.event = value;
} else if (fieldname === 'data') {
this.data.push(value);
}
return null;
}
}
Stream 类设计
typescript
// src/core/streaming.ts
export class Stream<Item> implements AsyncIterable<Item> {
controller: AbortController;
constructor(
private iterator: () => AsyncIterator<Item>,
controller: AbortController,
client?: OpenAI,
) {
this.controller = controller;
}
// 工厂方法:从 SSE Response 创建 Stream
static fromSSEResponse<Item>(
response: Response,
controller: AbortController,
client?: OpenAI,
): Stream<Item> {
let consumed = false;
async function* iterator(): AsyncIterator<Item> {
if (consumed) {
throw new OpenAIError('Cannot iterate over a consumed stream, use `.tee()` to split the stream.');
}
consumed = true;
let done = false;
try {
for await (const sse of _iterSSEMessages(response, controller)) {
if (done) continue;
if (sse.data.startsWith('[DONE]')) {
done = true;
continue;
}
// 解析 JSON,处理 error 事件
const data = JSON.parse(sse.data);
if (data?.error) {
throw new APIError(undefined, data.error, undefined, response.headers);
}
yield data;
}
} catch (e) {
if (isAbortError(e)) return;
throw e;
} finally {
if (!done) controller.abort();
}
}
return new Stream(iterator, controller, client);
}
// 零拷贝流分裂
tee(): [Stream<Item>, Stream<Item>] {
const left: Array<Promise<IteratorResult<Item>>> = [];
const right: Array<Promise<IteratorResult<Item>>> = [];
const iterator = this.iterator();
const teeIterator = (queue: Array<Promise<IteratorResult<Item>>>): AsyncIterator<Item> => {
return {
next: () => {
if (queue.length === 0) {
const result = iterator.next();
left.push(result); // 两个队列共享同一个 Promise
right.push(result);
}
return queue.shift()!;
},
};
};
return [
new Stream(() => teeIterator(left), this.controller),
new Stream(() => teeIterator(right), this.controller),
];
}
// 转为 NDJSON ReadableStream(用于服务端转发)
toReadableStream(): ReadableStream {
const self = this;
let iter: AsyncIterator<Item>;
return makeReadableStream({
async start() {
iter = self[Symbol.asyncIterator]();
},
async pull(ctrl: any) {
const { value, done } = await iter.next();
if (done) return ctrl.close();
ctrl.enqueue(encodeUTF8(JSON.stringify(value) + '\n'));
},
async cancel() {
await iter.return?.();
},
});
}
[Symbol.asyncIterator](): AsyncIterator<Item> {
return this.iterator();
}
}
使用方式
typescript
// 基本流式消费
const stream = await client.chat.completions.create({
model: 'gpt-4o',
messages: [{ role: 'user', content: 'Write a poem' }],
stream: true,
});
for await (const chunk of stream) {
process.stdout.write(chunk.choices[0]?.delta?.content || '');
}
// 流分裂:一份用于日志,一份用于业务处理
const [logStream, bizStream] = stream.tee();
// 日志记录
(async () => {
for await (const chunk of logStream) {
console.log('[LOG]', JSON.stringify(chunk));
}
})();
// 业务处理
for await (const chunk of bizStream) {
// 处理业务逻辑
}
// 转为 ReadableStream 用于 HTTP 响应转发
return new Response(stream.toReadableStream(), {
headers: { 'Content-Type': 'application/x-ndjson' },
});
设计洞察
流水线模式让每一层职责单一:
iterSSEChunks只负责找\n\n边界LineDecoder只负责按\n分行SSEDecoder只负责解析 SSE 协议
tee() 的实现尤其精妙:两个队列共享同一个底层迭代器的 Promise,避免了数据复制。只有当两个消费者速度不一致时,才会在各自队列中暂存未消费的 Promise。
五、设计亮点三:资源模式 --- 优雅的命名空间映射 📦
设计理念
每个 API endpoint 映射为一个 APIResource 子类,通过属性嵌套形成层级命名空间:
typescript
client.chat.completions.create()
client.files.create()
client.beta.assistants.create()
APIResource 基类
这可能是整个 SDK 中最简洁的类:
typescript
// src/core/resource.ts
export abstract class APIResource {
protected _client: OpenAI;
constructor(client: OpenAI) {
this._client = client;
}
}
只有 12 行代码!所有子类通过 this._client.post() 等方法发起请求。
资源实例化
在 Client 构造函数中一次性创建所有资源实例:
typescript
// src/client.ts
export class OpenAI {
completions: API.Completions = new API.Completions(this);
chat: API.Chat = new API.Chat(this);
embeddings: API.Embeddings = new API.Embeddings(this);
files: API.Files = new API.Files(this);
images: API.Images = new API.Images(this);
audio: API.Audio = new API.Audio(this);
moderations: API.Moderations = new API.Moderations(this);
models: API.Models = new API.Models(this);
fineTuning: API.FineTuning = new API.FineTuning(this);
beta: API.Beta = new API.Beta(this);
batches: API.Batches = new API.Batches(this);
uploads: API.Uploads = new API.Uploads(this);
responses: API.Responses = new API.Responses(this);
realtime: API.Realtime = new API.Realtime(this);
// ... 20+ resources
}
嵌套资源
Chat 类包含 Completions 子资源:
typescript
// src/resources/chat/completions/completions.ts
export class Completions extends APIResource {
messages: MessagesAPI.Messages = new MessagesAPI.Messages(this._client);
create(body: ChatCompletionCreateParamsNonStreaming, options?: RequestOptions): APIPromise<ChatCompletion>;
create(body: ChatCompletionCreateParamsStreaming, options?: RequestOptions): APIPromise<Stream<ChatCompletionChunk>>;
create(body: ChatCompletionCreateParamsBase, options?: RequestOptions): APIPromise<Stream<ChatCompletionChunk> | ChatCompletion>;
create(body: ChatCompletionCreateParams, options?: RequestOptions): APIPromise<ChatCompletion> | APIPromise<Stream<ChatCompletionChunk>> {
return this._client.post('/chat/completions', { body, ...options, stream: body.stream ?? false });
}
}
类型安全的流式重载
注意 create 方法的四个重载签名!这是 TypeScript 函数重载的典型应用:
typescript
// stream: false 或 undefined → 返回 APIPromise<ChatCompletion>
const completion = await client.chat.completions.create({
model: 'gpt-4o',
messages: [...],
stream: false, // 或不传
});
// TypeScript 推断 completion 类型为 ChatCompletion
// stream: true → 返回 APIPromise<Stream<ChatCompletionChunk>>
const stream = await client.chat.completions.create({
model: 'gpt-4o',
messages: [...],
stream: true,
});
// TypeScript 推断 stream 类型为 Stream<ChatCompletionChunk>
设计洞察
这种资源模式让 API 调用像操作对象属性一样自然:client.chat.completions.create() 读起来就像 "client 的 chat 的 completions 的 create 方法"。
TypeScript 重载让流式/非流式返回类型在编译期就确定,而不是运行时通过类型断言。这是真正的类型安全。
六、设计亮点四:自动分页 --- PagePromise 的双重身份 📄
问题引入
分页 API 需要手动管理页码/游标:
typescript
// 传统做法:手动翻页
let page = await client.models.list();
while (page.hasNextPage()) {
for (const model of page.data) {
console.log(model.id);
}
page = await page.getNextPage();
}
能否让用户像遍历数组一样遍历所有数据?
解决方案
PagePromise 同时继承 APIPromise 和实现 AsyncIterable --- 可以 await 获取单页,也可以 for-await 遍历所有。
源码分析
AbstractPage:分页基类
typescript
// src/core/pagination.ts
export abstract class AbstractPage<Item> implements AsyncIterable<Item> {
#client: OpenAI;
protected options: FinalRequestOptions;
protected response: Response;
protected body: unknown;
// 子类实现:获取当前页的数据
abstract getPaginatedItems(): Item[];
// 子类实现:构造下一页的请求参数
abstract nextPageRequestOptions(): PageRequestOptions | null;
hasNextPage(): boolean {
const items = this.getPaginatedItems();
if (!items.length) return false;
return this.nextPageRequestOptions() != null;
}
async getNextPage(): Promise<this> {
const nextOptions = this.nextPageRequestOptions();
if (!nextOptions) {
throw new OpenAIError('No next page expected');
}
return await this.#client.requestAPIList(this.constructor as any, nextOptions);
}
// 遍历所有页
async *iterPages(): AsyncGenerator<this> {
let page: this = this;
yield page;
while (page.hasNextPage()) {
page = await page.getNextPage();
yield page;
}
}
// 遍历所有数据项
async *[Symbol.asyncIterator](): AsyncGenerator<Item> {
for await (const page of this.iterPages()) {
for (const item of page.getPaginatedItems()) {
yield item;
}
}
}
}
CursorPage:游标分页实现
typescript
// src/core/pagination.ts
export class CursorPage<Item extends { id: string }> extends AbstractPage<Item> {
data: Array<Item>;
has_more: boolean;
getPaginatedItems(): Item[] {
return this.data ?? [];
}
override hasNextPage(): boolean {
if (this.has_more === false) return false;
return super.hasNextPage();
}
nextPageRequestOptions(): PageRequestOptions | null {
const data = this.getPaginatedItems();
const id = data[data.length - 1]?.id; // 用最后一项的 id 作为游标
if (!id) return null;
return {
...this.options,
query: { ...this.options.query, after: id },
};
}
}
PagePromise:桥接 Promise 和 AsyncIterable
typescript
// src/core/pagination.ts
export class PagePromise<PageClass extends AbstractPage<Item>, Item>
extends APIPromise<PageClass>
implements AsyncIterable<Item>
{
constructor(
client: OpenAI,
request: Promise<APIResponseProps>,
Page: new (...args: ConstructorParameters<typeof AbstractPage>) => PageClass,
) {
super(
client,
request,
async (client, props) =>
new Page(client, props.response, await defaultParseResponse(client, props), props.options),
);
}
// 实现 AsyncIterable:自动遍历所有页的所有数据
async *[Symbol.asyncIterator](): AsyncGenerator<Item> {
const page = await this; // 先 await 获取第一页
for await (const item of page) { // 再遍历所有数据
yield item;
}
}
}
使用方式
typescript
// 方式一:自动遍历所有页的所有数据(最简洁)
for await (const model of client.models.list()) {
console.log(model.id);
}
// 方式二:手动翻页(需要更多控制时)
const page = await client.models.list();
console.log(page.data); // 第一页数据
if (page.hasNextPage()) {
const nextPage = await page.getNextPage();
console.log(nextPage.data); // 第二页数据
}
// 方式三:遍历所有页(需要页级元信息时)
for await (const page of (await client.models.list()).iterPages()) {
console.log(`Page has ${page.data.length} items`);
}
设计洞察
PagePromise 的双重身份让分页 API 的使用变得极其灵活:
- 简单场景:
for await (const item of client.models.list())一行搞定 - 复杂场景:
await client.models.list()拿到 Page 对象后手动控制
这种设计让简单的事情保持简单,复杂的事情成为可能。
七、设计亮点五:错误处理 --- 工厂方法 + 类型体操 ⚠️
错误层级
工厂方法模式
typescript
// src/core/error.ts
export class APIError<
TStatus extends number | undefined = number | undefined,
THeaders extends Headers | undefined = Headers | undefined,
TError extends Object | undefined = Object | undefined,
> extends OpenAIError {
readonly status: TStatus;
readonly headers: THeaders;
readonly error: TError;
readonly code: string | null | undefined;
readonly param: string | null | undefined;
readonly type: string | undefined;
readonly requestID: string | null | undefined;
// 工厂方法:根据 HTTP 状态码创建对应的错误子类
static generate(
status: number | undefined,
errorResponse: Object | undefined,
message: string | undefined,
headers: Headers | undefined,
): APIError {
if (!status || !headers) {
return new APIConnectionError({ message, cause: castToError(errorResponse) });
}
const error = (errorResponse as Record<string, any>)?.['error'];
if (status === 400) return new BadRequestError(status, error, message, headers);
if (status === 401) return new AuthenticationError(status, error, message, headers);
if (status === 403) return new PermissionDeniedError(status, error, message, headers);
if (status === 404) return new NotFoundError(status, error, message, headers);
if (status === 409) return new ConflictError(status, error, message, headers);
if (status === 422) return new UnprocessableEntityError(status, error, message, headers);
if (status === 429) return new RateLimitError(status, error, message, headers);
if (status >= 500) return new InternalServerError(status, error, message, headers);
return new APIError(status, error, message, headers);
}
}
// 具体错误类型通过泛型参数精确描述
export class BadRequestError extends APIError<400, Headers> {}
export class RateLimitError extends APIError<429, Headers> {}
重试策略
typescript
// src/client.ts
private async shouldRetry(response: Response): Promise<boolean> {
// 服务端明确告知是否应该重试
const shouldRetryHeader = response.headers.get('x-should-retry');
if (shouldRetryHeader === 'true') return true;
if (shouldRetryHeader === 'false') return false;
// 请求超时、锁超时、限流、服务端错误 → 重试
if (response.status === 408) return true; // Request Timeout
if (response.status === 409) return true; // Conflict (lock timeout)
if (response.status === 429) return true; // Rate Limit
if (response.status >= 500) return true; // Server Error
return false;
}
private calculateDefaultRetryTimeoutMillis(retriesRemaining: number, maxRetries: number): number {
const initialRetryDelay = 0.5; // 500ms
const maxRetryDelay = 8.0; // 8s
const numRetries = maxRetries - retriesRemaining;
// 指数退避:0.5s * 2^n,最大 8s
const sleepSeconds = Math.min(initialRetryDelay * Math.pow(2, numRetries), maxRetryDelay);
// 25% 抖动,避免惊群效应
const jitter = 1 - Math.random() * 0.25;
return sleepSeconds * jitter * 1000;
}
使用方式
typescript
try {
await client.chat.completions.create({...});
} catch (e) {
if (e instanceof OpenAI.RateLimitError) {
// 限流,等待后重试
console.log('Rate limited, retrying...');
} else if (e instanceof OpenAI.AuthenticationError) {
// API Key 无效
console.error('Invalid API key');
} else if (e instanceof OpenAI.APIConnectionError) {
// 网络问题
console.error('Network error:', e.message);
} else {
throw e;
}
}
设计洞察
工厂方法 APIError.generate() 让 catch 可以精确捕获特定错误类型,而不是检查 error.status === 429。
三个泛型参数 APIError<TStatus, THeaders, TError> 让子类通过具体类型参数精确描述:
BadRequestError的status一定是400RateLimitError的status一定是429
这是 TypeScript 类型系统的高级应用。
八、设计亮点六:请求生命周期 --- 建造者模式的优雅编排 🔄
请求流程时序图
此时不执行请求 APIPromise-->>User: 返回 APIPromise User->>APIPromise: await / .then() APIPromise->>BuildRequest: buildRequest(options) Note over BuildRequest: URL 构建
Body 构建
Header 构建 BuildRequest-->>APIPromise: { req, url, timeout } APIPromise->>Fetch: fetchWithTimeout(url, req) Fetch-->>APIPromise: Response APIPromise->>Parse: defaultParseResponse() Parse-->>User: 解析后的数据
buildRequest 流水线
typescript
// src/client.ts
async buildRequest(
inputOptions: FinalRequestOptions,
{ retryCount = 0 }: { retryCount?: number } = {},
): Promise<{ req: FinalizedRequestInit; url: string; timeout: number }> {
const options = { ...inputOptions };
const { method, path, query, defaultBaseURL } = options;
// 1. URL 构建
const url = this.buildURL(path!, query as Record<string, unknown>, defaultBaseURL);
// 2. 超时配置
if ('timeout' in options) validatePositiveInteger('timeout', options.timeout);
options.timeout = options.timeout ?? this.timeout;
// 3. Body 构建(类型检测:JSON/FormData/Stream/Binary)
const { bodyHeaders, body } = this.buildBody({ options });
// 4. Header 构建(幂等性/认证/平台检测/NullableHeaders)
const reqHeaders = await this.buildHeaders({ options: inputOptions, method, bodyHeaders, retryCount });
const req: FinalizedRequestInit = {
method,
headers: reqHeaders,
...(options.signal && { signal: options.signal }),
...(body && { body }),
...this.fetchOptions,
...options.fetchOptions,
};
return { req, url, timeout: options.timeout };
}
Body 类型检测
typescript
// src/client.ts
private buildBody({ options: { body, headers: rawHeaders } }: { options: FinalRequestOptions }) {
if (!body) return { bodyHeaders: undefined, body: undefined };
const headers = buildHeaders([rawHeaders]);
if (
ArrayBuffer.isView(body) || // TypedArray
body instanceof ArrayBuffer || // ArrayBuffer
body instanceof DataView || // DataView
(typeof body === 'string' && headers.values.has('content-type')) || // 已指定 content-type 的字符串
((globalThis as any).Blob && body instanceof Blob) || // Blob/File
body instanceof FormData || // FormData → multipart/form-data
body instanceof URLSearchParams || // URLSearchParams → application/x-www-form-urlencoded
((globalThis as any).ReadableStream && body instanceof ReadableStream) // 流式上传
) {
return { bodyHeaders: undefined, body: body as BodyInit };
} else if (typeof body === 'object' && (Symbol.asyncIterator in body || Symbol.iterator in body)) {
// AsyncIterable → ReadableStream
return { bodyHeaders: undefined, body: Shims.ReadableStreamFrom(body as AsyncIterable<Uint8Array>) };
} else {
// 默认:JSON 序列化
return this.#encoder({ body, headers });
}
}
Header 构建与优先级
typescript
// src/client.ts
private async buildHeaders({
options,
method,
bodyHeaders,
retryCount,
}: {
options: FinalRequestOptions;
method: HTTPMethod;
bodyHeaders: HeadersLike;
retryCount: number;
}): Promise<Headers> {
// 幂等性 Header(非 GET 请求)
let idempotencyHeaders: HeadersLike = {};
if (this.idempotencyHeader && method !== 'get') {
if (!options.idempotencyKey) options.idempotencyKey = this.defaultIdempotencyKey();
idempotencyHeaders[this.idempotencyHeader] = options.idempotencyKey;
}
// 合并 Headers(按优先级从低到高)
const headers = buildHeaders([
idempotencyHeaders,
{
Accept: 'application/json',
'User-Agent': this.getUserAgent(),
'X-Stainless-Retry-Count': String(retryCount),
...getPlatformHeaders(), // 平台检测 Header
'OpenAI-Organization': this.organization,
'OpenAI-Project': this.project,
},
await this.authHeaders(options), // 认证 Header
this._options.defaultHeaders, // 客户端默认 Header
bodyHeaders, // Body 相关 Header
options.headers, // 请求级 Header(最高优先级)
]);
this.validateHeaders(headers);
return headers.values;
}
NullableHeaders:显式移除默认 Header
typescript
// src/internal/headers.ts
export type NullableHeaders = {
[brand_privateNullableHeaders]: true;
values: Headers;
nulls: Set<string>; // 显式设置为 null 的 header 名称
};
export const buildHeaders = (newHeaders: HeadersLike[]): NullableHeaders => {
const targetHeaders = new Headers();
const nullHeaders = new Set<string>();
for (const headers of newHeaders) {
for (const [name, value] of iterateHeaders(headers)) {
if (value === null) {
targetHeaders.delete(name);
nullHeaders.add(name.toLowerCase());
} else {
targetHeaders.append(name, value);
nullHeaders.delete(name.toLowerCase());
}
}
}
return { [brand_privateNullableHeaders]: true, values: targetHeaders, nulls: nullHeaders };
};
使用示例:
typescript
// 移除默认的 User-Agent header
await client.chat.completions.create({...}, {
headers: { 'User-Agent': null },
});
设计洞察
建造者模式让每个构建步骤独立可测、可覆盖:
buildURL()可以在子类中重写(Azure 需要不同的 URL 格式)authHeaders()可以在子类中重写(Azure 使用不同的认证方式)buildBody()智能检测类型,自动选择序列化方式
配置优先级 环境变量 → ClientOptions → RequestOptions 三层覆盖,让默认值合理、覆盖简单。
九、设计亮点七:高级流式助手 --- 事件驱动的 Delta 累积 🎯
ChatCompletionStream 设计
typescript
// src/lib/ChatCompletionStream.ts
export class ChatCompletionStream<ParsedT = null>
extends AbstractChatCompletionRunner<ChatCompletionStreamEvents<ParsedT>, ParsedT>
implements AsyncIterable<ChatCompletionChunk>
{
#params: ChatCompletionCreateParams | null;
#currentChatCompletionSnapshot: ChatCompletionSnapshot | undefined;
// 工厂方法
static createChatCompletion<ParsedT>(
client: OpenAI,
params: ChatCompletionStreamParams,
options?: RequestOptions,
): ChatCompletionStream<ParsedT> {
const runner = new ChatCompletionStream<ParsedT>(params);
runner._run(() =>
runner._runChatCompletion(
client,
{ ...params, stream: true },
{ ...options, headers: { ...options?.headers, 'X-Stainless-Helper-Method': 'stream' } },
),
);
return runner;
}
}
Delta 累积器
每个 SSE chunk 携带增量 delta,Stream 将其累积到 ChatCompletionSnapshot:
typescript
// src/lib/ChatCompletionStream.ts
#accumulateChatCompletion(chunk: ChatCompletionChunk): ChatCompletionSnapshot {
let snapshot = this.#currentChatCompletionSnapshot;
const { choices, ...rest } = chunk;
if (!snapshot) {
snapshot = this.#currentChatCompletionSnapshot = { ...rest, choices: [] };
} else {
Object.assign(snapshot, rest);
}
for (const { delta, finish_reason, index, logprobs = null, ...other } of chunk.choices) {
let choice = snapshot.choices[index];
if (!choice) {
choice = snapshot.choices[index] = { finish_reason, index, message: {}, logprobs, ...other };
}
// 累积 content
if (delta.content) {
choice.message.content = (choice.message.content || '') + delta.content;
// 支持自动解析的响应格式时,尝试增量解析
if (this.#getAutoParseableResponseFormat()) {
choice.message.parsed = partialParse(choice.message.content);
}
}
// 累积 refusal
if (delta.refusal) {
choice.message.refusal = (choice.message.refusal || '') + delta.refusal;
}
// 累积 tool_calls
if (delta.tool_calls) {
if (!choice.message.tool_calls) choice.message.tool_calls = [];
for (const { index, id, type, function: fn } of delta.tool_calls) {
const tool_call = (choice.message.tool_calls[index] ??= {});
if (id) tool_call.id = id;
if (type) tool_call.type = type;
if (fn) tool_call.function ??= { name: fn.name ?? '', arguments: '' };
if (fn?.name) tool_call.function!.name = fn.name;
if (fn?.arguments) {
tool_call.function!.arguments += fn.arguments;
// 增量解析 JSON 参数
if (shouldParseToolCall(this.#params, tool_call)) {
tool_call.function!.parsed_arguments = partialParse(tool_call.function!.arguments);
}
}
}
}
}
return snapshot;
}
细粒度事件
typescript
// src/lib/ChatCompletionStream.ts
export interface ChatCompletionStreamEvents<ParsedT = null> {
// 内容事件
content: (contentDelta: string, contentSnapshot: string) => void;
'content.delta': (props: ContentDeltaEvent) => void;
'content.done': (props: ContentDoneEvent<ParsedT>) => void;
// 拒绝事件
'refusal.delta': (props: RefusalDeltaEvent) => void;
'refusal.done': (props: RefusalDoneEvent) => void;
// 工具调用事件
'tool_calls.function.arguments.delta': (props: FunctionToolCallArgumentsDeltaEvent) => void;
'tool_calls.function.arguments.done': (props: FunctionToolCallArgumentsDoneEvent) => void;
// Log probs 事件
'logprobs.content.delta': (props: LogProbsContentDeltaEvent) => void;
'logprobs.content.done': (props: LogProbsContentDoneEvent) => void;
// Chunk 事件
chunk: (chunk: ChatCompletionChunk, snapshot: ChatCompletionSnapshot) => void;
}
Tool Calling 自动循环
typescript
// src/lib/AbstractChatCompletionRunner.ts
const DEFAULT_MAX_CHAT_COMPLETIONS = 10;
protected async _runTools<FunctionsArgs extends BaseFunctionsArgs>(
client: OpenAI,
params: ChatCompletionToolRunnerParams<FunctionsArgs>,
options?: RunnerOptions,
) {
const { maxChatCompletions = DEFAULT_MAX_CHAT_COMPLETIONS } = options || {};
for (let i = 0; i < maxChatCompletions; ++i) {
const chatCompletion = await this._createChatCompletion(client, {
...restParams,
tool_choice,
tools,
messages: [...this.messages],
}, options);
const message = chatCompletion.choices[0]?.message;
if (!message?.tool_calls?.length) {
return; // 没有工具调用,结束循环
}
// 处理每个工具调用
for (const tool_call of message.tool_calls) {
if (tool_call.type !== 'function') continue;
const fn = functionsByName[tool_call.function.name];
// 解析参数
const parsed = isRunnableFunctionWithParse(fn)
? await fn.parse(tool_call.function.arguments)
: tool_call.function.arguments;
// 调用函数
const rawContent = await fn.function(parsed, this);
const content = this.#stringifyFunctionCallResult(rawContent);
// 将结果添加到消息列表
this._addMessage({ role: 'tool', tool_call_id: tool_call.id, content });
}
}
}
使用方式
typescript
// 基本流式消费
const stream = client.beta.chat.completions.stream({
model: 'gpt-4o',
messages: [{ role: 'user', content: 'Tell me a story' }],
});
// 监听细粒度事件
stream.on('content.delta', ({ delta, snapshot }) => {
process.stdout.write(delta);
});
stream.on('tool_calls.function.arguments.done', ({ name, arguments: args }) => {
console.log(`Function called: ${name}(${args})`);
});
// 等待完成
const final = await stream.finalChatCompletion();
console.log('\n\nFinal:', final.choices[0].message.content);
// 使用 runTools 自动循环
const runner = client.chat.completions.runTools({
model: 'gpt-4o',
messages: [{ role: 'user', content: 'What is the weather in Tokyo?' }],
tools: [{
type: 'function',
function: {
name: 'get_weather',
parameters: { type: 'object', properties: { city: { type: 'string' } } },
function: async ({ city }) => {
return { temperature: 22, condition: 'sunny' };
},
},
}],
});
const result = await runner.finalContent();
设计洞察
EventEmitter + Accumulator 是流式 AI 应用的最佳拍档:
- EventEmitter 提供灵活的订阅机制
- Accumulator 将增量 delta 累积成完整状态
- 用户可以选择监听 delta(实时显示)或等待 done(完整数据)
部分 JSON 解析(partialParse)让流式 tool call 参数可以在完成前就开始处理,进一步提升用户体验。
十、设计亮点八:Azure 适配 --- 模板方法的教科书应用 ☁️
问题
Azure OpenAI 服务与 OpenAI API 有以下差异:
- URL 格式 :
/deployments/{model}/chat/completionsvs/chat/completions - 认证方式 :
api-keyheader vsAuthorization: Bearer - 查询参数 :需要
api-version
如何最小化代码改动来支持 Azure?
解决方案
AzureOpenAI 继承 OpenAI,只重写 buildRequest() 和 authHeaders() 两个方法:
typescript
// src/azure.ts
export class AzureOpenAI extends OpenAI {
deploymentName: string | undefined;
apiVersion: string = '';
constructor({
baseURL = readEnv('OPENAI_BASE_URL'),
apiKey = readEnv('AZURE_OPENAI_API_KEY'),
apiVersion = readEnv('OPENAI_API_VERSION'),
endpoint,
deployment,
azureADTokenProvider,
...opts
}: AzureClientOptions = {}) {
// 验证参数
if (!apiVersion) {
throw new Errors.OpenAIError('The OPENAI_API_VERSION environment variable is missing');
}
if (!azureADTokenProvider && !apiKey) {
throw new Errors.OpenAIError('Missing credentials');
}
if (azureADTokenProvider && apiKey) {
throw new Errors.OpenAIError('apiKey and azureADTokenProvider are mutually exclusive');
}
// 自动添加 api-version 到所有请求
opts.defaultQuery = { ...opts.defaultQuery, 'api-version': apiVersion };
// 构建 baseURL
if (!baseURL) {
if (!endpoint) endpoint = process.env['AZURE_OPENAI_ENDPOINT'];
if (!endpoint) throw new Errors.OpenAIError('Must provide endpoint or baseURL');
baseURL = `${endpoint}/openai`;
}
super({ apiKey: azureADTokenProvider ?? apiKey, baseURL, ...opts });
this.apiVersion = apiVersion;
this.deploymentName = deployment;
}
// 重写:修改 URL 格式
override async buildRequest(
options: FinalRequestOptions,
props: { retryCount?: number } = {},
): Promise<{ req: RequestInit & { headers: Headers }; url: string; timeout: number }> {
// 对于特定 endpoint,将 model 插入到 URL 中
if (_deployments_endpoints.has(options.path) && options.method === 'post' && options.body) {
const model = this.deploymentName || options.body['model'] || options.__metadata?.['model'];
if (model !== undefined && !this.baseURL.includes('/deployments')) {
options.path = `/deployments/${model}${options.path}`;
}
}
return super.buildRequest(options, props);
}
// 重写:修改认证方式
protected override async authHeaders(opts: FinalRequestOptions): Promise<NullableHeaders | undefined> {
if (typeof this._options.apiKey === 'string') {
return buildHeaders([{ 'api-key': this.apiKey }]); // 使用 api-key 而非 Authorization
}
return super.authHeaders(opts); // Azure AD Token Provider
}
}
// 需要 deployment 的 endpoint 列表
const _deployments_endpoints = new Set([
'/completions',
'/chat/completions',
'/embeddings',
'/audio/transcriptions',
'/audio/translations',
'/audio/speech',
'/images/generations',
'/batches',
'/images/edits',
]);
使用方式
typescript
import { AzureOpenAI } from 'openai';
const client = new AzureOpenAI({
endpoint: 'https://my-resource.openai.azure.com',
apiKey: process.env.AZURE_OPENAI_API_KEY,
apiVersion: '2024-02-15-preview',
deployment: 'my-gpt-4-deployment',
});
// 使用方式与 OpenAI 完全相同
const completion = await client.chat.completions.create({
model: 'gpt-4', // 会被自动替换为 deployment
messages: [{ role: 'user', content: 'Hello!' }],
});
设计洞察
这是模板方法模式的教科书应用:
- 基类
OpenAI定义了请求的整体流程(模板方法) - 子类
AzureOpenAI通过重写钩子方法(buildRequest,authHeaders)来定制行为 - 完全不同的认证和路由体系只需要最小的代码改动
这种设计让未来支持其他 OpenAI 兼容服务(如 Anthropic、本地部署)变得非常简单。
十一、设计亮点九:Realtime WebSocket --- 双范式并存 🔌
REST API vs Realtime API
| 特性 | REST API (HTTP) | Realtime API (WebSocket) |
|---|---|---|
| 通信模式 | 请求-响应 | 全双工 |
| 连接 | 每次请求新建 | 长连接 |
| 认证 | Authorization header |
subprotocol |
| 数据格式 | JSON / SSE | JSON events |
WebSocket 连接
typescript
// src/beta/realtime/websocket.ts
export class OpenAIRealtimeWebSocket extends OpenAIRealtimeEmitter {
url: URL;
socket: WebSocket;
constructor(
props: { model: string; dangerouslyAllowBrowser?: boolean },
client?: Pick<OpenAI, 'apiKey' | 'baseURL'>,
) {
super();
// 浏览器安全检查
if (!dangerouslyAllowBrowser && isRunningInBrowser()) {
throw new OpenAIError("It looks like you're running in a browser-like environment...");
}
// 构建 WebSocket URL
this.url = buildRealtimeURL(client, props.model);
// API key 通过 subprotocol 传递(而非 header)
this.socket = new WebSocket(this.url.toString(), [
'realtime',
`openai-insecure-api-key.${client.apiKey}`, // API key in subprotocol
'openai-beta.realtime-v1',
]);
// 监听消息
this.socket.addEventListener('message', (websocketEvent: MessageEvent) => {
const event = JSON.parse(websocketEvent.data) as RealtimeServerEvent;
this._emit('event', event);
if (event.type === 'error') {
this._onError(event);
} else {
this._emit(event.type, event);
}
});
this.socket.addEventListener('error', (event: any) => {
this._onError(null, event.message, null);
});
}
// 发送事件
send(event: RealtimeClientEvent) {
this.socket.send(JSON.stringify(event));
}
// 关闭连接
close(props?: { code: number; reason: string }) {
this.socket.close(props?.code ?? 1000, props?.reason ?? 'OK');
}
}
事件驱动架构
typescript
// src/beta/realtime/internal-base.ts
type RealtimeEvents = {
event: (event: RealtimeServerEvent) => void;
error: (error: OpenAIRealtimeError) => void;
} & {
[EventType in RealtimeServerEvent['type']]: (
event: Extract<RealtimeServerEvent, { type: EventType }>,
) => unknown;
};
export abstract class OpenAIRealtimeEmitter extends EventEmitter<RealtimeEvents> {
abstract send(event: RealtimeClientEvent): void;
abstract close(props?: { code: number; reason: string }): void;
protected _onError(event: ErrorEvent | null, message?: string, cause?: any): void {
const error = new OpenAIRealtimeError(message, event);
if (cause) error.cause = cause;
this._emit('error', error);
}
}
使用方式
typescript
import { OpenAIRealtimeWebSocket } from 'openai/beta/realtime/websocket';
const rt = new OpenAIRealtimeWebSocket({
model: 'gpt-4o-realtime-preview',
dangerouslyAllowBrowser: true,
});
// 监听事件
rt.on('session.created', (event) => {
console.log('Session created:', event.session.id);
});
rt.on('response.text.delta', (event) => {
process.stdout.write(event.delta);
});
rt.on('error', (error) => {
console.error('Error:', error.message);
});
// 发送事件
rt.send({
type: 'conversation.item.create',
item: {
type: 'message',
role: 'user',
content: [{ type: 'input_text', text: 'Hello!' }],
},
});
rt.send({ type: 'response.create' });
设计洞察
Realtime API 复用了核心的 EventEmitter 模式,保持了与 HTTP 流式 API 相似的编程模型:
- 都是事件驱动
- 都支持
on()监听 - 都有统一的错误处理
唯一的区别是:HTTP 流是单向的(只能接收),WebSocket 是双向的(可以发送和接收)。
十二、工程实践:构建系统与跨平台支持 🛠️
tsc-multi:一次编译,多种输出
json
// tsc-multi.json
{
"targets": [
{
"extname": ".js",
"module": "commonjs",
"shareHelpers": "internal/tslib.js"
},
{
"extname": ".mjs",
"module": "esnext",
"shareHelpers": "internal/tslib.mjs"
}
],
"projects": ["tsconfig.build.json"]
}
一次 TypeScript 编译同时产出:
.js(CommonJS) - Node.js require().mjs(ESM) - Node.js import / Browser.d.ts- TypeScript 类型定义
package.json exports 字段
json
// package.json
{
"type": "commonjs",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js"
},
"./*.mjs": {
"default": "./dist/*.mjs"
},
"./*.js": {
"default": "./dist/*.js"
},
"./*": {
"import": "./dist/*.mjs",
"require": "./dist/*.js"
}
}
}
这让同一个包可以被 CommonJS 和 ESM 项目使用。
可选 peer dependencies
json
{
"peerDependencies": {
"ws": "^8.18.0",
"zod": "^3.25 || ^4.0"
},
"peerDependenciesMeta": {
"ws": { "optional": true },
"zod": { "optional": true }
}
}
ws:只有使用 Realtime API 时才需要zod:只有使用 Structured Outputs 解析时才需要
这样核心功能不需要额外依赖,减少包体积。
平台检测与 Shim 层
typescript
// src/internal/detect-platform.ts
export function isRunningInBrowser(): boolean {
return (
typeof window !== 'undefined' &&
typeof window.document !== 'undefined' &&
typeof navigator !== 'undefined'
);
}
export function getPlatformHeaders(): Record<string, string> {
// 根据运行环境返回不同的 header
// X-Stainless-Runtime: node / browser / deno / cloudflare-worker / ...
// X-Stainless-Runtime-Version: ...
}
十三、设计模式总览 📚
| 设计模式 | 应用位置 | 目的 |
|---|---|---|
| 工厂方法 | APIError.generate() |
根据状态码创建对应错误类型 |
| 模板方法 | OpenAI.buildRequest() |
定义请求构建流程,子类重写钩子 |
| 建造者模式 | buildHeaders(), buildBody() |
分步骤构建复杂对象 |
| 观察者模式 | EventStream, EventEmitter |
事件驱动的流式处理 |
| 迭代器模式 | AsyncIterable, PagePromise |
统一遍历接口 |
| 适配器模式 | AzureOpenAI |
适配不同的 API 服务 |
| 策略模式 | parseResponse 参数 |
可替换的响应解析策略 |
| 装饰器模式 | _thenUnwrap() |
响应数据变换 |
| 单例模式 | parsedPromise 缓存 |
惰性计算 + 结果缓存 |
| 代理模式 | APIPromise |
延迟 Promise 解析 |
| 状态机模式 | SSEDecoder |
SSE 协议解析 |
| 流水线模式 | SSE 三层解析 | 职责分离的数据处理 |
十四、总结与启发 💡
openai-node 的核心设计哲学
- 惰性求值 :不到最后一刻不做计算(
APIPromise) - 类型安全:充分利用 TypeScript 类型系统(函数重载、泛型约束)
- 关注点分离:五层架构,每层职责清晰
- 最小侵入式扩展:模板方法 + 钩子函数,让扩展不需要修改核心代码
对我们自己构建 SDK 的启发
-
Promise 可以被创造性地延展
- 继承 Promise,重写
.then()/.catch()/.finally() - 提供
asResponse()、withResponse()等逃生通道
- 继承 Promise,重写
-
分层 + 模板方法 = 可扩展的多云适配
- 核心流程定义在基类
- 差异点提取为可重写的钩子方法
-
AsyncIterable 是流和分页的统一抽象
- 流式响应:
for await (const chunk of stream) - 分页 API:
for await (const item of page)
- 流式响应:
-
工厂方法让错误处理更精确
- 根据状态码自动选择错误类型
- 用户可以
catch (e) { if (e instanceof RateLimitError) ... }
-
EventEmitter + Accumulator 是流式 AI 应用的最佳拍档
- Delta 增量 + Snapshot 快照
- 细粒度事件让前端可以做更好的 UX
结语
openai-node 是一个精心设计的 SDK,每一个设计决策都经过深思熟虑。从惰性 Promise 到 SSE 流水线,从资源模式到 Azure 适配,处处体现着工程美学。
如果你正在构建自己的 API SDK,强烈建议通读这份源码。它不仅教你如何封装 API,更教你如何写出可维护、可扩展、类型安全的 TypeScript 代码。
希望这篇分析对你有帮助。如果有任何问题或想法,欢迎讨论!