## Purpose 定义 LLM checker 的配置模型、Provider/Mode 支持、执行观测、Expect 断言、失败 Phase 和测试行为。 ## Requirements ### Requirement: LLM Checker 注册与模块结构 系统 SHALL 提供 `type: llm` checker,用于大模型服务的应用层拨测。LLM checker MUST 位于 `src/server/checker/runner/llm/` 自包含目录,并通过 `src/server/checker/runner/index.ts` 注册到 `CheckerRegistry`。LLM checker SHALL 复用现有 checker 抽象、配置 schema 组装、启动期语义校验、引擎调度、存储序列化和共享 expect 基础设施。 #### Scenario: 注册 LLM checker - **WHEN** 系统初始化默认 checker registry - **THEN** registry SHALL 包含 `llm` 类型,且 `/api/meta` 返回的 `checkerTypes` SHALL 包含 `llm` #### Scenario: LLM checker 目录自包含 - **WHEN** 开发者查看 `src/server/checker/runner/llm/` 目录 - **THEN** 该目录 SHALL 包含 LLM checker 的类型、schema、语义校验、provider 创建、observation 构建、expect 断言、执行逻辑和模块入口 #### Scenario: 不扩展存储和 API 结构 - **WHEN** LLM checker 写入检查结果 - **THEN** 系统 SHALL 使用现有 `CheckResult`、`targets`、`check_results` 和 Dashboard API 结构,不新增 LLM 专用存储列或 Dashboard 指标字段 ### Requirement: LLM Provider 与调用模式 LLM checker SHALL 支持 `openai`、`openai-responses`、`anthropic` 三类 provider。`mode: http` SHALL 调用 AI SDK `generateText`。`mode: stream` SHALL 调用 AI SDK `streamText`。所有模型调用 MUST 将 `maxRetries` 固定为 `0`,并 MUST 使用引擎注入的 `ctx.signal` 响应超时和取消。 #### Scenario: OpenAI Chat Completions provider - **WHEN** target 配置 `llm.provider: openai` - **THEN** LLM checker SHALL 使用 `@ai-sdk/openai` 的 `openai.chat(model)` 创建模型调用对象 #### Scenario: OpenAI Responses provider - **WHEN** target 配置 `llm.provider: openai-responses` - **THEN** LLM checker SHALL 使用 `@ai-sdk/openai` 的 `openai.responses(model)` 创建模型调用对象 #### Scenario: Anthropic provider - **WHEN** target 配置 `llm.provider: anthropic` - **THEN** LLM checker SHALL 使用 `@ai-sdk/anthropic` 的 `anthropic.messages(model)` 创建模型调用对象 #### Scenario: 非流式调用模式 - **WHEN** target 配置 `llm.mode: http` 或省略 `llm.mode` - **THEN** LLM checker SHALL 调用 `generateText` 并从返回结果构建非流式 observation #### Scenario: 流式调用模式 - **WHEN** target 配置 `llm.mode: stream` - **THEN** LLM checker SHALL 调用 `streamText` 并消费 `fullStream` 构建流式 observation #### Scenario: 超时取消传递给 SDK - **WHEN** 引擎注入的 `ctx.signal` 被 abort - **THEN** LLM checker SHALL 将该 signal 传递给 AI SDK 调用并将取消或超时结果记录为检查失败 ### Requirement: LLM 配置解析与默认值 LLM checker SHALL 解析 `llm.provider`、`llm.url`、`llm.model`、`llm.prompt`、`llm.mode`、`llm.key`、`llm.authToken`、`llm.headers`、`llm.ignoreSSL`、`llm.options` 和 `llm.providerOptions`。`llm.options` SHALL 支持 `maxOutputTokens`(默认 `16`)、`temperature`(默认 `0`)、`topP`、`topK`、`presencePenalty`、`frequencyPenalty`、`stopSequences`(字符串数组)和 `seed`。`llm.mode` 默认值 SHALL 为 `http`,`llm.key` 默认值 SHALL 为空字符串,`llm.ignoreSSL` 默认值 SHALL 为 `false`。LLM checker MUST NOT 隐式读取 AI SDK 默认环境变量。 #### Scenario: 最简 LLM target 解析 - **WHEN** 系统读取只包含 `type: llm` 以及 `llm.provider`、`llm.url`、`llm.model`、`llm.prompt` 的 target - **THEN** 系统 SHALL 解析为 LLM target,并填充 `mode=http`、`key=""`、`ignoreSSL=false`、`options.maxOutputTokens=16`、`options.temperature=0` #### Scenario: headers 默认值合并 - **WHEN** `defaults.llm.headers` 和 target `llm.headers` 同时配置同名 header - **THEN** LLM checker SHALL 按原始 header key 浅合并 headers,并由 target `llm.headers` 覆盖 defaults 中同名 key #### Scenario: options 默认值合并 - **WHEN** `defaults.llm.options` 和 target `llm.options` 同时配置同名 option - **THEN** LLM checker SHALL 浅合并 options,并由 target `llm.options` 覆盖 defaults 中同名字段 #### Scenario: providerOptions 默认值合并 - **WHEN** `defaults.llm.providerOptions` 和 target `llm.providerOptions` 同时配置同名 provider namespace - **THEN** LLM checker SHALL 按 provider namespace 浅合并 providerOptions,并由 target namespace 覆盖 defaults 中同名 namespace #### Scenario: Anthropic Bearer token - **WHEN** target 配置 `llm.provider: anthropic` 和非空 `llm.authToken` - **THEN** LLM checker SHALL 将 `authToken` 映射到 Anthropic SDK 的 Bearer token 认证字段 #### Scenario: key 不隐式读取环境变量 - **WHEN** target 未配置 `llm.key` - **THEN** LLM checker SHALL 将 SDK provider 的 api key 设置为空字符串,而不是隐式读取 SDK 默认环境变量 ### Requirement: LLM HTTP Metadata 与 TLS LLM checker SHALL 通过 AI SDK provider 的 custom fetch 注入 observing fetch。observing fetch SHALL 调用 Bun `fetch`,在不消费 response body 的前提下记录 HTTP status、statusText 和 headers。`llm.ignoreSSL: true` 时,observing fetch SHALL 仅对当前 target 的 provider 请求使用 Bun `tls.rejectUnauthorized=false`。 #### Scenario: 捕获 HTTP metadata - **WHEN** AI SDK provider 发起模型 HTTP 请求并收到响应 - **THEN** observing fetch SHALL 记录 status code 和响应 headers,供 `expect.status` 与 `expect.headers` 使用 #### Scenario: 不消费响应体 - **WHEN** observing fetch 捕获 HTTP metadata - **THEN** observing fetch SHALL 返回原始 response 给 AI SDK,不提前读取或克隆消费 body #### Scenario: 忽略证书校验 - **WHEN** target 配置 `llm.ignoreSSL: true` - **THEN** observing fetch SHALL 对当前 target 的 provider 请求设置 `tls.rejectUnauthorized=false` ### Requirement: LLM Observation LLM checker SHALL 在 SDK 调用结果和 expect 断言之间构建 `LlmCheckObservation`。observation SHALL 包含 provider、model、mode、outputText、finishReason、rawFinishReason、usage、stream、http 和 warnings 中可观测的字段。`mode: http` 的 `outputText` SHALL 来自 `generateText.text`。`mode: stream` 的 `outputText` SHALL 来自 `fullStream` 中 `text-delta` 的原始文本聚合。 #### Scenario: 非流式 observation - **WHEN** `generateText` 调用成功 - **THEN** LLM checker SHALL 从 SDK result 中提取 outputText、finishReason、rawFinishReason、usage、response headers 和 HTTP metadata #### Scenario: 流式 observation - **WHEN** `streamText` 调用成功且 stream 正常完成 - **THEN** LLM checker SHALL 从 `fullStream` 聚合 outputText,并记录 stream.completed、firstTokenMs、finishReason、rawFinishReason、usage 和 HTTP metadata #### Scenario: APICallError observation - **WHEN** AI SDK 抛出带 statusCode 或 responseHeaders 的 `APICallError` - **THEN** LLM checker SHALL 构建包含可用 HTTP metadata 的 observation,并继续执行可执行的 status、headers 和 duration 断言 #### Scenario: 无 HTTP metadata 的 SDK 错误 - **WHEN** AI SDK 抛出不带 statusCode 和 responseHeaders 的错误 - **THEN** LLM checker SHALL 返回 `phase: "request"` 的 error failure ### Requirement: LLM Expect 断言 LLM checker SHALL 支持 `expect.status`、`expect.headers`、`expect.output`、`expect.finishReason`、`expect.rawFinishReason`、`expect.usage.inputTokens`、`expect.usage.outputTokens`、`expect.usage.totalTokens`、`expect.stream.completed`、`expect.stream.firstTokenMs` 和 `expect.durationMs`。`expect.status` SHALL 保持 HTTP 状态码数组语义并复用共享 status code 断言,未配置时在 Resolved expect 中物化默认 `[200]`。`expect.headers` SHALL 使用共享 `RawKeyedExpectations` 输入并在运行期使用 `KeyedExpectations`,header key 大小写不敏感。`expect.output` MUST 使用共享 `RawContentExpectations` 输入并在运行期使用 `ContentExpectations`。`expect.finishReason` 和 `expect.rawFinishReason` SHALL 使用共享 `RawValueExpectation` 输入并在运行期使用 `ValueExpectation`。`expect.usage.*`、`expect.stream.firstTokenMs` 和 `expect.durationMs` SHALL 使用共享 `RawValueExpectation` 输入并在运行期使用 `ValueExpectation`。LLM checker MUST 按固定顺序快速失败,非流式顺序为 status、headers、output、finishReason、rawFinishReason、usage、durationMs;流式顺序为 status、headers、stream.completed、stream.firstTokenMs、output、finishReason、rawFinishReason、usage、durationMs。 #### Scenario: 默认 status 断言 - **WHEN** LLM target 未配置 `expect.status` - **THEN** LLM checker SHALL 在 Resolved expect 中使用默认 `status: [200]` 语义 #### Scenario: expect headers 通过 - **WHEN** observing fetch 捕获的响应 headers 满足 `expect.headers` 配置 - **THEN** LLM checker SHALL 通过共享 header expectation 包装函数判定 headers 断言通过 #### Scenario: output ContentExpectations 通过 - **WHEN** LLM 输出文本满足 `expect.output` 中配置的全部 ContentExpectations - **THEN** LLM checker SHALL 判定 output 阶段通过 #### Scenario: finishReason ValueMatcher 通过 - **WHEN** observation.finishReason 为 `stop` 且 target 配置 `expect.finishReason: {equals: "stop"}` - **THEN** LLM checker SHALL 判定 finishReason 阶段通过 #### Scenario: rawFinishReason regex 通过 - **WHEN** observation.rawFinishReason 为 `end_turn` 且 target 配置 `expect.rawFinishReason: {regex: "^(stop|end_turn)$"}` - **THEN** LLM checker SHALL 判定 rawFinishReason 阶段通过 #### Scenario: usage matcher 通过 - **WHEN** observation.usage.totalTokens 为 14 且 target 配置 `expect.usage.totalTokens: {lte: 20}` - **THEN** LLM checker SHALL 判定 usage 阶段通过 #### Scenario: durationMs matcher 失败 - **WHEN** LLM target 配置 `expect.durationMs: {lte: 1000}` 且实际执行耗时为 1500ms - **THEN** LLM checker SHALL 返回 phase=`duration` 的 mismatch failure #### Scenario: 首个 expect 失败 - **WHEN** 多个 LLM expect 中某个较早顺序的断言失败 - **THEN** LLM checker SHALL 立即返回该断言对应的 mismatch failure,不继续执行后续断言 #### Scenario: 期望认证失败状态 - **WHEN** AI SDK 抛出带 HTTP status 401 的 `APICallError`,且 target 仅配置 `expect.status: [401]` - **THEN** LLM checker SHALL 判定本次检查为 `matched=true` #### Scenario: APICallError 缺失模型输出 - **WHEN** AI SDK 抛出带 HTTP status 的 `APICallError`,且 target 同时配置需要模型结果的 `expect.output` - **THEN** LLM checker SHALL 因 `outputText` 缺失返回 `phase: "output"` 的 mismatch failure ### Requirement: LLM Output 规则 LLM checker SHALL 使用共享 `ContentRules` 校验 `expect.output`。每个 output rule SHALL 为直接 `ValueMatcher`,或 `json`、`css`、`xpath` extractor 规则之一。直接 matcher SHALL 作用于原始输出字符串。`equals` SHALL 对原始输出字符串做严格相等比较。`contains` SHALL 判断原始输出是否包含子串。`regex` SHALL 对原始输出执行无 flags 正则匹配。`json` SHALL 将原始输出解析为 JSON,并用现有 JSONPath 子集和 `ValueMatcher` 校验提取值。`json.equals` SHALL 支持任意 JSON value。`css` 和 `xpath` 在 schema 层面可用,但 LLM 输出通常为纯文本或 JSON,实际场景中仅 `json` 提取器有意义。 #### Scenario: 原始输出严格相等 - **WHEN** `outputText` 为 `"OK\n"` 且 target 配置 `expect.output: [{ equals: "OK" }]` - **THEN** LLM checker SHALL 判定 output 断言失败,因为 equals 不自动 trim #### Scenario: output contains 通过 - **WHEN** `outputText` 包含配置的子串 - **THEN** LLM checker SHALL 判定该 output contains 规则通过 #### Scenario: output regex 通过 - **WHEN** `outputText` 匹配配置的合法 regex - **THEN** LLM checker SHALL 判定该 output regex 规则通过 #### Scenario: output JSONPath 字符串 equals 通过 - **WHEN** `outputText` 是 JSON 字符串且 JSONPath 提取字符串值满足 `equals` - **THEN** LLM checker SHALL 判定该 output json 规则通过 #### Scenario: output JSONPath 对象 equals 通过 - **WHEN** `outputText` 是 JSON 字符串且 JSONPath 提取对象值满足 `equals` - **THEN** LLM checker SHALL 使用深度相等判定该 output json 规则通过 #### Scenario: output JSONPath 存在性默认语义 - **WHEN** `outputText` 是 JSON 字符串且 target 配置 `expect.output: [{json: {path: "$.status"}}]` - **THEN** resolve 阶段 SHALL 将该 expectation 的 matcher 物化为 `{exists: true}`,运行期按存在性语义执行 #### Scenario: output 规则按顺序快速失败 - **WHEN** `expect.output` 包含多个 expectation 且第一条 expectation 失败 - **THEN** LLM checker SHALL 返回第一条失败 expectation 的 mismatch failure,不继续校验后续 output expectation ### Requirement: LLM Stream 断言 LLM checker SHALL 仅允许 `mode: stream` 使用 `expect.stream`。仅当用户配置了 `expect.stream` 且未配置 `expect.stream.completed` 时,resolve 阶段 SHALL 在 Resolved expect 中物化默认 `completed: true`;LLM checker MUST NOT 因为 `llm.mode: stream` 自动添加 `stream.completed` 断言。`expect.stream.firstTokenMs` SHALL 使用共享 `RawValueExpectation` 输入并在运行期使用 `ValueExpectation`,且仅统计第一个非空 `text-delta` 事件耗时,不统计 reasoning、tool call 或 source 事件。 #### Scenario: stream completed 默认值 - **WHEN** target 配置 `llm.mode: stream` 且配置 `expect.stream: {}` - **THEN** resolve 阶段 SHALL 在 Resolved expect 中物化 `stream.completed: true` 并要求 SDK stream 正常完成 #### Scenario: 未配置 expect.stream 不添加 completed - **WHEN** target 配置 `llm.mode: stream` 但未配置 `expect.stream` - **THEN** resolve 阶段 SHALL NOT 自动添加 `stream.completed` 断言 #### Scenario: stream error - **WHEN** `fullStream` 产生 error part - **THEN** LLM checker SHALL 返回 `phase: "stream"` 的 failure #### Scenario: firstTokenMs 达标 - **WHEN** target 配置 `expect.stream.firstTokenMs: {lte: 1000}` 且首个非空 text delta 耗时满足 matcher - **THEN** LLM checker SHALL 判定 firstTokenMs 断言通过 #### Scenario: firstTokenMs 缺失 - **WHEN** target 配置 `expect.stream.firstTokenMs` 但 stream 未产生非空 text delta - **THEN** LLM checker SHALL 返回 `phase: "stream"` 的 mismatch failure #### Scenario: APICallError 不被默认 completed 阻断 - **WHEN** `mode: stream` 的 SDK 调用在 stream 启动前抛出带 HTTP status 的 `APICallError` - **THEN** 默认 `stream.completed=true` SHALL NOT 阻断基于 status 和 headers 的 APICallError 状态探测 ### Requirement: LLM Failure Phase 与状态摘要 LLM checker SHALL 使用 `request`、`status`、`headers`、`stream`、`output`、`finishReason`、`rawFinishReason`、`usage`、`duration` 作为第一版 failure phase。成功结果的 API detail SHALL 从持久化 observation 动态构造,简短描述 provider、mode、HTTP status、finish reason、raw finish reason、first token、输出长度和 token usage 中可用的信息。observation 和 detail MUST NOT 写入完整 prompt、完整输出或 key。 #### Scenario: request failure - **WHEN** 模型请求因网络错误、认证调用异常、AbortSignal 或无 HTTP metadata 的 SDK 错误失败 - **THEN** LLM checker SHALL 返回 `phase: "request"` 的 error failure #### Scenario: output mismatch failure - **WHEN** 模型输出不满足 `expect.output` - **THEN** LLM checker SHALL 返回 `phase: "output"` 的 mismatch failure #### Scenario: 非流式成功摘要 - **WHEN** `provider: openai` 的非流式检查成功 - **THEN** detail SHALL 使用类似 `LLM openai http 200 finish=stop, output=2 chars, usage=12/2 tokens` 的简短格式 #### Scenario: 流式成功摘要 - **WHEN** `provider: anthropic` 的流式检查成功且存在 raw finish reason - **THEN** detail SHALL 使用类似 `LLM anthropic stream 200 finish=stop raw=end_turn, firstToken=624ms, output=2 chars` 的简短格式 #### Scenario: serialize 展示文本 - **WHEN** store 同步 LLM target - **THEN** LLM checker `serialize()` SHALL 返回类似 `openai:gpt-4o-mini @ https://api.openai.com/v1` 的 target 展示文本和 resolved config JSON ### Requirement: LLM Checker 测试策略 LLM checker 的自动化测试 MUST 不访问真实外部模型服务。测试 SHALL 使用本地 mock HTTP/SSE 服务模拟 OpenAI Chat Completions、OpenAI Responses 和 Anthropic Messages 的成功、错误和流式响应。测试 SHALL 覆盖 schema、语义校验、defaults 合并、变量替换、provider factory、observation、expect、execute、registry 注册、配置加载和 JSON Schema 导出。 #### Scenario: 本地 mock provider 测试成功路径 - **WHEN** 测试运行 LLM checker 的 OpenAI、OpenAI Responses 和 Anthropic 成功路径 - **THEN** 测试 SHALL 使用本地 mock 服务返回 provider 响应,不依赖外部网络或真实 API key #### Scenario: 本地 mock provider 测试错误路径 - **WHEN** 测试运行 401、429、500、超时、stream error、stream abort、缺 usage 或无文本输出路径 - **THEN** 测试 SHALL 断言 LLM checker 返回符合 spec 的 matched、failure phase、actual、detail 和 observation #### Scenario: 质量检查覆盖 LLM checker - **WHEN** 实现完成后执行质量检查 - **THEN** `bun run schema:check`、`bun run check` SHALL 通过