Files
ds2api/internal/deepseek/protocol/constants.go
706e68de23 fix: increase stream timeout constants for large-context models; guard against context-cancelled double-recording
- Increase StreamIdleTimeout from 90s to 300s and MaxKeepaliveCount from 10 to 40
  to prevent premature stream termination with DeepSeek V4 Pro (~50K token contexts)
- Add r.Context().Err() check after ConsumeSSE in empty_retry_runtime (chat + responses)
  to prevent historySession.error() from overwriting historySession.stopped()
  when the request context is cancelled

References:
- MaxKeepaliveCount=10 creates a 50s no-content timeout that kills the stream
  before DeepSeek V4 Pro can produce its first token with large contexts
- Hermes Agent reports 'No response from provider for 180s' because the
  underlying SSE connection was already terminated by ds2api at 50s
- Context cancellation path: OnContextDone -> stopped(), then finalize()
  with empty output -> retry -> error() overwrites stopped()
2026-05-01 21:11:36 +08:00

165 lines
4.5 KiB
Go

package protocol
import (
_ "embed"
"encoding/json"
"fmt"
)
const (
DeepSeekHost = "chat.deepseek.com"
DeepSeekLoginURL = "https://chat.deepseek.com/api/v0/users/login"
DeepSeekCreateSessionURL = "https://chat.deepseek.com/api/v0/chat_session/create"
DeepSeekCreatePowURL = "https://chat.deepseek.com/api/v0/chat/create_pow_challenge"
DeepSeekCompletionURL = "https://chat.deepseek.com/api/v0/chat/completion"
DeepSeekContinueURL = "https://chat.deepseek.com/api/v0/chat/continue"
DeepSeekUploadFileURL = "https://chat.deepseek.com/api/v0/file/upload_file"
DeepSeekFetchFilesURL = "https://chat.deepseek.com/api/v0/file/fetch_files"
DeepSeekFetchSessionURL = "https://chat.deepseek.com/api/v0/chat_session/fetch_page"
DeepSeekDeleteSessionURL = "https://chat.deepseek.com/api/v0/chat_session/delete"
DeepSeekDeleteAllSessionsURL = "https://chat.deepseek.com/api/v0/chat_session/delete_all"
DeepSeekCompletionTargetPath = "/api/v0/chat/completion"
DeepSeekUploadTargetPath = "/api/v0/file/upload_file"
)
var defaultStaticBaseHeaders = map[string]string{
"Host": "chat.deepseek.com",
"Accept": "application/json",
"Content-Type": "application/json",
"accept-charset": "UTF-8",
}
var defaultSkipContainsPatterns = []string{
"quasi_status",
"elapsed_secs",
"token_usage",
"pending_fragment",
"conversation_mode",
"fragments/-1/status",
"fragments/-2/status",
"fragments/-3/status",
}
var defaultSkipExactPaths = []string{
"response/search_status",
}
var ClientVersion string
var BaseHeaders = map[string]string{}
var SkipContainsPatterns = cloneStringSlice(defaultSkipContainsPatterns)
var SkipExactPathSet = toStringSet(defaultSkipExactPaths)
type clientConstants struct {
Name string `json:"name"`
Platform string `json:"platform"`
Version string `json:"version"`
AndroidAPILevel string `json:"android_api_level"`
Locale string `json:"locale"`
}
type sharedConstants struct {
Client clientConstants `json:"client"`
BaseHeaders map[string]string `json:"base_headers"`
SkipContainsPattern []string `json:"skip_contains_patterns"`
SkipExactPaths []string `json:"skip_exact_paths"`
}
//go:embed constants_shared.json
var sharedConstantsJSON []byte
func init() {
cfg := sharedConstants{}
if err := json.Unmarshal(sharedConstantsJSON, &cfg); err != nil {
panic(fmt.Errorf("load DeepSeek shared constants: %w", err))
}
applySharedConstants(cfg)
}
func applySharedConstants(cfg sharedConstants) {
client := normalizeClientConstants(cfg.Client)
ClientVersion = client.Version
BaseHeaders = buildBaseHeaders(client, cfg.BaseHeaders)
SkipContainsPatterns = cloneStringSlice(defaultSkipContainsPatterns)
if len(cfg.SkipContainsPattern) > 0 {
SkipContainsPatterns = cloneStringSlice(cfg.SkipContainsPattern)
}
SkipExactPathSet = toStringSet(defaultSkipExactPaths)
if len(cfg.SkipExactPaths) > 0 {
SkipExactPathSet = toStringSet(cfg.SkipExactPaths)
}
}
func normalizeClientConstants(in clientConstants) clientConstants {
if in.Name == "" {
in.Name = "DeepSeek"
}
if in.Platform == "" {
in.Platform = "android"
}
if in.AndroidAPILevel == "" {
in.AndroidAPILevel = "35"
}
if in.Locale == "" {
in.Locale = "zh_CN"
}
return in
}
func buildBaseHeaders(client clientConstants, overrides map[string]string) map[string]string {
out := cloneStringMap(defaultStaticBaseHeaders)
for k, v := range overrides {
if k == "" || v == "" {
continue
}
out[k] = v
}
if client.Name != "" && client.Version != "" {
userAgent := client.Name + "/" + client.Version
if client.Platform == "android" && client.AndroidAPILevel != "" {
userAgent += " Android/" + client.AndroidAPILevel
}
out["User-Agent"] = userAgent
}
if client.Platform != "" {
out["x-client-platform"] = client.Platform
}
if client.Version != "" {
out["x-client-version"] = client.Version
}
if client.Locale != "" {
out["x-client-locale"] = client.Locale
}
return out
}
func cloneStringMap(in map[string]string) map[string]string {
out := make(map[string]string, len(in))
for k, v := range in {
out[k] = v
}
return out
}
func cloneStringSlice(in []string) []string {
out := make([]string, len(in))
copy(out, in)
return out
}
func toStringSet(in []string) map[string]struct{} {
out := make(map[string]struct{}, len(in))
for _, v := range in {
if v == "" {
continue
}
out[v] = struct{}{}
}
return out
}
const (
KeepAliveTimeout = 5
StreamIdleTimeout = 300
MaxKeepaliveCount = 40
)