mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-02 07:25:26 +08:00
- 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()
165 lines
4.5 KiB
Go
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
|
|
)
|