diff --git a/API.en.md b/API.en.md index 04a26fb..ee1b73b 100644 --- a/API.en.md +++ b/API.en.md @@ -712,7 +712,7 @@ Reads runtime settings and status, including: - `compat` (`wide_input_strict_output`, `strip_reference_markers`) - `responses` / `embeddings` - `auto_delete` (`mode`: `none` / `single` / `all`; legacy `sessions=true` is still treated as `all`) -- `history_split` (`enabled` always returns `true`, `trigger_after_turns`) +- `current_input_file` (`enabled` defaults to `true`, plus `min_chars`) - `model_aliases` - `env_backed`, `needs_vercel_sync` - `toolcall` policy is fixed to `feature_match + high` and is no longer returned or editable via settings @@ -727,8 +727,9 @@ Hot-updates runtime settings. Supported fields: - `responses.store_ttl_seconds` - `embeddings.provider` - `auto_delete.mode` -- `history_split.trigger_after_turns` (`history_split.enabled` is forced on globally; legacy client writes are stored as `true`) +- `current_input_file.enabled` / `current_input_file.min_chars` - `model_aliases` +- `history_split` is retained only for legacy config compatibility and no longer affects requests - `toolcall` policy is fixed and is no longer writable through settings ### `POST /admin/settings/password` @@ -752,9 +753,9 @@ Imports full config with: The request can send config directly, or wrapped as `{"config": {...}, "mode":"merge"}`. Query params `?mode=merge` / `?mode=replace` are also supported. -`replace` mode replaces the full config shape while preserving Vercel sync metadata. `merge` mode merges `keys`, `api_keys`, `accounts`, and `model_aliases`, and overwrites non-empty fields under `admin`, `runtime`, `responses`, and `embeddings`. Manage `compat`, `auto_delete`, and `history_split` via `/admin/settings` or the config file; legacy `toolcall` fields are ignored. +`replace` mode replaces the full config shape while preserving Vercel sync metadata. `merge` mode merges `keys`, `api_keys`, `accounts`, and `model_aliases`, and overwrites non-empty fields under `admin`, `runtime`, `responses`, and `embeddings`. Manage `compat`, `auto_delete`, and `current_input_file` via `/admin/settings` or the config file; `history_split` remains only for legacy compatibility; legacy `toolcall` fields are ignored. -> Note: `merge` mode does not update `compat`, `auto_delete`, or `history_split`. +> Note: `merge` mode does not update `compat`, `auto_delete`, or `current_input_file`. ### `GET /admin/config/export` diff --git a/API.md b/API.md index a045b6c..8fb77ba 100644 --- a/API.md +++ b/API.md @@ -726,7 +726,7 @@ data: {"type":"message_stop"} - `compat`(`wide_input_strict_output`、`strip_reference_markers`) - `responses` / `embeddings` - `auto_delete`(`mode`:`none` / `single` / `all`;旧配置 `sessions=true` 仍按 `all` 处理) -- `history_split`(`enabled` 固定返回 `true`、`trigger_after_turns`) +- `current_input_file`(`enabled` 默认返回 `true`、`min_chars`) - `model_aliases` - `env_backed`、`needs_vercel_sync` - `toolcall` 策略已固定为 `feature_match + high`,不再通过 settings 返回或修改 @@ -741,8 +741,9 @@ data: {"type":"message_stop"} - `responses.store_ttl_seconds` - `embeddings.provider` - `auto_delete.mode` -- `history_split.trigger_after_turns`(`history_split.enabled` 已全局强制开启;旧客户端传入时会被保存为 `true`) +- `current_input_file.enabled` / `current_input_file.min_chars` - `model_aliases` +- `history_split` 仅作为旧配置兼容字段保留,不再影响请求处理 - `toolcall` 策略已固定,不再作为可写入字段 ### `POST /admin/settings/password` @@ -766,9 +767,9 @@ data: {"type":"message_stop"} 请求可直接传配置对象,或使用 `{"config": {...}, "mode":"merge"}` 包裹格式。 也支持在查询参数里传 `?mode=merge` / `?mode=replace`。 -`replace` 模式会按完整配置结构替换(保留 Vercel 同步元信息);`merge` 模式会合并 `keys`、`api_keys`、`accounts`、`model_aliases`,并覆盖 `admin`、`runtime`、`responses`、`embeddings` 中的非空字段。`compat`、`auto_delete`、`history_split` 建议通过 `/admin/settings` 或配置文件管理;`toolcall` 相关字段会被忽略。 +`replace` 模式会按完整配置结构替换(保留 Vercel 同步元信息);`merge` 模式会合并 `keys`、`api_keys`、`accounts`、`model_aliases`,并覆盖 `admin`、`runtime`、`responses`、`embeddings` 中的非空字段。`compat`、`auto_delete`、`current_input_file` 建议通过 `/admin/settings` 或配置文件管理;`history_split` 仅保留为旧配置兼容字段;`toolcall` 相关字段会被忽略。 -> 注意:`merge` 模式不会更新 `compat`、`auto_delete`、`history_split`。 +> 注意:`merge` 模式不会更新 `compat`、`auto_delete`、`current_input_file`。 ### `GET /admin/config/export` diff --git a/README.MD b/README.MD index fd975bf..3c7b4d9 100644 --- a/README.MD +++ b/README.MD @@ -290,9 +290,9 @@ go run ./cmd/ds2api - `model_aliases`:OpenAI / Claude / Gemini 共用的模型 alias 映射。 - `runtime`:账号并发、队列与 token 刷新策略,可通过 Admin Settings 热更新。 - `auto_delete.mode`:请求结束后的远端会话清理策略,支持 `none` / `single` / `all`。 -- `history_split`:轮次拆分策略;默认关闭,开启后默认从第二轮开始将旧历史上传为 `HISTORY.txt`。 -- `current_input_file`:独立拆分策略;默认开启且阈值为 `0`,触发时将完整上下文合并上传为隐藏上下文文件,并跳过 `HISTORY.txt`。 -- `history_split` 与 `current_input_file` 互斥,最多启用一个;两者都关闭时请求直接透传。 +- `history_split`:旧轮次拆分字段,已废弃并忽略,仅保留兼容旧配置。 +- `current_input_file`:唯一生效的独立拆分策略;默认开启且阈值为 `0`,触发时将完整上下文合并上传为隐藏上下文文件。 +- 如果关闭 `current_input_file`,请求会直接透传,不上传拆分上下文文件。 - `thinking_injection`:默认开启;在最新 user 消息末尾追加思考增强提示词,提高高强度推理与工具调用前的思考稳定性;`prompt` 留空时使用内置默认提示词。 环境变量完整列表见 [部署指南](docs/DEPLOY.md),接口鉴权规则见 [API.md](API.md#鉴权规则)。 diff --git a/README.en.md b/README.en.md index 13b6982..32390e5 100644 --- a/README.en.md +++ b/README.en.md @@ -278,7 +278,9 @@ Common fields: - `model_aliases`: one shared alias map for OpenAI / Claude / Gemini model names. - `runtime`: account concurrency, queueing, and token refresh behavior, hot-reloadable via Admin Settings. - `auto_delete.mode`: remote session cleanup after each request, supporting `none` / `single` / `all`. -- `history_split`: multi-turn history split policy, now forced on globally; tune its trigger threshold to avoid inlining all long history into the prompt. +- `history_split`: legacy multi-turn history split field, now ignored and kept only for backward-compatible config loading. +- `current_input_file`: the only active split mode; it is enabled by default and uploads the full context as a hidden context file once the character threshold is reached. +- If you turn off `current_input_file`, requests pass through directly without uploading any split context file. For the full environment variable list, see [docs/DEPLOY.en.md](docs/DEPLOY.en.md). For auth behavior, see [API.en.md](API.en.md#authentication). diff --git a/VERSION b/VERSION index ee74734..627a3f4 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.1.0 +4.1.1 diff --git a/config.example.json b/config.example.json index 14a25c5..d8e683b 100644 --- a/config.example.json +++ b/config.example.json @@ -50,10 +50,6 @@ "responses": { "store_ttl_seconds": 900 }, - "history_split": { - "enabled": false, - "trigger_after_turns": 1 - }, "current_input_file": { "enabled": true, "min_chars": 0 diff --git a/docs/ARCHITECTURE.en.md b/docs/ARCHITECTURE.en.md index 3ba24fa..df755f2 100644 --- a/docs/ARCHITECTURE.en.md +++ b/docs/ARCHITECTURE.en.md @@ -43,7 +43,7 @@ ds2api/ │ │ ├── responses/ # Responses API and response store │ │ ├── files/ # Files API and inline-file preprocessing │ │ ├── embeddings/ # Embeddings API -│ │ ├── history/ # OpenAI history split +│ │ ├── history/ # OpenAI context file handling │ │ └── shared/ # OpenAI HTTP errors/models/tool formatting │ ├── js/ # Node runtime related logic │ │ ├── chat-stream/ # Node streaming bridge diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index d5b8baf..8123ba5 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -43,7 +43,7 @@ ds2api/ │ │ ├── responses/ # Responses API 与 response store │ │ ├── files/ # Files API 与 inline file 预处理 │ │ ├── embeddings/ # Embeddings API -│ │ ├── history/ # OpenAI history split +│ │ ├── history/ # OpenAI context file handling │ │ └── shared/ # OpenAI HTTP 公共错误/模型/工具格式 │ ├── js/ # Node Runtime 相关逻辑 │ │ ├── chat-stream/ # Node 流式输出桥接 diff --git a/docs/prompt-compatibility.md b/docs/prompt-compatibility.md index c7a6ad4..c10ba8d 100644 --- a/docs/prompt-compatibility.md +++ b/docs/prompt-compatibility.md @@ -3,7 +3,7 @@ 文档导航:[总览](../README.MD) / [架构说明](./ARCHITECTURE.md) / [接口文档](../API.md) / [测试指南](./TESTING.md) > 本文档是 DS2API“把 OpenAI / Claude / Gemini 风格 API 请求兼容成 DeepSeek 网页对话纯文本上下文”的专项说明。 -> 这是项目最重要的兼容产物之一。凡是修改消息标准化、tool prompt 注入、tool history 保留、文件引用、history split、下游 completion payload 组装等行为,都必须同步更新本文档。 +> 这是项目最重要的兼容产物之一。凡是修改消息标准化、tool prompt 注入、tool history 保留、文件引用、current input file / legacy history_split、下游 completion payload 组装等行为,都必须同步更新本文档。 ## 1. 核心结论 @@ -45,7 +45,7 @@ DS2API 当前的核心思路,不是把客户端传来的 `messages`、`tools` -> promptcompat 统一消息标准化 -> tool prompt 注入 -> DeepSeek 风格 prompt 拼装 - -> 文件收集 / inline 上传 / history split(OpenAI 链路) + -> 文件收集 / inline 上传 / current input file(OpenAI 链路) -> completion payload -> 下游网页对话接口 ``` @@ -107,12 +107,11 @@ DS2API 当前的核心思路,不是把客户端传来的 `messages`、`tools` ## 5. prompt 是怎么拼出来的 -OpenAI Chat / Responses 在标准化后、history split / current input file 之前,会默认执行 `thinking_injection` 增强。它参考 DeepSeek V4 “把控制指令放在 user 消息末尾更稳定”的用法,在最新 user message 后追加思考增强提示词。当前内置默认提示词以 `Reasoning Effort: Absolute maximum with no shortcuts permitted.` 开头,并继续要求模型充分分解问题、覆盖潜在路径与边界条件、把完整推演过程显式写出。该开关默认启用,可通过 `thinking_injection.enabled=false` 关闭;也可以通过 `thinking_injection.prompt` 自定义提示词,留空时使用内置默认提示词。 +OpenAI Chat / Responses 在标准化后、current input file 之前,会默认执行 `thinking_injection` 增强。它参考 DeepSeek V4 “把控制指令放在 user 消息末尾更稳定”的用法,在最新 user message 后追加思考增强提示词。当前内置默认提示词以 `Reasoning Effort: Absolute maximum with no shortcuts permitted.` 开头,并继续要求模型充分分解问题、覆盖潜在路径与边界条件、把完整推演过程显式写出。该开关默认启用,可通过 `thinking_injection.enabled=false` 关闭;也可以通过 `thinking_injection.prompt` 自定义提示词,留空时使用内置默认提示词。 这段增强属于 prompt 可见上下文: - 普通请求会直接出现在最终 `prompt` 的最新 user block 末尾。 -- 如果触发 `HISTORY.txt`,它会保留在 live context 的最新 user turn 中。 - 如果触发 current input file,它会进入完整上下文文件中。 ### 5.1 角色标记 @@ -241,51 +240,22 @@ OpenAI 文件相关实现: ## 9. 多轮历史为什么不会一直完整内联在 prompt -兼容层提供两种拆分策略: +兼容层现在只保留 `current_input_file` 这一种拆分方式;旧的 `history_split` 已废弃,只保留为兼容旧配置的字段,不再参与请求处理。 -- `history_split` 是轮次拆分,默认关闭;开启后默认从第 2 个 user turn 起触发,可通过 `history_split.trigger_after_turns` 调整阈值。 -- `current_input_file` 是独立拆分,默认开启;它用于把“完整上下文”合并进隐藏上下文文件。当最新 user turn 的纯文本长度达到 `current_input_file.min_chars`(默认 `0`)时,兼容层会上传一个文件名为 `IGNORE.txt` 的上下文文件,并在 live prompt 中只保留一个中性的 user 消息要求模型直接回答最新请求,不再暴露文件名或要求模型读取本地文件。 - -两个策略互斥,最多只能启用一个。如果两个开关都关闭,请求会直接透传,不上传 `HISTORY.txt` 或 current input file。 +- `current_input_file` 默认开启;它用于把“完整上下文”合并进隐藏上下文文件。当最新 user turn 的纯文本长度达到 `current_input_file.min_chars`(默认 `0`)时,兼容层会上传一个文件名为 `IGNORE.txt` 的上下文文件,并在 live prompt 中只保留一个中性的 user 消息要求模型直接回答最新请求,不再暴露文件名或要求模型读取本地文件。 +- 如果 `current_input_file.enabled=false`,请求会直接透传,不上传任何拆分上下文文件。 +- 旧的 `history_split.enabled` / `history_split.trigger_after_turns` 会被读取进配置对象以保持兼容,但不会触发拆分上传,也不会影响 `current_input_file` 的默认开启。 相关实现: - 配置访问器: [internal/config/store_accessors.go](../internal/config/store_accessors.go) -- 历史拆分: - [internal/httpapi/openai/history/history_split.go](../internal/httpapi/openai/history/history_split.go) - 当前输入转文件: [internal/httpapi/openai/history/current_input_file.go](../internal/httpapi/openai/history/current_input_file.go) +- 旧历史拆分兼容壳: + [internal/httpapi/openai/history/history_split.go](../internal/httpapi/openai/history/history_split.go) -history split 触发后行为: - -1. 旧历史消息被切出去。 -2. 旧历史会被重新序列化成一个文本文件。 -3. 真正上传的文件名固定是 `HISTORY.txt`。 -4. 文件内容内部会使用 `IGNORE` 这层包装名来闭合 DeepSeek 官网原生文件标记。 -5. 该文件上传后,其 `file_id` 会排在 `ref_file_ids` 最前面。 -6. live prompt 只保留: - - system / developer - - 最新 user turn 起的上下文 - -历史文件内容不是普通自由文本,而是用同一套角色标记再次序列化出的 transcript: - -```text -[uploaded filename]: HISTORY.txt -[file content end] - -<|begin▁of▁sentence|><|User|>...<|Assistant|>...<|Tool|>... - -[file name]: IGNORE -[file content begin] -``` - -所以“完整上下文”在当前实现里,其实通常分散在两处: - -- `prompt` 里的 live context -- `ref_file_ids` 指向的 history transcript file - -当前输入转文件启用并触发时,不会同时启用 history split,也不会上传 `HISTORY.txt`。上传文件的真实文件名是 `IGNORE.txt`,文件内容是完整 `messages` 上下文;它仍会先用 OpenAI 消息标准化和 DeepSeek 角色标记序列化,再包进 `IGNORE` 文件边界里: +当前输入转文件启用并触发时,上传文件的真实文件名是 `IGNORE.txt`,文件内容是完整 `messages` 上下文;它仍会先用 OpenAI 消息标准化和 DeepSeek 角色标记序列化,再包进 `IGNORE` 文件边界里: ```text [uploaded filename]: IGNORE.txt @@ -309,7 +279,7 @@ history split 触发后行为: - Responses `instructions` 会 prepend 为 system message - `tools` 会注入 system prompt - `attachments` / `input_file` / inline 文件会进入 `ref_file_ids` -- history split 主要在这条链路里生效 +- current input file 主要在这条链路里生效,旧 `history_split` 仅作兼容字段保留 ### 10.2 Claude Messages @@ -340,15 +310,15 @@ history split 触发后行为: - 有 tools - 有一个文件型 systemprompt 附件 - 有历史 assistant tool call / tool result -- history split 已触发 +- current input file 已触发 那么最终上下文更接近: ```json { - "prompt": "<|begin▁of▁sentence|><|System|>原 system / developer\n\nYou have access to these tools: ...<|end▁of▁instructions|><|User|>最新问题<|Assistant|>", + "prompt": "<|begin▁of▁sentence|><|System|>原 system / developer\n\nYou have access to these tools: ...<|end▁of▁instructions|><|User|>The current request and prior conversation context have already been provided. Answer the latest user request directly.<|Assistant|>", "ref_file_ids": [ - "file-history-ignore", + "file-current-input-ignore", "file-systemprompt", "file-other-attachment" ], @@ -361,7 +331,7 @@ history split 触发后行为: - 大部分结构化语义被压进 `prompt` - 文件保持文件 -- 历史必要时拆文件 +- 需要时把完整上下文拆进隐藏上下文文件 ## 12. 修改时必须同步本文档的场景 @@ -374,7 +344,8 @@ history split 触发后行为: - tool result 注入方式变更 - tool prompt 模板或 tool_choice 约束变更 - inline 文件上传 / 文件引用收集规则变更 -- history split 触发条件、上传格式、`IGNORE` 包装格式变更 +- current input file 触发条件、上传格式、`IGNORE` 包装格式变更 +- 旧 `history_split` 兼容逻辑的读取、忽略或退化行为变更 - completion payload 字段语义变更 - Claude / Gemini 对这套统一语义的复用关系变更 diff --git a/internal/config/store_accessors.go b/internal/config/store_accessors.go index f5b8369..8f2e641 100644 --- a/internal/config/store_accessors.go +++ b/internal/config/store_accessors.go @@ -164,30 +164,16 @@ func (s *Store) AutoDeleteSessions() bool { } func (s *Store) HistorySplitEnabled() bool { - s.mu.RLock() - defer s.mu.RUnlock() - if s.cfg.HistorySplit.Enabled == nil { - return false - } - return *s.cfg.HistorySplit.Enabled + return false } func (s *Store) HistorySplitTriggerAfterTurns() int { - s.mu.RLock() - defer s.mu.RUnlock() - if s.cfg.HistorySplit.TriggerAfterTurns == nil || *s.cfg.HistorySplit.TriggerAfterTurns <= 0 { - return 1 - } - return *s.cfg.HistorySplit.TriggerAfterTurns + return 1 } func (s *Store) CurrentInputFileEnabled() bool { s.mu.RLock() defer s.mu.RUnlock() - historySplitEnabled := s.cfg.HistorySplit.Enabled != nil && *s.cfg.HistorySplit.Enabled - if historySplitEnabled { - return false - } if s.cfg.CurrentInputFile.Enabled == nil { return true } diff --git a/internal/config/store_accessors_test.go b/internal/config/store_accessors_test.go index 9b88e15..32ee741 100644 --- a/internal/config/store_accessors_test.go +++ b/internal/config/store_accessors_test.go @@ -3,41 +3,17 @@ package config import "testing" func TestStoreHistorySplitAccessors(t *testing.T) { - store := &Store{cfg: Config{}} - if store.HistorySplitEnabled() { - t.Fatal("expected history split disabled by default") - } - if got := store.HistorySplitTriggerAfterTurns(); got != 1 { - t.Fatalf("default history split trigger_after_turns=%d want=1", got) - } - enabled := true turns := 3 - store.cfg.HistorySplit = HistorySplitConfig{ + store := &Store{cfg: Config{HistorySplit: HistorySplitConfig{ Enabled: &enabled, TriggerAfterTurns: &turns, - } - - if !store.HistorySplitEnabled() { - t.Fatal("expected history split enabled") - } - if got := store.HistorySplitTriggerAfterTurns(); got != 3 { - t.Fatalf("history split trigger_after_turns=%d want=3", got) - } -} - -func TestStoreHistorySplitDisabledConfigStaysDisabled(t *testing.T) { - t.Setenv("DS2API_CONFIG_JSON", `{"keys":["k1"],"history_split":{"enabled":false,"trigger_after_turns":2}}`) - store := LoadStore() + }}} if store.HistorySplitEnabled() { - t.Fatal("expected history split disabled when config disables it") + t.Fatal("expected history split to stay disabled") } - snap := store.Snapshot() - if snap.HistorySplit.Enabled == nil || *snap.HistorySplit.Enabled { - t.Fatalf("expected history_split.enabled=false, got %#v", snap.HistorySplit.Enabled) - } - if got := store.HistorySplitTriggerAfterTurns(); got != 2 { - t.Fatalf("history split trigger_after_turns=%d want=2", got) + if got := store.HistorySplitTriggerAfterTurns(); got != 1 { + t.Fatalf("history split trigger_after_turns=%d want=1", got) } } @@ -67,8 +43,8 @@ func TestStoreCurrentInputFileAccessors(t *testing.T) { historyEnabled := true store.cfg.HistorySplit.Enabled = &historyEnabled - if store.CurrentInputFileEnabled() { - t.Fatal("expected history split to suppress current input file mode") + if !store.CurrentInputFileEnabled() { + t.Fatal("expected history split config to not suppress current input file mode") } } diff --git a/internal/config/validation.go b/internal/config/validation.go index d7bcb28..0ae41d3 100644 --- a/internal/config/validation.go +++ b/internal/config/validation.go @@ -24,15 +24,9 @@ func ValidateConfig(c Config) error { if err := ValidateAutoDeleteConfig(c.AutoDelete); err != nil { return err } - if err := ValidateHistorySplitConfig(c.HistorySplit); err != nil { - return err - } if err := ValidateCurrentInputFileConfig(c.CurrentInputFile); err != nil { return err } - if c.HistorySplit.Enabled != nil && *c.HistorySplit.Enabled && c.CurrentInputFile.Enabled != nil && *c.CurrentInputFile.Enabled { - return fmt.Errorf("history_split.enabled and current_input_file.enabled cannot both be true") - } if err := ValidateAccountProxyReferences(c.Accounts, c.Proxies); err != nil { return err } @@ -120,15 +114,6 @@ func ValidateAutoDeleteConfig(autoDelete AutoDeleteConfig) error { return ValidateAutoDeleteMode(autoDelete.Mode) } -func ValidateHistorySplitConfig(historySplit HistorySplitConfig) error { - if historySplit.TriggerAfterTurns != nil { - if err := ValidateIntRange("history_split.trigger_after_turns", *historySplit.TriggerAfterTurns, 1, 1000, true); err != nil { - return err - } - } - return nil -} - func ValidateCurrentInputFileConfig(currentInputFile CurrentInputFileConfig) error { if currentInputFile.MinChars != 0 { return ValidateIntRange("current_input_file.min_chars", currentInputFile.MinChars, 1, 100000000, true) diff --git a/internal/config/validation_test.go b/internal/config/validation_test.go index 67b80a1..46546b0 100644 --- a/internal/config/validation_test.go +++ b/internal/config/validation_test.go @@ -39,26 +39,11 @@ func TestValidateConfigRejectsInvalidValues(t *testing.T) { cfg: Config{AutoDelete: AutoDeleteConfig{Mode: "maybe"}}, want: "auto_delete.mode", }, - { - name: "history split", - cfg: Config{HistorySplit: HistorySplitConfig{ - TriggerAfterTurns: intPtr(0), - }}, - want: "history_split.trigger_after_turns", - }, { name: "current input file", cfg: Config{CurrentInputFile: CurrentInputFileConfig{MinChars: -1}}, want: "current_input_file.min_chars", }, - { - name: "split modes mutually exclusive", - cfg: Config{ - HistorySplit: HistorySplitConfig{Enabled: boolPtr(true)}, - CurrentInputFile: CurrentInputFileConfig{Enabled: boolPtr(true)}, - }, - want: "cannot both be true", - }, } for _, tc := range tests { @@ -79,7 +64,3 @@ func TestValidateConfigAcceptsLegacyAutoDeleteSessions(t *testing.T) { t.Fatalf("expected legacy auto_delete.sessions config to remain valid, got %v", err) } } - -func intPtr(v int) *int { return &v } - -func boolPtr(v bool) *bool { return &v } diff --git a/internal/httpapi/admin/handler_settings_test.go b/internal/httpapi/admin/handler_settings_test.go index fba6bd1..a06c2ff 100644 --- a/internal/httpapi/admin/handler_settings_test.go +++ b/internal/httpapi/admin/handler_settings_test.go @@ -47,7 +47,7 @@ func TestGetSettingsIncludesTokenRefreshInterval(t *testing.T) { } } -func TestGetSettingsIncludesHistorySplitDefaults(t *testing.T) { +func TestGetSettingsIncludesCurrentInputFileDefaults(t *testing.T) { h := newAdminTestHandler(t, `{"keys":["k1"]}`) req := httptest.NewRequest(http.MethodGet, "/admin/settings", nil) rec := httptest.NewRecorder() @@ -57,13 +57,6 @@ func TestGetSettingsIncludesHistorySplitDefaults(t *testing.T) { } var body map[string]any _ = json.Unmarshal(rec.Body.Bytes(), &body) - historySplit, _ := body["history_split"].(map[string]any) - if got := boolFrom(historySplit["enabled"]); got { - t.Fatalf("expected history_split.enabled=false, body=%v", body) - } - if got := intFrom(historySplit["trigger_after_turns"]); got != 1 { - t.Fatalf("expected history_split.trigger_after_turns=1, got %d body=%v", got, body) - } currentInputFile, _ := body["current_input_file"].(map[string]any) if got := boolFrom(currentInputFile["enabled"]); !got { t.Fatalf("expected current_input_file.enabled=true, body=%v", body) @@ -190,33 +183,6 @@ func TestUpdateSettingsWithoutRuntimeSkipsMergedRuntimeValidation(t *testing.T) } } -func TestUpdateSettingsHistorySplit(t *testing.T) { - h := newAdminTestHandler(t, `{"keys":["k1"]}`) - payload := map[string]any{ - "history_split": map[string]any{ - "enabled": true, - "trigger_after_turns": 3, - }, - } - b, _ := json.Marshal(payload) - req := httptest.NewRequest(http.MethodPut, "/admin/settings", bytes.NewReader(b)) - rec := httptest.NewRecorder() - h.updateSettings(rec, req) - if rec.Code != http.StatusOK { - t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String()) - } - snap := h.Store.Snapshot() - if snap.HistorySplit.Enabled == nil || !*snap.HistorySplit.Enabled { - t.Fatalf("expected history_split.enabled=true, got %#v", snap.HistorySplit.Enabled) - } - if snap.HistorySplit.TriggerAfterTurns == nil || *snap.HistorySplit.TriggerAfterTurns != 3 { - t.Fatalf("expected history_split.trigger_after_turns=3, got %#v", snap.HistorySplit.TriggerAfterTurns) - } - if snap.CurrentInputFile.Enabled == nil || *snap.CurrentInputFile.Enabled { - t.Fatalf("expected history split to disable current_input_file, got %#v", snap.CurrentInputFile.Enabled) - } -} - func TestUpdateSettingsCurrentInputFile(t *testing.T) { h := newAdminTestHandler(t, `{"keys":["k1"],"history_split":{"enabled":true,"trigger_after_turns":2}}`) payload := map[string]any{ @@ -239,8 +205,11 @@ 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.HistorySplit.Enabled == nil || *snap.HistorySplit.Enabled { - t.Fatalf("expected current input file to disable history_split, got %#v", snap.HistorySplit.Enabled) + if !h.Store.CurrentInputFileEnabled() { + t.Fatal("expected current input file accessor to stay enabled") + } + if h.Store.HistorySplitEnabled() { + t.Fatal("expected history split accessor to stay disabled") } } @@ -290,7 +259,7 @@ func TestUpdateSettingsCurrentInputFilePartialUpdatePreservesMinChars(t *testing } } -func TestUpdateSettingsRejectsTwoSplitModesEnabled(t *testing.T) { +func TestUpdateSettingsIgnoresHistorySplitPayload(t *testing.T) { h := newAdminTestHandler(t, `{"keys":["k1"]}`) payload := map[string]any{ "history_split": map[string]any{ @@ -306,8 +275,12 @@ func TestUpdateSettingsRejectsTwoSplitModesEnabled(t *testing.T) { req := httptest.NewRequest(http.MethodPut, "/admin/settings", bytes.NewReader(b)) rec := httptest.NewRecorder() h.updateSettings(rec, req) - if rec.Code != http.StatusBadRequest { - t.Fatalf("expected 400, got %d body=%s", rec.Code, rec.Body.String()) + if rec.Code != http.StatusOK { + t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String()) + } + snap := h.Store.Snapshot() + if snap.CurrentInputFile.Enabled == nil || !*snap.CurrentInputFile.Enabled { + t.Fatalf("expected current_input_file to remain enabled, got %#v", snap.CurrentInputFile.Enabled) } } diff --git a/internal/httpapi/admin/settings/handler_settings_parse.go b/internal/httpapi/admin/settings/handler_settings_parse.go index bd26c7f..2617503 100644 --- a/internal/httpapi/admin/settings/handler_settings_parse.go +++ b/internal/httpapi/admin/settings/handler_settings_parse.go @@ -21,7 +21,7 @@ func boolFrom(v any) bool { } } -func parseSettingsUpdateRequest(req map[string]any) (*config.AdminConfig, *config.RuntimeConfig, *config.CompatConfig, *config.ResponsesConfig, *config.EmbeddingsConfig, *config.AutoDeleteConfig, *config.HistorySplitConfig, *config.CurrentInputFileConfig, *config.ThinkingInjectionConfig, map[string]string, error) { +func parseSettingsUpdateRequest(req map[string]any) (*config.AdminConfig, *config.RuntimeConfig, *config.CompatConfig, *config.ResponsesConfig, *config.EmbeddingsConfig, *config.AutoDeleteConfig, *config.CurrentInputFileConfig, *config.ThinkingInjectionConfig, map[string]string, error) { var ( adminCfg *config.AdminConfig runtimeCfg *config.RuntimeConfig @@ -29,7 +29,6 @@ func parseSettingsUpdateRequest(req map[string]any) (*config.AdminConfig, *confi respCfg *config.ResponsesConfig embCfg *config.EmbeddingsConfig autoDeleteCfg *config.AutoDeleteConfig - historySplitCfg *config.HistorySplitConfig currentInputCfg *config.CurrentInputFileConfig thinkingInjCfg *config.ThinkingInjectionConfig aliasMap map[string]string @@ -40,7 +39,7 @@ func parseSettingsUpdateRequest(req map[string]any) (*config.AdminConfig, *confi if v, exists := raw["jwt_expire_hours"]; exists { n := intFrom(v) if err := config.ValidateIntRange("admin.jwt_expire_hours", n, 1, 720, true); err != nil { - return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, nil, nil, nil, nil, err } cfg.JWTExpireHours = n } @@ -52,33 +51,33 @@ func parseSettingsUpdateRequest(req map[string]any) (*config.AdminConfig, *confi if v, exists := raw["account_max_inflight"]; exists { n := intFrom(v) if err := config.ValidateIntRange("runtime.account_max_inflight", n, 1, 256, true); err != nil { - return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, nil, nil, nil, nil, err } cfg.AccountMaxInflight = n } if v, exists := raw["account_max_queue"]; exists { n := intFrom(v) if err := config.ValidateIntRange("runtime.account_max_queue", n, 1, 200000, true); err != nil { - return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, nil, nil, nil, nil, err } cfg.AccountMaxQueue = n } if v, exists := raw["global_max_inflight"]; exists { n := intFrom(v) if err := config.ValidateIntRange("runtime.global_max_inflight", n, 1, 200000, true); err != nil { - return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, nil, nil, nil, nil, err } cfg.GlobalMaxInflight = n } if v, exists := raw["token_refresh_interval_hours"]; exists { n := intFrom(v) if err := config.ValidateIntRange("runtime.token_refresh_interval_hours", n, 1, 720, true); err != nil { - return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, nil, nil, nil, nil, err } cfg.TokenRefreshIntervalHours = n } if cfg.AccountMaxInflight > 0 && cfg.GlobalMaxInflight > 0 && cfg.GlobalMaxInflight < cfg.AccountMaxInflight { - return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, fmt.Errorf("runtime.global_max_inflight must be >= runtime.account_max_inflight") + return nil, nil, nil, nil, nil, nil, nil, nil, nil, fmt.Errorf("runtime.global_max_inflight must be >= runtime.account_max_inflight") } runtimeCfg = cfg } @@ -101,7 +100,7 @@ func parseSettingsUpdateRequest(req map[string]any) (*config.AdminConfig, *confi if v, exists := raw["store_ttl_seconds"]; exists { n := intFrom(v) if err := config.ValidateIntRange("responses.store_ttl_seconds", n, 30, 86400, true); err != nil { - return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, nil, nil, nil, nil, err } cfg.StoreTTLSeconds = n } @@ -113,7 +112,7 @@ func parseSettingsUpdateRequest(req map[string]any) (*config.AdminConfig, *confi if v, exists := raw["provider"]; exists { p := strings.TrimSpace(fmt.Sprintf("%v", v)) if err := config.ValidateTrimmedString("embeddings.provider", p, false); err != nil { - return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, nil, nil, nil, nil, err } cfg.Provider = p } @@ -139,7 +138,7 @@ func parseSettingsUpdateRequest(req map[string]any) (*config.AdminConfig, *confi if v, exists := raw["mode"]; exists { mode := strings.ToLower(strings.TrimSpace(fmt.Sprintf("%v", v))) if err := config.ValidateAutoDeleteMode(mode); err != nil { - return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, nil, nil, nil, nil, err } if mode == "" { mode = "none" @@ -152,25 +151,6 @@ func parseSettingsUpdateRequest(req map[string]any) (*config.AdminConfig, *confi autoDeleteCfg = cfg } - if raw, ok := req["history_split"].(map[string]any); ok { - cfg := &config.HistorySplitConfig{} - if v, exists := raw["enabled"]; exists { - enabled := boolFrom(v) - cfg.Enabled = &enabled - } - if v, exists := raw["trigger_after_turns"]; exists { - n := intFrom(v) - if err := config.ValidateIntRange("history_split.trigger_after_turns", n, 1, 1000, true); err != nil { - return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, err - } - cfg.TriggerAfterTurns = &n - } - if err := config.ValidateHistorySplitConfig(*cfg); err != nil { - return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, err - } - historySplitCfg = cfg - } - if raw, ok := req["current_input_file"].(map[string]any); ok { cfg := &config.CurrentInputFileConfig{} if v, exists := raw["enabled"]; exists { @@ -180,18 +160,15 @@ func parseSettingsUpdateRequest(req map[string]any) (*config.AdminConfig, *confi if v, exists := raw["min_chars"]; exists { n := intFrom(v) if err := config.ValidateIntRange("current_input_file.min_chars", n, 0, 100000000, true); err != nil { - return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, nil, nil, nil, nil, err } cfg.MinChars = n } if err := config.ValidateCurrentInputFileConfig(*cfg); err != nil { - return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, nil, nil, nil, nil, err } currentInputCfg = cfg } - if boolPtrValue(historySplitCfgEnabled(historySplitCfg)) && boolPtrValue(currentInputCfgEnabled(currentInputCfg)) { - return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, fmt.Errorf("history_split.enabled and current_input_file.enabled cannot both be true") - } if raw, ok := req["thinking_injection"].(map[string]any); ok { cfg := &config.ThinkingInjectionConfig{} @@ -205,23 +182,5 @@ func parseSettingsUpdateRequest(req map[string]any) (*config.AdminConfig, *confi thinkingInjCfg = cfg } - return adminCfg, runtimeCfg, compatCfg, respCfg, embCfg, autoDeleteCfg, historySplitCfg, currentInputCfg, thinkingInjCfg, aliasMap, nil -} - -func historySplitCfgEnabled(cfg *config.HistorySplitConfig) *bool { - if cfg == nil { - return nil - } - return cfg.Enabled -} - -func currentInputCfgEnabled(cfg *config.CurrentInputFileConfig) *bool { - if cfg == nil { - return nil - } - return cfg.Enabled -} - -func boolPtrValue(v *bool) bool { - return v != nil && *v + return adminCfg, runtimeCfg, compatCfg, respCfg, embCfg, autoDeleteCfg, currentInputCfg, thinkingInjCfg, aliasMap, nil } diff --git a/internal/httpapi/admin/settings/handler_settings_read.go b/internal/httpapi/admin/settings/handler_settings_read.go index 6944d3d..1997b01 100644 --- a/internal/httpapi/admin/settings/handler_settings_read.go +++ b/internal/httpapi/admin/settings/handler_settings_read.go @@ -31,10 +31,6 @@ func (h *Handler) getSettings(w http.ResponseWriter, _ *http.Request) { "responses": snap.Responses, "embeddings": snap.Embeddings, "auto_delete": snap.AutoDelete, - "history_split": map[string]any{ - "enabled": h.Store.HistorySplitEnabled(), - "trigger_after_turns": h.Store.HistorySplitTriggerAfterTurns(), - }, "current_input_file": map[string]any{ "enabled": h.Store.CurrentInputFileEnabled(), "min_chars": h.Store.CurrentInputFileMinChars(), diff --git a/internal/httpapi/admin/settings/handler_settings_write.go b/internal/httpapi/admin/settings/handler_settings_write.go index 1958d5f..bb740f5 100644 --- a/internal/httpapi/admin/settings/handler_settings_write.go +++ b/internal/httpapi/admin/settings/handler_settings_write.go @@ -17,7 +17,7 @@ func (h *Handler) updateSettings(w http.ResponseWriter, r *http.Request) { return } - adminCfg, runtimeCfg, compatCfg, responsesCfg, embeddingsCfg, autoDeleteCfg, historySplitCfg, currentInputCfg, thinkingInjCfg, aliasMap, err := parseSettingsUpdateRequest(req) + adminCfg, runtimeCfg, compatCfg, responsesCfg, embeddingsCfg, autoDeleteCfg, currentInputCfg, thinkingInjCfg, aliasMap, err := parseSettingsUpdateRequest(req) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]any{"detail": err.Error()}) return @@ -71,26 +71,10 @@ func (h *Handler) updateSettings(w http.ResponseWriter, r *http.Request) { c.AutoDelete.Mode = autoDeleteCfg.Mode c.AutoDelete.Sessions = autoDeleteCfg.Sessions } - if historySplitCfg != nil { - if historySplitCfg.Enabled != nil { - c.HistorySplit.Enabled = historySplitCfg.Enabled - if *historySplitCfg.Enabled { - disabled := false - c.CurrentInputFile.Enabled = &disabled - } - } - if historySplitCfg.TriggerAfterTurns != nil { - c.HistorySplit.TriggerAfterTurns = historySplitCfg.TriggerAfterTurns - } - } if currentInputCfg != nil { if currentInputEnabledSet { c.CurrentInputFile.Enabled = currentInputCfg.Enabled } - if currentInputEnabledSet && currentInputCfg.Enabled != nil && *currentInputCfg.Enabled { - disabled := false - c.HistorySplit.Enabled = &disabled - } if currentInputMinCharsSet { c.CurrentInputFile.MinChars = currentInputCfg.MinChars } diff --git a/internal/httpapi/openai/chat/chat_history_test.go b/internal/httpapi/openai/chat/chat_history_test.go index 66dfc59..6d2479a 100644 --- a/internal/httpapi/openai/chat/chat_history_test.go +++ b/internal/httpapi/openai/chat/chat_history_test.go @@ -272,14 +272,13 @@ func TestChatCompletionsSkipsHistoryWhenDisabled(t *testing.T) { } } -func TestChatCompletionsHistorySplitPersistsHistoryText(t *testing.T) { +func TestChatCompletionsCurrentInputFilePersistsNeutralPrompt(t *testing.T) { historyStore := newTestChatHistoryStore(t) ds := &inlineUploadDSStub{} h := &Handler{ Store: mockOpenAIConfig{ wideInput: true, - historySplitEnabled: true, - historySplitTurns: 1, + currentInputEnabled: true, }, Auth: streamStatusAuthStub{}, DS: ds, @@ -308,19 +307,19 @@ func TestChatCompletionsHistorySplitPersistsHistoryText(t *testing.T) { if err != nil { t.Fatalf("expected detail item, got %v", err) } - if full.HistoryText == "" { - t.Fatalf("expected history text to be persisted") - } - if !strings.Contains(full.HistoryText, "first user turn") || !strings.Contains(full.HistoryText, "tool result") { - t.Fatalf("expected earlier turns in history text, got %q", full.HistoryText) - } - if strings.Contains(full.HistoryText, "latest user turn") { - t.Fatalf("expected latest turn to stay out of persisted history text, got %q", full.HistoryText) + if full.HistoryText != "" { + t.Fatalf("expected current input file flow to leave history text empty, got %q", full.HistoryText) } if len(ds.uploadCalls) != 1 { - t.Fatalf("expected history upload to happen, got %d", len(ds.uploadCalls)) + t.Fatalf("expected current input upload to happen, got %d", len(ds.uploadCalls)) } - if full.HistoryText != string(ds.uploadCalls[0].Data) { - t.Fatalf("expected persisted history text to match uploaded HISTORY.txt contents") + if ds.uploadCalls[0].Filename != "IGNORE.txt" { + t.Fatalf("expected IGNORE.txt upload, got %q", ds.uploadCalls[0].Filename) + } + if len(full.Messages) != 1 { + t.Fatalf("expected neutral prompt to be the only persisted message, got %#v", full.Messages) + } + if !strings.Contains(full.Messages[0].Content, "Answer the latest user request directly.") { + t.Fatalf("expected neutral prompt to be persisted, got %#v", full.Messages[0]) } } diff --git a/internal/httpapi/openai/chat/handler.go b/internal/httpapi/openai/chat/handler.go index da6c2ab..0d5eeff 100644 --- a/internal/httpapi/openai/chat/handler.go +++ b/internal/httpapi/openai/chat/handler.go @@ -42,20 +42,17 @@ func (h *Handler) compatStripReferenceMarkers() bool { return shared.CompatStripReferenceMarkers(h.Store) } -func (h *Handler) applyHistorySplit(ctx context.Context, a *auth.RequestAuth, stdReq promptcompat.StandardRequest) (promptcompat.StandardRequest, error) { +func (h *Handler) applyCurrentInputFile(ctx context.Context, a *auth.RequestAuth, stdReq promptcompat.StandardRequest) (promptcompat.StandardRequest, error) { if h == nil { return stdReq, nil } stdReq = shared.ApplyThinkingInjection(h.Store, stdReq) svc := history.Service{Store: h.Store, DS: h.DS} out, err := svc.ApplyCurrentInputFile(ctx, a, stdReq) - if err != nil { - return stdReq, err + if err != nil || out.CurrentInputFileApplied { + return out, err } - if out.CurrentInputFileApplied { - return out, nil - } - return svc.Apply(ctx, a, out) + return out, nil } func (h *Handler) preprocessInlineFileInputs(ctx context.Context, a *auth.RequestAuth, req map[string]any) error { @@ -91,7 +88,7 @@ func writeOpenAIInlineFileError(w http.ResponseWriter, err error) { files.WriteInlineFileError(w, err) } -func mapHistorySplitError(err error) (int, string) { +func mapCurrentInputFileError(err error) (int, string) { return history.MapError(err) } diff --git a/internal/httpapi/openai/chat/handler_chat.go b/internal/httpapi/openai/chat/handler_chat.go index 4ee77dc..bc17790 100644 --- a/internal/httpapi/openai/chat/handler_chat.go +++ b/internal/httpapi/openai/chat/handler_chat.go @@ -68,9 +68,9 @@ func (h *Handler) ChatCompletions(w http.ResponseWriter, r *http.Request) { writeOpenAIError(w, http.StatusBadRequest, err.Error()) return } - stdReq, err = h.applyHistorySplit(r.Context(), a, stdReq) + stdReq, err = h.applyCurrentInputFile(r.Context(), a, stdReq) if err != nil { - status, message := mapHistorySplitError(err) + status, message := mapCurrentInputFileError(err) writeOpenAIError(w, status, message) return } diff --git a/internal/httpapi/openai/chat/vercel_prepare_test.go b/internal/httpapi/openai/chat/vercel_prepare_test.go index 8cd948f..59e62d9 100644 --- a/internal/httpapi/openai/chat/vercel_prepare_test.go +++ b/internal/httpapi/openai/chat/vercel_prepare_test.go @@ -87,7 +87,7 @@ func TestStreamLeaseTTL(t *testing.T) { } } -func TestHandleVercelStreamPrepareAppliesHistorySplit(t *testing.T) { +func TestHandleVercelStreamPrepareAppliesCurrentInputFile(t *testing.T) { t.Setenv("VERCEL", "1") t.Setenv("DS2API_VERCEL_INTERNAL_SECRET", "stream-secret") @@ -95,8 +95,7 @@ func TestHandleVercelStreamPrepareAppliesHistorySplit(t *testing.T) { h := &Handler{ Store: mockOpenAIConfig{ wideInput: true, - historySplitEnabled: true, - historySplitTurns: 1, + currentInputEnabled: true, }, Auth: streamStatusAuthStub{}, DS: ds, @@ -119,7 +118,7 @@ func TestHandleVercelStreamPrepareAppliesHistorySplit(t *testing.T) { t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String()) } if len(ds.uploadCalls) != 1 { - t.Fatalf("expected 1 history upload, got %d", len(ds.uploadCalls)) + t.Fatalf("expected 1 current input upload, got %d", len(ds.uploadCalls)) } var body map[string]any @@ -131,11 +130,11 @@ func TestHandleVercelStreamPrepareAppliesHistorySplit(t *testing.T) { t.Fatalf("expected payload object, got %#v", body["payload"]) } promptText, _ := payload["prompt"].(string) - if !strings.Contains(promptText, "latest user turn") { - t.Fatalf("expected latest user turn in prompt, got %s", promptText) + if !strings.Contains(promptText, "Answer the latest user request directly.") { + t.Fatalf("expected neutral prompt, got %s", promptText) } - if strings.Contains(promptText, "first user turn") { - t.Fatalf("expected historical turns removed from prompt, got %s", promptText) + if strings.Contains(promptText, "first user turn") || strings.Contains(promptText, "latest user turn") { + t.Fatalf("expected original turns hidden from prompt, got %s", promptText) } refIDs, _ := payload["ref_file_ids"].([]any) if len(refIDs) == 0 || refIDs[0] != "file-inline-1" { @@ -143,7 +142,7 @@ func TestHandleVercelStreamPrepareAppliesHistorySplit(t *testing.T) { } } -func TestHandleVercelStreamPrepareMapsHistorySplitManagedAuthFailureTo401(t *testing.T) { +func TestHandleVercelStreamPrepareMapsCurrentInputFileManagedAuthFailureTo401(t *testing.T) { t.Setenv("VERCEL", "1") t.Setenv("DS2API_VERCEL_INTERNAL_SECRET", "stream-secret") @@ -153,8 +152,7 @@ func TestHandleVercelStreamPrepareMapsHistorySplitManagedAuthFailureTo401(t *tes h := &Handler{ Store: mockOpenAIConfig{ wideInput: true, - historySplitEnabled: true, - historySplitTurns: 1, + currentInputEnabled: true, }, Auth: streamStatusManagedAuthStub{}, DS: ds, diff --git a/internal/httpapi/openai/chat/vercel_stream.go b/internal/httpapi/openai/chat/vercel_stream.go index 2a59410..cf74f5f 100644 --- a/internal/httpapi/openai/chat/vercel_stream.go +++ b/internal/httpapi/openai/chat/vercel_stream.go @@ -69,9 +69,9 @@ func (h *Handler) handleVercelStreamPrepare(w http.ResponseWriter, r *http.Reque writeOpenAIError(w, http.StatusBadRequest, "stream must be true") return } - stdReq, err = h.applyHistorySplit(r.Context(), a, stdReq) + stdReq, err = h.applyCurrentInputFile(r.Context(), a, stdReq) if err != nil { - status, message := mapHistorySplitError(err) + status, message := mapCurrentInputFileError(err) writeOpenAIError(w, status, message) return } diff --git a/internal/httpapi/openai/history/current_input_file.go b/internal/httpapi/openai/history/current_input_file.go index d0cf990..981a5ee 100644 --- a/internal/httpapi/openai/history/current_input_file.go +++ b/internal/httpapi/openai/history/current_input_file.go @@ -28,8 +28,7 @@ func (s Service) ApplyCurrentInputFile(ctx context.Context, a *auth.RequestAuth, if index < 0 { return stdReq, nil } - historySplitReached := s.Store.HistorySplitEnabled() && wouldSplitHistory(stdReq.Messages, s.Store.HistorySplitTriggerAfterTurns()) - if len([]rune(text)) < threshold && !historySplitReached { + if len([]rune(text)) < threshold { return stdReq, nil } fileText := promptcompat.BuildOpenAICurrentInputContextTranscript(stdReq.Messages) @@ -84,11 +83,6 @@ func latestUserInputForFile(messages []any) (int, string) { return -1, "" } -func wouldSplitHistory(messages []any, triggerAfterTurns int) bool { - _, historyMessages := SplitOpenAIHistoryMessages(messages, triggerAfterTurns) - return len(historyMessages) > 0 -} - func currentInputFilePrompt() string { return "The current request and prior conversation context have already been provided. Answer the latest user request directly." } diff --git a/internal/httpapi/openai/history/history_split.go b/internal/httpapi/openai/history/history_split.go index de7bf51..9282147 100644 --- a/internal/httpapi/openai/history/history_split.go +++ b/internal/httpapi/openai/history/history_split.go @@ -2,60 +2,21 @@ package history import ( "context" - "errors" - "fmt" "strings" "ds2api/internal/auth" - dsclient "ds2api/internal/deepseek/client" "ds2api/internal/httpapi/openai/shared" "ds2api/internal/promptcompat" ) -const ( - historySplitFilename = "HISTORY.txt" - historySplitContentType = "text/plain; charset=utf-8" - historySplitPurpose = "assistants" -) - type Service struct { Store shared.ConfigReader DS shared.DeepSeekCaller } +// Apply is retained for legacy compatibility only. The active split path is +// current input file handling in ApplyCurrentInputFile. func (s Service) Apply(ctx context.Context, a *auth.RequestAuth, stdReq promptcompat.StandardRequest) (promptcompat.StandardRequest, error) { - if s.DS == nil || s.Store == nil || a == nil || !s.Store.HistorySplitEnabled() { - return stdReq, nil - } - - promptMessages, historyMessages := SplitOpenAIHistoryMessages(stdReq.Messages, s.Store.HistorySplitTriggerAfterTurns()) - if len(historyMessages) == 0 { - return stdReq, nil - } - - historyText := promptcompat.BuildOpenAIHistoryTranscript(historyMessages) - if strings.TrimSpace(historyText) == "" { - return stdReq, errors.New("history split produced empty transcript") - } - - result, err := s.DS.UploadFile(ctx, a, dsclient.UploadFileRequest{ - Filename: historySplitFilename, - ContentType: historySplitContentType, - Purpose: historySplitPurpose, - Data: []byte(historyText), - }, 3) - if err != nil { - return stdReq, fmt.Errorf("upload history file: %w", err) - } - fileID := strings.TrimSpace(result.ID) - if fileID == "" { - return stdReq, errors.New("upload history file returned empty file id") - } - - stdReq.Messages = promptMessages - stdReq.HistoryText = historyText - stdReq.RefFileIDs = prependUniqueRefFileID(stdReq.RefFileIDs, fileID) - stdReq.FinalPrompt, stdReq.ToolNames = promptcompat.BuildOpenAIPrompt(promptMessages, stdReq.ToolsRaw, "", stdReq.ToolChoice, stdReq.Thinking) return stdReq, nil } diff --git a/internal/httpapi/openai/history_split_test.go b/internal/httpapi/openai/history_split_test.go index 2fa6080..aa76575 100644 --- a/internal/httpapi/openai/history_split_test.go +++ b/internal/httpapi/openai/history_split_test.go @@ -60,9 +60,9 @@ func (streamStatusManagedAuthStub) DetermineCaller(_ *http.Request) (*auth.Reque func (streamStatusManagedAuthStub) Release(_ *auth.RequestAuth) {} -func TestBuildOpenAIHistoryTranscriptUsesInjectedFileWrapper(t *testing.T) { +func TestBuildOpenAICurrentInputContextTranscriptUsesInjectedFileWrapper(t *testing.T) { _, historyMessages := splitOpenAIHistoryMessages(historySplitTestMessages(), 1) - transcript := buildOpenAIHistoryTranscript(historyMessages) + transcript := buildOpenAICurrentInputContextTranscript(historyMessages) if !strings.HasPrefix(transcript, "[file content end]\n\n") { t.Fatalf("expected injected file wrapper prefix, got %q", transcript) @@ -107,7 +107,7 @@ func TestSplitOpenAIHistoryMessagesUsesLatestUserTurn(t *testing.T) { t.Fatalf("expected middle user turn to be moved into history, got %s", promptText) } - historyText := buildOpenAIHistoryTranscript(historyMessages) + historyText := buildOpenAICurrentInputContextTranscript(historyMessages) if !strings.Contains(historyText, "middle user turn") { t.Fatalf("expected middle user turn in split history, got %s", historyText) } @@ -116,13 +116,13 @@ func TestSplitOpenAIHistoryMessagesUsesLatestUserTurn(t *testing.T) { } } -func TestApplyHistorySplitSkipsFirstTurn(t *testing.T) { +func TestApplyCurrentInputFileSkipsShortInputWhenThresholdNotReached(t *testing.T) { ds := &inlineUploadDSStub{} h := &openAITestSurface{ Store: mockOpenAIConfig{ wideInput: true, - historySplitEnabled: true, - historySplitTurns: 1, + currentInputEnabled: true, + currentInputMin: 10, }, DS: ds, } @@ -137,9 +137,9 @@ func TestApplyHistorySplitSkipsFirstTurn(t *testing.T) { t.Fatalf("normalize failed: %v", err) } - out, err := h.applyHistorySplit(context.Background(), &auth.RequestAuth{DeepSeekToken: "token"}, stdReq) + out, err := h.applyCurrentInputFile(context.Background(), &auth.RequestAuth{DeepSeekToken: "token"}, stdReq) if err != nil { - t.Fatalf("apply history split failed: %v", err) + t.Fatalf("apply current input file failed: %v", err) } if len(ds.uploadCalls) != 0 { t.Fatalf("expected no upload on first turn, got %d", len(ds.uploadCalls)) @@ -153,10 +153,8 @@ func TestApplyThinkingInjectionAppendsLatestUserPrompt(t *testing.T) { ds := &inlineUploadDSStub{} h := &openAITestSurface{ Store: mockOpenAIConfig{ - wideInput: true, - historySplitEnabled: true, - historySplitTurns: 1, - thinkingInjection: boolPtr(true), + wideInput: true, + thinkingInjection: boolPtr(true), }, DS: ds, } @@ -171,7 +169,7 @@ func TestApplyThinkingInjectionAppendsLatestUserPrompt(t *testing.T) { t.Fatalf("normalize failed: %v", err) } - out, err := h.applyHistorySplit(context.Background(), &auth.RequestAuth{DeepSeekToken: "token"}, stdReq) + out, err := h.applyCurrentInputFile(context.Background(), &auth.RequestAuth{DeepSeekToken: "token"}, stdReq) if err != nil { t.Fatalf("apply thinking injection failed: %v", err) } @@ -204,7 +202,7 @@ func TestApplyThinkingInjectionUsesCustomPrompt(t *testing.T) { t.Fatalf("normalize failed: %v", err) } - out, err := h.applyHistorySplit(context.Background(), &auth.RequestAuth{DeepSeekToken: "token"}, stdReq) + out, err := h.applyCurrentInputFile(context.Background(), &auth.RequestAuth{DeepSeekToken: "token"}, stdReq) if err != nil { t.Fatalf("apply thinking injection failed: %v", err) } @@ -213,12 +211,11 @@ func TestApplyThinkingInjectionUsesCustomPrompt(t *testing.T) { } } -func TestApplyHistorySplitDirectPassThroughWhenBothSplitsDisabled(t *testing.T) { +func TestApplyCurrentInputFileDisabledPassThrough(t *testing.T) { ds := &inlineUploadDSStub{} h := &openAITestSurface{ Store: mockOpenAIConfig{ wideInput: true, - historySplitEnabled: false, currentInputEnabled: false, }, DS: ds, @@ -232,9 +229,9 @@ func TestApplyHistorySplitDirectPassThroughWhenBothSplitsDisabled(t *testing.T) t.Fatalf("normalize failed: %v", err) } - out, err := h.applyHistorySplit(context.Background(), &auth.RequestAuth{DeepSeekToken: "token"}, stdReq) + out, err := h.applyCurrentInputFile(context.Background(), &auth.RequestAuth{DeepSeekToken: "token"}, stdReq) if err != nil { - t.Fatalf("apply history split failed: %v", err) + t.Fatalf("apply current input file failed: %v", err) } if len(ds.uploadCalls) != 0 { t.Fatalf("expected no uploads when both split modes are disabled, got %d", len(ds.uploadCalls)) @@ -252,8 +249,6 @@ func TestApplyCurrentInputFileUploadsFirstTurnWithInjectedWrapper(t *testing.T) h := &openAITestSurface{ Store: mockOpenAIConfig{ wideInput: true, - historySplitEnabled: true, - historySplitTurns: 1, currentInputEnabled: true, currentInputMin: 10, thinkingInjection: boolPtr(true), @@ -271,7 +266,7 @@ func TestApplyCurrentInputFileUploadsFirstTurnWithInjectedWrapper(t *testing.T) t.Fatalf("normalize failed: %v", err) } - out, err := h.applyHistorySplit(context.Background(), &auth.RequestAuth{DeepSeekToken: "token"}, stdReq) + out, err := h.applyCurrentInputFile(context.Background(), &auth.RequestAuth{DeepSeekToken: "token"}, stdReq) if err != nil { t.Fatalf("apply current input file failed: %v", err) } @@ -309,15 +304,13 @@ func TestApplyCurrentInputFileUploadsFirstTurnWithInjectedWrapper(t *testing.T) } } -func TestApplyCurrentInputFileReplacesHistorySplitWithFullContextFile(t *testing.T) { +func TestApplyCurrentInputFileUploadsFullContextFile(t *testing.T) { ds := &inlineUploadDSStub{} h := &openAITestSurface{ Store: mockOpenAIConfig{ wideInput: true, - historySplitEnabled: true, - historySplitTurns: 1, currentInputEnabled: true, - currentInputMin: 1000, + currentInputMin: 0, thinkingInjection: boolPtr(true), }, DS: ds, @@ -331,12 +324,12 @@ func TestApplyCurrentInputFileReplacesHistorySplitWithFullContextFile(t *testing t.Fatalf("normalize failed: %v", err) } - out, err := h.applyHistorySplit(context.Background(), &auth.RequestAuth{DeepSeekToken: "token"}, stdReq) + out, err := h.applyCurrentInputFile(context.Background(), &auth.RequestAuth{DeepSeekToken: "token"}, stdReq) if err != nil { t.Fatalf("apply current input file failed: %v", err) } if !out.CurrentInputFileApplied { - t.Fatalf("expected current input file to replace history split") + t.Fatalf("expected current input file to apply") } if len(ds.uploadCalls) != 1 { t.Fatalf("expected one current input upload, got %d", len(ds.uploadCalls)) @@ -351,9 +344,6 @@ func TestApplyCurrentInputFileReplacesHistorySplitWithFullContextFile(t *testing t.Fatalf("expected full context file to contain %q, got %q", want, uploadedText) } } - if out.HistoryText != "" { - t.Fatalf("expected no HISTORY transcript when current input file replaces split, got %q", out.HistoryText) - } if strings.Contains(out.FinalPrompt, "first user turn") || strings.Contains(out.FinalPrompt, "latest user turn") || strings.Contains(out.FinalPrompt, "CURRENT_USER_INPUT.txt") || strings.Contains(out.FinalPrompt, "IGNORE.txt") || strings.Contains(out.FinalPrompt, "Read that file") { t.Fatalf("expected live prompt to use only a neutral continuation instruction, got %s", out.FinalPrompt) } @@ -362,13 +352,12 @@ func TestApplyCurrentInputFileReplacesHistorySplitWithFullContextFile(t *testing } } -func TestApplyHistorySplitCarriesHistoryText(t *testing.T) { +func TestApplyCurrentInputFileLeavesHistoryTextEmpty(t *testing.T) { ds := &inlineUploadDSStub{} h := &openAITestSurface{ Store: mockOpenAIConfig{ wideInput: true, - historySplitEnabled: true, - historySplitTurns: 1, + currentInputEnabled: true, }, DS: ds, } @@ -381,25 +370,24 @@ func TestApplyHistorySplitCarriesHistoryText(t *testing.T) { t.Fatalf("normalize failed: %v", err) } - out, err := h.applyHistorySplit(context.Background(), &auth.RequestAuth{DeepSeekToken: "token"}, stdReq) + out, err := h.applyCurrentInputFile(context.Background(), &auth.RequestAuth{DeepSeekToken: "token"}, stdReq) if err != nil { - t.Fatalf("apply history split failed: %v", err) + t.Fatalf("apply current input file failed: %v", err) } if len(ds.uploadCalls) != 1 { t.Fatalf("expected 1 upload call, got %d", len(ds.uploadCalls)) } - if out.HistoryText != string(ds.uploadCalls[0].Data) { - t.Fatalf("expected history text to be preserved on normalized request") + if out.HistoryText != "" { + t.Fatalf("expected current input file flow to leave history text empty, got %q", out.HistoryText) } } -func TestChatCompletionsHistorySplitUploadsHistoryFileAndKeepsLatestPrompt(t *testing.T) { +func TestChatCompletionsCurrentInputFileUploadsContextAndKeepsNeutralPrompt(t *testing.T) { ds := &inlineUploadDSStub{} h := &openAITestSurface{ Store: mockOpenAIConfig{ wideInput: true, - historySplitEnabled: true, - historySplitTurns: 1, + currentInputEnabled: true, }, Auth: streamStatusAuthStub{}, DS: ds, @@ -423,7 +411,7 @@ func TestChatCompletionsHistorySplitUploadsHistoryFileAndKeepsLatestPrompt(t *te t.Fatalf("expected 1 upload call, got %d", len(ds.uploadCalls)) } upload := ds.uploadCalls[0] - if upload.Filename != "HISTORY.txt" { + if upload.Filename != "IGNORE.txt" { t.Fatalf("unexpected upload filename: %q", upload.Filename) } if upload.Purpose != "assistants" { @@ -433,32 +421,31 @@ func TestChatCompletionsHistorySplitUploadsHistoryFileAndKeepsLatestPrompt(t *te if !strings.Contains(historyText, "[file content end]") || !strings.Contains(historyText, "[file name]: IGNORE") { t.Fatalf("expected injected IGNORE wrapper, got %s", historyText) } - if strings.Contains(historyText, "latest user turn") { - t.Fatalf("expected latest turn to remain live, got %s", historyText) + if !strings.Contains(historyText, "latest user turn") { + t.Fatalf("expected full context to include latest turn, got %s", historyText) } if ds.completionReq == nil { t.Fatal("expected completion payload to be captured") } promptText, _ := ds.completionReq["prompt"].(string) - if !strings.Contains(promptText, "latest user turn") { - t.Fatalf("expected latest turn in completion prompt, got %s", promptText) + if !strings.Contains(promptText, "Answer the latest user request directly.") { + t.Fatalf("expected neutral completion prompt, got %s", promptText) } - if strings.Contains(promptText, "first user turn") { - t.Fatalf("expected historical turns removed from completion prompt, got %s", promptText) + if strings.Contains(promptText, "first user turn") || strings.Contains(promptText, "latest user turn") { + t.Fatalf("expected prompt to hide original turns, got %s", promptText) } refIDs, _ := ds.completionReq["ref_file_ids"].([]any) if len(refIDs) == 0 || refIDs[0] != "file-inline-1" { - t.Fatalf("expected uploaded history file to be first ref_file_id, got %#v", ds.completionReq["ref_file_ids"]) + t.Fatalf("expected uploaded current input file to be first ref_file_id, got %#v", ds.completionReq["ref_file_ids"]) } } -func TestResponsesHistorySplitUploadsHistoryAndKeepsLatestPrompt(t *testing.T) { +func TestResponsesCurrentInputFileUploadsContextAndKeepsNeutralPrompt(t *testing.T) { ds := &inlineUploadDSStub{} h := &openAITestSurface{ Store: mockOpenAIConfig{ wideInput: true, - historySplitEnabled: true, - historySplitTurns: 1, + currentInputEnabled: true, }, Auth: streamStatusAuthStub{}, DS: ds, @@ -487,23 +474,22 @@ func TestResponsesHistorySplitUploadsHistoryAndKeepsLatestPrompt(t *testing.T) { t.Fatal("expected completion payload to be captured") } promptText, _ := ds.completionReq["prompt"].(string) - if !strings.Contains(promptText, "latest user turn") { - t.Fatalf("expected latest turn in completion prompt, got %s", promptText) + if !strings.Contains(promptText, "Answer the latest user request directly.") { + t.Fatalf("expected neutral completion prompt, got %s", promptText) } - if strings.Contains(promptText, "first user turn") { - t.Fatalf("expected historical turns removed from completion prompt, got %s", promptText) + if strings.Contains(promptText, "first user turn") || strings.Contains(promptText, "latest user turn") { + t.Fatalf("expected prompt to hide original turns, got %s", promptText) } } -func TestChatCompletionsHistorySplitMapsManagedAuthFailureTo401(t *testing.T) { +func TestChatCompletionsCurrentInputFileMapsManagedAuthFailureTo401(t *testing.T) { ds := &inlineUploadDSStub{ uploadErr: &dsclient.RequestFailure{Op: "upload file", Kind: dsclient.FailureManagedUnauthorized, Message: "expired token"}, } h := &openAITestSurface{ Store: mockOpenAIConfig{ wideInput: true, - historySplitEnabled: true, - historySplitTurns: 1, + currentInputEnabled: true, }, Auth: streamStatusManagedAuthStub{}, DS: ds, @@ -528,15 +514,14 @@ func TestChatCompletionsHistorySplitMapsManagedAuthFailureTo401(t *testing.T) { } } -func TestResponsesHistorySplitMapsDirectAuthFailureTo401(t *testing.T) { +func TestResponsesCurrentInputFileMapsDirectAuthFailureTo401(t *testing.T) { ds := &inlineUploadDSStub{ uploadErr: &dsclient.RequestFailure{Op: "upload file", Kind: dsclient.FailureDirectUnauthorized, Message: "invalid token"}, } h := &openAITestSurface{ Store: mockOpenAIConfig{ wideInput: true, - historySplitEnabled: true, - historySplitTurns: 1, + currentInputEnabled: true, }, Auth: streamStatusAuthStub{}, DS: ds, @@ -563,13 +548,12 @@ func TestResponsesHistorySplitMapsDirectAuthFailureTo401(t *testing.T) { } } -func TestChatCompletionsHistorySplitUploadFailureReturnsInternalServerError(t *testing.T) { +func TestChatCompletionsCurrentInputFileUploadFailureReturnsInternalServerError(t *testing.T) { ds := &inlineUploadDSStub{uploadErr: errors.New("boom")} h := &openAITestSurface{ Store: mockOpenAIConfig{ wideInput: true, - historySplitEnabled: true, - historySplitTurns: 1, + currentInputEnabled: true, }, Auth: streamStatusAuthStub{}, DS: ds, @@ -591,7 +575,7 @@ func TestChatCompletionsHistorySplitUploadFailureReturnsInternalServerError(t *t } } -func TestHistorySplitWorksAcrossAutoDeleteModes(t *testing.T) { +func TestCurrentInputFileWorksAcrossAutoDeleteModes(t *testing.T) { for _, mode := range []string{"none", "single", "all"} { t.Run(mode, func(t *testing.T) { ds := &inlineUploadDSStub{} @@ -599,8 +583,7 @@ func TestHistorySplitWorksAcrossAutoDeleteModes(t *testing.T) { Store: mockOpenAIConfig{ wideInput: true, autoDeleteMode: mode, - historySplitEnabled: true, - historySplitTurns: 1, + currentInputEnabled: true, }, Auth: streamStatusAuthStub{}, DS: ds, @@ -621,13 +604,13 @@ func TestHistorySplitWorksAcrossAutoDeleteModes(t *testing.T) { t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String()) } if len(ds.uploadCalls) != 1 { - t.Fatalf("expected history split upload for mode=%s, got %d", mode, len(ds.uploadCalls)) + t.Fatalf("expected current input upload for mode=%s, got %d", mode, len(ds.uploadCalls)) } if ds.completionReq == nil { t.Fatalf("expected completion payload for mode=%s", mode) } promptText, _ := ds.completionReq["prompt"].(string) - if !strings.Contains(promptText, "latest user turn") || strings.Contains(promptText, "first user turn") { + if !strings.Contains(promptText, "Answer the latest user request directly.") || strings.Contains(promptText, "first user turn") || strings.Contains(promptText, "latest user turn") { t.Fatalf("unexpected prompt for mode=%s: %s", mode, promptText) } }) diff --git a/internal/httpapi/openai/responses/handler.go b/internal/httpapi/openai/responses/handler.go index fc00da4..a5f243f 100644 --- a/internal/httpapi/openai/responses/handler.go +++ b/internal/httpapi/openai/responses/handler.go @@ -36,20 +36,17 @@ func (h *Handler) compatStripReferenceMarkers() bool { return shared.CompatStripReferenceMarkers(h.Store) } -func (h *Handler) applyHistorySplit(ctx context.Context, a *auth.RequestAuth, stdReq promptcompat.StandardRequest) (promptcompat.StandardRequest, error) { +func (h *Handler) applyCurrentInputFile(ctx context.Context, a *auth.RequestAuth, stdReq promptcompat.StandardRequest) (promptcompat.StandardRequest, error) { if h == nil { return stdReq, nil } stdReq = shared.ApplyThinkingInjection(h.Store, stdReq) svc := history.Service{Store: h.Store, DS: h.DS} out, err := svc.ApplyCurrentInputFile(ctx, a, stdReq) - if err != nil { - return stdReq, err + if err != nil || out.CurrentInputFileApplied { + return out, err } - if out.CurrentInputFileApplied { - return out, nil - } - return svc.Apply(ctx, a, out) + return out, nil } func (h *Handler) preprocessInlineFileInputs(ctx context.Context, a *auth.RequestAuth, req map[string]any) error { @@ -89,7 +86,7 @@ func writeOpenAIInlineFileError(w http.ResponseWriter, err error) { files.WriteInlineFileError(w, err) } -func mapHistorySplitError(err error) (int, string) { +func mapCurrentInputFileError(err error) (int, string) { return history.MapError(err) } diff --git a/internal/httpapi/openai/responses/responses_handler.go b/internal/httpapi/openai/responses/responses_handler.go index f32e3ec..1411622 100644 --- a/internal/httpapi/openai/responses/responses_handler.go +++ b/internal/httpapi/openai/responses/responses_handler.go @@ -85,9 +85,9 @@ func (h *Handler) Responses(w http.ResponseWriter, r *http.Request) { writeOpenAIError(w, http.StatusBadRequest, err.Error()) return } - stdReq, err = h.applyHistorySplit(r.Context(), a, stdReq) + stdReq, err = h.applyCurrentInputFile(r.Context(), a, stdReq) if err != nil { - status, message := mapHistorySplitError(err) + status, message := mapCurrentInputFileError(err) writeOpenAIError(w, status, message) return } diff --git a/internal/httpapi/openai/test_bridge_test.go b/internal/httpapi/openai/test_bridge_test.go index 6815589..7f04589 100644 --- a/internal/httpapi/openai/test_bridge_test.go +++ b/internal/httpapi/openai/test_bridge_test.go @@ -83,17 +83,14 @@ func (h *openAITestSurface) ChatCompletions(w http.ResponseWriter, r *http.Reque h.chatHandler().ChatCompletions(w, r) } -func (h *openAITestSurface) applyHistorySplit(ctx context.Context, a *auth.RequestAuth, stdReq promptcompat.StandardRequest) (promptcompat.StandardRequest, error) { +func (h *openAITestSurface) applyCurrentInputFile(ctx context.Context, a *auth.RequestAuth, stdReq promptcompat.StandardRequest) (promptcompat.StandardRequest, error) { stdReq = shared.ApplyThinkingInjection(h.Store, stdReq) svc := history.Service{Store: h.Store, DS: h.DS} out, err := svc.ApplyCurrentInputFile(ctx, a, stdReq) - if err != nil { - return stdReq, err + if err != nil || out.CurrentInputFileApplied { + return out, err } - if out.CurrentInputFileApplied { - return out, nil - } - return svc.Apply(ctx, a, out) + return out, nil } func (h *openAITestSurface) preprocessInlineFileInputs(ctx context.Context, a *auth.RequestAuth, req map[string]any) error { @@ -114,8 +111,8 @@ func splitOpenAIHistoryMessages(messages []any, triggerAfterTurns int) ([]any, [ return history.SplitOpenAIHistoryMessages(messages, triggerAfterTurns) } -func buildOpenAIHistoryTranscript(messages []any) string { - return promptcompat.BuildOpenAIHistoryTranscript(messages) +func buildOpenAICurrentInputContextTranscript(messages []any) string { + return promptcompat.BuildOpenAICurrentInputContextTranscript(messages) } func writeOpenAIError(w http.ResponseWriter, status int, message string) { diff --git a/webui/src/features/settings/HistorySplitSection.jsx b/webui/src/features/settings/HistorySplitSection.jsx index 30a0bc1..d4e84af 100644 --- a/webui/src/features/settings/HistorySplitSection.jsx +++ b/webui/src/features/settings/HistorySplitSection.jsx @@ -1,51 +1,9 @@ -export default function HistorySplitSection({ t, form, setForm }) { +export default function CurrentInputFileSection({ t, form, setForm }) { return (
{t('settings.historySplitDesc')}
-{t('settings.currentInputFileDesc')}
{t('settings.splitPassThroughHelp')}