版本号修改

This commit is contained in:
CJACK
2026-04-27 20:12:33 +08:00
parent 0378d8c0a9
commit fb43bd92f5
6 changed files with 223 additions and 49 deletions

View File

@@ -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` 或可见正文暴露。只有正文为空且思维链里也没有可执行工具调用时,才继续按空回复错误处理。

View File

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

View File

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

View File

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

View File

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

View File

@@ -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');