Skip to content

Commit 6d447de

Browse files
committed
feat(test): prepare-request:: cache-control
1 parent 88f0af9 commit 6d447de

File tree

17 files changed

+641
-150
lines changed

17 files changed

+641
-150
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ A [Nushell](https://www.nushell.sh) scriptable [MCP client](https://modelcontext
1414
* **Consistent API Across Models:** Connect to Gemini + Search and Anthropic + Search through a single, simple interface. ([Add providers easily.](docs/reference/provider-api.md))
1515
* **Persistent, Editable Conversations:** [Conversation threads](https://cablehead.github.io/xs/tutorials/threaded-conversations/) are saved across sessions. Review, edit, and control your own context window — no black-box history.
1616
* **Flexible Tool Integration:** Connect to MCP servers to extend functionality. `gpt2099` already rivals [Claude Code](https://docs.anthropic.com/en/docs/claude-code/overview) for local file editing, but with full provider independence and deeper flexibility.
17-
* **Document Support:** Upload and reference documents (PDFs, images, text files) directly in conversations with automatic content-type detection and caching.
17+
* **Document Support:** Upload and reference documents (PDFs, images, text files) directly in conversations with automatic content-type detection and optional caching.
1818

1919
Built on [cross.stream](https://github.com/cablehead/xs) for event-driven processing, `gpt2099` brings modern AI directly into your Nushell workflow — fully scriptable, fully inspectable, all in the terminal.
2020

docs/commands.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ gpt [OPTIONS]
1919
- `--provider-ptr (-p) <alias>` – Pointer to the provider/model
2020
- `--json (-j)` – Treat input as JSON
2121
- `--separator <str>` – Join list input with this separator
22-
- `--cache` – Enable ephemeral caching for this turn
22+
- `--cache` – Enable caching for this conversation turn
2323

2424
**Example:**
2525
```nushell
@@ -75,12 +75,12 @@ gpt document <PATH> [OPTIONS]
7575

7676
**Options:**
7777
- `--name (-n) <string>` – Custom name for the document
78-
- `--cache <string>`Cache control: "ephemeral" or "none"
78+
- `--cache`Enable caching for this document
7979
- `--bookmark (-b) <string>` – Bookmark this document registration
8080

8181
**Example:**
8282
```nushell
83-
gpt document ~/report.pdf --name "Q4 Report" --bookmark "quarterly"
83+
gpt document ~/report.pdf --name "Q4 Report" --cache --bookmark "quarterly"
8484
```
8585

8686
See: [How to work with documents](./how-to/work-with-documents.md)

docs/how-to/work-with-documents.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,15 @@ gpt document ~/chart.png --name "Sales Chart"
7575

7676
## Document Caching
7777

78-
Documents automatically use ephemeral caching with supported providers (like Anthropic) to improve performance on repeated references. You can control caching with the `--cache` flag:
78+
Documents can signal to the provider that they should be cached to improve performance on repeated references. Enable caching with the `--cache` flag:
7979

8080
```nushell
81-
gpt document ~/large-file.pdf --cache ephemeral
82-
gpt document ~/dynamic-data.json --cache none
81+
gpt document ~/large-file.pdf --cache
82+
gpt document ~/dynamic-data.json # no caching
8383
```
8484

85+
**Note:** Caching behavior depends on provider support - Anthropic uses ephemeral caching, while other providers may ignore the cache flag.
86+
8587
## Thread Management with Documents
8688

8789
Documents integrate seamlessly with conversation threading:

docs/reference/schemas.md

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ Content blocks are a union of these types:
3737
{
3838
type: "text"
3939
text: string
40-
cache_control?: {type: "ephemeral"}
4140
}
4241
```
4342

@@ -50,7 +49,6 @@ Content blocks are a union of these types:
5049
media_type: string # MIME type
5150
data: string # Base64-encoded content
5251
}
53-
cache_control?: {type: "ephemeral"}
5452
}
5553
```
5654

@@ -77,7 +75,6 @@ Content blocks are a union of these types:
7775

7876
**Schema Notes:**
7977
- `options.provider_ptr` is required for actual API calls but optional in stored contexts
80-
- `cache_control` only supported by Anthropic (ignored by Gemini)
8178
- `tool_use.id` auto-generated if missing (Gemini requirement)
8279
- `document_block.source.media_type` determines provider-specific handling
8380

@@ -114,9 +111,10 @@ Each turn in a thread is stored as a `gpt.turn` frame, with these top-level attr
114111
: Values: Turn ID(s) or bookmark name(s)
115112

116113
**`cache`**
117-
: Ephemeral cache flag for this turn
114+
: Cache flag for this turn
118115
: Type: `bool`
119116
: Default: `false`
117+
: Note: Provider-specific implementation (e.g., Anthropic uses ephemeral caching)
120118

121119
### Document-Specific Fields
122120

@@ -143,10 +141,10 @@ Each turn in a thread is stored as a `gpt.turn` frame, with these top-level attr
143141
: Size of the document in bytes
144142
: Type: `int`
145143

146-
**`cache_control`**
147-
: Caching directive
148-
: Type: `string`
149-
: Values: `"ephemeral"` for documents
144+
**`cache`**
145+
: Cache flag for this turn
146+
: Type: `bool`
147+
: Default: `false`
150148

151149
## Thread Record Schema
152150

gpt/ctx.nu

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ def frame-to-turn [frame: record] {
3232
[
3333
{
3434
type: "document"
35-
cache_control: {type: "ephemeral"}
3635
source: {
3736
type: "base64"
3837
media_type: $meta.content_type
@@ -43,24 +42,20 @@ def frame-to-turn [frame: record] {
4342
} else if (($meta | get content_type?) == "application/json") {
4443
$content_raw | from json
4544
} else {
46-
[
47-
(
45+
[
4846
{type: "text" text: $content_raw}
49-
| if $cache {
50-
insert cache_control {type: "ephemeral"}
51-
} else { $in }
52-
)
53-
]
54-
}
47+
]
48+
}
5549
)
5650

5751
{
5852
id: $frame.id
5953
role: $role
6054
content: $content
6155
options: $options_delta
62-
cache: $cache
63-
}
56+
} | if $cache {
57+
insert cache $cache
58+
} else { $in }
6459
}
6560

6661
# Follow the continues chain to produce a list of turns in chronological order

gpt/mod.nu

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export use ./prep.nu
2323
export def document [
2424
path: string # Path to the document file
2525
--name (-n): string # Optional name for the document (defaults to filename)
26-
--cache: string = "ephemeral" # Cache control: "ephemeral" or "none"
26+
--cache # Enable caching for this document
2727
--bookmark (-b): string # Bookmark this document registration
2828
] {
2929
# Validate file exists
@@ -76,7 +76,8 @@ export def document [
7676
document_name: $document_name
7777
original_path: ($path | path expand)
7878
file_size: $file_size
79-
cache_control: (if $cache == "ephemeral" { "ephemeral" } else { null })
79+
} | conditional-pipe $cache {
80+
insert cache true
8081
} | conditional-pipe ($bookmark | is-not-empty) {
8182
insert head $bookmark
8283
}
@@ -95,7 +96,7 @@ export def main [
9596
--provider-ptr (-p): string # a short alias for provider to going-forward
9697
--json (-j) # Treat input as JSON formatted content
9798
--separator: string = "\n\n---\n\n" # Separator used when joining lists of strings
98-
--cache # Enable ephemeral caching for this turn
99+
--cache # Enable caching for this turn
99100
] {
100101
let content = if $in == null {
101102
input "Enter prompt: "

gpt/providers/anthropic/mod.nu

Lines changed: 52 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -52,77 +52,71 @@ export def provider [] {
5252

5353
prepare-request: {|ctx: record tools?: list<record>|
5454
# anthropic only supports a single system message as a top level attribute
55-
let messages = $ctx.messages | select role content
55+
let messages = $ctx.messages
5656
let system_messages = $messages | where role == "system"
5757
let messages = $messages | where role != "system"
5858

59+
# Apply cache control limits at message level FIRST (max 4 breakpoints)
60+
# Use reverse approach: reverse -> keep first 4 cache messages -> reverse back
61+
let cache_count = $messages | where {|msg| $msg.cache? == true } | length
62+
63+
let messages = if $cache_count > 4 {
64+
$messages
65+
| reverse
66+
| generate {|msg state = {cache_kept: 0}|
67+
if ($msg.cache? == true) and ($state.cache_kept < 4) {
68+
{out: $msg next: {cache_kept: ($state.cache_kept + 1)}}
69+
} else {
70+
{out: ($msg | reject -i cache) next: $state}
71+
}
72+
}
73+
| reverse
74+
} else {
75+
$messages
76+
}
77+
78+
# THEN process content and apply cache to content blocks
5979
let messages = $messages | each {|msg|
60-
update content {|content|
61-
each {|part|
62-
match $part.type {
63-
"tool_result" => ($part | reject -i name)
64-
"document" => {
65-
# Convert based on media type
66-
let media_type = $part.source.media_type
67-
if ($media_type | str starts-with "text/") or ($media_type == "application/json") {
68-
# Decode base64 and convert to text block
69-
let decoded_content = $part.source.data | decode base64 | decode utf-8
70-
{
71-
type: "text"
72-
text: $decoded_content
73-
} | if ($part.cache_control? != null) {
74-
insert cache_control $part.cache_control
75-
} else { $in }
76-
} else if ($media_type | str starts-with "image/") {
77-
# Convert images to use type: "image" as per Anthropic API
78-
{
79-
type: "image"
80-
source: $part.source
81-
} | if ($part.cache_control? != null) {
82-
insert cache_control $part.cache_control
83-
} else { $in }
84-
} else {
85-
# Keep other binary documents as-is (PDFs, etc.)
86-
$part
80+
let has_cache = ($msg.cache? == true)
81+
let content = $msg.content | enumerate | each {|item|
82+
let part = $item.item
83+
let is_last = ($item.index == (($msg.content | length) - 1))
84+
85+
let converted_part = match $part.type {
86+
"tool_result" => ($part | reject -i name)
87+
"document" => {
88+
# Convert based on media type
89+
let media_type = $part.source.media_type
90+
if ($media_type | str starts-with "text/") or ($media_type == "application/json") {
91+
# Decode base64 and convert to text block
92+
let decoded_content = $part.source.data | decode base64 | decode utf-8
93+
{
94+
type: "text"
95+
text: $decoded_content
96+
}
97+
} else if ($media_type | str starts-with "image/") {
98+
# Convert images to use type: "image" as per Anthropic API
99+
{
100+
type: "image"
101+
source: $part.source
87102
}
103+
} else {
104+
# Keep other binary documents as-is (PDFs, etc.)
105+
$part
88106
}
89-
_ => $part
90107
}
108+
_ => $part
91109
}
92-
}
93-
}
94-
95-
# Apply cache control limits across all content blocks (max 4 breakpoints)
96-
let all_content = $messages | get content | flatten
97-
let cache_indices = $all_content | enumerate | where {|item| $item.item.cache_control? != null } | get index
98-
let cache_count = $cache_indices | length
99-
100-
let messages = if $cache_count > 4 {
101-
let remove_count = $cache_count - 4
102-
let remove_indices = $cache_indices | first $remove_count
103110

104-
let limited_content = $all_content | enumerate | each {|item|
105-
if $item.index in $remove_indices {
106-
$item.item | reject cache_control
111+
# Add cache_control to the last content block if message has cache
112+
if $has_cache and $is_last {
113+
$converted_part | insert cache_control {type: "ephemeral"}
107114
} else {
108-
$item.item
115+
$converted_part
109116
}
110117
}
111118

112-
# Rebuild messages with limited cache control
113-
let result = $messages | reduce --fold {messages: [] index: 0} {|msg acc|
114-
let msg_content_count = $msg.content | length
115-
let new_content = $limited_content | skip $acc.index | first $msg_content_count
116-
let new_msg = $msg | update content $new_content
117-
{
118-
messages: ($acc.messages | append $new_msg)
119-
index: ($acc.index + $msg_content_count)
120-
}
121-
}
122-
123-
$result.messages
124-
} else {
125-
$messages
119+
$msg | update content $content | reject -i cache
126120
}
127121

128122
let data = {

0 commit comments

Comments
 (0)