@codehz/ai 0.4.6 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +221 -75
  2. package/dist/index.d.mts +670 -523
  3. package/dist/index.mjs +3677 -2207
  4. package/dist/index.mjs.map +1 -1
  5. package/package.json +19 -8
  6. package/.github/workflows/publish.yml +0 -56
  7. package/.oxfmtrc.json +0 -12
  8. package/.oxlintrc.json +0 -34
  9. package/AGENTS.md +0 -37
  10. package/src/adapters/chat-completions.ts +0 -624
  11. package/src/adapters/index.ts +0 -44
  12. package/src/adapters/messages.ts +0 -635
  13. package/src/adapters/mock.ts +0 -934
  14. package/src/adapters/ollama.ts +0 -526
  15. package/src/adapters/responses.ts +0 -818
  16. package/src/core/aggregator.ts +0 -428
  17. package/src/core/client.ts +0 -36
  18. package/src/core/collect-stream.ts +0 -19
  19. package/src/core/errors.ts +0 -105
  20. package/src/core/event-factory.ts +0 -151
  21. package/src/core/index.ts +0 -18
  22. package/src/core/merge-auxiliary.ts +0 -22
  23. package/src/core/normalize.ts +0 -65
  24. package/src/core/validation.ts +0 -404
  25. package/src/helpers/adapter-auxiliary.ts +0 -155
  26. package/src/helpers/adapter-base.ts +0 -218
  27. package/src/helpers/adapter-security.ts +0 -126
  28. package/src/helpers/auxiliary-collector.ts +0 -166
  29. package/src/helpers/incremental-stream-parser.ts +0 -142
  30. package/src/helpers/index.ts +0 -87
  31. package/src/helpers/mapping.ts +0 -192
  32. package/src/helpers/provider-request-options.ts +0 -25
  33. package/src/helpers/provider-stream.ts +0 -147
  34. package/src/helpers/reasoning-level.ts +0 -86
  35. package/src/helpers/request-mapper.ts +0 -94
  36. package/src/helpers/synthetic-stream.ts +0 -188
  37. package/src/helpers/usage-mapping.ts +0 -110
  38. package/src/index.ts +0 -17
  39. package/src/types/adapter.ts +0 -42
  40. package/src/types/content.ts +0 -15
  41. package/src/types/events.ts +0 -138
  42. package/src/types/index.ts +0 -49
  43. package/src/types/items.ts +0 -57
  44. package/src/types/request.ts +0 -52
  45. package/src/types/response.ts +0 -68
  46. package/tsdown.config.ts +0 -10
