1
0

feat: 新增 LLM checker 支持大模型服务应用层拨测

基于 AI SDK v6 实现 openai/openai-responses/anthropic 三类 provider 的 http/stream 模式调用
支持 output/finishReason/usage/stream 等完整 expect 断言链路
新增 9 个源文件和 5 个测试文件共 78 个测试
更新 README/DEVELOPMENT/probes.example.yaml 和 probe-config.schema.json
This commit is contained in:
2026-05-19 00:06:53 +08:00
parent 52262a31f6
commit 349896bd02
24 changed files with 3511 additions and 8 deletions

View File

@@ -0,0 +1,131 @@
import type { APICallError } from "ai";
import type {
LlmCheckObservation,
LlmHttpMetadata,
LlmMode,
LlmProvider,
LlmStreamObservation,
LlmUsageObservation,
} from "./types";
export function buildObservationFromApiCallError(
error: APICallError,
provider: LlmProvider,
model: string,
mode: LlmMode,
): LlmCheckObservation {
const http: LlmHttpMetadata | null =
error.statusCode !== undefined
? {
headers: error.responseHeaders ?? {},
status: error.statusCode,
statusText: "",
}
: null;
return {
finishReason: null,
http,
mode,
model,
outputText: null,
provider,
rawFinishReason: null,
stream: null,
usage: null,
warnings: [],
};
}
export function buildObservationFromGenerateText(
provider: LlmProvider,
model: string,
mode: LlmMode,
result: {
finishReason: string;
rawFinishReason: null | string | undefined;
text: string;
usage: { inputTokens: number; outputTokens: number; totalTokens?: number | undefined };
warnings?: string[];
},
http: LlmHttpMetadata | null,
): LlmCheckObservation {
return {
finishReason: result.finishReason,
http,
mode,
model,
outputText: result.text,
provider,
rawFinishReason: result.rawFinishReason ?? null,
stream: null,
usage: {
inputTokens: result.usage.inputTokens ?? 0,
outputTokens: result.usage.outputTokens ?? 0,
totalTokens: result.usage.totalTokens ?? (result.usage.inputTokens ?? 0) + (result.usage.outputTokens ?? 0),
},
warnings: result.warnings ?? [],
};
}
export async function buildObservationFromStreamText(
provider: LlmProvider,
model: string,
mode: LlmMode,
fullStream: AsyncIterable<unknown>,
http: LlmHttpMetadata | null,
startMs: number,
): Promise<LlmCheckObservation> {
let outputText = "";
let firstTokenMs: null | number = null;
let completed = false;
let finishReason: null | string = null;
let rawFinishReason: null | string = null;
let usage: LlmUsageObservation | null = null;
const warnings: string[] = [];
for await (const part of fullStream) {
const p = part as Record<string, unknown>;
const type = p["type"] as string;
if (type === "text-delta") {
const delta = p["textDelta"] as string;
if (delta !== "") {
firstTokenMs ??= Math.round(performance.now() - startMs);
outputText += delta;
}
} else if (type === "finish") {
completed = true;
finishReason = (p["finishReason"] as string) ?? null;
rawFinishReason = (p["rawFinishReason"] as string | undefined) ?? null;
const totalUsage = p["totalUsage"] as
| undefined
| { inputTokens: number; outputTokens: number; totalTokens: number };
const partUsage = p["usage"] as undefined | { inputTokens: number; outputTokens: number };
usage = {
inputTokens: totalUsage?.inputTokens ?? partUsage?.inputTokens ?? 0,
outputTokens: totalUsage?.outputTokens ?? partUsage?.outputTokens ?? 0,
totalTokens: totalUsage?.totalTokens ?? (partUsage?.inputTokens ?? 0) + (partUsage?.outputTokens ?? 0),
};
} else if (type === "error") {
const err = p["error"] as Error | undefined;
warnings.push(err?.message ?? "stream error");
}
}
return {
finishReason,
http,
mode,
model,
outputText: outputText || null,
provider,
rawFinishReason,
stream: { completed, firstTokenMs } satisfies LlmStreamObservation,
usage,
warnings,
};
}