# 兼容性检测脚本

## 概述

本目录包含一组用于检测 LLM API 网关对 **OpenAI** 和 **Anthropic** 协议兼容性的测试脚本。通过向目标服务发送一系列结构化请求，验证响应格式、字段类型、错误处理等是否符合协议规范。

## 脚本结构

```
scripts/
├── core.py              # 公共基础设施
├── detect_openai.py     # OpenAI 兼容协议测试
└── detect_anthropic.py  # Anthropic 兼容协议测试
```

### core.py — 公共模块

提供所有检测脚本共享的基础功能：

| 函数/类 | 说明 |
|---------|------|
| `TestCase` | 测试用例数据类（URL、方法、请求头、请求体、验证器） |
| `TestResult` | 测试结果数据类（状态码、耗时、错误类型、响应内容） |
| `http_request()` | 普通 HTTP 请求（支持重试、自动 JSON 序列化） |
| `http_stream_request()` | 流式 HTTP 请求（SSE，支持重试） |
| `parse_sse_events()` | 从 SSE 响应文本中提取 `data:` 事件列表 |
| `create_ssl_context()` | 创建不验证证书的 SSL 上下文（测试环境用） |
| `run_test()` | 执行单个用例并打印结构化输出 |
| `run_test_suite()` | 执行完整测试套件并打印统计摘要 |
| `check_required_fields()` | 检查必需字段（通用验证辅助） |
| `check_field_type()` | 检查字段类型（通用验证辅助） |
| `check_enum_value()` | 检查枚举值（通用验证辅助） |
| `check_array_items_type()` | 检查数组元素类型（通用验证辅助） |
| `validate_response_structure()` | 组合上述函数的通用验证器 |

**注意**：`core.py` 只包含协议无关的通用功能。每个协议独有的响应验证函数应定义在各自的检测脚本中（如 `validate_openai_chat_completion_response` 在 `detect_openai.py` 中）。

### detect_openai.py — OpenAI 兼容测试

检测目标服务对 OpenAI Chat Completions API 的兼容程度。

**覆盖的 API 端点：**
- `GET /models` — 模型列表
- `GET /models/{model}` — 模型详情
- `POST /chat/completions` — 对话补全

**测试类别：**
- **正面用例**：基本对话、system/developer 角色、多轮对话、参数组合（temperature、top_p、seed、penalty、stop、n、max_tokens、max_completion_tokens、logit_bias、reasoning_effort、service_tier、verbosity、response_format）
- **扩展功能**：`--vision`（图片输入）、`--stream`（流式响应）、`--tools`（工具调用）、`--logprobs`（对数概率）、`json_schema`（结构化输出）
- **负面用例**：缺参数、空消息、无效认证、不存在的模型、畸形 JSON、max_tokens 负数/0、temperature 越界

**响应验证：**
- Models List：检查 `object: "list"`、`data` 数组中每个模型的 `id`、`object`、`created`、`owned_by`
- Model Retrieve：检查 `id`、`object: "model"`、`created`、`owned_by`
- Chat Completion：检查 `id`、`object: "chat.completion"`、`created`、`model`、`choices` 数组结构、`usage` 对象

### detect_anthropic.py — Anthropic 兼容测试

检测目标服务对 Anthropic Messages API 的兼容程度。

**覆盖的 API 端点：**
- `GET /v1/models` — 模型列表
- `GET /v1/models/{model}` — 模型详情
- `POST /v1/messages` — 消息对话
- `POST /v1/messages/count_tokens` — Token 计数

**测试类别：**
- **正面用例**：基本对话、system prompt（字符串/数组格式）、多轮对话、assistant prefill、content 数组格式、参数组合（temperature、top_p、top_k、max_tokens、stop_sequences、metadata）
- **扩展功能**：`--vision`（图片输入）、`--stream`（流式响应）、`--tools`（工具调用）、`--thinking`（扩展思维）
- **负面用例**：缺 header、无效认证、缺参数、空消息、畸形 JSON、非法 role、max_tokens 负数/0、temperature 越界

**响应验证：**
- Models List：检查 `data`、`has_more`、每个模型的 `id`、`type: "model"`、`display_name`、`created_at`
- Model Retrieve：检查 `id`、`type: "model"`、`display_name`、`created_at`
- Messages：检查 `id`、`type: "message"`、`role: "assistant"`、`content` 数组、`model`、`usage`
- Count Tokens：检查 `input_tokens` 为数字

## 使用方式

### 基本用法

```bash
# OpenAI 兼容测试
python3 scripts/detect_openai.py --base_url http://localhost:9826/v1

# Anthropic 兼容测试
python3 scripts/detect_anthropic.py --base_url http://localhost:9826
```

### 带认证

```bash
python3 scripts/detect_openai.py --base_url http://localhost:9826/v1 --api_key sk-xxx --model gpt-4o

python3 scripts/detect_anthropic.py --base_url http://localhost:9826 --api_key sk-xxx --model claude-sonnet-4-5
```

