How DS2API's History Splitting Feature Works: Automatic Context Management for DeepSeek

DS2API's history splitting feature automatically partitions older conversation turns into a separate history file that gets uploaded to DeepSeek, keeping the active prompt short while preserving full dialogue context through file references.

The CJackHwang/ds2api repository implements an intelligent context window management system that solves token limit constraints. This feature intercepts OpenAI-compatible requests, splits the message history based on configurable turn thresholds, and uploads the older portion as a referenced file. Understanding this mechanic is essential for optimizing DeepSeek API usage when handling long conversations.

Configuration and Trigger Settings

The history splitting behavior is controlled through the configuration layer, with specific parameters determining when splits occur.

Trigger After Turns

The HistorySplitTriggerAfterTurns() method in internal/config/store_accessors.go:70‑77 determines the threshold for splitting. By default, the system splits after 1 user turn, though this value is configurable.

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                // default
    }
    return *s.cfg.HistorySplit.TriggerAfterTurns
}

Forced Enable Setting

Unlike optional features, history splitting is always enabled in DS2API. The forceHistorySplitEnabled configuration in internal/config/config.go:44‑50 ensures the feature remains active for every request, preventing context overflow regardless of user settings.

Splitting Logic and Partition Algorithm

When an OpenAI-compatible request reaches the handler, the Service.Apply method invokes SplitOpenAIHistoryMessages to partition the message array.

The Splitting Algorithm

Located in internal/httpapi/openai/history/history_split.go:62‑112, the SplitOpenAIHistoryMessages function implements a turn-based counting mechanism:

  1. Scans the message list, counting only user turns (not assistant or system messages).
  2. Identifies the index of the last user turn.
  3. Compares the total user turn count against the configured triggerAfterTurns threshold.
  4. If user turns exceed the threshold, partitions the array at the last user turn.
func SplitOpenAIHistoryMessages(messages []any, triggerAfterTurns int) ([]any, []any) {
    // Counts user turns and finds lastUserIndex
    // Logic omitted for brevity
    
    if userTurns <= triggerAfterTurns || lastUserIndex < 0 {
        return messages, nil
    }

    // Returns: (promptMessages, historyMessages)
    // promptMessages contains everything from last user turn onward
    // historyMessages contains everything before the last user turn
}

Critical Partition Rules

System and developer messages receive special treatment. Even if they appear chronologically before the split point, they remain in the active promptMessages rather than moving to the history file. This ensures the model always receives system instructions and persona definitions in the immediate context.

History File Construction

Once partitioned, the historyMessages slice transforms into a formatted transcript through BuildOpenAIHistoryTranscript in internal/promptcompat/history_transcript.go:12‑19.

The function normalizes OpenAI-formatted messages, prepares them as a plain text transcript, and wraps them with DeepSeek-specific markers:

func BuildOpenAIHistoryTranscript(messages []any) string {
    normalized := NormalizeOpenAIMessagesForPrompt(messages, "")
    transcript := strings.TrimSpace(prompt.MessagesPrepare(normalized))
    if transcript == "" {
        return ""
    }
    // Special wrappers for DeepSeek file parser
    return fmt.Sprintf("[file content end]\n\n%s\n\n[file name]: %s\n[file content begin]\n",
        transcript, historySplitInjectedFilename)
}

The markers [file content end] and [file content begin] signal to the DeepSeek API that this content should be treated as an injected file rather than inline prompt text.

Uploading and Referencing

The Service.Apply method in internal/httpapi/openai/history/history_split.go:41‑47 uploads the formatted transcript via the DeepSeek client:

result, err := s.DS.UploadFile(ctx, a, dsclient.UploadFileRequest{
    Filename:    historySplitFilename,
    ContentType: historySplitContentType,
    Purpose:     historySplitPurpose,
    Data:        []byte(historyText),
}, 3)

After successful upload, the system performs three critical operations:

  • Adds the returned file ID to stdReq.RefFileIDs using prependUniqueRefFileID (ensuring no duplicate references)
  • Replaces the original message list with promptMessages (the recent turns only)
  • Rebuilds the request's FinalPrompt using promptcompat.BuildOpenAIPrompt

Practical Configuration Example

Configure history splitting behavior using JSON configuration:

{
  "keys": ["my-api-key"],
  "history_split": {
    "enabled": true,
    "trigger_after_turns": 2
  }
}

When trigger_after_turns is set to 2, the system maintains the most recent two user interactions directly in the prompt, uploading all prior conversation to the history file.

Manual Implementation Walkthrough

For custom integrations, invoke the splitting logic directly:

req := promptcompat.StandardRequest{
    Messages: []any{
        map[string]any{"role": "system", "content": "You are a helpful assistant."},
        map[string]any{"role": "user",   "content": "First question."},
        map[string]any{"role": "assistant", "content": "First answer."},
        map[string]any{"role": "user",   "content": "Second question."},
        map[string]any{"role": "assistant", "content": "Second answer."},
        map[string]any{"role": "user",   "content": "Current question."},
    },
}

// Split after 2 user turns
svc := history.Service{
    Store: myConfigStore,
    DS:    deepSeekClient,
}
newReq, err := svc.Apply(context.Background(), authInfo, req)

The resulting newReq contains:

  • Messages: Only the current question and preceding assistant turn
  • RefFileIDs: File ID referencing the uploaded history
  • HistoryText: Raw transcript with DeepSeek markers

Inspect generated transcripts for debugging:

fmt.Println(newReq.HistoryText)
// Output:
// [file content end]
// 
// system: You are a helpful assistant.
// user: First question.
// assistant: First answer.
// user: Second question.
// assistant: Second answer.
// 
// [file name]: HISTORY.txt
// [file content begin]

Summary

  • DS2API history splitting automatically manages long conversations by partitioning older turns into uploaded files
  • The trigger_after_turns configuration (default: 1) controls when splits occur, accessible via HistorySplitTriggerAfterTurns() in internal/config/store_accessors.go
  • System messages always remain in the active prompt regardless of split position
  • The SplitOpenAIHistoryMessages function in internal/httpapi/openai/history/history_split.go implements the core partitioning logic based on user turn counting
  • History files use special markers ([file content end], [file content begin]) created by BuildOpenAIHistoryTranscript to signal DeepSeek's parser
  • File references are added to RefFileIDs while the active Messages array contains only recent turns

Frequently Asked Questions

How does DS2API decide where to split conversation history?

DS2API counts user turns (not total messages) and identifies the last user message index. If the count exceeds trigger_after_turns, everything before that last user turn becomes history, while everything from that turn onward remains in the active prompt. System messages are exempt from this partitioning and always stay in the active prompt.

Can I disable history splitting in DS2API?

No. According to the source code in internal/config/config.go:44‑50, the forceHistorySplitEnabled setting ensures the feature is always active. While you can configure trigger_after_turns to high values to minimize splitting, the underlying functionality cannot be disabled as it is forced on at the configuration level.

What happens to the uploaded history file?

The transcript uploads to DeepSeek via UploadFile in internal/httpapi/openai/history/history_split.go:41‑47. The resulting file ID attaches to the request's RefFileIDs array, allowing DeepSeek's model to retrieve the full context through file references rather than inline prompt text, effectively extending the effective context window beyond standard token limits.

Why are system messages kept in the prompt instead of moved to history?

The SplitOpenAIHistoryMessages function in internal/httpapi/openai/history/history_split.go specifically preserves system and developer messages in the active prompt regardless of their chronological position. This design ensures the model always receives critical instruction, persona definition, and contextual guardrails directly in the immediate context window, where they have the strongest influence on response generation.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →