Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions API.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -770,8 +770,8 @@ Reads runtime settings and status, including:
- `runtime` (`account_max_inflight`, `account_max_queue`, `global_max_inflight`, `token_refresh_interval_hours`)
- `responses` / `embeddings`
- `auto_delete` (`mode`: `none` / `single` / `all`; legacy `sessions=true` is still treated as `all`)
- `current_input_file` (`enabled` defaults to `true`, plus `min_chars`)
- `thinking_injection` (`enabled` defaults to `true`, `prompt`, and `default_prompt`)
- `current_input_file` (`enabled` defaults to `true`, plus `min_chars`; `inline_max_tokens` defaults to `30000`)
- `thinking_injection` (`enabled` defaults to `false`, `prompt`, and `default_prompt`)
- `model_aliases`
- `env_backed`, `needs_vercel_sync`
- `parser_v2` (`mode`: `off` / `shadow` / `enforce`; `env_override`: whether an env var is active; read-only, ignored on PUT)
Expand All @@ -786,7 +786,7 @@ Hot-updates runtime settings. Supported fields:
- `responses.store_ttl_seconds`
- `embeddings.provider`
- `auto_delete.mode`
- `current_input_file.enabled` / `current_input_file.min_chars`
- `current_input_file.enabled` / `current_input_file.min_chars` / `current_input_file.inline_max_tokens`
- `thinking_injection.enabled` / `thinking_injection.prompt`
- `model_aliases`
- `toolcall` policy is fixed and is no longer writable through settings
Expand Down
6 changes: 3 additions & 3 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -776,8 +776,8 @@ data: {"type":"message_stop"}
- `runtime`(`account_max_inflight`、`account_max_queue`、`global_max_inflight`、`token_refresh_interval_hours`)
- `responses` / `embeddings`
- `auto_delete`(`mode`:`none` / `single` / `all`;旧配置 `sessions=true` 仍按 `all` 处理)
- `current_input_file`(`enabled` 默认返回 `true`、`min_chars`)
- `thinking_injection`(`enabled` 默认返回 `true`、`prompt`、`default_prompt`)
- `current_input_file`(`enabled` 默认返回 `true`、`min_chars`、`inline_max_tokens` 默认返回 `30000`
- `thinking_injection`(`enabled` 默认返回 `false`、`prompt`、`default_prompt`)
- `model_aliases`
- `env_backed`、`needs_vercel_sync`
- `parser_v2`(`mode`:`off` / `shadow` / `enforce`;`env_override`:是否被环境变量覆盖;只读,不接受 PUT 写入)
Expand All @@ -792,7 +792,7 @@ data: {"type":"message_stop"}
- `responses.store_ttl_seconds`
- `embeddings.provider`
- `auto_delete.mode`
- `current_input_file.enabled` / `current_input_file.min_chars`
- `current_input_file.enabled` / `current_input_file.min_chars` / `current_input_file.inline_max_tokens`
- `thinking_injection.enabled` / `thinking_injection.prompt`
- `model_aliases`
- `toolcall` 策略已固定,不再作为可写入字段
Expand Down
3 changes: 2 additions & 1 deletion config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@
},
"current_input_file": {
"enabled": true,
"min_chars": 0
"min_chars": 0,
"inline_max_tokens": 30000
},
"thinking_injection": {
"enabled": false,
Expand Down
4 changes: 2 additions & 2 deletions docs/prompt-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,9 +283,9 @@ OpenAI 的文件上传现在不再是“只传文件本体”的通用路径,

兼容层现在只保留 `current_input_file` 这一种拆分方式;旧的 `history_split` 配置字段已移除,读取旧配置时会忽略它且不会再写回。

- `current_input_file` 默认开启;它在统一 completion runtime 入口全局生效,用于把“完整上下文”合并进 `DS2API_HISTORY.txt` 上下文文件。当最新 user turn 的纯文本长度达到 `current_input_file.min_chars`(默认 `0`),runtime 会上传一个文件名为 `DS2API_HISTORY.txt` 的上下文文件。文件内容会先经过各协议入口的标准化,再序列化成按轮次编号的 `DS2API_HISTORY.txt` 风格 transcript,带有 `# DS2API_HISTORY.txt` 标题和 `=== N. ROLE ===` 分段;如果当前请求带有可用 tools 且 tool choice 不是 `none`,runtime 还会上传 `DS2API_TOOLS.txt`,只承载本次请求的工具名称、描述和参数 schema。live prompt 中则会给出一个 continuation 语气的 user 消息,引导模型从 `DS2API_HISTORY.txt` 的最新状态继续推进,并直接回答最新请求;如果有 `DS2API_TOOLS.txt`,live prompt 会额外要求模型把该文件视为可调用工具和 schema 的权威来源,避免把任务拉回起点,也避免把大段 schema 再次内联到 prompt。
- `current_input_file` 默认开启;它在统一 completion runtime 入口全局生效,并采用 inline-first 策略。runtime 会先按拆分前的完整 `PromptTokenText` 估算整体上下文 token;当整体上下文不超过 `current_input_file.inline_max_tokens`(默认 `30000`)时,请求保持完整 inline prompt,不上传生成的上下文或工具文件。只有最新 user turn 的纯文本长度达到 `current_input_file.min_chars`(默认 `0`)且整体上下文超过 inline 阈值时,runtime 才会把“完整上下文”合并进 `DS2API_HISTORY.txt` 上下文文件。文件内容会先经过各协议入口的标准化,再序列化成按轮次编号的 `DS2API_HISTORY.txt` 风格 transcript,带有 `# DS2API_HISTORY.txt` 标题和 `=== N. ROLE ===` 分段;如果当前请求带有可用 tools 且 tool choice 不是 `none`,runtime 还会上传 `DS2API_TOOLS.txt`,只承载本次请求的工具名称、描述和参数 schema。live prompt 中则会给出一个 continuation 语气的 user 消息,引导模型从 `DS2API_HISTORY.txt` 的最新状态继续推进,并直接回答最新请求;如果有 `DS2API_TOOLS.txt`,live prompt 会额外要求模型把该文件视为可调用工具和 schema 的权威来源,避免把任务拉回起点,也避免把大段 schema 再次内联到 prompt。
- 如果 `current_input_file.enabled=false`,请求会直接透传,不上传任何拆分上下文文件。
- 即使触发 `current_input_file` 后 live prompt 被缩短,对客户端回包里的上下文 token 统计,仍会沿用**拆分前的完整 prompt 语义**做计数,而不是按缩短后的占位 prompt 计算;否则会把真实上下文显著算小。
- 对客户端回包里的上下文 token 统计,短上下文 inline 路径直接按完整 prompt 计数;触发 `current_input_file` 后 live prompt 被缩短时,仍会沿用**拆分前的完整 prompt 语义**做计数,而不是按缩短后的占位 prompt 计算;否则会把真实上下文显著算小。
- 如果空输出重试最终需要切换托管账号,runtime 会在新账号下重新上传已生成的 `DS2API_HISTORY.txt` 和可选 `DS2API_TOOLS.txt`,再用新的 generated `file_id` 重建 completion payload;客户端原有文件引用会保留在 generated 文件之后。这样可以避免把旧账号不可见的 generated file_id 带到新账号请求里。
- runtime 会为 `DS2API_HISTORY.txt`、`DS2API_TOOLS.txt` 和 live prompt 计算稳定 SHA-256 内容 hash,并写入 completion request 日志字段;`DS2API_TOOLS.txt` 还会使用账号隔离、`model_type` 隔离、内容 hash 隔离的 5 分钟内存缓存。同一账号在短时间内复用完全相同的工具 schema 时,会复用已上传的 tools `file_id`;`DS2API_HISTORY.txt` 只记录 hash,不做 file_id 缓存,避免把长对话历史大量留存在本地内存。

Expand Down
7 changes: 4 additions & 3 deletions internal/config/codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func (c Config) MarshalJSON() ([]byte, error) {
m["embeddings"] = c.Embeddings
}
m["auto_delete"] = c.AutoDelete
if c.CurrentInputFile.Enabled != nil || c.CurrentInputFile.MinChars != 0 {
if c.CurrentInputFile.Enabled != nil || c.CurrentInputFile.MinChars != 0 || c.CurrentInputFile.InlineMaxTokens != 0 {
m["current_input_file"] = c.CurrentInputFile
}
if c.ThinkingInjection.Enabled != nil || strings.TrimSpace(c.ThinkingInjection.Prompt) != "" {
Expand Down Expand Up @@ -204,8 +204,9 @@ func (c Config) Clone() Config {
Embeddings: c.Embeddings,
AutoDelete: c.AutoDelete,
CurrentInputFile: CurrentInputFileConfig{
Enabled: cloneBoolPtr(c.CurrentInputFile.Enabled),
MinChars: c.CurrentInputFile.MinChars,
Enabled: cloneBoolPtr(c.CurrentInputFile.Enabled),
MinChars: c.CurrentInputFile.MinChars,
InlineMaxTokens: c.CurrentInputFile.InlineMaxTokens,
},
ThinkingInjection: ThinkingInjectionConfig{
Enabled: cloneBoolPtr(c.ThinkingInjection.Enabled),
Expand Down
5 changes: 3 additions & 2 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,9 @@ type AutoDeleteConfig struct {
}

type CurrentInputFileConfig struct {
Enabled *bool `json:"enabled,omitempty"`
MinChars int `json:"min_chars,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
MinChars int `json:"min_chars,omitempty"`
InlineMaxTokens int `json:"inline_max_tokens,omitempty"`
}

type ThinkingInjectionConfig struct {
Expand Down
9 changes: 9 additions & 0 deletions internal/config/store_accessors.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,15 @@ func (s *Store) CurrentInputFileMinChars() int {
return s.cfg.CurrentInputFile.MinChars
}

func (s *Store) CurrentInputFileInlineMaxTokens() int {
s.mu.RLock()
defer s.mu.RUnlock()
if s.cfg.CurrentInputFile.InlineMaxTokens > 0 {
return s.cfg.CurrentInputFile.InlineMaxTokens
}
return 30000
}

func (s *Store) ThinkingInjectionEnabled() bool {
s.mu.RLock()
defer s.mu.RUnlock()
Expand Down
8 changes: 7 additions & 1 deletion internal/config/store_accessors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@ func TestStoreCurrentInputFileAccessors(t *testing.T) {
if got := store.CurrentInputFileMinChars(); got != 0 {
t.Fatalf("default current input file min_chars=%d want=0", got)
}
if got := store.CurrentInputFileInlineMaxTokens(); got != 30000 {
t.Fatalf("default current input file inline_max_tokens=%d want=30000", got)
}

enabled := false
store.cfg.CurrentInputFile = CurrentInputFileConfig{Enabled: &enabled, MinChars: 12345}
store.cfg.CurrentInputFile = CurrentInputFileConfig{Enabled: &enabled, MinChars: 12345, InlineMaxTokens: 4096}
if store.CurrentInputFileEnabled() {
t.Fatal("expected current input file disabled")
}
Expand All @@ -25,6 +28,9 @@ func TestStoreCurrentInputFileAccessors(t *testing.T) {
if got := store.CurrentInputFileMinChars(); got != 12345 {
t.Fatalf("current input file min_chars=%d want=12345", got)
}
if got := store.CurrentInputFileInlineMaxTokens(); got != 4096 {
t.Fatalf("current input file inline_max_tokens=%d want=4096", got)
}
}

func TestStoreThinkingInjectionAccessors(t *testing.T) {
Expand Down
3 changes: 3 additions & 0 deletions internal/config/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ func ValidateCurrentInputFileConfig(currentInputFile CurrentInputFileConfig) err
if currentInputFile.MinChars != 0 {
return ValidateIntRange("current_input_file.min_chars", currentInputFile.MinChars, 1, 100000000, true)
}
if currentInputFile.InlineMaxTokens != 0 {
return ValidateIntRange("current_input_file.inline_max_tokens", currentInputFile.InlineMaxTokens, 1, 100000000, true)
Comment on lines 118 to +122

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate both current_input_file fields before returning

ValidateCurrentInputFileConfig returns immediately when min_chars is non-zero, so the new inline_max_tokens check never runs in configs that set both fields. A config like {min_chars: 1, inline_max_tokens: -1} is therefore accepted by ValidateConfig, which allows an invalid threshold into runtime behavior. Please avoid early return here and validate min_chars and inline_max_tokens independently so both constraints are enforced when both keys are present.

Useful? React with 👍 / 👎.

}
return nil
}

Expand Down
5 changes: 5 additions & 0 deletions internal/config/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ func TestValidateConfigRejectsInvalidValues(t *testing.T) {
cfg: Config{CurrentInputFile: CurrentInputFileConfig{MinChars: -1}},
want: "current_input_file.min_chars",
},
{
name: "current input file inline max tokens",
cfg: Config{CurrentInputFile: CurrentInputFileConfig{InlineMaxTokens: -1}},
want: "current_input_file.inline_max_tokens",
},
}

for _, tc := range tests {
Expand Down
24 changes: 19 additions & 5 deletions internal/httpapi/admin/handler_settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ func TestGetSettingsIncludesCurrentInputFileDefaults(t *testing.T) {
if got := intFrom(currentInputFile["min_chars"]); got != 0 {
t.Fatalf("expected current_input_file.min_chars=0, got %d body=%v", got, body)
}
if got := intFrom(currentInputFile["inline_max_tokens"]); got != 30000 {
t.Fatalf("expected current_input_file.inline_max_tokens=30000, got %d body=%v", got, body)
}
thinkingInjection, _ := body["thinking_injection"].(map[string]any)
if got := boolFrom(thinkingInjection["enabled"]); got {
t.Fatalf("expected thinking_injection.enabled=false, body=%v", body)
Expand Down Expand Up @@ -187,8 +190,9 @@ func TestUpdateSettingsCurrentInputFile(t *testing.T) {
h := newAdminTestHandler(t, `{"keys":["k1"],"history_split":{"enabled":true,"trigger_after_turns":2}}`)
payload := map[string]any{
"current_input_file": map[string]any{
"enabled": true,
"min_chars": 12345,
"enabled": true,
"min_chars": 12345,
"inline_max_tokens": 4096,
},
}
b, _ := json.Marshal(payload)
Expand All @@ -205,13 +209,16 @@ func TestUpdateSettingsCurrentInputFile(t *testing.T) {
if snap.CurrentInputFile.MinChars != 12345 {
t.Fatalf("expected current_input_file.min_chars=12345, got %#v", snap.CurrentInputFile)
}
if snap.CurrentInputFile.InlineMaxTokens != 4096 {
t.Fatalf("expected current_input_file.inline_max_tokens=4096, got %#v", snap.CurrentInputFile)
}
if !h.Store.CurrentInputFileEnabled() {
t.Fatal("expected current input file accessor to stay enabled")
}
}

func TestUpdateSettingsCurrentInputFilePartialUpdatePreservesEnabled(t *testing.T) {
h := newAdminTestHandler(t, `{"keys":["k1"],"current_input_file":{"enabled":false,"min_chars":777}}`)
h := newAdminTestHandler(t, `{"keys":["k1"],"current_input_file":{"enabled":false,"min_chars":777,"inline_max_tokens":8192}}`)
payload := map[string]any{
"current_input_file": map[string]any{
"min_chars": 5000,
Expand All @@ -231,13 +238,17 @@ func TestUpdateSettingsCurrentInputFilePartialUpdatePreservesEnabled(t *testing.
if snap.CurrentInputFile.MinChars != 5000 {
t.Fatalf("expected current_input_file.min_chars=5000, got %#v", snap.CurrentInputFile)
}
if snap.CurrentInputFile.InlineMaxTokens != 8192 {
t.Fatalf("expected current_input_file.inline_max_tokens preserved, got %#v", snap.CurrentInputFile)
}
}

func TestUpdateSettingsCurrentInputFilePartialUpdatePreservesMinChars(t *testing.T) {
h := newAdminTestHandler(t, `{"keys":["k1"],"current_input_file":{"enabled":false,"min_chars":777}}`)
h := newAdminTestHandler(t, `{"keys":["k1"],"current_input_file":{"enabled":false,"min_chars":777,"inline_max_tokens":8192}}`)
payload := map[string]any{
"current_input_file": map[string]any{
"enabled": true,
"enabled": true,
"inline_max_tokens": 16384,
},
}
b, _ := json.Marshal(payload)
Expand All @@ -254,6 +265,9 @@ func TestUpdateSettingsCurrentInputFilePartialUpdatePreservesMinChars(t *testing
if snap.CurrentInputFile.MinChars != 777 {
t.Fatalf("expected current_input_file.min_chars to remain 777, got %#v", snap.CurrentInputFile)
}
if snap.CurrentInputFile.InlineMaxTokens != 16384 {
t.Fatalf("expected current_input_file.inline_max_tokens=16384, got %#v", snap.CurrentInputFile)
}
}

func TestUpdateSettingsLogPartialUpdatePreservesOmittedFields(t *testing.T) {
Expand Down
7 changes: 7 additions & 0 deletions internal/httpapi/admin/settings/handler_settings_parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,13 @@ func parseSettingsUpdateRequest(req map[string]any) (*config.AdminConfig, *confi
}
cfg.MinChars = n
}
if v, exists := raw["inline_max_tokens"]; exists {
n := intFrom(v)
if err := config.ValidateIntRange("current_input_file.inline_max_tokens", n, 0, 100000000, true); err != nil {
return nil, nil, nil, nil, nil, nil, nil, nil, nil, err
}
cfg.InlineMaxTokens = n
}
if err := config.ValidateCurrentInputFileConfig(*cfg); err != nil {
return nil, nil, nil, nil, nil, nil, nil, nil, nil, err
}
Expand Down
5 changes: 3 additions & 2 deletions internal/httpapi/admin/settings/handler_settings_read.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ func (h *Handler) getSettings(w http.ResponseWriter, _ *http.Request) {
"embeddings": snap.Embeddings,
"auto_delete": snap.AutoDelete,
"current_input_file": map[string]any{
"enabled": h.Store.CurrentInputFileEnabled(),
"min_chars": h.Store.CurrentInputFileMinChars(),
"enabled": h.Store.CurrentInputFileEnabled(),
"min_chars": h.Store.CurrentInputFileMinChars(),
"inline_max_tokens": h.Store.CurrentInputFileInlineMaxTokens(),
},
"thinking_injection": map[string]any{
"enabled": h.Store.ThinkingInjectionEnabled(),
Expand Down
4 changes: 4 additions & 0 deletions internal/httpapi/admin/settings/handler_settings_write.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ func (h *Handler) updateSettings(w http.ResponseWriter, r *http.Request) {
}
currentInputEnabledSet := hasNestedSettingsKey(req, "current_input_file", "enabled")
currentInputMinCharsSet := hasNestedSettingsKey(req, "current_input_file", "min_chars")
currentInputInlineMaxTokensSet := hasNestedSettingsKey(req, "current_input_file", "inline_max_tokens")
thinkingInjectionEnabledSet := hasNestedSettingsKey(req, "thinking_injection", "enabled")
thinkingInjectionPromptSet := hasNestedSettingsKey(req, "thinking_injection", "prompt")
logLevelSet := hasNestedSettingsKey(req, "log", "level")
Expand Down Expand Up @@ -76,6 +77,9 @@ func (h *Handler) updateSettings(w http.ResponseWriter, r *http.Request) {
if currentInputMinCharsSet {
c.CurrentInputFile.MinChars = currentInputCfg.MinChars
}
if currentInputInlineMaxTokensSet {
c.CurrentInputFile.InlineMaxTokens = currentInputCfg.InlineMaxTokens
}
}
if thinkingInjCfg != nil {
if thinkingInjectionEnabledSet {
Expand Down
1 change: 1 addition & 0 deletions internal/httpapi/admin/shared/deps.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type ConfigStore interface {
AutoDeleteMode() string
CurrentInputFileEnabled() bool
CurrentInputFileMinChars() int
CurrentInputFileInlineMaxTokens() int
ThinkingInjectionEnabled() bool
ThinkingInjectionPrompt() string
AutoDeleteSessions() bool
Expand Down
29 changes: 29 additions & 0 deletions internal/httpapi/openai/history/current_input_cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ func (currentInputCacheTestStore) CurrentInputFileEnabled() bool { return true }
func (currentInputCacheTestStore) CurrentInputFileMinChars() int { return 0 }
func (currentInputCacheTestStore) ContextEngineMode() string { return "off" }

type inlineFirstTestStore struct{}

func (inlineFirstTestStore) CurrentInputFileEnabled() bool { return true }
func (inlineFirstTestStore) CurrentInputFileMinChars() int { return 0 }
func (inlineFirstTestStore) CurrentInputFileInlineMaxTokens() int { return 30000 }
func (inlineFirstTestStore) ContextEngineMode() string { return "off" }

type currentInputCacheTestDS struct {
uploads []dsclient.UploadFileRequest
}
Expand All @@ -26,6 +33,28 @@ func (d *currentInputCacheTestDS) UploadFile(_ context.Context, _ *auth.RequestA
return &dsclient.UploadFileResult{ID: fmt.Sprintf("file-%d", len(d.uploads))}, nil
}

func TestApplyCurrentInputFileKeepsShortContextInline(t *testing.T) {
ds := &currentInputCacheTestDS{}
svc := Service{Store: inlineFirstTestStore{}, DS: ds}
stdReq := cacheTestStandardRequest()
stdReq.FinalPrompt = "system rule\n\nsearch docs"
stdReq.PromptTokenText = stdReq.FinalPrompt

out, err := svc.ApplyCurrentInputFile(context.Background(), &auth.RequestAuth{DeepSeekToken: "token"}, stdReq)
if err != nil {
t.Fatalf("apply failed: %v", err)
}
if len(ds.uploads) != 0 {
t.Fatalf("expected short context to stay inline without uploads, got %d", len(ds.uploads))
}
if out.CurrentInputFileApplied {
t.Fatalf("expected current input file to remain unapplied for short context")
}
if out.FinalPrompt != stdReq.FinalPrompt || out.PromptTokenText != stdReq.PromptTokenText {
t.Fatalf("expected inline request to pass through unchanged, got %#v", out)
}
}

func TestApplyCurrentInputFileCachesToolsFileByStableHash(t *testing.T) {
ResetCurrentInputToolsFileCacheForTesting()
ds := &currentInputCacheTestDS{}
Expand Down
Loading
Loading