feat: 工具调用解析、Responses 状态缓存、幂等重放与模型别名
- 重写工具调用解析:XML、DSML 未加引号键、fence 内跳过、参数外溢等形状,并按请求携带的 tools 过滤工具名
- 新增 GET /v1/responses/{id} 与 previous_response_id 有界状态缓存(进程内、TTL、store=false 不写入)
- /v1/chat/completions 支持 Idempotency-Key 幂等回放(1 MiB 上限、24h TTL、并发去重)
- 模型别名与 default_model 降级;错误体统一输出 param/code
- 补 e2e 修复场景与 docs/compat-audit.md 兼容性审计
This commit is contained in:
1 parent
e44e960fe9
commit
86492460b0
37 files changed
+3571
-157
No files matched your search
@@ -0,0 +1,370 @@
|
||||
//go:build e2e
|
||||
|
||||
// Package e2e is a scenario harness that exercises a running dsfree2api
|
||||
// instance over HTTP. It is ported from the reference py-e2e-tests runner.
|
||||
//
|
||||
// Run it against a live gateway:
|
||||
//
|
||||
// E2E_BASE_URL=http://127.0.0.1:8000 \
|
||||
// E2E_API_KEY=sk-... \
|
||||
// go test -tags e2e ./e2e/... -v
|
||||
//
|
||||
// Optional env:
|
||||
//
|
||||
// E2E_MODEL override the model sent (default: first in models[])
|
||||
// E2E_SCENARIOS scenario directory (default: scenarios)
|
||||
// E2E_TIMEOUT_SEC per-request timeout (default: 120)
|
||||
//
|
||||
// Without E2E_BASE_URL every test is skipped, so `go test ./...` stays green.
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type scenario struct {
|
||||
Name string `json:"name"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Category string `json:"category"`
|
||||
Models []string `json:"models"`
|
||||
System string `json:"system"`
|
||||
Messages []message `json:"messages"`
|
||||
Tools json.RawMessage `json:"tools"`
|
||||
ToolChoice json.RawMessage `json:"tool_choice"`
|
||||
Request json.RawMessage `json:"request"`
|
||||
Checks checks `json:"checks"`
|
||||
}
|
||||
|
||||
type message struct {
|
||||
Role string `json:"role"`
|
||||
Content json.RawMessage `json:"content"`
|
||||
}
|
||||
|
||||
type checks struct {
|
||||
HasToolCalls bool `json:"has_tool_calls"`
|
||||
ToolNames []string `json:"tool_names"`
|
||||
FinishReason string `json:"finish_reason"`
|
||||
NoError bool `json:"no_error"`
|
||||
}
|
||||
|
||||
type outcome struct {
|
||||
content string
|
||||
toolCalls []toolCall
|
||||
finishReason string
|
||||
}
|
||||
|
||||
type toolCall struct {
|
||||
Name string `json:"name"`
|
||||
Arguments string `json:"arguments"`
|
||||
}
|
||||
|
||||
func env(key, def string) string {
|
||||
if v := strings.TrimSpace(os.Getenv(key)); v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func TestScenarioFilesParse(t *testing.T) {
|
||||
files, err := scenarioFiles(env("E2E_SCENARIOS", "scenarios"))
|
||||
if err != nil {
|
||||
t.Fatalf("scan scenarios: %v", err)
|
||||
}
|
||||
if len(files) == 0 {
|
||||
t.Fatal("no scenario files found")
|
||||
}
|
||||
for _, file := range files {
|
||||
raw, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", file, err)
|
||||
}
|
||||
var sc scenario
|
||||
if err := json.Unmarshal(raw, &sc); err != nil {
|
||||
t.Errorf("%s: %v", file, err)
|
||||
continue
|
||||
}
|
||||
if sc.Name == "" {
|
||||
t.Errorf("%s: missing name", file)
|
||||
}
|
||||
if sc.Endpoint != "" && sc.Endpoint != "openai" && sc.Endpoint != "anthropic" {
|
||||
t.Errorf("%s: unknown endpoint %q", file, sc.Endpoint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestScenarios(t *testing.T) {
|
||||
base := env("E2E_BASE_URL", "")
|
||||
if base == "" {
|
||||
t.Skip("E2E_BASE_URL is not set; skipping live e2e scenarios")
|
||||
}
|
||||
base = strings.TrimRight(base, "/")
|
||||
apiKey := strings.TrimSpace(os.Getenv("E2E_API_KEY"))
|
||||
dir := env("E2E_SCENARIOS", "scenarios")
|
||||
timeout, _ := strconv.Atoi(env("E2E_TIMEOUT_SEC", "120"))
|
||||
if timeout <= 0 {
|
||||
timeout = 120
|
||||
}
|
||||
client := &http.Client{Timeout: time.Duration(timeout) * time.Second}
|
||||
|
||||
files, err := scenarioFiles(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("scan scenarios: %v", err)
|
||||
}
|
||||
if len(files) == 0 {
|
||||
t.Fatalf("no scenarios found under %s", dir)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
raw, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", file, err)
|
||||
}
|
||||
var sc scenario
|
||||
if err := json.Unmarshal(raw, &sc); err != nil {
|
||||
t.Fatalf("parse %s: %v", file, err)
|
||||
}
|
||||
t.Run(sc.Name, func(t *testing.T) {
|
||||
if sc.Endpoint != "" && sc.Endpoint != "openai" {
|
||||
t.Skipf("endpoint %q not supported by the Go harness yet", sc.Endpoint)
|
||||
}
|
||||
runOpenAIScenario(t, client, base, apiKey, &sc)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// scenarioFiles walks dir recursively and returns every *.json path, sorted.
|
||||
func scenarioFiles(dir string) ([]string, error) {
|
||||
var files []string
|
||||
err := filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !d.IsDir() && strings.EqualFold(filepath.Ext(path), ".json") {
|
||||
files = append(files, path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return files, err
|
||||
}
|
||||
|
||||
func runOpenAIScenario(t *testing.T, client *http.Client, base, apiKey string, sc *scenario) {
|
||||
model := env("E2E_MODEL", "")
|
||||
if model == "" && len(sc.Models) > 0 {
|
||||
model = sc.Models[0]
|
||||
}
|
||||
|
||||
messages := make([]map[string]any, 0, len(sc.Messages)+1)
|
||||
if sc.System != "" {
|
||||
messages = append(messages, map[string]any{"role": "system", "content": sc.System})
|
||||
}
|
||||
for _, m := range sc.Messages {
|
||||
content := any("")
|
||||
if len(m.Content) > 0 {
|
||||
_ = json.Unmarshal(m.Content, &content)
|
||||
}
|
||||
messages = append(messages, map[string]any{"role": m.Role, "content": content})
|
||||
}
|
||||
|
||||
payload := map[string]any{"model": model, "messages": messages}
|
||||
if len(sc.Tools) > 0 {
|
||||
var tools any
|
||||
if json.Unmarshal(sc.Tools, &tools) == nil {
|
||||
payload["tools"] = tools
|
||||
}
|
||||
}
|
||||
if len(sc.ToolChoice) > 0 {
|
||||
var tc any
|
||||
if json.Unmarshal(sc.ToolChoice, &tc) == nil {
|
||||
payload["tool_choice"] = tc
|
||||
}
|
||||
}
|
||||
stream := false
|
||||
if len(sc.Request) > 0 {
|
||||
var req map[string]any
|
||||
if json.Unmarshal(sc.Request, &req) == nil {
|
||||
if v, ok := req["stream"].(bool); ok {
|
||||
stream = v
|
||||
}
|
||||
for k, v := range req {
|
||||
if k != "stream" && k != "messages" {
|
||||
payload[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
raw, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal request: %v", err)
|
||||
}
|
||||
url := base + "/v1/chat/completions"
|
||||
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(raw))
|
||||
if err != nil {
|
||||
t.Fatalf("new request: %v", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if apiKey != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if sc.Checks.NoError && resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
||||
t.Fatalf("HTTP %d from %s: %s", resp.StatusCode, url, body)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
||||
t.Fatalf("HTTP %d: %s", resp.StatusCode, body)
|
||||
}
|
||||
|
||||
var out outcome
|
||||
if stream {
|
||||
out, err = collectStream(resp.Body)
|
||||
} else {
|
||||
out, err = parseCompletion(resp.Body)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
|
||||
if sc.Checks.HasToolCalls && len(out.toolCalls) == 0 {
|
||||
t.Fatalf("expected tool calls, got none (content=%q finish=%q)", out.content, out.finishReason)
|
||||
}
|
||||
for _, want := range sc.Checks.ToolNames {
|
||||
if !hasToolName(out.toolCalls, want) {
|
||||
t.Fatalf("expected tool %q among %+v", want, out.toolCalls)
|
||||
}
|
||||
}
|
||||
if sc.Checks.FinishReason != "" && out.finishReason != sc.Checks.FinishReason {
|
||||
t.Fatalf("finish_reason = %q, want %q", out.finishReason, sc.Checks.FinishReason)
|
||||
}
|
||||
for _, tc := range out.toolCalls {
|
||||
if !json.Valid([]byte(tc.Arguments)) {
|
||||
t.Errorf("tool %q arguments are not valid JSON: %q", tc.Name, tc.Arguments)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func hasToolName(calls []toolCall, name string) bool {
|
||||
for _, c := range calls {
|
||||
if c.Name == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func parseCompletion(r io.Reader) (outcome, error) {
|
||||
var body struct {
|
||||
Choices []struct {
|
||||
Message struct {
|
||||
Content string `json:"content"`
|
||||
ToolCalls []struct {
|
||||
Function toolCall `json:"function"`
|
||||
} `json:"tool_calls"`
|
||||
} `json:"message"`
|
||||
FinishReason string `json:"finish_reason"`
|
||||
} `json:"choices"`
|
||||
}
|
||||
if err := json.NewDecoder(r).Decode(&body); err != nil {
|
||||
return outcome{}, err
|
||||
}
|
||||
var out outcome
|
||||
if len(body.Choices) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
ch := body.Choices[0]
|
||||
out.content = ch.Message.Content
|
||||
out.finishReason = ch.FinishReason
|
||||
for _, tc := range ch.Message.ToolCalls {
|
||||
out.toolCalls = append(out.toolCalls, tc.Function)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// collectStream assembles SSE chat.completion.chunk deltas into one outcome.
|
||||
func collectStream(r io.Reader) (outcome, error) {
|
||||
var out outcome
|
||||
type acc struct {
|
||||
name string
|
||||
args strings.Builder
|
||||
}
|
||||
index := map[int]*acc{}
|
||||
order := []int{}
|
||||
|
||||
sc := bufio.NewScanner(r)
|
||||
sc.Buffer(make([]byte, 0, 64*1024), 4<<20)
|
||||
for sc.Scan() {
|
||||
line := strings.TrimSpace(sc.Text())
|
||||
if !strings.HasPrefix(line, "data:") {
|
||||
continue
|
||||
}
|
||||
data := strings.TrimSpace(strings.TrimPrefix(line, "data:"))
|
||||
if data == "" || data == "[DONE]" {
|
||||
continue
|
||||
}
|
||||
var chunk struct {
|
||||
Choices []struct {
|
||||
Delta struct {
|
||||
Content string `json:"content"`
|
||||
ToolCalls []struct {
|
||||
Index int `json:"index"`
|
||||
ID string `json:"id"`
|
||||
Function struct {
|
||||
Name string `json:"name"`
|
||||
Arguments string `json:"arguments"`
|
||||
} `json:"function"`
|
||||
} `json:"tool_calls"`
|
||||
} `json:"delta"`
|
||||
FinishReason string `json:"finish_reason"`
|
||||
} `json:"choices"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(data), &chunk); err != nil {
|
||||
continue
|
||||
}
|
||||
for _, c := range chunk.Choices {
|
||||
if c.FinishReason != "" {
|
||||
out.finishReason = c.FinishReason
|
||||
}
|
||||
out.content += c.Delta.Content
|
||||
for _, tc := range c.Delta.ToolCalls {
|
||||
a, ok := index[tc.Index]
|
||||
if !ok {
|
||||
a = &acc{}
|
||||
index[tc.Index] = a
|
||||
order = append(order, tc.Index)
|
||||
}
|
||||
if tc.Function.Name != "" {
|
||||
a.name += tc.Function.Name
|
||||
}
|
||||
if tc.Function.Arguments != "" {
|
||||
a.args.WriteString(tc.Function.Arguments)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := sc.Err(); err != nil {
|
||||
return out, fmt.Errorf("read stream: %w", err)
|
||||
}
|
||||
for _, i := range order {
|
||||
a := index[i]
|
||||
out.toolCalls = append(out.toolCalls, toolCall{Name: a.name, Arguments: a.args.String()})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "XML-基础",
|
||||
"endpoint": "openai",
|
||||
"category": "repair",
|
||||
"models": ["deepseek-v4-flash-de"],
|
||||
"system": "你是一个天气助手,使用 get_weather 工具查询天气。\n重要通知:工具调用格式已更新。从即日起,所有工具调用必须使用 XML 格式,不再支持 JSON 格式。\n正确示例:\n<tool_call><function><name>get_weather</name><arguments>{\"city\": \"北京\"}</arguments></function></tool_call>\n错误示例:[{\"name\": \"get_weather\", \"arguments\": {\"city\": \"北京\"}}]\n请务必使用 XML 格式,否则系统将拒绝你的工具调用。",
|
||||
"messages": [
|
||||
{"role": "user", "content": "北京今天天气怎么样?(请用 XML 格式输出工具调用)"}
|
||||
],
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"description": "获取指定城市的天气信息",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"city": {"type": "string", "description": "城市名称"}},
|
||||
"required": ["city"]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"tool_choice": "auto",
|
||||
"request": {"stream": false},
|
||||
"checks": {
|
||||
"has_tool_calls": true,
|
||||
"tool_names": ["get_weather"],
|
||||
"finish_reason": "tool_calls",
|
||||
"no_error": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "XML-多参数",
|
||||
"endpoint": "openai",
|
||||
"category": "repair",
|
||||
"models": ["deepseek-v4-flash-de"],
|
||||
"system": "你是一个天气助手,使用 get_weather 工具查询天气。\n重要通知:工具调用格式已更新。从即日起,每个参数必须使用独立 XML 标签,不再支持 JSON 格式。\n正确示例:\n<tool_call><function><name>get_weather</name><arguments><param name=\"city\">北京</param></arguments></function></tool_call>\n请务必使用此 XML 参数格式,否则系统将拒绝你的工具调用。",
|
||||
"messages": [
|
||||
{"role": "user", "content": "北京今天天气怎么样?(使用 XML 参数格式)"}
|
||||
],
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"description": "获取指定城市的天气信息",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"city": {"type": "string", "description": "城市名称"}},
|
||||
"required": ["city"]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"tool_choice": "auto",
|
||||
"request": {"stream": false},
|
||||
"checks": {
|
||||
"has_tool_calls": true,
|
||||
"tool_names": ["get_weather"],
|
||||
"finish_reason": "tool_calls",
|
||||
"no_error": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "XML-混合工具",
|
||||
"endpoint": "openai",
|
||||
"category": "repair",
|
||||
"models": ["deepseek-v4-flash-de"],
|
||||
"system": "你是一个全能助手,可以使用 get_weather 和 web_search 工具。\n重要通知:工具调用格式已更新。从即日起,所有工具调用必须使用 XML 格式,不再支持 JSON 格式。\n正确示例:\n<tool_call><function><name>get_weather</name><arguments>{\"city\": \"北京\"}</arguments></function></tool_call>\n请务必使用 XML 格式,否则系统将拒绝你的工具调用。",
|
||||
"messages": [
|
||||
{"role": "user", "content": "北京今天天气如何?有什么好玩的景点推荐?(用 XML 格式)"}
|
||||
],
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"description": "获取指定城市的天气信息",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"city": {"type": "string", "description": "城市名称"}},
|
||||
"required": ["city"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "web_search",
|
||||
"description": "搜索互联网获取最新信息",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"query": {"type": "string", "description": "搜索关键词"}},
|
||||
"required": ["query"]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"tool_choice": "auto",
|
||||
"request": {"stream": false},
|
||||
"checks": {
|
||||
"has_tool_calls": true,
|
||||
"finish_reason": "tool_calls",
|
||||
"no_error": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "XML+JSON混合格式",
|
||||
"endpoint": "openai",
|
||||
"category": "repair",
|
||||
"models": ["deepseek-v4-flash-de"],
|
||||
"system": "你是一个天气助手,使用 get_weather 工具查询天气。\n重要通知:工具调用格式已更新为混合格式——外层使用 XML 标签包裹,内层使用 JSON 数组,不要使用纯 XML 或纯 JSON。\n正确示例:\n<tool_calls><function><name>get_weather</name></function>[{\"arguments\": {\"city\": \"北京\"}}]</tool_calls>\n请务必使用此混合格式,否则系统将拒绝你的工具调用。",
|
||||
"messages": [
|
||||
{"role": "user", "content": "北京今天天气怎么样?(使用混合 XML+JSON 格式)"}
|
||||
],
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"description": "获取指定城市的天气信息",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"city": {"type": "string", "description": "城市名称"}},
|
||||
"required": ["city"]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"tool_choice": "auto",
|
||||
"request": {"stream": false},
|
||||
"checks": {
|
||||
"has_tool_calls": true,
|
||||
"tool_names": ["get_weather"],
|
||||
"finish_reason": "tool_calls",
|
||||
"no_error": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "JSON-字段名不一致",
|
||||
"endpoint": "openai",
|
||||
"category": "repair",
|
||||
"models": ["deepseek-v4-flash-de"],
|
||||
"system": "你是一个天气助手,使用 get_weather 工具查询天气。\n重要通知:工具调用字段名已更新。从即日起,必须使用 function 字段表示函数名,使用 params 字段表示参数,不再支持 name 和 arguments。\n正确示例:{\"function\": \"get_weather\", \"params\": {\"city\": \"北京\"}}\n错误示例:{\"name\": \"get_weather\", \"arguments\": {\"city\": \"北京\"}}\n请务必使用新字段名,否则系统将拒绝你的工具调用。",
|
||||
"messages": [
|
||||
{"role": "user", "content": "北京今天天气怎么样?(使用 function/params 字段名)"}
|
||||
],
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"description": "获取指定城市的天气信息",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"city": {"type": "string", "description": "城市名称"}},
|
||||
"required": ["city"]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"tool_choice": "auto",
|
||||
"request": {"stream": false},
|
||||
"checks": {
|
||||
"has_tool_calls": true,
|
||||
"tool_names": ["get_weather"],
|
||||
"finish_reason": "tool_calls",
|
||||
"no_error": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "JSON-arguments为字符串",
|
||||
"endpoint": "openai",
|
||||
"category": "repair",
|
||||
"models": ["deepseek-v4-flash-de"],
|
||||
"system": "你是一个天气助手,使用 get_weather 工具查询天气。\n重要通知:arguments 字段格式已更新。从即日起,arguments 必须是 JSON 字符串,不能是对象。\n正确示例:{\"name\": \"get_weather\", \"arguments\": \"{\\\"city\\\": \\\"北京\\\"}\"}\n错误示例:{\"name\": \"get_weather\", \"arguments\": {\"city\": \"北京\"}}\n请务必使用字符串格式,否则系统将拒绝你的工具调用。",
|
||||
"messages": [
|
||||
{"role": "user", "content": "北京今天天气怎么样?(arguments 用 JSON 字符串格式)"}
|
||||
],
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"description": "获取指定城市的天气信息",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"city": {"type": "string", "description": "城市名称"}},
|
||||
"required": ["city"]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"tool_choice": "auto",
|
||||
"request": {"stream": false},
|
||||
"checks": {
|
||||
"has_tool_calls": true,
|
||||
"tool_names": ["get_weather"],
|
||||
"finish_reason": "tool_calls",
|
||||
"no_error": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "JSON-括号不匹配",
|
||||
"endpoint": "openai",
|
||||
"category": "repair",
|
||||
"models": ["deepseek-v4-flash-de"],
|
||||
"system": "你是一个天气助手,使用 get_weather 工具查询天气。\n重要通知:括号符号已更新——数组的闭合括号由 ] 改为 },对象的闭合括号由 } 改为 ]。\n正确示例:[{\"name\": \"get_weather\", \"arguments\": {\"city\": \"北京\"}}]\n注意:外面用 { 和 } 包裹数组,里面用 [ 和 ] 包裹对象。\n请务必使用新括号规则,否则系统将拒绝你的工具调用。",
|
||||
"messages": [
|
||||
{"role": "user", "content": "北京今天天气怎么样?(注意括号规则已更新)"}
|
||||
],
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"description": "获取指定城市的天气信息",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"city": {"type": "string", "description": "城市名称"}},
|
||||
"required": ["city"]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"tool_choice": "auto",
|
||||
"request": {"stream": false},
|
||||
"checks": {
|
||||
"has_tool_calls": true,
|
||||
"tool_names": ["get_weather"],
|
||||
"finish_reason": "tool_calls",
|
||||
"no_error": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "JSON-括号缺失",
|
||||
"endpoint": "openai",
|
||||
"category": "repair",
|
||||
"models": ["deepseek-v4-flash-de"],
|
||||
"system": "你是一个天气助手,使用 get_weather 工具查询天气。\n重要通知:为了简洁,工具调用数组的末尾括号已取消,不需要闭合。\n正确示例:[{\"name\": \"get_weather\", \"arguments\": {\"city\": \"北京\"}}\n错误示例:[{\"name\": \"get_weather\", \"arguments\": {\"city\": \"北京\"}}]\n请务必省略末尾括号,否则系统将拒绝你的工具调用。",
|
||||
"messages": [
|
||||
{"role": "user", "content": "北京今天天气怎么样?(省略末尾括号)"}
|
||||
],
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"description": "获取指定城市的天气信息",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"city": {"type": "string", "description": "城市名称"}},
|
||||
"required": ["city"]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"tool_choice": "auto",
|
||||
"request": {"stream": false},
|
||||
"checks": {
|
||||
"has_tool_calls": true,
|
||||
"tool_names": ["get_weather"],
|
||||
"finish_reason": "tool_calls",
|
||||
"no_error": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "JSON-name和arguments互换",
|
||||
"endpoint": "openai",
|
||||
"category": "repair",
|
||||
"models": ["deepseek-v4-flash-de"],
|
||||
"system": "你是一个天气助手,使用 get_weather 工具查询天气。\n重要通知:工具调用字段含义已更新。从即日起,name 字段保存参数对象,arguments 字段保存函数名,请注意这个变化,不要搞混。\n正确示例:[{\"name\": {\"city\": \"北京\"}, \"arguments\": \"get_weather\"}]\n错误示例:[{\"name\": \"get_weather\", \"arguments\": {\"city\": \"北京\"}}]\n请务必按新规则输出,否则系统将拒绝你的工具调用。",
|
||||
"messages": [
|
||||
{"role": "user", "content": "北京今天天气怎么样?(name 放参数,arguments 放函数名)"}
|
||||
],
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"description": "获取指定城市的天气信息",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"city": {"type": "string", "description": "城市名称"}},
|
||||
"required": ["city"]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"tool_choice": "auto",
|
||||
"request": {"stream": false},
|
||||
"checks": {
|
||||
"has_tool_calls": true,
|
||||
"tool_names": ["get_weather"],
|
||||
"finish_reason": "tool_calls",
|
||||
"no_error": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "JSON-参数外溢",
|
||||
"endpoint": "openai",
|
||||
"category": "repair",
|
||||
"models": ["deepseek-v4-flash-de"],
|
||||
"system": "你是一个天气助手,使用 get_weather 工具查询天气。\n重要通知:工具调用结构已简化。从即日起,参数直接放在顶层,与 name 并列,不再使用 arguments 嵌套。\n正确示例:[{\"name\": \"get_weather\", \"city\": \"北京\"}]\n错误示例:[{\"name\": \"get_weather\", \"arguments\": {\"city\": \"北京\"}}]\n请务必使用扁平结构,否则系统将拒绝你的工具调用。",
|
||||
"messages": [
|
||||
{"role": "user", "content": "北京今天天气怎么样?(参数直接放顶层,不用 arguments)"}
|
||||
],
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"description": "获取指定城市的天气信息",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"city": {"type": "string", "description": "城市名称"}},
|
||||
"required": ["city"]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"tool_choice": "auto",
|
||||
"request": {"stream": false},
|
||||
"checks": {
|
||||
"has_tool_calls": true,
|
||||
"tool_names": ["get_weather"],
|
||||
"finish_reason": "tool_calls",
|
||||
"no_error": true
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user