@@ -1,155 +0,0 @@
1
- import { WarningCode } from "../core/errors.js";
2
- import type { EventFactory } from "../core/event-factory.js";
3
- import type { AIStreamEvent, BillingInfo, NormalizedRequest, Usage, AuxiliaryInfo } from "../types/index.js";
4
- import { AuxiliaryCollector, type BillingSource, type LookupResult, type UsageSource } from "./auxiliary-collector.js";
5
-
6
- type MaybePromise<T> = T | Promise<T>;
7
-
8
- export type BillingPostprocessHook = (context: {
9
- request: NormalizedRequest;
10
- usage?: Usage;
11
- billing?: BillingInfo;
12
- auxiliary?: AuxiliaryInfo;
13
- }) => MaybePromise<Partial<BillingInfo> | undefined>;
14
-
15
- export type AuxiliaryFinalizeOptions = {
16
- lookup?: () => Promise<LookupResult>;
17
- lookupTimeoutMs?: number;
18
- postprocessBilling?: BillingPostprocessHook;
19
- postprocessBillingSource?: BillingSource;
20
- };
21
-
22
- export type AuxiliaryFinalizeResult = {
23
- events: AIStreamEvent[];
24
- usage?: Usage;
25
- billing?: BillingInfo;
26
- auxiliary?: AuxiliaryInfo;
27
- warnings?: string[];
28
- metadataSources?: string[];
29
- };
30
-
31
- export class AdapterAuxiliaryState {
32
- private readonly collector = new AuxiliaryCollector();
33
- private readonly metadataSources = new Set<string>();
34
-
35
- constructor(private readonly request: NormalizedRequest) {}
36
-
37
- recordUsage(usage: Partial<Usage>, source: UsageSource, raw?: unknown): void {
38
- if (this.request.include?.usage === "off" || isEmptyRecord(usage)) return;
39
- this.collector.recordUsage(usage, source, raw);
40
- }
41
-
42
- recordBilling(billing: Partial<BillingInfo>, source: BillingSource, raw?: unknown): void {
43
- if (this.request.include?.billing === "off" || isEmptyRecord(billing)) return;
44
- this.collector.recordBilling(billing, source, raw);
45
- }
46
-
47
- recordProviderMetadata(source: string, metadata: Record<string, unknown> | undefined): void {
48
- if (this.request.include?.providerMetadata === "off" || !metadata || isEmptyRecord(metadata)) return;
49
- this.collector.recordMetadata(metadata);
50
- this.metadataSources.add(source);
51
- }
52
-
53
- async finalize(factory: EventFactory, options: AuxiliaryFinalizeOptions = {}): Promise<AuxiliaryFinalizeResult> {
54
- if (options.lookup && this.shouldAttemptLookup()) {
55
- await this.collector.tryLookup(options.lookup, options.lookupTimeoutMs);
56
- }
57
-
58
- if (this.request.include?.billing !== "off" && options.postprocessBilling) {
59
- const snapshot = this.collector.build();
60
- if (!snapshot.billing) {
61
- const derived = await options.postprocessBilling({
62
- request: this.request,
63
- usage: snapshot.usage,
64
- billing: snapshot.billing,
65
- auxiliary: snapshot.auxiliary,
66
- });
67
- if (derived && !isEmptyRecord(derived)) {
68
- this.collector.recordBilling(
69
- {
70
- ...derived,
71
- isEstimated: derived.isEstimated ?? true,
72
- source: derived.source ?? "derived",
73
- },
74
- options.postprocessBillingSource ?? "derived",
75
- derived,
76
- );
77
- }
78
- }
79
- }
80
-
81
- const built = this.collector.build();
82
- const events: AIStreamEvent[] = [];
83
-
84
- if (built.usage || built.billing || built.auxiliary) {
85
- events.push(
86
- factory.responseAuxiliary({
87
- usage: built.usage,
88
- billing: built.billing,
89
- auxiliary: built.auxiliary,
90
- }),
91
- );
92
- }
93
-
94
- if (this.request.include?.usage !== "off" && !built.usage) {
95
- events.push(
96
- factory.responseWarning("Usage information was not provided by the provider", WarningCode.USAGE_MISSING),
97
- );
98
- }
99
-
100
- if (this.request.include?.billing !== "off") {
101
- if (!built.billing) {
102
- events.push(
103
- factory.responseWarning("Billing information was not provided by the provider", WarningCode.BILLING_MISSING),
104
- );
105
- } else if (built.billing.isEstimated) {
106
- events.push(factory.responseWarning("Billing amount is an estimate", WarningCode.BILLING_ESTIMATED));
107
- }
108
- }
109
-
110
- return {
111
- events,
112
- usage: built.usage,
113
- billing: built.billing,
114
- auxiliary: built.auxiliary,
115
- warnings: built.warnings,
116
- metadataSources: this.metadataSources.size > 0 ? [...this.metadataSources] : undefined,
117
- };
118
- }
119
-
120
- private shouldAttemptLookup(): boolean {
121
- if (
122
- this.request.include?.usage === "off" &&
123
- this.request.include?.billing === "off" &&
124
- this.request.include?.providerMetadata === "off"
125
- ) {
126
- return false;
127
- }
128
-
129
- const snapshot = this.collector.build();
130
- return (
131
- (this.request.include?.usage !== "off" && !snapshot.usage) ||
132
- (this.request.include?.billing !== "off" && !snapshot.billing) ||
133
- (this.request.include?.providerMetadata !== "off" && !snapshot.auxiliary?.providerMetadata)
134
- );
135
- }
136
- }
137
-
138
- export function emitMalformedStreamWarning(
139
- factory: EventFactory,
140
- options: {
141
- count: number;
142
- providerLabel: string;
143
- transportLabel: string;
144
- },
145
- ): AIStreamEvent | undefined {
146
- if (options.count < 1) return undefined;
147
- return factory.responseWarning(
148
- `Skipped ${options.count} malformed ${options.providerLabel} ${options.transportLabel}`,
149
- "STREAM_ERROR",
150
- );
151
- }
152
-
153
- function isEmptyRecord(value: object): boolean {
154
- return Object.keys(value).length === 0;
155
- }
@@ -1,218 +0,0 @@
1
- /**
2
- * Adapter 抽象基类
3
- *
4
- * 约定 adapter 的内部职责分层(build / invoke / parse / emit):
5
- * 1. buildRequest — 将 NormalizedRequest 转换为 provider 请求格式
6
- * 2. invokeProvider — 调用 provider API
7
- * 3. parseResponse — 解析 provider 响应为 canonical 中间态
8
- * 4. emitEvents — 产出 canonical 事件流
9
- *
10
- * 子类实现 buildRequest() 和 runStream(),
11
- * runStream 返回 AsyncIterable,事件实时发射给消费者。
12
- */
13
-
14
- import type {
15
- NormalizedRequest,
16
- BackendAdapter,
17
- AIStreamEvent,
18
- AIResponse,
19
- AuxiliaryInfo,
20
- OutputItem,
21
- ReplayItem,
22
- StopReason,
23
- Usage,
24
- BillingInfo,
25
- ToolCallItem,
26
- } from "../types/index.js";
27
- import { createEventFactory } from "../core/event-factory.js";
28
- import { AIMappingError, AIProviderError, AIRequestError, AIStreamError } from "../core/errors.js";
29
- import type { EventFactory } from "../core/event-factory.js";
30
- import { extractText } from "./mapping.js";
31
- import { AdapterAuxiliaryState } from "./adapter-auxiliary.js";
32
- import { mergeAuxiliary } from "../core/merge-auxiliary.js";
33
-
34
- // ── Adapter 解析中间结果 ──────────────────────────────────────
35
-
36
- export type ProviderResponse = unknown;
37
-
38
- /**
39
- * adapter 完成一轮处理后返回的最终结果。
40
- * 用于 buildResponse() 构建 AIResponse。
41
- */
42
- export type StreamResult = {
43
- output: OutputItem[];
44
- replay: ReplayItem[];
45
- stopReason?: StopReason;
46
- usage?: Usage;
47
- billing?: BillingInfo;
48
- providerMetadata?: Record<string, unknown>;
49
- auxiliary?: Partial<AuxiliaryInfo>;
50
- warnings?: string[];
51
- metadataSources?: string[];
52
- rawResponseId?: string;
53
- };
54
-
55
- // ── 抽象基类 ──────────────────────────────────────────────────
56
-
57
- export abstract class AdapterBase implements BackendAdapter {
58
- abstract readonly kind: "chat-completions" | "messages" | "responses" | "ollama" | "mock";
59
- abstract readonly isSyntheticStream: boolean;
60
-
61
- /**
62
- * stream 模板方法:
63
- * 1. 创建事件工厂,发射 response.started
64
- * 2. 构建 provider 请求
65
- * 3. 委托 runStream 发射全部流事件(含 response.completed)
66
- */
67
- async *stream(request: NormalizedRequest): AsyncIterable<AIStreamEvent> {
68
- // 若请求已被 abort,不发出任何事件
69
- request.signal?.throwIfAborted();
70
-
71
- const factory = createEventFactory({
72
- responseId: request.requestId,
73
- backend: { kind: this.kind, isSynthetic: this.isSyntheticStream },
74
- });
75
-
76
- yield factory.responseStarted(request.model);
77
-
78
- try {
79
- const providerRequest = await this.buildRequest(request);
80
- yield* this.runStream(providerRequest, factory, request);
81
- } catch (err) {
82
- if (err instanceof AIRequestError || err instanceof AIProviderError || err instanceof AIStreamError) {
83
- throw err;
84
- }
85
-
86
- if (err instanceof AIMappingError) {
87
- yield factory.responseWarning(err.message, "MAPPING_ERROR");
88
- const errorResp = this.buildResponse(request, { output: [], replay: [] }, factory);
89
- yield factory.responseCompleted({
90
- replay: errorResp.replay,
91
- stopReason: errorResp.stopReason,
92
- trace: errorResp.backend,
93
- usage: errorResp.usage,
94
- billing: errorResp.billing,
95
- auxiliary: errorResp.auxiliary,
96
- warnings: errorResp.warnings,
97
- });
98
- return;
99
- }
100
-
101
- throw err;
102
- }
103
- }
104
-
105
- // ── 子类必须实现 ──────────────────────────────────────────
106
-
107
- /** 将 NormalizedRequest 转换为 provider 请求格式。 */
108
- protected abstract buildRequest(request: NormalizedRequest): ProviderResponse | Promise<ProviderResponse>;
109
-
110
- /**
111
- * 执行流式请求,发射全部事件(含 response.completed)。
112
- * 子类负责:
113
- * - 调用 provider
114
- * - 解析每个 chunk
115
- * - 通过 factory 发射 item 事件
116
- * - 构建 StreamResult
117
- * - 发射 factory.responseCompleted(buildResponse(…))
118
- */
119
- protected abstract runStream(
120
- providerRequest: ProviderResponse,
121
- factory: EventFactory,
122
- request: NormalizedRequest,
123
- ): AsyncIterable<AIStreamEvent>;
124
-
125
- // ── 共享构造方法 ──────────────────────────────────────────
126
-
127
- /**
128
- * 从 StreamResult 构建完整 AIResponse。
129
- * 子类可在返回前自定义覆盖。
130
- */
131
- protected buildResponse(request: NormalizedRequest, result: StreamResult, _factory: EventFactory): AIResponse {
132
- const text = extractText(result.output);
133
- const warnings = mergeWarnings(result.warnings, _factory.warnings);
134
- const auxiliary = mergeAuxiliary(
135
- result.auxiliary,
136
- result.providerMetadata ? { providerMetadata: result.providerMetadata } : undefined,
137
- );
138
-
139
- return {
140
- id: request.requestId,
141
- output: result.output,
142
- replay: result.replay,
143
- text,
144
- toolCalls: result.output.filter((item): item is ToolCallItem => item.type === "tool_call"),
145
- stopReason: result.stopReason,
146
- usage: result.usage,
147
- billing: result.billing,
148
- auxiliary,
149
- warnings,
150
- backend: {
151
- requestId: request.requestId,
152
- rawResponseId: result.rawResponseId,
153
- adapter: this.kind,
154
- isSyntheticStream: this.isSyntheticStream,
155
- metadataSources: result.metadataSources,
156
- warnings,
157
- },
158
- };
159
- }
160
-
161
- /**
162
- * 统一 finalize auxiliary → response.completed。
163
- * adapter 在调用前组装 output / replay / stopReason 等业务字段。
164
- */
165
- protected async *emitStreamCompleted(
166
- factory: EventFactory,
167
- request: NormalizedRequest,
168
- auxiliary: AdapterAuxiliaryState,
169
- result: StreamResult,
170
- ): AsyncIterable<AIStreamEvent> {
171
- const auxiliaryResult = await auxiliary.finalize(factory);
172
- for (const event of auxiliaryResult.events) {
173
- yield event;
174
- }
175
-
176
- const finalResponse = this.buildResponse(
177
- request,
178
- {
179
- ...result,
180
- usage: result.usage ?? auxiliaryResult.usage,
181
- billing: result.billing ?? auxiliaryResult.billing,
182
- auxiliary: mergeAuxiliary(result.auxiliary, auxiliaryResult.auxiliary),
183
- warnings: mergeWarnings(result.warnings, auxiliaryResult.warnings),
184
- metadataSources: result.metadataSources ?? auxiliaryResult.metadataSources,
185
- },
186
- factory,
187
- );
188
-
189
- yield factory.responseCompleted({
190
- replay: finalResponse.replay,
191
- stopReason: finalResponse.stopReason,
192
- trace: finalResponse.backend,
193
- usage: finalResponse.usage,
194
- billing: finalResponse.billing,
195
- auxiliary: finalResponse.auxiliary,
196
- warnings: finalResponse.warnings,
197
- });
198
- }
199
-
200
- protected createAuxiliaryState(request: NormalizedRequest): AdapterAuxiliaryState {
201
- return new AdapterAuxiliaryState(request);
202
- }
203
- }
204
-
205
- function mergeWarnings(...groups: Array<string[] | undefined>): string[] | undefined {
206
- const merged: string[] = [];
207
-
208
- for (const group of groups) {
209
- if (!group) continue;
210
- for (const warning of group) {
211
- if (!merged.includes(warning)) {
212
- merged.push(warning);
213
- }
214
- }
215
- }
216
-
217
- return merged.length > 0 ? merged : undefined;
218
- }
@@ -1,126 +0,0 @@
1
- /**
2
- * Adapter 边界安全辅助
3
- *
4
- * - opaque replay 入站 envelope(大小 / 深度)
5
- * - provider HTTP 错误 body 出站脱敏
6
- */
7
-
8
- import { AIProviderError, AIRequestError } from "../core/errors.js";
9
-
10
- export const MAX_OPAQUE_PAYLOAD_BYTES = 65536;
11
- export const MAX_OPAQUE_JSON_DEPTH = 8;
12
- export const PROVIDER_ERROR_MESSAGE_MAX_LEN = 500;
13
- export const PROVIDER_ERROR_RAW_BODY_THRESHOLD = 200;
14
-
15
- export type OpaqueEnvelopeResult = { ok: true } | { ok: false; reason: string };
16
-
17
- /** 测量 JSON 值嵌套深度(对象/数组);循环引用按已访问节点深度计。 */
18
- export function measureJsonDepth(value: unknown, seen = new WeakSet<object>()): number {
19
- if (value === null || typeof value !== "object") {
20
- return 0;
21
- }
22
-
23
- if (seen.has(value)) {
24
- return 0;
25
- }
26
- seen.add(value);
27
-
28
- let maxChild = 0;
29
- if (Array.isArray(value)) {
30
- for (const item of value) {
31
- maxChild = Math.max(maxChild, measureJsonDepth(item, seen));
32
- }
33
- } else {
34
- for (const key of Object.keys(value as Record<string, unknown>)) {
35
- maxChild = Math.max(maxChild, measureJsonDepth((value as Record<string, unknown>)[key], seen));
36
- }
37
- }
38
-
39
- return 1 + maxChild;
40
- }
41
-
42
- /**
43
- * Opaque replay 通用 envelope:必须是 object、体积 ≤ 64KB、深度 ≤ 8。
44
- * 不校验 adapter 专用字段形状。
45
- */
46
- export function validateOpaqueReplayEnvelope(payload: unknown): OpaqueEnvelopeResult {
47
- if (typeof payload !== "object" || payload === null) {
48
- return { ok: false, reason: "payload must be an object" };
49
- }
50
-
51
- let raw: string;
52
- try {
53
- raw = JSON.stringify(payload);
54
- } catch {
55
- return { ok: false, reason: "payload is not JSON-serializable" };
56
- }
57
-
58
- if (raw === undefined) {
59
- return { ok: false, reason: "payload is not JSON-serializable" };
60
- }
61
-
62
- if (raw.length > MAX_OPAQUE_PAYLOAD_BYTES) {
63
- return {
64
- ok: false,
65
- reason: `opaque payload exceeds max size (${raw.length} > ${MAX_OPAQUE_PAYLOAD_BYTES})`,
66
- };
67
- }
68
-
69
- const depth = measureJsonDepth(payload);
70
- if (depth > MAX_OPAQUE_JSON_DEPTH) {
71
- return {
72
- ok: false,
73
- reason: `opaque payload nesting depth (${depth}) exceeds max (${MAX_OPAQUE_JSON_DEPTH})`,
74
- };
75
- }
76
-
77
- return { ok: true };
78
- }
79
-
80
- /** envelope 失败时抛 AIRequestError。 */
81
- export function assertOpaqueReplayEnvelope(payload: unknown): void {
82
- const result = validateOpaqueReplayEnvelope(payload);
83
- if (!result.ok) {
84
- throw new AIRequestError(`Invalid opaque replay payload: ${result.reason}`, "INVALID_OPAQUE_REPLAY");
85
- }
86
- }
87
-
88
- /**
89
- * 从 provider HTTP 错误 body 提取可对外暴露的短消息,避免泄漏 HTML / 内部路径等。
90
- */
91
- export function extractProviderErrorMessage(body: string, status: number): string {
92
- if (!body) return `HTTP ${status}`;
93
-
94
- try {
95
- const parsed: unknown = JSON.parse(body);
96
- if (parsed && typeof parsed === "object") {
97
- const record = parsed as Record<string, unknown>;
98
- const errorField = record.error;
99
- let msg: unknown;
100
- if (errorField && typeof errorField === "object" && errorField !== null) {
101
- msg = (errorField as Record<string, unknown>).message;
102
- }
103
- if (typeof msg !== "string") {
104
- msg = typeof errorField === "string" ? errorField : record.message;
105
- }
106
- if (typeof msg === "string" && msg.length > 0) {
107
- return msg.slice(0, PROVIDER_ERROR_MESSAGE_MAX_LEN);
108
- }
109
- }
110
- } catch {
111
- // not JSON
112
- }
113
-
114
- const trimmed = body.trimStart();
115
- if (trimmed.startsWith("<!") || trimmed.startsWith("<html") || body.length > PROVIDER_ERROR_RAW_BODY_THRESHOLD) {
116
- return `HTTP ${status}. Body omitted (${body.length} bytes)`;
117
- }
118
-
119
- return body.slice(0, PROVIDER_ERROR_MESSAGE_MAX_LEN);
120
- }
121
-
122
- /** 统一构造脱敏后的 AIProviderError。 */
123
- export function providerHttpError(status: number, body: string): AIProviderError {
124
- const safe = extractProviderErrorMessage(body, status);
125
- return new AIProviderError(`Provider returned ${status}: ${safe}`, "PROVIDER_ERROR", status, safe);
126
- }
@@ -1,166 +0,0 @@
1
- /**
2
- * 辅助信息采集器 (AuxiliaryCollector)
3
- *
4
- * 为 usage、billing、providerMetadata 提供统一的 best-effort 采集。
5
- *
6
- * 采集优先级(分层):
7
- * 1. 主响应 body / terminal event
8
- * 2. headers / trailers
9
- * 3. SDK metadata
10
- * 4. 一次 follow-up lookup
11
- * 5. derived estimate
12
- *
13
- * 约束:
14
- * - lookup 最多一次有界补查
15
- * - lookup 失败只记录 warning
16
- * - 不阻断主生成链路
17
- */
18
-
19
- import type { Usage, BillingInfo, AuxiliaryInfo } from "../types/index.js";
20
-
21
- // ── 来源类型 ──────────────────────────────────────────────────
22
-
23
- export type UsageSource = NonNullable<AuxiliaryInfo["usageSource"]>;
24
- export type BillingSource = NonNullable<AuxiliaryInfo["billingSource"]>;
25
-
26
- export type LookupResult = {
27
- usage?: Partial<Usage>;
28
- billing?: Partial<BillingInfo>;
29
- providerMetadata?: Record<string, unknown>;
30
- };
31
-
32
- // ── Collector ─────────────────────────────────────────────────
33
-
34
- export class AuxiliaryCollector {
35
- private usage: Partial<Usage> = {};
36
- private usageSource: UsageSource | undefined;
37
- private billing: Partial<BillingInfo> | undefined;
38
- private billingSource: BillingSource | undefined;
39
- private providerMetadata: Record<string, unknown> = {};
40
- private providerUsage: unknown;
41
- private providerBilling: unknown;
42
- private warnings: string[] = [];
43
- private lookupAttempted = false;
44
-
45
- // ── 记录方法 ──────────────────────────────────────────────
46
-
47
- /**
48
- * 记录 usage 信息。
49
- * 后调用的覆盖先调用的(优先级由调用方控制)。
50
- */
51
- recordUsage(usage: Partial<Usage>, source: UsageSource, raw?: unknown): this {
52
- this.usage = { ...this.usage, ...usage };
53
- this.usageSource = source;
54
- if (raw !== undefined) this.providerUsage = raw;
55
- return this;
56
- }
57
-
58
- /**
59
- * 记录 billing 信息。
60
- * 后调用的覆盖先调用的。
61
- */
62
- recordBilling(billing: Partial<BillingInfo>, source: BillingSource, raw?: unknown): this {
63
- this.billing = { ...this.billing, ...billing };
64
- this.billingSource = source;
65
- if (raw !== undefined) this.providerBilling = raw;
66
- return this;
67
- }
68
-
69
- /**
70
- * 记录 provider 元数据(非 canonical 的 key-value 信息)。
71
- */
72
- recordMetadata(metadata: Record<string, unknown>): this {
73
- this.providerMetadata = { ...this.providerMetadata, ...metadata };
74
- return this;
75
- }
76
-
77
- /**
78
- * 记录一条 warning。
79
- */
80
- recordWarning(message: string): this {
81
- this.warnings.push(message);
82
- return this;
83
- }
84
-
85
- // ── 有界 Lookup ───────────────────────────────────────────
86
-
87
- /**
88
- * 执行一次有界 follow-up lookup。
89
- * 最多调用一次;后续调用被忽略。
90
- * lookup 失败(抛错)仅记录 warning,不传播异常。
91
- */
92
- async tryLookup(lookupFn: () => Promise<LookupResult>, timeoutMs = 5_000): Promise<void> {
93
- if (this.lookupAttempted) return;
94
- this.lookupAttempted = true;
95
-
96
- try {
97
- const result = await withTimeout(lookupFn(), timeoutMs);
98
- if (result.usage) {
99
- this.recordUsage(result.usage, "lookup", result.usage);
100
- }
101
- if (result.billing) {
102
- const bill: Partial<BillingInfo> = {
103
- ...result.billing,
104
- source: result.billing?.source ?? "lookup",
105
- };
106
- this.recordBilling(bill, "lookup", result.billing);
107
- }
108
- if (result.providerMetadata) {
109
- this.recordMetadata(result.providerMetadata);
110
- }
111
- } catch (err) {
112
- this.recordWarning(`Auxiliary lookup failed: ${err instanceof Error ? err.message : String(err)}`);
113
- }
114
- }
115
-
116
- // ── 构建最终结果 ──────────────────────────────────────────
117
-
118
- /**
119
- * 构建最终的 usage / billing / auxiliary。
120
- * 所有字段均为可选的 — 拿不到就不给。
121
- */
122
- build(): { usage?: Usage; billing?: BillingInfo; auxiliary?: AuxiliaryInfo; warnings?: string[] } {
123
- const result: { usage?: Usage; billing?: BillingInfo; auxiliary?: AuxiliaryInfo; warnings?: string[] } = {};
124
-
125
- if (Object.keys(this.usage).length > 0) {
126
- result.usage = this.usage as Usage;
127
- }
128
-
129
- if (this.billing) {
130
- result.billing = this.billing as BillingInfo;
131
- }
132
-
133
- const aux: AuxiliaryInfo = {};
134
- if (this.usageSource) aux.usageSource = this.usageSource;
135
- if (this.billingSource) aux.billingSource = this.billingSource;
136
- if (this.providerUsage !== undefined) aux.providerUsage = this.providerUsage;
137
- if (this.providerBilling !== undefined) aux.providerBilling = this.providerBilling;
138
- if (Object.keys(this.providerMetadata).length > 0) aux.providerMetadata = this.providerMetadata;
139
-
140
- if (Object.keys(aux).length > 0) {
141
- result.auxiliary = aux;
142
- }
143
-
144
- if (this.warnings.length > 0) {
145
- result.warnings = [...this.warnings];
146
- }
147
-
148
- return result;
149
- }
150
-
151
- /**
152
- * 已使用的来源列表(用于 debugging)。
153
- */
154
- get sources(): { usage?: UsageSource; billing?: BillingSource } {
155
- return { usage: this.usageSource, billing: this.billingSource };
156
- }
157
- }
158
-
159
- // ── Helper ────────────────────────────────────────────────────
160
-
161
- function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
162
- return Promise.race([
163
- promise,
164
- new Promise<T>((_, reject) => setTimeout(() => reject(new Error(`Lookup timed out after ${ms}ms`)), ms)),
165
- ]);
166
- }