mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-05 08:55:28 +08:00
- replace legacy DeepSeek ids with the new deepseek-v4 model family\n- move thinking control to request parameters and preserve assistant reasoning content\n- switch history split to IGNORE transcript injection and map upload auth failures to 401\n- update admin defaults, API docs, samples, and tests for the new model scheme
238 lines
8.2 KiB
Go
238 lines
8.2 KiB
Go
package testsuite
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
func (r *Runner) caseHealthz(ctx context.Context, cc *caseContext) error {
|
|
resp, err := cc.request(ctx, requestSpec{Method: http.MethodGet, Path: "/healthz", Retryable: true})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cc.assert("status_200", resp.StatusCode == http.StatusOK, fmt.Sprintf("status=%d", resp.StatusCode))
|
|
var m map[string]any
|
|
_ = json.Unmarshal(resp.Body, &m)
|
|
cc.assert("status_ok", asString(m["status"]) == "ok", fmt.Sprintf("body=%s", string(resp.Body)))
|
|
|
|
headResp, headErr := cc.request(ctx, requestSpec{Method: http.MethodHead, Path: "/healthz", Retryable: true})
|
|
if headErr != nil {
|
|
return headErr
|
|
}
|
|
cc.assert("head_status_200", headResp.StatusCode == http.StatusOK, fmt.Sprintf("status=%d", headResp.StatusCode))
|
|
return nil
|
|
}
|
|
|
|
func (r *Runner) caseReadyz(ctx context.Context, cc *caseContext) error {
|
|
resp, err := cc.request(ctx, requestSpec{Method: http.MethodGet, Path: "/readyz", Retryable: true})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cc.assert("status_200", resp.StatusCode == http.StatusOK, fmt.Sprintf("status=%d", resp.StatusCode))
|
|
var m map[string]any
|
|
_ = json.Unmarshal(resp.Body, &m)
|
|
cc.assert("status_ready", asString(m["status"]) == "ready", fmt.Sprintf("body=%s", string(resp.Body)))
|
|
|
|
headResp, headErr := cc.request(ctx, requestSpec{Method: http.MethodHead, Path: "/readyz", Retryable: true})
|
|
if headErr != nil {
|
|
return headErr
|
|
}
|
|
cc.assert("head_status_200", headResp.StatusCode == http.StatusOK, fmt.Sprintf("status=%d", headResp.StatusCode))
|
|
return nil
|
|
}
|
|
|
|
func (r *Runner) caseModelsOpenAI(ctx context.Context, cc *caseContext) error {
|
|
resp, err := cc.request(ctx, requestSpec{Method: http.MethodGet, Path: "/v1/models", Retryable: true})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cc.assert("status_200", resp.StatusCode == http.StatusOK, fmt.Sprintf("status=%d", resp.StatusCode))
|
|
ids := extractModelIDs(resp.Body)
|
|
cc.assert("has_deepseek_chat", contains(ids, "deepseek-v4-flash"), strings.Join(ids, ","))
|
|
cc.assert("has_deepseek_reasoner", contains(ids, "deepseek-v4-pro"), strings.Join(ids, ","))
|
|
cc.assert("has_deepseek_expert_chat", contains(ids, "deepseek-v4-pro"), strings.Join(ids, ","))
|
|
cc.assert("has_deepseek_expert_reasoner", contains(ids, "deepseek-v4-pro"), strings.Join(ids, ","))
|
|
cc.assert("has_deepseek_vision_chat", contains(ids, "deepseek-v4-vision"), strings.Join(ids, ","))
|
|
cc.assert("has_deepseek_vision_reasoner", contains(ids, "deepseek-v4-vision"), strings.Join(ids, ","))
|
|
return nil
|
|
}
|
|
|
|
func (r *Runner) caseModelOpenAIByID(ctx context.Context, cc *caseContext) error {
|
|
resp, err := cc.request(ctx, requestSpec{Method: http.MethodGet, Path: "/v1/models/gpt-4o", Retryable: true})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cc.assert("status_200", resp.StatusCode == http.StatusOK, fmt.Sprintf("status=%d", resp.StatusCode))
|
|
var m map[string]any
|
|
_ = json.Unmarshal(resp.Body, &m)
|
|
cc.assert("object_model", asString(m["object"]) == "model", fmt.Sprintf("body=%s", string(resp.Body)))
|
|
cc.assert("id_deepseek_chat", asString(m["id"]) == "deepseek-v4-flash", fmt.Sprintf("body=%s", string(resp.Body)))
|
|
return nil
|
|
}
|
|
func (r *Runner) caseChatNonstream(ctx context.Context, cc *caseContext) error {
|
|
resp, err := cc.request(ctx, requestSpec{
|
|
Method: http.MethodPost,
|
|
Path: "/v1/chat/completions",
|
|
Headers: map[string]string{
|
|
"Authorization": "Bearer " + r.apiKey,
|
|
},
|
|
Body: map[string]any{
|
|
"model": "deepseek-v4-flash",
|
|
"messages": []map[string]any{
|
|
{"role": "user", "content": "请简单回复一句话"},
|
|
},
|
|
"stream": false,
|
|
},
|
|
Retryable: true,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cc.assert("status_200", resp.StatusCode == http.StatusOK, fmt.Sprintf("status=%d", resp.StatusCode))
|
|
var m map[string]any
|
|
_ = json.Unmarshal(resp.Body, &m)
|
|
cc.assert("object_chat_completion", asString(m["object"]) == "chat.completion", fmt.Sprintf("body=%s", string(resp.Body)))
|
|
choices, _ := m["choices"].([]any)
|
|
cc.assert("choices_non_empty", len(choices) > 0, fmt.Sprintf("body=%s", string(resp.Body)))
|
|
return nil
|
|
}
|
|
|
|
func (r *Runner) caseChatStream(ctx context.Context, cc *caseContext) error {
|
|
resp, err := cc.request(ctx, requestSpec{
|
|
Method: http.MethodPost,
|
|
Path: "/v1/chat/completions",
|
|
Headers: map[string]string{
|
|
"Authorization": "Bearer " + r.apiKey,
|
|
},
|
|
Body: map[string]any{
|
|
"model": "deepseek-v4-flash",
|
|
"messages": []map[string]any{
|
|
{"role": "user", "content": "请流式回复一句话"},
|
|
},
|
|
"stream": true,
|
|
},
|
|
Stream: true,
|
|
Retryable: true,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cc.assert("status_200", resp.StatusCode == http.StatusOK, fmt.Sprintf("status=%d", resp.StatusCode))
|
|
frames, done := parseSSEFrames(resp.Body)
|
|
cc.assert("frames_non_empty", len(frames) > 0, fmt.Sprintf("len=%d", len(frames)))
|
|
cc.assert("done_terminated", done, "expected [DONE]")
|
|
return nil
|
|
}
|
|
|
|
func (r *Runner) caseResponsesNonstream(ctx context.Context, cc *caseContext) error {
|
|
resp, err := cc.request(ctx, requestSpec{
|
|
Method: http.MethodPost,
|
|
Path: "/v1/responses",
|
|
Headers: map[string]string{
|
|
"Authorization": "Bearer " + r.apiKey,
|
|
},
|
|
Body: map[string]any{
|
|
"model": "gpt-4o",
|
|
"input": "请简要回答 hello",
|
|
},
|
|
Retryable: true,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cc.assert("status_200", resp.StatusCode == http.StatusOK, fmt.Sprintf("status=%d", resp.StatusCode))
|
|
var m map[string]any
|
|
_ = json.Unmarshal(resp.Body, &m)
|
|
cc.assert("object_response", asString(m["object"]) == "response", fmt.Sprintf("body=%s", string(resp.Body)))
|
|
responseID := asString(m["id"])
|
|
cc.assert("response_id_present", responseID != "", fmt.Sprintf("body=%s", string(resp.Body)))
|
|
if responseID != "" {
|
|
getResp, getErr := cc.request(ctx, requestSpec{
|
|
Method: http.MethodGet,
|
|
Path: "/v1/responses/" + responseID,
|
|
Headers: map[string]string{
|
|
"Authorization": "Bearer " + r.apiKey,
|
|
},
|
|
Retryable: true,
|
|
})
|
|
if getErr != nil {
|
|
return getErr
|
|
}
|
|
cc.assert("get_status_200", getResp.StatusCode == http.StatusOK, fmt.Sprintf("status=%d", getResp.StatusCode))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *Runner) caseResponsesStream(ctx context.Context, cc *caseContext) error {
|
|
resp, err := cc.request(ctx, requestSpec{
|
|
Method: http.MethodPost,
|
|
Path: "/v1/responses",
|
|
Headers: map[string]string{
|
|
"Authorization": "Bearer " + r.apiKey,
|
|
},
|
|
Body: map[string]any{
|
|
"model": "gpt-4o",
|
|
"input": "请流式回答 hello",
|
|
"stream": true,
|
|
},
|
|
Stream: true,
|
|
Retryable: true,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cc.assert("status_200", resp.StatusCode == http.StatusOK, fmt.Sprintf("status=%d", resp.StatusCode))
|
|
frames, done := parseSSEFrames(resp.Body)
|
|
cc.assert("frames_non_empty", len(frames) > 0, fmt.Sprintf("len=%d", len(frames)))
|
|
hasCreated := false
|
|
hasCompleted := false
|
|
for _, f := range frames {
|
|
switch asString(f["type"]) {
|
|
case "response.created":
|
|
hasCreated = true
|
|
case "response.completed":
|
|
hasCompleted = true
|
|
}
|
|
}
|
|
cc.assert("has_response_created", hasCreated, fmt.Sprintf("body=%s", string(resp.Body)))
|
|
cc.assert("has_response_completed", hasCompleted, fmt.Sprintf("body=%s", string(resp.Body)))
|
|
cc.assert("done_terminated", done, "expected [DONE]")
|
|
return nil
|
|
}
|
|
|
|
func (r *Runner) caseEmbeddings(ctx context.Context, cc *caseContext) error {
|
|
resp, err := cc.request(ctx, requestSpec{
|
|
Method: http.MethodPost,
|
|
Path: "/v1/embeddings",
|
|
Headers: map[string]string{
|
|
"Authorization": "Bearer " + r.apiKey,
|
|
},
|
|
Body: map[string]any{
|
|
"model": "gpt-4o",
|
|
"input": []string{"hello", "world"},
|
|
},
|
|
Retryable: true,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cc.assert("status_200_or_501", resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNotImplemented, fmt.Sprintf("status=%d", resp.StatusCode))
|
|
var m map[string]any
|
|
_ = json.Unmarshal(resp.Body, &m)
|
|
if resp.StatusCode == http.StatusOK {
|
|
cc.assert("object_list", asString(m["object"]) == "list", fmt.Sprintf("body=%s", string(resp.Body)))
|
|
data, _ := m["data"].([]any)
|
|
cc.assert("data_non_empty", len(data) > 0, fmt.Sprintf("body=%s", string(resp.Body)))
|
|
return nil
|
|
}
|
|
errObj, _ := m["error"].(map[string]any)
|
|
_, hasCode := errObj["code"]
|
|
_, hasParam := errObj["param"]
|
|
cc.assert("error_has_code", hasCode, fmt.Sprintf("body=%s", string(resp.Body)))
|
|
cc.assert("error_has_param", hasParam, fmt.Sprintf("body=%s", string(resp.Body)))
|
|
return nil
|
|
}
|