### 扩展测试

```bash
# 开启所有扩展测试
python3 scripts/detect_openai.py --base_url http://localhost:9826/v1 --all

python3 scripts/detect_anthropic.py --base_url http://localhost:9826 --all

# 单独开启某项
python3 scripts/detect_openai.py --base_url http://localhost:9826/v1 --stream --tools

python3 scripts/detect_anthropic.py --base_url http://localhost:9826 --stream --tools --thinking
```

### 命令行参数

| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--base_url` | API 基础地址（必填） | — |
| `--api_key` | API 密钥 | 空 |
| `--model` | 测试使用的模型名称 | `gpt-4o` / `claude-sonnet-4-5` |
| `--vision` | 执行视觉相关测试 | 关闭 |
| `--stream` | 执行流式响应测试 | 关闭 |
| `--tools` | 执行工具调用测试 | 关闭 |
| `--logprobs` | 执行 logprobs 测试（仅 OpenAI） | 关闭 |
| `--json_schema` | 执行 Structured Output 测试（仅 OpenAI） | 关闭 |
| `--thinking` | 执行扩展思维测试（仅 Anthropic） | 关闭 |
| `--all` | 开启所有扩展测试 | 关闭 |

## 输出示例

```
Anthropic 兼容性测试
目标: http://localhost:9826
模型: claude-sonnet-4-5
时间: 2026-04-21 10:30:00
用例: 35 个 | 扩展: stream, tools

[1/35] 获取模型列表 (GET /v1/models)

URL: GET http://localhost:9826/v1/models

Headers:
  x-api-key: sk-xxx
  anthropic-version: 2023-06-01

响应 (200, 0.12s):
{
  "data": [...],
  "has_more": false
}
✓ 响应验证通过

[5/35] 基本对话（仅 user）

URL: POST http://localhost:9826/v1/messages

Headers:
  x-api-key: sk-xxx
  Content-Type: application/json

入参:
{
  "model": "claude-sonnet-4-5",
  "max_tokens": 5,
  "messages": [{"role": "user", "content": "Hi"}]
}

响应 (200, 0.23s):
{
  "id": "msg_xxx",
  "type": "message",
  "role": "assistant",
  "content": [...],
  "model": "claude-sonnet-4-5",
  "usage": {"input_tokens": 10, "output_tokens": 5}
}
✓ 响应验证通过

测试完成 | 总计: 35 | 成功: 33 | 客户端错误: 2 | 服务端错误: 0 | 网络错误: 0
```

## 测试设计原则

1. **所有正面用例都启用响应验证器** — 任何响应结构偏差都会立即暴露，避免掩盖错误
2. **负面用例覆盖常见错误场景** — 缺参数、类型错误、范围越界、认证失败
3. **扩展功能通过 flag 按需开启** — 避免在基础测试中引入不必要的依赖
4. **验证器基于协议规范编写** — 严格检查必需字段、类型、枚举值
5. **流式与非流式覆盖一致** — 流式只是传输方式不同，功能覆盖范围应完全对应（见下文）

## 新增检测脚本开发流程

如需为新的协议（如 Google Gemini、Cohere 等）开发检测脚本，遵循以下流程：

### 1. 在新脚本中定义协议专用的验证函数

每个协议的响应结构是独特的，验证函数应定义在各自的脚本中，不要放入 `core.py`。例如：

```python
# 在 detect_gemini.py 中
def validate_gemini_generate_content_response(response_text: str) -> Tuple[bool, List[str]]:
    """验证 Gemini GenerateContent 响应"""
    errors = []
    try:
        data = json.loads(response_text)
    except json.JSONDecodeError as e:
        return False, [f"响应不是有效的JSON: {e}"]

    # 检查 Gemini 特有的字段
    required_fields = ["candidates", "usageMetadata"]
    for field in required_fields:
        if field not in data:
            errors.append(f"缺少必需字段: {field}")
    ...
    return len(errors) == 0, errors
```

### 2. 在 `core.py` 中只添加通用验证辅助

只有当多个协议都需要相同的验证逻辑时，才将函数提取到 `core.py`。目前已有的通用函数：

| 函数 | 说明 |
|------|------|
| `check_required_fields()` | 检查必需字段是否存在 |
| `check_field_type()` | 检查字段类型 |
| `check_enum_value()` | 检查枚举值 |
| `check_array_items_type()` | 检查数组元素类型 |
| `validate_response_structure()` | 组合上述函数的通用验证器 |
| `parse_sse_events()` | 从 SSE 响应文本中提取 `data:` 事件 |

### 3. 创建检测脚本模板

```python
#!/usr/bin/env python3
"""新协议兼容性接口测试脚本"""

