mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-02 07:25:26 +08:00
feat: enhance OpenAI response rendering to include reasoning and improve tool call detection from thinking channel, and refactor testing scripts for unified unit test execution.
This commit is contained in:
@@ -86,7 +86,7 @@ Manually build WebUI to `static/admin/`:
|
||||
go test ./...
|
||||
|
||||
# End-to-end live tests (real accounts)
|
||||
./scripts/testsuite/run-live.sh
|
||||
./tests/scripts/run-live.sh
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
@@ -86,7 +86,7 @@ docker-compose -f docker-compose.dev.yml up
|
||||
go test ./...
|
||||
|
||||
# 端到端全链路测试(真实账号)
|
||||
./scripts/testsuite/run-live.sh
|
||||
./tests/scripts/run-live.sh
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
@@ -518,7 +518,7 @@ curl http://127.0.0.1:5001/v1/chat/completions \
|
||||
Run the full live testsuite before release (real account tests):
|
||||
|
||||
```bash
|
||||
./scripts/testsuite/run-live.sh
|
||||
./tests/scripts/run-live.sh
|
||||
```
|
||||
|
||||
With custom flags:
|
||||
|
||||
@@ -518,7 +518,7 @@ curl http://127.0.0.1:5001/v1/chat/completions \
|
||||
建议在发布前执行完整的端到端测试集(使用真实账号):
|
||||
|
||||
```bash
|
||||
./scripts/testsuite/run-live.sh
|
||||
./tests/scripts/run-live.sh
|
||||
```
|
||||
|
||||
可自定义参数:
|
||||
|
||||
12
README.MD
12
README.MD
@@ -350,8 +350,10 @@ ds2api/
|
||||
│ ├── components/ # AccountManager / ApiTester / BatchImport / VercelSync / Login / LandingPage
|
||||
│ └── locales/ # 中英文语言包(zh.json / en.json)
|
||||
├── scripts/
|
||||
│ ├── build-webui.sh # WebUI 手动构建脚本
|
||||
│ └── testsuite/ # 测试集运行脚本
|
||||
│ └── build-webui.sh # WebUI 手动构建脚本
|
||||
├── tests/
|
||||
│ ├── compat/ # 兼容性测试夹具与期望输出
|
||||
│ └── scripts/ # 统一测试脚本入口(unit/e2e)
|
||||
├── static/admin/ # WebUI 构建产物(不提交到 Git)
|
||||
├── .github/
|
||||
│ ├── workflows/ # GitHub Actions(Release 自动构建)
|
||||
@@ -379,11 +381,11 @@ ds2api/
|
||||
## 测试
|
||||
|
||||
```bash
|
||||
# 单元测试
|
||||
go test ./...
|
||||
# 单元测试(Go + Node)
|
||||
./tests/scripts/run-unit-all.sh
|
||||
|
||||
# 一键端到端全链路测试(真实账号,生成完整请求/响应日志)
|
||||
./scripts/testsuite/run-live.sh
|
||||
./tests/scripts/run-live.sh
|
||||
|
||||
# 或自定义参数
|
||||
go run ./cmd/ds2api-tests \
|
||||
|
||||
12
README.en.md
12
README.en.md
@@ -350,8 +350,10 @@ ds2api/
|
||||
│ ├── components/ # AccountManager / ApiTester / BatchImport / VercelSync / Login / LandingPage
|
||||
│ └── locales/ # Language packs (zh.json / en.json)
|
||||
├── scripts/
|
||||
│ ├── build-webui.sh # Manual WebUI build script
|
||||
│ └── testsuite/ # Testsuite runner scripts
|
||||
│ └── build-webui.sh # Manual WebUI build script
|
||||
├── tests/
|
||||
│ ├── compat/ # Compatibility fixtures and expected outputs
|
||||
│ └── scripts/ # Unified test script entrypoints (unit/e2e)
|
||||
├── static/admin/ # WebUI build output (not committed to Git)
|
||||
├── .github/
|
||||
│ ├── workflows/ # GitHub Actions (Release artifact automation)
|
||||
@@ -379,11 +381,11 @@ ds2api/
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Unit tests
|
||||
go test ./...
|
||||
# Unit tests (Go + Node)
|
||||
./tests/scripts/run-unit-all.sh
|
||||
|
||||
# One-command live end-to-end tests (real accounts, full request/response logs)
|
||||
./scripts/testsuite/run-live.sh
|
||||
./tests/scripts/run-live.sh
|
||||
|
||||
# Or with custom flags
|
||||
go run ./cmd/ds2api-tests \
|
||||
|
||||
16
TESTING.md
16
TESTING.md
@@ -8,8 +8,10 @@ DS2API 提供两个层级的测试:
|
||||
|
||||
| 层级 | 命令 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 单元测试 | `go test ./...` | 不需要真实账号 |
|
||||
| 端到端测试 | `./scripts/testsuite/run-live.sh` | 使用真实账号执行全链路测试 |
|
||||
| 单元测试(Go) | `./tests/scripts/run-unit-go.sh` | 不需要真实账号 |
|
||||
| 单元测试(Node) | `./tests/scripts/run-unit-node.sh` | 不需要真实账号 |
|
||||
| 单元测试(全部) | `./tests/scripts/run-unit-all.sh` | 不需要真实账号 |
|
||||
| 端到端测试 | `./tests/scripts/run-live.sh` | 使用真实账号执行全链路测试 |
|
||||
|
||||
端到端测试集会录制完整的请求/响应日志,用于故障排查。
|
||||
|
||||
@@ -20,17 +22,19 @@ DS2API 提供两个层级的测试:
|
||||
### 单元测试 | Unit Tests
|
||||
|
||||
```bash
|
||||
go test ./...
|
||||
./tests/scripts/run-unit-all.sh
|
||||
```
|
||||
|
||||
```bash
|
||||
node --test api/helpers/stream-tool-sieve.test.js api/chat-stream.test.js api/compat/js_compat_test.js
|
||||
# 或按语言拆分执行
|
||||
./tests/scripts/run-unit-go.sh
|
||||
./tests/scripts/run-unit-node.sh
|
||||
```
|
||||
|
||||
### 端到端测试 | End-to-End Tests
|
||||
|
||||
```bash
|
||||
./scripts/testsuite/run-live.sh
|
||||
./tests/scripts/run-live.sh
|
||||
```
|
||||
|
||||
**默认行为**:
|
||||
@@ -179,7 +183,7 @@ go run ./cmd/ds2api-tests \
|
||||
|
||||
```bash
|
||||
# 确保 config.json 存在且包含有效测试账号
|
||||
./scripts/testsuite/run-live.sh
|
||||
./tests/scripts/run-live.sh
|
||||
exit_code=$?
|
||||
if [ $exit_code -ne 0 ]; then
|
||||
echo "Tests failed! Check artifacts for details."
|
||||
|
||||
@@ -47,27 +47,46 @@ func BuildResponseObject(responseID, model, finalPrompt, finalThinking, finalTex
|
||||
// produced a standalone structured payload. This prevents accidental
|
||||
// empty output_text on normal prose that merely contains tool_call-like text.
|
||||
detected := util.ParseStandaloneToolCalls(finalText, toolNames)
|
||||
toolCallsFromThinking := false
|
||||
if len(detected) == 0 && strings.TrimSpace(finalThinking) != "" {
|
||||
detected = util.ParseStandaloneToolCalls(finalThinking, toolNames)
|
||||
toolCallsFromThinking = len(detected) > 0
|
||||
}
|
||||
exposedOutputText := finalText
|
||||
output := make([]any, 0, 2)
|
||||
if len(detected) > 0 {
|
||||
exposedOutputText = ""
|
||||
if !toolCallsFromThinking || strings.TrimSpace(finalText) != "" {
|
||||
exposedOutputText = ""
|
||||
} else {
|
||||
exposedOutputText = finalThinking
|
||||
}
|
||||
if strings.TrimSpace(finalThinking) != "" {
|
||||
output = append(output, map[string]any{
|
||||
"type": "reasoning",
|
||||
"text": finalThinking,
|
||||
})
|
||||
}
|
||||
output = append(output, map[string]any{
|
||||
"type": "tool_calls",
|
||||
"tool_calls": util.FormatOpenAIToolCalls(detected),
|
||||
})
|
||||
} else {
|
||||
content := []any{
|
||||
map[string]any{
|
||||
"type": "output_text",
|
||||
"text": finalText,
|
||||
},
|
||||
}
|
||||
content := make([]any, 0, 2)
|
||||
if finalThinking != "" {
|
||||
content = append([]any{map[string]any{
|
||||
"type": "reasoning",
|
||||
"text": finalThinking,
|
||||
}}, content...)
|
||||
}
|
||||
if strings.TrimSpace(finalText) != "" {
|
||||
content = append(content, map[string]any{
|
||||
"type": "output_text",
|
||||
"text": finalText,
|
||||
})
|
||||
}
|
||||
if strings.TrimSpace(finalText) == "" && strings.TrimSpace(finalThinking) != "" {
|
||||
exposedOutputText = finalThinking
|
||||
}
|
||||
output = append(output, map[string]any{
|
||||
"type": "message",
|
||||
"id": "msg_" + strings.ReplaceAll(uuid.NewString(), "-", ""),
|
||||
|
||||
@@ -87,3 +87,60 @@ func TestBuildResponseObjectKeepsOutputTextForMixedProse(t *testing.T) {
|
||||
t.Fatalf("expected output type message, got %#v", first["type"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildResponseObjectReasoningOnlyFallsBackToOutputText(t *testing.T) {
|
||||
obj := BuildResponseObject(
|
||||
"resp_test",
|
||||
"gpt-4o",
|
||||
"prompt",
|
||||
"internal thinking content",
|
||||
"",
|
||||
nil,
|
||||
)
|
||||
|
||||
outputText, _ := obj["output_text"].(string)
|
||||
if outputText == "" {
|
||||
t.Fatalf("expected output_text fallback from reasoning when final text is empty")
|
||||
}
|
||||
|
||||
output, _ := obj["output"].([]any)
|
||||
if len(output) != 1 {
|
||||
t.Fatalf("expected one output item, got %#v", obj["output"])
|
||||
}
|
||||
first, _ := output[0].(map[string]any)
|
||||
if first["type"] != "message" {
|
||||
t.Fatalf("expected output type message, got %#v", first["type"])
|
||||
}
|
||||
content, _ := first["content"].([]any)
|
||||
if len(content) == 0 {
|
||||
t.Fatalf("expected reasoning content, got %#v", first["content"])
|
||||
}
|
||||
block0, _ := content[0].(map[string]any)
|
||||
if block0["type"] != "reasoning" {
|
||||
t.Fatalf("expected first content block reasoning, got %#v", block0["type"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildResponseObjectDetectsToolCallFromThinkingChannel(t *testing.T) {
|
||||
obj := BuildResponseObject(
|
||||
"resp_test",
|
||||
"gpt-4o",
|
||||
"prompt",
|
||||
`{"tool_calls":[{"name":"search","input":{"q":"from-thinking"}}]}`,
|
||||
"",
|
||||
[]string{"search"},
|
||||
)
|
||||
|
||||
output, _ := obj["output"].([]any)
|
||||
if len(output) != 2 {
|
||||
t.Fatalf("expected reasoning + tool_calls outputs, got %#v", obj["output"])
|
||||
}
|
||||
first, _ := output[0].(map[string]any)
|
||||
if first["type"] != "reasoning" {
|
||||
t.Fatalf("expected first output reasoning, got %#v", first["type"])
|
||||
}
|
||||
second, _ := output[1].(map[string]any)
|
||||
if second["type"] != "tool_calls" {
|
||||
t.Fatalf("expected second output tool_calls, got %#v", second["type"])
|
||||
}
|
||||
}
|
||||
|
||||
8
tests/scripts/run-unit-all.sh
Executable file
8
tests/scripts/run-unit-all.sh
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
./tests/scripts/run-unit-go.sh
|
||||
./tests/scripts/run-unit-node.sh
|
||||
7
tests/scripts/run-unit-go.sh
Executable file
7
tests/scripts/run-unit-go.sh
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
go test ./... "$@"
|
||||
7
tests/scripts/run-unit-node.sh
Executable file
7
tests/scripts/run-unit-node.sh
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
node --test api/helpers/stream-tool-sieve.test.js api/chat-stream.test.js api/compat/js_compat_test.js "$@"
|
||||
Reference in New Issue
Block a user