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:
131
src/server/checker/runner/llm/observation.ts
Normal file
131
src/server/checker/runner/llm/observation.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user