mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-01 23:15:27 +08:00
版本号修改
This commit is contained in:
@@ -98,6 +98,8 @@ DS2API 当前的核心思路,不是把客户端传来的 `messages`、`tools`
|
||||
- `prompt` 才是对话上下文主载体。
|
||||
- `ref_file_ids` 只承载文件引用,不承载普通文本消息。
|
||||
- `tools` 不会作为“原生工具 schema”直接下发给下游,而是被改写进 `prompt`。
|
||||
- 当前 `/v1/chat/completions` 业务路径仍是“每次请求新建一个远端 `chat_session_id`,并默认发送 `parent_message_id: null`”;因此 DS2API 对外默认表现为“新会话 + prompt 拼历史”,而不是复用 DeepSeek 原生会话树。
|
||||
- 但 DeepSeek 远端本身支持同一 `chat_session_id` 的跨轮次持续对话。2026-04-27 已用项目内现有 DeepSeek client 做过一次不改业务代码的双轮实测:同一 `chat_session_id` 下,第 1 轮返回 `request_message_id=1` / `response_message_id=2` / 文本 `SESSION_TEST_ONE`;第 2 轮重新获取一次 PoW,并发送 `parent_message_id=2` 后,成功返回 `request_message_id=3` / `response_message_id=4` / 文本 `SESSION_TEST_TWO`。这说明“同远端会话持续聊天”能力存在,且每轮需要携带正确的 parent/message 链接信息,同时重新获取对应轮次可用的 PoW。
|
||||
- OpenAI Chat / Responses 原生走统一 OpenAI 标准化与 DeepSeek payload 组装;Claude / Gemini 会尽量复用 OpenAI prompt/tool 语义,其中 Gemini 直接复用 `promptcompat.BuildOpenAIPromptForAdapter`,Claude 消息接口在可代理场景会转换为 OpenAI chat 形态再执行。
|
||||
- 客户端传入的 thinking / reasoning 开关会被归一到下游 `thinking_enabled`。Gemini `generationConfig.thinkingConfig.thinkingBudget` 会翻译成同一套 thinking 开关;关闭时即使上游返回 `response/thinking_content`,兼容层也不会把它当作可见正文输出。若最终解析出的模型名带 `-nothinking` 后缀,则会无条件强制关闭 thinking,优先级高于请求体中的 `thinking` / `reasoning` / `reasoning_effort`。Claude surface 在流式请求且未显式声明 `thinking` 时,仍按 Anthropic 语义默认关闭;但在非流式代理场景,兼容层会内部开启一次下游 thinking,用于捕获“正文为空、工具调用落在 thinking 里”的情况,随后在回包前剥离用户不可见的 thinking block。
|
||||
- 对 OpenAI Chat / Responses 的非流式收尾,如果最终可见正文为空,兼容层会优先尝试把思维链中的独立 DSML / XML 工具块当作真实工具调用解析出来。流式链路也会在收尾阶段做同样的 fallback 检测,但不会因为思维链内容去中途拦截或改写流式输出;thinking / reasoning 增量仍按原样先发,只有在结束收尾时才可能补发最终工具调用结果。补发结果会作为本轮 assistant 的结构化 `tool_calls` / `function_call` 输出返回,而不是塞进 `content` 文本;如果客户端没有开启 thinking / reasoning,思维链只用于检测,不会作为 `reasoning_content` 或可见正文暴露。只有正文为空且思维链里也没有可执行工具调用时,才继续按空回复错误处理。
|
||||
|
||||
@@ -3,6 +3,7 @@ package protocol
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -21,15 +22,11 @@ const (
|
||||
DeepSeekUploadTargetPath = "/api/v0/file/upload_file"
|
||||
)
|
||||
|
||||
var defaultBaseHeaders = map[string]string{
|
||||
"Host": "chat.deepseek.com",
|
||||
"User-Agent": "DeepSeek/1.8.0 Android/35",
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"x-client-platform": "android",
|
||||
"x-client-version": "1.8.0",
|
||||
"x-client-locale": "zh_CN",
|
||||
"accept-charset": "UTF-8",
|
||||
var defaultStaticBaseHeaders = map[string]string{
|
||||
"Host": "chat.deepseek.com",
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"accept-charset": "UTF-8",
|
||||
}
|
||||
|
||||
var defaultSkipContainsPatterns = []string{
|
||||
@@ -47,11 +44,21 @@ var defaultSkipExactPaths = []string{
|
||||
"response/search_status",
|
||||
}
|
||||
|
||||
var BaseHeaders = cloneStringMap(defaultBaseHeaders)
|
||||
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"`
|
||||
@@ -63,19 +70,68 @@ var sharedConstantsJSON []byte
|
||||
func init() {
|
||||
cfg := sharedConstants{}
|
||||
if err := json.Unmarshal(sharedConstantsJSON, &cfg); err != nil {
|
||||
return
|
||||
}
|
||||
if len(cfg.BaseHeaders) > 0 {
|
||||
BaseHeaders = cloneStringMap(cfg.BaseHeaders)
|
||||
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 {
|
||||
@@ -103,6 +159,6 @@ func toStringSet(in []string) map[string]struct{} {
|
||||
|
||||
const (
|
||||
KeepAliveTimeout = 5
|
||||
StreamIdleTimeout = 30
|
||||
StreamIdleTimeout = 90
|
||||
MaxKeepaliveCount = 10
|
||||
)
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
{
|
||||
"client": {
|
||||
"name": "DeepSeek",
|
||||
"platform": "android",
|
||||
"version": "2.0.1",
|
||||
"android_api_level": "35",
|
||||
"locale": "zh_CN"
|
||||
},
|
||||
"base_headers": {
|
||||
"Host": "chat.deepseek.com",
|
||||
"User-Agent": "DeepSeek/1.8.0 Android/35",
|
||||
"Accept": "application/json",
|
||||
"x-client-platform": "android",
|
||||
"x-client-version": "1.8.0",
|
||||
"x-client-locale": "zh_CN",
|
||||
"Content-Type": "application/json",
|
||||
"accept-charset": "UTF-8"
|
||||
},
|
||||
"skip_contains_patterns": [
|
||||
|
||||
@@ -1,11 +1,32 @@
|
||||
package protocol
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSharedConstantsLoaded(t *testing.T) {
|
||||
cfg := sharedConstants{}
|
||||
if err := json.Unmarshal(sharedConstantsJSON, &cfg); err != nil {
|
||||
t.Fatalf("failed to parse shared constants: %v", err)
|
||||
}
|
||||
client := normalizeClientConstants(cfg.Client)
|
||||
if ClientVersion != client.Version {
|
||||
t.Fatalf("unexpected client version=%q", ClientVersion)
|
||||
}
|
||||
wantUserAgent := client.Name + "/" + client.Version + " Android/" + client.AndroidAPILevel
|
||||
if BaseHeaders["User-Agent"] != wantUserAgent {
|
||||
t.Fatalf("unexpected user agent=%q", BaseHeaders["User-Agent"])
|
||||
}
|
||||
if BaseHeaders["x-client-platform"] != "android" {
|
||||
t.Fatalf("unexpected base header x-client-platform=%q", BaseHeaders["x-client-platform"])
|
||||
}
|
||||
if BaseHeaders["x-client-version"] != ClientVersion {
|
||||
t.Fatalf("unexpected base header x-client-version=%q", BaseHeaders["x-client-version"])
|
||||
}
|
||||
if BaseHeaders["Content-Type"] != "application/json" {
|
||||
t.Fatalf("unexpected base header Content-Type=%q", BaseHeaders["Content-Type"])
|
||||
}
|
||||
if len(SkipContainsPatterns) == 0 {
|
||||
t.Fatal("expected skip contains patterns to be loaded")
|
||||
}
|
||||
@@ -13,3 +34,23 @@ func TestSharedConstantsLoaded(t *testing.T) {
|
||||
t.Fatal("expected response/search_status in exact skip path set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientHeadersDerivedFromSharedVersion(t *testing.T) {
|
||||
client := normalizeClientConstants(clientConstants{
|
||||
Name: "DeepSeek",
|
||||
Platform: "android",
|
||||
Version: "9.8.7",
|
||||
AndroidAPILevel: "35",
|
||||
Locale: "zh_CN",
|
||||
})
|
||||
headers := buildBaseHeaders(client, map[string]string{
|
||||
"User-Agent": "stale",
|
||||
"x-client-version": "stale",
|
||||
})
|
||||
if headers["User-Agent"] != "DeepSeek/9.8.7 Android/35" {
|
||||
t.Fatalf("unexpected derived user agent=%q", headers["User-Agent"])
|
||||
}
|
||||
if headers["x-client-version"] != "9.8.7" {
|
||||
t.Fatalf("unexpected derived client version=%q", headers["x-client-version"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,17 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const DEFAULT_CLIENT = Object.freeze({
|
||||
name: 'DeepSeek',
|
||||
platform: 'android',
|
||||
androidApiLevel: '35',
|
||||
locale: 'zh_CN',
|
||||
});
|
||||
|
||||
const DEFAULT_BASE_HEADERS = Object.freeze({
|
||||
Host: 'chat.deepseek.com',
|
||||
'User-Agent': 'DeepSeek/1.8.0 Android/35',
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'x-client-platform': 'android',
|
||||
'x-client-version': '1.8.0',
|
||||
'x-client-locale': 'zh_CN',
|
||||
'accept-charset': 'UTF-8',
|
||||
});
|
||||
|
||||
@@ -29,38 +32,96 @@ const DEFAULT_SKIP_EXACT_PATHS = Object.freeze([
|
||||
'response/search_status',
|
||||
]);
|
||||
|
||||
function loadSharedConstants() {
|
||||
const sharedPath = path.resolve(__dirname, '../../internal/deepseek/constants_shared.json');
|
||||
try {
|
||||
const raw = fs.readFileSync(sharedPath, 'utf8');
|
||||
const parsed = JSON.parse(raw);
|
||||
const baseHeaders = parsed && typeof parsed.base_headers === 'object' && !Array.isArray(parsed.base_headers)
|
||||
? { ...DEFAULT_BASE_HEADERS, ...parsed.base_headers }
|
||||
: { ...DEFAULT_BASE_HEADERS };
|
||||
const skipPatterns = Array.isArray(parsed && parsed.skip_contains_patterns)
|
||||
? parsed.skip_contains_patterns.filter((v) => typeof v === 'string' && v !== '')
|
||||
: [...DEFAULT_SKIP_PATTERNS];
|
||||
const skipExactPaths = Array.isArray(parsed && parsed.skip_exact_paths)
|
||||
? parsed.skip_exact_paths.filter((v) => typeof v === 'string' && v !== '')
|
||||
: [...DEFAULT_SKIP_EXACT_PATHS];
|
||||
return {
|
||||
baseHeaders,
|
||||
skipPatterns,
|
||||
skipExactPaths,
|
||||
};
|
||||
} catch (_err) {
|
||||
return {
|
||||
baseHeaders: { ...DEFAULT_BASE_HEADERS },
|
||||
skipPatterns: [...DEFAULT_SKIP_PATTERNS],
|
||||
skipExactPaths: [...DEFAULT_SKIP_EXACT_PATHS],
|
||||
};
|
||||
function asNonEmptyString(value) {
|
||||
return typeof value === 'string' && value !== '' ? value : '';
|
||||
}
|
||||
|
||||
function normalizeClient(raw) {
|
||||
const client = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
|
||||
return {
|
||||
name: asNonEmptyString(client.name) || DEFAULT_CLIENT.name,
|
||||
platform: asNonEmptyString(client.platform) || DEFAULT_CLIENT.platform,
|
||||
version: asNonEmptyString(client.version),
|
||||
androidApiLevel: asNonEmptyString(client.android_api_level) || DEFAULT_CLIENT.androidApiLevel,
|
||||
locale: asNonEmptyString(client.locale) || DEFAULT_CLIENT.locale,
|
||||
};
|
||||
}
|
||||
|
||||
function buildBaseHeaders(parsed, client) {
|
||||
const rawBaseHeaders = parsed && typeof parsed.base_headers === 'object' && !Array.isArray(parsed.base_headers)
|
||||
? parsed.base_headers
|
||||
: {};
|
||||
const baseHeaders = { ...DEFAULT_BASE_HEADERS, ...rawBaseHeaders };
|
||||
if (client.name && client.version) {
|
||||
const androidSuffix = client.platform === 'android' && client.androidApiLevel
|
||||
? ` Android/${client.androidApiLevel}`
|
||||
: '';
|
||||
baseHeaders['User-Agent'] = `${client.name}/${client.version}${androidSuffix}`;
|
||||
}
|
||||
if (client.platform) {
|
||||
baseHeaders['x-client-platform'] = client.platform;
|
||||
}
|
||||
if (client.version) {
|
||||
baseHeaders['x-client-version'] = client.version;
|
||||
}
|
||||
if (client.locale) {
|
||||
baseHeaders['x-client-locale'] = client.locale;
|
||||
}
|
||||
return baseHeaders;
|
||||
}
|
||||
|
||||
function sharedConstantsPaths() {
|
||||
return [
|
||||
path.resolve(__dirname, '../../deepseek/protocol/constants_shared.json'),
|
||||
path.resolve(process.cwd(), 'internal/deepseek/protocol/constants_shared.json'),
|
||||
];
|
||||
}
|
||||
|
||||
function readSharedConstants() {
|
||||
try {
|
||||
return require('../../deepseek/protocol/constants_shared.json');
|
||||
} catch (_err) {
|
||||
// Fall through to filesystem candidates for test and local execution variants.
|
||||
}
|
||||
for (const sharedPath of sharedConstantsPaths()) {
|
||||
try {
|
||||
const raw = fs.readFileSync(sharedPath, 'utf8');
|
||||
return JSON.parse(raw);
|
||||
} catch (_err) {
|
||||
// Try the next candidate path; fall back to in-file structural defaults below.
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function loadSharedConstants() {
|
||||
const parsed = readSharedConstants();
|
||||
const client = normalizeClient(parsed && parsed.client);
|
||||
const skipPatterns = Array.isArray(parsed && parsed.skip_contains_patterns)
|
||||
? parsed.skip_contains_patterns.filter((v) => typeof v === 'string' && v !== '')
|
||||
: [...DEFAULT_SKIP_PATTERNS];
|
||||
const skipExactPaths = Array.isArray(parsed && parsed.skip_exact_paths)
|
||||
? parsed.skip_exact_paths.filter((v) => typeof v === 'string' && v !== '')
|
||||
: [...DEFAULT_SKIP_EXACT_PATHS];
|
||||
return {
|
||||
client,
|
||||
baseHeaders: buildBaseHeaders(parsed, client),
|
||||
skipPatterns,
|
||||
skipExactPaths,
|
||||
};
|
||||
}
|
||||
|
||||
const shared = loadSharedConstants();
|
||||
|
||||
module.exports = {
|
||||
CLIENT: Object.freeze({ ...shared.client }),
|
||||
CLIENT_VERSION: shared.client.version,
|
||||
BASE_HEADERS: Object.freeze(shared.baseHeaders),
|
||||
SKIP_PATTERNS: Object.freeze(shared.skipPatterns),
|
||||
SKIP_EXACT_PATHS: new Set(shared.skipExactPaths),
|
||||
__test: {
|
||||
buildBaseHeaders,
|
||||
normalizeClient,
|
||||
sharedConstantsPaths,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const chatStream = require('../../api/chat-stream.js');
|
||||
const deepseekConstants = require('../../internal/js/shared/deepseek-constants.js');
|
||||
const { parseToolCallsDetailed, parseStandaloneToolCallsDetailed } = require('../../internal/js/helpers/stream-tool-sieve.js');
|
||||
|
||||
const { parseChunkForContent, estimateTokens } = chatStream.__test;
|
||||
@@ -16,6 +17,15 @@ function readJSON(filePath) {
|
||||
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
}
|
||||
|
||||
test('js shared constants derive client headers from shared json', () => {
|
||||
const shared = readJSON(path.resolve(__dirname, '../../internal/deepseek/protocol/constants_shared.json'));
|
||||
const client = shared.client;
|
||||
assert.equal(deepseekConstants.CLIENT_VERSION, client.version);
|
||||
assert.equal(deepseekConstants.BASE_HEADERS['x-client-version'], client.version);
|
||||
assert.equal(deepseekConstants.BASE_HEADERS['User-Agent'], `${client.name}/${client.version} Android/${client.android_api_level}`);
|
||||
assert.equal(deepseekConstants.BASE_HEADERS['Content-Type'], 'application/json');
|
||||
});
|
||||
|
||||
test('js compat: sse fixtures', () => {
|
||||
const fixtureDir = path.join(compatRoot, 'fixtures', 'sse_chunks');
|
||||
const expectedDir = path.join(compatRoot, 'expected');
|
||||
|
||||
Reference in New Issue
Block a user