chore: update project files

This commit is contained in:
CJACK
2026-04-27 02:09:11 +08:00
parent 40d5e3ebb5
commit 90ce595325
27 changed files with 511 additions and 150 deletions

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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
}
}

View File

@@ -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",

View File

@@ -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 {

View File

@@ -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 {

View 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)
}
}

View File

@@ -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",

View File

@@ -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) {

View File

@@ -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 ""

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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"},
}

View File

@@ -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) {

View File

@@ -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)))
}

View File

@@ -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"])
}
}