import json
import argparse
from typing import Dict, List, Tuple, Any
from core import (
    create_ssl_context,
    TestCase,
    run_test_suite,
    validate_response_structure,
)

def build_headers(api_key: str) -> Dict[str, str]:
    """构建请求头"""
    ...

def validate_xxx_response(response_text: str) -> Tuple[bool, List[str]]:
    """验证响应结构（协议专用）"""
    ...

def validate_xxx_streaming_response(response_text: str) -> Tuple[bool, List[str]]:
    """验证流式响应结构（协议专用）"""
    from core import parse_sse_events
    ...

def main():
    parser = argparse.ArgumentParser(...)
    parser.add_argument("--base_url", required=True, ...)
    parser.add_argument("--api_key", default="", ...)
    parser.add_argument("--model", default="...", ...)
    parser.add_argument("--stream", action="store_true", ...)
    parser.add_argument("--all", action="store_true", ...)
    args = parser.parse_args()

    cases: List[TestCase] = []

    # ---- 共享定义（供流式和非流式用例共同使用）----
    # 将 tool、image_url 等定义放在所有功能块之前，
    # 避免流式和非流式块中重复定义
    tool_xxx = { ... }
    image_url = "..."

    # ==== 非流式正面用例（都添加 validator）====
    cases.append(TestCase(
        desc="...", method="...", url=..., headers=..., body=...,
        validator=validate_xxx_response
    ))

    # ==== 非流式负面用例（不添加 validator）====
    cases.append(TestCase(desc="...", method="...", url=..., headers=..., body=...))

    # ==== --stream ====
    if args.stream:
        # 核心对话流式用例：每个非流式正面用例都应有对应的流式版本
        # 仅传输方式不同（stream=True, stream=True），
        # 功能覆盖（参数、角色、多轮等）必须与非流式一致
        cases.append(TestCase(
            desc="流式...", method="POST", url=..., headers=headers,
            body={ ..., "stream": True },
            stream=True,
            validator=validate_xxx_streaming_response
        ))

        # 流式 + 其他 flag 组合（放在 --stream 块内部）
        if args.vision:
            cases.append(TestCase(
                desc="流式图片输入 (--stream + --vision)",
                ...,
                stream=True,
                validator=validate_xxx_streaming_response
            ))
        if args.tools:
            cases.append(TestCase(
                desc="流式工具调用 (--stream + --tools)",
                ...,
                stream=True,
                validator=validate_xxx_streaming_response
            ))

    run_test_suite(cases=cases, ssl_ctx=ssl_ctx, title="...", base_url=..., model=..., flags=...)

if __name__ == "__main__":
    main()
```

### 关键要点

- **协议专用验证函数放在各自的脚本中** — 不要污染 `core.py`
- **只有多协议通用的验证逻辑才提取到 `core.py`** — 遵循 DRY 原则但不过度抽象
- **所有正面用例必须添加 validator** — 确保响应结构正确
- **负面用例不添加 validator** — 预期返回错误响应
- **扩展功能用 flag 控制** — 保持基础测试轻量
- **遵循现有命名和代码风格** — 中文注释、类型注解、dataclass 使用

### 流式测试覆盖原则

流式（SSE）与非流式只是数据传输方式不同，服务端对请求参数的处理逻辑应完全一致。因此：

1. **每个非流式正面用例都应有对应的流式版本** — 包括不同的消息角色组合、参数组合、工具调用等
2. **共享定义提前声明** — `tool`、`image_url`、`json_schema` 等定义放在所有功能块之前，流式和非流式共用同一实例，避免重复定义
3. **flag 组合放在 `--stream` 块内部** — 流式+工具、流式+视觉等组合用例放在 `if args.stream:` 内部的 `if args.tools:` / `if args.vision:` 子块中，不需要单独的组合 flag
4. **负面用例不需要流式版本** — 参数校验发生在请求处理之前，与传输方式无关
5. **Models API 等非 Chat 端点不需要流式测试** — 它们本身不支持流式传输

| 用例类别 | 非流式 | 流式 |
|----------|--------|------|
| 基本对话 / 多轮对话 | ✓ | ✓ |
| 消息角色组合（system, developer 等） | ✓ | ✓ |
| 参数组合（temperature, top_p, max_tokens 等） | ✓ | ✓ |
| 工具调用（tool_choice 各模式） | ✓ | ✓（在 `--stream` 块内检查 `--tools`） |
| 视觉（图片输入） | ✓ | ✓（在 `--stream` 块内检查 `--vision`） |
| 扩展思维 / Logprobs 等特性 | ✓ | ✓（在 `--stream` 块内检查对应 flag） |
| 高级参数（service_tier, reasoning_effort 等） | ✓ | ✓ |
| 负面用例（缺参数、越界、认证失败） | ✓ | ✗（参数校验与传输方式无关） |
| Models API（GET 端点） | ✓ | ✗（不支持流式） |

## 许可证

MIT
