## 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 通过
