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

> Discover how DS2API's history splitting automatically manages context for DeepSeek by partitioning conversations, keeping prompts short while preserving full dialogue history.

- Repository: [CJACK./ds2api](https://github.com/CJackHwang/ds2api)
- Tags: internals
- Published: 2026-04-26

---

**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.

```go
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.

```go
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:

```go
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:

```go
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:

```json
{
  "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:

```go
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:

```go
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`](https://github.com/CJackHwang/ds2api/blob/main/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`](https://github.com/CJackHwang/ds2api/blob/main/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`](https://github.com/CJackHwang/ds2api/blob/main/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.