mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-09 02:45:29 +08:00
chore: update project files
This commit is contained in:
@@ -19,6 +19,16 @@ func TestGetModelConfigDeepSeekChat(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetModelConfigDeepSeekChatNoThinking(t *testing.T) {
|
||||
thinking, search, ok := GetModelConfig("deepseek-v4-flash-nothinking")
|
||||
if !ok {
|
||||
t.Fatal("expected ok for deepseek-v4-flash-nothinking")
|
||||
}
|
||||
if thinking || search {
|
||||
t.Fatalf("expected thinking=false search=false for deepseek-v4-flash-nothinking, got thinking=%v search=%v", thinking, search)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetModelConfigDeepSeekReasoner(t *testing.T) {
|
||||
thinking, search, ok := GetModelConfig("deepseek-v4-pro")
|
||||
if !ok {
|
||||
@@ -84,6 +94,10 @@ func TestGetModelTypeDefaultExpertAndVision(t *testing.T) {
|
||||
if !ok || defaultType != "default" {
|
||||
t.Fatalf("expected default model_type, got ok=%v model_type=%q", ok, defaultType)
|
||||
}
|
||||
defaultNoThinkingType, ok := GetModelType("deepseek-v4-flash-nothinking")
|
||||
if !ok || defaultNoThinkingType != "default" {
|
||||
t.Fatalf("expected default model_type for nothinking, got ok=%v model_type=%q", ok, defaultNoThinkingType)
|
||||
}
|
||||
expertType, ok := GetModelType("deepseek-v4-pro")
|
||||
if !ok || expertType != "expert" {
|
||||
t.Fatalf("expected expert model_type, got ok=%v model_type=%q", ok, expertType)
|
||||
@@ -734,12 +748,18 @@ func TestOpenAIModelsResponse(t *testing.T) {
|
||||
t.Fatal("expected non-empty models list")
|
||||
}
|
||||
expected := map[string]bool{
|
||||
"deepseek-v4-flash": false,
|
||||
"deepseek-v4-pro": false,
|
||||
"deepseek-v4-flash-search": false,
|
||||
"deepseek-v4-pro-search": false,
|
||||
"deepseek-v4-vision": false,
|
||||
"deepseek-v4-vision-search": false,
|
||||
"deepseek-v4-flash": false,
|
||||
"deepseek-v4-flash-nothinking": false,
|
||||
"deepseek-v4-pro": false,
|
||||
"deepseek-v4-pro-nothinking": false,
|
||||
"deepseek-v4-flash-search": false,
|
||||
"deepseek-v4-flash-search-nothinking": false,
|
||||
"deepseek-v4-pro-search": false,
|
||||
"deepseek-v4-pro-search-nothinking": false,
|
||||
"deepseek-v4-vision": false,
|
||||
"deepseek-v4-vision-nothinking": false,
|
||||
"deepseek-v4-vision-search": false,
|
||||
"deepseek-v4-vision-search-nothinking": false,
|
||||
}
|
||||
for _, model := range data {
|
||||
if _, ok := expected[model.ID]; ok {
|
||||
|
||||
@@ -13,6 +13,13 @@ func TestResolveModelDirectDeepSeek(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveModelDirectDeepSeekNoThinking(t *testing.T) {
|
||||
got, ok := ResolveModel(nil, "deepseek-v4-flash-nothinking")
|
||||
if !ok || got != "deepseek-v4-flash-nothinking" {
|
||||
t.Fatalf("expected deepseek-v4-flash-nothinking, got ok=%v model=%q", ok, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveModelAlias(t *testing.T) {
|
||||
got, ok := ResolveModel(nil, "gpt-4.1")
|
||||
if !ok || got != "deepseek-v4-flash" {
|
||||
@@ -34,6 +41,13 @@ func TestResolveLatestClaudeAlias(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveLatestClaudeAliasNoThinking(t *testing.T) {
|
||||
got, ok := ResolveModel(nil, "claude-sonnet-4-6-nothinking")
|
||||
if !ok || got != "deepseek-v4-flash-nothinking" {
|
||||
t.Fatalf("expected alias claude-sonnet-4-6-nothinking -> deepseek-v4-flash-nothinking, got ok=%v model=%q", ok, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveExpandedHistoricalAliases(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
@@ -68,6 +82,13 @@ func TestResolveModelHeuristicReasoner(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveModelHeuristicReasonerNoThinking(t *testing.T) {
|
||||
got, ok := ResolveModel(nil, "o3-super-nothinking")
|
||||
if !ok || got != "deepseek-v4-pro-nothinking" {
|
||||
t.Fatalf("expected heuristic reasoner nothinking, got ok=%v model=%q", ok, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveModelUnknown(t *testing.T) {
|
||||
_, ok := ResolveModel(nil, "totally-custom-model")
|
||||
if ok {
|
||||
|
||||
@@ -14,7 +14,9 @@ type ModelAliasReader interface {
|
||||
ModelAliases() map[string]string
|
||||
}
|
||||
|
||||
var DeepSeekModels = []ModelInfo{
|
||||
const noThinkingModelSuffix = "-nothinking"
|
||||
|
||||
var deepSeekBaseModels = []ModelInfo{
|
||||
{ID: "deepseek-v4-flash", Object: "model", Created: 1677610602, OwnedBy: "deepseek", Permission: []any{}},
|
||||
{ID: "deepseek-v4-pro", Object: "model", Created: 1677610602, OwnedBy: "deepseek", Permission: []any{}},
|
||||
{ID: "deepseek-v4-flash-search", Object: "model", Created: 1677610602, OwnedBy: "deepseek", Permission: []any{}},
|
||||
@@ -23,7 +25,9 @@ var DeepSeekModels = []ModelInfo{
|
||||
{ID: "deepseek-v4-vision-search", Object: "model", Created: 1677610602, OwnedBy: "deepseek", Permission: []any{}},
|
||||
}
|
||||
|
||||
var ClaudeModels = []ModelInfo{
|
||||
var DeepSeekModels = appendNoThinkingVariants(deepSeekBaseModels)
|
||||
|
||||
var claudeBaseModels = []ModelInfo{
|
||||
// Current aliases
|
||||
{ID: "claude-opus-4-6", Object: "model", Created: 1715635200, OwnedBy: "anthropic"},
|
||||
{ID: "claude-sonnet-4-6", Object: "model", Created: 1715635200, OwnedBy: "anthropic"},
|
||||
@@ -53,19 +57,26 @@ var ClaudeModels = []ModelInfo{
|
||||
{ID: "claude-3-haiku-20240307", Object: "model", Created: 1715635200, OwnedBy: "anthropic"},
|
||||
}
|
||||
|
||||
var ClaudeModels = appendNoThinkingVariants(claudeBaseModels)
|
||||
|
||||
func GetModelConfig(model string) (thinking bool, search bool, ok bool) {
|
||||
switch lower(model) {
|
||||
baseModel, noThinking := splitNoThinkingModel(model)
|
||||
if baseModel == "" {
|
||||
return false, false, false
|
||||
}
|
||||
switch baseModel {
|
||||
case "deepseek-v4-flash", "deepseek-v4-pro", "deepseek-v4-vision":
|
||||
return true, false, true
|
||||
return !noThinking, false, true
|
||||
case "deepseek-v4-flash-search", "deepseek-v4-pro-search", "deepseek-v4-vision-search":
|
||||
return true, true, true
|
||||
return !noThinking, true, true
|
||||
default:
|
||||
return false, false, false
|
||||
}
|
||||
}
|
||||
|
||||
func GetModelType(model string) (modelType string, ok bool) {
|
||||
switch lower(model) {
|
||||
baseModel, _ := splitNoThinkingModel(model)
|
||||
switch baseModel {
|
||||
case "deepseek-v4-flash", "deepseek-v4-flash-search":
|
||||
return "default", true
|
||||
case "deepseek-v4-pro", "deepseek-v4-pro-search":
|
||||
@@ -82,6 +93,11 @@ func IsSupportedDeepSeekModel(model string) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
func IsNoThinkingModel(model string) bool {
|
||||
_, noThinking := splitNoThinkingModel(model)
|
||||
return noThinking
|
||||
}
|
||||
|
||||
func DefaultModelAliases() map[string]string {
|
||||
return map[string]string{
|
||||
// OpenAI GPT / ChatGPT families
|
||||
@@ -191,62 +207,19 @@ func ResolveModel(store ModelAliasReader, requested string) (string, bool) {
|
||||
if model == "" {
|
||||
return "", false
|
||||
}
|
||||
if isRetiredHistoricalModel(model) {
|
||||
return "", false
|
||||
}
|
||||
aliases := loadModelAliases(store)
|
||||
if IsSupportedDeepSeekModel(model) {
|
||||
return model, true
|
||||
}
|
||||
aliases := DefaultModelAliases()
|
||||
if store != nil {
|
||||
for k, v := range store.ModelAliases() {
|
||||
aliases[lower(strings.TrimSpace(k))] = lower(strings.TrimSpace(v))
|
||||
}
|
||||
}
|
||||
if mapped, ok := aliases[model]; ok && IsSupportedDeepSeekModel(mapped) {
|
||||
return mapped, true
|
||||
}
|
||||
if strings.HasPrefix(model, "deepseek-") {
|
||||
baseModel, noThinking := splitNoThinkingModel(model)
|
||||
resolvedModel, ok := resolveCanonicalModel(aliases, baseModel)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
|
||||
knownFamily := false
|
||||
for _, prefix := range []string{
|
||||
"gpt-", "o1", "o3", "claude-", "gemini-", "llama-", "qwen-", "mistral-", "command-",
|
||||
} {
|
||||
if strings.HasPrefix(model, prefix) {
|
||||
knownFamily = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !knownFamily {
|
||||
return "", false
|
||||
}
|
||||
|
||||
useVision := strings.Contains(model, "vision")
|
||||
useReasoner := strings.Contains(model, "reason") ||
|
||||
strings.Contains(model, "reasoner") ||
|
||||
strings.HasPrefix(model, "o1") ||
|
||||
strings.HasPrefix(model, "o3") ||
|
||||
strings.Contains(model, "opus") ||
|
||||
strings.Contains(model, "slow") ||
|
||||
strings.Contains(model, "r1")
|
||||
useSearch := strings.Contains(model, "search")
|
||||
|
||||
switch {
|
||||
case useVision && useSearch:
|
||||
return "deepseek-v4-vision-search", true
|
||||
case useVision:
|
||||
return "deepseek-v4-vision", true
|
||||
case useReasoner && useSearch:
|
||||
return "deepseek-v4-pro-search", true
|
||||
case useReasoner:
|
||||
return "deepseek-v4-pro", true
|
||||
case useSearch:
|
||||
return "deepseek-v4-flash-search", true
|
||||
default:
|
||||
return "deepseek-v4-flash", true
|
||||
}
|
||||
return withNoThinkingVariant(resolvedModel, noThinking), true
|
||||
}
|
||||
|
||||
func isRetiredHistoricalModel(model string) bool {
|
||||
@@ -303,3 +276,100 @@ func ClaudeModelsResponse() map[string]any {
|
||||
resp["has_more"] = false
|
||||
return resp
|
||||
}
|
||||
|
||||
func appendNoThinkingVariants(models []ModelInfo) []ModelInfo {
|
||||
out := make([]ModelInfo, 0, len(models)*2)
|
||||
for _, model := range models {
|
||||
out = append(out, model)
|
||||
variant := model
|
||||
variant.ID = withNoThinkingVariant(model.ID, true)
|
||||
out = append(out, variant)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func splitNoThinkingModel(model string) (string, bool) {
|
||||
model = lower(strings.TrimSpace(model))
|
||||
if strings.HasSuffix(model, noThinkingModelSuffix) {
|
||||
return strings.TrimSuffix(model, noThinkingModelSuffix), true
|
||||
}
|
||||
return model, false
|
||||
}
|
||||
|
||||
func withNoThinkingVariant(model string, enabled bool) string {
|
||||
baseModel, _ := splitNoThinkingModel(model)
|
||||
if !enabled {
|
||||
return baseModel
|
||||
}
|
||||
if baseModel == "" {
|
||||
return ""
|
||||
}
|
||||
return baseModel + noThinkingModelSuffix
|
||||
}
|
||||
|
||||
func loadModelAliases(store ModelAliasReader) map[string]string {
|
||||
aliases := DefaultModelAliases()
|
||||
if store != nil {
|
||||
for k, v := range store.ModelAliases() {
|
||||
aliases[lower(strings.TrimSpace(k))] = lower(strings.TrimSpace(v))
|
||||
}
|
||||
}
|
||||
return aliases
|
||||
}
|
||||
|
||||
func resolveCanonicalModel(aliases map[string]string, model string) (string, bool) {
|
||||
model = lower(strings.TrimSpace(model))
|
||||
if model == "" {
|
||||
return "", false
|
||||
}
|
||||
if isRetiredHistoricalModel(model) {
|
||||
return "", false
|
||||
}
|
||||
if IsSupportedDeepSeekModel(model) {
|
||||
return model, true
|
||||
}
|
||||
if mapped, ok := aliases[model]; ok && IsSupportedDeepSeekModel(mapped) {
|
||||
return mapped, true
|
||||
}
|
||||
if strings.HasPrefix(model, "deepseek-") {
|
||||
return "", false
|
||||
}
|
||||
|
||||
knownFamily := false
|
||||
for _, prefix := range []string{
|
||||
"gpt-", "o1", "o3", "claude-", "gemini-", "llama-", "qwen-", "mistral-", "command-",
|
||||
} {
|
||||
if strings.HasPrefix(model, prefix) {
|
||||
knownFamily = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !knownFamily {
|
||||
return "", false
|
||||
}
|
||||
|
||||
useVision := strings.Contains(model, "vision")
|
||||
useReasoner := strings.Contains(model, "reason") ||
|
||||
strings.Contains(model, "reasoner") ||
|
||||
strings.HasPrefix(model, "o1") ||
|
||||
strings.HasPrefix(model, "o3") ||
|
||||
strings.Contains(model, "opus") ||
|
||||
strings.Contains(model, "slow") ||
|
||||
strings.Contains(model, "r1")
|
||||
useSearch := strings.Contains(model, "search")
|
||||
|
||||
switch {
|
||||
case useVision && useSearch:
|
||||
return "deepseek-v4-vision-search", true
|
||||
case useVision:
|
||||
return "deepseek-v4-vision", true
|
||||
case useReasoner && useSearch:
|
||||
return "deepseek-v4-pro-search", true
|
||||
case useReasoner:
|
||||
return "deepseek-v4-pro", true
|
||||
case useSearch:
|
||||
return "deepseek-v4-flash-search", true
|
||||
default:
|
||||
return "deepseek-v4-flash", true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,26 @@ func TestNormalizeClaudeRequestEnablesThinkingWhenRequested(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeClaudeRequestNoThinkingAliasForcesThinkingOff(t *testing.T) {
|
||||
req := map[string]any{
|
||||
"model": "claude-opus-4-6-nothinking",
|
||||
"messages": []any{
|
||||
map[string]any{"role": "user", "content": "hello"},
|
||||
},
|
||||
"thinking": map[string]any{"type": "enabled", "budget_tokens": 1024},
|
||||
}
|
||||
out, err := normalizeClaudeRequest(mockClaudeConfig{}, req)
|
||||
if err != nil {
|
||||
t.Fatalf("normalizeClaudeRequest error: %v", err)
|
||||
}
|
||||
if out.Standard.ResolvedModel != "deepseek-v4-pro-nothinking" {
|
||||
t.Fatalf("resolved model mismatch: got=%q", out.Standard.ResolvedModel)
|
||||
}
|
||||
if out.Standard.Thinking {
|
||||
t.Fatalf("expected nothinking alias to force downstream thinking off")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeClaudeRequestPrefersGlobalAliasMapping(t *testing.T) {
|
||||
req := map[string]any{
|
||||
"model": "claude-sonnet-4-6",
|
||||
|
||||
@@ -37,6 +37,9 @@ func normalizeClaudeRequest(store ConfigReader, req map[string]any) (claudeNorma
|
||||
searchEnabled = false
|
||||
}
|
||||
thinkingEnabled := util.ResolveThinkingEnabled(req, false)
|
||||
if config.IsNoThinkingModel(dsModel) {
|
||||
thinkingEnabled = false
|
||||
}
|
||||
finalPrompt := prompt.MessagesPrepareWithThinking(toMessageMaps(dsPayload["messages"]), thinkingEnabled)
|
||||
toolNames := extractClaudeToolNames(toolsRequested)
|
||||
if len(toolNames) == 0 && len(toolsRequested) > 0 {
|
||||
|
||||
@@ -22,6 +22,9 @@ func normalizeGeminiRequest(store ConfigReader, routeModel string, req map[strin
|
||||
}
|
||||
defaultThinkingEnabled, searchEnabled, _ := config.GetModelConfig(resolvedModel)
|
||||
thinkingEnabled := util.ResolveThinkingEnabled(req, defaultThinkingEnabled)
|
||||
if config.IsNoThinkingModel(resolvedModel) {
|
||||
thinkingEnabled = false
|
||||
}
|
||||
|
||||
messagesRaw := geminiMessagesFromRequest(req)
|
||||
if len(messagesRaw) == 0 {
|
||||
|
||||
28
internal/httpapi/gemini/convert_request_test.go
Normal file
28
internal/httpapi/gemini/convert_request_test.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package gemini
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNormalizeGeminiRequestNoThinkingModelForcesThinkingOff(t *testing.T) {
|
||||
req := map[string]any{
|
||||
"contents": []any{
|
||||
map[string]any{
|
||||
"role": "user",
|
||||
"parts": []any{map[string]any{"text": "hello"}},
|
||||
},
|
||||
},
|
||||
"reasoning_effort": "high",
|
||||
}
|
||||
out, err := normalizeGeminiRequest(testGeminiConfig{}, "gemini-2.5-pro-nothinking", req, false)
|
||||
if err != nil {
|
||||
t.Fatalf("normalizeGeminiRequest error: %v", err)
|
||||
}
|
||||
if out.ResolvedModel != "deepseek-v4-pro-nothinking" {
|
||||
t.Fatalf("resolved model mismatch: got=%q", out.ResolvedModel)
|
||||
}
|
||||
if out.Thinking {
|
||||
t.Fatalf("expected nothinking model to force thinking off")
|
||||
}
|
||||
if out.Search {
|
||||
t.Fatalf("expected search=false, got=%v", out.Search)
|
||||
}
|
||||
}
|
||||
@@ -80,6 +80,28 @@ func TestNormalizeOpenAIChatRequestWithConfigInterface(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeOpenAIChatRequestDisablesThinkingForNoThinkingModel(t *testing.T) {
|
||||
cfg := mockOpenAIConfig{wideInput: true}
|
||||
req := map[string]any{
|
||||
"model": "deepseek-v4-pro-nothinking",
|
||||
"messages": []any{map[string]any{"role": "user", "content": "hello"}},
|
||||
"reasoning_effort": "high",
|
||||
}
|
||||
out, err := promptcompat.NormalizeOpenAIChatRequest(cfg, req, "")
|
||||
if err != nil {
|
||||
t.Fatalf("promptcompat.NormalizeOpenAIChatRequest error: %v", err)
|
||||
}
|
||||
if out.ResolvedModel != "deepseek-v4-pro-nothinking" {
|
||||
t.Fatalf("resolved model mismatch: got=%q", out.ResolvedModel)
|
||||
}
|
||||
if out.Thinking {
|
||||
t.Fatalf("expected nothinking model to force thinking off")
|
||||
}
|
||||
if out.Search {
|
||||
t.Fatalf("expected search=false for deepseek-v4-pro-nothinking, got=%v", out.Search)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeOpenAIResponsesRequestWideInputPolicyFromInterface(t *testing.T) {
|
||||
req := map[string]any{
|
||||
"model": "deepseek-v4-flash",
|
||||
|
||||
@@ -22,6 +22,15 @@ func TestGetModelRouteDirectAndAlias(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("direct_nothinking", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/models/deepseek-v4-flash-nothinking", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("direct_expert", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/models/deepseek-v4-pro", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
@@ -48,6 +57,15 @@ func TestGetModelRouteDirectAndAlias(t *testing.T) {
|
||||
t.Fatalf("expected 200 for alias, got %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("alias_nothinking", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/models/claude-sonnet-4-6-nothinking", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200 for nothinking alias, got %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetModelRouteNotFound(t *testing.T) {
|
||||
|
||||
@@ -30,11 +30,6 @@ func MessagesPrepareWithThinking(messages []map[string]any, thinkingEnabled bool
|
||||
Text string
|
||||
}
|
||||
processed := make([]block, 0, len(messages))
|
||||
if thinkingEnabled {
|
||||
if instruction := buildConversationContinuityInstructions(thinkingEnabled); strings.TrimSpace(instruction) != "" {
|
||||
processed = append(processed, block{Role: "system", Text: instruction})
|
||||
}
|
||||
}
|
||||
for _, m := range messages {
|
||||
role, _ := m["role"].(string)
|
||||
text := NormalizeContent(m["content"])
|
||||
@@ -93,17 +88,6 @@ func formatRoleBlock(marker, text, endMarker string) string {
|
||||
return out
|
||||
}
|
||||
|
||||
func buildConversationContinuityInstructions(thinkingEnabled bool) string {
|
||||
lines := []string{
|
||||
"Continue the conversation from the full prior context and the latest tool results.",
|
||||
"Treat earlier messages as binding context; answer the user's current request as a continuation, not a restart.",
|
||||
}
|
||||
if thinkingEnabled {
|
||||
lines = append(lines, "Keep reasoning internal. Do not leave the final user-facing answer only in reasoning; always provide the answer in visible assistant content.")
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func NormalizeContent(v any) string {
|
||||
if v == nil {
|
||||
return ""
|
||||
|
||||
@@ -58,23 +58,14 @@ func TestNormalizeContentArrayFallsBackToContentWhenTextEmpty(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessagesPrepareWithThinkingAddsContinuityContract(t *testing.T) {
|
||||
func TestMessagesPrepareWithThinkingPreservesPromptShape(t *testing.T) {
|
||||
messages := []map[string]any{{"role": "user", "content": "Question"}}
|
||||
gotThinking := MessagesPrepareWithThinking(messages, true)
|
||||
gotPlain := MessagesPrepareWithThinking(messages, false)
|
||||
if gotThinking == gotPlain {
|
||||
t.Fatalf("expected thinking-enabled prompt to include extra continuity instructions")
|
||||
if gotThinking != gotPlain {
|
||||
t.Fatalf("expected thinking flag not to add extra continuity instructions, got thinking=%q plain=%q", gotThinking, gotPlain)
|
||||
}
|
||||
if !strings.HasSuffix(gotThinking, "<|Assistant|>") {
|
||||
t.Fatalf("expected assistant suffix, got %q", gotThinking)
|
||||
}
|
||||
if !strings.Contains(gotThinking, "Continue the conversation from the full prior context") {
|
||||
t.Fatalf("expected continuity instruction in thinking prompt, got %q", gotThinking)
|
||||
}
|
||||
if !strings.Contains(gotThinking, "final user-facing answer only in reasoning") {
|
||||
t.Fatalf("expected visible-answer instruction in thinking prompt, got %q", gotThinking)
|
||||
}
|
||||
if strings.Contains(gotPlain, "Continue the conversation from the full prior context") {
|
||||
t.Fatalf("did not expect thinking-only instruction in plain prompt, got %q", gotPlain)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,16 +88,14 @@ func TestBuildOpenAIFinalPrompt_VercelPreparePathKeepsFinalAnswerInstruction(t *
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildOpenAIFinalPromptWithThinkingAddsContinuationContract(t *testing.T) {
|
||||
func TestBuildOpenAIFinalPromptWithThinkingKeepsPromptUnchanged(t *testing.T) {
|
||||
messages := []any{
|
||||
map[string]any{"role": "user", "content": "继续回答上一个问题"},
|
||||
}
|
||||
|
||||
finalPrompt, _ := buildOpenAIFinalPrompt(messages, nil, "", true)
|
||||
if !strings.Contains(finalPrompt, "Continue the conversation from the full prior context") {
|
||||
t.Fatalf("expected continuation contract in thinking prompt, got=%q", finalPrompt)
|
||||
}
|
||||
if !strings.Contains(finalPrompt, "final user-facing answer only in reasoning") {
|
||||
t.Fatalf("expected visible-answer contract in thinking prompt, got=%q", finalPrompt)
|
||||
finalPromptThinking, _ := buildOpenAIFinalPrompt(messages, nil, "", true)
|
||||
finalPromptPlain, _ := buildOpenAIFinalPrompt(messages, nil, "", false)
|
||||
if finalPromptThinking != finalPromptPlain {
|
||||
t.Fatalf("expected thinking flag not to prepend continuation contract, thinking=%q plain=%q", finalPromptThinking, finalPromptPlain)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,9 @@ func NormalizeOpenAIChatRequest(store ConfigReader, req map[string]any, traceID
|
||||
}
|
||||
defaultThinkingEnabled, searchEnabled, _ := config.GetModelConfig(resolvedModel)
|
||||
thinkingEnabled := util.ResolveThinkingEnabled(req, defaultThinkingEnabled)
|
||||
if config.IsNoThinkingModel(resolvedModel) {
|
||||
thinkingEnabled = false
|
||||
}
|
||||
responseModel := strings.TrimSpace(model)
|
||||
if responseModel == "" {
|
||||
responseModel = resolvedModel
|
||||
@@ -65,6 +68,9 @@ func NormalizeOpenAIResponsesRequest(store ConfigReader, req map[string]any, tra
|
||||
}
|
||||
defaultThinkingEnabled, searchEnabled, _ := config.GetModelConfig(resolvedModel)
|
||||
thinkingEnabled := util.ResolveThinkingEnabled(req, defaultThinkingEnabled)
|
||||
if config.IsNoThinkingModel(resolvedModel) {
|
||||
thinkingEnabled = false
|
||||
}
|
||||
|
||||
// Keep width-control as an explicit policy hook even if current default is true.
|
||||
allowWideInput := true
|
||||
|
||||
@@ -11,6 +11,7 @@ func TestStandardRequestCompletionPayloadSetsModelTypeFromResolvedModel(t *testi
|
||||
modelType string
|
||||
}{
|
||||
{name: "default", model: "deepseek-v4-flash", thinking: false, search: false, modelType: "default"},
|
||||
{name: "default_nothinking", model: "deepseek-v4-flash-nothinking", thinking: false, search: false, modelType: "default"},
|
||||
{name: "expert", model: "deepseek-v4-pro", thinking: true, search: false, modelType: "expert"},
|
||||
{name: "vision", model: "deepseek-v4-vision-search", thinking: false, search: true, modelType: "vision"},
|
||||
}
|
||||
|
||||
@@ -3,12 +3,10 @@ package promptcompat
|
||||
import "strings"
|
||||
|
||||
const (
|
||||
ThinkingInjectionMarker = "【思维链格式要求】"
|
||||
DefaultThinkingInjectionPrompt = ThinkingInjectionMarker + "在你的思考过程(<think>标签内)中,请严格按照以下规则进行思考,不要遗漏:\n" +
|
||||
"1. 分析阶段:分析用户需求是什么。\n" +
|
||||
"2. 构思阶段:构思下一步动作,我要干什么。\n" +
|
||||
"3. 工具调用阶段:为了满足用户需求,我需要调用什么工具;如果不需要工具,明确说明不需要调用工具。\n" +
|
||||
"4. 回顾格式:完整复述一遍 System 要求的 XML 工具调用格式要求,回顾错误示例和正确示例,说明我要如何正确调用工具。"
|
||||
ThinkingInjectionMarker = "Reasoning Effort: Absolute maximum with no shortcuts permitted."
|
||||
DefaultThinkingInjectionPrompt = ThinkingInjectionMarker + "\n" +
|
||||
"You MUST be very thorough in your thinking and comprehensively decompose the problem to resolve the root cause, rigorously stress-testing your logic against all potential paths, edge cases, and adversarial scenarios.\n" +
|
||||
"Explicitly write out your entire deliberation process, documenting every intermediate step, considered alternative, and rejected hypothesis to ensure absolutely no assumption is left unchecked."
|
||||
)
|
||||
|
||||
func AppendThinkingInjectionToLatestUser(messages []any) ([]any, bool) {
|
||||
|
||||
@@ -116,6 +116,18 @@ func TestConvertClaudeToDeepSeekUsesGlobalAliasResolution(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertClaudeToDeepSeekUsesNoThinkingAliasResolution(t *testing.T) {
|
||||
store := config.LoadStore()
|
||||
req := map[string]any{
|
||||
"model": "claude-sonnet-4-6-nothinking",
|
||||
"messages": []any{map[string]any{"role": "user", "content": "Hi"}},
|
||||
}
|
||||
out := ConvertClaudeToDeepSeek(req, store)
|
||||
if out["model"] != "deepseek-v4-flash-nothinking" {
|
||||
t.Fatalf("expected noThinking alias resolution, got model=%q", out["model"])
|
||||
}
|
||||
}
|
||||
|
||||
func contains(s, sub string) bool {
|
||||
return len(s) >= len(sub) && (s == sub || len(sub) == 0 || (len(s) > 0 && (indexOf(s, sub) >= 0)))
|
||||
}
|
||||
|
||||
@@ -372,3 +372,16 @@ func TestConvertClaudeToDeepSeekUsesExplicitModelAlias(t *testing.T) {
|
||||
t.Fatalf("expected explicit alias override, got %q", out["model"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertClaudeToDeepSeekUsesExplicitNoThinkingModelAlias(t *testing.T) {
|
||||
t.Setenv("DS2API_CONFIG_JSON", `{"keys":[],"accounts":[],"model_aliases":{"claude-sonnet-4-6":"deepseek-v4-pro-search"}}`)
|
||||
store := config.LoadStore()
|
||||
req := map[string]any{
|
||||
"model": "claude-sonnet-4-6-nothinking",
|
||||
"messages": []any{map[string]any{"role": "user", "content": "Hi"}},
|
||||
}
|
||||
out := ConvertClaudeToDeepSeek(req, store)
|
||||
if out["model"] != "deepseek-v4-pro-search-nothinking" {
|
||||
t.Fatalf("expected explicit alias override with nothinking suffix, got %q", out["model"])
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user