Compare commits

...

7 Commits
v4.4.2 ... main

Author SHA1 Message Date
CJACK.
aa29084038 Merge pull request #434 from CJackHwang/dev
Merge pull request #433 from CJackHwang/codex/flash-searchpro-search

Remove heuristic model name resolution and require explicit aliases or canonical IDs
2026-05-06 00:38:20 +08:00
CJACK.
21c1527c79 Merge pull request #433 from CJackHwang/codex/flash-searchpro-search
Remove heuristic model name resolution and require explicit aliases or canonical IDs
2026-05-06 00:03:44 +08:00
CJACK.
7ec0d99702 Merge pull request #431 from CJackHwang/main
Fix OpenAI stream heartbeat and avoid empty choices
2026-05-06 00:02:25 +08:00
CJACK.
7e639667f8 refactor: remove heuristic model resolution and enforce allowlist 2026-05-06 00:00:27 +08:00
CJACK.
066c48c107 Bump version from 4.4.2 to 4.4.3 2026-05-05 22:29:36 +08:00
CJACK.
d69b0658ea Merge pull request #430 from NgoQuocViet2001/ai/openai-stream-empty-choices
fix(openai): avoid empty choices stream heartbeat
2026-05-05 22:24:21 +08:00
NgoQuocViet2001
4315b424bf fix(openai): keep stream heartbeat choice-free 2026-05-05 21:13:38 +07:00
5 changed files with 15 additions and 116 deletions

View File

@@ -1 +1 @@
4.4.2 4.4.3

View File

@@ -75,20 +75,6 @@ func TestResolveExpandedHistoricalAliases(t *testing.T) {
} }
} }
func TestResolveModelHeuristicReasoner(t *testing.T) {
got, ok := ResolveModel(nil, "o3-super")
if !ok || got != "deepseek-v4-pro" {
t.Fatalf("expected heuristic reasoner, got ok=%v model=%q", ok, got)
}
}
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) { func TestResolveModelUnknown(t *testing.T) {
_, ok := ResolveModel(nil, "totally-custom-model") _, ok := ResolveModel(nil, "totally-custom-model")
if ok { if ok {
@@ -96,6 +82,13 @@ func TestResolveModelUnknown(t *testing.T) {
} }
} }
func TestResolveModelUnknownKnownFamilyName(t *testing.T) {
_, ok := ResolveModel(nil, "gpt-5.5-pro-search")
if ok {
t.Fatal("expected unknown known-family model to fail resolve without alias")
}
}
func TestResolveModelRejectsLegacyDeepSeekIDs(t *testing.T) { func TestResolveModelRejectsLegacyDeepSeekIDs(t *testing.T) {
legacyModels := []string{ legacyModels := []string{
"deepseek-chat", "deepseek-chat",
@@ -151,13 +144,6 @@ func TestResolveModelCustomAliasToVision(t *testing.T) {
} }
} }
func TestResolveModelHeuristicVisionIgnoresSearchSuffix(t *testing.T) {
got, ok := ResolveModel(nil, "gemini-vision-search")
if !ok || got != "deepseek-v4-vision" {
t.Fatalf("expected heuristic vision alias to resolve without search variant, got ok=%v model=%q", ok, got)
}
}
func TestClaudeModelsResponsePaginationFields(t *testing.T) { func TestClaudeModelsResponsePaginationFields(t *testing.T) {
resp := ClaudeModelsResponse() resp := ClaudeModelsResponse()
if _, ok := resp["first_id"]; !ok { if _, ok := resp["first_id"]; !ok {

View File

@@ -214,26 +214,10 @@ func ResolveModel(store ModelAliasReader, requested string) (string, bool) {
return mapped, true return mapped, true
} }
baseModel, noThinking := splitNoThinkingModel(model) baseModel, noThinking := splitNoThinkingModel(model)
resolvedModel, ok := resolveCanonicalModel(aliases, baseModel) if mapped, ok := aliases[baseModel]; ok && IsSupportedDeepSeekModel(mapped) {
if !ok { return withNoThinkingVariant(mapped, noThinking), true
}
return "", false return "", false
}
return withNoThinkingVariant(resolvedModel, noThinking), true
}
func isRetiredHistoricalModel(model string) bool {
switch {
case strings.HasPrefix(model, "claude-1."):
return true
case strings.HasPrefix(model, "claude-2."):
return true
case strings.HasPrefix(model, "claude-instant-"):
return true
case strings.HasPrefix(model, "gpt-3.5"):
return true
default:
return false
}
} }
func lower(s string) string { func lower(s string) string {
@@ -315,58 +299,3 @@ func loadModelAliases(store ModelAliasReader) map[string]string {
} }
return aliases 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:
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

@@ -127,13 +127,7 @@ func (s *chatStreamRuntime) sendKeepAlive() {
return return
} }
_, _ = s.w.Write([]byte(": keep-alive\n\n")) _, _ = s.w.Write([]byte(": keep-alive\n\n"))
s.sendChunk(openaifmt.BuildChatStreamChunk( _ = s.rc.Flush()
s.completionID,
s.created,
s.model,
[]map[string]any{},
nil,
))
} }
func (s *chatStreamRuntime) sendChunk(v any) { func (s *chatStreamRuntime) sendChunk(v any) {

View File

@@ -10,7 +10,7 @@ import (
"ds2api/internal/promptcompat" "ds2api/internal/promptcompat"
) )
func TestChatStreamKeepAliveEmitsEmptyChoiceDataFrame(t *testing.T) { func TestChatStreamKeepAliveUsesCommentOnly(t *testing.T) {
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
runtime := newChatStreamRuntime( runtime := newChatStreamRuntime(
rec, rec,
@@ -40,18 +40,8 @@ func TestChatStreamKeepAliveEmitsEmptyChoiceDataFrame(t *testing.T) {
if done { if done {
t.Fatalf("keep-alive must not emit [DONE], body=%q", body) t.Fatalf("keep-alive must not emit [DONE], body=%q", body)
} }
if len(frames) != 1 { if len(frames) != 0 {
t.Fatalf("expected one data frame, got %d body=%q", len(frames), body) t.Fatalf("keep-alive must not emit JSON data frames, got %#v body=%q", frames, body)
}
if got := asString(frames[0]["id"]); got != "chatcmpl-test" {
t.Fatalf("expected completion id to be preserved, got %q", got)
}
if got := asString(frames[0]["object"]); got != "chat.completion.chunk" {
t.Fatalf("expected chat chunk object, got %q", got)
}
choices, _ := frames[0]["choices"].([]any)
if len(choices) != 0 {
t.Fatalf("expected empty choices heartbeat, got %#v", choices)
} }
} }